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:
- Global entries not overridden by project entries
- Project entries not overridden by runtime entries
- 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 viawriteRuntimeOpencodeConfigalways take precedence over local MCPs (type: "local") from static files. - The
validateMcpConfigfunction enforces type safety, accepting only"remote"or"local"values indev/apps/server/src/mcp.ts. - Server-side operations like
addMcpautomatically store entries as remote MCPs in the runtime snapshot, marking them withsource: "config.remote". - The UI in
mcp-view.tsxdistinguishes 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →