How the MCP OAuth Flow Works for Authenticating External Servers in Kimi Code
The MCP OAuth flow in Kimi Code is a per-process orchestration system that uses temporary localhost callback servers and persistent disk storage to acquire and reuse access tokens for external MCP servers without requiring permanent public endpoints.
The Model-Context-Protocol (MCP) OAuth flow enables Kimi Code agents to securely authenticate with external HTTP-based MCP servers. Implemented in the @moonshot-ai/agent-core package, this system handles the complete Device-Code authorization lifecycle—from initiating authentication to persisting tokens for future sessions. This guide explains the architecture, implementation details, and practical usage based on the Kimi Code source code.
MCP OAuth Flow Architecture Overview
The authentication system centers on McpOAuthService, a singleton-style orchestrator that manages one McpOAuthClientProvider per server. This design ensures serialized OAuth interactions and prevents race conditions when multiple tools request authentication simultaneously.
The flow consists of six coordinated stages: provider retrieval, authorization initiation, user interaction, flow completion, token reuse, and cleanup. Each stage is implemented with specific error handling and state validation to protect against CSRF attacks and mismatched redirect URIs.
Stage 1: Provider Retrieval and Caching
The McpOAuthService.getProvider(serverName, serverUrl) method returns a cached provider or creates a new one. In packages/agent-core/src/mcp/oauth/service.ts lines 79-92, this method:
- Generates a unique key from
serverNameandserverUrl - Retrieves an existing
McpOAuthClientProviderfrom the internalMapif present - Creates and caches a new provider if none exists
- Configures the provider to persist credentials to
<KIMI_HOME>/credentials/mcp/
// From service.ts:79-92
const key = `${serverName}::${serverUrl}`;
let provider = this.providers.get(key);
if (!provider) {
provider = new McpOAuthClientProvider({
serverName,
serverUrl,
storagePath: path.join(this.kimiHome, 'credentials', 'mcp'),
});
this.providers.set(key, provider);
}
return provider;
The provider handles all server-specific operations: token storage, Dynamic Client Registration (DCR), and URL generation for the OAuth flow.
Stage 2: Beginning Authorization with Temporary Callback Server
The McpOAuthService.beginAuthorization() method in service.ts lines 106-150 drives the SDK's auth() orchestrator. This is the critical setup phase:
- Short-circuit check: If valid tokens already exist, throws
AlreadyAuthorizedError - Callback server startup: Calls
startCallbackServer()to create a temporary local HTTP listener - Provider configuration: Sets the provider's redirect URL to the callback server address
- Stale registration cleanup: Invalidates any existing DCR registration to prevent redirect URI mismatches
- SDK invocation: Calls
auth()withauthorize: trueto obtain the authorization URL
The method returns an object containing authorizationUrl, complete callback, and cancel callback:
// Conceptual structure based on service.ts:106-150
async beginAuthorization(serverName, serverUrl) {
const provider = this.getProvider(serverName, serverUrl);
// Check existing tokens
if (await provider.hasValidTokens()) {
throw new AlreadyAuthorizedError(serverName);
}
// Start temporary callback server
const callbackServer = await startCallbackServer(0); // Random available port
// Configure provider and clean stale registrations
provider.setRedirectUrl(callbackServer.getUrl());
await provider.invalidateRegistration();
// Drive SDK auth to get authorization URL
const result = await auth(provider, { authorize: true });
// result.status === 'REDIRECT' → extract authorizationUrl
return {
authorizationUrl: result.authorizationUrl,
complete: () => this.completeAuthorization(callbackServer, provider),
cancel: () => this.cancelAuthorization(callbackServer, provider),
};
}
The temporary callback server eliminates the need for a permanent public endpoint, making this suitable for local development and desktop applications.
Stage 3: User Authentication Flow
Once beginAuthorization() returns, the caller—typically the synthetic tool mcp__<server>__authenticate—presents the authorizationUrl to the user. Implementation options include:
- Opening the system default browser automatically
- Displaying the URL in the IDE interface for manual navigation
- Rendering the URL in a terminal or notification
The user authenticates with the external OAuth provider (e.g., GitHub, Google, custom provider), which then redirects to the localhost callback server with code and state parameters.
Stage 4: Completing the Flow and Token Storage
The complete() callback returned by beginAuthorization() handles the final stage. In service.ts lines 169-199:
- Waits for the callback server to receive the redirect via
callbackServer.waitForCode() - Validates the
stateparameter against the expected value to prevent CSRF attacks - Calls
auth()again with the received authorization code - On success, stores tokens to disk via the provider
- Tears down the callback server
- Notifies registered
McpOAuthCredentialsCoordinatorinstances of credential changes
// From service.ts:169-199
async complete() {
const callbackResult = await callbackServer.waitForCode();
// CSRF protection
if (callbackResult.state !== expectedState) {
throw new Error('CSRF state mismatch in OAuth callback');
}
// Exchange code for tokens
const authResult = await auth(provider, {
authorize: false,
authorizationCode: callbackResult.code,
});
// Persist and notify
await provider.saveTokens(authResult.tokens);
this.coordinators.forEach(c => c.onCredentialsChanged(serverName, serverUrl));
callbackServer.close();
return authResult;
}
Token storage uses a simple JSON-file store in src/mcp/oauth/store.ts, with credentials organized by server identity.
Stage 5: Token Reuse for Subsequent Connections
After initial authorization, McpOAuthService.hasTokens(serverName, serverUrl) (lines 95-98) checks for stored credentials. When tokens exist:
- The OAuth flow is bypassed entirely
- Stored tokens are attached directly to the HTTP transport
- Connection latency is reduced by eliminating the authorization round-trip
// From service.ts:95-98
async hasTokens(serverName: string, serverUrl: string): Promise<boolean> {
const provider = this.getProvider(serverName, serverUrl);
return provider.hasValidTokens();
}
This design optimizes for the common case where users authenticate once and use the server repeatedly across sessions.
Stage 6: Cancellation and Cleanup
The cancel() callback closes the callback server and resets provider state. For explicit token clearing, McpOAuthService.invalidate() accepts a scope parameter controlling deletion granularity:
'all'— Removes all credentials including DCR registration'tokens'— Removes only access and refresh tokens'discovery'— Removes only server metadata discovery data
// From service.ts:120-128
async invalidate(serverName, serverUrl, scope) {
const provider = this.getProvider(serverName, serverUrl);
await provider.invalidate(scope);
this.coordinators.forEach(c => c.onCredentialsChanged(serverName, serverUrl));
}
Implementation Example
Here's a complete working example using the McpOAuthService API:
import { McpOAuthService } from '@moonshot-ai/agent-core/src/mcp/oauth';
// Initialize with default KIMI home directory
const oauthService = new McpOAuthService();
async function authenticateMcpServer() {
try {
const { authorizationUrl, complete, cancel } = await oauthService.beginAuthorization(
'github-mcp', // Human-readable server name
'https://api.github.com/mcp', // MCP server base URL
);
console.log('Please authenticate:', authorizationUrl.toString());
// Optional: open browser automatically
// await open(authorizationUrl.toString());
// Wait for user to complete flow (5 minute timeout)
await complete({ timeoutMs: 5 * 60_000 });
console.log('Authentication successful—tokens stored.');
} catch (error) {
if (error.name === 'AlreadyAuthorizedError') {
console.log('Server already has valid credentials.');
} else {
console.error('Authentication failed:', error.message);
await oauthService.invalidate('github-mcp', 'https://api.github.com/mcp', 'tokens');
}
}
}
Security Guarantees and Error Handling
The Kimi Code MCP OAuth implementation provides several security properties:
- CSRF protection: State parameter validation in the callback handler
- Redirect URI integrity: Stale registration invalidation prevents URI mismatch errors
- No token exposure: Tokens never transit through Kimi Code servers; all exchanges are direct between client and MCP server
- Process isolation: One provider per server prevents cross-request token contamination
Errors are wrapped with contextual messages at each stage, making debugging straightforward. Common failure modes include network timeouts, invalid client credentials, and user cancellation.
Key Source Files
| Package | File Path | Purpose |
|---|---|---|
agent-core |
src/mcp/oauth/service.ts |
Core orchestrator: provider management, flow coordination, callbacks |
agent-core |
src/mcp/oauth/provider.ts |
McpOAuthClientProvider: token storage, DCR, URL generation |
agent-core |
src/mcp/oauth/callback-server.ts |
Temporary localhost HTTP server for OAuth redirects |
agent-core |
src/mcp/oauth/store.ts |
JSON-file persistence for MCP credentials |
protocol |
src/rest/oauth.ts |
REST API definitions for OAuth flow endpoints |
node-sdk |
src/v2/global-mcp.ts |
SDK exposure and engine integration |
Summary
- Single provider per server:
McpOAuthServicecaches oneMcpOAuthClientProviderperserverName/serverUrlpair, ensuring serialized OAuth interactions - Temporary callback architecture: The
startCallbackServer()approach eliminates the need for permanent public endpoints, enabling desktop-first authentication - Automatic token reuse:
hasTokens()checks allow subsequent connections to bypass OAuth entirely, attaching stored credentials directly to HTTP transports - Granular invalidation: The
invalidate()method withscopeparameter supports fine-grained credential cleanup - CSRF-protected completion: The
complete()callback validates state parameters before exchanging authorization codes
Frequently Asked Questions
What happens if I call beginAuthorization when tokens already exist?
The method throws AlreadyAuthorizedError immediately after checking provider.hasValidTokens(). This prevents unnecessary re-authentication and potential user confusion. To force re-authentication, call invalidate(serverName, serverUrl, 'tokens') first.
How long do tokens persist across Kimi Code sessions?
Tokens are stored on disk at <KIMI_HOME>/credentials/mcp/ and persist indefinitely until explicitly invalidated or revoked by the external OAuth provider. The McpOAuthClientProvider handles token refresh automatically when access tokens expire.
Can multiple MCP servers authenticate simultaneously?
Yes—each server has its own isolated McpOAuthClientProvider instance. However, within a single server, the per-process provider cache ensures that only one OAuth flow runs at a time, preventing race conditions in the stateful Device-Code flow.
What port does the temporary callback server use?
The callback server binds to port 0, which allows the operating system to assign an available ephemeral port. The actual URL (typically http://localhost:<random-port>/callback) is retrieved via callbackServer.getUrl() and registered as the OAuth redirect URI.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →