Skip to content
Open
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
1 change: 1 addition & 0 deletions src/mcp/server/mcpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
26 changes: 26 additions & 0 deletions tests/interaction/mcpserver/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading