MCP Server
OmniLoop ships a Model Context Protocol server, so an AI agent (Claude, Cursor, Windsurf, or any MCP host) can drive a live control loop without a human at the dashboard.
The bar is not “the dashboard, but callable”. It is that an agent can close the debugging loop on its own: observe a failure, localise it to a line of source, form a hypothesis, test it live, verify the fix reproduces, and hand back a config diff. The canonical session it is built for:
describe_params → what can I touch, and within what ranges?set_watchpoint → halt the instant reward goes non-finitewait_for_halt → (blocks; no polling)get_halt_context → tripped on `reward` @ tick 8412; here is the state, the exception, the file, the line, the source ±5why --tick 8412 → the causal chain that led hereset_variable → try lr = 1e-4 (returns a correlation_id)effects_of <cid> → what did my change actually do?resume / stepverify_replay → does the fixed run reproduce deterministically?export_tuned_config → the config diff to commitInstall & run
Section titled “Install & run”pip install 'omniloop[mcp]'omniloop mcp # stdio, read-only (default)omniloop mcp --allow-control # + live mutation, halt/step, watchpointsomniloop mcp --list-tools # print the surface, start nothingomniloop mcp speaks JSON-RPC over stdin/stdout and is meant to be launched by
an MCP host, not run in a terminal. Configure it like any other stdio server:
{ "mcpServers": { "omniloop": { "command": "python", "args": ["-m", "omniloop.mcp", "--allow-control"], "env": { "OMNILOOP_SESSION_ID": "my_run" } } }}The live tools talk to the relay started by omniloop up; journal tools need
nothing running at all.
Operating modes
Section titled “Operating modes”| Mode | Behaviour |
|---|---|
observe (default) |
Read-only. Every tool except mutation, halt/resume/step, watchpoints, bounds and reset. |
control |
The full surface. Opt in with --allow-control or OMNILOOP_MCP_MODE=control. |
journal |
Journal tools only. Never touches shared memory or the relay. |
The mode decides which tools exist, not which ones refuse when called: a tool the agent cannot see is one it cannot be talked into using.
Defaulting to observe is deliberate. The failure mode of a misconfigured agent
with write access to a robot control loop is not a bad commit.
Configuration
Section titled “Configuration”| Variable | Default | Purpose |
|---|---|---|
OMNILOOP_MCP_MODE |
observe |
observe, control, or journal. |
OMNILOOP_MCP_ALLOW_CONTROL |
unset | 1 is shorthand for control mode. |
OMNILOOP_MCP_SERVER_URL |
ws://127.0.0.1:8000/ws |
Relay to drive. An http:// URL is accepted. |
OMNILOOP_MCP_TIMEOUT |
15 |
Ceiling in seconds on one relay round trip. |
OMNILOOP_AUTH_TOKEN |
unset | Required when the relay was started with one. |
OMNILOOP_SESSION_ID |
default |
Which shared-memory session the relay reads. |
Orient — what am I looking at?
Section titled “Orient — what am I looking at?”| Tool | Description |
|---|---|
omniloop_list_sessions |
Live / stale / absent channels with publisher PIDs. Call this first. Surfaces the two biggest time-wasters: two loops publishing into one session, and a stale channel that reads like a running loop because shared memory retains the last frame forever. |
omniloop_describe_params |
The declared parameter schema — name, type, kind, min/max/step, default, group. The entry point that turns blind mutation into informed mutation. |
omniloop_describe_run |
Journal summary: records by kind, event histogram, tick span, duration, segments, hash checkpoints, recovered tuned config, integrity. |
omniloop_get_source_map |
The files the instrumented loop was built from, plus the execution graph and active node. Live, or from a journal. |
Observe — what is it doing?
Section titled “Observe — what is it doing?”| Tool | Description |
|---|---|
omniloop_get_state |
The current telemetry frame. Unfiltered, answers with a summary; pass names for exact values. Reports age_seconds, so a stale channel is visible. |
omniloop_wait_for_halt |
Blocks until the loop halts, then returns the full halt context. Timeout-bounded, reports MCP progress. |
omniloop_get_halt_context |
Everything about why the loop is frozen in one call: tripped watchpoint, exception type and message, file, line, the ±5 lines of source, the traceback recovered from the journal, the state frame, and candidate blackbox dumps. |
omniloop_summarize_window |
Per-variable statistics over a tick range — min/max/mean/stdev, trend, first non-finite tick, largest jumps. The shape of a divergence instead of 400 frames of numbers. |
Control — change it (control mode only)
Section titled “Control — change it (control mode only)”| Tool | Description |
|---|---|
omniloop_set_variable |
Change one variable. Validated against the declared schema first; returns the correlation_id; reports applied: false with the clamp when the loop is not running the requested value. |
omniloop_set_variables |
Several variables under one correlation id — tuning a gain triple as three calls is three different experiments. |
omniloop_halt / omniloop_resume / omniloop_step |
Freeze at the next tick barrier, release it, or advance exactly one tick. Each confirms the transition from telemetry rather than assuming it. |
omniloop_set_watchpoint / omniloop_clear_watchpoint |
Arm or disarm a conditional-halt tripwire (non_finite, less_than, greater_than, deadline_overshoot). |
omniloop_set_bounds |
Register a hard [min, max] safety envelope — installed in the target’s core (which clamps every write into registered memory) and as a relay-side refusal. Reports target_confirmed. |
omniloop_reset |
Drop pending mutations and restore declared defaults — the undo after a bad experiment. |
Diagnose — why did that happen?
Section titled “Diagnose — why did that happen?”| Tool | Description |
|---|---|
omniloop_why |
The causal chain leading to a tick: the mutations, control and lifecycle events that explain it. |
omniloop_effects_of |
Given a correlation_id, what followed — how an agent attributes consequences to its own change. |
omniloop_query_journal |
Bounded event slice by kind and tick range. |
omniloop_diff |
First tick where two runs’ state hashes disagree, with how many checkpoints they actually share. |
omniloop_state_at_tick |
State reconstructed at an arbitrary tick, for A/B against a divergence. |
omniloop_search_journal |
The first tick a predicate holds (non_finite, less_than, greater_than), evaluated offline — one call instead of a hand-rolled binary search. |
Real-time & integrity
Section titled “Real-time & integrity”| Tool | Description |
|---|---|
omniloop_analyze_timing |
Tick-duration distribution, jitter, and deadline overshoots over a recorded run. |
omniloop_analyze_timing_live |
The same over the live window, labelled with how much of the loop the poll actually saw. |
omniloop_list_watchpoints |
Which tripwires are armed: name (a variable, a prefix_* group, or *), condition, threshold, latched, and members. The safety counterpart to describe_params — that says what you may change, this says what will stop the loop if you change it badly. |
omniloop_get_channel_health |
IPC counters plus coverage — the fraction of published frames the server read. Low coverage means every live observation is a sample. |
omniloop_get_clamps |
Reads active safety bounds and target-enforced limits (bounds vs target_bounds). |
omniloop_dump_flight_recorder |
Capture a black box right now — “this looks wrong, snapshot it” — without waiting for an exception. Asks the target for its own ring; falls back to the relay’s observed frames and says which it returned. |
Verify & extract
Section titled “Verify & extract”| Tool | Description |
|---|---|
omniloop_verify_replay |
Determinism between a run and a re-run. Read verdict, not the divergence count. |
omniloop_export_tuned_config |
The final tuned values from the mutation trail — the deliverable of a tuning session. |
omniloop_export_mcap |
Hand the run to a Foxglove/MCAP pipeline. |
Resources and prompts
Section titled “Resources and prompts”Addressable content, so an agent can pull it into context without spending a tool call:
| Resource | Content |
|---|---|
omni://schema |
The declared parameter schema |
omni://session/current/state |
The latest live telemetry frame |
omni://journal/{path}/summary |
describe_run for a journal |
omni://journal/{path}/events |
A bounded event slice |
omni://source/{path} |
A source file the loop was built from |
Prompts for the workflows that are otherwise five tool calls of boilerplate:
debug_frozen_loop, tune_parameter, compare_two_runs, verify_determinism.
What this surface promises
Section titled “What this surface promises”Five rules bind every tool, and they are why the output looks the way it does.
Discovery before action. omniloop_describe_params is the entry point. An
agent should never guess a variable name, type, or safe range.
Block, don’t poll. omniloop_wait_for_halt blocks inside the relay, next to
the telemetry that resolves it. Polling a 1 kHz loop from an agent is
unaffordable in both latency and tokens.
Bounded output by default. Every reader takes names / tick ranges /
limit, and answers with a summary rather than a dump.
Attribution. Every mutating tool returns its correlation_id, so
omniloop_effects_of can tell the agent what its own change caused.
Fail honestly. Partial journals, unverifiable replays, subsampled telemetry and clamped mutations are reported as such:
- A journal recovered from damage is
integrity.partial, in every tool that reads it. omniloop_verify_replayreportscannot_verify— not “diverged at tick 0” — when every checkpoint disagreed. That flag means the comparison itself is invalid (different program, different hash rule, non-reproducible loop), and an agent told “diverged at tick 0” will confidently debug a bug that does not exist.omniloop_set_variablereads the value back and reportsapplied: falseif the loop is not running what was asked for. A silently clamped write is otherwise indistinguishable from a no-op.omniloop_get_channel_healthreportscoverage, because an agent that does not know frames were dropped will draw confident conclusions from a subsampled signal.
Security
Section titled “Security”The MCP server inherits the relay’s trust boundary: loopback only, with the same
OMNILOOP_AUTH_TOKEN when one is configured. Tool calls are stamped with a
Principal like any other mutation, so journal events record which agent
changed what.
Deferred
Section titled “Deferred”omniloop_list_watchpoints used to be on this list, on the grounds that
get_halt_context reports the trip and the agent armed the tripwires itself.
That reasoning held for one agent in one uninterrupted session and failed
everywhere else — across a restart, across a human-then-agent handover, and on
any machine where “which safety conditions are live” has a physical answer. It
is the same argument that gave limits their readback, so it now ships.
| Tool | Why |
|---|---|
omniloop_fork_timeline |
Spawns subprocesses. The riskiest surface to hand an unattended agent for the least clear payoff. |
omniloop_trigger_exception |
Deliberate fault injection; same reasoning as fork. |
omniloop_suggest_params |
Sequencing an optimiser through an agent is a design problem in itself. |
Dashboard cursor concepts (go_to_line, set_execution_node) and the stateful
replay cursor (start_replay / get_frame / stop_replay) are deliberately
not exposed: the first move a human’s viewport and mean nothing to an agent,
and the second is superseded by the stateless omniloop_state_at_tick and
omniloop_query_journal.