Skip to content

MCP-Integration

源码版本rust-v0.145.0

Die MCP-Integration (Model Context Protocol) von codex arbeitet bidirektional: nach innen verbindet rmcp-client die vom Nutzer konfigurierten MCP-Server (stdio / streamable HTTP / in-process) und exponiert deren Werkzeuge, Ressourcen und Prompts an das Modell; nach außen verpackt der Crate mcp-server codex selbst als MCP-Server, sodass andere MCP-Hosts (z. B. Claude Desktop) codex als Werkzeug aufrufen können. codex-mcp ist die Laufzeit dazwischen und verwaltet Verbindungslebenszyklus, Tool-Katalog und Elicitation-Routing.

Verantwortlichkeiten

  1. McpConnectionManager hält mehrere RmcpClient und bietet Aggregat-Schnittstellen wie list_all_tools / call_tool / list_all_resources; tool filter, approval mode und tool timeout werden hier verwaltet (codex-rs/codex-mcp/src/connection_manager.rs:116-124).
  2. RmcpClient unterstützt drei Transporte: new_in_process_client (Erweiterung prozessintern), new_stdio_client (Kindprozess), new_streamable_http_client (Remote-HTTP + OAuth); alle nutzen die Abstraktion TransportRecipe (codex-rs/rmcp-client/src/rmcp_client.rs:326-389).
  3. Der Crate mcp-server verpackt codex als MCP-Server: stdin liest JSON-RPC; MessageProcessor::process_request dispatcht Standard-MCP-Methoden wie InitializeRequest / ListToolsRequest / CallToolRequest; intern startet ThreadManager die codex-Sitzung (codex-rs/mcp-server/src/lib.rs:59-172).
  4. Der Einstieg des codex-MCP-Tool-Aufrufs run_codex_tool_session startet thread_manager.start_thread, reicht den Prompt als Op::UserInput ein und verknüpft mit sub_id den MCP-tools/call-Request mit den codex-Ereignissen (codex-rs/mcp-server/src/codex_tool_runner.rs:57-141).
  5. McpCli-Subkommandos (list / get / add / remove / login / logout) verwalten die MCP-Serverkonfiguration in ~/.codex/config.toml; OAuth-Login läuft über perform_oauth_login (codex-rs/cli/src/mcp_cmd.rs:45-61).

Entwurfsbeweggründe

codex teilt MCP in zwei Crates auf, weil Server und Client völlig unterschiedliche Rollen haben: Der Client (rmcp-client + codex-mcp) läuft innerhalb des codex-Prozesses und exponiert die Werkzeuge externer MCP-Server an das Modell; der Server (mcp-server) exponiert codex selbst an andere MCP-Hosts. Beide teilen sich das rmcp-SDK, hängen aber zur Laufzeit nicht voneinander ab, sodass der Server-Crate unabhängig kompilieren kann und der Client-Crate nicht durch die ThreadManager-Startlogik des Servers verschmutzt wird.

Die Abstraktion TransportRecipe existiert, weil die Verbindungswiederherstellung und Lebenszyklusverwaltung von stdio / streamable HTTP / in-process grundverschieden sind: Ein stdio-Kindprozess, der abstürzt, muss neu gestartet werden; eine abgebrochene HTTP-Verbindung muss neu verbinden und OAuth erneut senden; in-process hat keine Transportschicht. Mit der Transportinformation in der recipe kann RmcpClient nach Verbindungsabbruch den Transport gemäß recipe neu aufbauen.

supports_parallel_tool_calls / default_tools_approval_mode / tool_approval_modes in McpServerMetadata sind per-Server-Konfiguration aus der Config — manche MCP-Server unterstützen keine nebenläufigen Aufrufe (viele codex-apps), manche Werkzeuge brauchen eigene Approval. tool_approval_mode(tool_name) priorisiert nach Tool-Name, fällt auf den Server-Standard zurück und zuletzt auf den globalen Standard.

Der Approval-Flow des codex-MCP-Tools nutzt Elicitation: Wenn codex intern einen Befehl ausführen oder einen Patch anwenden will, führt es das nicht direkt aus, sondern sendet elicitation/create an den MCP-Host (etwa die Claude-Desktop-UI), sodass der Host einen Bestätigungsdialog anzeigt. ExecApprovalElicitRequestParams trägt codex_command / codex_cwd / codex_parsed_cmd; der Client rendert daraus die Bestätigungs-UI.

McpCatalogBuilder löst Namenskonflikte gleichen Servernamens über RegistrationPrecedence: config vor plugin vor compatibility vor extension; letztere können nur Lücken der vorherigen füllen. McpServerConflictAction entscheidet bei Konflikt zwischen disable und überschreiben.

Wichtige Dateien

codex-rs/mcp-server/src/lib.rs:59-203run_main startet stdin reader / processor / stdout writer als drei Tasks; EOF löst kaskadierendes Schließen aus.codex-rs/mcp-server/src/message_processor.rs:41-130MessageProcessor hält ThreadManager und dispatcht Standard-MCP-Methoden.codex-rs/mcp-server/src/codex_tool_config.rs:25-101CodexToolCallParam; konfigurierbare Felder, wenn der Client das codex-Tool aufruft (prompt / model / cwd / approval / sandbox / config).codex-rs/mcp-server/src/codex_tool_runner.rs:57-141run_codex_tool_session; startet die codex-Sitzung und reicht den initialen Prompt ein.codex-rs/mcp-server/src/exec_approval.rs:20-48ExecApprovalElicitRequestParams; elicitation-Parameter der exec-Approval.codex-rs/rmcp-client/src/rmcp_client.rs:326-389 — Drei Transport-Konstruktoren von RmcpClient.codex-rs/codex-mcp/src/connection_manager.rs:116-124 — Felder des McpConnectionManager: clients / metadata / required_servers / elicitation_requests.codex-rs/codex-mcp/src/connection_manager.rs:848-883 — Implementierung von call_tool; tool_filter lehnt deaktivierte Tools ab.codex-rs/codex-mcp/src/catalog.rs:79-170McpServerRegistration und McpCatalogBuilder; behandeln Konflikte aus config / plugin / extension.codex-rs/codex-mcp/src/server.rs:15-94EffectiveMcpServer und McpServerMetadata; Launch-Strategie und per-tool-Approval zur Laufzeit.codex-rs/cli/src/mcp_cmd.rs:45-105McpCli-Subkommandos und AddMcpTransportArgs.

Die drei Tasks des mcp-server sind explizit stdin / processor / stdout getrennt — ein typischer line-delimited JSON-RPC-Server. EOF löst drop von incoming_tx aus; der Processor erhält None und beendet; der stdout-Writer folgt:

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 nutzt die MCP-Request-ID als codex-sub_id, sodass nachfolgende codex-Ereignisse (ExecApproval, PatchApproval) auf den ursprünglichen tools/call-Request zurückgeführt werden können. Die Fehlerpfade (start_thread fehlgeschlagen, submit fehlgeschlagen) senden jeweils explizit 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 läuft vor dem Routing an den konkreten Client durch tool_filter.allows(tool); ein deaktiviertes Tool liefert direkt einen Fehler; tool_timeout ist per-Server konfiguriert, damit ein langsamer MCP-Server nicht den gesamten Turn blockiert:

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

Datenfluss

Grenzen und Fehler

  • tool filter lehnt deaktivierte Tools ab: call_tool prüft in der ersten Zeile client.tool_filter.allows(tool); bei Verstoß direkt Fehler, ohne den Client zu erreichen (codex-rs/codex-mcp/src/connection_manager.rs:855-860).
  • start_thread-Fehler liefert Fehler zurück: run_codex_tool_session sendet bei Fehler von thread_manager.start_thread ein CallToolResult::error und kehrt zurück, ohne zu submitten (codex-rs/mcp-server/src/codex_tool_runner.rs:65-78).
  • Fehlende required-Server schlagen fehl: validate_required_servers läuft beim Start; fehlt einer, wird direkt ein Fehler gemeldet; aber wait_for_server_ready gibt ein Timeout-Puffer, sodass ein langsam startender Server nicht sofort als tot gilt (codex-rs/codex-mcp/src/connection_manager.rs:399-485).
  • OAuth-Credentials separat gespeichert: OAuthCredentialsStoreMode und AuthKeyringBackendKind von rmcp-client erlauben OAuth-Token via Keyring oder Datei; delete_oauth_tokens ist der Einstieg des logout-Subcommands (codex-rs/cli/src/mcp_cmd.rs:168-172).
  • McpServerConflictAction bei gleichnamigem Konflikt: Registrieren config und plugin denselben Server, entscheidet precedence, wer gewinnt; disable ist der weiche Konfliktfall (codex-rs/codex-mcp/src/catalog.rs:170-184).

Zusammenfassung

Die MCP-Integration hat zwei Seiten: rmcp-client + codex-mcp aggregieren die Werkzeuge externer MCP-Server für den internen Gebrauch; mcp-server exponiert codex selbst als MCP-Server nach außen. Beide basieren auf dem rmcp-SDK, haben aber unabhängige Laufzeit und Lifecycle. McpConnectionManager ist der Kern der Client-Seite; run_codex_tool_session ist der Tool-Call-Einstieg der Server-Seite. Wie die codex-Sitzung nach dem Start die Modell-Schleife treibt, siehe Agent-Hauptschleife; wie ein MCP-Server unter dem app-server-Prozess hängt, siehe App-server-Architektur.