Problem
receive_response() streams MCP tool results as UserMessage containing ToolResultBlock, indistinguishable from real user messages without inspecting the content blocks. This makes it easy for the model to misinterpret tool results as new user input, causing infinite agent loops.
Reproduction
When using create_sdk_mcp_server to register a custom MCP tool (e.g. send_message), the receive_response() stream looks like:
AssistantMessage → [ToolUseBlock(name='mcp__clawless__send_message', ...)]
UserMessage → [ToolResultBlock(tool_use_id='...', content='Message sent')]
AssistantMessage → [ToolUseBlock(name='mcp__clawless__send_message', ...)] # agent loops
UserMessage → [ToolResultBlock(...)]
... repeats indefinitely
The agent sees the UserMessage (tool result) and interprets it as requiring a response, calling the tool again, creating an infinite loop.
Expected behavior
One of:
- Distinct message type: Stream tool results as a
ToolResultMessage (or similar) rather than UserMessage, so consumers can differentiate without inspecting content blocks.
- Filtered stream: Provide an option to only yield "meaningful" messages (assistant text, result) and handle tool round-trips internally.
- Documentation: At minimum, document that
UserMessage in the stream may contain ToolResultBlock and is not always a real user message.
Workaround
We worked around this with:
- System prompt instructions explicitly telling the model that tool results are not user messages
- Content validation in the tool handler (rejecting trivially short messages)
- Per-turn rate limiting on tool calls
These are defense-in-depth but the root cause is the ambiguous message typing.
Environment
claude-agent-sdk (Python)
- MCP tools via
create_sdk_mcp_server
Problem
receive_response()streams MCP tool results asUserMessagecontainingToolResultBlock, indistinguishable from real user messages without inspecting the content blocks. This makes it easy for the model to misinterpret tool results as new user input, causing infinite agent loops.Reproduction
When using
create_sdk_mcp_serverto register a custom MCP tool (e.g.send_message), thereceive_response()stream looks like:The agent sees the
UserMessage(tool result) and interprets it as requiring a response, calling the tool again, creating an infinite loop.Expected behavior
One of:
ToolResultMessage(or similar) rather thanUserMessage, so consumers can differentiate without inspecting content blocks.UserMessagein the stream may containToolResultBlockand is not always a real user message.Workaround
We worked around this with:
These are defense-in-depth but the root cause is the ambiguous message typing.
Environment
claude-agent-sdk(Python)create_sdk_mcp_server