From 722ff236487dfff8d027187fe49ca9b7c383334e Mon Sep 17 00:00:00 2001 From: Brian Soby Date: Thu, 29 May 2025 11:25:25 -0600 Subject: [PATCH 01/10] Fix: Prevent session manager shutdown on individual session crash --- src/mcp/server/streamable_http_manager.py | 49 +++++-- tests/server/test_streamable_http_manager.py | 132 ++++++++++++++++++- 2 files changed, 167 insertions(+), 14 deletions(-) diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index 41b807388..b3fd0276f 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -52,7 +52,6 @@ class StreamableHTTPSessionManager: json_response: Whether to use JSON responses instead of SSE streams stateless: If True, creates a completely fresh transport for each request with no session tracking or state persistence between requests. - """ def __init__( @@ -173,12 +172,15 @@ async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STA async with http_transport.connect() as streams: read_stream, write_stream = streams task_status.started() - await self.app.run( - read_stream, - write_stream, - self.app.create_initialization_options(), - stateless=True, - ) + try: + await self.app.run( + read_stream, + write_stream, + self.app.create_initialization_options(), + stateless=True, + ) + except Exception as e: + logger.error(f"Stateless session crashed: {e}", exc_info=True) # Assert task group is not None for type checking assert self._task_group is not None @@ -233,12 +235,33 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE async with http_transport.connect() as streams: read_stream, write_stream = streams task_status.started() - await self.app.run( - read_stream, - write_stream, - self.app.create_initialization_options(), - stateless=False, # Stateful mode - ) + try: + await self.app.run( + read_stream, + write_stream, + self.app.create_initialization_options(), + stateless=False, # Stateful mode + ) + except Exception as e: + logger.error( + f"Session {http_transport.mcp_session_id} crashed: {e}", + exc_info=True, + ) + finally: + # Cleanup logic + if ( + http_transport.mcp_session_id + and http_transport.mcp_session_id + in self._server_instances + ): + logger.info( + "Cleaning up crashed/terminated session " + f"{http_transport.mcp_session_id} from " + "active instances." + ) + del self._server_instances[ + http_transport.mcp_session_id + ] # Assert task group is not None for type checking assert self._task_group is not None diff --git a/tests/server/test_streamable_http_manager.py b/tests/server/test_streamable_http_manager.py index 65828b63b..63396d88d 100644 --- a/tests/server/test_streamable_http_manager.py +++ b/tests/server/test_streamable_http_manager.py @@ -1,9 +1,12 @@ """Tests for StreamableHTTPSessionManager.""" +from unittest.mock import AsyncMock + import anyio import pytest from mcp.server.lowlevel import Server +from mcp.server.streamable_http import MCP_SESSION_ID_HEADER from mcp.server.streamable_http_manager import StreamableHTTPSessionManager @@ -70,4 +73,131 @@ async def send(message): with pytest.raises(RuntimeError) as excinfo: await manager.handle_request(scope, receive, send) - assert "Task group is not initialized. Make sure to use run()." in str(excinfo.value) + assert "Task group is not initialized. Make sure to use run()." in str( + excinfo.value + ) + + +class TestException(Exception): + __test__ = False # Prevent pytest from collecting this as a test class + pass + + +@pytest.fixture +async def running_manager(): + app = Server("test-cleanup-server") + # It's important that the app instance used by the manager is the one we can patch + manager = StreamableHTTPSessionManager(app=app) + async with manager.run(): + # Patch app.run here if it's simpler, or patch it within the test + yield manager, app + + +@pytest.mark.anyio +async def test_stateful_session_cleanup_on_graceful_exit(running_manager): + manager, app = running_manager + + mock_mcp_run = AsyncMock(return_value=None) + # This will be called by StreamableHTTPSessionManager's run_server -> self.app.run + app.run = mock_mcp_run + + sent_messages = [] + + async def mock_send(message): + sent_messages.append(message) + + scope = {"type": "http", "method": "POST", "path": "/mcp", "headers": []} + + async def mock_receive(): + return {"type": "http.request", "body": b"", "more_body": False} + + # Trigger session creation + await manager.handle_request(scope, mock_receive, mock_send) + + # Extract session ID from response headers + session_id = None + for msg in sent_messages: + if msg["type"] == "http.response.start": + for header_name, header_value in msg.get("headers", []): + if header_name.decode().lower() == MCP_SESSION_ID_HEADER.lower(): + session_id = header_value.decode() + break + if session_id: # Break outer loop if session_id is found + break + + assert session_id is not None, "Session ID not found in response headers" + + # Ensure MCPServer.run was called + mock_mcp_run.assert_called_once() + + # At this point, mock_mcp_run has completed, and the finally block in + # StreamableHTTPSessionManager's run_server should have executed. + + # To ensure the task spawned by handle_request finishes and cleanup occurs: + # Give other tasks a chance to run. This is important for the finally block. + await anyio.sleep(0.01) + + assert ( + session_id not in manager._server_instances + ), "Session ID should be removed from _server_instances after graceful exit" + assert ( + not manager._server_instances + ), "No sessions should be tracked after the only session exits gracefully" + + +@pytest.mark.anyio +async def test_stateful_session_cleanup_on_exception(running_manager): + manager, app = running_manager + + mock_mcp_run = AsyncMock(side_effect=TestException("Simulated crash")) + app.run = mock_mcp_run + + sent_messages = [] + + async def mock_send(message): + sent_messages.append(message) + # If an exception occurs, the transport might try to send an error response + # For this test, we mostly care that the session is established enough + # to get an ID + if message["type"] == "http.response.start" and message["status"] >= 500: + pass # Expected if TestException propagates that far up the transport + + scope = {"type": "http", "method": "POST", "path": "/mcp", "headers": []} + + async def mock_receive(): + return {"type": "http.request", "body": b"", "more_body": False} + + # It's possible handle_request itself might raise an error if the TestException + # isn't caught by the transport layer before propagating. + # The key is that the session manager's internal task for MCPServer.run + # encounters the exception. + try: + await manager.handle_request(scope, mock_receive, mock_send) + except TestException: + # This might be caught here if not handled by StreamableHTTPServerTransport's + # error handling + pass + + session_id = None + for msg in sent_messages: + if msg["type"] == "http.response.start": + for header_name, header_value in msg.get("headers", []): + if header_name.decode().lower() == MCP_SESSION_ID_HEADER.lower(): + session_id = header_value.decode() + break + if session_id: # Break outer loop if session_id is found + break + + assert session_id is not None, "Session ID not found in response headers" + + mock_mcp_run.assert_called_once() + + # Give other tasks a chance to run to ensure the finally block executes + await anyio.sleep(0.01) + + assert ( + session_id not in manager._server_instances + ), "Session ID should be removed from _server_instances after an exception" + assert ( + not manager._server_instances + ), "No sessions should be tracked after the only session crashes" From 415fa191ea6144f48501bd7582aca6f1f68a7747 Mon Sep 17 00:00:00 2001 From: Brian Soby Date: Thu, 29 May 2025 11:56:00 -0600 Subject: [PATCH 02/10] only remove error sessions and not those that have not been terminated --- src/mcp/server/streamable_http_manager.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index b3fd0276f..77e8f44a3 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -248,14 +248,18 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE exc_info=True, ) finally: - # Cleanup logic + # Only remove from instances if not terminated if ( http_transport.mcp_session_id and http_transport.mcp_session_id in self._server_instances + and not ( + hasattr(http_transport, "_terminated") + and http_transport._terminated + ) ): logger.info( - "Cleaning up crashed/terminated session " + "Cleaning up crashed session " f"{http_transport.mcp_session_id} from " "active instances." ) From be47a85821b307fa4ed87cec2e7b8874eded535b Mon Sep 17 00:00:00 2001 From: Brian Soby Date: Thu, 29 May 2025 13:41:09 -0600 Subject: [PATCH 03/10] lint --- src/mcp/server/streamable_http_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index 77e8f44a3..1d91de8b1 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -255,7 +255,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE in self._server_instances and not ( hasattr(http_transport, "_terminated") - and http_transport._terminated + and http_transport._terminated # pyright: ignore ) ): logger.info( From 0d477c4000fb6f63c4d55f2a5d9e8146475980dc Mon Sep 17 00:00:00 2001 From: Brian Soby Date: Mon, 2 Jun 2025 16:02:13 -0600 Subject: [PATCH 04/10] step down logging from error to warning --- src/mcp/server/streamable_http_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index 1d91de8b1..0bf194542 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -180,7 +180,7 @@ async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STA stateless=True, ) except Exception as e: - logger.error(f"Stateless session crashed: {e}", exc_info=True) + logger.warning(f"Stateless session crashed: {e}", exc_info=True) # Assert task group is not None for type checking assert self._task_group is not None @@ -243,7 +243,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE stateless=False, # Stateful mode ) except Exception as e: - logger.error( + logger.warning( f"Session {http_transport.mcp_session_id} crashed: {e}", exc_info=True, ) From 68fc9ca7ce39220484d4d2c7142996830e4507c1 Mon Sep 17 00:00:00 2001 From: Brian Soby Date: Wed, 4 Jun 2025 13:19:19 -0600 Subject: [PATCH 05/10] step down logging for other places subject to network closures/disconnects --- src/mcp/server/streamable_http.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index 1b37acd43..b63288ca0 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -452,7 +452,7 @@ async def sse_writer(): ): break except Exception as e: - logger.exception(f"Error in SSE writer: {e}") + logger.warning(f"Error in SSE writer: {e}", exc_info=True) finally: logger.debug("Closing SSE writer") await self._clean_up_memory_streams(request_id) @@ -482,13 +482,13 @@ async def sse_writer(): session_message = SessionMessage(message, metadata=metadata) await writer.send(session_message) except Exception: - logger.exception("SSE response error") + logger.warning("SSE response error", exc_info=True) await sse_stream_writer.aclose() await sse_stream_reader.aclose() await self._clean_up_memory_streams(request_id) except Exception as err: - logger.exception("Error handling POST request") + logger.warning("Error handling POST request", exc_info=True) response = self._create_error_response( f"Error handling POST request: {err}", HTTPStatus.INTERNAL_SERVER_ERROR, @@ -570,7 +570,7 @@ async def standalone_sse_writer(): event_data = self._create_event_data(event_message) await sse_stream_writer.send(event_data) except Exception as e: - logger.exception(f"Error in standalone SSE writer: {e}") + logger.warning(f"Error in standalone SSE writer: {e}", exc_info=True) finally: logger.debug("Closing standalone SSE writer") await self._clean_up_memory_streams(GET_STREAM_KEY) @@ -586,7 +586,7 @@ async def standalone_sse_writer(): # This will send headers immediately and establish the SSE connection await response(request.scope, request.receive, send) except Exception as e: - logger.exception(f"Error in standalone SSE response: {e}") + logger.warning(f"Error in standalone SSE response: {e}", exc_info=True) await sse_stream_writer.aclose() await sse_stream_reader.aclose() await self._clean_up_memory_streams(GET_STREAM_KEY) From 615336a5bf4b6018506c2617d7c21b55d721de84 Mon Sep 17 00:00:00 2001 From: Brian Soby Date: Sun, 15 Jun 2025 17:37:55 -0600 Subject: [PATCH 06/10] ruff --- src/mcp/server/streamable_http_manager.py | 10 +++----- tests/server/test_streamable_http_manager.py | 24 ++++++++------------ 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index 0bf194542..a939b0af0 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -251,11 +251,9 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE # Only remove from instances if not terminated if ( http_transport.mcp_session_id - and http_transport.mcp_session_id - in self._server_instances + and http_transport.mcp_session_id in self._server_instances and not ( - hasattr(http_transport, "_terminated") - and http_transport._terminated # pyright: ignore + hasattr(http_transport, "_terminated") and http_transport._terminated # pyright: ignore ) ): logger.info( @@ -263,9 +261,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE f"{http_transport.mcp_session_id} from " "active instances." ) - del self._server_instances[ - http_transport.mcp_session_id - ] + del self._server_instances[http_transport.mcp_session_id] # Assert task group is not None for type checking assert self._task_group is not None diff --git a/tests/server/test_streamable_http_manager.py b/tests/server/test_streamable_http_manager.py index 63396d88d..c7e25ad05 100644 --- a/tests/server/test_streamable_http_manager.py +++ b/tests/server/test_streamable_http_manager.py @@ -73,9 +73,7 @@ async def send(message): with pytest.raises(RuntimeError) as excinfo: await manager.handle_request(scope, receive, send) - assert "Task group is not initialized. Make sure to use run()." in str( - excinfo.value - ) + assert "Task group is not initialized. Make sure to use run()." in str(excinfo.value) class TestException(Exception): @@ -137,12 +135,10 @@ async def mock_receive(): # Give other tasks a chance to run. This is important for the finally block. await anyio.sleep(0.01) - assert ( - session_id not in manager._server_instances - ), "Session ID should be removed from _server_instances after graceful exit" - assert ( - not manager._server_instances - ), "No sessions should be tracked after the only session exits gracefully" + assert session_id not in manager._server_instances, ( + "Session ID should be removed from _server_instances after graceful exit" + ) + assert not manager._server_instances, "No sessions should be tracked after the only session exits gracefully" @pytest.mark.anyio @@ -195,9 +191,7 @@ async def mock_receive(): # Give other tasks a chance to run to ensure the finally block executes await anyio.sleep(0.01) - assert ( - session_id not in manager._server_instances - ), "Session ID should be removed from _server_instances after an exception" - assert ( - not manager._server_instances - ), "No sessions should be tracked after the only session crashes" + assert session_id not in manager._server_instances, ( + "Session ID should be removed from _server_instances after an exception" + ) + assert not manager._server_instances, "No sessions should be tracked after the only session crashes" From 83593015fa316ca01fc88f569013d614ac9168d7 Mon Sep 17 00:00:00 2001 From: Brian Soby Date: Sun, 15 Jun 2025 17:41:26 -0600 Subject: [PATCH 07/10] linter differences of opinion --- tests/server/test_streamable_http_manager.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/server/test_streamable_http_manager.py b/tests/server/test_streamable_http_manager.py index c7e25ad05..caef08c2f 100644 --- a/tests/server/test_streamable_http_manager.py +++ b/tests/server/test_streamable_http_manager.py @@ -135,9 +135,9 @@ async def mock_receive(): # Give other tasks a chance to run. This is important for the finally block. await anyio.sleep(0.01) - assert session_id not in manager._server_instances, ( - "Session ID should be removed from _server_instances after graceful exit" - ) + assert ( + session_id not in manager._server_instances + ), "Session ID should be removed from _server_instances after graceful exit" assert not manager._server_instances, "No sessions should be tracked after the only session exits gracefully" @@ -191,7 +191,7 @@ async def mock_receive(): # Give other tasks a chance to run to ensure the finally block executes await anyio.sleep(0.01) - assert session_id not in manager._server_instances, ( - "Session ID should be removed from _server_instances after an exception" - ) + assert ( + session_id not in manager._server_instances + ), "Session ID should be removed from _server_instances after an exception" assert not manager._server_instances, "No sessions should be tracked after the only session crashes" From ec245fdff96d5e19c1e9b77cf25557a0098f700e Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 8 Jul 2025 17:54:45 +0100 Subject: [PATCH 08/10] Apply review feedback - Add Content-Type headers to test scopes to fix CI - Add public is_terminated property to avoid accessing private _terminated - Remove unnecessary try-catch from test - Change logging from warning to error for session crashes --- src/mcp/server/streamable_http.py | 15 +++++--- src/mcp/server/streamable_http_manager.py | 8 ++--- tests/server/test_streamable_http_manager.py | 38 ++++++++++---------- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index b63288ca0..6ff65d179 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -173,6 +173,11 @@ def __init__( ] = {} self._terminated = False + @property + def is_terminated(self) -> bool: + """Check if this transport has been explicitly terminated.""" + return self._terminated + def _create_error_response( self, error_message: str, @@ -452,7 +457,7 @@ async def sse_writer(): ): break except Exception as e: - logger.warning(f"Error in SSE writer: {e}", exc_info=True) + logger.exception(f"Error in SSE writer: {e}") finally: logger.debug("Closing SSE writer") await self._clean_up_memory_streams(request_id) @@ -482,13 +487,13 @@ async def sse_writer(): session_message = SessionMessage(message, metadata=metadata) await writer.send(session_message) except Exception: - logger.warning("SSE response error", exc_info=True) + logger.exception("SSE response error") await sse_stream_writer.aclose() await sse_stream_reader.aclose() await self._clean_up_memory_streams(request_id) except Exception as err: - logger.warning("Error handling POST request", exc_info=True) + logger.exception("Error handling POST request") response = self._create_error_response( f"Error handling POST request: {err}", HTTPStatus.INTERNAL_SERVER_ERROR, @@ -570,7 +575,7 @@ async def standalone_sse_writer(): event_data = self._create_event_data(event_message) await sse_stream_writer.send(event_data) except Exception as e: - logger.warning(f"Error in standalone SSE writer: {e}", exc_info=True) + logger.exception(f"Error in standalone SSE writer: {e}") finally: logger.debug("Closing standalone SSE writer") await self._clean_up_memory_streams(GET_STREAM_KEY) @@ -586,7 +591,7 @@ async def standalone_sse_writer(): # This will send headers immediately and establish the SSE connection await response(request.scope, request.receive, send) except Exception as e: - logger.warning(f"Error in standalone SSE response: {e}", exc_info=True) + logger.exception(f"Error in standalone SSE response: {e}") await sse_stream_writer.aclose() await sse_stream_reader.aclose() await self._clean_up_memory_streams(GET_STREAM_KEY) diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index a939b0af0..7211e4ff4 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -180,7 +180,7 @@ async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STA stateless=True, ) except Exception as e: - logger.warning(f"Stateless session crashed: {e}", exc_info=True) + logger.error(f"Stateless session crashed: {e}", exc_info=True) # Assert task group is not None for type checking assert self._task_group is not None @@ -243,7 +243,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE stateless=False, # Stateful mode ) except Exception as e: - logger.warning( + logger.error( f"Session {http_transport.mcp_session_id} crashed: {e}", exc_info=True, ) @@ -252,9 +252,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE if ( http_transport.mcp_session_id and http_transport.mcp_session_id in self._server_instances - and not ( - hasattr(http_transport, "_terminated") and http_transport._terminated # pyright: ignore - ) + and not http_transport.is_terminated ): logger.info( "Cleaning up crashed session " diff --git a/tests/server/test_streamable_http_manager.py b/tests/server/test_streamable_http_manager.py index caef08c2f..a406adfa3 100644 --- a/tests/server/test_streamable_http_manager.py +++ b/tests/server/test_streamable_http_manager.py @@ -104,7 +104,12 @@ async def test_stateful_session_cleanup_on_graceful_exit(running_manager): async def mock_send(message): sent_messages.append(message) - scope = {"type": "http", "method": "POST", "path": "/mcp", "headers": []} + scope = { + "type": "http", + "method": "POST", + "path": "/mcp", + "headers": [(b"content-type", b"application/json")], + } async def mock_receive(): return {"type": "http.request", "body": b"", "more_body": False} @@ -135,9 +140,9 @@ async def mock_receive(): # Give other tasks a chance to run. This is important for the finally block. await anyio.sleep(0.01) - assert ( - session_id not in manager._server_instances - ), "Session ID should be removed from _server_instances after graceful exit" + assert session_id not in manager._server_instances, ( + "Session ID should be removed from _server_instances after graceful exit" + ) assert not manager._server_instances, "No sessions should be tracked after the only session exits gracefully" @@ -158,21 +163,18 @@ async def mock_send(message): if message["type"] == "http.response.start" and message["status"] >= 500: pass # Expected if TestException propagates that far up the transport - scope = {"type": "http", "method": "POST", "path": "/mcp", "headers": []} + scope = { + "type": "http", + "method": "POST", + "path": "/mcp", + "headers": [(b"content-type", b"application/json")], + } async def mock_receive(): return {"type": "http.request", "body": b"", "more_body": False} - # It's possible handle_request itself might raise an error if the TestException - # isn't caught by the transport layer before propagating. - # The key is that the session manager's internal task for MCPServer.run - # encounters the exception. - try: - await manager.handle_request(scope, mock_receive, mock_send) - except TestException: - # This might be caught here if not handled by StreamableHTTPServerTransport's - # error handling - pass + # Trigger session creation + await manager.handle_request(scope, mock_receive, mock_send) session_id = None for msg in sent_messages: @@ -191,7 +193,7 @@ async def mock_receive(): # Give other tasks a chance to run to ensure the finally block executes await anyio.sleep(0.01) - assert ( - session_id not in manager._server_instances - ), "Session ID should be removed from _server_instances after an exception" + assert session_id not in manager._server_instances, ( + "Session ID should be removed from _server_instances after an exception" + ) assert not manager._server_instances, "No sessions should be tracked after the only session crashes" From 0cb1f05ff1cccfdc8276257c7312e5392d140a80 Mon Sep 17 00:00:00 2001 From: Brian Soby Date: Wed, 9 Jul 2025 08:17:27 -0600 Subject: [PATCH 09/10] Update src/mcp/server/streamable_http_manager.py Co-authored-by: Inna Harper --- src/mcp/server/streamable_http_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index 7211e4ff4..46183e0ec 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -180,7 +180,7 @@ async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STA stateless=True, ) except Exception as e: - logger.error(f"Stateless session crashed: {e}", exc_info=True) + logger.exception("Stateless session crashed") # Assert task group is not None for type checking assert self._task_group is not None From 46d0452e2923dadb82ab99a26105bf20ddf26d06 Mon Sep 17 00:00:00 2001 From: Brian Soby Date: Wed, 9 Jul 2025 08:26:04 -0600 Subject: [PATCH 10/10] ruff --- src/mcp/server/streamable_http_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index 46183e0ec..e953ca39f 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -179,7 +179,7 @@ async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STA self.app.create_initialization_options(), stateless=True, ) - except Exception as e: + except Exception: logger.exception("Stateless session crashed") # Assert task group is not None for type checking