English 书架

第 5 部

扩展系统

扩展点只有在每个信任边界都显式时才有用。

第 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。

MCP 信任平面:分开 server provenance、清洗后的 tool name、discovery、routing、elicitation 与结构化 observation
MCP 通过区分 server provenance、transport、discovery、normalized specs 和 routing 来扩展能力,而不缠住 runtime。

源码边界: 本章只有在链接到固定 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:

LayerOwner必须稳定的内容
Server configurationcodex-mcp config 与 effective server mergetransport、OAuth state、sandbox policy、built-ins、plugin attribution
Connection lifecycleMcpConnectionManagerstartup status、managed clients、cancellation、elicitation state
Model-visible projectiontool normalization 与 core exposuresafe names、deduplicated namespaces、direct/deferred tool lists
Execution routingToolInfo 里的 provenanceraw server name、raw tool name、shaped observation、error boundary

不变量很简单:Codex 可以给模型看一个整理后的 function,但 model-visible string 永远不能变成路由权威。

一、MCP 创造 provenance boundary

MCP 有三种身份,不能混为一谈。

身份谁使用为什么存在
Raw server identityMCP client 和 connection manager让真实 server、transport 和 tool namespace 可寻址。
Model-visible namemodel prompt 和 tool schema给模型一个适合 tool-call naming limits 的安全标识。
Provenance recordruntime 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_namespacecallable_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() 分别接收 servertool,检查 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 所有 schemadirect/deferred exposureprompt budget 与 stable tool surface
hosted app access 随 account 变化暴露所有 directory tools按 connector IDs 和 app-tool enablement 过滤只有 accessible hosted tools 成为 direct tools
tool names 冲突信任 raw namessanitize 并 hash callable partsmodel-visible names 保持唯一
execution 需要 raw metadata用 public names 覆盖 raw names保留 ToolInfo.tool.nameserver_namedispatch 保留 provenance

所以 MCP discovery 是 runtime projection,不是 registry mutation。完整 capability set 可以存在边界之后,只有安全子集进入 model-visible view。

MCP 与 client tools 经过 canonical name、provenance、direct spec、deferred tool search、unavailable placeholder 和 route-back handler lookup
Tool provenance 把 discovery、model-visible naming、deferred exposure 与 route-back handler lookup 分开。

二、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,然后变成 ReadyCancelledFailed,最后汇总到 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::McpThreadManagerhandle_list_tools() 只暴露 codexcodex-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,调用方才知道如何恢复。

FailureVerified source surfaceRuntime meaningRecovery
startup timeout/auth errorstartup status 与 mcp_init_error_display()server 未成为 available展示 config/auth guidance;required 时阻塞
disabled toolToolFilter::allows()call_tool() checktool 存在但不允许用于这个 server不 dispatch,返回结构化 error
stale hosted app cacheapp-tool cache refresh pathcatalog 可能旧,server health 未证明hard refresh 或标记 capability stale
resource pagination failureper-server resource aggregation warning某个 server 的 read/list family 失败保留其他 servers 的 resources
elicitation cancelledelicitation manager resolution path必要 input 未提供停止该 operation,不编造数据
unknown outbound tooloutbound handle_call_tool()外部 client 请求了非契约 operation返回 MCP tool error
Tool router 把 function、custom、local shell、tool search 和 MCP response items 转成 payload kinds,再进入 handler matching
Router gate 是 model-visible call 变成分类 payload 的位置,之后 runtime handler 才能执行。

这张 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。

应用到实践

  1. 先拆 identity,再 dispatch。 当外部工具可能重名或改名时使用。分开 raw server identity、raw tool name、model-visible name 和 provenance。风险:解析 public tool name 来决定 route。
  2. 把 discovery 当 projection。 当 catalog 大到不能塞进一次 prompt 时使用。先形成完整 capability catalog,再选择 direct 或 deferred exposure。风险:把“known”等同于“已进入当前 model request”。
  3. 让 server lifecycle 可观察。 当 optional extensions 可能失败时使用。分别发 startup status 和 required-server failures。风险:让一个不稳定 optional server 拖垮 runtime。
  4. 分开 read 与 side effect。 当 protocol 同时有 tools、resources 和 templates 时使用。共享 clients,不共享语义。风险:把每个 resource 都变成 callable function。
  5. 窄化 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 configurationcodex-rs/codex-mcp/src/mcp/mod.rs
Connection managercodex-rs/codex-mcp/src/connection_manager.rs
Tool metadata and normalizationcodex-rs/codex-mcp/src/tools.rs
Core direct/deferred tool exposurecodex-rs/core/src/mcp_tool_exposure.rs
Resource and template routingcodex-rs/codex-mcp/src/connection_manager.rs
Outbound Codex MCP servercodex-rs/mcp-server/src/message_processor.rs