@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,333 @@
1
+ import { asCentrsError, formatCentrsErrorText } from "../errors.ts";
2
+ import {
3
+ buildExecuteErrorEnvelope,
4
+ type ExecuteOutputFormat,
5
+ type ExecuteRequest,
6
+ executeEnvelope,
7
+ executeOutputFormats,
8
+ renderExecuteEnvelope,
9
+ } from "../execute.ts";
10
+ import { describeCentrs } from "../index.ts";
11
+ import {
12
+ type CliCommandMetadata,
13
+ expectValue,
14
+ renderCommandHelp,
15
+ } from "./common.ts";
16
+
17
+ export const executeCommand: CliCommandMetadata = {
18
+ name: "execute",
19
+ usage: "centrs execute <target> <command> [flags]",
20
+ summary:
21
+ "Run a RouterOS read or write command via native API, REST, or mac-telnet.",
22
+ options: [
23
+ {
24
+ flag: "--via",
25
+ valueName: "<native-api|rest-api|mac-telnet>",
26
+ description:
27
+ "Pin the protocol selector; no silent downgrade when set. A bare MAC target defaults to mac-telnet.",
28
+ },
29
+ {
30
+ flag: "--host",
31
+ valueName: "<host|url>",
32
+ description: "Override the resolved host or base URL for the target.",
33
+ },
34
+ {
35
+ flag: "--port",
36
+ valueName: "<port>",
37
+ description: "Override the resolved management port.",
38
+ },
39
+ {
40
+ flag: "--username / --user / -u",
41
+ valueName: "<name>",
42
+ description:
43
+ "RouterOS username (aliases `--user`, `-u`). Falls back to `CENTRS_USERNAME`.",
44
+ },
45
+ {
46
+ flag: "--password",
47
+ valueName: "<secret>",
48
+ description: "RouterOS password. Falls back to `CENTRS_PASSWORD`.",
49
+ },
50
+ {
51
+ flag: "--ssh-key",
52
+ valueName: "<path>",
53
+ description:
54
+ "`--via ssh`: explicit private-key path. Falls back to `CENTRS_SSH_KEY` / the `ssh-agent`.",
55
+ },
56
+ {
57
+ flag: "--insecure",
58
+ description:
59
+ "Disable SSH host-key verification (`--via ssh`: accepts changed/impersonated keys, not just new) or accept a self-signed `api-ssl` TLS cert. Default verifies.",
60
+ },
61
+ {
62
+ flag: "--cdb-file",
63
+ valueName: "<path>",
64
+ description: "Read target credentials from a WinBox CDB file.",
65
+ },
66
+ {
67
+ flag: "--cdb-password",
68
+ valueName: "<password>",
69
+ description: "Decrypt an encrypted WinBox CDB file.",
70
+ },
71
+ {
72
+ flag: "--resolve",
73
+ valueName: "<none|arp>",
74
+ description:
75
+ "Resolve a MAC-address target to an IP via the host ARP cache (default none).",
76
+ },
77
+ {
78
+ flag: "--timeout",
79
+ valueName: "<duration>",
80
+ description: "Per-request timeout (for REST, max 60s).",
81
+ },
82
+ {
83
+ flag: "--validate[=false]",
84
+ description:
85
+ "Run RouterOS :parse and /console/inspect validation before execution (default true).",
86
+ },
87
+ {
88
+ flag: "--yes",
89
+ description:
90
+ "Confirm write-shaped add/set/remove commands in non-interactive runs.",
91
+ },
92
+ {
93
+ flag: "--max-results",
94
+ valueName: "<bytes>",
95
+ description: "Fail if the rendered envelope exceeds this byte budget.",
96
+ },
97
+ {
98
+ flag: "--format",
99
+ valueName: "<text|json|yaml>",
100
+ description:
101
+ "Output format for the CLI response. Defaults to text; use --json or --format json for the structured envelope.",
102
+ },
103
+ {
104
+ flag: "--json",
105
+ description: "Shortcut for `--format json`.",
106
+ },
107
+ {
108
+ flag: "--verbose",
109
+ description: "Include additional context in text output.",
110
+ },
111
+ {
112
+ flag: "--",
113
+ description:
114
+ "End centrs option parsing: every following token is the literal RouterOS command, even flag-shaped ones (e.g. `-- /interface print where disabled=yes`).",
115
+ },
116
+ ],
117
+ };
118
+
119
+ interface ExecuteCliArgs extends ExecuteRequest {
120
+ help?: boolean;
121
+ format?: ExecuteOutputFormat;
122
+ }
123
+
124
+ export function parseExecuteCliArgs(args: readonly string[]): ExecuteCliArgs {
125
+ const parsed: ExecuteCliArgs = { command: "" };
126
+ const positional: string[] = [];
127
+ let endOfOptions = false;
128
+
129
+ for (let index = 0; index < args.length; index += 1) {
130
+ const arg = args[index];
131
+ if (arg === undefined) {
132
+ continue;
133
+ }
134
+ // A bare `--` ends centrs option parsing: every later token is part of the
135
+ // literal RouterOS command, even flag-shaped ones. Lets a command carry
136
+ // tokens like `--` / `-foo` (or a value that looks like a flag) without the
137
+ // parser claiming them. `--` itself is consumed, not added to the command.
138
+ if (endOfOptions) {
139
+ positional.push(arg);
140
+ continue;
141
+ }
142
+ if (arg === "--") {
143
+ endOfOptions = true;
144
+ continue;
145
+ }
146
+ if (arg.startsWith("--validate=")) {
147
+ parsed.validate = parseBooleanFlag(arg.slice("--validate=".length), arg);
148
+ continue;
149
+ }
150
+ switch (arg) {
151
+ case "--help":
152
+ case "-h":
153
+ parsed.help = true;
154
+ break;
155
+ case "--json":
156
+ parsed.format = "json";
157
+ break;
158
+ case "--verbose":
159
+ parsed.verbose = true;
160
+ break;
161
+ case "--yes":
162
+ parsed.yes = true;
163
+ break;
164
+ case "--validate":
165
+ parsed.validate = true;
166
+ break;
167
+ case "--no-validate":
168
+ parsed.validate = false;
169
+ break;
170
+ case "--via":
171
+ parsed.via = expectValue(args, ++index, arg);
172
+ break;
173
+ case "--host":
174
+ parsed.host = expectValue(args, ++index, arg);
175
+ break;
176
+ case "--port":
177
+ parsed.port = parseIntegerFlag(expectValue(args, ++index, arg), arg);
178
+ break;
179
+ case "--user":
180
+ case "-u":
181
+ case "--username":
182
+ parsed.username = expectValue(args, ++index, arg);
183
+ break;
184
+ case "--password":
185
+ parsed.password = expectValue(args, ++index, arg);
186
+ break;
187
+ case "--ssh-key":
188
+ parsed.sshKey = expectValue(args, ++index, arg);
189
+ break;
190
+ case "--insecure":
191
+ parsed.insecure = true;
192
+ break;
193
+ case "--cdb-file":
194
+ parsed.cdbFile = expectValue(args, ++index, arg);
195
+ break;
196
+ case "--cdb-password":
197
+ parsed.cdbPassword = expectValue(args, ++index, arg);
198
+ break;
199
+ case "--resolve":
200
+ parsed.resolve = expectValue(args, ++index, arg);
201
+ break;
202
+ case "--timeout":
203
+ parsed.timeout = expectValue(args, ++index, arg);
204
+ break;
205
+ case "--max-results":
206
+ parsed.maxResultsBytes = parseIntegerFlag(
207
+ expectValue(args, ++index, arg),
208
+ arg,
209
+ );
210
+ break;
211
+ case "--format": {
212
+ const value = expectValue(args, ++index, arg);
213
+ if (!executeOutputFormats.includes(value as ExecuteOutputFormat)) {
214
+ throw new Error(
215
+ `--format must be one of ${executeOutputFormats.join(", ")}; got ${value}.`,
216
+ );
217
+ }
218
+ parsed.format = value as ExecuteOutputFormat;
219
+ break;
220
+ }
221
+ default:
222
+ if (arg.startsWith("-")) {
223
+ throw new Error(`Unknown execute flag: ${arg}`);
224
+ }
225
+ positional.push(arg);
226
+ break;
227
+ }
228
+ }
229
+
230
+ parsed.targetInput = positional[0];
231
+ parsed.command = positional.slice(1).join(" ");
232
+ parsed.stdinIsTty = process.stdin.isTTY;
233
+ return parsed;
234
+ }
235
+
236
+ export async function runExecuteCli(args: readonly string[]): Promise<number> {
237
+ let parsed: ExecuteCliArgs | undefined;
238
+ try {
239
+ parsed = parseExecuteCliArgs(args);
240
+ if (parsed.help) {
241
+ console.log(renderCommandHelp(describeCentrs(), executeCommand));
242
+ return 0;
243
+ }
244
+
245
+ const envelope = await executeEnvelope(parsed);
246
+ const resolvedFormat =
247
+ (
248
+ envelope.meta.operation as {
249
+ request?: { format?: ExecuteOutputFormat };
250
+ }
251
+ )?.request?.format ??
252
+ parsed.format ??
253
+ "text";
254
+ const rendered = renderExecuteEnvelope(envelope, resolvedFormat, {
255
+ verbose: parsed.verbose,
256
+ });
257
+ if (envelope.ok) {
258
+ console.log(rendered);
259
+ return 0;
260
+ }
261
+ console.error(rendered);
262
+ return 1;
263
+ } catch (error) {
264
+ // Parse/usage errors land here too: surface them through the same typed
265
+ // envelope as every other runner (no raw message, no exit code 2).
266
+ const format = inferExecuteFormat(args, parsed);
267
+ if (format === "json" || format === "yaml") {
268
+ const envelope = buildExecuteErrorEnvelope(
269
+ parsed ?? { command: "" },
270
+ error,
271
+ );
272
+ console.error(
273
+ renderExecuteEnvelope(envelope, format, {
274
+ verbose: parsed?.verbose ?? false,
275
+ }),
276
+ );
277
+ } else {
278
+ console.error(
279
+ formatCentrsErrorText(
280
+ asCentrsError(error, {
281
+ code: "input/invalid-command",
282
+ summary: error instanceof Error ? error.message : String(error),
283
+ remediation:
284
+ "Use `centrs execute --help` to inspect the supported command shape and flags.",
285
+ }),
286
+ { verbose: parsed?.verbose ?? args.includes("--verbose") },
287
+ ),
288
+ );
289
+ }
290
+ return 1;
291
+ }
292
+ }
293
+
294
+ function inferExecuteFormat(
295
+ args: readonly string[],
296
+ parsed?: ExecuteCliArgs,
297
+ ): ExecuteOutputFormat {
298
+ if (parsed?.format) {
299
+ return parsed.format;
300
+ }
301
+ if (args.includes("--json")) {
302
+ return "json";
303
+ }
304
+ const index = args.indexOf("--format");
305
+ if (index >= 0) {
306
+ const value = args[index + 1];
307
+ if (
308
+ value !== undefined &&
309
+ executeOutputFormats.includes(value as ExecuteOutputFormat)
310
+ ) {
311
+ return value as ExecuteOutputFormat;
312
+ }
313
+ }
314
+ return "text";
315
+ }
316
+
317
+ function parseIntegerFlag(value: string, flag: string): number {
318
+ const parsed = Number.parseInt(value, 10);
319
+ if (!Number.isInteger(parsed)) {
320
+ throw new Error(`${flag} must be an integer; got ${value}.`);
321
+ }
322
+ return parsed;
323
+ }
324
+
325
+ function parseBooleanFlag(value: string, flag: string): boolean {
326
+ if (["true", "1", "yes", "on"].includes(value.toLowerCase())) {
327
+ return true;
328
+ }
329
+ if (["false", "0", "no", "off"].includes(value.toLowerCase())) {
330
+ return false;
331
+ }
332
+ throw new Error(`${flag} must be true or false; got ${value}.`);
333
+ }
package/src/cli/mcp.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * `centrs mcp` CLI surface: starts the stdio MCP server. Arg parsing here is
3
+ * deliberately thin — the server's behavior is governed by the CDB allowlist
4
+ * and the global toggles (`--cdb-file`/`--cdb-password`/
5
+ * `--allow-adhoc-targets`), resolved in `../mcp/config.ts`.
6
+ *
7
+ * Transport is stdio only; HTTP/remote access is the proxy surface's job.
8
+ */
9
+
10
+ import { asCentrsError, formatCentrsErrorText } from "../errors.ts";
11
+ import { describeCentrs } from "../index.ts";
12
+ import { resolveMcpConfig } from "../mcp/config.ts";
13
+ import { runMcpStdio } from "../mcp/server.ts";
14
+ import {
15
+ type CliCommandMetadata,
16
+ expectValue,
17
+ renderCommandHelp,
18
+ } from "./common.ts";
19
+
20
+ export const mcpCommand: CliCommandMetadata = {
21
+ name: "mcp",
22
+ usage: "centrs mcp [start] [--cdb-file <path>] [--allow-adhoc-targets]",
23
+ summary:
24
+ "Start the centrs MCP server (stdio) — scoped RouterOS tools gated by the CDB allowlist.",
25
+ options: [
26
+ {
27
+ flag: "--cdb-file",
28
+ valueName: "<path>",
29
+ description:
30
+ "CDB allowlist path (default `~/.config/tikoci/winbox.cdb`, or CENTRS_CDB_FILE).",
31
+ },
32
+ {
33
+ flag: "--cdb-password",
34
+ valueName: "<password>",
35
+ description:
36
+ "Decrypt password for an encrypted CDB (or CENTRS_CDB_PASSWORD).",
37
+ },
38
+ {
39
+ flag: "--allow-adhoc-targets",
40
+ description:
41
+ "Reserved unsafe escape hatch for future inline targets; current tools still use CDB-only targets.",
42
+ },
43
+ { flag: "--help", description: "Show this help." },
44
+ ],
45
+ };
46
+
47
+ interface McpCliArgs {
48
+ help?: boolean;
49
+ cdbFile?: string;
50
+ cdbPassword?: string;
51
+ allowAdhocTargets?: boolean;
52
+ }
53
+
54
+ export function parseMcpCliArgs(args: readonly string[]): McpCliArgs {
55
+ const parsed: McpCliArgs = {};
56
+ for (let index = 0; index < args.length; index += 1) {
57
+ const arg = args[index];
58
+ switch (arg) {
59
+ case "start":
60
+ // Optional verb; `centrs mcp` and `centrs mcp start` are equivalent.
61
+ break;
62
+ case "--help":
63
+ case "-h":
64
+ parsed.help = true;
65
+ break;
66
+ case "--cdb-file":
67
+ parsed.cdbFile = expectValue(args, ++index, "--cdb-file");
68
+ break;
69
+ case "--cdb-password":
70
+ parsed.cdbPassword = expectValue(args, ++index, "--cdb-password");
71
+ break;
72
+ case "--allow-adhoc-targets":
73
+ parsed.allowAdhocTargets = true;
74
+ break;
75
+ default:
76
+ throw new Error(`Unknown flag for centrs mcp: ${arg}`);
77
+ }
78
+ }
79
+ return parsed;
80
+ }
81
+
82
+ export async function runMcpCli(args: readonly string[]): Promise<number> {
83
+ try {
84
+ const parsed = parseMcpCliArgs(args);
85
+ if (parsed.help) {
86
+ console.log(renderCommandHelp(describeCentrs(), mcpCommand));
87
+ return 0;
88
+ }
89
+ const config = resolveMcpConfig({
90
+ cdbFile: parsed.cdbFile,
91
+ cdbPassword: parsed.cdbPassword,
92
+ allowAdhocTargets: parsed.allowAdhocTargets,
93
+ });
94
+ await runMcpStdio(config);
95
+ return 0;
96
+ } catch (error) {
97
+ console.error(
98
+ formatCentrsErrorText(
99
+ asCentrsError(error, {
100
+ code: "internal/mcp-start",
101
+ summary: error instanceof Error ? error.message : String(error),
102
+ remediation:
103
+ "Check `centrs mcp --help` for supported flags and the CDB path.",
104
+ }),
105
+ ),
106
+ );
107
+ return 1;
108
+ }
109
+ }