MCP Bridge

Focus Deep-Dive

Connecting external MCP tool servers to the agent runtime

The MCP (Model Context Protocol) bridge integrates external tool servers into the agent runtime. MCP tools are discovered, schema-validated, and dispatched through the same pipeline as built-in tools — the agent doesn’t know or care whether a tool is native or remote. The bridge handles connection lifecycle, skill snapshots, environment matching, and cleanup.

📚 What is this & why does it matter?

The MCP bridge is how OpenClaw becomes extensible without modifying its core. Any developer can run an MCP server that exposes custom tools — database queries, deployment scripts, proprietary APIs — and the agent can invoke them as naturally as built-in file operations. Without the bridge, every new tool capability would require a code change and a release.

💡 Real-world analogy

The MCP bridge is like a USB hub for your computer. Your laptop (agent runtime) has built-in speakers and a keyboard (built-in tools), but through the USB hub (MCP bridge) you can connect any peripheral — external drives, MIDI controllers, lab equipment — and they all appear as standard devices to the operating system. The hub handles device discovery, driver loading, and hot-plug removal.

Contents

Overview

Primary role

Bridge external MCP tool servers into the agent’s unified tool runtime alongside built-in tools.

Main inputs

MCP server URLs/commands, skill definitions, agent filter configurations, runtime extension manifests.

Key decisions

Connection strategy (stdio vs. HTTP), skill matching by environment, tool allowlist/denylist filtering, cleanup timing.

Outputs / side effects

Registered tools in the runtime, skill snapshots, spawned MCP connections, extension lifecycle events.

flowchart LR
  subgraph External["External MCP Servers"]
    MCP1["MCP Server A"]
    MCP2["MCP Server B"]
  end
  subgraph Bridge["MCP Bridge"]
    Disc["Tool Discovery"]
    Schema["Schema Registration"]
    Filter["Allow/Deny Filter"]
  end
  subgraph Runtime["Agent Tool Runtime"]
    Builtin["Built-in Tools"]
    MCPTools["MCP Tools"]
    Skills["Skill Tools"]
    Dispatch["Tool Dispatcher"]
  end
  MCP1 --> Disc
  MCP2 --> Disc
  Disc --> Schema
  Schema --> Filter
  Filter --> MCPTools
  Builtin --> Dispatch
  MCPTools --> Dispatch
  Skills --> Dispatch

MCP tools are first-class citizens in the runtime. After discovery and schema registration, they are indistinguishable from built-in tools to the agent. The only difference is dispatch latency — MCP calls involve an IPC or network round-trip.

Decision Points

DecisionWhat controls itWhy it matters
MCP connection transportServer config: stdio (subprocess) vs. http (network)Stdio is faster and more secure for local servers; HTTP enables remote tool servers
Skill environment matchingskills-runtime.ts environment detection (brew/npm/go/uv)Skills are matched to the runtime environment so agent invokes the correct package manager
Tool allow/deny filteringAgent-level filter in AgentCommandOptsRestricts which discovered tools the agent may actually invoke; defense-in-depth
MCP cleanup timingcleanupBundleMcpOnRunEnd flagControls whether MCP server connections are torn down after each agent run or kept alive for reuse
Skill snapshot versioningSkillSnapshot.version fieldEnsures cached snapshots are invalidated when skill definitions change
Spawned agent MCP isolationsessions-spawn-tool.ts spawn configChild agents can inherit parent MCP connections or get their own isolated set

How It Works

Tool Runtime Initialization

The tool runtime (tool-runtime.helpers.ts) initializes three tool categories in a specific order: LSP tools (language server protocol), MCP tools (model context protocol), and codex tools (built-in). MCP initialization happens second because it may depend on LSP-provided workspace context:

// tool-runtime.helpers.ts — initialization sequence
export async function initializeToolRuntime(
  opts: AgentCommandOpts
): Promise<ToolRuntime> {
  const runtime: ToolRuntime = { tools: new Map(), cleanup: [] };

  // Phase 1: LSP tools (workspace-aware)
  await registerLspTools(runtime, opts.workspace);

  // Phase 2: MCP tools (external servers)
  const mcpConnections = await discoverAndConnectMcp(opts.mcpServers);
  for (const conn of mcpConnections) {
    const tools = await conn.listTools();
    for (const tool of tools) {
      if (passesFilter(tool.name, opts.toolFilter)) {
        runtime.tools.set(tool.name, wrapMcpTool(tool, conn));
      }
    }
    if (opts.cleanupBundleMcpOnRunEnd) {
      runtime.cleanup.push(() => conn.close());
    }
  }

  // Phase 3: Built-in codex tools
  registerBuiltinTools(runtime, opts);

  // Phase 4: Skill-based tools
  const snapshot = await buildSkillSnapshot(opts.skills);
  registerSkillTools(runtime, snapshot);

  return runtime;
}
Key detail: MCP tools are registered before built-in tools. If an MCP server provides a tool with the same name as a built-in, the built-in takes precedence (registered last wins). This ensures built-in tools cannot be accidentally overridden by external servers.

Skill Snapshot System

Skills are packaged as SkillSnapshot objects that capture everything needed to invoke a skill at a point in time. The snapshot system (skills-runtime.ts) resolves skill definitions, matches them to the runtime environment, and builds immutable snapshots:

// skills-runtime.ts — SkillSnapshot type
interface SkillSnapshot {
  id: string;                          // Unique skill identifier
  version: string;                     // Snapshot version for cache invalidation
  prompt: string;                      // Resolved prompt text
  skills: ResolvedSkill[];             // List of resolved skill definitions
  agentFilter: NormalizedAgentFilter;  // Tool allow/deny list
  environment: SkillEnvironment;       // Detected runtime (brew/npm/go/uv)
}

interface ResolvedSkill {
  name: string;
  description: string;
  inputSchema: TSchema;                // TypeBox schema for skill parameters
  executor: SkillExecutor;             // Function that runs the skill
  tags: string[];                      // Environment tags for matching
}

interface NormalizedAgentFilter {
  allow?: string[];                    // Allowlist of tool names (glob patterns)
  deny?: string[];                     // Denylist of tool names (glob patterns)
}

// Environment detection
type SkillEnvironment = "brew" | "npm" | "go" | "uv" | "system";

function detectEnvironment(): SkillEnvironment {
  if (process.env.HOMEBREW_PREFIX) return "brew";
  if (existsSync("package.json")) return "npm";
  if (existsSync("go.mod")) return "go";
  if (existsSync("pyproject.toml")) return "uv";
  return "system";
}

Tool Allow/Deny Filtering

The agent-level filter restricts which tools can be invoked, regardless of whether they are built-in, MCP, or skill-based. Filters use glob patterns for flexible matching:

// Filter evaluation
function passesFilter(toolName: string, filter: NormalizedAgentFilter): boolean {
  // If allowlist exists, tool must match at least one pattern
  if (filter.allow && filter.allow.length > 0) {
    const allowed = filter.allow.some(pattern => minimatch(toolName, pattern));
    if (!allowed) return false;
  }
  // If denylist exists, tool must not match any pattern
  if (filter.deny && filter.deny.length > 0) {
    const denied = filter.deny.some(pattern => minimatch(toolName, pattern));
    if (denied) return false;
  }
  return true;
}

// Example filter configurations:
// Allow only read tools: { allow: ["read_*", "list_*"] }
// Block exec tools:      { deny: ["exec", "process_*"] }
// Allow everything:      {} (empty filter)
Security note: Deny filters are evaluated after allow filters. A tool must pass both checks. This means { allow: ["*"], deny: ["exec"] } allows everything except exec.

Runtime Extensions

The extension system (extensions.ts) manages the broader lifecycle of runtime extensions, of which MCP tool servers are one category. Extensions can also provide provider adapters, channel handlers, and lifecycle hooks:

Composability: MCP is one of several extension mechanisms. A single extension package can register both an MCP tool server and a provider adapter, allowing a third-party integration (e.g., a cloud platform SDK) to expose both custom tools and a custom LLM endpoint.

Spawned Agent MCP Connections

When an agent spawns a child agent via sessions-spawn-tool.ts, the child can either inherit the parent’s MCP connections or establish its own. This is controlled by the spawn configuration:

// sessions-spawn-tool.ts — MCP inheritance
interface SpawnConfig {
  inheritMcp: boolean;         // true = share parent's MCP connections
  mcpServers?: McpServerDef[]; // Additional MCP servers for child only
  toolFilter?: NormalizedAgentFilter;  // Child-specific filter
}

async function spawnAgent(config: SpawnConfig, parentRuntime: ToolRuntime) {
  const childOpts: AgentCommandOpts = {
    mcpServers: config.inheritMcp
      ? [...parentRuntime.mcpConnections, ...(config.mcpServers ?? [])]
      : config.mcpServers ?? [],
    toolFilter: config.toolFilter ?? { allow: ["*"] },
    cleanupBundleMcpOnRunEnd: !config.inheritMcp,
  };
  return initializeToolRuntime(childOpts);
}

MCP Connection Lifecycle

sequenceDiagram
  participant Agent as Agent Runtime
  participant Helper as tool-runtime.helpers
  participant MCP as MCP Server
  participant Skills as skills-runtime
  participant Dispatch as Tool Dispatcher

  Agent->>Helper: initializeToolRuntime(opts)
  Helper->>MCP: connect(serverConfig)
  MCP-->>Helper: connection established
  Helper->>MCP: listTools()
  MCP-->>Helper: tool definitions + schemas
  Helper->>Helper: passesFilter(tool, opts.toolFilter)
  Helper->>Agent: wrapMcpTool(tool, conn)
  Agent->>Skills: buildSkillSnapshot(opts.skills)
  Skills-->>Agent: SkillSnapshot
  Agent->>Dispatch: register all tools

  Note over Agent,Dispatch: Runtime ready — tools invokable

  Agent->>Dispatch: tool call from LLM
  Dispatch->>MCP: execute(toolName, args)
  MCP-->>Dispatch: tool result
  Dispatch-->>Agent: tool_result message

  Note over Agent,MCP: On run end (if cleanupBundleMcpOnRunEnd)
  Agent->>MCP: close()

The connection lifecycle is deterministic: connect → discover → filter → register → use → cleanup. Cleanup only runs if cleanupBundleMcpOnRunEnd is set; otherwise connections persist for the session duration, enabling reuse across multiple agent runs.

Annotated Code Walkthrough

Walk through connecting an MCP server, discovering its tools, and making them available to the agent:

Define MCP server configurations. Each entry specifies a transport and optional tool filter:

// MCP server configuration
interface McpServerDef {
  id: string;                         // Unique server identifier
  transport: "stdio" | "http";        // Connection transport
  command?: string;                   // For stdio: subprocess command
  args?: string[];                    // For stdio: command arguments
  url?: string;                       // For http: server URL
  headers?: Record<string, string>;   // For http: custom headers
  toolFilter?: NormalizedAgentFilter; // Server-specific tool filter
}

// Example configurations
const servers: McpServerDef[] = [
  {
    id: "db-tools",
    transport: "stdio",
    command: "npx",
    args: ["-y", "@company/db-mcp-server"],
    toolFilter: { deny: ["drop_*"] },    // Never allow DROP operations
  },
  {
    id: "deploy-tools",
    transport: "http",
    url: "https://internal.company.com/mcp",
    headers: { Authorization: "Bearer ${DEPLOY_TOKEN}" },
  },
];

Connect to each MCP server and discover its available tools. The bridge handles transport negotiation and protocol handshake:

// MCP discovery and connection
async function discoverAndConnectMcp(
  servers: McpServerDef[]
): Promise<McpConnection[]> {
  const connections: McpConnection[] = [];

  for (const server of servers) {
    try {
      const conn = server.transport === "stdio"
        ? await connectStdio(server.command!, server.args ?? [])
        : await connectHttp(server.url!, server.headers);

      // Protocol handshake — negotiate capabilities
      await conn.initialize({ clientInfo: { name: "openclaw", version: "1.0" } });

      connections.push(conn);
    } catch (err) {
      console.warn(`MCP server ${server.id} failed to connect:`, err);
      // Non-fatal: agent runs without this server's tools
    }
  }
  return connections;
}
Resilience: MCP connection failures are non-fatal. The agent starts without the failed server’s tools and logs a warning. This prevents a single misconfigured MCP server from blocking the entire agent.

Wrap each discovered MCP tool as an AnyAgentTool so it integrates seamlessly with the built-in tool pipeline (schema validation, result truncation, context guard):

// Wrapping MCP tools as AnyAgentTool
function wrapMcpTool(tool: McpToolDef, conn: McpConnection): AnyAgentTool {
  return {
    name: tool.name,
    description: tool.description,
    schema: convertJsonSchemaToTypebox(tool.inputSchema),
    metadata: {
      category: inferCategory(tool.name),
      ownerOnly: false,
      sandboxRequired: false,
    },
    async execute(args, ctx) {
      const result = await conn.callTool(tool.name, args);
      return {
        content: result.content,
        isError: result.isError ?? false,
      };
    },
  };
}

Register skill-based tools from the snapshot. Skills are resolved, matched to the environment, and converted to AnyAgentTool:

// skills-runtime.ts — building and registering skill tools
async function buildSkillSnapshot(
  skillDefs: SkillDefinition[]
): Promise<SkillSnapshot> {
  const env = detectEnvironment();
  const resolved = skillDefs
    .filter(s => s.tags.length === 0 || s.tags.includes(env))
    .map(s => resolveSkill(s, env));

  return {
    id: crypto.randomUUID(),
    version: computeHash(resolved),
    prompt: resolved.map(s => s.prompt).join("\n"),
    skills: resolved,
    agentFilter: { allow: ["*"] },
    environment: env,
  };
}

function registerSkillTools(runtime: ToolRuntime, snapshot: SkillSnapshot) {
  for (const skill of snapshot.skills) {
    runtime.tools.set(skill.name, {
      name: skill.name,
      description: skill.description,
      schema: skill.inputSchema,
      metadata: { category: "exec", ownerOnly: false, sandboxRequired: false },
      execute: (args, ctx) => skill.executor(args, ctx),
    });
  }
}

Edge Cases & Error Handling

ScenarioBehaviorCode Location
MCP server fails to connectNon-fatal warning; agent runs without that server’s toolsdiscoverAndConnectMcp()
MCP server disconnects mid-runTool calls to disconnected server return connection error; agent can retry or use alternative toolsMCP connection wrapper
MCP tool name conflicts with built-inBuilt-in tool wins (registered last). MCP tool is shadowed with a warning loginitializeToolRuntime()
Skill environment mismatchSkill is silently skipped during snapshot building; only matching-environment skills are registeredbuildSkillSnapshot()
Tool denied by agent filterTool is not registered in runtime; LLM never sees it in available tools listpassesFilter()
Child agent inherits invalid MCPParent’s closed connections are detected and skipped during child initializationsessions-spawn-tool.ts
MCP server returns oversized resultStandard truncation and context guard apply (same as built-in tools)tool-result-truncation.ts

Configuration

Config KeyDefaultDescription
mcpServers[]Array of MCP server definitions to connect at startup
cleanupBundleMcpOnRunEndfalseTear down MCP connections after each agent run
toolFilter.allow["*"]Glob patterns for allowed tool names
toolFilter.deny[]Glob patterns for denied tool names
skills.environmentOverridenullForce skill environment instead of auto-detecting
mcp.connectTimeoutMs10000Timeout for MCP server connection establishment
mcp.callTimeoutMs30000Timeout for individual MCP tool calls

See also:

Directory Map

Source files for MCP bridge
src/agents/tools/
├── tool-runtime.helpers.ts         ← Tool runtime initialization (LSP, MCP, codex)
├── skills-runtime.ts               ← Skill snapshot building and invocation
├── extensions.ts                   ← Runtime extension management
├── sessions-spawn-tool.ts          ← Agent spawning with MCP connection inheritance
├── mcp/
│   ├── connect-stdio.ts            ← Stdio transport connection
│   ├── connect-http.ts             ← HTTP transport connection
│   ├── protocol.ts                 ← MCP protocol handshake and message types
│   └── wrap-tool.ts                ← MCP tool → AnyAgentTool wrapper
└── filters/
    ├── agent-filter.ts             ← Allow/deny filter evaluation
    └── normalize-filter.ts         ← Filter pattern normalization