Bridge external MCP tool servers into the agent’s unified tool runtime alongside built-in tools.
MCP Bridge
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
MCP server URLs/commands, skill definitions, agent filter configurations, runtime extension manifests.
Connection strategy (stdio vs. HTTP), skill matching by environment, tool allowlist/denylist filtering, cleanup timing.
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
| Decision | What controls it | Why it matters |
|---|---|---|
| MCP connection transport | Server config: stdio (subprocess) vs. http (network) | Stdio is faster and more secure for local servers; HTTP enables remote tool servers |
| Skill environment matching | skills-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 filtering | Agent-level filter in AgentCommandOpts | Restricts which discovered tools the agent may actually invoke; defense-in-depth |
| MCP cleanup timing | cleanupBundleMcpOnRunEnd flag | Controls whether MCP server connections are torn down after each agent run or kept alive for reuse |
| Skill snapshot versioning | SkillSnapshot.version field | Ensures cached snapshots are invalidated when skill definitions change |
| Spawned agent MCP isolation | sessions-spawn-tool.ts spawn config | Child 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;
}
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)
{ 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:
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;
}
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
| Scenario | Behavior | Code Location |
|---|---|---|
| MCP server fails to connect | Non-fatal warning; agent runs without that server’s tools | discoverAndConnectMcp() |
| MCP server disconnects mid-run | Tool calls to disconnected server return connection error; agent can retry or use alternative tools | MCP connection wrapper |
| MCP tool name conflicts with built-in | Built-in tool wins (registered last). MCP tool is shadowed with a warning log | initializeToolRuntime() |
| Skill environment mismatch | Skill is silently skipped during snapshot building; only matching-environment skills are registered | buildSkillSnapshot() |
| Tool denied by agent filter | Tool is not registered in runtime; LLM never sees it in available tools list | passesFilter() |
| Child agent inherits invalid MCP | Parent’s closed connections are detected and skipped during child initialization | sessions-spawn-tool.ts |
| MCP server returns oversized result | Standard truncation and context guard apply (same as built-in tools) | tool-result-truncation.ts |
Configuration
| Config Key | Default | Description |
|---|---|---|
mcpServers | [] | Array of MCP server definitions to connect at startup |
cleanupBundleMcpOnRunEnd | false | Tear down MCP connections after each agent run |
toolFilter.allow | ["*"] | Glob patterns for allowed tool names |
toolFilter.deny | [] | Glob patterns for denied tool names |
skills.environmentOverride | null | Force skill environment instead of auto-detecting |
mcp.connectTimeoutMs | 10000 | Timeout for MCP server connection establishment |
mcp.callTimeoutMs | 30000 | Timeout for individual MCP tool calls |
Related Topics
See also:
- Tool Use overview — parent module covering all tool-related subsystems
- Function Calling — how tool calls are validated, repaired, and dispatched
- Sandbox Execution — Docker/SSH backends for isolated tool execution
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