diff --git a/README.md b/README.md index 254671c8f..7a553ebbd 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/capabilities.md b/docs/capabilities.md index 301e850fe..d436a00cd 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -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: diff --git a/docs/client.md b/docs/client.md index d28765fd0..8f1c65327 100644 --- a/docs/client.md +++ b/docs/client.md @@ -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. diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 000000000..2773e5e8d --- /dev/null +++ b/docs/protocol.md @@ -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`. diff --git a/docs/server.md b/docs/server.md index fb0766d5b..7dbf64290 100644 --- a/docs/server.md +++ b/docs/server.md @@ -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(); + +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. + +```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 -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. +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 diff --git a/src/examples/server/elicitationFormExample.ts b/src/examples/server/elicitationFormExample.ts index d220806d3..0ea9c1934 100644 --- a/src/examples/server/elicitationFormExample.ts +++ b/src/examples/server/elicitationFormExample.ts @@ -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'] diff --git a/src/examples/server/progressExample.ts b/src/examples/server/progressExample.ts new file mode 100644 index 000000000..da50c84eb --- /dev/null +++ b/src/examples/server/progressExample.ts @@ -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); +});