How OpenWork MCP Server Configuration Handles Remote and Local Types

OpenWork MCP server configuration manages MCP definitions through a three-layer hierarchy where remote MCPs (marked with "type": "remote") stored in runtime configurations always override local MCPs (marked with "type": "local") defined in static configuration files.

The different-ai/openwork repository implements a sophisticated MCP (Managed Connector Provider) server configuration system that distinguishes between remote and local MCP types. This architecture allows developers to manage connectors through both static configuration files and dynamic runtime storage, with clear precedence rules governing how conflicts are resolved in dev/apps/server/src/mcp.ts.

The Three-Layer Configuration Hierarchy

The configuration system organizes MCP definitions across three distinct layers, each with specific storage locations and source identifiers.

Global Configuration Layer

Stored in ~/.config/opencode/config.jsonc (or the path returned by resolveGlobalOpenCodeConfigPath()), the global layer contains MCP definitions available across all workspaces. Entries from this layer receive the source label "config.global" and typically use "type": "local".

Project Configuration Layer

Located at <workspace>/.opencode/config.jsonc, the project layer provides workspace-specific MCP definitions. These entries carry the source label "config.project" and also default to "type": "local".

Remote Runtime Layer

Remote MCPs reside in the workspace's runtime Opencode config, stored as JSON under the server's data directory and accessed via readRuntimeOpencodeConfig(). These entries use the source label "config.remote" and explicitly set "type": "remote".

Configuration Merging and Precedence Rules

The core merging logic in dev/apps/server/src/mcp.ts implements strict precedence when combining layers. The listMcpFromRuntimeSnapshot function processes entries in the following order:

  1. Global entries not overridden by project entries
  2. Project entries not overridden by runtime entries
  3. Runtime (remote) entries—always taking highest precedence
// Global entries that are not overridden by a project entry
for (const [name, entry] of Object.entries(globalMcpMap)) {
  if (Object.prototype.hasOwnProperty.call(projectMcpMap, name)) continue;
  items.push({ name, config: entry, source: "config.global", … });
}

// Project entries that are not overridden by a runtime entry
for (const [name, entry] of Object.entries(projectMcpMap)) {
  if (Object.prototype.hasOwnProperty.call(runtimeMap, name)) continue;
  items.push({ name, config: entry, source: "config.project", … });
}

// Runtime (remote) entries – always win
for (const [name, entry] of Object.entries(runtimeMap)) {
  items.push({ name, config: entry, source: "config.remote", … });
}

This ensures remote MCPs always win over local definitions, while local MCPs serve as fallbacks when no remote entry exists.

Type Validation and Runtime Storage

Both addMcp and setMcpEnabled functions call validateMcpConfig to ensure every MCP entry contains a valid type field—either "remote" or "local".

When adding a new MCP through the server API, the system stores the entry in the runtime snapshot:

export async function addMcp(serverConfig, workspaceId, name, config) {
  validateMcpName(name);
  validateMcpConfig(config);
  const runtimeConfig = await readRuntimeOpencodeConfig(serverConfig, workspaceId);
  const mcpMap = { …runtimeMcpMap(runtimeConfig) };
  mcpMap[name] = config;                  // ← stored in runtime → “remote”
  await writeRuntimeOpencodeConfig(serverConfig, workspaceId, cur => ({ …cur, mcp: mcpMap }));
}

This automatic storage in the runtime configuration marks the MCP as remote, regardless of the original type specified.

UI Representation and User Interface Handling

The settings page in dev/apps/app/src/react-app/domains/settings/pages/mcp-view.tsx renders the MCP type to users:

<span className="text-dls-text">
  {props.entry.config.type === "remote" ? t("mcp.type_cloud") : t("mcp.type_local")}
</span>

Remote MCPs display as Cloud while local MCPs display as Local. The UI also conditionally exposes connection URLs only for remote entries:

url={detailConnectMcp.config.type === "remote" ? detailConnectMcp.config.url : undefined}
oauth={detailConnectMcp.config.type === "remote"}

Practical Implementation Examples

Adding a Remote MCP

To programmatically add a remote MCP through the server API:

await addMcp(serverConfig, "ws_123", "my-remote-mcp", {
  type: "remote",
  url: "https://example.com/mcp",
  enabled: true,
  oauth: {}                     // optional OAuth config
});
// → stored as source "config.remote"

Defining a Local MCP in Static Config

Create or edit <workspace>/.opencode/config.jsonc:

{
  "mcp": {
    "my-local-mcp": {
      "type": "local",
      "command": ["node", "my-mcp.js"]
    }
  }
}

This requires no server-side calls and appears as "config.project".

Listing All MCPs with Merged View

import { listMcp } from "./mcp.ts";

const items = await listMcp(serverConfig, "ws_123", workspaceRoot);
items.forEach(item => {
  console.log(`${item.name} – ${item.source} – ${item.config.type}`);
});
/* Example output:
   my-remote-mcp – config.remote – remote
   my-local-mcp  – config.project – local
*/

Disabling an MCP

await setMcpEnabled(serverConfig, "ws_123", "my-remote-mcp", false);

Summary

  • OpenWork MCP server configuration uses a three-layer hierarchy: global (~/.config/opencode/config.jsonc), project (<workspace>/.opencode/config.jsonc), and remote runtime configs.
  • Remote MCPs (type: "remote") stored in runtime configs via writeRuntimeOpencodeConfig always take precedence over local MCPs (type: "local") from static files.
  • The validateMcpConfig function enforces type safety, accepting only "remote" or "local" values in dev/apps/server/src/mcp.ts.
  • Server-side operations like addMcp automatically store entries as remote MCPs in the runtime snapshot, marking them with source: "config.remote".
  • The UI in mcp-view.tsx distinguishes types visually, showing "Cloud" for remote and "Local" for static configurations, while only exposing URL fields for remote types.

Frequently Asked Questions

What determines whether an MCP is classified as remote or local in OpenWork?

The classification depends on storage location rather than connection method. MCPs stored in the runtime Opencode config (managed through addMcp or server APIs) become remote with type: "remote", while those defined in ~/.config/opencode/config.jsonc or <workspace>/.opencode/config.jsonc are local with type: "local".

Can a local MCP override a remote MCP with the same name?

No. According to the merging logic in dev/apps/server/src/mcp.ts, runtime (remote) entries always win. The listMcpFromRuntimeSnapshot function checks for existing runtime entries before including project or global definitions, ensuring remote MCPs maintain highest precedence regardless of where local definitions exist.

How does the system validate MCP type definitions?

The validateMcpConfig function validates every MCP configuration before storage, ensuring the type field contains either "remote" or "local". This validation occurs in both addMcp and setMcpEnabled functions to prevent invalid type assignments and maintain configuration integrity.

Where are remote MCP configurations physically stored?

Remote MCPs reside in the workspace's runtime Opencode config, accessed through readRuntimeOpencodeConfig() and written via writeRuntimeOpencodeConfig() in dev/apps/server/src/runtime-opencode-config-store.ts. This JSON file lives under the server's data directory and maintains the source: "config.remote" identifier throughout the system.

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 →