OpenCode MCP Server Architecture and OAuth Flow: A Deep Dive

OpenCode implements a modular MCP architecture supporting both local stdio processes and remote HTTP/SSE endpoints, with a secure OAuth 2.0 flow that uses a local callback server, CSRF state validation, and persistent credential storage tied to specific server URLs.

OpenCode (from the anomalyco/opencode repository) integrates external Model Context Protocol (MCP) services through a layered architecture that cleanly separates configuration, transport, and authentication concerns. This design enables both local tool execution via subprocesses and remote service integration via HTTP, with enterprise-grade OAuth 2.0 security for credential management.

Core Architecture Components

The MCP implementation in OpenCode is organized into distinct modules, each handling a specific layer of the protocol stack:

Component Responsibility Key Source File
MCP Manager Reads the mcp configuration section, creates clients, tracks lifecycle, and exposes high-level APIs (add, status, tools, startAuth, authenticate) packages/opencode/src/mcp/index.ts
MCP Client Implements the MCP wire protocol (tool lists, prompt handling, resource reading) over various transports @modelcontextprotocol/sdk/client (imported in index.ts)
Transport Layer StreamableHTTPClientTransport, SSEClientTransport, and StdioClientTransport handle remote HTTP/SSE and local stdio connections respectively Created dynamically in index.ts
OAuth Provider Supplies client metadata, tokens, code verifiers, and state handling to the MCP SDK; persists credentials via McpAuth packages/opencode/src/mcp/oauth-provider.ts
OAuth Callback Server Local HTTP server (default port 19876) that receives the IdP redirect, validates the state parameter, and resolves the pending authentication promise packages/opencode/src/mcp/oauth-callback.ts
Credential Store JSON file storage at ~/.local/share/opencode/mcp-auth.json recording tokens, client info, code verifiers, OAuth state, and associated server URLs packages/opencode/src/mcp/auth.ts

MCP Server Lifecycle

OpenCode manages MCP connections through a structured lifecycle that handles both local and remote server types:

1. Startup and Configuration

During initialization, MCP.state() reads the user-provided mcp section from opencode.json and iterates over each configured server. For enabled servers, it invokes the create() method to establish connections.

2. Transport Selection and Connection

The system selects transports based on server type:

  • Remote servers: Use StreamableHTTPClientTransport for standard HTTP or SSEClientTransport for Server-Sent Events. Both transports optionally receive an authProvider (the McpOAuthProvider instance) when OAuth is enabled.
  • Local servers: Spawn the command defined in the command array via StdioClientTransport, creating a subprocess with the specified environment variables.

3. Notification Handling and Tool Exposure

Once connected, the client registers a handler for ToolListChangedNotificationSchema. When the remote server pushes tool list changes, the system emits a Mcp.ToolsChanged bus event to notify the rest of the application.

Connected clients are queried for available tools, prompts, and resources. Each tool is wrapped into an OpenCode AI-SDK dynamic tool (dynamicTool) with a JSON schema derived directly from the MCP definition, enabling seamless integration with the AI orchestration layer.

OAuth 2.0 Flow Implementation

OpenCode implements a complete OAuth 2.0 authorization code flow with PKCE (Proof Key for Code Exchange) for secure authentication with remote MCP servers:

Step 1: Initiate Authentication (MCP.startAuth)

When starting authentication for a configured server (e.g., "github"):

  1. The callback server is ensured running via McpOAuthCallback.ensureRunning() on port 19876.
  2. A cryptographically secure state parameter is generated using crypto.getRandomValues and stored via McpAuth.updateOAuthState.
  3. A fresh McpOAuthProvider is instantiated (without client info initially) and paired with a StreamableHTTPClientTransport.
  4. The transport attempts connection, triggering the SDK's OAuth flow.
// Conceptual usage
const { authorizationUrl } = await MCP.startAuth("github");
console.log("Open this URL:", authorizationUrl);

Step 2: Handle Unauthorized Response

When client.connect(transport) encounters a 401 (UnauthorizedError), the MCP SDK invokes the provider's redirectToAuthorization method. The McpOAuthProvider forwards this authorization URL to the callback server's onRedirect handler, making it available to the application.

Step 3: Browser Authorization (MCP.authenticate)

The MCP.authenticate method receives the authorization URL from startAuth and opens the user's default browser using the open library. If the browser fails to open, a BrowserOpenFailed event is emitted, allowing fallback handling.

Step 4: Callback Handling

The local HTTP server at 127.0.0.1:19876/mcp/oauth/callback receives the IdP redirect:

  1. Extracts the code and state query parameters.
  2. Validates the state against the pending authentication map.
  3. Resolves the awaiting promise with the authorization code.

If state validation fails, the server returns a 400 error with an "OAuth callback missing state parameter" message.

Step 5: Token Exchange (MCP.finishAuth)

Once the callback resolves:

  1. Retrieves the pending transport for the server from pendingOAuthTransports.
  2. Calls transport.finishAuth(code), which performs the token exchange using the stored codeVerifier and client information.
  3. Persists the resulting tokens and client registration info via McpAuth.updateTokens and updateClientInfo.
  4. Clears the pending transport and re-adds the server using add() to establish a fully authenticated client connection.

Step 6: Token Refresh

The McpOAuthProvider implements the SDK's OAuthClientProvider interface, supplying stored tokens via tokens() and automatically refreshing them when expired. New tokens are persisted through saveTokens, ensuring continuous access without user intervention.

Security Safeguards

OpenCode implements multiple security layers to protect OAuth credentials and prevent CSRF attacks:

  • CSRF Protection: The state parameter is generated using crypto.getRandomValues, stored via McpAuth.updateOAuthState, and strictly validated in the callback server. Mismatched or missing state parameters result in immediate 400 errors.

  • Server URL Binding: McpAuth records the serverUrl alongside tokens and client info. The getForUrl method ensures credentials are only used when the URL matches exactly, preventing credential leakage across different MCP endpoints.

  • Dynamic Client Registration: If the server supports dynamic registration and no client ID is pre-configured, the provider returns undefined from clientInformation(), triggering automatic client registration. The resulting clientId and clientSecret are saved via saveClientInformation.

  • Credential File Permissions: The JSON credential file at ~/.local/share/opencode/mcp-auth.json is written with mode 0o600 (owner-only read/write), protecting tokens from other users on the system.

Configuration and Usage Examples

Basic Configuration

Define MCP servers in your opencode.json configuration file:

{
  "mcp": {
    "github": {
      "type": "remote",
      "url": "https://api.github.com/mcp",
      "headers": { "Accept": "application/vnd.mcp+json" },
      "oauth": {
        "clientId": "${env:GITHUB_MCP_CLIENT_ID}",
        "clientSecret": "${env:GITHUB_MCP_CLIENT_SECRET}",
        "scope": "repo"
      }
    },
    "local-tools": {
      "type": "local",
      "command": ["npx", "-y", "my-mcp-command"],
      "environment": { "DEBUG": "true" }
    }
  }
}

Programmatic OAuth Flow

Initiate and complete authentication programmatically:

import { MCP } from "opencode/mcp"

// Start the flow (generates state and opens the callback server)
const { authorizationUrl } = await MCP.startAuth("github")
console.log("Open this URL in a browser:", authorizationUrl)

// After the user authorizes, finish the flow
const finalStatus = await MCP.authenticate("github")
if (finalStatus.status === "connected") {
  console.log("MCP server is now authenticated")
}

The authenticate method internally opens the browser, waits for the callback at 127.0.0.1:19876, exchanges the authorization code for tokens, and re-establishes the connection with fresh credentials.

Summary

  • Modular Architecture: OpenCode separates MCP concerns into distinct layers—configuration, transport (HTTP/SSE/stdio), client protocol, and OAuth provider—enabling both local subprocess tools and remote HTTP services.

  • Dual Transport Support: The system automatically selects StreamableHTTPClientTransport or SSEClientTransport for remote servers, and StdioClientTransport for local commands defined in opencode.json.

  • Complete OAuth 2.0 Implementation: The flow includes PKCE support, cryptographically secure state generation, local callback server binding to port 19876, automatic token refresh, and persistent storage in ~/.local/share/opencode/mcp-auth.json.

  • Security-First Design: Credentials are bound to specific server URLs, protected with 0o600 file permissions, and guarded against CSRF through strict state parameter validation in packages/opencode/src/mcp/oauth-callback.ts.

Frequently Asked Questions

How does OpenCode handle authentication for remote MCP servers?

OpenCode implements a complete OAuth 2.0 authorization code flow with PKCE. When authentication is initiated via MCP.startAuth(), the system generates a secure state parameter, starts a local callback server on port 19876, and opens the user's browser to the authorization URL. After the user grants permission, the callback server receives the code, validates the state, and MCP.finishAuth() exchanges the code for tokens that are persisted to ~/.local/share/opencode/mcp-auth.json with strict 0o600 permissions.

What is the difference between local and remote MCP servers in OpenCode?

Local MCP servers run as subprocesses on the user's machine using StdioClientTransport, executing commands defined in the command array of the configuration. Remote MCP servers connect via HTTP using StreamableHTTPClientTransport or SSEClientTransport, supporting OAuth authentication and receiving server-pushed notifications. The architecture handles both types through the same MCP manager interface in packages/opencode/src/mcp/index.ts, automatically selecting the appropriate transport based on the type field in opencode.json.

How does OpenCode prevent CSRF attacks during OAuth authentication?

The system generates a cryptographically secure random state parameter using crypto.getRandomValues in packages/opencode/src/mcp/oauth-provider.ts, stores it via McpAuth.updateOAuthState(), and includes it in the authorization request. When the identity provider redirects back to the local callback server at 127.0.0.1:19876/mcp/oauth/callback, the code in packages/opencode/src/mcp/oauth-callback.ts extracts and validates the state against the pending authentication map. If the state is missing or mismatched, the server returns a 400 error and rejects the authentication attempt.

Where are OAuth credentials stored and how are they protected?

Credentials are stored in a JSON file at ~/.local/share/opencode/mcp-auth.json, managed by the McpAuth class in packages/opencode/src/mcp/auth.ts. The file contains access tokens, refresh tokens, client IDs, client secrets, code verifiers, and OAuth state, each bound to a specific server URL to prevent cross-server credential leakage. Security is enforced through Unix file permissions: the credential file is written with mode 0o600 (owner read/write only), ensuring that other users on the system cannot access sensitive authentication data.

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 →