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
StreamableHTTPClientTransportfor standard HTTP orSSEClientTransportfor Server-Sent Events. Both transports optionally receive anauthProvider(theMcpOAuthProviderinstance) when OAuth is enabled. - Local servers: Spawn the command defined in the
commandarray viaStdioClientTransport, 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"):
- The callback server is ensured running via
McpOAuthCallback.ensureRunning()on port 19876. - A cryptographically secure
stateparameter is generated usingcrypto.getRandomValuesand stored viaMcpAuth.updateOAuthState. - A fresh
McpOAuthProvideris instantiated (without client info initially) and paired with aStreamableHTTPClientTransport. - 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:
- Extracts the
codeandstatequery parameters. - Validates the state against the pending authentication map.
- 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:
- Retrieves the pending transport for the server from
pendingOAuthTransports. - Calls
transport.finishAuth(code), which performs the token exchange using the storedcodeVerifierand client information. - Persists the resulting tokens and client registration info via
McpAuth.updateTokensandupdateClientInfo. - 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
stateparameter is generated usingcrypto.getRandomValues, stored viaMcpAuth.updateOAuthState, and strictly validated in the callback server. Mismatched or missing state parameters result in immediate 400 errors. -
Server URL Binding:
McpAuthrecords theserverUrlalongside tokens and client info. ThegetForUrlmethod 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
undefinedfromclientInformation(), triggering automatic client registration. The resultingclientIdandclientSecretare saved viasaveClientInformation. -
Credential File Permissions: The JSON credential file at
~/.local/share/opencode/mcp-auth.jsonis written with mode0o600(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
StreamableHTTPClientTransportorSSEClientTransportfor remote servers, andStdioClientTransportfor local commands defined inopencode.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →