@tikoci/centrs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/GLOSSARY.txt +420 -0
  3. package/README.md +196 -0
  4. package/SECURITY.md +30 -0
  5. package/commands/AGENTS.md +50 -0
  6. package/commands/btest/README.md +257 -0
  7. package/commands/btest/examples.md +190 -0
  8. package/commands/check/README.md +59 -0
  9. package/commands/devices/README.md +405 -0
  10. package/commands/devices/examples.md +488 -0
  11. package/commands/discover/README.md +170 -0
  12. package/commands/discover/examples.md +98 -0
  13. package/commands/execute/README.md +208 -0
  14. package/commands/execute/examples.md +353 -0
  15. package/commands/mcp/README.md +260 -0
  16. package/commands/mcp/examples.md +158 -0
  17. package/commands/retrieve/README.md +209 -0
  18. package/commands/retrieve/examples.md +306 -0
  19. package/commands/settings/README.md +69 -0
  20. package/commands/stream/README.md +70 -0
  21. package/commands/terminal/README.md +177 -0
  22. package/commands/terminal/examples.md +144 -0
  23. package/commands/transfer/README.md +315 -0
  24. package/commands/transfer/examples.md +368 -0
  25. package/docs/CONSTITUTION.md +417 -0
  26. package/docs/MATRIX.md +224 -0
  27. package/docs/errors/README.md +74 -0
  28. package/docs/errors/auth/failed.md +9 -0
  29. package/docs/errors/cdb/already-exists.md +9 -0
  30. package/docs/errors/cdb/created.md +9 -0
  31. package/docs/errors/cdb/decrypt-failed.md +9 -0
  32. package/docs/errors/cdb/empty-group.md +9 -0
  33. package/docs/errors/cdb/invalid-option.md +9 -0
  34. package/docs/errors/cdb/invalid-record.md +9 -0
  35. package/docs/errors/cdb/not-found-target.md +9 -0
  36. package/docs/errors/cdb/not-found.md +9 -0
  37. package/docs/errors/cdb/override-applied.md +9 -0
  38. package/docs/errors/cdb/parse-failed.md +9 -0
  39. package/docs/errors/cdb/password-not-needed.md +9 -0
  40. package/docs/errors/cdb/password-required.md +9 -0
  41. package/docs/errors/cdb/reserved-key.md +9 -0
  42. package/docs/errors/cdb/reserved-option.md +9 -0
  43. package/docs/errors/cdb/target-not-registered.md +9 -0
  44. package/docs/errors/cdb/unknown-field.md +9 -0
  45. package/docs/errors/cdb/unknown-option.md +9 -0
  46. package/docs/errors/cdb/write-not-permitted.md +9 -0
  47. package/docs/errors/discover/broadcast-unavailable.md +9 -0
  48. package/docs/errors/discover/failed.md +9 -0
  49. package/docs/errors/identity/ambiguous.md +9 -0
  50. package/docs/errors/identity/no-match.md +9 -0
  51. package/docs/errors/input/invalid-command.md +9 -0
  52. package/docs/errors/input/invalid-match.md +9 -0
  53. package/docs/errors/input/invalid-path.md +9 -0
  54. package/docs/errors/input/invalid-routeros-path.md +9 -0
  55. package/docs/errors/input/invalid-target.md +9 -0
  56. package/docs/errors/input/local-file-not-found.md +9 -0
  57. package/docs/errors/input/mac-address.md +9 -0
  58. package/docs/errors/input/max-results-exceeded.md +9 -0
  59. package/docs/errors/internal/devices-failed.md +9 -0
  60. package/docs/errors/internal/mcp-start.md +9 -0
  61. package/docs/errors/internal/mcp-tool.md +9 -0
  62. package/docs/errors/internal/native-api-length.md +9 -0
  63. package/docs/errors/internal/unhandled.md +9 -0
  64. package/docs/errors/internal/unreachable.md +9 -0
  65. package/docs/errors/mndp/encode-failed.md +9 -0
  66. package/docs/errors/mndp/listen-failed.md +9 -0
  67. package/docs/errors/mndp/malformed.md +9 -0
  68. package/docs/errors/routeros/api-fatal.md +9 -0
  69. package/docs/errors/routeros/api-protocol.md +9 -0
  70. package/docs/errors/routeros/api-trap.md +9 -0
  71. package/docs/errors/routeros/btest-protocol.md +12 -0
  72. package/docs/errors/routeros/btest-too-many-sessions.md +9 -0
  73. package/docs/errors/routeros/command-failed.md +14 -0
  74. package/docs/errors/routeros/ec-srp5-protocol.md +11 -0
  75. package/docs/errors/routeros/error.md +9 -0
  76. package/docs/errors/routeros/invalid-value.md +14 -0
  77. package/docs/errors/routeros/mac-telnet-error.md +9 -0
  78. package/docs/errors/routeros/mac-telnet-not-ready.md +9 -0
  79. package/docs/errors/routeros/mac-telnet-protocol.md +9 -0
  80. package/docs/errors/routeros/mac-telnet-unsupported-auth.md +9 -0
  81. package/docs/errors/routeros/protocol-not-implemented.md +9 -0
  82. package/docs/errors/routeros/request-failed.md +9 -0
  83. package/docs/errors/routeros/session-closed.md +13 -0
  84. package/docs/errors/routeros/unknown-attribute.md +13 -0
  85. package/docs/errors/routeros/unknown-path.md +15 -0
  86. package/docs/errors/routeros/unsupported-capability.md +9 -0
  87. package/docs/errors/settings/invalid-boolean.md +9 -0
  88. package/docs/errors/settings/invalid-format.md +9 -0
  89. package/docs/errors/settings/invalid-integer.md +9 -0
  90. package/docs/errors/settings/invalid-timeout.md +9 -0
  91. package/docs/errors/settings/invalid-via.md +9 -0
  92. package/docs/errors/settings/unsafe-protocol-blocked.md +9 -0
  93. package/docs/errors/target/mac-not-in-arp.md +14 -0
  94. package/docs/errors/target/mac-required.md +23 -0
  95. package/docs/errors/target/mac-unresolved.md +17 -0
  96. package/docs/errors/target/unresolved.md +9 -0
  97. package/docs/errors/transport/auth-failed.md +9 -0
  98. package/docs/errors/transport/auto-method.md +9 -0
  99. package/docs/errors/transport/capability-unsupported.md +22 -0
  100. package/docs/errors/transport/checksum-unavailable.md +9 -0
  101. package/docs/errors/transport/connection-closed.md +9 -0
  102. package/docs/errors/transport/connection-refused.md +9 -0
  103. package/docs/errors/transport/dns.md +9 -0
  104. package/docs/errors/transport/host-key-mismatch.md +9 -0
  105. package/docs/errors/transport/incomplete-transfer.md +9 -0
  106. package/docs/errors/transport/insecure-trust.md +9 -0
  107. package/docs/errors/transport/local-tool-missing.md +11 -0
  108. package/docs/errors/transport/network.md +9 -0
  109. package/docs/errors/transport/timeout.md +9 -0
  110. package/docs/errors/transport/tls-certificate.md +9 -0
  111. package/docs/errors/transport/unreachable.md +9 -0
  112. package/docs/errors/transport/unsupported-operation.md +9 -0
  113. package/docs/errors/usage/confirmation-required.md +9 -0
  114. package/docs/errors/usage/conflicting-flags.md +9 -0
  115. package/docs/errors/usage/invalid-concurrency.md +9 -0
  116. package/docs/errors/usage/missing-group.md +9 -0
  117. package/docs/errors/usage/not-implemented.md +9 -0
  118. package/docs/errors/usage/target-exists.md +9 -0
  119. package/docs/errors/usage/timeout-out-of-range.md +9 -0
  120. package/docs/errors/validation/not-implemented.md +9 -0
  121. package/docs/errors/validation/option.md +9 -0
  122. package/docs/errors/validation/syntax.md +9 -0
  123. package/docs/errors/validation/unknown-attribute.md +9 -0
  124. package/docs/errors/validation/unknown-path.md +9 -0
  125. package/package.json +96 -0
  126. package/src/AGENTS.md +7 -0
  127. package/src/btest.ts +906 -0
  128. package/src/cli/btest.ts +455 -0
  129. package/src/cli/common.ts +53 -0
  130. package/src/cli/devices.ts +527 -0
  131. package/src/cli/discover.ts +186 -0
  132. package/src/cli/execute.ts +333 -0
  133. package/src/cli/mcp.ts +109 -0
  134. package/src/cli/retrieve.ts +383 -0
  135. package/src/cli/terminal.ts +303 -0
  136. package/src/cli/transfer.ts +471 -0
  137. package/src/cli.ts +115 -0
  138. package/src/core/envelope.ts +221 -0
  139. package/src/core/error-catalog.ts +469 -0
  140. package/src/core/routeros-errors.ts +268 -0
  141. package/src/data/AGENTS.md +11 -0
  142. package/src/data/dude-db.ts +0 -0
  143. package/src/data/mndp-cache.ts +132 -0
  144. package/src/data/mndp.ts +405 -0
  145. package/src/data/winbox-cdb-write.ts +200 -0
  146. package/src/data/winbox-cdb.ts +913 -0
  147. package/src/devices-schema.ts +107 -0
  148. package/src/devices.ts +1717 -0
  149. package/src/discover.ts +802 -0
  150. package/src/errors.ts +189 -0
  151. package/src/execute.ts +1350 -0
  152. package/src/index.ts +353 -0
  153. package/src/mcp/config.ts +66 -0
  154. package/src/mcp/resources.ts +98 -0
  155. package/src/mcp/safety.ts +119 -0
  156. package/src/mcp/server.ts +225 -0
  157. package/src/mcp/tools.ts +657 -0
  158. package/src/mcp.ts +33 -0
  159. package/src/protocols/AGENTS.md +14 -0
  160. package/src/protocols/adapter.ts +866 -0
  161. package/src/protocols/btest-session.ts +1493 -0
  162. package/src/protocols/btest.ts +447 -0
  163. package/src/protocols/ec-srp5.ts +466 -0
  164. package/src/protocols/index.ts +118 -0
  165. package/src/protocols/mac-telnet-console.ts +655 -0
  166. package/src/protocols/mac-telnet.ts +1299 -0
  167. package/src/protocols/mtwei.ts +47 -0
  168. package/src/protocols/native-api.ts +796 -0
  169. package/src/protocols/sftp.ts +427 -0
  170. package/src/protocols/ssh.ts +297 -0
  171. package/src/resolver/cdb.ts +580 -0
  172. package/src/resolver/comment-kv.ts +360 -0
  173. package/src/resolver/index.ts +74 -0
  174. package/src/resolver/mac.ts +227 -0
  175. package/src/resolver/settings.ts +228 -0
  176. package/src/resolver/target.ts +442 -0
  177. package/src/retrieve-fanout.ts +533 -0
  178. package/src/retrieve.ts +1215 -0
  179. package/src/terminal.ts +444 -0
  180. package/src/transfer.ts +1554 -0
  181. package/src/tui.ts +22 -0
  182. package/src/webproxy.ts +16 -0
@@ -0,0 +1,260 @@
1
+ # mcp
2
+
3
+ The MCP frontend: an [Model Context Protocol](https://modelcontextprotocol.io)
4
+ server that exposes the centrs core to AI agents through a **small, scoped set of
5
+ verbs** over the same canonicalize → validate → run path the CLI/API use. It is
6
+ not a per-command tool firehose.
7
+
8
+ Status: `CHR-passed` (Phase 1 + first Phase 2 CDB mutation). The stdio server
9
+ (`src/mcp/server.ts`, `tools.ts`, `safety.ts`, `resources.ts`, `config.ts`) is
10
+ built and the examples below (1–10) run green against CHR 7.23 via
11
+ `test/integration/mcp.test.ts`, including the gated `mcp=rw` + `confirm:true`
12
+ RouterOS write and in-band CDB registration through `centrs_devices add`.
13
+ `src/mcp.ts` is the runtime barrel (`@tikoci/centrs/mcp`); `docs/MATRIX.md` is
14
+ the authoritative status.
15
+
16
+ Run it:
17
+
18
+ ```bash
19
+ bunx @tikoci/centrs mcp # stdio transport (default)
20
+ centrs mcp # same, when installed
21
+ centrs mcp start # explicit verb; equivalent to the above
22
+ ```
23
+
24
+ Bun is required (centrs uses Bun APIs and the bun-native CDB codec), as with
25
+ `@tikoci/rosetta`.
26
+
27
+ ## Client install snippets
28
+
29
+ For an MCP client that accepts JSON server config, use `bunx` directly:
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "centrs": {
35
+ "command": "bunx",
36
+ "args": ["@tikoci/centrs", "mcp"],
37
+ "env": {
38
+ "CENTRS_CDB_FILE": "/absolute/path/to/winbox.cdb"
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ If `centrs` is already installed on `PATH`, the equivalent command is:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "centrs": {
51
+ "command": "centrs",
52
+ "args": ["mcp"],
53
+ "env": {
54
+ "CENTRS_CDB_FILE": "/absolute/path/to/winbox.cdb"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ Use `CENTRS_CDB_PASSWORD` to read or write an encrypted CDB. The write layer
62
+ decrypts under the loaded password, mutates the record set, and re-encrypts with
63
+ a fresh salt before the atomic rename. Prefer an unencrypted throwaway CDB for
64
+ bench harnesses and CI.
65
+
66
+ ## Why a scoped surface (grounding)
67
+
68
+ `~/GitHub/bench-routeros-tools` (`REPORT.md`, `docs/REPORT_LIVE_CHR.md`)
69
+ benchmarked RouterOS agent support. Load-bearing findings that shape this design:
70
+
71
+ - **Never expose one tool per RouterOS command.** A 166-tool execution MCP costs
72
+ ~28K always-on tokens and, even free, is ambiguous to select (36/166 tools
73
+ match a task; the correct tool is a clear top-3 pick only 62% of the time;
74
+ destructive tools sit lexically next to safe ones). The recommendation is a
75
+ *handful of verbs* over a canonical `{path, verb, args}`, gated by validation.
76
+ - **explain → validate → run split.** A cheap, safe knowledge/validation tier
77
+ gates a thin runner. centrs already has this shape internally.
78
+ - **The parse scheme catches parser-level truth that schema-inspect can miss.** In
79
+ the bench, `/console/inspect` accepted a command form the real console parser
80
+ rejected. RouterOS 7.23 no longer reproduces that exact `blackhole=yes` case,
81
+ but the lesson stands: schema-presence is not parser-acceptance.
82
+ `:put [:parse "<cmd>"]` runs the real parser, and centrs runs **both** `:parse`
83
+ and `/console/inspect`, so the MCP exposes validation as a first-class
84
+ **dry-run** tool (`centrs_validate`) that never mutates.
85
+ - **Stateful intents need device-validate-before-apply.** Gold using
86
+ `interface=ether2` fails on a CHR exposing only `ether1`. Pair `execute` with
87
+ cheap readback (`retrieve`).
88
+
89
+ ## Intent
90
+
91
+ - One MCP tool per centrs **verb**, not per RouterOS command. Each tool is a thin
92
+ adapter over the existing core function and returns the standard envelope
93
+ (`{ ok, data?, warnings?, error?, meta }`) as JSON text content — the same
94
+ shape the CLI renders (constitution: result envelope; invariant: one settings
95
+ vocabulary across API/CLI/MCP/TUI/proxy).
96
+ - Validation is the product here too: `validate=true` by default, lead with
97
+ `:parse`, never disable to make a call pass (constitution: validation).
98
+ - The MCP server is an adapter over the root TypeScript API; it adds no new
99
+ RouterOS semantics, only the MCP transport, the tool schemas, and the
100
+ CDB-backed safety boundary below.
101
+
102
+ ## Tools
103
+
104
+ | Tool | Core mapping | Reads/Writes | Target source |
105
+ | ---- | ------------ | ------------ | ------------- |
106
+ | `centrs_explain` | `canonicalizeExecuteCommand` | offline, none | none (string only) |
107
+ | `centrs_validate` | execute validation only (`:parse` + `/console/inspect`), **no run** | read-only | CDB |
108
+ | `centrs_retrieve` | `retrieve` / `retrieveGroup` | read-only | CDB |
109
+ | `centrs_execute` | `execute` | read or write | CDB |
110
+ | `centrs_devices` | `devices` (`list`/`show`/`groups`/`add`/`edit`/`set`/`remove`) | CDB read/write | CDB file |
111
+ | `centrs_discover` | `discover` (`listen` + optional `save`) | local read; `save` writes CDB | n/a |
112
+
113
+ - `centrs_explain` is the only tool that needs no CDB and no device — it
114
+ canonicalizes a CLI string to `{ path, verb, args, mode, writeShaped }` so an
115
+ agent can plan before touching anything. centrs owns the script-vs-structured
116
+ gate (constitution: canonicalizer ownership); the MCP must not widen what
117
+ counts as `structured`.
118
+ - `centrs_validate` is the headline distilled from the bench: a dry-run that runs
119
+ the real parser plus schema inspection against a registered target and returns
120
+ the validation envelope **without executing**. Use it to reject schema-invalid
121
+ commands before any write.
122
+ - `centrs_retrieve` mirrors `retrieve`, including attribute projection,
123
+ `listAttributes`, and `group` fanout.
124
+ - `centrs_execute` mirrors `execute`. Read-shaped commands run subject only to
125
+ CDB membership; write-shaped add/set/remove are gated by the CDB policy below.
126
+ Its MCP `destructiveHint` is **conservative — always `true`**, even though the
127
+ tool also serves read-shaped commands: a single execute tool cannot vary the
128
+ static annotation by argument, so it advertises the worst case and lets the
129
+ canonicalizer's write-shape check + the CDB `mcp=rw`/`confirm` gates do the real
130
+ enforcement. Agents that route purely on read-only commands should prefer
131
+ `centrs_retrieve` (truly `readOnlyHint`).
132
+ - `centrs_devices` is the in-band way to inspect and grow the allowlist:
133
+ `list`/`show`/`groups` are read-only; `add`/`set`/`remove` mutate the active CDB
134
+ and require `confirm: true`. `edit` is reserved for the future interactive
135
+ editor and returns `usage/not-implemented` (use `set` to modify a record). MCP
136
+ device envelopes redact saved passwords and expose only `passwordSet`.
137
+ - `centrs_discover` mirrors `discover`: it listens for MNDP neighbors, returns
138
+ the standard discovery envelope, and can persist found neighbors into the
139
+ active CDB with `save: true` + `confirm: true`. Saved records use the same
140
+ provenance/group behavior as the CLI and no RouterOS credentials are returned.
141
+
142
+ ## Safety model — the CDB is the allowlist
143
+
144
+ The authorization boundary is the WinBox CDB (constitution: identity and CDB),
145
+ which is already centrs's device registry **and** native credential store. The
146
+ MCP server applies it as a hard allowlist:
147
+
148
+ - **Targets resolve through the CDB only** — by name, MAC, or `group`. Inline
149
+ ad-hoc `host` + `username` + `password` are **rejected by default** with
150
+ `cdb/target-not-registered`, whose `fix` points at `centrs_devices add`. An
151
+ agent therefore cannot reach an arbitrary router with arbitrary credentials.
152
+ - **CDB source** follows the constitution: default
153
+ `~/.config/tikoci/winbox.cdb`, overridden by `CENTRS_CDB_FILE` / `--cdb-file`,
154
+ or a freshly created CDB the operator hands the server at start. An encrypted
155
+ CDB is decrypted with `--cdb-password` (or `CENTRS_CDB_PASSWORD`). With no CDB,
156
+ the server still serves `centrs_explain` (offline); CDB mutations require a
157
+ loadable CDB file.
158
+ - **Credentials live in the CDB and are never returned by MCP.** `centrs_devices
159
+ add`/`edit` may write credentials into the active CDB, but tool results and
160
+ resources redact saved passwords and expose only `passwordSet`.
161
+ - **Per-device RouterOS read/write policy lives in the CDB.** A device is
162
+ writable only when its record opts in via the comment-kv key `mcp=rw` (default
163
+ `ro`). Reads are allowed for any registered device. Write-shaped
164
+ `centrs_execute` calls require the resolved target to be `mcp=rw`; `mcp` joins
165
+ the comment-kv allowlist (`src/resolver/comment-kv.ts`) and, per the
166
+ constitution, must round-trip through env/CLI/API as well as CDB.
167
+ - **Per-call `confirm: true`** stays as a belt-and-suspenders for write-shaped
168
+ calls (the non-TTY analogue of the CLI's `--yes`). A RouterOS write attempt
169
+ against an `mcp=ro` target fails with typed `cdb/write-not-permitted`
170
+ regardless of `confirm`; a local CDB mutation through `centrs_devices` or
171
+ `centrs_discover` `save` fails with `usage/confirmation-required` unless
172
+ explicitly confirmed.
173
+ - **`--allow-adhoc-targets`** (off by default) is reserved for one-off lab/power
174
+ user flows. Current RouterOS-facing tools expose no inline host+credential
175
+ schema, so the CDB allowlist remains the only active target source.
176
+
177
+ This makes the safety story declarative and auditable: *what the agent may touch*
178
+ and *whether it may write* are both data in a file the operator controls, not
179
+ flags buried in a prompt.
180
+
181
+ ## Resources
182
+
183
+ The server publishes two read-only MCP **resources** and points its
184
+ `instructions` at them so a client discovers them without guessing:
185
+
186
+ - `centrs://devices` — the known devices in the active CDB (the allowlist
187
+ itself): target, group, user, record type, and the resolved `mcp` write policy
188
+ per record. No passwords. This is how an agent learns what it may act on.
189
+ - `centrs://errors` — the centrs error-code catalog: each known `family/code`
190
+ with its stable details URL (`https://tikoci.github.io/centrs/errors/<code>`),
191
+ including the MCP-specific `cdb/target-not-registered` and
192
+ `cdb/write-not-permitted`. Lets an agent resolve an envelope error without a
193
+ round-trip.
194
+
195
+ ## Transports
196
+
197
+ - **stdio is the only MCP transport.** It matches the common local MCP client
198
+ model and keeps the agent boundary on the operator's machine.
199
+ - **HTTP is the proxy surface's job, not the MCP server's.** Remote/HTTP access
200
+ to CDB-gated RouterOS lives in `src/webproxy.ts` (the `centrs-proxy` surface),
201
+ which fronts the same CDB. The MCP server deliberately grows no HTTP/TLS
202
+ listener of its own, so there is one place that owns network exposure.
203
+
204
+ ## Future work
205
+
206
+ - **Manifest dump for benchmarking (`centrs mcp --list-tools`).** A device-free
207
+ command that prints the registered tool + resource schemas and the server
208
+ `instructions` as JSON, so a harness can measure centrs's always-on context
209
+ cost the same way `bench-routeros-tools` counts mikrotik-mcp's 166 schemas —
210
+ without booting a CHR. Pairs with the stable example IDs already mapped in
211
+ `examples.md` (example N ↔ one assertion in `test/integration/mcp.test.ts`) so
212
+ the bench can cite per-trap results. Requested by the bench review
213
+ (`~/GitHub/bench-routeros-tools/docs/AGENTIC_FUTURES.md` → "benchmark centrs as
214
+ the realized scoped-execution tier").
215
+ - **`__default__` fallback-credentials CDB record** is now a **core** concept,
216
+ not MCP-specific — see `docs/CONSTITUTION.md` (Default-device record) and
217
+ `commands/devices/README.md`. It supplies fallback creds/metadata for a device
218
+ whose own record leaves them unset (precedence args → ENV → device →
219
+ `__default__` → built-in). MCP-specific rule: `__default__` **never widens the
220
+ allowlist** — an unregistered target is still rejected; `__default__` only
221
+ fills missing creds for an already-registered device (see the safety model
222
+ above). Implementation lands with packaging (Phase 4).
223
+ - **MikroTik docs enrichment.** `centrs_explain` may later annotate a canonical
224
+ `{path, verb}` with a link into MikroTik's new Docusaurus manual at
225
+ <https://manual.mikrotik.com> (CLI/API reference, still WIP upstream). This is
226
+ an independent, optional reference — centrs does not depend on rosetta or any
227
+ other MCP for its canonicalization.
228
+
229
+ ## Open shape questions
230
+
231
+ These are resolved for the current design and recorded here for provenance:
232
+
233
+ - **HTTP transport** → owned by the proxy surface, not MCP (resolved).
234
+ - **Doc context** → centrs logic only; `manual.mikrotik.com` is a future,
235
+ optional enrichment source (resolved).
236
+ - **Resources vs tools** → ship both `centrs://devices` and `centrs://errors` as
237
+ resources, advertised via server instructions (resolved).
238
+
239
+ These refinements sit on top of the grounded surface above; none blocks the
240
+ Phase 1 stdio read/validate cells.
241
+
242
+ ## CHR test shape
243
+
244
+ Per the done definition, MCP cells advance only with CHR evidence. The
245
+ integration test (`test/integration/mcp.test.ts`) boots a CHR via
246
+ `@tikoci/quickchr`, registers it in a throwaway CDB to exercise the allowlist
247
+ boundary, starts the server, and drives it with an in-process MCP client over
248
+ stdio. It asserts, at minimum:
249
+
250
+ - an unregistered target is rejected (`cdb/target-not-registered`);
251
+ - `centrs_validate` accepts parser-valid command text and rejects
252
+ schema-invalid attributes without executing;
253
+ - `centrs_retrieve` returns structured records;
254
+ - `centrs_execute` (read-shaped) runs and returns the envelope;
255
+ - a write against an `mcp=ro` target is refused (`cdb/write-not-permitted`) and
256
+ the same write against an `mcp=rw` target succeeds with `confirm: true`;
257
+ - `centrs_devices add` writes a new record into the throwaway CDB and returns no
258
+ saved password material.
259
+
260
+ Each example in `commands/mcp/examples.md` maps to one such assertion.
@@ -0,0 +1,158 @@
1
+ # mcp — examples
2
+
3
+ Each numbered example is an executable spec for the MCP surface. The integration
4
+ test `test/integration/mcp.test.ts` boots a CHR 7.23 router with
5
+ `@tikoci/quickchr`, registers it in a throwaway CDB, starts the centrs MCP server
6
+ over stdio, and drives it with an in-process MCP client. Each example below maps
7
+ to one assertion. If a line here is not exercised by a test, the test file is
8
+ wrong; if a line passes only with `validate=false`, the **implementation** is
9
+ wrong (see `docs/CONSTITUTION.md`).
10
+
11
+ These examples are MCP **tool calls** (JSON `params.arguments`), not shell
12
+ commands. `$NAME` is the CDB name the harness assigns the booted CHR; `$U` / `$P`
13
+ are CHR credentials stored in the CDB record for RouterOS actions. Example 10
14
+ also writes credentials into the throwaway CDB through `centrs_devices add`, but
15
+ MCP never returns saved password material. The result is the standard centrs
16
+ envelope returned as the tool's JSON text content.
17
+
18
+ ## Phase 1 — read + validate (stdio)
19
+
20
+ ### 1. `centrs_explain` canonicalizes offline (no CDB, no device)
21
+
22
+ ```json
23
+ { "name": "centrs_explain",
24
+ "arguments": { "command": "/ip/route/add dst-address=10.99.0.0/24 blackhole=yes" } }
25
+ ```
26
+
27
+ Envelope: `ok: true`, `data.path = "/ip/route"`, `data.verb = "add"`,
28
+ `data.mode = "structured"`, `data.writeShaped = true`, `data.args` includes
29
+ `dst-address` and `blackhole`. No network or CDB access occurs.
30
+
31
+ ### 2. Unregistered target is rejected (CDB is the allowlist)
32
+
33
+ ```json
34
+ { "name": "centrs_retrieve",
35
+ "arguments": { "target": "192.0.2.1", "path": "/system/resource" } }
36
+ ```
37
+
38
+ Envelope: `ok: false`, `error.code = "cdb/target-not-registered"`, and
39
+ `error.fix`/`remediation` names `centrs_devices add`. No connection is attempted.
40
+ RouterOS-facing MCP tools accept only a CDB `target` (name/MAC/URL) — there is
41
+ no inline `host`/`username`/`password` executor parameter, so RouterOS actions
42
+ use credentials already stored in the CDB.
43
+
44
+ ### 3. `centrs_validate` accepts the bare `blackhole` flag
45
+
46
+ ```json
47
+ { "name": "centrs_validate",
48
+ "arguments": { "target": "$NAME",
49
+ "command": "/ip/route/add dst-address=10.99.0.0/24 blackhole" } }
50
+ ```
51
+
52
+ Envelope: `ok: true`, `meta.validation.source` includes `:put [:parse]` (the
53
+ RouterOS parser accepted the bare flag), and **no route is created** (dry-run
54
+ only — a follow-up `centrs_retrieve` of `/ip/route` shows no `10.99.0.0/24`). A
55
+ bare flag like `blackhole` routes through script-mode `:parse`, so the
56
+ `/console/inspect` semantic gate does not apply here — `:parse` is the gate that
57
+ matters for the bench gold-bug in example 4.
58
+
59
+ ### 4. `centrs_validate` rejects a schema-invalid command (the dual gate)
60
+
61
+ ```json
62
+ { "name": "centrs_validate",
63
+ "arguments": { "target": "$NAME",
64
+ "command": "/ip/route/add dst-address=10.99.0.0/24 nonexistentattr=1" } }
65
+ ```
66
+
67
+ Envelope: `ok: false`, `error.code = "validation/unknown-attribute"` — the
68
+ `/console/inspect` semantic gate rejects the unknown attribute and **no route is
69
+ created**. This proves the validate tool fails closed on bad input.
70
+
71
+ > Bench note: the bench's original gold-bug command
72
+ > `/ip/route/add dst-address=10.99.0.0/24 blackhole=yes` (where `/console/inspect`
73
+ > accepted a value the console `:parse` rejected) does **not** reproduce on
74
+ > RouterOS 7.23 — the device parser now accepts `blackhole=yes`. centrs still runs
75
+ > **both** gates (`:parse` + `/console/inspect`) so the stricter of the two wins
76
+ > whenever a version does reject it; this example asserts the still-true,
77
+ > version-robust behavior (unknown-attribute rejection) instead.
78
+
79
+ ### 5. `centrs_retrieve` returns structured records
80
+
81
+ ```json
82
+ { "name": "centrs_retrieve",
83
+ "arguments": { "target": "$NAME", "path": "/system/resource",
84
+ "attributes": ["version", "board-name"] } }
85
+ ```
86
+
87
+ Envelope: `ok: true`, `data` is a single object carrying `version` and
88
+ `board-name` (the `/system/resource` menu is a single record, not a list),
89
+ `meta.via = "rest-api"`.
90
+
91
+ ### 6. `centrs_execute` runs a read-shaped command
92
+
93
+ ```json
94
+ { "name": "centrs_execute",
95
+ "arguments": { "target": "$NAME", "command": "/system/identity/print" } }
96
+ ```
97
+
98
+ Envelope: `ok: true`, `meta.operation.request.write = false`, and
99
+ `meta.validation` shows the parse gate ran.
100
+
101
+ ## CHR-verified gated writes
102
+
103
+ ### 7. Write against an `mcp=ro` target is refused
104
+
105
+ With `$NAME` registered `mcp=ro` (default):
106
+
107
+ ```json
108
+ { "name": "centrs_execute",
109
+ "arguments": { "target": "$NAME",
110
+ "command": "/ip/address/add address=198.51.100.10/32 interface=ether1",
111
+ "confirm": true } }
112
+ ```
113
+
114
+ Envelope: `ok: false`, `error.code = "cdb/write-not-permitted"`. The router is not
115
+ mutated even though `confirm` is true — the CDB policy is authoritative.
116
+
117
+ ### 8. Write against an `mcp=rw` target succeeds with `confirm: true`
118
+
119
+ With `$NAME` registered `mcp=rw`:
120
+
121
+ ```json
122
+ { "name": "centrs_execute",
123
+ "arguments": { "target": "$NAME",
124
+ "command": "/ip/address/add address=198.51.100.10/32 interface=ether1 comment=centrs-mcp",
125
+ "confirm": true } }
126
+ ```
127
+
128
+ Envelope: `ok: true`, `data.ret` matches `/^\*[0-9A-F]+$/`,
129
+ `meta.operation.request.write = true`, `meta.validation.source` includes both
130
+ `:parse` and `/console/inspect`.
131
+
132
+ ### 9. Write without `confirm` on an `mcp=rw` target fails closed
133
+
134
+ ```json
135
+ { "name": "centrs_execute",
136
+ "arguments": { "target": "$NAME",
137
+ "command": "/ip/address/remove [find comment=centrs-mcp]" } }
138
+ ```
139
+
140
+ Envelope: `ok: false`, `error.code = "usage/confirmation-required"`; the address
141
+ added in example 8 still exists. (Cleanup runs with `confirm: true`.)
142
+
143
+ ## CHR-verified CDB mutations
144
+
145
+ ### 10. `centrs_devices add` grows the allowlist in-band
146
+
147
+ ```json
148
+ { "name": "centrs_devices",
149
+ "arguments": { "op": "add", "target": "lab-edge",
150
+ "user": "$U", "password": "$P",
151
+ "comment": "mcp=ro",
152
+ "confirm": true } }
153
+ ```
154
+
155
+ Envelope: `ok: true`, the CDB now resolves `lab-edge`, `data.entry.password` is
156
+ absent, and `data.entry.passwordSet` reports whether a password was saved. A
157
+ subsequent `centrs_devices show` for `lab-edge` succeeds. This uses the
158
+ throwaway CDB; no real device is contacted by the registration itself.
@@ -0,0 +1,209 @@
1
+ # retrieve
2
+
3
+ Read RouterOS state. RouterOS menu reads model `<path>/<verb>` where the verb
4
+ is `print`-style (`print`, `get`, and async POST-shaped reads as they're
5
+ added). SNMP reads use `retrieve <router> snmp <oid|MIB name>` and resolve
6
+ names through a MikroTik MIB cache downloaded from mikrotik.com.
7
+
8
+ > "POST-shaped reads" are RouterOS menu reads that require a request body
9
+ > (e.g. paged or query-driven `print`) and are therefore issued as REST `POST`
10
+ > rather than `GET`. centrs identifies them from the command tree and routes
11
+ > them accordingly as they're added.
12
+
13
+ Structured formats return an array of records, or — with `--attribute` — a
14
+ value or row of values wrapped in the standard envelope
15
+ (`docs/CONSTITUTION.md`). The default `text` format renders data and errors for
16
+ humans.
17
+
18
+ ## Synopsis
19
+
20
+ ```text
21
+ centrs retrieve <router> <path>[/<shortcut>] [flags]
22
+ centrs retrieve <router> snmp <oid|MIB name> [flags]
23
+ ```
24
+
25
+ - `<router>` — IP, DNS, MAC, or CDB-resolved name. See constitution: identity.
26
+ - `<path>` — RouterOS path beginning with `/`, e.g. `/ip/address`. The verb is
27
+ inferred (`print` for list paths, `get` for singletons like
28
+ `/system/resource`). If the unnamed arg does not start with `/`, it is a
29
+ reserved shortcut — currently none are defined; future examples include
30
+ `defconf` → `/system/default-configuration/get`.
31
+ - Reading one row by name is **flag-only** (there is no row positional):
32
+ `--query name=ether1` filters RouterOS rows and returns an **array**; the
33
+ caller takes the match. `--query`/`--filter` always return an array — there is
34
+ no singleton-by-name shape. (Decided: the old `<.id|name>` positional is
35
+ dropped.)
36
+ - `snmp <oid|MIB name>` — planned retrieve-only SNMP form. OIDs are used
37
+ directly; MIB names are resolved from the cached MikroTik MIB that matches
38
+ the selected RouterOS version/channel once that cache is implemented.
39
+
40
+ ## Flags
41
+
42
+ | Flag | Behavior |
43
+ | ----------------------------------- | ----------------------------------------------------------------------------------------- |
44
+ | `--attribute <name>` | Project a single attribute. Repeatable, or comma-separated. |
45
+ | `--attributes <a,b,c>` | Alias for `--attribute` with comma-separated input. |
46
+ | `--all-attributes` | Equivalent to RouterOS `details=true`. Mutually exclusive with `--attribute(s)`. |
47
+ | `--list-attributes` (alias `--list`) | Return the available attribute names for the path. No `print`/`get` is run. |
48
+ | `--once` | Bounded single read of a monitor-style menu (RouterOS `once`): returns **one** envelope and never follows. Open-ended follow is the separate `stream` verb. See constitution: protocol selection. |
49
+ | `--query <expr>` | RouterOS-side **row** filter (maps to `.query`), repeatable; returns matching rows as an **array**. This is how you read one row by name (`--query name=ether1`). **Not Implemented** yet — returns `validation/not-implemented`. |
50
+ | `--filter <expr>` | RouterOS row filter; same `.query` mapping and not-implemented handling as `--query`. |
51
+ | `--where <attr>=<value>` | Device-class selector: fan out across CDB records whose stored fact/comment-kv matches (e.g. `--where board=RB5009`). Repeatable (AND). Filters *which devices*, not RouterOS rows. See constitution: target selection. |
52
+ | `--max-bytes <n>` | Byte budget for the rendered payload. If the response would exceed it, centrs truncates to fit, keeps `ok: true`, and adds a warning + `meta.truncated`. Not an error. (Renamed from `--max-results`; the legacy flag still parses until the code split lands.) |
53
+ | `--max-rows <n>` | Maximum row count for list reads. Excess rows are clipped; `ok: true` with a warning + `meta.truncated`. |
54
+ | `--format text` (default) | Human-readable rendering (default). Errors print `[code] summary` + `Fix:` lines. |
55
+ | `--format json` (alias `--json`) | Structured JSON envelope (set `CENTRS_FORMAT=json` to make it the default). |
56
+ | `--format yaml` (alias `--yaml`) | YAML rendering of the same envelope. |
57
+ | `--resolve <none\|arp>` | Resolve a MAC-address target to an IP via the host ARP cache. Default `none`. |
58
+ | `--via <protocol>` | Pin the transport (`rest-api` default, or `native-api`). No silent downgrade. See constitution: protocol selection. |
59
+ | `--validate=false` | Escape hatch; default is `true`. See constitution: validation is the product. |
60
+ | `--timeout <ms>` | Request timeout. REST: ≤ 60000. native-api allows longer. |
61
+ | `--username` (alias `--user`, `-u`) / `--password` | Override CDB-resolved or env credentials. |
62
+ | `--host <host\|url>` | Override the resolved host or base URL for the target. |
63
+ | `--port <n>` | Override the transport port. native-api defaults to 8728 (TLS api-ssl when `--port 8729`).|
64
+ | `--cdb-file` / `--cdb-password` | Override CDB file location / decrypt password. |
65
+ | `--ros-version <version>` | SNMP MIB lookup only: pin the MikroTik MIB version to cache/download. |
66
+ | `--ros-channel <channel>` | SNMP MIB lookup only: `stable`, `long-term`, `testing`, or `development`; default `stable`. |
67
+ | `--group <name>` | Fan out across every CDB target in WinBox group `<name>`. Repeatable, and combinable with `<router>` positionals plus `--all`/`--default`; the union is de-duped by CDB record index. See **Group fanout**. |
68
+ | `--concurrency <n>` | Max in-flight targets during fanout (integer ≥ 1). Defaults are transport-aware: `rest-api` 8, `native-api` 4. Rejected with `usage/invalid-concurrency` otherwise. |
69
+
70
+ ## Validation
71
+
72
+ Default validator: `/console/inspect request=syntax path=<comma-joined>,print`
73
+ (or `,get` for singleton paths). Validator must reject unknown paths and
74
+ unknown attributes with `validation/*` codes that include the inspect-suggested
75
+ alternatives in the error envelope.
76
+
77
+ If `--all-attributes` is combined with `--attribute(s)`, fail with
78
+ `usage/conflicting-flags` *before* hitting the network.
79
+
80
+ SNMP validation is separate from RouterOS `/console/inspect`: OIDs and MIB
81
+ names must resolve through the MIB cache before any SNMP request is sent.
82
+
83
+ ## SNMP MIB cache policy
84
+
85
+ Signed off (2026-06-06): cache MikroTik MIBs under the XDG cache root,
86
+ `${XDG_CACHE_HOME:-~/.cache}/tikoci/snmp-mibs`, not beside the CDB. The cache is
87
+ derived data; deleting it must never remove inventory or credentials. Use one
88
+ subdirectory per exact RouterOS version, for example
89
+ `routeros/7.23/mikrotik.mib`, plus metadata containing source URL, ETag,
90
+ Last-Modified, SHA-256, fetched-at, and the channel (if a channel resolved it).
91
+
92
+ Version selection for MIB-name lookup is deterministic. `--ros-version` wins and
93
+ downloads `https://download.mikrotik.com/routeros/<version>/mikrotik.mib`.
94
+ Otherwise `--ros-channel` (default `stable`) is resolved through
95
+ `https://upgrade.mikrotik.com/routeros/NEWESTa7.<channel>` and that exact version
96
+ is used. If target metadata already carries a RouterOS version from a trusted
97
+ source, it may supply the default version, but the SNMP path must not make an
98
+ extra REST/native call merely to discover a version.
99
+
100
+ Invalidation is by exact version plus HTTP validators. A missing version cache is
101
+ downloaded before lookup; an existing cache is reused offline. When online, a
102
+ cache older than 24h is revalidated with ETag/Last-Modified and replaced
103
+ atomically if MikroTik republishes the file. Channel pointers are short-lived:
104
+ re-resolve the channel after 24h, then use the cache for the resulting exact
105
+ version.
106
+
107
+ Offline behavior is intentionally conservative. Numeric OIDs do not require the
108
+ MIB cache and proceed normally. MIB-name lookup succeeds only when the selected
109
+ exact version is already cached; if not, fail before SNMP I/O with
110
+ `snmp/mib-cache-miss` and a fix that tells the caller to go online once or pass a
111
+ cached `--ros-version`. Do not silently fall back to a different version's MIB.
112
+
113
+ RouterOS grounding: MikroTik documents SNMP as retrieve/write-capable but centrs
114
+ uses it as retrieve-only; the same page points users to the MikroTik MIB download
115
+ surface. The per-version `mikrotik.mib` URL under `download.mikrotik.com` has
116
+ been verified for current stable/development versions, but treating it as the
117
+ long-term canonical URL is an assumption until the implementation has fallback
118
+ handling.
119
+
120
+ ## Output shape
121
+
122
+ ```ts
123
+ {
124
+ ok: true,
125
+ data: [ { ".id": "*1", address: "192.0.2.1/24", ... }, ... ], // array of rows
126
+ // or for singletons (e.g. /system/resource):
127
+ // data: { uptime: "...", "cpu-load": 7, ... },
128
+ // or for --attribute:
129
+ // data: "192.0.2.1/24", // single value
130
+ meta: { target, via, settings, validation, timing }
131
+ }
132
+ ```
133
+
134
+ `--max-bytes` / `--max-rows` truncation populates
135
+ `meta.truncated = { returned, total, totalBytes }` and keeps `ok: true` with a
136
+ warning entry — it is not an error. (Earlier text said a byte overflow returns an
137
+ *error* with the needed size; superseded — truncation is an `ok: true` footnote so
138
+ partial data stays usable.)
139
+
140
+ ## Group fanout
141
+
142
+ `--group <name>` loads the CDB once, expands every matching WinBox record in
143
+ that group, de-dupes by CDB record index, and runs the same per-target retrieve
144
+ pipeline for each target. Per the shared target-selector grammar, `--group` is
145
+ repeatable and combines freely with one or more `<router>` positionals, `--all`
146
+ (every CDB record), and `--default` (the `__default__` record); the union is
147
+ de-duped by CDB record index. (Earlier drafts made `--group` mutually exclusive
148
+ with a positional — superseded by the liberal selector model.)
149
+
150
+ Fanout output is one outer envelope whose `ok` reports orchestration success:
151
+ `ok: false` is reserved for failures before reliable per-target results exist
152
+ (for example invalid flags or CDB decrypt failure). Per-target success/failure
153
+ is data, not metadata:
154
+
155
+ ```ts
156
+ {
157
+ ok: true,
158
+ data: {
159
+ summary: { total: 2, ok: 1, failed: 1 },
160
+ targets: [
161
+ { ok: true, data: { ... }, meta: { target: { recordIndex: 0 }, ... } },
162
+ { ok: false, error: { code: "transport/connection-refused", ... }, meta: { target: { recordIndex: 1 }, ... } }
163
+ ]
164
+ },
165
+ warnings: [],
166
+ meta: {
167
+ target: {},
168
+ via: "rest-api", // null if inner targets disagree on protocol
169
+ settings: { ... },
170
+ operation: {
171
+ kind: "fanout",
172
+ group: "prod-edge",
173
+ concurrency: 8,
174
+ summary: { total: 2, ok: 1, failed: 1 },
175
+ request: { path: "/system/resource", validate: true, format: "json", ... }
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ An empty or unknown group is `ok: true` with
182
+ `data.summary = { total: 0, ok: 0, failed: 0 }`, `data.targets = []`, and a
183
+ `cdb/empty-group` warning. Validation runs per target because RouterOS schemas
184
+ can differ by version. Fanout retries each target up to two times only for
185
+ `transport/network` and `transport/connection-closed`; REST 5xx failures are
186
+ mapped to `transport/connection-closed`. It does not retry `routeros/*`,
187
+ `validation/*`, `auth/*`, `cdb/*`, `target/*`, timeouts, DNS, TLS, or refused
188
+ connections.
189
+
190
+ ## Definition of done
191
+
192
+ This command is `CHR-passed` only when every line in `examples.md` runs green
193
+ against a real CHR through `bun run test:integration`. Disabling validation to
194
+ reach green is forbidden. See `docs/CONSTITUTION.md` for the full done rule.
195
+
196
+ ## Notes for future cells
197
+
198
+ - **native-api** — implemented for retrieve (`--via native-api`, `CHR-passed`;
199
+ see `examples.md` N1–N11 and `test/integration/native-api-retrieve.test.ts`).
200
+ Validation still runs through `/console/inspect` (issued as a native-API
201
+ command). Attribute values arrive as strings because the binary API carries
202
+ no JSON scalar types; the envelope shape is otherwise identical to REST.
203
+ - **snmp** — retrieve-only OID/MIB reads. It does not validate through
204
+ `/console/inspect` and does not execute RouterOS CLI.
205
+ - **ssh / mac-telnet / romon / winbox-terminal** — execute surfaces, not
206
+ retrieve surfaces.
207
+ - **tips** — when a `<router>` fails to resolve against an empty CDB, retrieve
208
+ emits the same `tip/no-devices` advice as `devices list`, steering toward
209
+ `centrs devices discover` and `centrs settings`.