Codex Control Plane MCP

by aresyn

Not rated
GitHub

About

Durable MCP control plane for long-running Codex Desktop tasks.

Details

Author
aresyn
Categories
Developer Tools, Automation, Project Management

Setup

Install Codex Control Plane MCP in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/aresyn/codex-control-plane-mcp

Follow the installation instructions in the repository README, then restart your MCP client.

Durable MCP control plane for long-running Codex Desktop tasks.

Reliable Codex Desktop automation for long tasks.

codex-control-plane-mcpturns Codex Desktop andcodex-app-serverinto a durable worker that an MCP client can drive safely. Send a task, get anoperationIdorworkflowIdright away, poll until the work finishes, approve Plan Mode when needed, then read the final report.

The server handles the awkward parts that thin wrappers usually leave to the caller: app-server startup, thread and turn creation, retry safety, duplicate prompt protection, Plan Mode, approvals, local history, diagnostics, and repair.

OpenClaw and Hermes are first-class clients, but the server is useful for any local orchestrator that needs Codex Desktop to do long-running work without holding one MCP call open for hours.

MCP client / orchestrator -> submit a task or start a Plan Mode workflow <- receive operationId or workflowId immediately -> poll status -> answer approvals or approve the plan <- read final report, diagnostics, threadId, and turnId

- no multi-hour MCP calls;
- no duplicate Codex turns after a client retry;
- no blind fire-and-forget task submission;
- a local SQLite record of operations, workflows, turns, hooks, and diagnostics.

For a more detailed decision guide, seedocs/THIN_WRAPPERS.md.

- Full live target: Windows with Codex Desktop andcodex-app-server.
- Linux and macOS: protocol-only checks for now.
- Local-first: not intended to be exposed as a public network service.

This is a local-first control plane for trusted Codex Desktop environments.

Do not expose it as a network service without authentication.

- useread-onlyfor untrusted repositories;
- useon-requestapproval when testing new workflows;
- Plan Mode never runs with aread-onlysandbox. If a caller requestsread-only, MCP raises that turn toworkspace-writeand reports the adjustment in status output;
- keepstate/,logs/,.env, and.codex/private.

- Durable async queue for Codex write operations.
- Retry-safeclient_request_idhandling.
- Active duplicate prompt detection.
- SQLite leases and heartbeats for competing MCP processes.
- Recovery after MCP restart duringthread/startorturn/start.
- Durableturn/steerfor adding context to an active turn without creating a second turn.
- Durablethread/forkfor branching an existing thread, with or without an initial message.
- Plan Mode workflows: start plan, poll, approve, execute, read final report.
- Plan Mode runtime floor:workspace-write, withruntimePolicyAdjustedin status when MCP raises aread-onlyrequest.
- Code review workflows through app-serverreview/start, with polling and final report capture.
- Structured final reports withoutput_schema.
- Thread lifecycle tools for archive, unarchive, and pollable compaction.
- Workflow goal sync with Codex Desktop thread goals.
- Image and local image inputs for turns that start throughturn/start.
- Pending approvals and questions exposed as pollable MCP state.
- Turn interrupts bythreadId/turnId,operationId, orworkflowId.
- Runtime inventory for models, permission profiles, sandbox readiness, hooks, skills, provider features, account status, usage bands, rate-limit state, and supported app-server methods.
- Health checks, diagnostics, issue analysis, and dry-run repairs.
- MCP-owned hook history in SQLite for search, summaries, and fallback reads.
- Redacted app-server progress journal for deltas, warnings, model reroutes, and token usage.
- Structured MCP errors that automation code can branch on.

Write and control actions go throughcodex-app-server. The server does not mutate Codex internal SQLite databases or transcript files.

python -m pip install "codex-control-plane-mcp @ git+https://github.com/aresyn/codex-control-plane-mcp.git"
git clone https://github.com/aresyn/codex-control-plane-mcp.git cd codex-control-plane-mcp py -m venv .venv .\.venv\Scripts\Activate.ps1 python -m pip install -e ".[dev]" python -m pytest -q
codex-control-plane-mcp-admin init --state-db .\state\codex-mcp-state.sqlite3 --projects-root C:\Users\you\Projects
{ "mcpServers": { "codex-control-plane": { "command": "codex-control-plane-mcp", "args": [] } } }

More copy-paste examples for Claude Desktop, Cursor, VS Code-style MCP clients, local checkouts, installed packages, and central worker mode are available inexamples/mcp-client-configs.md.

The oldopenclaw-codex-mcpandopenclaw-codex-mcp-hookscommands remain as compatibility aliases for one release line.

The defaultinlinemode is still the simplest setup: one MCP process can submit and execute operations. For OpenClaw, Hermes, or any setup with several MCP clients, use a central worker instead.

- every MCP client uses the sameCODEX_HOMEandCODEX_MCP_STATE_DB;
- OpenClaw gateway entries run withCODEX_MCP_EXECUTION_MODE=client;
- one long-runningcodex-control-plane-mcp-workerprocess ownscodex-app-server, leases, queue slots, and resource locks;
- clients callcodex_submit_task, then poll status. They do not execute queued operations themselves.

$env:CODEX_MCP_EXECUTION_MODE = "worker" codex-control-plane-mcp-worker

Safe observation mode, useful before switching a live gateway:

codex-control-plane-mcp-worker --observe
CODEX_MCP_MAX_ACTIVE_TURNS_GLOBAL=4 CODEX_MCP_MAX_ACTIVE_TURNS_PER_PROJECT=3 CODEX_MCP_MAX_ACTIVE_TURNS_PER_AGENT=3 CODEX_MCP_MAX_ACTIVE_TURNS_PER_THREAD=1 CODEX_MCP_MAX_ACTIVE_WRITE_TURNS_PER_PROJECT=1 CODEX_MCP_MAX_APP_SERVER_PENDING_REQUESTS=8

For write turns in the same project, passresource_keystocodex_submit_task. Without them,workspace-writeanddanger-full-accessturns take a broad project write lock. With disjoint keys, the worker may run several write turns in parallel.

- codex_get_worker_status
- codex_get_queue_status
- codex_get_concurrency_status
- codex_get_worker_command_status

codex_get_operation_statusalso returnsqueueState,workerState,slotState, andresourceLockState. A running turn hasslotState.claimed=trueand aslotClaimwith the worker id, slot type, and claim time.codex_get_queue_statusseparates queued work from running turn operations, auxiliary operations, active turn slots, and lock conflicts.

When a workflow is waiting for capacity,codex_get_workflow_statusmirrors the nested operation queue state inworkflowOperationQueueState. UsenextRecommendedAction="wait_for_worker_slot"for slot pressure andnextRecommendedAction="wait_for_resource_lock"for write lock conflicts. Do not create another operation for the same work while either action is returned.

The admin helper can generate a fuller client config, install hooks, and run a protocol smoke:

codex-control-plane-mcp-admin init --state-db .\state\codex-mcp-state.sqlite3 --projects-root C:\Users\you\Projects

The command prints a JSON block you can copy into an MCP client config. It does not print secrets or private prompts.

You can also install only the Codex hooks:

codex-control-plane-mcp-hooks install --state-db .\state\codex-mcp-state.sqlite3 codex-control-plane-mcp-hooks status codex-control-plane-mcp-hooks doctor

The installer backs up~/.codex/hooks.json, merges its handlers with your existing hooks, storesstateDbas an absolute path, and writes prompts, visible agent progress text, final answers, and turn status into the MCP state DB. Tool calls and command outputs are not recorded by default. Restart Codex after installing or changing hooks.

For turns launched throughcodex-app-server, the server mirrors the accepted prompt, visible assistant messages, and turn status into the same SQLite history. That keeps search and status reads useful even when app-server does not execute user hooks itself.

codex_submit_task -> operationId codex_get_operation_status(operationId) -> queued / running / waiting_for_approval / completed / failed

Use the sameclient_request_idwhen a caller retries after a transport timeout. The retry returns the existing operation instead of creating another turn.

Attach screenshots or other image evidence:

codex_submit_task( operation_type="start_chat", message="Analyze this screen.", input_items=[ {"type": "localImage", "path": ".\\screens\\error.png", "detail": "low"}, {"type": "image", "url": "https://example.com/screenshot.png", "detail": "high"} ] )

Image inputs are accepted only for operation types that start a new turn:start_chat,send_message,execute_plan, andfork_threadwith an initial message. MCP sends the path or URL tocodex-app-server, but operation status and diagnostics return only safe metadata such as type, detail, size, extension, and hashes. Binary image content, raw URLs, and full local image paths are not stored in public status payloads.

codex_submit_task(operation_type="steer_turn", thread_id=..., expected_turn_id=..., message=...) -> operationId codex_get_operation_status(operationId) -> follows the target turn until completed / failed / interrupted

Usesteer_turnonly while the target turn is active. For a completed thread, usesend_messageinstead.

codex_submit_task(operation_type="fork_thread", source_thread_id=...) -> operationId codex_get_operation_status(operationId) -> completed, threadId=<forkedThreadId>
codex_submit_task(operation_type="fork_thread", source_thread_id=..., message=...) -> operationId codex_get_operation_status(operationId) -> follows the first turn in the forked thread

Useclient_request_idfor retry-safe fork requests. Without it, each call is treated as a new fork request.threadIdin operation status is the forked thread; the source thread is reported inforkState.sourceThreadId.

codex_archive_thread(thread_id) -> completed codex_unarchive_thread(thread_id) -> completed codex_start_thread_compaction(thread_id) -> actionId codex_get_thread_compaction_status(actionId) -> running / completed / unknown_after_app_server_exit

Archive and unarchive are audit actions around app-serverthread/archiveandthread/unarchive. They refuse to run while the thread has an active turn or a pending interaction. Compaction uses its own lightweightactionIdbecausethread/compact/startis asynchronous. Publicthread/deleteis intentionally not exposed.

codex_submit_task(operation_type="start_chat", message=..., output_schema={...}) codex_approve_plan(workflowId, output_schema={...}) -> operationId / executionOperationId codex_get_operation_status(operationId) codex_get_workflow_status(workflowId) -> finalReport.text + finalReport.structured

output_schemais passed to app-serverturn/startand is tracked by a schema hash in status output. Object schemas must use the strict form required by Codex: setadditionalPropertiestofalse. MCP stores the final assistant message as readable text, then parses JSON object output intofinalReport.structuredwhen Codex returns valid JSON. Plain text still works and stays available infinalReport.text.

MCP does not extract hidden chain-of-thought and does not store raw tool payloads or command output in final reports.

codex_start_plan_workflow -> workflowId codex_get_workflow_status(workflowId) -> wait_plan / review_plan / execute_plan codex_approve_plan(workflowId) -> executionOperationId codex_get_workflow_status(workflowId) -> finalReport

Plan Mode has a runtime floor. The public default write policy is stillread-onlyandon-request, but Plan Mode needs a writable workspace on Windows. If the caller or server default resolves toread-only, MCP sendsworkspace-writetocodex-app-serverand returnsrequestedSandbox,effectiveSandbox, andruntimePolicyAdjustedin workflow and operation status.

Mirror a workflow goal into Codex Desktop when the client has one:

codex_start_plan_workflow(goal="Review the migration plan", goal_completion_action="clear") codex_get_workflow_status(workflowId, refresh_live_goal=true) -> threadGoal.syncState + threadGoal.currentGoal

MCP writes a thread goal only when the client passesgoal. Managed goals useclearafter completion by default. Useset_completeorleavewhen the goal should remain visible after the workflow ends. Normal workflow polling is passive; userefresh_live_goal=trueonly when you want MCP to call live app-server goal methods.

codex_start_review_workflow(thread_id=..., target_type="base_branch", base_branch="main") -> workflowId codex_get_workflow_status(workflowId) -> wait_review / read_review_report

Or let MCP create a service thread for a local checkout:

codex_start_review_workflow(cwd=..., target_type="uncommitted_changes") -> workflowId codex_get_workflow_status(workflowId) -> reviewThreadId + reviewTurnId + finalReport

Review workflows do not write files by themselves. They run inside the selected Codex sandbox and approval policy. Useclient_request_idwhen a caller may retry the start request after a transport timeout.

codex_list_pending_interactions codex_answer_pending_interaction
codex_get_runtime_capabilities codex_health_summary codex_collect_diagnostics codex_analyze_issue codex_repair_issue

Repair actions default todry_run=true.

Status and diagnostic tools also returnagentGuidanceandagentGuidanceTextwhen MCP sees a blocker, failed state, stale run, pending interaction, duplicate prompt, auth problem, rate limit, or unsafe recovery loop. Agents should followagentGuidance.instructionsbefore deciding to retry or stop. IfagentGuidance.loopGuard.allowed=false, stop automatic recovery, collect diagnostics, and ask a human. Do not create a newclient_request_idafter a timeout unless the guidance explicitly says to start a replacement workflow.

For a broken Plan Mode workflow, useretry_workflow_with_runtime_policy. It creates a new workflow with the selected sandbox and approval policy, links it to the old workflow throughworkflowRetryState, and does not revive the old terminal turn.

codex_health_summaryis about current readiness by default. Old stale or orphaned rows are reported inhistoricalDebt, but they do not make fresh orchestration look broken when the worker, queue, and app-server are currently healthy. Use targeted cleanup for that debt instead of blocking new work.

Status payloads now separate freshness signals:

- operationRowAgeSeconds: age of the durable operation row;
- turnFreshness.lastProgressAgeSeconds: age of the last turn progress event;
- workerFreshness.heartbeatAgeSeconds: age of the worker heartbeat;
- stalenessMeaning="operation_row_age"for the compatibilitystalenessSecondsfield.

Public status payloads are agent-safe. Operation and workflow status returnrequestSummaryinstead of rawrequest; it contains ids, runtime policy, scheduling intent, input item state, output schema hash, resource keys, and text hashes. It does not include the full prompt, full instructions, raw title, raw image URL/path, exact token counts, raw command output, or private paths. Use your own stored task text plusrequestSummary.*.sha256for correlation.

codex_get_queue_statusonly recommendswait_for_worker_slotwhen there is actual queued work blocked by slots. If there are running turns butqueueSummary.queued == 0, the queue action isnone.

Usecodex_get_runtime_capabilitiesbefore orchestration or after reconnect. It starts the MCP-owned app-server if needed, calls short best-effort inventory methods, and returns a cached snapshot for five minutes.

Inclientmode, the client process does not start its own app-server for live inventory. It returns a passive worker-managed snapshot when one exists. Withrefresh=true, it queues a worker command and returnsrefreshCommandId; pollcodex_get_worker_command_statusto read the refreshed inventory.

- model count, default model, hidden flags, input modalities, reasoning efforts, and service tier count;
- permission profiles byidanddescription;
- Windows sandbox readiness;
- provider capabilities for web search, image generation, and namespace tools;
- hook and skill counts without raw hook commands or absolute skill paths;
- redacted account status, coarse usage bands, and operational rate-limit state;
- supported app-server schema methods with a compact source, version, and hash.

Account inventory is safe to show to an orchestrator. It reports whether Codex is authenticated, the account and plan type, whether an email exists, whether usage data is available, and whether a rate limit or credits issue is visible. It does not return raw email, account identifiers, credit balances, spend limits, exact spend used, daily usage buckets, or exact token counts.

If one inventory method times out or fails, the tool still returnsok=truewithruntimeCapabilities.status="partial"and a machine-readable warning inmethodResults. Setrefresh=trueto bypass the cache.codex_health_summaryshows a smallruntimeCapabilitiessubset from the last collected snapshot and does not start app-server on its own. Passinclude_account=falsewhen a client does not need account, usage, or rate-limit status.

codex_get_turn_statusandcodex_get_operation_statusinclude a compactprogressEventsblock by default. It captures app-server-visible progress such as assistant text deltas, plan deltas, reasoning summary text, token usage, model reroutes, and warnings.

The journal helps with orchestration and troubleshooting. It does not extract hidden chain-of-thought. It also does not store raw tool payloads, command output, or full unified diffs by default. Diff events are reduced to safe counts, such as changed line count and diff size.

Useprogress_events=0when a client wants the older, message-only status shape. Useprogress_max_charsto cap returned progress text.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.