[BUG] headersHelper output not applied to HTTP MCP requests -- SDK ignores tokens and falls through to OAuth 2.1 browser flow
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 is configured for an HTTP MCP server, the script runs successfully and returns valid JSON containing Authorization and API key headers, but those headers are never sent in any outgoing HTTP request.
Instead of using the headersHelper-provided credentials, the SDK ignores them and follows the full MCP OAuth 2.1 Authorization Code flow:
- POST /v1/mcp (no auth) -> 401
- GET /.well-known/oauth-authorization-server -> fails
- GET /.well-known/openid-configuration -> fails
- GET /.well-known/oauth-protected-resource -> fails
- POST /register (dynamic client registration) -> fails
This was captured via mitmproxy, which confirmed the headersHelper script is invoked at startup and produces correct output, but none of the Authorization or custom API key headers appear in any of the 6 requests the SDK makes before giving up and reporting the server as failed.
The result is that HTTP MCP servers using Bearer token or API key auth (the vast majority of existing APIs) cannot connect. headersHelper is the documented mechanism for this use case, but it has no effect on what headers are actually sent.
What Should Happen?
headersHelper output should be applied to outgoing HTTP MCP requests so that servers using pre-obtained Bearer tokens or API keys can authenticate without requiring the MCP OAuth 2.1 Authorization Code browser flow.
Specifically:
- The initial POST request to the MCP endpoint should include the headers returned by headersHelper (Authorization, X-Api-Key, etc.) before the server has a chance to return 401.
- If the server still returns 401 after headersHelper headers are applied, the SDK may fall back to OAuth discovery -- but headersHelper should be the first attempt.
- Servers that do not implement OAuth 2.1 discovery or dynamic client registration should be fully supported via headersHelper alone, without requiring browser-based login.
headersHelper is the correct and documented mechanism for machine-to-machine auth (client_credentials, API keys, pre-obtained tokens). It should work independently of whether the server implements the full MCP OAuth 2.1 spec.
Error Messages/Logs
MCP server status shown in /mcp:
Status: failed
Issue: Streamable HTTP error: Error POSTing to endpoint:
Auth: not authenticated
SDK auth failed: HTTP 403: Invalid OAuth error response:
SyntaxError: JSON Parse error: Unrecognized token '<'.
Raw body: <html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
</body>
</html>
headersHelper debug log (script runs and succeeds, but headers never reach the server):
[Tue Jun 2 12:20:25 PDT 2026] invoked TEA_ENV=dev CLIENT_ID=set CLIENT_SECRET=set API_KEY=set
[Tue Jun 2 12:20:26 PDT 2026] auth OK -- token prefix: eyJraWQiOiJ...
mitmproxy capture of all 6 requests -- none contain Authorization or API key headers:
POST /v1/mcp HTTP/1.1
Accept: application/json, text/event-stream
Content-Type: application/json
User-Agent: claude-code/2.1.161 (cli)
Host: api.example.com
--> 401 Unauthorized {"message":"Unauthorized"}
GET /.well-known/oauth-authorization-server HTTP/1.1
User-Agent: claude-code/2.1.161 (cli)
MCP-Protocol-Version: 2025-11-25
Host: api.example.com
--> 403 Forbidden {"message":"Forbidden"}
GET /.well-known/openid-configuration HTTP/1.1
User-Agent: claude-code/2.1.161 (cli)
MCP-Protocol-Version: 2025-11-25
Host: api.example.com
--> 403 Forbidden {"message":"Forbidden"}
GET /.well-known/oauth-protected-resource/v1/mcp HTTP/1.1
User-Agent: claude-code/2.1.161 (cli)
MCP-Protocol-Version: 2025-11-25
Host: api.example.com
--> 403 Forbidden {"message":"Forbidden"}
GET /.well-known/oauth-protected-resource HTTP/1.1
User-Agent: claude-code/2.1.161 (cli)
MCP-Protocol-Version: 2025-11-25
Host: api.example.com
--> 403 Forbidden {"message":"Forbidden"}
POST /register HTTP/1.1
Content-Type: application/json
User-Agent: claude-code/2.1.161 (cli)
Host: api.example.com
{"client_name":"Claude Code (my-server)","redirect_uris":["http://localhost:3118/callback"],
"grant_types":["authorization_code","refresh_token"],"response_types":["code"],
"token_endpoint_auth_method":"none"}
--> 403 Forbidden
Steps to Reproduce
- Create a headersHelper script that returns a Bearer token and API key:
#!/bin/bash
# get-token.sh -- fetches a token via OAuth2 client_credentials and returns
# it as JSON headers for the MCP SDK to use
TOKEN=$(curl -s -X POST "https://auth.example.com/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}" \
| jq -r '.access_token')
echo "{\"Authorization\": \"Bearer $TOKEN\", \"X-Api-Key\": \"${API_KEY}\"}"
- Verify the script produces valid output:
$ CLIENT_ID=xxx CLIENT_SECRET=yyy API_KEY=zzz bash get-token.sh
{"Authorization": "Bearer eyJ...", "X-Api-Key": "abc123"}
- Configure an HTTP MCP server in ~/.claude.json with headersHelper:
{
"mcpServers": {
"my-server": {
"type": "http",
"url": "https://api.example.com/v1/mcp",
"headersHelper": "env CLIENT_ID=xxx CLIENT_SECRET=yyy API_KEY=zzz /path/to/get-token.sh"
}
}
}
- The target MCP server requires Bearer token auth. It returns 401 for
unauthenticated requests. It does not implement OAuth 2.1 discovery
or dynamic client registration.
- Start Claude Code and run /mcp.
- Observe that the server shows as "failed" with "SDK auth failed".
- To confirm headersHelper runs but headers are not applied, intercept
traffic with mitmproxy:
Terminal 1:
mitmproxy --listen-port 8888
Terminal 2:
HTTPS_PROXY=http://localhost:8888 claude
- In mitmproxy, observe that all 6 requests to the MCP server are made
without any Authorization or X-Api-Key headers, despite headersHelper
running successfully and returning them.
Claude Model
Sonnet (default)
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.161 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
HEADERSHELPER IS INVOKED BUT OUTPUT IS DISCARDED
The script runs at startup (confirmed via debug logging in the script itself).
The token fetch succeeds. The script exits 0 with valid JSON on stdout.
The JSON is never applied to any outgoing request.
SDK FOLLOWS FULL MCP OAUTH 2.1 FLOW REGARDLESS OF HEADERSHELPER
The SDK always makes these 6 requests in sequence before giving up:
- Unauthenticated initialize
2-5. OAuth discovery endpoints (well-known)
- Dynamic client registration (/register)
headersHelper appears to have no effect on this flow.
THE FIRST REQUEST SHOULD CARRY HEADERSHELPER HEADERS
For servers using pre-obtained tokens (client_credentials, API keys), the first POST to the MCP endpoint should include the Authorization header. Getting a 401 on the first request triggers the OAuth dance unnecessarily.
FETCH API FORBIDDEN HEADERS
Testing showed that the User-Agent header provided via headersHelper is also not applied, which is consistent with Bun's fetch() treating User-Agent as a forbidden header per the WHATWG Fetch spec. It is unclear whether Authorization is also being silently dropped due to similar restrictions, or whether the headers are simply never passed to the HTTP client at all.
CONFIRMED WITH MITMPROXY
All traffic confirmed via mitmproxy (HTTPS_PROXY=http://localhost:8888).
The 6-request OAuth dance is consistent across multiple connection attempts.
Zero requests carry Authorization or X-Api-Key headers.
SERVER TYPE
The MCP server uses streamable HTTP transport in sync/stateless mode.
Each request is independent -- no session state, no SSE streaming.
The server works correctly when called directly with valid credentials (confirmed via curl and Node.js test scripts).
CLAUDE CODE VERSION TESTED
2.1.156, 2.1.160, 2.1.161 -- all exhibit the same behavior.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗