You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Active Streamable HTTP sessions are not terminated during shutdown #2150
This is primarily a repost of this fastmcp issue as it seems to actually be a bug in the mcp python sdk and I want to make sure we've got visibility over here.
When shutting down a FastMCP server (CTRL+C) while clients have active streaming connections, Uvicorn logs:
ERROR: ASGI callable returned without completing response.
The error occurs because StreamableHTTPSessionManager cancels the task group without first gracefully terminating active SSE/streaming connections.
Expected behavior would be the active connections being closed with a proper http response before the rest of shutdown.
This abruptly cancels all tasks in the task group without first gracefully terminating active sessions. The EventSourceResponse coroutines (running inside the cancelled tasks) are killed mid-stream before they can send a proper HTTP response completion, which triggers Uvicorn's ASGI callable returned without completing response error.
There are actually two compounding problems:
run() never calls terminate() on active transports before cancelling the task group. It just clears the dict.
terminate() itself doesn't close _sse_stream_writers. It only closes _request_streams via _clean_up_memory_streams(), but the SSE stream writers (which keep EventSourceResponse alive) are stored separately in self._sse_stream_writers and are never touched during terminate(). Closing the request streams alone doesn't cause the SSE response to complete gracefully — the EventSourceResponse keeps waiting on its sse_stream_reader.
Proposed Fix
File: src/mcp/server/streamable_http_manager.py — In the run() method's finally block, iterate over all active transports and call terminate() on each before cancelling the task group:
finally:
logger.info("StreamableHTTP session manager shutting down")
# Gracefully terminate all active sessions before cancelling tasksfortransportinlist(self._server_instances.values()):
try:
awaittransport.terminate()
exceptException:
logger.debug("Error terminating transport during shutdown")
tg.cancel_scope.cancel()
self._task_group=Noneself._server_instances.clear()
File: src/mcp/server/streamable_http.py — In terminate(), also close all _sse_stream_writers so that active EventSourceResponse instances complete gracefully:
asyncdefterminate(self) ->None:
ifself._terminated:
returnself._terminated=Truelogger.info(f"Terminating session: {self.mcp_session_id}")
# Close all SSE stream writers to allow EventSourceResponse to completeforwriterinlist(self._sse_stream_writers.values()):
writer.close()
self._sse_stream_writers.clear()
# Close all request streamsrequest_stream_keys=list(self._request_streams.keys())
forkeyinrequest_stream_keys:
awaitself._clean_up_memory_streams(key)
self._request_streams.clear()
# ... rest of stream cleanup
Edge Cases
Timing: terminate() must complete before tg.cancel_scope.cancel() — if it's done concurrently within the cancelled scope, the terminate calls themselves get cancelled. Using anyio.move_on_after() as a safety net around each terminate call could help avoid a hung shutdown.
Stateless mode: Not affected since stateless transports are terminated per-request already (line 211).
Concurrent requests: A request arriving during the shutdown window (after terminate starts but before cancel) could hit a partially-terminated transport. The existing self._terminated check at the top of handle_request() (line 389) should handle this, returning 404.
close() vs aclose(): The _sse_stream_writers use MemoryObjectSendStream — calling .close() (sync) is sufficient and avoids awaiting inside the shutdown path, which is more robust.
I would like to work on this. I plan to keep the scope focused on graceful shutdown for active Streamable HTTP sessions: make transport termination close SSE stream writers, have the session manager terminate active transports before cancelling the task group, and add regression coverage for shutdown cleanup behavior. I will avoid changing stateless request handling or the normal request path beyond the shutdown cleanup path.
I'd like to work on this. My plan: (1) make erminate() in streamable_http.py close _sse_stream_writers so EventSourceResponse completes gracefully, (2) have the session manager in streamable_http_manager.py terminate active transports before cancelling the task group on shutdown. Kept scoped to the shutdown cleanup path — no changes to stateless or normal request handling. Let me know if that sounds right.
Activity
fix: gracefully terminate active SSE connections during shutdown
weiguangli-io commentedon Mar 9, 2026
Root Cause Analysis
The issue is in
StreamableHTTPSessionManager.run()(src/mcp/server/streamable_http_manager.py, lines 129-137). On shutdown, thefinallyblock does:This abruptly cancels all tasks in the task group without first gracefully terminating active sessions. The
EventSourceResponsecoroutines (running inside the cancelled tasks) are killed mid-stream before they can send a proper HTTP response completion, which triggers Uvicorn'sASGI callable returned without completing responseerror.There are actually two compounding problems:
run()never callsterminate()on active transports before cancelling the task group. It just clears the dict.terminate()itself doesn't close_sse_stream_writers. It only closes_request_streamsvia_clean_up_memory_streams(), but the SSE stream writers (which keepEventSourceResponsealive) are stored separately inself._sse_stream_writersand are never touched duringterminate(). Closing the request streams alone doesn't cause the SSE response to complete gracefully — theEventSourceResponsekeeps waiting on itssse_stream_reader.Proposed Fix
File:
src/mcp/server/streamable_http_manager.py— In therun()method'sfinallyblock, iterate over all active transports and callterminate()on each before cancelling the task group:File:
src/mcp/server/streamable_http.py— Interminate(), also close all_sse_stream_writersso that activeEventSourceResponseinstances complete gracefully:Edge Cases
terminate()must complete beforetg.cancel_scope.cancel()— if it's done concurrently within the cancelled scope, the terminate calls themselves get cancelled. Usinganyio.move_on_after()as a safety net around each terminate call could help avoid a hung shutdown.self._terminatedcheck at the top ofhandle_request()(line 389) should handle this, returning 404.close()vsaclose(): The_sse_stream_writersuseMemoryObjectSendStream— calling.close()(sync) is sufficient and avoids awaiting inside the shutdown path, which is more robust.fix: terminate active StreamableHTTP sessions during shutdown (modelc…
Hackerchen716 commentedon May 29, 2026
I would like to work on this. I plan to keep the scope focused on graceful shutdown for active Streamable HTTP sessions: make transport termination close SSE stream writers, have the session manager terminate active transports before cancelling the task group, and add regression coverage for shutdown cleanup behavior. I will avoid changing stateless request handling or the normal request path beyond the shutdown cleanup path.
shivamsingh-007 commentedon Jul 12, 2026
I'd like to work on this. My plan: (1) make erminate() in streamable_http.py close _sse_stream_writers so EventSourceResponse completes gracefully, (2) have the session manager in streamable_http_manager.py terminate active transports before cancelling the task group on shutdown. Kept scoped to the shutdown cleanup path — no changes to stateless or normal request handling. Let me know if that sounds right.
style: satisfy markdownlint on modelcontextprotocol#2150 notes
test(server): cover terminate cleanup of orphaned request streams