---
title: "Authorization with the MCP Server Failed After OAuth Completes"
description: "The client reports connected but your MCP server sees no credential, or rejects the one it gets. Fix the redirect stripping Authorization, or the audience."
canonicalUrl: "https://zuplo.com/learn/mcp/errors/oauth-completes-but-no-token-sent"
pageType: "mcp-error"
errorString: "Authorization with the MCP server failed"
lastVerified: "2026-07-27"
specVersion: "2026-07-28"
---

# `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.

```bash
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.

**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. If this is what you see, this is not your problem.

**Clients also report this as:**

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

**In short.** 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.

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

### Claude custom connectors

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

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

```json
{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["mcp:read"],
  "bearer_methods_supported": ["header"]
}
```

[Anthropic: troubleshooting connectors](https://claude.com/docs/connectors/building/troubleshooting)

### ChatGPT connectors

If the connector was added in ChatGPT or Codex

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

```json
{
  "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](https://developers.openai.com/plugins/build/auth)

### Microsoft Entra ID

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

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. 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. 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. 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](https://claude.com/docs/connectors/building/troubleshooting#microsoft-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. 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. 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. 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. 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. 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)](https://github.com/microsoft/vscode/issues/321834)

### Cursor

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

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. 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. 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. Confirm what Cursor actually sent before changing anything server-side: Output panel, then MCP Logs in the dropdown.
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. 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. 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.

```json
{
  "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](https://forum.cursor.com/t/mcp-oauth-token-not-used-after-successful-authentication/155694)

### 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. 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. `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. 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. 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. 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](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/docs/authorization.md)

### 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

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.

**Participants:** MCP client (holds a valid token), Redirect at the edge (apex to www.), MCP server (the redirect target)

1. **MCP client → Redirect at the edge** — `POST /mcp` — `Authorization: Bearer eyJ...` _(carries the credential)_
2. **Redirect at the edge → MCP client** — `308 Permanent Redirect` — `Location: www.example.com/mcp` _(redirect)_
3. **MCP client → MCP server** — `POST /mcp (retried)` — `no Authorization header`
4. **MCP server → MCP client** — `401 Unauthorized` — `error="invalid_token"` _(response; this step fails — the flow stops here)_

Notes:

- **Steps 2–3** — 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.
- **Step 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.

## 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. See [Zuplo MCP Gateway](/mcp-gateway).

## Keep reading

- **Similar error** — [`Your MCP server returns a login page or 403 instead of 401`](/learn/mcp/errors/302-redirect-instead-of-401) — Something in front of your MCP server is enforcing authentication built for humans in browsers. Instead of the 401 an MCP client needs, it returns a login page, a redirect to one, or a bare 403 — so the client never learns how to authenticate.
- **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** — [`MCP headers config ignored when server has OAuth discovery`](/learn/mcp/errors/configured-headers-ignored-when-oauth-discovery-present) — 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.
- **Error reference** — [every documented MCP error](/learn/mcp/errors)
