MCP 运行时
OpenClaw 把 Model Context Protocol 客户端能力收敛在 src/agents/agent-bundle-mcp-runtime.ts 一处。它构建于官方 @modelcontextprotocol/sdk 之上,把外部 MCP server 暴露的工具接入到 agent 的工具空间——但接入的粒度不是全局,而是每会话一组。
MCP server 在 OpenClaw 中是会话作用域(session-scoped)的:每个会话拥有自己独立的一组 MCP 客户端连接。两个并行会话即使读取同一份服务器配置,也会各自建立 Client + Transport,互不串扰。这样工具调用的状态、子进程、长连接都被严格隔离在会话边界内,会话销毁时连接随之拆除。
但连接的生命周期并不与会话一对一绑死。运行时之上有一个进程级的单例 manager(由 SESSION_MCP_RUNTIME_MANAGER_KEY 锚定),它负责为各会话登记、复用与回收连接。当一个会话的 MCP 配置指纹未变化时,已有连接被直接复用,避免重复握手与子进程冷启动;只有配置真正变化或会话空闲超时,才会触发重建或回收。
会话级隔离
每个会话获得独立的 MCP 客户端连接集合,工具状态、子进程与长连接不跨会话共享。
进程单例 manager
由全局 Symbol 注册表锚定的唯一 manager 统管所有会话的连接登记、复用与回收。
配置指纹复用
会话级 MCP 配置被哈希成指纹,指纹不变即复用既有连接,跳过重复握手与冷启动。
BundleMcpSession 会话结构
运行时把一条 MCP 连接的全部状态压缩进一个 BundleMcpSession 记录——服务器名、SDK 客户端、底层传输、传输类型、请求超时、是否支持并行调用,以及一个可选的 stderr 分离回调。
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
type BundleMcpSession = {
serverName: string;
client: Client;
transport: Transport;
transportType: "stdio" | "sse" | "streamable-http";
requestTimeoutMs: number;
supportsParallelToolCalls: boolean;
detachStderr?: () => void;
};
const SESSION_MCP_RUNTIME_MANAGER_KEY = Symbol.for("openclaw.sessionMcpRuntimeManager");
- serverName — 该连接对应的 MCP server 在配置中的逻辑名,用作工具命名空间前缀与日志标识。
- client — SDK 的
Client实例,承载listTools()/callTool()等协议级请求。 - transport — 实现
Transport接口的底层载体,client 通过它收发 JSON-RPC 消息。 - transportType — 三选一的字面量联合,标明该连接走的是 stdio、sse 还是 streamable-http。
- requestTimeoutMs — 单次协议请求的超时上限,源自服务器配置,避免请求无限挂起。
- supportsParallelToolCalls — 是否允许对该 server 并发派发多个工具调用,作为并行执行的开关。
- detachStderr — 可选回调,仅 stdio 传输有意义,用于在拆除连接时解除对子进程 stderr 的监听。
进程单例锚点
用全局 Symbol 注册表的 Symbol.for("openclaw.sessionMcpRuntimeManager") 作为键,确保即便模块被多次加载,整个进程也只有一个 manager 实例被反复取用。
子进程 stderr 分离
当传输是 stdio 时,MCP server 是一个本地子进程;detachStderr 在连接销毁阶段被调用,解除 stderr 转发,防止句柄与监听器泄漏。其它传输类型该字段为空。
三种传输类型
同一套 Client 协议逻辑可以跑在三种不同的物理载体上。transportType 字段记录的就是当前连接落在哪一种之上——从本地子进程管道,到远端长连接,再到可恢复的 HTTP 会话。
| transportType | 载体 | 典型场景 |
|---|---|---|
stdio | 子进程 stdin/stdout 管道 | 本地 MCP server 可执行文件 |
| sse | Server-Sent Events 长连接 | 远端只读流式 server |
| streamable-http | StreamableHTTPClientTransport | HTTP 双向 / 可恢复会话 |
配置入口在 embedded-agent-mcp.ts 的 loadEmbeddedAgentMcpConfig():它读取并物化嵌入式 agent 的 MCP 服务器声明。读出的配置记录交由 mcp-config-shared.ts 的 isMcpConfigRecord() 做结构校验,确保每条 server 记录字段合法。真正把一条配置记录解析为可用的 Transport 实例,则发生在 mcp-transport.ts 中——它根据记录里声明的传输形态分派构造对应的 transport。
stdio 子进程,带 URL 的记录则按声明落到 sse 或 streamable-http。运行时只持有解析后的 transport 与 transportType,对上层调用完全透明。工具调用链与超时
接入一个 MCP server 后,运行时先拉取它的工具目录,再按需调用。整条链路上每一步都有显式的超时与失败冷却保护,避免一个失灵的 server 拖垮整个会话。
工具调用链路始于 Client.listTools()——拉取 server 暴露的工具列表,把每个工具的名称与 JSON Schema 注册进 agent 的工具空间。实际执行时调用 Client.callTool(),传入工具名与参数,结果以 SDK 的 CallToolResult 类型返回(含内容块与可选的 isError 标记)。两步都受 requestTimeoutMs 约束。
| 常量 | 值 | 含义 |
|---|---|---|
BUNDLE_MCP_CATALOG_LIST_TIMEOUT_MS | 1500ms | listTools() 目录拉取超时 |
BUNDLE_MCP_FAILURE_COOLDOWN_MS | 60000ms | 失败后指数退避冷却窗 |
| idle TTL | 10 分钟 | resolveSessionMcpRuntimeIdleTtlMs() 空闲回收 |
export function createBundleMcpJsonSchemaValidator(): jsonSchemaValidator;
export function resolveSessionMcpConfigSummary(params: {...}): {...};
export function resolveSessionMcpConfigFingerprint(params: {...}): string;
function resolveSessionMcpRuntimeIdleTtlMs(cfg?: OpenClawConfig): number; // 默认 10 分钟
supportsParallelToolCalls 决定能否对同一个 server 并发派发多个 callTool()。为 false 时调用被串行化,确保不支持并发的 server 不会因同时收到多个请求而错乱。配置指纹与连接复用
manager 用一枚配置指纹决定一条会话的 MCP 连接该复用还是重建。指纹不变就沿用既有连接,配置一变就重建,长时间无人问津则按 TTL 回收——把握手与子进程冷启动成本压到最低。
resolveSessionMcpConfigFingerprint() 把会话级 MCP 配置(服务器集合、传输声明、超时等)哈希成一个稳定的指纹字符串。每次会话需要 MCP 运行时,manager 先比对指纹:若与登记中的连接指纹一致,则直接复用现有 Client 与 Transport,跳过重新握手与 listTools() 目录拉取;只有指纹变化才会拆除旧连接并重建会话。
空闲的会话连接由 resolveSessionMcpRuntimeIdleTtlMs() 给出的 TTL(默认 10 分钟)管控——超过该窗口仍无调用的连接被回收,stdio 子进程随之关闭、detachStderr 被调用。工具参数的合法性则由 createBundleMcpJsonSchemaValidator() 产出的校验器按 server 暴露的 JSON Schema 把关。
配置指纹
resolveSessionMcpConfigFingerprint() 把会话配置哈希成稳定字符串;指纹一致即复用连接,变化才触发重建。
空闲回收
超过 resolveSessionMcpRuntimeIdleTtlMs() 给出的 10 分钟空闲窗口的连接被回收,释放子进程与长连接资源。