docs: comprehensive feature documentation for SEP-1730 Tier 1 (#1548)
This commit is contained in:
parent
2084a22074
commit
342ea394ca
7 changed files with 742 additions and 5 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
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`.
|
||||
303
docs/server.md
303
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<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
|
||||
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
58
src/examples/server/progressExample.ts
Normal file
58
src/examples/server/progressExample.ts
Normal 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);
|
||||
});
|
||||
Loading…
Reference in a new issue