MCP OAuth explained: the 401 pointer, PKCE, dynamic registration and scopes, walked end to end
MCP authorization is OAuth 2.1: a 401 points to the server's metadata, the client registers itself, PKCE protects the code, and a scoped.
On this page
- Why MCP uses OAuth at all
- The flow, step by step
- What each piece protects against
- PKCE: why the code is useless to a thief
- Dynamic client registration: why nobody creates an app in a console
- Scopes: narrowing, never widening
- The same flow on a real server
- PKCE flow vs pasted token: what you give up
- FAQ
- Next step
MCP authorization is OAuth 2.1 with a few mandatory pieces. An unauthenticated request to a remote MCP server gets a 401 that points to the server's metadata; the client follows it to the authorization server, identifies itself, opens the browser so the person can consent to scopes, and exchanges a PKCE-protected code for a short-lived access token and a rotating refresh token. The result is a scoped, revocable credential instead of a key pasted into a config file.
That is the whole flow in one paragraph. The rest of this post walks it step by step against the MCP authorization specification (revision 2025-11-25), explains what PKCE, dynamic client registration and scopes each protect against, and shows the flow as it actually runs against a real server, ours, so you can see what each stage looks like from the client's side.
Why MCP uses OAuth at all
A local MCP server over stdio reads its credentials from the environment, and the spec says it should not use OAuth. A remote server is different: one deployment serves many people, each with different permissions, from AI apps that the server operator has never seen. Three problems follow.
- The server needs to know who is asking, on every call.
- The person needs to limit what the app may do, without handing it everything.
- Both need a way to end the access later, without changing passwords or rotating keys by hand.
A pasted API key solves the first problem badly and the other two not at all. OAuth was designed for exactly this shape, an app acting for a person at a service, and OAuth 2.1 is the consolidated, stricter version of it. The glossary entry on OAuth 2.1 lists what it removed from 2.0; for MCP the relevant parts are that PKCE is required, redirect URIs must match exactly, and tokens may not travel in query strings.
The MCP authorization spec builds on five documents: the OAuth 2.1 draft (draft-ietf-oauth-v2-1-13), Authorization Server Metadata (RFC 8414), Dynamic Client Registration (RFC 7591), Protected Resource Metadata (RFC 9728) and the Client ID Metadata Documents draft. You do not need to read them to use an MCP server, but each one answers one of the questions below.
The flow, step by step
Here is what happens between the moment you add a remote server and the moment the model makes its first tool call. The client does all of it; you see a browser tab.
- The client calls the server without a token. The server, acting as an OAuth resource server, answers
401 Unauthorizedwith aWWW-Authenticate: Bearerheader containingresource_metadata, a URL. The spec requires servers to implement RFC 9728 and requires clients to parse this header. If the header is missing, the client must try the well-known URIs instead. - The client fetches the protected resource metadata. The document names at least one
authorization_serversentry and may listscopes_supported. - The client discovers the authorization server. It fetches that server's metadata via RFC 8414 (
/.well-known/oauth-authorization-server) or OpenID Connect Discovery; clients must support both. The metadata lists the authorization, token and registration endpoints, the supported PKCE methods and, if present,client_id_metadata_document_supported. - The client checks PKCE support. If
code_challenge_methods_supportedis absent from the metadata, the client must refuse to proceed. There is no silent downgrade. - The client identifies itself. Three options, in the spec's priority order: pre-registered credentials if it has them; a Client ID Metadata Document, where the client ID is an HTTPS URL to a JSON file the client hosts; or dynamic client registration, a
POSTto the registration endpoint that returns aclient_idon the spot. The spec now says servers and clients should support metadata documents and may support dynamic registration as a fallback. - The client opens the browser. The authorization URL carries the client ID, the redirect URI, the requested scopes, a
statevalue, the PKCEcode_challengewithcode_challenge_method=S256, and theresourceparameter naming the MCP server (RFC 8707). The person signs in and sees a consent screen listing the client's name and the scopes. - The authorization server redirects back with a code. The code is short-lived and single-use.
- The client exchanges the code for tokens. The token request carries the code, the original PKCE
code_verifierand the sameresourceparameter. The response is an access token and, usually, a refresh token. - Every MCP request carries the token in
Authorization: Bearer, never in the URL, and the server validates on each call that the token is valid and was issued for this server specifically. - When a call needs more scope, the server answers
403witherror="insufficient_scope"and the scopes required. The client runs the flow again for the wider set; the spec calls this step-up authorization and tells clients to cap retries.
What each piece protects against
| Mechanism | Defined in | The attack it stops |
|---|---|---|
| Protected resource metadata | RFC 9728 | Clients guessing or being tricked into using the wrong authorization server |
| Authorization server metadata | RFC 8414 | Hard-coded endpoints that break when the server changes |
| PKCE (S256) | RFC 7636, OAuth 2.1 | A stolen authorization code being redeemed by someone else |
| Exact redirect URI matching | OAuth 2.1 | Open redirects that send the code to an attacker |
state parameter | OAuth 2.1 | Cross-site request forgery on the callback |
resource parameter | RFC 8707 | A token for one server being replayed at another |
| Audience validation on the server | RFC 8707, OAuth 2.1 | Token passthrough and confused-deputy attacks |
| Short-lived access tokens | OAuth 2.1 | A leaked token staying useful for long |
| Rotating refresh tokens | OAuth 2.1 | A stolen refresh token being reused silently |
| Scopes and consent | OAuth 2.0/2.1 | An app doing more than the person agreed to |
Two of these deserve a closer look because they are the ones people ask about.
PKCE: why the code is useless to a thief
Most MCP clients are public clients: a desktop app, an editor or a CLI with no secret it could keep. In the authorization code flow, the code comes back through a redirect, and on a laptop or phone a redirect can be intercepted by another program registered for the same URL. Before PKCE, whoever held the code could redeem it.
PKCE adds a one-time secret per sign-in. The client generates a random code_verifier, keeps it, and sends code_challenge = BASE64URL(SHA256(code_verifier)) with the authorization request. When it exchanges the code, it presents the original verifier. The authorization server hashes it, compares it with the stored challenge, and refuses the exchange if they differ. A thief with the code but not the verifier gets nothing. The PKCE entry covers the plain method and why OAuth 2.1 and MCP say to use S256 whenever possible.
Dynamic client registration: why nobody creates an app in a console
Classic OAuth assumed a developer registered each app in advance. That does not scale to a world where any of thousands of AI clients may connect to any of thousands of MCP servers with no prior relationship. Dynamic client registration (RFC 7591) lets the client POST its own metadata, a name and redirect URIs, to the registration endpoint and receive a client_id immediately.
The 2025-11-25 revision adds a cleaner answer, Client ID Metadata Documents: the client ID is itself an HTTPS URL pointing at a JSON document that describes the client. The authorization server fetches it, checks that the document's client_id matches the URL exactly, and validates redirect URIs against it. The spec says clients and servers should support this and may keep dynamic registration for compatibility. Because any client can register, a careful authorization server treats registration as conveying no trust: it shows the client's name and redirect host plainly on the consent screen and warns when the redirect is a bare localhost port, which the spec notes cannot be fully protected by metadata documents alone.
Scopes: narrowing, never widening
Scopes are the names on the consent screen: work.read, crm.write, and so on. An app asks for a set, the person approves some or all, and the token carries the result. The MCP spec tells clients to request the scopes from the 401 challenge's scope parameter if present, otherwise the server's scopes_supported, and to step up later rather than ask for everything at once.
The rule that matters most is one the OAuth standard leaves to the API: a scope describes what the app may ask for on the person's behalf, and it must never exceed what that person can do themselves. A good server checks both on every call: does the token hold the scope, and is the person behind it allowed to do this to this record?
The same flow on a real server
This is what the steps above look like against the Atrix MCP server at https://atrix.dev/api/mcp, which we run and use daily, so you can match the abstract flow to concrete behaviour. The developer guide has the client-by-client instructions.
In Claude Code, the whole setup is claude mcp add --transport http atrix https://atrix.dev/api/mcp, then /mcp and Authenticate. The command deliberately carries no credential. What happens next:
- The first request gets
401with the RFC 9728 pointer. Discovery follows RFC 8414 and RFC 9728. - The client registers itself dynamically; there is no client secret anywhere, because every client, including our own apps, is a public client.
- PKCE is
S256only. The server advertises onlyS256in its metadata and refusesplain. - The browser opens; you choose a workspace and approve scopes. Codes live 60 seconds and work once.
- The client receives an access token with the prefix
atx_oat_, valid for one hour, and a refresh token with the prefixatx_ort_, valid for 90 days and rotated on every use. - Scopes never add to your permissions. They are resolved per call against the account behind the token, so if a teammate's access is revoked, every token they created stops reaching anything on the next request. Nine scopes also require the matching permission on the account, such as deal values or employment records, and pay is not grantable at all.
- Limits are 120 calls and 30 writes per minute per token. Every call is audited by tool, outcome and subject, never by contents.
For CI jobs and servers with no browser, there is a personal access token instead, prefixed atx_pat_, sent as --header "Authorization: Bearer ...". It works, and it is the worse choice anywhere a browser exists: it lives as plaintext on disk, it is as long-lived as you chose at creation, and it skips the consent screen. We keep that option for pipelines and recommend OAuth for people.
PKCE flow vs pasted token: what you give up
| OAuth 2.1 with PKCE | Pasted personal access token | |
|---|---|---|
| How you connect | Add URL, approve in browser | Create token in settings, paste into config |
| Lifetime | Access token 1 hour, refresh 90 days rotating (Atrix) | 30 days to forever, as chosen |
| Stored where | Client's secure store | Config file, often plaintext |
| Scoped by | Consent screen, step-up later | Chosen at creation |
| Revoked by | Revoking the grant or the account | Deleting the token |
| Needs a browser | Yes | No |
| Right for | People using an AI app | CI, cron, headless servers |
FAQ
Does MCP require OAuth?
Authorization is optional in the protocol. When a server uses an HTTP transport and supports authorization, the spec says it should conform to the OAuth 2.1-based authorization specification. stdio servers should not use it and should take credentials from the environment.
What is the difference between OAuth 2.0 and OAuth 2.1 for MCP?
OAuth 2.1 removes the implicit and password grants, requires PKCE for the authorization code flow, requires exact redirect URI matching, forbids bearer tokens in query strings, and requires refresh token rotation or sender-constraining for public clients. MCP inherits all of these.
Why does my MCP client refuse to connect to an authorization server?
The most common reason is missing PKCE support: if code_challenge_methods_supported is absent from the authorization server's metadata, an MCP client must refuse to proceed. The second is a missing or malformed WWW-Authenticate header on the 401.
Is dynamic client registration still required?
No. The 2025-11-25 revision says clients and servers should support Client ID Metadata Documents and may support dynamic registration as a fallback. Many servers, including Atrix, still offer dynamic registration because older clients rely on it.
Can an MCP token do more than the person who created it?
It should not. Scopes narrow access; they do not add to it. A well-built server resolves the person's permissions on every call, so a token can never reach more than its owner can, and loses access the moment the owner does.
When should I use a personal access token instead of OAuth?
Only where there is no browser: CI pipelines, cron jobs, headless servers. Everywhere else, OAuth gives you a shorter-lived, scoped, revocable credential without storing a secret on disk.
Next step
The developer guide walks the Atrix flow client by client, with the exact commands for Claude Code, Claude Desktop, ChatGPT and Cursor. If a term above was new, the OAuth 2.1 and PKCE entries are the shortest explanations we know of.