Dynamic Client Registration and OAuth 2.0 for Remote MCP
Implementing RFC 7591 Dynamic Client Registration (DCR) and OAuth 2.0 Token Exchange for authenticating autonomous AI agents connecting to remote MCP servers.
Generate & Validate Multi-Client MCP Config
One-click export with environment variables & path locators for Claude Desktop, Cursor, Windsurf, and OpenAI Codex CLI.
Dynamic Client Registration and OAuth 2.0 for Remote MCP
As Model Context Protocol (MCP) servers transition to remote cloud environments accessible via Streamable HTTP and Server-Sent Events (SSE), static API keys become an acute enterprise vulnerability. Autonomous agents, ephemeral developer IDE containers, and multi-tenant agent frameworks cannot securely hardcode static credentials without risking credential sprawl, permission over-provisioning, and severe compliance violations under SOC 2 and ISO 27001.
Securing remote MCP infrastructure at scale requires standardized Dynamic Client Registration (DCR, RFC 7591) paired with OAuth 2.0 Token Exchange (RFC 8693). This enables AI clients to register ephemeral credentials on-the-fly, negotiate cryptographically scoped tool execution permissions, and automatically rotate access tokens without human intervention.
This guide provides the complete architectural pattern, wire-level protocol flows, and concrete implementation for establishing zero-trust OAuth 2.0 and DCR authentication for remote MCP servers.
1. Authentication Architecture: Ephemeral AI Clients
In an enterprise DCR architecture, an AI client host (e.g., Cursor, Claude Code, or a cloud-hosted LangGraph runner) first contacts the enterprise Identity Provider (IdP) or MCP Authorization Server. It registers its instance, obtains an ephemeral client_id and client_secret, exchanges an employee or workload identity token for an scoped MCP Access Token, and connects to the remote server.
sequenceDiagram
autonumber
participant Agent as AI Host Client (e.g. Cursor / Agent)
participant AuthServer as OAuth 2.0 / DCR Server (Okta/Keycloak)
participant MCP as Remote MCP Server (Streamable HTTP)
Agent->>AuthServer: POST /register (RFC 7591 DCR Request)
AuthServer-->>Agent: 201 Created (client_id, client_secret, scopes)
Agent->>AuthServer: POST /token (Grant: client_credentials / token-exchange)
AuthServer-->>Agent: 200 OK (access_token: mcp:db:read mcp:git:pr)
Agent->>MCP: GET /sse (Authorization: Bearer <access_token>)
MCP-->>Agent: 200 OK (Event Stream Established)
Agent->>MCP: POST /messages [tools/call: query_records]
MCP-->>Agent: 200 OK (Execution Result)2. Dynamic Client Registration (RFC 7591) Wire Flow
When a developer spins up a new ephemeral development sandbox or an autonomous agent initializes a task, the client programmatically registers itself using RFC 7591:
Client Registration Request (POST /oauth/register)
POST /oauth/register HTTP/1.1
Host: auth.enterprise.internal
Content-Type: application/json
Authorization: Bearer <INITIAL_REGISTRATION_TOKEN>
{
"client_name": "Cursor-DevInstance-user-8921",
"grant_types": ["client_credentials", "urn:ietf:params:oauth:grant-type:token-exchange"],
"response_types": ["token"],
"scope": "mcp:tools:read mcp:tools:call:db_query mcp:tools:call:k8s_read",
"token_endpoint_auth_method": "private_key_jwt",
"software_id": "4c9d8a-cursor-agent",
"software_version": "2026.3.1"
}Authorization Server Registration Response
The server validates the policy, provisions the client, bounds its maximum permissions, and sets an aggressive registration expiration:
HTTP/1.1 201 Created
Content-Type: application/json
Cache-Control: no-store
{
"client_id": "mcp-client-881b29a-4c22",
"client_secret": "sec_77f81a0e8d91c2b3e4f5a6b7c8d9e0f1",
"client_id_issued_at": 1773446400,
"client_secret_expires_at": 1773532800,
"registration_client_uri": "https://auth.enterprise.internal/oauth/register/mcp-client-881b29a-4c22",
"scope": "mcp:tools:read mcp:tools:call:db_query"
}[!NOTE] In production environments, client credentials registered via DCR should be granted a maximum Time-To-Live (TTL) of 24 hours. Ephemeral agent workers terminate their credentials upon task completion.
3. Scoped Token Minting & JSON-RPC Access Control
Once registered, the AI host exchanges its identity for an MCP-scoped JWT access token:
Token Request (POST /oauth/token)
POST /oauth/token HTTP/1.1
Host: auth.enterprise.internal
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=mcp-client-881b29a-4c22
&client_secret=sec_77f81a0e8d91c2b3e4f5a6b7c8d9e0f1
&scope=mcp:tools:call:db_queryResulting JWT Payload (Decoded)
{
"iss": "https://auth.enterprise.internal",
"sub": "mcp-client-881b29a-4c22",
"aud": "https://mcp-server.enterprise.internal",
"exp": 1773450000,
"nbf": 1773446400,
"scope": "mcp:tools:read mcp:tools:call:db_query",
"enterprise": {
"org_id": "engineering_core",
"user_email": "jane.dev@enterprise.internal",
"clearance_level": "l4"
}
}4. MCP Server Authorization Middleware Implementation (TypeScript)
The remote MCP server verifies the JWT token on both the initial SSE connection and on individual tools/call POST invocations, enforcing granular scope checks per tool.
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
import fastifyJwt from '@fastify/jwt';
import jwksRsa from 'jwks-rsa';
const app = Fastify({ logger: true });
// 1. Configure JWKS validation for enterprise IdP (Okta / Keycloak)
const jwksClient = jwksRsa({
jwksUri: 'https://auth.enterprise.internal/.well-known/jwks.json',
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 10
});
// Helper: Custom JWT Secret Provider for Remote Key Sets
const getPublicKey = async (req: FastifyRequest, token: any) => {
const decodedToken = token as { header: { kid: string } };
const key = await jwksClient.getSigningKey(decodedToken.header.kid);
return key.getPublicKey();
};
app.register(fastifyJwt, {
secret: getPublicKey,
algorithms: ['RS256']
});
// 2. Scope Verification Decorator
function enforceToolScope(requiredScope: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
try {
await request.jwtVerify();
const user = request.user as { scope?: string };
const scopes = (user.scope || '').split(' ');
if (!scopes.includes(requiredScope) && !scopes.includes('mcp:admin')) {
reply.status(403).send({
jsonrpc: '2.0',
id: (request.body as any)?.id || null,
error: {
code: -32003,
message: `Forbidden: Missing required OAuth 2.0 scope '${requiredScope}'`,
data: { requiredScope, currentScopes: scopes }
}
});
}
} catch (err) {
reply.status(401).send({
jsonrpc: '2.0',
id: (request.body as any)?.id || null,
error: {
code: -32002,
message: 'Unauthorized: Invalid or expired OAuth 2.0 Bearer token'
}
});
}
};
}
// 3. Authenticated Streamable SSE Endpoint
app.get('/sse', { preHandler: [app.authenticate] }, async (request, reply) => {
reply.raw.setHeader('Content-Type', 'text/event-stream');
reply.raw.setHeader('Cache-Control', 'no-cache');
reply.raw.setHeader('Connection', 'keep-alive');
reply.raw.flushHeaders();
const sessionId = generateSessionId();
reply.raw.write(`event: endpoint\ndata: /messages?sessionId=${sessionId}\n\n`);
});
// 4. Scoped Tool Execution Route
app.post('/messages', async (request: FastifyRequest, reply: FastifyReply) => {
const body = request.body as { jsonrpc: string; id: any; method: string; params: any };
if (body.method === 'tools/call') {
const toolName = body.params?.name;
const requiredScope = `mcp:tools:call:${toolName}`;
// Execute dynamic scope evaluation
await enforceToolScope(requiredScope)(request, reply);
if (reply.sent) return;
// Proceed to tool execution
const result = await dispatchToolExecution(toolName, body.params?.arguments);
return reply.send({
jsonrpc: '2.0',
id: body.id,
result: { content: [{ type: 'text', text: JSON.stringify(result) }] }
});
}
// Handle standard lifecycle methods
return reply.send({ jsonrpc: '2.0', id: body.id, result: { status: 'acknowledged' } });
});
async function dispatchToolExecution(name: string, args: any) {
return { status: 'success', data: 'Protected enterprise data payload' };
}
function generateSessionId() {
return 'sess_' + Math.random().toString(36).substring(2, 15);
}
app.listen({ port: 8080, host: '0.0.0.0' });5. Token Exchange (RFC 8693) for Multi-Agent Workflows
In agentic chains where an orchestrator agent delegates subtasks to specialized worker agents, passing the orchestrator's broad administrative token violates least privilege. Instead, the orchestrator performs an OAuth 2.0 Token Exchange (RFC 8693) to downscope permissions for downstream subagents.
POST /oauth/token HTTP/1.1
Host: auth.enterprise.internal
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=eyJhbGciOiJSUzI1Ni... (Orchestrator Token)
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=https://mcp-analytics.internal
&scope=mcp:tools:call:aggregate_metricsThe authorization server returns an access token restricted solely to aggregate_metrics, guaranteeing that if the subagent experiences a prompt injection attack, it cannot invoke broader tools like delete_database or update_user_roles.
6. Standard Client Configuration with Dynamic Tokens
Modern MCP hosts support dynamic bearer authentication via environment variables or local token helper binaries.
Cursor / Claude Desktop Setup (mcp.json)
{
"mcpServers": {
"enterprise-oauth-mcp": {
"url": "https://mcp.ai.enterprise.com/sse",
"headers": {
"Authorization": "Bearer ${ENTERPRISE_OAUTH_TOKEN}"
}
}
}
}Token Helper Automated Refresh Daemon
To automate token acquisition, enterprise IT teams deploy an OS-level token daemon that handles DCR and refreshes the local token cache in ~/.mcp/tokens.json:
# Developer initial bootstrap
mcp-auth-cli login --dcr-endpoint=https://auth.enterprise.internal/oauth/register
# Token helper automatically writes active JWT:
export ENTERPRISE_OAUTH_TOKEN=$(cat ~/.mcp/active_token.jwt)Related Security & Architecture Guides
Build your full agent toolstack in the Visual Generator
Combine Dynamic Client Registration and OAuth 2.0 for Remote MCP with databases, search APIs, and memory graphs in a single configuration file.
Dynamic Client Registration and OAuth 2.0 for Remote MCP FAQ
What is the Dynamic Client Registration and OAuth 2.0 for Remote MCP?
Implementing RFC 7591 Dynamic Client Registration (DCR) and OAuth 2.0 Token Exchange for authenticating autonomous AI agents connecting to remote MCP servers.
How do I configure Dynamic Client Registration and OAuth 2.0 for Remote MCP in Claude Desktop or Cursor?
You can copy the configuration JSON from our guide or launch the interactive MCP Codex Config Generator at https://mcp-codex.com/generator to export valid configs in 1 click.
Can I use Dynamic Client Registration and OAuth 2.0 for Remote MCP with the OpenAI Codex CLI?
Yes, OpenAI Codex CLI supports Model Context Protocol. You can add it directly to ~/.codex/config.toml or pass arguments to codex mcp add.
Specializing in Model Context Protocol (MCP) integrations, autonomous AI agent orchestration, and distributed developer toolchains. Researches and benchmarks production MCP client-server architectures across OpenAI Codex, Claude, and Cursor.
Related Guides
Deploying Remote MCP Servers on Kubernetes at Scale
Production guide to deploying containerized remote Model Context Protocol (MCP) servers on Kubernetes with Helm, JSON-RPC queue-based HPA, and secure Ingress.
EnterpriseHardening MCP for SOC 2 and HIPAA Enterprise Workflows
Compliance architecture for Model Context Protocol (MCP): Client-side PII masking, immutable JSON-RPC audit logging, and zero-knowledge data pipelines for SOC 2 and HIPAA.
EnterpriseSandboxing MCP Server Execution: Containers to MicroVMs
Isolating Model Context Protocol (MCP) server execution to neutralize arbitrary code execution, filesystem escapes, and credential exfiltration using Docker rootless, gVisor, and Firecracker microVMs.