This is a personal note that I decided to share. It reflects my understanding of a subject and may contain errors and approximations. Feel free to contribute by contacting me here. Any help will be credited!


MCP (Model Context Protocol) is an open protocol that enables the connection of AI assistants to external tools and data sources. Companies can leverage their existing systems by building MCP Servers that empower their collaborators to interact with internal APIs. Existing authorization servers can easily be used for MCP Servers, minimizing authorization overhead.

With Adrien AUDOUARD, we deploy MCP Servers in our company. This note clarifies the key points of the MCP OAuth Authorization specification (protocol version 2025-11-25) and details what happens when an MCP Client connects to a protected MCP Server.

Key Points in the MCP OAuth Authorization Specification

RFC Standards at Play

The MCP authorization flow is mainly built on RFC standards that are worth knowing:

It is important to understand that, according to RFC 9728, the client discovers the authorization server dynamically at runtime from the MCP Server itself. Unlike classic web applications, you cannot configure the authorization server in the client.

MCP Server registration type with the Authorization Server : Public vs. Confidential

In OAuth’s model, the MCP Server takes on the role of a client relative to the authorization server. During configuration, you must choose between declaring it as a public or confidential client. The decision is driven by several factors:

Public Client Confidential Client
Secret needed No secret Secret storage (client secret)
Use cases Suitable for : - local MCP Server - receives and validates tokens - makes outbound authenticated call by forwarding the user’s token to downstream APIs. Suitable for : - hosted MCP Server - make outbound authenticated call (server-to-server)

MCP Client registration type with the Authorization Server : Public vs. Confidential

The same public vs. confidential distinction applies to the MCP Client. It depends on where the client runs and whether it can safely store a secret.

Public Client Confidential Client
Secret needed No secret Secret storage (client secret)
Use cases Suitable for local MCP Client ex: CLI Tools like Claude Code or Desktop app like Claude Suitable for hosted MCP Client (server-to-server) ex: Cloud agent pipeline calling a MCP Server autonomously
Auth flow Auth Code + PKCE (browser login) Client credentials

Client Registration

Pre-registered client

In a controlled environment, when building internal MCP Servers in a company, for example, you should pre-register your client. So Client and Server know each other, and there is an existing relationship.

This approach limits interoperability and cannot be deployed for customer-facing MCP Servers.

Client registration at runtime

Most of the time, the MCP Client can connect to many different MCP Servers from different organizations. It cannot be pre-registered with every authorization server it might encounter.

This is why the MCP specification states that MCP Clients and Authorization Servers may support client registration via two mechanisms:

As of the 2025-11-25 spec revision, the MCP specification is moving from DCR to Client ID Metadata Document.

The MCP Client Connection Flow (HTTP-based transport)

Here is what actually happens when an MCP Client connects to a protected MCP Server.

Step 1 — Initialize Handshake

The MCP Client starts normally. It connects to the MCP Server URL, and the two parties execute the initialize handshake. The server returns its capabilities: tools, prompts, resources, etc. No authentication yet at this stage. The client needs to discover the server’s capabilities before knowing which resource to authenticate against.

{
  "capabilities": {
    "logging": {},
    "completions": {},
    "prompts": {
      "listChanged": true
    },
    "resources": {
      "subscribe": false,
      "listChanged": true
    },
    "tools": {
      "listChanged": true
    }
  },
  "serverInfo": {
    "name": "my-mcp-server",
    "version": "0.0.10-SNAPSHOT"
  }
}

Step 2 — First Protected Request Returns 401

The first time the MCP Client calls a protected resource, the server responds with:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="...", resource_metadata="https://<MCP_SERVER>/.well-known/oauth-protected-resource"

The WWW-Authenticate header tells the client: you need a bearer token, and here is where to find out how to get one.

This is part of RFC 9728 — OAuth 2.0 Protected Resource Metadata. If the MCP Server does not specify the protected resource URL, it should support Well-Known URI as specified in the standard. It allows the MCP Client to discover the authorization configuration.

The MCP Client must support both discovery mechanisms:

  • resource_metadata in WWW-Authenticate header.
  • Well-Known URI.

Step 3 — Discover the Authorization Server (RFC 9728)

The MCP Client calls the protected resource metadata endpoint on the MCP Server:

Example of protected resource metadata endpoint:

GET /.well-known/oauth-protected-resource

The response is a JSON document that includes at least one authorization_servers entry:

{
  "resource": "https://<MCP_SERVER>",
  "authorization_servers": ["https://<YOUR_AS_URL>"],
  "scopes_supported": ["mcp:tools", "mcp:resources"]
}

The scopes_supported field lets the MCP Server advertise granular permissions (e.g., read-only vs. read-write access to tools). The MCP Client can then include the relevant scope parameter in its authorization request.

This is the key moment: the client now knows which authorization server is responsible for this resource.

Step 4 — Discover Authorization Server Metadata (RFC 8414)

Using the authorization server URL obtained in the previous step, the MCP Client calls:

GET https://<YOUR_AS_URL>/.well-known/oauth-authorization-server

The response contains everything the client needs to obtain tokens:

{
  "issuer": "https://<YOUR_AS_URL>",
  "authorization_endpoint": "https://<YOUR_AS_URL>/protocol/openid-connect/auth",
  "token_endpoint": "https://<YOUR_AS_URL>/protocol/openid-connect/token",
  "registration_endpoint": "https://<YOUR_AS_URL>/clients-registrations/openid-connect",
  "code_challenge_methods_supported": ["S256"]
}

Step 5 — (Optional) Client Registration

If the client is not already registered on this authorization server, it should use one of the two client registration flows (Client Metadata or DCR).

Step 6 — Authorization Code Flow with PKCE

For a public client (like Claude Code), the client initiates a standard OAuth 2.0 Authorization Code flow with PKCE:

  1. Generates a code_verifier and its SHA-256 hash code_challenge.
  2. Redirects the user to the authorization_endpoint with code_challenge and code_challenge_method=S256.
  3. After user authentication and consent, the authorization server redirects back with an authorization_code.
  4. The client exchanges the code for tokens at the token_endpoint, sending the original code_verifier.

For a confidential client (server-to-server), this step is replaced by a simple client credentials grant; no user interaction needed.

Step 7 — Call the MCP Server with the Access Token

The client now retries the original request with the access token:

Authorization: Bearer <access_token>
⚠ Info
The MCP specification’s sequence diagram covers this flow in full detail and is worth consulting alongside the steps above: https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-server-discovery-sequence-diagram

Step 8 — Token Refresh

MCP specification supports token refresh. This is important because MCP sessions can be long-lived (think of an agent running for several minutes or a long coding session with Claude Code). When the access token expires, the MCP Client should use the refresh token to get a new one without asking the user to log in again.

A Note on Authorization Server Configuration (with Keycloak)

The official tutorial uses a confidential client. This configuration is suited for MCP Servers that need to generate tokens on their own, but not for MCP Servers that use the caller’s security context to perform requests.

For local MCP Clients (Claude Code for example), a public client with PKCE is the recommended approach. In Keycloak, this means disabling “Client authentication” and enabling the Standard flow with S256 as PKCE method.

Building your MCP Server with the SDK

MCP SDKs (TypeScript, Python, Java, and others) handle the MCP specification for you: tools, prompts, resources, and also the authorization part. This means you get Protected Resource Metadata exposure out of the box, along with everything else.

On the authorization side, your MCP Server needs to:

  1. Return a 401 with the appropriate WWW-Authenticate header on protected endpoints.
  2. Serve the /.well-known/oauth-protected-resource document pointing to your authorization server URL.

If you use the TypeScript or Python MCP SDK, the two points above are handled by the SDK.

If you use the Java MCP SDK, it is slightly different. It relies on the underlying framework security (Spring Framework, Quarkus…). The authorization part of the SDK is integrated as hooks in the server transport layer, which is Jakarta Servlet. More details in the repository Readme.

In a Spring environment:

  • Use Spring AI MCP (built on top of Java MCP SDK).
  • MCP Security handles the responsibility of exposing Protected Resource Metadata.
  • For integration details: Spring AI MCP Security documentation.

References