# OpenCode MCP Server Architecture and OAuth Flow: A Deep Dive

> Explore the OpenCode MCP server architecture and OAuth flow. Learn about its modular design, local callback server, CSRF validation, and persistent credential storage for secure API access.

- Repository: [Anomaly/opencode](https://github.com/anomalyco/opencode)
- Tags: deep-dive
- Published: 2026-02-16

---

**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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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.

```typescript
// 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`](https://github.com/anomalyco/opencode/blob/main/opencode.json) configuration file:

```json
{
  "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:

```typescript
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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/mcp/index.ts), automatically selecting the appropriate transport based on the `type` field in [`opencode.json`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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.