@usex/mikrotik-mcp 3.54.0 → 3.55.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-44v6pb38.js";
100
100
 
101
101
  // src/cli.ts
102
102
  import { existsSync as existsSync2 } from "fs";
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-83q9t27c.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";
@@ -1762,7 +1762,7 @@ function parseKvTokens(chunk) {
1762
1762
  return out;
1763
1763
  }
1764
1764
  var INDEX_LINE = /^\s*(\d+)\s+(.*)$/;
1765
- var LEADING_FLAGS = /^([A-Z]+)(?=\s|$)/;
1765
+ var LEADING_FLAGS = /^([A-Z][A-Za-z]*)(?=\s|$)/;
1766
1766
  function unionColumns(rows) {
1767
1767
  const seen = new Set;
1768
1768
  const cols = [];
@@ -8313,7 +8313,7 @@ var cache = null;
8313
8313
  async function gateway() {
8314
8314
  if (cache)
8315
8315
  return cache;
8316
- const { moduleCatalog } = await import("./cli-xt3qvpz0.js");
8316
+ const { moduleCatalog } = await import("./cli-yxah49n1.js");
8317
8317
  const forIndex = [];
8318
8318
  const byName = new Map;
8319
8319
  for (const mod of moduleCatalog) {
@@ -9338,10 +9338,44 @@ function covers(key, aVal, bVal) {
9338
9338
  return cidrContains(aVal, bVal);
9339
9339
  return false;
9340
9340
  }
9341
+ var INTERFACE_LIST_PAIRS = [
9342
+ ["in-interface-list", "in-interface"],
9343
+ ["out-interface-list", "out-interface"]
9344
+ ];
9345
+ var _ifaceLists;
9346
+ function interfaceListCovers(aKey, aVal, bKey, bVal) {
9347
+ if (!_ifaceLists)
9348
+ return;
9349
+ for (const [listKey, ifaceKey] of INTERFACE_LIST_PAIRS) {
9350
+ if (aKey === listKey && bKey === ifaceKey) {
9351
+ const negated = aVal.startsWith("!");
9352
+ const listName = negated ? aVal.slice(1) : aVal;
9353
+ const members = _ifaceLists.get(listName);
9354
+ if (!members)
9355
+ return;
9356
+ const isMember = members.has(bVal);
9357
+ return negated ? !isMember : isMember;
9358
+ }
9359
+ }
9360
+ return;
9361
+ }
9341
9362
  function aCoversB(a, b) {
9342
9363
  for (const [k, v] of Object.entries(a.match)) {
9343
9364
  const bv = b.match[k];
9344
- if (bv === undefined || !covers(k, v, bv))
9365
+ if (bv !== undefined) {
9366
+ if (!covers(k, v, bv))
9367
+ return false;
9368
+ continue;
9369
+ }
9370
+ let crossCovered = false;
9371
+ for (const [bk, bval] of Object.entries(b.match)) {
9372
+ const result = interfaceListCovers(k, v, bk, bval);
9373
+ if (result === true) {
9374
+ crossCovered = true;
9375
+ break;
9376
+ }
9377
+ }
9378
+ if (!crossCovered)
9345
9379
  return false;
9346
9380
  }
9347
9381
  return true;
@@ -9518,6 +9552,7 @@ function grade2(score) {
9518
9552
  return "critical";
9519
9553
  }
9520
9554
  function auditFirewall(input) {
9555
+ _ifaceLists = input.interfaceLists;
9521
9556
  const findings = [];
9522
9557
  if (input.filter)
9523
9558
  findings.push(...auditFilter(input.filter));
@@ -9566,6 +9601,25 @@ async function fetchRules(path, ctx) {
9566
9601
  return [];
9567
9602
  return parseRecords(out).rows;
9568
9603
  }
9604
+ async function fetchInterfaceListMembers(ctx) {
9605
+ const out = await executeMikrotikCommand("/interface list member print detail", ctx);
9606
+ const members = new Map;
9607
+ if (looksLikeError(out) || isEmpty(out))
9608
+ return members;
9609
+ for (const row of parseRecords(out).rows) {
9610
+ const list = row.list;
9611
+ const iface = row.interface;
9612
+ if (!list || !iface)
9613
+ continue;
9614
+ let set = members.get(list);
9615
+ if (!set) {
9616
+ set = new Set;
9617
+ members.set(list, set);
9618
+ }
9619
+ set.add(iface);
9620
+ }
9621
+ return members;
9622
+ }
9569
9623
  var firewallAuditTools = [
9570
9624
  defineTool({
9571
9625
  name: "firewall_audit",
@@ -9580,10 +9634,16 @@ var firewallAuditTools = [
9580
9634
  async handler(a, ctx) {
9581
9635
  const device = resolveDeviceName(ctx.device);
9582
9636
  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 });
9637
+ const [filterRows, natRows, mangleRows, interfaceLists] = await Promise.all([
9638
+ fetchRules("/ip firewall filter", ctx),
9639
+ a.include_nat ? fetchRules("/ip firewall nat", ctx) : Promise.resolve(undefined),
9640
+ a.include_mangle ? fetchRules("/ip firewall mangle", ctx) : Promise.resolve(undefined),
9641
+ fetchInterfaceListMembers(ctx)
9642
+ ]);
9643
+ const filter = rulesFromRows(filterRows);
9644
+ const nat = natRows ? rulesFromRows(natRows) : undefined;
9645
+ const mangle = mangleRows ? rulesFromRows(mangleRows) : undefined;
9646
+ const report = auditFirewall({ filter, nat, mangle, interfaceLists });
9587
9647
  const structuredContent = {
9588
9648
  __mikrotikView: "firewall-audit",
9589
9649
  device,
@@ -10048,6 +10108,22 @@ Management access may be partially restricted \u2014 review immediately.`;
10048
10108
  // src/tools/firewall-filter.ts
10049
10109
  import { z as z28 } from "zod";
10050
10110
  var isDigits = (s) => /^\d+$/.test(s);
10111
+ async function resolveFilterRuleId(ruleId, ctx) {
10112
+ if (/^\d+$/.test(ruleId)) {
10113
+ const id = `*${ruleId}`;
10114
+ const byId = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
10115
+ if (byId.trim() !== "0")
10116
+ return id;
10117
+ const idsRaw = await executeMikrotikCommand(`:foreach i in=[/ip firewall filter find] do={:put $i}`, ctx);
10118
+ if (isEmpty(idsRaw))
10119
+ return null;
10120
+ const ids = idsRaw.trim().split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
10121
+ const pos = Number.parseInt(ruleId, 10);
10122
+ return pos >= 0 && pos < ids.length ? ids[pos] : null;
10123
+ }
10124
+ const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${ruleId}`, ctx);
10125
+ return count.trim() !== "0" ? ruleId : null;
10126
+ }
10051
10127
  async function updateFilterRule(a, ctx) {
10052
10128
  ctx.info(`Updating firewall filter rule: rule_id=${a.rule_id}`);
10053
10129
  const updates = [];
@@ -10116,7 +10192,9 @@ async function updateFilterRule(a, ctx) {
10116
10192
  }
10117
10193
  if (updates.length === 0)
10118
10194
  return "No updates specified.";
10119
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10195
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10196
+ if (!id)
10197
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10120
10198
  const cmd = `/ip firewall filter set ${id} ${updates.join(" ")}`;
10121
10199
  const result = await executeMikrotikCommand(cmd, ctx);
10122
10200
  if (looksLikeError(result))
@@ -10260,7 +10338,7 @@ ${details}`;
10260
10338
  filters.push("invalid=yes");
10261
10339
  if (a.dynamic_only)
10262
10340
  filters.push("dynamic=yes");
10263
- const result = await executeMikrotikCommand(`/ip firewall filter print${whereClause(filters)}`, ctx);
10341
+ const result = await executeMikrotikCommand(`/ip firewall filter print detail${whereClause(filters)}`, ctx);
10264
10342
  return isEmpty(result) ? "No firewall filter rules found matching the criteria." : `FIREWALL FILTER RULES:
10265
10343
 
10266
10344
  ${result}`;
@@ -10270,15 +10348,17 @@ ${result}`;
10270
10348
  name: "get_filter_rule",
10271
10349
  title: "Get Firewall Filter Rule",
10272
10350
  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".',
10351
+ 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
10352
  inputSchema: {
10275
- rule_id: z28.string().describe('Rule ID from list output e.g. "*1" or "0"')
10353
+ rule_id: z28.string().describe('Rule .id e.g. "*1F", or bare position number e.g. "3"')
10276
10354
  },
10277
10355
  async handler(a, ctx) {
10278
10356
  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;
10357
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10358
+ if (!id)
10359
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10280
10360
  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:
10361
+ return isEmpty(result) ? `Firewall filter rule '${a.rule_id}' not found.` : `FIREWALL FILTER RULE DETAILS:
10282
10362
 
10283
10363
  ${result}`;
10284
10364
  }
@@ -10353,14 +10433,13 @@ ${result}`;
10353
10433
  inputSchema: { rule_id: z28.string() },
10354
10434
  async handler(a, ctx) {
10355
10435
  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.`;
10436
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10437
+ if (!id)
10438
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10360
10439
  const result = await executeMikrotikCommand(`/ip firewall filter remove ${id}`, ctx);
10361
10440
  if (looksLikeError(result))
10362
10441
  return `Failed to remove firewall filter rule: ${result}`;
10363
- return `Firewall filter rule with ID '${a.rule_id}' removed successfully.`;
10442
+ return `Firewall filter rule '${a.rule_id}' (${id}) removed successfully.`;
10364
10443
  }
10365
10444
  }),
10366
10445
  defineTool({
@@ -10374,14 +10453,13 @@ ${result}`;
10374
10453
  },
10375
10454
  async handler(a, ctx) {
10376
10455
  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.`;
10456
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10457
+ if (!id)
10458
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10381
10459
  const result = await executeMikrotikCommand(`/ip firewall filter move ${id} destination=${a.destination}`, ctx);
10382
10460
  if (looksLikeError(result))
10383
10461
  return `Failed to move firewall filter rule: ${result}`;
10384
- return `Firewall filter rule with ID '${a.rule_id}' moved to position ${a.destination}.`;
10462
+ return `Firewall filter rule '${a.rule_id}' (${id}) moved to position ${a.destination}.`;
10385
10463
  }
10386
10464
  }),
10387
10465
  defineTool({
@@ -20657,13 +20735,27 @@ function analyzeRootCause(data) {
20657
20735
  }
20658
20736
  }
20659
20737
  if (downInterfaces.length === 0 && errorInterfaces.length === 0) {
20738
+ if (data.interfaces.length === 0) {
20739
+ evidence.push({
20740
+ dimension: "interfaces",
20741
+ severity: "warning",
20742
+ summary: "Interface data unavailable \u2014 the device returned no parseable interface records. " + "Run `/interface print detail` manually to verify."
20743
+ });
20744
+ } else {
20745
+ evidence.push({
20746
+ dimension: "interfaces",
20747
+ severity: "ok",
20748
+ summary: `All ${data.interfaces.length} interfaces healthy`
20749
+ });
20750
+ }
20751
+ }
20752
+ if (data.routeCount === 0) {
20660
20753
  evidence.push({
20661
- dimension: "interfaces",
20662
- severity: "ok",
20663
- summary: `All ${data.interfaces.length} interfaces healthy`
20754
+ dimension: "routing",
20755
+ severity: "warning",
20756
+ summary: "Route data unavailable \u2014 the device returned no parseable route records. " + "Run `/ip route print detail` manually to verify."
20664
20757
  });
20665
- }
20666
- if (!data.defaultRouteExists) {
20758
+ } else if (!data.defaultRouteExists) {
20667
20759
  evidence.push({
20668
20760
  dimension: "routing",
20669
20761
  severity: "critical",
@@ -20676,7 +20768,7 @@ function analyzeRootCause(data) {
20676
20768
  summary: `Default route present, ${data.routeCount} total routes`
20677
20769
  });
20678
20770
  }
20679
- const ospfDown = data.ospfNeighbors.filter((n) => n.state.toLowerCase() !== "full");
20771
+ const ospfDown = data.ospfNeighbors.filter((n) => (n.id || n.address) && n.state.toLowerCase() !== "full");
20680
20772
  for (const n of ospfDown) {
20681
20773
  evidence.push({
20682
20774
  dimension: "routing",
@@ -20685,7 +20777,7 @@ function analyzeRootCause(data) {
20685
20777
  reference: n.id
20686
20778
  });
20687
20779
  }
20688
- const bgpDown = data.bgpPeers.filter((p) => !p.state.toLowerCase().includes("established"));
20780
+ const bgpDown = data.bgpPeers.filter((p) => (p.id || p.address) && !p.state.toLowerCase().includes("established"));
20689
20781
  for (const p of bgpDown) {
20690
20782
  evidence.push({
20691
20783
  dimension: "routing",
@@ -20788,7 +20880,13 @@ function analyzeRootCause(data) {
20788
20880
  summary: "No DNS servers configured"
20789
20881
  });
20790
20882
  }
20791
- if (data.cpuLoad > 90) {
20883
+ if (data.cpuLoad === 0 && data.memoryUsedPct === 0 && !data.rosVersion && !data.uptime) {
20884
+ evidence.push({
20885
+ dimension: "resources",
20886
+ severity: "warning",
20887
+ 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."
20888
+ });
20889
+ } else if (data.cpuLoad > 90) {
20792
20890
  evidence.push({
20793
20891
  dimension: "resources",
20794
20892
  severity: "critical",
@@ -20807,24 +20905,26 @@ function analyzeRootCause(data) {
20807
20905
  summary: `CPU load: ${data.cpuLoad}%`
20808
20906
  });
20809
20907
  }
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
- });
20908
+ if (data.rosVersion || data.uptime || data.cpuLoad > 0 || data.memoryUsedPct > 0) {
20909
+ if (data.memoryUsedPct > 90) {
20910
+ evidence.push({
20911
+ dimension: "resources",
20912
+ severity: "critical",
20913
+ summary: `Memory critically low: ${data.memoryUsedPct}% used`
20914
+ });
20915
+ } else if (data.memoryUsedPct > 75) {
20916
+ evidence.push({
20917
+ dimension: "resources",
20918
+ severity: "warning",
20919
+ summary: `Memory pressure: ${data.memoryUsedPct}% used`
20920
+ });
20921
+ } else {
20922
+ evidence.push({
20923
+ dimension: "resources",
20924
+ severity: "ok",
20925
+ summary: `Memory: ${data.memoryUsedPct}% used`
20926
+ });
20927
+ }
20828
20928
  }
20829
20929
  const errorLogs = data.relevantLogs.filter((l) => l.topics.includes("error") || l.topics.includes("critical") || l.topics.includes("warning"));
20830
20930
  const firewallLogs = data.relevantLogs.filter((l) => l.topics.includes("firewall"));
@@ -20861,14 +20961,24 @@ function analyzeRootCause(data) {
20861
20961
  summary: "No concerning log entries in the last 10 minutes"
20862
20962
  });
20863
20963
  }
20964
+ const isServerBinding = (t) => /-(in|server)$/.test(t.type) || t.name.startsWith("<");
20864
20965
  const downTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled);
20865
20966
  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
- });
20967
+ if (isServerBinding(t)) {
20968
+ evidence.push({
20969
+ dimension: "vpn",
20970
+ severity: "info",
20971
+ summary: `Server binding ${t.name} (${t.type}) is idle \u2014 no active client session`,
20972
+ reference: t.name
20973
+ });
20974
+ } else {
20975
+ evidence.push({
20976
+ dimension: "vpn",
20977
+ severity: "critical",
20978
+ summary: `Tunnel ${t.name} (${t.type}) is down`,
20979
+ reference: t.name
20980
+ });
20981
+ }
20872
20982
  }
20873
20983
  if (data.tunnelInterfaces.length > 0 && downTunnels.length === 0) {
20874
20984
  evidence.push({
@@ -20878,6 +20988,19 @@ function analyzeRootCause(data) {
20878
20988
  });
20879
20989
  }
20880
20990
  correlateRootCauses(data, evidence, rootCauses);
20991
+ if (data.ping && data.ping.lossPct === 0) {
20992
+ const contradicted = new Set([
20993
+ "Missing default route",
20994
+ "Missing source NAT / masquerade",
20995
+ "Interface link failure"
20996
+ ]);
20997
+ for (const rc of rootCauses) {
20998
+ if (contradicted.has(rc.cause) && rc.confidence !== "low") {
20999
+ rc.confidence = "low";
21000
+ rc.explanation += " (Note: ping to the target succeeded with 0% loss, which contradicts this hypothesis.)";
21001
+ }
21002
+ }
21003
+ }
20881
21004
  rootCauses.sort((a, b) => CONFIDENCE_RANK[a.confidence] - CONFIDENCE_RANK[b.confidence]);
20882
21005
  const dimensionSummary = ALL_DIMENSIONS.map((dim) => {
20883
21006
  const dimEvidence = evidence.filter((e) => e.dimension === dim);
@@ -21016,14 +21139,15 @@ function correlateRootCauses(data, evidence, causes) {
21016
21139
  });
21017
21140
  }
21018
21141
  }
21019
- const downTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled);
21020
- if (downTunnels.length > 0) {
21142
+ const isServerTunnel = (t) => /-(in|server)$/.test(t.type) || t.name.startsWith("<");
21143
+ const realDownTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled && !isServerTunnel(t));
21144
+ if (realDownTunnels.length > 0) {
21021
21145
  causes.push({
21022
21146
  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.",
21147
+ 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
21148
  confidence: "high",
21025
21149
  evidence: evidence.filter((e) => e.dimension === "vpn" && e.severity === "critical"),
21026
- fixes: downTunnels.map((t) => `/interface enable [find name="${t.name}"]`),
21150
+ fixes: realDownTunnels.map((t) => `/interface enable [find name="${t.name}"]`),
21027
21151
  dimensions: ["vpn"]
21028
21152
  });
21029
21153
  }
@@ -21252,7 +21376,7 @@ async function collectRoutes(ctx) {
21252
21376
  }
21253
21377
  async function collectOspfNeighbors(ctx) {
21254
21378
  const raw = await safe("/routing ospf neighbor print detail", ctx);
21255
- if (!raw)
21379
+ if (!raw || isEmpty(raw))
21256
21380
  return [];
21257
21381
  return parseRecords(raw).rows.map((r) => ({
21258
21382
  id: r["neighbor-id"] ?? r.router ?? "",
@@ -21260,13 +21384,13 @@ async function collectOspfNeighbors(ctx) {
21260
21384
  state: r.state ?? "",
21261
21385
  interface: r.interface ?? "",
21262
21386
  uptime: r.uptime
21263
- }));
21387
+ })).filter((n) => n.id || n.address);
21264
21388
  }
21265
21389
  async function collectBgpPeers(ctx) {
21266
21390
  const raw = await safe("/routing bgp session print detail", ctx);
21267
- if (!raw) {
21391
+ if (!raw || isEmpty(raw)) {
21268
21392
  const raw2 = await safe("/routing bgp peer print detail", ctx);
21269
- if (!raw2)
21393
+ if (!raw2 || isEmpty(raw2))
21270
21394
  return [];
21271
21395
  return parseRecords(raw2).rows.map((r) => ({
21272
21396
  id: r.name ?? "",
@@ -21274,7 +21398,7 @@ async function collectBgpPeers(ctx) {
21274
21398
  state: r.state ?? "",
21275
21399
  interface: r.interface ?? "",
21276
21400
  uptime: r.uptime
21277
- }));
21401
+ })).filter((p) => p.id || p.address);
21278
21402
  }
21279
21403
  return parseRecords(raw).rows.map((r) => ({
21280
21404
  id: r.name ?? r["remote.address"] ?? "",
@@ -21282,7 +21406,7 @@ async function collectBgpPeers(ctx) {
21282
21406
  state: r.state ?? r.established ?? "",
21283
21407
  interface: r.interface ?? "",
21284
21408
  uptime: r.uptime
21285
- }));
21409
+ })).filter((p) => p.id || p.address);
21286
21410
  }
21287
21411
  async function collectFirewallRules(target, ctx) {
21288
21412
  const raw = await safe("/ip firewall filter print detail", ctx);
@@ -21382,7 +21506,7 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21382
21506
  arpEntries,
21383
21507
  dhcpLeases,
21384
21508
  dnsResult,
21385
- dnsSettingsRaw,
21509
+ dnsSettingsParts,
21386
21510
  resourceRaw,
21387
21511
  logs,
21388
21512
  tunnels
@@ -21398,13 +21522,17 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21398
21522
  dims.has("arp_dhcp") ? collectArp(ctx) : Promise.resolve([]),
21399
21523
  dims.has("arp_dhcp") ? collectDhcpLeases(ctx) : Promise.resolve([]),
21400
21524
  dims.has("dns") && !isIpLike(target) ? safe(`[:resolve ${quoteValue(target)}]`, ctx) : Promise.resolve(undefined),
21401
- dims.has("dns") ? safe("/ip dns print", ctx) : Promise.resolve(""),
21525
+ dims.has("dns") ? Promise.all([
21526
+ safe(":put [/ip dns get servers]", ctx),
21527
+ safe(":put [/ip dns get dynamic-servers]", ctx),
21528
+ safe(":put [/ip dns get allow-remote-requests]", ctx)
21529
+ ]) : Promise.resolve(["", "", ""]),
21402
21530
  dims.has("resources") ? safe("/system resource print", ctx) : Promise.resolve(""),
21403
21531
  dims.has("logs") ? collectLogs(target, ctx) : Promise.resolve([]),
21404
21532
  dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([])
21405
21533
  ]);
21406
21534
  const ping = pingResult ? parsePingSummary(pingResult) ?? undefined : undefined;
21407
- const dnsKv = parseKeyValues(dnsSettingsRaw);
21535
+ const [dnsServersRaw, dnsDynamicRaw, dnsAllowRemoteRaw] = dnsSettingsParts;
21408
21536
  const resKv = parseKeyValues(resourceRaw);
21409
21537
  const totalMem = parseSize(resKv["total-memory"]) ?? 0;
21410
21538
  const freeMem = parseSize(resKv["free-memory"]) ?? 0;
@@ -21426,8 +21554,8 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21426
21554
  arpEntries,
21427
21555
  dhcpLeases,
21428
21556
  dnsResolveResult: dnsResult ?? undefined,
21429
- dnsServers: [dnsKv.servers, dnsKv["dynamic-servers"]].filter(Boolean).join(",") || "",
21430
- dnsAllowRemote: (dnsKv["allow-remote-requests"] ?? "").toLowerCase() === "yes",
21557
+ dnsServers: [dnsServersRaw.trim(), dnsDynamicRaw.trim()].filter(Boolean).join(",") || "",
21558
+ dnsAllowRemote: dnsAllowRemoteRaw.trim().toLowerCase() === "yes" || dnsAllowRemoteRaw.trim().toLowerCase() === "true",
21431
21559
  cpuLoad: parsePercent(resKv["cpu-load"]) ?? 0,
21432
21560
  memoryUsedPct: memUsedPct,
21433
21561
  uptime: resKv.uptime ?? "",
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./cli-njttbdf8.js";
7
+ } from "./cli-44v6pb38.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -1743,7 +1743,7 @@ function parseKvTokens(chunk) {
1743
1743
  return out;
1744
1744
  }
1745
1745
  var INDEX_LINE = /^\s*(\d+)\s+(.*)$/;
1746
- var LEADING_FLAGS = /^([A-Z]+)(?=\s|$)/;
1746
+ var LEADING_FLAGS = /^([A-Z][A-Za-z]*)(?=\s|$)/;
1747
1747
  function unionColumns(rows) {
1748
1748
  const seen = new Set;
1749
1749
  const cols = [];
@@ -8243,7 +8243,7 @@ var cache = null;
8243
8243
  async function gateway() {
8244
8244
  if (cache)
8245
8245
  return cache;
8246
- const { moduleCatalog } = await import("./library-84y0nvhr.js");
8246
+ const { moduleCatalog } = await import("./library-cs13jqp9.js");
8247
8247
  const forIndex = [];
8248
8248
  const byName = new Map;
8249
8249
  for (const mod of moduleCatalog) {
@@ -9268,10 +9268,44 @@ function covers(key, aVal, bVal) {
9268
9268
  return cidrContains(aVal, bVal);
9269
9269
  return false;
9270
9270
  }
9271
+ var INTERFACE_LIST_PAIRS = [
9272
+ ["in-interface-list", "in-interface"],
9273
+ ["out-interface-list", "out-interface"]
9274
+ ];
9275
+ var _ifaceLists;
9276
+ function interfaceListCovers(aKey, aVal, bKey, bVal) {
9277
+ if (!_ifaceLists)
9278
+ return;
9279
+ for (const [listKey, ifaceKey] of INTERFACE_LIST_PAIRS) {
9280
+ if (aKey === listKey && bKey === ifaceKey) {
9281
+ const negated = aVal.startsWith("!");
9282
+ const listName = negated ? aVal.slice(1) : aVal;
9283
+ const members = _ifaceLists.get(listName);
9284
+ if (!members)
9285
+ return;
9286
+ const isMember = members.has(bVal);
9287
+ return negated ? !isMember : isMember;
9288
+ }
9289
+ }
9290
+ return;
9291
+ }
9271
9292
  function aCoversB(a, b) {
9272
9293
  for (const [k, v] of Object.entries(a.match)) {
9273
9294
  const bv = b.match[k];
9274
- if (bv === undefined || !covers(k, v, bv))
9295
+ if (bv !== undefined) {
9296
+ if (!covers(k, v, bv))
9297
+ return false;
9298
+ continue;
9299
+ }
9300
+ let crossCovered = false;
9301
+ for (const [bk, bval] of Object.entries(b.match)) {
9302
+ const result = interfaceListCovers(k, v, bk, bval);
9303
+ if (result === true) {
9304
+ crossCovered = true;
9305
+ break;
9306
+ }
9307
+ }
9308
+ if (!crossCovered)
9275
9309
  return false;
9276
9310
  }
9277
9311
  return true;
@@ -9448,6 +9482,7 @@ function grade2(score) {
9448
9482
  return "critical";
9449
9483
  }
9450
9484
  function auditFirewall(input) {
9485
+ _ifaceLists = input.interfaceLists;
9451
9486
  const findings = [];
9452
9487
  if (input.filter)
9453
9488
  findings.push(...auditFilter(input.filter));
@@ -9496,6 +9531,25 @@ async function fetchRules(path, ctx) {
9496
9531
  return [];
9497
9532
  return parseRecords(out).rows;
9498
9533
  }
9534
+ async function fetchInterfaceListMembers(ctx) {
9535
+ const out = await executeMikrotikCommand("/interface list member print detail", ctx);
9536
+ const members = new Map;
9537
+ if (looksLikeError(out) || isEmpty(out))
9538
+ return members;
9539
+ for (const row of parseRecords(out).rows) {
9540
+ const list = row.list;
9541
+ const iface = row.interface;
9542
+ if (!list || !iface)
9543
+ continue;
9544
+ let set = members.get(list);
9545
+ if (!set) {
9546
+ set = new Set;
9547
+ members.set(list, set);
9548
+ }
9549
+ set.add(iface);
9550
+ }
9551
+ return members;
9552
+ }
9499
9553
  var firewallAuditTools = [
9500
9554
  defineTool({
9501
9555
  name: "firewall_audit",
@@ -9510,10 +9564,16 @@ var firewallAuditTools = [
9510
9564
  async handler(a, ctx) {
9511
9565
  const device = resolveDeviceName(ctx.device);
9512
9566
  ctx.info(`Auditing firewall for '${device}'`);
9513
- const filter = rulesFromRows(await fetchRules("/ip firewall filter", ctx));
9514
- const nat = a.include_nat ? rulesFromRows(await fetchRules("/ip firewall nat", ctx)) : undefined;
9515
- const mangle = a.include_mangle ? rulesFromRows(await fetchRules("/ip firewall mangle", ctx)) : undefined;
9516
- const report = auditFirewall({ filter, nat, mangle });
9567
+ const [filterRows, natRows, mangleRows, interfaceLists] = await Promise.all([
9568
+ fetchRules("/ip firewall filter", ctx),
9569
+ a.include_nat ? fetchRules("/ip firewall nat", ctx) : Promise.resolve(undefined),
9570
+ a.include_mangle ? fetchRules("/ip firewall mangle", ctx) : Promise.resolve(undefined),
9571
+ fetchInterfaceListMembers(ctx)
9572
+ ]);
9573
+ const filter = rulesFromRows(filterRows);
9574
+ const nat = natRows ? rulesFromRows(natRows) : undefined;
9575
+ const mangle = mangleRows ? rulesFromRows(mangleRows) : undefined;
9576
+ const report = auditFirewall({ filter, nat, mangle, interfaceLists });
9517
9577
  const structuredContent = {
9518
9578
  __mikrotikView: "firewall-audit",
9519
9579
  device,
@@ -9978,6 +10038,22 @@ Management access may be partially restricted \u2014 review immediately.`;
9978
10038
  // src/tools/firewall-filter.ts
9979
10039
  import { z as z28 } from "zod";
9980
10040
  var isDigits = (s) => /^\d+$/.test(s);
10041
+ async function resolveFilterRuleId(ruleId, ctx) {
10042
+ if (/^\d+$/.test(ruleId)) {
10043
+ const id = `*${ruleId}`;
10044
+ const byId = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
10045
+ if (byId.trim() !== "0")
10046
+ return id;
10047
+ const idsRaw = await executeMikrotikCommand(`:foreach i in=[/ip firewall filter find] do={:put $i}`, ctx);
10048
+ if (isEmpty(idsRaw))
10049
+ return null;
10050
+ const ids = idsRaw.trim().split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
10051
+ const pos = Number.parseInt(ruleId, 10);
10052
+ return pos >= 0 && pos < ids.length ? ids[pos] : null;
10053
+ }
10054
+ const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${ruleId}`, ctx);
10055
+ return count.trim() !== "0" ? ruleId : null;
10056
+ }
9981
10057
  async function updateFilterRule(a, ctx) {
9982
10058
  ctx.info(`Updating firewall filter rule: rule_id=${a.rule_id}`);
9983
10059
  const updates = [];
@@ -10046,7 +10122,9 @@ async function updateFilterRule(a, ctx) {
10046
10122
  }
10047
10123
  if (updates.length === 0)
10048
10124
  return "No updates specified.";
10049
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10125
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10126
+ if (!id)
10127
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10050
10128
  const cmd = `/ip firewall filter set ${id} ${updates.join(" ")}`;
10051
10129
  const result = await executeMikrotikCommand(cmd, ctx);
10052
10130
  if (looksLikeError(result))
@@ -10190,7 +10268,7 @@ ${details}`;
10190
10268
  filters.push("invalid=yes");
10191
10269
  if (a.dynamic_only)
10192
10270
  filters.push("dynamic=yes");
10193
- const result = await executeMikrotikCommand(`/ip firewall filter print${whereClause(filters)}`, ctx);
10271
+ const result = await executeMikrotikCommand(`/ip firewall filter print detail${whereClause(filters)}`, ctx);
10194
10272
  return isEmpty(result) ? "No firewall filter rules found matching the criteria." : `FIREWALL FILTER RULES:
10195
10273
 
10196
10274
  ${result}`;
@@ -10200,15 +10278,17 @@ ${result}`;
10200
10278
  name: "get_filter_rule",
10201
10279
  title: "Get Firewall Filter Rule",
10202
10280
  annotations: READ,
10203
- 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".',
10281
+ 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.",
10204
10282
  inputSchema: {
10205
- rule_id: z28.string().describe('Rule ID from list output e.g. "*1" or "0"')
10283
+ rule_id: z28.string().describe('Rule .id e.g. "*1F", or bare position number e.g. "3"')
10206
10284
  },
10207
10285
  async handler(a, ctx) {
10208
10286
  ctx.info(`Getting firewall filter rule details: rule_id=${a.rule_id}`);
10209
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10287
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10288
+ if (!id)
10289
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10210
10290
  const result = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${id}`, ctx);
10211
- return isEmpty(result) ? `Firewall filter rule with ID '${a.rule_id}' not found.` : `FIREWALL FILTER RULE DETAILS:
10291
+ return isEmpty(result) ? `Firewall filter rule '${a.rule_id}' not found.` : `FIREWALL FILTER RULE DETAILS:
10212
10292
 
10213
10293
  ${result}`;
10214
10294
  }
@@ -10283,14 +10363,13 @@ ${result}`;
10283
10363
  inputSchema: { rule_id: z28.string() },
10284
10364
  async handler(a, ctx) {
10285
10365
  ctx.info(`Removing firewall filter rule: rule_id=${a.rule_id}`);
10286
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10287
- const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
10288
- if (count.trim() === "0")
10289
- return `Firewall filter rule with ID '${a.rule_id}' not found.`;
10366
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10367
+ if (!id)
10368
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10290
10369
  const result = await executeMikrotikCommand(`/ip firewall filter remove ${id}`, ctx);
10291
10370
  if (looksLikeError(result))
10292
10371
  return `Failed to remove firewall filter rule: ${result}`;
10293
- return `Firewall filter rule with ID '${a.rule_id}' removed successfully.`;
10372
+ return `Firewall filter rule '${a.rule_id}' (${id}) removed successfully.`;
10294
10373
  }
10295
10374
  }),
10296
10375
  defineTool({
@@ -10304,14 +10383,13 @@ ${result}`;
10304
10383
  },
10305
10384
  async handler(a, ctx) {
10306
10385
  ctx.info(`Moving firewall filter rule: rule_id=${a.rule_id} to position ${a.destination}`);
10307
- const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
10308
- const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
10309
- if (count.trim() === "0")
10310
- return `Firewall filter rule with ID '${a.rule_id}' not found.`;
10386
+ const id = await resolveFilterRuleId(a.rule_id, ctx);
10387
+ if (!id)
10388
+ return `Firewall filter rule '${a.rule_id}' not found.`;
10311
10389
  const result = await executeMikrotikCommand(`/ip firewall filter move ${id} destination=${a.destination}`, ctx);
10312
10390
  if (looksLikeError(result))
10313
10391
  return `Failed to move firewall filter rule: ${result}`;
10314
- return `Firewall filter rule with ID '${a.rule_id}' moved to position ${a.destination}.`;
10392
+ return `Firewall filter rule '${a.rule_id}' (${id}) moved to position ${a.destination}.`;
10315
10393
  }
10316
10394
  }),
10317
10395
  defineTool({
@@ -20587,13 +20665,27 @@ function analyzeRootCause(data) {
20587
20665
  }
20588
20666
  }
20589
20667
  if (downInterfaces.length === 0 && errorInterfaces.length === 0) {
20668
+ if (data.interfaces.length === 0) {
20669
+ evidence.push({
20670
+ dimension: "interfaces",
20671
+ severity: "warning",
20672
+ summary: "Interface data unavailable \u2014 the device returned no parseable interface records. " + "Run `/interface print detail` manually to verify."
20673
+ });
20674
+ } else {
20675
+ evidence.push({
20676
+ dimension: "interfaces",
20677
+ severity: "ok",
20678
+ summary: `All ${data.interfaces.length} interfaces healthy`
20679
+ });
20680
+ }
20681
+ }
20682
+ if (data.routeCount === 0) {
20590
20683
  evidence.push({
20591
- dimension: "interfaces",
20592
- severity: "ok",
20593
- summary: `All ${data.interfaces.length} interfaces healthy`
20684
+ dimension: "routing",
20685
+ severity: "warning",
20686
+ summary: "Route data unavailable \u2014 the device returned no parseable route records. " + "Run `/ip route print detail` manually to verify."
20594
20687
  });
20595
- }
20596
- if (!data.defaultRouteExists) {
20688
+ } else if (!data.defaultRouteExists) {
20597
20689
  evidence.push({
20598
20690
  dimension: "routing",
20599
20691
  severity: "critical",
@@ -20606,7 +20698,7 @@ function analyzeRootCause(data) {
20606
20698
  summary: `Default route present, ${data.routeCount} total routes`
20607
20699
  });
20608
20700
  }
20609
- const ospfDown = data.ospfNeighbors.filter((n) => n.state.toLowerCase() !== "full");
20701
+ const ospfDown = data.ospfNeighbors.filter((n) => (n.id || n.address) && n.state.toLowerCase() !== "full");
20610
20702
  for (const n of ospfDown) {
20611
20703
  evidence.push({
20612
20704
  dimension: "routing",
@@ -20615,7 +20707,7 @@ function analyzeRootCause(data) {
20615
20707
  reference: n.id
20616
20708
  });
20617
20709
  }
20618
- const bgpDown = data.bgpPeers.filter((p) => !p.state.toLowerCase().includes("established"));
20710
+ const bgpDown = data.bgpPeers.filter((p) => (p.id || p.address) && !p.state.toLowerCase().includes("established"));
20619
20711
  for (const p of bgpDown) {
20620
20712
  evidence.push({
20621
20713
  dimension: "routing",
@@ -20718,7 +20810,13 @@ function analyzeRootCause(data) {
20718
20810
  summary: "No DNS servers configured"
20719
20811
  });
20720
20812
  }
20721
- if (data.cpuLoad > 90) {
20813
+ if (data.cpuLoad === 0 && data.memoryUsedPct === 0 && !data.rosVersion && !data.uptime) {
20814
+ evidence.push({
20815
+ dimension: "resources",
20816
+ severity: "warning",
20817
+ 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."
20818
+ });
20819
+ } else if (data.cpuLoad > 90) {
20722
20820
  evidence.push({
20723
20821
  dimension: "resources",
20724
20822
  severity: "critical",
@@ -20737,24 +20835,26 @@ function analyzeRootCause(data) {
20737
20835
  summary: `CPU load: ${data.cpuLoad}%`
20738
20836
  });
20739
20837
  }
20740
- if (data.memoryUsedPct > 90) {
20741
- evidence.push({
20742
- dimension: "resources",
20743
- severity: "critical",
20744
- summary: `Memory critically low: ${data.memoryUsedPct}% used`
20745
- });
20746
- } else if (data.memoryUsedPct > 75) {
20747
- evidence.push({
20748
- dimension: "resources",
20749
- severity: "warning",
20750
- summary: `Memory pressure: ${data.memoryUsedPct}% used`
20751
- });
20752
- } else {
20753
- evidence.push({
20754
- dimension: "resources",
20755
- severity: "ok",
20756
- summary: `Memory: ${data.memoryUsedPct}% used`
20757
- });
20838
+ if (data.rosVersion || data.uptime || data.cpuLoad > 0 || data.memoryUsedPct > 0) {
20839
+ if (data.memoryUsedPct > 90) {
20840
+ evidence.push({
20841
+ dimension: "resources",
20842
+ severity: "critical",
20843
+ summary: `Memory critically low: ${data.memoryUsedPct}% used`
20844
+ });
20845
+ } else if (data.memoryUsedPct > 75) {
20846
+ evidence.push({
20847
+ dimension: "resources",
20848
+ severity: "warning",
20849
+ summary: `Memory pressure: ${data.memoryUsedPct}% used`
20850
+ });
20851
+ } else {
20852
+ evidence.push({
20853
+ dimension: "resources",
20854
+ severity: "ok",
20855
+ summary: `Memory: ${data.memoryUsedPct}% used`
20856
+ });
20857
+ }
20758
20858
  }
20759
20859
  const errorLogs = data.relevantLogs.filter((l) => l.topics.includes("error") || l.topics.includes("critical") || l.topics.includes("warning"));
20760
20860
  const firewallLogs = data.relevantLogs.filter((l) => l.topics.includes("firewall"));
@@ -20791,14 +20891,24 @@ function analyzeRootCause(data) {
20791
20891
  summary: "No concerning log entries in the last 10 minutes"
20792
20892
  });
20793
20893
  }
20894
+ const isServerBinding = (t) => /-(in|server)$/.test(t.type) || t.name.startsWith("<");
20794
20895
  const downTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled);
20795
20896
  for (const t of downTunnels) {
20796
- evidence.push({
20797
- dimension: "vpn",
20798
- severity: "critical",
20799
- summary: `Tunnel ${t.name} (${t.type}) is down`,
20800
- reference: t.name
20801
- });
20897
+ if (isServerBinding(t)) {
20898
+ evidence.push({
20899
+ dimension: "vpn",
20900
+ severity: "info",
20901
+ summary: `Server binding ${t.name} (${t.type}) is idle \u2014 no active client session`,
20902
+ reference: t.name
20903
+ });
20904
+ } else {
20905
+ evidence.push({
20906
+ dimension: "vpn",
20907
+ severity: "critical",
20908
+ summary: `Tunnel ${t.name} (${t.type}) is down`,
20909
+ reference: t.name
20910
+ });
20911
+ }
20802
20912
  }
20803
20913
  if (data.tunnelInterfaces.length > 0 && downTunnels.length === 0) {
20804
20914
  evidence.push({
@@ -20808,6 +20918,19 @@ function analyzeRootCause(data) {
20808
20918
  });
20809
20919
  }
20810
20920
  correlateRootCauses(data, evidence, rootCauses);
20921
+ if (data.ping && data.ping.lossPct === 0) {
20922
+ const contradicted = new Set([
20923
+ "Missing default route",
20924
+ "Missing source NAT / masquerade",
20925
+ "Interface link failure"
20926
+ ]);
20927
+ for (const rc of rootCauses) {
20928
+ if (contradicted.has(rc.cause) && rc.confidence !== "low") {
20929
+ rc.confidence = "low";
20930
+ rc.explanation += " (Note: ping to the target succeeded with 0% loss, which contradicts this hypothesis.)";
20931
+ }
20932
+ }
20933
+ }
20811
20934
  rootCauses.sort((a, b) => CONFIDENCE_RANK[a.confidence] - CONFIDENCE_RANK[b.confidence]);
20812
20935
  const dimensionSummary = ALL_DIMENSIONS.map((dim) => {
20813
20936
  const dimEvidence = evidence.filter((e) => e.dimension === dim);
@@ -20946,14 +21069,15 @@ function correlateRootCauses(data, evidence, causes) {
20946
21069
  });
20947
21070
  }
20948
21071
  }
20949
- const downTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled);
20950
- if (downTunnels.length > 0) {
21072
+ const isServerTunnel = (t) => /-(in|server)$/.test(t.type) || t.name.startsWith("<");
21073
+ const realDownTunnels = data.tunnelInterfaces.filter((t) => !t.running && !t.disabled && !isServerTunnel(t));
21074
+ if (realDownTunnels.length > 0) {
20951
21075
  causes.push({
20952
21076
  cause: "VPN/tunnel interface down",
20953
- 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.",
21077
+ 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.",
20954
21078
  confidence: "high",
20955
21079
  evidence: evidence.filter((e) => e.dimension === "vpn" && e.severity === "critical"),
20956
- fixes: downTunnels.map((t) => `/interface enable [find name="${t.name}"]`),
21080
+ fixes: realDownTunnels.map((t) => `/interface enable [find name="${t.name}"]`),
20957
21081
  dimensions: ["vpn"]
20958
21082
  });
20959
21083
  }
@@ -21182,7 +21306,7 @@ async function collectRoutes(ctx) {
21182
21306
  }
21183
21307
  async function collectOspfNeighbors(ctx) {
21184
21308
  const raw = await safe("/routing ospf neighbor print detail", ctx);
21185
- if (!raw)
21309
+ if (!raw || isEmpty(raw))
21186
21310
  return [];
21187
21311
  return parseRecords(raw).rows.map((r) => ({
21188
21312
  id: r["neighbor-id"] ?? r.router ?? "",
@@ -21190,13 +21314,13 @@ async function collectOspfNeighbors(ctx) {
21190
21314
  state: r.state ?? "",
21191
21315
  interface: r.interface ?? "",
21192
21316
  uptime: r.uptime
21193
- }));
21317
+ })).filter((n) => n.id || n.address);
21194
21318
  }
21195
21319
  async function collectBgpPeers(ctx) {
21196
21320
  const raw = await safe("/routing bgp session print detail", ctx);
21197
- if (!raw) {
21321
+ if (!raw || isEmpty(raw)) {
21198
21322
  const raw2 = await safe("/routing bgp peer print detail", ctx);
21199
- if (!raw2)
21323
+ if (!raw2 || isEmpty(raw2))
21200
21324
  return [];
21201
21325
  return parseRecords(raw2).rows.map((r) => ({
21202
21326
  id: r.name ?? "",
@@ -21204,7 +21328,7 @@ async function collectBgpPeers(ctx) {
21204
21328
  state: r.state ?? "",
21205
21329
  interface: r.interface ?? "",
21206
21330
  uptime: r.uptime
21207
- }));
21331
+ })).filter((p) => p.id || p.address);
21208
21332
  }
21209
21333
  return parseRecords(raw).rows.map((r) => ({
21210
21334
  id: r.name ?? r["remote.address"] ?? "",
@@ -21212,7 +21336,7 @@ async function collectBgpPeers(ctx) {
21212
21336
  state: r.state ?? r.established ?? "",
21213
21337
  interface: r.interface ?? "",
21214
21338
  uptime: r.uptime
21215
- }));
21339
+ })).filter((p) => p.id || p.address);
21216
21340
  }
21217
21341
  async function collectFirewallRules(target, ctx) {
21218
21342
  const raw = await safe("/ip firewall filter print detail", ctx);
@@ -21312,7 +21436,7 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21312
21436
  arpEntries,
21313
21437
  dhcpLeases,
21314
21438
  dnsResult,
21315
- dnsSettingsRaw,
21439
+ dnsSettingsParts,
21316
21440
  resourceRaw,
21317
21441
  logs,
21318
21442
  tunnels
@@ -21328,13 +21452,17 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21328
21452
  dims.has("arp_dhcp") ? collectArp(ctx) : Promise.resolve([]),
21329
21453
  dims.has("arp_dhcp") ? collectDhcpLeases(ctx) : Promise.resolve([]),
21330
21454
  dims.has("dns") && !isIpLike(target) ? safe(`[:resolve ${quoteValue(target)}]`, ctx) : Promise.resolve(undefined),
21331
- dims.has("dns") ? safe("/ip dns print", ctx) : Promise.resolve(""),
21455
+ dims.has("dns") ? Promise.all([
21456
+ safe(":put [/ip dns get servers]", ctx),
21457
+ safe(":put [/ip dns get dynamic-servers]", ctx),
21458
+ safe(":put [/ip dns get allow-remote-requests]", ctx)
21459
+ ]) : Promise.resolve(["", "", ""]),
21332
21460
  dims.has("resources") ? safe("/system resource print", ctx) : Promise.resolve(""),
21333
21461
  dims.has("logs") ? collectLogs(target, ctx) : Promise.resolve([]),
21334
21462
  dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([])
21335
21463
  ]);
21336
21464
  const ping = pingResult ? parsePingSummary(pingResult) ?? undefined : undefined;
21337
- const dnsKv = parseKeyValues(dnsSettingsRaw);
21465
+ const [dnsServersRaw, dnsDynamicRaw, dnsAllowRemoteRaw] = dnsSettingsParts;
21338
21466
  const resKv = parseKeyValues(resourceRaw);
21339
21467
  const totalMem = parseSize(resKv["total-memory"]) ?? 0;
21340
21468
  const freeMem = parseSize(resKv["free-memory"]) ?? 0;
@@ -21356,8 +21484,8 @@ async function collectDiagnosticData(target, ctx, dimensions) {
21356
21484
  arpEntries,
21357
21485
  dhcpLeases,
21358
21486
  dnsResolveResult: dnsResult ?? undefined,
21359
- dnsServers: [dnsKv.servers, dnsKv["dynamic-servers"]].filter(Boolean).join(",") || "",
21360
- dnsAllowRemote: (dnsKv["allow-remote-requests"] ?? "").toLowerCase() === "yes",
21487
+ dnsServers: [dnsServersRaw.trim(), dnsDynamicRaw.trim()].filter(Boolean).join(",") || "",
21488
+ dnsAllowRemote: dnsAllowRemoteRaw.trim().toLowerCase() === "yes" || dnsAllowRemoteRaw.trim().toLowerCase() === "true",
21361
21489
  cpuLoad: parsePercent(resKv["cpu-load"]) ?? 0,
21362
21490
  memoryUsedPct: memUsedPct,
21363
21491
  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-83q9t27c.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usex/mikrotik-mcp",
3
- "version": "3.54.0",
3
+ "version": "3.55.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",