Codex · EN · Part 3
Ch 12 MCP Integration
12-ch-12-MCP-Integration.md
第12章 MCP Integration
MCP Protocol Overview
The Model Context Protocol is an open standard that defines how AI clients communicate with tool providers. Think of it as USB for AI tools – a standardized connector that lets any client plug into any tool server. The protocol specifies three core capabilities:
Tools are executable functions that the server exposes. Each tool has a name, description, and JSON Schema defining its parameters. When the LLM needs to perform an action, it receives a list of available tools and can invoke them by name with arguments.
Resources are readable data that the server provides. Unlike tools, which are actions, resources are static or dynamic data – files, database records, API responses. Resources are identified by URIs and can include metadata like MIME types.
Prompts are reusable prompt templates that the server exposes. These are pre-written prompts that users or agents can invoke to get consistent starting points for complex tasks.
🤿 深入底层:MCP uses JSON-RPC 2.0 for all communication. Messages are serialized as JSON and transported over stdio (for local servers) or HTTP/WebSocket (for remote servers). Each message has an
idfield for request/response correlation and amethodfield indicating the operation.
Why does MCP exist? Without a standard protocol, each AI tool would need its own integration. You'd need separate SDKs for database access, file operations, and API calls. MCP unifies this: a single protocol that works across all tool providers, from local scripts to cloud services. The protocol is transport-agnostic, supports both synchronous and streaming operations, and includes built-in capabilities for discovery, authentication, and progress reporting.
MCP Client Implementation
Codex CLI acts as an MCP client through the McpConnectionManager, which owns one RmcpClient per configured server. The connection lifecycle begins when Codex starts up: it reads the mcp_servers section from config.toml, initializes connections to enabled servers, and discovers their capabilities.
# Example MCP server configuration in config.toml
[mcp_servers.postgres]
transport = { type = "stdio", command = "npx", args = ["@modelcontextprotocol/server-postgres"] }
enabled = true
startup_timeout_sec = 30
[mcp_servers.github]
transport = { type = "streamable_http", url = "https://api.github.com/mcp/" }
enabled = true
The connection flow follows a sequence of handshakes and discoveries:
Rendering diagram...
When a server connection is established, Codex sends an initialize request with its client information and capabilities. The server responds with its own capabilities, declaring which features it supports (tools, resources, prompts, elicitation, etc.). After initialization, Codex queries the server for its available tools using tools/list, which returns an array of tool definitions with names, descriptions, and JSON Schema parameters.
☕ 茶水间时间:The
McpConnectionManageruses a clever optimization for ChatGPT's built-in apps MCP server – it caches the tool list on disk at~/.codex/cache/codex_apps_tools/. Since ChatGPT apps tools change rarely, this cache dramatically speeds up subsequent starts while still allowing manual refresh viahard_refresh_codex_apps_tools_cache().
Error handling in MCP connections is multi-layered. Connection failures during startup are collected into an McpStartupCompleteEvent that lists which servers succeeded, failed, or were cancelled. Individual tool call failures return error responses to the LLM with context about what went wrong. The manager supports reconnection through cancellation tokens – if a connection drops, the async task can be restarted without recreating the entire manager.
The AsyncManagedClient wrapper around each RmcpClient provides a startup snapshot capability. While the full connection is initializing (which can take up to 30 seconds for remote servers), the client returns a cached snapshot of tools from the previous session. This allows Codex to start immediately with stale tools while fresh ones load in the background – a progressive enhancement pattern that prioritizes responsiveness over completeness.
MCP Tools in Codex CLI
MCP tools don't exist in isolation – they must be registered in Codex's ToolRegistry to be exposed to the LLM. The registration process transforms MCP tool definitions into Codex's internal ToolSpec format, translating schemas and adding metadata.
The ToolRegistry at codex-rs/core/src/tools/registry.rs maintains a map of tool handlers keyed by qualified name. For MCP tools, the qualified name includes both the server name and the tool name, joined by __ delimiter. A tool query from server postgres becomes mcp__postgres__query – namespacing that prevents collisions when multiple servers expose tools with the same name.
// From codex-rs/codex-mcp/src/mcp_connection_manager.rs:138-182
fn qualify_tools<I>(tools: I) -> HashMap<String, ToolInfo>
where
I: IntoIterator<Item = ToolInfo>,
{
let mut qualified_tools = HashMap::new();
for tool in tools {
let qualified_name = if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
format!("mcp{}{}{}{}",
MCP_TOOL_NAME_DELIMITER, tool.server_name,
MCP_TOOL_NAME_DELIMITER, tool.tool_name)
} else {
format!("{}{}", tool.tool_namespace, tool.tool_name)
};
// Sanitize to match OpenAI tool name requirements
let sanitized = sanitize_responses_api_tool_name(&qualified_name);
qualified_tools.insert(sanitized, tool);
}
qualified_tools
}
Schema translation is critical because MCP uses JSON Schema Draft 2020-12, while Codex's Responses API expects a slightly different format. The ToolInfo struct captures the raw MCP tool definition, which is then converted to Codex's ToolSpec format when building the tools payload for the LLM.
When the LLM calls an MCP tool, the invocation flows through McpHandler:
// From codex-rs/core/src/tools/handlers/mcp.rs:12-56
pub struct McpHandler;
impl ToolHandler for McpHandler {
type Output = CallToolResult;
fn kind(&self) -> ToolKind {
ToolKind::Mcp
}
async fn handle(&self, invocation: ToolInvocation)
-> Result<Self::Output, FunctionCallError>
{
let ToolInvocation { session, turn, call_id, payload, .. } = invocation;
let (server, tool, raw_arguments) = match payload {
ToolPayload::Mcp { server, tool, raw_arguments }
=> (server, tool, raw_arguments),
_ => return Err(FunctionCallError::RespondToModel(
"mcp handler received unsupported payload".to_string()
)),
};
handle_mcp_tool_call(Arc::clone(&session), &turn,
call_id, server, tool, raw_arguments).await
}
}
The handler extracts the server and tool name from the payload, then delegates to handle_mcp_tool_call, which looks up the McpConnectionManager, calls the appropriate server via call_tool, and formats the result for the LLM. Results are transformed from MCP's CallToolResult format into Codex's ResponseInputItem format, with content blocks mapped appropriately.
🤿 深入底层:The approval flow for MCP tools respects the
approval_policysetting. When a skill requires an MCP server that's not authenticated, Codex can prompt the user to log in viacodex mcp login <server>. This seamless integration means MCP dependencies don't break the interactive experience – users get guided through setup when needed.
Codex CLI as MCP Server
Codex CLI doesn't just consume MCP – it can also act as an MCP server, exposing its own capabilities to other MCP clients. This is useful when you want to control Codex programmatically from another tool, or when building multi-agent systems where Codex is one component among many.
Running Codex as an MCP server:
# Start Codex in MCP server mode
codex mcp-server
# From another process, connect via stdio
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}' | codex mcp-server
The server implementation in codex-rs/mcp-server/ follows the standard MCP server pattern: read JSON-RPC messages from stdin, process them, and write responses to stdout. The main loop in run_main spawns three tasks:
- stdin_reader: Reads lines from stdin and deserializes them as
IncomingMessage - processor: Handles requests, responses, and notifications via
MessageProcessor - stdout_writer: Serializes outgoing messages and writes them to stdout with newlines
The MessageProcessor dispatches incoming requests to handlers based on method name. For tools/list, it returns two custom tools:
// From codex-rs/mcp-server/src/message_processor.rs:314-330
async fn handle_list_tools(&self, id: RequestId,
params: Option<rmcp::model::PaginatedRequestParams>)
{
let result = rmcp::model::ListToolsResult {
meta: None,
tools: vec![
create_tool_for_codex_tool_call_param(),
create_tool_for_codex_tool_call_reply_param(),
],
next_cursor: None,
};
self.outgoing.send_response(id, result).await;
}
The codex tool accepts a prompt parameter and creates a new Codex session. The codex-reply tool accepts both thread_id and prompt parameters, continuing an existing conversation. This dual-interface design supports both one-shot queries and multi-turn conversations from MCP clients.
When a client calls tools/call with the codex tool, the server spawns an async task that runs the full Codex session via ThreadManager. Session events (items, turn completions, errors) are streamed back to the client as JSON-RPC notifications, enabling real-time progress updates. The running_requests_id_to_codex_uuid map tracks which MCP request IDs correspond to which Codex thread IDs, allowing cancellation via notifications/cancelled.
⚠️ 避坑指南:When running Codex as an MCP server, remember that it bypasses the TUI entirely. All interaction happens through JSON-RPC messages. Ensure your MCP client handles the streaming notifications properly – otherwise you'll miss session updates and approval prompts.
The server capabilities advertised during initialization include tools.list_changed = true, indicating that the available tools can change dynamically (though Codex's tools are currently static). Future extensions could add resources (exposing Codex's state, threads, or memories) or prompts (pre-built conversation templates), but the current implementation focuses on tool-based interaction.
Chapter Exercises
-
Connect to a public MCP server: Find a community MCP server (like the filesystem or git servers), configure it in your
config.toml, and verify connection withcodex exec "list all available tools". -
Implement a simple MCP tool: Create a minimal MCP server in Python or TypeScript that exposes one tool (e.g.,
get_weather). Connect Codex to it and invoke the tool through natural language. -
Run Codex CLI as an MCP server: Start
codex mcp-serverin one terminal. In another, use a JSON-RPC client to initialize the connection, list tools, and call thecodextool with a prompt.
With core components understood – TUI, app-server protocol, sandboxing, and MCP integration – Part IV explores advanced topics. The next chapter examines building custom tools that extend Codex's capabilities beyond its built-in set.