
# 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/cancelled` is not expected over HTTP.
- **Legacy** — the client sends `notifications/cancelled` naming the in-flight request id.

Check it between units of work:

```ts
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:

```ts
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`:

```ts
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()`:

```ts
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.

:read-more{to="/guide/modern/streaming" title="Request-scoped streaming"}

On the legacy era, notifications go out over the [GET/SSE stream](/guide/legacy/sse), and `onProgress` receives progress notifications sent _by the client_:

```ts
defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  onProgress(notification, event) {
    console.log(`Progress: ${notification.progress}/${notification.total ?? "?"}`);
  },
  tools: [/* ... */],
});
```
