Skip to content

Commit 6c44f2f

Browse files
g97iulio1609Copilot
andcommitted
fix: gracefully terminate active sessions on shutdown
During StreamableHTTPSessionManager shutdown, iterate through all active transports and call terminate() before cancelling the task group. This allows SSE connections to receive a proper HTTP response instead of being abruptly dropped, which caused Uvicorn to log 'ASGI callable returned without completing response'. Fixes #2150 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 62575ed commit 6c44f2f

2 files changed

Lines changed: 31 additions & 0 deletions

File tree

src/mcp/server/streamable_http_manager.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]:
130130
yield # Let the application run
131131
finally:
132132
logger.info("StreamableHTTP session manager shutting down")
133+
# Gracefully terminate all active sessions before cancelling
134+
# the task group so that SSE connections receive a proper
135+
# HTTP response instead of being abruptly dropped.
136+
for session_id, transport in list(self._server_instances.items()):
137+
try:
138+
await transport.terminate()
139+
logger.debug(f"Terminated session {session_id} during shutdown")
140+
except Exception: # pragma: no cover
141+
logger.debug(f"Error terminating session {session_id} during shutdown", exc_info=True)
133142
# Cancel task group to stop all spawned tasks
134143
tg.cancel_scope.cancel()
135144
self._task_group = None

tests/server/test_streamable_http_manager.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,25 @@ def test_session_idle_timeout_rejects_non_positive():
410410
def test_session_idle_timeout_rejects_stateless():
411411
with pytest.raises(RuntimeError, match="not supported in stateless"):
412412
StreamableHTTPSessionManager(app=Server("test"), session_idle_timeout=30, stateless=True)
413+
414+
415+
@pytest.mark.anyio
416+
async def test_shutdown_terminates_active_sessions():
417+
"""Test that run() shutdown terminates active transports before cancelling tasks."""
418+
app = Server("test-shutdown-terminate")
419+
manager = StreamableHTTPSessionManager(app=app)
420+
421+
# We'll manually inject a mock transport into _server_instances
422+
# and verify terminate() is called during shutdown.
423+
mock_transport = AsyncMock(spec=StreamableHTTPServerTransport)
424+
mock_transport.mcp_session_id = "test-session-1"
425+
mock_transport.is_terminated = False
426+
427+
async with manager.run():
428+
# Inject mock transport as if a session was created
429+
manager._server_instances["test-session-1"] = mock_transport
430+
431+
# After exiting run(), terminate should have been called
432+
mock_transport.terminate.assert_awaited_once()
433+
# Server instances should be cleared
434+
assert len(manager._server_instances) == 0

0 commit comments

Comments
 (0)