@usex/mikrotik-mcp 4.13.0 → 4.15.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 +237 -130
- package/dist/index.js +1 -1
- package/dist/shared/{cli-p6yc41tg.js → cli-9s02dq5d.js} +1 -1
- package/dist/shared/{cli-07emrpkr.js → cli-w6ecrhxa.js} +1161 -1041
- package/dist/shared/{library-pczgrhdt.js → library-212xbkwr.js} +1037 -956
- package/dist/shared/{library-txj981yv.js → library-c0fxxq85.js} +1 -1
- package/dist/ui/observability.html +60 -60
- package/package.json +1 -1
- package/prompts/harden-against-bruteforce.md +140 -0
- package/schemas/tool-catalog.json +7 -9
- package/schemas/tools/create_filter_rule.json +4 -6
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)
|
|
@@ -1492,9 +1637,35 @@ function restartProcess() {
|
|
|
1492
1637
|
setTimeout(() => process.exit(0), 500);
|
|
1493
1638
|
return ok;
|
|
1494
1639
|
}
|
|
1640
|
+
function runUpgrade(spec) {
|
|
1641
|
+
return new Promise((resolve) => {
|
|
1642
|
+
let out = "";
|
|
1643
|
+
const child = spawn(process.execPath, ["i", "-g", spec], {
|
|
1644
|
+
cwd: process.cwd(),
|
|
1645
|
+
env: process.env,
|
|
1646
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1647
|
+
});
|
|
1648
|
+
const timer3 = setTimeout(() => {
|
|
1649
|
+
child.kill();
|
|
1650
|
+
resolve({ ok: false, log: `${out}
|
|
1651
|
+
(timed out after 180s)` });
|
|
1652
|
+
}, 180000);
|
|
1653
|
+
child.stdout?.on("data", (d) => out += d.toString());
|
|
1654
|
+
child.stderr?.on("data", (d) => out += d.toString());
|
|
1655
|
+
child.on("error", (e) => {
|
|
1656
|
+
clearTimeout(timer3);
|
|
1657
|
+
resolve({ ok: false, log: `${out}
|
|
1658
|
+
spawn error: ${String(e)}` });
|
|
1659
|
+
});
|
|
1660
|
+
child.on("exit", (code) => {
|
|
1661
|
+
clearTimeout(timer3);
|
|
1662
|
+
resolve({ ok: code === 0, log: out.trim() || `(exit ${code})` });
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1495
1666
|
function dashboardHtml() {
|
|
1496
1667
|
try {
|
|
1497
|
-
return
|
|
1668
|
+
return readFileSync3(join3(UI_DIST_DIR, "observability.html"), "utf8");
|
|
1498
1669
|
} catch {
|
|
1499
1670
|
return `<!doctype html><meta charset=utf-8><body style="font:14px system-ui;padding:24px;background:#0b0d10;color:#e8eaed">
|
|
1500
1671
|
<h2>MikroTik MCP \u2014 Observability Dashboard</h2>
|
|
@@ -1726,7 +1897,7 @@ function configSchemaJson() {
|
|
|
1726
1897
|
return {
|
|
1727
1898
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1728
1899
|
title: "MikrotikConfig",
|
|
1729
|
-
...
|
|
1900
|
+
...z2.toJSONSchema(MikrotikConfigSchema, { target: "draft-2020-12" })
|
|
1730
1901
|
};
|
|
1731
1902
|
}
|
|
1732
1903
|
function clampRollback(v) {
|
|
@@ -2280,7 +2451,7 @@ async function featureRoutes(req, url) {
|
|
|
2280
2451
|
const raw = b?.dir?.trim();
|
|
2281
2452
|
if (!raw)
|
|
2282
2453
|
return json3({ error: "dir required" }, 400);
|
|
2283
|
-
const dir = raw === "~" || raw.startsWith("~/") ?
|
|
2454
|
+
const dir = raw === "~" || raw.startsWith("~/") ? join3(homedir(), raw.slice(1)) : raw;
|
|
2284
2455
|
const next = { ...getConfig(), backupDir: dir };
|
|
2285
2456
|
setConfig(next);
|
|
2286
2457
|
try {
|
|
@@ -2388,7 +2559,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
2388
2559
|
});
|
|
2389
2560
|
startHealthChecks(30000);
|
|
2390
2561
|
try {
|
|
2391
|
-
usageStore = await openUsageStore(
|
|
2562
|
+
usageStore = await openUsageStore(join3(dirname4(cfg.dbPath), "usage.db"));
|
|
2392
2563
|
startUsageSampler(usageStore);
|
|
2393
2564
|
} catch (e) {
|
|
2394
2565
|
logger.warn(`[${SERVER_TAG2}] usage history disabled: ${String(e)}`);
|
|
@@ -2405,7 +2576,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
2405
2576
|
source: getConfigSource,
|
|
2406
2577
|
readFile: (pth) => {
|
|
2407
2578
|
try {
|
|
2408
|
-
return
|
|
2579
|
+
return readFileSync3(pth, "utf8");
|
|
2409
2580
|
} catch {
|
|
2410
2581
|
return null;
|
|
2411
2582
|
}
|
|
@@ -2554,6 +2725,63 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
2554
2725
|
return json3({ error: e instanceof Error ? e.message : "fetch failed" }, 502);
|
|
2555
2726
|
}
|
|
2556
2727
|
}
|
|
2728
|
+
if (url.pathname === "/api/catalog" && req.method === "GET") {
|
|
2729
|
+
const modules = moduleCatalog.map((m) => ({
|
|
2730
|
+
label: m.label,
|
|
2731
|
+
slug: m.slug,
|
|
2732
|
+
group: m.group,
|
|
2733
|
+
description: m.description,
|
|
2734
|
+
toolCount: m.tools.length,
|
|
2735
|
+
tools: m.tools.map((t) => ({
|
|
2736
|
+
name: t.name,
|
|
2737
|
+
title: t.title,
|
|
2738
|
+
description: t.description,
|
|
2739
|
+
risk: riskOf(t.annotations)
|
|
2740
|
+
}))
|
|
2741
|
+
}));
|
|
2742
|
+
const prompts = listPrompts();
|
|
2743
|
+
const groups = [...new Set(modules.map((m) => m.group))].sort();
|
|
2744
|
+
return json3({
|
|
2745
|
+
modules,
|
|
2746
|
+
prompts,
|
|
2747
|
+
groups,
|
|
2748
|
+
counts: {
|
|
2749
|
+
modules: modules.length,
|
|
2750
|
+
tools: modules.reduce((n, m) => n + m.toolCount, 0),
|
|
2751
|
+
prompts: prompts.length,
|
|
2752
|
+
groups: groups.length
|
|
2753
|
+
},
|
|
2754
|
+
version: VERSION
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
if (url.pathname === "/api/releases" && req.method === "GET") {
|
|
2758
|
+
try {
|
|
2759
|
+
return json3(await fetchAllReleases());
|
|
2760
|
+
} catch (e) {
|
|
2761
|
+
return json3({ error: e instanceof Error ? e.message : "fetch failed" }, 502);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
if (url.pathname === "/api/upgrade" && req.method === "POST") {
|
|
2765
|
+
const body = await readJson(req);
|
|
2766
|
+
const version = String(body?.version ?? "latest");
|
|
2767
|
+
if (version !== "latest" && !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
2768
|
+
return json3({ ok: false, error: `Invalid version "${version}" (use "latest" or x.y.z).` }, 400);
|
|
2769
|
+
}
|
|
2770
|
+
const spec = `@usex/mikrotik-mcp@${version}`;
|
|
2771
|
+
logger.warn(`Dashboard requested upgrade: bun i -g ${spec}`);
|
|
2772
|
+
const { ok, log } = await runUpgrade(spec);
|
|
2773
|
+
if (!ok)
|
|
2774
|
+
return json3({ ok: false, version, log }, 500);
|
|
2775
|
+
const willRestart = body?.restart !== false;
|
|
2776
|
+
const relaunched = willRestart ? restartProcess() : false;
|
|
2777
|
+
return json3({
|
|
2778
|
+
ok: true,
|
|
2779
|
+
version,
|
|
2780
|
+
log,
|
|
2781
|
+
restarting: relaunched,
|
|
2782
|
+
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."
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2557
2785
|
if (url.pathname === "/api/meta") {
|
|
2558
2786
|
const f = facets(db);
|
|
2559
2787
|
return json3({
|
|
@@ -2696,127 +2924,6 @@ function corsHeaders(origin, configured) {
|
|
|
2696
2924
|
// src/server.ts
|
|
2697
2925
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2698
2926
|
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
2927
|
var INSTRUCTIONS = `MikroTik RouterOS management over SSH.
|
|
2821
2928
|
|
|
2822
2929
|
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";
|