docs: comprehensive feature documentation for SEP-1730 Tier 1 (#1548)

This commit is contained in:
Felix Weinberger 2026-02-17 16:22:16 +00:00 • committed by GitHub
commit 342ea394ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 742 additions and 5 deletions

View file

@ -154,6 +154,7 @@ For more details on how to run these examples (including recommended commands an
- [docs/server.md](docs/server.md) – building and running MCP servers, transports, tools/resources/prompts, CORS, DNS rebinding, and multi-node deployment.
- [docs/client.md](docs/client.md) – using the high-level client, transports, backwards compatibility, and OAuth helpers.
- [docs/capabilities.md](docs/capabilities.md) – sampling, elicitation (form and URL), and experimental task-based execution.
- [docs/protocol.md](docs/protocol.md) – protocol features: ping, progress, cancellation, pagination, capability negotiation, and JSON Schema.
- [docs/faq.md](docs/faq.md) – environment and troubleshooting FAQs (including Node.js Web Crypto support).
- External references:
- [Model Context Protocol documentation](https://modelcontextprotocol.io)

View file

@ -27,6 +27,99 @@ Runnable example:
The `simpleStreamableHttp` server also includes a `collect-user-info` tool that demonstrates how to drive elicitation from a tool and handle the response.
#### Schema validation
Elicitation schemas support validation constraints on each field. The server validates responses automatically against the `requestedSchema` using the SDK's JSON Schema validator.
```typescript
const result = await server.server.elicitInput({
mode: 'form',
message: 'Enter your details:',
requestedSchema: {
type: 'object',
properties: {
email: {
type: 'string',
title: 'Email',
format: 'email',
minLength: 5
},
age: {
type: 'integer',
title: 'Age',
minimum: 0,
maximum: 150
}
},
required: ['email']
}
});
```
String fields support `minLength`, `maxLength`, and `format` (`'email'`, `'uri'`, `'date'`, `'date-time'`). Number fields support `minimum` and `maximum`.
#### Default values
Schema properties can include `default` values. When the client declares the `applyDefaults` capability, the SDK automatically fills in defaults for fields the user doesn't provide.
> **Note:** `applyDefaults` is a TypeScript SDK extension — it is not part of the MCP protocol specification.
```typescript
// Client declares applyDefaults:
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{ capabilities: { elicitation: { form: { applyDefaults: true } } } }
);
// Server schema with defaults:
requestedSchema: {
type: 'object',
properties: {
newsletter: { type: 'boolean', title: 'Newsletter', default: false },
theme: { type: 'string', title: 'Theme', default: 'dark' }
}
}
```
#### Enum values
Elicitation schemas support several enum patterns for single-select and multi-select fields:
```typescript
requestedSchema: {
type: 'object',
properties: {
// Simple enum (untitled options)
color: {
type: 'string',
title: 'Favorite Color',
enum: ['red', 'green', 'blue'],
default: 'blue'
},
// Titled enum with display labels
priority: {
type: 'string',
title: 'Priority',
oneOf: [
{ const: 'low', title: 'Low Priority' },
{ const: 'medium', title: 'Medium Priority' },
{ const: 'high', title: 'High Priority' }
]
},
// Multi-select
tags: {
type: 'array',
title: 'Tags',
items: { type: 'string', enum: ['frontend', 'backend', 'docs'] },
minItems: 1,
maxItems: 3
}
}
}
```
For a full example with validation, defaults, and enums, see [`elicitationFormExample.ts`](../src/examples/server/elicitationFormExample.ts).
### URL elicitation
URL elicitation is designed for sensitive data and secure web‑based flows (e.g., collecting an API key, confirming a payment, or doing third‑party OAuth). Instead of returning form data, the server asks the client to open a URL and the rest of the flow happens in the browser.
@ -46,6 +139,23 @@ Key points:
Sensitive information **must not** be collected via form elicitation; always use URL elicitation or out‑of‑band flows for secrets.
#### Complete notification
When a URL elicitation flow finishes (the user completes the browser-based action), the server sends a `notifications/elicitation/complete` notification to the client. This tells the client the out-of-band flow is done and any pending UI can be dismissed.
Use `createElicitationCompletionNotifier` on the low-level server to create a callback that sends this notification:
```typescript
// Create a notifier for a specific elicitation:
const notifyComplete = server.server.createElicitationCompletionNotifier('setup-123');
// Later, when the browser flow completes (e.g. via webhook):
await notifyComplete();
// Client receives: { method: 'notifications/elicitation/complete', params: { elicitationId: 'setup-123' } }
```
See [`elicitationUrlExample.ts`](../src/examples/server/elicitationUrlExample.ts) for a full working example.
## Task-based execution (experimental)
Task-based execution enables “call-now, fetch-later” patterns for long-running operations. Instead of returning a result immediately, a tool creates a task that can be polled or resumed later.
@ -70,7 +180,7 @@ For a runnable example that uses the in-memory store shipped with the SDK, see:
On the client, you use:
- `client.experimental.tasks.callToolStream(...)` to start a tool call that may create a task and emit status updates over time.
- `client.getTask(...)` and `client.getTaskResult(...)` to check status and fetch results after reconnecting.
- `client.experimental.tasks.getTask(...)` and `client.experimental.tasks.getTaskResult(...)` to check status and fetch results after reconnecting.
The interactive client in:

View file

@ -58,3 +58,53 @@ These examples show how to:
- Perform dynamic client registration if needed.
- Acquire access tokens.
- Attach OAuth credentials to Streamable HTTP requests.
## stdio transport
Use `StdioClientTransport` to connect to a server that runs as a local child process:
```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const transport = new StdioClientTransport({
command: 'node',
args: ['server.js'],
env: { NODE_ENV: 'production' },
cwd: '/path/to/server'
});
const client = new Client({ name: 'my-client', version: '1.0.0' });
await client.connect(transport);
// connect() calls transport.start() automatically, spawning the child process
```
The transport communicates over the child process's stdin/stdout using JSON-RPC. The `stderr` option controls where the child's stderr goes (defaults to `'inherit'`).
## Roots
Roots let a client expose filesystem locations to the server, so the server knows which directories or files are relevant. Declare the `roots` capability and register a handler:
```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ListRootsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
const client = new Client({ name: 'my-client', version: '1.0.0' }, { capabilities: { roots: { listChanged: true } } });
client.setRequestHandler(ListRootsRequestSchema, async () => {
return {
roots: [
{ uri: 'file:///home/user/project', name: 'My Project' },
{ uri: 'file:///home/user/data', name: 'Data Directory' }
]
};
});
```
When the set of roots changes, notify the server so it can re-query:
```typescript
await client.sendRootsListChanged();
```
Root URIs must use the `file://` scheme. The `listChanged: true` capability flag is required to send change notifications.

200
docs/protocol.md Normal file
View 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`.

View file

@ -45,6 +45,23 @@ Examples:
- Stateless Streamable HTTP: [`simpleStatelessStreamableHttp.ts`](../src/examples/server/simpleStatelessStreamableHttp.ts)
- Stateful with resumability: [`simpleStreamableHttp.ts`](../src/examples/server/simpleStreamableHttp.ts)
### stdio
For local integrations where the client spawns the server as a child process, use `StdioServerTransport`. Communication happens over stdin/stdout using JSON-RPC:
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
// ... register tools, resources, prompts ...
const transport = new StdioServerTransport();
await server.connect(transport);
```
This is the simplest transport — no HTTP server setup required. The client uses `StdioClientTransport` to spawn and communicate with the server process (see [docs/client.md](client.md#stdio-transport)).
### Deprecated HTTP + SSE
The older HTTP+SSE transport (protocol version 2024‑11‑05) is supported only for backwards compatibility. New implementations should prefer Streamable HTTP.
@ -128,11 +145,91 @@ This snippet is illustrative only; for runnable servers that expose tools, see:
- [`simpleStreamableHttp.ts`](../src/examples/server/simpleStreamableHttp.ts)
- [`toolWithSampleServer.ts`](../src/examples/server/toolWithSampleServer.ts)
#### Image and audio results
Tools can return image and audio content alongside text. Use base64-encoded data with the appropriate MIME type:
```typescript
// e.g. const chartPngBase64 = fs.readFileSync('chart.png').toString('base64');
server.registerTool('generate-chart', { description: 'Generate a chart image' }, async () => ({
content: [
{
type: 'image',
data: chartPngBase64,
mimeType: 'image/png'
}
]
}));
// e.g. const audioBase64 = fs.readFileSync('speech.wav').toString('base64');
server.registerTool(
'text-to-speech',
{
description: 'Convert text to speech',
inputSchema: { text: z.string() }
},
async ({ text }) => ({
content: [
{
type: 'audio',
data: audioBase64,
mimeType: 'audio/wav'
}
]
})
);
```
#### Embedded resource results
Tools can return embedded resources, allowing the tool to attach full resource objects in its response:
```typescript
server.registerTool('fetch-data', { description: 'Fetch and return data as a resource' }, async () => ({
content: [
{
type: 'resource',
resource: {
uri: 'data://result',
mimeType: 'application/json',
text: JSON.stringify({ key: 'value' })
}
}
]
}));
```
#### Error handling
To indicate that a tool call failed, set `isError: true` in the result. The content describes what went wrong:
```typescript
server.registerTool('risky-operation', { description: 'An operation that might fail' }, async () => {
try {
const result = await doSomething();
return { content: [{ type: 'text', text: result }] };
} catch (err) {
return {
content: [{ type: 'text', text: `Error: ${err.message}` }],
isError: true
};
}
});
```
#### Tool change notifications
When tools are added, removed, or updated at runtime, the server automatically notifies connected clients. This happens when you call `registerTool()`, or use `remove()`, `enable()`, `disable()`, or `update()` on a `RegisteredTool`. You can also trigger it manually:
```typescript
server.sendToolListChanged();
```
#### ResourceLink outputs
Tools can return `resource_link` content items to reference large resources without embedding them directly, allowing clients to fetch only what they need.
The README’s `list-files` example shows the pattern conceptually; for concrete usage, see the Streamable HTTP examples in `src/examples/server`.
The README's `list-files` example shows the pattern conceptually; for concrete usage, see the Streamable HTTP examples in `src/examples/server`.
### Resources
@ -155,7 +252,70 @@ server.registerResource(
);
```
Dynamic resources use `ResourceTemplate` and can support completions on path parameters. For full runnable examples of resources:
#### Binary resources
Resources can return binary data using `blob` (base64-encoded) instead of `text`:
```typescript
server.registerResource('logo', 'images://logo.png', { title: 'Logo', mimeType: 'image/png' }, async uri => ({
contents: [{ uri: uri.href, blob: logoPngBase64 }]
}));
```
#### Resource templates
Dynamic resources use `ResourceTemplate` to match URI patterns. The template parameters are passed to the read callback:
```typescript
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
server.registerResource('user-profile', new ResourceTemplate('users://{userId}/profile', { list: undefined }), { title: 'User Profile', mimeType: 'application/json' }, async (uri, { userId }) => ({
contents: [
{
uri: uri.href,
text: JSON.stringify(await getUser(userId))
}
]
}));
```
#### Subscribing and unsubscribing
Clients can subscribe to resource changes. The server declares subscription support via the `resources.subscribe` capability, which `McpServer` enables automatically when resources are registered.
To handle subscriptions, register handlers on the low-level server for `SubscribeRequestSchema` and `UnsubscribeRequestSchema`:
```typescript
import { SubscribeRequestSchema, UnsubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';
const subscriptions = new Set<string>();
server.server.setRequestHandler(SubscribeRequestSchema, async request => {
subscriptions.add(request.params.uri);
return {};
});
server.server.setRequestHandler(UnsubscribeRequestSchema, async request => {
subscriptions.delete(request.params.uri);
return {};
});
```
When a subscribed resource changes, notify the client:
```typescript
if (subscriptions.has(resourceUri)) {
await server.server.sendResourceUpdated({ uri: resourceUri });
}
```
Resource list changes (adding/removing resources) are notified automatically when using `registerResource()`, `remove()`, `enable()`, or `disable()`. You can also trigger it manually:
```typescript
server.sendResourceListChanged();
```
For full runnable examples of resources:
- [`simpleStreamableHttp.ts`](../src/examples/server/simpleStreamableHttp.ts)
@ -187,15 +347,150 @@ server.registerPrompt(
);
```
#### Image content in prompts
Prompts can include image content in their messages:
```typescript
server.registerPrompt(
'analyze-image',
{
title: 'Analyze Image',
description: 'Analyze an image',
argsSchema: { imageBase64: z.string() }
},
({ imageBase64 }) => ({
messages: [
{
role: 'user',
content: {
type: 'image',
data: imageBase64,
mimeType: 'image/png'
}
}
]
})
);
```
#### Embedded resources in prompts
Prompts can embed resource content in their messages:
```typescript
server.registerPrompt(
'summarize-doc',
{
title: 'Summarize Document',
description: 'Summarize a document resource'
},
() => ({
messages: [
{
role: 'user',
content: {
type: 'resource',
resource: {
uri: 'docs://readme',
mimeType: 'text/plain',
text: 'Document content here...'
}
}
}
]
})
);
```
#### Prompt change notifications
Like tools, prompt list changes are notified automatically when using `registerPrompt()`, `remove()`, `enable()`, or `disable()`. You can also trigger it manually:
```typescript
server.sendPromptListChanged();
```
For prompts integrated into a full server, see:
- [`simpleStreamableHttp.ts`](../src/examples/server/simpleStreamableHttp.ts)
### Completions
Both prompts and resources can support argument completions. On the client side, you use `client.complete()` with a reference to the prompt or resource and the partially‑typed argument.
Both prompts and resources can support argument completions using the `completable` wrapper. This lets clients offer autocomplete suggestions as users type.
See the MCP spec sections on prompts and resources for complete details, and [`simpleStreamableHttp.ts`](../src/examples/client/simpleStreamableHttp.ts) for client‑side usage patterns.
```typescript
import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
server.registerPrompt(
'greet',
{
title: 'Greeting',
description: 'Generate a greeting',
argsSchema: {
name: completable(z.string(), value => {
// Return suggestions matching the partial input
const names = ['Alice', 'Bob', 'Charlie'];
return names.filter(n => n.toLowerCase().startsWith(value.toLowerCase()));
})
}
},
({ name }) => ({
messages: [{ role: 'user', content: { type: 'text', text: `Hello, ${name}!` } }]
})
);
```
Resource templates also support completions on their path parameters via `completable`. On the client side, use `client.complete()` with a reference to the prompt or resource and the partially-typed argument:
```typescript
const result = await client.complete({
ref: { type: 'ref/prompt', name: 'greet' },
argument: { name: 'name', value: 'Al' }
});
console.log(result.completion.values); // ['Alice']
```
### Logging
The server can send log messages to the client using `server.sendLoggingMessage()`. Clients can request a minimum log level via the `logging/setLevel` request, which `McpServer` handles automatically — messages below the requested level are suppressed.
```typescript
// Send a log message from a tool handler:
server.registerTool(
'process-data',
{
description: 'Process some data',
inputSchema: { data: z.string() }
},
async ({ data }, extra) => {
await server.sendLoggingMessage({ level: 'info', data: `Processing: ${data}` }, extra.sessionId);
// ... do work ...
return { content: [{ type: 'text', text: 'Done' }] };
}
);
```
For a full example, see [`simpleStreamableHttp.ts`](../src/examples/server/simpleStreamableHttp.ts) which uses `sendLoggingMessage` throughout.
Log levels in order: `debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`.
#### Log level filtering
Clients can request a minimum log level via `logging/setLevel`. The low-level `Server` handles this automatically when the `logging` capability is enabled — it stores the requested level per session and suppresses messages below it. You can also send log messages directly using
`sendLoggingMessage`:
```typescript
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { logging: {} } });
// Client requests: only show 'warning' and above
// (handled automatically by the Server)
// These will be sent or suppressed based on the client's requested level:
await server.sendLoggingMessage({ level: 'debug', data: 'verbose detail' }); // suppressed
await server.sendLoggingMessage({ level: 'warning', data: 'something is off' }); // sent
await server.sendLoggingMessage({ level: 'error', data: 'something broke' }); // sent
```
### Display names and metadata

View file

@ -72,6 +72,29 @@ const getServer = () => {
title: 'Newsletter',
description: 'Subscribe to newsletter?',
default: false
},
role: {
type: 'string',
title: 'Role',
description: 'Your primary role',
oneOf: [
{ const: 'developer', title: 'Developer' },
{ const: 'designer', title: 'Designer' },
{ const: 'manager', title: 'Manager' },
{ const: 'other', title: 'Other' }
],
default: 'developer'
},
interests: {
type: 'array',
title: 'Interests',
description: 'Select your areas of interest',
items: {
type: 'string',
enum: ['frontend', 'backend', 'mobile', 'devops', 'ai']
},
minItems: 1,
maxItems: 3
}
},
required: ['username', 'email', 'password']

View file

@ -0,0 +1,58 @@
/**
* Example: Progress notifications over stdio.
*
* Demonstrates a tool that reports progress to the client while processing.
*
* Run:
* npx tsx src/examples/server/progressExample.ts
*
* Then connect a client with an `onprogress` callback (see docs/protocol.md).
*/
import { McpServer } from '../../server/mcp.js';
import { StdioServerTransport } from '../../server/stdio.js';
import { z } from 'zod';
const server = new McpServer({ name: 'progress-example', version: '1.0.0' }, { capabilities: { logging: {} } });
server.registerTool(
'count',
{
description: 'Count to N with progress updates',
inputSchema: { n: z.number().int().min(1).max(100) }
},
async ({ n }, extra) => {
for (let i = 1; i <= n; i++) {
if (extra.signal.aborted) {
return { content: [{ type: 'text', text: `Cancelled at ${i}` }], isError: true };
}
if (extra._meta?.progressToken !== undefined) {
await extra.sendNotification({
method: 'notifications/progress',
params: {
progressToken: extra._meta.progressToken,
progress: i,
total: n,
message: `Counting: ${i}/${n}`
}
});
}
// Simulate work
await new Promise(resolve => setTimeout(resolve, 100));
}
return { content: [{ type: 'text', text: `Counted to ${n}` }] };
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(error => {
console.error('Server error:', error);
process.exit(1);
});