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

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.