@tikoci/rosetta 0.2.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.
package/src/mcp.ts ADDED
@@ -0,0 +1,725 @@
1
+ /**
2
+ * mcp.ts — MCP server for RouterOS documentation retrieval.
3
+ *
4
+ * Exposes a local SQLite+FTS5 index of RouterOS docs as MCP tools,
5
+ * enabling LLM agents to search documentation, look up properties,
6
+ * and browse the command tree.
7
+ *
8
+ * CLI flags (for compiled binary or `bun run src/mcp.ts`):
9
+ * --setup [--force] Download database + print MCP client config
10
+ * --version Print version
11
+ * --help Print usage
12
+ * (default) Start MCP server (stdio transport)
13
+ *
14
+ * Environment variables:
15
+ * DB_PATH — absolute path to ros-help.db (default: next to binary or project root)
16
+ */
17
+
18
+ declare const VERSION: string;
19
+ declare const IS_COMPILED: boolean;
20
+
21
+ // ── CLI dispatch (before MCP server init) ──
22
+
23
+ const args = process.argv.slice(2);
24
+
25
+ if (args.includes("--version") || args.includes("-v")) {
26
+ const ver = typeof VERSION !== "undefined" ? VERSION : "dev";
27
+ console.log(`rosetta ${ver}`);
28
+ process.exit(0);
29
+ }
30
+
31
+ if (args.includes("--help") || args.includes("-h")) {
32
+ const ver = typeof VERSION !== "undefined" ? VERSION : "dev";
33
+ console.log(`rosetta ${ver} — MCP server for RouterOS documentation`);
34
+ console.log();
35
+ console.log("Usage:");
36
+ console.log(" rosetta Start MCP server (stdio transport)");
37
+ console.log(" rosetta --setup Download database + print MCP client config");
38
+ console.log(" rosetta --setup --force Re-download database");
39
+ console.log(" rosetta --version Print version");
40
+ console.log(" rosetta --help Print this help");
41
+ console.log();
42
+ console.log("Environment:");
43
+ console.log(" DB_PATH Absolute path to ros-help.db (optional)");
44
+ process.exit(0);
45
+ }
46
+
47
+ // Wrap in async IIFE — bun build --compile does not support top-level await
48
+ (async () => {
49
+
50
+ if (args.includes("--setup")) {
51
+ const { runSetup } = await import("./setup.ts");
52
+ await runSetup(args.includes("--force"));
53
+ process.exit(0);
54
+ }
55
+
56
+ // ── MCP Server ──
57
+
58
+ const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
59
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
60
+ const { z } = await import("zod/v3");
61
+
62
+ // Dynamic imports — db.ts eagerly opens the DB file on import,
63
+ // so we must import after the --setup guard to avoid creating
64
+ // an empty ros-help.db on fresh installs.
65
+ //
66
+ // Check if DB has data BEFORE importing db.ts. If empty/missing,
67
+ // auto-download so db.ts opens the real database.
68
+ const _baseDir =
69
+ typeof IS_COMPILED !== "undefined" && IS_COMPILED
70
+ ? (await import("node:path")).dirname(process.execPath)
71
+ : (await import("node:path")).resolve(import.meta.dirname, "..");
72
+ const _dbPath =
73
+ process.env.DB_PATH?.trim() || (await import("node:path")).join(_baseDir, "ros-help.db");
74
+
75
+ const _pageCount = (() => {
76
+ try {
77
+ const check = new (require("bun:sqlite").default)(_dbPath, { readonly: true });
78
+ const row = check.prepare("SELECT COUNT(*) AS c FROM pages").get() as { c: number };
79
+ check.close();
80
+ return row.c;
81
+ } catch {
82
+ return 0;
83
+ }
84
+ })();
85
+
86
+ if (_pageCount === 0) {
87
+ const { downloadDb } = await import("./setup.ts");
88
+ // Use stderr — stdout is the MCP stdio transport
89
+ const log = (msg: string) => process.stderr.write(`${msg}\n`);
90
+ try {
91
+ await downloadDb(_dbPath, log);
92
+ log("Database downloaded successfully.");
93
+ } catch (e) {
94
+ log(`Auto-download failed: ${e}`);
95
+ log(`Run: ${process.argv[0]} --setup`);
96
+ }
97
+ }
98
+
99
+ // Now import db.ts (opens the DB) and query.ts
100
+ const { getDbStats, initDb } = await import("./db.ts");
101
+ const {
102
+ browseCommands,
103
+ browseCommandsAtVersion,
104
+ checkCommandVersions,
105
+ fetchCurrentVersions,
106
+ getPage,
107
+ lookupProperty,
108
+ searchCallouts,
109
+ searchChangelogs,
110
+ searchDevices,
111
+ searchPages,
112
+ searchProperties,
113
+ } = await import("./query.ts");
114
+
115
+ initDb();
116
+
117
+ const server = new McpServer({
118
+ name: "rosetta",
119
+ version: typeof VERSION !== "undefined" ? VERSION : "0.2.0",
120
+ });
121
+
122
+ // ---- routeros_search ----
123
+
124
+ server.registerTool(
125
+ "routeros_search",
126
+ {
127
+ description: `Search RouterOS documentation using natural language.
128
+
129
+ This is the **primary discovery tool**. Start here, then drill down with other tools.
130
+
131
+ Capabilities:
132
+ - Full-text search with BM25 ranking and Porter stemming
133
+ ("configuring" matches "configuration", "configured", etc.)
134
+ - Proximity matching for compound terms ("firewall filter", "bridge vlan")
135
+ - Results include page title, breadcrumb path, help.mikrotik.com URL, and excerpt
136
+ - If AND returns nothing, the engine automatically retries with OR
137
+
138
+ Workflow — what to do next:
139
+ → routeros_get_page: retrieve full content for a result (use page ID from results)
140
+ → routeros_search_properties: find specific properties mentioned in results
141
+ → routeros_search_callouts: find warnings/notes about topics in results
142
+ → routeros_command_tree: browse the command hierarchy for a feature
143
+
144
+ Tips:
145
+ - Use specific technical terms: "DHCP relay agent" not "how to set up DHCP"
146
+ - Documentation: 317 pages from March 2026 Confluence export (~515K words)
147
+ - Docs reflect the then-current long-term release (~7.22), not version-pinned
148
+ - Command data: RouterOS 7.9–7.23beta2. No v6 data available.
149
+ - v6 had different syntax and subsystems — answers for v6 are unreliable.`,
150
+ inputSchema: {
151
+ query: z.string().describe("Natural language search query"),
152
+ limit: z
153
+ .number()
154
+ .int()
155
+ .min(1)
156
+ .max(50)
157
+ .optional()
158
+ .default(8)
159
+ .describe("Max results (default 8)"),
160
+ },
161
+ },
162
+ async ({ query, limit }) => {
163
+ const result = searchPages(query, limit);
164
+ return {
165
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
166
+ };
167
+ },
168
+ );
169
+
170
+ // ---- routeros_get_page ----
171
+
172
+ server.registerTool(
173
+ "routeros_get_page",
174
+ {
175
+ description: `Get the full text of a RouterOS documentation page by ID or title.
176
+
177
+ Use after routeros_search identifies a relevant page. Pass the numeric page ID
178
+ (from search results) or the exact page title (case-insensitive).
179
+
180
+ Returns: plain text, code blocks, and callout blocks (notes, warnings, info, tips).
181
+ Callouts contain crucial caveats and edge-case details — always review them.
182
+
183
+ **Large page handling:** Always set max_length (e.g., 30000) on the first call.
184
+ Some pages are 100K+ chars. When max_length is set and the page exceeds it,
185
+ pages with sections return a **table of contents** instead of truncated text.
186
+ The TOC lists each section's heading, hierarchy level, character count, and
187
+ deep-link URL. Re-call with the section parameter to retrieve specific sections.
188
+
189
+ **Section parameter:** Pass a section heading or anchor_id (from the TOC)
190
+ to get that section's content. Parent sections automatically include all
191
+ sub-section content, so requesting a top-level heading gives you everything
192
+ under it.
193
+
194
+ Recommended workflow for large pages:
195
+ 1. Call with max_length=30000 → get TOC if page is large
196
+ 2. Review section headings to find the relevant section
197
+ 3. Call again with section="Section Name" to get that section's content
198
+
199
+ Workflow — what to do with this content:
200
+ → routeros_search_properties: look up specific properties mentioned in text
201
+ → routeros_lookup_property: get exact details for a named property
202
+ → routeros_search_callouts: find related warnings across other pages
203
+ → routeros_command_tree: browse the command path for features on this page`,
204
+ inputSchema: {
205
+ page: z
206
+ .string()
207
+ .describe("Page ID (numeric) or exact page title"),
208
+ max_length: z
209
+ .number()
210
+ .int()
211
+ .min(1000)
212
+ .optional()
213
+ .describe("Recommended: set to 30000. Max combined text+code length. If exceeded and page has sections, returns a TOC instead of truncated text. Omit only if you need the entire page."),
214
+ section: z
215
+ .string()
216
+ .optional()
217
+ .describe("Section heading or anchor_id from TOC. Returns only that section's content."),
218
+ },
219
+ },
220
+ async ({ page, max_length, section }) => {
221
+ const result = getPage(page, max_length, section);
222
+ if (!result) {
223
+ return {
224
+ content: [{ type: "text", text: `Page not found: ${page}` }],
225
+ isError: true,
226
+ };
227
+ }
228
+ return {
229
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
230
+ };
231
+ },
232
+ );
233
+
234
+ // ---- routeros_lookup_property ----
235
+
236
+ server.registerTool(
237
+ "routeros_lookup_property",
238
+ {
239
+ description: `Look up a specific RouterOS configuration property by exact name.
240
+
241
+ Returns type, default value, description, and documentation page.
242
+ Optionally filter by command path to disambiguate (e.g., "disabled" appears everywhere).
243
+
244
+ This requires the **exact property name**. If you don't know the name:
245
+ → routeros_search_properties: full-text search across property descriptions
246
+ → routeros_search: find the documentation page, then read it with routeros_get_page
247
+
248
+ Examples:
249
+ - name: "add-default-route" → DHCP client property
250
+ - name: "dhcp-snooping" → bridge DHCP snooping toggle
251
+ - name: "disabled", command_path: "/ip/firewall/filter" → firewall filter property
252
+ - name: "chain" → shows all properties named "chain" across all pages`,
253
+ inputSchema: {
254
+ name: z.string().describe("Property name (e.g., 'add-default-route', 'chain')"),
255
+ command_path: z
256
+ .string()
257
+ .optional()
258
+ .describe("RouterOS command path to narrow results (e.g., '/ip/firewall/filter')"),
259
+ },
260
+ },
261
+ async ({ name, command_path }) => {
262
+ const results = lookupProperty(name, command_path);
263
+ if (results.length === 0) {
264
+ return {
265
+ content: [
266
+ {
267
+ type: "text",
268
+ text: `No property found: "${name}"${command_path ? ` under ${command_path}` : ""}\n\nTry instead:\n- routeros_search_properties with a keyword from the property description\n- routeros_search to find the documentation page, then routeros_get_page to read it\n- routeros_command_tree at the command path to see available args`,
269
+ },
270
+ ],
271
+ };
272
+ }
273
+ return {
274
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
275
+ };
276
+ },
277
+ );
278
+
279
+ // ---- routeros_search_properties ----
280
+
281
+ server.registerTool(
282
+ "routeros_search_properties",
283
+ {
284
+ description: `Search RouterOS properties by name or description text.
285
+
286
+ Full-text search across 4,860 property names and descriptions from 145 pages.
287
+ Use when you don't know the exact property name but know what it does.
288
+ If AND returns nothing, the engine automatically retries with OR.
289
+
290
+ If this returns empty:
291
+ → routeros_search: find the documentation page containing the feature
292
+ → routeros_get_page: read the page — properties are embedded in page text
293
+ → routeros_command_tree: browse args at a command path for property names
294
+
295
+ Examples:
296
+ - "gateway reachability check" → finds check-gateway properties
297
+ - "snooping" → finds dhcp-snooping, igmp-snooping properties
298
+ - "trusted" → finds bridge port trusted property`,
299
+ inputSchema: {
300
+ query: z.string().describe("Search query for property descriptions"),
301
+ limit: z
302
+ .number()
303
+ .int()
304
+ .min(1)
305
+ .max(50)
306
+ .optional()
307
+ .default(10)
308
+ .describe("Max results (default 10)"),
309
+ },
310
+ },
311
+ async ({ query, limit }) => {
312
+ const results = searchProperties(query, limit);
313
+ if (results.length === 0) {
314
+ return {
315
+ content: [{ type: "text", text: `No properties matched: "${query}"\n\nTry instead:\n- routeros_search to find the documentation page containing this feature\n- routeros_get_page to read properties directly from page text\n- routeros_command_tree to browse args at the command path\n- Shorter/different keywords (property descriptions are brief)` }],
316
+ };
317
+ }
318
+ return {
319
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
320
+ };
321
+ },
322
+ );
323
+
324
+ // ---- routeros_command_tree ----
325
+
326
+ server.registerTool(
327
+ "routeros_command_tree",
328
+ {
329
+ description: `Browse the RouterOS command tree hierarchy.
330
+
331
+ Given a menu path, returns all direct children (subdirectories, commands, and
332
+ arguments). Each child includes its type and linked documentation page if available.
333
+ Useful for discovering what's available under a command path.
334
+
335
+ Optionally filter by RouterOS version to check what exists in a specific release.
336
+ Command data covers versions 7.9–7.23beta2. No v6 data.
337
+
338
+ Workflow — combine with other tools:
339
+ → routeros_get_page: read the linked documentation page for a command
340
+ → routeros_lookup_property: look up arg names as properties for details
341
+ → routeros_command_version_check: check when a command was added
342
+ → routeros_search_properties: search for properties under this path
343
+
344
+ Examples:
345
+ - path: "/ip" → address, arp, dhcp-client, dhcp-server, firewall, route, etc.
346
+ - path: "/ip/firewall" → filter, nat, mangle, raw, address-list, etc.
347
+ - path: "", version: "7.15" → top-level menus as of RouterOS 7.15`,
348
+ inputSchema: {
349
+ path: z
350
+ .string()
351
+ .optional()
352
+ .default("")
353
+ .describe("RouterOS menu path (e.g., '/ip/firewall'). Empty for top-level."),
354
+ version: z
355
+ .string()
356
+ .optional()
357
+ .describe("RouterOS version to filter by (e.g., '7.15'). Omit for latest."),
358
+ },
359
+ },
360
+ async ({ path, version }) => {
361
+ const cmdPath = path?.trim() || "";
362
+ const results = version
363
+ ? browseCommandsAtVersion(cmdPath, version)
364
+ : browseCommands(cmdPath);
365
+
366
+ if (results.length === 0) {
367
+ return {
368
+ content: [{ type: "text", text: `No commands found under: ${path || "/"}` }],
369
+ };
370
+ }
371
+ return {
372
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
373
+ };
374
+ },
375
+ );
376
+
377
+ // ---- routeros_stats ----
378
+
379
+ server.registerTool(
380
+ "routeros_stats",
381
+ {
382
+ description: `Get database statistics for the RouterOS documentation index.
383
+
384
+ Returns page count, property count, callout count, changelog count, command count, link coverage,
385
+ version range, and documentation export date.
386
+
387
+ Knowledge boundaries:
388
+ - Documentation: March 2026 Confluence HTML export (317 pages), aligned with long-term ~7.22
389
+ - Command tree: RouterOS 7.9–7.23beta2 from inspect.json (with extra-packages from CHR)
390
+ - No RouterOS v6 data available — v6 syntax and subsystems differ significantly from v7
391
+ - For versions older than 7.9, no command tree data exists
392
+ - Versions older than current long-term are unpatched by MikroTik
393
+ - Absence of a peripheral in docs doesn't mean unsupported — most MBIM modems work`,
394
+ inputSchema: {},
395
+ },
396
+ async () => {
397
+ const stats = getDbStats();
398
+ return {
399
+ content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
400
+ };
401
+ },
402
+ );
403
+
404
+ // ---- routeros_search_callouts ----
405
+
406
+ server.registerTool(
407
+ "routeros_search_callouts",
408
+ {
409
+ description: `Search note, warning, tip, and info callout blocks across all RouterOS documentation.
410
+
411
+ 1,034 callouts containing important caveats, edge cases, and non-obvious behavior.
412
+ Useful for finding warnings about hardware offloading, compatibility notes,
413
+ or unexpected feature interactions that aren't obvious from main page text.
414
+
415
+ Query tips:
416
+ - Use SHORT keyword queries (1-2 terms). Callouts are brief — multi-word NL phrases often miss.
417
+ - "bridge" finds more than "bridge VLAN spanning tree conflict"
418
+ - Pass type only (no query) to browse callouts of that type
419
+ - If AND finds nothing, the engine automatically retries with OR
420
+
421
+ Optionally filter by callout type: "note" (426), "info" (357), "warning" (213), or "tip" (38).
422
+
423
+ Examples:
424
+ - query: "hardware offload" → warnings about bridge HW offloading limitations
425
+ - query: "VLAN", type: "warning" → only VLAN-related warnings
426
+ - query: "bridge", type: "warning" → bridge-related warnings
427
+ - type: "warning", limit: 20 → browse 20 warnings (no search term needed)`,
428
+ inputSchema: {
429
+ query: z.string().optional().default("").describe("Search keywords for callout content (keep short — 1-2 terms work best)"),
430
+ type: z
431
+ .enum(["note", "warning", "info", "tip"])
432
+ .optional()
433
+ .describe("Filter by callout type"),
434
+ limit: z
435
+ .number()
436
+ .int()
437
+ .min(1)
438
+ .max(50)
439
+ .optional()
440
+ .default(10)
441
+ .describe("Max results (default 10)"),
442
+ },
443
+ },
444
+ async ({ query, type, limit }) => {
445
+ const results = searchCallouts(query, type, limit);
446
+ return {
447
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
448
+ };
449
+ },
450
+ );
451
+
452
+ // ---- routeros_search_changelogs ----
453
+
454
+ server.registerTool(
455
+ "routeros_search_changelogs",
456
+ {
457
+ description: `Search MikroTik RouterOS changelogs — parsed per-entry with category and breaking-change flags.
458
+
459
+ Each entry is one *) or !) line from MikroTik's official changelogs, parsed into category + description.
460
+ Entries marked !) are breaking changes that may require config adjustments after upgrade.
461
+
462
+ **Upgrade-breakage workflow**: User says "X broke after upgrading from A to B":
463
+ 1. Search changelogs with from_version=A, to_version=B, and the subsystem as query
464
+ 2. Look for !) breaking changes that explain the behavior change
465
+ 3. → routeros_get_page for the subsystem's documentation
466
+ 4. → routeros_search_callouts for version-specific warnings
467
+ 5. → routeros_command_version_check to see if commands were added/removed
468
+
469
+ Supports: FTS keyword search, version range filtering, category filtering, breaking-only mode.
470
+ Categories are subsystem names: bgp, bridge, dhcpv4-server, wifi, ipsec, console, container, etc.
471
+
472
+ Empty query with filters → browse mode (e.g., all breaking changes in 7.22).
473
+ Coverage depends on which versions were extracted — typically matches ros_versions table.`,
474
+ inputSchema: {
475
+ query: z
476
+ .string()
477
+ .optional()
478
+ .default("")
479
+ .describe("Search text (FTS). Omit for filter-only browse"),
480
+ version: z
481
+ .string()
482
+ .optional()
483
+ .describe("Exact version (e.g., '7.22'). Mutually exclusive with from/to"),
484
+ from_version: z
485
+ .string()
486
+ .optional()
487
+ .describe("Start of version range, inclusive (e.g., '7.21')"),
488
+ to_version: z
489
+ .string()
490
+ .optional()
491
+ .describe("End of version range, inclusive (e.g., '7.22.1')"),
492
+ category: z
493
+ .string()
494
+ .optional()
495
+ .describe("Filter by subsystem category (e.g., 'bgp', 'bridge', 'wifi')"),
496
+ breaking_only: z
497
+ .boolean()
498
+ .optional()
499
+ .describe("Only return !) breaking/important changes"),
500
+ limit: z
501
+ .number()
502
+ .int()
503
+ .min(1)
504
+ .max(100)
505
+ .optional()
506
+ .default(20)
507
+ .describe("Max results (default 20)"),
508
+ },
509
+ },
510
+ async ({ query, version, from_version, to_version, category, breaking_only, limit }) => {
511
+ const results = searchChangelogs(query || "", {
512
+ version,
513
+ fromVersion: from_version,
514
+ toVersion: to_version,
515
+ category,
516
+ breakingOnly: breaking_only,
517
+ limit,
518
+ });
519
+
520
+ if (results.length === 0) {
521
+ const hints = [
522
+ query ? "Try broader search terms or remove the query to browse by filters" : null,
523
+ version ? `No changelog data for version ${version} — it may not have been extracted` : null,
524
+ from_version || to_version ? "Try widening the version range" : null,
525
+ category ? `Try without category filter, or check spelling (categories are lowercase: bgp, bridge, wifi, etc.)` : null,
526
+ breaking_only ? "Try without breaking_only — the change may not be marked as breaking" : null,
527
+ "Use routeros_search or routeros_search_callouts for documentation-based answers",
528
+ ].filter(Boolean);
529
+ return {
530
+ content: [
531
+ {
532
+ type: "text",
533
+ text: `No changelog entries matched${query ? `: "${query}"` : ""}${version ? ` (version: ${version})` : ""}${from_version || to_version ? ` (range: ${from_version || "?"} → ${to_version || "?"})` : ""}\n\nTry:\n${hints.map((h) => `- ${h}`).join("\n")}`,
534
+ },
535
+ ],
536
+ };
537
+ }
538
+ return {
539
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
540
+ };
541
+ },
542
+ );
543
+
544
+ // ---- routeros_command_version_check ----
545
+
546
+ server.registerTool(
547
+ "routeros_command_version_check",
548
+ {
549
+ description: `Check which RouterOS versions include a specific command path.
550
+
551
+ Returns the list of versions where the command exists, plus first_seen/last_seen.
552
+ If the command exists in our earliest tracked version, a note warns that it likely
553
+ predates our data — check the documentation page for earlier version references.
554
+
555
+ Useful for answering "is /container supported in 7.12?" or "when was /ip/firewall/raw added?".
556
+
557
+ Command data covers versions 7.9–7.23beta2. No v6 data.
558
+ For versions below 7.9, no command tree data exists — the command may still exist there.
559
+ Cross-reference with routeros_get_page or routeros_search_callouts for version mentions
560
+ in documentation text. → routeros_search_changelogs to see what changed between versions.
561
+
562
+ Examples:
563
+ - command_path: "/container" → shows versions where container support exists
564
+ - command_path: "/ip/firewall/raw" → shows version range`,
565
+ inputSchema: {
566
+ command_path: z
567
+ .string()
568
+ .describe("RouterOS command path (e.g., '/container', '/ip/firewall/raw')"),
569
+ },
570
+ },
571
+ async ({ command_path }) => {
572
+ const result = checkCommandVersions(command_path);
573
+ return {
574
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
575
+ };
576
+ },
577
+ );
578
+
579
+ // ---- routeros_device_lookup ----
580
+
581
+ server.registerTool(
582
+ "routeros_device_lookup",
583
+ {
584
+ description: `Look up MikroTik hardware specs or search for devices matching criteria.
585
+
586
+ 144 products from mikrotik.com/products/matrix (March 2026). Returns hardware specs
587
+ including CPU, RAM, storage, ports, PoE, wireless, license level, and price.
588
+
589
+ **How it works:**
590
+ - If query matches a product name or code exactly → returns full specs for that device
591
+ - Otherwise → FTS search + optional structured filters → returns matching devices
592
+ - Filters can be used alone (no query) to find devices by capability
593
+
594
+ **License levels** determine feature availability:
595
+ - L3: CPE/home (no routing protocols, limited queues)
596
+ - L4: standard (OSPF, BGP, all firewall features)
597
+ - L5: ISP (unlimited tunnels, no peer limits)
598
+ - L6: controller (CAPsMAN unlimited, full cluster)
599
+
600
+ **Architecture** determines available packages and performance characteristics:
601
+ - ARM 64bit: modern high-end (CCR2xxx, CRS5xx, hAP ax², RB5009)
602
+ - ARM 32bit: mid-range (Audience, cAP ax, some switches)
603
+ - MMIPS: budget gigabit (hEX, hEX S)
604
+ - MIPSBE: legacy (older hAP, BaseBox, SXT)
605
+ - SMIPS: lowest-end (hAP lite)
606
+
607
+ Workflow — combine with other tools:
608
+ → routeros_search: find documentation for features relevant to a device
609
+ → routeros_command_tree: check commands available for a feature
610
+ → routeros_current_versions: check latest firmware for the device
611
+
612
+ Data: 144 products, March 2026 snapshot. Not all MikroTik products ever made — only currently listed products.`,
613
+ inputSchema: {
614
+ query: z
615
+ .string()
616
+ .optional()
617
+ .default("")
618
+ .describe("Product name, code, or search terms (e.g., 'hAP ax³', 'CCR2216', 'ARM 64bit router')"),
619
+ architecture: z
620
+ .string()
621
+ .optional()
622
+ .describe("Filter: ARM 64bit, ARM 32bit, MIPSBE, MMIPS, or SMIPS"),
623
+ min_ram_mb: z
624
+ .number()
625
+ .int()
626
+ .optional()
627
+ .describe("Filter: minimum RAM in megabytes (e.g., 256, 1024)"),
628
+ license_level: z
629
+ .number()
630
+ .int()
631
+ .optional()
632
+ .describe("Filter: exact license level (3, 4, 5, or 6)"),
633
+ min_storage_mb: z
634
+ .number()
635
+ .int()
636
+ .optional()
637
+ .describe("Filter: minimum storage in megabytes (e.g., 128). Devices with 16 MB storage can't fit extra packages"),
638
+ has_poe: z
639
+ .boolean()
640
+ .optional()
641
+ .describe("Filter: device has PoE in or PoE out"),
642
+ has_wireless: z
643
+ .boolean()
644
+ .optional()
645
+ .describe("Filter: device has wireless radios (2.4 GHz and/or 5 GHz)"),
646
+ has_lte: z
647
+ .boolean()
648
+ .optional()
649
+ .describe("Filter: device has LTE/cellular capability (SIM slot)"),
650
+ limit: z
651
+ .number()
652
+ .int()
653
+ .min(1)
654
+ .max(50)
655
+ .optional()
656
+ .default(10)
657
+ .describe("Max results (default 10)"),
658
+ },
659
+ },
660
+ async ({ query, architecture, min_ram_mb, min_storage_mb, license_level, has_poe, has_wireless, has_lte, limit }) => {
661
+ const filters = {
662
+ ...(architecture ? { architecture } : {}),
663
+ ...(min_ram_mb ? { min_ram_mb } : {}),
664
+ ...(min_storage_mb ? { min_storage_mb } : {}),
665
+ ...(license_level ? { license_level } : {}),
666
+ ...(has_poe ? { has_poe } : {}),
667
+ ...(has_wireless ? { has_wireless } : {}),
668
+ ...(has_lte ? { has_lte } : {}),
669
+ };
670
+ const result = searchDevices(query || "", filters, limit);
671
+
672
+ if (result.results.length === 0) {
673
+ const hints = [
674
+ query ? "Try a shorter or different product name" : null,
675
+ Object.keys(filters).length > 0 ? "Try removing some filters" : null,
676
+ "Use routeros_search to find documentation pages about this topic",
677
+ ].filter(Boolean);
678
+ return {
679
+ content: [
680
+ {
681
+ type: "text",
682
+ text: `No devices matched${query ? `: "${query}"` : ""}${Object.keys(filters).length > 0 ? ` (with ${Object.keys(filters).length} filter${Object.keys(filters).length > 1 ? "s" : ""})` : ""}\n\nTry:\n${hints.map((h) => `- ${h}`).join("\n")}`,
683
+ },
684
+ ],
685
+ };
686
+ }
687
+ return {
688
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
689
+ };
690
+ },
691
+ );
692
+
693
+ // ---- routeros_current_versions ----
694
+
695
+ server.registerTool(
696
+ "routeros_current_versions",
697
+ {
698
+ description: `Fetch current RouterOS version numbers from MikroTik's upgrade server.
699
+
700
+ Returns the latest version for each release channel: stable, long-term, testing, development.
701
+ Useful for determining if a user's version is current, outdated, or unpatched.
702
+
703
+ Key context for version reasoning:
704
+ - The long-term channel is the recommended minimum — MikroTik does not patch older branches
705
+ - Our documentation aligns with the long-term release at export time (~7.22)
706
+ - Our command tree data covers 7.9–7.23beta2
707
+ - If a user's version is older than the current long-term, recommend upgrading
708
+
709
+ Requires network access to upgrade.mikrotik.com.`,
710
+ inputSchema: {},
711
+ },
712
+ async () => {
713
+ const result = await fetchCurrentVersions();
714
+ return {
715
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
716
+ };
717
+ },
718
+ );
719
+
720
+ // ---- Start ----
721
+
722
+ const transport = new StdioServerTransport();
723
+ await server.connect(transport);
724
+
725
+ })();