[BUG] mcp headersHelper not re-invoked when access token expires on long-lived HTTP transport

Resolved 💬 3 comments Opened Apr 25, 2026 by duizabojul Closed Jun 10, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

headersHelper runs only when a new transport is created. The header it returns is cached on the transport for the transport's entire lifetime — which, for the http transport, is hours, because HTTP/2 keepalive keeps the connection up across long idle periods. When the underlying token expires (~5 min is typical for OIDC access tokens), subsequent requests get 401 from the upstream, and the SDK falls back to its OAuth auth provider instead of re-invoking the helper. If the upstream doesn't implement the MCP OAuth resource handshake, the user sees a Invalid OAuth error response and is stuck until they manually run /mcp → "Authentication cleared".

The doc at https://code.claude.com/docs/en/mcp#use-dynamic-headers-for-custom-authentication says:

The helper runs fresh on each connection (at session start and on reconnect). There is no caching, so your script is responsible for any token reuse.

That framing implies the helper runs often enough that token reuse is the script's responsibility. In practice, "on each connection" means once per transport, and the "no caching" line is misleading — there's effectively a multi-hour cache on the cached Authorization header, and the script can't refresh it because the SDK never asks again. A CLI that does silent token refresh internally (mine does) is moot, because its first invocation is also its last for the lifetime of the transport.

What Should Happen?

The cached header should not silently outlive the underlying token. Either the helper should be re-invoked when the upstream returns 401 (preferable, since headersHelper is a more specific configuration than the OAuth auth provider and should be treated as authoritative when both are present), or the docs should state explicitly that for the http transport, headersHelper runs once per transport and is unsuitable for tokens that expire faster than the transport's lifetime — which excludes the very example use cases the docs list ("Kerberos, short-lived tokens, or an internal SSO").

Error Messages/Logs

From `~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-<server>/<file>.jsonl`.

Initial connect (works):


Executing headersHelper to get dynamic headers
Successfully retrieved 1 headers from headersHelper
HTTP transport options: {... "Authorization":"[REDACTED]", "hasAuthProvider":true ...}
Successfully connected (transport: http) in 241ms
Connection established with capabilities: {... serverVersion: ...}


Next tool call ~1h46min later — note no second `Executing headersHelper` between:


Calling MCP tool: <name>
No token data found
Saving discovery state (authServer: https://internal.example.com/)
No client info found
HTTP connection dropped after 6371s uptime
Connection error: HTTP 404: Invalid OAuth error response: SyntaxError: JSON Parse error: Unexpected EOF. Raw body:
Tool '<name>' failed after 0s: HTTP 404: Invalid OAuth error response: ...


After `/mcp` → "Authentication cleared", the transport is torn down and rebuilt; the helper runs again and the next call succeeds:


HTTP transport closed/disconnected, attempting automatic reconnection
Initializing HTTP transport to ...
Executing headersHelper to get dynamic headers
Successfully retrieved 1 headers from headersHelper
Successfully connected (transport: http) in 114ms
HTTP reconnection successful after 142ms (attempt 1)

Steps to Reproduce

  1. Configure an HTTP MCP server in ~/.claude.json with a headersHelper that returns a short-lived bearer token (e.g. an OIDC access token, ~5 min TTL):

``json
"internal": {
"type": "http",
"url": "https://internal.example.com/mcp",
"headersHelper": "echo '{\"Authorization\": \"Bearer '\"$(mycli auth print-access-token)\"'\"}'"
}
``

  1. The upstream MCP server validates the bearer JWT and returns 401 when it's expired or invalid. It does not implement the MCP OAuth resource handshake (no WWW-Authenticate: Bearer resource_metadata=… on 401s, no /.well-known/oauth-protected-resource).
  2. Start a Claude Code session and call a tool on the MCP server. Works.
  3. Wait until the access token TTL elapses (~5 min in our case). Don't make any MCP calls in the meantime.
  4. Call any tool on the MCP server.

Expected: helper re-runs, fresh token is sent, request succeeds.

Actual: HTTP 404: Invalid OAuth error response: SyntaxError: JSON Parse error: Unexpected EOF. Raw body: with no Executing headersHelper line in the MCP log between step 3 and step 5.

Claude Model

None

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

2.1.119

Platform

Google Vertex AI

Operating System

macOS

Terminal/Shell

VS Code integrated terminal

Additional Information

_No response_

View original on GitHub ↗

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