A local MCP server runs on your machine with your credentials. A remote one is a web service that other people's AI clients call, so it needs to know who is calling and what they may do. MCP's answer is OAuth. This post walks through the flow in the order it happens, shows what a server must return at each step, and includes a small tested sketch of the server's decision logic.
I read the MCP authorization specification page on 2026-10-16. Its internal links pointed at the 2026-07-28 version of the spec. The spec uses RFC 2119 words (MUST, SHOULD), and I quote those exactly because they are the part that changes. Specs move, so re-read the current page before shipping.
When do you need MCP authorization at all?
Less often than the hype suggests. The spec says authorization is optional. If you use the STDIO transport (a local process), it says implementations SHOULD NOT follow the OAuth flow and should read credentials from the environment. OAuth applies to HTTP-based transports, where a remote client calls your server.
If your server only runs locally, spend your effort on permissions and tool scoping instead. If you are new to the protocol, start with the MCP overview.
Who plays which role?
| Role | Who | Spec language |
|---|---|---|
| Resource server | Your MCP server | "acts as an OAuth 2.1 resource server" |
| Client | The AI app calling it | "acts as an OAuth 2.1 client" |
| Authorization server | Issues tokens; may be separate from your server | Implementation details are out of scope |
The key consequence: your MCP server does not have to be the thing that logs users in. It only has to accept tokens meant for it and say where to get them.
What happens step by step?
Following the sequence diagram in the spec:
- The client calls your server with no token. You answer
401 Unauthorizedwith aWWW-Authenticateheader that includes aresource_metadataURL, and optionally ascope. - The client fetches your Protected Resource Metadata (RFC 9728). The spec says MCP servers MUST implement it and clients MUST use it to discover the authorization server.
- The client discovers the authorization server's metadata, using OAuth 2.0 Authorization Server Metadata (RFC 8414) or OpenID Connect Discovery. The authorization server must provide at least one; clients must support both.
- The client gets a client ID. Three mechanisms are named: Client ID Metadata Documents (recommended with SHOULD), pre-registration, or Dynamic Client Registration (which the spec calls deprecated, kept for compatibility).
- The client sends the user to authorize, with PKCE parameters and the
resourceparameter. The user approves in a browser. - The client exchanges the code for a token, again with the
resourceparameter and the PKCE verifier. - The client calls your server with
Authorization: Bearer <token>on every request. Tokens MUST NOT be put in the URI query string.
A detail that is easy to miss: before redirecting the user, the client MUST record the issuer from the validated authorization server metadata and later compare it with the iss value in the response (RFC 9207), to resist mix-up attacks. Servers that include iss must advertise it in their metadata.
What must your server check on every request?
Straight from the page's token handling section:
- Validate access tokens as OAuth 2.1 requires.
- "MCP servers MUST validate that access tokens were issued specifically for them as the intended audience."
- Invalid or expired tokens MUST get an HTTP 401.
- Servers MUST only accept tokens valid for their own resources, and MUST NOT accept or transit any other tokens.
That last rule is the "no token passthrough" rule. If your MCP server calls another API on a user's behalf, it needs its own credentials for that call. It must not forward the token it received.
What do the error responses look like?
The spec gives two examples. No or invalid token:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"
Valid token, not enough scope, at runtime:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
scope="files:write",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
error_description="File write permission required for this operation"
Rules attached to these: servers SHOULD include the scopes needed for the current operation, and SHOULD include all of them in a single challenge rather than asking one at a time. Clients then compute the union of previously requested scopes and the challenged ones, reauthorize (step-up), and retry only a few times before treating it as a permanent failure.
A tested sketch of the server decision
Below is the decision logic as pure functions, with a toy dictionary in place of real token validation. Real servers verify a signed JWT or call an introspection endpoint. The status codes and header shapes follow the spec examples above. The choice of 400 for a token in the query string is mine (the spec says tokens MUST NOT be there and lists 400 for malformed requests).
def handle(headers, query, needed_scope, now=1000):
if "access_token" in query:
return 400, {} # tokens must not be in the query string
auth = headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return challenge(401, scope=needed_scope)
claims = TOKENS.get(auth[7:])
if claims is None or claims["exp"] <= now:
return challenge(401, scope=needed_scope)
if claims["aud"] != RESOURCE: # audience binding: not issued for us
return challenge(401, scope=needed_scope)
if needed_scope not in claims["scope"].split():
return challenge(403, scope=needed_scope, error="insufficient_scope")
return 200, {}
I ran seven cases on 2026-10-16:
no token -> 401 Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource", scope="files:read"
read token, read op -> 200
read token, write op -> 403 Bearer error="insufficient_scope", resource_metadata="...", scope="files:write"
write token, write op -> 200
token for another server -> 401 Bearer resource_metadata="...", scope="files:read"
expired token -> 401 Bearer resource_metadata="...", scope="files:read"
token in query string -> 400
(The metadata URL is shortened in the middle rows for readability; the real output repeated the full URL.)
The row to study is "token for another server". It is a perfectly valid token, signed and unexpired, and the server still refuses it, because the audience is not this server. That is the check most home-made servers skip.
What should you decide before you build?
- Do you need remote access at all? If not, stay local and skip all of this.
- Who is the authorization server? Using an established identity provider avoids writing OAuth yourself. The spec leaves this open.
- Which scopes, and how fine-grained? Keep the minimal set in
scopes_supportedand request more by step-up, as the spec suggests. - How will you test it? The sketch above is a unit-test shape. Add tests for audience, expiry and scope for every tool.
- What do you log? Record who called which tool with which scope. The approval gate pattern pairs well with this for destructive tools.
For the wider picture of risks beyond auth, see the MCP interview questions on security.
What I could not verify
- Real-world client behaviour. I ran no live MCP client or authorization server; the sketch is local and toy.
- The sub-pages of the spec (discovery, registration, security considerations) in detail. I read the main authorization page that links to them.
- Which popular clients support Client ID Metadata Documents today.



