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:
- OAuth 2.1 Authorization Framework (DRAFT): The upcoming revision of OAuth 2.0 that consolidates best practices. MCP authorization is built on top of it.
- RFC 9728 — OAuth 2.0 Protected Resource Metadata: Describes how a resource server (your MCP Server) advertises which authorization servers protect it.
- RFC 8414 — OAuth 2.0 Authorization Server Metadata: Describes how an authorization server exposes its configuration (token endpoint, supported grant types, etc.).
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:
- OAuth Client ID Metadata Document, standard still in draft.
- OAuth 2.0 Dynamic Client Registration Protocol, also known as DCR.
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_metadatainWWW-Authenticateheader.- 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:
- Generates a
code_verifierand its SHA-256 hashcode_challenge. - Redirects the user to the
authorization_endpointwithcode_challengeandcode_challenge_method=S256. - After user authentication and consent, the authorization server redirects back with an
authorization_code. - The client exchanges the code for tokens at the
token_endpoint, sending the originalcode_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>
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:
- Return a
401with the appropriateWWW-Authenticateheader on protected endpoints. - Serve the
/.well-known/oauth-protected-resourcedocument 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
- MCP Authorization Specification
- MCP Authorization — Sequence Diagram
- MCP Authorization Tutorial
- OAuth 2.1 Authorization Framework (DRAFT)
- RFC 9728 — OAuth 2.0 Protected Resource Metadata
- RFC 8414 — OAuth 2.0 Authorization Server Metadata
- RFC 7591 — OAuth 2.0 Dynamic Client Registration
- RFC 7636 — PKCE
- OAuth Client ID Metadata Document (DRAFT)
- MCP TypeScript SDK — GitHub
- MCP Python SDK — GitHub
- MCP Java SDK — GitHub
- Spring AI MCP Security