From e15e3f82aefce6167e47ee2944d76fb559233ad2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Oct 2025 17:20:36 +0000 Subject: [PATCH] fix: preserve is_error attribute from MCP tool output in ToolResultBlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix bug where the is_error attribute from MCP tool output was being disregarded instead of being passed to the resulting ToolResultBlock. The issue was in the create_sdk_mcp_server function's call_tool handler, which was only processing the content field from tool results and ignoring the is_error field. Now when a tool returns {"is_error": True}, the handler raises an exception so the MCP decorator can properly set isError=True in the CallToolResult. Changes: - Modified call_tool handler in create_sdk_mcp_server to check for is_error field - When is_error=True, raise RuntimeError with tool's error message - Added comprehensive test for is_error field handling Fixes issue where ToolResultBlock.is_error was None instead of True when MCP tools returned error results. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/claude_agent_sdk/__init__.py | 15 +++++++- tests/test_sdk_mcp_integration.py | 59 +++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/claude_agent_sdk/__init__.py b/src/claude_agent_sdk/__init__.py index 6c44747..20a55e4 100644 --- a/src/claude_agent_sdk/__init__.py +++ b/src/claude_agent_sdk/__init__.py @@ -270,7 +270,20 @@ def create_sdk_mcp_server( # Call the tool's handler with arguments result = await tool_def.handler(arguments) - # Convert result to MCP format + # Check if the tool result indicates an error + # If so, we need to raise an exception so the MCP decorator + # can properly set isError=True in the CallToolResult + if result.get("is_error", False): + # Extract error message from content if available + error_message = "Tool execution failed" + if "content" in result and result["content"]: + for item in result["content"]: + if item.get("type") == "text" and item.get("text"): + error_message = item["text"] + break + raise RuntimeError(error_message) + + # Convert result to MCP format for successful execution # The decorator expects us to return the content, not a CallToolResult # It will wrap our return value in CallToolResult content = [] diff --git a/tests/test_sdk_mcp_integration.py b/tests/test_sdk_mcp_integration.py index b76c8e1..34cface 100644 --- a/tests/test_sdk_mcp_integration.py +++ b/tests/test_sdk_mcp_integration.py @@ -145,6 +145,65 @@ async def test_error_handling(): assert "Expected error" in str(result.root.content[0].text) +@pytest.mark.asyncio +async def test_is_error_field_handling(): + """Test that tools can return is_error field and it's properly handled.""" + + @tool("error_tool", "Returns error via is_error field", {}) + async def error_tool(args: dict[str, Any]) -> dict[str, Any]: + return { + "content": [{"type": "text", "text": "Tool error message"}], + "is_error": True, + } + + @tool("success_tool", "Returns success with is_error=False", {}) + async def success_tool(args: dict[str, Any]) -> dict[str, Any]: + return { + "content": [{"type": "text", "text": "Tool success message"}], + "is_error": False, + } + + @tool("normal_tool", "Returns without is_error field", {}) + async def normal_tool(args: dict[str, Any]) -> dict[str, Any]: + return {"content": [{"type": "text", "text": "Normal response"}]} + + server_config = create_sdk_mcp_server( + name="error-field-test", tools=[error_tool, success_tool, normal_tool] + ) + + server = server_config["instance"] + from mcp.types import CallToolRequest, CallToolRequestParams + + call_handler = server.request_handlers[CallToolRequest] + + # Test error_tool - should have isError=True + error_request = CallToolRequest( + method="tools/call", + params=CallToolRequestParams(name="error_tool", arguments={}), + ) + error_result = await call_handler(error_request) + assert error_result.root.isError is True + assert "Tool error message" in error_result.root.content[0].text + + # Test success_tool - should have isError=False + success_request = CallToolRequest( + method="tools/call", + params=CallToolRequestParams(name="success_tool", arguments={}), + ) + success_result = await call_handler(success_request) + assert success_result.root.isError is False + assert "Tool success message" in success_result.root.content[0].text + + # Test normal_tool - should have isError=False (default) + normal_request = CallToolRequest( + method="tools/call", + params=CallToolRequestParams(name="normal_tool", arguments={}), + ) + normal_result = await call_handler(normal_request) + assert normal_result.root.isError is False + assert "Normal response" in normal_result.root.content[0].text + + @pytest.mark.asyncio async def test_mixed_servers(): """Test that SDK and external MCP servers can work together."""