diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index c241e831a..5a71cb3f5 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -784,6 +784,12 @@ async def terminate(self) -> None: self._terminated = True logger.info(f"Terminating session: {self.mcp_session_id}") + # Close all SSE stream writers so that active EventSourceResponse + # coroutines complete gracefully instead of being cancelled mid-stream. + for writer in list(self._sse_stream_writers.values()): # pragma: no cover + 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 8a7b765e8..3be3a124e 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -130,6 +130,14 @@ 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 so that EventSourceResponse coroutines can complete + # and Uvicorn does not log ASGI-incomplete-response errors. + for transport in list(self._server_instances.values()): # pragma: no cover + try: + await transport.terminate() + except Exception: # pragma: no cover + logger.exception("Error terminating transport during shutdown") # Cancel task group to stop all spawned tasks tg.cancel_scope.cancel() self._task_group = None