codex-monitor

codex-monitor

An MCP server that provides monitoring tools for Codex agents, allowing them to create and wait on conditions using commands, log files, file events, and custom probes, replacing polling loops with event-driven blocking.

Category
访问服务器

README

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:

  1. Create a monitor for a condition.
  2. Block on it.
  3. 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_wait is 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 all or any of a set.
  • Programmable. Drop a .mjs file in ~/.config/codex-monitor/probes/ to add a condition type. No fork required.

Install

npm install -g @naowalrahman/codex-monitor

The 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).

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
monitor_create Register a condition. Returns a monitor id immediately, or blocks until it settles when wait_timeout_seconds is set.
monitor_wait Blocks until the listed monitors settle (mode: all|any). A wait_timeout returns control; the monitors keep running.
monitor_status Non-blocking snapshot (for a quick look, not for polling).
monitor_cancel Settle an active monitor as cancelled.

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:

  • command is 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.
  • file and log exist 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 command condition or a custom probe. Ready-made http, process, and tcp probes 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 test

MIT 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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选