sse-polling
Legacy mechanism (2025 handshake era).
Last-Event-IDresumability and the sessionful transport are removed in the 2026-07-28 protocol (SEP-2575) with no modern-era equivalent; the closest 2026-era pattern is client-side reconnection over a persistedDiscoverResult—reconnect/. TODO(maxisbey): revisit before beta.
SEP-1699 server-initiated SSE disconnection with Last-Event-ID replay. The
server's EventStore stamps every SSE event with an ID and opens each response
stream with a priming event; mid-handler the tool calls
ctx.close_sse_stream() to release the open HTTP response (freeing a
connection slot), keeps emitting progress into the event store, and returns.
The client transport sees the stream end, reconnects with Last-Event-ID, and
the event store replays everything it missed — await client.call_tool(...)
resolves as if the disconnect never happened.
Run it
# HTTP — the client self-hosts the app on a free port, runs, then tears it down
uv run python -m stories.sse_polling.client --http --legacy
# same, against the lowlevel-API server variant
uv run python -m stories.sse_polling.client --http --legacy --server server_lowlevel
# against a server you run yourself (real uvicorn on :8000)
uv run python -m stories.sse_polling.server --port 8000 &
SERVER_PID=$!
uv run python -m stories.sse_polling.client --http http://127.0.0.1:8000/mcp --legacy
kill "$SERVER_PID"
What to look at
client.pymain— opens withasync with Client(target, mode=mode). There is no client-side resumability configuration: theClientand thestreamable_http_clienttransport handle the priming event, the SSEretry:hint, and theLast-Event-IDreconnect automatically. The assertion that the"after-close"progress message arrived is the proof — it was emitted while no SSE stream was open.server.py—streamable_http_app(event_store=..., retry_interval=0). Passing anEventStoreis what enables resumability: every SSE event gets an ID and the response opens with a priming event so the client always has aLast-Event-IDto reconnect with.retry_interval=0makes the client's reconnect wait a no-op (the SSEretry:hint).server.py—await ctx.close_sse_stream(). Ends the current request's SSE response without cancelling the handler. Everything emitted afterwards goes to the event store and is replayed on reconnect. A no-op when noevent_storeis configured.server_lowlevel.py—ctx.close_sse_stream. On the lowlevel API the callback is an optional field onServerRequestContext; it isNoneunless an event store is wired and the negotiated version is in the 2025 era.
Caveats
streamable_http_app(...)is a hosting entry that reshapes in a later release; this story calls it directly because the event-store and retry-interval kwargs are the point.- DNS-rebinding protection is disabled (
transport_security=NO_DNS_REBIND) because the in-process httpx client sends noOriginheader. Drop the kwarg for a real deployment. event_store.pyhere is example-grade only (sequential IDs, no eviction). A production server would back theEventStoreinterface with persistent storage.
Spec
Resumability and Redelivery · SEP-1699 (server-initiated SSE close)
See also
standalone_get/ (the standalone-stream sibling of close_sse_stream()),
reconnect/ (the modern-era reconnection story — persisted DiscoverResult,
no event store), streaming/ (in-flight progress + cancellation without the
disconnect).