@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/LICENSE +21 -0
- package/README.md +333 -0
- package/bin/rosetta.js +34 -0
- package/matrix/2026-03-25/matrix.csv +145 -0
- package/matrix/CLAUDE.md +7 -0
- package/matrix/get-mikrotik-products-csv.sh +20 -0
- package/package.json +34 -0
- package/src/assess-html.ts +267 -0
- package/src/db.ts +360 -0
- package/src/extract-all-versions.ts +147 -0
- package/src/extract-changelogs.ts +266 -0
- package/src/extract-commands.ts +175 -0
- package/src/extract-devices.ts +194 -0
- package/src/extract-html.ts +379 -0
- package/src/extract-properties.ts +234 -0
- package/src/link-commands.ts +208 -0
- package/src/mcp.ts +725 -0
- package/src/query.test.ts +994 -0
- package/src/query.ts +990 -0
- package/src/release.test.ts +280 -0
- package/src/restraml.ts +65 -0
- package/src/search.ts +49 -0
- package/src/setup.ts +224 -0
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
|
+
})();
|