Skip to content

MCP 整合

源码版本rust-v0.145.0

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 路由。

職責

  1. 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)。
  2. 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)。
  3. mcp-server crate 把 codex 包成 MCP server:stdin 讀 JSON-RPC,MessageProcessor::process_request 分發 InitializeRequest / ListToolsRequest / CallToolRequest 等標準 MCP 方法,內部用 ThreadManager 起 codex 會話 (codex-rs/mcp-server/src/lib.rs:59-172)。
  4. codex MCP tool 呼叫入口 run_codex_tool_sessionthread_manager.start_thread,把 prompt 作為 Op::UserInput 提交,然後用 sub_id 關聯 MCP tools/call request 與 codex 事件 (codex-rs/mcp-server/src/codex_tool_runner.rs:57-141)。
  5. 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。

McpCatalogBuilderRegistrationPrecedence 解決同 server 名衝突:config 優先於 plugin 優先於 compatibility 優先於 extension,後者只能填前者的空缺。McpServerConflictAction 讓衝突時決定是 disable 還是覆蓋。

關鍵檔案

codex-rs/mcp-server/src/lib.rs:59-203run_main 起 stdin reader / processor / stdout writer 三個 task,EOF 觸發級聯關閉。codex-rs/mcp-server/src/message_processor.rs:41-130MessageProcessorThreadManager,dispatch 標準 MCP 方法。codex-rs/mcp-server/src/codex_tool_config.rs:25-101CodexToolCallParam,客戶端呼叫 codex tool 時的可配置欄位(prompt / model / cwd / approval / sandbox / config)。codex-rs/mcp-server/src/codex_tool_runner.rs:57-141run_codex_tool_session,起 codex 會話並提交初始 prompt。codex-rs/mcp-server/src/exec_approval.rs:20-48ExecApprovalElicitRequestParams,exec 審批的 elicitation 參數。codex-rs/rmcp-client/src/rmcp_client.rs:326-389RmcpClient 三種 transport 建構子。codex-rs/codex-mcp/src/connection_manager.rs:116-124McpConnectionManager 欄位,持 clients / metadata / required_servers / elicitation_requests。codex-rs/codex-mcp/src/connection_manager.rs:848-883call_tool 實現,tool_filter 拒絕 disabled tool。codex-rs/codex-mcp/src/catalog.rs:79-170McpServerRegistrationMcpCatalogBuilder,處理 config / plugin / extension 來源衝突。codex-rs/codex-mcp/src/server.rs:15-94EffectiveMcpServerMcpServerMetadata,執行時附加的 launch 策略與 per-tool approval。codex-rs/cli/src/mcp_cmd.rs:45-105McpCli 子命令與 AddMcpTransportArgs

mcp-server 的三個 task 是顯式的 stdin / processor / stdout 分離,典型的 line-delimited JSON-RPC server。EOF 觸發 incoming_tx drop,processor 收到 None 退出,stdout writer 跟著結束:

rust
// 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:

rust
// 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:

rust
// 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}`"))?;
    // ...
}

資料流

邊界與失敗

小結

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 架構