technique
Authorization for remote servers
How a remote MCP server decides who may call it: OAuth 2.1 with the server as a resource server, discovery of the authorization server, client registration, and token checks.
Before this
This page assumes you are comfortable with:
- techniqueTransports: stdio and Streamable HTTPThe two ways messages travel: a local child process over stdin and stdout, or a remote endpoint over HTTP POST with optional streamed responses, plus long-lived subscriptions.
- prerequisiteOAuth basicsAccess tokens, scopes, the authorization code flow with PKCE, issuers and audiences: the vocabulary MCP authorization is built from.
Why you need this
A server on your laptop over stdio is reached only by the host that launched it. A server on the internet is reached by anyone who knows its address. If its tools read someone's files or send messages in their name, it must know, on every request, which person the caller acts for and what that person allowed. The Authorization section of the 2026-07-28 specification answers that with standard OAuth 2.1, plus rules for how a client that has never seen your server before finds out where to log in. This is stage 4 of a server's life, "Secure it".
The idea
When authorization applies
The Authorization section covers HTTP transports only. It says implementations using stdio "SHOULD NOT follow this specification, and instead retrieve credentials from the environment": the host launches the server with an API key in an environment variable, and the operating system's process boundary does the rest. Authorization is also optional: a public, read-only server may skip it.
Who plays which OAuth role
| OAuth role | In MCP |
|---|---|
| Resource owner | The person using the host |
| Client | The host's MCP client |
| Resource server | Your MCP server |
| Authorization server | A login service: your own, or an identity provider you use |
The MCP server is only a resource server. It checks tokens; it does not have to issue them.
The pieces, in the order a client meets them
- 401 with a pointer. A request without a valid token gets HTTP
401 Unauthorizedand aWWW-Authenticate: Bearerheader. Itsresource_metadataparameter gives the address of the server's protected resource metadata, and ascopeparameter can name the scopes this request needs. - Protected resource metadata (RFC 9728). A small JSON document the server must publish. Its
authorization_serversfield lists at least one authorization server. If the 401 carries no pointer, the client tries the well-known paths: first/.well-known/oauth-protected-resourcefollowed by the MCP endpoint's path, then the same name at the root. - Authorization server discovery. The client fetches the authorization server's metadata, trying
/.well-known/oauth-authorization-serverand then the OpenID Connect/.well-known/openid-configurationforms in a fixed order. Theissuerin the document must be identical to the issuer the client used to build the address, or the client must not use it. - PKCE check. If the metadata has no
code_challenge_methods_supported, the client must refuse to continue. With PKCE it usesS256. - Client registration. The client needs a
client_id. The preferred way is a Client ID Metadata Document: the client publishes a JSON file at an https address and uses that address itself as itsclient_id. The authorization server fetches the file, checks that itsclient_idmatches the address exactly, and checks the redirect address against the file'sredirect_uris. Servers advertise support withclient_id_metadata_document_supported: true. A client that already has a pre-registeredclient_idfor this authorization server uses that first. Dynamic Client Registration, where the client posts its details to a registration endpoint, is deprecated in 2026-07-28 and kept only for authorization servers that do not support metadata documents. - Authorization code with PKCE, as on OAuth basics, with one addition: the
resourceparameter, the MCP server's canonical address, in both the authorization request and the token request. That asks the authorization server to bind the token's audience to this one MCP server. - Issuer check on the way back. Before redirecting, the client records the authorization server's
issuer. When the code comes back, if the response carries anissparameter the client compares it to the recorded value with plain string comparison and rejects a mismatch. If the server's metadata says it sendsissand it is missing, the client rejects that too. This stops a mix-up attack, where a malicious authorization server tricks the client into sending it a code issued by an honest one. - Bearer token on every request.
Authorization: Bearer <token>goes on every HTTP request, never in the URL.
What the server must check
The server, as resource server, validates every token: signature, expiry, issuer, and audience. The audience must be this server. The Authorization section says servers "MUST only accept tokens that are valid for use with their own resources" and "MUST NOT accept or transit any other tokens."
Token passthrough is the forbidden shortcut where a server accepts whatever token the client sends and forwards it to an upstream API. If your server calls an upstream API, it acts as an OAuth client of its own and uses a separate token issued for that API. The Security Best Practices guide lists why: the upstream API's rate limits and audit logs see the wrong caller, a token stolen from one service works at another, and your server can no longer tell clients apart.
Scopes per tool
Scopes let one token carry only what a task needs. A natural design gives each group of tools a scope: files:read for list_files and read_file, files:write for write_file. The server checks the scope inside each tool call, not only at the door. When a token lacks a scope, the server answers 403 Forbidden with error="insufficient_scope" and the needed scopes in scope, and the client runs a step-up authorization asking for the union of the scopes it already had and the new ones. The tools/list result may also differ by the token's scopes, since credentials are part of each request.
Worked example
A host connects for the first time to a file server at https://mcp.example.com/mcp. Its authorization server is https://auth.example.com, and the host publishes its client metadata at https://app.example.com/oauth/client-metadata.json. The host runs on the person's machine and receives the code at http://127.0.0.1:3000/callback. Values such as codes and tokens are shortened. Inside the query string in step 4, addresses are percent-encoded, as they must be on the wire.
| # | From, to | Request | Response | What the client checks |
|---|---|---|---|---|
| 1 | Client to MCP server | POST https://mcp.example.com/mcp, a tools/call for list_files, no Authorization header |
401, WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource", scope="files:read" |
Extracts the metadata address and the scope. |
| 2 | Client to MCP server | GET https://mcp.example.com/.well-known/oauth-protected-resource |
200, JSON with "resource": "https://mcp.example.com/mcp", "authorization_servers": ["https://auth.example.com"], "scopes_supported": ["files:read", "files:write"] |
Picks https://auth.example.com. |
| 3 | Client to authorization server | GET https://auth.example.com/.well-known/oauth-authorization-server |
200, JSON with "issuer": "https://auth.example.com", authorization_endpoint, token_endpoint, "code_challenge_methods_supported": ["S256"], "client_id_metadata_document_supported": true, "authorization_response_iss_parameter_supported": true |
issuer is identical to what it asked for; PKCE supported; metadata documents supported. Records the issuer. |
| 4 | Browser to authorization server | GET https://auth.example.com/authorize?response_type=code&client_id=https%3A%2F%2Fapp.example.com%2Foauth%2Fclient-metadata.json&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fcallback&scope=files%3Aread&resource=https%3A%2F%2Fmcp.example.com%2Fmcp&code_challenge=E9Mel...&code_challenge_method=S256&state=af0ifjsldkj |
Login page | Has stored the code verifier, state, and expected issuer together. |
| 5 | Authorization server to client's host | GET https://app.example.com/oauth/client-metadata.json |
200, JSON with client_id equal to that address, client_name, redirect_uris |
(Server side) client_id matches, redirect_uri from step 4 is listed. |
| 6 | Authorization server to browser | Consent screen naming the client and files:read, then a redirect |
http://127.0.0.1:3000/callback?code=SplxlO...&state=af0ifjsldkj&iss=https%3A%2F%2Fauth.example.com |
state matches; decoded iss equals the recorded issuer. |
| 7 | Client to authorization server | POST https://auth.example.com/token with form fields grant_type=authorization_code, code, code_verifier=dBjf..., redirect_uri, client_id, and resource set to https://mcp.example.com/mcp |
200, access_token, "token_type": "Bearer", "expires_in": 3600 |
Stores the token for this server only. |
| 8 | Client to MCP server | POST https://mcp.example.com/mcp, the same tools/call, Authorization: Bearer eyJ... |
200, the tool result |
(Server side) signature, expiry, iss is https://auth.example.com, aud is https://mcp.example.com/mcp, scope includes files:read. |
| 9 | Client to MCP server | Later: tools/call for write_file with the same token |
403, WWW-Authenticate: Bearer error="insufficient_scope", scope="files:write", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource" |
Starts step-up with scopes files:read files:write, then retries. |
Step 1 is the only request the person never notices. Steps 4 to 6 are the only ones they see. From step 8 on, every request carries the token, and the server never remembers anything between requests: the token is the whole proof each time.
In a server's life
- Secure it (stage 4): this page is the "who may call" half; Security threats and defenses is the "what can go wrong when a model calls" half.
- Test and ship (stage 5): Deploying remote servers adds where the token-checking code runs and where secrets live.
- Build and connect (stage 3): the token rides the HTTP layer from Transports. In the Python SDK,
MCPServeracceptsauthsettings and atoken_verifierfor this check.
Common mistakes
- Accepting any valid-looking token. Symptom: tokens issued for a different service work against your server. Check
audagainst your own canonical address. - Forwarding the client's token upstream. Symptom: the upstream API's logs show requests "from the user" that your server made, and its rate limits stop applying per client. Get your own upstream token.
- No
scopein the 401. Symptom: clients request every scope inscopes_supportedand people face an alarming consent screen. Name the scopes each request needs. - One scope for everything. Symptom: a token meant for reading can delete. Split scopes by risk and check them per tool.
- Client skips the
isscomparison. Symptom: none until a malicious authorization server collects codes meant for an honest one. - Trailing slash drift. Symptom: tokens are rejected because
resourcewashttps://mcp.example.com/mcp/at the client andhttps://mcp.example.com/mcpat the server. The Authorization section recommends the form without the trailing slash; pick one and use it everywhere.
Cost
The first connection costs about eight HTTP round trips plus the person's login and consent, once per authorization server rather than per request. After that, each request costs one token check: a cached public key, one signature verification, and a handful of comparisons. The operational cost is running or renting an authorization server and keeping its keys, and the engineering cost is real: discovery, registration, PKCE, iss validation, and step-up are each easy to get subtly wrong, so use a maintained OAuth library on both sides and test the failure paths, not only the happy path.
Going further
- RFC 9728, OAuth 2.0 Protected Resource Metadata.
- RFC 8707, Resource Indicators, the source of the
resourceparameter. - RFC 9207, issuer identification, for the
isscheck. - The OAuth Client ID Metadata Document draft.
- The MCP Security Best Practices guide's sections on token passthrough, scope minimization, and server-side request forgery during discovery.