On this page
Client runs OAuth instead of your header
MCP headers config ignored when server has OAuth discovery Your credential never reaches the wire: the client found OAuth discovery metadata on the server first and started the OAuth flow instead of sending the header you configured.
- MCP specification
- 2026-07-28
Is this you?
Ask your server the two questions a client asks before it decides which credential to use. If either answer points at OAuth, the client has a discovery document to follow and your configured header loses.
- This error
WWW-Authenticate: Bearer resource_metadata="…", or any200or302from a well-known path, while your client is configured with a header. That is a discovery document the client will follow instead of your credential. A302deserves a second look: a catch-all route or an SPA fallback that redirects unknown paths reads as discovery to a client even though you never implemented OAuth.- Then the fix is the next section.
- Working
- For a server you intend to authenticate with a static header: no
WWW-Authenticateline at all, or one with noresource_metadataparameter, and404from all three well-known paths. Re-run the POST with your header: anything other than a401— a JSON-RPC result, or a400about a missing session ID — means the credential was accepted. For a server you intend to run the specification's OAuth flow on, the mirror image is healthy —resource_metadatapresent and200from the protected-resource path. - Then this isn't your problem — try the error reference.
5 other strings clients print for this
- [Bug] MCP Server Authorization Header Not Recognized, Falls Back to OAuth
- it refuses to obey an Authorization header now and tries to use oauth
- .mcp.json ignores headers field for HTTP MCP servers
- MCP server with basic auth headers works in user mcp.json but triggers OAuth flow when configured via extension .mcp.json
- Error: HTTP 403: Invalid OAuth error response: SyntaxError: JSON Parse error: Unrecognized token '<'.
Fix it
Pick one credential per route and make the server advertise only that one. For a header-authenticated route, answer an unauthenticated request with a bare 401 carrying no resource_metadata parameter, and return 404 from /.well-known/oauth-protected-resource, from that path with your MCP route appended, and from /.well-known/oauth-authorization-server; with nothing to discover, a client that probes falls back to the header you configured. If the OAuth flow is what you actually want, keep the metadata and take the header out of the client config instead — one path cannot reliably offer both.
Have your agent fix this
Copy a prompt that sends your coding agent to this page to find and apply the right fix for your project.
The cause depends on your platform. Pick yours.
Where are you fixing this?
Cursor
If you connect from Cursor and your server publishes OAuth discovery metadata
- 1 Set the credential as
headerson anmcpServersentry with aurl. Cursor resolves${env:NAME}incommand,args,env,url, andheaders. That part is documented and correct — the problem is what wins. - 2 Expect discovery to win. Cursor staff confirmed the precedence on the bug report: when the server answers the OAuth discovery endpoints, Cursor starts the OAuth flow before it sends the POST that would have carried your header, and there is no logic to skip OAuth when
Authorizationis already set. Staff re-confirmed it reproducing on 3.5.17 on 2026-05-22, with no fix shipped, so re-test on your build before assuming it still bites. - 3 Read the downstream symptom as this bug. It surfaces as a registration error rather than an auth error —
Incompatible auth server: does not support dynamic client registration— because Cursor is running a flow you never asked for. - 4 Return 404 for every OAuth discovery path on the route that expects a static header. This is the server-side workaround Cursor staff endorsed, adapted from the Caddyfile in the thread.
- 5 If what you actually want is OAuth and the failure is registration rather than the header, add an
authobject withCLIENT_ID(andCLIENT_SECRET) to skip Dynamic Client Registration.
VS Code
If you configure the server in VS Code
- 1 Put the credential in
.vscode/mcp.jsonor the user-profilemcp.json, underservers.<name>.headers, on an entry whose type ishttp. The documented form for a secret is aninputsentry referenced as${input:<id>}— VS Code prompts once and stores the value. - 2 Don't use the open-plugins
.mcp.jsonformat for a header-authenticated server. It has noinputssupport, which VS Code closed as out of scope because.mcp.jsonfollows the open-plugins specification, and until 1.124.0 it discarded staticheadersoutright — producing an OAuth registration prompt for a server that worked fine from the usermcp.json. - 3 On 1.124.0 or later with the header still missing from the wire, run
Developer: Set Log Level > Traceand read the MCP output. That is the exact check VS Code used to verify the fix. - 4 Add an
oauthobject withclientIdonly when you actually want the OAuth flow; it is a separate field fromheaders, not a fallback for it.
Claude Code
If you connect from Claude Code
- 1 Add the header at the CLI with
claude mcp add --transport http <name> <url> --header "Authorization: Bearer your-token";-tand-Hare the short forms. In.mcp.jsonthe same thing isheaderson an entry whose type ishttp, and${VAR}or${VAR:-default}expand from the environment. - 2 Read a failed connection as a bad credential, not as a fallback. Claude Code's documented precedence is the opposite of Cursor's: if you configured
headers.Authorizationand the server rejects it, Claude Code reports the connection as failed instead of falling back to OAuth. So a configured header that isn't working means a bad token or the wrong endpoint — and if you meant to use OAuth, remove the header. - 3 Set
"type": "http"on the entry. An entry with aurland no type is read as a stdio server and skipped, withMCP server "<name>" has a "url" but no "type"; add "type": "http" (or "sse" / "ws") to this entry, and a header on a skipped server is never sent at all. - 4 Upgrade before you debug further. Reports that the header never reaches the wire have come in twice and ended differently:
anthropics/claude-code#33817, forheaders, was fixed and closed in April 2026; #64894, forheadersHelper, was closed as stale in July 2026 without a fix. Read your own server log to see which requests arrive before you look for a server-side cause. - 5 For a token that must be minted per connection use
headersHelper, a command that prints a JSON object of headers to stdout. Dynamic headers override staticheadersof the same name.
Claude custom connectors
If the client is Claude, Claude Desktop, or Cowork connecting as a custom connector
- 1 Enter the credential in the Request headers section of the Add custom connector dialog, which is in beta and rolled out per customer. An organization administrator enters the value once, Claude sends it verbatim on every request, and the name has to come from an allowlist that includes
authorization,x-api-key, andx-auth-token. Up to four. - 2 Keep headers and OAuth together where you can: a header configured on an OAuth connection is sent alongside the bearer token, which is how a routing or tenant header reaches a gateway.
Authorizationis the documented exception — "OAuth owns that header, so it cannot be configured as a request header on an OAuth connection". That is this error, in the one place the vendor writes it down: if the server advertises discovery, the connector authenticates with OAuth and yourAuthorizationvalue is not sent. - 3 So pick one per connection. For a shared service account, stop advertising OAuth discovery on that route and configure
authorizationas a request header. For per-user identity, use OAuth and move the static credential to a different header name. - 4 Type
Bearerin front of the token, including the space. Claude sends the value exactly as entered and adds no scheme. - 5 Make the server reachable from Anthropic's cloud, not only from the machine running the app — that is where the connection comes from.
MCP Python SDK
If your server is built on the MCP Python SDK and you want it to accept static keys
- 1 Find the two arguments that publish discovery:
token_verifier=andauth=AuthSettings(...). Passing them mounts/.well-known/oauth-protected-resource/mcpand makes an unauthenticated request answer401withWWW-Authenticate: Bearer error="invalid_token", …, resource_metadata="…". That pointer is exactly what clients follow instead of your header. - 2 Drop both. They have to travel together — pass one without the other and the constructor raises
ValueError— so an API-key check belongs in ASGI middleware in front of the MCP app, with noauth=at all. - 3 Answer an unauthenticated request with a plain
401, and if you sendWWW-Authenticateat all, sendBearerwith noresource_metadataparameter. - 4 Expect the same pair in either version. The class is
MCPServerin v2, the current stable line, andFastMCPin 1.x, which now takes bug fixes and security patches only —pip install mcpgives you 2.x, so pinmcp>=1.28,<2if you have not migrated.token_verifier=/auth=AuthSettings(...)behaves identically in both.
MCP TypeScript SDK
If your server is built on the MCP TypeScript SDK
- 1 Find the two calls that put you in the discovery flow.
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)onrequireBearerAuthstampsresource_metadataonto the 401 challenge, and the SDK docs say plainly that this challenge is what starts a client's OAuth flow. - 2 Mount neither document on a route that expects a static key.
mcpAuthMetadataRouter({ oauthMetadata, resourceServerUrl }), from@modelcontextprotocol/express, serves them at/.well-known/oauth-protected-resource/mcpand a root/.well-known/oauth-authorization-servermirror for clients that probe the origin directly. Verify the key in your own middleware instead, and return a 401 with noresource_metadata. - 3 For a server that must serve both, keep the metadata router and the token-gated route on one path and the key-checked route on another. Then confirm the root authorization-server mirror is not answering for the key-checked path, because a root document is found for every path on the host.
- 4 On web-standard hosts — Workers, Deno, Bun, and Hono — the same two pieces live in
@modelcontextprotocol/server:requireBearerAuthunder that same name, andoauthMetadataResponsein place of the router. Note v1 shipped as one@modelcontextprotocol/sdkpackage, still at 1.30.0, while v2 split into@modelcontextprotocol/clientand@modelcontextprotocol/server.
Something else
Every client that can present a static header has to choose between it and whatever discovery says, and no specification text settles the order — so fix this on the server, not in the client. Decide per route: a route authenticated by a static header publishes no protected-resource metadata and puts no resource_metadata on its 401, and a route running the specification's OAuth flow does both. Then check whatever sits in front of the server — a proxy, an ingress rule, framework middleware — is not serving a root /.well-known/oauth-protected-resource document, because a root document is found for every path on the host.
Why it happens
Discovery is keyed on your server, not on your config. A WWW-Authenticate challenge carrying resource_metadata, or a 200 from /.well-known/oauth-protected-resource, tells a client to treat the server as an OAuth protected resource and go get a token.
The specification (revision 2025-11-25) says nothing about a static header the user configured, so each client sets its own precedence. Some probe discovery before they ever send the request your header would have ridden on, and the server log shows it: GET /.well-known/oauth-authorization-server, GET /.well-known/oauth-protected-resource/mcp → 200, and no POST /mcp carrying Authorization anywhere.
The same bug arrives from the other end too — a server that has run on header auth for months breaks the day someone adds specification auth to it, because that metadata is now the first thing every client finds.
The exchange
- MCP client Authorization header configured
- MCP server publishes OAuth metadata
-
A client might probe the well-known paths before it sends anything else. A 200 from either of them is all it takes.
MCP client to MCP server
GET /.well-known/oauth-protected-resource/mcp -
MCP server to MCP client
200 OK {"authorization_servers":["https://auth.example.com"]} -
The client held the credential the whole time. The specification says nothing about whether a user-configured header outranks discovered metadata, so each client decides for itself — and the ones that probe first never send the POST the header would have ridden on.
Inside MCP client
Treats the server as an OAuth protected resource (this step fails) headers.Authorization never sentFlow stops here
A route gets one credential story, which leaves the question of what reaches the API behind it — not the credential the client sent.
-
Our upstream API wants its own key, not the caller's token.
-
mcp-token-exchange-inboundThat's what it gets: inbound auth headers are stripped and an independent upstream credential is minted per user or per gateway. Forwarding the caller's token is what the specification forbids with a MUST NOT.
-
One upstream only takes a static bearer key. Does that break the pattern?
-
set-upstream-api-key-inboundNo. The route strips the inbound token and injects the upstream key, so a key-only server sits behind the same sign-in as the rest.
-
Which of our tools can this caller see at all?
-
mcp-capability-filter-inboundOnly what the route lists, enforced before the request is forwarded — so a read-only view and a full-power view of one upstream are two routes, not two code paths.
-
Which credential did that call actually use?
-
capability_invocationEach event names the caller, the capability, the upstream, and the outcome, so a credential problem resolves to a request rather than to a guess.
All of these attach to one MCP route's policies.inbound — the same policy engine on the way in and on the way out. Your MCP server keeps
the code and the authentication it has today.
Stop debugging auth on every MCP server
A gateway in front of your MCP servers handles discovery, audience binding, and token validation once — for every server and every client.