MCP connect is issued before `headersHelper` resolves — slow helpers silently lose their auth header
Claude Code 2.1.266 (desktop, Windows 11 26200)
Summary
When an MCP server is configured with headersHelper, Claude Code issues the initial connect request without waiting for the helper process to finish. If the helper is still running, the request goes out with no Authorization header, the server answers 401/403, and the server is marked "requires authentication" for the entire session.
The helper is not cancelled — it completes moments later and returns a perfectly valid credential, which is then discarded. The documented 10-second helper timeout is never reached; the connect fires well before it.
Because the outcome depends on how fast each helper returns, a different subset of servers fails on each launch. Servers whose helpers happen to be fast bind normally, which makes this look like a per-server problem rather than a timing one. It cost me about a day of misdiagnosis — I was convinced one specific server was "immune" until I caught it failing too.
Evidence
Eight MCP servers, each with a headersHelper that fetches a bearer from AWS Secrets Manager via the AWS CLI. I instrumented one helper to append a timestamp and elapsed time on each invocation (the token value was never logged), then correlated against the reverse-proxy access log in front of the MCP servers.
One launch, single helper (openbrain):
| time (UTC) | event |
|---|---|
| 05:57:47.227 | helper invoked (pid 19512) |
| 05:57:52.593 | client POSTs to the MCP server → HTTP 403, no Authorization header |
| 05:57:52.728 | helper returns a valid token — 135 ms too late |
Helper elapsed: 5,501 ms. The client waited ~5.37 s and then sent the request anyway.
In the same 2 ms window, two other servers whose helpers had already finished sent a valid bearer and bound normally:
05:57:52.591 /mcp/truenas 400 <- valid bearer, no session yet -> binds
05:57:52.593 /mcp/cloudflare 400 <- valid bearer, no session yet -> binds
05:57:52.593 /mcp/openbrain 403 <- NO bearer -> needs-auth
05:57:52.593 /mcp/billcom 403 <- NO bearer -> needs-auth
05:57:52.709 /mcp/truenas-admin 403 <- NO bearer -> needs-auth
05:57:52.835 /mcp/openengine 403 <- NO bearer -> needs-auth
(These servers return 403 for a missing/invalid bearer and 400 Server not initialized for a valid bearer with no session yet, so the access log alone distinguishes "no credential" from "credential fine".)
The retry ~250 ms later fails identically, because the helper is still running.
On a different launch of the same config, all servers failed this way; on another, only one did. Every bearer tests valid by curl seconds afterwards.
Why helpers are slow
Each helper shells out to a cloud CLI. Measured on this machine: 1.7–2.0 s run alone, 2.0–3.8 s with all eight concurrent on an idle box, and 5.5 s during an actual app launch, when the helpers compete with Claude Code's own startup for CPU. Any helper that performs a network call (cloud secret manager, vault read, op read, gcloud) is in this range, so this is not an exotic configuration.
Expected behaviour
The connect should not be sent until the helper resolves or its timeout expires. Failing that, a connect rejected with 401/403 should be retried after the outstanding helper resolves, rather than retried immediately with the same missing header and then abandoned.
Suggested fixes
awaitthe helper before dispatching the first request (bounded by the existing 10 s timeout). Simplest and matches the documented contract.- If a pre-flight connect is wanted, treat "helper still pending" as not yet ready rather than no credential, and dispatch when it resolves.
- On 401/403, re-run the helper and retry after it returns; today's retry reuses the same pending state and fails identically.
- Surface it: a log line such as
headersHelper for <server> did not resolve before connectwould have made this self-diagnosing. Today nothing client-side records it — the only evidence is on the server.
Related: #84778 — because a failed attach at startup is terminal for the session, losing this race costs the whole session rather than a retry.
Workaround
Make the helper return in milliseconds so it always wins the race — e.g. cache the credential locally (encrypted at rest) and only call the secret manager when the cache is stale. That took the slowest helper from 5,501 ms to ~600 ms here, and the remaining cost is process startup rather than the credential fetch. This is a workaround for a timing bug, not a fix: any user whose helper is slower than the client's window still loses, silently.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗