MCP 工具调用 · 连接管理与工具目录
MCP 在 Codex 里分三层:线程级的 McpRuntime 负责连接与重连,McpConnectionSet 管一批 RMCP 客户端,每次采样前冻结出一个 McpBinding 作为这一步的工具目录。工具进目录时被过滤、改名成 mcp__服务器 命名空间下的函数,并按模型能力决定直接暴露还是留给 tool_search;调用时先按注解与 approval_mode 决定要不要审批,审批本身以 elicitation 表单呈现,服务器发起的 elicitation 则按审批策略自动处理或转给用户。
MCP 工具调用 · 连接管理与工具目录
工具系统总览里,MCP 工具是 build_tool_router 的第二个来源。这一篇把它展开:服务器怎么连上,工具怎么进目录、叫什么名字、给不给模型看,模型调用时经过哪些审批,服务器反过来向用户提问又怎么处理。传输与 OAuth 登录留给 MCP 客户端。
用户看到的样子
每个服务器是 config.toml 里的一张 [mcp_servers.<名字>] 表(完整字段见 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
McpManager(codex-rs/core/src/mcp.rs)把几路来源合成一份服务器清单:配置文件、插件(全局安装的与本线程选中的)、扩展贡献的服务器、兼容性内置项(codex_apps 就在这里),同名冲突按优先级裁决。McpRuntime 是线程独有的可变状态,注释说它“Owns all mutable MCP state for one Codex thread”:配置变了、OAuth 凭据恢复了、选中的插件变了,就标记为脏,下次采样前据此重建连接集合(还能用的连接会被复用),再原子地发布出去。连接集合里是一个个 RMCP 客户端,负责启动、发 McpStartupUpdate / McpStartupComplete 事件、汇总工具与资源。真正交给工具系统的是 McpBinding:
/// 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 会先等这个服务器启动完成,再进入并行闸门。
进目录:过滤、改名与暴露
每个工具在 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__:
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 字节时按总览里说的办法压缩(服务器可用 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,或者工具自己带了只读注解,都算可并行:
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)
调用:审批、执行与结果
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 与工具注解决定:
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,可能由自动审查(自动审查)代答,也可能呈现给用户。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 等待命令输出的截止时间会据此顺延。
和《从 LLM 到 Coding Agent》对照
MCP 与工具懒加载讲了两件事:用标准协议接入外部工具,以及用延迟加载加工具搜索应对几百个工具。Codex 两件都做了,而且多了几层教学实现没有的东西:按步冻结的 McpBinding 固定每一步给模型看的目录,执行时再对照最新目录准备调用,命名规范化与哈希后缀处理跨服务器撞名,按注解分级的审批与持久化的“不再询问”,以及服务器反向提问的 elicitation 通道。教学实现里的 search_tools 在这里叫 tool_search,检索用 BM25,下一篇会讲到它的参数与返回。