MCP OAuth 2.1: Implement Auth Without Dynamic Client Registration
HTTP-transport MCP servers require OAuth 2.1, but the default client registration step assumes your auth provider supports Dynamic Client Registration (DCR). Most major providers - Auth0, Amazon Cognito, Okta - do not support DCR out of the box, and the MCP spec says nothing about what to do when they don't. There are two practical patterns to unblock any existing setup: pre-registering the client manually (the drop-in fix for Auth0 and Cognito users), and switching to the Client Credentials grant for server-to-server agent pipelines. This guide covers both, plus a walkthrough of the PKCE authorization code flow and the fix for VS Code's mcp oauth: disabled error.
Key takeaways:
- OAuth 2.1 is the required authorization framework for all HTTP-transport MCP servers; STDIO servers can use environment-based credentials instead
- Dynamic Client Registration is part of the spec flow but is explicitly optional; pre-registration is a supported alternative
- The Client Credentials grant fits machine-to-machine agents that never involve a browser redirect
- VS Code shows
mcp oauth: disabledwhen the server's authorization endpoint is unreachable or misconfigured
Why HTTP-transport MCP requires OAuth 2.1
The MCP protocol uses OAuth 2.1 (draft-ietf-oauth-v2-1-13) for HTTP-transport servers because these servers are remotely hosted and need a standard way to establish that a user is authorized to access them. STDIO-based servers, which run locally alongside the client, can skip the whole flow and rely on environment credentials or embedded secrets instead. (Source: MCP Authorization Spec)
OAuth 2.1 tightens up its predecessor by removing the implicit grant (which leaked tokens into browser history), mandating PKCE for all authorization code flows, and requiring refresh token rotation to turn token theft into a detectable event. The resource indicator extension (RFC 8707) binds tokens to a specific server URI, which prevents a token issued for one MCP server from being replayed against another. (Source: Practical DevSecOps OAuth 2.1)
The HTTP authorization flow consists of six stages: the server returns a 401 Unauthorized with a link to its Protected Resource Metadata document; the client discovers the authorization server; the client registers (via DCR or pre-registration); the user authenticates with PKCE; the client exchanges the code for tokens; and every subsequent request carries the bearer token in the Authorization header. (Source: MCP Authorization Spec)
The DCR blocker: why your auth provider rejects the standard flow
Dynamic Client Registration (RFC 7591) lets an MCP client register itself with the authorization server at runtime by posting its metadata to a registration_endpoint. The MCP spec treats this as the default client registration mechanism. The problem: most self-hosted and SaaS auth providers do not expose a registration endpoint that accepts unauthenticated requests. (Source: MCP Authorization Spec)
| Auth provider | DCR supported out of the box? | Notes |
|---|---|---|
| Keycloak | Yes | DCR enabled per-realm; client URIs must be trusted hosts |
| Auth0 | No (by default) | Requires Management API + custom middleware |
| Amazon Cognito | No | No registration endpoint; pre-registration required |
| Okta | No (by default) | Requires Dynamic Client Registration API add-on |
| Azure AD / Entra | No | App registration via portal or Graph API |
| Custom OAuth server | Depends | Implement RFC 7591 to enable |
When the MCP client hits an auth server that does not respond to DCR, the spec states: "It is the responsibility of the client developer to provide an affordance for the end-user to enter client information manually." In practice, this means the client needs to be pre-registered and carry static credentials. (Source: MCP Authorization Spec)
Pattern 1: Pre-registering your MCP client
Pre-registration replaces the DCR step with a one-time manual setup in your auth provider's dashboard or API, after which the client carries a static client_id (and optionally a client_secret for confidential clients). The rest of the OAuth 2.1 + PKCE flow works identically.
The MCP server still needs to expose Protected Resource Metadata (RFC 9728) so the client can discover which authorization server to use:
{
"resource": "https://your-mcp-server.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["mcp:tools"]
}
This document is served from /.well-known/oauth-protected-resource on the MCP server. Most MCP SDKs expose a helper router (mcpAuthMetadataRouter in the TypeScript SDK) that generates this response automatically from your configuration. (Source: MCP Authorization Spec)
For Auth0, create a Machine-to-Machine application in the dashboard, enable the scopes your server requires, and hardcode the returned client_id in your MCP client configuration. For Amazon Cognito, create an App Client with the appropriate OAuth flows enabled (Authorization Code + PKCE) and note the client ID from the pool settings. Neither requires any changes to the MCP server itself.
Operator note (first-hand): For local development and testing with a DCR-capable server, the fastest path is Keycloak in Docker: docker run -p 127.0.0.1:8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak start-dev. After creating a mcp:tools client scope and configuring trusted hosts under Client Registration, VS Code's mcp oauth: disabled disappears on reconnect. Keycloak's OIDC discovery endpoint (http://localhost:8080/realms/master/.well-known/openid-configuration) exposes the registration_endpoint that MCP clients expect.
Pattern 2: Client Credentials for machine-to-machine agents
Some agent pipelines never involve a human browser redirect. A backend agent that calls an internal MCP server on a schedule, or an orchestrator that invokes a tool server with no user context, can use the Client Credentials grant instead of the authorization code + PKCE flow. Client Credentials exchanges a client_id and client_secret directly for an access token at the token endpoint.
POST https://auth.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=<your-client-id>
&client_secret=<your-client-secret>
&scope=mcp:tools
The MCP server validates this token identically to a token obtained via the PKCE flow. The difference is that the token represents the client application's identity, not a specific user. (Source: Practical DevSecOps OAuth 2.1)
Client Credentials is the right choice when: the agent runs unattended (no human in the loop), the tool server contains no user-specific data, and you can store the client secret securely (in a secret manager, not in source code). It is the wrong choice when the MCP server enforces per-user scopes or needs the user's identity in the access token.
Walking through the PKCE authorization code flow
For human-in-the-loop agents that need per-user tokens, the PKCE flow works as follows:
- The agent generates a random 32-byte
code_verifierand computescode_challenge = BASE64URL(SHA256(code_verifier)). - It redirects the user to the authorization endpoint with
response_type=code,client_id,redirect_uri,scope=mcp:tools,code_challenge, andcode_challenge_method=S256. - The user authenticates and grants the requested scopes. The authorization server redirects to
redirect_uriwith an authorizationcode. - The agent posts
code,code_verifier,client_id, andredirect_urito the token endpoint. - The authorization server validates that
SHA256(code_verifier)matches the originalcode_challengeand returns an access token and refresh token. - The agent includes
Authorization: Bearer <access_token>in every MCP request. (Source: MCP Authorization Spec)
The MCP server validates the token and checks the audience claim to confirm the token was issued for this specific server (RFC 8707 resource indicator), rejecting tokens from other servers that share the same authorization server. (Source: Practical DevSecOps OAuth 2.1)
Fixing "VS Code MCP OAuth: disabled"
When VS Code shows mcp oauth: disabled for an HTTP-transport MCP server, there are three common root causes.
The Protected Resource Metadata endpoint (/.well-known/oauth-protected-resource) is unreachable or returns an unexpected response. Check with curl https://your-server.example.com/.well-known/oauth-protected-resource and confirm the JSON includes authorization_servers and resource fields.
The authorization server's discovery endpoint (RFC 8414) is not returning registration_endpoint or authorization_endpoint. For Auth0 and Cognito, this endpoint exists but does not include registration_endpoint because DCR is disabled; VS Code may fall back to a disabled state when it cannot complete registration dynamically. Switching to static configuration (providing client_id in the VS Code MCP config) resolves this.
The server is using STDIO transport, which does not support OAuth at all. If the server command is a stdio entry, OAuth configuration has no effect by design. (Source: MCP Authorization Spec)
Frequently asked questions
Does MCP require OAuth 2.1 for all servers?
No. OAuth 2.1 is required only for HTTP-transport MCP servers (Streamable HTTP, SSE). STDIO servers run locally alongside the client and can use environment variables, API keys, or credentials embedded by the host application instead. The spec explicitly states that OAuth flows "are designed for HTTP-based transports where the MCP server is remotely-hosted."
What is Dynamic Client Registration (DCR) and why does it cause problems?
DCR (RFC 7591) lets an MCP client automatically register itself with the authorization server at runtime by posting metadata to a registration_endpoint. It causes problems because most production auth providers, including Auth0, Cognito, Okta, and Azure AD, do not expose this endpoint by default. You can work around DCR by pre-registering the client manually in your provider's dashboard and giving the MCP client a static client_id.
Can I use long-lived API keys instead of OAuth for HTTP-transport MCP servers?
The MCP spec does not support API keys as a replacement for the OAuth 2.1 authorization flow in HTTP-transport deployments. Long-lived API keys lack PKCE protection, audience binding, and refresh token rotation, making them vulnerable to replay attacks across servers. Use the Client Credentials grant (short-lived tokens with automatic expiry) for server-to-server scenarios instead.
How does the PKCE code verifier prevent authorization code interception?
PKCE (RFC 7636) requires the agent to generate a random code_verifier and send only its SHA-256 hash (code_challenge) during the authorization redirect. The authorization server stores the hash. When the code is exchanged for a token, the agent sends the original code_verifier; the server recomputes the hash and checks that it matches. An attacker who intercepts the authorization code cannot exchange it without also knowing the code_verifier.
What does audience validation do in MCP token verification?
The aud claim in the access token names the MCP server URI it was issued for (via RFC 8707 Resource Indicators). The MCP server checks that the token's audience matches its own URI, rejecting any token issued for a different API even if both share the same authorization server. This prevents a compromised server from replaying tokens intended for another.
Related coverage
- MCP transport security: STDIO, SSE, and Streamable HTTP risks
- FastMCP OAuth token validation: server-side patterns and pitfalls
- How to check if your MCP server is exposed
References
- MCP Authorization Spec - https://modelcontextprotocol.io/docs/tutorials/security/authorization
- OAuth 2.1 Draft - https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13
- Practical DevSecOps OAuth 2.1 - https://www.practical-devsecops.com/mcp-oauth-2-1-implementation/
- RFC 7591 Dynamic Client Registration - https://datatracker.ietf.org/doc/html/rfc7591
- TypeScript MCP auth sample - https://github.com/localden/min-ts-mcp-auth



