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 envVar references 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. The auth block describes how to authenticate, not the secret itself.
  • auth.envVar: Names the environment variable containing the actual token. The loader reads process.env.OPENWORK_MCP_TOKEN at 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

  1. Base file read: opencode.jsonc is parsed from the repository root
  2. Overlay detection: If NODE_ENV=development, the loader looks for opencode.dev.jsonc
  3. Deep merge: Overlay values recursively replace base values
  4. Secret injection: All envVar references are resolved from process.env
  5. Validation: The configuration object is checked for required MCP fields
  6. 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.jsonc with url pointing 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.mcp in opencode.jsonc, specifying url, auth, and timeoutMs
  • Secret injection happens at runtime via envVar references, keeping credentials out of source control
  • Configuration loading in src/utils/opencode.ts handles parsing, merging, and validation
  • McpClient in src/mcp/client.ts consumes 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:

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 →