/** * Self-contained MCP server demonstrating JSON Schema 2020-12 tool inputs, * enabled by SEP-2106 (tool inputSchema/outputSchema conform to JSON Schema * 2020-12). Each tool in `demo-tools.ts` exercises a different 2020-12 keyword * that was not permitted in a tool input before SEP-2106 (enum varieties, * top-level oneOf, discriminated unions, $ref/$defs, if/then/else, prefixItems * tuples). * * Built on the 2.0 SDK's `fromJsonSchema` adapter, which advertises each raw * JSON Schema verbatim in tools/list and strictly validates tools/call * arguments against it (via @cfworker/json-schema). * * Confirmed against @modelcontextprotocol/server@2.0.0 (GA). In 2.0, * `StdioServerTransport` and `CfWorkerJsonSchemaValidator` live on subpath * exports (see the imports below); `McpServer`, `fromJsonSchema`, and * `registerTool` are unchanged from the 2.0 alpha. * * SEP-2106: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2106-json-schema-2020-12.md * * Run over stdio: npm start (then point MCP Inspector or any client at it) * Run over HTTP: npm run start:http (Streamable HTTP at /mcp — see http.ts) */ import { McpServer, fromJsonSchema } from "@modelcontextprotocol/server"; import type { CallToolResult } from "@modelcontextprotocol/server"; // Since 2.0 beta these two live on subpath exports, not the package root // (alpha.2 imported both from "@modelcontextprotocol/server"): import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/server/validators/cf-worker"; import { pathToFileURL } from "node:url"; import { DEMO_TOOLS } from "./demo-tools.js"; // Validate against JSON Schema 2020-12 explicitly. The SDK's default validator // does NOT use the 2020-12 dialect, so newer keywords like `prefixItems` are // misinterpreted (e.g. `items: false` gets applied to every element). Passing a // 2020-12 validator is required for full SEP-2106 fidelity. const validator = new CfWorkerJsonSchemaValidator({ draft: "2020-12" }); // A canned `structuredContent` per tool that CONFORMS to that tool's outputSchema, // so the server demonstrates a real 2020-12 outputSchema round-trip (advertise + // return + validate). Mirrors the Python sibling's _OUTPUTS. const OUTPUTS: Record = { "get-enum-selections": { result: "applied", appliedCount: 3 }, "lookup-record": { found: true, name: "Acme Corp" }, "create-payment": { receiptId: "rcpt_001", outcome: { status: "settled", settledAt: "2026-06-25T12:00:00Z" }, }, "create-shipment": { shipmentId: "shp_001", origin: { city: "San Francisco", eta: "2026-06-26" }, destination: { city: "London", eta: "2026-06-28" }, }, "register-address": { status: "active", validUntil: "2027-06-25" }, "plot-point": { plotted: [3, 7], ok: true }, }; /** Shared handler: echo the validated arguments AND return a 2020-12-conforming * structuredContent for the tool's outputSchema. */ function echo(name: string, args: Record | undefined): CallToolResult { const entries = Object.entries(args ?? {}); return { content: [ { type: "text", text: entries.length > 0 ? `You provided:\n${entries .map(([k, v]) => `- ${k}: ${JSON.stringify(v)}`) .join("\n")}` : "No arguments provided.", }, ], structuredContent: OUTPUTS[name] as Record | undefined, }; } /** Build the demo server (exported so it can be driven in-process by tests). */ export function createServer(): McpServer { const server = new McpServer( { // Identity mirrors the public HF Space name (huggingface.co/spaces/ // olaservo/json-schema-2020-12-ts). Keep `version` in sync with // package.json. name: "json-schema-2020-12-ts", title: "JSON Schema 2020-12 tool inputs (TypeScript)", version: "1.0.0", description: "Demo MCP server: six tools whose inputSchemas exercise the JSON Schema 2020-12 keywords SEP-2106 allows (top-level oneOf, const unions, $ref/$defs, if/then, prefixItems, SEP-1330 enum varieties).", websiteUrl: "https://olaservo-sandyland.static.hf.space/mcp/tool-schemas/index.html", }, { capabilities: { tools: {} }, // The six tools are static per deploy — let 2026-07-28 clients cache // tools/list for a day, shared caches included (keyless public demo). cacheHints: { "tools/list": { ttlMs: 86_400_000, cacheScope: "public" } }, } ); for (const tool of DEMO_TOOLS) { server.registerTool( tool.name, { title: tool.title, description: tool.description, inputSchema: fromJsonSchema(tool.jsonSchema, validator), outputSchema: tool.outputSchema ? fromJsonSchema(tool.outputSchema, validator) : undefined, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async (args): Promise => echo(tool.name, args as Record) ); } return server; } async function main(): Promise { const server = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.error( `json-schema-2020-12-ts running on stdio (${DEMO_TOOLS.length} tools)` ); process.on("SIGINT", async () => { await server.close(); process.exit(0); }); } // Only launch the stdio server when this module is run directly (npm start / // selftest spawn `tsx src/server.ts`). When src/http.ts imports `createServer` // to mount the same server over Streamable HTTP, this guard keeps stdio from // starting and stealing the process's stdin/stdout. const isEntrypoint = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href; if (isEntrypoint) { main().catch((error) => { console.error("Server error:", error); process.exit(1); }); }