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:

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

  1. 401 with a pointer. A request without a valid token gets HTTP 401 Unauthorized and a WWW-Authenticate: Bearer header. Its resource_metadata parameter gives the address of the server's protected resource metadata, and a scope parameter can name the scopes this request needs.
  2. Protected resource metadata (RFC 9728). A small JSON document the server must publish. Its authorization_servers field lists at least one authorization server. If the 401 carries no pointer, the client tries the well-known paths: first /.well-known/oauth-protected-resource followed by the MCP endpoint's path, then the same name at the root.
  3. Authorization server discovery. The client fetches the authorization server's metadata, trying /.well-known/oauth-authorization-server and then the OpenID Connect /.well-known/openid-configuration forms in a fixed order. The issuer in the document must be identical to the issuer the client used to build the address, or the client must not use it.
  4. PKCE check. If the metadata has no code_challenge_methods_supported, the client must refuse to continue. With PKCE it uses S256.
  5. 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 its client_id. The authorization server fetches the file, checks that its client_id matches the address exactly, and checks the redirect address against the file's redirect_uris. Servers advertise support with client_id_metadata_document_supported: true. A client that already has a pre-registered client_id for 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.
  6. Authorization code with PKCE, as on OAuth basics, with one addition: the resource parameter, 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.
  7. 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 an iss parameter the client compares it to the recorded value with plain string comparison and rejects a mismatch. If the server's metadata says it sends iss and 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.
  8. 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, MCPServer accepts auth settings and a token_verifier for this check.

Common mistakes

  • Accepting any valid-looking token. Symptom: tokens issued for a different service work against your server. Check aud against 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 scope in the 401. Symptom: clients request every scope in scopes_supported and 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 iss comparison. Symptom: none until a malicious authorization server collects codes meant for an honest one.
  • Trailing slash drift. Symptom: tokens are rejected because resource was https://mcp.example.com/mcp/ at the client and https://mcp.example.com/mcp at 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 resource parameter.
  • RFC 9207, issuer identification, for the iss check.
  • 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.

Leads to

Back to Building and maintaining MCP servers