How Opencode JSONC Configuration Works with MCP Servers in OpenWork
OpenWork uses opencode.jsonc—a JSON-with-comments configuration file—to declaratively define MCP server endpoints, authentication, and connection parameters, which the runtime loads on startup and exposes via getConfig() for the MCP client to consume.
The opencode.jsonc format is central to how OpenWork connects to its Managed Capabilities Platform (MCP). This approach separates infrastructure concerns from application logic, allowing teams to switch MCP environments, rotate credentials, or adjust timeouts without modifying source code.
Understanding the Opencode JSONC Format
OpenWork adopted JSONC (JSON with Comments) as its configuration language. The .jsonc extension signals that C-style comments are permitted, making it easier to document configuration choices inline.
According to the OpenWork source code, the configuration loader resides in src/utils/opencode.ts. This module handles:
- Parsing JSONC files with comment stripping
- Deep-merging environment-specific overlays (e.g.,
opencode.dev.jsonc) - Resolving
envVarreferences to inject secrets at runtime
The loader never persists resolved secrets to disk or logs, ensuring credentials remain in memory only.
MCP Server Configuration Structure
The MCP server is defined under the servers.mcp key in opencode.jsonc:
{
"servers": {
"mcp": {
"url": "https://api.openworklabs.com/mcp/agent",
"auth": {
"type": "bearer",
"envVar": "OPENWORK_MCP_TOKEN"
},
"timeoutMs": 30000
}
}
}
Configuration Fields Explained
url: The base endpoint for all MCP operations. Switch this value to point at staging, production, or local mock servers.auth.type: Currently supports"bearer"token authentication. Theauthblock describes how to authenticate, not the secret itself.auth.envVar: Names the environment variable containing the actual token. The loader readsprocess.env.OPENWORK_MCP_TOKENat startup and injects its value.timeoutMs: Optional network timeout in milliseconds. Defaults to 15000 if omitted.
Configuration Loading and Runtime Flow
The startup sequence in src/main.ts orchestrates configuration initialization:
// src/main.ts — simplified startup flow
import { loadOpencode } from '@/utils/opencode';
async function bootstrap() {
const config = await loadOpencode(); // reads opencode.jsonc + overlay
// config.servers.mcp now contains resolved URL and token
initializeMcpClient(config.servers.mcp);
// ... remainder of application startup
}
Step-by-Step Loading Process
- Base file read:
opencode.jsoncis parsed from the repository root - Overlay detection: If
NODE_ENV=development, the loader looks foropencode.dev.jsonc - Deep merge: Overlay values recursively replace base values
- Secret injection: All
envVarreferences are resolved fromprocess.env - Validation: The configuration object is checked for required MCP fields
- Export:
getConfig()returns a frozen configuration singleton
The src/config/index.ts module exposes the loaded configuration:
// src/config/index.ts
import { readOpencode } from '@/utils/opencode';
let cachedConfig: ReturnType<typeof readOpencode> | null = null;
export const getConfig = () => {
if (!cachedConfig) {
cachedConfig = readOpencode();
}
return cachedConfig;
};
MCP Client Implementation
The McpClient class in src/mcp/client.ts consumes the configuration to build a typed API wrapper:
// src/mcp/client.ts
import { getConfig } from '@/config';
interface McpConfig {
url: string;
auth: { type: string; token: string };
timeoutMs?: number;
}
export class McpClient {
private baseUrl: string;
private token: string;
private timeout: number;
constructor() {
const cfg = getConfig().servers.mcp as McpConfig;
this.baseUrl = cfg.url;
this.token = cfg.auth.token; // injected from envVar at load time
this.timeout = cfg.timeoutMs ?? 15000;
}
async request<T>(path: string, init?: RequestInit): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const resp = await fetch(`${this.baseUrl}${path}`, {
...init,
headers: {
...init?.headers,
Authorization: `Bearer ${this.token}`,
},
signal: controller.signal,
});
if (!resp.ok) {
throw new Error(`MCP request failed: ${resp.status} ${resp.statusText}`);
}
return await resp.json() as T;
} finally {
clearTimeout(timeoutId);
}
}
}
All MCP operations—skill discovery, plugin execution, policy fetching—route through this client, ensuring consistent authentication, timeout handling, and error propagation.
Environment-Specific Overrides
One of the strongest features of the Opencode configuration system is overlay support. Teams can maintain multiple environment configurations without modifying the base file.
Local Development Example
Create opencode.dev.jsonc in the repository root:
{
"servers": {
"mcp": {
"url": "http://localhost:4000/mcp/agent"
// auth and timeoutMs inherited from base opencode.jsonc
}
}
}
Start the application with NODE_ENV=development, and the MCP client automatically connects to your local server. The token is still injected from OPENWORK_MCP_TOKEN, which can point to a development credential.
Staging and Production Patterns
- Staging:
opencode.staging.jsoncwithurlpointing to staging MCP gateway - Production: No overlay; production values live in the base
opencode.jsonc - CI/CD: Override via environment variables injected by your deployment pipeline
Security Considerations
The Opencode design explicitly avoids credential exposure:
| Risk | Mitigation |
|---|---|
| Secrets in version control | envVar references only; actual tokens never committed |
| Secret logging | Loader excludes auth.token from serializations |
| Token leakage in error messages | McpClient sanitizes headers before throwing |
When debugging configuration, use getConfig() but never log the full auth object. The OpenWork codebase includes utility functions to redact sensitive fields.
Complete Working Example
Here's an end-to-end pattern for integrating MCP operations with Opencode configuration:
// src/skills/loader.ts
import { McpClient } from '@/mcp/client';
interface Skill {
id: string;
name: string;
version: string;
capabilities: string[];
}
export class SkillLoader {
private client: McpClient;
constructor() {
this.client = new McpClient();
}
async fetchAvailable(): Promise<Skill[]> {
return this.client.request<Skill[]>('/v1/skills');
}
async execute(skillId: string, input: unknown): Promise<unknown> {
return this.client.request<unknown>(`/v1/skills/${skillId}/execute`, {
method: 'POST',
body: JSON.stringify(input),
headers: { 'Content-Type': 'application/json' },
});
}
}
This module requires zero hardcoded URLs or credentials—it inherits everything from opencode.jsonc through the McpClient constructor chain.
Summary
- Opencode JSONC is OpenWork's declarative configuration format, supporting comments and environment overlays
- MCP server configuration lives under
servers.mcpinopencode.jsonc, specifyingurl,auth, andtimeoutMs - Secret injection happens at runtime via
envVarreferences, keeping credentials out of source control - Configuration loading in
src/utils/opencode.tshandles parsing, merging, and validation - McpClient in
src/mcp/client.tsconsumes the resolved configuration to provide authenticated, timeout-aware API access - Environment-specific overlays enable seamless switching between local, staging, and production MCP endpoints
Frequently Asked Questions
What is the difference between JSON and JSONC configuration files?
JSONC is a superset of JSON that allows C-style (//) and block (/* */) comments. OpenWork uses the .jsonc extension to indicate that parsers should strip comments before JSON validation. This lets teams document configuration decisions directly in the file without breaking parsers.
How does OpenWork handle MCP authentication tokens securely?
The opencode.jsonc file never contains actual tokens. Instead, it references an environment variable name via auth.envVar. When the configuration loads, src/utils/opencode.ts reads process.env[envVar] and injects the value into the configuration object in memory only. The resolved token is never written to logs, caches, or serialized output.
Can I use multiple MCP servers with different configurations?
The current OpenWork schema supports a single MCP server under servers.mcp. For multi-server scenarios, you would define additional server blocks under servers with different keys (e.g., servers.mcpStaging), then modify src/mcp/client.ts to accept a server key parameter. The configuration structure supports this extension without breaking changes.
What happens if OPENWORK_MCP_TOKEN is not set at runtime?
The loadOpencode() function in src/utils/opencode.ts will throw a configuration validation error during startup, preventing the application from initializing with an incomplete MCP setup. This fail-fast behavior ensures that authentication failures are caught early rather than surfacing as mysterious 401 errors during operation.
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 →