Skip to content
Closed
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
7 changes: 7 additions & 0 deletions src/mcp/server/fastmcp/tools/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations as _annotations

import functools
import httpx
import inspect
from collections.abc import Callable
from functools import cached_property
Expand Down Expand Up @@ -99,6 +100,12 @@ async def run(
result = self.fn_metadata.convert_result(result)

return result
except httpx.HTTPStatusError as e:
try:
error_detail = e.response.json()
except:
error_detail = e.response.text
raise ToolError(f"Error executing tool {self.name}: [{e.response.status_code}] {error_detail}")
Comment on lines +103 to +108
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should be handling httpx specific errors in a framework that's agnostic of the transport - FastMCP might be using stdio after all, so this seems like the wrong place to handle this kind of error.

except Exception as e:
raise ToolError(f"Error executing tool {self.name}: {e}") from e

Expand Down
49 changes: 49 additions & 0 deletions tests/server/fastmcp/test_tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,55 @@ def name_shrimp(tank: MyShrimpTank, ctx: Context[ServerSessionT, None]) -> list[
)
assert result == ["rex", "gertrude"]

@pytest.mark.anyio
async def test_tool_run_httpx_json_error(self):
"""Test Tool.run() handling HTTPStatusError with JSON response."""
import httpx
from unittest.mock import Mock

def tool_with_httpx_error(x: int) -> str:
mock_response = Mock()
mock_response.status_code = 404
mock_response.json.return_value = {"error": "Not found", "code": "RESOURCE_NOT_FOUND"}
raise httpx.HTTPStatusError("Not found", request=Mock(), response=mock_response)

manager = ToolManager()
tool = manager.add_tool(tool_with_httpx_error)

with pytest.raises(ToolError, match="Error executing tool tool_with_httpx_error: [404]"):
await tool.run({"x": 42})

@pytest.mark.anyio
async def test_tool_run_httpx_text_error(self):
"""Test Tool.run() handling HTTPStatusError with text response."""
import httpx
from unittest.mock import Mock

def tool_with_httpx_text_error(x: int) -> str:
mock_response = Mock()
mock_response.status_code = 500
mock_response.json.side_effect = Exception("Not JSON")
mock_response.text = "Internal Server Error"
raise httpx.HTTPStatusError("Server error", request=Mock(), response=mock_response)

manager = ToolManager()
tool = manager.add_tool(tool_with_httpx_text_error)

with pytest.raises(ToolError, match="Error executing tool tool_with_httpx_text_error: [500] Internal Server Error"):
await tool.run({"x": 42})

@pytest.mark.anyio
async def test_tool_run_generic_exception(self):
"""Test Tool.run() handling generic exceptions."""
def tool_with_error(x: int) -> str:
raise ValueError("Something went wrong")

manager = ToolManager()
tool = manager.add_tool(tool_with_error)

with pytest.raises(ToolError, match="Error executing tool tool_with_error: Something went wrong"):
await tool.run({"x": 42})


class TestToolSchema:
@pytest.mark.anyio
Expand Down
Loading