第 17 章:MCP:没有运行时耦合的外部工具
阅读契约: 本章回答一个问题:外部 MCP server 如何进入 Codex turn,却不变成 Codex runtime 的一部分?阅读时跟住四个 owner:server configuration、connection lifecycle、model-visible tool shape、provenance-based routing。读完后,应该能解释为什么模型看到的是整理后的 tool name,而执行仍然使用 raw server/tool pair。

源码边界: 本章只有在链接到固定 Codex commit 或本章源码地图的 files、types、functions、schemas、request shapes、event shapes 时,才把说法视为 verified source。把 MCP 读成 trust plane,是从这些可见 owner 得出的 surrounding contract inference。本章不声称知道 provider internals 或 hosted-app 的私有策略。
第 16 章把终端 UI 看成共享 runtime contract 上的 client。本章继续向外走一步:如果 client 可以观察和驱动 thread,却不拥有 runtime,那么外部工具也必须能参与一次 turn,而不是变成可信 runtime code。
你在这里:clients 已经通过 typed events 观察 threads,并通过受控 requests 把 decisions 送回 runtime。
问题:外部工具需要暴露给模型,但 server transport、authentication、命名和失败语义不能泄漏进核心 turn loop。
心智模型:MCP 是四阶段边界:定义 effective servers,启动 clients,暴露 normalized tool specs,再按保存的 provenance 路由调用。
把 MCP 说成“更多工具”会抹掉关键结构。内置 shell tool 的 handler 在 Codex 内部;MCP tool 来自某个 server,可能通过 stdio、HTTP、executor-backed process 或 in-process adapter 运行。模型不该选择这些 transport,turn loop 也不该把外部 server 当作编译进 runtime 的代码。
因此,一个看似简单的 capability 被拆成几种 owner:
| Layer | Owner | 必须稳定的内容 |
|---|---|---|
| Server configuration | codex-mcp config 与 effective server merge | transport、OAuth state、sandbox policy、built-ins、plugin attribution |
| Connection lifecycle | McpConnectionManager | startup status、managed clients、cancellation、elicitation state |
| Model-visible projection | tool normalization 与 core exposure | safe names、deduplicated namespaces、direct/deferred tool lists |
| Execution routing | ToolInfo 里的 provenance | raw server name、raw tool name、shaped observation、error boundary |
不变量很简单:Codex 可以给模型看一个整理后的 function,但 model-visible string 永远不能变成路由权威。
一、MCP 创造 provenance boundary
MCP 有三种身份,不能混为一谈。
| 身份 | 谁使用 | 为什么存在 |
|---|---|---|
| Raw server identity | MCP client 和 connection manager | 让真实 server、transport 和 tool namespace 可寻址。 |
| Model-visible name | model prompt 和 tool schema | 给模型一个适合 tool-call naming limits 的安全标识。 |
| Provenance record | runtime router 和 audit trail | 把模型可见调用映射回所属 server 与 raw tool。 |
这个拆分不是样式问题。外部 server 可以选择会冲突的名字,也可以使用不适合 tool calling 的字符;工具来源还会影响审批、沙箱和用户解释。Codex 因而把命名视为 adaptation step。
源码里有两个明显边界。McpConfig 保存长期 MCP runtime settings,例如 OAuth state location、approval policy、sandbox executable、configured servers、built-ins 和 plugin capability summaries。ToolInfo 同时保留两侧名称:server_name 与 raw tool 用于协议调用,callable_namespace 与 callable_name 用于 model-facing declaration。
Shape-level 上,一个被发现的 tool 像这样:
{
"server_name": "github",
"tool": {
"name": "create_pull_request",
"input_schema": { "type": "object" }
},
"callable_namespace": "GitHub",
"callable_name": "create_pull_request",
"connector_id": "github"
}
Normalization 之后,模型看到的 callable_namespace / callable_name 可能更短,也可能带 hash suffix,尤其是在 raw identities 冲突或超过 API 长度限制时。raw tool.name 仍然保留给执行路径。normalize_tools_for_model() 是这个拆分的 verified source:它清洗 model-visible parts,必要时添加 hash suffix,按 raw identity 排序,并写入最终 callable fields,同时不丢弃 raw metadata。
1.1 Discovery 不是 dispatch
Discovery 产出 model-facing catalog;dispatch 使用保存的 origin。生命周期是:
effective server
-> managed MCP client
-> listed raw tools
-> normalized ToolInfo records
-> model-visible tool declarations
-> tool call resolved back to (server, raw tool)
-> structured CallToolResult returned to Codex
McpConnectionManager::list_all_tools() 从 managed clients 聚合 listed tools,并在 manager return boundary 做 normalization。McpConnectionManager::call_tool() 分别接收 server 和 tool,检查 per-server tool filter,调用 raw MCP tool name,并把 MCP content 转成 Codex protocol 的 CallToolResult。
这防止了一个常见 bug:靠解析 public tool name 重建 provenance。像 github__create_pull_request 这样的名称对模型有用,但真实 route 是保存下来的 server/tool pair。
1.2 Direct/deferred exposure 保护 context
Tool discovery 可能生成太多 declarations,无法全部塞进一次 prompt。Codex 因而把完整 MCP catalog 与直接进入模型请求的集合分开。build_mcp_tool_exposure() 返回 direct_tools 和可选 deferred_tools。Direct set 可以让 explicitly enabled hosted app tools 保持可见,而更大的 MCP catalog 留在 search/deferred loading 后面。
| Pressure | 会失败的简单方案 | Codex 机制 | 保护的不变量 |
|---|---|---|---|
| discovered tools 太多 | 每次请求 dump 所有 schema | direct/deferred exposure | prompt budget 与 stable tool surface |
| hosted app access 随 account 变化 | 暴露所有 directory tools | 按 connector IDs 和 app-tool enablement 过滤 | 只有 accessible hosted tools 成为 direct tools |
| tool names 冲突 | 信任 raw names | sanitize 并 hash callable parts | model-visible names 保持唯一 |
| execution 需要 raw metadata | 用 public names 覆盖 raw names | 保留 ToolInfo.tool.name 和 server_name | dispatch 保留 provenance |
所以 MCP discovery 是 runtime projection,不是 registry mutation。完整 capability set 可以存在边界之后,只有安全子集进入 model-visible view。

二、Server lifecycle 留在边界后方
Connection manager 不只是 function map。源码注释说明它拥有 running clients、startup status events、server origin metadata、aggregated tools/resources/templates、tool routing,以及 core 使用的 public manager API。McpConnectionManager 的字段也对应这个说法:managed clients、server metadata、hosted app enablement、elicitation requests、startup cancellation token。
Startup 设计成报告 status,而不是无限阻塞整个 runtime。在 McpConnectionManager::new() 里,每个 enabled server 先发 Starting,然后变成 Ready、Cancelled 或 Failed,最后汇总到 startup-complete summary。Required server 的失败可以通过 required_startup_failures() 查询。
这个 lifecycle 保护 availability invariant:
optional server fails
-> startup status records failure
-> unavailable tools are absent or stale
-> ordinary turn processing can continue
required server fails
-> required_startup_failures reports a concrete blocker
-> caller can stop or ask the user to fix auth/config
2.1 Elicitation 是 runtime request,不是隐藏 prompt text
有些 MCP operation 需要更多用户或客户端输入。Connection manager 持有 ElicitationRequestManager,允许调用方更新 approval policy 和 permission profile,并暴露 resolve_elicitation()。这让 elicitation 成为结构化 runtime path,而不是让外部 server 静默编造缺失用户决定或把指令塞进 model prompt。
2.2 Resources 与 templates 保持 read/list operations
MCP servers 可以暴露 resources 和 resource templates,不只是 tools。Codex 把这些 request families 分开。list_all_resources() 与 list_all_resource_templates() 按 server 聚合,并在单个 server 失败时 warning。Per-server read_resource() 在 error context 里保留 server name。
这个区别很重要:resource 可能成为 context,tool 可能产生 side effect,template 可能代表参数化读取。共享 clients 可以;共享语义就是边界错误。
三、Hosted app tools 只在后段汇合
Hosted app tools 暴露给模型后看起来像 MCP tools,但 source of truth 不同。它们依赖 connector IDs、directory metadata、account access 和 hosted app enablement。Core exposure 让这种后段汇合可见:hosted app MCP tools 必须经过 connectors list 和 codex_app_tool_is_enabled() 过滤后,才能成为 direct tools。
安全规则是:
MCP server tool -> server provenance proves where to call
hosted app tool -> connector/access metadata proves whether to expose
model-visible tool -> one tool boundary, after the previous checks
一个 hosted app 可以出现在 directory 里,但当前 account 不能用。一个 MCP server 可以被配置,但 startup 失败。两种情况都应该以 capability state 呈现给 clients,而不是藏成模型困惑。
四、Outbound Codex MCP 是 bridge,不是第二套 runtime
Codex 也有 outbound 方向:把 Codex 自己暴露成 MCP server。源码显示这个 surface 很窄。MessageProcessor::new() 创建带 SessionSource::Mcp 的 ThreadManager。handle_list_tools() 只暴露 codex 和 codex-reply 两个工具。handle_call_tool() 也只 dispatch 这些名字,对 unknown tools 返回 error。
Shape-level 上,outbound bridge 像这样:
{
"tools/list": ["codex", "codex-reply"],
"tools/call": {
"codex": "start a Codex session from validated arguments",
"codex-reply": "send a reply to an existing thread_id"
},
"notifications/cancelled": "submit Interrupt to the mapped Codex thread"
}
这不是 inbound MCP 的对称镜像。Inbound MCP 让 Codex 消费外部工具生态;outbound MCP 让外部 MCP client 通过窄 tool surface 启动或继续 Codex work 并接收结果。把 thread、turn、approval、rollout 等所有 native 概念都导出成任意 MCP capabilities,会模糊产品契约。
五、Failure conditions 定义边界
MCP failure 首先是 extension failure,然后才可能是 agent failure。保留 failure category,调用方才知道如何恢复。
| Failure | Verified source surface | Runtime meaning | Recovery |
|---|---|---|---|
| startup timeout/auth error | startup status 与 mcp_init_error_display() | server 未成为 available | 展示 config/auth guidance;required 时阻塞 |
| disabled tool | ToolFilter::allows() 与 call_tool() check | tool 存在但不允许用于这个 server | 不 dispatch,返回结构化 error |
| stale hosted app cache | app-tool cache refresh path | catalog 可能旧,server health 未证明 | hard refresh 或标记 capability stale |
| resource pagination failure | per-server resource aggregation warning | 某个 server 的 read/list family 失败 | 保留其他 servers 的 resources |
| elicitation cancelled | elicitation manager resolution path | 必要 input 未提供 | 停止该 operation,不编造数据 |
| unknown outbound tool | outbound handle_call_tool() | 外部 client 请求了非契约 operation | 返回 MCP tool error |

这张 failure table 也是 trust table。任何无法由 config、connection status、connector access 或 provenance 证明的东西,都不该提升为 model-visible capability。
Trace Ledger
| 问题 | 第 17 章答案 |
|---|---|
| 用户请求现在在哪里? | 它可以从 model-visible tool boundary 进入外部 MCP server,也可以从外部 MCP client 进入窄 Codex MCP bridge。 |
| 什么数据结构携带它? | McpConfig、effective server metadata、managed clients、ToolInfo、direct/deferred exposure、raw (server, tool) routing、resource/template requests 和 CallToolResult。 |
| 谁拥有下一步决策? | 模型选择 visible tool;Codex 解析 provenance 和 policy;MCP server 执行 operation;Codex shaping observation。 |
| 必须保持什么不变? | Model-visible names 可以被重写,但 raw server/tool provenance、account access、approval policy 和 error category 必须保持完整。 |
| 这里可能怎么失败? | startup、OAuth、listing、stale cache、disabled tools、name collision、resource access、elicitation、unknown outbound tools 或 tool-call failure。 |
应用到实践
- 先拆 identity,再 dispatch。 当外部工具可能重名或改名时使用。分开 raw server identity、raw tool name、model-visible name 和 provenance。风险:解析 public tool name 来决定 route。
- 把 discovery 当 projection。 当 catalog 大到不能塞进一次 prompt 时使用。先形成完整 capability catalog,再选择 direct 或 deferred exposure。风险:把“known”等同于“已进入当前 model request”。
- 让 server lifecycle 可观察。 当 optional extensions 可能失败时使用。分别发 startup status 和 required-server failures。风险:让一个不稳定 optional server 拖垮 runtime。
- 分开 read 与 side effect。 当 protocol 同时有 tools、resources 和 templates 时使用。共享 clients,不共享语义。风险:把每个 resource 都变成 callable function。
- 窄化 outbound bridge。 当另一个 protocol 想驱动 Codex 时使用。只映射 start/reply/cancel 这类稳定 operations。风险:把整个 native runtime 镜像成 MCP tools。
接下来
MCP 是一个 extension plane,但不是唯一的 extension plane。第 18 章上移一层,讨论 MCP routing 开始之前,skills、plugin contributions、connectors 和 typed prompt fragments 如何先决定 capability 是否存在。
源码地图
| 概念 | 源码锚点 |
|---|---|
| MCP configuration | codex-rs/codex-mcp/src/mcp/mod.rs |
| Connection manager | codex-rs/codex-mcp/src/connection_manager.rs |
| Tool metadata and normalization | codex-rs/codex-mcp/src/tools.rs |
| Core direct/deferred tool exposure | codex-rs/core/src/mcp_tool_exposure.rs |
| Resource and template routing | codex-rs/codex-mcp/src/connection_manager.rs |
| Outbound Codex MCP server | codex-rs/mcp-server/src/message_processor.rs |