@usex/mikrotik-mcp 3.51.1 → 3.53.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.
@@ -5738,7 +5738,7 @@ function renderComplianceReport(report, device, options) {
5738
5738
  `).trimEnd();
5739
5739
  }
5740
5740
 
5741
- // src/tools/compliance-audit.ts
5741
+ // src/utils/safe-exec.ts
5742
5742
  async function safe(cmd, ctx) {
5743
5743
  try {
5744
5744
  const out = await executeMikrotikCommand(cmd, ctx);
@@ -5747,6 +5747,8 @@ async function safe(cmd, ctx) {
5747
5747
  return "";
5748
5748
  }
5749
5749
  }
5750
+
5751
+ // src/tools/compliance-audit.ts
5750
5752
  async function fetchComplianceState(ctx) {
5751
5753
  const [
5752
5754
  sshRaw,
@@ -7104,6 +7106,31 @@ ${created.join(`
7104
7106
  // src/tools/container.ts
7105
7107
  import { z as z17 } from "zod";
7106
7108
 
7109
+ // src/utils/ip.ts
7110
+ function isIpAddress(s) {
7111
+ return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(s);
7112
+ }
7113
+ function isIpLike(s) {
7114
+ return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d+)?$/.test(s);
7115
+ }
7116
+ function isPrivateIp(ip) {
7117
+ const p = ip.split(".").map(Number);
7118
+ if (p.length !== 4)
7119
+ return false;
7120
+ if (p[0] === 10)
7121
+ return true;
7122
+ if (p[0] === 172 && p[1] >= 16 && p[1] <= 31)
7123
+ return true;
7124
+ if (p[0] === 192 && p[1] === 168)
7125
+ return true;
7126
+ if (p[0] === 169 && p[1] === 254)
7127
+ return true;
7128
+ return false;
7129
+ }
7130
+ // src/utils/num.ts
7131
+ function num(s) {
7132
+ return Number.parseInt(s ?? "0", 10) || 0;
7133
+ }
7107
7134
  // src/utils/or-match.ts
7108
7135
  function orMatch(field, values, op = "~") {
7109
7136
  if (values.length === 0)
@@ -7875,7 +7902,7 @@ var cache = null;
7875
7902
  async function gateway() {
7876
7903
  if (cache)
7877
7904
  return cache;
7878
- const { moduleCatalog } = await import("./library-510csx1a.js");
7905
+ const { moduleCatalog } = await import("./library-4npjv6jq.js");
7879
7906
  const forIndex = [];
7880
7907
  const byName = new Map;
7881
7908
  for (const mod of moduleCatalog) {
@@ -8845,7 +8872,7 @@ function rulesFromRows(rows) {
8845
8872
  else
8846
8873
  match[k] = v;
8847
8874
  }
8848
- const num = (s) => {
8875
+ const num2 = (s) => {
8849
8876
  if (s == null)
8850
8877
  return;
8851
8878
  const n = Number(s.replace(/\s/g, ""));
@@ -8858,8 +8885,8 @@ function rulesFromRows(rows) {
8858
8885
  disabled: flags.includes("X"),
8859
8886
  dynamic: flags.includes("D"),
8860
8887
  comment: r.comment,
8861
- packets: num(r.packets),
8862
- bytes: num(r.bytes),
8888
+ packets: num2(r.packets),
8889
+ bytes: num2(r.bytes),
8863
8890
  match,
8864
8891
  transform,
8865
8892
  raw: r
@@ -8928,6 +8955,8 @@ function matchSummary(rule) {
8928
8955
  "dst-port",
8929
8956
  "in-interface",
8930
8957
  "out-interface",
8958
+ "in-interface-list",
8959
+ "out-interface-list",
8931
8960
  "connection-state",
8932
8961
  "src-address-list",
8933
8962
  "dst-address-list"
@@ -9676,11 +9705,12 @@ async function updateFilterRule(a, ctx) {
9676
9705
  }
9677
9706
  if (updates.length === 0)
9678
9707
  return "No updates specified.";
9679
- const cmd = `/ip firewall filter set ${a.rule_id} ${updates.join(" ")}`;
9708
+ const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
9709
+ const cmd = `/ip firewall filter set ${id} ${updates.join(" ")}`;
9680
9710
  const result = await executeMikrotikCommand(cmd, ctx);
9681
9711
  if (looksLikeError(result))
9682
9712
  return `Failed to update firewall filter rule: ${result}`;
9683
- const details = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${a.rule_id}`, ctx);
9713
+ const details = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${id}`, ctx);
9684
9714
  return `Firewall filter rule updated successfully:
9685
9715
 
9686
9716
  ${details}`;
@@ -9835,7 +9865,8 @@ ${result}`;
9835
9865
  },
9836
9866
  async handler(a, ctx) {
9837
9867
  ctx.info(`Getting firewall filter rule details: rule_id=${a.rule_id}`);
9838
- const result = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${a.rule_id}`, ctx);
9868
+ const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
9869
+ const result = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${id}`, ctx);
9839
9870
  return isEmpty(result) ? `Firewall filter rule with ID '${a.rule_id}' not found.` : `FIREWALL FILTER RULE DETAILS:
9840
9871
 
9841
9872
  ${result}`;
@@ -9911,10 +9942,11 @@ ${result}`;
9911
9942
  inputSchema: { rule_id: z28.string() },
9912
9943
  async handler(a, ctx) {
9913
9944
  ctx.info(`Removing firewall filter rule: rule_id=${a.rule_id}`);
9914
- const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${a.rule_id}`, ctx);
9945
+ const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
9946
+ const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
9915
9947
  if (count.trim() === "0")
9916
9948
  return `Firewall filter rule with ID '${a.rule_id}' not found.`;
9917
- const result = await executeMikrotikCommand(`/ip firewall filter remove ${a.rule_id}`, ctx);
9949
+ const result = await executeMikrotikCommand(`/ip firewall filter remove ${id}`, ctx);
9918
9950
  if (looksLikeError(result))
9919
9951
  return `Failed to remove firewall filter rule: ${result}`;
9920
9952
  return `Firewall filter rule with ID '${a.rule_id}' removed successfully.`;
@@ -9931,10 +9963,11 @@ ${result}`;
9931
9963
  },
9932
9964
  async handler(a, ctx) {
9933
9965
  ctx.info(`Moving firewall filter rule: rule_id=${a.rule_id} to position ${a.destination}`);
9934
- const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${a.rule_id}`, ctx);
9966
+ const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
9967
+ const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
9935
9968
  if (count.trim() === "0")
9936
9969
  return `Firewall filter rule with ID '${a.rule_id}' not found.`;
9937
- const result = await executeMikrotikCommand(`/ip firewall filter move ${a.rule_id} destination=${a.destination}`, ctx);
9970
+ const result = await executeMikrotikCommand(`/ip firewall filter move ${id} destination=${a.destination}`, ctx);
9938
9971
  if (looksLikeError(result))
9939
9972
  return `Failed to move firewall filter rule: ${result}`;
9940
9973
  return `Firewall filter rule with ID '${a.rule_id}' moved to position ${a.destination}.`;
@@ -10831,7 +10864,7 @@ function assessUpgradeReadiness(state) {
10831
10864
  });
10832
10865
  }
10833
10866
  const cpuNum = Number.parseInt(state.cpuLoad, 10);
10834
- if (!isNaN(cpuNum) && cpuNum > 80) {
10867
+ if (!Number.isNaN(cpuNum) && cpuNum > 80) {
10835
10868
  checks.push({
10836
10869
  label: "CPU load",
10837
10870
  status: "caution",
@@ -10844,8 +10877,8 @@ function assessUpgradeReadiness(state) {
10844
10877
  detail: `${state.cpuLoad}`
10845
10878
  });
10846
10879
  }
10847
- const free = parseMem(state.freeMemory);
10848
- const total = parseMem(state.totalMemory);
10880
+ const free = parseSize(state.freeMemory) ?? 0;
10881
+ const total = parseSize(state.totalMemory) ?? 0;
10849
10882
  if (free > 0 && total > 0) {
10850
10883
  const pct2 = Math.round(free / total * 100);
10851
10884
  if (pct2 < 20) {
@@ -10882,20 +10915,6 @@ function assessUpgradeReadiness(state) {
10882
10915
  }
10883
10916
  return checks;
10884
10917
  }
10885
- function parseMem(s) {
10886
- const m = s.match(/([\d.]+)\s*(MiB|GiB|KiB)?/i);
10887
- if (!m)
10888
- return 0;
10889
- const val = Number.parseFloat(m[1]);
10890
- const unit = (m[2] ?? "").toLowerCase();
10891
- if (unit === "gib")
10892
- return val * 1024 * 1024 * 1024;
10893
- if (unit === "mib")
10894
- return val * 1024 * 1024;
10895
- if (unit === "kib")
10896
- return val * 1024;
10897
- return val;
10898
- }
10899
10918
  function renderFirmwareStatus(state, device) {
10900
10919
  const lines = [];
10901
10920
  lines.push(`FIRMWARE STATUS \u2014 ${device}`);
@@ -11033,14 +11052,6 @@ function renderReadiness(checks) {
11033
11052
  }
11034
11053
 
11035
11054
  // src/tools/firmware-lifecycle.ts
11036
- async function safe2(cmd, ctx) {
11037
- try {
11038
- const out = await executeMikrotikCommand(cmd, ctx);
11039
- return looksLikeError(out) ? "" : out;
11040
- } catch {
11041
- return "";
11042
- }
11043
- }
11044
11055
  function parsePackages(raw) {
11045
11056
  if (!raw)
11046
11057
  return [];
@@ -11085,10 +11096,10 @@ function parseUpdateInfo(raw) {
11085
11096
  }
11086
11097
  async function fetchFirmwareState(ctx) {
11087
11098
  const [resourceRaw, packagesRaw, routerboardRaw, updateRaw] = await Promise.all([
11088
- safe2("/system resource print", ctx),
11089
- safe2("/system package print detail", ctx),
11090
- safe2("/system routerboard print", ctx),
11091
- safe2("/system package update print", ctx)
11099
+ safe("/system resource print", ctx),
11100
+ safe("/system package print detail", ctx),
11101
+ safe("/system routerboard print", ctx),
11102
+ safe("/system package update print", ctx)
11092
11103
  ]);
11093
11104
  const res = parseKeyValues(resourceRaw);
11094
11105
  return {
@@ -11106,12 +11117,12 @@ async function fetchFirmwareState(ctx) {
11106
11117
  }
11107
11118
  async function captureHealthSnapshot(ctx) {
11108
11119
  const [resourceRaw, healthRaw, routesRaw, interfacesRaw, pppRaw, dhcpRaw] = await Promise.all([
11109
- safe2("/system resource print", ctx),
11110
- safe2("/system health print", ctx),
11111
- safe2("/ip route print count-only", ctx),
11112
- safe2("/interface print detail", ctx),
11113
- safe2("/ppp active print count-only", ctx),
11114
- safe2("/ip dhcp-server lease print count-only where status=bound", ctx)
11120
+ safe("/system resource print", ctx),
11121
+ safe("/system health print", ctx),
11122
+ safe("/ip route print count-only", ctx),
11123
+ safe("/interface print detail", ctx),
11124
+ safe("/ppp active print count-only", ctx),
11125
+ safe("/ip dhcp-server lease print count-only where status=bound", ctx)
11115
11126
  ]);
11116
11127
  const res = parseKeyValues(resourceRaw);
11117
11128
  const ifRows = interfacesRaw ? parseRecords(interfacesRaw).rows : [];
@@ -11211,8 +11222,8 @@ var firmwareLifecycleTools = [
11211
11222
  if (looksLikeError(chResult))
11212
11223
  return `Failed to set channel: ${chResult}`;
11213
11224
  }
11214
- const checkResult = await executeMikrotikCommand("/system package update check-for-updates once", ctx);
11215
- const updateRaw = await safe2("/system package update print", ctx);
11225
+ await executeMikrotikCommand("/system package update check-for-updates once", ctx);
11226
+ const updateRaw = await safe("/system package update print", ctx);
11216
11227
  const updateInfo = parseUpdateInfo(updateRaw);
11217
11228
  if (!updateInfo?.updateAvailable) {
11218
11229
  return `No update available on '${device}'.
@@ -11226,7 +11237,7 @@ var firmwareLifecycleTools = [
11226
11237
  if (looksLikeError(dlResult)) {
11227
11238
  return `Failed to download update packages: ${dlResult}`;
11228
11239
  }
11229
- const postRaw = await safe2("/system package update print", ctx);
11240
+ const postRaw = await safe("/system package update print", ctx);
11230
11241
  const postStatus = parseKeyValues(postRaw);
11231
11242
  const lines = [];
11232
11243
  lines.push(`FIRMWARE STAGED \u2014 ${device}`);
@@ -11259,7 +11270,7 @@ var firmwareLifecycleTools = [
11259
11270
  return "Upgrade not confirmed. Pass confirm=true to proceed. The device WILL reboot.";
11260
11271
  const device = resolveDeviceName(ctx.device);
11261
11272
  ctx.info(`Firmware upgrade on '${device}'`);
11262
- const updateRaw = await safe2("/system package update print", ctx);
11273
+ const updateRaw = await safe("/system package update print", ctx);
11263
11274
  const updateInfo = parseUpdateInfo(updateRaw);
11264
11275
  if (!updateInfo?.updateAvailable) {
11265
11276
  return `No update available on '${device}'. ` + `Run \`firmware_check\` or \`firmware_stage\` first.
@@ -11368,7 +11379,7 @@ var firmwareLifecycleTools = [
11368
11379
  lines.push(` Current: ${state.routerboard.currentFirmware} \u2192 Available: ${state.routerboard.upgradeFirmware}`);
11369
11380
  lines.push(" Use `firmware_upgrade` with `upgrade_routerboard=true` to include it.");
11370
11381
  }
11371
- const schedRaw = await safe2('/system scheduler print detail where name="mcp-firmware-upgrade"', ctx);
11382
+ const schedRaw = await safe('/system scheduler print detail where name="mcp-firmware-upgrade"', ctx);
11372
11383
  if (schedRaw && !isEmpty(schedRaw)) {
11373
11384
  lines.push("");
11374
11385
  lines.push("PENDING SCHEDULED UPGRADE:");
@@ -20326,7 +20337,7 @@ function analyzeRootCause(data) {
20326
20337
  summary: `${boundLeases.length} active DHCP leases`
20327
20338
  });
20328
20339
  }
20329
- if (isIpAddress(data.target)) {
20340
+ if (isIpAddress(data.target) && isPrivateIp(data.target)) {
20330
20341
  const lease = data.dhcpLeases.find((l) => l.address === data.target);
20331
20342
  const arp = data.arpEntries.find((e) => e.address === data.target);
20332
20343
  if (!lease && !arp) {
@@ -20581,7 +20592,7 @@ function correlateRootCauses(data, evidence, causes) {
20581
20592
  dimensions: ["resources"]
20582
20593
  });
20583
20594
  }
20584
- if (isIpAddress(data.target)) {
20595
+ if (isIpAddress(data.target) && isPrivateIp(data.target)) {
20585
20596
  const arp = data.arpEntries.find((e) => e.address === data.target);
20586
20597
  if (arp && !arp.complete && data.ping?.lossPct === 100) {
20587
20598
  causes.push({
@@ -20636,9 +20647,6 @@ function correlateRootCauses(data, evidence, causes) {
20636
20647
  });
20637
20648
  }
20638
20649
  }
20639
- function isIpAddress(s) {
20640
- return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(s);
20641
- }
20642
20650
  function formatRule(r) {
20643
20651
  const parts = [`chain=${r.chain}`, `action=${r.action}`];
20644
20652
  if (r.srcAddress)
@@ -20798,19 +20806,8 @@ ${footer}`;
20798
20806
  }
20799
20807
 
20800
20808
  // src/tools/root-cause.ts
20801
- async function safe3(cmd, ctx) {
20802
- try {
20803
- const out = await executeMikrotikCommand(cmd, ctx);
20804
- return looksLikeError(out) ? "" : out;
20805
- } catch {
20806
- return "";
20807
- }
20808
- }
20809
- function num(s) {
20810
- return Number.parseInt(s ?? "0", 10) || 0;
20811
- }
20812
20809
  async function collectInterfaces(ctx) {
20813
- const raw = await safe3("/interface print detail", ctx);
20810
+ const raw = await safe("/interface print detail", ctx);
20814
20811
  if (!raw)
20815
20812
  return [];
20816
20813
  return parseRecords(raw).rows.map((r) => ({
@@ -20828,7 +20825,7 @@ async function collectInterfaces(ctx) {
20828
20825
  }));
20829
20826
  }
20830
20827
  async function collectRoutes(ctx) {
20831
- const raw = await safe3("/ip route print detail", ctx);
20828
+ const raw = await safe("/ip route print detail", ctx);
20832
20829
  if (!raw)
20833
20830
  return { routes: [], count: 0, hasDefault: false };
20834
20831
  const rows = parseRecords(raw).rows;
@@ -20843,7 +20840,7 @@ async function collectRoutes(ctx) {
20843
20840
  return { routes, count: routes.length, hasDefault };
20844
20841
  }
20845
20842
  async function collectOspfNeighbors(ctx) {
20846
- const raw = await safe3("/routing ospf neighbor print detail", ctx);
20843
+ const raw = await safe("/routing ospf neighbor print detail", ctx);
20847
20844
  if (!raw)
20848
20845
  return [];
20849
20846
  return parseRecords(raw).rows.map((r) => ({
@@ -20855,9 +20852,9 @@ async function collectOspfNeighbors(ctx) {
20855
20852
  }));
20856
20853
  }
20857
20854
  async function collectBgpPeers(ctx) {
20858
- const raw = await safe3("/routing bgp session print detail", ctx);
20855
+ const raw = await safe("/routing bgp session print detail", ctx);
20859
20856
  if (!raw) {
20860
- const raw2 = await safe3("/routing bgp peer print detail", ctx);
20857
+ const raw2 = await safe("/routing bgp peer print detail", ctx);
20861
20858
  if (!raw2)
20862
20859
  return [];
20863
20860
  return parseRecords(raw2).rows.map((r) => ({
@@ -20877,12 +20874,12 @@ async function collectBgpPeers(ctx) {
20877
20874
  }));
20878
20875
  }
20879
20876
  async function collectFirewallRules(target, ctx) {
20880
- const raw = await safe3("/ip firewall filter print detail", ctx);
20877
+ const raw = await safe("/ip firewall filter print detail", ctx);
20881
20878
  if (!raw)
20882
20879
  return { matching: [], totalCount: 0 };
20883
20880
  const rows = parseRecords(raw).rows;
20884
20881
  const all = rows.map(parseFirewallRow);
20885
- const isIp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(target);
20882
+ const isIp = isIpAddress(target);
20886
20883
  const matching = isIp ? all.filter((r) => !r.srcAddress || r.srcAddress === target || !r.dstAddress || r.dstAddress === target) : all;
20887
20884
  return { matching, totalCount: all.length };
20888
20885
  }
@@ -20902,13 +20899,13 @@ function parseFirewallRow(r, i) {
20902
20899
  };
20903
20900
  }
20904
20901
  async function collectNatRules(ctx) {
20905
- const raw = await safe3("/ip firewall nat print detail", ctx);
20902
+ const raw = await safe("/ip firewall nat print detail", ctx);
20906
20903
  if (!raw)
20907
20904
  return [];
20908
20905
  return parseRecords(raw).rows.map(parseFirewallRow);
20909
20906
  }
20910
20907
  async function collectArp(ctx) {
20911
- const raw = await safe3("/ip arp print detail", ctx);
20908
+ const raw = await safe("/ip arp print detail", ctx);
20912
20909
  if (!raw)
20913
20910
  return [];
20914
20911
  return parseRecords(raw).rows.map((r) => ({
@@ -20920,7 +20917,7 @@ async function collectArp(ctx) {
20920
20917
  }));
20921
20918
  }
20922
20919
  async function collectDhcpLeases(ctx) {
20923
- const raw = await safe3("/ip dhcp-server lease print detail", ctx);
20920
+ const raw = await safe("/ip dhcp-server lease print detail", ctx);
20924
20921
  if (!raw)
20925
20922
  return [];
20926
20923
  return parseRecords(raw).rows.map((r) => ({
@@ -20933,7 +20930,7 @@ async function collectDhcpLeases(ctx) {
20933
20930
  }));
20934
20931
  }
20935
20932
  async function collectLogs(target, ctx) {
20936
- const raw = await safe3('/log print where topics~"error" or topics~"warning" or topics~"critical" or topics~"firewall" or topics~"system"', ctx);
20933
+ const raw = await safe('/log print where topics~"error" or topics~"warning" or topics~"critical" or topics~"firewall" or topics~"system"', ctx);
20937
20934
  if (!raw)
20938
20935
  return [];
20939
20936
  const rows = parseRecords(raw).rows;
@@ -20944,7 +20941,7 @@ async function collectLogs(target, ctx) {
20944
20941
  }));
20945
20942
  }
20946
20943
  async function collectTunnels(ctx) {
20947
- const raw = await safe3('/interface print detail where type~"gre|ipip|eoip|vxlan|wireguard|ovpn|sstp|pptp|l2tp"', ctx);
20944
+ const raw = await safe('/interface print detail where type~"gre|ipip|eoip|vxlan|wireguard|ovpn|sstp|pptp|l2tp"', ctx);
20948
20945
  if (!raw)
20949
20946
  return [];
20950
20947
  return parseRecords(raw).rows.map((r) => ({
@@ -20979,27 +20976,27 @@ async function collectDiagnosticData(target, ctx, dimensions) {
20979
20976
  logs,
20980
20977
  tunnels
20981
20978
  ] = await Promise.all([
20982
- dims.has("connectivity") ? safe3(`/ping ${quoteValue(target)} count=5`, ctx) : Promise.resolve(""),
20979
+ dims.has("connectivity") ? safe(`/ping ${quoteValue(target)} count=5`, ctx) : Promise.resolve(""),
20983
20980
  dims.has("interfaces") ? collectInterfaces(ctx) : Promise.resolve([]),
20984
20981
  dims.has("routing") ? collectRoutes(ctx) : Promise.resolve({ routes: [], count: 0, hasDefault: true }),
20985
20982
  dims.has("routing") ? collectOspfNeighbors(ctx) : Promise.resolve([]),
20986
20983
  dims.has("routing") ? collectBgpPeers(ctx) : Promise.resolve([]),
20987
20984
  dims.has("firewall") ? collectFirewallRules(target, ctx) : Promise.resolve({ matching: [], totalCount: 0 }),
20988
20985
  dims.has("nat") ? collectNatRules(ctx) : Promise.resolve([]),
20989
- dims.has("nat") ? safe3("/ip firewall connection print count-only", ctx) : Promise.resolve("0"),
20986
+ dims.has("nat") ? safe("/ip firewall connection print count-only", ctx) : Promise.resolve("0"),
20990
20987
  dims.has("arp_dhcp") ? collectArp(ctx) : Promise.resolve([]),
20991
20988
  dims.has("arp_dhcp") ? collectDhcpLeases(ctx) : Promise.resolve([]),
20992
- dims.has("dns") && !isIpLike(target) ? safe3(`[:resolve ${quoteValue(target)}]`, ctx) : Promise.resolve(undefined),
20993
- dims.has("dns") ? safe3("/ip dns print", ctx) : Promise.resolve(""),
20994
- dims.has("resources") ? safe3("/system resource print", ctx) : Promise.resolve(""),
20989
+ dims.has("dns") && !isIpLike(target) ? safe(`[:resolve ${quoteValue(target)}]`, ctx) : Promise.resolve(undefined),
20990
+ dims.has("dns") ? safe("/ip dns print", ctx) : Promise.resolve(""),
20991
+ dims.has("resources") ? safe("/system resource print", ctx) : Promise.resolve(""),
20995
20992
  dims.has("logs") ? collectLogs(target, ctx) : Promise.resolve([]),
20996
20993
  dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([])
20997
20994
  ]);
20998
20995
  const ping = pingResult ? parsePingSummary(pingResult) ?? undefined : undefined;
20999
20996
  const dnsKv = parseKeyValues(dnsSettingsRaw);
21000
20997
  const resKv = parseKeyValues(resourceRaw);
21001
- const totalMem = parseMemMiB(resKv["total-memory"]);
21002
- const freeMem = parseMemMiB(resKv["free-memory"]);
20998
+ const totalMem = parseSize(resKv["total-memory"]) ?? 0;
20999
+ const freeMem = parseSize(resKv["free-memory"]) ?? 0;
21003
21000
  const memUsedPct = totalMem > 0 ? Math.round((totalMem - freeMem) / totalMem * 100) : 0;
21004
21001
  return {
21005
21002
  target,
@@ -21018,9 +21015,9 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21018
21015
  arpEntries,
21019
21016
  dhcpLeases,
21020
21017
  dnsResolveResult: dnsResult ?? undefined,
21021
- dnsServers: dnsKv.servers ?? "",
21018
+ dnsServers: [dnsKv.servers, dnsKv["dynamic-servers"]].filter(Boolean).join(",") || "",
21022
21019
  dnsAllowRemote: (dnsKv["allow-remote-requests"] ?? "").toLowerCase() === "yes",
21023
- cpuLoad: num(resKv["cpu-load"]),
21020
+ cpuLoad: parsePercent(resKv["cpu-load"]) ?? 0,
21024
21021
  memoryUsedPct: memUsedPct,
21025
21022
  uptime: resKv.uptime ?? "",
21026
21023
  rosVersion: resKv.version ?? "",
@@ -21028,23 +21025,6 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21028
21025
  tunnelInterfaces: tunnels
21029
21026
  };
21030
21027
  }
21031
- function isIpLike(s) {
21032
- return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d+)?$/.test(s);
21033
- }
21034
- function parseMemMiB(s) {
21035
- if (!s)
21036
- return 0;
21037
- const m = s.match(/([\d.]+)\s*(MiB|GiB|KiB)?/i);
21038
- if (!m)
21039
- return 0;
21040
- const v = Number.parseFloat(m[1]);
21041
- const u = (m[2] ?? "").toLowerCase();
21042
- if (u === "gib")
21043
- return v * 1024;
21044
- if (u === "kib")
21045
- return v / 1024;
21046
- return v;
21047
- }
21048
21028
  var dimensionEnum = z86.enum(ALL_DIMENSIONS).describe("Diagnostic dimension to investigate.");
21049
21029
  var rootCauseTools = [
21050
21030
  defineTool({
@@ -21086,9 +21066,9 @@ var rootCauseTools = [
21086
21066
  const device = resolveDeviceName(ctx.device);
21087
21067
  ctx.info(`Tracing path from '${device}' to ${a.target}`);
21088
21068
  const [traceResult, pingResult, routeResult] = await Promise.all([
21089
- safe3(`/tool traceroute ${quoteValue(a.target)} count=${a.count} use-dns=${a.use_dns ? "yes" : "no"}`, ctx),
21090
- safe3(`/ping ${quoteValue(a.target)} count=5`, ctx),
21091
- safe3(`/ip route print where dst-address=0.0.0.0/0`, ctx)
21069
+ safe(`/tool traceroute ${quoteValue(a.target)} count=${a.count} use-dns=${a.use_dns ? "yes" : "no"}`, ctx),
21070
+ safe(`/ping ${quoteValue(a.target)} count=5`, ctx),
21071
+ safe(`/ip route print where dst-address=0.0.0.0/0`, ctx)
21092
21072
  ]);
21093
21073
  const ping = parsePingSummary(pingResult);
21094
21074
  const lines = [];
@@ -21150,7 +21130,7 @@ var rootCauseTools = [
21150
21130
  filters.push(`message~"${a.keyword}"`);
21151
21131
  }
21152
21132
  const where = filters.length > 0 ? ` where ${filters.join(" ")}` : "";
21153
- const raw = await safe3(`/log print detail${where}`, ctx);
21133
+ const raw = await safe(`/log print detail${where}`, ctx);
21154
21134
  if (!raw || isEmpty(raw)) {
21155
21135
  return `No matching log events found on '${device}' in the last ${a.time_window}.`;
21156
21136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usex/mikrotik-mcp",
3
- "version": "3.51.1",
3
+ "version": "3.53.0",
4
4
  "description": "MCP server for MikroTik RouterOS — 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: audit-security-posture
3
+ title: Run a compliance security audit
4
+ description: Comprehensive security audit across SSH, services, firewall, users, DNS, certificates, SNMP, hardening, and VPN — scored A+ through F with actionable fix commands.
5
+ arguments:
6
+ - name: categories
7
+ description: Comma-separated categories to audit (e.g. "ssh,firewall,users"). Omit for a full audit across all 9 categories.
8
+ required: false
9
+ ---
10
+
11
+ Run a comprehensive security compliance audit on this MikroTik device. The audit
12
+ covers 36 checks across 9 categories — SSH, management services, firewall, user
13
+ accounts, DNS, certificates, SNMP, system hardening, and VPN — and scores the
14
+ device from A+ (excellent) down to F (critical).
15
+
16
+ Categories to audit: {{categories}}
17
+
18
+ Follow this workflow:
19
+
20
+ 1. **Run the audit.** Call `run_compliance_audit` (optionally filtered to
21
+ `{{categories}}`). Review the per-check results: each check reports pass, fail,
22
+ or warn with a severity level and a specific fix command.
23
+
24
+ 2. **Present the results.** Organize findings by severity (critical → low):
25
+ - The overall grade (A+ through F) and numeric score.
26
+ - A summary table of failed and warning checks.
27
+ - For each failing check: what it found, why it matters, and the exact fix
28
+ command provided by the audit engine.
29
+
30
+ 3. **Remediate (with user approval).** If the user wants to fix failing checks,
31
+ call `audit_remediate` with `dry_run=true` first to preview what commands would
32
+ run. Then, with approval, call `audit_remediate` with `dry_run=false` to apply.
33
+
34
+ 4. **Re-audit.** After remediation, run `run_compliance_audit` again to confirm
35
+ the score improved and no new issues were introduced.
36
+
37
+ 5. **Fleet-wide (optional).** If multiple devices are configured, offer to run
38
+ `audit_fleet` for a consolidated report with aggregate scores and the most
39
+ common failures across the fleet.
40
+
41
+ Do not apply any fixes without user approval. Present the audit report first.
@@ -9,9 +9,12 @@ Create a restore point for this MikroTik device and then write up a clear,
9
9
  human-readable summary of how it's configured. This is read-mostly: the only
10
10
  change is creating a backup/export.
11
11
 
12
- 1. **Restore point** — `create_backup` (binary, for full restore) and
12
+ 1. **Local snapshot (zero device footprint)** — `capture_config_snapshot` first.
13
+ This stores a text `/export` in the MCP host's local database — no file is
14
+ written on the router's flash. Give it a descriptive `label` (e.g. `pre-audit`).
15
+ 2. **Device restore point** — `create_backup` (binary, for full restore) and
13
16
  `create_export` (text `.rsc`, for review/diff). List them with `list_backups`.
14
- 2. **Inventory** — gather the configuration with read tools and organize it:
17
+ 3. **Inventory** — gather the configuration with read tools and organize it:
15
18
  - System: `get_system_identity`, `get_system_resources`, `get_routerboard`,
16
19
  `get_installed_packages`.
17
20
  - L2/L3: `list_interfaces`, `list_vlan_interfaces`, `list_bridges`,
@@ -23,10 +26,12 @@ change is creating a backup/export.
23
26
  `list_users`, `list_certificates`.
24
27
  - VPN/QoS: `list_wireguard_interfaces` + `list_wireguard_peers`,
25
28
  `list_simple_queues`, `list_queue_trees`.
29
+ - Containers: `list_containers` (if the container feature is enabled).
26
30
  - Automation: `list_schedulers`, `list_scripts`.
27
31
 
28
32
  Produce a structured Markdown report: a one-paragraph overview, a table of
29
33
  interfaces and addressing, the firewall posture, and a "things worth reviewing"
30
- section (defaults left in place, disabled-but-present rules, expiring certs).
31
- Reference the backup/export filenames you created so the user knows their restore
32
- point.
34
+ section (defaults left in place, disabled-but-present rules, expiring certs
35
+ check `list_certificates` for any expiring within 30 days).
36
+ Reference the snapshot id and backup/export filenames you created so the user
37
+ knows their restore points.
@@ -23,6 +23,9 @@ Decision guidance — weigh these MikroTik options:
23
23
  - **WireGuard** — fastest, simplest, modern. Best for MikroTik↔MikroTik and
24
24
  laptops/phones with the WireGuard app. No built-in OS client on older systems.
25
25
  Tools: `create_wireguard_interface`, `add_wireguard_peer`, `generate_wireguard_client_config`.
26
+ For **3+ sites** (full-mesh or hub-spoke): `build_wireguard_mesh`.
27
+ For **user onboarding** (generate config, add peer, revoke later):
28
+ `onboard_wireguard_user`, `revoke_wireguard_user`.
26
29
  - **IPsec (IKEv2)** — the interoperability choice for site-to-site with
27
30
  _other vendors_ (Cisco/Fortinet/pfSense) and for native iOS/Windows IKEv2
28
31
  road-warrior. Most config surface. Tools: `create_ipsec_*` (profile/peer/
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: design-qos-policy
3
+ title: Design a traffic shaping policy
4
+ description: Build a QoS queue hierarchy from business requirements — define traffic classes, priorities, and bandwidth guarantees with preview before apply.
5
+ arguments:
6
+ - name: wan_bandwidth
7
+ description: WAN bandwidth in download/upload format (e.g. "100M/50M", "1G/1G").
8
+ required: true
9
+ - name: classes
10
+ description: Describe traffic classes and priorities (e.g. "VoIP=highest, video conferencing=high, web=normal, bulk downloads=low"). If omitted, a sensible default is used.
11
+ required: false
12
+ ---
13
+
14
+ Design and deploy a traffic shaping (QoS) policy on this MikroTik device that
15
+ prioritizes important traffic and prevents bulk downloads from saturating the
16
+ link. All changes are previewed before applying.
17
+
18
+ WAN bandwidth: {{wan_bandwidth}}
19
+ Traffic classes: {{classes}}
20
+
21
+ Steps:
22
+
23
+ 1. **Understand the network.** Call `list_interfaces` and `list_ip_addresses` to
24
+ identify the WAN interface. Call `list_queue_trees` and `list_simple_queues`
25
+ to check if any QoS is already in place.
26
+
27
+ 2. **Design the policy.** Translate {{classes}} into a queue hierarchy:
28
+ - A parent queue on the WAN interface capped at {{wan_bandwidth}}.
29
+ - Child queues for each traffic class with appropriate priorities,
30
+ guaranteed minimum bandwidth, and burst settings.
31
+ Call `apply_traffic_shaping` in **preview mode** to see the queue tree
32
+ structure before applying.
33
+
34
+ 3. **Review the queue tree.** Present a table showing:
35
+ - Each traffic class, its priority level, guaranteed bandwidth, max bandwidth.
36
+ - Packet marks or connection marks used to classify traffic.
37
+ - Mangle rules that mark traffic into classes.
38
+
39
+ 4. **Apply.** With user approval, deploy the QoS policy.
40
+
41
+ 5. **Verify.** Call `list_queue_trees` to confirm the hierarchy is in place.
42
+ Monitor briefly to see traffic flowing through the queues.
43
+
44
+ 6. **Capacity planning (optional).** Call `forecast_link_saturation` on the WAN
45
+ interface to project when the current link will reach capacity at the observed
46
+ growth rate — useful for planning upgrades.
47
+
48
+ Report the complete QoS policy: queue hierarchy, classification rules, and
49
+ bandwidth allocations per class.