@usex/mikrotik-mcp 3.54.0 → 3.56.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
@@ -96,7 +96,7 @@ import {
96
96
  toggleAaaEntity,
97
97
  updateAaaEntity,
98
98
  writeBackup
99
- } from "./shared/cli-njttbdf8.js";
99
+ } from "./shared/cli-thhk9f7n.js";
100
100
 
101
101
  // src/cli.ts
102
102
  import { existsSync as existsSync2 } from "fs";
@@ -1559,6 +1559,7 @@ function devicesPayload(store) {
1559
1559
  authMode: dc.mac ? "mac-telnet" : dc.keyFilename || dc.privateKey ? "key" : dc.password ? "password" : "none",
1560
1560
  isDefault: name === cfg.defaultDevice,
1561
1561
  description: dc.description,
1562
+ disabled: !!dc.disabled,
1562
1563
  jumpVia: dc.jumpVia,
1563
1564
  jumpHost: dc.jumpHost ? { host: dc.jumpHost.host, port: dc.jumpHost.port } : undefined,
1564
1565
  status: getDeviceStatus(name),
@@ -2373,6 +2374,42 @@ async function runDashboard(cfg, transportLabel) {
2373
2374
  if (url.pathname === "/api/devices") {
2374
2375
  return json3(devicesPayload(db));
2375
2376
  }
2377
+ if (url.pathname === "/api/devices/toggle" && req.method === "POST") {
2378
+ const b = await readJson(req);
2379
+ if (typeof b?.device !== "string" || typeof b?.disabled !== "boolean") {
2380
+ return json3({ error: "device (string) and disabled (boolean) are required" }, 400);
2381
+ }
2382
+ const cfg2 = getConfig();
2383
+ if (!(b.device in cfg2.devices))
2384
+ return json3({ error: `unknown device: ${b.device}` }, 404);
2385
+ const dc = cfg2.devices[b.device];
2386
+ const next = {
2387
+ ...cfg2,
2388
+ devices: {
2389
+ ...cfg2.devices,
2390
+ [b.device]: { ...dc, disabled: b.disabled || undefined }
2391
+ }
2392
+ };
2393
+ setConfig(next);
2394
+ let persisted = true;
2395
+ let warning;
2396
+ try {
2397
+ atomicWrite(getConfigSource().path, serializeConfig(next));
2398
+ } catch (e) {
2399
+ persisted = false;
2400
+ warning = `applied live but not saved to disk: ${e instanceof Error ? e.message : String(e)}`;
2401
+ }
2402
+ if (persisted) {
2403
+ recordVersion(getConfig(), "auto", Date.now(), `device ${b.disabled ? "disabled" : "enabled"}: ${b.device}`);
2404
+ }
2405
+ return json3({
2406
+ ok: true,
2407
+ persisted,
2408
+ requiresReconnect: true,
2409
+ warning,
2410
+ ...devicesPayload(db)
2411
+ });
2412
+ }
2376
2413
  if (url.pathname === "/api/topology") {
2377
2414
  return json3(topologyPayload());
2378
2415
  }
package/dist/index.d.ts CHANGED
@@ -47,7 +47,7 @@ interface UiLink {
47
47
  }
48
48
  declare function setConfig(cfg: MikrotikConfig): void;
49
49
  declare function getConfig(): MikrotikConfig;
50
- /** Names of every configured device, plus which one is the default. */
50
+ /** Names of every ENABLED configured device, plus which one is the default. */
51
51
  declare function listDevices(): {
52
52
  names: string[];
53
53
  default: string;
@@ -55,6 +55,7 @@ declare function listDevices(): {
55
55
  /**
56
56
  * Resolve a (possibly undefined) device name to a concrete, existing config key.
57
57
  * Accepts a config key OR a device's free-text label; falls back to the default.
58
+ * Only resolves to enabled devices.
58
59
  *
59
60
  * Matching is EXACT (key first, then label) — never fuzzy/substring — so a name
60
61
  * like "Ali Home" can never collapse onto a different device such as "home".
@@ -71,7 +72,7 @@ interface DeviceDirectoryEntry {
71
72
  /**
72
73
  * Return the connection config for a device by key or label (or the default when
73
74
  * `name` is undefined). Throws if an explicit name matches neither a key nor a
74
- * label.
75
+ * label, or if the device is disabled.
75
76
  */
76
77
  declare function getDevice(name?: string): DeviceConfig;
77
78
  /** Options threaded into every tool registration. */
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  resolveDeviceName,
23
23
  selectToolModules,
24
24
  setConfig
25
- } from "./shared/library-hk90ad18.js";
25
+ } from "./shared/library-ymtf9zyk.js";
26
26
  // src/server.ts
27
27
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
28
28
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./cli-njttbdf8.js";
7
+ } from "./cli-thhk9f7n.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -337,7 +337,8 @@ var DeviceConfigSchema = z.object({
337
337
  sourceMac: z.string().optional(),
338
338
  macHost: z.string().optional(),
339
339
  macPort: z.coerce.number().int().positive().optional(),
340
- description: z.string().optional()
340
+ description: z.string().optional(),
341
+ disabled: z.boolean().optional()
341
342
  });
342
343
  var S3ConfigSchema = z.object({
343
344
  accessKeyId: z.string().optional(),
@@ -584,13 +585,17 @@ function setConfig(cfg) {
584
585
  function getConfig() {
585
586
  return active;
586
587
  }
588
+ function isEnabled(dc) {
589
+ return !dc.disabled;
590
+ }
587
591
  function listDevices() {
588
- return { names: Object.keys(active.devices), default: active.defaultDevice };
592
+ const names = Object.entries(active.devices).filter(([, dc]) => isEnabled(dc)).map(([k]) => k);
593
+ return { names, default: active.defaultDevice };
589
594
  }
590
595
  function deviceKeyForLabel(name) {
591
596
  const target = name.trim().toLowerCase();
592
597
  for (const [key, dc] of Object.entries(active.devices)) {
593
- if (dc.description && dc.description.trim().toLowerCase() === target)
598
+ if (isEnabled(dc) && dc.description && dc.description.trim().toLowerCase() === target)
594
599
  return key;
595
600
  }
596
601
  return;
@@ -599,6 +604,8 @@ function deviceLabels() {
599
604
  const seen = new Set;
600
605
  const out = [];
601
606
  for (const [key, dc] of Object.entries(active.devices)) {
607
+ if (!isEnabled(dc))
608
+ continue;
602
609
  const label = dc.description?.trim();
603
610
  if (label && label !== key && !(label in active.devices) && !seen.has(label)) {
604
611
  seen.add(label);
@@ -609,13 +616,17 @@ function deviceLabels() {
609
616
  }
610
617
  function resolveDeviceName(name) {
611
618
  if (name) {
612
- if (name in active.devices)
619
+ if (name in active.devices && isEnabled(active.devices[name]))
613
620
  return name;
614
621
  const byLabel = deviceKeyForLabel(name);
615
622
  if (byLabel)
616
623
  return byLabel;
617
624
  }
618
- return active.defaultDevice in active.devices ? active.defaultDevice : Object.keys(active.devices)[0] ?? active.defaultDevice;
625
+ if (active.defaultDevice in active.devices && isEnabled(active.devices[active.defaultDevice])) {
626
+ return active.defaultDevice;
627
+ }
628
+ const firstEnabled = Object.entries(active.devices).find(([, dc]) => isEnabled(dc));
629
+ return firstEnabled ? firstEnabled[0] : active.defaultDevice;
619
630
  }
620
631
  function deviceTarget(dc) {
621
632
  if (!dc)
@@ -623,7 +634,7 @@ function deviceTarget(dc) {
623
634
  return dc.mac ? `MAC ${dc.mac}` : `${dc.host}:${dc.port ?? 22}`;
624
635
  }
625
636
  function deviceDirectory() {
626
- return Object.entries(active.devices).map(([key, dc]) => ({
637
+ return Object.entries(active.devices).filter(([, dc]) => isEnabled(dc)).map(([key, dc]) => ({
627
638
  key,
628
639
  label: dc.description?.trim() || undefined,
629
640
  target: deviceTarget(dc),
@@ -643,6 +654,9 @@ function getDevice(name) {
643
654
  const dc = active.devices[key];
644
655
  if (!dc)
645
656
  throw new Error(`No device configuration available for '${key}'.`);
657
+ if (dc.disabled) {
658
+ throw new Error(`Device '${key}' is disabled. Enable it from the dashboard or config file.`);
659
+ }
646
660
  return dc;
647
661
  }
648
662
 
@@ -1762,7 +1776,7 @@ function parseKvTokens(chunk) {
1762
1776
  return out;
1763
1777
  }
1764
1778
  var INDEX_LINE = /^\s*(\d+)\s+(.*)$/;
1765
- var LEADING_FLAGS = /^([A-Z]+)(?=\s|$)/;
1779
+ var LEADING_FLAGS = /^([A-Z][A-Za-z]*)(?=\s|$)/;
1766
1780
  function unionColumns(rows) {
1767
1781
  const seen = new Set;
1768
1782
  const cols = [];
@@ -8313,7 +8327,7 @@ var cache = null;
8313
8327
  async function gateway() {
8314
8328
  if (cache)
8315
8329
  return cache;
8316
- const { moduleCatalog } = await import("./cli-xt3qvpz0.js");
8330
+ const { moduleCatalog } = await import("./cli-0ctwspf1.js");
8317
8331
  const forIndex = [];
8318
8332
  const byName = new Map;
8319
8333
  for (const mod of moduleCatalog) {
@@ -9338,10 +9352,44 @@ function covers(key, aVal, bVal) {
9338
9352
  return cidrContains(aVal, bVal);
9339
9353
  return false;
9340
9354
  }
9355
+ var INTERFACE_LIST_PAIRS = [
9356
+ ["in-interface-list", "in-interface"],
9357
+ ["out-interface-list", "out-interface"]
9358
+ ];
9359
+ var _ifaceLists;
9360
+ function interfaceListCovers(aKey, aVal, bKey, bVal) {
9361
+ if (!_ifaceLists)
9362
+ return;
9363
+ for (const [listKey, ifaceKey] of INTERFACE_LIST_PAIRS) {
9364
+ if (aKey === listKey && bKey === ifaceKey) {
9365
+ const negated = aVal.startsWith("!");
9366
+ const listName = negated ? aVal.slice(1) : aVal;
9367
+ const members = _ifaceLists.get(listName);
9368
+ if (!members)
9369
+ return;
9370
+ const isMember = members.has(bVal);
9371
+ return negated ? !isMember : isMember;
9372
+ }
9373
+ }
9374
+ return;
9375
+ }
9341
9376
  function aCoversB(a, b) {
9342
9377
  for (const [k, v] of Object.entries(a.match)) {
9343
9378
  const bv = b.match[k];
9344
- if (bv === undefined || !covers(k, v, bv))
9379
+ if (bv !== undefined) {
9380
+ if (!covers(k, v, bv))
9381
+ return false;
9382
+ continue;
9383
+ }
9384
+ let crossCovered = false;
9385
+ for (const [bk, bval] of Object.entries(b.match)) {
9386
+ const result = interfaceListCovers(k, v, bk, bval);
9387
+ if (result === true) {
9388
+ crossCovered = true;
9389
+ break;
9390
+ }
9391
+ }
9392
+ if (!crossCovered)
9345
9393
  return false;
9346
9394
  }
9347
9395
  return true;
@@ -9518,6 +9566,7 @@ function grade2(score) {
9518
9566
  return "critical";
9519
9567
  }
9520
9568
  function auditFirewall(input) {
9569
+ _ifaceLists = input.interfaceLists;
9521
9570
  const findings = [];
9522
9571
  if (input.filter)
9523
9572
  findings.push(...auditFilter(input.filter));
@@ -9566,6 +9615,25 @@ async function fetchRules(path, ctx) {
9566
9615
  return [];
9567
9616
  return parseRecords(out).rows;
9568
9617
  }
9618
+ async function fetchInterfaceListMembers(ctx) {
9619
+ const out = await executeMikrotikCommand("/interface list member print detail", ctx);
9620
+ const members = new Map;
9621
+ if (looksLikeError(out) || isEmpty(out))
9622
+ return members;
9623
+ for (const row of parseRecords(out).rows) {
9624
+ const list = row.list;
9625
+ const iface = row.interface;
9626
+ if (!list || !iface)
9627
+ continue;
9628
+ let set = members.get(list);
9629
+ if (!set) {
9630
+ set = new Set;
9631
+ members.set(list, set);
9632
+ }
9633
+ set.add(iface);
9634
+ }
9635
+ return members;
9636
+ }
9569
9637
  var firewallAuditTools = [
9570
9638
  defineTool({
9571
9639
  name: "firewall_audit",
@@ -9580,10 +9648,16 @@ var firewallAuditTools = [
9580
9648
  async handler(a, ctx) {
9581
9649
  const device = resolveDeviceName(ctx.device);
9582
9650
  ctx.info(`Auditing firewall for '${device}'`);
9583
- const filter = rulesFromRows(await fetchRules("/ip firewall filter", ctx));
9584
- const nat = a.include_nat ? rulesFromRows(await fetchRules("/ip firewall nat", ctx)) : undefined;
9585
- const mangle = a.include_mangle ? rulesFromRows(await fetchRules("/ip firewall mangle", ctx)) : undefined;
9586
- const report = auditFirewall({ filter, nat, mangle });
9651
+ const [filterRows, natRows, mangleRows, interfaceLists] = await Promise.all([
9652
+ fetchRules("/ip firewall filter", ctx),
9653
+ a.include_nat ? fetchRules("/ip firewall nat", ctx) : Promise.resolve(undefined),
9654
+ a.include_mangle ? fetchRules("/ip firewall mangle", ctx) : Promise.resolve(undefined),
9655
+ fetchInterfaceListMembers(ctx)
9656
+ ]);
9657
+ const filter = rulesFromRows(filterRows);
9658
+ const nat = natRows ? rulesFromRows(natRows) : undefined;
9659
+ const mangle = mangleRows ? rulesFromRows(mangleRows) : undefined;
9660
+ const report = auditFirewall({ filter, nat, mangle, interfaceLists });
9587
9661
  const structuredContent = {
9588
9662
  __mikrotikView: "firewall-audit",
9589
9663
  device,
@@ -10048,6 +10122,22 @@ Management access may be partially restricted \u2014 review immediately.`;
10048
10122
  // src/tools/firewall-filter.ts
10049
10123
  import { z as z28 } from "zod";
10050
10124
  var isDigits = (s) => /^\d+$/.test(s);
10125
+ async function resolveFilterRuleId(ruleId, ctx) {
10126
+ if (/^\d+$/.test(ruleId)) {
10127
+ const id = `*${ruleId}`;
10128
+ const byId = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
10129
+ if (byId.trim() !== "0")
10130
+ return id;
10131
+ const idsRaw = await executeMikrotikCommand(`:foreach i in=[/ip firewall filter find] do={:put $i}`, ctx);
10132
+ if (isEmpty(idsRaw))
10133
+ return null;
10134
+ const ids = idsRaw.trim().split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
10135
+ const pos = Number.parseInt(ruleId, 10);
10136
+ return pos >= 0 && pos < ids.length ? ids[pos] : null;
10137
+ }
10138
+ const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${ruleId}`, ctx);
10139
+ return count.trim() !== "0" ? ruleId : null;
10140
+ }
10051
10141
  async function updateFilterRule(a, ctx) {
10052
10142
  ctx.info(`Updating firewall filter rule: rule_id=${a.rule_id}`);
10053
10143
  const updates = [];
@@ -10116,7 +10206,9 @@ async function updateFilterRule(a, ctx) {
10116
10206
  }
10117
10207
  if (updates.length === 0)
10118
10208
  return "No updates specified.";
10119
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10209
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10210
+ if (!id)
10211
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10120
10212
  const cmd = `/ip firewall filter set ${id} ${updates.join(" ")}`;
10121
10213
  const result = await executeMikrotikCommand(cmd, ctx);
10122
10214
  if (looksLikeError(result))
@@ -10260,7 +10352,7 @@ ${details}`;
10260
10352
  filters.push("invalid=yes");
10261
10353
  if (a.dynamic_only)
10262
10354
  filters.push("dynamic=yes");
10263
- const result = await executeMikrotikCommand(`/ip firewall filter print${whereClause(filters)}`, ctx);
10355
+ const result = await executeMikrotikCommand(`/ip firewall filter print detail${whereClause(filters)}`, ctx);
10264
10356
  return isEmpty(result) ? "No firewall filter rules found matching the criteria." : `FIREWALL FILTER RULES:
10265
10357
 
10266
10358
  ${result}`;
@@ -10270,15 +10362,17 @@ ${result}`;
10270
10362
  name: "get_filter_rule",
10271
10363
  title: "Get Firewall Filter Rule",
10272
10364
  annotations: READ,
10273
- description: "Gets the full detail of one IPv4 firewall FILTER rule (`/ip firewall filter`) by id \u2014 " + "every matcher, action, counter and flag. For IPv6 use get_ipv6_filter_rule. " + 'rule_id: the `.id` from list_filter_rules e.g. "*1" or "0".',
10365
+ description: "Gets the full detail of one IPv4 firewall FILTER rule (`/ip firewall filter`) by id \u2014 " + "every matcher, action, counter and flag. For IPv6 use get_ipv6_filter_rule. " + 'rule_id: preferably the `.id` from list_filter_rules e.g. "*1F". A bare number like "3" ' + "is tried as `.id=*3` first, then as the positional row index if no `.id` matches.",
10274
10366
  inputSchema: {
10275
- rule_id: z28.string().describe('Rule ID from list output e.g. "*1" or "0"')
10367
+ rule_id: z28.string().describe('Rule .id e.g. "*1F", or bare position number e.g. "3"')
10276
10368
  },
10277
10369
  async handler(a, ctx) {
10278
10370
  ctx.info(`Getting firewall filter rule details: rule_id=${a.rule_id}`);
10279
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10371
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10372
+ if (!id)
10373
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10280
10374
  const result = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${id}`, ctx);
10281
- return isEmpty(result) ? `Firewall filter rule with ID '${a.rule_id}' not found.` : `FIREWALL FILTER RULE DETAILS:
10375
+ return isEmpty(result) ? `Firewall filter rule '${a.rule_id}' not found.` : `FIREWALL FILTER RULE DETAILS:
10282
10376
 
10283
10377
  ${result}`;
10284
10378
  }
@@ -10353,14 +10447,13 @@ ${result}`;
10353
10447
  inputSchema: { rule_id: z28.string() },
10354
10448
  async handler(a, ctx) {
10355
10449
  ctx.info(`Removing firewall filter rule: rule_id=${a.rule_id}`);
10356
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10357
- const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
10358
- if (count.trim() === "0")
10359
- return `Firewall filter rule with ID '${a.rule_id}' not found.`;
10450
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10451
+ if (!id)
10452
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10360
10453
  const result = await executeMikrotikCommand(`/ip firewall filter remove ${id}`, ctx);
10361
10454
  if (looksLikeError(result))
10362
10455
  return `Failed to remove firewall filter rule: ${result}`;
10363
- return `Firewall filter rule with ID '${a.rule_id}' removed successfully.`;
10456
+ return `Firewall filter rule '${a.rule_id}' (${id}) removed successfully.`;
10364
10457
  }
10365
10458
  }),
10366
10459
  defineTool({
@@ -10374,14 +10467,13 @@ ${result}`;
10374
10467
  },
10375
10468
  async handler(a, ctx) {
10376
10469
  ctx.info(`Moving firewall filter rule: rule_id=${a.rule_id} to position ${a.destination}`);
10377
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10378
- const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
10379
- if (count.trim() === "0")
10380
- return `Firewall filter rule with ID '${a.rule_id}' not found.`;
10470
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10471
+ if (!id)
10472
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10381
10473
  const result = await executeMikrotikCommand(`/ip firewall filter move ${id} destination=${a.destination}`, ctx);
10382
10474
  if (looksLikeError(result))
10383
10475
  return `Failed to move firewall filter rule: ${result}`;
10384
- return `Firewall filter rule with ID '${a.rule_id}' moved to position ${a.destination}.`;
10476
+ return `Firewall filter rule '${a.rule_id}' (${id}) moved to position ${a.destination}.`;
10385
10477
  }
10386
10478
  }),
10387
10479
  defineTool({
@@ -20657,13 +20749,27 @@ function analyzeRootCause(data) {
20657
20749
  }
20658
20750
  }
20659
20751
  if (downInterfaces.length === 0 && errorInterfaces.length === 0) {
20752
+ if (data.interfaces.length === 0) {
20753
+ evidence.push({
20754
+ dimension: "interfaces",
20755
+ severity: "warning",
20756
+ summary: "Interface data unavailable \u2014 the device returned no parseable interface records. " + "Run `/interface print detail` manually to verify."
20757
+ });
20758
+ } else {
20759
+ evidence.push({
20760
+ dimension: "interfaces",
20761
+ severity: "ok",
20762
+ summary: `All ${data.interfaces.length} interfaces healthy`
20763
+ });
20764
+ }
20765
+ }
20766
+ if (data.routeCount === 0) {
20660
20767
  evidence.push({
20661
- dimension: "interfaces",
20662
- severity: "ok",
20663
- summary: `All ${data.interfaces.length} interfaces healthy`
20768
+ dimension: "routing",
20769
+ severity: "warning",
20770
+ summary: "Route data unavailable \u2014 the device returned no parseable route records. " + "Run `/ip route print detail` manually to verify."
20664
20771
  });
20665
- }
20666
- if (!data.defaultRouteExists) {
20772
+ } else if (!data.defaultRouteExists) {
20667
20773
  evidence.push({
20668
20774
  dimension: "routing",
20669
20775
  severity: "critical",
@@ -20676,7 +20782,7 @@ function analyzeRootCause(data) {
20676
20782
  summary: `Default route present, ${data.routeCount} total routes`
20677
20783
  });
20678
20784
  }
20679
- const ospfDown = data.ospfNeighbors.filter((n) => n.state.toLowerCase() !== "full");
20785
+ const ospfDown = data.ospfNeighbors.filter((n) => (n.id || n.address) && n.state.toLowerCase() !== "full");
20680
20786
  for (const n of ospfDown) {
20681
20787
  evidence.push({
20682
20788
  dimension: "routing",
@@ -20685,7 +20791,7 @@ function analyzeRootCause(data) {
20685
20791
  reference: n.id
20686
20792
  });
20687
20793
  }
20688
- const bgpDown = data.bgpPeers.filter((p) => !p.state.toLowerCase().includes("established"));
20794
+ const bgpDown = data.bgpPeers.filter((p) => (p.id || p.address) && !p.state.toLowerCase().includes("established"));
20689
20795
  for (const p of bgpDown) {
20690
20796
  evidence.push({
20691
20797
  dimension: "routing",
@@ -20788,7 +20894,13 @@ function analyzeRootCause(data) {
20788
20894
  summary: "No DNS servers configured"
20789
20895
  });
20790
20896
  }
20791
- if (data.cpuLoad > 90) {
20897
+ if (data.cpuLoad === 0 && data.memoryUsedPct === 0 && !data.rosVersion && !data.uptime) {
20898
+ evidence.push({
20899
+ dimension: "resources",
20900
+ severity: "warning",
20901
+ summary: "System resource data unavailable \u2014 CPU 0%, memory 0%, no version or uptime. " + "The `/system resource print` output may not have been parsed correctly."
20902
+ });
20903
+ } else if (data.cpuLoad > 90) {
20792
20904
  evidence.push({
20793
20905
  dimension: "resources",
20794
20906
  severity: "critical",
@@ -20807,24 +20919,26 @@ function analyzeRootCause(data) {
20807
20919
  summary: `CPU load: ${data.cpuLoad}%`
20808
20920
  });
20809
20921
  }
20810
- if (data.memoryUsedPct > 90) {
20811
- evidence.push({
20812
- dimension: "resources",
20813
- severity: "critical",
20814
- summary: `Memory critically low: ${data.memoryUsedPct}% used`
20815
- });
20816
- } else if (data.memoryUsedPct > 75) {
20817
- evidence.push({
20818
- dimension: "resources",
20819
- severity: "warning",
20820
- summary: `Memory pressure: ${data.memoryUsedPct}% used`
20821
- });
20822
- } else {
20823
- evidence.push({
20824
- dimension: "resources",
20825
- severity: "ok",
20826
- summary: `Memory: ${data.memoryUsedPct}% used`
20827
- });
20922
+ if (data.rosVersion || data.uptime || data.cpuLoad > 0 || data.memoryUsedPct > 0) {
20923
+ if (data.memoryUsedPct > 90) {
20924
+ evidence.push({
20925
+ dimension: "resources",
20926
+ severity: "critical",
20927
+ summary: `Memory critically low: ${data.memoryUsedPct}% used`
20928
+ });
20929
+ } else if (data.memoryUsedPct > 75) {
20930
+ evidence.push({
20931
+ dimension: "resources",
20932
+ severity: "warning",
20933
+ summary: `Memory pressure: ${data.memoryUsedPct}% used`
20934
+ });
20935
+ } else {
20936
+ evidence.push({
20937
+ dimension: "resources",
20938
+ severity: "ok",
20939
+ summary: `Memory: ${data.memoryUsedPct}% used`
20940
+ });
20941
+ }
20828
20942
  }
20829
20943
  const errorLogs = data.relevantLogs.filter((l) => l.topics.includes("error") || l.topics.includes("critical") || l.topics.includes("warning"));
20830
20944
  const firewallLogs = data.relevantLogs.filter((l) => l.topics.includes("firewall"));
@@ -20861,14 +20975,24 @@ function analyzeRootCause(data) {
20861
20975
  summary: "No concerning log entries in the last 10 minutes"
20862
20976
  });
20863
20977
  }
20978
+ const isServerBinding = (t) => /-(in|server)$/.test(t.type) || t.name.startsWith("<");
20864
20979
  const downTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled);
20865
20980
  for (const t of downTunnels) {
20866
- evidence.push({
20867
- dimension: "vpn",
20868
- severity: "critical",
20869
- summary: `Tunnel ${t.name} (${t.type}) is down`,
20870
- reference: t.name
20871
- });
20981
+ if (isServerBinding(t)) {
20982
+ evidence.push({
20983
+ dimension: "vpn",
20984
+ severity: "info",
20985
+ summary: `Server binding ${t.name} (${t.type}) is idle \u2014 no active client session`,
20986
+ reference: t.name
20987
+ });
20988
+ } else {
20989
+ evidence.push({
20990
+ dimension: "vpn",
20991
+ severity: "critical",
20992
+ summary: `Tunnel ${t.name} (${t.type}) is down`,
20993
+ reference: t.name
20994
+ });
20995
+ }
20872
20996
  }
20873
20997
  if (data.tunnelInterfaces.length > 0 && downTunnels.length === 0) {
20874
20998
  evidence.push({
@@ -20878,6 +21002,19 @@ function analyzeRootCause(data) {
20878
21002
  });
20879
21003
  }
20880
21004
  correlateRootCauses(data, evidence, rootCauses);
21005
+ if (data.ping && data.ping.lossPct === 0) {
21006
+ const contradicted = new Set([
21007
+ "Missing default route",
21008
+ "Missing source NAT / masquerade",
21009
+ "Interface link failure"
21010
+ ]);
21011
+ for (const rc of rootCauses) {
21012
+ if (contradicted.has(rc.cause) && rc.confidence !== "low") {
21013
+ rc.confidence = "low";
21014
+ rc.explanation += " (Note: ping to the target succeeded with 0% loss, which contradicts this hypothesis.)";
21015
+ }
21016
+ }
21017
+ }
20881
21018
  rootCauses.sort((a, b) => CONFIDENCE_RANK[a.confidence] - CONFIDENCE_RANK[b.confidence]);
20882
21019
  const dimensionSummary = ALL_DIMENSIONS.map((dim) => {
20883
21020
  const dimEvidence = evidence.filter((e) => e.dimension === dim);
@@ -21016,14 +21153,15 @@ function correlateRootCauses(data, evidence, causes) {
21016
21153
  });
21017
21154
  }
21018
21155
  }
21019
- const downTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled);
21020
- if (downTunnels.length > 0) {
21156
+ const isServerTunnel = (t) => /-(in|server)$/.test(t.type) || t.name.startsWith("<");
21157
+ const realDownTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled && !isServerTunnel(t));
21158
+ if (realDownTunnels.length > 0) {
21021
21159
  causes.push({
21022
21160
  cause: "VPN/tunnel interface down",
21023
- explanation: `Tunnel(s) ${downTunnels.map((t) => `${t.name} (${t.type})`).join(", ")} are down. ` + "Traffic destined for remote networks over these tunnels will be black-holed.",
21161
+ explanation: `Tunnel(s) ${realDownTunnels.map((t) => `${t.name} (${t.type})`).join(", ")} are down. ` + "Traffic destined for remote networks over these tunnels will be black-holed.",
21024
21162
  confidence: "high",
21025
21163
  evidence: evidence.filter((e) => e.dimension === "vpn" && e.severity === "critical"),
21026
- fixes: downTunnels.map((t) => `/interface enable [find name="${t.name}"]`),
21164
+ fixes: realDownTunnels.map((t) => `/interface enable [find name="${t.name}"]`),
21027
21165
  dimensions: ["vpn"]
21028
21166
  });
21029
21167
  }
@@ -21252,7 +21390,7 @@ async function collectRoutes(ctx) {
21252
21390
  }
21253
21391
  async function collectOspfNeighbors(ctx) {
21254
21392
  const raw = await safe("/routing ospf neighbor print detail", ctx);
21255
- if (!raw)
21393
+ if (!raw || isEmpty(raw))
21256
21394
  return [];
21257
21395
  return parseRecords(raw).rows.map((r) => ({
21258
21396
  id: r["neighbor-id"] ?? r.router ?? "",
@@ -21260,13 +21398,13 @@ async function collectOspfNeighbors(ctx) {
21260
21398
  state: r.state ?? "",
21261
21399
  interface: r.interface ?? "",
21262
21400
  uptime: r.uptime
21263
- }));
21401
+ })).filter((n) => n.id || n.address);
21264
21402
  }
21265
21403
  async function collectBgpPeers(ctx) {
21266
21404
  const raw = await safe("/routing bgp session print detail", ctx);
21267
- if (!raw) {
21405
+ if (!raw || isEmpty(raw)) {
21268
21406
  const raw2 = await safe("/routing bgp peer print detail", ctx);
21269
- if (!raw2)
21407
+ if (!raw2 || isEmpty(raw2))
21270
21408
  return [];
21271
21409
  return parseRecords(raw2).rows.map((r) => ({
21272
21410
  id: r.name ?? "",
@@ -21274,7 +21412,7 @@ async function collectBgpPeers(ctx) {
21274
21412
  state: r.state ?? "",
21275
21413
  interface: r.interface ?? "",
21276
21414
  uptime: r.uptime
21277
- }));
21415
+ })).filter((p) => p.id || p.address);
21278
21416
  }
21279
21417
  return parseRecords(raw).rows.map((r) => ({
21280
21418
  id: r.name ?? r["remote.address"] ?? "",
@@ -21282,7 +21420,7 @@ async function collectBgpPeers(ctx) {
21282
21420
  state: r.state ?? r.established ?? "",
21283
21421
  interface: r.interface ?? "",
21284
21422
  uptime: r.uptime
21285
- }));
21423
+ })).filter((p) => p.id || p.address);
21286
21424
  }
21287
21425
  async function collectFirewallRules(target, ctx) {
21288
21426
  const raw = await safe("/ip firewall filter print detail", ctx);
@@ -21382,7 +21520,7 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21382
21520
  arpEntries,
21383
21521
  dhcpLeases,
21384
21522
  dnsResult,
21385
- dnsSettingsRaw,
21523
+ dnsSettingsParts,
21386
21524
  resourceRaw,
21387
21525
  logs,
21388
21526
  tunnels
@@ -21398,13 +21536,17 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21398
21536
  dims.has("arp_dhcp") ? collectArp(ctx) : Promise.resolve([]),
21399
21537
  dims.has("arp_dhcp") ? collectDhcpLeases(ctx) : Promise.resolve([]),
21400
21538
  dims.has("dns") && !isIpLike(target) ? safe(`[:resolve ${quoteValue(target)}]`, ctx) : Promise.resolve(undefined),
21401
- dims.has("dns") ? safe("/ip dns print", ctx) : Promise.resolve(""),
21539
+ dims.has("dns") ? Promise.all([
21540
+ safe(":put [/ip dns get servers]", ctx),
21541
+ safe(":put [/ip dns get dynamic-servers]", ctx),
21542
+ safe(":put [/ip dns get allow-remote-requests]", ctx)
21543
+ ]) : Promise.resolve(["", "", ""]),
21402
21544
  dims.has("resources") ? safe("/system resource print", ctx) : Promise.resolve(""),
21403
21545
  dims.has("logs") ? collectLogs(target, ctx) : Promise.resolve([]),
21404
21546
  dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([])
21405
21547
  ]);
21406
21548
  const ping = pingResult ? parsePingSummary(pingResult) ?? undefined : undefined;
21407
- const dnsKv = parseKeyValues(dnsSettingsRaw);
21549
+ const [dnsServersRaw, dnsDynamicRaw, dnsAllowRemoteRaw] = dnsSettingsParts;
21408
21550
  const resKv = parseKeyValues(resourceRaw);
21409
21551
  const totalMem = parseSize(resKv["total-memory"]) ?? 0;
21410
21552
  const freeMem = parseSize(resKv["free-memory"]) ?? 0;
@@ -21426,8 +21568,8 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21426
21568
  arpEntries,
21427
21569
  dhcpLeases,
21428
21570
  dnsResolveResult: dnsResult ?? undefined,
21429
- dnsServers: [dnsKv.servers, dnsKv["dynamic-servers"]].filter(Boolean).join(",") || "",
21430
- dnsAllowRemote: (dnsKv["allow-remote-requests"] ?? "").toLowerCase() === "yes",
21571
+ dnsServers: [dnsServersRaw.trim(), dnsDynamicRaw.trim()].filter(Boolean).join(",") || "",
21572
+ dnsAllowRemote: dnsAllowRemoteRaw.trim().toLowerCase() === "yes" || dnsAllowRemoteRaw.trim().toLowerCase() === "true",
21431
21573
  cpuLoad: parsePercent(resKv["cpu-load"]) ?? 0,
21432
21574
  memoryUsedPct: memUsedPct,
21433
21575
  uptime: resKv.uptime ?? "",
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./library-hk90ad18.js";
7
+ } from "./library-ymtf9zyk.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,