SSE Streaming
The standalone GET stream, legacy only.
Note
The standalone GET stream was removed in 2026-07-28 — use subscriptions/listen 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.
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. 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: 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:
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, origin validation, and the session header when sessions are enabled. Body limits do not apply — a GET carries no body.