Skip to content

Commit c215aef

Browse files
committed
fix: propagate MCPError from tool handlers as JSON-RPC error
Previously, only UrlElicitationRequiredError was re-raised; any other MCPError raised inside a tool function was caught by the bare 'except Exception' clause and wrapped into a ToolError, which then surfaced as a CallToolResult(isError=True) instead of a structured JSON-RPC error response. Since UrlElicitationRequiredError is a subclass of MCPError, broadening the guard to 'except MCPError' covers all cases including the original one. Fixes #2770
1 parent 7267818 commit c215aef

2 files changed

Lines changed: 19 additions & 4 deletions

File tree

src/mcp/server/mcpserver/tools/base.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
1111
from mcp.server.mcpserver.utilities.func_metadata import FuncMetadata, func_metadata
1212
from mcp.shared._callable_inspection import is_async_callable
13-
from mcp.shared.exceptions import UrlElicitationRequiredError
13+
from mcp.shared.exceptions import MCPError
1414
from mcp.shared.tool_name_validation import validate_and_warn_tool_name
1515
from mcp.types import Icon, ToolAnnotations
1616

@@ -111,9 +111,7 @@ async def run(
111111
result = self.fn_metadata.convert_result(result)
112112

113113
return result
114-
except UrlElicitationRequiredError:
115-
# Re-raise UrlElicitationRequiredError so it can be properly handled
116-
# as an MCP error response with code -32042
114+
except MCPError:
117115
raise
118116
except Exception as e:
119117
raise ToolError(f"Error executing tool {self.name}: {e}") from e

tests/server/mcpserver/test_url_elicitation_error_throw.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,23 @@ async def multi_auth(ctx: Context) -> str:
9090
assert url_error.elicitations[1].elicitation_id == "gdrive-auth"
9191

9292

93+
@pytest.mark.anyio
94+
async def test_mcp_error_propagates_as_json_rpc_error():
95+
"""Test that MCPError raised from a tool propagates as a JSON-RPC error, not isError result."""
96+
mcp = MCPServer(name="McpErrorServer")
97+
98+
@mcp.tool(description="A tool that raises a plain MCPError")
99+
async def failing_tool(ctx: Context) -> str:
100+
raise MCPError(-32000, "custom MCP error")
101+
102+
async with Client(mcp) as client:
103+
with pytest.raises(MCPError) as exc_info:
104+
await client.call_tool("failing_tool", {})
105+
106+
assert exc_info.value.error.code == -32000
107+
assert exc_info.value.error.message == "custom MCP error"
108+
109+
93110
@pytest.mark.anyio
94111
async def test_normal_exceptions_still_return_error_result():
95112
"""Test that normal exceptions still return CallToolResult with is_error=True."""

0 commit comments

Comments
 (0)