From bf92ad938b74f644daa0a44c20fb5d54419e3017 Mon Sep 17 00:00:00 2001 From: spaceorc Date: Thu, 11 Dec 2025 18:17:33 +0000 Subject: [PATCH] Fix SDK MCP server error handling: use isError instead of is_error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP library's CallToolResult type uses camelCase `isError` property, but the code was checking for snake_case `is_error`. This caused error states from SDK MCP tool failures to not be propagated correctly in the JSONRPC response. Changes: - Fix property name in query.py from `result.root.is_error` to `result.root.isError` - Add test case to verify error handling through JSONRPC handler - Test confirms error flag is now properly included in responses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/claude_agent_sdk/_internal/query.py | 2 +- tests/test_sdk_mcp_integration.py | 66 +++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/claude_agent_sdk/_internal/query.py b/src/claude_agent_sdk/_internal/query.py index c30fc159..28a0cbd0 100644 --- a/src/claude_agent_sdk/_internal/query.py +++ b/src/claude_agent_sdk/_internal/query.py @@ -488,7 +488,7 @@ async def _handle_sdk_mcp_request( ) response_data = {"content": content} - if hasattr(result.root, "is_error") and result.root.is_error: + if hasattr(result.root, "isError") and result.root.isError: response_data["is_error"] = True # type: ignore[assignment] return { diff --git a/tests/test_sdk_mcp_integration.py b/tests/test_sdk_mcp_integration.py index d3260073..612d7565 100644 --- a/tests/test_sdk_mcp_integration.py +++ b/tests/test_sdk_mcp_integration.py @@ -263,3 +263,69 @@ async def generate_chart(args: dict[str, Any]) -> dict[str, Any]: assert len(tool_executions) == 1 assert tool_executions[0]["name"] == "generate_chart" assert tool_executions[0]["args"]["title"] == "Sales Report" + + +@pytest.mark.asyncio +async def test_error_handling_through_jsonrpc(): + """Test that tool errors are properly handled through the JSONRPC handler.""" + + @tool("fail", "Always fails", {}) + async def fail_tool(args: dict[str, Any]) -> dict[str, Any]: + raise ValueError("Expected error") + + server_config = create_sdk_mcp_server(name="error-test", tools=[fail_tool]) + + # Import the Query class to test the JSONRPC handler + from claude_agent_sdk._internal.query import Query + from claude_agent_sdk._internal.transport import Transport + + # Extract the SDK MCP server instance + sdk_mcp_servers = {"error": server_config["instance"]} + + # We need a mock transport + class MockTransport(Transport): + async def connect(self) -> None: + pass + + async def write(self, data: str) -> None: + pass + + async def read_messages(self): + # AsyncIterator that yields nothing + if False: + yield + + async def close(self) -> None: + pass + + def is_ready(self) -> bool: + return True + + async def end_input(self) -> None: + pass + + transport = MockTransport() + query = Query( + transport=transport, + is_streaming_mode=False, + sdk_mcp_servers=sdk_mcp_servers, + ) + + # Manually invoke the SDK MCP request handler + jsonrpc_message = { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": {"name": "fail", "arguments": {}}, + } + + response = await query._handle_sdk_mcp_request("error", jsonrpc_message) + + # The response should include is_error: true + assert response is not None + assert response["jsonrpc"] == "2.0" + assert response["id"] == 1 + assert "result" in response + assert "is_error" in response["result"] + assert response["result"]["is_error"] is True + assert "Expected error" in str(response["result"]["content"])