docs: rebuild the v2 documentation around a task-based page tree (#2397)

This commit is contained in:
Felix Weinberger 2026-06-30 21:18:42 +01:00 • committed by GitHub
commit b153505428
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 11884 additions and 4971 deletions

168
docs/servers/completion.md Normal file
View file

@ -0,0 +1,168 @@
---
shape: how-to
---
# Completion
**Completion** is server-side autocomplete for prompt arguments and resource template variables: the client sends the partial value the user has typed so far, your callback returns the matching suggestions.
## Wrap an argument with `completable`
`completable` wraps one field of a [prompt's](./prompts.md) `argsSchema` — the schema validates exactly as before, and the second argument suggests values for the field.
```ts source="../../examples/guides/servers/completion.examples.ts#completable_language"
import { completable, McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const languages = ['typescript', 'javascript', 'python', 'rust', 'go'];
const server = new McpServer({ name: 'review', version: '1.0.0' });
server.registerPrompt(
'review-code',
{
description: 'Review code for best practices',
argsSchema: z.object({
language: completable(z.string().describe('Programming language'), value =>
languages.filter(language => language.startsWith(value))
)
})
},
({ language }) => ({
messages: [
{
role: 'user' as const,
content: { type: 'text' as const, text: `Review this ${language} code for best practices.` }
}
]
})
);
```
The first `completable` field also registers the server's `completion/complete` handler and advertises the **completions** capability — nothing to declare. A request for completions of `language` with the partial value `ty` now returns `typescript`.
Every result quoted on this page comes from `client.complete()` on an in-memory `Client` connected to this server — [Try it from a client](#try-it-from-a-client) shows the call, and [Test a server](../testing.md) shows the wiring.
## Return suggestions from the complete callback
The callback receives the value typed so far and returns every match — `string[]`, or a promise of one when the lookup is async. Register a second prompt whose `repo` argument completes from an async list.
```ts source="../../examples/guides/servers/completion.examples.ts#registerPrompt_async"
const branchesByRepo: Record<string, string[]> = {
'typescript-sdk': ['main', 'release/1.x', 'release/2.x'],
'python-sdk': ['main', 'release/1.x'],
inspector: ['main']
};
async function listRepos(): Promise<string[]> {
return Object.keys(branchesByRepo);
}
server.registerPrompt(
'review-pr',
{
description: 'Review the open pull requests on one branch',
argsSchema: z.object({
repo: completable(z.string().describe('Repository name'), async value => {
const repos = await listRepos();
return repos.filter(repo => repo.startsWith(value));
}),
branch: completable(z.string().describe('Target branch'), completeBranch)
})
},
({ repo, branch }) => ({
messages: [
{
role: 'user' as const,
content: { type: 'text' as const, text: `Review the open pull requests on ${repo}@${branch}.` }
}
]
})
);
```
Return the full match list; the SDK truncates `values` to 100 entries and fills in `total` and `hasMore`. Completing `repo` with the value `ty` returns:
```
{ values: [ 'typescript-sdk' ], total: 1, hasMore: false }
```
`branch` points at `completeBranch`, defined in the next section.
## Use the other arguments for context
The callback's optional second parameter carries `arguments`: the values the client has already filled in for the prompt's other arguments. Use it to make one field's suggestions depend on another's.
```ts source="../../examples/guides/servers/completion.examples.ts#completeCallback_context"
async function completeBranch(value: string, context?: { arguments?: Record<string, string> }): Promise<string[]> {
const repo = context?.arguments?.repo;
if (!repo) return [];
return (branchesByRepo[repo] ?? []).filter(branch => branch.startsWith(value));
}
```
With `repo: 'typescript-sdk'` already filled in, completing `branch` with the value `rel` returns:
```
{ values: [ 'release/1.x', 'release/2.x' ], total: 2, hasMore: false }
```
Clients are not required to send `context` — return an empty list when it is missing, never throw.
## Complete a resource template variable
[Resource template](./resources.md) variables complete through the template's `complete` map — one callback per URI variable, with the same `(value, context?)` signature. `completable` is for prompt arguments only.
```ts source="../../examples/guides/servers/completion.examples.ts#resourceTemplate_complete"
server.registerResource(
'readme',
new ResourceTemplate('repo://{repo}/readme', {
list: undefined,
complete: {
repo: async value => {
const repos = await listRepos();
return repos.filter(repo => repo.startsWith(value));
}
}
}),
{ description: 'The README of one repository' },
async (uri, { repo }) => ({
contents: [{ uri: uri.href, text: `Repository: ${repo}` }]
})
);
```
The completion request targets the template by its URI pattern: `ref: { type: 'ref/resource', uri: 'repo://{repo}/readme' }` with `argument: { name: 'repo', value: 'py' }` returns `python-sdk`.
## Try it from a client
`Client.complete()` sends `completion/complete`. Complete `language` on `review-code` with an empty value to get the whole list.
```ts source="../../examples/guides/servers/completion.examples.ts#complete_client"
const result = await client.complete({
ref: { type: 'ref/prompt', name: 'review-code' },
argument: { name: 'language', value: '' }
});
console.log(result.completion);
```
The server's callback ran once and produced every value:
```
{
values: [ 'typescript', 'javascript', 'python', 'rust', 'go' ],
total: 5,
hasMore: false
}
```
::: tip
A host issues the same request as the end user types: MCP Inspector requests completions while you fill in a prompt's arguments and shows the returned `values` as suggestions.
:::
## Recap
- `completable(schema, callback)` attaches autocompletion to one prompt argument; the schema validates exactly as before.
- The callback receives the partial value and returns the full match list, synchronously or as a promise; the SDK caps `values` at 100 and sets `total` and `hasMore`.
- `context.arguments` carries the prompt's already-filled arguments, so one field's suggestions can depend on another's.
- Resource template variables complete through the template's `complete` map, not `completable`.
- The first completable registration advertises the `completions` capability; `Client.complete()` sends the request.

185
docs/servers/elicitation.md Normal file
View file

@ -0,0 +1,185 @@
---
shape: how-to
---
# Elicitation
A tool handler asks the end user a question mid-call with `ctx.mcpReq.elicitInput` — the connected client puts the question in front of them and the promise resolves with their answer.
## Ask for input with a form
**Form mode** carries a `message` and a `requestedSchema`: a flat JSON Schema of primitive fields the client renders as a form.
```ts source="../../examples/guides/servers/elicitation.examples.ts#registerTool_elicitForm"
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const server = new McpServer({ name: 'feedback', version: '1.0.0' });
server.registerTool(
'collect-feedback',
{
description: 'Ask the user how something went',
inputSchema: z.object({ topic: z.string() })
},
async ({ topic }, ctx) => {
const result = await ctx.mcpReq.elicitInput({
mode: 'form',
message: `How was ${topic}?`,
requestedSchema: {
type: 'object',
properties: {
rating: { type: 'number', title: 'Rating (1-5)', minimum: 1, maximum: 5 },
comment: { type: 'string', title: 'Comment' }
},
required: ['rating']
}
});
if (result.action !== 'accept') {
return { content: [{ type: 'text', text: `Feedback ${result.action}.` }] };
}
return { content: [{ type: 'text', text: `Recorded: ${JSON.stringify(result.content)}` }] };
}
);
```
`result.action` records what the end user did — `accept`, `decline`, or `cancel` — and `result.content` carries the submitted fields on accept only. The SDK validates accepted content against `requestedSchema` before `elicitInput` resolves, so the fields you read match the schema you sent.
::: info
On a 2026-07-28 connection `elicitInput` throws — a handler returns the request instead; see [Input required](./input-required.md) and [Protocol versions](../protocol-versions.md).
:::
The answer comes from the connected client's `elicitation/create` handler. Every call on this page uses an in-memory client whose handler stands in for a real host's UI — [Handle requests from the server](../clients/server-requests.md) covers the client side in full.
```ts source="../../examples/guides/servers/elicitation.examples.ts#Client_elicitationHandler"
const client = new Client({ name: 'feedback-host', version: '1.0.0' }, { capabilities: { elicitation: { form: {}, url: {} } } });
client.setRequestHandler('elicitation/create', async request => {
if (request.params.mode === 'url') {
// Open request.params.url in the user's browser; answer when they finish.
return { action: 'accept' };
}
// Render request.params.requestedSchema as a form; return what the user typed.
return { action: 'accept', content: { rating: 5, comment: 'Smooth setup' } };
});
```
Call `collect-feedback` and the elicitation round-trips through that handler inside the one tool call.
```ts source="../../examples/guides/servers/elicitation.examples.ts#callTool_collectFeedback"
const result = await client.callTool({ name: 'collect-feedback', arguments: { topic: 'the new editor' } });
console.log(result.content);
```
The handler resumes with the submitted fields and returns:
```
[
{
type: 'text',
text: 'Recorded: {"rating":5,"comment":"Smooth setup"}'
}
]
```
## Handle every action
Return a distinct result for each `action` so the model knows whether the end user confirmed, refused, or never answered.
```ts source="../../examples/guides/servers/elicitation.examples.ts#registerTool_elicitActions"
server.registerTool(
'delete-dataset',
{
description: 'Delete a dataset after the user confirms',
inputSchema: z.object({ name: z.string() })
},
async ({ name }, ctx) => {
const result = await ctx.mcpReq.elicitInput({
mode: 'form',
message: `Delete ${name}? This cannot be undone.`,
requestedSchema: {
type: 'object',
properties: { confirm: { type: 'boolean', title: 'Yes, delete it' } },
required: ['confirm']
}
});
switch (result.action) {
case 'accept':
if (result.content?.confirm !== true) {
return { content: [{ type: 'text', text: 'Box left unchecked - nothing deleted.' }] };
}
return { content: [{ type: 'text', text: `Deleted ${name}.` }] };
case 'decline':
return { content: [{ type: 'text', text: 'Declined - nothing deleted.' }] };
case 'cancel':
return { content: [{ type: 'text', text: 'Dismissed - ask again later.' }] };
}
}
);
```
`result.content` is end-user input: schema-valid, still untrusted — the `accept` branch checks that the box was actually ticked before acting. Decline the form and the tool answers from the `decline` branch:
```
[ { type: 'text', text: 'Declined - nothing deleted.' } ]
```
## Send the end user to a URL
**URL mode** replaces the form with a browser flow: pass `url` and a unique `elicitationId` instead of `requestedSchema`.
```ts source="../../examples/guides/servers/elicitation.examples.ts#registerTool_elicitUrl"
server.registerTool(
'link-account',
{
description: 'Link a billing account through a hosted sign-in flow',
inputSchema: z.object({ provider: z.string() })
},
async ({ provider }, ctx) => {
const result = await ctx.mcpReq.elicitInput({
mode: 'url',
message: `Sign in to ${provider} to link your account`,
url: `https://billing.example.com/connect/${encodeURIComponent(provider)}`,
elicitationId: crypto.randomUUID()
});
if (result.action !== 'accept') {
return { content: [{ type: 'text', text: `Sign-in ${result.action}.` }] };
}
return { content: [{ type: 'text', text: `Linked ${provider}.` }] };
}
);
```
The client opens the URL and answers once the end user finishes there; whatever the page collects — credentials, payment details, API keys — stays in the browser and never crosses the MCP connection. The handler's `url` branch above accepts, so `link-account` returns:
```
[ { type: 'text', text: 'Linked github.' } ]
```
## Keep secrets out of forms
Form answers travel back through the client and land in the model's context like any other tool result.
::: warning
Never collect sensitive information — passwords, API keys, payment details — through form elicitation. Use URL mode or an out-of-band flow instead.
:::
## Require the elicitation capability
Elicitation only works against a client that declared the `elicitation` capability — per mode: `form`, `url` — when it connected. Against a client without it, `elicitInput` throws before anything reaches the wire, and the thrown message comes back as an ordinary `isError` tool result:
```
{
content: [
{ type: 'text', text: 'Client does not support form elicitation.' }
],
isError: true
}
```
## Recap
- `ctx.mcpReq.elicitInput` sends an `elicitation/create` request mid-handler and resolves with the end user's answer.
- Form mode carries a `message` and a flat JSON-Schema `requestedSchema`; the SDK validates accepted content against it.
- `result.action` is `accept`, `decline`, or `cancel`; `result.content` is present only on accept.
- URL mode hands the end user a browser flow — use it for anything sensitive.
- Calls against a client that never declared the `elicitation` capability fail before reaching the wire.

209
docs/servers/errors.md Normal file
View file

@ -0,0 +1,209 @@
---
shape: how-to
---
# Errors
A **tool error** is a successful JSON-RPC result with `isError: true` that the model reads and recovers from. A **protocol error** is a JSON-RPC error response the model never sees.
## Return a tool error with `isError`
Return `isError: true` from a tool handler to report a failure the model should see.
```ts source="../../examples/guides/servers/errors.examples.ts#registerTool_isError"
import { McpServer, ProtocolError, ProtocolErrorCode, ResourceNotFoundError, ResourceTemplate } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const notes = new Map([['welcome', 'Read tools.md first.']]);
const server = new McpServer({ name: 'notes', version: '1.0.0' });
server.registerTool(
'read-note',
{
description: 'Read a note by its id',
inputSchema: z.object({ id: z.string() })
},
async ({ id }) => {
const note = notes.get(id);
if (!note) {
return {
content: [{ type: 'text', text: `No note with id "${id}". Known ids: ${[...notes.keys()].join(', ')}` }],
isError: true
};
}
return { content: [{ type: 'text', text: note }] };
}
);
```
Every call on this page comes from an in-memory `Client` connected to the server above — [Test a server](../testing.md) shows that wiring. Call `read-note` with an id that does not exist.
```ts source="../../examples/guides/servers/errors.examples.ts#callTool_isError"
const missing = await client.callTool({ name: 'read-note', arguments: { id: 'drafts' } });
console.log(missing);
```
The `tools/call` response is an ordinary result:
```
{
content: [
{
type: 'text',
text: 'No note with id "drafts". Known ids: welcome'
}
],
isError: true
}
```
The model reads the message, sees `welcome` in it, and retries with an id that exists. Put the recovery hint in `text` — it is the only thing the model has to work with.
## Let a thrown exception become a tool error
Throw instead: the SDK catches anything a tool handler throws and converts it to the same `isError: true` shape.
```ts source="../../examples/guides/servers/errors.examples.ts#registerTool_throw"
server.registerTool(
'delete-note',
{
description: 'Delete a note by its id',
inputSchema: z.object({ id: z.string() })
},
async ({ id }) => {
if (!notes.delete(id)) {
throw new Error(`Cannot delete "${id}": no such note`);
}
return { content: [{ type: 'text', text: `Deleted "${id}"` }] };
}
);
```
Call `delete-note` with the same missing id.
```ts source="../../examples/guides/servers/errors.examples.ts#callTool_throw"
const thrown = await client.callTool({ name: 'delete-note', arguments: { id: 'drafts' } });
console.log(thrown);
```
The exception's `message` becomes the result's `content` text:
```
{
content: [ { type: 'text', text: 'Cannot delete "drafts": no such note' } ],
isError: true
}
```
A throw and an explicit `isError: true` produce the same shape; returning explicitly gives you control over `content`. The SDK skips `outputSchema` validation on any `isError` result.
## Throw a protocol error
Resource, prompt, and completion callbacks have no `isError` channel. Throw `ProtocolError(code, message, data?)` when the request itself is wrong.
```ts source="../../examples/guides/servers/errors.examples.ts#registerResource_protocolError"
server.registerResource(
'note',
new ResourceTemplate('note://{id}', { list: undefined }),
{ description: 'A note by its id' },
async (uri, { id }) => {
const noteId = String(id);
if (!/^[a-z]+$/.test(noteId)) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Note ids are lowercase letters, got "${noteId}"`);
}
const note = notes.get(noteId);
if (!note) throw new ResourceNotFoundError(uri.href);
return { contents: [{ uri: uri.href, text: note }] };
}
);
```
::: info Coming from v1?
`ProtocolError` and `ProtocolErrorCode` replace v1's `McpError` and `ErrorCode` — run the codemod, then see the [upgrade guide](../migration/upgrade-to-v2.md).
:::
Read the resource with an id the callback rejects.
```ts source="../../examples/guides/servers/errors.examples.ts#readResource_protocolError"
try {
await client.readResource({ uri: 'note://42' });
} catch (error) {
const { code, message } = error as ProtocolError;
console.log({ code, message });
}
```
`readResource` rejects with a `ProtocolError` carrying the wire fields:
```
{ code: -32602, message: 'Note ids are lowercase letters, got "42"' }
```
On the wire this is a JSON-RPC error response — `{ code, message, data? }` instead of a `result` — and the host's MCP client handles it; the model never sees it. A non-`ProtocolError` exception thrown from one of these callbacks surfaces as `-32603` Internal Error with the exception's message.
## Choose between tool error and protocol error
Pick by audience. The model drives `tools/call`, so a failure it can recover from — a missing record, a bad argument, a transient upstream fault — belongs in `isError: true` with a message that names the fix. The host application drives `resources/read`, `prompts/get`, and `completion/complete`, so failures there are protocol errors addressed to the caller's code.
The handler decides which channel exists:
- A tool handler produces only tool errors. The SDK converts every exception it throws — including a thrown `ProtocolError` — into an `isError: true` result. `UrlElicitationRequiredError` is the one exception; it propagates as a JSON-RPC error so the host can open the URL — see [Elicitation](./elicitation.md).
- A resource, prompt, or completion callback produces only protocol errors. Throw a `ProtocolError`.
## Use the typed error subclasses
Each subclass picks the right `ProtocolErrorCode` and packs structured `data` for you. `ResourceNotFoundError` takes the missing URI — the read callback above already throws it for a well-formed id with no note.
```ts source="../../examples/guides/servers/errors.examples.ts#readResource_notFound"
try {
await client.readResource({ uri: 'note://archived' });
} catch (error) {
const { code, message, data } = error as ResourceNotFoundError;
console.log({ code, message, data });
}
```
The error carries the requested URI in `data` and the code the spec mandates for a `resources/read` miss:
```
{
code: -32602,
message: 'Resource not found: note://archived',
data: { uri: 'note://archived' }
}
```
Three more subclasses cover the other structured protocol errors:
- `UrlElicitationRequiredError(elicitations)` — `-32042`; the only error a tool handler can propagate. See [Elicitation](./elicitation.md).
- `UnsupportedProtocolVersionError({ supported, requested })` — `-32022`; `data.supported` lets the peer pick a version and retry.
- `MissingRequiredClientCapabilityError({ requiredCapabilities })` — `-32021`; `data.requiredCapabilities` names exactly what the client must declare.
Match these by `code` and `data` shape, not by `instanceof` — `instanceof` fails across separately bundled copies of the SDK.
## Look up a protocol error code
`ProtocolErrorCode` is the complete vocabulary of wire codes the SDK sends and recognizes.
| Member | Code | Meaning |
| --- | --- | --- |
| `ParseError` | `-32700` | The message was not valid JSON. |
| `InvalidRequest` | `-32600` | The message was not a valid JSON-RPC request. |
| `MethodNotFound` | `-32601` | No handler is registered for the method. |
| `InvalidParams` | `-32602` | The params are wrong — also the code for a `resources/read` miss. |
| `InternalError` | `-32603` | The handler threw something other than a `ProtocolError`. |
| `ResourceNotFound` | `-32002` | Receive-tolerated only: the SDK answers a `resources/read` miss with `-32602` and never emits `-32002`. Throw `ResourceNotFoundError` instead. |
| `MissingRequiredClientCapability` | `-32021` | The request needs a capability the client did not declare. |
| `UnsupportedProtocolVersion` | `-32022` | The requested protocol version is unknown to the receiver or unsupported by it. |
| `UrlElicitationRequired` | `-32042` | The tool needs the user to visit a URL before it can complete. |
`-32021` and `-32022` are new in protocol revision 2026-07-28 — see [Protocol versions](../protocol-versions.md).
## Recap
- `isError: true` is a successful JSON-RPC result carrying a tool failure the model reads and acts on.
- A tool handler that throws produces the same `isError: true` result; the exception's `message` becomes the `content` text.
- A tool handler cannot produce a protocol error — only `UrlElicitationRequiredError` escapes.
- `ProtocolError` and its subclasses, thrown from resource, prompt, and completion callbacks, become JSON-RPC error responses the model never sees.
- `ResourceNotFoundError` and the other subclasses pick the code and pack structured `data`; match them by `code` and `data`, not `instanceof`.
- The table above lists every `ProtocolErrorCode` member.

View file

@ -0,0 +1,251 @@
---
shape: how-to
---
# input_required
An **`input_required`** result is how a `tools/call`, `prompts/get`, or `resources/read` handler asks the connected client for input mid-call: the handler returns the embedded requests, the client answers them and retries the call, and the handler runs again with the responses.
## Return `input_required` instead of pushing a request
The handler reads what already arrived with `acceptedContent`; while the answer is missing it returns `inputRequired(...)` instead of a tool result.
```ts source="../../examples/guides/servers/input-required.examples.ts#registerTool_inputRequired"
server.registerTool(
'deploy',
{
description: 'Deploy after the operator confirms',
inputSchema: z.object({ env: z.string() })
},
async ({ env }, ctx): Promise<CallToolResult | InputRequiredResult> => {
const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm');
if (confirmed?.confirm !== true) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: `Deploy to ${env}?`,
requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] }
})
}
});
}
return { content: [{ type: 'text', text: `Deployed to ${env}` }] };
}
);
```
The first round returns `resultType: 'input_required'` carrying the `confirm` request. The client fulfils it and retries `deploy` with the answer in `inputResponses`; on re-entry `acceptedContent` finds it and the handler finishes.
Every call on this page comes from an in-memory `Client` with an `elicitation/create` handler — [Test a server](../testing.md) shows that wiring. Calling `deploy` once produces both rounds:
```
[client] elicitation/create → Deploy to prod?
{ content: [ { type: 'text', text: 'Deployed to prod' } ] }
```
`inputRequired(spec)` throws a `TypeError` unless `spec` carries at least one of `inputRequests` or `requestState`. Each embedded request is checked against the capabilities the client declared; a missing capability rejects the call with `-32021` before anything reaches the wire.
::: info Coming from v1?
`ctx.mcpReq.elicitInput` and `ctx.mcpReq.requestSampling` are the 2025-era push channels — they throw on a 2026-07-28 request. See [Elicitation](./elicitation.md) and the [upgrade guide](../migration/upgrade-to-v2.md).
:::
## Read the responses on re-entry
`ctx.mcpReq.inputResponses` comes from the client — treat it as untrusted. Pass a Zod schema as `acceptedContent`'s third argument and the value reaches your handler already validated and typed.
```ts source="../../examples/guides/servers/input-required.examples.ts#acceptedContent_schema"
server.registerTool(
'tag-release',
{
description: 'Tag a release after the operator confirms',
inputSchema: z.object({ tag: z.string() })
},
async ({ tag }, ctx): Promise<CallToolResult | InputRequiredResult> => {
const view = inputResponse(ctx.mcpReq.inputResponses, 'confirm');
if (view.kind === 'elicit' && view.action !== 'accept') {
return { content: [{ type: 'text', text: 'Tagging cancelled by the operator' }], isError: true };
}
const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', z.object({ confirm: z.boolean() }));
if (confirmed?.confirm !== true) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: `Tag ${tag}?`,
requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] }
})
}
});
}
return { content: [{ type: 'text', text: `Tagged ${tag}` }] };
}
);
```
`acceptedContent` returns `undefined` for a missing, declined, or cancelled answer alike — re-issuing the request is the right move for all three only when the request is idempotent. `inputResponse` returns a discriminated view (`missing` / `elicit` / `sampling` / `roots`) when you need to tell a refusal from a first entry. A client that declines:
```
[client] elicitation/create → Tag v2.1.0?
{
content: [ { type: 'text', text: 'Tagging cancelled by the operator' } ],
isError: true
}
```
## Write the handler write-once
Write one handler that runs on every round: read each answer first, then request only the keys still missing. `inputRequests` is a map, so one round carries every outstanding request.
```ts source="../../examples/guides/servers/input-required.examples.ts#registerTool_writeOnce"
server.registerTool(
'provision',
{ description: 'Provision a database', inputSchema: z.object({}) },
async (_args, ctx): Promise<CallToolResult | InputRequiredResult> => {
const name = acceptedContent(ctx.mcpReq.inputResponses, 'name', z.object({ name: z.string() }));
const region = acceptedContent(ctx.mcpReq.inputResponses, 'region', z.object({ region: z.string() }));
if (name === undefined || region === undefined) {
return inputRequired({
inputRequests: {
...(name === undefined && {
name: inputRequired.elicit({
message: 'Database name?',
requestedSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }
})
}),
...(region === undefined && {
region: inputRequired.elicit({
message: 'Which region?',
requestedSchema: { type: 'object', properties: { region: { type: 'string' } }, required: ['region'] }
})
})
}
});
}
return { content: [{ type: 'text', text: `Provisioned ${name.name} in ${region.region}` }] };
}
);
```
Round one finds neither key, so both requests go out together; round two finds both and the handler returns.
```
[client] elicitation/create → Database name?
[client] elicitation/create → Which region?
{
content: [ { type: 'text', text: 'Provisioned analytics in eu-west-1' } ]
}
```
`inputResponses` holds only the latest round's answers, and nothing else on the server survives between rounds. A flow whose rounds must run in **sequence** carries what it has learned in `requestState`, below.
## Pick the embedded request kind
Each value in `inputRequests` is one embedded request, named by the builder that constructs it: `inputRequired.elicit` (form), `inputRequired.elicitUrl` (out-of-band URL), `inputRequired.createMessage` (sampling), and `inputRequired.listRoots()`.
```ts source="../../examples/guides/servers/input-required.examples.ts#inputRequired_kinds"
const next = inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: 'Continue?',
requestedSchema: { type: 'object', properties: { ok: { type: 'boolean' } }, required: ['ok'] }
}),
signin: inputRequired.elicitUrl({ message: 'Sign in to continue', url: 'https://example.com/auth' }),
summary: inputRequired.createMessage({
messages: [{ role: 'user', content: { type: 'text', text: 'Summarize the diff' } }],
maxTokens: 200
}),
roots: inputRequired.listRoots()
}
});
```
`acceptedContent` only reads accepted form elicitations; read the sampling and roots responses through `inputResponse`, which discriminates all four kinds. [Elicitation](./elicitation.md) covers `requestedSchema` and URL mode in full.
::: warning
Sampling and roots are deprecated as of protocol revision 2026-07-28 (SEP-2577) — see [Sampling](./sampling.md). Reach for the elicitation builders first.
:::
## Carry state across rounds with `requestState`
To run rounds in sequence, return an opaque `requestState` string alongside the requests. The client echoes it back byte-for-byte on the retry, and `ctx.mcpReq.requestState<State>()` reads its decoded payload on re-entry. Mint it with the codec from the next section.
```ts source="../../examples/guides/servers/input-required.examples.ts#requestState_mint"
server.registerTool(
'wipe-cache',
{ description: 'Confirm, then pick a scope, then wipe', inputSchema: z.object({}) },
async (_args, ctx): Promise<CallToolResult | InputRequiredResult> => {
const state = ctx.mcpReq.requestState<{ step: string }>();
if (state?.step !== 'confirmed') {
const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm');
if (confirmed?.confirm !== true) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: 'Really wipe the cache?',
requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] }
})
}
});
}
// Mint only what the response above already proved: the operator confirmed.
return inputRequired({
inputRequests: {
scope: inputRequired.elicit({
message: 'Which scope?',
requestedSchema: { type: 'object', properties: { scope: { type: 'string' } }, required: ['scope'] }
})
},
requestState: await stateCodec.mint({ step: 'confirmed' })
});
}
const scope = acceptedContent<{ scope: string }>(ctx.mcpReq.inputResponses, 'scope');
return { content: [{ type: 'text', text: `Wiped ${scope?.scope ?? 'all'}` }] };
}
);
```
Mint only what earlier rounds already proved. The token is bearer proof of whatever it claims: state minted as `{ step: 'confirmed' }` before the confirmation arrives grants that step to anyone who echoes it. One call drives all three entries:
```
[client] elicitation/create → Really wipe the cache?
[client] elicitation/create → Which scope?
{ content: [ { type: 'text', text: 'Wiped sessions' } ] }
```
## Protect `requestState` with the codec
`requestState` round-trips through the client and comes back as attacker-controlled input; the SDK applies no protection of its own. `createRequestStateCodec` returns an HMAC-SHA256 `{ mint, verify }` pair — pass `verify` as `ServerOptions.requestState.verify` and it runs before every handler entry that carries state.
```ts source="../../examples/guides/servers/input-required.examples.ts#requestState_codec"
const stateCodec = createRequestStateCodec<{ step: string }>({
key: crypto.getRandomValues(new Uint8Array(32)), // >= 32 bytes; share it across instances in a fleet
ttlSeconds: 600
});
const server = new McpServer({ name: 'releases', version: '1.0.0' }, { requestState: { verify: stateCodec.verify } });
```
With the hook in place, the accessor hands the handler `verify`'s decoded payload, and tampered or expired state never reaches the handler at all. Retrying `wipe-cache` with `requestState: 'tampered'` answers a wire-level protocol error:
```
-32602 Invalid or expired requestState
```
::: warning
The codec is signed, not encrypted — the client can base64url-decode the payload. Keep secrets out of it.
:::
## Let the shim serve older clients
The handlers above already serve every connection. On a connection that predates 2026-07-28, the SDK's legacy shim — on by default — fulfils an `input_required` return by pushing real `elicitation/create`, `sampling/createMessage`, and `roots/list` requests over the session, then re-enters the handler with the collected responses and the byte-exact `requestState` echo. Every result quoted on this page came from such a connection.
Set `ServerOptions.inputRequired.legacyShim: false` to fail loudly instead. Which revision a connection negotiates is covered in [Protocol versions](../protocol-versions.md).
## Recap
- A handler asks for input by returning `inputRequired(...)`; the client answers the embedded requests and retries the call.
- `inputRequired(spec)` needs at least one of `inputRequests` or `requestState`, and throws a `TypeError` without one.
- `acceptedContent(ctx.mcpReq.inputResponses, key, schema)` validates the untrusted client answer before it reaches your code; `inputResponse` discriminates declines and the non-elicitation kinds.
- A write-once handler re-derives its position on every entry and requests only what is still missing.
- `requestState` is the only cross-round memory; protect it with `createRequestStateCodec` and mint only what earlier rounds proved.
- The legacy shim serves the same handlers to pre-2026-07-28 clients.

View file

@ -0,0 +1,205 @@
---
shape: how-to
---
# Logging, progress, and cancellation
Every handler receives a **context** as its second argument; the request-scoped helpers — progress, logging, and the cancellation signal — live on `ctx.mcpReq`.
## Report progress from a handler
A client that wants progress puts a `progressToken` in the request's `_meta`. Read it from `ctx.mcpReq._meta` and send each update with `ctx.mcpReq.notify`.
```ts source="../../examples/guides/servers/logging-progress-cancellation.examples.ts#registerTool_progress"
server.registerTool(
'process-files',
{
description: 'Process files with progress updates',
inputSchema: z.object({ files: z.array(z.string()) })
},
async ({ files }, ctx) => {
const progressToken = ctx.mcpReq._meta?.progressToken;
for (let i = 0; i < files.length; i++) {
// ... process files[i] ...
if (progressToken !== undefined) {
await ctx.mcpReq.notify({
method: 'notifications/progress',
params: { progressToken, progress: i + 1, total: files.length, message: `Processed ${files[i]}` }
});
}
}
return { content: [{ type: 'text', text: `Processed ${files.length} files` }] };
}
);
```
Every call on this page comes from an in-memory `Client` connected to this server — [Test a server](../testing.md) shows that wiring. Pass an `onprogress` callback and the SDK puts the `progressToken` in `_meta` for you.
```ts source="../../examples/guides/servers/logging-progress-cancellation.examples.ts#callTool_onprogress"
const result = await client.callTool(
{ name: 'process-files', arguments: { files: ['a.csv', 'b.csv', 'c.csv'] } },
{ onprogress: update => console.log(update) }
);
console.log(result.content);
```
The callback fires once per file, then the call resolves:
```
{ progress: 1, total: 3, message: 'Processed a.csv' }
{ progress: 2, total: 3, message: 'Processed b.csv' }
{ progress: 3, total: 3, message: 'Processed c.csv' }
[ { type: 'text', text: 'Processed 3 files' } ]
```
## Skip progress when the client did not ask
Drop `onprogress` and the same request arrives with no `progressToken`, so the guard in the handler sends nothing.
```ts source="../../examples/guides/servers/logging-progress-cancellation.examples.ts#callTool_noProgress"
const quiet = await client.callTool({ name: 'process-files', arguments: { files: ['d.csv', 'e.csv'] } });
console.log(quiet.content);
```
Only the result comes back:
```
[ { type: 'text', text: 'Processed 2 files' } ]
```
`progress` must increase on every notification for the same token; `total` and `message` are optional.
## Log to the client
::: warning Deprecated — SEP-2577
Log to `stderr` (stdio servers) or use OpenTelemetry instead. **MCP logging** is deprecated as of protocol version 2026-07-28 (SEP-2577) and stays functional through the deprecation window (at least twelve months) — see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated).
:::
Declare the `logging` capability when you construct the server.
```ts source="../../examples/guides/servers/logging-progress-cancellation.examples.ts#logging_capability"
const server = new McpServer({ name: 'file-processor', version: '1.0.0' }, { capabilities: { logging: {} } });
```
`ctx.mcpReq.log(level, data)` then sends a `notifications/message` from inside any handler — `data` is any JSON value.
```ts source="../../examples/guides/servers/logging-progress-cancellation.examples.ts#registerTool_logging"
server.registerTool(
'validate-records',
{
description: 'Validate records before import',
inputSchema: z.object({ records: z.array(z.string()) })
},
async ({ records }, ctx) => {
await ctx.mcpReq.log('info', `Validating ${records.length} records`);
const invalid = records.filter(record => !record.endsWith('.csv'));
if (invalid.length > 0) {
await ctx.mcpReq.log('warning', { invalid });
}
return { content: [{ type: 'text', text: `${records.length - invalid.length} of ${records.length} records are valid` }] };
}
);
```
The connected client surfaces each one through its `notifications/message` handler.
```ts source="../../examples/guides/servers/logging-progress-cancellation.examples.ts#setNotificationHandler_message"
client.setNotificationHandler('notifications/message', notification => {
console.log(notification.params.level, notification.params.data);
});
```
Calling `validate-records` with one bad record delivers both log notifications before the result:
```
info Validating 2 records
warning { invalid: [ 'b.txt' ] }
[ { type: 'text', text: '1 of 2 records are valid' } ]
```
How the client's log level reaches `ctx.mcpReq.log` differs by protocol era — see [Protocol versions](../protocol-versions.md).
## Stop work when the request is cancelled
`ctx.mcpReq.signal` is an `AbortSignal`. The SDK aborts it when the client sends `notifications/cancelled` for the request, and when the connection closes — check it before each unit of work.
```ts source="../../examples/guides/servers/logging-progress-cancellation.examples.ts#registerTool_abort"
server.registerTool(
'scan-archive',
{
description: 'Scan every page of the archive',
inputSchema: z.object({ pages: z.number().int() })
},
async ({ pages }, ctx) => {
let scanned = 0;
for (let page = 0; page < pages; page++) {
if (ctx.mcpReq.signal.aborted) {
console.error(`Stopped after ${scanned} of ${pages} pages: ${ctx.mcpReq.signal.reason}`);
break;
}
await new Promise(resolve => setTimeout(resolve, 100)); // ... scan one page ...
scanned++;
}
return { content: [{ type: 'text', text: `Scanned ${scanned} pages` }] };
}
);
```
Cancel from the client by aborting the signal you pass to the call.
```ts source="../../examples/guides/servers/logging-progress-cancellation.examples.ts#callTool_abort"
const controller = new AbortController();
const scan = client.callTool({ name: 'scan-archive', arguments: { pages: 40 } }, { signal: controller.signal });
// the end user clicks Stop while the scan runs
setTimeout(() => controller.abort('the end user clicked Stop'), 5);
await scan.catch(error => console.log(String(error)));
```
The call rejects on the client and the handler stops at its next check; the abort reason travels in the notification and comes out as `ctx.mcpReq.signal.reason`:
```
SdkError: the end user clicked Stop
Stopped after 1 of 40 pages: the end user clicked Stop
```
The first line is the client's rejection, the second is the handler's `console.error` on the server. The SDK never sends a response for a cancelled request and discards whatever the handler still returns.
## Pass the signal to your own I/O
Hand the same signal to `fetch` — or any API that accepts an `AbortSignal` — and cancellation propagates into the work the handler started.
```ts source="../../examples/guides/servers/logging-progress-cancellation.examples.ts#registerTool_forwardSignal"
const SOURCE_URLS = {
readme: 'https://example.com/sources/readme.md',
changelog: 'https://example.com/sources/changelog.md'
};
server.registerTool(
'fetch-source',
{
description: 'Download one of the known source files',
inputSchema: z.object({ source: z.enum(['readme', 'changelog']) })
},
async ({ source }, ctx) => {
const response = await fetch(SOURCE_URLS[source], { signal: ctx.mcpReq.signal });
return { content: [{ type: 'text', text: await response.text() }] };
}
);
```
On cancellation `fetch` rejects mid-download and the handler unwinds with it, so no work outlives the request.
::: warning
Resolve an identifier against a fixed list, as `fetch-source` does. A tool that fetches a caller-supplied URL lets any connected client drive requests from your server's network position (server-side request forgery).
:::
## Recap
- Every handler receives a context as its second argument; the request-scoped helpers live on `ctx.mcpReq`.
- `ctx.mcpReq.notify` sends `notifications/progress` when the request carried a `progressToken`; `progress` must increase on each one.
- `ctx.mcpReq.log(level, data)` sends `notifications/message` once the `logging` capability is declared; MCP logging is deprecated (SEP-2577).
- `ctx.mcpReq.signal` aborts on cancellation and disconnect — check it in long loops and forward it to your own I/O.

View file

@ -0,0 +1,111 @@
---
shape: how-to
---
# Notifications
A **notification** is a one-way message your server pushes to a connected client; change notifications tell clients that a list or a resource they cached is stale.
## Send a list-changed notification
Start from a server with one tool.
```ts source="../../examples/guides/servers/notifications.examples.ts#notifications_server"
import { McpServer } from '@modelcontextprotocol/server';
const jobs = ['nightly-backup'];
const server = new McpServer({ name: 'jobs', version: '1.0.0' });
server.registerTool('list-jobs', { description: 'List the configured jobs' }, async () => ({
content: [{ type: 'text', text: jobs.join('\n') }]
}));
```
Every notification on this page is observed by an in-memory `Client` connected to the server above — [Test a server](../testing.md) shows that wiring — which logs each notification method it receives. Push a tool-list change yourself when the tool set changes for a reason the registration API cannot see.
```ts source="../../examples/guides/servers/notifications.examples.ts#sendToolListChanged_basic"
server.sendToolListChanged();
```
The client receives one `notifications/tools/list_changed` and re-fetches `tools/list`:
```
notifications/tools/list_changed
```
`sendPromptListChanged()` and `sendResourceListChanged()` are the prompt and resource siblings.
## Let registration changes notify for you
Hold on to the handle `registerTool` returns — every mutation through it sends the matching list-changed on its own.
```ts source="../../examples/guides/servers/notifications.examples.ts#registeredTool_update"
const report = server.registerTool('run-report', { description: 'Run the weekly report' }, async () => ({
content: [{ type: 'text', text: 'report queued' }]
}));
report.update({ description: 'Run the weekly report and email it' });
report.disable();
```
Registering, updating, and disabling each sent a notification of its own — three more, none explicit:
```
notifications/tools/list_changed
notifications/tools/list_changed
notifications/tools/list_changed
```
`enable()` and `remove()` notify the same way, and the handles returned by `registerResource` and `registerPrompt` send `notifications/resources/list_changed` and `notifications/prompts/list_changed`. Most servers never call a `send*ListChanged()` method directly.
## Advertise the `listChanged` capability
`McpServer` advertised `tools: { listChanged: true }` the moment you registered a tool, and does the same for prompts and resources. Only the [low-level `Server`](../advanced/low-level-server.md) needs the capability declared up front.
```ts source="../../examples/guides/servers/notifications.examples.ts#Server_listChanged"
const lowLevel = new Server({ name: 'jobs', version: '1.0.0' }, { capabilities: { tools: { listChanged: true } } });
```
The low-level `Server` refuses to send a notification its capabilities do not cover — `sendToolListChanged()` throws without a `tools` capability — and clients use the `listChanged` flag to decide which notification types to ask for.
## Publish a resource update through the handler
On a 2026-07-28 connection, change notifications reach a client only on a [`subscriptions/listen`](../clients/subscriptions.md) stream the client opens — see [Protocol versions](../protocol-versions.md). Behind [`createMcpHandler`](../serving/http.md) the `McpServer` instance is per-request, so publish through the handler, not the instance: `notify` is a typed facade over the handler's open subscription streams.
```ts source="../../examples/guides/servers/notifications.examples.ts#handler_notifyResourceUpdated"
const handler = createMcpHandler(() => buildJobsServer());
// After config://app changes:
handler.notify.resourceUpdated('config://app');
```
Every client whose stream listed `config://app` receives `notifications/resources/updated` carrying that URI; `notify.toolsChanged()`, `notify.promptsChanged()`, and `notify.resourcesChanged()` publish the three list-changed types the same way. Per-resource updates have one extra gate: the server your factory builds must advertise `resources: { subscribe: true }`.
::: tip
On stdio, [`serveStdio`](../serving/stdio.md) routes the instance's own `send*ListChanged()` and `sendResourceUpdated()` calls onto its open subscription stream — no `notify` facade needed.
:::
::: info Coming from 2025-era subscriptions
A 2025-era connection delivers per-resource updates without a listen stream — [Subscriptions](../clients/subscriptions.md#fall-back-to-legacy-per-resource-subscribe) covers that path, and [Protocol versions](../protocol-versions.md) the era split.
:::
## Pick an event bus for multi-process deployments
The `bus` option accepts any `ServerEventBus`; `InMemoryServerEventBus` is what `createMcpHandler` builds when you omit it.
```ts source="../../examples/guides/servers/notifications.examples.ts#createMcpHandler_bus"
const bus = new InMemoryServerEventBus();
const shared = createMcpHandler(() => buildJobsServer(), { bus });
```
The in-memory bus never leaves the process, so one process needs nothing more. Run more than one and you implement the two-method `ServerEventBus` interface — `publish` and `subscribe` — over your own pub/sub backend, then pass the same instance to every handler; see [Sessions, state, and scaling](../serving/sessions-state-scaling.md).
## Recap
- `sendToolListChanged()`, `sendPromptListChanged()`, and `sendResourceListChanged()` push a list-changed notification to connected clients.
- Registering, updating, enabling, disabling, or removing through a registration handle sends the matching list-changed for you.
- `McpServer` advertises `listChanged` as you register; only the low-level `Server` declares it up front.
- Behind `createMcpHandler`, publish through `handler.notify`; delivery reaches every open subscription stream that opted in.
- One process needs no `bus`; more than one shares a `ServerEventBus`.

205
docs/servers/prompts.md Normal file
View file

@ -0,0 +1,205 @@
---
shape: how-to
---
# Prompts
A **prompt** is a message template a connected client invokes by name. Clients surface prompts directly to people — slash commands, menu entries — where [tools](./tools.md) are picked by the model.
## Register a prompt
`registerPrompt` takes a name, a config, and a callback that returns the messages. `argsSchema` is a Zod object schema describing the arguments.
```ts source="../../examples/guides/servers/prompts.examples.ts#registerPrompt_review"
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const server = new McpServer({ name: 'review', version: '1.0.0' });
server.registerPrompt(
'review-code',
{
title: 'Code Review',
description: 'Review code for best practices and potential issues',
argsSchema: z.object({
code: z.string().describe('The code to review')
})
},
({ code }) => ({
messages: [
{
role: 'user' as const,
content: { type: 'text' as const, text: `Review this code:\n\n${code}` }
}
]
})
);
```
`prompts/list` now advertises `review-code` with one required argument, `code`.
::: tip
`.describe()` survives the conversion: `prompts/list` carries `The code to review` as the `code` argument's `description` — what a client shows next to the input field.
:::
::: info Coming from v1?
`registerPrompt` replaces `prompt()` — run the codemod, then see the [upgrade guide](../migration/upgrade-to-v2.md).
:::
Every call on this page comes from an in-memory `Client` connected to the server above — [Test a server](../testing.md) shows that wiring — and an MCP host does the same when someone picks the prompt. Fetch it with `getPrompt`.
```ts source="../../examples/guides/servers/prompts.examples.ts#getPrompt_review"
const result = await client.getPrompt({ name: 'review-code', arguments: { code: 'let x = 1' } });
console.log(result.messages);
```
The callback's messages come back with the argument filled in:
```
[
{
role: 'user',
content: { type: 'text', text: 'Review this code:\n\nlet x = 1' }
}
]
```
## Validate the arguments with the schema
Drop the required argument.
```ts source="../../examples/guides/servers/prompts.examples.ts#getPrompt_invalid"
import type { ProtocolError } from '@modelcontextprotocol/client';
try {
await client.getPrompt({ name: 'review-code', arguments: {} });
} catch (error) {
const { code, message } = error as ProtocolError;
console.log(code, message);
}
```
The SDK rejects the request before your callback runs:
```
-32602 Invalid arguments for prompt review-code: code: Invalid input: expected string, received undefined
```
A failed prompt validation is a protocol error — `getPrompt` rejects with a `ProtocolError` carrying code `-32602` (Invalid params). A [tool](./tools.md) argument rejection comes back as an `isError: true` result instead.
From that one schema the SDK derives the argument list `prompts/list` advertises, validates `prompts/get` arguments before your callback runs, and infers the callback's argument types.
## Build the messages
The callback returns `{ messages }`. Each message names a `role` — `'user'` or `'assistant'` — and one `content` block; add an `assistant` message after the `user` message to seed how the reply starts.
```ts source="../../examples/guides/servers/prompts.examples.ts#registerPrompt_messages"
server.registerPrompt(
'explain-error',
{
description: 'Explain a compiler error and suggest the smallest fix',
argsSchema: z.object({ error: z.string() })
},
({ error }) => ({
messages: [
{
role: 'user' as const,
content: { type: 'text' as const, text: `Explain this compiler error:\n\n${error}` }
},
{
role: 'assistant' as const,
content: { type: 'text' as const, text: 'The one-line cause:' }
}
]
})
);
```
The host hands the messages to the model in order, so the trailing `assistant` message becomes the start of its reply. `content` accepts the same union a tool result does: `text`, `image`, `audio`, `resource_link`, and `resource`.
## Embed a resource in a message
`type: 'resource'` puts a resource's contents inside a message. Register the resource as usual — see [Resources](./resources.md) — and embed the same `uri`, `mimeType`, and `text` in the prompt.
```ts source="../../examples/guides/servers/prompts.examples.ts#registerPrompt_embedResource"
const styleGuide = '- Prefer const over let.\n- No single-letter identifiers.';
server.registerResource('style-guide', 'doc://style-guide', { mimeType: 'text/markdown' }, async uri => ({
contents: [{ uri: uri.href, mimeType: 'text/markdown', text: styleGuide }]
}));
server.registerPrompt(
'review-against-style',
{
description: 'Review code against the team style guide',
argsSchema: z.object({ code: z.string() })
},
({ code }) => ({
messages: [
{
role: 'user' as const,
content: {
type: 'resource' as const,
resource: { uri: 'doc://style-guide', mimeType: 'text/markdown', text: styleGuide }
}
},
{
role: 'user' as const,
content: { type: 'text' as const, text: `Review this code against the style guide:\n\n${code}` }
}
]
})
);
```
`prompts/get` returns the style guide inline as the first message, so the client never makes a second `resources/read` round trip:
```
{
role: 'user',
content: {
type: 'resource',
resource: {
uri: 'doc://style-guide',
mimeType: 'text/markdown',
text: '- Prefer const over let.\n- No single-letter identifiers.'
}
}
}
```
The `uri` tells the client which registered resource the embedded copy came from.
## Offer argument autocompletion
Wrap an argument with `completable()` to suggest values while a client fills in the form.
```ts source="../../examples/guides/servers/prompts.examples.ts#registerPrompt_completable"
import { completable } from '@modelcontextprotocol/server';
server.registerPrompt(
'translate',
{
description: 'Translate a snippet into another language',
argsSchema: z.object({
language: completable(z.string(), value =>
['typescript', 'python', 'rust', 'go'].filter(language => language.startsWith(value))
),
code: z.string()
})
},
({ language, code }) => ({
messages: [{ role: 'user' as const, content: { type: 'text' as const, text: `Translate to ${language}:\n\n${code}` } }]
})
);
```
The client sends `completion/complete` with the characters typed so far; the SDK runs your function and returns the matching values. [Completion](./completion.md) covers the request flow and context-aware suggestions.
## Recap
- `registerPrompt(name, config, callback)` registers a prompt; clients discover it through `prompts/list`.
- `argsSchema` is one Zod object: the advertised argument list, argument validation, and the callback's argument types.
- Arguments that fail the schema reject `prompts/get` with a `-32602` protocol error; the callback never runs.
- The callback returns `{ messages }`; each message names a `role` and one `content` block.
- A message can embed a registered resource's contents with `type: 'resource'`.
- `completable()` adds per-argument autocompletion.

222
docs/servers/resources.md Normal file
View file

@ -0,0 +1,222 @@
---
shape: how-to
---
# Resources
A **resource** is read-only data — a file, a database row, a rendered report — that a connected client lists, reads, and attaches as context for the model. The client decides what to read: resources are application-controlled, where [tools](./tools.md) are model-controlled.
## Register a static resource
`registerResource` takes a name, a fixed URI, metadata, and a read callback.
```ts source="../../examples/guides/servers/resources.examples.ts#registerResource_static"
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
const server = new McpServer({ name: 'workspace', version: '1.0.0' });
server.registerResource(
'config',
'config://app',
{
title: 'Application Config',
description: 'Application configuration data',
mimeType: 'text/plain'
},
async uri => ({
contents: [{ uri: uri.href, text: 'log_level=info\nregion=eu-west-1' }]
})
);
```
`resources/list` now advertises `config://app` with that metadata, and `resources/read` on `config://app` runs the callback.
::: info Coming from v1?
`registerResource` replaces `resource()` — run the codemod, then see the [upgrade guide](../migration/upgrade-to-v2.md).
:::
## Return the contents from the read callback
The callback returns `{ contents: [...] }`. Add a resource whose contents hold two items: each one echoes the `uri` it answers for and carries either `text` or a base64 `blob`.
```ts source="../../examples/guides/servers/resources.examples.ts#registerResource_report"
// A 1x1 PNG; a production server reads these bytes from disk or object storage.
const chartPng = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=';
server.registerResource(
'report',
'report://latest',
{
title: 'Latest usage report',
description: 'Weekly usage summary with a rendered chart',
mimeType: 'text/markdown'
},
async uri => ({
contents: [
{ uri: uri.href, mimeType: 'text/markdown', text: 'Active installs grew 12% week over week.' },
{ uri: uri.href, mimeType: 'image/png', blob: chartPng }
]
})
);
```
Every call on this page comes from an in-memory `Client` connected to the server above — [Test a server](../testing.md) shows that wiring — and an MCP host does the same over stdio or HTTP. Read the resource.
```ts source="../../examples/guides/servers/resources.examples.ts#readResource_report"
const { contents } = await client.readResource({ uri: 'report://latest' });
console.log(contents);
```
The callback's array comes back unchanged, one entry per item:
```
[
{
uri: 'report://latest',
mimeType: 'text/markdown',
text: 'Active installs grew 12% week over week.'
},
{
uri: 'report://latest',
mimeType: 'image/png',
blob: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII='
}
]
```
The `mimeType` on each item describes that item; the `mimeType` in the registration config describes the resource as a whole in `resources/list`.
## Add a resource template
A `ResourceTemplate` registers a whole URI pattern instead of one URI. `list` is a required key — pass `undefined` when the instances are unbounded.
```ts source="../../examples/guides/servers/resources.examples.ts#registerResource_template"
server.registerResource(
'user-profile',
new ResourceTemplate('users://{userId}/profile', { list: undefined }),
{
title: 'User Profile',
description: 'Profile data for one user',
mimeType: 'application/json'
},
async (uri, { userId }) => ({
contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ userId, plan: 'pro' }) }]
})
);
```
The matched variables arrive parsed as the read callback's second argument. Read any URI the pattern matches.
```ts source="../../examples/guides/servers/resources.examples.ts#readResource_template"
const profile = await client.readResource({ uri: 'users://7/profile' });
console.log(profile.contents);
```
The callback ran with `userId` bound to `'7'`:
```
[
{
uri: 'users://7/profile',
mimeType: 'application/json',
text: '{"userId":"7","plan":"pro"}'
}
]
```
## List the template's instances
`users://{userId}/profile` is readable but never appears in `resources/list` — with `list: undefined` there is nothing to enumerate. Register a template over an enumerable set and give it a `list` callback.
```ts source="../../examples/guides/servers/resources.examples.ts#registerResource_list"
server.registerResource(
'team-roster',
new ResourceTemplate('teams://{teamId}/roster', {
list: async () => ({
resources: [
{ uri: 'teams://core/roster', name: 'Core team roster' },
{ uri: 'teams://growth/roster', name: 'Growth team roster' }
]
})
}),
{
description: 'Members of one team',
mimeType: 'text/plain'
},
async (uri, { teamId }) => ({
contents: [{ uri: uri.href, text: `Members of team ${teamId}` }]
})
);
```
`resources/list` merges the static resources with every template's `list` results:
```ts source="../../examples/guides/servers/resources.examples.ts#listResources"
const { resources } = await client.listResources();
console.log(resources.map(resource => resource.uri));
```
Both `teams://` rosters are discoverable; the `users://` template contributes nothing:
```
[
'config://app',
'report://latest',
'teams://core/roster',
'teams://growth/roster'
]
```
`resources/templates/list` still advertises both URI patterns (`client.listResourceTemplates()`), so a client that already knows a `userId` builds the concrete URI itself.
## Sanitize file-backed paths
A template variable that becomes a filesystem path is client-controlled input. Resolve it to a real path and reject anything outside the root before you read.
```ts source="../../examples/guides/servers/resources.examples.ts#registerResource_file"
import { readFile, realpath } from 'node:fs/promises';
import path from 'node:path';
const DOCS_ROOT = path.resolve('./docs');
server.registerResource(
'doc',
new ResourceTemplate('docs://{file}', { list: undefined }),
{
description: 'A markdown page from the docs directory',
mimeType: 'text/markdown'
},
async (uri, { file }) => {
const requested = await realpath(path.join(DOCS_ROOT, String(file)));
if (!requested.startsWith(DOCS_ROOT + path.sep)) {
throw new Error(`${uri.href} resolves outside the docs root`);
}
return { contents: [{ uri: uri.href, text: await readFile(requested, 'utf8') }] };
}
);
```
`realpath` collapses `..` segments and symlinks to the path that is actually on disk; the `startsWith` check then rejects anything that escaped `DOCS_ROOT`. Throw on rejection — [Errors](./errors.md) covers how a thrown error reaches the client.
::: warning
Never pass a template variable or a client-supplied URI to a filesystem API unchecked. `..` arrives raw and percent-encoded, and a symlink inside the root can point outside it — compare resolved real paths, never the strings the client sent.
:::
## Tell clients when a resource changes
Registering, enabling, disabling, or removing a resource already sends `notifications/resources/list_changed`. Send it yourself when the set changes for a reason the SDK cannot see.
```ts source="../../examples/guides/servers/resources.examples.ts#sendResourceListChanged"
server.sendResourceListChanged();
```
The notification tells connected clients to call `resources/list` again. A change to one resource's content is a different signal, `notifications/resources/updated` — [Notifications](./notifications.md) covers both.
## Recap
- `registerResource(name, uri, config, readCallback)` registers a resource at a fixed URI.
- The read callback returns `{ contents: [...] }`; each item echoes the `uri` and carries `text` or a base64 `blob`.
- A `ResourceTemplate` registers a URI pattern; the matched variables arrive parsed as the read callback's second argument.
- A template's `list` callback is what makes its instances appear in `resources/list`.
- Resolve file-backed paths to their real location and reject anything outside the root before reading.
- Registration changes emit `notifications/resources/list_changed` automatically.

80
docs/servers/sampling.md Normal file
View file

@ -0,0 +1,80 @@
---
shape: how-to
---
# Sampling
::: warning Deprecated — SEP-2577
Call your LLM provider's API directly from your server instead. **Sampling** is deprecated as of protocol version 2026-07-28 (SEP-2577) and stays functional on 2025-era connections for at least twelve months — see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated).
:::
## Replace sampling with a direct provider call
Sampling routes an LLM call through the connected client: a tool handler sends a prompt, the host runs it through a model it controls, and the handler resumes with the completion. The 2026-07-28 revision removes the server-to-client request channel that carries it.
Migrate by importing your LLM provider's SDK into the server and calling it from the tool handler with your own API key. The handler keeps its shape; the `requestSampling` call is the only line that changes, and you stop depending on what the client supports.
## Request a completion from the client
`ctx.mcpReq.requestSampling` sends a `sampling/createMessage` request to the connected client from inside a tool handler. The client runs the messages through its model and resolves the call with the completion.
```ts source="../../examples/guides/servers/sampling.examples.ts#registerTool_sampling"
server.registerTool(
'summarize',
{
description: 'Summarize text using the client LLM',
inputSchema: z.object({ text: z.string() })
},
async ({ text }, ctx) => {
const response = await ctx.mcpReq.requestSampling({
messages: [{ role: 'user', content: { type: 'text', text: `Summarize in one sentence:\n\n${text}` } }],
maxTokens: 500
});
return { content: [{ type: 'text', text: `Model (${response.model}): ${JSON.stringify(response.content)}` }] };
}
);
```
The handler blocks until the client answers, so your server never holds the key for the model that does the work — the host does.
::: info
On a 2026-07-28 connection `requestSampling` throws. The replacement on that revision is returning an embedded `createMessage` request from the handler — [input_required](./input-required.md) owns that form. Era differences are listed in [Protocol versions](../protocol-versions.md).
:::
## Read the model's reply
The response is a `CreateMessageResult`: the client decides which model fulfils the request and returns its name as `model`, plus the assistant `role` and one `content` block. The handler above folds it into its tool result, so calling `summarize` from a client whose model is named `host-model` returns:
```
[
{
type: 'text',
text: 'Model (host-model): {"type":"text","text":"Sampling lets a tool ask the client for a completion."}'
}
]
```
## Require the sampling capability
`requestSampling` only works against a client that declared the `sampling` capability and registered a `sampling/createMessage` handler — [Handle requests from the server](../clients/server-requests.md) covers that side.
Pass `enforceStrictCapabilities: true` to the `McpServer` constructor and the SDK checks the client's declared capabilities before it sends any server-initiated request. Against a client that never declared `sampling`, `requestSampling` then throws inside your handler, and the call comes back as an ordinary `isError` tool result:
```
{
content: [
{
type: 'text',
text: 'Client does not support sampling (required for sampling/createMessage)'
}
],
isError: true
}
```
## Recap
- Sampling is deprecated (SEP-2577); the migration target is a direct LLM provider call from your server.
- `ctx.mcpReq.requestSampling({ messages, maxTokens })` asks the connected client's model for a completion mid-handler.
- The client picks the model; the result carries `model`, `role`, and `content`.
- On a 2026-07-28 connection `requestSampling` throws; the embedded-request form lives on the input_required page.
- The client must declare the `sampling` capability; `enforceStrictCapabilities: true` rejects the request before the wire when it did not.

159
docs/servers/tools.md Normal file
View file

@ -0,0 +1,159 @@
---
status: calibration
shape: how-to
---
# Tools
A **tool** is an action a connected client — and the model driving it — can invoke on your server.
## Add a tool
`registerTool` takes a name, a config, and a handler. `inputSchema` is a Zod schema — the only schema you write.
```ts source="../../examples/guides/servers/tools.examples.ts#registerTool_search"
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const catalog = [
{ name: 'Espresso cup', price: 12 },
{ name: 'Travel mug', price: 24 },
{ name: 'Mug rack', price: 36 }
];
const server = new McpServer({ name: 'catalog', version: '1.0.0' });
server.registerTool(
'search',
{
description: 'Search the product catalog',
inputSchema: z.object({
query: z.string().describe('Substring to match against product names'),
limit: z.number().int().max(50).optional()
})
},
async ({ query, limit }) => {
const hits = catalog.filter(product => product.name.toLowerCase().includes(query.toLowerCase()));
const names = hits.slice(0, limit ?? 10).map(product => product.name);
return { content: [{ type: 'text', text: names.join('\n') }] };
}
);
```
From that one schema the SDK derives the JSON Schema the model sees, validates arguments before your handler runs, and infers the handler's argument types.
`tools/list` now advertises `search`, and the SDK has already parsed every call that reaches your handler.
::: tip
`.describe()` survives the conversion: the JSON Schema advertised for `query` carries `Substring to match against product names` as its `description` — the only documentation the model gets for that argument.
:::
::: info Coming from v1?
`registerTool` replaces `tool()` — run the codemod, then see the [upgrade guide](../migration/upgrade-to-v2.md).
:::
## Call it
Every call on this page comes from an in-memory `Client` connected to the server above — [Test a server](../testing.md) shows that wiring — and an MCP host does the same over stdio or HTTP. Call the tool with valid arguments.
```ts source="../../examples/guides/servers/tools.examples.ts#callTool_search"
const result = await client.callTool({ name: 'search', arguments: { query: 'mug' } });
console.log(result.content);
```
The handler's `content` comes back unchanged:
```
[ { type: 'text', text: 'Travel mug\nMug rack' } ]
```
## Send arguments the schema rejects
Change one argument: a `limit` the schema caps at 50.
```ts source="../../examples/guides/servers/tools.examples.ts#callTool_invalid"
const rejected = await client.callTool({ name: 'search', arguments: { query: 'mug', limit: 999 } });
console.log(rejected);
```
The SDK rejects the arguments before your handler runs:
```
{
content: [
{
type: 'text',
text: 'Input validation error: Invalid arguments for tool search: limit: Too big: expected number to be <=50'
}
],
isError: true
}
```
The rejection is an ordinary tool result with `isError: true`, so the model reads the message and retries with arguments that fit the schema. Thrown errors and protocol-level failures are their own topic — see [Errors](errors.md).
## Return structured output
Add `outputSchema` and return the matching value as `structuredContent`, next to the human-readable `content`.
```ts source="../../examples/guides/servers/tools.examples.ts#registerTool_structured"
server.registerTool(
'product-details',
{
description: 'Look up one product by its exact name',
inputSchema: z.object({ name: z.string() }),
outputSchema: z.object({ name: z.string(), price: z.number() })
},
async ({ name }) => {
const product = catalog.find(candidate => candidate.name === name);
if (!product) throw new Error(`No product named ${name}`);
const output = { name: product.name, price: product.price };
return {
content: [{ type: 'text', text: JSON.stringify(output) }],
structuredContent: output
};
}
);
```
The SDK validates `structuredContent` against `outputSchema` before the result leaves your server, and advertises the derived JSON Schema in `tools/list` so clients can validate it too.
Calling `product-details` with `{ name: 'Travel mug' }` returns both renderings:
```
{
content: [ { type: 'text', text: '{"name":"Travel mug","price":24}' } ],
structuredContent: { name: 'Travel mug', price: 24 }
}
```
The wire encoding of structured results differs by protocol era — see [Protocol versions](../protocol-versions.md).
## Annotate the tool
`title` is the display name; `annotations` are behavior hints for the client.
```ts source="../../examples/guides/servers/tools.examples.ts#registerTool_annotations"
server.registerTool(
'clear-catalog',
{
title: 'Clear the catalog',
description: 'Remove every product from the catalog',
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true }
},
async () => {
catalog.length = 0;
return { content: [{ type: 'text', text: 'Catalog cleared' }] };
}
);
```
A tool that takes no arguments omits `inputSchema`. Annotations never change how the SDK runs the tool — clients use them to decide what to put in front of the end user: a host can auto-approve a read-only tool and require confirmation before a destructive one.
## Recap
- `registerTool(name, config, handler)` registers a tool; `inputSchema` is a Zod object schema.
- The one schema yields the advertised JSON Schema, argument validation, and the handler's argument types.
- Arguments that fail the schema come back as an `isError: true` tool result; the handler never runs.
- `outputSchema` plus `structuredContent` add machine-readable results, validated before they leave the server.
- `title` and `annotations` describe the tool to clients and never change execution.