Skip to content

MCP Server

An AI coding agent is typically the host — it connects to LLM providers, invokes tools, and drives the conversation. But there is a compelling second mode: the agent as a server in the Model Context Protocol (MCP), exposing its capabilities (code editing, shell execution, repo analysis) as tools that a different host can call. This enables chaining agents, embedding the agent inside an IDE’s MCP-aware assistant, or building multi-agent workflows where a coordinator delegates coding tasks to a specialized agent.

The hard problem is lifecycle management. A coding agent session is stateful: it has a working directory, a conversation history, sandbox permissions, and approval gates that require human input. Exposing this over a protocol that expects stateless tool calls means the server must manage threads, stream events asynchronously, and reverse the approval flow — sending requests to the client when human input is needed, then blocking the agent loop until the response arrives. For MCP client-side connection/auth/tool-discovery internals, see MCP Client. For HTTP-layer MCP management in client-server architectures, see MCP Integration.


Repository: references/aider/ | Pinned SHA: b9050e1d

Aider has no MCP server capability. There are no MCP dependencies in requirements.in, no server-mode CLI flag, and no code for exposing Aider’s functionality over any protocol. Aider is strictly a terminal-interactive tool or a one-shot CLI (--message flag). It cannot be embedded in another system as a tool provider.


Repository: references/codex/codex-rs/ | Pinned SHA: 4ab44e2c5

Codex has the only full MCP server implementation among the three references. The server exposes the entire agent as two MCP tools over stdio, with async event streaming and a reverse-request pattern for approval gates. The implementation spans a dedicated mcp-server crate with ~1,400 lines of Rust.

File: cli/src/main.rs:99-100, 589-590

// CLI subcommand registration
McpServer,
// Dispatch
Some(Subcommand::McpServer) => {
codex_mcp_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?;
}

Invoked via codex mcp-server. The server reads JSON-RPC 2.0 from stdin and writes to stdout, following the MCP stdio transport spec.

File: mcp-server/src/lib.rs:51-154

The server runs as four concurrent tokio tasks:

  1. Stdin reader (lines 67-87): Reads line-delimited JSON-RPC messages, parses each as IncomingMessage, pushes to a bounded mpsc channel (capacity 128). Exits on EOF.

  2. Message processor (lines 104-123): Core routing loop. Receives from the incoming channel, dispatches to handler functions based on JSON-RPC method. Long-running operations (tool calls) are spawned as background tasks to avoid blocking the message loop.

  3. Stdout writer (lines 126-146): Receives from an unbounded outgoing channel, serializes each message as JSON with a newline delimiter, writes to stdout.

  4. Background tool runners: Not a fixed task pool — spawned on-demand via tokio::spawn() for each tool call. Each runner drives a Codex session event loop and sends events back through the outgoing channel.

stdin → BufReader → serde_json::from_str → IncomingMessage
incoming_tx (bounded mpsc, capacity 128)
MessageProcessor → route to handler
↓ (spawns background tasks)
codex_tool_runner → event loop
outgoing_tx (unbounded mpsc) → stdout writer
JSON + "\n" → stdout

File: mcp-server/src/message_processor.rs:183-269

On receiving initialize, the server responds with:

  • name: "codex-mcp-server"
  • version: env!("CARGO_PKG_VERSION")
  • Capabilities: tools: Some(ToolsCapability { list_changed: Some(true) })
  • All other capabilities (resources, prompts, logging) default to None

A custom user_agent field is included via get_codex_user_agent(), built from the client’s client_info.name and client_info.version. Only one initialize call is allowed per connection; a second returns an error.

File: mcp-server/src/message_processor.rs:304-320, mcp-server/src/codex_tool_config.rs

The server exposes exactly two tools:

Schema: Auto-generated from Rust struct via schemars (draft2019-09).

Input parameters (codex_tool_config.rs:19-64):

pub struct CodexToolCallParam {
pub prompt: String, // Required: initial user prompt
pub model: Option<String>, // e.g., "gpt-5.2-codex"
pub profile: Option<String>, // Config profile name
pub cwd: Option<String>, // Working directory
pub approval_policy: Option<CodexToolCallApprovalPolicy>,
pub sandbox: Option<CodexToolCallSandboxMode>,
pub config: Option<HashMap<String, serde_json::Value>>,
pub base_instructions: Option<String>,
pub developer_instructions: Option<String>,
pub compact_prompt: Option<String>,
}

Output (codex_tool_config.rs:136-149):

{
"type": "object",
"properties": {
"threadId": { "type": "string" },
"content": { "type": "string" }
},
"required": ["threadId", "content"]
}

The threadId in the response is critical — it’s how the client resumes the session with follow-up messages.

Tool 2: codex-reply — Continue an Existing Session

Section titled “Tool 2: codex-reply — Continue an Existing Session”

Takes a threadId and a new prompt. Looks up the existing CodexThread via the ThreadManager and sends a follow-up Op::UserInput.

File: mcp-server/src/message_processor.rs:346-516

When a tools/call request arrives:

  1. Parse arguments as CodexToolCallParam (for codex) or CodexToolCallReplyParam (for codex-reply)
  2. Convert to Config via tool_cfg.into_config()
  3. Spawn a background task via tokio::spawn():
    • run_codex_tool_session() calls thread_manager.start_thread(config) → returns NewThread { thread_id, thread, session_configured }
    • Sends SessionConfigured event as an MCP notification
    • Stores the mapping request_id → thread_id for cancellation
    • Submits Op::UserInput with the prompt
    • Enters the event loop

The key insight: the tool call response is not returned immediately. The background task streams events as notifications and only sends the tool call response when the session completes (or errors).

File: mcp-server/src/codex_tool_runner.rs:191-393

The core of the server is the event loop inside each background task:

loop {
match thread.next_event().await {
Ok(event) => {
// 1. Forward every event as an MCP notification
outgoing.send_event_as_notification(&event, meta).await;
// 2. Handle specific event types
match event.msg {
EventMsg::ExecApprovalRequest(..) => {
handle_exec_approval_request(..).await;
continue; // Don't break — wait for approval response
},
EventMsg::ApplyPatchApprovalRequest(..) => {
handle_patch_approval_request(..).await;
continue;
},
EventMsg::TurnComplete(TurnCompleteEvent { last_agent_message }) => {
let result = create_call_tool_result_with_thread_id(thread_id, text, None);
outgoing.send_response(request_id, result).await;
break; // Session complete
},
EventMsg::Error(err_event) => {
outgoing.send_response(request_id, error_result).await;
break;
},
_ => continue, // Other events silently forwarded
}
},
Err(e) => {
outgoing.send_response(request_id, error_result).await;
break;
}
}
}

All events are streamed to the client as MCP notifications before the server processes them. This means the client can render real-time progress (file edits, command output, thinking tokens) while the session runs.

File: mcp-server/src/outgoing_message.rs:200-230

Events are sent as JSON-RPC notifications with method "codex/event":

{
"jsonrpc": "2.0",
"method": "codex/event",
"params": {
"_meta": {
"requestId": "3",
"threadId": "uuid-..."
},
"id": "event-uuid",
"msg": { "type": "turnComplete", "lastAgentMessage": "..." }
}
}

The _meta.requestId field correlates events back to the original tools/call request, allowing the client to associate events with the correct tool invocation.

The most architecturally interesting feature is the approval flow reversal. When the Codex agent needs human approval (to run a command or apply a patch), the MCP server sends a JSON-RPC request to the client — reversing the normal client→server direction.

File: mcp-server/src/exec_approval.rs:20-110

pub struct ExecApprovalElicitRequestParams {
pub message: String, // "Allow Codex to run `command` in `/cwd`?"
pub requested_schema: Value, // {"type":"object","properties":{}}
pub thread_id: ThreadId,
pub codex_elicitation: String, // "exec-approval"
pub codex_mcp_tool_call_id: String,
pub codex_event_id: String,
pub codex_call_id: String,
pub codex_command: Vec<String>, // Command argv
pub codex_cwd: PathBuf,
pub codex_parsed_cmd: Vec<ParsedCommand>,
}

The server sends this as method: "elicitation/create". The client (IDE, coordinator agent, or human operator) must respond with { "decision": "allowed" | "denied" }.

The flow:

  1. Server sends elicitation/create request with params
  2. send_request() creates a oneshot channel and stores a callback in request_id_to_callback
  3. Client receives the request, makes a decision, responds
  4. Response triggers on_exec_approval_response() in a background task
  5. Background task deserializes the response and calls codex.submit(Op::ExecApproval { id, decision })
  6. The Codex agent loop resumes with the approval decision

File: mcp-server/src/patch_approval.rs:20-101

Identical pattern to exec approval, but for code changes:

pub struct PatchApprovalElicitRequestParams {
pub message: String, // "Allow Codex to apply proposed code changes?"
pub codex_elicitation: String, // "patch-approval"
pub codex_reason: Option<String>,
pub codex_grant_root: Option<PathBuf>,
pub codex_changes: HashMap<PathBuf, FileChange>,
// ... standard fields
}

The codex_changes field contains the full diff per file, allowing the client to render a review UI.

Sessions are identified by ThreadId (a UUID). The ThreadManager (initialized with SessionSource::Mcp to indicate MCP context) keeps threads alive across tool calls. The client receives the threadId in the first tool call response and passes it back via codex-reply for follow-up messages. Thread lifetime: until the process exits or the thread is explicitly closed.

codex-rs/mcp-server/
├── src/
│ ├── main.rs (7 lines) Entry point
│ ├── lib.rs (155 lines) Server runtime, task coordination
│ ├── message_processor.rs (600 lines) MCP protocol routing
│ ├── codex_tool_runner.rs (413 lines) Session event loop
│ ├── codex_tool_config.rs (150+ lines) Tool schema definitions
│ ├── outgoing_message.rs (330+ lines) JSON-RPC message formatting
│ ├── patch_approval.rs (143 lines) Patch approval elicitation
│ ├── exec_approval.rs (148 lines) Exec approval elicitation
│ └── tool_handlers/mod.rs (65 bytes) Module stub
└── Cargo.toml Dependencies: rmcp, tokio, serde_json, tracing

Repository: references/opencode/packages/opencode/src/ | Pinned SHA: 7ed449974

OpenCode does not expose an MCP server interface. It is exclusively an MCP client (see MCP Client). However, OpenCode does expose a comprehensive HTTP/SSE/WebSocket server that serves a purpose similar to what an MCP server would — it allows external clients (TUI, web browser, programmatic tools) to drive AI sessions remotely.

File: server/server.ts:47-623

OpenCode runs a Hono HTTP server on Bun with:

  • Auto-negotiated port starting at 4096, falling back to OS-provided
  • Default binding to 127.0.0.1 (localhost only)
  • Optional mDNS service discovery (opencode.local)
  • Basic auth via OPENCODE_SERVER_PASSWORD environment variable
  • CORS whitelist for localhost, 127.0.0.1, Tauri origins, *.opencode.ai

The CLI exposes two modes:

  • opencode serve — Headless server, no browser
  • opencode web — Opens browser automatically

The server has 12 route modules spanning ~100 endpoints:

Route ModulePathPurpose
/sessionSession & chatCreate sessions, send messages (streaming), fork, revert, share
/mcpMCP managementAdd/remove MCP servers, OAuth flows, connect/disconnect
/ptyTerminalCreate PTY sessions, WebSocket streaming
/providerAI providersList providers, OAuth for provider auth
/permissionApproval UXList pending permissions, reply (once/always/reject)
/questionUser questionsList pending questions from agent, reply/reject
/fileFile operationsSearch, read, status (ripgrep, glob)
/configConfigurationGet/set instance config
/projectProject metadataList projects, get current
/globalSystem stateHealth check, global events, dispose
/tuiTUI controlAppend/submit prompts, execute commands
/experimentalTools & worktreesList tools with schemas, manage git worktrees

SSE endpoint (server.ts:486-541):

GET /event → text/event-stream

Returns all business events (session updates, permission requests, etc.) with 10-second heartbeat. This is how the TUI and web client receive real-time updates.

Message streaming (server/routes/session.ts):

POST /session/:sessionID/message → application/json (streamed)

Streams the assistant’s response in real-time as the LLM generates it.

WebSocket (server/routes/pty.ts):

GET /pty/:ptyID/connect → WebSocket upgrade

Used exclusively for terminal (PTY) interaction — bidirectional character streaming.

File: server/routes/permission.ts

External clients handle approval prompts via HTTP:

GET /permission/ → List pending permission requests
POST /permission/:requestID/reply → { reply: "once"|"always"|"reject", message?: string }

This is the programmatic equivalent of the TUI approval modal. A coordinator agent or IDE integration can poll for pending permissions and respond programmatically.

OpenCode’s HTTP/SSE API is functionally richer than what MCP provides — it includes session management, permission handling, file operations, PTY streaming, project management, and worktree management. MCP’s tool-call abstraction would flatten all of this into generic tools/call requests, losing the typed API contracts and streaming semantics. OpenCode chose HTTP as its integration surface because it needed a full application protocol, not just a tool-call interface.


The approval flow reversal is the hardest part of MCP server mode. In normal operation, the agent asks the user for approval via the TUI. When running as an MCP server, “the user” is the MCP client — which might be another agent, an IDE, or a headless automation system. Codex solves this with the elicitation/create reverse-request pattern, but this requires the client to implement an elicitation/create handler, which is not standard MCP. Clients that don’t support elicitation can’t use Codex in any mode that requires approval.

Session statefulness conflicts with MCP’s tool-call model. MCP tools are designed to be stateless: call a tool, get a result. A coding agent session is deeply stateful — the LLM has context, files are modified, git state changes. Codex bridges this with the threadId pattern, but the client must track these IDs and pass them correctly. A client that creates a new codex tool call for every message will waste context and create redundant sessions.

Event streaming is non-standard. Codex uses "codex/event" notifications to stream progress, which is a custom extension. Standard MCP clients may ignore these notifications, leaving the user with no feedback until the tool call completes. For long-running coding sessions, this is a poor experience.

The two-tool surface is deliberately narrow. Codex exposes only codex and codex-reply, not individual tools like “read file” or “search”. This is an intentional design: the Codex agent decides internally which tools to use, and the MCP client just describes the task. This prevents the client from micromanaging the agent’s tool usage, but it also means the client has no way to constrain the agent to specific operations (e.g., “only read files, don’t edit anything”) without using the approval_policy and sandbox parameters.

OpenCode’s HTTP approach trades MCP compatibility for expressiveness. The 100+ endpoint HTTP API is far more capable than what MCP supports, but it means OpenCode cannot be plugged into an MCP-aware IDE without a translation layer. As MCP adoption grows, this may become a limitation.


OpenOxide will support running as an MCP server via openoxide mcp-server, following Codex’s architecture closely but with improvements informed by both Codex’s and OpenCode’s approaches.

  • Primary: stdio (JSON-RPC 2.0, line-delimited) — required for MCP compatibility
  • Secondary (v2): Streamable HTTP transport for network-accessible server mode, following the MCP 2025-03-26 spec

Three tools instead of Codex’s two:

ToolPurposeInput
openoxideStart a new coding sessionprompt, model, cwd, sandbox, approval_policy, instructions
openoxide-replyContinue an existing sessionthread_id, prompt
openoxide-queryRead-only: ask a question about the codebase without making changesthread_id (optional), query, cwd

The third tool (openoxide-query) addresses the common case where a coordinator agent wants to understand code without authorizing any modifications. It runs with SandboxPolicy::ReadOnly and AskForApproval::Never, returning only text analysis.

  • openoxide-mcp-server: Server runtime. Depends on rmcp for MCP protocol types, tokio for async runtime, and openoxide-core for the agent loop. Follows the four-task architecture (stdin reader, message processor, stdout writer, background runners).
  • openoxide-mcp-protocol: Shared MCP types (Tool, Resource, notification formats). Separated from the server crate so the MCP client can reuse the types.

Use MCP’s notifications/message method (part of the sampling capability) rather than a custom codex/event method. This improves compatibility with standard MCP clients. For clients that support the logging capability, also emit progress as log messages.

Implement the reverse-request pattern using elicitation/create, following Codex. Additionally, support a fallback mode where the server auto-approves within the configured sandbox policy if the client does not implement elicitation/create. Detect this during initialization by checking the client’s declared capabilities.

pub enum ApprovalStrategy {
/// Client supports elicitation — send requests and wait for responses
Elicitation,
/// Client does not support elicitation — auto-approve within sandbox policy
AutoWithinPolicy,
/// Client does not support elicitation — deny all operations requiring approval
DenyOnApproval,
}

Sessions identified by ThreadId (UUID v7 for sortability). Thread state stored in the same JSONL format used by the TUI session manager. The openoxide-reply tool reuses the existing thread; the openoxide tool creates a new one. Threads are garbage-collected after 30 minutes of inactivity (configurable).

  1. Schema generation via schemars (Codex pattern). Auto-derive JSON Schema from Rust structs for tool input/output schemas.
  2. Background task spawning (Codex pattern). Tool calls spawn tokio::spawn() tasks; the message processor never blocks.
  3. Metadata correlation in notifications. Every event includes _meta.requestId and _meta.threadId so clients can correlate events to tool calls.
  4. Graceful shutdown: On stdin EOF, send SIGTERM to all running sessions, wait 2 seconds, then force-kill. Clean up JSONL session files on exit.