diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index f2f4407ce..bfe3abcd7 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -771,6 +771,11 @@ async def terminate(self) -> None: self._terminated = True logger.info(f"Terminating session: {self.mcp_session_id}") + # 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() + # We need a copy of the keys to avoid modification during iteration request_stream_keys = list(self._request_streams.keys()) diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index 81350a8f2..b9a27a41f 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -133,6 +133,12 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: yield # Let the application run 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() self._task_group = None