@usex/mikrotik-mcp 4.14.0 → 4.16.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/dist/cli.js +245 -131
- package/dist/index.js +1 -1
- package/dist/shared/{cli-cb2zp05h.js → cli-9s02dq5d.js} +1 -1
- package/dist/shared/{cli-bk79xfcq.js → cli-w6ecrhxa.js} +49 -6
- package/dist/shared/{library-k3ygd0y8.js → library-212xbkwr.js} +9 -5
- package/dist/shared/{library-sfdm0xh9.js → library-c0fxxq85.js} +1 -1
- package/dist/ui/observability.html +60 -60
- package/package.json +1 -1
- package/schemas/tool-catalog.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
devicesView,
|
|
46
46
|
diffLines,
|
|
47
47
|
executeMikrotikCommand,
|
|
48
|
+
fetchAllReleases,
|
|
48
49
|
fetchDevices,
|
|
49
50
|
fetchLatestRelease,
|
|
50
51
|
getConfig,
|
|
@@ -89,6 +90,7 @@ import {
|
|
|
89
90
|
resetRadiusCounters,
|
|
90
91
|
resolveDeviceName,
|
|
91
92
|
restoreLocalBackup,
|
|
93
|
+
riskOf,
|
|
92
94
|
s3Target,
|
|
93
95
|
sampleAllTraffic,
|
|
94
96
|
sampleDeviceTraffic,
|
|
@@ -106,18 +108,18 @@ import {
|
|
|
106
108
|
updateAaaEntity,
|
|
107
109
|
updateSummaryLine,
|
|
108
110
|
writeBackup
|
|
109
|
-
} from "./shared/cli-
|
|
111
|
+
} from "./shared/cli-w6ecrhxa.js";
|
|
110
112
|
|
|
111
113
|
// src/cli.ts
|
|
112
114
|
import { existsSync as existsSync2 } from "fs";
|
|
113
115
|
|
|
114
116
|
// src/observability/dashboard.ts
|
|
115
117
|
import { spawn } from "child_process";
|
|
116
|
-
import { readFileSync as
|
|
118
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
117
119
|
import { homedir, networkInterfaces } from "os";
|
|
118
|
-
import { dirname as dirname4, join as
|
|
120
|
+
import { dirname as dirname4, join as join3 } from "path";
|
|
119
121
|
var {serve } = globalThis.Bun;
|
|
120
|
-
import { z } from "zod";
|
|
122
|
+
import { z as z2 } from "zod";
|
|
121
123
|
|
|
122
124
|
// src/observability/modules.ts
|
|
123
125
|
var lcSet = (xs) => new Set((xs ?? []).map((s) => s.toLowerCase()));
|
|
@@ -370,6 +372,149 @@ function historyBytes() {
|
|
|
370
372
|
return total;
|
|
371
373
|
}
|
|
372
374
|
|
|
375
|
+
// src/prompts/index.ts
|
|
376
|
+
import { readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
377
|
+
import { join as join2 } from "path";
|
|
378
|
+
import { z } from "zod";
|
|
379
|
+
function parseFrontmatter(raw) {
|
|
380
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
381
|
+
if (!match)
|
|
382
|
+
return null;
|
|
383
|
+
const [, fm, body] = match;
|
|
384
|
+
const meta = {};
|
|
385
|
+
const args = [];
|
|
386
|
+
let cur = null;
|
|
387
|
+
let inArgs = false;
|
|
388
|
+
for (const line of fm.split(`
|
|
389
|
+
`)) {
|
|
390
|
+
if (/^arguments:\s*$/.test(line)) {
|
|
391
|
+
inArgs = true;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (inArgs) {
|
|
395
|
+
const item = line.match(/^[ \t]*-[ \t]*name:[ \t]*(\S.*)$/);
|
|
396
|
+
if (item) {
|
|
397
|
+
if (cur)
|
|
398
|
+
args.push(cur);
|
|
399
|
+
cur = { name: item[1].trim() };
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const prop = line.match(/^[ \t]+(description|required):[ \t]*(\S.*)$/);
|
|
403
|
+
if (prop && cur) {
|
|
404
|
+
if (prop[1] === "required")
|
|
405
|
+
cur.required = /^(true|yes)$/i.test(prop[2].trim());
|
|
406
|
+
else
|
|
407
|
+
cur.description = prop[2].trim().replace(/^["']|["']$/g, "");
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (/^\S/.test(line))
|
|
411
|
+
inArgs = false;
|
|
412
|
+
}
|
|
413
|
+
const kv = line.match(/^(\w+):[ \t]*(\S.*)?$/);
|
|
414
|
+
if (kv && !inArgs)
|
|
415
|
+
meta[kv[1]] = (kv[2] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
416
|
+
}
|
|
417
|
+
if (cur)
|
|
418
|
+
args.push(cur);
|
|
419
|
+
if (!meta.name)
|
|
420
|
+
return null;
|
|
421
|
+
return {
|
|
422
|
+
name: meta.name,
|
|
423
|
+
title: meta.title ?? meta.name,
|
|
424
|
+
description: meta.description ?? "",
|
|
425
|
+
arguments: args,
|
|
426
|
+
body: body.trim()
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function substitute(body, vars) {
|
|
430
|
+
return body.replace(/\{\{\s*(\w+)\s*\}\}/g, (whole, key) => {
|
|
431
|
+
const v = vars[key];
|
|
432
|
+
if (v === undefined || v === null || v === "")
|
|
433
|
+
return whole;
|
|
434
|
+
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function deviceArgDescription(directory) {
|
|
438
|
+
if (directory && directory.length > 0) {
|
|
439
|
+
const rows = directory.map((d) => `\u2022 ${d.key}${d.label && d.label !== d.key ? ` ("${d.label}")` : ""} \u2192 ${d.target}${d.isDefault ? " [default]" : ""}`).join(`
|
|
440
|
+
`);
|
|
441
|
+
return "Which configured MikroTik device to run this workflow on. " + `Pass the EXACT config key or its label. Configured devices:
|
|
442
|
+
` + `${rows}
|
|
443
|
+
` + "Omit to use the default device.";
|
|
444
|
+
}
|
|
445
|
+
return "Which configured MikroTik device to run this workflow on. Omit to use the default device.";
|
|
446
|
+
}
|
|
447
|
+
function listPrompts() {
|
|
448
|
+
let files;
|
|
449
|
+
try {
|
|
450
|
+
files = readdirSync2(PROMPTS_DIR).filter((f) => f.endsWith(".md"));
|
|
451
|
+
} catch {
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
const out = [];
|
|
455
|
+
for (const file of files) {
|
|
456
|
+
try {
|
|
457
|
+
const parsed = parseFrontmatter(readFileSync2(join2(PROMPTS_DIR, file), "utf8"));
|
|
458
|
+
if (parsed) {
|
|
459
|
+
out.push({
|
|
460
|
+
name: parsed.name,
|
|
461
|
+
title: parsed.title,
|
|
462
|
+
description: parsed.description,
|
|
463
|
+
arguments: parsed.arguments,
|
|
464
|
+
body: parsed.body
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
} catch {}
|
|
468
|
+
}
|
|
469
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
470
|
+
}
|
|
471
|
+
function registerPrompts(server, opts = {}) {
|
|
472
|
+
let files;
|
|
473
|
+
try {
|
|
474
|
+
files = readdirSync2(PROMPTS_DIR).filter((f) => f.endsWith(".md"));
|
|
475
|
+
} catch {
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
const multiDevice = opts.deviceNames && opts.deviceNames.length > 1;
|
|
479
|
+
const selectorNames = multiDevice ? [...new Set([...opts.deviceNames, ...opts.deviceAliases ?? []])] : [];
|
|
480
|
+
let count = 0;
|
|
481
|
+
for (const file of files) {
|
|
482
|
+
let parsed;
|
|
483
|
+
try {
|
|
484
|
+
parsed = parseFrontmatter(readFileSync2(join2(PROMPTS_DIR, file), "utf8"));
|
|
485
|
+
} catch (e) {
|
|
486
|
+
logger.warn(`Skipping prompt ${file}: ${String(e)}`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (!parsed) {
|
|
490
|
+
logger.warn(`Skipping prompt ${file}: missing or invalid frontmatter`);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const argsSchema = {};
|
|
494
|
+
for (const arg of parsed.arguments) {
|
|
495
|
+
const s = z.string().describe(arg.description ?? "");
|
|
496
|
+
argsSchema[arg.name] = arg.required ? s : s.optional();
|
|
497
|
+
}
|
|
498
|
+
const hasOwnDevice = parsed.arguments.some((a) => a.name === "device" || a.name === "device_a");
|
|
499
|
+
if (multiDevice && !hasOwnDevice) {
|
|
500
|
+
argsSchema.device = z.enum(selectorNames).optional().describe(deviceArgDescription(opts.deviceDirectory));
|
|
501
|
+
}
|
|
502
|
+
server.registerPrompt(parsed.name, { title: parsed.title, description: parsed.description, argsSchema }, (args) => ({
|
|
503
|
+
messages: [
|
|
504
|
+
{
|
|
505
|
+
role: "user",
|
|
506
|
+
content: {
|
|
507
|
+
type: "text",
|
|
508
|
+
text: substitute(parsed.body, args)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
]
|
|
512
|
+
}));
|
|
513
|
+
count++;
|
|
514
|
+
}
|
|
515
|
+
return count;
|
|
516
|
+
}
|
|
517
|
+
|
|
373
518
|
// src/observability/topology.ts
|
|
374
519
|
function normMac(mac) {
|
|
375
520
|
if (!mac)
|
|
@@ -1039,10 +1184,17 @@ async function sampleClients(store, device, ts) {
|
|
|
1039
1184
|
}
|
|
1040
1185
|
store.recordClientSamples(device, ts, samples);
|
|
1041
1186
|
}
|
|
1187
|
+
var noUserManager = new Set;
|
|
1042
1188
|
async function ingestSessions(store, device) {
|
|
1189
|
+
if (noUserManager.has(device))
|
|
1190
|
+
return;
|
|
1043
1191
|
const ctx = createContext(undefined, device);
|
|
1044
1192
|
const out = await executeMikrotikCommand("/user-manager session print detail", ctx);
|
|
1045
|
-
if (
|
|
1193
|
+
if (commandUnsupported(out)) {
|
|
1194
|
+
noUserManager.add(device);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (isEmpty(out) || looksLikeError(out))
|
|
1046
1198
|
return;
|
|
1047
1199
|
const sessions = [];
|
|
1048
1200
|
for (const row of parseRecords(out).rows) {
|
|
@@ -1492,9 +1644,35 @@ function restartProcess() {
|
|
|
1492
1644
|
setTimeout(() => process.exit(0), 500);
|
|
1493
1645
|
return ok;
|
|
1494
1646
|
}
|
|
1647
|
+
function runUpgrade(spec) {
|
|
1648
|
+
return new Promise((resolve) => {
|
|
1649
|
+
let out = "";
|
|
1650
|
+
const child = spawn(process.execPath, ["i", "-g", spec], {
|
|
1651
|
+
cwd: process.cwd(),
|
|
1652
|
+
env: process.env,
|
|
1653
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1654
|
+
});
|
|
1655
|
+
const timer3 = setTimeout(() => {
|
|
1656
|
+
child.kill();
|
|
1657
|
+
resolve({ ok: false, log: `${out}
|
|
1658
|
+
(timed out after 180s)` });
|
|
1659
|
+
}, 180000);
|
|
1660
|
+
child.stdout?.on("data", (d) => out += d.toString());
|
|
1661
|
+
child.stderr?.on("data", (d) => out += d.toString());
|
|
1662
|
+
child.on("error", (e) => {
|
|
1663
|
+
clearTimeout(timer3);
|
|
1664
|
+
resolve({ ok: false, log: `${out}
|
|
1665
|
+
spawn error: ${String(e)}` });
|
|
1666
|
+
});
|
|
1667
|
+
child.on("exit", (code) => {
|
|
1668
|
+
clearTimeout(timer3);
|
|
1669
|
+
resolve({ ok: code === 0, log: out.trim() || `(exit ${code})` });
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1495
1673
|
function dashboardHtml() {
|
|
1496
1674
|
try {
|
|
1497
|
-
return
|
|
1675
|
+
return readFileSync3(join3(UI_DIST_DIR, "observability.html"), "utf8");
|
|
1498
1676
|
} catch {
|
|
1499
1677
|
return `<!doctype html><meta charset=utf-8><body style="font:14px system-ui;padding:24px;background:#0b0d10;color:#e8eaed">
|
|
1500
1678
|
<h2>MikroTik MCP \u2014 Observability Dashboard</h2>
|
|
@@ -1726,7 +1904,7 @@ function configSchemaJson() {
|
|
|
1726
1904
|
return {
|
|
1727
1905
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1728
1906
|
title: "MikrotikConfig",
|
|
1729
|
-
...
|
|
1907
|
+
...z2.toJSONSchema(MikrotikConfigSchema, { target: "draft-2020-12" })
|
|
1730
1908
|
};
|
|
1731
1909
|
}
|
|
1732
1910
|
function clampRollback(v) {
|
|
@@ -2280,7 +2458,7 @@ async function featureRoutes(req, url) {
|
|
|
2280
2458
|
const raw = b?.dir?.trim();
|
|
2281
2459
|
if (!raw)
|
|
2282
2460
|
return json3({ error: "dir required" }, 400);
|
|
2283
|
-
const dir = raw === "~" || raw.startsWith("~/") ?
|
|
2461
|
+
const dir = raw === "~" || raw.startsWith("~/") ? join3(homedir(), raw.slice(1)) : raw;
|
|
2284
2462
|
const next = { ...getConfig(), backupDir: dir };
|
|
2285
2463
|
setConfig(next);
|
|
2286
2464
|
try {
|
|
@@ -2388,7 +2566,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
2388
2566
|
});
|
|
2389
2567
|
startHealthChecks(30000);
|
|
2390
2568
|
try {
|
|
2391
|
-
usageStore = await openUsageStore(
|
|
2569
|
+
usageStore = await openUsageStore(join3(dirname4(cfg.dbPath), "usage.db"));
|
|
2392
2570
|
startUsageSampler(usageStore);
|
|
2393
2571
|
} catch (e) {
|
|
2394
2572
|
logger.warn(`[${SERVER_TAG2}] usage history disabled: ${String(e)}`);
|
|
@@ -2405,7 +2583,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
2405
2583
|
source: getConfigSource,
|
|
2406
2584
|
readFile: (pth) => {
|
|
2407
2585
|
try {
|
|
2408
|
-
return
|
|
2586
|
+
return readFileSync3(pth, "utf8");
|
|
2409
2587
|
} catch {
|
|
2410
2588
|
return null;
|
|
2411
2589
|
}
|
|
@@ -2554,6 +2732,63 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
2554
2732
|
return json3({ error: e instanceof Error ? e.message : "fetch failed" }, 502);
|
|
2555
2733
|
}
|
|
2556
2734
|
}
|
|
2735
|
+
if (url.pathname === "/api/catalog" && req.method === "GET") {
|
|
2736
|
+
const modules = moduleCatalog.map((m) => ({
|
|
2737
|
+
label: m.label,
|
|
2738
|
+
slug: m.slug,
|
|
2739
|
+
group: m.group,
|
|
2740
|
+
description: m.description,
|
|
2741
|
+
toolCount: m.tools.length,
|
|
2742
|
+
tools: m.tools.map((t) => ({
|
|
2743
|
+
name: t.name,
|
|
2744
|
+
title: t.title,
|
|
2745
|
+
description: t.description,
|
|
2746
|
+
risk: riskOf(t.annotations)
|
|
2747
|
+
}))
|
|
2748
|
+
}));
|
|
2749
|
+
const prompts = listPrompts();
|
|
2750
|
+
const groups = [...new Set(modules.map((m) => m.group))].sort();
|
|
2751
|
+
return json3({
|
|
2752
|
+
modules,
|
|
2753
|
+
prompts,
|
|
2754
|
+
groups,
|
|
2755
|
+
counts: {
|
|
2756
|
+
modules: modules.length,
|
|
2757
|
+
tools: modules.reduce((n, m) => n + m.toolCount, 0),
|
|
2758
|
+
prompts: prompts.length,
|
|
2759
|
+
groups: groups.length
|
|
2760
|
+
},
|
|
2761
|
+
version: VERSION
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
if (url.pathname === "/api/releases" && req.method === "GET") {
|
|
2765
|
+
try {
|
|
2766
|
+
return json3(await fetchAllReleases());
|
|
2767
|
+
} catch (e) {
|
|
2768
|
+
return json3({ error: e instanceof Error ? e.message : "fetch failed" }, 502);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
if (url.pathname === "/api/upgrade" && req.method === "POST") {
|
|
2772
|
+
const body = await readJson(req);
|
|
2773
|
+
const version = String(body?.version ?? "latest");
|
|
2774
|
+
if (version !== "latest" && !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
2775
|
+
return json3({ ok: false, error: `Invalid version "${version}" (use "latest" or x.y.z).` }, 400);
|
|
2776
|
+
}
|
|
2777
|
+
const spec = `@usex/mikrotik-mcp@${version}`;
|
|
2778
|
+
logger.warn(`Dashboard requested upgrade: bun i -g ${spec}`);
|
|
2779
|
+
const { ok, log } = await runUpgrade(spec);
|
|
2780
|
+
if (!ok)
|
|
2781
|
+
return json3({ ok: false, version, log }, 500);
|
|
2782
|
+
const willRestart = body?.restart !== false;
|
|
2783
|
+
const relaunched = willRestart ? restartProcess() : false;
|
|
2784
|
+
return json3({
|
|
2785
|
+
ok: true,
|
|
2786
|
+
version,
|
|
2787
|
+
log,
|
|
2788
|
+
restarting: relaunched,
|
|
2789
|
+
note: relaunched ? "Installed. Restarting on the new version now \u2014 reconnect shortly." : "Installed. Restart the server (or use the restart button) for it to take effect."
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2557
2792
|
if (url.pathname === "/api/meta") {
|
|
2558
2793
|
const f = facets(db);
|
|
2559
2794
|
return json3({
|
|
@@ -2696,127 +2931,6 @@ function corsHeaders(origin, configured) {
|
|
|
2696
2931
|
// src/server.ts
|
|
2697
2932
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2698
2933
|
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2699
|
-
|
|
2700
|
-
// src/prompts/index.ts
|
|
2701
|
-
import { readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
2702
|
-
import { join as join3 } from "path";
|
|
2703
|
-
import { z as z2 } from "zod";
|
|
2704
|
-
function parseFrontmatter(raw) {
|
|
2705
|
-
const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
2706
|
-
if (!match)
|
|
2707
|
-
return null;
|
|
2708
|
-
const [, fm, body] = match;
|
|
2709
|
-
const meta = {};
|
|
2710
|
-
const args = [];
|
|
2711
|
-
let cur = null;
|
|
2712
|
-
let inArgs = false;
|
|
2713
|
-
for (const line of fm.split(`
|
|
2714
|
-
`)) {
|
|
2715
|
-
if (/^arguments:\s*$/.test(line)) {
|
|
2716
|
-
inArgs = true;
|
|
2717
|
-
continue;
|
|
2718
|
-
}
|
|
2719
|
-
if (inArgs) {
|
|
2720
|
-
const item = line.match(/^[ \t]*-[ \t]*name:[ \t]*(\S.*)$/);
|
|
2721
|
-
if (item) {
|
|
2722
|
-
if (cur)
|
|
2723
|
-
args.push(cur);
|
|
2724
|
-
cur = { name: item[1].trim() };
|
|
2725
|
-
continue;
|
|
2726
|
-
}
|
|
2727
|
-
const prop = line.match(/^[ \t]+(description|required):[ \t]*(\S.*)$/);
|
|
2728
|
-
if (prop && cur) {
|
|
2729
|
-
if (prop[1] === "required")
|
|
2730
|
-
cur.required = /^(true|yes)$/i.test(prop[2].trim());
|
|
2731
|
-
else
|
|
2732
|
-
cur.description = prop[2].trim().replace(/^["']|["']$/g, "");
|
|
2733
|
-
continue;
|
|
2734
|
-
}
|
|
2735
|
-
if (/^\S/.test(line))
|
|
2736
|
-
inArgs = false;
|
|
2737
|
-
}
|
|
2738
|
-
const kv = line.match(/^(\w+):[ \t]*(\S.*)?$/);
|
|
2739
|
-
if (kv && !inArgs)
|
|
2740
|
-
meta[kv[1]] = (kv[2] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
2741
|
-
}
|
|
2742
|
-
if (cur)
|
|
2743
|
-
args.push(cur);
|
|
2744
|
-
if (!meta.name)
|
|
2745
|
-
return null;
|
|
2746
|
-
return {
|
|
2747
|
-
name: meta.name,
|
|
2748
|
-
title: meta.title ?? meta.name,
|
|
2749
|
-
description: meta.description ?? "",
|
|
2750
|
-
arguments: args,
|
|
2751
|
-
body: body.trim()
|
|
2752
|
-
};
|
|
2753
|
-
}
|
|
2754
|
-
function substitute(body, vars) {
|
|
2755
|
-
return body.replace(/\{\{\s*(\w+)\s*\}\}/g, (whole, key) => {
|
|
2756
|
-
const v = vars[key];
|
|
2757
|
-
if (v === undefined || v === null || v === "")
|
|
2758
|
-
return whole;
|
|
2759
|
-
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
2760
|
-
});
|
|
2761
|
-
}
|
|
2762
|
-
function deviceArgDescription(directory) {
|
|
2763
|
-
if (directory && directory.length > 0) {
|
|
2764
|
-
const rows = directory.map((d) => `\u2022 ${d.key}${d.label && d.label !== d.key ? ` ("${d.label}")` : ""} \u2192 ${d.target}${d.isDefault ? " [default]" : ""}`).join(`
|
|
2765
|
-
`);
|
|
2766
|
-
return "Which configured MikroTik device to run this workflow on. " + `Pass the EXACT config key or its label. Configured devices:
|
|
2767
|
-
` + `${rows}
|
|
2768
|
-
` + "Omit to use the default device.";
|
|
2769
|
-
}
|
|
2770
|
-
return "Which configured MikroTik device to run this workflow on. Omit to use the default device.";
|
|
2771
|
-
}
|
|
2772
|
-
function registerPrompts(server, opts = {}) {
|
|
2773
|
-
let files;
|
|
2774
|
-
try {
|
|
2775
|
-
files = readdirSync2(PROMPTS_DIR).filter((f) => f.endsWith(".md"));
|
|
2776
|
-
} catch {
|
|
2777
|
-
return 0;
|
|
2778
|
-
}
|
|
2779
|
-
const multiDevice = opts.deviceNames && opts.deviceNames.length > 1;
|
|
2780
|
-
const selectorNames = multiDevice ? [...new Set([...opts.deviceNames, ...opts.deviceAliases ?? []])] : [];
|
|
2781
|
-
let count = 0;
|
|
2782
|
-
for (const file of files) {
|
|
2783
|
-
let parsed;
|
|
2784
|
-
try {
|
|
2785
|
-
parsed = parseFrontmatter(readFileSync3(join3(PROMPTS_DIR, file), "utf8"));
|
|
2786
|
-
} catch (e) {
|
|
2787
|
-
logger.warn(`Skipping prompt ${file}: ${String(e)}`);
|
|
2788
|
-
continue;
|
|
2789
|
-
}
|
|
2790
|
-
if (!parsed) {
|
|
2791
|
-
logger.warn(`Skipping prompt ${file}: missing or invalid frontmatter`);
|
|
2792
|
-
continue;
|
|
2793
|
-
}
|
|
2794
|
-
const argsSchema = {};
|
|
2795
|
-
for (const arg of parsed.arguments) {
|
|
2796
|
-
const s = z2.string().describe(arg.description ?? "");
|
|
2797
|
-
argsSchema[arg.name] = arg.required ? s : s.optional();
|
|
2798
|
-
}
|
|
2799
|
-
const hasOwnDevice = parsed.arguments.some((a) => a.name === "device" || a.name === "device_a");
|
|
2800
|
-
if (multiDevice && !hasOwnDevice) {
|
|
2801
|
-
argsSchema.device = z2.enum(selectorNames).optional().describe(deviceArgDescription(opts.deviceDirectory));
|
|
2802
|
-
}
|
|
2803
|
-
server.registerPrompt(parsed.name, { title: parsed.title, description: parsed.description, argsSchema }, (args) => ({
|
|
2804
|
-
messages: [
|
|
2805
|
-
{
|
|
2806
|
-
role: "user",
|
|
2807
|
-
content: {
|
|
2808
|
-
type: "text",
|
|
2809
|
-
text: substitute(parsed.body, args)
|
|
2810
|
-
}
|
|
2811
|
-
}
|
|
2812
|
-
]
|
|
2813
|
-
}));
|
|
2814
|
-
count++;
|
|
2815
|
-
}
|
|
2816
|
-
return count;
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
// src/server.ts
|
|
2820
2934
|
var INSTRUCTIONS = `MikroTik RouterOS management over SSH.
|
|
2821
2935
|
|
|
2822
2936
|
This server exposes RouterOS configuration as MCP tools grouped by subsystem:
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
selectToolModules,
|
|
30
30
|
setConfig,
|
|
31
31
|
updateSummaryLine
|
|
32
|
-
} from "./shared/library-
|
|
32
|
+
} from "./shared/library-212xbkwr.js";
|
|
33
33
|
// src/server.ts
|
|
34
34
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
35
35
|
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -8995,7 +8995,7 @@ var cache = null;
|
|
|
8995
8995
|
async function gateway() {
|
|
8996
8996
|
if (cache)
|
|
8997
8997
|
return cache;
|
|
8998
|
-
const { moduleCatalog } = await import("./cli-
|
|
8998
|
+
const { moduleCatalog } = await import("./cli-9s02dq5d.js");
|
|
8999
8999
|
const forIndex = [];
|
|
9000
9000
|
const byName = new Map;
|
|
9001
9001
|
for (const mod of moduleCatalog) {
|
|
@@ -27043,12 +27043,12 @@ function saveFileCache(data) {
|
|
|
27043
27043
|
}
|
|
27044
27044
|
async function fetchLatestRelease() {
|
|
27045
27045
|
if (memoryCache && Date.now() - memoryCache.fetchedAt < MEMORY_CACHE_TTL) {
|
|
27046
|
-
return memoryCache.data;
|
|
27046
|
+
return withCurrentRelation(memoryCache.data);
|
|
27047
27047
|
}
|
|
27048
27048
|
const fileCached = loadFileCache();
|
|
27049
27049
|
if (fileCached) {
|
|
27050
27050
|
memoryCache = { data: fileCached, fetchedAt: Date.now() };
|
|
27051
|
-
return fileCached;
|
|
27051
|
+
return withCurrentRelation(fileCached);
|
|
27052
27052
|
}
|
|
27053
27053
|
const res = await fetch(GITHUB_API, {
|
|
27054
27054
|
headers: {
|
|
@@ -27073,7 +27073,11 @@ async function fetchLatestRelease() {
|
|
|
27073
27073
|
};
|
|
27074
27074
|
memoryCache = { data, fetchedAt: Date.now() };
|
|
27075
27075
|
saveFileCache(data);
|
|
27076
|
-
return data;
|
|
27076
|
+
return withCurrentRelation(data);
|
|
27077
|
+
}
|
|
27078
|
+
function withCurrentRelation(r) {
|
|
27079
|
+
const cmp = compareVersions(r.version, VERSION);
|
|
27080
|
+
return { ...r, currentVersion: VERSION, isNewer: cmp > 0, isAhead: cmp < 0 };
|
|
27077
27081
|
}
|
|
27078
27082
|
async function checkForUpdate() {
|
|
27079
27083
|
try {
|
|
@@ -27082,7 +27086,7 @@ async function checkForUpdate() {
|
|
|
27082
27086
|
} catch (e) {
|
|
27083
27087
|
const stale = loadFileCache(Infinity);
|
|
27084
27088
|
if (stale) {
|
|
27085
|
-
return { release: stale, checkedAt: Date.now(), fromCache: true };
|
|
27089
|
+
return { release: withCurrentRelation(stale), checkedAt: Date.now(), fromCache: true };
|
|
27086
27090
|
}
|
|
27087
27091
|
return {
|
|
27088
27092
|
release: null,
|
|
@@ -27113,6 +27117,45 @@ function updateSummaryLine(release) {
|
|
|
27113
27117
|
return null;
|
|
27114
27118
|
return `Server update available: MikroTik MCP v${release.version} ` + `(you are running v${VERSION}). ` + `Call check_server_pulse for release notes and upgrade commands, ` + `or upgrade directly: bun i -g @usex/mikrotik-mcp@latest`;
|
|
27115
27119
|
}
|
|
27120
|
+
var RELEASES_API = "https://api.github.com/repos/mikrotik-mcp/mikrotik-mcp/releases?per_page=100";
|
|
27121
|
+
var releasesCache = null;
|
|
27122
|
+
async function fetchAllReleases() {
|
|
27123
|
+
if (releasesCache && Date.now() - releasesCache.fetchedAt < MEMORY_CACHE_TTL) {
|
|
27124
|
+
return releasesCache.data;
|
|
27125
|
+
}
|
|
27126
|
+
const res = await fetch(RELEASES_API, {
|
|
27127
|
+
headers: {
|
|
27128
|
+
accept: "application/vnd.github+json",
|
|
27129
|
+
"user-agent": `mikrotik-mcp/${VERSION}`
|
|
27130
|
+
}
|
|
27131
|
+
});
|
|
27132
|
+
if (!res.ok)
|
|
27133
|
+
throw new Error(`GitHub API ${res.status}`);
|
|
27134
|
+
const raw = await res.json();
|
|
27135
|
+
const releases = raw.filter((r) => !r.draft).map((r) => {
|
|
27136
|
+
const version = r.tag_name.replace(/^v/, "");
|
|
27137
|
+
const cmp = compareVersions(version, VERSION);
|
|
27138
|
+
return {
|
|
27139
|
+
version,
|
|
27140
|
+
name: r.name || `v${version}`,
|
|
27141
|
+
body: r.body || "",
|
|
27142
|
+
publishedAt: r.published_at,
|
|
27143
|
+
url: r.html_url,
|
|
27144
|
+
prerelease: r.prerelease,
|
|
27145
|
+
relation: cmp === 0 ? "current" : cmp > 0 ? "newer" : "older"
|
|
27146
|
+
};
|
|
27147
|
+
}).sort((a, b) => compareVersions(b.version, a.version));
|
|
27148
|
+
const latest = releases.find((r) => !r.prerelease) ?? releases[0] ?? null;
|
|
27149
|
+
const data = {
|
|
27150
|
+
currentVersion: VERSION,
|
|
27151
|
+
latestVersion: latest ? latest.version : null,
|
|
27152
|
+
updateAvailable: latest ? compareVersions(latest.version, VERSION) > 0 : false,
|
|
27153
|
+
releases,
|
|
27154
|
+
fetchedAt: Date.now()
|
|
27155
|
+
};
|
|
27156
|
+
releasesCache = { data, fetchedAt: Date.now() };
|
|
27157
|
+
return data;
|
|
27158
|
+
}
|
|
27116
27159
|
|
|
27117
27160
|
// src/tools/server-pulse.ts
|
|
27118
27161
|
var UPGRADE_COMMANDS = {
|
|
@@ -32231,4 +32274,4 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
|
|
|
32231
32274
|
}).map((m) => m.tools);
|
|
32232
32275
|
}
|
|
32233
32276
|
|
|
32234
|
-
export { __require, DEFAULT_SNAPSHOT_DB, DEFAULT_CONFIG_HISTORY_DIR, DeviceConfigSchema, ToolFilterSchema, MikrotikConfigSchema, getConfigSource, loadConfig, logger, setConfig, getConfig, listDevices, deviceLabels, resolveDeviceName, deviceDirectory, isMacTelnetDevice, createDeviceClient, describeTransport, isPoolEnabled, closeAll, poolStatus, getMemoryStore, closeMemoryStore, reopenMemoryStore, executeMikrotikCommand, createContext, Cmd, isEmpty, looksLikeError, commandUnsupported, parseKeyValues, parseRouterosDate, parseSize, parseSystemResource, parseRecords, parseLeadingNumber, REDACTED, redact, configureRecorder, getEventStore, subscribe, subscriberCount, registerTools, fetchDevices, sampleDeviceTraffic, sampleAllTraffic, setDeviceLimits, blockDevice, allowDevice, makeDeviceStatic, setDeviceIp, setDeviceLabel, removeDeviceLease, devicesView, PROMPTS_DIR, UI_DIST_DIR, registerUiResources, backupDir, listBackups, readBackup, writeBackup, deleteBackup, renameBackup, createLocalBackup, restoreLocalBackup, isS3Configured, getS3Client, presignExpiresIn, s3Target, splitCommands, buildChangePlan, renderPlan, diffLines, normalizeExport, analyzeDrift, attributeChanges, openSnapshotStore, DEFAULT_TZSP_PORT, capture2 as capture, AAA_ENTITIES, listAaaEntity, addAaaEntity, updateAaaEntity, removeAaaEntity, toggleAaaEntity, getRadiusIncoming, setRadiusIncoming, resetRadiusCounters, getUmSettings, setUmSettings, VERSION, WEBSITE_URL, LOGO_URL, SERVER_TITLE, SERVER_DESCRIPTION, SERVER_NAME, PKG_META, loadFileCacheSync, fetchLatestRelease, checkForUpdate, updateSummaryLine, moduleCatalog, allToolModules, ALWAYS_ON_MODULES, selectToolModules };
|
|
32277
|
+
export { __require, DEFAULT_SNAPSHOT_DB, DEFAULT_CONFIG_HISTORY_DIR, DeviceConfigSchema, ToolFilterSchema, MikrotikConfigSchema, getConfigSource, loadConfig, logger, setConfig, getConfig, listDevices, deviceLabels, resolveDeviceName, deviceDirectory, isMacTelnetDevice, createDeviceClient, describeTransport, isPoolEnabled, closeAll, poolStatus, getMemoryStore, closeMemoryStore, reopenMemoryStore, executeMikrotikCommand, createContext, Cmd, isEmpty, looksLikeError, commandUnsupported, parseKeyValues, parseRouterosDate, parseSize, parseSystemResource, parseRecords, parseLeadingNumber, riskOf, REDACTED, redact, configureRecorder, getEventStore, subscribe, subscriberCount, registerTools, fetchDevices, sampleDeviceTraffic, sampleAllTraffic, setDeviceLimits, blockDevice, allowDevice, makeDeviceStatic, setDeviceIp, setDeviceLabel, removeDeviceLease, devicesView, PROMPTS_DIR, UI_DIST_DIR, registerUiResources, backupDir, listBackups, readBackup, writeBackup, deleteBackup, renameBackup, createLocalBackup, restoreLocalBackup, isS3Configured, getS3Client, presignExpiresIn, s3Target, splitCommands, buildChangePlan, renderPlan, diffLines, normalizeExport, analyzeDrift, attributeChanges, openSnapshotStore, DEFAULT_TZSP_PORT, capture2 as capture, AAA_ENTITIES, listAaaEntity, addAaaEntity, updateAaaEntity, removeAaaEntity, toggleAaaEntity, getRadiusIncoming, setRadiusIncoming, resetRadiusCounters, getUmSettings, setUmSettings, VERSION, WEBSITE_URL, LOGO_URL, SERVER_TITLE, SERVER_DESCRIPTION, SERVER_NAME, PKG_META, loadFileCacheSync, fetchLatestRelease, checkForUpdate, updateSummaryLine, fetchAllReleases, moduleCatalog, allToolModules, ALWAYS_ON_MODULES, selectToolModules };
|
|
@@ -8804,7 +8804,7 @@ var cache = null;
|
|
|
8804
8804
|
async function gateway() {
|
|
8805
8805
|
if (cache)
|
|
8806
8806
|
return cache;
|
|
8807
|
-
const { moduleCatalog } = await import("./library-
|
|
8807
|
+
const { moduleCatalog } = await import("./library-c0fxxq85.js");
|
|
8808
8808
|
const forIndex = [];
|
|
8809
8809
|
const byName = new Map;
|
|
8810
8810
|
for (const mod of moduleCatalog) {
|
|
@@ -26851,12 +26851,12 @@ function saveFileCache(data) {
|
|
|
26851
26851
|
}
|
|
26852
26852
|
async function fetchLatestRelease() {
|
|
26853
26853
|
if (memoryCache && Date.now() - memoryCache.fetchedAt < MEMORY_CACHE_TTL) {
|
|
26854
|
-
return memoryCache.data;
|
|
26854
|
+
return withCurrentRelation(memoryCache.data);
|
|
26855
26855
|
}
|
|
26856
26856
|
const fileCached = loadFileCache();
|
|
26857
26857
|
if (fileCached) {
|
|
26858
26858
|
memoryCache = { data: fileCached, fetchedAt: Date.now() };
|
|
26859
|
-
return fileCached;
|
|
26859
|
+
return withCurrentRelation(fileCached);
|
|
26860
26860
|
}
|
|
26861
26861
|
const res = await fetch(GITHUB_API, {
|
|
26862
26862
|
headers: {
|
|
@@ -26881,7 +26881,11 @@ async function fetchLatestRelease() {
|
|
|
26881
26881
|
};
|
|
26882
26882
|
memoryCache = { data, fetchedAt: Date.now() };
|
|
26883
26883
|
saveFileCache(data);
|
|
26884
|
-
return data;
|
|
26884
|
+
return withCurrentRelation(data);
|
|
26885
|
+
}
|
|
26886
|
+
function withCurrentRelation(r) {
|
|
26887
|
+
const cmp = compareVersions(r.version, VERSION);
|
|
26888
|
+
return { ...r, currentVersion: VERSION, isNewer: cmp > 0, isAhead: cmp < 0 };
|
|
26885
26889
|
}
|
|
26886
26890
|
async function checkForUpdate() {
|
|
26887
26891
|
try {
|
|
@@ -26890,7 +26894,7 @@ async function checkForUpdate() {
|
|
|
26890
26894
|
} catch (e) {
|
|
26891
26895
|
const stale = loadFileCache(Infinity);
|
|
26892
26896
|
if (stale) {
|
|
26893
|
-
return { release: stale, checkedAt: Date.now(), fromCache: true };
|
|
26897
|
+
return { release: withCurrentRelation(stale), checkedAt: Date.now(), fromCache: true };
|
|
26894
26898
|
}
|
|
26895
26899
|
return {
|
|
26896
26900
|
release: null,
|