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
215 lines
6.8 KiB
TypeScript
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'] },
|
|
},
|
|
];
|