docs: rebuild the v2 documentation around a task-based page tree (#2397)

This commit is contained in:
Felix Weinberger 2026-06-30 21:18:42 +01:00 • committed by GitHub
commit b153505428
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 11884 additions and 4971 deletions

View file

@ -0,0 +1,100 @@
/**
* Companion example for `docs/advanced/custom-methods.md`.
*
* Every `ts` fence on that page is synced from a `//#region` in this file
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
* regions connects an in-memory client and produces the output the page quotes
* verbatim.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/advanced/custom-methods.examples.ts # from examples/
*
* @module
*/
/* eslint-disable no-console */
//#region setRequestHandler_custom
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const SearchParams = z.object({ query: z.string(), limit: z.number().int().default(10) });
const SearchResult = z.object({ items: z.array(z.string()) });
const mcp = new McpServer({ name: 'acme-search', version: '1.0.0' });
mcp.server.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async ({ query, limit }) => {
return { items: Array.from({ length: limit }, (_, index) => `${query}-${index}`) };
});
//#endregion setRequestHandler_custom
// "Declare an extension capability" — must happen before the server connects.
//#region registerCapabilities_extensions
mcp.server.registerCapabilities({
extensions: { 'com.example/feature-flags': { flags: ['dark-mode', 'beta-search'] } }
});
//#endregion registerCapabilities_extensions
// ---------------------------------------------------------------------------
// Harness (not shown on the page). An in-memory client drives the calls whose
// output advanced/custom-methods.md quotes verbatim. Any MCP client behaves
// the same. Imported dynamically so the page's lead region stays self-contained.
// ---------------------------------------------------------------------------
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
const client = new Client({ name: 'custom-methods-docs-harness', version: '1.0.0' });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await mcp.connect(serverTransport);
await client.connect(clientTransport);
// "Call it from the client" — the result the page quotes.
//#region request_custom
const result = await client.request({ method: 'acme/search', params: { query: 'mcp', limit: 3 } }, SearchResult);
console.log(result);
//#endregion request_custom
// Proof for the page's ::: tip — params that fail `SearchParams` are rejected
// before the handler runs. Throws (non-zero exit) if the claim is false.
let rejection: Error | undefined;
try {
await client.request({ method: 'acme/search', params: { query: 42 } }, SearchResult);
} catch (error) {
rejection = error as Error;
}
if (!rejection) {
throw new Error('custom-methods.md tip claim failed: invalid params were accepted');
}
console.log(rejection.message);
// "Read the negotiated extensions on the client" — the map the page quotes.
//#region getServerCapabilities_extensions
const extensions = client.getServerCapabilities()?.extensions ?? {};
console.log(extensions);
//#endregion getServerCapabilities_extensions
// "Send a custom notification from the handler" — registering the method again
// replaces its handler with one that reports progress.
//#region setRequestHandler_notify
mcp.server.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async ({ query, limit }, ctx) => {
await ctx.mcpReq.notify({ method: 'acme/searchProgress', params: { stage: 'start', pct: 0 } });
const items = Array.from({ length: limit }, (_, index) => `${query}-${index}`);
await ctx.mcpReq.notify({ method: 'acme/searchProgress', params: { stage: 'done', pct: 1 } });
return { items };
});
//#endregion setRequestHandler_notify
// "Receive it on the client" — the progress params the page quotes.
//#region setNotificationHandler_custom
const SearchProgressParams = z.object({ stage: z.string(), pct: z.number() });
client.setNotificationHandler('acme/searchProgress', { params: SearchProgressParams }, params => {
console.log(params);
});
await client.request({ method: 'acme/search', params: { query: 'mcp', limit: 1 } }, SearchResult);
//#endregion setNotificationHandler_custom
// Let the notification microtasks drain before tearing the pair down.
await new Promise(resolve => setImmediate(resolve));
await client.close();
await mcp.close();

View file

@ -0,0 +1,210 @@
/**
* Companion example for `docs/advanced/custom-transports.md`.
*
* Every `ts` fence on that page is synced from a `//#region` in this file
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
* regions connects a client and a server over the loopback transport and
* produces the output the page quotes verbatim.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/advanced/custom-transports.examples.ts # from examples/
*
* @module
*/
/* eslint-disable no-console */
//#region transport_loopback
import type { JSONRPCMessage, Transport } from '@modelcontextprotocol/server';
export class LoopbackTransport implements Transport {
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
private peer?: LoopbackTransport;
/** Cross-wire two ends: whatever one end sends, the other receives. */
static link(a: LoopbackTransport, b: LoopbackTransport): void {
a.peer = b;
b.peer = a;
}
async start(): Promise<void> {
// Open your channel here. The loopback has nothing to open.
}
async send(message: JSONRPCMessage): Promise<void> {
const peer = this.peer;
if (!peer) throw new Error('Loopback peer is gone');
queueMicrotask(() => peer.onmessage?.(message));
}
async close(): Promise<void> {
this.peer = undefined;
this.onclose?.();
}
}
//#endregion transport_loopback
// "Connect it like a built-in transport" — produces the output the page quotes.
//#region connect_loopback
import { Client } from '@modelcontextprotocol/client';
import { McpServer } from '@modelcontextprotocol/server';
const server = new McpServer({ name: 'loopback-demo', version: '1.0.0' });
server.registerTool('ping', { description: 'Reply with pong' }, async () => ({
content: [{ type: 'text', text: 'pong' }]
}));
const client = new Client({ name: 'loopback-client', version: '1.0.0' });
const serverEnd = new LoopbackTransport();
const clientEnd = new LoopbackTransport();
LoopbackTransport.link(serverEnd, clientEnd);
await server.connect(serverEnd);
await client.connect(clientEnd);
const result = await client.callTool({ name: 'ping' });
console.log(result.content);
//#endregion connect_loopback
// "Frame messages over a byte stream" — typechecked, not run (it needs a real
// socket). The runnable proof on this page is the loopback transport above.
//#region transport_socket
import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/server';
import type { Socket } from 'node:net';
export class SocketTransport implements Transport {
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
private readonly readBuffer = new ReadBuffer();
constructor(private readonly socket: Socket) {}
async start(): Promise<void> {
this.socket.on('data', chunk => {
try {
this.readBuffer.append(chunk);
let message = this.readBuffer.readMessage();
while (message !== null) {
this.onmessage?.(message);
message = this.readBuffer.readMessage();
}
} catch (error) {
this.onerror?.(error as Error);
}
});
this.socket.on('error', error => this.onerror?.(error));
this.socket.on('close', () => this.onclose?.());
}
async send(message: JSONRPCMessage): Promise<void> {
this.socket.write(serializeMessage(message));
}
async close(): Promise<void> {
this.socket.end();
}
}
//#endregion transport_socket
// "Report a session ID and the negotiated version".
//#region transport_session
export class SessionLoopbackTransport extends LoopbackTransport {
sessionId?: string;
protocolVersion?: string;
setProtocolVersion(version: string): void {
this.protocolVersion = version;
}
}
//#endregion transport_session
// "Opt into per-request cancellation" — typechecked, not run.
import type { TransportSendOptions } from '@modelcontextprotocol/server';
import { parseJSONRPCMessage } from '@modelcontextprotocol/server';
export class HttpPostTransport implements Transport {
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
constructor(private readonly endpoint: URL) {}
//#region transport_perRequest
readonly hasPerRequestStream = true;
async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void> {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: { 'content-type': 'application/json', ...options?.headers },
body: JSON.stringify(message),
signal: options?.requestSignal
});
this.onmessage?.(parseJSONRPCMessage(await response.json()));
}
//#endregion transport_perRequest
async start(): Promise<void> {}
async close(): Promise<void> {
this.onclose?.();
}
}
// ---------------------------------------------------------------------------
// Harness (not shown on the page). Drives the remaining claims the page makes
// and throws (non-zero exit) if any of them is false.
// ---------------------------------------------------------------------------
await client.close();
await server.close();
// "Report a session ID and the negotiated version" — the version the page quotes.
const sessionServer = new McpServer({ name: 'loopback-demo', version: '1.0.0' });
const sessionClient = new Client({ name: 'loopback-client', version: '1.0.0' });
const sessionServerEnd = new SessionLoopbackTransport();
const sessionClientEnd = new SessionLoopbackTransport();
LoopbackTransport.link(sessionServerEnd, sessionClientEnd);
await sessionServer.connect(sessionServerEnd);
await sessionClient.connect(sessionClientEnd);
console.log(sessionClientEnd.protocolVersion);
if (sessionServerEnd.protocolVersion !== sessionClientEnd.protocolVersion) {
throw new Error(
`custom-transports.md claim failed: server end negotiated ${sessionServerEnd.protocolVersion}, client end ${sessionClientEnd.protocolVersion}`
);
}
await sessionClient.close();
await sessionServer.close();
// "Test it against the in-memory pair" — the reference transport returns the
// same result the loopback produced.
const referenceServer = new McpServer({ name: 'loopback-demo', version: '1.0.0' });
referenceServer.registerTool('ping', { description: 'Reply with pong' }, async () => ({
content: [{ type: 'text', text: 'pong' }]
}));
const referenceClient = new Client({ name: 'loopback-client', version: '1.0.0' });
//#region inMemory_pair
import { InMemoryTransport } from '@modelcontextprotocol/client';
const [inMemoryClientEnd, inMemoryServerEnd] = InMemoryTransport.createLinkedPair();
//#endregion inMemory_pair
await referenceServer.connect(inMemoryServerEnd);
await referenceClient.connect(inMemoryClientEnd);
const referenceResult = await referenceClient.callTool({ name: 'ping' });
if (JSON.stringify(referenceResult.content) !== JSON.stringify(result.content)) {
throw new Error(`custom-transports.md claim failed: in-memory result ${JSON.stringify(referenceResult.content)}`);
}
await referenceClient.close();
await referenceServer.close();

View file

@ -0,0 +1,135 @@
/**
* Runnable, type-checked companion for `docs/advanced/gateway.md`.
*
* Each `//#region` block is synced byte-for-byte into that page's code fences
* (`pnpm sync:snippets --check` reports drift).
*
* The page's program runs for real: the harness below builds a
* `createMcpHandler` server and routes `globalThis.fetch` for
* `http://localhost:3000/mcp` into `handler.fetch`, so the HTTP regions
* execute in-process without binding a port. The output the page quotes
* verbatim is whatever this file prints. The server's `request_count` tool
* returns how many MCP requests reached the process (`createMcpHandler`
* builds one server instance per request), which is what proves that
* `connect({ prior })` sent nothing.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/advanced/gateway.examples.ts # from examples/
*
* @module
*/
/* eslint-disable no-console */
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
// ---------------------------------------------------------------------------
// Harness (not shown on the page). Every `new McpServer(...)` is one inbound
// MCP request, so the module-level counter is the number of requests the
// process has answered — `request_count` exposes it to the page's clients.
// ---------------------------------------------------------------------------
let requests = 0;
const handler = createMcpHandler(() => {
requests++;
const server = new McpServer({ name: 'gateway-target', version: '1.0.0' }, { capabilities: { tools: { listChanged: true } } });
server.registerTool('echo', { description: 'Echo the input back', inputSchema: z.object({ text: z.string() }) }, async ({ text }) => ({
content: [{ type: 'text', text }]
}));
server.registerTool(
'request_count',
{ description: 'Number of MCP requests this server process has answered', outputSchema: z.object({ requests: z.number() }) },
async () => ({ content: [{ type: 'text', text: String(requests) }], structuredContent: { requests } })
);
return server;
});
const realFetch = globalThis.fetch;
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const request = new Request(input, init);
if (new URL(request.url).host === 'localhost:3000') return handler.fetch(request);
return realFetch(input, init);
}) as typeof fetch;
// ## Connect with a prior discover result
//#region connect_prior
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const url = new URL('http://localhost:3000/mcp');
// Probe once …
const bootstrap = new Client({ name: 'gateway', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
await bootstrap.connect(new StreamableHTTPClientTransport(url));
const persisted = JSON.stringify(bootstrap.getDiscoverResult());
// … then every other client connects with zero round trips.
const worker = new Client({ name: 'worker', version: '1.0.0' });
await worker.connect(new StreamableHTTPClientTransport(url), { prior: JSON.parse(persisted) });
//#endregion connect_prior
// ## Probe once at bootstrap
//#region bootstrap_probe
console.log(bootstrap.getDiscoverResult());
//#endregion bootstrap_probe
// ## Persist the advertisement
//#region persist_advertisement
import type { DiscoverResult } from '@modelcontextprotocol/client';
const prior = JSON.parse(persisted) as DiscoverResult;
//#endregion persist_advertisement
// ## Fan out to workers
//#region fan_out
const fleet = await Promise.all(
['worker-a', 'worker-b', 'worker-c'].map(async name => {
const replica = new Client({ name, version: '1.0.0' });
await replica.connect(new StreamableHTTPClientTransport(url), { prior });
return replica;
})
);
const proof = await worker.callTool({ name: 'request_count' });
console.log(proof.structuredContent);
//#endregion fan_out
// ## Open a listen stream when a worker needs notifications
//#region listen_worker
const subscription = await worker.listen({ toolsListChanged: true });
console.log(subscription.honoredFilter);
//#endregion listen_worker
await subscription.close();
// ## Handle a stale or incompatible advertisement
//#region prior_stale
import { SdkError, SdkErrorCode } from '@modelcontextprotocol/client';
const stale: DiscoverResult = { ...prior, supportedVersions: ['2025-06-18'] };
const late = new Client({ name: 'worker-d', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
try {
await late.connect(new StreamableHTTPClientTransport(url), { prior: stale });
} catch (error) {
if (!(error instanceof SdkError) || error.code !== SdkErrorCode.EraNegotiationFailed) throw error;
console.log(error.code);
// Fall back to a fresh probe, then re-persist getDiscoverResult().
await late.connect(new StreamableHTTPClientTransport(url));
console.log('re-probed:', late.getNegotiatedProtocolVersion());
}
//#endregion prior_stale
// ---------------------------------------------------------------------------
// Harness teardown.
// ---------------------------------------------------------------------------
for (const client of [bootstrap, worker, late, ...fleet]) await client.close();
await handler.close();
globalThis.fetch = realFetch;

View file

@ -0,0 +1,179 @@
/**
* Companion example for `docs/advanced/low-level-server.md`.
*
* Every `ts` fence on that page is synced from a `//#region` in this file
* (`pnpm sync:snippets --check`). The file also runs: the harness between the
* regions connects in-memory clients and produces every output the page quotes
* verbatim, exiting non-zero on drift.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/advanced/low-level-server.examples.ts # from examples/
*
* `lowLevel_serve` lives in a never-invoked wrapper: `serveStdio` would bind
* this process's real stdin/stdout, so that one region is typecheck-only.
*
* @module
*/
/* eslint-disable no-console, import/no-duplicates */
// Harness imports. The page's lead block (the first region) carries its own
// `Server` import so the rendered fence stands alone.
import { createMcpHandler, fromJsonSchema, McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
// ---------------------------------------------------------------------------
// "Build the server and list your tools by hand"
// ---------------------------------------------------------------------------
//#region lowLevel_listTools
import { Server } from '@modelcontextprotocol/server';
const catalog = [
{ name: 'Espresso cup', price: 12 },
{ name: 'Travel mug', price: 24 },
{ name: 'Mug rack', price: 36 }
];
const server = new Server({ name: 'catalog', version: '1.0.0' }, { capabilities: { tools: {} } });
server.setRequestHandler('tools/list', async () => ({
tools: [
{
name: 'search',
description: 'Search the product catalog',
inputSchema: {
type: 'object',
properties: { query: { type: 'string', description: 'Substring to match against product names' } },
required: ['query']
}
}
]
}));
//#endregion lowLevel_listTools
// ---------------------------------------------------------------------------
// "Handle tools/call yourself"
// ---------------------------------------------------------------------------
//#region lowLevel_callTool
server.setRequestHandler('tools/call', async request => {
if (request.params.name !== 'search') {
return { content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], isError: true };
}
const { query } = request.params.arguments as { query: string };
const hits = catalog.filter(product => product.name.toLowerCase().includes(query.toLowerCase()));
return { content: [{ type: 'text', text: hits.map(product => product.name).join('\n') }] };
});
//#endregion lowLevel_callTool
// ---------------------------------------------------------------------------
// Harness (not shown on the page). An in-memory client drives the calls whose
// output low-level-server.md quotes verbatim. Imported dynamically so the
// page's lead region stays self-contained.
// ---------------------------------------------------------------------------
const { Client, InMemoryTransport, ProtocolError } = await import('@modelcontextprotocol/client');
const client = new Client({ name: 'low-level-docs-harness', version: '1.0.0' });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
// The handler answers a valid call exactly like the McpServer version.
const result = await client.callTool({ name: 'search', arguments: { query: 'mug' } });
console.log(result.content);
// Nothing validated `query`, so a wrongly-typed argument reaches the handler
// and crashes it: the client sees a JSON-RPC error, not a tool result.
const crashed = await client.callTool({ name: 'search', arguments: { query: 42 } }).catch((error: unknown) => error);
if (!(crashed instanceof ProtocolError)) {
throw new Error(`low-level-server.md expected the unvalidated call to reject: ${JSON.stringify(crashed)}`);
}
console.log(`${crashed.name} ${crashed.code}: ${crashed.message}`);
// ---------------------------------------------------------------------------
// "Validate arguments yourself"
// ---------------------------------------------------------------------------
//#region lowLevel_validate
const SearchArguments = fromJsonSchema<{ query: string }>({
type: 'object',
properties: { query: { type: 'string' } },
required: ['query']
});
server.setRequestHandler('tools/call', async request => {
if (request.params.name !== 'search') {
return { content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], isError: true };
}
const parsed = await SearchArguments['~standard'].validate(request.params.arguments ?? {});
if (parsed.issues) {
return { content: [{ type: 'text', text: parsed.issues.map(issue => issue.message).join('; ') }], isError: true };
}
const hits = catalog.filter(product => product.name.toLowerCase().includes(parsed.value.query.toLowerCase()));
return { content: [{ type: 'text', text: hits.map(product => product.name).join('\n') }] };
});
//#endregion lowLevel_validate
// The same wrongly-typed call now comes back as an ordinary isError result.
const rejected = await client.callTool({ name: 'search', arguments: { query: 42 } });
console.log(rejected);
if (rejected.isError !== true) {
throw new Error(`low-level-server.md expected the validated call to return isError: ${JSON.stringify(rejected)}`);
}
await client.close();
await server.close();
// ---------------------------------------------------------------------------
// "Serve it with the same entry points" — typecheck-only. `serveStdio` would
// take over this process's stdin/stdout, so the harness never calls this.
// ---------------------------------------------------------------------------
function lowLevel_serve() {
//#region lowLevel_serve
serveStdio(() => server);
createMcpHandler(() => server);
//#endregion lowLevel_serve
}
void lowLevel_serve;
// ---------------------------------------------------------------------------
// "Reach the low level from McpServer"
// ---------------------------------------------------------------------------
//#region lowLevel_escapeHatch
const mcp = new McpServer({ name: 'catalog', version: '1.0.0' }, { capabilities: { resources: { subscribe: true } } });
mcp.registerTool(
'search',
{ description: 'Search the product catalog', inputSchema: z.object({ query: z.string() }) },
async ({ query }) => {
const names = catalog.filter(product => product.name.includes(query)).map(product => product.name);
return { content: [{ type: 'text', text: names.join('\n') }] };
}
);
const subscriptions = new Set<string>();
mcp.server.setRequestHandler('resources/subscribe', async request => {
subscriptions.add(request.params.uri);
return {};
});
//#endregion lowLevel_escapeHatch
// Harness: prove the page's claim for the section above — `registerTool` still
// owns `tools/list`, and the hand-registered handler answers
// `resources/subscribe` on the same connection.
const mcpClient = new Client({ name: 'low-level-docs-harness', version: '1.0.0' });
const [mcpClientTransport, mcpServerTransport] = InMemoryTransport.createLinkedPair();
await mcp.connect(mcpServerTransport);
await mcpClient.connect(mcpClientTransport);
const { tools } = await mcpClient.listTools();
await mcpClient.subscribeResource({ uri: 'demo://config' });
if (tools.length !== 1 || tools[0]?.name !== 'search' || !subscriptions.has('demo://config')) {
throw new Error(`low-level-server.md escape-hatch claim failed: ${JSON.stringify({ tools, subscriptions: [...subscriptions] })}`);
}
await mcpClient.close();
await mcp.close();

View file

@ -0,0 +1,129 @@
/**
* Companion example for `docs/advanced/schema-libraries.md`.
*
* Every `ts` fence on that page is synced from a `//#region` in this file
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
* regions connects an in-memory client and produces the output the page quotes
* verbatim.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/advanced/schema-libraries.examples.ts # from examples/
*
* @module
*/
/* eslint-disable no-console */
//#region registerTool_arktype
import { McpServer } from '@modelcontextprotocol/server';
import { type } from 'arktype';
const server = new McpServer({ name: 'schema-zoo', version: '1.0.0' });
server.registerTool(
'greet',
{
description: 'Greet someone by name',
inputSchema: type({ name: 'string', 'times?': '1 <= number.integer <= 5' })
},
async ({ name, times }) => ({
content: [{ type: 'text', text: Array.from({ length: times ?? 1 }, () => `Hello, ${name}`).join('\n') }]
})
);
//#endregion registerTool_arktype
//#region registerTool_valibot
import { toStandardJsonSchema } from '@valibot/to-json-schema';
import * as v from 'valibot';
server.registerTool(
'shout',
{ description: 'Greet someone, loudly', inputSchema: toStandardJsonSchema(v.object({ name: v.string() })) },
async ({ name }) => ({ content: [{ type: 'text', text: `HELLO, ${name.toUpperCase()}` }] })
);
//#endregion registerTool_valibot
//#region registerTool_fromJsonSchema
import { fromJsonSchema } from '@modelcontextprotocol/server';
server.registerTool(
'farewell',
{
description: 'Say goodbye',
inputSchema: fromJsonSchema<{ name: string }>({
type: 'object',
properties: { name: { type: 'string' } },
required: ['name']
})
},
async ({ name }) => ({ content: [{ type: 'text', text: `Goodbye, ${name}` }] })
);
//#endregion registerTool_fromJsonSchema
//#region registerTool_outputSchema
server.registerTool(
'measure',
{
description: 'Measure the length of a name',
inputSchema: type({ name: 'string' }),
outputSchema: type({ name: 'string', length: 'number' })
},
async ({ name }) => {
const output = { name, length: name.length };
return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output };
}
);
//#endregion registerTool_outputSchema
//#region jsonSchemaValidator_ajv
import { addFormats, Ajv, AjvJsonSchemaValidator } from '@modelcontextprotocol/server/validators/ajv';
const ajv = new Ajv({ strict: true, allErrors: true });
addFormats(ajv);
const validator = new AjvJsonSchemaValidator(ajv);
const strict = new McpServer({ name: 'schema-zoo', version: '1.0.0' }, { jsonSchemaValidator: validator });
//#endregion jsonSchemaValidator_ajv
//#region jsonSchemaValidator_cfWorker
import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';
const edge = new McpServer({ name: 'schema-zoo', version: '1.0.0' }, { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() });
//#endregion jsonSchemaValidator_cfWorker
// ---------------------------------------------------------------------------
// Harness (not shown on the page). An in-memory client drives the calls whose
// output advanced/schema-libraries.md quotes verbatim. Any MCP client behaves
// the same. Imported dynamically so the page's lead region stays
// self-contained.
// ---------------------------------------------------------------------------
const { Client, InMemoryTransport } = await import('@modelcontextprotocol/client');
const client = new Client({ name: 'schema-libraries-docs-harness', version: '1.0.0' });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
// "Register a tool with an ArkType schema" — the rejection the page quotes.
const rejected = await client.callTool({ name: 'greet', arguments: { name: 'Ada', times: 99 } });
console.log(rejected);
// "Register a tool with a Valibot schema" — proves the page's prose claim that
// the call validates and dispatches like any other tool.
const shouted = await client.callTool({ name: 'shout', arguments: { name: 'Ada' } });
if (JSON.stringify(shouted.content) !== JSON.stringify([{ type: 'text', text: 'HELLO, ADA' }])) {
throw new Error(`schema-libraries.md valibot claim failed: ${JSON.stringify(shouted)}`);
}
// "Start from JSON Schema you already have" — the advertised schema the page quotes.
const { tools } = await client.listTools();
const farewell = tools.find(tool => tool.name === 'farewell');
console.log(farewell?.inputSchema);
// "Validate structured output with any library" — the structured result the page quotes.
const measured = await client.callTool({ name: 'measure', arguments: { name: 'Ada' } });
console.log(measured);
await client.close();
await server.close();
await strict.close();
await edge.close();

View file

@ -0,0 +1,104 @@
/**
* Companion example for `docs/advanced/wire-schemas.md`.
*
* Every `ts` fence on that page is synced from a `//#region` in this file
* (`pnpm sync:snippets --check`). The file also runs: it is the gateway path
* the page walks through — raw JSON in, validated with the schemas from
* `@modelcontextprotocol/core`, no `Client` or `Server` anywhere — and it
* prints the exact output the page quotes verbatim.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/advanced/wire-schemas.examples.ts # from examples/
*
* @module
*/
/* eslint-disable @typescript-eslint/no-unused-vars, no-console */
// "Validate a wire payload" — the happy path the page quotes.
//#region wireSchemas_validateResult
import { CallToolResultSchema } from '@modelcontextprotocol/core';
// The body an upstream server returned for a tools/call you forwarded.
const body: unknown = JSON.parse('{"content":[{"type":"text","text":"Travel mug"}]}');
const parsed = CallToolResultSchema.safeParse(body);
if (!parsed.success) {
throw new Error(`upstream returned an invalid tools/call result: ${parsed.error.message}`);
}
console.log(parsed.data.content);
//#endregion wireSchemas_validateResult
// "Validate a wire payload" — the rejection the page quotes.
//#region wireSchemas_validateResult_invalid
const malformed = CallToolResultSchema.safeParse({ content: 'Travel mug' });
console.log(malformed.error?.issues);
//#endregion wireSchemas_validateResult_invalid
// "Pick the schema for the message you hold" — the undecoded envelope.
//#region wireSchemas_envelope
import { JSONRPCMessageSchema } from '@modelcontextprotocol/core';
const frame = '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"query":"mug"}}}';
const message = JSONRPCMessageSchema.parse(JSON.parse(frame));
//#endregion wireSchemas_envelope
// "Route raw JSON-RPC in a proxy" — branch on method, then the per-method schema.
//#region wireSchemas_route
import { CallToolRequestSchema } from '@modelcontextprotocol/core';
if ('method' in message) {
switch (message.method) {
case 'tools/call': {
const call = CallToolRequestSchema.parse(message);
console.log(`forward tools/call for ${call.params.name} upstream`);
break;
}
default:
console.log(`forward ${message.method} unchanged`);
}
}
//#endregion wireSchemas_route
// "Validate OAuth and discovery metadata" — the second export group.
//#region wireSchemas_oauthMetadata
import { OAuthMetadataSchema } from '@modelcontextprotocol/core';
// In production this body comes from GET <issuer>/.well-known/oauth-authorization-server.
const response = new Response(
JSON.stringify({
issuer: 'https://auth.example.com',
authorization_endpoint: 'https://auth.example.com/authorize',
token_endpoint: 'https://auth.example.com/token',
response_types_supported: ['code']
})
);
const metadata = OAuthMetadataSchema.parse(await response.json());
console.log(metadata.token_endpoint);
//#endregion wireSchemas_oauthMetadata
// "Get the TypeScript types, guards and errors from the SDK packages" — core is
// Zod values only; the names live in /client and /server.
//#region wireSchemas_types
import type { CallToolResult } from '@modelcontextprotocol/client';
import * as z from 'zod/v4';
// The SDK's spec type and the schema's own inferred output describe the same value.
const relayed: CallToolResult = parsed.data;
type CallToolResultFromCore = z.infer<typeof CallToolResultSchema>;
//#endregion wireSchemas_types
// ---------------------------------------------------------------------------
// Self-checks (not shown on the page). Throw — non-zero exit — if any claim
// the page makes stops being true.
// ---------------------------------------------------------------------------
if (malformed.success) {
throw new Error('wire-schemas.md claim failed: a non-array `content` must not parse');
}
if (!('method' in message) || message.method !== 'tools/call') {
throw new Error('wire-schemas.md claim failed: the envelope did not narrow to a tools/call request');
}
if (relayed.content[0]?.type !== 'text') {
throw new Error('wire-schemas.md claim failed: parsed result is not assignable to CallToolResult');
}