fix: preserve is_error attribute from MCP tool output in ToolResultBlock

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 <noreply@anthropic.com>
This commit is contained in:
Claude 2025-10-14 17:20:36 +00:00
parent f896cd6f7f
commit e15e3f82ae
No known key found for this signature in database
2 changed files with 73 additions and 1 deletions

View file

@ -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 = []

View file

@ -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."""