SDK headless: MCP tools silently missing on first turn when stdio server startup is slower than the (new) non-blocking pre-wait — regression for single-turn sessions since CLI 2.1.144
Environment
- claude-agent-sdk (Python) 0.2.114, bundled Claude Code CLI 2.1.205
- macOS (arm64), stdio MCP server (Python)
- Single-turn headless usage: one
client.connect()→ oneclient.query()→ stream → done
What happens
Since CLI 2.1.144 (changelog: "Improved SDK/headless MCP startup: pre-wait now overlaps startup instead of blocking before the first turn"), the first API request no longer waits for stdio MCP servers to finish connecting. The effective wait window is roughly ~2s.
If an MCP server takes longer than that to start (in our case: a Python stdio server cold-starting under macOS launchd background QoS consistently needs 2.4–3.1s), the first turn's tool snapshot is taken while the server is still pending:
- the model sees zero MCP tools and answers "I don't have that tool",
- no error or warning is emitted anywhere (the only trace is
status: 'pending'in the init message), - for single-turn sessions (bot/request-response backends) the tools are simply lost — there is no later turn for the late server to contribute to.
On ≤2.1.119 (SDK 0.1.68) the CLI blocked on MCP connection before the first turn (up to 30s), so the same setup worked for months. Cross-swapping bundled CLI binaries between SDK 0.1.68/0.2.114 (cli_path) moves the bug with the CLI version, and the SDK-side serialization is byte-identical — it's purely the CLI behavior change.
Minimal repro
slow_server.py — a trivial stdio MCP server that sleeps before serving:
import sys, time
time.sleep(3) # simulate slow cold start (e.g. background QoS, heavy imports)
# ... then run a minimal MCP stdio server exposing one tool `echo_probe` ...
client.py:
import anyio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
async def main():
options = ClaudeAgentOptions(
mcp_servers={"t": {"command": "python3", "args": ["slow_server.py"]}},
allowed_tools=["mcp__t__echo_probe"],
permission_mode="dontAsk",
max_turns=1,
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Use the echo_probe tool and report its output.")
async for msg in client.receive_response():
print(msg)
anyio.run(main)
- With
time.sleep(3): init arrives at ~2.4s with the serverpending, request body contains no MCP tools, the model answers it has no such tool. Reproduces 100%. - With
time.sleep(0)(or on CLI ≤2.1.119): serverconnected, tool is called normally.
Expected / requested
An option for SDK/headless single-turn usage to wait for configured MCP servers before the first turn, e.g. ClaudeAgentOptions(wait_for_mcp_servers=True) or a CLI flag — or restoring the blocking pre-wait for --print/SDK mode. Silent tool loss is very hard to diagnose (config, discovery and session UI all look correct; only the per-request tool snapshot is empty).
Workaround we ship
After client.connect(), poll client.get_mcp_status() until every configured server reports connected (15s cap, then proceed), before the first query(). Works, but every SDK consumer with a non-trivial stdio server on a loaded/QoS-limited host will hit this.