Skip to content

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-finite
wait_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 ±5
why --tick 8412 → the causal chain that led here
set_variable → try lr = 1e-4 (returns a correlation_id)
effects_of <cid> → what did my change actually do?
resume / step
verify_replay → does the fixed run reproduce deterministically?
export_tuned_config → the config diff to commit
Terminal window
pip install 'omniloop[mcp]'
omniloop mcp # stdio, read-only (default)
omniloop mcp --allow-control # + live mutation, halt/step, watchpoints
omniloop mcp --list-tools # print the surface, start nothing

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

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.

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

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.

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_replay reports cannot_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_variable reads the value back and reports applied: false if the loop is not running what was asked for. A silently clamped write is otherwise indistinguishable from a no-op.
  • omniloop_get_channel_health reports coverage, because an agent that does not know frames were dropped will draw confident conclusions from a subsampled signal.

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.

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.