MCP connect has no jitter under concurrent sessions, and ToolSearch then reports the failed servers as nonexistent

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Aug 3, 2026

Summary

Two linked defects turn a transient MCP connection slowdown into a permanent, silent loss of capability that the model then reasons over incorrectly.

  1. There is no jitter anywhere in the MCP connect or retry path. Concurrent sessions all dispatch their connects at t=0 and all retry at the same three instants, amplifying the congestion they are retrying against.
  2. When a server consequently fails to connect, ToolSearch reports it identically to a tool that does not exist. The session concludes the capability is unavailable and routes around it.

The second is the damaging one: it produces wrong decisions rather than merely delay. In our incident the assistant was moments from abandoning a queued-message task and rebuilding it against a raw HTTP endpoint, while the server in question was healthy and actively serving other clients.

Environment: Claude Code 2.1.220, native binary install, Windows 11, ~17 configured MCP servers (mix of stdio and HTTP).

1. No jitter on connect or retry

Initial connects dispatch both transport batches together:

Promise.all([
  Fee(servers.filter(isLocal),  I, { concurrency: getMcpServerConnectionBatchSize() }),        // default 3
  Fee(servers.filter(!isLocal), I, { concurrency: getRemoteMcpServerConnectionBatchSize() }),  // default 20
])

Fee is a vendored p-map — a concurrency-capped pool that starts each item the instant a slot frees, with no per-item delay. With 11 HTTP servers against a remote cap of 20, the cap never binds and every connect fires simultaneously.

The retry schedule is a hardcoded array consumed without modification:

VlE = [500, 1500, 4000]
async function Qyl(...) { for (let i of VlE) { await vr(i); ... } }

vr is a plain sleep. No Math.random, no scaling, at either the call site or the helper. So N concurrent sessions each retry every failing server at exactly 500ms, 1500ms and 4000ms — in lockstep, onto the same hosts that are already congested.

Worth noting the codebase clearly knows about jitter: it is implemented in at least four other subsystems (the cron scheduler's getCronJitterConfig, remote-bridge heartbeat/OAuth heartbeatJitterFraction, telemetry upload retry, and a bundled grpc-js BackoffTimeout). The MCP path appears to be a straightforward omission rather than a deliberate choice.

Two further gaps in the same area:

  • Retry is remote-only. stdio servers get no initial-connect retry at all.
  • The total retry budget is ~6s, against connects we measured at 13–26s under load. It cannot succeed in the case it exists for.

2. ToolSearch reports a failed server as a nonexistent tool

ToolSearchTool does wait for connecting servers, as documented. Reading the implementation: if the initial snapshot search returns zero matches and the query is judged relevant to a server currently in "pending" state, it polls the live registry every 50ms for up to 5s, then re-runs the matcher against refreshed tool lists.

The wait is gated on pending. A server that has already failed is not pending, so the branch is skipped entirely and the bare No matching deferred tools found returns instantly — byte-identical to the result for a tool that never existed.

Pending servers are named in the result (Some MCP servers are still connecting: X). Failed servers are not named at all. From the model's perspective there is no signal distinguishing "temporarily unavailable, wait" from "does not exist, give up", and it reliably chooses the latter.

Measured evidence

Logs at %LOCALAPPDATA%\claude-cli-nodejs\Cache\<project>\mcp-logs-<server>\<ISO>.jsonl.

Session start 02:58:04Z. Roughly 14 sessions started within the same hour, each attaching ~17 servers concurrently — on the order of 240 simultaneous connects.

02:58:13Z  reachy (type: http, http://127.0.0.1:8775/mcp/) connect begins
02:58:43Z  "Connection timeout triggered after 30005ms" -> "Connection failed"
           log ends; no further entries for this server

The same server, the same morning, in sibling sessions: successful connects of 26.2s and 13.4s. Also in the failing session: RapidMCP 18.5s (succeeded), chrome-devtools 17.2s (succeeded). So the 30s MCP_TIMEOUT default is not a generous margin under load — it is a coin flip.

This is a loopback server and it was demonstrably healthy throughout: its own health endpoint returned 200 continuously across the window, netstat showed the listening socket plus established connections from other client processes, and a hand-driven initialize + tools/list against it returned all 27 tools in 81ms.

Six ToolSearch calls over the following ~50 minutes all returned the bare message. The server did eventually attach at roughly the 50-minute mark with no intervention.

Requests, in priority order

  1. Add jitter to initial connect and to the retry schedule. Cheapest fix, needs no configuration, and helps every user rather than only those who discover MCP_TIMEOUT. Existing precedent in four other subsystems in the same codebase.
  1. Name failed servers in the empty ToolSearch result, as pending ones already are. A single clause — "the following servers failed to connect: X" — removes the wrong-decision failure mode entirely. This is small and high-impact.
  1. Give initial connect a retry budget that can actually succeed, and extend it to stdio. ~6s against 13–26s connects cannot work.
  1. Document the MCP tuning variables. MCP_TIMEOUT, MCP_CONNECT_TIMEOUT_MS, MCP_SERVER_CONNECTION_BATCH_SIZE, MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE and MCP_CONNECTION_NONBLOCKING all exist and none appear in the public docs. MCP_TIMEOUT's 30000ms default is the direct cause here and there is no documented way to discover it.

Related

  • #42148 (closed, not planned) — per-turn frozen deferred-tool snapshot. The wait does refresh in 2.1.220, so that specific mechanism looks changed, but the user-visible symptom persists via the pending-state gate described above.
  • #76239 (open) — slow-connecting server tools silently dropped.
  • #16837 (open) — MCP_TIMEOUT clamped above 60s.
  • #43389 (open) — no --json on claude mcp list, which makes a scripted pre-flight readiness check awkward to parse.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗