fix: #879 return McpError as a structured error result instead of crashing the agent run (#2598)

This commit is contained in:
Aditya Singh
2026-03-04 18:56:01 -08:00
committed by GitHub
parent 8d3aa15c20
commit 2e1d608bc6
2 changed files with 72 additions and 0 deletions
+18
View File
@@ -13,6 +13,11 @@ from typing_extensions import NotRequired, TypedDict
from .. import _debug
from ..exceptions import AgentsException, ModelBehaviorError, UserError
try:
from mcp.shared.exceptions import McpError as _McpError
except ImportError: # pragma: no cover mcp is optional on Python < 3.10
_McpError = None # type: ignore[assignment, misc]
from ..logger import logger
from ..run_context import RunContextWrapper
from ..strict_schema import ensure_strict_json_schema
@@ -360,6 +365,19 @@ class MCPUtil:
# Re-raise UserError as-is (it already has a good message)
raise
except Exception as e:
if _McpError is not None and isinstance(e, _McpError):
# An MCP-level error (e.g. upstream HTTP 4xx/5xx, tool not found, etc.)
# is not a programming error re-raise so the FunctionTool failure
# pipeline (failure_error_function) can handle it. The default handler
# will surface the message as a structured error result; callers who set
# failure_error_function=None will have the error raised as documented.
error_text = e.error.message if hasattr(e, "error") and e.error else str(e)
logger.warning(
f"MCP tool {tool.name} on server '{server.name}' returned an error: "
f"{error_text}"
)
raise
logger.error(f"Error invoking MCP tool {tool.name} on server '{server.name}': {e}")
raise AgentsException(
f"Error invoking MCP tool {tool.name} on server '{server.name}': {e}"
+54
View File
@@ -192,6 +192,60 @@ async def test_mcp_invocation_crash_causes_error(caplog: pytest.LogCaptureFixtur
assert "Error invoking MCP tool test_tool_1" in caplog.text
@pytest.mark.asyncio
async def test_mcp_invocation_mcp_error_reraises(caplog: pytest.LogCaptureFixture):
"""Test that McpError from server.call_tool is re-raised so the FunctionTool failure
pipeline (failure_error_function) can handle it.
When an MCP server raises McpError (e.g. upstream HTTP 4xx/5xx), invoke_mcp_tool
re-raises so the configured failure_error_function shapes the model-visible error.
With the default failure_error_function the FunctionTool returns a string error
result; with failure_error_function=None the error is propagated to the caller.
"""
caplog.set_level(logging.DEBUG)
from mcp.shared.exceptions import McpError
from mcp.types import ErrorData
class McpErrorFakeMCPServer(FakeMCPServer):
async def call_tool(
self,
tool_name: str,
arguments: dict[str, Any] | None,
meta: dict[str, Any] | None = None,
):
raise McpError(ErrorData(code=-32000, message="upstream 422 Unprocessable Entity"))
server = McpErrorFakeMCPServer()
server.add_tool("search", {})
ctx = RunContextWrapper(context=None)
tool = MCPTool(name="search", inputSchema={})
# invoke_mcp_tool itself should re-raise McpError
with pytest.raises(McpError):
await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}")
# Warning (not error) should be logged before re-raising
assert "returned an error" in caplog.text
# Via FunctionTool with default failure_error_function: error becomes a string result
mcp_tool = MCPTool(name="search", inputSchema={})
agent = Agent(name="test-agent")
function_tool = MCPUtil.to_function_tool(
mcp_tool, server, convert_schemas_to_strict=False, agent=agent
)
tool_context = ToolContext(
context=None,
tool_name="search",
tool_call_id="test_call_mcp_error",
tool_arguments="{}",
)
result = await function_tool.on_invoke_tool(tool_context, "{}")
assert isinstance(result, str)
assert "upstream 422 Unprocessable Entity" in result or "error" in result.lower()
@pytest.mark.asyncio
async def test_mcp_tool_graceful_error_handling(caplog: pytest.LogCaptureFixture):
"""Test that MCP tool errors are handled gracefully when invoked via FunctionTool.