From fa45f05b02e7fd1e53f05893e16d452146d016c5 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 09:46:45 +0100 Subject: [PATCH 1/2] fix: catch ClosedResourceError in _handle_message error recovery path When a client disconnects during a request, _handle_message receives the exception and tries to send_log_message() back to notify the client. Since the write stream is already closed, this raises ClosedResourceError (or BrokenResourceError), which is unhandled in the TaskGroup and crashes the stateless session with an ExceptionGroup. Wrap the send_log_message() call in _handle_message's Exception branch with a try/except for ClosedResourceError and BrokenResourceError, since failing to notify a disconnected client is expected and harmless. Fixes #2064 --- src/mcp/server/lowlevel/server.py | 15 +++-- .../test_lowlevel_exception_handling.py | 58 +++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index aee644040..b843cc645 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -417,11 +417,16 @@ async def _handle_message( ) case Exception(): logger.error(f"Received exception from stream: {message}") - await session.send_log_message( - level="error", - data="Internal Server Error", - logger="mcp.server.exception_handler", - ) + try: + await session.send_log_message( + level="error", + data="Internal Server Error", + logger="mcp.server.exception_handler", + ) + except (anyio.ClosedResourceError, anyio.BrokenResourceError): + logger.debug( + "Could not send error log to client (stream already closed)" + ) if raise_exceptions: raise message case _: diff --git a/tests/server/test_lowlevel_exception_handling.py b/tests/server/test_lowlevel_exception_handling.py index 848b35b29..3da4ec1ea 100644 --- a/tests/server/test_lowlevel_exception_handling.py +++ b/tests/server/test_lowlevel_exception_handling.py @@ -72,3 +72,61 @@ async def test_normal_message_handling_not_affected(): # Verify _handle_request was called server._handle_request.assert_called_once() + + +@pytest.mark.anyio +async def test_exception_handling_survives_closed_write_stream(): + """When the client disconnects, send_log_message may raise ClosedResourceError. + + The server should catch it gracefully instead of crashing the session. + Regression test for GH-2064. + """ + import anyio + + server = Server("test-server") + session = Mock(spec=ServerSession) + session.send_log_message = AsyncMock(side_effect=anyio.ClosedResourceError) + + test_exception = RuntimeError("client disconnected") + + # Should NOT raise — the ClosedResourceError from send_log_message is caught + await server._handle_message(test_exception, session, {}, raise_exceptions=False) + + session.send_log_message.assert_called_once() + + +@pytest.mark.anyio +async def test_exception_handling_survives_broken_write_stream(): + """When the write stream is broken, send_log_message may raise BrokenResourceError. + + The server should catch it gracefully. + Regression test for GH-2064. + """ + import anyio + + server = Server("test-server") + session = Mock(spec=ServerSession) + session.send_log_message = AsyncMock(side_effect=anyio.BrokenResourceError) + + test_exception = RuntimeError("broken pipe") + + await server._handle_message(test_exception, session, {}, raise_exceptions=False) + + session.send_log_message.assert_called_once() + + +@pytest.mark.anyio +async def test_exception_handling_raises_when_configured_despite_closed_stream(): + """With raise_exceptions=True, the original error is still re-raised + even when send_log_message fails with ClosedResourceError. + """ + import anyio + + server = Server("test-server") + session = Mock(spec=ServerSession) + session.send_log_message = AsyncMock(side_effect=anyio.ClosedResourceError) + + test_exception = RuntimeError("original error") + + with pytest.raises(RuntimeError, match="original error"): + await server._handle_message(test_exception, session, {}, raise_exceptions=True) From 6e63d96c49f65b5c753a250096d119d29e3a779f Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 14:05:49 +0100 Subject: [PATCH 2/2] style: apply ruff format --- src/mcp/server/lowlevel/server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index b843cc645..2c0427174 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -424,9 +424,7 @@ async def _handle_message( logger="mcp.server.exception_handler", ) except (anyio.ClosedResourceError, anyio.BrokenResourceError): - logger.debug( - "Could not send error log to client (stream already closed)" - ) + logger.debug("Could not send error log to client (stream already closed)") if raise_exceptions: raise message case _: