fix: propagate McpError as JSON-RPC error response in lowlevel server
Fixes #2770
Motivation and Context
When a tool handler registered via the lowlevel server's @server.call_tool() decorator raises McpError, the handler catches it via the generic except Exception block and wraps it as CallToolResult(isError=True). This drops the structured error code on the wire, so clients cannot distinguish error types without parsing the message string.
The UrlElicitationRequiredError subclass already has a re-raise bypass for exactly this reason, but the parent McpError class does not. (The v2 high-level API on main already handles this correctly.)
How Has This Been Tested?
- Added a regression test asserting that
McpErrorraised in a lowlevel tool handler reaches the client as a JSON-RPC error with the original code, message, and data preserved (it fails without this change), plus a companion test asserting non-McpErrorexceptions still returnCallToolResult(isError=True). - Ran the lowlevel server and elicitation test suites locally: all passing.
Breaking Changes
Tool handlers that deliberately raise McpError now produce a JSON-RPC error response (the documented behavior for the error, matching UrlElicitationRequiredError and the v2 API) instead of an isError: true result. Handlers that raise other exceptions are unaffected.
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
This scopes the fix to the lowlevel server per the issue's proposed fix. #2772 covers related ground but also changes FastMCP Tool.run() behavior; this PR is the minimal lowlevel-only change with regression tests at the lowlevel layer, in case the maintainers prefer to take that separately.