@usex/mikrotik-mcp 5.2.0 → 5.3.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
@@ -230,7 +230,7 @@ import {
230
230
  validatePolicyText,
231
231
  worstSeverity,
232
232
  writeBackup
233
- } from "./shared/cli-mqb3rqfy.js";
233
+ } from "./shared/cli-54yv0kd3.js";
234
234
 
235
235
  // src/cli.ts
236
236
  import { existsSync as existsSync2 } from "fs";
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  selectToolModules,
30
30
  setConfig,
31
31
  updateSummaryLine
32
- } from "./shared/library-tq8phch8.js";
32
+ } from "./shared/library-s7s3s14v.js";
33
33
  // src/server.ts
34
34
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
35
35
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -10832,7 +10832,7 @@ var cache2 = null;
10832
10832
  async function gateway() {
10833
10833
  if (cache2)
10834
10834
  return cache2;
10835
- const { moduleCatalog } = await import("./cli-s9fc4c7j.js");
10835
+ const { moduleCatalog } = await import("./cli-f80vccvs.js");
10836
10836
  const forIndex = [];
10837
10837
  const byName = new Map;
10838
10838
  for (const mod of moduleCatalog) {
@@ -28428,6 +28428,8 @@ function analyzeRootCause(data) {
28428
28428
  summary: `All ${data.tunnelInterfaces.length} tunnel(s) running`
28429
28429
  });
28430
28430
  }
28431
+ const mtuFindings = analyzeTunnelMtu(data);
28432
+ evidence.push(...mtuFindings.evidence);
28431
28433
  correlateRootCauses(data, evidence, rootCauses);
28432
28434
  if (data.ping && data.ping.lossPct === 0) {
28433
28435
  const contradicted = new Set([
@@ -28462,6 +28464,105 @@ function analyzeRootCause(data) {
28462
28464
  dimensionSummary
28463
28465
  };
28464
28466
  }
28467
+ var TUNNEL_OVERHEAD = [
28468
+ { match: "gre", bytes: 24, label: "GRE", path: "/interface gre" },
28469
+ { match: "ipip", bytes: 20, label: "IPIP", path: "/interface ipip" },
28470
+ { match: "eoip", bytes: 42, label: "EoIP", path: "/interface eoip" },
28471
+ { match: "vxlan", bytes: 50, label: "VXLAN", path: "/interface vxlan" },
28472
+ { match: "wg", bytes: 60, label: "WireGuard", path: "/interface wireguard" },
28473
+ { match: "l2tp", bytes: 100, label: "L2TP" },
28474
+ { match: "pptp", bytes: 100, label: "PPTP" },
28475
+ { match: "sstp", bytes: 100, label: "SSTP" },
28476
+ { match: "ovpn", bytes: 100, label: "OpenVPN" }
28477
+ ];
28478
+ var PPP_FAMILY = /l2tp|pptp|sstp|ovpn|pppoe/;
28479
+ function tunnelOverhead(type) {
28480
+ const t = type.toLowerCase();
28481
+ return TUNNEL_OVERHEAD.find((o) => t.includes(o.match));
28482
+ }
28483
+ function analyzeTunnelMtu(data) {
28484
+ const evidence = [];
28485
+ const unclamped = [];
28486
+ const oversizedMtu = [];
28487
+ const tunnels = data.tunnelInterfaces.filter((t) => !t.disabled);
28488
+ const clampRules = data.mangleRules.filter((r) => r.action === "change-mss" && !r.disabled);
28489
+ const globalClamp = clampRules.some((r) => r.chain === "forward" || r.chain === "postrouting" || r.chain === "output");
28490
+ const strandedClamp = clampRules.length > 0 && !globalClamp;
28491
+ const pppoeWan = data.interfaces.some((i) => i.type.toLowerCase().includes("pppoe"));
28492
+ const pathBudget = pppoeWan ? 1492 : 1500;
28493
+ const pppClampedEverywhere = data.pppProfilesMissingMssClamp.length === 0;
28494
+ for (const t of tunnels) {
28495
+ const overhead = tunnelOverhead(t.type);
28496
+ if (!overhead)
28497
+ continue;
28498
+ const effective = t.actualMtu ?? t.mtu;
28499
+ const budget = pathBudget - overhead.bytes;
28500
+ if (effective > budget) {
28501
+ oversizedMtu.push({ iface: t, effective, budget, label: overhead.label });
28502
+ evidence.push({
28503
+ dimension: "vpn",
28504
+ severity: "warning",
28505
+ summary: `Tunnel ${t.name} MTU ${effective} exceeds the ${budget}-byte budget for ` + `${overhead.label} (${overhead.bytes} B overhead${pppoeWan ? " + PPPoE 8 B" : ""})`,
28506
+ detail: "Packets at this size need fragmentation the transit path will not perform, " + "so they are dropped silently.",
28507
+ reference: t.name
28508
+ });
28509
+ }
28510
+ const covered = t.clampTcpMss === true || globalClamp || PPP_FAMILY.test(t.type.toLowerCase()) && pppClampedEverywhere;
28511
+ if (!covered) {
28512
+ unclamped.push(t);
28513
+ evidence.push({
28514
+ dimension: "vpn",
28515
+ severity: "warning",
28516
+ summary: `Tunnel ${t.name} (${overhead.label}) has no TCP MSS clamp`,
28517
+ detail: "Hosts derive their MSS from their own LAN MTU and set DF, so full-size " + "segments enter the tunnel and are dropped in transit. Small flows (ping, " + "logins) work; large ones (file transfer, photo/video upload, TLS) stall.",
28518
+ reference: t.name
28519
+ });
28520
+ }
28521
+ }
28522
+ if (strandedClamp) {
28523
+ evidence.push({
28524
+ dimension: "vpn",
28525
+ severity: "warning",
28526
+ summary: `A change-mss rule exists but only in chain '${clampRules[0].chain}' \u2014 ` + "transit traffic is clamped in forward/postrouting, not there"
28527
+ });
28528
+ }
28529
+ if (data.pppProfilesMissingMssClamp.length > 0 && tunnels.some((t) => PPP_FAMILY.test(t.type))) {
28530
+ evidence.push({
28531
+ dimension: "vpn",
28532
+ severity: "warning",
28533
+ summary: `PPP profile(s) without change-tcp-mss: ${data.pppProfilesMissingMssClamp.join(", ")}`,
28534
+ detail: "PPP-family tunnels (L2TP/PPTP/SSTP/OpenVPN/PPPoE) clamp MSS through their " + "profile. Sessions using these profiles are unprotected."
28535
+ });
28536
+ }
28537
+ const keepaliveOff = data.wireguardPeers.filter((p) => !p.disabled && p.persistentKeepalive <= 0);
28538
+ if (keepaliveOff.length > 0) {
28539
+ evidence.push({
28540
+ dimension: "vpn",
28541
+ severity: "warning",
28542
+ summary: `${keepaliveOff.length} WireGuard peer(s) have persistent-keepalive off`,
28543
+ detail: "NAT and stateful firewalls drop idle UDP mappings after ~30 s. Without " + "keepalive the peer becomes unreachable from this side until it transmits.",
28544
+ reference: keepaliveOff[0].interface
28545
+ });
28546
+ }
28547
+ if (tunnels.length > 0 && unclamped.length === 0 && oversizedMtu.length === 0 && keepaliveOff.length === 0) {
28548
+ evidence.push({
28549
+ dimension: "vpn",
28550
+ severity: "ok",
28551
+ summary: `MTU/MSS sane on all ${tunnels.length} tunnel(s)`
28552
+ });
28553
+ }
28554
+ return { evidence, unclamped, oversizedMtu, keepaliveOff };
28555
+ }
28556
+ function suggestedMtu(type, pppoeWan) {
28557
+ const o = tunnelOverhead(type);
28558
+ return o ? (pppoeWan ? 1492 : 1500) - o.bytes : undefined;
28559
+ }
28560
+ function mtuFixCommand(iface, mtu) {
28561
+ const o = tunnelOverhead(iface.type);
28562
+ if (o?.path)
28563
+ return `${o.path} set [find name="${iface.name}"] mtu=${mtu}`;
28564
+ return `# ${iface.name} (${o?.label ?? iface.type}): set max-mtu=${mtu} max-mru=${mtu} on the ` + "client interface, or on the server settings for an inbound session";
28565
+ }
28465
28566
  function correlateRootCauses(data, evidence, causes) {
28466
28567
  const criticals = evidence.filter((e) => e.severity === "critical");
28467
28568
  const warnings = evidence.filter((e) => e.severity === "warning");
@@ -28592,6 +28693,41 @@ function correlateRootCauses(data, evidence, causes) {
28592
28693
  dimensions: ["vpn"]
28593
28694
  });
28594
28695
  }
28696
+ const mtu = analyzeTunnelMtu(data);
28697
+ const pppoeWan = data.interfaces.some((i) => i.type.toLowerCase().includes("pppoe"));
28698
+ if (mtu.unclamped.length > 0 || mtu.oversizedMtu.length > 0) {
28699
+ const names = [
28700
+ ...new Set([...mtu.unclamped, ...mtu.oversizedMtu.map((o) => o.iface)].map((t) => t.name))
28701
+ ];
28702
+ const bothProblems = mtu.unclamped.length > 0 && mtu.oversizedMtu.length > 0;
28703
+ const pingOk = data.ping?.lossPct === 0;
28704
+ causes.push({
28705
+ cause: "MTU/MSS black hole on tunnel",
28706
+ explanation: `Tunnel(s) ${names.join(", ")} carry traffic larger than the path can deliver. ` + "Encapsulation shrinks the usable MTU, but hosts still negotiate a TCP MSS from " + "their own LAN MTU and set the don't-fragment bit; transit routers drop the " + "oversized packets and the ICMP 'fragmentation needed' reply is commonly filtered, " + "so the sender never learns to back off. The result is a tunnel that pings clean " + "and passes logins while file transfers, media uploads and some HTTPS sites hang.",
28707
+ confidence: bothProblems || pingOk ? "high" : "medium",
28708
+ evidence: mtu.evidence.filter((e) => e.severity === "warning"),
28709
+ fixes: [
28710
+ ...mtu.oversizedMtu.map((o) => mtuFixCommand(o.iface, suggestedMtu(o.iface.type, pppoeWan) ?? o.budget)),
28711
+ ...mtu.unclamped.length > 0 ? [
28712
+ "# Clamp TCP MSS for every forwarded flow (covers all tunnel types):",
28713
+ "/ip firewall mangle add chain=forward protocol=tcp tcp-flags=syn " + 'tcp-mss=1400-65535 action=change-mss new-mss=clamp-to-pmtu comment="clamp MSS to PMTU"',
28714
+ "# PPP-family tunnels (L2TP/PPTP/SSTP/OVPN) can clamp via their profile instead:",
28715
+ "/ppp profile set [find] change-tcp-mss=yes"
28716
+ ] : []
28717
+ ],
28718
+ dimensions: ["vpn", "firewall"]
28719
+ });
28720
+ }
28721
+ if (mtu.keepaliveOff.length > 0) {
28722
+ causes.push({
28723
+ cause: "WireGuard peer unreachable after idle (no persistent-keepalive)",
28724
+ explanation: `${mtu.keepaliveOff.length} enabled peer(s) have persistent-keepalive off: ` + `${mtu.keepaliveOff.map((p) => `${p.interface}/${p.publicKey.slice(0, 12)}\u2026`).join(", ")}. ` + "WireGuard is silent when idle, so the NAT/firewall mapping the peer punched " + "expires (typically ~30 s) and this side can no longer initiate. The tunnel " + "appears to work whenever the peer starts the traffic and to be down otherwise.",
28725
+ confidence: "medium",
28726
+ evidence: evidence.filter((e) => e.dimension === "vpn" && e.severity === "warning"),
28727
+ fixes: mtu.keepaliveOff.map((p) => `/interface wireguard peers set [find public-key="${p.publicKey}"] persistent-keepalive=25s`),
28728
+ dimensions: ["vpn"]
28729
+ });
28730
+ }
28595
28731
  const flapping = data.interfaces.filter((i) => i.linkDowns > 5 && i.running);
28596
28732
  if (flapping.length > 0 && data.ping && data.ping.lossPct > 0 && data.ping.lossPct < 100) {
28597
28733
  causes.push({
@@ -28797,7 +28933,9 @@ async function collectInterfaces(ctx) {
28797
28933
  rxErrors: num(r["rx-error"]),
28798
28934
  linkDowns: num(r["link-downs"]),
28799
28935
  lastLinkDownTime: r["last-link-down-time"],
28800
- mtu: num(r.mtu) || 1500
28936
+ mtu: num(r.mtu) || 1500,
28937
+ actualMtu: num(r["actual-mtu"]) || undefined,
28938
+ clampTcpMss: r["clamp-tcp-mss"] === undefined ? undefined : isYes(r["clamp-tcp-mss"])
28801
28939
  }));
28802
28940
  }
28803
28941
  async function collectRoutes(ctx) {
@@ -28930,9 +29068,35 @@ async function collectTunnels(ctx) {
28930
29068
  txErrors: num(r["tx-error"]),
28931
29069
  rxErrors: num(r["rx-error"]),
28932
29070
  linkDowns: num(r["link-downs"]),
28933
- mtu: num(r.mtu) || 1500
29071
+ mtu: num(r.mtu) || 1500,
29072
+ actualMtu: num(r["actual-mtu"]) || undefined,
29073
+ clampTcpMss: r["clamp-tcp-mss"] === undefined ? undefined : isYes(r["clamp-tcp-mss"])
29074
+ }));
29075
+ }
29076
+ async function collectMangleRules(ctx) {
29077
+ const raw = await safe("/ip firewall mangle print detail", ctx);
29078
+ if (!raw)
29079
+ return [];
29080
+ return parseRecords(raw).rows.map(parseFirewallRow);
29081
+ }
29082
+ async function collectWireguardPeers(ctx) {
29083
+ const raw = await safe("/interface wireguard peers print detail", ctx);
29084
+ if (!raw || isEmpty(raw))
29085
+ return [];
29086
+ return parseRecords(raw).rows.map((r) => ({
29087
+ interface: r.interface ?? "",
29088
+ publicKey: r["public-key"] ?? "",
29089
+ endpoint: r["endpoint-address"] || undefined,
29090
+ persistentKeepalive: num(r["persistent-keepalive"]),
29091
+ disabled: (r.flags ?? "").includes("X") || r.disabled === "true"
28934
29092
  }));
28935
29093
  }
29094
+ async function collectPppProfilesMissingMssClamp(ctx) {
29095
+ const raw = await safe("/ppp profile print detail", ctx);
29096
+ if (!raw || isEmpty(raw))
29097
+ return [];
29098
+ return parseRecords(raw).rows.filter((r) => !isYes(r["change-tcp-mss"])).map((r) => r.name ?? "").filter(Boolean);
29099
+ }
28936
29100
  async function collectDiagnosticData(target, ctx, dimensions) {
28937
29101
  const dims = new Set(dimensions ?? ALL_DIMENSIONS);
28938
29102
  const [
@@ -28950,7 +29114,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
28950
29114
  dnsSettingsParts,
28951
29115
  resourceRaw,
28952
29116
  logs,
28953
- tunnels
29117
+ tunnels,
29118
+ mangleRules,
29119
+ wireguardPeers,
29120
+ pppProfilesMissingMssClamp
28954
29121
  ] = await Promise.all([
28955
29122
  dims.has("connectivity") ? safe(`/ping ${quoteValue(target)} count=5`, ctx) : Promise.resolve(""),
28956
29123
  dims.has("interfaces") ? collectInterfaces(ctx) : Promise.resolve([]),
@@ -28970,7 +29137,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
28970
29137
  ]) : Promise.resolve(["", "", ""]),
28971
29138
  dims.has("resources") ? safe("/system resource print", ctx) : Promise.resolve(""),
28972
29139
  dims.has("logs") ? collectLogs(target, ctx) : Promise.resolve([]),
28973
- dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([])
29140
+ dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([]),
29141
+ dims.has("vpn") || dims.has("firewall") ? collectMangleRules(ctx) : Promise.resolve([]),
29142
+ dims.has("vpn") ? collectWireguardPeers(ctx) : Promise.resolve([]),
29143
+ dims.has("vpn") ? collectPppProfilesMissingMssClamp(ctx) : Promise.resolve([])
28974
29144
  ]);
28975
29145
  const ping = pingResult ? parsePingSummary(pingResult) ?? undefined : undefined;
28976
29146
  const [dnsServersRaw, dnsDynamicRaw, dnsAllowRemoteRaw] = dnsSettingsParts;
@@ -29002,7 +29172,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
29002
29172
  uptime: resKv.uptime ?? "",
29003
29173
  rosVersion: resKv.version ?? "",
29004
29174
  relevantLogs: logs,
29005
- tunnelInterfaces: tunnels
29175
+ tunnelInterfaces: tunnels,
29176
+ mangleRules,
29177
+ wireguardPeers,
29178
+ pppProfilesMissingMssClamp
29006
29179
  };
29007
29180
  }
29008
29181
  var dimensionEnum = z94.enum(ALL_DIMENSIONS).describe("Diagnostic dimension to investigate.");
@@ -29011,7 +29184,7 @@ var rootCauseTools = [
29011
29184
  name: "diagnose",
29012
29185
  title: "Intelligent Root-Cause Diagnosis",
29013
29186
  annotations: READ,
29014
- description: "Autonomously investigate a network problem across all diagnostic dimensions: " + "connectivity (ping), interface state & error counters, routing table & BGP/OSPF " + "neighbors, firewall rules & hit counters, NAT & connection tracking, ARP/DHCP " + "state, DNS resolution, CPU/memory pressure, system logs, and VPN tunnel state. " + "Correlates the evidence to deliver ranked root-cause hypotheses with confidence " + "levels, plain-language explanations, and exact RouterOS fix commands. " + "Pass an IP address, hostname, or symptom description as the target. " + "For hop-by-hop path analysis use `trace_path`; for log-specific investigation " + "use `correlate_events`; for fix commands only use `suggest_fix`.",
29187
+ description: "Autonomously investigate a network problem across all diagnostic dimensions: " + "connectivity (ping), interface state & error counters, routing table & BGP/OSPF " + "neighbors, firewall rules & hit counters, NAT & connection tracking, ARP/DHCP " + "state, DNS resolution, CPU/memory pressure, system logs, and VPN tunnel state \u2014 " + "including the tunnel MTU / TCP-MSS-clamp / WireGuard-keepalive checks that catch a " + "PMTU black hole (tunnel pings fine but large transfers, uploads and some HTTPS hang). " + "Correlates the evidence to deliver ranked root-cause hypotheses with confidence " + "levels, plain-language explanations, and exact RouterOS fix commands. " + "Pass an IP address, hostname, or symptom description as the target. " + "For hop-by-hop path analysis use `trace_path`; for log-specific investigation " + "use `correlate_events`; for fix commands only use `suggest_fix`.",
29015
29188
  inputSchema: {
29016
29189
  target: z94.string().describe("The target to investigate \u2014 an IP address (e.g. '8.8.8.8'), hostname " + "(e.g. 'google.com'), or network/subnet (e.g. '10.0.0.0/24')."),
29017
29190
  dimensions: z94.array(dimensionEnum).optional().describe(`Limit investigation to specific dimensions. ` + `Omit to check all: ${ALL_DIMENSIONS.join(", ")}`),
@@ -34834,7 +35007,7 @@ var riskByTool = null;
34834
35007
  async function riskIndex() {
34835
35008
  if (riskByTool)
34836
35009
  return riskByTool;
34837
- const { moduleCatalog } = await import("./cli-s9fc4c7j.js");
35010
+ const { moduleCatalog } = await import("./cli-f80vccvs.js");
34838
35011
  const index = new Map;
34839
35012
  for (const mod of moduleCatalog) {
34840
35013
  for (const tool of mod.tools)
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./cli-mqb3rqfy.js";
7
+ } from "./cli-54yv0kd3.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./library-tq8phch8.js";
7
+ } from "./library-s7s3s14v.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -10607,7 +10607,7 @@ var cache2 = null;
10607
10607
  async function gateway() {
10608
10608
  if (cache2)
10609
10609
  return cache2;
10610
- const { moduleCatalog } = await import("./library-xgy70fsf.js");
10610
+ const { moduleCatalog } = await import("./library-4a4mfnxf.js");
10611
10611
  const forIndex = [];
10612
10612
  const byName = new Map;
10613
10613
  for (const mod of moduleCatalog) {
@@ -28176,6 +28176,8 @@ function analyzeRootCause(data) {
28176
28176
  summary: `All ${data.tunnelInterfaces.length} tunnel(s) running`
28177
28177
  });
28178
28178
  }
28179
+ const mtuFindings = analyzeTunnelMtu(data);
28180
+ evidence.push(...mtuFindings.evidence);
28179
28181
  correlateRootCauses(data, evidence, rootCauses);
28180
28182
  if (data.ping && data.ping.lossPct === 0) {
28181
28183
  const contradicted = new Set([
@@ -28210,6 +28212,105 @@ function analyzeRootCause(data) {
28210
28212
  dimensionSummary
28211
28213
  };
28212
28214
  }
28215
+ var TUNNEL_OVERHEAD = [
28216
+ { match: "gre", bytes: 24, label: "GRE", path: "/interface gre" },
28217
+ { match: "ipip", bytes: 20, label: "IPIP", path: "/interface ipip" },
28218
+ { match: "eoip", bytes: 42, label: "EoIP", path: "/interface eoip" },
28219
+ { match: "vxlan", bytes: 50, label: "VXLAN", path: "/interface vxlan" },
28220
+ { match: "wg", bytes: 60, label: "WireGuard", path: "/interface wireguard" },
28221
+ { match: "l2tp", bytes: 100, label: "L2TP" },
28222
+ { match: "pptp", bytes: 100, label: "PPTP" },
28223
+ { match: "sstp", bytes: 100, label: "SSTP" },
28224
+ { match: "ovpn", bytes: 100, label: "OpenVPN" }
28225
+ ];
28226
+ var PPP_FAMILY = /l2tp|pptp|sstp|ovpn|pppoe/;
28227
+ function tunnelOverhead(type) {
28228
+ const t = type.toLowerCase();
28229
+ return TUNNEL_OVERHEAD.find((o) => t.includes(o.match));
28230
+ }
28231
+ function analyzeTunnelMtu(data) {
28232
+ const evidence = [];
28233
+ const unclamped = [];
28234
+ const oversizedMtu = [];
28235
+ const tunnels = data.tunnelInterfaces.filter((t) => !t.disabled);
28236
+ const clampRules = data.mangleRules.filter((r) => r.action === "change-mss" && !r.disabled);
28237
+ const globalClamp = clampRules.some((r) => r.chain === "forward" || r.chain === "postrouting" || r.chain === "output");
28238
+ const strandedClamp = clampRules.length > 0 && !globalClamp;
28239
+ const pppoeWan = data.interfaces.some((i) => i.type.toLowerCase().includes("pppoe"));
28240
+ const pathBudget = pppoeWan ? 1492 : 1500;
28241
+ const pppClampedEverywhere = data.pppProfilesMissingMssClamp.length === 0;
28242
+ for (const t of tunnels) {
28243
+ const overhead = tunnelOverhead(t.type);
28244
+ if (!overhead)
28245
+ continue;
28246
+ const effective = t.actualMtu ?? t.mtu;
28247
+ const budget = pathBudget - overhead.bytes;
28248
+ if (effective > budget) {
28249
+ oversizedMtu.push({ iface: t, effective, budget, label: overhead.label });
28250
+ evidence.push({
28251
+ dimension: "vpn",
28252
+ severity: "warning",
28253
+ summary: `Tunnel ${t.name} MTU ${effective} exceeds the ${budget}-byte budget for ` + `${overhead.label} (${overhead.bytes} B overhead${pppoeWan ? " + PPPoE 8 B" : ""})`,
28254
+ detail: "Packets at this size need fragmentation the transit path will not perform, " + "so they are dropped silently.",
28255
+ reference: t.name
28256
+ });
28257
+ }
28258
+ const covered = t.clampTcpMss === true || globalClamp || PPP_FAMILY.test(t.type.toLowerCase()) && pppClampedEverywhere;
28259
+ if (!covered) {
28260
+ unclamped.push(t);
28261
+ evidence.push({
28262
+ dimension: "vpn",
28263
+ severity: "warning",
28264
+ summary: `Tunnel ${t.name} (${overhead.label}) has no TCP MSS clamp`,
28265
+ detail: "Hosts derive their MSS from their own LAN MTU and set DF, so full-size " + "segments enter the tunnel and are dropped in transit. Small flows (ping, " + "logins) work; large ones (file transfer, photo/video upload, TLS) stall.",
28266
+ reference: t.name
28267
+ });
28268
+ }
28269
+ }
28270
+ if (strandedClamp) {
28271
+ evidence.push({
28272
+ dimension: "vpn",
28273
+ severity: "warning",
28274
+ summary: `A change-mss rule exists but only in chain '${clampRules[0].chain}' \u2014 ` + "transit traffic is clamped in forward/postrouting, not there"
28275
+ });
28276
+ }
28277
+ if (data.pppProfilesMissingMssClamp.length > 0 && tunnels.some((t) => PPP_FAMILY.test(t.type))) {
28278
+ evidence.push({
28279
+ dimension: "vpn",
28280
+ severity: "warning",
28281
+ summary: `PPP profile(s) without change-tcp-mss: ${data.pppProfilesMissingMssClamp.join(", ")}`,
28282
+ detail: "PPP-family tunnels (L2TP/PPTP/SSTP/OpenVPN/PPPoE) clamp MSS through their " + "profile. Sessions using these profiles are unprotected."
28283
+ });
28284
+ }
28285
+ const keepaliveOff = data.wireguardPeers.filter((p) => !p.disabled && p.persistentKeepalive <= 0);
28286
+ if (keepaliveOff.length > 0) {
28287
+ evidence.push({
28288
+ dimension: "vpn",
28289
+ severity: "warning",
28290
+ summary: `${keepaliveOff.length} WireGuard peer(s) have persistent-keepalive off`,
28291
+ detail: "NAT and stateful firewalls drop idle UDP mappings after ~30 s. Without " + "keepalive the peer becomes unreachable from this side until it transmits.",
28292
+ reference: keepaliveOff[0].interface
28293
+ });
28294
+ }
28295
+ if (tunnels.length > 0 && unclamped.length === 0 && oversizedMtu.length === 0 && keepaliveOff.length === 0) {
28296
+ evidence.push({
28297
+ dimension: "vpn",
28298
+ severity: "ok",
28299
+ summary: `MTU/MSS sane on all ${tunnels.length} tunnel(s)`
28300
+ });
28301
+ }
28302
+ return { evidence, unclamped, oversizedMtu, keepaliveOff };
28303
+ }
28304
+ function suggestedMtu(type, pppoeWan) {
28305
+ const o = tunnelOverhead(type);
28306
+ return o ? (pppoeWan ? 1492 : 1500) - o.bytes : undefined;
28307
+ }
28308
+ function mtuFixCommand(iface, mtu) {
28309
+ const o = tunnelOverhead(iface.type);
28310
+ if (o?.path)
28311
+ return `${o.path} set [find name="${iface.name}"] mtu=${mtu}`;
28312
+ return `# ${iface.name} (${o?.label ?? iface.type}): set max-mtu=${mtu} max-mru=${mtu} on the ` + "client interface, or on the server settings for an inbound session";
28313
+ }
28213
28314
  function correlateRootCauses(data, evidence, causes) {
28214
28315
  const criticals = evidence.filter((e) => e.severity === "critical");
28215
28316
  const warnings = evidence.filter((e) => e.severity === "warning");
@@ -28340,6 +28441,41 @@ function correlateRootCauses(data, evidence, causes) {
28340
28441
  dimensions: ["vpn"]
28341
28442
  });
28342
28443
  }
28444
+ const mtu = analyzeTunnelMtu(data);
28445
+ const pppoeWan = data.interfaces.some((i) => i.type.toLowerCase().includes("pppoe"));
28446
+ if (mtu.unclamped.length > 0 || mtu.oversizedMtu.length > 0) {
28447
+ const names = [
28448
+ ...new Set([...mtu.unclamped, ...mtu.oversizedMtu.map((o) => o.iface)].map((t) => t.name))
28449
+ ];
28450
+ const bothProblems = mtu.unclamped.length > 0 && mtu.oversizedMtu.length > 0;
28451
+ const pingOk = data.ping?.lossPct === 0;
28452
+ causes.push({
28453
+ cause: "MTU/MSS black hole on tunnel",
28454
+ explanation: `Tunnel(s) ${names.join(", ")} carry traffic larger than the path can deliver. ` + "Encapsulation shrinks the usable MTU, but hosts still negotiate a TCP MSS from " + "their own LAN MTU and set the don't-fragment bit; transit routers drop the " + "oversized packets and the ICMP 'fragmentation needed' reply is commonly filtered, " + "so the sender never learns to back off. The result is a tunnel that pings clean " + "and passes logins while file transfers, media uploads and some HTTPS sites hang.",
28455
+ confidence: bothProblems || pingOk ? "high" : "medium",
28456
+ evidence: mtu.evidence.filter((e) => e.severity === "warning"),
28457
+ fixes: [
28458
+ ...mtu.oversizedMtu.map((o) => mtuFixCommand(o.iface, suggestedMtu(o.iface.type, pppoeWan) ?? o.budget)),
28459
+ ...mtu.unclamped.length > 0 ? [
28460
+ "# Clamp TCP MSS for every forwarded flow (covers all tunnel types):",
28461
+ "/ip firewall mangle add chain=forward protocol=tcp tcp-flags=syn " + 'tcp-mss=1400-65535 action=change-mss new-mss=clamp-to-pmtu comment="clamp MSS to PMTU"',
28462
+ "# PPP-family tunnels (L2TP/PPTP/SSTP/OVPN) can clamp via their profile instead:",
28463
+ "/ppp profile set [find] change-tcp-mss=yes"
28464
+ ] : []
28465
+ ],
28466
+ dimensions: ["vpn", "firewall"]
28467
+ });
28468
+ }
28469
+ if (mtu.keepaliveOff.length > 0) {
28470
+ causes.push({
28471
+ cause: "WireGuard peer unreachable after idle (no persistent-keepalive)",
28472
+ explanation: `${mtu.keepaliveOff.length} enabled peer(s) have persistent-keepalive off: ` + `${mtu.keepaliveOff.map((p) => `${p.interface}/${p.publicKey.slice(0, 12)}\u2026`).join(", ")}. ` + "WireGuard is silent when idle, so the NAT/firewall mapping the peer punched " + "expires (typically ~30 s) and this side can no longer initiate. The tunnel " + "appears to work whenever the peer starts the traffic and to be down otherwise.",
28473
+ confidence: "medium",
28474
+ evidence: evidence.filter((e) => e.dimension === "vpn" && e.severity === "warning"),
28475
+ fixes: mtu.keepaliveOff.map((p) => `/interface wireguard peers set [find public-key="${p.publicKey}"] persistent-keepalive=25s`),
28476
+ dimensions: ["vpn"]
28477
+ });
28478
+ }
28343
28479
  const flapping = data.interfaces.filter((i) => i.linkDowns > 5 && i.running);
28344
28480
  if (flapping.length > 0 && data.ping && data.ping.lossPct > 0 && data.ping.lossPct < 100) {
28345
28481
  causes.push({
@@ -28545,7 +28681,9 @@ async function collectInterfaces(ctx) {
28545
28681
  rxErrors: num(r["rx-error"]),
28546
28682
  linkDowns: num(r["link-downs"]),
28547
28683
  lastLinkDownTime: r["last-link-down-time"],
28548
- mtu: num(r.mtu) || 1500
28684
+ mtu: num(r.mtu) || 1500,
28685
+ actualMtu: num(r["actual-mtu"]) || undefined,
28686
+ clampTcpMss: r["clamp-tcp-mss"] === undefined ? undefined : isYes(r["clamp-tcp-mss"])
28549
28687
  }));
28550
28688
  }
28551
28689
  async function collectRoutes(ctx) {
@@ -28678,9 +28816,35 @@ async function collectTunnels(ctx) {
28678
28816
  txErrors: num(r["tx-error"]),
28679
28817
  rxErrors: num(r["rx-error"]),
28680
28818
  linkDowns: num(r["link-downs"]),
28681
- mtu: num(r.mtu) || 1500
28819
+ mtu: num(r.mtu) || 1500,
28820
+ actualMtu: num(r["actual-mtu"]) || undefined,
28821
+ clampTcpMss: r["clamp-tcp-mss"] === undefined ? undefined : isYes(r["clamp-tcp-mss"])
28822
+ }));
28823
+ }
28824
+ async function collectMangleRules(ctx) {
28825
+ const raw = await safe("/ip firewall mangle print detail", ctx);
28826
+ if (!raw)
28827
+ return [];
28828
+ return parseRecords(raw).rows.map(parseFirewallRow);
28829
+ }
28830
+ async function collectWireguardPeers(ctx) {
28831
+ const raw = await safe("/interface wireguard peers print detail", ctx);
28832
+ if (!raw || isEmpty(raw))
28833
+ return [];
28834
+ return parseRecords(raw).rows.map((r) => ({
28835
+ interface: r.interface ?? "",
28836
+ publicKey: r["public-key"] ?? "",
28837
+ endpoint: r["endpoint-address"] || undefined,
28838
+ persistentKeepalive: num(r["persistent-keepalive"]),
28839
+ disabled: (r.flags ?? "").includes("X") || r.disabled === "true"
28682
28840
  }));
28683
28841
  }
28842
+ async function collectPppProfilesMissingMssClamp(ctx) {
28843
+ const raw = await safe("/ppp profile print detail", ctx);
28844
+ if (!raw || isEmpty(raw))
28845
+ return [];
28846
+ return parseRecords(raw).rows.filter((r) => !isYes(r["change-tcp-mss"])).map((r) => r.name ?? "").filter(Boolean);
28847
+ }
28684
28848
  async function collectDiagnosticData(target, ctx, dimensions) {
28685
28849
  const dims = new Set(dimensions ?? ALL_DIMENSIONS);
28686
28850
  const [
@@ -28698,7 +28862,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
28698
28862
  dnsSettingsParts,
28699
28863
  resourceRaw,
28700
28864
  logs,
28701
- tunnels
28865
+ tunnels,
28866
+ mangleRules,
28867
+ wireguardPeers,
28868
+ pppProfilesMissingMssClamp
28702
28869
  ] = await Promise.all([
28703
28870
  dims.has("connectivity") ? safe(`/ping ${quoteValue(target)} count=5`, ctx) : Promise.resolve(""),
28704
28871
  dims.has("interfaces") ? collectInterfaces(ctx) : Promise.resolve([]),
@@ -28718,7 +28885,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
28718
28885
  ]) : Promise.resolve(["", "", ""]),
28719
28886
  dims.has("resources") ? safe("/system resource print", ctx) : Promise.resolve(""),
28720
28887
  dims.has("logs") ? collectLogs(target, ctx) : Promise.resolve([]),
28721
- dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([])
28888
+ dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([]),
28889
+ dims.has("vpn") || dims.has("firewall") ? collectMangleRules(ctx) : Promise.resolve([]),
28890
+ dims.has("vpn") ? collectWireguardPeers(ctx) : Promise.resolve([]),
28891
+ dims.has("vpn") ? collectPppProfilesMissingMssClamp(ctx) : Promise.resolve([])
28722
28892
  ]);
28723
28893
  const ping = pingResult ? parsePingSummary(pingResult) ?? undefined : undefined;
28724
28894
  const [dnsServersRaw, dnsDynamicRaw, dnsAllowRemoteRaw] = dnsSettingsParts;
@@ -28750,7 +28920,10 @@ async function collectDiagnosticData(target, ctx, dimensions) {
28750
28920
  uptime: resKv.uptime ?? "",
28751
28921
  rosVersion: resKv.version ?? "",
28752
28922
  relevantLogs: logs,
28753
- tunnelInterfaces: tunnels
28923
+ tunnelInterfaces: tunnels,
28924
+ mangleRules,
28925
+ wireguardPeers,
28926
+ pppProfilesMissingMssClamp
28754
28927
  };
28755
28928
  }
28756
28929
  var dimensionEnum = z94.enum(ALL_DIMENSIONS).describe("Diagnostic dimension to investigate.");
@@ -28759,7 +28932,7 @@ var rootCauseTools = [
28759
28932
  name: "diagnose",
28760
28933
  title: "Intelligent Root-Cause Diagnosis",
28761
28934
  annotations: READ,
28762
- description: "Autonomously investigate a network problem across all diagnostic dimensions: " + "connectivity (ping), interface state & error counters, routing table & BGP/OSPF " + "neighbors, firewall rules & hit counters, NAT & connection tracking, ARP/DHCP " + "state, DNS resolution, CPU/memory pressure, system logs, and VPN tunnel state. " + "Correlates the evidence to deliver ranked root-cause hypotheses with confidence " + "levels, plain-language explanations, and exact RouterOS fix commands. " + "Pass an IP address, hostname, or symptom description as the target. " + "For hop-by-hop path analysis use `trace_path`; for log-specific investigation " + "use `correlate_events`; for fix commands only use `suggest_fix`.",
28935
+ description: "Autonomously investigate a network problem across all diagnostic dimensions: " + "connectivity (ping), interface state & error counters, routing table & BGP/OSPF " + "neighbors, firewall rules & hit counters, NAT & connection tracking, ARP/DHCP " + "state, DNS resolution, CPU/memory pressure, system logs, and VPN tunnel state \u2014 " + "including the tunnel MTU / TCP-MSS-clamp / WireGuard-keepalive checks that catch a " + "PMTU black hole (tunnel pings fine but large transfers, uploads and some HTTPS hang). " + "Correlates the evidence to deliver ranked root-cause hypotheses with confidence " + "levels, plain-language explanations, and exact RouterOS fix commands. " + "Pass an IP address, hostname, or symptom description as the target. " + "For hop-by-hop path analysis use `trace_path`; for log-specific investigation " + "use `correlate_events`; for fix commands only use `suggest_fix`.",
28763
28936
  inputSchema: {
28764
28937
  target: z94.string().describe("The target to investigate \u2014 an IP address (e.g. '8.8.8.8'), hostname " + "(e.g. 'google.com'), or network/subnet (e.g. '10.0.0.0/24')."),
28765
28938
  dimensions: z94.array(dimensionEnum).optional().describe(`Limit investigation to specific dimensions. ` + `Omit to check all: ${ALL_DIMENSIONS.join(", ")}`),
@@ -34334,7 +34507,7 @@ var riskByTool = null;
34334
34507
  async function riskIndex() {
34335
34508
  if (riskByTool)
34336
34509
  return riskByTool;
34337
- const { moduleCatalog } = await import("./library-xgy70fsf.js");
34510
+ const { moduleCatalog } = await import("./library-4a4mfnxf.js");
34338
34511
  const index = new Map;
34339
34512
  for (const mod of moduleCatalog) {
34340
34513
  for (const tool of mod.tools)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usex/mikrotik-mcp",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "MCP server for MikroTik RouterOS — 780+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
5
5
  "keywords": [
6
6
  "ai",
@@ -74,6 +74,7 @@
74
74
  "prepublish:security": "npm run security:all",
75
75
  "test": "vp test run",
76
76
  "test:watch": "vp test",
77
+ "test:coverage": "vp test --ui --coverage --watch",
77
78
  "release": "release-it",
78
79
  "test:types": "tsc --noEmit",
79
80
  "check": "vp check",
@@ -104,6 +105,8 @@
104
105
  "@types/react-dom": "^19.2.4",
105
106
  "@types/ssh2": "^1.15.5",
106
107
  "@types/update-notifier": "^6.0.8",
108
+ "@vitest/coverage-v8": "4.1.10",
109
+ "@vitest/ui": "4.1.10",
107
110
  "bunup": "^0.16.32",
108
111
  "class-variance-authority": "^0.7.1",
109
112
  "clsx": "^2.1.1",
@@ -47,8 +47,20 @@ identical on both ends):
47
47
  accepted from {{peer_address}} on the input chain, and add a NAT _bypass_
48
48
  (accept/no-nat) rule so {{local_subnet}}→{{remote_subnet}} traffic is NOT
49
49
  masqueraded. Apply firewall edits under `enable_safe_mode`.
50
- 7. **Verify** — `get_ipsec_active_peers` and `get_ipsec_installed_sa` to confirm
51
- the tunnel established; `ping` a remote host with src-address in {{local_subnet}}.
50
+ 7. **MTU / MSS** — policy-mode IPsec has no interface whose MTU you can lower, so
51
+ MSS clamping is the _only_ lever, and skipping it is the classic "tunnel is up
52
+ but large transfers hang" failure. ESP tunnel mode costs ~73 bytes (more with
53
+ NAT-T/UDP-4500). Add `create_mangle_rule`: `chain=forward`, `protocol=tcp`,
54
+ `tcp_flags=syn`, `tcp_mss=1400-65535`, `action=change-mss`,
55
+ `new_mss=clamp-to-pmtu` — or a fixed `new_mss=1360` when the path MTU is known
56
+ and PMTU discovery is unreliable. Endpoints size their MSS from their own LAN
57
+ MTU and set DF; without the clamp those packets are dropped in transit and the
58
+ ICMP "fragmentation needed" is usually filtered, so the sender never learns.
59
+ 8. **Verify** — `get_ipsec_active_peers` and `get_ipsec_installed_sa` to confirm
60
+ the tunnel established; `ping` a remote host with src-address in
61
+ {{local_subnet}}. Then repeat with size 1400 and `do-not-fragment` — small
62
+ pings passing while large ones fail means the MSS clamp in step 7 is missing or
63
+ not matching.
52
64
 
53
65
  Present the matching parameter set for the remote engineer and the exact tool
54
66
  calls before applying. Never echo the pre-shared key back in plaintext beyond
@@ -33,7 +33,19 @@ Build order:
33
33
  4. **Enable the server** — `set_l2tp_server` with `enabled=true`,
34
34
  `default_profile=l2tp-profile`, `use_ipsec=required`, and a strong
35
35
  `ipsec_secret` (this is the IPsec pre-shared key clients enter).
36
- `authentication=mschap2`.
36
+ `authentication=mschap2`. Also set on the same call:
37
+ - `max_mtu`/`max_mru` = **1400** (L2TP + IPsec ESP overhead easily exceeds
38
+ 100 bytes; leaving 1450 causes the "connects fine, transfers stall" symptom),
39
+ - `keepalive_timeout` = **30** — dead client sessions otherwise linger and hold
40
+ their pool address until the default timeout expires.
41
+
42
+ The `change_tcp_mss=yes` set on the profile in step 2 makes MSS follow the
43
+ negotiated MTU automatically — that is the PPP-family equivalent of a
44
+ `change-mss` mangle rule, and it is why L2TP does not need one. If clients
45
+ still stall on large transfers, add the mangle rule anyway
46
+ (`create_mangle_rule`: `chain=forward`, `protocol=tcp`, `tcp_flags=syn`,
47
+ `tcp_mss=1400-65535`, `action=change-mss`, `new_mss=clamp-to-pmtu`).
48
+
37
49
  5. **Firewall** — accept UDP 500, UDP 4500, UDP 1701, and IP protocol 50 (ESP)
38
50
  on the input chain from the internet; allow the {{vpn_pool}} range to reach the
39
51
  LAN/internet in the forward chain as required. Apply under `enable_safe_mode`,
@@ -123,11 +123,14 @@ Per side, under Safe Mode (`enable_safe_mode` `device=<name>` → edits → veri
123
123
  WireGuard adds ~60 bytes of overhead. Small pings work but large flows (TLS, file
124
124
  transfer) stall if MTU is wrong:
125
125
 
126
- - Set the wg interface MTU to **1420** (1412 if the WAN is PPPoE) via
126
+ - Set the wg interface MTU to **1420** (1412 if the WAN is PPPoE, 1280 if the path
127
+ is unknown/multi-hop — 1280 is the always-safe floor) via
127
128
  `update_wireguard_interface`.
128
- - Clamp TCP MSS on the `forward` chain (`create_filter_rule` mangle
129
- `action=change-mss new-mss=clamp-to-pmtu tcp-flags=syn`) or set it per the
130
- interface MTU, so TCP sessions negotiate a size that fits.
129
+ - Clamp TCP MSS with `create_mangle_rule` (**not** `create_filter_rule` — only the
130
+ mangle table has `change-mss`): `chain=forward`, `protocol=tcp`,
131
+ `tcp_flags=syn`, `tcp_mss=1400-65535`, `action=change-mss`,
132
+ `new_mss=clamp-to-pmtu`. WireGuard has no per-interface MSS option, so this
133
+ mangle rule is the only place to fix MSS.
131
134
 
132
135
  ## 8. Verify end to end
133
136
 
@@ -42,5 +42,26 @@ Steps:
42
42
  5. **Client config** — call `generate_wireguard_client_config` with the server
43
43
  public key, {{endpoint}}, the listen port, and the assigned client address, and
44
44
  present the resulting `[Interface]/[Peer]` config for the user to import.
45
+ 6. **Keepalive** — set `persistent_keepalive` to `25` on every peer
46
+ (`add_wireguard_peer`/`update_wireguard_peer`) and `client_keepalive=25s` in the
47
+ generated client config. Roaming clients sit behind NAT; a UDP mapping typically
48
+ expires after ~30 s of silence, after which the server cannot reach the client
49
+ until the client speaks first. 25 s keeps the mapping alive.
50
+ 7. **MTU / MSS** — the step most WireGuard deployments skip, and the reason a VPN
51
+ "works" for logins but stalls on file transfers, photo/video uploads and some
52
+ web pages:
53
+ - WireGuard adds ~60 bytes. Set the interface MTU to **1420**
54
+ (`update_wireguard_interface`); use **1412** on a PPPoE WAN, or **1280** when
55
+ the path is unknown — 1280 always fits.
56
+ - MTU alone is not enough. Endpoints derive their TCP MSS from _their own_ NIC
57
+ MTU, so a 1460-byte-MSS session still emits oversized packets with DF set;
58
+ mid-path routers drop rather than fragment and the ICMP "fragmentation needed"
59
+ is often filtered — a PMTU black hole. Clamp it with `create_mangle_rule`:
60
+ `chain=forward`, `protocol=tcp`, `tcp_flags=syn`, `tcp_mss=1400-65535`,
61
+ `action=change-mss`, `new_mss=clamp-to-pmtu`. WireGuard has no per-interface
62
+ MSS setting, so mangle is the only place this can be fixed.
63
+ - Verify: `ping` a host across the tunnel with size 1400 and `do-not-fragment`.
64
+ Small pings succeeding while this fails is the black-hole signature.
45
65
 
46
- Report the server public key, the peer you added, and the full client config.
66
+ Report the server public key, the peer you added, the MTU/MSS/keepalive values
67
+ applied, and the full client config.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "generated": "by scripts/gen-schemas.ts — do not edit by hand",
5
5
  "toolCount": 885,
6
6
  "tools": [
@@ -24868,7 +24868,7 @@
24868
24868
  "idempotentHint": true,
24869
24869
  "openWorldHint": false
24870
24870
  },
24871
- "description": "Autonomously investigate a network problem across all diagnostic dimensions: connectivity (ping), interface state & error counters, routing table & BGP/OSPF neighbors, firewall rules & hit counters, NAT & connection tracking, ARP/DHCP state, DNS resolution, CPU/memory pressure, system logs, and VPN tunnel state. Correlates the evidence to deliver ranked root-cause hypotheses with confidence levels, plain-language explanations, and exact RouterOS fix commands. Pass an IP address, hostname, or symptom description as the target. For hop-by-hop path analysis use `trace_path`; for log-specific investigation use `correlate_events`; for fix commands only use `suggest_fix`.",
24871
+ "description": "Autonomously investigate a network problem across all diagnostic dimensions: connectivity (ping), interface state & error counters, routing table & BGP/OSPF neighbors, firewall rules & hit counters, NAT & connection tracking, ARP/DHCP state, DNS resolution, CPU/memory pressure, system logs, and VPN tunnel state — including the tunnel MTU / TCP-MSS-clamp / WireGuard-keepalive checks that catch a PMTU black hole (tunnel pings fine but large transfers, uploads and some HTTPS hang). Correlates the evidence to deliver ranked root-cause hypotheses with confidence levels, plain-language explanations, and exact RouterOS fix commands. Pass an IP address, hostname, or symptom description as the target. For hop-by-hop path analysis use `trace_path`; for log-specific investigation use `correlate_events`; for fix commands only use `suggest_fix`.",
24872
24872
  "inputSchema": {
24873
24873
  "$schema": "https://json-schema.org/draft/2020-12/schema",
24874
24874
  "type": "object",