Skip to content

Latest commit

 

History

History
200 lines (153 loc) · 6.19 KB

File metadata and controls

200 lines (153 loc) · 6.19 KB

Protocol features

This page covers cross-cutting protocol mechanics that apply to both clients and servers.

Ping

Both client and server expose a ping() method for health checks. The remote side responds automatically — no handler registration is needed.

// Client pinging the server:
await client.ping();

// With a timeout (milliseconds):
await client.ping({ timeout: 5000 });

// Server pinging the client (via the low-level server, no timeout option):
await server.server.ping();

Progress notifications

Long-running requests can report progress to the caller. The SDK handles progressToken assignment automatically when you provide an onprogress callback.

Receiving progress (client side):

const result = await client.callTool({ name: 'long-task', arguments: {} }, CallToolResultSchema, {
    onprogress: progress => {
        // progress has: { progress: number, total?: number, message?: string }
        console.log(`${progress.progress}/${progress.total}: ${progress.message}`);
    },
    timeout: 30000,
    resetTimeoutOnProgress: true
});

Sending progress (server side, from a tool handler):

server.registerTool(
    'count',
    {
        description: 'Count to N with progress updates',
        inputSchema: { n: z.number() }
    },
    async ({ n }, extra) => {
        for (let i = 1; i <= n; i++) {
            if (extra._meta?.progressToken !== undefined) {
                await extra.sendNotification({
                    method: 'notifications/progress',
                    params: {
                        progressToken: extra._meta.progressToken,
                        progress: i,
                        total: n,
                        message: `Counting: ${i}/${n}`
                    }
                });
            }
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        return { content: [{ type: 'text', text: `Counted to ${n}` }] };
    }
);

For a runnable example, see progressExample.ts.

Cancellation

Requests can be cancelled by the caller using an AbortSignal. The SDK sends a notifications/cancelled message to the remote side and aborts the handler via its signal.

Client cancelling a request:

const controller = new AbortController();

const resultPromise = client.callTool({ name: 'slow-tool', arguments: {} }, CallToolResultSchema, { signal: controller.signal });

// Cancel after 5 seconds:
setTimeout(() => controller.abort('User cancelled'), 5000);

Server handler responding to cancellation:

server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
    for (let i = 0; i < 100; i++) {
        if (extra.signal.aborted) {
            return { content: [{ type: 'text', text: 'Cancelled' }], isError: true };
        }
        await doWork();
    }
    return { content: [{ type: 'text', text: 'Done' }] };
});

Pagination

All list methods (listTools, listPrompts, listResources, listResourceTemplates) support cursor-based pagination. Pass cursor from the previous response's nextCursor to fetch the next page.

let cursor: string | undefined;
const allTools: Tool[] = [];

do {
    const result = await client.listTools({ cursor });
    allTools.push(...result.tools);
    cursor = result.nextCursor;
} while (cursor);

The same pattern applies to listPrompts, listResources, and listResourceTemplates.

Capability negotiation

Both client and server declare their capabilities during the initialize handshake. The SDK enforces these — attempting to use an undeclared capability throws an error.

Client capabilities are set at construction time:

const client = new Client(
    { name: 'my-client', version: '1.0.0' },
    {
        capabilities: {
            roots: { listChanged: true },
            sampling: {},
            elicitation: { form: {} }
        }
    }
);

After connecting, inspect what the server supports:

await client.connect(transport);

const caps = client.getServerCapabilities();
if (caps?.tools) {
    const tools = await client.listTools();
}
if (caps?.resources?.subscribe) {
    // server supports resource subscriptions
}

Server capabilities are inferred from registered handlers. When using McpServer, capabilities are set automatically based on what you register (tools, resources, prompts). With the low-level Server, you declare them in the constructor.

Protocol version negotiation

The SDK automatically negotiates protocol versions during initialize. The client sends LATEST_PROTOCOL_VERSION and the server responds with the highest mutually supported version.

Supported versions are defined in SUPPORTED_PROTOCOL_VERSIONS (currently 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07). If the server responds with an unsupported version, the client throws an error.

Version negotiation is handled automatically by client.connect(). After connecting, you can inspect the result:

await client.connect(transport);

const serverVersion = client.getServerVersion();
// { name: 'my-server', version: '1.0.0' }

const serverCaps = client.getServerCapabilities();
// { tools: { listChanged: true }, resources: { subscribe: true }, ... }

JSON Schema 2020-12

MCP uses JSON Schema 2020-12 for tool input and output schemas. When using McpServer with Zod, schemas are converted to JSON Schema automatically:

server.registerTool(
    'calculate',
    {
        description: 'Add two numbers',
        inputSchema: { a: z.number(), b: z.number() }
    },
    async ({ a, b }) => ({
        content: [{ type: 'text', text: String(a + b) }]
    })
);

With the low-level Server, you provide JSON Schema directly:

{
  name: 'calculate',
  inputSchema: {
    type: 'object',
    properties: {
      a: { type: 'number' },
      b: { type: 'number' }
    },
    required: ['a', 'b']
  }
}

The SDK validates tool outputs against outputSchema (when provided) using a pluggable JSON Schema validator. The default validator uses Ajv; a Cloudflare Workers-compatible alternative is available via CfWorkerJsonSchemaValidator.