How the OpenWork Gateway Runtime Manages MCP Connections: Architecture and Implementation

The OpenWork gateway runtime treats MCP (Model Context Protocol) connections as configuration-driven, dynamically-registered endpoints that are discovered through a hierarchical merge of project, global, and runtime configs, validated against tool-policy denies, and invoked via a minimal JSON-RPC transport layer.

The different-ai/openwork repository implements a sophisticated gateway server under apps/server that orchestrates MCP lifecycles for cloud-provider integrations. This article examines how the runtime discovers, validates, and communicates with MCP endpoints using the source code from the dev branch.

The Three-Stage Connection Lifecycle

The gateway manages MCP connections through a pipeline that separates configuration management from transport execution.

Stage 1: Loading and Merging Configuration

The runtime constructs a unified view of available MCPs by reading three distinct configuration sources in apps/server/src/mcp.ts. First, it loads the project-level opencode.json from the workspace root. Second, it resolves the global OpenCode config via resolveGlobalOpenCodeConfigPath(), typically located at ~/.config/openwork. Third, it retrieves runtime-registered MCPs from the workspace-specific runtime Opencode config (a JSONC file managed through writeRuntimeOpencodeConfig).

The merging follows a precedence hierarchy: runtime entries override project settings, which override global defaults. This allows dynamically added MCPs to supersede static configurations without modifying files on disk. The listMcp and listMcpFromRuntimeSnapshot functions orchestrate this merge, parsing all files with JSONC support to allow comments.

Stage 2: Diagnosing Tool Policy Denies

Before marking an MCP as available, the runtime evaluates security policies through diagnoseMcpToolDeniesFromConfigs. This function aggregates deny rules from both legacy tools maps and modern permission objects found in project and global configurations.

The helper functions collectToolsDenyArray and collectPermissionRulesetDenies flatten these configurations into a ToolPolicyMap. Each MCP's tools are then checked against glob-style deny patterns using minimatch. Project-level allow rules can override global denies, but only when the pattern matches the exact tool ID. Denied MCPs surface in the toolDenies array, allowing the UI to disable or hide affected endpoints while keeping the configuration intact.

Stage 3: Runtime JSON-RPC Interaction

When a client requests a resource, the gateway initiates transport in apps/server/src/connect-mcp-transport.ts. The readMcpResourceText function executes a strict handshake sequence:

  1. Initialize: Sends the initialize request and records the mcp-session-id header
  2. Notify: Transmits notifications/initialized as a silent confirmation
  3. Read: Issues resources/read for the target URI

The readMcpPayload parser handles both standard JSON responses and text/event-stream payloads for streaming tool output. All transport calls respect the enabled flag and abort immediately if tool-policy denies are present, ensuring disabled MCPs never receive network traffic.

Configuration Hierarchy and MCP Discovery

The discovery process begins with reading JSONC files using readJsoncFile with allowInvalid: true for resilience:

const { data: config } = await readJsoncFile(opencodeConfigPath(workspaceRoot), {}, { allowInvalid: true });
const { data: globalConfig } = await readJsoncFile(resolveGlobalOpenCodeConfigPath(), {}, { allowInvalid: true });

The getMcpConfig utility extracts the top-level mcp object from each config, while runtimeMcpMap parses the dynamic runtime store. The final inventory construction iterates through global, project, and runtime maps in order, attaching any discovered denies:

const toolDenies = diagnoseMcpToolDeniesFromConfigs({ projectConfig: config, globalConfig, name });
items.push({
  name,
  config: entry,
  source,
  disabledByTools: toolDenies.length > 0 || undefined,
  ...(toolDenies.length ? { toolDenies } : {}),
});

Tool Policy Enforcement and Deny Logic

The permission system supports both legacy boolean tool maps and granular policy objects. The runtime creates a flattened view of all deny rules, then evaluates each tool identifier against glob patterns. This allows administrators to block broad categories (e.g., filesystem/*) while permitting specific exceptions at the project level.

When diagnoseMcpToolDeniesFromConfigs identifies conflicts, it populates the toolDenies metadata with the specific matched patterns. This diagnostic information propagates to the MCP Management UI in the desktop application, providing transparency about why certain cloud-provider tools are unavailable.

Runtime Transport and Resource Reading

The transport layer implements the MCP specification through a minimal JSON-RPC client. The mcpPost function manages the underlying HTTP communication, while readMcpResourceText handles the stateful conversation flow.

To read a resource from an active MCP:

import { readMcpResourceText } from "./connect-mcp-transport.ts";

const text = await readMcpResourceText({
  config: { url: "https://api.mycloud.example/v1", enabled: true },
  uri: "workspace:/myfile.txt",
  fetcher: fetch,
  clientName: "openwork-desktop",
});

if (text) {
  console.log("File contents:", text);
}

If the MCP is disabled or denied, the function returns null immediately without network overhead.

Managing MCPs Programmatically

The server exposes lifecycle APIs in apps/server/src/mcp.ts for dynamic management. To add a new cloud provider at runtime:

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

await addMcp(serverConfig, workspaceId, "my-cloud-mcp", {
  url: "https://api.mycloud.example/v1",
  headers: { Authorization: "Bearer …" },
  enabled: true,
});

The addMcp function validates the name via validateUserMcpName and the schema via validateMcpConfig before persisting to the runtime config. Disabling an MCP prevents further transport calls:

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

await setMcpEnabled(serverConfig, workspaceId, "my-cloud-mcp", false);

Listing all MCPs with their diagnostic status:

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

const mcps = await listMcp(serverConfig, workspaceId, "/path/to/workspace");
console.log(mcps.map(m => ({
  name: m.name,
  source: m.source,
  disabled: !!m.disabledByTools,
  denies: m.toolDenies?.map(d => d.matched) ?? [],
})));

Key Source Files and Architecture

The implementation spans several critical files in the different-ai/openwork repository:

Summary

  • The OpenWork gateway runtime manages MCP connections through a three-stage pipeline: configuration merging, tool-policy validation, and JSON-RPC transport execution.
  • Configurations merge in precedence order (runtime → project → global), allowing dynamic overrides without static file modifications.
  • The diagnoseMcpToolDeniesFromConfigs function enforces security policies using glob-pattern matching against legacy and modern permission schemas.
  • Transport operations in connect-mcp-transport.ts follow the MCP specification with initialize-notify-read sequencing and support for both JSON and SSE response parsing.
  • Lifecycle APIs (addMcp, setMcpEnabled) persist changes to a workspace-specific runtime config distinct from version-controlled opencode.json files.

Frequently Asked Questions

How does the OpenWork gateway handle conflicting MCP configurations between project and global scopes?

Runtime-registered MCPs take highest precedence, followed by project-level configurations, then global settings. When listMcp merges these sources in apps/server/src/mcp.ts, later entries supersede earlier ones in the iteration sequence. This hierarchy ensures that dynamically added cloud providers via the UI or API override static definitions, while project-specific settings can customize global defaults without affecting other workspaces.

What happens when an MCP tool matches a deny pattern in the configuration?

The runtime evaluates deny rules during the discovery phase through diagnoseMcpToolDeniesFromConfigs. If a tool ID matches a glob pattern in tools.deny or the permission ruleset, the function populates the toolDenies array for that MCP. The transport layer checks this metadata before executing readMcpResourceText, immediately returning null if denies are present. The UI uses this diagnostic information to display disabled states and provide transparency about policy restrictions.

Can the OpenWork gateway runtime manage MCPs added after the server has started?

Yes, the architecture fully supports dynamic registration through the runtime Opencode config. The addMcp function validates new configurations via validateMcpConfig and persists them via writeRuntimeOpencodeConfig to a JSONC file separate from the static project config. These runtime entries are loaded through listMcpFromRuntimeSnapshot and immediately become available for transport connections without requiring a server restart, though they are scoped to the specific workspace.

What transport protocol does the OpenWork gateway use for MCP communication?

The gateway implements a minimal JSON-RPC transport over HTTP as defined in apps/server/src/connect-mcp-transport.ts. The readMcpResourceText function executes a strict initialization handshake (initialize request, initialized notification, then resource/read), maintaining session state through the mcp-session-id header. The parser handling in readMcpPayload supports both standard JSON responses and text/event-stream formats for streaming tool outputs, ensuring compatibility with diverse cloud-provider implementations.

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 →