# OAuth 2.1 for MCP Servers: An End-to-End Guide

> Source: <https://dev.to/ignacio_gonzalezbohorque/-oauth-21-for-mcp-servers-an-end-to-end-guide-51p6>
> Published: 2026-11-12 12:42:00+00:00

Table of contents

*How an AI client discovers a login provider, identifies itself with CIMD, obtains a resource-bound token, and calls protected MCP tools.*

Imagine a company has a database and exposes a few operations through a remote MCP server:

`list_customers`` get_customer``update_customer`
An employee asks an AI application to retrieve a customer record. The application is the **MCP host**; inside it, an **MCP client** connects to the server. The server is available at `https://mcp.example.com/mcp` and protects customer data. A separate identity provider, such as the company’s Keycloak service or a hosted provider, signs the employee in and issues OAuth tokens.

The goal is to let the employee’s client use only the permitted tools, while the MCP server can verify who is calling, what access was granted, and whether the token was issued for this server.

**The simple mental model:** MCP describes the operation being requested. OAuth provides a verifiable access pass for the caller. The client carries the pass in the HTTP request; the server checks it before executing the operation.

| Party | Responsibility | Example | 
|---|---|---|
| MCP client | Sends MCP requests and obtains/attaches OAuth tokens | AI application or IDE | 
| MCP server / resource server | Exposes tools and verifies incoming tokens | `https://mcp.example.com/mcp` | 
| Authorization server (AS) | Signs users in and issues tokens | `https://login.example.net` | 

The authorization server and MCP server are different roles, even when one vendor hosts both. In the common arrangement, the MCP server **does not sign the user in and does not mint the access token**. It publishes information about which authorization server it trusts, then validates tokens from that issuer.

OAuth is not mandatory for every MCP deployment. A local stdio server, a public read-only service, or an application with another trust model may not use this remote OAuth flow. This article concerns a **protected remote MCP server over HTTP**.

This is a learning diagram, not a claim that every network exchange uses the same HTTP endpoint or that every host exposes each step to the user. A real MCP client usually performs the discovery and token flow automatically.

The client wants to call `get_customer`. The MCP body might look like this in a sessionless protocol version:

```
POST /mcp HTTP/1.1
Host: mcp.example.com
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_customer

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_customer",
    "arguments": { "customer_id": "cust_123" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}
```

If there is no valid bearer token, the server rejects the HTTP request before the tool runs:

```
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="customers:read"
```

A **401** means the request has not presented acceptable authentication credentials. The `WWW-Authenticate` challenge tells the client how to begin authorization. It can point at PRM and identify the scope needed for access.

The actual MCP protocol revision matters. Older revisions use `initialize` and session identifiers; the 2026-07-28 revision has no initialize handshake or `Mcp-Session-Id`. OAuth is independent of that lifecycle: a protected HTTP request still carries an access token in either era.

The client fetches the URL named in the challenge. The document is **Protected Resource Metadata (PRM)**, defined by [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728).

A simplified example:

```
{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://login.example.net"],
  "scopes_supported": ["customers:read", "customers:write"],
  "bearer_methods_supported": ["header"]
}
```

PRM describes the protected resource—in this example, the MCP endpoint. It says which authorization server or servers can issue acceptable tokens and may list supported scopes. It does not authenticate the user or issue a token.

**Where does PRM come from?** The MCP server operator configures and serves it as a public JSON document at the appropriate well-known URL. It must be reachable without an access token so an unauthenticated client can discover how to authenticate. The URL is derived from the protected resource URL according to RFC 9728; for a resource whose path is `/mcp`, current implementations commonly use a path-aware URL such as `/.well-known/oauth-protected-resource/mcp`.

**Where does the client keep it?** The client fetches the document over HTTPS and reads it to continue discovery. It may cache public metadata according to its implementation and HTTP caching rules. It is not an access token, a secret, or MCP `_meta`.

The MCP server is the source of its own PRM because it is the resource whose accepted issuer and canonical identifier the client needs to learn. Treating arbitrary metadata as trusted without validating its origin and values would be unsafe.

PRM names an authorization-server issuer. The client fetches that issuer’s **Authorization Server Metadata (ASM)** document, using [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) or OpenID Connect Discovery where applicable.

```
{
  "issuer": "https://login.example.net",
  "authorization_endpoint": "https://login.example.net/authorize",
  "token_endpoint": "https://login.example.net/token",
  "jwks_uri": "https://login.example.net/.well-known/jwks.json",
  "code_challenge_methods_supported": ["S256"],
  "client_id_metadata_document_supported": true,
  "scopes_supported": ["customers:read", "customers:write"]
}
```

ASM describes the **authorization server**: where authorization and token requests go, and which OAuth features it supports. It is a separate document because the same authorization server can serve many resource servers.

The client checks that the metadata’s `issuer` matches the issuer it intended to contact. This helps prevent it from mixing up authorization servers. The `jwks_uri` is a public-key location; it is commonly used by the MCP server to obtain keys for verifying signed tokens. The client reads the metadata; the resource server uses the keys to validate tokens.

Before the authorization server can process the login flow, it needs an OAuth **client identifier**: which software is asking to access the user’s data? This identifies the application, not the human user.

MCP’s 2026-07-28 client-registration guidance gives a selection order:

See the [MCP client registration specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/authorization/client-registration.mdx).

With CIMD, the client uses a stable HTTPS URL as its `client_id`. That URL serves a JSON document describing the client. For example, a company might publish:

```
{
  "client_id": "https://assistant.example.com/oauth/client-metadata.json",
  "client_name": "Example AI Assistant",
  "client_uri": "https://assistant.example.com",
  "redirect_uris": ["http://127.0.0.1:53682/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

This is illustrative; clients must follow the exact fields and validation requirements in the current CIMD specification. In particular, the `client_id` must be an HTTPS URL with a path, and its value must match the URL where the metadata is published exactly. The authorization server fetches the document, checks it, and validates that the requested redirect URI is one the document permits.

The authorization server advertises CIMD support in ASM, for example:

```
{ "client_id_metadata_document_supported": true }
```

If supported, the client can send this URL as its `client_id`. The AS obtains the client’s metadata from that URL instead of requiring a new registration record for every client/authorization-server pairing.

MCP clients often encounter authorization servers they have never seen before. Traditional pre-registration requires someone to create a client record in advance. DCR solved the onboarding problem by allowing clients to POST their metadata to a registration endpoint and receive a newly created client ID. But DCR requires the AS to operate a registration endpoint and manage registrations created dynamically.

CIMD changes where the client identity comes from: the client publishes its metadata at a stable HTTPS address it controls, and the AS retrieves that document when needed. The identifier can be reused with multiple authorization servers that support CIMD, without registering the client separately at each one. This reduces registration state and makes client identity more directly tied to control of a web domain.

CIMD is **not proof that the user is safe**, nor is it a replacement for user authentication, PKCE, scopes, or token validation. It identifies and describes the OAuth client. The authorization server still needs to validate the document and redirect URI; the user still signs in; and the MCP server still checks the resulting token.

**DCR is deprecated in the current MCP specification, not instantly removed.** It remains a compatibility path when an authorization server does not support CIMD. The video’s ScaleKit walkthrough is useful for understanding the DCR-based onboarding it demonstrates, but its statement that DCR is the default should not be repeated as current MCP guidance. [Current MCP registration guidance](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/authorization/client-registration.mdx)

The client starts an OAuth Authorization Code flow. Before opening the browser, it generates a high-entropy random secret called the **code verifier** and keeps it locally. It derives a **code challenge** using SHA-256 and Base64URL encoding:

```
code_verifier  = random secret stored temporarily by the client
code_challenge = BASE64URL(SHA256(code_verifier))
```

The client sends the challenge to the authorization endpoint, not the verifier. The verifier is held back until the client redeems the authorization code.

A simplified authorization request might contain:

```
response_type=code
client_id=https://assistant.example.com/oauth/client-metadata.json
redirect_uri=http://127.0.0.1:53682/callback
code_challenge=<derived-value>
code_challenge_method=S256
resource=https://mcp.example.com/mcp
scope=customers:read
state=<random-one-time-value>
```

The parameters have distinct jobs:

| Parameter | Meaning | 
|---|---|
| `client_id` | Which app is requesting authorization? With CIMD, this is the client metadata URL. | 
| `redirect_uri` | Where should the browser return after authorization? It must match a URI registered in the client metadata. | 
| `code_challenge` | What proof must be presented later to redeem the code? | 
| `resource` | Which protected resource should accept the resulting token? Here, the MCP server URL. | 
| `scope` | What kinds of access are requested? For example, read customer records. | 
| `state` | A client-generated value used to match the callback to the login attempt and defend against forged callbacks. | 

**Resource and scope are not the same.** Resource answers “which service is this token for?” Scope answers “what access is being requested at that service?”

PKCE is especially useful for public clients such as desktop apps, CLI tools, and IDEs, which cannot safely keep a client secret. The AS should advertise and support `S256`; the client should not silently downgrade to a weaker method.

The authorization server shows its sign-in and consent pages. The user signs in and approves access, if consent is required.

The AS then redirects the browser to the client’s registered `redirect_uri`, carrying a short-lived **authorization code** and the original `state` value. A local-app callback could look like:

```
http://127.0.0.1:53682/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj
```

The client is listening on that callback address (or has registered an application link) and reads the parameters. It checks that `state` matches the value it generated. Under the current MCP security guidance, the client also validates the authorization-response issuer (`iss`) when the AS provides it and advertises that behavior, to guard against authorization-server mix-up.

The browser redirect carries an **authorization code**, not the access token. The code is temporary and intended to be exchanged by the client.

The client sends a back-channel HTTPS request to the AS token endpoint. In simplified form:

```
grant_type=authorization_code
code=<authorization-code>
redirect_uri=http://127.0.0.1:53682/callback
client_id=https://assistant.example.com/oauth/client-metadata.json
code_verifier=<original-random-secret>
resource=https://mcp.example.com/mcp
```

The AS verifies the authorization code, client identity, redirect URI, and PKCE verifier. The verifier must correspond to the challenge from the earlier authorization request. If checks pass, the AS issues an access token and may issue a refresh token according to its policy.

A token response can look conceptually like this:

```
{
  "access_token": "<opaque-or-signed-token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "customers:read"
}
```

An access token is not necessarily a JWT. If it is a JWT, it may contain claims such as issuer (`iss`), subject (` sub`), audience (` aud`), scope, and expiration (` exp`). If it is opaque, the MCP server may validate it through introspection or another provider-supported method. Do not assume every provider returns a JWT.

The client stores and manages the token according to its security model, then repeats the original MCP operation. It places the access token in the HTTP header:

```
Authorization: Bearer <access-token>
```

The token does **not** go in MCP `_meta`. `_meta` carries MCP protocol information and client capabilities. The bearer token is an HTTP authorization credential and belongs in the `Authorization` header. A refresh token, when issued, is also OAuth credential material; it is not sent to the MCP server.

In the sessionless 2026-07-28 protocol, the request still includes the appropriate protocol headers and `_meta`, alongside the separate HTTP authorization header:

```
POST /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer <access-token>
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_customer
```

The client may reuse a valid token for later calls to that resource. When it expires, the client may use a refresh token to obtain another access token, subject to the authorization server’s rules.

The MCP server must not trust a token just because it is present or because it decodes as a JWT. It validates the token using the authorization server’s supported verification method and applies authorization policy. Typical checks include:

`iss`):` aud`):` exp`):
A valid `customers:read` scope does not automatically authorize access to every customer in the database. The handler must still enforce tenant and record-level permissions.

If the token is missing or invalid, the server returns HTTP 401 with an appropriate bearer challenge. If the token is valid but lacks a required scope, the server commonly returns HTTP 403 with an `insufficient_scope` challenge. Only an authorized request proceeds to the MCP handler.

OAuth is adjacent to, but separate from, the protocol features learned in earlier days:

| MCP concept | Question it answers | OAuth relationship | 
|---|---|---|
| JSON-RPC request ID and method | What operation is being requested, and which response matches it? | Not authentication. | 
| MCP capabilities | Which protocol features can each side use? | Not permission to access customer data. | 
| HTTP transport | How does a remote request travel? | Carries the `Authorization` header. | 
| `Mcp-Session-Id` (older protocol versions) | Which stateful protocol session is this? | Not an access token. | 
| `_meta` (modern protocol) | What protocol/client metadata accompanies this request? | Not a place to store bearer tokens. | 
| OAuth access token | Who authenticated, what grants were issued, and for which resource? | Validated before MCP processing. | 
| OAuth scope | What class of access was granted? | Used by the server’s authorization policy. | 
| MCP roots | Which filesystem locations are relevant to the client? | Advisory, not an OAuth permission or OS sandbox. | 
| MCP sampling | May the server request a client model completion? | Separate capability; it does not authorize data access. | 
| MRTR `requestState` | How can a paused MCP operation continue on a later round? | Not OAuth state or a bearer token. | 

A modern MRTR retry can carry OAuth and MCP fields at the same time: the HTTP bearer token authenticates the caller; `_meta` describes MCP protocol/client details; `inputResponses` supplies the answers to the server’s round-trip requests; and `requestState` carries opaque server continuation data. They are not interchangeable.

The client may temporarily hold the PKCE verifier until the authorization-code exchange finishes. It then stores the access token and, if issued, refresh token using an application-appropriate secure storage mechanism. For a desktop app, that may be the operating system keychain. Browser apps use different storage and threat models. Exact behavior is client-specific.

The client may also cache public PRM/ASM documents and the CIMD metadata it serves, following implementation and HTTP cache rules. These documents are public metadata, not credentials.

The MCP server publishes its PRM and keeps the configuration necessary to validate tokens—such as the expected issuer, audience/resource, scopes, and public-key source or introspection settings. It does not need to store a protocol session merely to validate a bearer token. A stateless JWT validation path can validate each request independently, though revocation and opaque-token policies may require additional infrastructure.

No. The access token is sent as an HTTP bearer credential. MCP `_meta` is protocol metadata. Never put access tokens or refresh tokens in `_meta`, tool arguments, prompts, or logs.

No. CIMD describes the **client application**. The user still authenticates at the authorization server. The MCP server still validates the resulting token.

No. The 2026-07-28 MCP specification deprecates DCR in favor of CIMD, but keeps it as a backward-compatible option for authorization servers that do not support CIMD. Follow the server metadata and the current client-registration priority.

A valid signature alone is not enough. The server must also check issuer, audience, expiry, scope, and application-level access. A token for another API must not be accepted just because the same identity provider signed it.

No. Resource identifies the intended server; scope identifies the requested level/type of access at that server.

Usually, **401** means authentication is missing or invalid. **403** means the caller is authenticated but lacks permission for the requested operation, such as a required scope.

No. Sessionless MCP removes protocol session setup from the 2026-07-28 revision, but a protected HTTP endpoint still authenticates each request using its bearer token.

No. Roots are a signal about relevant directories. The operating system, container, or sandbox must enforce actual file access limits.

`WWW-Authenticate` challenge for missing or invalid credentials.`resource` and the expected audience identify this MCP server, not a broad shared API audience.`customers:read` and `customers:write` and request only what is needed.`S256`; validate callback `state` and authorization-response issuer as applicable.`_meta`, tool arguments, model-visible content, and browser bundles.
The video’s central picture remains useful: an MCP client needs an access token; the server challenges with 401; PRM points toward an authorization server; PKCE protects code redemption; the `resource` parameter helps bind the token to the intended server; and the server validates the token before allowing tool use.

Its client-registration segment needs a date-aware update. It presents DCR as the onboarding mechanism and CIMD as the newer preferred option. Under the MCP 2026-07-28 specification, **CIMD is now the preferred dynamic registration approach**, while DCR is deprecated but retained for compatibility. If the authorization server advertises `client_id_metadata_document_supported: true`, a capable client should use CIMD according to the registration priority. If CIMD is unsupported, DCR may still be used if the server advertises a registration endpoint.

The database example makes the roles tangible:

`https://mcp.example.com/mcp`.
“For a protected remote MCP server, the client first gets a 401 challenge pointing to RFC 9728 Protected Resource Metadata. That tells it which authorization server protects the MCP resource. It discovers the AS endpoints through RFC 8414 or OIDC metadata, then identifies itself using pre-registration if configured, otherwise CIMD when supported, and DCR only as a compatibility fallback. The client runs Authorization Code with PKCE S256, requests a token for the MCP server using the RFC 8707 resource indicator and the minimum needed scopes, receives the authorization code through the registered browser callback, exchanges it with the PKCE verifier, and retries the MCP request with an HTTP bearer token. The server validates issuer, audience, expiry, scopes, and user-level authorization before executing the tool.”
