MCP 集成
codex 的 MCP (Model Context Protocol) 集成双向工作:对内通过 rmcp-client 连接用户配置的 MCP servers(stdio / streamable HTTP / in-process),把这些 server 的工具、资源、prompt 暴露给模型;对外通过 mcp-server crate 把 codex 自己包装成一个 MCP server,让其他 MCP host(比如 Claude Desktop)可以调 codex 当工具用。codex-mcp 是中间的运行时,管理连接生命周期、tool catalog、elicitation 路由。
职责
McpConnectionManager持有多个RmcpClient,提供list_all_tools/call_tool/list_all_resources等聚合接口,tool filter、approval mode、tool timeout 都在这里管 (codex-rs/codex-mcp/src/connection_manager.rs:116-124)。RmcpClient支持三种 transport:new_in_process_client(扩展进程内)、new_stdio_client(子进程)、new_streamable_http_client(远程 HTTP + OAuth),共用TransportRecipe抽象 (codex-rs/rmcp-client/src/rmcp_client.rs:326-389)。mcp-servercrate 把 codex 包成 MCP server:stdin 读 JSON-RPC,MessageProcessor::process_request分发InitializeRequest/ListToolsRequest/CallToolRequest等标准 MCP 方法,内部用ThreadManager起 codex 会话 (codex-rs/mcp-server/src/lib.rs:59-172)。codexMCP tool 调用入口run_codex_tool_session起thread_manager.start_thread,把 prompt 作为Op::UserInput提交,然后用sub_id关联 MCPtools/callrequest 与 codex 事件 (codex-rs/mcp-server/src/codex_tool_runner.rs:57-141)。McpCli子命令(list/get/add/remove/login/logout)管~/.codex/config.toml里的 MCP servers 配置,OAuth login 走perform_oauth_login(codex-rs/cli/src/mcp_cmd.rs:45-61)。
设计动机
codex 把 MCP 拆成两个 crate 而不是一个,因为 server 和 client 是完全不同的角色:client(rmcp-client + codex-mcp)是 codex 进程内跑的,把外部 MCP server 工具暴露给模型;server(mcp-server)是把 codex 自己暴露给别的 MCP host。两边共用 rmcp SDK 但运行时互相不依赖,这样 server crate 可以独立编译,client crate 也不会被 server 的 ThreadManager 启动逻辑污染。
TransportRecipe 抽象是因为 stdio / streamable HTTP / in-process 三种 transport 的连接恢复、生命周期管理完全不同:stdio 子进程崩了要重启,HTTP 连接断了要重连 + 重发 OAuth,in-process 没有传输层。把 transport 信息存进 recipe,RmcpClient 在断线后能按 recipe 重建 transport。
McpServerMetadata 里的 supports_parallel_tool_calls / default_tools_approval_mode / tool_approval_modes 是从 config 里读的 per-server 配置——有些 MCP server 不支持并发调用(codex apps 里很多),有些工具要单独审批。tool_approval_mode(tool_name) 按 tool 名优先,落到 server 默认,最后兜底全局默认。
codex MCP tool 的 approval 流程用 elicitation:codex 内部要 exec 命令或 apply patch 时,不直接执行,而是发 elicitation/create 给 MCP host(比如 Claude Desktop UI),让 host 弹个确认框。ExecApprovalElicitRequestParams 里带 codex_command / codex_cwd / codex_parsed_cmd,client 拿到这些字段渲染确认 UI。
McpCatalogBuilder 用 RegistrationPrecedence 解决同 server 名冲突:config 优先于 plugin 优先于 compatibility 优先于 extension,后者只能填前者的空缺。McpServerConflictAction 让冲突时决定是 disable 还是覆盖。
关键文件
codex-rs/mcp-server/src/lib.rs:59-203 — run_main 起 stdin reader / processor / stdout writer 三个 task,EOF 触发级联关闭。codex-rs/mcp-server/src/message_processor.rs:41-130 — MessageProcessor 持 ThreadManager,dispatch 标准 MCP 方法。codex-rs/mcp-server/src/codex_tool_config.rs:25-101 — CodexToolCallParam,客户端调用 codex tool 时的可配置字段(prompt / model / cwd / approval / sandbox / config)。codex-rs/mcp-server/src/codex_tool_runner.rs:57-141 — run_codex_tool_session,起 codex 会话并提交初始 prompt。codex-rs/mcp-server/src/exec_approval.rs:20-48 — ExecApprovalElicitRequestParams,exec 审批的 elicitation 参数。codex-rs/rmcp-client/src/rmcp_client.rs:326-389 — RmcpClient 三种 transport 构造器。codex-rs/codex-mcp/src/connection_manager.rs:116-124 — McpConnectionManager 字段,持 clients / metadata / required_servers / elicitation_requests。codex-rs/codex-mcp/src/connection_manager.rs:848-883 — call_tool 实现,tool_filter 拒绝 disabled tool。codex-rs/codex-mcp/src/catalog.rs:79-170 — McpServerRegistration 与 McpCatalogBuilder,处理 config / plugin / extension 来源冲突。codex-rs/codex-mcp/src/server.rs:15-94 — EffectiveMcpServer 与 McpServerMetadata,运行时附加的 launch 策略与 per-tool approval。codex-rs/cli/src/mcp_cmd.rs:45-105 — McpCli 子命令与 AddMcpTransportArgs。mcp-server 的三个 task 是显式的 stdin / processor / stdout 分离,典型的 line-delimited JSON-RPC server。EOF 触发 incoming_tx drop,processor 收到 None 退出,stdout writer 跟着结束:
// mcp-server/src/lib.rs:148-172 — processor 主循环
let processor_handle = tokio::spawn({
let mut processor = MessageProcessor::new(/* ... */).await;
async move {
while let Some(msg) = incoming_rx.recv().await {
match msg {
JsonRpcMessage::Request(r) => processor.process_request(r).await,
JsonRpcMessage::Response(r) => processor.process_response(r).await,
JsonRpcMessage::Notification(n) => processor.process_notification(n).await,
JsonRpcMessage::Error(e) => processor.process_error(e),
}
}
info!("processor task exited (channel closed)");
}
});run_codex_tool_session 用 MCP request id 当 codex sub_id,这样后续 codex 事件(ExecApproval、PatchApproval)都能关联回原 tools/call request。失败的几条路径(start_thread 失败、submit 失败)都显式发 CallToolResult::error:
// mcp-server/src/codex_tool_runner.rs:98-119 — sub_id 关联 + submit
let sub_id = id.to_string();
running_requests_id_to_codex_uuid
.lock()
.await
.insert(id.clone(), thread_id);
let submission = Submission {
id: sub_id.clone(),
op: Op::UserInput {
items: vec![UserInput::Text {
text: initial_prompt.clone(),
text_elements: Vec::new(),
}],
// ...
},
// ...
};
if let Err(e) = thread.submit_with_id(submission).await {
tracing::error!("Failed to submit initial prompt: {e}");
let result = create_call_tool_result_with_thread_id(
thread_id, format!("Failed to submit initial prompt: {e}"), Some(true),
);
outgoing.send_response(id.clone(), result).await;
running_requests_id_to_codex_uuid.lock().await.remove(&id);
return;
}call_tool 在路由到具体 client 前先过 tool_filter.allows(tool),disabled tool 直接报错;tool_timeout 是 per-server 配置,避免慢 MCP server 拖住整个 turn:
// codex-mcp/src/connection_manager.rs:848-866 — call_tool 路由
pub async fn call_tool(&self, server: &str, tool: &str, arguments: Option<serde_json::Value>, meta: Option<serde_json::Value>) -> Result<CallToolResult> {
let client = self.client_by_name(server).await?;
if !client.tool_filter.allows(tool) {
return Err(anyhow!("tool '{tool}' is disabled for MCP server '{server}'"));
}
let result: rmcp::model::CallToolResult = client
.client
.call_tool(tool.to_string(), arguments, meta, client.tool_timeout)
.await
.with_context(|| format!("tool call failed for `{server}/{tool}`"))?;
// ...
}数据流
边界与失败
- tool filter 拒绝 disabled tool:
call_tool第一行检查client.tool_filter.allows(tool),直接返回错误,不进 client (codex-rs/codex-mcp/src/connection_manager.rs:855-860)。 start_thread失败要回错:run_codex_tool_session在thread_manager.start_thread出错时发CallToolResult::error并 return,不进 submit (codex-rs/mcp-server/src/codex_tool_runner.rs:65-78)。- required server 缺失会校验失败:
validate_required_servers在启动时跑,缺了直接报错;但wait_for_server_ready给了 timeout 缓冲,慢启动的 server 不会立即判死 (codex-rs/codex-mcp/src/connection_manager.rs:399-485)。 - OAuth 凭据独立存储:
rmcp-client的OAuthCredentialsStoreMode与AuthKeyringBackendKind让 OAuth token 可以走 keyring 或文件,delete_oauth_tokens是logout子命令的入口 (codex-rs/cli/src/mcp_cmd.rs:168-172)。 McpServerConflictAction处理同名冲突:config 和 plugin 都注册了同名 server 时,precedence 决定谁赢,disable是软冲突的处理方式 (codex-rs/codex-mcp/src/catalog.rs:170-184)。
小结
MCP 集成分两端:rmcp-client + codex-mcp 把外部 MCP server 的工具聚合给 codex 内部使用,mcp-server 把 codex 自己暴露成 MCP server 给外部 host 调。两边的协议基础都是 rmcp SDK,但运行时与生命周期独立。McpConnectionManager 是 client 端的核心,run_codex_tool_session 是 server 端的 tool-call 入口。要看 codex 会话被起出来后怎么跑模型循环,接 Agent 主循环;要看 MCP server 如何挂在 app-server 进程下,看 App-server 架构。