Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions src/mcp/server/fastmcp/tools/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions src/mcp/server/lowlevel/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
10 changes: 6 additions & 4 deletions tests/client/test_list_roots_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 6 additions & 4 deletions tests/client/test_sampling_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions tests/server/fastmcp/test_url_elicitation_error_throw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading