anthropics / anthropics/claude-code

MCP connect is issued before `headersHelper` resolves — slow helpers silently lose their auth header

Open
#93,776 1 comment 0 reactions 0 assignees View on GitHub
area:mcp bug has repro platform:windows
Dominant language
Python
Stars
145k
Forks
23.1k
PR merge metrics
PR metrics pending

Description

**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

1. `await` the helper before dispatching the first request (bounded by the existing 10 s timeout). Simplest and matches the documented contract.
2. If a pre-flight connect is wanted, treat "helper still pending" as *not yet ready* rather than *no credential*, and dispatch when it resolves.
3. On 401/403, re-run the helper and retry **after** it returns; today's retry reuses the same pending state and fails identically.
4. Surface it: a log line such as `headersHelper for did not resolve before connect` would 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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Read the MCP connection startup path and trace how headersHelper completion is handled before the initial connect and retry. Reproduce with a helper delayed beyond the current request window, correlating helper completion with the outgoing Authorization header. Done means connect waits for the helper or its documented timeout, and a 401/403 retry does not reuse a still-pending credential.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws
Domain
api, authentication
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.