Skip to content

Commit fe0a371

Browse files
committed
fix(server): deregister terminated streamable HTTP sessions
1 parent 6e30452 commit fe0a371

2 files changed

Lines changed: 33 additions & 0 deletions

File tree

src/mcp/server/streamable_http_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ async def _handle_stateful_request(self, scope: Scope, receive: Receive, send: S
281281
if transport.idle_scope is not None and self.session_idle_timeout is not None:
282282
transport.idle_scope.deadline = anyio.current_time() + self.session_idle_timeout # pragma: no cover
283283
await transport.handle_request(scope, receive, send)
284+
if transport.is_terminated:
285+
self._server_instances.pop(request_mcp_session_id, None)
286+
self._session_owners.pop(request_mcp_session_id, None)
284287
return
285288

286289
if request_mcp_session_id is None:

tests/server/test_streamable_http_manager.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,36 @@ async def mock_receive():
304304
assert not manager._server_instances, "No sessions should be tracked after the only session exits gracefully"
305305

306306

307+
@pytest.mark.anyio
308+
async def test_terminated_existing_session_is_removed_from_registry(
309+
running_manager: tuple[StreamableHTTPSessionManager, Server],
310+
):
311+
manager, _app = running_manager
312+
session_id = "terminated-session"
313+
transport = AsyncMock()
314+
transport.is_terminated = True
315+
transport.idle_scope = None
316+
manager._server_instances[session_id] = transport
317+
318+
scope = {
319+
"type": "http",
320+
"method": "DELETE",
321+
"path": "/mcp",
322+
"headers": [(MCP_SESSION_ID_HEADER.encode(), session_id.encode())],
323+
}
324+
325+
async def mock_receive():
326+
return {"type": "http.request", "body": b"", "more_body": False}
327+
328+
async def mock_send(_message: Message):
329+
pass
330+
331+
await manager.handle_request(scope, mock_receive, mock_send)
332+
333+
assert session_id not in manager._server_instances
334+
assert session_id not in manager._session_owners
335+
336+
307337
@pytest.mark.anyio
308338
async def test_stateful_session_cleanup_on_exception(running_manager: tuple[StreamableHTTPSessionManager, Server]):
309339
manager, _app = running_manager

0 commit comments

Comments
 (0)