@usex/mikrotik-mcp 3.56.0 → 3.58.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 CHANGED
@@ -8,12 +8,18 @@ import {
8
8
  DEFAULT_SNAPSHOT_DB,
9
9
  DEFAULT_TZSP_PORT,
10
10
  DeviceConfigSchema,
11
+ LOGO_URL,
11
12
  MikrotikConfigSchema,
12
- PROJECT_ROOT,
13
+ PKG_META,
13
14
  PROMPTS_DIR,
14
15
  REDACTED,
16
+ SERVER_DESCRIPTION,
17
+ SERVER_NAME,
18
+ SERVER_TITLE,
15
19
  ToolFilterSchema,
16
20
  UI_DIST_DIR,
21
+ VERSION,
22
+ WEBSITE_URL,
17
23
  __require,
18
24
  addAaaEntity,
19
25
  allToolModules,
@@ -24,6 +30,7 @@ import {
24
30
  blockDevice,
25
31
  buildChangePlan,
26
32
  capture,
33
+ checkForUpdate,
27
34
  closeAll,
28
35
  closeMemoryStore,
29
36
  commandUnsupported,
@@ -39,6 +46,7 @@ import {
39
46
  diffLines,
40
47
  executeMikrotikCommand,
41
48
  fetchDevices,
49
+ fetchLatestRelease,
42
50
  getConfig,
43
51
  getConfigSource,
44
52
  getEventStore,
@@ -54,6 +62,7 @@ import {
54
62
  listBackups,
55
63
  listDevices,
56
64
  loadConfig,
65
+ loadFileCacheSync,
57
66
  logger,
58
67
  looksLikeError,
59
68
  makeDeviceStatic,
@@ -95,16 +104,17 @@ import {
95
104
  subscriberCount,
96
105
  toggleAaaEntity,
97
106
  updateAaaEntity,
107
+ updateSummaryLine,
98
108
  writeBackup
99
- } from "./shared/cli-thhk9f7n.js";
109
+ } from "./shared/cli-r5p22dag.js";
100
110
 
101
111
  // src/cli.ts
102
112
  import { existsSync as existsSync2 } from "fs";
103
113
 
104
114
  // src/observability/dashboard.ts
105
- import { readFileSync as readFileSync3 } from "fs";
115
+ import { readFileSync as readFileSync2 } from "fs";
106
116
  import { homedir, networkInterfaces } from "os";
107
- import { dirname as dirname4, join as join3 } from "path";
117
+ import { dirname as dirname4, join as join2 } from "path";
108
118
  var {serve } = globalThis.Bun;
109
119
  import { z } from "zod";
110
120
 
@@ -1148,18 +1158,6 @@ function computeStats(events, opts) {
1148
1158
  };
1149
1159
  }
1150
1160
 
1151
- // src/version.ts
1152
- import { readFileSync as readFileSync2 } from "fs";
1153
- import { join as join2 } from "path";
1154
- var pkg = JSON.parse(readFileSync2(join2(PROJECT_ROOT, "package.json"), "utf-8"));
1155
- var VERSION = pkg.version ?? "0.0.0";
1156
- var WEBSITE_URL = pkg.homepage ?? "";
1157
- var LOGO_URL = pkg.logoIcon ?? "";
1158
- var SERVER_TITLE = pkg.name ?? "mcp-mikrotik";
1159
- var SERVER_DESCRIPTION = pkg.description ?? "";
1160
- var SERVER_NAME = "mcp-mikrotik";
1161
- var PKG_META = pkg;
1162
-
1163
1161
  // src/observability/drift-routes.ts
1164
1162
  var storePromise = null;
1165
1163
  function getStore() {
@@ -1425,51 +1423,13 @@ async function memoryRoutes(req, url) {
1425
1423
 
1426
1424
  // src/observability/dashboard.ts
1427
1425
  var SERVER_TAG2 = "mikrotik-mcp";
1428
- var releaseCache = null;
1429
- var RELEASE_CACHE_TTL = 15 * 60 * 1000;
1430
- function compareVersions(a, b) {
1431
- const pa = a.replace(/^v/, "").split(".").map(Number);
1432
- const pb = b.replace(/^v/, "").split(".").map(Number);
1433
- for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
1434
- const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
1435
- if (diff !== 0)
1436
- return diff;
1437
- }
1438
- return 0;
1439
- }
1440
- async function fetchLatestRelease() {
1441
- if (releaseCache && Date.now() - releaseCache.fetchedAt < RELEASE_CACHE_TTL) {
1442
- return releaseCache.data;
1443
- }
1444
- const res = await fetch("https://api.github.com/repos/ali-master/mikrotik-mcp/releases/latest", {
1445
- headers: {
1446
- accept: "application/vnd.github+json",
1447
- "user-agent": "mikrotik-mcp-dashboard"
1448
- }
1449
- });
1450
- if (!res.ok)
1451
- throw new Error(`GitHub API ${res.status}`);
1452
- const gh = await res.json();
1453
- const latestVersion = gh.tag_name.replace(/^v/, "");
1454
- const data = {
1455
- version: latestVersion,
1456
- name: gh.name || `v${latestVersion}`,
1457
- body: gh.body || "",
1458
- publishedAt: gh.published_at,
1459
- url: gh.html_url,
1460
- isNewer: compareVersions(latestVersion, VERSION) > 0,
1461
- currentVersion: VERSION
1462
- };
1463
- releaseCache = { data, fetchedAt: Date.now() };
1464
- return data;
1465
- }
1466
1426
  var JSON_HEADERS3 = { "content-type": "application/json; charset=utf-8" };
1467
1427
  function json3(body, status = 200) {
1468
1428
  return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS3 });
1469
1429
  }
1470
1430
  function dashboardHtml() {
1471
1431
  try {
1472
- return readFileSync3(join3(UI_DIST_DIR, "observability.html"), "utf8");
1432
+ return readFileSync2(join2(UI_DIST_DIR, "observability.html"), "utf8");
1473
1433
  } catch {
1474
1434
  return `<!doctype html><meta charset=utf-8><body style="font:14px system-ui;padding:24px;background:#0b0d10;color:#e8eaed">
1475
1435
  <h2>MikroTik MCP \u2014 Observability Dashboard</h2>
@@ -2156,7 +2116,7 @@ async function featureRoutes(req, url) {
2156
2116
  const raw = b?.dir?.trim();
2157
2117
  if (!raw)
2158
2118
  return json3({ error: "dir required" }, 400);
2159
- const dir = raw === "~" || raw.startsWith("~/") ? join3(homedir(), raw.slice(1)) : raw;
2119
+ const dir = raw === "~" || raw.startsWith("~/") ? join2(homedir(), raw.slice(1)) : raw;
2160
2120
  const next = { ...getConfig(), backupDir: dir };
2161
2121
  setConfig(next);
2162
2122
  try {
@@ -2264,7 +2224,7 @@ async function runDashboard(cfg, transportLabel) {
2264
2224
  });
2265
2225
  startHealthChecks(30000);
2266
2226
  try {
2267
- usageStore = await openUsageStore(join3(dirname4(cfg.dbPath), "usage.db"));
2227
+ usageStore = await openUsageStore(join2(dirname4(cfg.dbPath), "usage.db"));
2268
2228
  startUsageSampler(usageStore);
2269
2229
  } catch (e) {
2270
2230
  logger.warn(`[${SERVER_TAG2}] usage history disabled: ${String(e)}`);
@@ -2281,7 +2241,7 @@ async function runDashboard(cfg, transportLabel) {
2281
2241
  source: getConfigSource,
2282
2242
  readFile: (pth) => {
2283
2243
  try {
2284
- return readFileSync3(pth, "utf8");
2244
+ return readFileSync2(pth, "utf8");
2285
2245
  } catch {
2286
2246
  return null;
2287
2247
  }
@@ -2564,8 +2524,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2564
2524
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2565
2525
 
2566
2526
  // src/prompts/index.ts
2567
- import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
2568
- import { join as join4 } from "path";
2527
+ import { readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
2528
+ import { join as join3 } from "path";
2569
2529
  import { z as z2 } from "zod";
2570
2530
  function parseFrontmatter(raw) {
2571
2531
  const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
@@ -2625,18 +2585,30 @@ function substitute(body, vars) {
2625
2585
  return typeof v === "object" ? JSON.stringify(v) : String(v);
2626
2586
  });
2627
2587
  }
2628
- function registerPrompts(server) {
2588
+ function deviceArgDescription(directory) {
2589
+ if (directory && directory.length > 0) {
2590
+ const rows = directory.map((d) => `\u2022 ${d.key}${d.label && d.label !== d.key ? ` ("${d.label}")` : ""} \u2192 ${d.target}${d.isDefault ? " [default]" : ""}`).join(`
2591
+ `);
2592
+ return "Which configured MikroTik device to run this workflow on. " + `Pass the EXACT config key or its label. Configured devices:
2593
+ ` + `${rows}
2594
+ ` + "Omit to use the default device.";
2595
+ }
2596
+ return "Which configured MikroTik device to run this workflow on. Omit to use the default device.";
2597
+ }
2598
+ function registerPrompts(server, opts = {}) {
2629
2599
  let files;
2630
2600
  try {
2631
2601
  files = readdirSync2(PROMPTS_DIR).filter((f) => f.endsWith(".md"));
2632
2602
  } catch {
2633
2603
  return 0;
2634
2604
  }
2605
+ const multiDevice = opts.deviceNames && opts.deviceNames.length > 1;
2606
+ const selectorNames = multiDevice ? [...new Set([...opts.deviceNames, ...opts.deviceAliases ?? []])] : [];
2635
2607
  let count = 0;
2636
2608
  for (const file of files) {
2637
2609
  let parsed;
2638
2610
  try {
2639
- parsed = parseFrontmatter(readFileSync4(join4(PROMPTS_DIR, file), "utf8"));
2611
+ parsed = parseFrontmatter(readFileSync3(join3(PROMPTS_DIR, file), "utf8"));
2640
2612
  } catch (e) {
2641
2613
  logger.warn(`Skipping prompt ${file}: ${String(e)}`);
2642
2614
  continue;
@@ -2650,6 +2622,10 @@ function registerPrompts(server) {
2650
2622
  const s = z2.string().describe(arg.description ?? "");
2651
2623
  argsSchema[arg.name] = arg.required ? s : s.optional();
2652
2624
  }
2625
+ const hasOwnDevice = parsed.arguments.some((a) => a.name === "device" || a.name === "device_a");
2626
+ if (multiDevice && !hasOwnDevice) {
2627
+ argsSchema.device = z2.enum(selectorNames).optional().describe(deviceArgDescription(opts.deviceDirectory));
2628
+ }
2653
2629
  server.registerPrompt(parsed.name, { title: parsed.title, description: parsed.description, argsSchema }, (args) => ({
2654
2630
  messages: [
2655
2631
  {
@@ -2718,7 +2694,17 @@ function createServer(opts = {}) {
2718
2694
  process.title = `Mikrotik MCP Server v${VERSION}`;
2719
2695
  const { names, default: defaultDevice } = listDevices();
2720
2696
  const readOnly = getConfig().readOnly;
2721
- const instructions = names.length > 1 ? INSTRUCTIONS + MULTI_DEVICE_INSTRUCTIONS.replace("{{names}}", names.join(", ")).replace("{{default}}", defaultDevice) : INSTRUCTIONS;
2697
+ let instructions = names.length > 1 ? INSTRUCTIONS + MULTI_DEVICE_INSTRUCTIONS.replace("{{names}}", names.join(", ")).replace("{{default}}", defaultDevice) : INSTRUCTIONS;
2698
+ if (!getConfig().disableUpdateCheck) {
2699
+ const cached = loadFileCacheSync();
2700
+ if (cached) {
2701
+ const note = updateSummaryLine(cached);
2702
+ if (note)
2703
+ instructions += `
2704
+
2705
+ ${note}`;
2706
+ }
2707
+ }
2722
2708
  const server = new McpServer({
2723
2709
  name: SERVER_NAME,
2724
2710
  version: VERSION,
@@ -2728,8 +2714,8 @@ function createServer(opts = {}) {
2728
2714
  icons: [
2729
2715
  {
2730
2716
  src: LOGO_URL,
2731
- mimeType: "image/png",
2732
- sizes: ["192x192"],
2717
+ mimeType: "image/svg+xml",
2718
+ sizes: ["any"],
2733
2719
  theme: "light"
2734
2720
  }
2735
2721
  ]
@@ -2751,7 +2737,11 @@ function createServer(opts = {}) {
2751
2737
  appViews: getConfig().mcp.appViews,
2752
2738
  readOnly
2753
2739
  });
2754
- const promptCount = registerPrompts(server);
2740
+ const promptCount = registerPrompts(server, {
2741
+ deviceNames: names,
2742
+ deviceAliases: deviceLabels(),
2743
+ deviceDirectory: deviceDirectory()
2744
+ });
2755
2745
  const uiViewCount = registerUiResources(server);
2756
2746
  installToolPagination(server, getConfig().mcp.toolPageSize);
2757
2747
  return { server, toolCount, promptCount, uiViewCount, readOnly };
@@ -2866,6 +2856,13 @@ async function runHttp(mcp) {
2866
2856
  }
2867
2857
  });
2868
2858
  logger.info(`MCP server ready on http://${mcp.host}:${mcp.port}${mcpPath} \u2014 ${toolCount} tools, ${promptCount} prompts, ${uiViewCount} app views ` + `(${mcp.transport})${readOnly ? " [READ-ONLY]" : ""}`);
2859
+ if (!getConfig().disableUpdateCheck) {
2860
+ checkForUpdate().then((r) => {
2861
+ if (r.release?.isNewer) {
2862
+ logger.info(`[update] MikroTik MCP v${r.release.version} available (running v${VERSION}). ` + `Upgrade: bun i -g @usex/mikrotik-mcp@latest`);
2863
+ }
2864
+ }).catch(() => {});
2865
+ }
2869
2866
  }
2870
2867
 
2871
2868
  // src/transport/stdio.ts
@@ -2875,6 +2872,21 @@ async function runStdio() {
2875
2872
  const transport = new StdioServerTransport;
2876
2873
  await server.connect(transport);
2877
2874
  logger.info(`MCP server ready on stdio \u2014 ${toolCount} tools, ${promptCount} prompts, ${uiViewCount} app views${readOnly ? " [READ-ONLY]" : ""}`);
2875
+ if (!getConfig().disableUpdateCheck) {
2876
+ whisperUpdate(server).catch(() => {});
2877
+ }
2878
+ }
2879
+ async function whisperUpdate(server) {
2880
+ const result = await checkForUpdate();
2881
+ if (!result.release?.isNewer)
2882
+ return;
2883
+ const r = result.release;
2884
+ try {
2885
+ server.server.sendLoggingMessage({
2886
+ level: "info",
2887
+ data: `[MikroTik MCP] Update available: v${r.version} (running v${VERSION}). ` + `${r.name ? `"${r.name}" \u2014 ` : ""}` + `Call check_server_pulse for release notes, or upgrade: bun i -g @usex/mikrotik-mcp@latest`
2888
+ }).catch(() => {});
2889
+ } catch {}
2878
2890
  }
2879
2891
 
2880
2892
  // src/cli-logo.ts
package/dist/index.d.ts CHANGED
@@ -324,5 +324,5 @@ declare class SafeModeManager {
324
324
  declare function getSafeModeManager(deviceName: string): SafeModeManager;
325
325
  declare const allToolModules: ToolModule[];
326
326
  declare const VERSION: string;
327
- declare const SERVER_NAME = "mcp-mikrotik";
327
+ declare const SERVER_NAME = "mikrotik-mcp";
328
328
  export { setConfig, resolveDeviceName, registerTools, loadConfig, listDevices, getSafeModeManager, getDevice, getConfig, executeMikrotikCommand, defineTool, createServer, allToolModules, VERSION, ToolModule, ToolContext, SafeModeManager, SERVER_NAME, RegisterableTool, MikrotikConfigSchema, MikrotikConfig, MikroTikSSHClient, DeviceConfigSchema, DeviceConfig };
package/dist/index.js CHANGED
@@ -1,11 +1,16 @@
1
1
  // @bun
2
2
  import {
3
3
  DeviceConfigSchema,
4
+ LOGO_URL,
4
5
  MikroTikSSHClient,
5
6
  MikrotikConfigSchema,
6
- PROJECT_ROOT,
7
7
  PROMPTS_DIR,
8
+ SERVER_DESCRIPTION,
9
+ SERVER_NAME,
10
+ SERVER_TITLE,
8
11
  SafeModeManager,
12
+ VERSION,
13
+ WEBSITE_URL,
9
14
  allToolModules,
10
15
  defineTool,
11
16
  deviceDirectory,
@@ -16,13 +21,15 @@ import {
16
21
  getSafeModeManager,
17
22
  listDevices,
18
23
  loadConfig,
24
+ loadFileCacheSync,
19
25
  logger,
20
26
  registerTools,
21
27
  registerUiResources,
22
28
  resolveDeviceName,
23
29
  selectToolModules,
24
- setConfig
25
- } from "./shared/library-ymtf9zyk.js";
30
+ setConfig,
31
+ updateSummaryLine
32
+ } from "./shared/library-84jwmz7a.js";
26
33
  // src/server.ts
27
34
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
28
35
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -89,13 +96,25 @@ function substitute(body, vars) {
89
96
  return typeof v === "object" ? JSON.stringify(v) : String(v);
90
97
  });
91
98
  }
92
- function registerPrompts(server) {
99
+ function deviceArgDescription(directory) {
100
+ if (directory && directory.length > 0) {
101
+ const rows = directory.map((d) => `\u2022 ${d.key}${d.label && d.label !== d.key ? ` ("${d.label}")` : ""} \u2192 ${d.target}${d.isDefault ? " [default]" : ""}`).join(`
102
+ `);
103
+ return "Which configured MikroTik device to run this workflow on. " + `Pass the EXACT config key or its label. Configured devices:
104
+ ` + `${rows}
105
+ ` + "Omit to use the default device.";
106
+ }
107
+ return "Which configured MikroTik device to run this workflow on. Omit to use the default device.";
108
+ }
109
+ function registerPrompts(server, opts = {}) {
93
110
  let files;
94
111
  try {
95
112
  files = readdirSync(PROMPTS_DIR).filter((f) => f.endsWith(".md"));
96
113
  } catch {
97
114
  return 0;
98
115
  }
116
+ const multiDevice = opts.deviceNames && opts.deviceNames.length > 1;
117
+ const selectorNames = multiDevice ? [...new Set([...opts.deviceNames, ...opts.deviceAliases ?? []])] : [];
99
118
  let count = 0;
100
119
  for (const file of files) {
101
120
  let parsed;
@@ -114,6 +133,10 @@ function registerPrompts(server) {
114
133
  const s = z.string().describe(arg.description ?? "");
115
134
  argsSchema[arg.name] = arg.required ? s : s.optional();
116
135
  }
136
+ const hasOwnDevice = parsed.arguments.some((a) => a.name === "device" || a.name === "device_a");
137
+ if (multiDevice && !hasOwnDevice) {
138
+ argsSchema.device = z.enum(selectorNames).optional().describe(deviceArgDescription(opts.deviceDirectory));
139
+ }
117
140
  server.registerPrompt(parsed.name, { title: parsed.title, description: parsed.description, argsSchema }, (args) => ({
118
141
  messages: [
119
142
  {
@@ -130,17 +153,6 @@ function registerPrompts(server) {
130
153
  return count;
131
154
  }
132
155
 
133
- // src/version.ts
134
- import { readFileSync as readFileSync2 } from "fs";
135
- import { join as join2 } from "path";
136
- var pkg = JSON.parse(readFileSync2(join2(PROJECT_ROOT, "package.json"), "utf-8"));
137
- var VERSION = pkg.version ?? "0.0.0";
138
- var WEBSITE_URL = pkg.homepage ?? "";
139
- var LOGO_URL = pkg.logoIcon ?? "";
140
- var SERVER_TITLE = pkg.name ?? "mcp-mikrotik";
141
- var SERVER_DESCRIPTION = pkg.description ?? "";
142
- var SERVER_NAME = "mcp-mikrotik";
143
-
144
156
  // src/server.ts
145
157
  var INSTRUCTIONS = `MikroTik RouterOS management over SSH.
146
158
 
@@ -193,7 +205,17 @@ function createServer(opts = {}) {
193
205
  process.title = `Mikrotik MCP Server v${VERSION}`;
194
206
  const { names, default: defaultDevice } = listDevices();
195
207
  const readOnly = getConfig().readOnly;
196
- const instructions = names.length > 1 ? INSTRUCTIONS + MULTI_DEVICE_INSTRUCTIONS.replace("{{names}}", names.join(", ")).replace("{{default}}", defaultDevice) : INSTRUCTIONS;
208
+ let instructions = names.length > 1 ? INSTRUCTIONS + MULTI_DEVICE_INSTRUCTIONS.replace("{{names}}", names.join(", ")).replace("{{default}}", defaultDevice) : INSTRUCTIONS;
209
+ if (!getConfig().disableUpdateCheck) {
210
+ const cached = loadFileCacheSync();
211
+ if (cached) {
212
+ const note = updateSummaryLine(cached);
213
+ if (note)
214
+ instructions += `
215
+
216
+ ${note}`;
217
+ }
218
+ }
197
219
  const server = new McpServer({
198
220
  name: SERVER_NAME,
199
221
  version: VERSION,
@@ -203,8 +225,8 @@ function createServer(opts = {}) {
203
225
  icons: [
204
226
  {
205
227
  src: LOGO_URL,
206
- mimeType: "image/png",
207
- sizes: ["192x192"],
228
+ mimeType: "image/svg+xml",
229
+ sizes: ["any"],
208
230
  theme: "light"
209
231
  }
210
232
  ]
@@ -226,7 +248,11 @@ function createServer(opts = {}) {
226
248
  appViews: getConfig().mcp.appViews,
227
249
  readOnly
228
250
  });
229
- const promptCount = registerPrompts(server);
251
+ const promptCount = registerPrompts(server, {
252
+ deviceNames: names,
253
+ deviceAliases: deviceLabels(),
254
+ deviceDirectory: deviceDirectory()
255
+ });
230
256
  const uiViewCount = registerUiResources(server);
231
257
  installToolPagination(server, getConfig().mcp.toolPageSize);
232
258
  return { server, toolCount, promptCount, uiViewCount, readOnly };