Skip to content

Intégration MCP

源码版本rust-v0.145.0

L'intégration MCP (Model Context Protocol) de codex fonctionne dans les deux sens : vers l'intérieur, via rmcp-client, elle se connecte aux MCP servers configurés par l'utilisateur (stdio / streamable HTTP / in-process) et expose leurs outils, ressources et prompts au modèle ; vers l'extérieur, via le crate mcp-server, elle emballe codex lui-même en MCP server pour qu'un autre MCP host (par ex. Claude Desktop) puisse appeler codex comme un outil. codex-mcp est le runtime du milieu, qui gère le cycle de vie des connexions, le catalog d'outils et le routing d'elicitation.

Responsabilités

  1. McpConnectionManager détient plusieurs RmcpClient et fournit des interfaces agrégées list_all_tools / call_tool / list_all_resources ; tool filter, approval mode et tool timeout se gèrent ici (codex-rs/codex-mcp/src/connection_manager.rs:116-124).
  2. RmcpClient supporte trois transports : new_in_process_client (extension en process), new_stdio_client (sous-processus), new_streamable_http_client (HTTP distant + OAuth), tous unifiés par l'abstraction TransportRecipe (codex-rs/rmcp-client/src/rmcp_client.rs:326-389).
  3. Le crate mcp-server emballe codex en MCP server : il lit du JSON-RPC sur stdin, MessageProcessor::process_request dispatche les méthodes MCP standard InitializeRequest / ListToolsRequest / CallToolRequest, et utilise ThreadManager en interne pour lancer une session codex (codex-rs/mcp-server/src/lib.rs:59-172).
  4. L'entrée d'appel d'outil MCP run_codex_tool_session lance thread_manager.start_thread, soumet le prompt comme Op::UserInput, puis associe le sub_id à la requête tools/call MCP et aux événements codex (codex-rs/mcp-server/src/codex_tool_runner.rs:57-141).
  5. La sous-commande McpCli (list / get / add / remove / login / logout) gère la config des MCP servers dans ~/.codex/config.toml ; le login OAuth passe par perform_oauth_login (codex-rs/cli/src/mcp_cmd.rs:45-61).

Motivations de conception

codex a éclaté MCP en deux crate plutôt qu'un seul, parce que server et client ont des rôles totalement différents : le client (rmcp-client + codex-mcp) tourne dans le processus codex pour exposer au modèle les outils des MCP servers externes ; le server (mcp-server) expose codex lui-même à un autre MCP host. Les deux partagent le SDK rmcp mais ne dépendent pas l'un de l'autre à l'exécution, si bien que le crate server peut compiler indépendamment, et le crate client n'est pas pollué par la logique de démarrage du ThreadManager du server.

L'abstraction TransportRecipe existe parce que les trois transports (stdio / streamable HTTP / in-process) ont des gestions très différentes de reconnexion et de cycle de vie : un sous-processus stdio planté doit être relancé, une connexion HTTP coupée doit se reconnecter + re-faire OAuth, in-process n'a pas de couche transport. En stockant les infos transport dans la recipe, RmcpClient peut reconstruire le transport selon la recipe après une coupure.

Les champs supports_parallel_tool_calls / default_tools_approval_mode / tool_approval_modes de McpServerMetadata sont des configs par server lues depuis config — certains MCP servers ne supportent pas les appels concurrents (beaucoup dans codex apps), certains outils doivent être approuvés séparément. tool_approval_mode(tool_name) priorise par nom d'outil, puis retombe au défaut du server, puis au défaut global.

Le flux d'approval de l'outil MCP codex utilise elicitation : quand codex doit en interne exécuter une commande ou appliquer un patch, il ne l'exécute pas directement, il envoie elicitation/create au MCP host (par ex. l'UI Claude Desktop) pour qu'il ouvre une confirmation. ExecApprovalElicitRequestParams embarque codex_command / codex_cwd / codex_parsed_cmd, et le client render une UI de confirmation à partir de ces champs.

McpCatalogBuilder utilise RegistrationPrecedence pour résoudre les conflits de même nom de server : config > plugin > compatibility > extension, ce dernier ne fait que combler les trous du précédent. McpServerConflictAction décide, en cas de conflit, entre disable et override.

Fichiers clés

codex-rs/mcp-server/src/lib.rs:59-203run_main lance trois task (stdin reader / processor / stdout writer), EOF déclenche un shutdown en cascade.codex-rs/mcp-server/src/message_processor.rs:41-130MessageProcessor détient ThreadManager, dispatche les méthodes MCP standard.codex-rs/mcp-server/src/codex_tool_config.rs:25-101CodexToolCallParam, champs configurables quand un client appelle l'outil codex (prompt / model / cwd / approval / sandbox / config).codex-rs/mcp-server/src/codex_tool_runner.rs:57-141run_codex_tool_session, lance une session codex et soumet le prompt initial.codex-rs/mcp-server/src/exec_approval.rs:20-48ExecApprovalElicitRequestParams, paramètres d'elicitation pour l'approval exec.codex-rs/rmcp-client/src/rmcp_client.rs:326-389 — trois constructeurs de transport de RmcpClient.codex-rs/codex-mcp/src/connection_manager.rs:116-124 — champs de McpConnectionManager, porte clients / metadata / required_servers / elicitation_requests.codex-rs/codex-mcp/src/connection_manager.rs:848-883 — implémentation call_tool, tool_filter refuse les outils disabled.codex-rs/codex-mcp/src/catalog.rs:79-170McpServerRegistration et McpCatalogBuilder, gèrent les conflits config / plugin / extension.codex-rs/codex-mcp/src/server.rs:15-94EffectiveMcpServer et McpServerMetadata, stratégie de lancement runtime et approval par outil.codex-rs/cli/src/mcp_cmd.rs:45-105 — sous-commandes McpCli et AddMcpTransportArgs.

Les trois task de mcp-server sont explicitement séparés stdin / processor / stdout, un server line-delimited JSON-RPC classique. L'EOF fait drop incoming_tx, le processor reçoit None et sort, le writer stdout suit :

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 utilise le request id MCP comme sub_id codex, pour que les événements codex suivants (ExecApproval, PatchApproval) puissent être rattachés à la requête tools/call d'origine. Les chemins d'échec (start_thread, submit) envoient explicitement un 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 passe d'abord par tool_filter.allows(tool) avant de router vers un client ; un outil disabled renvoie une erreur directe. tool_timeout est une config par server, pour éviter qu'un MCP server lent ne bloque tout un 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}`"))?;
    // ...
}

Flux de données

Limites et échecs

  • tool filter refuse les outils disabled : la première ligne de call_tool vérifie client.tool_filter.allows(tool) et renvoie une erreur directe, sans entrer dans le client (codex-rs/codex-mcp/src/connection_manager.rs:855-860).
  • start_thread échoue => erreur remontée : run_codex_tool_session envoie CallToolResult::error et return en cas d'erreur de thread_manager.start_thread, sans submit (codex-rs/mcp-server/src/codex_tool_runner.rs:65-78).
  • Required server manquant => validation en échec : validate_required_servers s'exécute au démarrage et remonte une erreur en cas de manque ; mais wait_for_server_ready laisse un timeout tampon, un server à démarrage lent n'est pas déclaré mort immédiatement (codex-rs/codex-mcp/src/connection_manager.rs:399-485).
  • Identifiants OAuth stockés séparément : OAuthCredentialsStoreMode et AuthKeyringBackendKind de rmcp-client permettent au token OAuth d'aller dans le keyring ou en fichier ; delete_oauth_tokens est l'entrée de la sous-commande logout (codex-rs/cli/src/mcp_cmd.rs:168-172).
  • McpServerConflictAction gère les conflits de même nom : quand config et plugin enregistrent un même server, la precedence décide du gagnant ; disable est le mode de résolution d'un conflit mou (codex-rs/codex-mcp/src/catalog.rs:170-184).

Récapitulatif

L'intégration MCP a deux bouts : rmcp-client + codex-mcp agrègent les outils des MCP servers externes pour un usage interne à codex ; mcp-server expose codex lui-même comme MCP server pour un host externe. Les deux parts partagent le SDK rmcp, mais leur runtime et leur cycle de vie sont indépendants. McpConnectionManager est le cœur côté client ; run_codex_tool_session est l'entrée d'appel d'outil côté server. Pour voir comment une session codex lancée fait tourner la boucle modèle, enchaînez sur Boucle principale de l'Agent ; pour voir comment un MCP server s'attache au processus app-server, voir Architecture App-server.