
# SSE Streaming

> The standalone `GET` stream, legacy only.

> [!NOTE]
> The standalone `GET` stream was removed in `2026-07-28` — use [`subscriptions/listen`](/guide/modern/subscriptions) instead.

Legacy clients open a `GET` connection to the MCP endpoint to receive server→client messages. Streaming is **explicitly opt-in**: without an `onStream` callback, `GET` answers `405`, telling the client that this endpoint does not stream.

```ts
defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  onStream(stream, event) {
    // Push a JSON-RPC notification to the client
    stream.push({
      data: JSON.stringify({
        jsonrpc: "2.0",
        method: "custom/update",
        params: { status: "ready" },
      }),
    });
  },
  tools: [/* ... */],
});
```

`stream` is an h3 [event stream](https://h3.dev/guide/basics/response#server-sent-events-sse). Anything you push must be a complete JSON-RPC message — this is the channel for `notifications/resources/updated`, `notifications/tools/list_changed`, and `notifications/message`.

## Resumption

The handler sends a priming event on connection so proxies flush the stream immediately, and honors resumption per [SEP-1699](https://modelcontextprotocol.io/specification/2025-11-25): a reconnecting client sends `Last-Event-ID` (or a `lastEventId` query parameter), and the value is available as `event.context.mcp.lastEventId`.

Replaying what the client missed is up to you — keep a bounded ring buffer of recent events keyed by id, and push from the last acknowledged one:

```ts
onStream(stream, event) {
  const from = event.context.mcp?.lastEventId;
  for (const missed of recentEvents(from)) {
    stream.push({ id: missed.id, data: JSON.stringify(missed.message) });
  }
  const off = subscribe((message) => stream.push({ id: nextId(), data: JSON.stringify(message) }));
  // clean up when the client disconnects
  event.req.signal?.addEventListener("abort", off, { once: true });
}
```

> [!IMPORTANT]
> A stream is a server-lifetime resource held open by the client. Always unregister your source when the connection ends, and bound how many streams and how much replay history you are willing to keep — an open connection that nobody cleans up is the easiest resource leak to ship.

## Sessions and Auth

`GET` goes through the same checks as `POST`: [auth](/security/auth), [origin validation](/security/origin), and the session header when [sessions](/guide/legacy/sessions) are enabled. Body limits do not apply — a `GET` carries no body.
