Summary
Custom tools defined with @tool that return search_result content blocks have those blocks silently dropped before they reach Claude. This prevents using native citations with custom RAG tools in the Agent SDK.
Environment
claude-agent-sdk: 0.1.33
anthropic: 0.79.0
- Python: 3.14.2
- OS: macOS (Darwin 24.3.0)
Expected behavior
A @tool handler returning {"content": [{"type": "search_result", ...}]} should pass the search_result block through to Claude, enabling Claude to produce structured citations (search_result_location) in its response — the same behavior as when using the anthropic SDK directly.
Actual behavior
The search_result block is silently dropped. Claude receives an empty tool result ({"content": []}) and cannot cite the content because it never sees it.
Root cause
The SDK's internal MCP tool result handler in claude_agent_sdk/_internal/query.py (around line 486-497) only recognizes two content block types:
for item in result.root.content:
if hasattr(item, "text"):
content.append({"type": "text", "text": item.text})
elif hasattr(item, "data") and hasattr(item, "mimeType"):
content.append({"type": "image", "data": item.data, "mimeType": item.mimeType})
A search_result block has neither a .text attribute nor .data+.mimeType attributes, so it falls through both checks and is silently discarded.
Additionally, message_parser.py (around line 98-123) only handles text, thinking, tool_use, and tool_result block types when parsing assistant messages. Even if search_result blocks reached Claude and it produced TextBlocks with citations fields, the parser would not preserve the citation data.
Reproduction
Minimal reproduction using ClaudeSDKClient with a custom tool:
import asyncio
import json
from claude_agent_sdk import (
tool,
create_sdk_mcp_server,
ClaudeSDKClient,
ClaudeAgentOptions,
AssistantMessage,
ResultMessage,
)
SEARCH_RESULT_BLOCK = {
"type": "search_result",
"source": "https://example.com/article",
"title": "Example Article",
"content": [
{
"type": "text",
"text": "The answer to the question is 42.",
}
],
"citations": {"enabled": True},
}
@tool(
name="search_kb",
description="Search the knowledge base",
input_schema={
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
)
async def search_kb(args):
print(f"[tool called] search_kb({args})")
return {"content": [SEARCH_RESULT_BLOCK]}
async def main():
server = create_sdk_mcp_server(name="test", tools=[search_kb])
options = ClaudeAgentOptions(
model="claude-sonnet-4-5-20250929",
max_turns=2,
mcp_servers={"test": server},
allowed_tools=["mcp__test__search_kb"],
permission_mode="bypassPermissions",
system_prompt="Use the search tool to answer questions.",
)
async with ClaudeSDKClient(options=options) as client:
await client.query("What is the answer? Use the search tool.")
async for msg in client.receive_messages():
if hasattr(msg, "content") and isinstance(msg.content, list):
for block in msg.content:
# Check tool result — content will be [] (empty)
if hasattr(block, "tool_use_id"):
print(f"Tool result content: {block.content}")
# Expected: [{"type": "search_result", ...}]
# Actual: []
if isinstance(msg, ResultMessage):
break
asyncio.run(main())
Observed output — the tool result message shows:
{
"content": [
{
"tool_use_id": "toolu_...",
"content": [],
"is_error": null
}
]
}
The content array is empty — the search_result block was dropped.
For comparison, the same search_result block works correctly when sent via the anthropic SDK directly:
import anthropic, asyncio
async def main():
client = anthropic.AsyncAnthropic()
response = await client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=1024,
messages=[
{"role": "user", "content": "What is the answer?"},
{"role": "assistant", "content": [
{"type": "tool_use", "id": "tu_1", "name": "search", "input": {"q": "answer"}}
]},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "tu_1", "content": [
{
"type": "search_result",
"source": "https://example.com/article",
"title": "Example Article",
"content": [{"type": "text", "text": "The answer is 42."}],
"citations": {"enabled": True},
}
]}
]},
],
tools=[{"name": "search", "description": "Search", "input_schema": {"type": "object", "properties": {"q": {"type": "string"}}}}],
)
for block in response.model_dump()["content"]:
if block.get("citations"):
print("Citations work:", block["citations"])
asyncio.run(main())
This produces structured citations with search_result_location type as expected.
Suggested fix
In query.py, the tool result content handler should pass through block types it doesn't recognize (including search_result) rather than silently dropping them:
for item in result.root.content:
if hasattr(item, "text"):
content.append({"type": "text", "text": item.text})
elif hasattr(item, "data") and hasattr(item, "mimeType"):
content.append({"type": "image", "data": item.data, "mimeType": item.mimeType})
else:
# Pass through unknown block types (e.g., search_result)
# so they reach the Anthropic API intact
if hasattr(item, "model_dump"):
content.append(item.model_dump())
elif isinstance(item, dict):
content.append(item)
Similarly, message_parser.py should preserve the citations field on TextBlock responses from Claude, and ideally the ContentBlock union in types.py should include a type for citation-bearing text blocks.
Impact
This blocks using the Agent SDK for RAG applications that need structured, verifiable citations — a key feature of the Anthropic API's search results. The only workaround is to bypass the Agent SDK and use anthropic.AsyncAnthropic directly for any turn that needs citation support.
Summary
Custom tools defined with
@toolthat returnsearch_resultcontent blocks have those blocks silently dropped before they reach Claude. This prevents using native citations with custom RAG tools in the Agent SDK.Environment
claude-agent-sdk: 0.1.33anthropic: 0.79.0Expected behavior
A
@toolhandler returning{"content": [{"type": "search_result", ...}]}should pass thesearch_resultblock through to Claude, enabling Claude to produce structured citations (search_result_location) in its response — the same behavior as when using theanthropicSDK directly.Actual behavior
The
search_resultblock is silently dropped. Claude receives an empty tool result ({"content": []}) and cannot cite the content because it never sees it.Root cause
The SDK's internal MCP tool result handler in
claude_agent_sdk/_internal/query.py(around line 486-497) only recognizes two content block types:A
search_resultblock has neither a.textattribute nor.data+.mimeTypeattributes, so it falls through both checks and is silently discarded.Additionally,
message_parser.py(around line 98-123) only handlestext,thinking,tool_use, andtool_resultblock types when parsing assistant messages. Even ifsearch_resultblocks reached Claude and it producedTextBlocks withcitationsfields, the parser would not preserve the citation data.Reproduction
Minimal reproduction using
ClaudeSDKClientwith a custom tool:Observed output — the tool result message shows:
{ "content": [ { "tool_use_id": "toolu_...", "content": [], "is_error": null } ] }The
contentarray is empty — thesearch_resultblock was dropped.For comparison, the same
search_resultblock works correctly when sent via theanthropicSDK directly:This produces structured citations with
search_result_locationtype as expected.Suggested fix
In
query.py, the tool result content handler should pass through block types it doesn't recognize (includingsearch_result) rather than silently dropping them:Similarly,
message_parser.pyshould preserve thecitationsfield onTextBlockresponses from Claude, and ideally theContentBlockunion intypes.pyshould include a type for citation-bearing text blocks.Impact
This blocks using the Agent SDK for RAG applications that need structured, verifiable citations — a key feature of the Anthropic API's search results. The only workaround is to bypass the Agent SDK and use
anthropic.AsyncAnthropicdirectly for any turn that needs citation support.