Skip to content

Commit fe48759

Browse files
committed
fix: pass through unrecognized MCP content block types
The content conversion loop in _handle_sdk_mcp_request only handled text and image blocks. Other valid MCP content types (EmbeddedResource, ResourceLink, AudioContent, and any future types) were silently dropped. Add an else branch that uses model_dump() for Pydantic objects, passes dicts through directly, and falls back to text coercion for anything else. Fixes #574
1 parent d6f0352 commit fe48759

File tree

2 files changed

+81
-0
lines changed

2 files changed

+81
-0
lines changed

src/claude_agent_sdk/_internal/query.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,15 @@ async def _handle_sdk_mcp_request(
498498
"mimeType": item.mimeType,
499499
}
500500
)
501+
else:
502+
# Forward-compatible: pass through unrecognized
503+
# content block types (e.g. search_result)
504+
if hasattr(item, "model_dump"):
505+
content.append(item.model_dump(mode="json"))
506+
elif isinstance(item, dict):
507+
content.append(item)
508+
else:
509+
content.append({"type": "text", "text": str(item)})
501510

502511
response_data = {"content": content}
503512
if hasattr(result.root, "is_error") and result.root.is_error:

tests/test_sdk_mcp_integration.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,75 @@ async def plain_tool(args: dict[str, Any]) -> dict[str, Any]:
379379

380380
# Tool without annotations should not have the key
381381
assert "annotations" not in tools_by_name["plain_tool"]
382+
383+
384+
@pytest.mark.asyncio
385+
async def test_unrecognized_content_blocks_passed_through():
386+
"""Test that non-text/image content blocks are not silently dropped.
387+
388+
The content conversion loop in _handle_sdk_mcp_request only handled text
389+
and image blocks. Other valid MCP content types like EmbeddedResource and
390+
ResourceLink were silently discarded.
391+
392+
Regression test for https://github.com/anthropics/claude-agent-sdk-python/issues/574
393+
"""
394+
from unittest.mock import AsyncMock
395+
396+
from mcp.server import Server
397+
from mcp.types import (
398+
CallToolResult,
399+
EmbeddedResource,
400+
ServerResult,
401+
TextContent,
402+
TextResourceContents,
403+
)
404+
405+
from claude_agent_sdk._internal.query import Query
406+
407+
# Build a fake MCP handler that returns an EmbeddedResource content block
408+
# alongside a normal text block
409+
embedded = EmbeddedResource(
410+
type="resource",
411+
resource=TextResourceContents(
412+
uri="file:///test.txt",
413+
text="file contents here",
414+
mimeType="text/plain",
415+
),
416+
)
417+
fake_result = ServerResult(
418+
root=CallToolResult(
419+
content=[
420+
TextContent(type="text", text="Here are the results"),
421+
embedded,
422+
]
423+
)
424+
)
425+
426+
server = Server("test-server")
427+
handler = AsyncMock(return_value=fake_result)
428+
server.request_handlers[CallToolRequest] = handler
429+
430+
query_instance = Query.__new__(Query)
431+
query_instance.sdk_mcp_servers = {"test": server}
432+
433+
response = await query_instance._handle_sdk_mcp_request(
434+
"test",
435+
{
436+
"jsonrpc": "2.0",
437+
"id": 1,
438+
"method": "tools/call",
439+
"params": {"name": "test_tool", "arguments": {}},
440+
},
441+
)
442+
443+
assert response is not None
444+
content = response["result"]["content"]
445+
446+
# Text block should be preserved as before
447+
assert content[0] == {"type": "text", "text": "Here are the results"}
448+
449+
# EmbeddedResource block should be passed through via model_dump(), not dropped
450+
assert len(content) == 2
451+
assert content[1]["type"] == "resource"
452+
assert content[1]["resource"]["uri"] == "file:///test.txt"
453+
assert content[1]["resource"]["text"] == "file contents here"

0 commit comments

Comments
 (0)