From 1a53bc70126accc4539a6582bf88760e63176aab Mon Sep 17 00:00:00 2001 From: Radhakrishnan Pachyappan Date: Sun, 9 Aug 2026 00:38:29 +0530 Subject: [PATCH] fix: log exceptions in _handle_call_tool at ERROR level Resource and prompt handlers already call logger.exception() when an error occurs. Tool calls were silently swallowing exceptions into CallToolResult(is_error=True) with no server-side log, making it impossible for operators to diagnose tool failures without client-side visibility. Adds logger.exception() before the is_error return in _handle_call_tool, matching the pattern used in _handle_read_resource and _handle_get_prompt. Fixes #3266 Signed-off-by: Radhakrishnan Panchayappan Signed-off-by: Radhakrishnan Pachyappan --- src/mcp/server/mcpserver/server.py | 1 + tests/interaction/mcpserver/test_tools.py | 26 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index bc79c44a36..0e2e2eca90 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -421,6 +421,7 @@ async def _handle_call_tool( except MCPError: raise except Exception as e: + logger.exception(f"Error calling tool {params.name}") return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True) async def _handle_list_resources( diff --git a/tests/interaction/mcpserver/test_tools.py b/tests/interaction/mcpserver/test_tools.py index a6418ac9c5..3ff7e2af85 100644 --- a/tests/interaction/mcpserver/test_tools.py +++ b/tests/interaction/mcpserver/test_tools.py @@ -119,6 +119,32 @@ def flux() -> str: ) +@requirement("mcpserver:tool:handler-throws") +async def test_call_tool_exception_is_logged_server_side( + connect: Connect, caplog: pytest.LogCaptureFixture +) -> None: + """A tool exception is logged at ERROR level on the server so operators can diagnose failures. + + The is_error result reaches the client, but without server-side logging the root cause is + invisible in server logs. This test asserts the log record is emitted alongside the result. + """ + mcp = MCPServer("errors") + + @mcp.tool() + def boom() -> str: + raise RuntimeError("something went wrong") + + with caplog.at_level(logging.ERROR, logger="mcp.server.mcpserver.server"): + async with connect(mcp) as client: + result = await client.call_tool("boom", {}) + + assert result.is_error is True + assert any( + rec.levelno == logging.ERROR and "boom" in rec.message + for rec in caplog.records + ) + + @requirement("mcpserver:tool:unknown-name") async def test_call_tool_unknown_name_returns_error_result(connect: Connect, unstamped: Unstamp) -> None: """Calling a tool name that was never registered is reported as an is_error result.