diff --git a/src/mcp/server/fastmcp/tools/base.py b/src/mcp/server/fastmcp/tools/base.py index 1ae6d90d19..f955806759 100644 --- a/src/mcp/server/fastmcp/tools/base.py +++ b/src/mcp/server/fastmcp/tools/base.py @@ -11,7 +11,7 @@ from mcp.server.fastmcp.exceptions import ToolError from mcp.server.fastmcp.utilities.context_injection import find_context_parameter from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata -from mcp.shared.exceptions import UrlElicitationRequiredError +from mcp.shared.exceptions import McpError from mcp.shared.tool_name_validation import validate_and_warn_tool_name from mcp.types import Icon, ToolAnnotations @@ -109,9 +109,7 @@ async def run( result = self.fn_metadata.convert_result(result) return result - except UrlElicitationRequiredError: - # Re-raise UrlElicitationRequiredError so it can be properly handled - # as an MCP error response with code -32042 + except McpError: raise except Exception as e: raise ToolError(f"Error executing tool {self.name}: {e}") from e diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 7d925de32b..591195a6dc 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -90,7 +90,7 @@ async def main(): from mcp.server.models import InitializationOptions from mcp.server.session import ServerSession from mcp.shared.context import RequestContext -from mcp.shared.exceptions import McpError, UrlElicitationRequiredError +from mcp.shared.exceptions import McpError from mcp.shared.message import ServerMessageMetadata, SessionMessage from mcp.shared.session import RequestResponder from mcp.shared.tool_name_validation import validate_and_warn_tool_name @@ -576,9 +576,7 @@ async def handler(req: types.CallToolRequest): isError=False, ) ) - except UrlElicitationRequiredError: - # Re-raise UrlElicitationRequiredError so it can be properly handled - # by _handle_request, which converts it to an error response with code -32042 + except McpError: raise except Exception as e: return self._make_error_result(str(e)) diff --git a/tests/client/test_list_roots_callback.py b/tests/client/test_list_roots_callback.py index 0da0fff07a..40be47a5a0 100644 --- a/tests/client/test_list_roots_callback.py +++ b/tests/client/test_list_roots_callback.py @@ -52,7 +52,9 @@ async def test_list_roots(context: Context[ServerSession, None], message: str): # Test without list_roots callback async with create_session(server._mcp_server) as client_session: # Make a request to trigger sampling callback - result = await client_session.call_tool("test_list_roots", {"message": "test message"}) - assert result.isError is True - assert isinstance(result.content[0], TextContent) - assert result.content[0].text == "Error executing tool test_list_roots: List roots not supported" + # McpError propagates as a JSON-RPC error, not CallToolResult(isError=True) + from mcp.shared.exceptions import McpError + + with pytest.raises(McpError) as exc_info: + await client_session.call_tool("test_list_roots", {"message": "test message"}) + assert "List roots not supported" in exc_info.value.error.message diff --git a/tests/client/test_sampling_callback.py b/tests/client/test_sampling_callback.py index 733364a767..160109f5bd 100644 --- a/tests/client/test_sampling_callback.py +++ b/tests/client/test_sampling_callback.py @@ -54,10 +54,12 @@ async def test_sampling_tool(message: str): # Test without sampling callback async with create_session(server._mcp_server) as client_session: # Make a request to trigger sampling callback - result = await client_session.call_tool("test_sampling", {"message": "Test message for sampling"}) - assert result.isError is True - assert isinstance(result.content[0], TextContent) - assert result.content[0].text == "Error executing tool test_sampling: Sampling not supported" + # McpError propagates as a JSON-RPC error, not CallToolResult(isError=True) + from mcp.shared.exceptions import McpError + + with pytest.raises(McpError) as exc_info: + await client_session.call_tool("test_sampling", {"message": "Test message for sampling"}) + assert "Sampling not supported" in exc_info.value.error.message @pytest.mark.anyio diff --git a/tests/server/fastmcp/test_url_elicitation_error_throw.py b/tests/server/fastmcp/test_url_elicitation_error_throw.py index 2d7eda4ab4..1b6369de9b 100644 --- a/tests/server/fastmcp/test_url_elicitation_error_throw.py +++ b/tests/server/fastmcp/test_url_elicitation_error_throw.py @@ -111,3 +111,24 @@ async def failing_tool(ctx: Context[ServerSession, None]) -> str: assert len(result.content) == 1 assert isinstance(result.content[0], types.TextContent) assert "Something went wrong" in result.content[0].text + + +@pytest.mark.anyio +async def test_mcp_error_in_tool_handler_propagates_as_jsonrpc_error(): + """Test that McpError raised from a tool is received as a JSON-RPC error, not isError:true.""" + mcp = FastMCP(name="McpErrorServer") + + @mcp.tool(description="A tool that raises McpError") + async def server_fault_tool(ctx: Context[ServerSession, None]) -> str: + raise McpError(types.ErrorData(code=-32000, message="server fault")) + + async with create_connected_server_and_client_session(mcp._mcp_server) as client_session: + await client_session.initialize() + + # McpError should propagate as a JSON-RPC error, not CallToolResult(isError=True) + with pytest.raises(McpError) as exc_info: + await client_session.call_tool("server_fault_tool", {}) + + error = exc_info.value.error + assert error.code == -32000 + assert error.message == "server fault"