Skip to content

SDK MCP Servers Fail with Streaming Input in Python SDK v0.1.5 #295

Description

@oranshayer

SDK MCP Servers Fail with Streaming Input in Python SDK v0.1.5

Environment

  • SDK Version: claude-agent-sdk 0.1.5 (Python)
  • Python Version: 3.11.9
  • OS: macOS (Darwin 24.5.0)
  • Claude Code CLI: @anthropic-ai/claude-code@2.0.28

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

CLIConnectionError: ProcessTransport is not ready for writing
Error: Expected message type 'user' or 'control', got 'undefined'

Full traceback:

ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+-+---------------- 1 ----------------
  | Traceback (most recent call last):
  |   File "claude_agent_sdk/_internal/query.py", line 303, in _handle_control_request
  |     await self.transport.write(json.dumps(success_response) + "\n")
  |   File "claude_agent_sdk/_internal/transport/subprocess_cli.py", line 410, in write
  |     raise CLIConnectionError("ProcessTransport is not ready for writing")
  | claude_agent_sdk._errors.CLIConnectionError: ProcessTransport is not ready for writing

Minimal Reproduction

import asyncio
from claude_agent_sdk import query, tool, create_sdk_mcp_server
from claude_agent_sdk.types import ClaudeAgentOptions

# Define a simple tool
@tool("calculate", "Perform calculations", {"expression": str})
async def calculate(args: dict) -> dict:
    result = eval(args["expression"])
    return {"content": [{"type": "text", "text": f"Result: {result}"}]}

# Create SDK MCP server
server = create_sdk_mcp_server(name="utilities", version="1.0.0", tools=[calculate])

# Async generator (streaming input)
async def message_gen():
    yield {"type": "user", "message": {"role": "user", "content": "Calculate 5 + 3"}}

# This FAILS with ProcessTransport error
async def test():
    async for msg in query(
        prompt=message_gen(),  # ❌ Streaming input fails
        options=ClaudeAgentOptions(mcp_servers={"utilities": server})
    ):
        print(msg)

asyncio.run(test())

Workaround

Using a simple string prompt instead of async generator works:

# This WORKS
async def test():
    async for msg in query(
        prompt="Calculate 5 + 3",  # ✅ Simple string works
        options=ClaudeAgentOptions(mcp_servers={"utilities": server})
    ):
        print(msg)

asyncio.run(test())

Expected Behavior

Per the Custom Tools documentation, SDK MCP servers should work with streaming input mode (async generators).

Additional Notes

  • The issue occurs with SDK MCP servers only (in-process, created via create_sdk_mcp_server())
  • External MCP servers (stdio/HTTP subprocess) likely unaffected (different transport)
  • The bug appears to be in subprocess_cli.py attempting ProcessTransport for in-process tools
  • Workaround is functional but contradicts documentation

Related

Activity

  1. modesty commented on Jan 1, 2026

    @modesty

    +1, same problem

  2. MrJnrman commented on Jan 15, 2026

    @MrJnrman

    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 Task tool:

    @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 same ProcessTransport is not ready for writing error 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 _ready flag is set to False before the MCP response can be written back, causing the CLI to receive no response and return "Stream closed" as the tool result.

  3. qing-ant commented on Mar 25, 2026

    @qing-ant
    Contributor

    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_request succeeds in calling the MCP tool handler, but the subsequent transport.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions