# MCP 工具调用 · 连接管理与工具目录

> MCP 在 Codex 里分三层：线程级的 McpRuntime 负责连接与重连，McpConnectionSet 管一批 RMCP 客户端，每次采样前冻结出一个 McpBinding 作为这一步的工具目录。工具进目录时被过滤、改名成 mcp__服务器 命名空间下的函数，并按模型能力决定直接暴露还是留给 tool_search；调用时先按注解与 approval_mode 决定要不要审批，审批本身以 elicitation 表单呈现，服务器发起的 elicitation 则按审批策略自动处理或转给用户。

- 作者：David（道雾轩）
- 专栏：Codex 源码解读（https://daiw.net/manual/codex-source.md）
- 最后更新：2026-09-29
- 原文：https://daiw.net/manual/codex-source/mcp-tool-calls
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

# MCP 工具调用 · 连接管理与工具目录

[工具系统总览](https://daiw.net/manual/codex-source/tool-architecture)里，MCP 工具是 `build_tool_router` 的第二个来源。这一篇把它展开：服务器怎么连上，工具怎么进目录、叫什么名字、给不给模型看，模型调用时经过哪些审批，服务器反过来向用户提问又怎么处理。传输与 OAuth 登录留给 [MCP 客户端](https://daiw.net/manual/codex-source/mcp-client-oauth)。

## 用户看到的样子

每个服务器是 `config.toml` 里的一张 `[mcp_servers.<名字>]` 表（完整字段见 [MCP](https://daiw.net/manual/codex/mcp)）。和本篇相关的有：`enabled_tools` / `disabled_tools`（白名单与黑名单）、`required`（必需服务器起不来就让会话启动失败）、`startup_timeout_sec` 与 `tool_timeout_sec`（源码默认 30 秒与 300 秒，`codex-rs/codex-mcp/src/rmcp_client.rs:103`）、`default_tools_approval_mode` 与 `tools.<工具名>.approval_mode`（`auto`、`prompt`、`writes`、`approve`）、`supports_parallel_tool_calls`，以及单个工具的 `output_token_limit`。会话里 `/mcp` 查看服务器状态；hook 里 MCP 工具的名字形如 `mcp__fs__read_file`。插件也可以自带 MCP 服务器，ChatGPT 的应用连接器则走一个保留名为 `codex_apps` 的内置服务器。

## 三层对象：Runtime、ConnectionSet 与 Binding

```mermaid
flowchart TB
  CFG[config.toml · 插件 · 扩展 · 内置 codex_apps] --> MM[McpManager<br/>合并来源 解决重名]
  MM --> RT[McpRuntime<br/>线程级 · 原子发布]
  RT --> CS[McpConnectionSet<br/>一批 RMCP 客户端 · 启动事件]
  CS --> L[tools/list 分页<br/>过滤 · 规范化命名]
  L --> B[McpBinding<br/>本次采样冻结的目录与调用句柄]
  B --> SP[spec_plan<br/>McpHandler 登记进注册表]
  CACHE[McpToolCatalogCache<br/>进程级 LRU] -.-> CS
```

`McpManager`（`codex-rs/core/src/mcp.rs`）把几路来源合成一份服务器清单：配置文件、插件（全局安装的与本线程选中的）、扩展贡献的服务器、兼容性内置项（`codex_apps` 就在这里），同名冲突按优先级裁决。`McpRuntime` 是线程独有的可变状态，注释说它“Owns all mutable MCP state for one Codex thread”：配置变了、OAuth 凭据恢复了、选中的插件变了，就标记为脏，下次采样前据此重建连接集合（还能用的连接会被复用），再原子地发布出去。连接集合里是一个个 RMCP 客户端，负责启动、发 `McpStartupUpdate` / `McpStartupComplete` 事件、汇总工具与资源。真正交给工具系统的是 `McpBinding`：

```rust
/// The exact tool catalog and execution handles shared by compatible sampling steps.
pub struct McpBinding {
    connections: Arc<McpConnectionSet>,
    clients: Arc<McpBindingClients>,
    config: Arc<McpConfig>,
    plugins_available: bool,
    tools: Vec<ToolInfo>,
    calls: HashMap<(String, String), PreparedMcpCall>,
}
```

（`codex-rs/codex-mcp/src/binding.rs:31`）

`StepContext` 里存的就是它：这一步给模型看哪些 MCP 工具，以它冻结的目录为准。真正执行时，`McpHandler` 调用 `Session::prepare_mcp_call`，先把标记为脏的运行时刷新掉，再对照当前发布的连接与策略准备调用。`McpRuntime::prepare_call` 的注释写明 “Its model-visible name stays fixed while execution metadata comes from the live catalog”：名字按当初给模型看的算，执行用的客户端与元数据取最新的；新目录里已经没有这个工具，调用就不会发出。

启动也有讲究。`McpStartupPolicy` 分 `Eager`（发布时就启动）与 `LazyWhenCached`（有缓存目录的服务器等第一次用到再启动）；目录缓存 `McpToolCatalogCache` 是进程级的 LRU，最多 32 项、30 分钟过期。首轮构建工具表时，非必需服务器最多等 `mcp_optional_startup_grace_ms`（默认 1 秒），没起来的先缺席。模型真的调用某个服务器的工具时，`McpHandler` 的 `wait_until_ready` 会先等这个服务器启动完成，再进入[并行闸门](https://daiw.net/manual/codex-source/tool-architecture)。

## 进目录：过滤、改名与暴露

每个工具在 Codex 里是一个 `ToolInfo`：原始的服务器名与工具名留着发协议请求用，另有一对给模型看的 `callable_namespace` / `callable_name`。进目录要过三关。

过滤：先按 `enabled_tools` 白名单、再按 `disabled_tools` 黑名单；工具的 `_meta` 里如果按 MCP Apps 扩展声明了可见性、却没列出 `model`，它只给界面用，不进模型目录（`tool_is_model_visible`）。`tools/list` 分页收集时每个服务器最多 2,048 项，`codex_apps` 放宽到 8,192 项。

改名：模型侧的名字只允许字母、数字和下划线，其余字符一律换成 `_`，命名空间默认加上历史前缀 `mcp__`：

```rust
const MCP_TOOL_NAME_DELIMITER: &str = "__";
const MAX_TOOL_NAME_LENGTH: usize = 128;
const CALLABLE_NAME_HASH_LEN: usize = 12;
fn callable_namespace_with_prefix(namespace: &str, prefix_mcp_tool_names: bool) -> String {
    if !prefix_mcp_tool_names || namespace.starts_with(LEGACY_MCP_TOOL_NAME_PREFIX) {
        namespace.to_string()
    } else {
        format!("{LEGACY_MCP_TOOL_NAME_PREFIX}{namespace}")
    }
}
```

（`codex-rs/codex-mcp/src/tools.rs:225`）

规范化之后如果两个不同的服务器撞成同一个命名空间，或者同一命名空间里撞出同名工具，就在后面追加 `_` 加原始身份 SHA-1 的前 12 位十六进制；拼起来超过 128 字节时截短再加哈希，保证名字唯一且不超 API 上限。去掉 `mcp__` 前缀的 `non_prefixed_mcp_tool_names` 还在开发中。

暴露：`McpHandler` 把每个工具包成一个只含一个函数的 `namespace` spec，命名空间的描述取服务器初始化时给的 `instructions`，应用连接器则写成 “Tools for working with 某某.”；`spec_plan` 再把同名命名空间合并，于是一个服务器在请求里就是一个命名空间。输入 schema 超过 5,000 字节时按[总览](https://daiw.net/manual/codex-source/tool-architecture)里说的办法压缩（服务器可用 `tool_input_schema_max_bytes` 调整）。模型支持 `tool_search` 时 MCP 工具默认是 `Deferred`，不进初始清单；agent 插件提供的 MCP 工具另有预算，单个 spec 不超过 8,000 字节、合计不超过 64,000 字节，超出的登记为 `Hidden`（`codex-rs/core/src/mcp_tool_exposure.rs:18`）。并发方面，服务器配了 `supports_parallel_tool_calls`，或者工具自己带了只读注解，都算可并行：

```rust
    fn supports_parallel_tool_calls(&self) -> bool {
        // Correctly implemented MCP servers should tolerate parallel calls to
        // tools that advertise themselves as read-only.
        self.tool_info.supports_parallel_tool_calls
            || self
                .tool_info
                .tool
                .annotations
                .as_ref()
                .and_then(|annotations| annotations.read_only_hint)
                .unwrap_or(false)
    }
```

（`codex-rs/core/src/tools/handlers/mcp.rs:148`）

## 调用：审批、执行与结果

```mermaid
sequenceDiagram
  participant M as 模型
  participant H as McpHandler
  participant C as handle_mcp_tool_call
  participant A as 审批
  participant S as MCP 服务器
  M->>H: FunctionCall namespace mcp__docs name search
  H->>H: prepare_mcp_call 对照最新目录准备调用
  H->>C: 原始服务器名 原始工具名 参数
  C->>C: 参数解析 · 应用策略检查
  C->>A: maybe_request_mcp_tool_approval
  A-->>C: 批准 · 本会话批准 · 永久批准 · 拒绝
  C->>S: tools/call 附带 _meta
  S-->>C: CallToolResult
  C-->>H: 去掉不支持的图片音频 · 截断
  H-->>M: Wall time 头部加结果
```

`handle_mcp_tool_call`（`codex-rs/core/src/mcp_tool_call.rs:128`）先把参数解析成 JSON，解析失败直接回一条错误结果；当前运行时里找不到这个工具，回 `MCP tool ... is not available to the model`；`codex_apps` 的工具还要过应用配置，被禁用的回 `MCP tool call blocked by app configuration`。然后是审批。审批策略为 `never` 且处于完全访问（或者该工具的 `approval_mode` 是 `approve`）时直接放行；否则按 `approval_mode` 与工具注解决定：

```rust
fn requires_mcp_tool_approval_for_mode(
    annotations: Option<&ToolAnnotations>,
    approval_mode: AppToolApproval,
) -> bool {
    match approval_mode {
        AppToolApproval::Auto => requires_mcp_tool_approval(annotations),
        AppToolApproval::Prompt => true,
        AppToolApproval::Writes => !annotations
            .and_then(|annotations| annotations.read_only_hint)
            .unwrap_or(false),
        AppToolApproval::Approve => false,
    }
}
```

（`codex-rs/core/src/mcp_tool_call.rs:2461`）

`auto` 模式下的 `requires_mcp_tool_approval` 规则是：声明了破坏性的要问；声明了只读的不问；其余情况下，只有明确声明“非破坏性”且“非开放世界”才不问，没有注解就要问。本会话已经批准过的同一工具直接放行。需要问时，请求交给 `Session::request_approval`，可能由自动审查（[自动审查](https://daiw.net/manual/codex-source/guardian)）代答，也可能呈现给用户。`tool_call_mcp_elicitation` 特性（默认开）让这个审批本身以一张 MCP elicitation 表单出现，选项是 “Allow”、“Allow for this session”、“Allow and don't ask me again”、“Cancel”；选 “Allow and don't ask me again” 时，Codex 会把 `mcp_servers.<服务器>.tools.<工具>.approval_mode = "approve"` 写进对应的配置文件。应用连接器的常见写操作还有现成的提问模板，`codex-rs/core/assets/consequential_tool_message_templates.json` 里有 55 条，例如 `Allow {connector_name} to add a comment to a pull request?`。

批准之后才真正发出 `tools/call`，发的是原始工具名。请求的 `_meta` 里带上线程与会话 ID；服务器在能力里声明了 `codex/sandbox-state-meta` 的，还会收到当前的权限配置与沙箱工作目录，怎么用由服务器自己决定。结果回来后，模型不支持图片或音频输入时，对应的内容块被换成一句 `<image content omitted because you do not support image input>` 之类的说明；再按该工具的 `output_token_limit`（没配就用模型的截断策略）截断，前面加一行 `Wall time`。

## 服务器反过来提问：elicitation

MCP 允许服务器在处理请求时向用户要信息（表单或打开一个 URL）。`codex-rs/codex-mcp/src/elicitation.rs` 的文件头说得清楚：这里决定一个请求是自动接受、按策略拒绝，还是作为协议事件交给前端、等用户答复。几条主要规则：

- 线程被设为自动拒绝、请求是“推荐工具”类、找不到对应服务器的权限配置时，一律 `Decline`；
- 审批策略为 `never` 且处于完全访问时，不需要填任何字段的确认类表单直接 `Accept`；
- 审批策略拒绝 elicitation（`never`，或细粒度配置关掉了 `mcp_elicitations`）时 `Decline`；
- 其余情况先问自动审查，没有结论再发 `ElicitationRequest` 事件给前端，并用一个 Codex 自己生成的请求 ID 登记回调，用户答完由 `resolve_elicitation` 送回服务器。

等待期间会登记一次“用户交互暂停”，[unified exec](https://daiw.net/manual/codex-source/unified-exec) 等待命令输出的截止时间会据此顺延。

## 和《从 LLM 到 Coding Agent》对照

[MCP 与工具懒加载](https://daiw.net/manual/llm-to-agent/mcp)讲了两件事：用标准协议接入外部工具，以及用延迟加载加工具搜索应对几百个工具。Codex 两件都做了，而且多了几层教学实现没有的东西：按步冻结的 `McpBinding` 固定每一步给模型看的目录，执行时再对照最新目录准备调用，命名规范化与哈希后缀处理跨服务器撞名，按注解分级的审批与持久化的“不再询问”，以及服务器反向提问的 elicitation 通道。教学实现里的 `search_tools` 在这里叫 `tool_search`，检索用 BM25，下一篇会讲到它的参数与返回。

---

上一篇：[apply_patch · Codex 的文件编辑格式](https://daiw.net/manual/codex-source/apply-patch) · 下一篇：[其它内置工具 · 计划、提问、权限与搜索](https://daiw.net/manual/codex-source/builtin-tools)
