---
title: "MCP Headers Config Ignored When Server Has OAuth Discovery"
description: "Your MCP client found OAuth discovery metadata and started a token flow instead of sending the Authorization header you configured. The fix per client."
canonicalUrl: "https://zuplo.com/learn/mcp/errors/configured-headers-ignored-when-oauth-discovery-present"
pageType: "mcp-error"
errorString: "MCP headers config ignored when server has OAuth discovery"
lastVerified: "2026-07-27"
specVersion: "2026-07-28"
---

# `MCP headers config ignored when server has OAuth discovery`

> The credential is sitting in your client config and never reaches the wire: the client found OAuth discovery metadata on your server first, and switched to the OAuth flow instead of sending the header you set.

_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.

```bash
# 1. What does an unauthenticated request advertise?
curl -sS -D - -o /dev/null -X POST https://your-server.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
  | grep -i '^www-authenticate'

# 2. Does discovery metadata exist at either location?
for p in /.well-known/oauth-protected-resource/mcp \
         /.well-known/oauth-protected-resource \
         /.well-known/oauth-authorization-server; do
  printf '%s -> ' "$p"
  curl -sS -o /dev/null -w '%{http_code}\n' "https://your-server.example.com$p"
done
```

**This error** — `WWW-Authenticate: Bearer resource_metadata="…"`, or any `200` or `302` from 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. A `302` deserves 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.

**Working** — For a server you intend to authenticate with a static header: no `WWW-Authenticate` line at all, or one with no `resource_metadata` parameter, and `404` from all three well-known paths. Re-run the POST with your header: anything other than a `401` — a JSON-RPC result, or a `400` about a missing session ID — means the credential was accepted. For a server you intend to run the spec OAuth flow on, the mirror image is healthy — `resource_metadata` present and `200` from the protected-resource path. If this is what you see, this is not your problem.

**Clients also report this as:**

- `[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

**In short.** Pick one credential per route, and make the server advertise only that one. For a route authenticated by a static header, 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 no discovery document to find, 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.

The symptom is identical across platforms; the cause is not.

### Cursor

If you connect from Cursor and your server publishes OAuth discovery metadata

1. Cursor spells a static credential as `headers` on an `mcpServers` entry with a `url`, and resolves `${env:NAME}` in `command`, `args`, `env`, `url` and `headers`. That part is documented and correct — the problem is what wins.
2. 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 `Authorization` is already set. Staff re-confirmed it reproducing on 3.5.17 on 2026-05-22, with no fix shipped; re-test on your build before assuming it still bites.
3. The downstream symptom is often a registration error rather than an auth error — `Incompatible auth server: does not support dynamic client registration` — because Cursor is now running a flow you never asked for.
4. The workaround Cursor staff endorsed is server-side: return 404 for every OAuth discovery path on the route that expects a static header. The snippet below is 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 `auth` object with `CLIENT_ID` (and `CLIENT_SECRET`) to skip Dynamic Client Registration.

```text
# Caddy: force the client off the discovery path for this route
@oauth_discovery path /.well-known/oauth-protected-resource* /.well-known/oauth-authorization-server* /.well-known/openid-configuration*
respond @oauth_discovery 404

@mcp path /mcp*
handle @mcp {
  reverse_proxy https://your-mcp-server.internal
}
```

[Cursor: staff-confirmed bug report](https://forum.cursor.com/t/mcp-headers-config-ignored-when-server-has-oauth-discovery/156054)

### VS Code

If you configure the server in VS Code

1. Put the credential in `.vscode/mcp.json` or the user-profile `mcp.json`, under `servers.<name>.headers`, on an entry whose type is `http`. The documented form for a secret is an `inputs` entry referenced as `${input:<id>}` — VS Code prompts once and stores the value.
2. Do not use the open-plugins `.mcp.json` format for a header-authenticated server. It has no `inputs` support, which VS Code closed as out of scope because `.mcp.json` follows the open-plugins spec, and until 1.124.0 it discarded static `headers` outright — producing an OAuth registration prompt for a server that worked fine from the user `mcp.json`.
3. On 1.124.0 or later with the header still missing from the wire, run `Developer: Set Log Level > Trace` and read the MCP output. That is the exact check VS Code used to verify the fix.
4. Add an `oauth` object with `clientId` only when you actually want the OAuth flow; it is a separate field from `headers`, not a fallback for it.

```json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "api-token",
      "description": "MCP server API token",
      "password": true
    }
  ],
  "servers": {
    "internal-api": {
      "type": "http",
      "url": "https://mcp.internal.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${input:api-token}"
      }
    }
  }
}
```

[VS Code: MCP configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration)

### 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"`; `-t` and `-H` are the short forms. In `.mcp.json` the same thing is `headers` on an entry whose type is `http`, and `${VAR}` or `${VAR:-default}` expand from the environment.
2. The documented precedence is the opposite of Cursor's: if you configured `headers.Authorization` and the server rejects it, Claude Code reports the connection as failed instead of falling back to OAuth. A configured header that is not working therefore means a bad token or the wrong endpoint — or remove the header if you meant to use OAuth.
3. An entry with a `url` and no type is read as a stdio server and skipped, with `MCP server "<name>" has a "url" but no "type"; add "type": "http" (or "sse" / "ws") to this entry`. A header on a skipped server is never sent at all.
4. Reports that the header never reaches the wire have come in twice, and they ended differently: `anthropics/claude-code` #33817, for `headers`, was fixed and closed in April 2026, so upgrade before you debug it. #64894, for `headersHelper`, was closed as stale in July 2026 without a fix. Read your own server log to see which requests actually arrive before you go looking 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 static `headers` of the same name.

```bash
claude mcp add --transport http internal-api https://mcp.internal.example.com/mcp \
  --header "Authorization: Bearer $INTERNAL_API_KEY"
```

[Claude Code: MCP reference](https://code.claude.com/docs/en/mcp)

### Claude custom connectors

If the client is Claude, Claude Desktop, or Cowork connecting as a custom connector

1. A custom connector does take static headers — the Request headers section of the Add custom connector dialog, in beta at the time of writing and rolled out per customer. An organisation 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` and `x-auth-token`. Up to four.
2. Headers and OAuth are not exclusive: a header configured on an OAuth connection is sent alongside the bearer token, which is how a routing or tenant header reaches a gateway. `Authorization` is 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 your `Authorization` value is not sent.
3. So pick one per connection. For a shared service account, stop advertising OAuth discovery on that route and configure `authorization` as a request header. For per-user identity, use OAuth and move the static credential to a different header name.
4. Claude sends the value exactly as entered and adds no scheme, so an `authorization` header needs `Bearer ` typed in front of the token, including the space.
5. The connection is made from Anthropic's cloud, not from the machine running the app, so an internal server also has to be reachable from there.

[Claude: authenticating with request headers](https://claude.com/docs/connectors/custom/remote-mcp)

### MCP Python SDK

If your server is built on the MCP Python SDK and you want it to accept static keys

1. `token_verifier=` and `auth=AuthSettings(...)` are what publish discovery. Passing them mounts `/.well-known/oauth-protected-resource/mcp` and makes an unauthenticated request answer `401` with `WWW-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 no `auth=` at all.
3. Answer an unauthenticated request with a plain `401`, and if you send `WWW-Authenticate` at all, send `Bearer` with no `resource_metadata` parameter.
4. The class is `MCPServer` in v2 (`2.0.0rc1`) and `FastMCP` in the 1.x line, which is still the recommended production release; the `token_verifier=` / `auth=AuthSettings(...)` pair is the same in both.

```python
# Publishes discovery — clients will follow it instead of your header
mcp = MCPServer(
    "Notes",
    token_verifier=StaticTokenVerifier(),
    auth=AuthSettings(
        issuer_url=AnyHttpUrl("https://auth.example.com"),
        resource_server_url=AnyHttpUrl("https://mcp.example.com/mcp"),
        required_scopes=["notes:read"],
    ),
)

# Static-key server: no auth=, no token_verifier=, no well-known route
mcp = MCPServer("Notes")
```

[MCP Python SDK: authorization](https://py.sdk.modelcontextprotocol.io/v2/run/authorization)

### MCP TypeScript SDK

If your server is built on the MCP TypeScript SDK

1. Two calls put you in the discovery flow. `resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)` on `requireBearerAuth` is what stamps `resource_metadata` onto the 401 challenge, and the SDK docs say plainly that this challenge is what starts a client's OAuth flow.
2. `mcpAuthMetadataRouter({ oauthMetadata, resourceServerUrl })` is what serves the documents, at `/.well-known/oauth-protected-resource/mcp` and a root `/.well-known/oauth-authorization-server` mirror for clients that probe the origin directly. Mount neither on a route that expects a static key — verify the key in your own middleware and return a 401 with no `resource_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, Hono — the same two pieces are `requireBearerAuth` from `@modelcontextprotocol/server` and `oauthMetadataResponse`.

```ts
// This is the line that advertises discovery on the 401
const auth = requireBearerAuth({
  verifier,
  requiredScopes: ['mcp'],
  resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
});

// Static-key route: omit resourceMetadataUrl, and do not mount
// mcpAuthMetadataRouter on this path.
```

[MCP TypeScript SDK: require authorization](https://ts.sdk.modelcontextprotocol.io/v2/serving/authorization.html)

### Something else

Every client that can present a static header has to choose between it and whatever discovery says, and no spec 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 spec 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 — and the spec (revision 2025-11-25) says nothing at all about a static header the user configured, so each client picks its own precedence. Some probe discovery before they ever send the request your header would have ridden on; the server log shows it plainly as `GET /.well-known/oauth-authorization-server`, `GET /.well-known/oauth-protected-resource/mcp → 200`, and no `POST /mcp` carrying `Authorization` anywhere. The mirror image is the same bug from the other end: a server that has run on header auth for months breaks the day someone adds spec auth to it, because the new metadata is now the first thing every client finds.

### The exchange

The MCP client probes the well-known protected-resource path before it sends anything else. The server answers 200 with discovery metadata, so the client treats the server as an OAuth protected resource and starts a token flow. The configured Authorization header is never sent, and the POST that would have carried it is never made.

**Participants:** MCP client (Authorization header configured), MCP server (publishes OAuth metadata)

1. **MCP client → MCP server** — `GET /.well-known/oauth-protected-resource/mcp`
2. **MCP server → MCP client** — `200 OK` — `{"authorization_servers":["https://auth.example.com"]}` _(response)_
3. **MCP client** (internal step) — Treats the server as an OAuth protected resource — `headers.Authorization never sent` _(this step fails — the flow stops here)_

Notes:

- **Steps 1–2** — A client may probe the well-known paths before it sends anything else. A 200 from either of them is all it takes.
- **Step 3** — The client held the credential the whole time. The spec 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.

## What lands on you next

The lesson underneath this bug is that a route gets one credential story. Which raises the question of what the API behind it should see — and that is not the credential the client sent.

- **Our upstream API wants its own key, not the caller's token.** — `mcp-token-exchange-inbound` — The spec forbids forwarding an inbound token with a MUST NOT. Inbound auth headers are stripped and an independent upstream credential is minted per user or per gateway.
- **One upstream only takes a static bearer key. Does that break the pattern?** — `set-upstream-api-key-inbound` — No. 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 should this caller see at all?** — `mcp-capability-filter-inbound` — An allow-list per route, enforced before the request is forwarded — 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_invocation` — Each 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. See [Zuplo MCP Gateway](/mcp-gateway).

## Keep reading

- **Prevent it** — [API Key Authentication for MCP Servers](/learn/mcp/authentication/api-key) — Service-to-service calls where you control both the agent and the MCP server
- **Similar error** — [`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`](/learn/mcp/errors/401-without-www-authenticate) — Your server answers 401 correctly, but the response carries no pointer to protected resource metadata — so the client has nothing to discover the authorization server with, and the OAuth flow never starts.
- **Similar error** — [`Dynamic Client Registration not supported`](/learn/mcp/errors/dynamic-client-registration-not-supported) — The MCP client reached your authorization server's metadata and found no way to obtain a client ID — no registration endpoint, no Client ID Metadata Document support, and none configured in the client — so it stopped before the OAuth flow began.
- **Error reference** — [every documented MCP error](/learn/mcp/errors)
