codex-monitor
This server provides tools to create and manage background monitors that watch for conditions, allowing agents to block until long-running tasks complete without polling.
Create monitors (
monitor_create): Register condition watchers forcommand(shell command with exit code/regex predicates),file(filesystem events: exists, deleted, changed, stable), andlog(tail and match regex patterns). All support custom polling, timeouts, and environment variables.Wait for monitors (
monitor_wait): Block until one (any) or all (all) monitors reach a terminal state (satisfied, failed, timeout, cancelled) with an optionalwait_timeout_seconds. Emits progress notifications every 15s.Create and wait (
monitor_run): Convenience tool that combines creation and blocking wait into a single call.Check status (
monitor_status): Get a non-blocking snapshot of one or all monitors.Cancel monitors (
monitor_cancel): Stop an active monitor with an optional reason.Custom probes: Extend condition types via user-installed
.mjsplugins (e.g., HTTP, TCP, process).
Key behaviors:
Event-driven: File and log conditions use filesystem notifications, reducing polling.
Adaptive backoff: Command conditions poll with configurable intervals and jitter.
Session-scoped: Monitors live and die with the server process.
Blocking by design:
monitor_waitpauses execution until conditions settle, avoiding polling loops.
Monitors Docker containers and their health statuses, enabling agents to wait for a container to become healthy or detect unhealthy states.
Monitors GitHub Actions workflow runs, enabling agents to wait for a run to finish and check whether it succeeded or failed.
Monitors Kubernetes resources and rollouts, allowing agents to wait for a deployment rollout to complete successfully.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@codex-monitor wait for the training job to finish"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
codex-monitor
Claude Code-style monitors for Codex: pause the agent until an arbitrary condition becomes true.
Agents watching long-running work (cluster jobs, builds, deploys, training runs) usually degenerate into sleep 30 && squeue ... loops that burn tokens, spam the transcript, and wake the model dozens of times to learn nothing. openai/codex#13733 tracks the problem. codex-monitor replaces the loop with three steps:
Create a monitor for a condition.
Block on it.
Wake up exactly once.
Command-agnostic. Conditions are declarative: a shell command plus output predicates, a log regex, a file event, plus any probe type you install. Nothing is hardcoded for Slurm, Docker, or Kubernetes, yet all of them work out of the box.
Event-driven. File and log conditions use filesystem notifications; probed conditions use adaptive backoff with jitter. Evaluation happens entirely inside the plugin process. The model never writes a polling loop.
Blocking by design.
monitor_waitis one MCP tool call that does not return until the condition settles. That is the pause.Session-scoped by design. Monitors live and die with the Codex session that created them.
Concurrent and composable. Run any number of monitors; wait for
alloranyof a set.Programmable. Drop a
.mjsfile in~/.config/codex-monitor/probes/to add a condition type. No fork required.
Install
npm install -g @naowalrahman/codex-monitorThe package is scoped, but the binary it installs is plain codex-monitor.
Then register it in ~/.codex/config.toml:
[mcp_servers.monitor]
command = "codex-monitor"
# monitor_wait blocks on purpose. Give the tool call room to block.
tool_timeout_sec = 86400
startup_timeout_sec = 20(Or skip the global install and use command = "npx", args = ["-y", "@naowalrahman/codex-monitor"].)
Finally, teach the agent to reach for monitors by adding this to your AGENTS.md:
## Waiting for long-running work
Never wait for long-running work (jobs, builds, deploys, servers, downloads)
by sleeping and re-checking in a loop. Instead use the `monitor` MCP tools:
create a monitor describing the completion/failure condition, then call
`monitor_wait`, which blocks until the condition settles and returns evidence.
Prefer `log`/`file` conditions when output is written to disk (they are
event-driven), and `command` conditions with `success_when`/`failure_when`
predicates for anything with a CLI (squeue, docker, kubectl, gh run).Related MCP server: mcp-codex-dev
The model, in 30 seconds
A monitor is a condition plus an evaluation policy plus a lifecycle:
┌────────────────────────── settles once ──────────────────────────┐
active ─┤ satisfied the condition became true │
│ failed a failure predicate matched (job crashed, etc.) │
│ timeout the monitor's own deadline passed │
│ cancelled monitor_cancel │
└──────────────────────────────────────────────────────────────────┘The agent sees four tools:
Tool | Behavior |
| Register a condition. Returns a monitor id immediately, or blocks until it settles when |
| Blocks until the listed monitors settle ( |
| Non-blocking snapshot (for a quick look, not for polling). |
| Settle an active monitor as |
Every tool returns the same { monitors, outcome?, hint? } shape as compact JSON.
The surface is four tools rather than five on purpose. MCP re-sends every tool schema to the model on each request and cannot share schemas between tools, so a separate create-and-wait tool would repeat the entire condition union, about 2.8kB of JSON Schema, for one saved round trip. Folding it into monitor_create as a flag cut the surface from ~10.2kB to ~6.3kB. test/server.test.ts holds that budget.
Condition types
The core ships three, and the split is deliberate:
commandis the universal sampling adapter: run a program, apply predicates to the result, forget. Anything with a CLI is monitorable this way, which is what makes the system command-agnostic.fileandlogexist because sampling cannot express them. They hold state across evaluations (a log tail tracks a byte offset so it only matches newly appended content; "unchanged for 10s" spans multiple observations) and they are event-driven (fs.watch wakes them in milliseconds, not on the next poll boundary). In a command-only world that state would live in the model's context, which is what this plugin exists to prevent.Everything else, including HTTP readiness, PID exit, TCP ports, and queue depths, is stateless sampling. Write it as a
commandcondition or a custom probe. Ready-madehttp,process, andtcpprobes ship in examples/probes.
command: the universal adapter
Runs a shell command per evaluation (with backoff, inside the plugin) and applies declarative predicates to its exit code and combined output. This is how you monitor anything with a CLI:
// Slurm job, distinguishing success from failure
{
"name": "slurm job 812345",
"condition": {
"type": "command",
"command": "sacct -j 812345 -n -o State | head -1",
"success_when": { "output_matches": "COMPLETED" },
"failure_when": { "output_matches": "FAILED|CANCELLED|TIMEOUT|OUT_OF_ME" },
},
"poll": { "interval_seconds": 15, "max_interval_seconds": 120 },
"timeout_seconds": 43200,
}// Docker container becomes healthy
{
"type": "command",
"command": "docker inspect -f '{{.State.Health.Status}}' api",
"success_when": { "output_matches": "healthy" },
"failure_when": { "output_matches": "unhealthy" },
}// Kubernetes rollout finished
{
"type": "command",
"command": "kubectl rollout status deploy/web --timeout=1s",
"success_when": { "exit_code": 0 },
}// GitHub Actions run finished
{
"type": "command",
"command": "gh run view 123456789 --json status,conclusion -q '.status + \" \" + .conclusion'",
"success_when": { "output_matches": "completed success" },
"failure_when": {
"output_matches": "completed (failure|cancelled|timed_out)",
},
}Predicates: exit_code (int or list), output_matches, output_not_matches (regexes). All present fields must hold, and failure_when is checked before success_when.
log: event-driven regex tail
Tails a file by byte offset (cheap on huge logs, survives rotation and truncation) and settles when appended content matches:
{
"type": "log",
"path": "/data/run7/train.log",
"pattern": "epoch 100/100 .* val_loss",
"failure_pattern": "Traceback|CUDA out of memory",
}file: filesystem events
exists (appears), deleted (gone), changed (mtime or size moved after the monitor started), stable (unchanged for stable_seconds, which is how you catch "download finished"):
{
"type": "file",
"path": "/results/model.safetensors",
"event": "stable",
"stable_seconds": 10,
}Lifetime and scope
A monitor belongs to the session that created it. Codex spawns one codex-monitor server per session; monitors are held in that process's memory, and when the session ends the server exits and every monitor dies with it. This is deliberate. It keeps the mental model exact (what you see in monitor_status is exactly what exists), it makes unlimited concurrent sessions safe by construction, and it leaves nothing on disk. If a job outlives your session, recreate the monitor in the next one; the underlying job is the durable thing, not the watcher. The only thing in the config directory is your custom probes.
Custom probes
Drop a module in ~/.config/codex-monitor/probes/. To install the ready-made ones:
mkdir -p "$(codex-monitor home)/probes" && cp examples/probes/http.mjs "$(codex-monitor home)/probes/"The config directory resolves in this order: $CODEX_MONITOR_HOME, then %APPDATA%\codex-monitor on Windows, then $XDG_CONFIG_HOME/codex-monitor, then ~/.config/codex-monitor. Run codex-monitor home to print what it resolved to.
Custom condition types pass schema validation with their fields untouched, since the probe factory owns validation and defaults, and they become creatable through the same MCP tools immediately. A complete probe:
// ~/.config/codex-monitor/probes/tcp.mjs
export default {
type: "tcp",
create: (cond) => ({
defaultPoll: { interval_seconds: 1, max_interval_seconds: 15 },
async check() {
const net = await import("node:net");
return new Promise((resolve) => {
const sock = net.connect({
host: cond.host,
port: cond.port,
timeout: 2000,
});
sock.on("connect", () => {
sock.destroy();
resolve({
status: "satisfied",
detail: `${cond.host}:${cond.port} accepting connections`,
});
});
sock.on("error", () =>
resolve({ status: "pending", detail: "connection refused" }),
);
sock.on("timeout", () => {
sock.destroy();
resolve({ status: "pending", detail: "connect timeout" });
});
});
},
}),
};A probe implements check() (which the engine schedules with backoff) and/or start(host)/stop() (event-driven, pushing outcomes via host.emit). See docs/ARCHITECTURE.md and examples/probes.
CLI
codex-monitor # start the MCP server on stdio (what Codex runs)
codex-monitor home # print the config directory (custom probes: <home>/probes)Why blocking tool calls (and the timeout caveat)
MCP has no "call the model back later" primitive, so the only way to genuinely pause an agent mid-task is a tool call that does not return. monitor_wait embraces that. It emits MCP progress notifications every 15s while blocked, and you should set tool_timeout_sec generously for this server (see Install). If a wait does get cut off, by a client timeout or by wait_timeout_seconds, nothing is lost: the monitor is still running, or already settled with the result held in monitor_status, and one more monitor_wait on the same id resumes the pause. That retry is a resume, not a poll loop.
Limitations and roadmap
Monitors do not outlive their session, by design (see Lifetime and scope). If you need watchers that keep evaluating with no session open, that is a job for a real scheduler.
Composite conditions are covered by
monitor_wait(mode=any|all)over multiple monitors. Inline boolean condition algebra is future work.Desktop notifications and webhooks on settle are future work.
Development
npm install
npm run build
npm testMIT licensed. Contributions welcome. New built-in probe types should be generic: no tool-specific integrations, since that is what command and custom probes are for.
Available Tools
5 toolsmonitor_cancelCancel monitorA
Stop an active monitor. Settled monitors are left untouched.
Inputs: id (required), reason.
monitor_createCreate monitorA
Create a background monitor that watches for a condition and settles when it becomes true. Evaluation happens inside the plugin, so never write your own sleep/poll loop. Returns a monitor id; pass it to monitor_wait to pause until the condition holds. Use this for anything long-running: cluster jobs, builds, deploys, downloads, servers coming up. Installed condition types: command, file, log.
Inputs: condition (required), name, poll, timeout_seconds.
monitor_runCreate monitor and waitA
Convenience: monitor_create + monitor_wait in one blocking call. Creates the monitor and pauses until it settles (or wait_timeout_seconds elapses, in which case the monitor keeps running and you can monitor_wait on the returned id later).
Inputs: condition (required), name, poll, timeout_seconds, +1 more.
monitor_statusMonitor statusA
Snapshot of monitors without blocking. Omit ids for all monitors. Use for a quick look; use monitor_wait (not repeated status calls) to wait for completion.
Inputs: ids.
monitor_waitWait for monitorsA
Block until the given monitor(s) settle (satisfied, failed, timeout, or cancelled). mode 'all' waits for every listed monitor; 'any' returns as soon as one settles. If wait_timeout_seconds elapses first, returns outcome 'wait_timeout'. Monitors keep running whenever a wait ends early, including when the call is interrupted, so call monitor_wait again with the same ids to resume.
Inputs: ids (required), mode, wait_timeout_seconds.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 5 tools
Each tool has a clearly distinct role in the monitor lifecycle: create, wait, run, status, and cancel. There is no overlap—monitor_run combines create and wait, but it is a convenience wrapper rather than a competing tool.
All tool names follow the same `monitor_<verb>` pattern, making the action and resource predictable. The verbs are consistent and match the function of each tool.
Five tools cover the full monitor workflow without bloat or missing essentials. This is a well-scoped set for a monitoring utility.
The set provides complete lifecycle coverage: create, wait, cancel, status, and a combined create-and-wait convenience. There are no obvious dead ends—a user can create, monitor, and clean up monitors with these tools.
Maintenance
Related MCP Connectors
MCP server for building and testing AI agents with multi-model experimentation and insights.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
The MCP server for Azure DevOps, bringing the power of Azure DevOps directly to your agents.
MCP server for Uptrack uptime monitoring. Manage monitors and incidents from AI agents.
Related MCP Servers
- AlicenseAqualityDmaintenanceAn MCP server that enables coding agents to execute and manage long-running shell commands asynchronously with capabilities for process monitoring, interaction, and lifecycle management.730 PyPI14MIT
- AlicenseAqualityDmaintenanceAn MCP server that integrates Codex CLI into Claude Code workflows for code writing, execution, and review with session management. It features real-time progress monitoring via a local HTTP dashboard and supports detailed configuration for various coding tools.611 npm166MIT
- AlicenseAqualityBmaintenanceAn MCP server that wraps the OpenAI Codex SDK to deploy multiple specialized AI agents with individual configurations for models, sandboxing, and behavior. It enables users to manage dedicated tools for tasks like code review and test writing through a customizable agent factory.264 npm20ISC
- AlicenseNot gradedqualityCmaintenanceMCP server that enables AI coding agents to communicate, share state, and coordinate work in real time via MCP tools or REST API.246 npm9MIT