6. MCP
6. MCP
6.1 原理:连接生命周期与目录快照生命周期
MCP 把外部能力统一为 Server 暴露的 Tools、Resources、Prompts。Harness 作为 Client,负责配置合并、连接、能力发现、命名空间、权限、调用、结果归一化和失效刷新。
理解 MCP 加载时要同时画出两条生命周期:
- 连接生命周期:配置 → transport → initialize → ready/needs-auth/failed → reconnect/close;
- 目录快照生命周期:list tools/resources/prompts → 规范化与策略过滤 → 模型暴露 → list_changed/配置变化 → 失效并发布新快照。
两者不能混成一个“已连接”布尔值。连接仍然存活,不代表工具目录没有变化;工具 Schema 已缓存,也不代表原连接、认证或 Session ID 仍然有效。最强的一致性边界是“一次模型采样看到的工具目录,与执行该 ToolCall 的 client、配置、权限和 catalog revision 属于同一个 binding”。
完整链路:
配置加载 // 读取 MCP Server 地址、传输方式、认证和启停配置
→ 建立 stdio / SSE / Streamable HTTP / in-process transport // 创建与 Server 匹配的双向通信通道
→ initialize 能力协商 // 交换协议版本及双方支持的能力集合
→ list_tools/list_resources/list_prompts // 发现 Server 当前暴露的工具、资源和提示模板
→ 转为模型 Tool Schema // 将 MCP Tool 定义转换成模型可理解的函数 Schema
→ 模型生成 tool call // 模型基于工具描述产生全限定名和结构化参数
→ 权限检查与审批 // 在访问外部 Server 前校验身份、范围和风险
→ call_tool // 通过已初始化连接发送实际工具调用请求
→ 进度/elicitation/错误/超时处理 // 消费进度、补充信息请求及各种协议终态
→ 结果大小控制与上下文注入 // 对结果校验、裁剪,或保存到外部文件并只保留引用
关键问题:
- 命名冲突:使用
mcp__server__tool全限定名,权限规则也必须匹配全限定名,避免外部工具伪装成内建Write。 - 能力与连接分离:工具清单可缓存,但连接断开、会话过期或授权过期要重新初始化。
- 大结果限制:MCP 输出可能远超上下文,应分页、过滤、截断或存入外部文件,只给模型摘要和受控引用。
- 不可信元数据:Tool 描述、schema、resource URI 和
_meta都来自外部 Server,需要长度上限、协议校验和安全渲染。 - 认证:远程 Server 可能使用 OAuth;Token 应放安全存储,支持过期刷新、撤销与 step-up authorization。
6.2 MCP 加载、刷新与调用绑定图
图表加载中…
图中的“预热”只是把 D—L 提前执行来降低延迟;真正的正确性仍由 M—P 保证。否则后台刷新恰好与 ToolCall 并发时,模型可能拿旧 Schema 调新 Server。
6.3 Claude Code:两阶段配置、AppState 目录与定向失效
6.3.1 配置加载与连接状态
Claude Code 的 MCP 管理器先把新 Server 放入 pending,配置被删除或连接参数变化时识别 stale client,先取消旧重连定时器,再清理旧连接,避免旧闭包用过期配置重新连回并覆盖新状态:useManageMCPConnections.ts:765、useManageMCPConnections.ts:782、useManageMCPConnections.ts:791。
真正连接采用两阶段加载:先读取本地、项目、插件和动态配置并立即连接;远端账户配置的网络请求同时启动,返回后经过策略过滤与内容去重,再作为第二批连接。这样慢远端配置不会阻塞本地 stdio Server:useManageMCPConnections.ts:856、useManageMCPConnections.ts:861、useManageMCPConnections.ts:875、useManageMCPConnections.ts:889、useManageMCPConnections.ts:904。
Server 状态至少包括 pending、connected、needs-auth、failed、disabled。disabled Server 在连接前就被分流,近期 401 或只有 OAuth discovery 没有 Token 的远端 Server直接进入 needs-auth,并只暴露认证工具,避免周期性无效探测:client.ts:2228、client.ts:2243、client.ts:2303、client.ts:2317。
本地 stdio/sdk 与远端网络 Server 使用两个并发队列。本地队列并发更低以限制进程创建资源竞争,远端队列并发更高;pMap 按槽位连续调度,某个慢 Server 不会卡住下一整批:client.ts:2214、client.ts:2220、client.ts:2266、client.ts:2390。
6.3.2 initialize 后如何形成模型工具目录
client.connect(transport) 完成 MCP initialize,随后读取 Server capabilities、version 和 instructions;instructions 超长会被截断:client.ts:1050、client.ts:1159、client.ts:1161、client.ts:1163。只有连接成功后,才按 capabilities 并行加载 Tools、Prompts、MCP Skills 和 Resources:client.ts:2326、client.ts:2344、client.ts:2346。
Tools 通过 tools/list 获取并先清理异常 Unicode,再转换为内部 Tool。默认名称使用 mcp__server__tool,同时保存原始 server/tool 映射;外部 _meta.searchHint 会压平换行,description 有长度上限,readOnly/destructive/openWorld annotations 被转换为权限和并发判断输入:client.ts:1745、client.ts:1754、client.ts:1759、client.ts:1768、client.ts:1770、client.ts:1778、client.ts:1791、client.ts:1797。
MCP Tool 的内部 checkPermissions 返回 passthrough,让统一权限层按全限定名和参数决定,而不是信任 Server 自报的安全属性:client.ts:1816。Prompts 通过 prompts/list 转成动态 Command,实际调用时再 getPrompt;Resources 通过 resources/list 附加 server 名称:client.ts:2002、client.ts:2035、client.ts:2044、client.ts:2075。
连接结果不会逐个立刻重渲染 AppState。管理器把同一时间窗口内到达的 Server 更新放入队列,每 16ms 批量合并;更新某个 Server 时只替换它的 tools、commands 和 resources,disabled/failed 会明确清空旧能力,防止幽灵工具继续出现在模型目录:useManageMCPConnections.ts:203、useManageMCPConnections.ts:216、useManageMCPConnections.ts:245、useManageMCPConnections.ts:255、useManageMCPConnections.ts:293。
6.3.3 缓存失效必须和变化类型对应
Tools、Prompts、Resources 与 MCP Skills 使用按 Server 名称键控的有界 LRU 缓存:client.ts:1726、client.ts:1745、client.ts:2002、client.ts:2035。连接关闭时不仅删除连接 memo,还同时删除四类 fetch 缓存,否则新连接会继承旧目录:client.ts:1375、client.ts:1385、client.ts:1388、client.ts:1398。
Server 声明 listChanged capability 后,管理器注册三类定向刷新:
tools/list_changed:只删除 tools cache,重新 list tools 并替换该 Server 的工具:useManageMCPConnections.ts:616、useManageMCPConnections.ts:627、useManageMCPConnections.ts:631;prompts/list_changed:删除 prompts cache,同时复用 MCP Skills cache,再合并新 commands:useManageMCPConnections.ts:667、useManageMCPConnections.ts:679、useManageMCPConnections.ts:681;resources/list_changed:删除 resources cache;由于 MCP Skills 来自 Resources,还要删除 MCP Skills 与 prompts cache,并发重建三者,避免并发通知最后写入陈旧 commands:useManageMCPConnections.ts:705、useManageMCPConnections.ts:717、useManageMCPConnections.ts:719、useManageMCPConnections.ts:725。
这说明缓存失效图必须跟依赖图一致:MCP Skill 依赖 Resources,所以 Resource 变化会向上影响 Skill Search Index;Prompt 变化却不应无故重读 Resource Skill。
6.3.4 调用时重连与失败恢复
内部 Tool 闭包保存发现时的 tool metadata,但每次调用前会 ensureConnectedClient,连接缓存失效后取得新 client,再执行 URL Elicitation 与 callTool:client.ts:1835、client.ts:1860、client.ts:1864、client.ts:1865。HTTP MCP Session 过期时只自动恢复重试一次,避免无限重放可能有副作用的 ToolCall:client.ts:1861、client.ts:1913、client.ts:1916。
远端 transport 关闭使用指数退避重连;stdio/sdk 不做同样的自动重连。达到最大次数后进入 failed 并清空目录:useManageMCPConnections.ts:354、useManageMCPConnections.ts:370、useManageMCPConnections.ts:417、useManageMCPConnections.ts:446。这种实现通过 AppState 更新保持 UI 与目录一致,但没有像 Codex PreparedMcpCall 那样把 ToolCall 显式绑定到 catalog revision;设计新系统时应补上版本检查,避免重连后 Schema 已变化而旧闭包继续调用。
6.4 Codex:不可变 Runtime、Step Binding 与版本拒绝
6.4.1 Desired State、dirty 和唯一发布者
Codex 先从 Session 配置、认证、Turn 环境、sandbox 和已选择 capability roots 计算 MCP desired state,再生成当前 Step 的 Runtime projection:mcp.rs:93、mcp.rs:104、mcp.rs:118。配置、认证或 capability root 变化只负责标记 dirty;refresh_mcp_if_dirty 通过单 permit semaphore 保证同一时间只有一个发布者,claim 后重新读取最新期望状态:mcp.rs:150、mcp.rs:170、mcp.rs:177、mcp.rs:195。
刷新任务如果在发布前被取消,McpRefreshInvalidationGuard 会把 dirty 恢复,确保下一次 Step 不会误以为刷新已经完成:mcp_refresh.rs:7、mcp_refresh.rs:21、mcp_refresh.rs:42、mcp_refresh.rs:48。后台预热使用有界 channel 合并重复请求,只提前准备最新状态;源码明确指出 exact model step 才是正确性路径:mcp_prewarm.rs:1、mcp_prewarm.rs:9、mcp_prewarm.rs:45、mcp_prewarm.rs:58。
6.4.2 ArcSwap 发布与旧请求继续持有旧快照
McpRuntime 用 ArcSwap<PublishedMcpRuntime> 保存当前不可变快照,快照包含连接集、配置、认证、插件状态和 capability roots:runtime.rs:70、runtime.rs:74、runtime.rs:80。刷新时先构造新的 McpConnectionSet,完整构造后一次 store 原子替换,再打开 publication gate;读取者不会观察到“新配置 + 旧连接”这种半发布状态:runtime.rs:163、runtime.rs:184、runtime.rs:191、runtime.rs:200。
原子替换不会强制杀死正在执行的旧请求。旧的 McpBinding 持有自己的 Arc<McpConnectionSet>、clients、config、tools 和 prepared calls,直到该采样请求结束:binding.rs:1、binding.rs:29、binding.rs:31。这比“全局连接对象就地修改”更容易证明并发正确性。
6.4.3 required Server 与 Step 目录固化
Turn 会从显式 mcp://server mention、Skill 依赖和插件能力收集 required servers。mcp_runtime_for_step 先刷新 dirty Runtime,再捕获需要的 Server 已完成 startup 的 binding;初始化阶段也会把所有 required Server 失败聚合后一次报告:turn.rs:650、turn.rs:692、mcp.rs:278、mcp.rs:295、required.rs:10、required.rs:15。
McpBinding 的 tools() 是当前模型采样看到的冻结目录;prepare_call 返回同一 binding 中已经绑定的 exact client 和 metadata,而不是再去全局 Map 用名称查一次:binding.rs:78、binding.rs:83。Session 侧单独提供 prepare_mcp_call,先刷新,再从当前 binding 准备调用:mcp_runtime.rs:43、mcp_runtime.rs:49。
更关键的是 PreparedMcpCall 保存准备时的 catalog_revision 和共享 revision source。真正执行副作用前再次比较;如果目录已经替换,旧调用被拒绝并要求重新构建 Step,而不是拿旧参数调用新 Schema:binding.rs:159、binding.rs:166、binding.rs:260、binding.rs:271。
6.4.4 initialize、目录缓存与工具暴露预算
每个 Server 完成 initialize 后读取 capability。Server 可通过 experimental capability 明确禁止 Tool Catalog Cache;随后才执行 uncached list tools,并把结果发布到共享 cache,只有最新 fetch ticket 能覆盖缓存:rmcp_client.rs:843、rmcp_client.rs:864、rmcp_client.rs:869、rmcp_client.rs:888、rmcp_client.rs:892、rmcp_client.rs:905。
工具进入模型前还要经过暴露策略。普通 MCP Tool 先过滤 model visibility;Apps Tool 还必须有 connector id,并经过 App policy 对 destructive/open-world 等属性判断:mcp_tool_exposure.rs:21、mcp_tool_exposure.rs:30、mcp_tool_exposure.rs:90、mcp_tool_exposure.rs:98。开启 Tool Search 时工具默认 deferred,否则 direct;Agent Plugin MCP Tool 还受单个 8KB、总计 64KB Schema 预算约束,超限工具 hidden 而不是继续挤占 Prompt:mcp_tool_exposure.rs:14、mcp_tool_exposure.rs:36、mcp_tool_exposure.rs:55、mcp_tool_exposure.rs:71。
6.5 两套 MCP 加载策略对比
| 维度 | Claude Code | Codex | 设计结论 |
|---|---|---|---|
| 配置加载 | 本地配置先连接,远端账户配置第二阶段到达 | 每 Step 从 Session、环境、认证投影 desired state | 慢配置源不应阻塞本地能力,但 Step 前必须固化最终视图 |
| 连接状态 | AppState 保存 pending/connected/needs-auth/failed/disabled | McpRuntime 发布连接集,required Server 可等待并聚合失败 | 连接状态与工具目录状态要分开建模 |
| 并发 | 本地与远端使用不同并发上限 | ConnectionSet 并发启动,Runtime 由单一 gate 发布 | 并发优化不能制造不确定发布顺序 |
| 目录缓存 | 按 Server 名称的有界 LRU,通知驱动定向失效 | initialize 后共享 Tool Catalog Cache,可由 Server 禁用 | 缓存键必须包含足够的 transport、认证和环境身份 |
| 发布方式 | 16ms 批量更新 AppState 中该 Server 的能力 | ArcSwap 原子替换不可变 Published Runtime | 读者不能看到半新半旧的配置与连接组合 |
| Tool 暴露 | 全限定名、描述截断、风险 annotations、统一权限 passthrough | visibility、App policy、direct/deferred/hidden 与字节预算 | 协议发现结果不能未经治理直接进入模型 Prompt |
| 调用绑定 | Tool 闭包调用前确保连接有效,Session 过期最多重试一次 | Step 冻结 McpBinding,Prepared Call 校验 catalog revision | Schema、client、权限与调用必须来自同一版本 |
| 变化通知 | tools/prompts/resources 分别失效,Resource 变化联动 MCP Skill | dirty → refresh gate → 构造新连接集 → 原子发布 | 失效传播应沿真实依赖图,而不是全部清空或完全不清 |
6.6 参考实现伪代码
async function refreshMcpRuntimeIfDirty(session): // 在模型 Step 前发布与最新配置一致的 MCP Runtime
if not session.mcpRefresh.pending: // 先走无变化快速路径,避免每个 Step 重建所有连接
return session.mcpRuntime.current // 返回当前已发布的不可变 Runtime 快照
lock = await session.mcpRefresh.singlePublisher.acquire() // 获取唯一发布权,防止多个刷新任务交错覆盖
try: // 确保取消或异常时能够恢复 dirty 状态
if not session.mcpRefresh.claimLatestInvalidation(): // 再次确认本任务确实领取到尚未处理的变化
return session.mcpRuntime.current // 其他发布者已完成时直接复用最新快照
guard = restoreDirtyUnlessPublished(session.mcpRefresh) // 创建发布保护器,未完成发布就自动恢复 pending
desired = snapshotConfigAuthEnvironmentAndPlugins(session) // 一次读取配置、认证、环境、插件和权限期望状态
servers = resolveEffectiveServers(desired) // 合并来源、去重并过滤 disabled 或策略禁止的 Server
connections = await connectWithTransportAwareLimits(servers) // 按本地进程与远端网络使用不同并发上限建立连接
catalogs = await initializeAndListCapabilities(connections) // 完成 initialize 后并行发现 Tools、Resources、Prompts 和 Skills
governed = validateNamespaceFilterAndBudget(catalogs, desired.policy) // 校验外部元数据并执行命名空间、可见性、权限和字节预算
nextRuntime = freezeRuntime(desired, connections, governed) // 冻结配置、client 与目录,禁止发布后就地修改
session.mcpRuntime.atomicPublish(nextRuntime) // 一次原子替换,让读者只看到完整旧快照或完整新快照
guard.markPublished() // 标记发布成功,取消保护器的 dirty 恢复动作
return nextRuntime // 返回可供当前 Step 捕获的最新 Runtime
finally: // 无论成功、失败还是取消都要释放唯一发布权
lock.release() // 允许后续配置变化启动下一次刷新
async function executeBoundMcpCall(stepBinding, fqName, args): // 使用模型采样时冻结的目录执行一次 MCP ToolCall
server, tool = parseQualifiedMcpName(fqName) // 从全限定名解析 Server 与 Tool,避免外部工具冒充内建能力
prepared = stepBinding.prepareCall(server, tool) // 取得与该 Step 的 Schema、client、配置和权限绑定的调用对象
require(prepared != null) // 工具不在冻结目录中时拒绝执行,不能回退到全局最新名称查找
require(await prepared.catalogRevisionStillCurrent()) // 在副作用前确认目录版本未被刷新替换
validatedArgs = validate(prepared.inputSchema, args) // 按模型实际看到的同一份 Schema 验证结构化参数
decision = await approveMcpAction(prepared.authority, validatedArgs) // 使用绑定的 Server 来源和权限策略完成审批或等待用户
require(decision.isAllowed) // 拒绝、取消或超时都不能向远端发送 call_tool
result = await prepared.callWithTimeoutAndElicitation(validatedArgs) // 在可取消超时内调用,并处理进度与补充信息请求
normalized = validateExternalMcpContent(result) // 校验外部内容块、URI、结构化数据和不可信元信息
if estimateTokens(normalized) > stepBinding.outputBudget: // 判断完整响应是否会挤占过多上下文窗口
handle = persistLargeMcpOutput(normalized) // 把大结果保存到受控存储并生成后续读取句柄
return summarizeWithBoundedReference(normalized, handle) // 只向模型返回有界摘要和明确的按需读取方式
return normalized // 小结果直接作为 ToolResult 回到 Agent Loop
6.7 出现异常时如何判断
| 层级 | 典型现象 | 应检查的状态或证据 | 不应做的错误重试 |
|---|---|---|---|
| 配置层 | Server 不在列表或重复出现 | scope、动态配置、插件重载键、内容去重、stale config hash | 不要在未确认来源时反复新增同名配置 |
| 认证层 | needs-auth、401、Token 更新后仍失败 | OAuth discovery、安全存储缓存、auth version、账号是否变化 | 不要按固定周期无限探测无 Token Server |
| Transport 层 | timeout、ECONNRESET、进程退出 | stdio stderr、HTTP/SSE close、连接 uptime、重连次数 | 不确认幂等性时不要自动重放已经发出的 ToolCall |
| 协议层 | initialize 成功但没有工具 | capabilities、tools/list 响应、Server 是否关闭目录缓存 | 不要把“空目录”直接判定成网络故障 |
| 目录层 | 工具仍是旧 Schema 或出现幽灵工具 | list_changed、fetch cache、catalog revision、disabled/failed 是否清目录 | 不要只重连却保留旧 tools/resources/prompts cache |
| 暴露层 | Server 有 Tool 但模型看不到 | model visibility、App policy、deferred search、Schema 字节预算 | 不要绕过策略把远端原始列表直接注入 Prompt |
| 权限层 | 工具可见但调用被拒绝 | 全限定名规则、参数风险、Server 来源、Elicitation 状态 | 不要把 Server 的 readOnlyHint 当作本地最终授权 |
| 执行层 | Session 过期、目录变化后调用失败 | exact client、catalog revision、是否允许一次安全恢复 | 不要用旧参数静默调用新版本 Schema |
| 输出层 | Tool 成功但上下文爆满 | 内容类型、Token 估算、二进制与大文本持久化 | 不要把未验证的完整外部响应直接拼进会话 |
6.8 高频工程检验题
问:MCP 是不是 Function Calling 的替代品?
不是。Function Calling 是模型输出结构化工具意图的机制;MCP 是 Harness 与外部能力提供方之间的标准协议。常见链路是“模型 Function Call → Harness 路由 → 本地权限与版本检查 → MCP call_tool”。
问:为什么连接成功后还可能不能调用工具?
因为连接、目录、暴露和权限是四个阶段。initialize 成功只表示协议连接建立;Server 可能没有声明 tools capability,list 结果可能被策略或字节预算隐藏,ToolCall 还可能被本地审批拒绝。诊断时应逐层看状态,不应只看 connected。
问:MCP 工具变化后为什么推荐重新采样?
模型生成参数时依赖当时看到的 description 和 input schema。如果目录变化后仍执行旧 ToolCall,就破坏了判断与执行的一致性。Codex 的 Prepared Call 会检查 catalog revision,变化后拒绝旧调用;通用实现也应重新构建 Step,而不是把旧参数交给新工具。
6.9 原理深化:MCP 的一致性边界是“连接版本 + 工具快照 + 调用授权”
MCP Client 不能只缓存一张工具表。initialize 协商的是某次连接的能力,tools/list 得到的是该连接版本的目录,权限决策又依赖当前 Session 和参数。可靠实现必须把三者一起绑定:模型看到的 Schema、执行使用的 client、批准使用的 authority 必须属于同一个可验证版本。
Claude Code 的优势是产品状态完整:两阶段配置加载、细粒度 list_changed、AppState 批量更新、needs-auth 与远程重连都直接服务于交互体验。Codex 的优势是并发正确性边界清晰:dirty 只表示期望变化,单一 gate 构造新 Runtime,ArcSwap 原子发布,Step 捕获不可变 binding,Prepared Call 在副作用前检查 catalog revision。
把两者合起来,可以得到企业级 MCP Harness 的四条原则:
- 发现结果不等于模型权限:所有远端 Tool 都要经过命名空间、策略、预算和本地审批;
- 预热不承担正确性:后台任务只降低延迟,Step 前的刷新与 binding 才决定执行版本;
- 失效传播遵循依赖图:Resource 变化可能影响 MCP Skill,连接关闭必须同时失效其目录缓存;
- 失败按阶段分类:Transport、Protocol、Catalog、Permission、Tool 和 Output 失败具有不同重试语义。
这套设计避免 MCP 成为绕过 Harness 的“协议捷径”。无论能力来自本地进程、远端服务、插件还是 App Connector,进入 Agent Loop 后都必须服从同一套快照、审批、可观测、输出预算和恢复规则。
6.10 DeepSeek Harness:MCP 工具目录按连接代际原子替换
packages/mcp/mcp-client 的每个插件实例只连接一个 stdio 或 Streamable HTTP Server,serverName 在同一 Cordis app 内必须唯一。模型可见名称为 mcp__<serverName>__<rawName>;超长或非法字符会规范化并追加身份 hash,线路上始终使用原始 tool name,不反向解析公开名。
mcp-client/src/tools.ts 用两阶段同步工具:先分页拉取并构造完整新代定义,任何失败都保留旧代;再撤销旧 disposer 并注册新代,如果名称冲突则回滚到“该 Server 零工具”,不暴露半套目录。连接监督器负责首次就绪、掉线重连、调用超时、在途任务静默化和卸载;MCP isError 被转换为统一 Tool Runtime 错误结果。