Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
112 changes: 111 additions & 1 deletion docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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:

Expand Down
50 changes: 50 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 200 additions & 0 deletions docs/protocol.md
Original file line number Diff line number Diff line change
@@ -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
Loading