Repository navigation
SDK MCP Servers Fail with Streaming Input in Python SDK v0.1.5 #295
Description
Activity
+1, same problem
Confirming bug still exists in v0.1.19 + additional findings
Environment
- SDK Version: 0.1.19 (latest)
- Python: 3.13
- OS: macOS Darwin 25.2.0
Additional Reproduction Case: Subagents
The bug also occurs when using string prompts (not async generators) if MCP tools are called by subagents via the
Tasktool:@tool("save_result", "Save a result", {"data": str}) async def save_result(args: dict) -> dict: print(f"[TOOL CALLED] {args}") # Never prints! return {"content": [{"type": "text", "text": "Saved"}]} server = create_sdk_mcp_server(name="my_tools", tools=[save_result]) worker_agent = AgentDefinition( description="Worker that saves results", prompt="Process data and call save_result with your findings.", tools=["mcp__my_tools__save_result"], model="sonnet", ) # String prompt - but subagent triggers MCP call async for message in query( prompt="Analyze this data and save results", options=ClaudeAgentOptions( agents={"worker": worker_agent}, mcp_servers={"my_tools": server}, allowed_tools=["Task", "mcp__my_tools__save_result"], ), ): print(message)
The orchestrator invokes subagents via
Task, and when those subagents call MCP tools, the sameProcessTransport is not ready for writingerror occurs.Stack Trace
+ Exception Group Traceback (most recent call last): | File "test_mcp_minimal.py", line 73, in <module> | asyncio.run(main()) | File ".../asyncio/runners.py", line 194, in run | return runner.run(main) | File ".../asyncio/runners.py", line 118, in run | return self._loop.run_until_complete(task) | File ".../asyncio/base_events.py", line 720, in run_until_complete | return future.result() | File "test_mcp_minimal.py", line 45, in main | async for message in query( | File ".../claude_agent_sdk/query.py", line 123, in query | async for message in client.process_query( | yield message | File ".../claude_agent_sdk/_internal/client.py", line 124, in process_query | await query.close() | File ".../claude_agent_sdk/_internal/query.py", line 608, in close | with suppress(anyio.get_cancelled_exc_class()): | File ".../contextlib.py", line 468, in __exit__ | raise rest | File ".../claude_agent_sdk/_internal/query.py", line 609, in close | await self._tg.__aexit__(None, None, None) | File ".../anyio/_backends/_asyncio.py", line 783, in __aexit__ | raise BaseExceptionGroup( | "unhandled errors in a TaskGroup", self._exceptions | ) from None | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception) +-+---------------- 1 ---------------- | Traceback (most recent call last): | File ".../claude_agent_sdk/_internal/query.py", line 325, in _handle_control_request | await self.transport.write(json.dumps(success_response) + "\n") | File ".../claude_agent_sdk/_internal/transport/subprocess_cli.py", line 529, in write | raise CLIConnectionError("ProcessTransport is not ready for writing") | claude_agent_sdk._errors.CLIConnectionError: ProcessTransport is not ready for writing | | During handling of the above exception, another exception occurred: | | Traceback (most recent call last): | File ".../claude_agent_sdk/_internal/query.py", line 337, in _handle_control_request | await self.transport.write(json.dumps(error_response) + "\n") | File ".../claude_agent_sdk/_internal/transport/subprocess_cli.py", line 529, in write | raise CLIConnectionError("ProcessTransport is not ready for writing") | claude_agent_sdk._errors.CLIConnectionError: ProcessTransport is not ready for writing +------------------------------------Root Cause Analysis
Traced the issue to a race condition in
_handle_control_request:# query.py - _handle_control_request method if subtype == "mcp_message": # This succeeds - tool handler is called mcp_response = await self._handle_sdk_mcp_request(server_name, mcp_message) # This fails - transport closed before response can be written await self.transport.write(json.dumps(success_response) + "\n") # ^^^ CLIConnectionError: ProcessTransport is not ready for writing
The transport's
_readyflag is set toFalsebefore the MCP response can be written back, causing the CLI to receive no response and return "Stream closed" as the tool result.This issue shares the same root cause as #730 —
wait_for_result_and_end_input()closes stdin (setting_ready = False) before the MCP response can be written back via the bidirectional control protocol. The stack trace from the Jan 15 comment confirms this:_handle_control_requestsucceeds in calling the MCP tool handler, but the subsequenttransport.write()fails because the transport was closed by the stdin timeout.PR #731 removes the timeout entirely when hooks or MCP servers are active, which should resolve both the async generator streaming case from the original report and the subagent+MCP case from the follow-up comment. Closing as duplicate of #730.
SDK MCP Servers Fail with Streaming Input in Python SDK v0.1.5
Environment
Description
SDK-created MCP servers (using
create_sdk_mcp_server()) fail when used with streaming input mode (async generators), despite documentation stating "Custom MCP tools require streaming input mode."Error
Full traceback:
Minimal Reproduction
Workaround
Using a simple string prompt instead of async generator works:
Expected Behavior
Per the Custom Tools documentation, SDK MCP servers should work with streaming input mode (async generators).
Additional Notes
create_sdk_mcp_server())subprocess_cli.pyattempting ProcessTransport for in-process toolsRelated