[Bug] MCP Server Authorization Header Not Recognized, Falls Back to OAuth

Status Fixed / completed
Reported on v2.1.74
Maintainer reply None cached
Activity 11 comments · opened Mar 13, 2026 · closed Apr 22, 2026

Bug Description
mcp does not work anymore for servers that use Authorization header instead of OAuth, it refuses to obey an Authorization header now and tries to use oauth.

This worked yesterday just fine.

{
  "mcpServers": {
    "myserver": {
      "type": "http",
      "url": "http://some.internal.mcp.server.com/mcp/message",
      "headers": {
        "Authorization": "Bearer REDACTED"
      }
    }
  }
}

Environment Info

  • Platform: darwin
  • Terminal: iTerm.app
  • Version: 2.1.74
  • Feedback ID: 2b50b5dc-afb3-45de-b385-4263dd8cd4a7

Errors

[{"error":"Error: NON-FATAL: Lock acquisition failed for /Users/kevin/.local/share/claude/versions/2.1.74 (expected in multi-process scenarios)\n    at kvT (/$bunfs/root/src/entrypoints/cli.js:2644:2025)\n    at Cbq (/$bunfs/root/src/entrypoints/cli.js:2644:1169)\n    at processTicksAndRejections (native:7:39)","timestamp":"2026-03-13T01:03:24.698Z"},{"error":"Error: ripgrep exited with code null\n    at <anonymous> (/$bunfs/root/src/entrypoints/cli.js:100:18244)\n    at emit (node:events:98:22)\n    at #maybeClose (node:child_process:766:16)\n    at #handleOnExit (node:child_process:520:72)\n    at processTicksAndRejections (native:7:39)","timestamp":"2026-03-13T01:04:08.866Z"},{"error":"RipgrepTimeoutError: Ripgrep search timed out after 20 seconds. The search may have matched files but did not complete in time. Try searching a more specific path or pattern.\n    at $ (/$bunfs/root/src/entrypoints/cli.js:102:242)\n    at <anonymous> (/$bunfs/root/src/entrypoints/cli.js:102:456)\n    at <anonymous> (/$bunfs/root/src/entrypoints/cli.js:100:18315)\n    at emit (node:events:98:22)\n    at #maybeClose (node:child_process:766:16)\n    at #handleOnExit (node:child_process:520:72)\n    at processTicksAndRejections (native:7:39)","timestamp":"2026-03-13T01:04:08.867Z"}]

View original on GitHub ↗

11 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/2831
  2. https://github.com/anthropics/claude-code/issues/7290
  3. https://github.com/anthropics/claude-code/issues/29562

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

stantonk · 5 months ago

It's not a duplicate, this worked fine one day ago

0xhmn · 5 months ago

Same bug, confirmed from the server side. Custom Streamable HTTP MCP server (Python, uvicorn, ASGI) using the official mcp SDK's StreamableHTTPServerTransport. Configured with "type": "http" and bearer token via Authorization header.

The sequence:

  1. Claude Code sends GET /.well-known/oauth-authorization-server
  2. Server returns 404 (no OAuth implementation)
  3. Claude Code assumes default OAuth endpoints exist (/authorize, /token, /register), enters the OAuth flow
  4. Claude Code never sends POST /mcp

POST /mcp works fine when called directly:

curl -X POST http://localhost:8767/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}'

# 200 OK, valid MCP initialize result

The server returns 401 only on failed auth. On a valid request, it returns 200. The MCP spec says the client should try POST /mcp first and only enter OAuth on 401. A 404 on the well-known endpoint should not trigger it.

stantonk · 5 months ago

@0xhmn yep! this bug is maddening

stantonk · 5 months ago

Hmm, so it is now working after updating the Java MCP SDK from 0.16.0 to 0.17.1. It appears that claude code started sending a "form" field in the elicitation object during initialization:

{"capabilities": {"elicitation": {"form": {}}}}

MCP server "demo": HTTP Connection failed after 351ms: MCP error -32603:
Unrecognized field "form" (class io.modelcontextprotocol.spec.McpSchema$ClientCapabilities$Elicitation),
not marked as ignorable (0 known properties: ])

The MCP Java SDK 0.16.0 had the Elicitation record with zero fields — it only accepted {"elicitation": {}}. Jackson strict deserialization rejected the unknown form field, which got wrapped in a JSON-RPC error response.

@0xhmn not sure if this may be your issue or not!

0xhmn · 5 months ago

OK found a workaround:

Claude Code's HTTP transport omits the Accept: text/event-stream, application/json header when connecting to Streamable HTTP servers. My server rejected the request with 406 Not Acceptable (the MCP Streamable HTTP spec requires clients to accept both content types). Claude Code read that 406 as an auth failure and dropped into the OAuth flow:

# what Claude Code sends
curl -X POST http://localhost:8768/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"initialize","id":1,...}'
# → 406 Not Acceptable

Fix: Force the Accept header in .mcp.json:

{
  "mcpServers": {
    "my-server": {
      "type": "http",
      "url": "http://localhost:8768/mcp",
      "headers": {
        "Accept": "text/event-stream, application/json"
      }
    }
  }
}

With this change now I am getting 200:

curl -X POST http://localhost:8768/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","method":"initialize","id":1,...}'
# → 200 OK

After this change /mcp shows the server connected.

mkrisher · 5 months ago

I tried forcing the Accept header in a mcp server config in claude_desktop_config.json and it doesn't work. Upon start, the desktop app says the config is invalid.

shigechika · 4 months ago

Another workaround: mcp-stdio bridges stdio ↔ Streamable HTTP, so Claude Code never attempts OAuth discovery or touches HTTP headers directly.

claude mcp add myserver \
  -e MCP_BEARER_TOKEN=YOUR_TOKEN \
  -- mcp-stdio http://some.internal.mcp.server.com/mcp/message

This sidesteps both issues discussed here:

  • Accept header — mcp-stdio sends Accept: application/json, text/event-stream by default
  • Bearer token ignoredMCP_BEARER_TOKEN env var is sent as Authorization: Bearer ... on every request
  • OAuth fallback — stdio servers skip OAuth discovery entirely

Works with pip install mcp-stdio, uvx mcp-stdio, or brew install shigechika/tap/mcp-stdio.

yesnickcarter · 4 months ago

_Claude Code help me write up a bug report. I found this one, so I'll add all the details here._

Title

StreamableHTTPClientTransport does not apply requestInit headers to MCP requests — custom Authorization headers are ignored in interactive sessions

Description

When configuring an HTTP MCP server in ~/.claude.json with custom headers (e.g., Bearer token auth), the headers are sent correctly by claude mcp list but are never sent by interactive Claude Code sessions. This makes it impossible to connect to HTTP MCP servers that use API key / Bearer token authentication (as opposed to OAuth).

Root Cause

The StreamableHTTPClientTransport class in the MCP TypeScript SDK creates two fetch wrappers:

  • this._fetch — the raw fetch function (no custom headers)
  • this._fetchWithInit — fetch wrapped with requestInit headers via createFetchWithInit()

The transport uses this._fetch for all actual MCP requests (GET SSE probe, POST messages, DELETE):

// streamableHttp.js line 89 (GET)
const response = await (this._fetch ?? fetch)(this._url, { method: 'GET', headers, ... });

// streamableHttp.js line 306 (POST)
const response = await (this._fetch ?? fetch)(this._url, init);

But it only passes this._fetchWithInit to the OAuth auth flow:

// streamableHttp.js line 46
result = await auth(this._authProvider, { ..., fetchFn: this._fetchWithInit });

This means custom requestInit headers (including Authorization) are never included in actual MCP requests — only in OAuth discovery/registration requests.

Interaction with authProvider

Claude Code provides a built-in authProvider for all HTTP MCP transports (to support OAuth servers like Gmail/Calendar). When an authProvider is present:

  1. _commonHeaders() checks this._authProvider.tokens() — no tokens on first request, so no Authorization header is set
  2. The POST goes out without the custom Authorization header
  3. Server returns 401 (missing auth)
  4. The SDK enters the OAuth flow (because authProvider exists and status is 401)
  5. OAuth fails (no OAuth server exists)
  6. Connection shows "needs authentication"

claude mcp list works because it uses a separate code path that sends the custom headers directly (confirmed via server-side logging — the health check POST includes the Bearer token with UA claude-code/2.1.104 (cli), while the interactive session sends OAuth discovery requests with UA Bun/1.3.12 and no Authorization header).

Steps to Reproduce

  1. Deploy any HTTP MCP server that requires Bearer token auth (returns 401 without valid Authorization header)
  2. Configure it in ~/.claude.json:

``json
{
"mcpServers": {
"my-server": {
"type": "http",
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer my_api_key"
}
}
}
}
``

  1. Run claude mcp list — shows "Connected"
  2. Start an interactive claude session — shows "needs authentication"

Expected Behavior

Custom headers from the MCP config should be included in all HTTP requests to the MCP server, not just health checks.

Suggested Fix (SDK)

In StreamableHTTPClientTransport, use this._fetchWithInit instead of this._fetch for all MCP requests:

- const response = await (this._fetch ?? fetch)(this._url, init);
+ const response = await (this._fetchWithInit ?? this._fetch ?? fetch)(this._url, init);

Or, ensure _commonHeaders() merges headers from requestInit when no OAuth tokens are available.

Workaround

Pass the API key as a URL query parameter instead of a header:

{
  "mcpServers": {
    "my-server": {
      "type": "http",
      "url": "https://api.example.com/mcp?key=my_api_key"
    }
  }
}

The server must extract the key from the query string. This is not ideal for production (URLs are logged by proxies, CDNs, and load balancers) but is the only workaround until the SDK is fixed.

Environment

  • Claude Code v2.1.104
  • MCP TypeScript SDK (version bundled with Claude Code)
  • macOS, CLI terminal
  • Server: Hono on Node.js, deployed to Railway behind Cloudflare

Evidence

Server-side logs showing the difference:

# claude mcp list — headers included, works
[MCP Auth] POST /mcp | Auth: Bearer ip_live_5bXb0... | UA: claude-code/2.1.104 (cli)

# Interactive session — no auth, enters OAuth discovery
[MCP Auth] GET /mcp/.well-known/openid-configuration | Auth: NONE | UA: Bun/1.3.12
yesnickcarter · 4 months ago

_I am building an MCP server with API Token authentication, which is how I found this bug. I used the MCP Builder Skill from Anthropic and still couldn't get my server to work with Claude Code. I fed one of the workarounds above to Claude Code, and it made the following change to the MCP server code, which seems to give me a fix without putting the API Key in the URL._

---
The fix: Return 405 on unauthenticated GET to /mcp. The SDK skips SSE, sends POST with the configured Authorization header, auth succeeds. Standard header-based auth, no keys in URLs.

⏺ The fix: return 405 on any GET to /mcp that doesn't have an Authorization header.

In src/mcp/index.ts, before the auth middleware:

  mcpRouter.use("*", async (c, next) => {                                                                                            
    if (c.req.method === "GET" && !c.req.header("Authorization") && !c.req.query("key")) {
      return c.text("", 405);                                                                                                        
    }                                                                                                                                
    return next();                                                                                                                   
  });         

Why it works: The SDK sends custom headers from the client config on POST but not on the initial GET (SSE probe). Without this fix, the unauthenticated GET hits the auth middleware → 401 → triggers OAuth. The SDK specifically treats 405 as "server doesn't support SSE GET" and gracefully skips to POST, which does include the configured Authorization header.

github-actions[bot] · 4 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.