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 serverName and serverUrl
  • Retrieves an existing McpOAuthClientProvider from the internal Map if 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:

  1. Short-circuit check: If valid tokens already exist, throws AlreadyAuthorizedError
  2. Callback server startup: Calls startCallbackServer() to create a temporary local HTTP listener
  3. Provider configuration: Sets the provider's redirect URL to the callback server address
  4. Stale registration cleanup: Invalidates any existing DCR registration to prevent redirect URI mismatches
  5. SDK invocation: Calls auth() with authorize: true to 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 state parameter 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 McpOAuthCredentialsCoordinator instances 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: McpOAuthService caches one McpOAuthClientProvider per serverName/serverUrl pair, 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 with scope parameter 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →