The Codex MCP-Server Pattern: Expose a Harness as One Coarse MCP Tool Running an Agent Underneath
Research date: Aug 8, 2026. Focus: the coarse approach to exposing a harness as a service — one MCP tool that runs a full agent session underneath. Reference implementation is OpenAI's codex mcp-server, read from the Apache-2.0 source (codex-rs/mcp-server/ in https://github.com/openai/codex) plus the official docs. Every claim is cited inline. Companion articles: shareable-agent-harness.md, session-harness-sandbox.md, agent-loop-pattern.md, multi-cli-orchestration.md.
TL;DR
Yes — codex effectively ships the whole framework for this use case, built and open source.
codex mcp-server is exactly the coarse pattern you described: a process that speaks MCP, exposing one tool (codex) that starts a fresh Codex agent session, runs the full agent loop over a cwd (sandboxed, approval-gated), and returns the final answer plus a threadId; and a second tool (codex-reply) that continues that same session by threadId. Stateless by default, stateful on demand. If your harness can be Codex, you don't build the MCP layer at all. If your harness is OpenCode-specific, you copy the same two-tool shape with an MCP SDK and a subprocess.
- Reference docs: https://developers.openai.com/codex/mcp-server
- Reference source (Apache-2.0): https://github.com/openai/codex/tree/main/codex-rs/mcp-server
1. The coarse pattern: an agent under one tool
The design question for a code-inspection harness ("what is the TLS config in the live system of services foo, bar?", where the answer lives across ~200 source files) is tool granularity, not transport:
| Coarse — one tool runs the agent | Fine — expose grep/read/list primitives |
|
|---|---|---|
| Where reasoning lives | inside the harness, next to the source | in the caller, blind to your codebase |
| Caller context | small — only the answer returns | large — file contents stream into the caller |
| Insulation | caller never sees raw files | caller sees everything it reads |
| Multi-file questions | natural (the agent searches) | hundreds of round-trips |
| Session semantics | one agent session = one tool call | caller orchestrates everything |
For "answer a question about a codebase," coarse is the norm — it is precisely the shape OpenAI chose for Codex, and the A2A community's "agents are not tools" critique (https://discuss.google.dev/t/agents-are-not-tools/192812) is about constraining an agent to a tool interface, which is acceptable — even desirable — when the caller only wants the answer.
2. The reference implementation: codex mcp-server
2.1 What it is
A single Rust binary (codex-rs/mcp-server/) that implements MCP over stdio using the rmcp Rust MCP crate, and embeds codex-core as a library in-process — it does not spawn codex as a subprocess (https://github.com/openai/codex/tree/main/codex-rs/mcp-server, https://developers.openai.com/codex/mcp-server). Run it with:
codex mcp-server
It exposes two tools (from tools/list):
codex — start a session. Parameters (source: codex_tool_config.rs, https://github.com/openai/codex/blob/main/codex-rs/mcp-server/src/codex_tool_config.rs):
| Param | Type | Purpose |
|---|---|---|
prompt |
required string | initial user prompt |
model |
optional string | e.g. gpt-5.2-codex |
cwd |
optional string | working directory for the session (the codebase) |
approval_policy |
enum | untrusted, on-request, never |
sandbox |
enum | read-only, workspace-write, danger-full-access |
config |
object | overrides for $CODEX_HOME/config.toml |
base_instructions |
optional string | replace the default base instructions |
developer_instructions |
optional string | injected developer-role message |
compact_prompt |
optional string | prompt used when compacting |
codex-reply — continue a session: { prompt, threadId }. threadId comes from the structuredContent of a previous codex result.
2.2 How a call is executed (from the source)
codex_tool_runner.rs (https://github.com/openai/codex/blob/main/codex-rs/mcp-server/src/codex_tool_runner.rs) is the heart of the pattern:
thread_manager.start_thread(...)creates a brand-newCodexThreadfor everycodexcall.- The prompt is submitted as a
Submission { op: Op::UserInput { text: prompt, ... } }. - The runner streams agent events back to the client as MCP notifications (approval requests, patch requests, plan deltas, errors) while the loop runs.
- On
TurnComplete, the last agent message is returned as thetools/callresult, and — critically — thethreadIdis embedded instructuredContent:
let structured_content = json!({
"threadId": thread_id,
"content": content_text,
});
The comment in the source explains the contract: "To adhere to MCP tools/call response format, include the Codex threadId in the structured_content field of the response. Some MCP clients ignore content when structuredContent is present, so mirror the text there as well."
-
codex-reply(viarun_codex_tool_session_reply) looks up the existing thread bythreadIdand submits the next prompt to it — same session, continued. -
Approval flows (shell exec, patch apply) are surfaced to the client as elicitations — a human in the loop can approve/deny mid-session.
So the OpenAI model is precisely:
codex tool call { prompt, cwd, sandbox, approval_policy }
-> new Codex agent thread, full loop over <cwd>
-> stream events as notifications
-> return { final_answer, threadId }
codex-reply tool call { prompt, threadId }
-> continue the SAME thread
-> return { next_answer, threadId }
3. Adapting this to the TLS code-inspection use case
The TLS use case is a perfect fit for the coarse pattern. Two ways to get it:
Option A — Use codex mcp-server as-is (zero build)
If Codex is acceptable as the underlying agent, codex mcp-server is the service. Point it at the codebase:
codex tool call {
prompt: "What is the TLS config in the live system of services foo and bar?
Look at how TLS is configured in the repo; cite file:line.",
cwd: "/srv/src", # your codebase
sandbox: "read-only", # inspection only
approval_policy: "never" # no shell approvals needed for read-only inspection
}
-> codex greps/reads the ~200 files itself, reasons, returns the answer + file:line
-> { answer, threadId: "019b..." }
Your user (in Claude Code, Codex, or any MCP client) just adds the server once (claude mcp add --transport http ... or the equivalent), then asks questions. The harness, AGENTS.md conventions, sandbox, and approvals are all Codex's existing machinery.
Option B — Mirror the pattern for an OpenCode harness
If your harness is OpenCode-specific (custom skills, commands, MCP servers, prompts in the workspace), build a thin MCP server that maps the same two tools onto the OpenCode session API (opencode serve → POST /session and POST /session/:id/message, https://opencode.ai/docs/server/):
investigate tool call { question, repo } # ~= codex
-> opencode run --attach <serve> --format json "read AGENTS.md; answer: <question>"
-> return { answer, session_id }
continue_investigation tool call { session_id, followup } # ~= codex-reply
-> opencode run --attach <serve> --session <session_id> --format json "<followup>"
-> return { answer, session_id }
The principle is identical to Codex's: one tool = one agent session; a handle (session_id/threadId) lets the caller continue it.
A practical hybrid for large codebases
Don't let the agent re-grep 200 files every round. Have the first investigate return citations (file:line), and let subsequent turns reference them — the agent reads the already-identified files on continuation instead of searching anew. MCP tools can return resource_link content so the client can fetch specific files if needed (https://modelcontextprotocol.io/specification/2026-07-28/server/tools).
4. Multi-turn: is a session needed, or is stateless better?
The Codex answer: stateless by default, stateful on demand
Codex's design is deliberate and answers your question directly:
- Each
codextool call starts a brand-new thread. The tool is stateless: nothing is remembered between calls unless the caller carries thethreadId. - State exists only when the caller opts in — by passing
threadIdtocodex-reply. This is the MCP-recommended "stateful tool via explicit handle" pattern (MCP has no protocol-level session; a handle is just a string in a result and an argument to the next call, https://modelcontextprotocol.io/specification/2026-07-28/server/tools).
| Stateless (new call each time) | Stateful (threadId continuation) | |
|---|---|---|
| Server memory | none — no retained context | thread held server-side |
| When to use | independent questions; parallel inspection; cheap | follow-ups that reference prior reasoning/artifacts |
| Cost | re-reads context each call | context accumulates (can be compacted) |
| Isolation | perfect — no cross-query leakage | must scope threads per user |
Recommendation for your use case
Hybrid, and it's what Codex already does: expose codex-style (stateless) for fresh questions, codex-reply-style for continuations. A multi-turn inspection session then looks like:
turn 1 codex { prompt: "TLS config for foo, bar?" } -> { answer, threadId: T }
turn 2 codex-reply { prompt: "Now focus on foo's ingress TLS", threadId: T }
turn 3 codex-reply { prompt: "Summarize the differences", threadId: T }
If the underlying agent loop can take minutes (200-file investigation), don't block a single tools/call: use MCP Tasks (return a durable taskId, caller polls; task can even go input_required mid-run, https://modelcontextprotocol.io/extensions/tasks/overview) or rely on the client's own auto-backgrounding of long calls. Statelessness is better whenever queries are independent; sessions are better when they aren't.
5. How to implement it: SDK, subprocess, or use codex directly?
Three real paths, from least to most work:
Path 1 — Use codex mcp-server directly (recommended if Codex suffices)
Zero MCP development. codex mcp-server already implements the coarse pattern, sandboxing, approvals, threadId continuation, and event streaming. This is the strongest answer to "does codex provide the whole framework?": yes — the framework is this binary, Apache-2.0, and it's the reference OpenAI itself documents for building multi-agent workflows with the Agents SDK (https://developers.openai.com/codex/mcp-server). You only add deployment concerns: run it as a service, protect it (auth), scope cwd/sandbox per tenant.
Path 2 — MCP SDK + subprocess (for an OpenCode or other harness)
Build a small MCP server with an official MCP SDK (https://modelcontextprotocol.io/), where each tool handler spawns the harness as a subprocess (opencode run --format json ... or codex exec --json ...) and returns its output. Concretely:
# mcp server (Python SDK), tool "investigate"
@mcp.tool()
async def investigate(question: str, repo: str) -> str:
result = subprocess.run(
["opencode", "run", "--dir", repo, "--format", "json", question],
capture_output=True, text=True, timeout=600)
final = json.loads(result.stdout)[-1] # last event = final assistant message
return final
- Pros: harness-agnostic (any CLI), simple, easy to test; the harness is a black box you can upgrade independently.
- Cons: per-call process spawn overhead (mitigate with
opencode run --attachto a warmopencode serve); continuation requires you to thread a session ID yourself (opencode run --session <id>); no in-process event streaming.
Path 3 — Embed the harness as a library (what codex itself does)
The codex MCP server does not spawn a subprocess — it links codex-core as a Rust library and drives ThreadManager/CodexThread in-process (https://github.com/openai/codex/tree/main/codex-rs/mcp-server). Do this when your harness is a library you can embed: you get direct event streaming, approval/elicitation control, and no process-boundary overhead — at the cost of writing the loop integration yourself. For an OpenCode harness the in-process equivalent is the JS/TS SDK (@opencode-ai/sdk), where you drive sessions from code rather than spawning the CLI (https://opencode.ai/docs/sdk/).
| Path 1: codex as-is | Path 2: SDK + subprocess | Path 3: embed library | |
|---|---|---|---|
| Build effort | none | low | medium-high |
| Harness | Codex only | any CLI (opencode, codex exec) | your own library |
| Multi-turn | built-in (threadId) |
you implement (session ID) | built-in (library sessions) |
| Streaming/approvals | yes | limited | full |
| Best when | codex is fine as the agent | your harness is a CLI you want to expose | you need deep control |
6. Containerizing it: a minimal Dockerfile
Two facts drive the container shape:
codex mcp-serverspeaks only stdio. Itsmain.rs/lib.rshand-roll a JSON-RPC loop over stdin/stdout (https://github.com/openai/codex/blob/main/codex-rs/mcp-server/src/lib.rs); it has no HTTP endpoint of its own. A remote client (claude mcp add --transport http ...) therefore needs a stdio→HTTP bridge in front of it.- The natural bridge is
mcp-proxy— a TypeScript streamable-HTTP/SSE proxy that spawns a stdio MCP server and serves it on HTTP, with API-key auth, CORS, request/keep-alive timeouts, and stateless mode (https://github.com/punkpeye/mcp-proxy). It's the same proxy FastMCP uses, MIT licensed.
So the production shape is two processes in one container (or one container each):
[remote MCP client] --HTTP+API key--> mcp-proxy --stdio--> codex mcp-server
\-> codex-core agent loop over /workspace
A minimal Dockerfile:
# Node base gives both runtimes: node (for mcp-proxy) + codex installable
FROM node:22-slim AS base
ENV CODEX_HOME=/codex-home \
OPENAI_API_KEY="" # set at runtime; or pre-login into $CODEX_HOME
WORKDIR /app
# 1) Install the Codex CLI (official installer)
RUN curl -fsSL https://chatgpt.com/codex/install.sh | sh \
&& mv ~/.local/bin/codex /usr/local/bin/codex
# 2) Install the stdio -> HTTP bridge
RUN npm install -g mcp-proxy
# 3) The codebase the agent inspects (TLS use case) — or mount at runtime
COPY ./src /workspace
# Entrypoint: bridge exposes codex mcp-server over HTTP on :8080
# -X-API-Key required for auth
# --requestTimeout raised so long 200-file inspections aren't killed early
EXPOSE 8080
CMD ["sh", "-c", "exec mcp-proxy --port 8080 --apiKey \"${MCP_API_KEY}\" \
--requestTimeout 600000 -- codex mcp-server"]
Run it:
docker build -t codex-mcp .
docker run -d --name codex-mcp \
-p 8080:8080 \
-e MCP_API_KEY=sekret \
-e OPENAI_API_KEY=sk-... \ # or login once and bake $CODEX_HOME/auth
-v /path/to/repo:/workspace:ro \# read-only inspection
codex-mcp
Then a user connects to the container's address:
claude mcp add --transport http codex http://host:8080/mcp \
--header "Authorization: Bearer sekret"
Containerization notes specific to this pattern:
- Credentials stay server-side.
OPENAI_API_KEY(or a pre-authenticated$CODEX_HOME) lives in the container only; consumers authenticate with the bridge'sMCP_API_KEY.mcp-proxyvalidatesX-API-Keyand notes to serve over HTTPS in production (https://github.com/punkpeye/mcp-proxy). - Read-only codebase. For the TLS-inspection use case mount
/workspace:roand have clients passcwd: /workspacewithsandbox: read-onlyin thecodextool call — the agent can grep/read the ~200 files but cannot mutate them. - Stateless vs stateful maps to container lifecycle.
mcp-proxydefaults to one server instance per connection;--statelesscreates a fresh instance per request (for serverless/load-balanced deployments). Either way,codex'sthreadIdcontinuation still works — the threads live inside the onecodex mcp-serverprocess (https://github.com/punkpeye/mcp-proxy). - Timeouts. The default
mcp-proxy --requestTimeoutis 300 s; raise it (or rely on MCP Tasks / client auto-backgrounding) for investigations that exceed a few minutes (https://github.com/punkpeye/mcp-proxy, https://code.claude.com/docs/en/mcp). - Multi-container option. For isolation run
mcp-proxyandcodex mcp-serverin separate containers: the proxy's upstream is then-- codex mcp-serverrun via a small sidecar command that the proxy spawns (stdio still) — or use two images and a tiny launcher. The single-container version above is the minimal start.
7. Final judgment
The coarse approach is not just viable — it is the canonical pattern, shipped by OpenAI as codex mcp-server. The design you should copy:
- Two tools, not a dozen:
investigate(start a session) +continue(continue by handle), mirroringcodex/codex-reply. - A full agent session per tool call, sandboxed and approval-gated,
cwdscoped to the codebase. - Stateless by default; stateful by explicit
threadId/session_idhandle, per the MCP stateful-tools guidance. - Return
structuredContentwith the handle, so clients that ignorecontentstill see it. - Long runs go through MCP Tasks or client auto-backgrounding rather than blocking.
And to the "does codex provide the whole framework?" question: for this use case, yes — codex mcp-server is the whole framework, open source and documented; you only add deployment. You would build your own MCP layer only when the underlying agent must be your own harness (skills, commands, MCP configs) rather than Codex.
8. Caveats
- Codex is the agent:
codex mcp-serverruns Codex's loop/tools/models. Your workspace's OpenCode skills/commands/MCP servers won't be loaded by it; use Path 2/3 if the harness must be yours. - Auth and tenancy: an MCP server that runs a full agent loop is a remote-code-execution boundary. Scope
cwd, sandbox, and approvals per caller; keep model credentials server-side. - Timeouts/output caps: long inspections can exceed client limits (Claude Code auto-backgrounds past ~2 min, ~25k-token output cap, https://code.claude.com/docs/en/mcp). Return
taskId/threadIdand let the caller poll. - Cost: each
codexcall is a full agent turn — meter per query/session.
References (fetched and verified Aug 8, 2026)
- OpenAI — Use Codex with the Agents SDK / Running Codex as an MCP server: https://developers.openai.com/codex/mcp-server
- OpenAI — Codex repository (Apache-2.0), MCP server crate: https://github.com/openai/codex/tree/main/codex-rs/mcp-server
- Source —
codex_tool_runner.rs(thread start, streaming,threadIdinstructuredContent): https://github.com/openai/codex/blob/main/codex-rs/mcp-server/src/codex_tool_runner.rs - Source —
codex_tool_config.rs(codex/codex-replytool schemas, params): https://github.com/openai/codex/blob/main/codex-rs/mcp-server/src/codex_tool_config.rs - Source —
codex-mcp/src/lib.rs(stdio JSON-RPC loop, in-process codex-core): https://github.com/openai/codex/blob/main/codex-rs/mcp-server/src/lib.rs - OpenAI — Codex Non-interactive mode (
codex exec --json/--output-schema/--sandbox): https://developers.openai.com/codex/non-interactive-mode - MCP — Tools spec (stateful tools via explicit handles,
resource_link,structuredContent): https://modelcontextprotocol.io/specification/2026-07-28/server/tools - MCP — Tasks extension (long-running,
input_required, polling): https://modelcontextprotocol.io/extensions/tasks/overview - OpenCode — Server (session API for a Path-2 wrapper): https://opencode.ai/docs/server/
- OpenCode — CLI (
opencode run --format json,--attach,--session): https://opencode.ai/docs/cli/ - OpenCode — SDK (
@opencode-ai/sdk, structured output): https://opencode.ai/docs/sdk/ - punkpeye — mcp-proxy (stdio → streamable HTTP/SSE bridge, API-key auth, timeouts): https://github.com/punkpeye/mcp-proxy
- Anthropic — Connect Claude Code to tools via MCP (client timeouts, output caps): https://code.claude.com/docs/en/mcp
- A2A — What is A2A? / "agents are not tools" context: https://a2a-protocol.org/latest/topics/what-is-a2a/
- "Agents are not tools" discussion (Google Developers): https://discuss.google.dev/t/agents-are-not-tools/192812