All checks were successful
CD / Build and push images (push) Successful in 3m50s
CI / Lint, typecheck, test (push) Successful in 4m2s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Successful in 5m37s
CI / Import/export fidelity gate (push) Successful in 47s
AI clients talk to the instance directly at /api/mcp — under the /api/ path (deviation from the issue's literal /mcp) so every existing reverse proxy already routes it; no deployment changes anywhere. - Transport: official @modelcontextprotocol/sdk server, STATELESS — each POST builds a fresh server+transport pair, no session store, replicas stay trivial; GET/DELETE answer 405. Auth per PAT bearer (#104 tokens), per-token rate limit (429 + Retry-After). - Own switches, independent of REST: instance mcp.enabled (admin settings, default off; off = 404, feature invisible) + pond setting mcpEnabled (pond-settings toggle, default off) — pinned independent in both directions by tests. - Tools (thin wrappers over the #104 services, same permission gates, audit-logged writes): list_ponds, list_pages, read_page, search, create_page, update_page (replace semantics through the collab-owned restore path — open editors converge), add_comment, list_labels, set_page_labels (exact replace), export_pond (link to the REST ZIP). Tool errors carry the api error codes; results carry stable slugs/ids. MCP resources stay the documented stage-2 stretch goal. - Deliberately on the SDK's low-level Server API with a hand-written tool table (mcp-tools.ts): the typed registerTool generics drove tsc out of memory in a program this size; manual Zod validation keeps the wire behavior explicit. - PublicApiService exposure filtering parameterized ('api' | 'mcp', shared pondFeatureEnabled helper) — one implementation, two switches. - Docs: "Connect Claude Code / MCP clients" section in public-api.md (claude mcp add one-liner + mcp-remote bridge for stdio clients). Verification: 8-test e2e pack driving the real MCP SDK client over Streamable HTTP against a listening api (initialize + tools/list, switch independence in both directions, anonymous/garbage 401, opt-in 404 semantics, page roundtrip incl. restore-NOTIFY, labels/comments, read scope blocked from writes with scope_required); live check through the web proxy against the seeded stack (tools list, create, read, update, search — LIVE CHECK PASSED); full api suite 61/61 files green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
173 lines
5.5 KiB
TypeScript
173 lines
5.5 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);
|
|
|
|
export const MCP_TOOL_INPUTS = {
|
|
list_ponds: z.object({}),
|
|
list_pages: z.object({ pond }),
|
|
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(''),
|
|
}),
|
|
update_page: z
|
|
.object({
|
|
pond,
|
|
page,
|
|
title: z.string().min(1).max(200).optional(),
|
|
markdown: z.string().optional(),
|
|
})
|
|
.refine((input) => input.title !== undefined || input.markdown !== undefined, {
|
|
message: 'title or markdown 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, labels, timestamps.',
|
|
inputSchema: { type: 'object', properties: { pond: pondProp }, 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' },
|
|
},
|
|
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' },
|
|
},
|
|
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'] },
|
|
},
|
|
];
|