Skip to content

MCP 統合

源码版本rust-v0.145.0

codex の MCP (Model Context Protocol) 統合は双方向に動く:対内には rmcp-client でユーザが設定した MCP server(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_requestInitializeRequest / 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 を再構築できる。

McpServerMetadatasupports_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 の承認フローは elicitation を使う:codex 内部がコマンド exec や apply patch をする時、直接実行せず elicitation/create を MCP host(Claude Desktop UI など)に送り、host に確認ダイアログを出させる。ExecApprovalElicitRequestParamscodex_command / codex_cwd / codex_parsed_cmd を持ち、client はこれらのフィールドで確認 UI を描く。

McpCatalogBuilderRegistrationPrecedence で同名 server 衝突を解決する:config が plugin より優先、plugin が compatibility より優先、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 を持ち、標準 MCP メソッドを dispatch する。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}`"))?;
    // ...
}

データフロー

境界と失敗

  • 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_sessionthread_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 がタイムアウトの猶予を与え、起動の遅い server を即座に死判定しない (codex-rs/codex-mcp/src/connection_manager.rs:399-485)。
  • OAuth クレデンシャルは独立保存:rmcp-clientOAuthCredentialsStoreModeAuthKeyringBackendKind で OAuth token を keyring またはファイルに置ける。delete_oauth_tokenslogout サブコマンドの入口 (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 アーキテクチャ を参照。