docs: comprehensive feature documentation for SEP-1730 Tier 1 (#1548)
This commit is contained in:
parent
2084a22074
commit
342ea394ca
1 changed files with 742 additions and 5 deletions
200
docs/protocol.md
Normal file
200
docs/protocol.md
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
## 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.
|
||||
|
||||
```typescript
|
||||
// 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):
|
||||
|
||||
```typescript
|
||||
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):
|
||||
|
||||
```typescript
|
||||
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`](../src/examples/server/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**:
|
||||
|
||||
```typescript
|
||||
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**:
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
{
|
||||
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`.
|
||||
Loading…
Reference in a new issue