Skip to content

fix: clean up SSE session on client disconnect#2200

Merged
maxisbey merged 5 commits intomodelcontextprotocol:mainfrom
Varun6578:fix/sse-session-cleanup
Mar 4, 2026
Merged

fix: clean up SSE session on client disconnect#2200
maxisbey merged 5 commits intomodelcontextprotocol:mainfrom
Varun6578:fix/sse-session-cleanup

Conversation

@Varun6578
Copy link
Contributor

@Varun6578 Varun6578 commented Mar 2, 2026

Note

EDIT from @maxisbey: Retargeting to Fixes #1227 — see #2200 (comment) for details. The fix is correct and useful, but #423 is a different bug (client-side EventSource auto-reconnect without re-initialize) already mitigated by #822 and #1478. The "server reload" claim below is inaccurate — a uvicorn restart gives a fresh empty dict; this leak only occurs within a single process lifetime.


Summary

Fixes #1227

After a client disconnects from the SSE endpoint, the session entry was never removed from _read_stream_writers. This caused stale sessions to accumulate, and after a server reload old session IDs could still be found in the dict (with closed streams), leading to confusing behavior.

Changes

src/mcp/server/sse.py:

  • Added self._read_stream_writers.pop(session_id, None) in the disconnect handler (response_wrapper) to clean up the session entry when a client disconnects

This is a one-line fix. The streams were already being closed on disconnect; this ensures the session dict entry is also removed so that subsequent POST requests to the old session ID correctly receive a 404 response.

Remove session from _read_stream_writers when a client disconnects
from the SSE endpoint. Previously, stale session entries accumulated
indefinitely, causing issues after server reloads where old session
IDs would still be found in the dict (but with closed streams).

Fixes modelcontextprotocol#423

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@maxisbey
Copy link
Contributor

maxisbey commented Mar 2, 2026

Please provide evidence this fixes the original issue, I'd be keen to see both a reproduction and a unit test if possible

@maxisbey maxisbey added the needs more work Not ready to be merged yet, needs additional follow-up from the author(s). label Mar 2, 2026
Add test_sse_session_cleanup_on_disconnect that verifies:
- After a client disconnects, the stale session is removed from
  _read_stream_writers
- POST requests to the disconnected session return 404 (not 202)

This provides evidence that the fix for modelcontextprotocol#423 works correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Varun6578
Copy link
Contributor Author

Varun6578 commented Mar 2, 2026

I've added a regression test (test_sse_session_cleanup_on_disconnect in tests/shared/test_sse.py) that:

  1. Reproduces the issue: Connects a client session, captures the session ID via on_session_created, then disconnects.
  2. Verifies the fix: After disconnect, POSTs to the stale session ID and asserts it returns 404. Before the fix, it would return 202, accepting the message into a dead session.

The test uses a retry loop with a 5-second timeout to account for the async disconnect processing, and runs alongside the existing 26 SSE tests (all passing).

Varun Sharma and others added 2 commits March 2, 2026 18:01
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…l#1227)

The fix addresses stale session entries in _read_stream_writers after
disconnect (202 + ClosedResourceError symptom from modelcontextprotocol#1227), not the
EventSource auto-reconnect scenario from modelcontextprotocol#423 which is a client-side
concern already mitigated by modelcontextprotocol#822 and modelcontextprotocol#1478.
@maxisbey
Copy link
Contributor

maxisbey commented Mar 4, 2026

Thanks for this @Varun6578 — the fix is correct and the test is solid (fails on main with 202 != 404, passes here, stable across 30 runs). Merging.

A few housekeeping notes for the record:

This actually fixes #1227, not #423. I've retargeted the PR accordingly. #1227 is the exact production symptom this addresses:

"POST /messages/?session_id=... HTTP/1.1" 202 Accepted
...
anyio.ClosedResourceError

That issue was closed as not-planned because nobody provided a repro — your test is that repro.

Why this isn't #423: #423's reproduction is a full uvicorn restart. After a process restart, _read_stream_writers is a fresh empty dict — stale entries are physically impossible. What's actually happening in #423 is the browser's EventSource silently auto-reconnects after the server comes back up, the server hands it a brand-new session, and the client sends tools/list without re-running initialize. That's client-side; the server-side crash it caused was already addressed by #822 (wraps the RuntimeError, sends a JSON-RPC error back instead of blowing up) and #1478 (fixed the notifications/initialized/tools/list race from this comment). I ran the #423 repro with and without this patch — identical behaviour both times.

Spec alignment: The legacy SSE spec (2024-11-05) is silent on dead-session responses, but Streamable HTTP (2025-03-26 onwards) says "the server MAY terminate the session at any time, after which it MUST respond to requests containing that session ID with HTTP 404 Not Found." The server already returns 404 for never-existed session IDs; this makes disconnected sessions hit the same path.

Re: #2044 — that's a byte-identical diff without a test; will close as superseded.

Pushed one small commit to update the test docstring reference.

AI Disclaimer

On Python 3.11 with coverage 7.10.7 (lowest-direct), plain statements
immediately after async with sse_client(...) exits are not traced
reliably due to cancellation in __aexit__ interacting with 3.11's
zero-cost exception bytecode. Entering a new async with block does
generate a line event, so removing the intermediate assert and indexing
the list directly inside the httpx block sidesteps the tracer gap
without needing a pragma.
@maxisbey maxisbey merged commit b3149d2 into modelcontextprotocol:main Mar 4, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs more work Not ready to be merged yet, needs additional follow-up from the author(s).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSE: ConnectionClosed exception after session disconnect

2 participants