Skip to content

fix: gracefully terminate active Streamable HTTP sessions during shutdown (#2150)#2746

Open
kaushik701 wants to merge 2 commits into
modelcontextprotocol:mainfrom
kaushik701:fix/graceful-shutdown-streamable-http
Open

fix: gracefully terminate active Streamable HTTP sessions during shutdown (#2150)#2746
kaushik701 wants to merge 2 commits into
modelcontextprotocol:mainfrom
kaushik701:fix/graceful-shutdown-streamable-http

Conversation

@kaushik701
Copy link
Copy Markdown

Fixes #2150

Problem

When shutting down a FastMCP/MCP server (CTRL+C) while clients have active streaming connections, Uvicorn logs:

ERROR: ASGI callable returned without completing response.

There were two compounding issues:

  1. StreamableHTTPSessionManager.run() never called terminate() on active transports before cancelling the task group — it just cleared the dict, leaving EventSourceResponse coroutines killed mid-stream.
  2. terminate() itself didn't close _sse_stream_writers. It only cleaned up _request_streams, but the SSE stream writers (which keep EventSourceResponse alive) were stored separately and never touched, so EventSourceResponse kept waiting on its reader.

Fix

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 tasks
    for transport in list(self._server_instances.values()):
        try:
            await transport.terminate()
        except Exception:
            logger.debug("Error terminating transport during shutdown", exc_info=True)
    # Cancel task group to stop all spawned tasks
    tg.cancel_scope.cancel()
    ...

src/mcp/server/streamable_http.py — In terminate(), close all _sse_stream_writers before cleaning up request streams, so active EventSourceResponse instances can complete gracefully:

# Close all SSE stream writers to allow EventSourceResponse to complete gracefully
for writer in list(self._sse_stream_writers.values()):
    writer.close()
self._sse_stream_writers.clear()

Notes

  • Stateless mode is not affected (transports are already terminated per-request).
    • The _terminated flag ensures that any concurrent requests arriving during shutdown get a 404, not a partial response.
    • Using sync .close() on MemoryObjectSendStream (rather than await aclose()) is safe and avoids awaiting inside the shutdown path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Active Streamable HTTP sessions are not terminated during shutdown

1 participant