
# Request-Scoped Streaming

> Notifications delivered on the response stream of the request that caused them.

When a modern request carries `_meta.progressToken` or `_meta["io.modelcontextprotocol/logLevel"]`, its response is an SSE stream instead of a JSON body. Progress and log messages then travel with the request they belong to — no separate channel, no correlation to reconstruct.

```ts
const importTool = defineMcpTool({
  name: "import",
  handler: async (event) => {
    const mcp = event.context.mcp!;
    for (let i = 1; i <= 10; i++) {
      mcp.progress?.(i, 10, `row ${i}`);
      mcp.log?.("info", { row: i }, "importer");
      await importRow(i);
    }
    return { content: [{ type: "text", text: "Imported" }] };
  },
});
```

## The Helpers

| Call                               | Emits                    | Silent when                                 |
| ---------------------------------- | ------------------------ | ------------------------------------------- |
| `progress(progress, total?, msg?)` | `notifications/progress` | no `progressToken` on the request           |
| `log(level, data, logger?)`        | `notifications/message`  | client asked for no level, or a coarser one |
| `notify(method, params?)`          | any notification         | the request has no stream                   |

`log()` gates on the requested level exactly as the spec requires: a client asking for `warning` never receives your `debug` lines. The eight levels are `debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`.

> [!NOTE]
> A request only gets a stream when the client opted in. Without `progressToken` or `logLevel` in `_meta` the response is a plain JSON body and `progress()` / `log()` / `notify()` **silently do nothing** — there is nowhere to deliver a notification and the spec offers no out-of-band path. This is by design, not a failure: write handlers that work either way.

## Cancellation

Closing the response stream aborts the request. The transport signal is linked into `event.context.mcp.signal`, so the same check that handles legacy `notifications/cancelled` covers it:

```ts
if (event.context.mcp?.signal?.aborted) return;
```

This _is_ the modern cancellation mechanism — `notifications/cancelled` is not expected over HTTP.

:read-more{to="/guide/basics/cancellation" title="Cancellation & progress"}

## What Does Not Go Here

Change notifications — `notifications/tools/list_changed`, `notifications/resources/updated` — belong on a [`subscriptions/listen`](/guide/modern/subscriptions) stream, not on a request-scoped one. A request-scoped stream lives and dies with its request; nothing outside that request should be published on it.

## Errors Mid-Stream

Once the stream is open the HTTP status is already `200`, so a handler that fails afterwards is reported as an error frame on the stream rather than an HTTP error. The same message rules apply: throw `McpJsonRpcError` to say something specific, or the error collapses to `-32603 "Internal error"`.

:read-more{to="/guide/basics/errors" title="Errors"}
