Driving RouterOS with centrs

SkillFiles & storage

Use whenever a task touches a real MikroTik RouterOS device or CHR: reading or changing config, running a RouterOS CLI command non-interactively, checking a command is well-formed before sending it, moving files on or off a device, discovering neighbors, or doing the same thing across several routers. Reach for centrs (@tikoci/centrs, CLI + Bun/TS) rather than hand-rolling curl against /rest or scripting ssh: it resolves the router's address, credentials, port and protocol from your WinBox address book or a quickchr VM, validates the RouterOS command before it runs, and returns one structured envelope (data, warnings, tips, meta) whatever transport carried it - REST, native API, SSH/SFTP, MAC-Telnet, MNDP. Also covers the parts agents routinely miss: offline `explain`, `--json`, fan-out, the `--yes` write gate, and how to read a rejection. Not for booting the CHR itself (routeros-quickchr-cli from a shell, routeros-quickchr for the library) or for RouterOS documentation (rosetta MCP, routeros-fundamentals).

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Driving RouterOS with centrs skill

What this skill tells your AI

The instructions your AI receives, as published by tikoci/routeros-skills in routeros-centrs/SKILL.md and read by ahel’s review.

What this is for

@tikoci/centrs is a friendly conduit to RouterOS, not a configuration abstraction. You still speak RouterOS; centrs handles the parts that are tedious and error-prone to do by hand:

  • resolving <router> to an address, credentials, port, and a protocol;
  • validating a RouterOS-shaped command before it runs;
  • returning the same structured envelope whatever transport carried the call.

Validation and structured diagnostics are the product. Without them it would be a worse curl.

Reach for centrs when you need to run a RouterOS command, read state, or move a file — from a shell script, a test, or an agent loop.

Don't reach for it when you need RouterOS documentation (use the rosetta MCP or the routeros-fundamentals skill), or when you need to create the router itself (use routeros-quickchr-cli from a shell, or routeros-quickchr for the TypeScript library — centrs consumes a quickchr VM, it does not boot one).

Status: 0.1.x preview under active development. The repo README publishes preview builds under npm's next tag, so pin @next rather than assuming latest tracks them — confirm with npm view @tikoci/centrs dist-tags. Expect breaking changes before 1.0 and exercise writes on lab or disposable targets first. docs/MATRIX.md is the single source of truth for what works today; treat anything not green there as not-yet-shipped.

Install and first call

bunx @tikoci/centrs@next --help                 # no install
bun add @tikoci/centrs@next                     # library + local CLI

The offline analyzer needs no router and no credentials — it is the cheapest way to confirm centrs works at all:

bunx @tikoci/centrs@next explain '/ip/address print' --json

The loop: explain → run

This is the pattern centrs exists for. Analyze the command offline, then run it.

# 1. Is this well-formed, and what will it actually do?
centrs explain '/ip/address/add address=192.0.2.1/24 interface=ether2'

# 2. Run it. Writes need --yes when there is no TTY to prompt on.
centrs execute lab --yes '/ip/address/add address=192.0.2.1/24 interface=ether2'

# 3. Read it back as data.
centrs retrieve lab /ip/address --json

explain is offline: it opens no connection, canonicalizes the input, reports structure and syntax diagnostics, and tells you which centrs command would carry it. It is also the first thing to run when a command is rejected — see When a command is rejected.

explain is deliberately conservative: a pass verdict means centrs found nothing wrong offline, never that RouterOS will accept it. The envelope says so itself with runtimeAcceptance: "not-proven". Live device probes are designed, not shipped.

Naming a router

Every router-facing command resolves a target. Four ways, in the order you will want them:

You haveUse
A CHR booted by quickchr--quickchr <name> — resolves host, port, and credentials from the live VM; no credential plumbing at all
A router saved in WinBoxthe positional <router> — centrs reads ~/.config/tikoci/winbox.cdb (the WinBox address book is the device registry; centrs keeps no store of its own)
Neither--host / --port / --username / --password, or CENTRS_USERNAME / CENTRS_PASSWORD
Nothing yetcentrs discover over MNDP, --save to write found neighbors into the CDB

--quickchr is exclusive of positional targets and of --host/--port/--username/ --password; pick one mechanism per call.

Fan out instead of looping

--quickchr repeats, and the CDB selectors fan out too. Prefer these to a shell loop — one envelope, bounded concurrency, per-target errors that do not abort the rest:

centrs retrieve --quickchr sun --quickchr earth --quickchr comet /system/resource --json
centrs execute --group edge --yes '/system/ntp/client/set enabled=yes'
centrs retrieve --all /system/resource --json

Fan-out selectors: --group <name>, --all, --where <attr>=<value>, --near, --bbox. --default is a target selector too, but it picks the single reserved __default__ record rather than fanning out.

The command map

Twelve commands. The three obvious ones are not the whole surface — explain, api, and discover are the ones most callers never find.

CommandPurposeState
retrieve <router> <path>Read RouterOS stateshipped (REST, native API)
execute <router> '<cli>'Run a RouterOS CLI-shaped read/writeshipped (REST, native API, SSH, MAC-Telnet)
api <router> <endpoint>Structured passthrough, gh api style: -X PUT, -f k=v, --query, --streamshipped (REST, native API)
explain '<input>'Offline analysis of a RouterOS commandshipped offline; live probes designed
transfer <router> upload|download|list|remove|mkdir|copyDevice filesshipped (REST, native API, SFTP)
terminal <router>Interactive consoleshipped (SSH, MAC-Telnet)
discoverMNDP neighbor discovery, --save into the CDBshipped
devicesThe CDB device registry, and the atomic write layer every CDB mutation routes through — including discover --saveshipped
settingscentrs's own preferences (centrs.env)shipped
mcpScoped stdio MCP server, CDB-gatedshipped
btestMikroTik bandwidth test, client or servershipped
checkReachability + health batterydesigned only — not implemented

retrieve vs execute vs api: retrieve reads a menu; api is the structured operation surface (it can write); execute takes a literal RouterOS CLI string and is the only one that reaches SSH and MAC-Telnet. There is no update command — CLI-shaped writes ride execute.

Read the envelope, not the text

Every call returns one shape, whatever the transport (the one exception is api --raw, which deliberately strips the envelope and emits bare RouterOS JSON):

{
  "ok": true,
  "data": [ /* the payload */ ],
  "warnings": [],            // always present; non-fatal anomalies about this result
  "tips": [],                // always present; advice that is NOT a problem
  "meta": {
    "target": {},            // resolved target + where each field came from
    "via": "rest-api",       // the protocol actually chosen
    "settings": {},          // which setting won, and from which source
    "validation": {},        // validator name + result, if validation ran
    "operation": { "objectCount": 3 }
  }
}

Pass --json (or --format json). This is the single most-missed thing about centrs. retrieve and execute default to --format text, and their text output renders data as pretty-printed JSON — so piping the default straight into a JSON parser appears to work while silently discarding warnings, tips, and all of meta. (api already defaults to JSON.)

Two consequences worth internalizing:

  • meta.operation.objectCount is the reliable row count. data itself is shape-unstable today: zero rows come back as an empty object, one row as a bare object, and N rows as an array (centrs#360). All three need handling, and the empty object is the one that bites — it is truthy, so the obvious d ? [d] : [] invents a row that does not exist, exactly during the failover or empty-menu read you were measuring. Take the count from meta, and normalize with something that excludes it:

    const rows = Array.isArray(d)
      ? d
      : d && typeof d === "object" && Object.keys(d).length > 0
        ? [d]
        : [];
    
  • tips and warnings are separate channels on purpose. A tip is explicitly not a problem; do not treat a non-empty tips array as a failure.

Writing

Write-shaped commands are gated. With a TTY, centrs prompts; without one it refuses:

$ centrs execute lab '/ip/address/add address=192.0.2.1/24 interface=ether2'
[usage/confirmation-required] Write-shaped RouterOS execute commands require explicit confirmation.
Fix: Pass `--yes` in non-interactive automation, or answer `yes` at the TTY prompt after reviewing the command.

So: a non-interactive execute or api write needs --yes, as does a mutating transfer fan-out across several routers. Overwriting an existing file is a separate gate — transfer --force / --overwrite, not --yes. Never disable validation to make a write succeed: validation is the product, and --no-validate should be a deliberate, explained choice, not a workaround.

Files

centrs transfer <router> upload ./routeros-7.24.4.npk
centrs transfer <router> list
centrs transfer <router> download flash/backup.rsc ./backup.rsc

RouterOS's /file contents write caps at 60 KB, so REST and native API cannot carry a large upload. With no --via pinned, centrs already notices the size and auto-selects SFTP — you do not need to pick the transport. On a device with no SSH key installed that auto-hop can fail on authentication rather than on size: the SSH clients run with BatchMode=yes, so an empty or password-only credential is not usable there even though REST accepts it.

When a command is rejected

Validation is two stages, and which one rejected you is the whole diagnosis:

  1. Offline — the same analyzer explain uses runs first, with no connection. A syntax fault is refused here with the offending byte span, and its remediation tells you to run centrs explain to see it in context. No round trip happens.
  2. Device — :parse plus /console/inspect on the router, for the semantic half offline analysis cannot decide.

--validate=false disables both. A clean offline pass is necessary, never sufficient.

The device stage is where the error text is thin, and the failure mode is worth knowing because it costs agents real time:

$ centrs execute lab '/container/print'
[validation/syntax] RouterOS rejected the command syntax while parsing it.
Fix: Fix the RouterOS CLI syntax (quotes, brackets, attribute form), then retry.

That remediation is often wrong. validation/syntax is what you get when the path does not exist for any reason — including a menu whose package is not installed or whose device-mode feature is off (centrs#361). Re-quoting a command that has no quoting problem is an infinite loop. When you see it:

  1. Run centrs explain '<the same command>'. If it passes offline, the input is well-formed and the problem is the device, not the string — and since stage 1 already ran the same analyzer, a rejection you received from a router is by definition one offline analysis let through. Re-quoting cannot help.
  2. Check the device — centrs retrieve <router> /system/package --json, then centrs retrieve <router> /system/device-mode --json. A missing container, zerotier, or wireless package is the usual answer.
  3. Only then suspect the syntax — and use explain's diagnostics rather than guessing.

Re-run with --verbose to get the error context, which carries the device's own :parse output; the default two-line render drops it, along with the per-code details URL and the (line N column M) position (centrs#362). Every error code has a page at https://tikoci.github.io/centrs/errors/<code>.

Known rough edges

Behaviors to plan around rather than debug from scratch. Each links to the tracking issue — if you hit one, add evidence there rather than working around it silently.

  • A hang can be the success path. /system/device-mode/update container=yes blocks by design while RouterOS waits for a hard power-cycle to confirm. centrs reports that as a plain timeout on every transport (centrs#363). For a CHR, set device-mode at VM creation and let quickchr do the power-cycle. The same applies to /system/reboot, package installs, and unbounded /ping.
  • No wait primitive. There is no --wait / --until; readiness and convergence polling is the caller's job today (centrs#364). Poll a cheap read (retrieve <router> /system/resource) against a wall-clock deadline, and do not silence its errors — "not converged" and "centrs failed" must stay distinguishable.
  • Output can contain secrets. centrs redacts credentials it holds; it has no notion of secrets RouterOS returns (centrs#359). /file print detail can inline private keys and API tokens, and /export show-sensitive=yes is one word from the safe default. Prefer targeted reads over broad dumps, especially through MCP, where the output leaves the machine.
  • REST timeouts cap at 60 s. --timeout above that is rejected on rest-api.
  • check is not implemented. It is designed in the matrix; do not build on it.

Authoritative docs & related skills

  • centrs repo: README · docs/CLI.md (generated full flag reference) · docs/CONSTITUTION.md (envelope, error model, protocol selection) · docs/MATRIX.md (what actually works) · commands/ (per-command contract and worked examples)
  • routeros-quickchr-cli / routeros-quickchr — boot the CHR that --quickchr then targets: the -cli skill for the shell path, the other for TypeScript harnesses. The pair is the normal grounding loop: quickchr creates the router, centrs drives it.
  • routeros-fundamentals / routeros-scripting — what to actually say to RouterOS once centrs can reach it.
  • routeros-syntax-inspection — /console/inspect and :parse, the machinery behind centrs's validation gate.
  • routeros-mac-telnet / routeros-mndp — the L2 protocols behind execute --via mac-telnet and discover.

This skill is a starting point, not a manual. docs/CLI.md and commands/<name>/ in the repo are authoritative and versioned; when they disagree with this file, they win — and the disagreement is worth reporting at https://github.com/tikoci/centrs/issues.

Signals

GitHub stars
63
Forks
15
Last commit
Sep 2026
Advanced
Catalog kind
skill
Key
routeros-centrs
Source
github.com/tikoci/routeros-skills