Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions src/claude_agent_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,15 @@ def create_sdk_mcp_server(
- ClaudeAgentOptions: Configuration for using servers with query()
"""
from mcp.server import Server
from mcp.types import ImageContent, TextContent, Tool
from mcp.types import (
AudioContent,
CallToolResult,
EmbeddedResource,
ImageContent,
ResourceLink,
TextContent,
Tool,
)

# Create MCP server instance
server = Server(name, version=version)
Expand Down Expand Up @@ -317,9 +325,13 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
result = await tool_def.handler(arguments)

# Convert result to MCP format
# The decorator expects us to return the content, not a CallToolResult
# It will wrap our return value in CallToolResult
content: list[TextContent | ImageContent] = []
content: list[
TextContent
| ImageContent
| AudioContent
| ResourceLink
| EmbeddedResource
] = []
if "content" in result:
for item in result["content"]:
if item.get("type") == "text":
Expand All @@ -333,8 +345,9 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
)
)

# Return just the content list - the decorator wraps it
return content
return CallToolResult(
content=content, isError=result.get("is_error", False)
)
Comment thread
qing-ant marked this conversation as resolved.

# Return SDK server configuration
return McpSdkServerConfig(type="sdk", name=name, instance=server)
Expand Down
4 changes: 2 additions & 2 deletions src/claude_agent_sdk/_internal/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,8 @@ async def _handle_sdk_mcp_request(
)

response_data = {"content": content}
if hasattr(result.root, "is_error") and result.root.is_error:
response_data["is_error"] = True # type: ignore[assignment]
if hasattr(result.root, "isError") and result.root.isError:
response_data["isError"] = True # type: ignore[assignment]

return {
"jsonrpc": "2.0",
Expand Down
36 changes: 36 additions & 0 deletions tests/test_sdk_mcp_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,42 @@ async def fail_tool(args: dict[str, Any]) -> dict[str, Any]:
assert "Expected error" in str(result.root.content[0].text)


@pytest.mark.asyncio
async def test_is_error_flag_propagated():
"""Test that is_error flag from tool result dict is propagated to CallToolResult."""

@tool("divide", "Divide two numbers", {"a": float, "b": float})
async def divide(args: dict[str, Any]) -> dict[str, Any]:
if args["b"] == 0:
return {
"content": [{"type": "text", "text": "Division by zero"}],
"is_error": True,
}
return {"content": [{"type": "text", "text": str(args["a"] / args["b"])}]}

server_config = create_sdk_mcp_server(name="error-flag-test", tools=[divide])
server = server_config["instance"]
call_handler = server.request_handlers[CallToolRequest]

# Test error case — is_error: True should be propagated
error_request = CallToolRequest(
method="tools/call",
params=CallToolRequestParams(name="divide", arguments={"a": 1, "b": 0}),
)
result = await call_handler(error_request)
assert result.root.isError is True
assert result.root.content[0].text == "Division by zero"

# Test success case — is_error should default to False
success_request = CallToolRequest(
Comment thread
qing-ant marked this conversation as resolved.
method="tools/call",
params=CallToolRequestParams(name="divide", arguments={"a": 6, "b": 3}),
)
result = await call_handler(success_request)
assert result.root.isError is not True
assert "2.0" in result.root.content[0].text


@pytest.mark.asyncio
async def test_mixed_servers():
"""Test that SDK and external MCP servers can work together."""
Expand Down
Loading