Race between initialize control_request and MCP refresh causes tools to drop from first request

Resolved 💬 2 comments Opened Apr 17, 2026 by cmd8 Closed May 26, 2026

What breaks

When the CLI is driven by the Agent SDK with sdkMcpServers configured, the first messages.create sometimes ships without the SDK MCP tools. The tools[] array then differs between Turn 1 and Turn 2 of the same conversation. Since tools[] is the first byte segment of Anthropic's prompt cache prefix, the cache misses on Turn 2.

In our runs this happens on roughly 50% of CLI spawns. Cache hit rate dropped from ~98% to ~82% on a workload where every turn should be a full prefix read.

Versions

  • @anthropic-ai/claude-code: reproduced on 2.1.77 and 2.1.110
  • @anthropic-ai/claude-agent-sdk-typescript: 0.2.108
  • Transport: stdin to CLI subprocess

Cause

The SDK writes two stdin payloads back-to-back with no await between them:

  1. { control_request: "initialize", sdkMcpServers: [...] }
  2. The first user message

Inside the CLI (minified symbols from cli.js):

  • dHA generator calls H6() unawaited at the top
  • H6 reads the MCP server map O at entry
  • If H6 runs before the CLI drains the initialize message, O is still {}
  • H6 skips the MCP refresh branch
  • The first messages.create is sent with no SDK MCP tools
  • Later the initialize message arrives, fills O, and a second H6 runs via the h6 handler, but the wire request has already been sent
SDK                                     CLI
---                                     ---
spawn CLI                    --->       boot, dHA starts
                                        H6() fires (unawaited), O={}, tools = N
write initialize to stdin    --->       buffered
write user message to stdin  --->       buffered

                                        CLI drains stdin. Two orderings:

                                        A) initialize first -> O populated ->
                                           h6 handler fires H6 -> tools = N+M
                                           request ships with N+M tools, cache OK

                                        B) user message first -> dHA continues
                                           with tools = N
                                           request ships with N tools, cache breaks

Proposed fix

Add an explicit gate inside dHA:

// At the top of dHA
let __initDone = false;
let __initResolve;
const __initPromise = new Promise(r => __initResolve = r);

// H6 awaits the gate before proceeding
async function H6() {
  if (!__initDone) {
    await __initPromise;
  }
  // existing H6 body follows
}

// initialize control_request handler resolves the gate
// after O is populated
else if (u6.request.subtype === "initialize") {
  if (u6.request.sdkMcpServers && u6.request.sdkMcpServers.length > 0) {
    // existing code that fills O
  }
  __initDone = true;
  __initResolve();
  // existing ack follows
}

Three edits. Promise resolves once per CLI run. H6 awaits it once; after that, __initDone=true short-circuits the await. No polling, no added latency on the passing path.

With this gate in place we got 10/10 passes on back-to-back runs where the old code was failing about half the time.

Reproduction

Any client that spawns the CLI via the Agent SDK with sdkMcpServers configured and issues two turns in the same conversation will hit Turn 2 cache misses on a large fraction of CLI spawns.

Related issues

  • #44869 (prompt cache not utilized, same class of symptom)
  • #44045 (a similar MCP timing hypothesis, later withdrawn by the author)
  • #41778, #35899 (MCP tools missing on first turn in remote triggers)
  • #42442, #49019 (stdio MCP servers: Connected but tools never register)

Notes

The same root mechanism also exists in the SDK: it sends both stdin messages without awaiting the initialize ack. A one-line SDK-side fix (await A.initialization in Rv inside assistant.mjs) is filed separately at anthropics/claude-agent-sdk-typescript#286. In our runs that fix alone passed 9/13 while the CLI gate passed 10/10, so there may be a second race we have not traced. Either way, the CLI gate above fixes what we can see.

View original on GitHub ↗

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