fix: propagate McpError from tool handlers as JSON-RPC error
Motivation and Context
When a tool handler raises McpError, both the lowlevel server's call_tool decorator and FastMCP's Tool.run() catch it via except Exception and wrap it as CallToolResult(isError=True). This loses the structured error code on the wire — clients cannot distinguish error types without parsing the message string.
The existing UrlElicitationRequiredError (a subclass of McpError) already had a re-raise bypass, but the parent McpError class did not. The v2 McpServer on main already handles this correctly with except MCPError: raise.
Fixes #2770
How Has This Been Tested?
- Added
test_mcp_error_in_tool_handler_propagates_as_jsonrpc_error— verifies McpError propagates as JSON-RPC error with code preserved - Existing
test_url_elicitation_error_thrown_from_tool— confirms UrlElicitationRequiredError still works (subclass of McpError) - Existing
test_normal_exceptions_still_return_error_result— confirms regular exceptions still become isError:true - Full
tests/server/suite passes (503 tests)
Breaking Changes
Servers that intentionally raise McpError in tool handlers expecting it to be wrapped as isError:true will now get a JSON-RPC error response instead. This is the correct behavior per MCP spec — McpError represents protocol-level errors, not tool execution failures.
Types of changes
- Bug fix (non-breaking change which fixes an issue)
Checklist
- I have read the MCP Documentation
- My code follows the repository's style guidelines
- New and existing tests pass locally
- I have added appropriate error handling
- I have added or updated documentation as needed
Additional context
Two locations fixed:
src/mcp/server/lowlevel/server.py— thecall_tooldecorator handlersrc/mcp/server/fastmcp/tools/base.py— theTool.run()method
Both now use except McpError: raise which subsumes the previous except UrlElicitationRequiredError: raise since it's a subclass.