MCP-Integration
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
McpConnectionManagerhält mehrereRmcpClientund bietet Aggregat-Schnittstellen wielist_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).RmcpClientunterstützt drei Transporte:new_in_process_client(Erweiterung prozessintern),new_stdio_client(Kindprozess),new_streamable_http_client(Remote-HTTP + OAuth); alle nutzen die AbstraktionTransportRecipe(codex-rs/rmcp-client/src/rmcp_client.rs:326-389).- Der Crate
mcp-serververpackt codex als MCP-Server: stdin liest JSON-RPC;MessageProcessor::process_requestdispatcht Standard-MCP-Methoden wieInitializeRequest/ListToolsRequest/CallToolRequest; intern startetThreadManagerdie codex-Sitzung (codex-rs/mcp-server/src/lib.rs:59-172). - Der Einstieg des
codex-MCP-Tool-Aufrufsrun_codex_tool_sessionstartetthread_manager.start_thread, reicht den Prompt alsOp::UserInputein und verknüpft mitsub_idden MCP-tools/call-Request mit den codex-Ereignissen (codex-rs/mcp-server/src/codex_tool_runner.rs:57-141). McpCli-Subkommandos (list/get/add/remove/login/logout) verwalten die MCP-Serverkonfiguration in~/.codex/config.toml; OAuth-Login läuft überperform_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-203 — run_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-130 — MessageProcessor hält ThreadManager und dispatcht Standard-MCP-Methoden.codex-rs/mcp-server/src/codex_tool_config.rs:25-101 — CodexToolCallParam; 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-141 — run_codex_tool_session; startet die codex-Sitzung und reicht den initialen Prompt ein.codex-rs/mcp-server/src/exec_approval.rs:20-48 — ExecApprovalElicitRequestParams; 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-170 — McpServerRegistration und McpCatalogBuilder; behandeln Konflikte aus config / plugin / extension.codex-rs/codex-mcp/src/server.rs:15-94 — EffectiveMcpServer und McpServerMetadata; Launch-Strategie und per-tool-Approval zur Laufzeit.codex-rs/cli/src/mcp_cmd.rs:45-105 — McpCli-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:
// 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:
// 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:
// 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_toolprüft in der ersten Zeileclient.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_sessionsendet bei Fehler vonthread_manager.start_threadeinCallToolResult::errorund kehrt zurück, ohne zu submitten (codex-rs/mcp-server/src/codex_tool_runner.rs:65-78).- Fehlende required-Server schlagen fehl:
validate_required_serversläuft beim Start; fehlt einer, wird direkt ein Fehler gemeldet; aberwait_for_server_readygibt 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:
OAuthCredentialsStoreModeundAuthKeyringBackendKindvonrmcp-clienterlauben OAuth-Token via Keyring oder Datei;delete_oauth_tokensist der Einstieg deslogout-Subcommands (codex-rs/cli/src/mcp_cmd.rs:168-172). McpServerConflictActionbei gleichnamigem Konflikt: Registrieren config und plugin denselben Server, entscheidet precedence, wer gewinnt;disableist 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.