Streaming input dispatches turn 1 before the MCP server connects — a single-turn session never sees the tools
With the streaming-input form of query() (the async-generator prompt) and an HTTP MCP server configured, the first turn dispatches before the MCP server has finished connecting: turn 1's model call runs with the server still pending, so its tools are absent from that call's toolset. The tools appear only from turn 2 onward — which means a maxTurns: 1 session never sees them at all, and the model truthfully replies that it has no such tool.
The signal that gives it away
- The agent answers "I don't have a tool for that" (or lists no mcp__<server>__* tools when asked what it can see) — on a session that is single-turn by design. The same code with a multi-turn conversation works, because turn 2+ does have the tools; only the *first* turn is toolless. That "works when I keep chatting, fails when it's one-shot" asymmetry is the tell.
- Every health check on the MCP server passes. The server is reachable, it answers, it will connect — the tools are simply not attached *yet* at the instant the first model call is assembled. So the failure presents as the agent refusing work while all infrastructure looks green, which sends you debugging the server (auth, URL, transport) when the server is fine.
- Polling session.mcpServerStatus() concurrently confirms the race directly: at the moment turn 1 dispatches the status is pending, then it flips to connected a moment later. If you can log that timeline, you have proof it's ordering, not connectivity.
- It reproduces specifically with streaming input — the async-generator prompt that yields the first user message immediately. That immediate yield is the natural shape of a single-turn session, and it's exactly what loses the race: the generator hands the CLI a user message before MCP initialization has reached a terminal state.
Reported against @anthropic-ai/claude-agent-sdk 0.3.201 (Node 22 / Linux amd64, also reproduced on macOS arm64), with an MCP server of type: "http" (streamable HTTP): [anthropics/claude-agent-sdk-typescript#368](https://github.com/anthropics/claude-agent-sdk-typescript/issues/368).
Root cause
The first model call does not wait for configured MCP servers to reach a terminal status (connected or failed) before it dispatches. Under streaming input the first turn can fire immediately — the async generator yields the opening message while the server is still pending — so the model's toolset for that call is assembled without the server's tools. Nothing errors: from the model's point of view the tool genuinely does not exist for that call, so it answers accordingly. This is not a connectivity fault and not an application bug; it's an initialization-ordering gap between "first user message is ready to send" and "declared MCP servers have finished connecting."
How to fix it — block on readiness before the first yield
Until the SDK awaits MCP readiness itself, the consumer has to sequence it: don't yield the first user message until the server reports connected. Poll session.mcpServerStatus() in a bounded loop before the first turn, with a deadline so a genuinely dead server can't hang you forever, and reconnect once if the status comes back failed (per the reporter's workaround):
``javascript
async function waitForMcp(session, name, deadlineMs) {
const start = Date.now();
let reconnected = false;
while (Date.now() - start < deadlineMs) {
const status = await Promise.race([
session.mcpServerStatus()
.then((list) => list.find((s) => s.name === name)?.status)
.catch(() => undefined),
new Promise((resolve) => setTimeout(() => resolve(undefined), 1000)),
]);
if (status === "connected") return "connected";
if (status === "failed" && !reconnected) {
reconnected = true;
await session.reconnectMcpServer(name).catch(() => {});
}
await new Promise((r) => setTimeout(r, 300));
}
return "timed-out";
}
``
This is boilerplate every streaming-input consumer with MCP servers appears to need, and it costs a fixed poll loop on every session start — the reporter names both costs explicitly. The requested SDK-side fix is for the first model call to wait for configured MCP servers to reach a terminal status, or for the session to expose a promise you can await on for MCP readiness so the caller can sequence the first yield without polling.
What does NOT work
- Trusting the MCP server health check. The server passing every reachability/health probe tells you it *will* connect, not that it *has* connected before turn 1 was assembled. The whole bug lives in that gap; a green health check is exactly what makes teams debug the server instead of the ordering.
- Debugging the server transport, URL, or auth. The server is fine — it reaches connected moments later on its own. Time spent on connectivity is time spent on the wrong half of the race.
- A `maxTurns: 1` session and hoping. A single-turn session dispatches its one and only turn during the pending window and then ends; there is no turn 2 to inherit the tools. Single-turn is precisely the configuration that *never* recovers on its own — either raise readiness before the first yield or the tools are permanently absent for that session.
- Assuming the async generator yielding "later" saves you. Even code that looks like it does async work before yielding can still hand over the first message before MCP init reaches a terminal state; the fix is to *explicitly* gate the first yield on observed connected status, not to rely on incidental delay.
- Retrying the same turn without re-checking status. A blind retry that doesn't confirm the server reached connected (and doesn't reconnectMcpServer on failed) can just race again; the loop above is bounded and status-driven for that reason.
Related
- What doesn't work — lazy-initializing an error-tracking SDK to save boot time — the same shape of defect: a component initialized too late to be present for the first event, so the earliest work silently runs without it while every config/connectivity check passes. - Static verification doesn't prove runtime behavior — the SEED / ACTIVATE / VERIFY / NEGATIVE checklist — "the server is healthy" and "the tools were attached to this specific model call" are different questions; a passing health probe proves the first and says nothing about the second. - Subscriber connected, zero events — a 5-point checklist before you blame the hub — a sibling "connected, authenticated, and still nothing" streaming failure where a healthy-looking connection log is not proof the payload actually carried what the consumer needed. - The tool stays 'running' for hours: a synchronous LLM call with no timeout blocks the event loop — another agent-SDK tool-availability failure that surfaces as the agent silently not doing the tool-backed work, with no exception to point at. FONTI: ["https://github.com/anthropics/claude-agent-sdk-typescript/issues/368"]
Verified against
9 claims checked against these sources
Source: Sinapsi — verified compositional memory, queryable by LLMs. Query this wiki live from your assistant over MCP, or build your own verified wiki (public, or private for your team). CC BY 4.0 — reuse with attribution to Sinapsi.