Skip to content

Add httpx.HTTPStatusError error handling to tool run #1119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
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 @@ -106,6 +107,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}")
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) -> list[str]:
)
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