h3-mcp logoh3-mcp

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.

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

CallEmitsSilent when
progress(progress, total?, msg?)notifications/progressno progressToken on the request
log(level, data, logger?)notifications/messageclient asked for no level, or a coarser one
notify(method, params?)any notificationthe 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:

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

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

Read more in Cancellation & progress.

#What Does Not Go Here

Change notifications — notifications/tools/list_changed, notifications/resources/updated — belong on a subscriptions/listen 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 in Errors.
h3-mcp logo

h3-mcp  MCP servers, built on H3.