Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/mcp/server/lowlevel/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,11 +417,14 @@ 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.warning("Could not send error log to client: write stream already closed")
if raise_exceptions:
raise message
case _:
Expand Down
46 changes: 46 additions & 0 deletions tests/server/test_lowlevel_exception_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,49 @@ 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_tolerates_closed_write_stream():
"""send_log_message should not crash when the client has already disconnected."""
import anyio

server = Server("test-server")
session = Mock(spec=ServerSession)
session.send_log_message = AsyncMock(side_effect=anyio.ClosedResourceError)

test_exception = RuntimeError("client went away")

# Must not raise — the ClosedResourceError should be caught internally
await server._handle_message(test_exception, session, {}, raise_exceptions=False)


@pytest.mark.anyio
async def test_exception_handling_tolerates_broken_write_stream():
"""send_log_message should not crash when the write stream is broken."""
import anyio

server = Server("test-server")
session = Mock(spec=ServerSession)
session.send_log_message = AsyncMock(side_effect=anyio.BrokenResourceError)

test_exception = RuntimeError("client went away")

# Must not raise — the BrokenResourceError should be caught internally
await server._handle_message(test_exception, session, {}, raise_exceptions=False)


@pytest.mark.anyio
async def test_exception_handling_closed_stream_with_raise_exceptions():
"""Even with raise_exceptions=True, ClosedResourceError from log should be tolerated."""
import anyio

server = Server("test-server")
session = Mock(spec=ServerSession)
session.send_log_message = AsyncMock(side_effect=anyio.ClosedResourceError)

test_exception = RuntimeError("client went away")

# raise_exceptions re-raises the *original* exception, not the ClosedResourceError
with pytest.raises(RuntimeError, match="client went away"):
await server._handle_message(test_exception, session, {}, raise_exceptions=True)