| name | codex-control-plane-mcp |
| description | Durable MCP control plane for long-running Codex Desktop tasks with retry-safe operations, Plan Mode workflows, and approval handling |
| triggers | ["set up codex control plane for long running tasks","create a durable codex desktop workflow","handle codex plan mode approvals","submit retry safe codex operations","manage codex desktop automation with mcp","poll codex task status and diagnostics","configure codex control plane mcp server","troubleshoot codex desktop operations"] |
codex-control-plane-mcp
Skill by ara.so — MCP Skills collection.
codex-control-plane-mcp is a durable MCP server that turns Codex Desktop into a reliable worker for long-running tasks. It provides retry-safe operations, Plan Mode workflows, approval handling, and comprehensive diagnostics through a simple poll-based API.
Overview
Unlike thin Codex wrappers that block on multi-hour calls or lose state on retry, this control plane provides:
- Durable async operations: Submit a task, get an
operationId immediately, poll until complete
- Retry safety: Same
client_request_id returns existing operation instead of creating duplicates
- Plan Mode workflows: Start plan → poll → approve → execute → read final report
- Approval handling: Pending interactions exposed as pollable MCP state
- Diagnostics: Health checks, issue analysis, and dry-run repairs
- SQLite persistence: Local history of operations, workflows, turns, hooks, and diagnostics
Installation
Using pipx (recommended)
pipx install codex-control-plane-mcp
Using uvx (run directly)
uvx codex-control-plane-mcp
From GitHub
python -m pip install "codex-control-plane-mcp @ git+https://github.com/aresyn/codex-control-plane-mcp.git"
Local development
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
Initial Setup
Generate configuration
codex-control-plane-mcp-admin init --state-db .\state\codex-mcp-state.sqlite3 --projects-root C:\Users\you\Projects
This generates a JSON config block you can add to your MCP client configuration.
Install 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
Important: Restart Codex Desktop after installing or changing hooks.
MCP client configuration
Minimal stdio entry for MCP client config (e.g., claude_desktop_config.json):
{
"mcpServers": {
"codex-control-plane": {
"command": "codex-control-plane-mcp",
"args": [],
"env": {
"CODEX_MCP_STATE_DB": "C:\\Users\\you\\state\\codex-mcp-state.sqlite3",
"CODEX_PROJECTS_ROOT": "C:\\Users\\you\\Projects",
"CODEX_CONTROL_PLANE_MCP_LOG": "C:\\Users\\you\\logs\\codex-mcp.log"
}
}
}
}
Configuration
Configuration via environment variables or CODEX_CONTROL_PLANE_MCP_CONFIG JSON file:
| Variable | Description | Default |
|---|
CODEX_HOME | Codex home directory | %USERPROFILE%\.codex |
CODEX_PROJECTS_ROOT | Project root for catalog/read tools | - |
CODEX_ALLOWED_ROOTS | Semicolon-separated path allowlist | - |
CODEX_PROJECTS_REGISTRY | Optional JSON project registry | - |
CODEX_MCP_STATE_DB | Local MCP state database | - |
CODEX_CONTROL_PLANE_MCP_LOG | Log file path | - |
CODEX_MCP_HOOK_HISTORY_ENABLED | Enable SQLite hook history | true |
Core Workflows
1. Submit a Durable Task
result = mcp_client.call_tool("codex_submit_task", {
"operation_type": "send_message",
"project_id": "my-project",
"prompt": "Refactor the authentication module to use OAuth2",
"client_request_id": "unique-request-id-1",
"wait_for_completion": False
})
operation_id = result["operationId"]
status = mcp_client.call_tool("codex_get_operation_status", {
"operation_id": operation_id
})
2. Steer an Active Turn
Add context to an active turn without creating a new one:
result = mcp_client.call_tool("codex_submit_task", {
"operation_type": "send_message",
"project_id": "my-project",
"prompt": "Add logging to the API handlers",
"client_request_id": "req-1"
})
thread_id = result["threadId"]
turn_id = result["turnId"]
steer_result = mcp_client.call_tool("codex_submit_task", {
"operation_type": "steer_turn",
"thread_id": thread_id,
"expected_turn_id": turn_id,
"message": "Also add error handling for network timeouts",
"client_request_id": "req-2"
})
steer_status = mcp_client.call_tool("codex_get_operation_status", {
"operation_id": steer_result["operationId"]
})
3. Plan Mode Workflow
workflow_result = mcp_client.call_tool("codex_start_plan_workflow", {
"project_id": "my-project",
"prompt": "Migrate database from MySQL to PostgreSQL",
"client_request_id": "plan-req-1"
})
workflow_id = workflow_result["workflowId"]
status = mcp_client.call_tool("codex_get_workflow_status", {
"workflow_id": workflow_id
})
if status["phase"] == "review_plan":
approve_result = mcp_client.call_tool("codex_approve_plan", {
"workflow_id": workflow_id,
"approved": True,
"feedback": None
})
execution_op_id = approve_result["executionOperationId"]
final_status = mcp_client.call_tool("codex_get_workflow_status", {
"workflow_id": workflow_id
})
if final_status["phase"] == "completed":
print(final_status["finalReport"])
4. Handle Pending Approvals
pending = mcp_client.call_tool("codex_list_pending_interactions", {})
for interaction in pending["interactions"]:
if interaction["type"] == "approval_required":
mcp_client.call_tool("codex_answer_pending_interaction", {
"interaction_id": interaction["id"],
"approved": True,
"answer": None
})
5. Runtime Capabilities Check
Always check capabilities on startup or reconnect:
capabilities = mcp_client.call_tool("codex_get_runtime_capabilities", {
"refresh": False
})
print(f"Models: {capabilities['runtimeCapabilities']['modelCount']}")
print(f"Default model: {capabilities['runtimeCapabilities']['defaultModel']}")
print(f"Sandbox ready: {capabilities['runtimeCapabilities']['sandboxReady']}")
print(f"Hooks: {capabilities['runtimeCapabilities']['hookCount']}")
methods = capabilities['runtimeCapabilities']['supportedAppServerMethods']
for method in methods:
print(f"{method['method']} - {method['source']}")
6. Health Summary
Get a quick health check without starting app-server:
health = mcp_client.call_tool("codex_health_summary", {})
print(f"Server: {health['version']['serverName']} v{health['version']['serverVersion']}")
print(f"Contract: {health['version']['contractVersion']}")
print(f"App server: {health['appServer']['status']}")
print(f"State DB: {health['stateDb']['ok']}")
print(f"Hooks: {health['hooks']['installedCount']}")
Diagnostics and Troubleshooting
Collect diagnostics
diagnostics = mcp_client.call_tool("codex_collect_diagnostics", {
"include_runtime_capabilities": True,
"include_recent_operations": True,
"include_app_server_logs": True
})
Analyze issues
analysis = mcp_client.call_tool("codex_analyze_issue", {
"symptom": "operation_timeout",
"context": {
"operation_id": "op-123",
"thread_id": "thread-456"
}
})
print(f"Severity: {analysis['severity']}")
for check in analysis['checks']:
print(f"{check['check']}: {check['status']} - {check['message']}")
Repair issues
repair = mcp_client.call_tool("codex_repair_issue", {
"issue_code": "stale_app_server",
"dry_run": True
})
if repair["ok"]:
actual_repair = mcp_client.call_tool("codex_repair_issue", {
"issue_code": "stale_app_server",
"dry_run": False
})
Progress Tracking
Get detailed progress events from operations:
status = mcp_client.call_tool("codex_get_operation_status", {
"operation_id": "op-123",
"progress_events": 50,
"progress_max_chars": 10000
})
for event in status.get("progressEvents", []):
print(f"[{event['timestamp']}] {event['type']}: {event.get('text', '')}")
if event["type"] == "assistant_text_delta":
print(f" Delta: {event['delta']}")
elif event["type"] == "token_usage":
print(f" Tokens: {event['totalTokens']}")
elif event["type"] == "model_reroute":
print(f" From: {event['fromModel']} → To: {event['toModel']}")
Interrupt Operations
interrupt = mcp_client.call_tool("codex_interrupt_turn", {
"operation_id": "op-123"
})
interrupt = mcp_client.call_tool("codex_interrupt_turn", {
"workflow_id": "wf-456"
})
interrupt = mcp_client.call_tool("codex_interrupt_turn", {
"thread_id": "thread-789",
"turn_id": "turn-012"
})
Reading Chat History
projects = mcp_client.call_tool("codex_list_projects", {})
chats = mcp_client.call_tool("codex_list_project_chats", {
"project_id": "my-project"
})
chat = mcp_client.call_tool("codex_get_chat", {
"project_id": "my-project",
"thread_id": "thread-123"
})
results = mcp_client.call_tool("codex_search_chats", {
"query": "authentication refactor",
"project_id": "my-project",
"limit": 10
})
Error Handling
All tools return structured errors:
result = mcp_client.call_tool("codex_submit_task", {
"operation_type": "send_message",
"project_id": "unknown-project",
"prompt": "Test"
})
if not result["ok"]:
error = result["error"]
print(f"Error: {error['code']}")
print(f"Message: {error['message']}")
print(f"Retryable: {error['retryable']}")
print(f"Details: {error.get('details', {})}")
Common error codes:
OPERATION_NOT_FOUND: Operation ID doesn't exist
WORKFLOW_NOT_FOUND: Workflow ID doesn't exist
INVALID_WORKFLOW_PHASE: Can't perform action in current workflow phase
APP_SERVER_ERROR: App server call failed
INVALID_OPERATION_TYPE: Unknown operation type
DUPLICATE_PROMPT: Active turn already exists with this prompt
Common Patterns
Retry-safe task submission
import uuid
client_request_id = str(uuid.uuid4())
try:
result = mcp_client.call_tool("codex_submit_task", {
"operation_type": "send_message",
"project_id": "my-project",
"prompt": "Implement feature X",
"client_request_id": client_request_id
})
except TimeoutError:
result = mcp_client.call_tool("codex_submit_task", {
"operation_type": "send_message",
"project_id": "my-project",
"prompt": "Implement feature X",
"client_request_id": client_request_id
})
Long-running task polling
import time
operation_id = "op-123"
max_wait = 3600
poll_interval = 5
elapsed = 0
while elapsed < max_wait:
status = mcp_client.call_tool("codex_get_operation_status", {
"operation_id": operation_id
})
state = status["state"]
if state == "completed":
print(f"Success: {status['result']}")
break
elif state == "failed":
print(f"Failed: {status['error']}")
break
elif state == "waiting_for_approval":
pending = mcp_client.call_tool("codex_list_pending_interactions", {})
time.sleep(poll_interval)
elapsed += poll_interval
Plan Mode with feedback
workflow_id = "wf-123"
while True:
status = mcp_client.call_tool("codex_get_workflow_status", {
"workflow_id": workflow_id
})
if status["phase"] == "review_plan":
plan = status["planSummary"]
feedback_result = mcp_client.call_tool("codex_approve_plan", {
"workflow_id": workflow_id,
"approved": False,
"feedback": "Please add database migration rollback steps"
})
continue
if status["phase"] == "review_plan":
mcp_client.call_tool("codex_approve_plan", {
"workflow_id": workflow_id,
"approved": True
})
break
time.sleep(5)
Troubleshooting
App server won't start
status = mcp_client.call_tool("codex_get_app_server_status", {})
if status["status"] != "running":
restart = mcp_client.call_tool("codex_restart_app_server", {})
if not restart["ok"]:
diag = mcp_client.call_tool("codex_collect_diagnostics", {
"include_app_server_logs": True
})
print(diag["appServer"])
Operations stuck in "running"
mcp_client.call_tool("codex_interrupt_turn", {
"operation_id": "stuck-op-id"
})
analysis = mcp_client.call_tool("codex_analyze_issue", {
"symptom": "operation_timeout",
"context": {"operation_id": "stuck-op-id"}
})
Hooks not recording history
# Check hook status
codex-control-plane-mcp-hooks status
# Verify state DB configuration
codex-control-plane-mcp-hooks doctor
# Reinstall hooks if needed
codex-control-plane-mcp-hooks install --state-db .\state\codex-mcp-state.sqlite3
Duplicate turn detection
The control plane prevents creating duplicate turns with the same prompt. If you get a DUPLICATE_PROMPT error:
result = mcp_client.call_tool("codex_submit_task", {
"operation_type": "send_message",
"project_id": "my-project",
"prompt": "Same prompt as before"
})
if not result["ok"] and result["error"]["code"] == "DUPLICATE_PROMPT":
details = result["error"]["details"]
existing_op_id = details["existingOperationId"]
status = mcp_client.call_tool("codex_get_operation_status", {
"operation_id": existing_op_id
})
Best Practices
- Always use
client_request_id for retry safety
- Check capabilities on startup with
codex_get_runtime_capabilities
- Poll, don't block on long operations
- Handle approvals through
codex_list_pending_interactions
- Use Plan Mode for complex, multi-step tasks
- Enable hook history for better diagnostics
- Run in
dry_run mode before applying repairs
- Keep state DB and logs private — they may contain sensitive data
- Restart Codex Desktop after hook installation or changes
- Use
read-only permission for untrusted repositories