Skip to content

Integración MCP

源码版本rust-v0.145.0

La integración MCP (Model Context Protocol) de codex funciona en ambos sentidos: hacia dentro, vía rmcp-client, se conecta a los MCP servers que el usuario haya configurado (stdio / streamable HTTP / in-process), exponiendo al modelo las herramientas, recursos y prompts de esos servers; hacia fuera, el crate mcp-server envuelve al propio codex como un MCP server, de modo que otros hosts MCP (como Claude Desktop) pueden invocar a codex como herramienta. codex-mcp es el runtime intermedio, gestionando el ciclo de vida de las conexiones, el catálogo de herramientas y el enrutado de elicitation.

Responsabilidades

  1. McpConnectionManager sostiene varios RmcpClient y ofrece interfaces agregadas como list_all_tools / call_tool / list_all_resources; tool filter, approval mode y tool timeout se gestionan aquí (codex-rs/codex-mcp/src/connection_manager.rs:116-124).
  2. RmcpClient soporta tres transportes: new_in_process_client (extensión en proceso), new_stdio_client (subproceso), new_streamable_http_client (HTTP remoto + OAuth), unificados bajo la abstracción TransportRecipe (codex-rs/rmcp-client/src/rmcp_client.rs:326-389).
  3. El crate mcp-server envuelve a codex como MCP server: lee JSON-RPC de stdin, MessageProcessor::process_request dispatcha los métodos MCP estándar (InitializeRequest / ListToolsRequest / CallToolRequest, etc.) y por dentro usa ThreadManager para arrancar la sesión de codex (codex-rs/mcp-server/src/lib.rs:59-172).
  4. La entrada de tool call MCP run_codex_tool_session arranca thread_manager.start_thread, envía el prompt como Op::UserInput y luego usa sub_id para asociar la request tools/call de MCP con los eventos de codex (codex-rs/mcp-server/src/codex_tool_runner.rs:57-141).
  5. Los subcomandos McpCli (list / get / add / remove / login / logout) gestionan la configuración de MCP servers en ~/.codex/config.toml; el OAuth login va por perform_oauth_login (codex-rs/cli/src/mcp_cmd.rs:45-61).

Motivación de diseño

codex parte MCP en dos crates y no en uno, porque los roles de server y client son completamente distintos: el client (rmcp-client + codex-mcp) corre dentro del proceso codex y expone al modelo las herramientas de los MCP servers externos; el server (mcp-server) expone al propio codex a otros hosts MCP. Ambos comparten el SDK rmcp, pero sus runtimes no dependen entre sí, así que el crate server compila de forma independiente y el crate client no se contamina con la lógica de arranque del ThreadManager del server.

La abstracción TransportRecipe existe porque los tres transportes (stdio / streamable HTTP / in-process) tienen gestión de reconexión y ciclo de vida completamente distintos: si el subproceso stdio crashea, hay que reiniciarlo; si la conexión HTTP cae, hay que reconectar y volver a pedir OAuth; el in-process no tiene capa de transporte. Al guardar la información del transport en el recipe, RmcpClient puede reconstruir el transport según el recipe tras una desconexión.

McpServerMetadata con supports_parallel_tool_calls / default_tools_approval_mode / tool_approval_modes son configuraciones per-server leídas desde config: algunos MCP servers no soportan llamadas concurrentes (muchos en codex apps), y algunas herramientas requieren aprobación individual. tool_approval_mode(tool_name) prioriza por nombre de herramienta, luego cae al valor por defecto del server, y por último al global.

El flujo de approval del MCP tool codex usa elicitation: cuando codex por dentro necesita exec un comando o apply patch, no lo ejecuta directamente, sino que envía elicitation/create al host MCP (por ejemplo la UI de Claude Desktop) para que el host muestre un diálogo de confirmación. ExecApprovalElicitRequestParams lleva codex_command / codex_cwd / codex_parsed_cmd, y el client usa esos campos para renderizar la UI de confirmación.

McpCatalogBuilder resuelve conflictos de nombre entre servers con RegistrationPrecedence: config prevalece sobre plugin, sobre compatibility, sobre extension; las últimas solo pueden rellenar los huecos de las anteriores. McpServerConflictAction decide si ante conflicto se deshabilita o se sobrescribe.

Archivos clave

codex-rs/mcp-server/src/lib.rs:59-203run_main levanta tres tasks: stdin reader / processor / stdout writer; el EOF dispara el cierre en cascada.codex-rs/mcp-server/src/message_processor.rs:41-130MessageProcessor sostiene el ThreadManager y dispatcha los métodos MCP estándar.codex-rs/mcp-server/src/codex_tool_config.rs:25-101CodexToolCallParam, campos configurables cuando el client invoca el tool codex (prompt / model / cwd / approval / sandbox / config).codex-rs/mcp-server/src/codex_tool_runner.rs:57-141run_codex_tool_session, arranca la sesión de codex y envía el prompt inicial.codex-rs/mcp-server/src/exec_approval.rs:20-48ExecApprovalElicitRequestParams, parámetros de elicitation para la aprobación de exec.codex-rs/rmcp-client/src/rmcp_client.rs:326-389 — constructores de los tres transportes de RmcpClient.codex-rs/codex-mcp/src/connection_manager.rs:116-124 — campos de McpConnectionManager: clients / metadata / required_servers / elicitation_requests.codex-rs/codex-mcp/src/connection_manager.rs:848-883 — implementación de call_tool, el tool_filter rechaza las tools disabled.codex-rs/codex-mcp/src/catalog.rs:79-170McpServerRegistration y McpCatalogBuilder, resuelven conflictos entre orígenes config / plugin / extension.codex-rs/codex-mcp/src/server.rs:15-94EffectiveMcpServer y McpServerMetadata, estrategia de launch añadida en runtime y approval por tool.codex-rs/cli/src/mcp_cmd.rs:45-105 — subcomandos McpCli y AddMcpTransportArgs.

Las tres tasks de mcp-server son explícitamente stdin / processor / stdout separados, típico server JSON-RPC line-delimited. El EOF suelta incoming_tx, el processor recibe None y sale, y el stdout writer termina en cadena:

rust
// mcp-server/src/lib.rs:148-172 — bucle principal del 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 usa el MCP request id como sub_id de codex, de modo que los eventos posteriores (ExecApproval, PatchApproval) se puedan asociar a la request tools/call original. Las rutas de fallo (start_thread falla, submit falla) envían explícitamente CallToolResult::error:

rust
// mcp-server/src/codex_tool_runner.rs:98-119 — asociación por 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, antes de enrutar al client concreto, pasa por tool_filter.allows(tool); una tool disabled devuelve error directo. tool_timeout es por server, para evitar que un MCP server lento atasque todo el turn:

rust
// codex-mcp/src/connection_manager.rs:848-866 — ruta de 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}`"))?;
    // ...
}

Flujo de datos

Bordes y fallos

  • tool filter rechaza tools disabled: la primera línea de call_tool comprueba client.tool_filter.allows(tool) y devuelve error directo, sin entrar al client (codex-rs/codex-mcp/src/connection_manager.rs:855-860).
  • start_thread fallido devuelve error: run_codex_tool_session, si thread_manager.start_thread da error, envía CallToolResult::error y regresa, sin pasar a submit (codex-rs/mcp-server/src/codex_tool_runner.rs:65-78).
  • Falta un server requerido falla la validación: validate_required_servers corre al arranque y suelta error si falta; pero wait_for_server_ready deja un margen de timeout, así que un server de arranque lento no se descarta al instante (codex-rs/codex-mcp/src/connection_manager.rs:399-485).
  • Credenciales OAuth en almacenamiento separado: OAuthCredentialsStoreMode y AuthKeyringBackendKind de rmcp-client permiten que el token OAuth vaya por keyring o por archivo; delete_oauth_tokens es la entrada del subcomando logout (codex-rs/cli/src/mcp_cmd.rs:168-172).
  • McpServerConflictAction gestiona conflictos de mismo nombre: si config y plugin registran un server con el mismo nombre, la precedence decide quién gana; disable es la forma de tratar el conflicto blando (codex-rs/codex-mcp/src/catalog.rs:170-184).

Resumen

La integración MCP se parte en dos: rmcp-client + codex-mcp agregan a codex las herramientas de los MCP servers externos; mcp-server expone al propio codex como MCP server para que lo llame un host externo. Ambas comparten el SDK rmcp, pero con runtime y ciclo de vida independientes. McpConnectionManager es el núcleo del lado client; run_codex_tool_session, la entrada de tool call del lado server. Para ver cómo, tras arrancar la sesión de codex, corre el bucle del modelo, sigue por Bucle principal del Agent; para ver cómo los MCP servers se cuelgan del proceso app-server, consulta Arquitectura de App-server.