From 4ea102b433e1684d853be75e91e309c1bddaa6d2 Mon Sep 17 00:00:00 2001 From: Bill Easton Date: Tue, 14 Apr 2026 11:10:23 -0500 Subject: [PATCH] fix: ProxyTool crashes on non-TextContent error responses (#3926) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: handle non-TextContent error responses in ProxyTool 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.6 (1M context) * Avoid serializing binary content into ToolError messages Use type name instead of str(content) to prevent dumping large base64 payloads into error messages. 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.6 (1M context) * ruff format fix 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/fastmcp/server/providers/proxy.py | 8 ++++- .../providers/proxy/test_proxy_server.py | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/fastmcp/server/providers/proxy.py b/src/fastmcp/server/providers/proxy.py index b59024c70..fd6f44228 100644 --- a/src/fastmcp/server/providers/proxy.py +++ b/src/fastmcp/server/providers/proxy.py @@ -158,7 +158,13 @@ class ProxyTool(Tool): name=backend_name, arguments=arguments, meta=meta ) if result.isError: - raise ToolError(cast(mcp.types.TextContent, result.content[0]).text) + first = result.content[0] if result.content else None + if isinstance(first, mcp.types.TextContent): + raise ToolError(first.text) + elif first is None: + raise ToolError("Tool returned an error with no content") + else: + raise ToolError(f"Tool returned an error ({type(first).__name__})") # Preserve backend's meta (includes task metadata for background tasks) return ToolResult( content=result.content, diff --git a/tests/server/providers/proxy/test_proxy_server.py b/tests/server/providers/proxy/test_proxy_server.py index 827607028..fb4c746e9 100644 --- a/tests/server/providers/proxy/test_proxy_server.py +++ b/tests/server/providers/proxy/test_proxy_server.py @@ -330,6 +330,36 @@ class TestTools: async with Client(proxy_server) as client: await client.call_tool("error_tool", {}) + async def test_error_tool_with_image_content(self, proxy_server): + """Non-TextContent error responses should not crash with AttributeError.""" + error_result = mcp_types.CallToolResult( + content=[ + mcp_types.ImageContent( + type="image", data="abc123", mimeType="image/png" + ) + ], + isError=True, + ) + with patch.object( + Client, "call_tool_mcp", new_callable=AsyncMock, return_value=error_result + ): + with pytest.raises(ToolError): + async with Client(proxy_server) as client: + await client.call_tool("error_tool", {}) + + async def test_error_tool_with_empty_content(self, proxy_server): + """Error responses with empty content should not crash.""" + error_result = mcp_types.CallToolResult( + content=[], + isError=True, + ) + with patch.object( + Client, "call_tool_mcp", new_callable=AsyncMock, return_value=error_result + ): + with pytest.raises(ToolError): + async with Client(proxy_server) as client: + await client.call_tool("error_tool", {}) + async def test_call_tool_forwards_meta(self, fastmcp_server, proxy_server): """Test that metadata from proxied tool results is properly forwarded."""