MCP OAuth Flow Debugger

Check an MCP authorization flow: the resource parameter that binds the token to your server and nowhere else, PKCE, and the 401 challenge a client needs before it can discover anything at all.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Check the flow. Nothing leaves this tab.

Wanted a different tool?

  • MCP Transport Checker if the connection fails before authorization matters, because the two HTTP transports fail with a 404 rather than a version error.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

An authorization request with no resource

The token comes back valid and bound to nothing, so every MCP server the user authorized will accept it

GET /authorize?response_type=code&client_id=abc123&redirect_uri=https%3A%2F%2Fclient.example%2Fcb&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=xyz HTTP/1.1
Host: auth.example.com

PKCE in name only

code_challenge_method=plain sends the verifier in the clear, so anything that can read the authorization request can complete the exchange

GET /authorize?response_type=code&client_id=abc123&redirect_uri=https%3A%2F%2Fclient.example%2Fcb&resource=https%3A%2F%2Fmcp.example.com%2Fmcp&code_challenge=abc&code_challenge_method=plain&state=xyz HTTP/1.1
Host: auth.example.com

A 401 that discovers nothing

Without resource_metadata in the challenge the client has no way to find the authorization server, and reports a credentials failure instead

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Content-Type: application/json

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Leaving out the `resource` parameter

    Without it the authorization server issues a token with no audience binding, so any MCP server the token is presented to will accept it. One compromised or merely careless server can then replay it against every other one the user authorized.

    Instead:Send `resource` on both the authorization request and the token request, set to the server's canonical URI. RFC 8707 is what makes the token specific to one server.

  2. Accepting a token the client happened to have

    Passing a token through that was not issued for this server defeats the audience check entirely, and it is the single thing the MCP specification calls out as forbidden. The server becomes a confused deputy for whatever the token was really for.

    Instead:Validate the audience against your own canonical URI and reject anything else with a 401, even when the token is otherwise valid and signed by an issuer you trust.

  3. Returning a bare 401 with no `WWW-Authenticate` header

    The challenge is how a client discovers where the protected resource metadata lives. Without it the client has nothing to go on and reports a generic authentication failure, which reads as a credentials problem rather than a missing discovery document.

    Instead:Return `WWW-Authenticate: Bearer resource_metadata="https://server/.well-known/oauth-protected-resource"`, which is the whole entry point to the flow.

  4. Treating PKCE as optional for a confidential client

    MCP requires PKCE for every client, not only public ones, and `code_challenge_method=plain` is PKCE in name only: anything that can see the request can see the verifier.

    Instead:Always `S256`, always with `state` alongside it. They defend different attacks and neither substitutes for the other.

The resource parameter is what stops the token working everywhere

MCP servers are OAuth 2.1 resource servers, and the one requirement that is easiest to skip is the one that makes a token mean anything: RFC 8707 Resource Indicators.

Without it, a token issued for any MCP server works at all of them

The client sends resource on the authorization request AND on the token request, and the authorization server puts that value in the token's audience. The MCP server then checks the audience against its own canonical URI. Leave the parameter out and there is no audience to check, so a token minted for one server is accepted by every server that trusts the same authorization server. Nothing fails while you are testing, which is exactly why it survives to production.

resource=https://mcp.example.com    bound to one server

(absent)                            valid at every server that
                                    trusts the same authorization
                                    server, which is the confused
                                    deputy this exists to prevent

The canonical form is exact, and a trailing slash breaks it

The authorization server binds the audience to the string it was given and the MCP server compares it exactly. Lowercase the scheme and host, drop the fragment and the query, and drop the trailing slash unless the canonical URI genuinely has a path. A mismatch produces a 401 from a server that issued the token itself, which is a confusing place to start looking.

The 401 is where discovery starts

An unauthenticated request should come back with WWW-Authenticate: Bearer and a resource_metadata pointing at the server's protected resource metadata, per RFC 9728. That document names the authorization servers. Without the challenge the client has nowhere to go, and the flow stops at the first 401 with no useful error.

PKCE is not optional, and plain is not PKCE

MCP clients are OAuth 2.1 public clients, so every authorization code request needs a code challenge. The plain method sends the verifier as the challenge, so anyone who can see the authorization request already has the verifier and PKCE protects nothing. S256 is the only method OAuth 2.1 permits.

A server must never accept a token that was not issued for it

The specification forbids token passthrough in both directions. Accepting a token whose audience names another resource turns your server into a way to spend somebody else's credential. Forwarding a client's token to an upstream API turns it into a confused deputy for that upstream. Get your own token for the upstream, bound to your own identity.

What this cannot see

It reads what you paste and makes no requests. It cannot fetch your metadata documents, verify a signature, or decode the token. Most importantly it cannot tell you whether your authorization server actually honours the resource parameter, which is the thing worth testing next: some ignore it, and the symptom is a token with no audience and a server with nothing to reject. Paste as many parts of the flow as you have and each recognised one is checked.