fix: propagate McpError from tool handlers as JSON-RPC error

Closed
ME
10d ago
+37-16

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:

  1. src/mcp/server/lowlevel/server.py — the call_tool decorator handler
  2. src/mcp/server/fastmcp/tools/base.py — the Tool.run() method

Both now use except McpError: raise which subsumes the previous except UrlElicitationRequiredError: raise since it's a subclass.