[BUG] query() with string prompt + mcp_servers blocks generator until result — no streaming events

Resolved 💬 3 comments Opened Mar 31, 2026 by axisrow Closed Apr 3, 2026

Summary

When query() is called with a string prompt and mcp_servers containing SDK MCP servers, the async generator does not yield any events until the entire conversation completes (final result message). All intermediate events (SystemMessage, StreamEvent, AssistantMessage) are buffered in the memory channel but never reach the consumer.

This makes include_partial_messages=True ineffective for string prompts with MCP servers — the option works correctly only with AsyncIterable prompts.

Root cause

In claude_agent_sdk/_internal/client.py, process_query() lines 131–150:

if isinstance(prompt, str):
    user_message = {
        "type": "user", "session_id": "",
        "message": {"role": "user", "content": prompt},
        "parent_tool_use_id": None,
    }
    await chosen_transport.write(json.dumps(user_message) + "\n")
    await query.wait_for_result_and_end_input()   # ← BLOCKS generator
elif isinstance(prompt, AsyncIterable):
    query.spawn_task(query.stream_input(prompt))   # ← runs in background

# Only AFTER the above does yielding begin:
async for data in query.receive_messages():
    message = parse_message(data)
    if message is not None:
        yield message

For string prompts: wait_for_result_and_end_input() awaits _first_result_event (query.py line 697), which is set only when a "result" type message arrives (query.py line 233). The generator is blocked — receive_messages() never starts until the entire conversation is done.

For AsyncIterable prompts: stream_input() is spawned as a background task. receive_messages() starts immediately, yielding events as they arrive.

Impact

  • Simple queries (no tool calls): work fine — result arrives in seconds, buffered events flush quickly
  • Tool-calling queries (MCP tools): total time = tool execution + second API call. If this exceeds the consumer's timeout (e.g. 120s), the consumer never receives any events at all — not even SystemMessage
  • include_partial_messages=True has no effect for string prompts — all events are buffered regardless

Reproduction

from claude_agent_sdk import query, ClaudeAgentOptions, create_sdk_mcp_server, SdkMcpTool
import asyncio, time

# Create MCP server with a slow tool
async def slow_handler(seconds: int = 30):
    await asyncio.sleep(seconds)
    return f"Slept {seconds}s"

server = create_sdk_mcp_server(name="test", tools=[
    SdkMcpTool(name="slow_tool", description="Sleeps", 
               input_schema={"type": "object", "properties": {"seconds": {"type": "integer"}}},
               handler=slow_handler)
])

options = ClaudeAgentOptions(
    mcp_servers={"test": server},
    allowed_tools=["mcp__test__slow_tool"],
    include_partial_messages=True,  # has no effect for string prompts
)

async def main():
    t0 = time.monotonic()
    async for msg in query(prompt="call slow_tool with 30 seconds", options=options):
        # First event arrives only AFTER slow_tool completes + Claude responds
        print(f"+{time.monotonic()-t0:.1f}s: {type(msg).__name__}")

asyncio.run(main())

Expected: SystemMessage arrives within ~5s, StreamEvents stream during API response
Actual: First event arrives only after ~40-60s (tool execution + second API call complete)

Workaround

Wrap the string prompt in an AsyncIterable to use the non-blocking code path:

async def as_prompt_stream(text: str):
    yield {
        "type": "user",
        "session_id": "",
        "message": {"role": "user", "content": text},
        "parent_tool_use_id": None,
    }

# Use AsyncIterable instead of string:
async for msg in query(prompt=as_prompt_stream("call slow_tool"), options=options):
    print(type(msg).__name__)  # events stream immediately

Suggested fix

In process_query(), the string prompt path should match the AsyncIterable path — spawn wait_for_result_and_end_input as a background task instead of awaiting it inline:

if isinstance(prompt, str):
    await chosen_transport.write(json.dumps(user_message) + "\n")
    query.spawn_task(query._wait_and_end_input())  # background, not blocking

Related

  • #35077 — describes the same wait_for_result_and_end_input / firstResultReceivedResolve mechanism causing stdin closure issues with background subagents

Environment

  • claude-agent-sdk: 0.1.52
  • Python: 3.12
  • Platform: macOS (Darwin 25.3.0)

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗