Skip to content

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

Open
mengyunxie wants to merge 1 commit into
modelcontextprotocol:v1.xfrom
mengyunxie:fix/propagate-mcp-error-in-tool-handler
Open

fix: propagate McpError from tool handlers as JSON-RPC error#2772
mengyunxie wants to merge 1 commit into
modelcontextprotocol:v1.xfrom
mengyunxie:fix/propagate-mcp-error-in-tool-handler

Conversation

@mengyunxie
Copy link
Copy Markdown

@mengyunxie mengyunxie commented Jun 2, 2026

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.

@mengyunxie mengyunxie force-pushed the fix/propagate-mcp-error-in-tool-handler branch from a475afc to 60f8fc7 Compare June 2, 2026 23:32
Copy link
Copy Markdown

@StantonMatt StantonMatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked this against head 60f8fc7.

Focused local run passed:

uv run --frozen pytest tests/server/fastmcp/test_url_elicitation_error_throw.py tests/client/test_list_roots_callback.py tests/client/test_sampling_callback.py -q -> 8 passed

I also ran a small low-level Server.call_tool() probe because src/mcp/server/lowlevel/server.py changes separately from the FastMCP path. In that probe, McpError(ErrorData(code=-32000, message="server fault")) reached ClientSession.call_tool() as McpError, while a regular ValueError("regular failure") still returned CallToolResult(isError=True). That covers the low-level decorator behavior I would have wanted checked here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants