Cancellation & Progress
Stop work nobody is waiting for, and report on work that continues.
#Cancellation
Every request handler gets an AbortSignal on event.context.mcp.signal. How it fires depends on the era, but the signal is the same:
- Modern — the client closes the SSE response stream. That is the cancellation mechanism;
notifications/cancelledis not expected over HTTP. - Legacy — the client sends
notifications/cancellednaming the in-flight request id.
Check it between units of work:
const longRunningTool = defineMcpTool({
name: "process",
description: "Long-running task",
handler: async (event) => {
const { signal } = event.context.mcp!;
for (let i = 0; i < 100; i++) {
if (signal?.aborted) {
return { isError: true, content: [{ type: "text", text: "Cancelled" }] };
}
await doWork(i);
}
return { content: [{ type: "text", text: "Done" }] };
},
});Better still, hand the signal to whatever you await so cancellation is immediate rather than checked at the top of the next loop:
const res = await fetch(url, { signal: event.context.mcp!.signal });Cancellation is cooperative. Nothing is killed for you: a handler that ignores the signal runs to completion, and its result is discarded.
#Observing Cancellations
onCancelled fires for legacy notifications/cancelled:
defineMcpHandler({
name: "my-server",
version: "1.0.0",
onCancelled(notification, event) {
console.log(`Request ${notification.requestId} cancelled: ${notification.reason}`);
},
tools: [longRunningTool],
});The alias notifications/canceled (one l) is accepted too, for clients that shipped the misspelling.
#Progress
Clients opt into progress by attaching _meta.progressToken to a request. The token is on the context as event.context.mcp.progressToken, and on the modern era you emit updates through progress():
handler: async (event) => {
const mcp = event.context.mcp!;
for (let i = 1; i <= 10; i++) {
mcp.progress?.(i, 10, `row ${i}`);
await importRow(i);
}
return { content: [{ type: "text", text: "Imported" }] };
};progress() no-ops when the client did not opt in — there is no stream to deliver on, and the spec gives no out-of-band channel.
On the legacy era, notifications go out over the GET/SSE stream, and onProgress receives progress notifications sent by the client:
defineMcpHandler({
name: "my-server",
version: "1.0.0",
onProgress(notification, event) {
console.log(`Progress: ${notification.progress}/${notification.total ?? "?"}`);
},
tools: [/* ... */],
});