Zuplo
MCP error

Authorization with the MCP server failed

A token was issued and the client says it is connected, but the credential never reaches your server intact: either the client does not attach it, or it carries an audience your server is right to reject.

MCP Specification
2026-07-28

Is this you?

Replay the exact token your server was handed. Take it from your token endpoint's log, and POST it to the URL you registered with the client — not the URL your server thinks it lives at. One response separates a credential the client never sent from a credential your server refuses.

TerminalRun this to check — it changes nothing
curl -sS -i -X POST 'https://your-server.example.com/mcp' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
This error
Two shapes. A 301, 302, 307 or 308 with a Location on a different host — the hop that drops your Authorization header. Or 401 with WWW-Authenticate: Bearer error="invalid_token": the token was minted for someone else. Decode it and compare aud and iss against the canonical MCP server URL and the issuer you advertise.
Then the fix is the next section.
Working
Anything that is not an auth challenge. HTTP/1.1 200 with a JSON-RPC result on a stateless server; on a server that keeps sessions, a 400 complaining about a missing Mcp-Session-Id — this curl skips initialize, and the spec has such servers answer 400. Either way your validator accepted the token, so the credential is being lost on the way in rather than rejected on arrival. Look at the redirect and at what the client attaches, not at your validator.
Then this is not your problem — try the error reference.
6 other strings clients print for this
  • Authorization with the MCP server failed. You can check your credentials and permissions. If this persists, share this reference with support: "ofid_..."
  • UnauthorizedError: Unauthorized
  • {"error":"invalid_token","error_description":"Authentication required"}
  • Missing Authorization header
  • AADSTS9010010
  • invalid_target

Fix it

Replay an access token your authorization server issued against the exact URL you registered with the client. If that URL answers a 301, 302, 307 or 308 pointing at a different origin, register the redirect target instead — standard HTTP clients drop Authorization on a cross-origin hop. If it answers 401 with error="invalid_token", the token did arrive: decode it and check aud and exp before anything else. If aud names another service, audience-restrict the token at the authorization server to the canonical MCP server URL the client sends as the RFC 8707 resource parameter, rather than loosening the aud check in your server. If the replay succeeds and the server works only for whoever added the connector, nothing is broken there — each user has to authorize separately.

Have your agent fix this

Copies a prompt that sends your coding agent to this page, then has it find and apply the right fix for your project.

The cause depends on your platform. Pick yours.

Claude custom connectors

If the connector was added in Claude.ai, Claude Desktop or Cowork and reports itself connected

  1. 1 Claude follows redirects, so a URL that answers 301, 302, 307 or 308 to a different host — apex to www., region routing, a vanity domain in front of a CDN — loses the Authorization header on the hop. Anthropic documents this as a cause of Authorization with the MCP server failed, and as the reason the same server works from Claude Code and MCP Inspector: local clients fail fast on a redirect instead of following it. Register the URL your server actually listens on.
  2. 2 Claude sends the RFC 8707 resource parameter on both the authorization and the token request, set to the canonical form of your MCP server URL: lowercase scheme and host, no trailing slash, no fragment, no default port, path included. Issue tokens with that audience, and compare aud against the canonical value rather than byte-comparing what the user typed.
  3. 3 The resource field in your protected resource metadata must match the MCP server URL exactly as the user enters it in Claude, path included. If authorization_servers lists more than one issuer, Claude uses the first entry and does not fall back to later ones.
  4. 4 Each user authorizes for themselves. On Team and Enterprise plans only an Owner can add a connector, from Admin settings, and every member then finds it under Settings, Connectors and clicks Connect to authenticate individually — so a connector that works only for the person who set it up is documented behaviour, not a bug.
  5. 5 Claude re-runs OAuth only on a transport-level 401. A 200 carrying isError: true is handed to the model as a tool result and produces no auth prompt, which is how anonymous tool calls repeat indefinitely. Claude also refreshes reactively on a 401, so return invalid_grant — not invalid_request or a custom code — when a refresh token is dead.
JSONjson
{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["mcp:read"],
  "bearer_methods_supported": ["header"]
}
Anthropic: troubleshooting connectors

ChatGPT connectors

If the connector was added in ChatGPT or Codex

  1. 1 ChatGPT appends resource=<the resource value from your protected resource metadata> to both the authorization and the token request. OpenAI's own documentation tells you to configure the authorization server to copy that value into the access token, commonly the aud claim. If your identity provider ignores the parameter, every token you get back carries the provider's default audience and your server is correct to reject it.
  2. 2 ChatGPT will not show its linking UI unless both halves are in place: per-tool securitySchemes in the tool definition (oauth2 with the scopes you need, or noauth) and a runtime error result carrying _meta["mcp/www_authenticate"] with both an error and an error_description. With only one half, protected tools keep being called anonymously and no sign-in is ever offered.
  3. 3 Register the production redirect URI https://chatgpt.com/connector/oauth/{callback_id} shown on the app management page. Apps published before the change keep working on the legacy https://chatgpt.com/connector_platform_oauth_redirect.
  4. 4 There is no non-OAuth fallback to reach for. ChatGPT does not support machine-to-machine grants — client credentials, service accounts, JWT bearer assertions — and cannot present a customer-supplied API key or mTLS certificate. It does present an OpenAI-managed client certificate whose SAN dnsName is mtls.prod.connectors.openai.com, if you want to identify the client at the transport layer.
  5. 5 Verification is entirely yours: check the signature and iss against the authorization server's JWKS, reject on exp and nbf, confirm aud or the resource claim names your server, then enforce scopes. On failure return 401 with a WWW-Authenticate header pointing back at your protected resource metadata.
JSONjson
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [{ "type": "text", "text": "Authentication required: no access token provided." }],
    "_meta": {
      "mcp/www_authenticate": [
        "Bearer resource_metadata=\"https://mcp.example.com/.well-known/oauth-protected-resource\", error=\"invalid_token\", error_description=\"You need to login to continue\""
      ]
    },
    "isError": true
  }
}
OpenAI: authenticate your users

Microsoft Entra ID

If your authorization server is Microsoft Entra ID and the token request fails with AADSTS9010010 or invalid_target

  1. 1 Entra is refusing the resource value the client sent because it matches no Application ID URI on your app registration. The default api://{client-id} is not sufficient here: the client sends the full MCP server URL, path included.
  2. 2 In the Microsoft Entra admin center, open the app registration that represents the protected API — not the OAuth client, if those are separate registrations. Under Expose an API, add your MCP server URL as an additional Application ID URI (identifierUris in the manifest). It must match exactly, path included, without a trailing slash.
  3. 3 If something in front of your server validates the audience for you — Azure App Service Authentication, for instance — add the API app's Application (client) ID and its api:// URI to Allowed token audiences. That is a different list from Allowed client applications.
  4. 4 If the OAuth client and the API are separate registrations, confirm the client holds an admin-consented API permission for the scope your API exposes.
Anthropic: Entra ID rejects the resource value

VS Code

If the server is configured in mcp.json and VS Code keeps re-sending a token your server rejects

  1. 1 VS Code caches both the access token and the dynamically registered client_id. When that credential goes stale — expiry, a rotated client, a restarted server — a 401 from a previously authorized server does not trigger a refresh, a re-registration, or fresh discovery. VS Code re-sends the same token and the server stays in a failed state. Reported as microsoft/vscode#321834 and still open; no fix has shipped.
  2. 2 Recover by hand: run Authentication: Remove Dynamic Authentication Providers from the Command Palette, then restart the server from MCP: List Servers. That forces a fresh registration and OAuth flow.
  3. 3 For GitHub or Microsoft Entra authorization servers, check which servers hold your account under Manage Trusted MCP Servers in the Accounts menu. A token bound to the wrong account is indistinguishable from a wrong audience when you are only reading your server's logs.
  4. 4 Your authorization server's redirect URI list must include both http://127.0.0.1:33418 and https://vscode.dev/redirect. VS Code tries dynamic client registration first and falls back to a client-credentials workflow if the identity provider does not support it.
  5. 5 Read VS Code's own account of the exchange before touching your validator: MCP: List Servers, pick the server, choose Show Output.
VS Code issue #321834 (open)

Cursor

If Cursor flips back to Needs authentication after a successful sign-in, and you are working over Remote SSH

  1. 1 Check your version first: Cursor staff attributed this to Remote SSH specifically — the callback completes on the local side while the MCP extension runs on the remote host, so the saved tokens never reach the remote extension host — and said it was fixed in v3.3. If you are past that, this branch is not your problem.
  2. 2 The signature, from the forum report: after a successful flow the client logs Saving tokens and OAuth authorization completed, reloads, then immediately begins a new flow (Saving PKCE code verifier) and ends at UnauthorizedError: Unauthorized. The server sees only discovery requests — no POST ever arrives carrying the bearer. A separate report on the same forum covers Cursor not refreshing MCP access tokens at all.
  3. 3 The workaround staff gave for the Remote SSH case is to skip the browser flow entirely on the remote host: an API key in a header, from an environment variable.
  4. 4 Confirm what Cursor actually sent before changing anything server-side: Output panel, then MCP Logs in the dropdown.
  5. 5 If your provider issues a fixed client, skip dynamic registration. Add an auth object to the remote server entry with CLIENT_ID, an optional CLIENT_SECRET for confidential clients, and scopes. Omit scopes and Cursor reads scopes_supported from /.well-known/oauth-authorization-server.
  6. 6 Cursor's redirect URIs are fixed per surface: https://www.cursor.com/agents/mcp/oauth/callback for web and Cursor Agents, http://localhost:8787/callback for the desktop app. Register both if your users authenticate from both, since the server is identified through the OAuth state parameter rather than a per-server callback.
  7. 7 There is no documented RFC 8707 resource behaviour for Cursor, so do not assume the token you get back is audience-restricted to your server. Validate iss and aud, and read what actually arrives.
JSONjson
{
  "mcpServers": {
    "oauth-server": {
      "url": "https://mcp.example.com/mcp",
      "auth": {
        "CLIENT_ID": "${env:MCP_CLIENT_ID}",
        "CLIENT_SECRET": "${env:MCP_CLIENT_SECRET}",
        "scopes": ["mcp:read", "mcp:write"]
      }
    }
  }
}
Cursor forum: OAuth token not used after authentication

MCP SDKs (server side)

If you build the server on an official MCP SDK and want the audience check and the 401 to be right

  1. 1 Neither official SDK checks the audience for you. The Python SDK hands you a TokenVerifier whose verify_token returns an AccessToken or None; the example in the SDK docs is a stub with no verification at all. Compare the token's audience against the resource_server_url you set in AuthSettings and return None on a mismatch.
  2. 2 AccessToken.resource is the RFC 8707 resource indicator — populate it, so nothing downstream has to re-derive which resource the token was minted for.
  3. 3 The SDK's RequireAuthMiddleware then answers 401 with WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required", plus a resource_metadata parameter when you configure resource_metadata_url. Configure it: without the pointer the client has to fall back to probing well-known paths.
  4. 4 In the TypeScript SDK the same job is requireBearerAuth({ verifier, requiredScopes, resourceMetadataUrl }), and your OAuthTokenVerifier.verifyAccessToken must throw an OAuthError with OAuthErrorCode.InvalidToken for a wrong audience. Note it also rejects any token whose expiresAt is unset, so populate it from the introspection response or the JWT exp.
  5. 5 Keep the status codes straight: 401 with error="invalid_token" for a missing, expired or wrong-audience token; 403 with error="insufficient_scope" and a scope parameter only for a scope shortfall. Never answer 200 with a tool error that says "please sign in" — clients pass that to the model as a result and no auth prompt appears.
python
from mcp.server.auth.provider import AccessToken, TokenVerifier

RESOURCE = "https://mcp.example.com/mcp"

class AudienceCheckingVerifier(TokenVerifier):
    async def verify_token(self, token: str) -> AccessToken | None:
        claims = await introspect(token)  # your IdP call, or local JWT verify
        if RESOURCE not in claims.get("aud", []):
            return None  # a token for another service is not a token for you
        return AccessToken(
            token=token,
            client_id=claims["client_id"],
            scopes=claims["scope"].split(),
            expires_at=claims["exp"],
            resource=RESOURCE,
        )
MCP Python SDK: authorization

Something else

Two questions settle this for any client and any identity provider, and the curl above answers both. If the credential never arrived, find the hop that rewrote the URL — a redirect to www., a region router, a vanity domain in front of a CDN — and register the host that actually serves /mcp, because an HTTP client drops Authorization when a redirect crosses origins. If it arrived and you rejected it, decode it: the MCP specification requires you to accept only tokens issued for you as the audience, so make the client send the RFC 8707 resource parameter naming your canonical MCP server URL on both the authorization and the token request, and make the authorization server audience-restrict to that value — a custom audience, an API registration, a resource identifier, whatever it calls the setting. Fix the audience where the token is minted, never by widening the aud check in your server.

Why it happens

Three different failures leave the same shape in your access log. A cross-origin redirect on the MCP endpoint — the client attaches Authorization, follows the 3xx, and standard HTTP client behaviour drops the header, so the redirect target logs an unauthenticated request. An audience mismatch — the client sends the RFC 8707 resource parameter naming your canonical MCP server URL, the authorization server ignores it and mints a token for its own default audience, and your aud check correctly refuses it. Per-user credential scoping — each user authorizes a connector for themselves, so a server that works for whoever set it up and returns 401 to everyone else is doing what the client documents.

The exchange

Each step in the exchange, in order.

  • MCP client holds a valid token
  • Redirect at the edge apex to www.
  • MCP server the redirect target
  1. MCP client to Redirect at the edge

    POST /mcp Authorization: Bearer eyJ...
  2. A redirect that crosses to a different origin makes an HTTP client drop the Authorization header. The retried request is the one your logs show, and it carries no credential.

    Redirect at the edge to MCP client

    308 Permanent Redirect Location: www.example.com/mcp
  3. MCP client to MCP server

    POST /mcp (retried) no Authorization header
  4. The same 401 appears when the token does arrive but was minted for another audience. Compare the aud claim against the canonical MCP server URL before you go looking for a redirect.

    MCP server to MCP client

    401 Unauthorized error="invalid_token"

    Flow stops here

The MCP client posts to the URL it was given, carrying a valid bearer token. That host answers 308 to a different origin. The client retries against the redirect target, and standard HTTP behaviour has dropped the Authorization header on the way, so the MCP server answers 401 invalid_token.
Zuplo

What lands on you next

A token now arrives on every call. What it says is who the caller is — which is less than it sounds, and it is the last thing this page can tell you.

A valid token is a yes-or-no bit. Some callers should get less.

require-user-claims-inbound

Rules over the caller's claims — groups, roles, tenant, service-account subject — decide whether the call proceeds, with a 403 when it does not.

Which tools should exist for this caller in the first place?

mcp-capability-filter-inbound

A per-route allow-list of tools, prompts and resources. A capability the route does not expose is refused with MethodNotFound before the request is forwarded.

Is this token meant for us, or for some other service?

RFC 8707 resource indicators

A gateway-issued token is bound to one route's canonical resource URI, so a token minted elsewhere is rejected here rather than accepted because it parses.

What did the agent do with it?

capability_invocation

Every tool call emits an event carrying the caller, the capability, the upstream, the outcome and the latency. Tokens are never logged.

All of these attach to one MCP route's policies.inbound — the same policy engine on the way in and on the way out.

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.