dorfteich/apps/api/src/mcp/mcp-tools.ts
Claude Fable 5 d73b120d06 #148: Seitenlisten filtern nach createdSince/updatedSince
Neues pageListQuerySchema (ISO 8601, Kulanz für Datum ohne Zeit),
Query-Parameter auf interner und Public-API-Seitenliste, Prisma-where
mit gte; neue Indizes (pondId, createdAt)/(pondId, updatedAt) als
Migration. OpenAPI-Parameter, MCP-Parität (list_pages
created_since/updated_since), Doku (api-guide, mcp-guide,
public-api.md), DB-Test inkl. 400 bei ungültigem Datum.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 00:49:52 +02:00

215 lines
6.8 KiB
TypeScript

import { z } from 'zod';
/**
* The MCP tool table (issue #105): names, English descriptions, and input
* schemas — the JSON Schema served on tools/list is written by hand next
* to the Zod schema that actually validates calls. Kept as plain data (and
* validated in mcp.service.ts, not through the SDK's typed helpers): the
* SDK's generic tool registration blows up TypeScript's inference when
* combined with a program this size.
*/
const pond = z.string().min(1);
const page = z.string().min(1);
/** ISO 8601 instant for `…_since` filters (issue #148). */
const sinceInstant = z
.string()
.trim()
.regex(/^\d{4}-\d{2}-\d{2}([T ].+)?$/)
.refine((value) => !Number.isNaN(Date.parse(value)))
.transform((value) => new Date(value));
export const MCP_TOOL_INPUTS = {
list_ponds: z.object({}),
list_pages: z.object({
pond,
created_since: sinceInstant.optional(),
updated_since: sinceInstant.optional(),
}),
read_page: z.object({ pond, page }),
search: z.object({
query: z.string().min(1),
pond: pond.optional(),
label: z.string().min(1).optional(),
}),
create_page: z.object({
pond,
title: z.string().min(1).max(200),
markdown: z.string().default(''),
parent: page.optional(),
}),
update_page: z
.object({
pond,
page,
title: z.string().min(1).max(200).optional(),
markdown: z.string().optional(),
parent: page.nullable().optional(),
})
.refine(
(input) =>
input.title !== undefined || input.markdown !== undefined || input.parent !== undefined,
{ message: 'title, markdown, or parent required' },
),
add_comment: z.object({ pond, page, text: z.string().min(1) }),
list_labels: z.object({ pond }),
set_page_labels: z.object({ pond, page, labelIds: z.array(z.string()) }),
export_pond: z.object({ pond }),
} as const;
export type McpToolName = keyof typeof MCP_TOOL_INPUTS;
const pondProp = { type: 'string', description: 'Pond slug (from list_ponds)' } as const;
const pageProp = { type: 'string', description: 'Page slug (from list_pages or search)' } as const;
/** What tools/list advertises; must stay in sync with the Zod table above. */
export const MCP_TOOL_DEFINITIONS: {
name: McpToolName;
description: string;
inputSchema: object;
}[] = [
{
name: 'list_ponds',
description:
'List the ponds (workspaces) this token can reach through MCP. ' +
'Returns slugs — every other tool takes a pond slug.',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'list_pages',
description:
'List the readable pages of a pond: slug, title, parent (the page tree), labels, ' +
'timestamps. Optional created_since/updated_since (ISO 8601) narrow to pages ' +
'created/changed at or after that instant.',
inputSchema: {
type: 'object',
properties: {
pond: pondProp,
created_since: {
type: 'string',
description: 'ISO 8601 instant — only pages created at/after it',
},
updated_since: {
type: 'string',
description: 'ISO 8601 instant — only pages updated at/after it',
},
},
required: ['pond'],
},
},
{
name: 'read_page',
description: 'Read one page as Markdown plus metadata (title, labels, timestamps).',
inputSchema: {
type: 'object',
properties: { pond: pondProp, page: pageProp },
required: ['pond', 'page'],
},
},
{
name: 'search',
description:
'Full-text search across the reachable ponds. Returns pond/page slugs and snippets ' +
'with matches wrapped in **…** — chain into read_page.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search terms' },
pond: { ...pondProp, description: 'Restrict to one pond (slug)' },
label: { type: 'string', description: 'Restrict to pages carrying this label id' },
},
required: ['query'],
},
},
{
name: 'create_page',
description:
'Create a page from a title and Markdown content (requires a write-scope token and ' +
'editor rights in the pond). Returns the created page including its slug.',
inputSchema: {
type: 'object',
properties: {
pond: pondProp,
title: { type: 'string', maxLength: 200 },
markdown: { type: 'string', description: 'Initial content as Markdown' },
parent: {
type: 'string',
description: 'Optional parent page slug — nests the new page under it',
},
},
required: ['pond', 'title'],
},
},
{
name: 'update_page',
description:
'Update a page: set a new title and/or REPLACE the whole content with the given ' +
'Markdown (write scope required). The change is applied through the live ' +
'collaborative document, so open editors converge; the previous state stays in the ' +
'version history.',
inputSchema: {
type: 'object',
properties: {
pond: pondProp,
page: pageProp,
title: { type: 'string', maxLength: 200 },
markdown: { type: 'string', description: 'Replacement content as Markdown' },
parent: {
type: ['string', 'null'],
description:
'Move the page in the tree: a page slug nests it, null moves it to the top level',
},
},
required: ['pond', 'page'],
},
},
{
name: 'add_comment',
description:
"Comment on a page (write scope; the pond's comment policy applies). Markdown is " +
'supported.',
inputSchema: {
type: 'object',
properties: {
pond: pondProp,
page: pageProp,
text: { type: 'string', description: 'Comment body (Markdown)' },
},
required: ['pond', 'page', 'text'],
},
},
{
name: 'list_labels',
description: "The pond's label tree (ids + names) — ids feed set_page_labels.",
inputSchema: { type: 'object', properties: { pond: pondProp }, required: ['pond'] },
},
{
name: 'set_page_labels',
description:
"Replace a page's labels with exactly the given label ids (write scope). An empty " +
'list removes all labels.',
inputSchema: {
type: 'object',
properties: {
pond: pondProp,
page: pageProp,
labelIds: {
type: 'array',
items: { type: 'string' },
description: 'Label ids from list_labels',
},
},
required: ['pond', 'page', 'labelIds'],
},
},
{
name: 'export_pond',
description:
'A download link for the whole pond as a Markdown ZIP. The link goes through the ' +
'public REST API, so the instance REST switch and the same token are needed to ' +
'fetch it.',
inputSchema: { type: 'object', properties: { pond: pondProp }, required: ['pond'] },
},
];