codex-offload-mcp

codex-offload-mcp

An MCP server for running Claude Code and the Codex CLI as a pair: Claude drives, and hands self-contained tasks to Codex as background jobs.

Category
Visit Server

README

codex-offload-mcp

An MCP server for running Claude Code and the Codex CLI as a pair: Claude drives, and hands self-contained tasks to Codex as background jobs.

codex_start returns a job id immediately instead of blocking, so the driving model keeps working while Codex works alongside it — two agents on the problem at once, rather than one waiting on the other.

Why this exists

To use two coding models together rather than one at a time. Claude Code holds the conversation, the context and the judgment about what to do next; Codex is a capable second agent that can be given a well-specified piece of work and left to get on with it. Neither replaces the other, and the interesting part is what they do concurrently.

Blocking would defeat the entire point. If handing work to Codex meant waiting for Codex, there would be no pair — just one model parked while the other runs, which is strictly worse than doing the work yourself. So dispatch returns in about a second and the job continues in a detached process that outlives even this server. The driving model carries on reasoning, reading and answering, and collects the result when it is ready.

That concurrency is also what makes the delegation worth specifying carefully. Codex cannot see the conversation, so a task has to travel as a complete brief — and the work that comes back is checked against git rather than taken on trust, because handing work between models is precisely where "I did the thing" and "the thing got done" come apart.

Tools

Tool Returns
codex_start { jobId, state, … } — immediately, job runs in the background
codex_execute_plan { jobId, … } — hand Codex a finished plan to carry out faithfully
codex_status State, elapsed time, commands run, files touched, recent activity
codex_result Structured handoff report + git-verified changes; progress if still running
codex_reply Continues a job's Codex thread with a follow-up; returns a new jobId
codex_models Available models and the reasoning efforts each one accepts
codex_cancel Kills the job and its child processes
codex_list Known jobs, newest first, optionally filtered by state

None of them block.

The handoff

Getting work back from Codex matters as much as sending it. Three things make the return trip trustworthy:

1. A typed report. Jobs run with --output-schema, so the final message is not prose but a fixed shape:

{
  "summary": "...",
  "status": "complete | partial | blocked",
  "filesChanged": [{ "path": "...", "change": "..." }],
  "verification": [{ "command": "...", "passed": false, "details": "..." }],
  "followUps": ["..."],
  "blockers": ["..."],
  "documentation": "which docs were updated, or why none were warranted",
  "confidence": "high | medium | low"
}

This makes Codex commit to whether it actually finished and whether its checks passed, instead of burying a failed test in a paragraph. Pass structured: false for a job where you want prose.

2. Changes verified against git, not self-reported. codex_result also returns actualChanges, computed by diffing the repo against the commit and dirty-file set recorded when the job started. Files already modified beforehand are flagged preexisting so they are not mistaken for Codex's work. When the report and actualChanges disagree, actualChanges is the one to trust.

3. A way to push back. codex_reply continues the original Codex thread, so corrections ("you missed the error path", "now update the tests") land with all of Codex's context intact rather than starting a cold job that has to rediscover everything.

Install

Prerequisites

  • Node.js 20+
  • The Codex CLI, installed and authenticated — run codex login and confirm codex --version works. This server drives that binary; without it every job fails immediately.

Build

git clone https://github.com/jgt87/codex-offload-mcp.git
cd codex-offload-mcp
npm install
npm run build

This produces dist/index.js. Note its absolute path — every step below needs it.

Add to VS Code

MCP support is built into current VS Code; if the Command Palette lists MCP: commands, you have it. Pick either route:

Guided. Command Palette (Ctrl+Shift+P) → MCP: Add Server → Command (stdio). Enter node as the command and the absolute path to dist/index.js as the argument, then name it codex-offload.

By hand. Command Palette → MCP: Open User Configuration to open your user mcp.json (%APPDATA%\Code\User\mcp.json on Windows), and add the server:

{
  "servers": {
    "codex-offload": {
      "type": "stdio",
      "command": "node",
      "args": ["C:/path/to/codex-offload-mcp/dist/index.js"]
    }
  }
}

Use forward slashes on Windows, or escape backslashes as \\ — a raw C:\path is invalid JSON and the server will silently fail to start.

To scope it to one project instead of your whole profile, use MCP: Open Workspace Folder Configuration and put the same servers block in .vscode/mcp.json. That file can be committed, which gives everyone on the repo the same tools.

Verify. Open the Chat view, switch to Agent mode, click Configure Tools, and confirm the codex_* tools appear and are enabled. MCP: List Servers shows the server's status and its logs if it failed to start.

Add to Claude Code

claude mcp add codex-offload --scope user -- node /absolute/path/to/dist/index.js

Confirm with /mcp in a session, or claude mcp list from a shell.

After changing the code

A running server keeps serving the old dist/, so rebuild and restart it:

npm run build
  • VS Code — MCP: List Servers → select the server → Restart. (The experimental chat.mcp.autoStart setting can do this for you.)
  • Claude Code — restart the session; MCP servers connect at session start.

Using it

You do not call the tools by name. Ask for what you want and Claude selects them:

You say Tool
"Offload to Codex: migrate the auth module to the new API" codex_start → jobId, immediately
"How's that Codex job doing?" codex_status
"Get the Codex result" codex_result
"Tell Codex it missed the error path" codex_reply
"What Codex jobs are running?" codex_list
"Which models can Codex use?" codex_models
"Kill that job" codex_cancel

The pattern worth building a habit around is dispatch-then-continue: "Offload the test migration to Codex, and while that runs, walk me through the router."

Checking its work

codex_result gives you Codex's own report and actualChanges from git. When they disagree, git wins. For anything that matters, go further than reading the report: run the tests, and break the thing under test to confirm they actually fail. A suite that passes proves less than a suite you have watched fail for the right reason.

Collaboration modes

One-shot offload — hand Codex a task, collect the result — is the base case. On top of it are a few named ways to split work between the two models, each with a slash command so you can start one from any repo. Install the commands by copying them into your Claude Code commands directory:

cp .claude/commands/*.md ~/.claude/commands/
Command Pattern Who does what
/plan-execute <task> plan → execute Claude designs the change and writes a concrete plan; Codex carries it out faithfully in the background.
/codex-review <task> execute → review Codex does the work; Claude reviews the git diff and sends corrections with codex_reply.
/codex-split <task> split & parallelize Claude decomposes the task into independent chunks and dispatches several Codex jobs at once, then reassembles.
/codex-draft <task> draft → refine Codex produces a fast first draft cheaply; Claude refines it in-process where context and taste are needed.

The split is the same one the whole server is built on: the reasoning, the judgement and the conversation context stay with Claude; the expensive output tokens — typing out a plan, a bulk draft, a mechanical migration — go to Codex, which bills separately. Three of the four modes need no new code; they are patterns over codex_start, codex_result and codex_reply, named and given a front door.

plan→execute is the exception — it has machinery of its own. It adds a tool, codex_execute_plan, that takes a finished plan rather than an open task. Codex is told the plan came from another model and to follow it faithfully — and, crucially, to stop and report a blocker instead of quietly substituting its own design when a step is wrong, so you can revise the plan and resume with codex_reply. Because the design thinking is already done and lives in the plan, execution usually needs less reasoning effort than the whole task would; routing still reads the plan text and effort stays overridable, so pin a lower reasoningEffort when the steps are mechanical.

It fits the same seam as the documentation instruction: the faithful-execution framing is prepended to what Codex receives, but codex_status and codex_list still show the plan you actually handed over, not the machinery around it.

Orchestration

Two decisions get made per task, and they are handled very differently.

Whether to delegate is not decided here. There is no scheduler, no queue and no second model triaging work. The only thing steering that choice is the codex_start tool description, which the calling model reads at call time and judges against.

That is deliberate. The decision needs the one thing this process cannot see — the conversation. Whether a task is self-contained, whether there is useful work to do while it runs, whether you are about to change your mind about the approach: none of that is visible from inside an MCP server, so the judgment stays with the model that has the context, and this server sticks to running the job and checking the result. Editing that description in src/index.ts is how you change delegation behaviour in general.

One dial you can turn without touching code: how much to offload. The whether-decision stays the model's, but you can bias it. Set the CODEX_MCP_OFFLOAD_LEVEL environment variable on the server to conservative, balanced (the default) or aggressive, and it appends a matching instruction to the codex_start description the model reads — aggressive lowers the bar so more work goes to Codex (useful when you want to conserve Claude's own usage), conservative raises it. It steers judgment rather than enforcing a rule: the hard exclusions — needs conversation context, exploratory, trivial triage — still hold at every level. It is read once at startup, so restart the server after changing it, and codex_models reports the active setting so you can confirm it took. In your MCP config it goes in the server's env block:

{
  "servers": {
    "codex-offload": {
      "type": "stdio",
      "command": "node",
      "args": ["C:/path/to/codex-offload-mcp/dist/index.js"],
      "env": { "CODEX_MCP_OFFLOAD_LEVEL": "aggressive" }
    }
  }
}

Which model and how hard it thinks are decided here — see below. That part is a genuine heuristic, and the honest framing is that it is the one invented thing in the pipeline: no API reports "this task is mechanical". So it is built to be inspectable rather than trusted. Every job records the tier and the reason it was picked, and any explicit value overrides it.

What should be offloaded

All of these need to hold:

Test Why
Self-contained Codex cannot see the conversation. Anything resting on what was just worked out must be restated in full — and if restating it is most of the work, offloading is a net loss.
Slow Minutes, not seconds. Below ~30s the round trip costs more than it saves.
Real work to do meanwhile Dispatching and then sitting on codex_status gains nothing.
Verifiable afterwards Mechanical enough that actualChanges from git shows whether it went right.
Scoped to one cwd Codex writes to disk directly; ambiguous scope means unwanted edits.

Keep it in-process when the task needs conversation context, is fast, blocks the next decision, or is surgical enough that specifying it precisely costs more than just doing it.

Exploratory work is the main trap. Investigations where each measurement changes what you look at next cannot be offloaded — by the time the prompt can be written, the thinking is already done. A useful tell is a wrong hypothesis: if you expect to have one, keep the work in-process.

Misjudging is asymmetric. A bad question wastes a minute; a bad delegation writes files to disk. That asymmetry is why codex_result checks Codex's self-report against git instead of trusting it — the design already assumes a delegation can be wrong. Prefer sandbox: "read-only" for anything analytical.

Choosing model and effort

codex_start picks both from the task text unless you pass them. Here is the whole path a call takes, from prompt to spawned process:

codex_start(prompt, model?, reasoningEffort?, autoRoute?)
   │
   ├── autoRoute: false ────────────────────► skip everything below
   │                                          (caller's values, else config.toml)
   ▼
classify(prompt)                              src/route.ts — keyword match, no model call
   │
   ├── matches HARD patterns?      ── yes ──► tier = hard
   ├── matches MECHANICAL patterns? ─ yes ──► tier = mechanical
   └── neither ─────────────────────────────► tier = standard          (hard wins ties)
   │
   ▼
pick model            match tier's wording against each model's vendor description
   │                  ("frontier" / "balanced" / "fast, affordable")
   │                  no match → Codex's own first-ranked model
   ▼
pick effort           mechanical → low   standard → medium   hard → high
   │
   ▼
clamp to model        effort not in this model's supported list?
   │                  → nearest supported level at or below it
   ▼
apply overrides       explicit model / reasoningEffort replace whatever was chosen
   │
   ▼
validate              model accepts this effort?
   │                        │
   │                        └── no ──► error returned in ms, nothing spawned
   ▼
spawn `codex exec --json` detached, recording {tier, rationale, auto} on the job

Only the classify step is invented. Everything from "pick model" down is driven by the model index Codex itself maintains, so the lineup and the legal effort levels are facts rather than guesses.

The tiers:

Tier Signals Effort Model
mechanical renames, moving files, formatting, typos, applying a stated pattern low the one described as fast/affordable
standard anything without a strong signal — the default medium the one described as balanced/everyday
hard concurrency, races, deadlocks, leaks, security, architecture, trade-offs, root-cause work high the one described as frontier

A prompt matching both mechanical and hard is treated as hard: under-thinking a subtle problem costs more than over-thinking a simple one.

Resolved against a real lineup, the assignment looks like this — the model column is whatever the index currently offers, not a fixed list:

  TASK                                         TIER          MODEL            EFFORT
  ───────────────────────────────────────────  ────────────  ───────────────  ──────
  "Rename getUser to fetchUser across the      mechanical    gpt-5.6-luna     low
   repo and update the call sites"             │             fast, affordable
                                               └─ matched: "rename"

  "Add an endpoint returning the current       standard      gpt-5.6-terra    medium
   user's projects"                            │             balanced/everyday
                                               └─ no strong signal → default

  "Fix the race condition in the job           hard          gpt-5.6-sol      high
   scheduler that drops events under load"     │             frontier
                                               └─ matched: "race condition"

  "Rename the lock helper while fixing the     hard          gpt-5.6-sol      high
   deadlock it causes"                         │
                                               └─ matched both; hard wins

The model lineup is discovered, not hardcoded. Models and their accepted effort levels are read from Codex's own index (~/.codex/models_cache.json), so a model released after this server was written is picked up automatically. Routing re-reads the index whenever Codex rewrites it (the file's mtime changes), so a long-running server keeps up with Codex rotating or dropping models instead of routing to one that no longer exists and failing the job after it spawns. Tiers map onto that lineup by matching the vendor's own descriptions, which means a renamed model still routes sensibly. codex_models reports the current lineup, including whether the index was actually readable. (The model names shown inside the tool descriptions are still the startup snapshot — schema text is fixed for an MCP session — so a restart refreshes those, but routing itself does not need one.)

This matters more than it sounds. The first version of this feature hardcoded the effort list, and it was wrong within the hour — it invented none and minimal, which no model advertises, and omitted ultra, which two models support.

Effort is clamped to what the chosen model accepts, so a model topping out at xhigh never receives ultra. If you pin a model and an effort it cannot take, the call fails immediately with the model's real list — rather than failing the job minutes later, which is what Codex does on its own, since it forwards the value unchecked.

Overriding. An explicit model or reasoningEffort always wins, and partial pinning works — fix the model, let the effort be routed, or the reverse. autoRoute: false disables inference entirely and falls back to your ~/.codex/config.toml defaults. Worth knowing that if that file sets model_reasoning_effort = "high", every un-routed job runs at high until you say otherwise.

Every job records what was chosen and why:

"routing": {
  "tier": "mechanical",
  "rationale": "classified as mechanical — matched mechanical wording (rename); model gpt-5.6-luna chosen for this tier; effort low",
  "auto": true
}

The classifier is keyword matching, and deliberately so — a cleverer one would need a model call, which would add latency to a tool whose whole promise is returning immediately. It will misread things. That is why it explains itself and why every part of it can be overridden.

codex_reply takes reasoningEffort too, defaulting to the parent job's. Worth raising when a first attempt failed for want of thinking, and lowering when the follow-up is a mechanical fixup.

The feedback loop

Work does not come back trusted. Every job produces two independent accounts of itself, and the loop closes by comparing them:

  codex_start
      │    git baseline captured first: current commit + already-dirty files
      ▼
  codex exec --json   (detached — outlives this server)
      │    streams events.jsonl while it works
      ▼
  codex_status  ──  poll as often as you like; never blocks
      │
      ▼
  codex_result
      │
      ├───────────────────────────┐
      ▼                           ▼
  Codex's report             actualChanges
  what it believes it did    what git says changed,
  summary, verification,     measured against the
  confidence                 baseline above; files
      │                      already dirty flagged
      │                      preexisting
      │                           │
      └─────────────┬─────────────┘
                    ▼
           caller compares them   ── git wins any disagreement
                    │
         ┌──────────┴───────────┐
         ▼                      ▼
   agree, tests pass      disagree, or partial
         │                      │
         ▼                      ▼
      accept              codex_reply
                                │
                                ▼
                 new job on the SAME Codex thread
                 full context retained, effort adjustable
                                │
                                └──►  back to codex_status, above

Three properties make this worth the machinery:

The two accounts are produced independently. The report is what Codex says; actualChanges is computed by diffing the repo against the baseline captured before the job started. Files you had already modified are flagged preexisting, so they are never mistaken for Codex's work. When the two disagree, git wins.

The correction path keeps context. codex_reply resumes the original thread by its thread_id, so "you missed the error path" lands with everything Codex already knows, instead of a cold job that has to rediscover the codebase.

The loop is closed by you, not by the router. Nothing here learns. A job records its tier and rationale so you can look back and see whether the classification was reasonable, but a misrouted task does not adjust anything for next time — you pin model or reasoningEffort on the retry, or edit the patterns in src/route.ts. If routing keeps getting a particular kind of task wrong, that is a signal to change the keyword lists, and it is meant to be done by hand.

How it works

What happens when a task is offloaded

  1. Baseline. codex_start records the state of cwd before Codex touches anything: the current commit, plus the set of files already dirty. This is what makes the later verification honest — files you had already modified are flagged preexisting rather than blamed on Codex.
  2. Routing. Unless you pinned them, model and reasoning effort are chosen from the task text and clamped to what that model accepts. An effort the model cannot take is rejected here, before anything is spawned.
  3. Dispatch. The prompt is written to prompt.txt and codex exec --json is spawned detached, with the prompt fed over stdin. The call returns a job id immediately; nothing blocks, ever.
  4. Streaming. Codex writes JSONL events to events.jsonl as it works, and its final answer to last-message.txt. Both are on disk, not in memory.
  5. Polling. codex_status parses the tail of the event stream into a progress view — commands run, files touched, recent activity. It reports; it never waits.
  6. Collection. codex_result returns Codex's structured report and re-diffs the repo against the baseline from step 1 to produce actualChanges. Two independent accounts of the same work.
  7. Follow-up. codex_reply resumes the original Codex thread by its thread_id, so a correction lands with all of Codex's context intact rather than starting cold.

Under the hood

Each job spawns codex exec --json as a detached process, with:

  • the prompt piped over stdin — no command-line length or quoting limits
  • --json events streamed to events.jsonl (parsed for the progress view)
  • -o last-message.txt capturing the final answer
  • metadata in meta.json

Jobs live in ~/.codex-mcp/jobs/<jobId>/ (override with CODEX_MCP_JOBS_DIR). Finished jobs older than seven days are pruned at startup.

Because jobs are detached, they survive this server restarting. A job started by a previous process has nobody listening for its exit, so running is only trusted while its pid is alive; otherwise state is recovered from whether a result file was produced.

The Codex binary is resolved to the real platform executable inside the npm package, so jobs spawn with an argv array rather than through a shell. Override with CODEX_BIN if needed.

Sandbox

sandbox defaults to workspace-write: Codex may edit files under cwd. That is the point — it does real work — but it means the working tree changes underneath you. Re-read files after a job finishes rather than trusting anything cached from before.

  • read-only — analysis and review, no writes
  • workspace-write (default) — edits within cwd (plus any addDirs)
  • danger-full-access — unrestricted; only when explicitly asked for

Cancelling does not roll back edits already made.

Documentation

Jobs that can write are asked to document themselves. A standing instruction is appended to the prompt telling Codex to update the project's existing documentation — README.md, AGENTS.md, CLAUDE.md, files under docs/ — when the change alters behaviour, adds a feature, or makes an existing statement untrue.

This exists because Codex starts cold. It cannot see the conversation that led to the task, so unless something asks, documentation simply never gets written and the caller has to remember every single time.

The instruction leans hard in one direction: edit what exists, do not create new files. An unqualified "document your changes" reliably produces a CHANGES.md or NOTES.md in every repository it touches, which is worse than nothing — it fragments what a reader has to consult and goes stale immediately. Codex is told not to invent a changelog, to match the surrounding document's voice and structure, and to prioritise correcting statements the change made untrue over adding new prose. It is also told to skip documentation when the work does not warrant it, and to say so.

The structured report carries a required documentation field, so a job has to account for what it wrote or explain why it wrote nothing. actualChanges from git shows which .md files were actually touched, so the claim is checkable rather than taken on faith — the same split the rest of the handoff uses.

Setting Behaviour
default On for workspace-write and danger-full-access
documentation: false Suppressed; the prompt is passed through untouched
sandbox: "read-only" Always off — the job could not write a file even if asked

A codex_reply follow-up inherits the parent job's setting, so a correction to documented work keeps the docs in step with it. The instruction is appended only to what Codex receives; codex_status and codex_list still show the prompt you actually wrote.

Writing good job prompts

Codex starts cold. It cannot see the Claude conversation, so a prompt must carry its own context: what to change, which files, the constraints, and what "done" looks like.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured