HTTP MCP: SDK client does not attach configured --header on tool calls (Claude Code 2.1.114, Windows)
Summary
On a Windows machine running claude-code 2.1.114, an HTTP-transport MCP server configured via claude mcp add --transport http --header "Authorization: Bearer <token>" stores the header correctly (visible in claude mcp get), reports ✓ Connected in claude mcp list, and accepts the token on direct curl.exe calls. Tool calls initiated through Claude Code, however, return Unauthorized. The server access log shows the failing requests have a different user-agent suffix than the successful ones: claude-code/2.1.114 (sdk-cli) instead of claude-code/2.1.114 (cli). The (sdk-cli) POSTs arrive with no Authorization header the server can recognize. The (cli) POSTs on the same config at the same moment carry the header correctly and succeed.
Appears related to #48514 on macOS. Windows confirmation plus additional evidence below.
Environment
claude-code2.1.114 (stable)- Windows 10/11, PowerShell
- MCP server: HTTP streamable transport, Bearer token auth validated server-side by an auth resolver comparing the incoming
Authorization: Bearer <token>against static env-configured keys
Steps to reproduce
- Configure an HTTP MCP server with static Bearer auth:
claude mcp add example-hub -s user http://localhost:3007/mcp `
--transport http `
--header "Authorization: Bearer <token>"
- Verify the header is stored:
claude mcp get example-hub
Output includes:
URL: http://localhost:3007/mcp
Transport: http
Headers:
Authorization: Bearer <token>
(The header is shown verbatim, modulo #44888's unrelated redaction issue.)
- Verify the client reports connected:
claude mcp list
# -> example-hub: http://localhost:3007/mcp (HTTP) - ✓ Connected
- Close Claude Code completely. Relaunch. Do NOT run
/mcp. Ask for a tool that routes to this server.
- The tool call fails with
Unauthorized.
Expected
Tool call succeeds. The configured Authorization: Bearer <token> header is attached to the outbound POST /mcp request, same as it is for any other MCP client path that sends this header (including the (cli) user-agent path from the same binary).
Actual
Tool call fails. Server receives a request with no valid Authorization header.
Evidence
1. Server access log: side-by-side diff from the same client, same config, same version, 60 seconds apart
Only the user-agent suffix differs. Everything else, including the client IP, the endpoint, the configured MCP entry, the binary version, and the configured credentials, is identical:
<client-ip> - - [18/Apr/2026:11:52:40 -0500] "POST /mcp HTTP/1.1" 200 179 "-" "claude-code/2.1.114 (cli)"
<client-ip> - - [18/Apr/2026:11:52:40 -0500] "POST /mcp HTTP/1.1" 200 6766 "-" "claude-code/2.1.114 (cli)"
<client-ip> - - [18/Apr/2026:11:52:40 -0500] "POST /mcp HTTP/1.1" 202 0 "-" "claude-code/2.1.114 (cli)"
<client-ip> - - [18/Apr/2026:11:53:40 -0500] "POST /mcp HTTP/1.1" 401 97 "-" "claude-code/2.1.114 (sdk-cli)"
<client-ip> - - [18/Apr/2026:11:53:40 -0500] "GET /.well-known/oauth-authorization-server HTTP/1.1" 200 394 "-" "claude-code/2.1.114 (sdk-cli)"
<client-ip> - - [18/Apr/2026:11:53:40 -0500] "GET /.well-known/oauth-protected-resource HTTP/1.1" 200 177 "-" "claude-code/2.1.114 (sdk-cli)"
The (cli) POSTs at 11:52:40 return the full tool-list body (6766 bytes). The (sdk-cli) POST at 11:53:40 returns 401 (97 bytes, "error":"Unauthorized."). After the 401, (sdk-cli) fetches both .well-known endpoints (RFC 9728 protected-resource discovery) and eventually fails with:
Incompatible auth server: does not support dynamic client registration.
That RFC 9728 dead-end is downstream of the real defect. The first POST /mcp arrived without the configured Bearer credential at all. The server would have accepted it if the header had been attached, just like it accepted the (cli) POSTs a minute earlier.
2. Direct curl with the same token to the same endpoint succeeds
From the same PowerShell session, same host, same moment:
curl.exe -X POST http://localhost:3007/mcp `
-H "Authorization: Bearer $env:KEY" `
-H "Content-Type: application/json" `
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' `
-o probe.json -w "HTTP %{http_code} size %{size_download}`n"
# -> HTTP 200 size 6766
Token valid. Endpoint valid. Header format valid. The only variable between this working path and the failing Claude Code tool-call path is which HTTP client the request goes through inside the same claude-code binary.
3. Not a URL-scheme or localhost artifact
Initial hypothesis was that the (sdk-cli) path might drop credentials on non-HTTPS origins for security and succeed on http://localhost:* (trusted-origin short-circuit). It does not. The above failing repro is against http://localhost:3007/mcp, via an SSH tunnel set up specifically to test this theory. Same 401, same missing header. Bug triggers regardless of URL scheme, hostname, or origin trust.
4. claude mcp list and claude mcp get lie about the state
claude mcp list reports ✓ Connected, which is strictly a health probe that the (cli) path can complete successfully. That masks the defect. A user has no way to know from CLI diagnostics that tool calls will fail until they actually try one.
Related
- #48514 same bug on macOS with
headersandheadersHelper. Theplatform:macoslabel should widen. We confirm the same root symptom on Windows. - #44774 different surface (
headersHelpertransient failures poisoning keychain), same net effect: sdk-cli stops attaching credentials. - #47424 "Remote HTTP MCP server with Bearer token auth fails: OAuth flow attempted instead." Same symptom trajectory: Bearer config ignored, OAuth discovery attempted instead, bails.
- #39271 regression note: HTTP MCP Bearer worked in v2.1.81, broke in v2.1.83. Candidate bisection target for root-cause.
- #44888 separate but same config surface:
claude mcp getandclaude mcp add --headerleak Authorization values to stdout.
Workarounds
None work cleanly from the client side alone.
- Server-side query-string fallback. If the MCP server accepts the credential as a
?key=<token>(or equivalent) query parameter, putting it in the URL bypasses the bug. URLs are preserved through both paths. Only the header is dropped. This is a security regression: query-string secrets get logged to access logs and shell history. It only works when the specific server supports the fallback. Not suitable for any public MCP deployment. - Version pin. #39271 reports the Bearer path worked in 2.1.81. A downgrade may work. We have not verified this path ourselves.
- Alternate machine. The same MCP server works correctly from a macOS client running the same version. Windows users are blocked end-to-end on any HTTP MCP server that authenticates via static Bearer.
Suggested next steps for maintainers
- The
(sdk-cli)vs(cli)user-agent split in the same binary is the most actionable signal. Whichever internal construct produces the(sdk-cli)suffix is where the outbound-header collection is not being threaded through. claude mcp list's✓ Connectedprobe should run through the same code path as real tool calls, or warn when the probe path and the tool-call path would behave differently on auth.- The
platform:macoslabel on #48514 should widen. This is not macOS-specific.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗