mikromcp 1.9.0 → 1.11.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/main.js CHANGED
@@ -498,6 +498,9 @@ function parseRouterOSValue(key2, value) {
498
498
  function isTrue(v) {
499
499
  return v === true || v === "true" || v === "yes";
500
500
  }
501
+ function sameValue(recordValue, desired) {
502
+ return String(recordValue) === String(desired);
503
+ }
501
504
  function parseRecord(raw) {
502
505
  const parsed = {};
503
506
  for (const [key2, value] of Object.entries(raw)) {
@@ -602,12 +605,18 @@ var listInterfacesTool = {
602
605
  return true;
603
606
  });
604
607
  }
605
- const { items: paginated, total, hasMore } = paginate(interfaces, parsed.offset, parsed.limit);
608
+ const {
609
+ items: paginated,
610
+ total,
611
+ hasMore
612
+ } = paginate(interfaces, parsed.offset, parsed.limit);
606
613
  const results2 = paginated.map((iface) => {
607
614
  const rec = iface;
608
615
  const isRunning2 = isTrue(rec.running);
609
616
  const isDisabled = isTrue(rec.disabled);
610
- const enriched = { status: isDisabled ? "disabled" : isRunning2 ? "up" : "down" };
617
+ const enriched = {
618
+ status: isDisabled ? "disabled" : isRunning2 ? "up" : "down"
619
+ };
611
620
  for (const [key2, value] of Object.entries(rec)) {
612
621
  if (parsed.includeCounters || !COUNTER_PROPS.includes(key2)) {
613
622
  enriched[key2] = value;
@@ -809,9 +818,13 @@ var manageIpAddressTool = {
809
818
  const changes = {};
810
819
  const diff = [];
811
820
  const disabledStr = parsed.disabled ? "true" : "false";
812
- if (rec.disabled !== disabledStr) {
821
+ if (isTrue(rec.disabled) !== parsed.disabled) {
813
822
  changes.disabled = disabledStr;
814
- diff.push({ property: "disabled", before: rec.disabled ?? null, after: disabledStr });
823
+ diff.push({
824
+ property: "disabled",
825
+ before: rec.disabled === void 0 ? null : String(rec.disabled),
826
+ after: disabledStr
827
+ });
815
828
  }
816
829
  if (comment !== void 0 && (rec.comment ?? "") !== comment) {
817
830
  changes.comment = comment;
@@ -1038,7 +1051,11 @@ var manageDhcpLeaseTool = {
1038
1051
  if (isAlreadyStatic) {
1039
1052
  return {
1040
1053
  content: `Lease for ${upperMac} (${existing.address ?? "unknown"}) is already static. No changes made.`,
1041
- structuredContent: { action: "already_static", macAddress: upperMac, id: existing[".id"] }
1054
+ structuredContent: {
1055
+ action: "already_static",
1056
+ macAddress: upperMac,
1057
+ id: existing[".id"]
1058
+ }
1042
1059
  };
1043
1060
  }
1044
1061
  if (parsed.dryRun) {
@@ -1144,7 +1161,14 @@ var listRoutesTool = {
1144
1161
  paginated,
1145
1162
  total,
1146
1163
  parsed.offset,
1147
- (r) => compactFields(r, ["dst-address", "gateway", "distance", "routing-table", "active", "disabled"])
1164
+ (r) => compactFields(r, [
1165
+ "dst-address",
1166
+ "gateway",
1167
+ "distance",
1168
+ "routing-table",
1169
+ "active",
1170
+ "disabled"
1171
+ ])
1148
1172
  ),
1149
1173
  structuredContent: {
1150
1174
  routerId: context.routerId,
@@ -1221,9 +1245,9 @@ var manageRouteTool = {
1221
1245
  if (parsed.action === "add") {
1222
1246
  if (existing) {
1223
1247
  const rec = existing;
1224
- const existingDistance = rec.distance ?? "1";
1248
+ const existingDistance = String(rec.distance ?? "1");
1225
1249
  const existingDisabled = isTrue(rec.disabled);
1226
- const sameDistance = existingDistance === String(parsed.distance);
1250
+ const sameDistance = sameValue(rec.distance ?? 1, parsed.distance);
1227
1251
  const sameDisabled = existingDisabled === parsed.disabled;
1228
1252
  if (sameDistance && sameDisabled) {
1229
1253
  return {
@@ -1681,13 +1705,51 @@ var firewallTools = [listFirewallRulesTool, manageFirewallRuleTool];
1681
1705
 
1682
1706
  // src/domain/tools/diagnostic-tools.ts
1683
1707
  import { z as z9 } from "zod";
1708
+
1709
+ // src/adapter/routeros-command.ts
1710
+ var ROUTEROS_COMMAND = /^\/[a-z][a-z0-9-]*(?: [a-z][a-z0-9-]*)+$/;
1711
+ var ROUTEROS_PARAMETER = /^[a-z][a-z0-9-]*$/;
1712
+ var CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
1713
+ function isRouterOsCommandString(value) {
1714
+ return !CONTROL_CHARACTER.test(value);
1715
+ }
1716
+ function quoteRouterOsString(value) {
1717
+ if (!isRouterOsCommandString(value)) {
1718
+ throw new Error("RouterOS command values must not contain control characters");
1719
+ }
1720
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$");
1721
+ return `"${escaped}"`;
1722
+ }
1723
+ function buildRouterOsCommand(command, parameters) {
1724
+ if (!ROUTEROS_COMMAND.test(command)) {
1725
+ throw new Error("Invalid RouterOS command");
1726
+ }
1727
+ const parts = [command];
1728
+ for (const [name, value] of Object.entries(parameters)) {
1729
+ if (!ROUTEROS_PARAMETER.test(name)) {
1730
+ throw new Error("Invalid RouterOS parameter");
1731
+ }
1732
+ if (typeof value === "number") {
1733
+ if (!Number.isSafeInteger(value)) {
1734
+ throw new Error("RouterOS numeric command values must be safe integers");
1735
+ }
1736
+ parts.push(`${name}=${value}`);
1737
+ } else {
1738
+ parts.push(`${name}=${quoteRouterOsString(value)}`);
1739
+ }
1740
+ }
1741
+ return parts.join(" ");
1742
+ }
1743
+
1744
+ // src/domain/tools/diagnostic-tools.ts
1684
1745
  var log7 = createLogger("diagnostic-tools");
1746
+ var routerOsCommandString = z9.string().refine(isRouterOsCommandString, "Must not contain control characters");
1685
1747
  var pingInputSchema = z9.object({
1686
1748
  routerId,
1687
- address: z9.string().describe("Target IP address or hostname to ping"),
1749
+ address: routerOsCommandString.describe("Target IP address or hostname to ping"),
1688
1750
  count: z9.number().int().min(1).max(20).default(4).describe("Number of ICMP echo requests (1\u201320)"),
1689
1751
  size: z9.number().int().min(14).max(65535).default(56).describe("Packet size in bytes (14\u201365535)"),
1690
- routingTable: z9.string().optional().describe("Routing table to use for the ping")
1752
+ routingTable: routerOsCommandString.optional().describe("Routing table to use for the ping")
1691
1753
  }).strict();
1692
1754
  var pingTool = {
1693
1755
  name: "ping",
@@ -1704,13 +1766,15 @@ var pingTool = {
1704
1766
  const parsed = pingInputSchema.parse(params);
1705
1767
  log7.info({ routerId: context.routerId, address: parsed.address }, "Pinging");
1706
1768
  try {
1707
- const parts = [
1708
- `/tool ping address=${parsed.address}`,
1709
- `count=${parsed.count}`,
1710
- `size=${parsed.size}`
1711
- ];
1712
- if (parsed.routingTable !== void 0) parts.push(`routing-table=${parsed.routingTable}`);
1713
- const output = await context.sshClient.execute(parts.join(" "));
1769
+ const parameters = {
1770
+ address: parsed.address,
1771
+ count: parsed.count,
1772
+ size: parsed.size
1773
+ };
1774
+ if (parsed.routingTable !== void 0) parameters["routing-table"] = parsed.routingTable;
1775
+ const output = await context.sshClient.execute(
1776
+ buildRouterOsCommand("/tool ping", parameters)
1777
+ );
1714
1778
  const s = output.match(/sent=(\d+)/)?.[1] ?? String(parsed.count);
1715
1779
  const r = output.match(/received=(\d+)/)?.[1] ?? "0";
1716
1780
  const loss = output.match(/packet-loss=(\S+)/)?.[1] ?? "100%";
@@ -1738,7 +1802,7 @@ var pingTool = {
1738
1802
  };
1739
1803
  var tracerouteInputSchema = z9.object({
1740
1804
  routerId,
1741
- address: z9.string().describe("Target IP address or hostname to trace"),
1805
+ address: routerOsCommandString.describe("Target IP address or hostname to trace"),
1742
1806
  count: z9.number().int().min(1).max(5).default(3).describe("Probes per hop (1\u20135)"),
1743
1807
  maxHops: z9.number().int().min(1).max(30).default(15).describe("Maximum number of hops (1\u201330)")
1744
1808
  }).strict();
@@ -1757,12 +1821,13 @@ var tracerouteTool = {
1757
1821
  const parsed = tracerouteInputSchema.parse(params);
1758
1822
  log7.info({ routerId: context.routerId, address: parsed.address }, "Tracerouting");
1759
1823
  try {
1760
- const parts = [
1761
- `/tool traceroute address=${parsed.address}`,
1762
- `count=${parsed.count}`,
1763
- `max-hops=${parsed.maxHops}`
1764
- ];
1765
- const output = await context.sshClient.execute(parts.join(" "));
1824
+ const output = await context.sshClient.execute(
1825
+ buildRouterOsCommand("/tool traceroute", {
1826
+ address: parsed.address,
1827
+ count: parsed.count,
1828
+ "max-hops": parsed.maxHops
1829
+ })
1830
+ );
1766
1831
  const hopLines = output.split("\n").filter((l) => /^\s*\d+\s+\S+/.test(l));
1767
1832
  const hops = hopLines.map((line) => {
1768
1833
  const cols = line.trim().split(/\s+/);
@@ -1793,10 +1858,10 @@ var tracerouteTool = {
1793
1858
  };
1794
1859
  var torchInputSchema = z9.object({
1795
1860
  routerId,
1796
- interface: z9.string().describe("Interface name to monitor (e.g. ether1, bridge1)"),
1861
+ interface: routerOsCommandString.describe("Interface name to monitor (e.g. ether1, bridge1)"),
1797
1862
  duration: z9.number().int().min(1).max(30).default(5).describe("Capture duration in seconds (1\u201330)"),
1798
- srcAddress: z9.string().optional().describe("Filter by source IP address"),
1799
- dstAddress: z9.string().optional().describe("Filter by destination IP address")
1863
+ srcAddress: routerOsCommandString.optional().describe("Filter by source IP address"),
1864
+ dstAddress: routerOsCommandString.optional().describe("Filter by destination IP address")
1800
1865
  }).strict();
1801
1866
  function parseTorchOutput(output) {
1802
1867
  const flows = [];
@@ -1829,10 +1894,13 @@ var torchTool = {
1829
1894
  const parsed = torchInputSchema.parse(params);
1830
1895
  log7.info({ routerId: context.routerId, interface: parsed.interface }, "Running torch");
1831
1896
  try {
1832
- const parts = [`/tool torch interface=${parsed.interface}`];
1833
- if (parsed.srcAddress !== void 0) parts.push(`src-address=${parsed.srcAddress}`);
1834
- if (parsed.dstAddress !== void 0) parts.push(`dst-address=${parsed.dstAddress}`);
1835
- const raw = await context.sshClient.execute(parts.join(" "), (parsed.duration + 1) * 1e3);
1897
+ const parameters = { interface: parsed.interface };
1898
+ if (parsed.srcAddress !== void 0) parameters["src-address"] = parsed.srcAddress;
1899
+ if (parsed.dstAddress !== void 0) parameters["dst-address"] = parsed.dstAddress;
1900
+ const raw = await context.sshClient.execute(
1901
+ buildRouterOsCommand("/tool torch", parameters),
1902
+ (parsed.duration + 1) * 1e3
1903
+ );
1836
1904
  const clean = raw.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
1837
1905
  const flows = parseTorchOutput(clean);
1838
1906
  const lines = [
@@ -1927,7 +1995,14 @@ function parseRouterClock(dateStr, timeStr) {
1927
1995
  const ss = parseInt(time[3], 10);
1928
1996
  const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr);
1929
1997
  if (iso) {
1930
- return new Date(parseInt(iso[1], 10), parseInt(iso[2], 10) - 1, parseInt(iso[3], 10), hh, mm, ss);
1998
+ return new Date(
1999
+ parseInt(iso[1], 10),
2000
+ parseInt(iso[2], 10) - 1,
2001
+ parseInt(iso[3], 10),
2002
+ hh,
2003
+ mm,
2004
+ ss
2005
+ );
1931
2006
  }
1932
2007
  const named = /^([a-z]{3})\/(\d{2})\/(\d{4})$/i.exec(dateStr);
1933
2008
  if (named) {
@@ -1963,7 +2038,8 @@ var getLogTool = {
1963
2038
  const clock = Array.isArray(clockResult) ? clockResult[0] : clockResult;
1964
2039
  const routerNow = parseRouterClock(clock?.date, clock?.time);
1965
2040
  if (routerNow !== null) now = routerNow;
1966
- else log7.warn({ routerId: context.routerId }, "Unparseable router clock; using host clock");
2041
+ else
2042
+ log7.warn({ routerId: context.routerId }, "Unparseable router clock; using host clock");
1967
2043
  } catch {
1968
2044
  log7.warn({ routerId: context.routerId }, "Router clock unavailable; using host clock");
1969
2045
  }
@@ -2077,9 +2153,7 @@ function checkCommand(command, policy) {
2077
2153
  }
2078
2154
  if (policy.allow.length > 0) {
2079
2155
  const normAllow = policy.allow.map((p) => normalizeCommand(p));
2080
- const everySegmentAllowed = normSegs.every(
2081
- (seg) => normAllow.some((p) => globMatch(p, seg))
2082
- );
2156
+ const everySegmentAllowed = normSegs.every((seg) => normAllow.some((p) => globMatch(p, seg)));
2083
2157
  if (!everySegmentAllowed) {
2084
2158
  throw new MikroMCPError({
2085
2159
  category: "VALIDATION" /* VALIDATION */,
@@ -2350,7 +2424,14 @@ var listBridgesTool = {
2350
2424
  paginated,
2351
2425
  total,
2352
2426
  parsed.offset,
2353
- (b) => compactFields(b, ["name", "protocol-mode", "vlan-filtering", "mtu", "disabled", "comment"])
2427
+ (b) => compactFields(b, [
2428
+ "name",
2429
+ "protocol-mode",
2430
+ "vlan-filtering",
2431
+ "mtu",
2432
+ "disabled",
2433
+ "comment"
2434
+ ])
2354
2435
  ),
2355
2436
  structuredContent: {
2356
2437
  routerId: context.routerId,
@@ -2601,7 +2682,11 @@ var listWifiTool = {
2601
2682
  limit: void 0,
2602
2683
  offset: void 0
2603
2684
  });
2604
- const { items: paginated, total, hasMore } = paginate(interfaces, parsed.offset, parsed.limit);
2685
+ const {
2686
+ items: paginated,
2687
+ total,
2688
+ hasMore
2689
+ } = paginate(interfaces, parsed.offset, parsed.limit);
2605
2690
  return {
2606
2691
  content: listContent(
2607
2692
  "WiFi interfaces",
@@ -2725,16 +2810,22 @@ var manageWifiTool = {
2725
2810
  const rec = existing[0];
2726
2811
  const changes = {};
2727
2812
  const diff = [];
2728
- if (parsed.disabled !== void 0) {
2813
+ if (parsed.disabled !== void 0 && isTrue(rec.disabled) !== parsed.disabled) {
2729
2814
  const desired = parsed.disabled ? "true" : "false";
2730
- if (rec.disabled !== desired) {
2731
- changes.disabled = desired;
2732
- diff.push({ property: "disabled", before: rec.disabled ?? null, after: desired });
2733
- }
2815
+ changes.disabled = desired;
2816
+ diff.push({
2817
+ property: "disabled",
2818
+ before: rec.disabled === void 0 ? null : String(rec.disabled),
2819
+ after: desired
2820
+ });
2734
2821
  }
2735
- if (parsed.ssid !== void 0 && rec.ssid !== parsed.ssid) {
2822
+ if (parsed.ssid !== void 0 && !sameValue(rec.ssid, parsed.ssid)) {
2736
2823
  changes.ssid = parsed.ssid;
2737
- diff.push({ property: "ssid", before: rec.ssid ?? null, after: parsed.ssid });
2824
+ diff.push({
2825
+ property: "ssid",
2826
+ before: rec.ssid === void 0 ? null : String(rec.ssid),
2827
+ after: parsed.ssid
2828
+ });
2738
2829
  }
2739
2830
  if (diff.length === 0) {
2740
2831
  return {
@@ -2788,7 +2879,11 @@ var listWgTool = {
2788
2879
  limit: void 0,
2789
2880
  offset: void 0
2790
2881
  });
2791
- const { items: paginated, total, hasMore } = paginate(interfaces, parsed.offset, parsed.limit);
2882
+ const {
2883
+ items: paginated,
2884
+ total,
2885
+ hasMore
2886
+ } = paginate(interfaces, parsed.offset, parsed.limit);
2792
2887
  return {
2793
2888
  content: listContent(
2794
2889
  "WireGuard interfaces",
@@ -2848,7 +2943,13 @@ var listPeersTool = {
2848
2943
  peers,
2849
2944
  total,
2850
2945
  parsed.offset,
2851
- (p) => compactFields(p, ["interface", "public-key", "endpoint-address", "allowed-address", "disabled"])
2946
+ (p) => compactFields(p, [
2947
+ "interface",
2948
+ "public-key",
2949
+ "endpoint-address",
2950
+ "allowed-address",
2951
+ "disabled"
2952
+ ])
2852
2953
  ),
2853
2954
  structuredContent: {
2854
2955
  routerId: context.routerId,
@@ -3018,10 +3119,7 @@ var manageWgIfaceTool = {
3018
3119
  const body = { name: parsed.name, mtu: String(parsed.mtu) };
3019
3120
  if (parsed.listenPort !== void 0) body["listen-port"] = String(parsed.listenPort);
3020
3121
  if (parsed.comment) body.comment = parsed.comment;
3021
- const created = await context.routerClient.create(
3022
- "interface/wireguard",
3023
- body
3024
- );
3122
+ const created = await context.routerClient.create("interface/wireguard", body);
3025
3123
  log11.info({ name: parsed.name, id: created[".id"] }, "WireGuard interface created");
3026
3124
  return {
3027
3125
  content: `Created WireGuard interface "${parsed.name}". Public key: ${created["public-key"] ?? "unknown"}`,
@@ -3366,7 +3464,6 @@ var manageDnsSettingsTool = {
3366
3464
  try {
3367
3465
  const results2 = await context.routerClient.get("ip/dns");
3368
3466
  const current = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
3369
- const id = current[".id"];
3370
3467
  const changes = {};
3371
3468
  const diff = [];
3372
3469
  if (parsed.servers !== void 0 && current.servers !== parsed.servers) {
@@ -3375,27 +3472,43 @@ var manageDnsSettingsTool = {
3375
3472
  }
3376
3473
  if (parsed.allowRemoteRequests !== void 0) {
3377
3474
  const next = String(parsed.allowRemoteRequests);
3378
- if (current["allow-remote-requests"] !== next) {
3475
+ if (isTrue(current["allow-remote-requests"]) !== parsed.allowRemoteRequests) {
3379
3476
  changes["allow-remote-requests"] = next;
3380
- diff.push({ property: "allow-remote-requests", before: current["allow-remote-requests"] ?? null, after: next });
3477
+ diff.push({
3478
+ property: "allow-remote-requests",
3479
+ before: current["allow-remote-requests"] === void 0 ? null : String(current["allow-remote-requests"]),
3480
+ after: next
3481
+ });
3381
3482
  }
3382
3483
  }
3383
3484
  if (parsed.maxUdpPacketSize !== void 0) {
3384
3485
  const next = String(parsed.maxUdpPacketSize);
3385
- if (current["max-udp-packet-size"] !== next) {
3486
+ if (!sameValue(current["max-udp-packet-size"], next)) {
3386
3487
  changes["max-udp-packet-size"] = next;
3387
- diff.push({ property: "max-udp-packet-size", before: current["max-udp-packet-size"] ?? null, after: next });
3488
+ diff.push({
3489
+ property: "max-udp-packet-size",
3490
+ before: current["max-udp-packet-size"] === void 0 ? null : String(current["max-udp-packet-size"]),
3491
+ after: next
3492
+ });
3388
3493
  }
3389
3494
  }
3390
3495
  if (parsed.cacheMaxTtl !== void 0 && current["cache-max-ttl"] !== parsed.cacheMaxTtl) {
3391
3496
  changes["cache-max-ttl"] = parsed.cacheMaxTtl;
3392
- diff.push({ property: "cache-max-ttl", before: current["cache-max-ttl"] ?? null, after: parsed.cacheMaxTtl });
3497
+ diff.push({
3498
+ property: "cache-max-ttl",
3499
+ before: current["cache-max-ttl"] ?? null,
3500
+ after: parsed.cacheMaxTtl
3501
+ });
3393
3502
  }
3394
3503
  if (parsed.cacheSize !== void 0) {
3395
3504
  const next = String(parsed.cacheSize);
3396
- if (current["cache-size"] !== next) {
3505
+ if (!sameValue(current["cache-size"], next)) {
3397
3506
  changes["cache-size"] = next;
3398
- diff.push({ property: "cache-size", before: current["cache-size"] ?? null, after: next });
3507
+ diff.push({
3508
+ property: "cache-size",
3509
+ before: current["cache-size"] === void 0 ? null : String(current["cache-size"]),
3510
+ after: next
3511
+ });
3399
3512
  }
3400
3513
  }
3401
3514
  if (Object.keys(changes).length === 0) {
@@ -3410,8 +3523,11 @@ var manageDnsSettingsTool = {
3410
3523
  structuredContent: { action: "dry_run", diff }
3411
3524
  };
3412
3525
  }
3413
- await context.routerClient.update("ip/dns", id, changes);
3414
- log12.info({ routerId: context.routerId, changes: Object.keys(changes) }, "DNS settings updated");
3526
+ await context.routerClient.execute("ip/dns/set", changes);
3527
+ log12.info(
3528
+ { routerId: context.routerId, changes: Object.keys(changes) },
3529
+ "DNS settings updated"
3530
+ );
3415
3531
  return {
3416
3532
  content: `Updated DNS settings on ${context.routerId}.`,
3417
3533
  structuredContent: { action: "updated", routerId: context.routerId, diff }
@@ -3421,7 +3537,12 @@ var manageDnsSettingsTool = {
3421
3537
  }
3422
3538
  }
3423
3539
  };
3424
- var dnsTools = [listDnsTool, manageDnsTool, getDnsSettingsTool, manageDnsSettingsTool];
3540
+ var dnsTools = [
3541
+ listDnsTool,
3542
+ manageDnsTool,
3543
+ getDnsSettingsTool,
3544
+ manageDnsSettingsTool
3545
+ ];
3425
3546
 
3426
3547
  // src/domain/tools/mangle-tools.ts
3427
3548
  import { z as z15 } from "zod";
@@ -3905,7 +4026,14 @@ var listRoutingRulesTool = {
3905
4026
  rules,
3906
4027
  rules.length,
3907
4028
  0,
3908
- (r) => compactFields(r, ["src-address", "dst-address", "action", "table", "interface", "disabled"])
4029
+ (r) => compactFields(r, [
4030
+ "src-address",
4031
+ "dst-address",
4032
+ "action",
4033
+ "table",
4034
+ "interface",
4035
+ "disabled"
4036
+ ])
3909
4037
  ),
3910
4038
  structuredContent: { routerId: context.routerId, rules, total: rules.length }
3911
4039
  };
@@ -5050,7 +5178,7 @@ var uploadFileInputSchema = z22.object({
5050
5178
  routerId,
5051
5179
  name: z22.string().describe("Target filename on the router (e.g. flash/my-script.rsc)"),
5052
5180
  content: z22.string().describe("File content to upload (text)"),
5053
- dryRun: z22.boolean().default(false).describe("Validate FTP connectivity without writing the file")
5181
+ dryRun: z22.boolean().default(false).describe("Validate SFTP or FTP connectivity without writing the target file")
5054
5182
  }).strict();
5055
5183
  var uploadFileTool = {
5056
5184
  name: "upload_file",
@@ -5074,14 +5202,27 @@ var uploadFileTool = {
5074
5202
  await context.sftpClient.upload(`${parsed.name}.mikromcp-probe`, "");
5075
5203
  return {
5076
5204
  content: `Dry run: SFTP connectivity to ${context.routerId} verified. Would upload "${parsed.name}".`,
5077
- structuredContent: { action: "dry_run", transport: "sftp", name: parsed.name, routerId: context.routerId }
5205
+ structuredContent: {
5206
+ action: "dry_run",
5207
+ transport: "sftp",
5208
+ name: parsed.name,
5209
+ routerId: context.routerId
5210
+ }
5078
5211
  };
5079
5212
  } catch (sftpErr) {
5080
- log20.warn({ err: sftpErr, routerId: context.routerId }, "SFTP dry-run probe failed \u2014 falling back to FTP");
5213
+ log20.warn(
5214
+ { err: sftpErr, routerId: context.routerId },
5215
+ "SFTP dry-run probe failed \u2014 falling back to FTP"
5216
+ );
5081
5217
  await context.ftpClient.connect();
5082
5218
  return {
5083
5219
  content: `Dry run: FTP connectivity to ${context.routerId} verified${ftpPlaintextNote}. Would upload "${parsed.name}".`,
5084
- structuredContent: { action: "dry_run", transport: "ftp", name: parsed.name, routerId: context.routerId }
5220
+ structuredContent: {
5221
+ action: "dry_run",
5222
+ transport: "ftp",
5223
+ name: parsed.name,
5224
+ routerId: context.routerId
5225
+ }
5085
5226
  };
5086
5227
  }
5087
5228
  }
@@ -5090,15 +5231,28 @@ var uploadFileTool = {
5090
5231
  log20.info({ name: parsed.name }, "File uploaded via SFTP");
5091
5232
  return {
5092
5233
  content: `Uploaded "${parsed.name}" to ${context.routerId} via SFTP.`,
5093
- structuredContent: { action: "uploaded", transport: "sftp", name: parsed.name, routerId: context.routerId }
5234
+ structuredContent: {
5235
+ action: "uploaded",
5236
+ transport: "sftp",
5237
+ name: parsed.name,
5238
+ routerId: context.routerId
5239
+ }
5094
5240
  };
5095
5241
  } catch (sftpErr) {
5096
- log20.warn({ err: sftpErr, routerId: context.routerId }, "SFTP upload failed \u2014 falling back to FTP");
5242
+ log20.warn(
5243
+ { err: sftpErr, routerId: context.routerId },
5244
+ "SFTP upload failed \u2014 falling back to FTP"
5245
+ );
5097
5246
  await context.ftpClient.upload(parsed.name, parsed.content);
5098
5247
  log20.info({ name: parsed.name }, "File uploaded via FTP");
5099
5248
  return {
5100
5249
  content: `Uploaded "${parsed.name}" to ${context.routerId}${ftpPlaintextNote}.`,
5101
- structuredContent: { action: "uploaded", transport: "ftp", name: parsed.name, routerId: context.routerId }
5250
+ structuredContent: {
5251
+ action: "uploaded",
5252
+ transport: "ftp",
5253
+ name: parsed.name,
5254
+ routerId: context.routerId
5255
+ }
5102
5256
  };
5103
5257
  }
5104
5258
  } catch (err) {
@@ -6235,12 +6389,12 @@ var RouterOSRestClient = class {
6235
6389
  const query = buildListQuery(options);
6236
6390
  let raw;
6237
6391
  if (query.method === "POST") {
6238
- raw = await this.doRequest("POST", `${this.baseUrl}/${path}`, query.body);
6392
+ raw = await this.doRequest("POST", `${this.baseUrl}/${path}/print`, query.body);
6239
6393
  } else {
6240
6394
  const qs = query.queryParams ? "?" + new URLSearchParams(query.queryParams).toString() : "";
6241
6395
  raw = await this.doRequest("GET", `${this.baseUrl}/${path}${qs}`);
6242
6396
  }
6243
- const rawArray = Array.isArray(raw) ? raw : [raw];
6397
+ const rawArray = Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw];
6244
6398
  const parsed = parseRecords(rawArray);
6245
6399
  if (options?.limit !== void 0 || options?.offset !== void 0) {
6246
6400
  const page = applyPagination(parsed, options?.limit, options?.offset);
@@ -6550,7 +6704,12 @@ var RUNTIME_FIELDS = /* @__PURE__ */ new Set([
6550
6704
  "tx-byte",
6551
6705
  "rx-packet",
6552
6706
  "tx-packet",
6553
- "uptime"
6707
+ "uptime",
6708
+ // Read-only fields on set-menu singletons. Harmless in a normal diff, but a
6709
+ // singleton is restored by writing the whole record back through /set, and
6710
+ // RouterOS rejects the request outright if it carries a read-only parameter.
6711
+ "cache-used",
6712
+ "dynamic-servers"
6554
6713
  ]);
6555
6714
  var ORDER_SENSITIVE_PATHS = /* @__PURE__ */ new Set([
6556
6715
  "ip/firewall/filter",
@@ -6586,16 +6745,16 @@ var SEMANTIC_KEYS = {
6586
6745
  "system/scheduler": ["name"],
6587
6746
  "system/script": ["name"],
6588
6747
  "system/package": ["name"],
6589
- "container": ["name"],
6590
- "certificate": ["name"],
6591
- "file": ["name"],
6748
+ container: ["name"],
6749
+ certificate: ["name"],
6750
+ file: ["name"],
6592
6751
  "interface/vrrp": ["name"],
6593
6752
  "ip/dhcp-server": ["name"],
6594
6753
  "ip/ipsec/peer": ["name"],
6595
6754
  "ip/pool": ["name"],
6596
6755
  "queue/simple": ["name"],
6597
6756
  "tool/netwatch": ["host"],
6598
- "user": ["name"]
6757
+ user: ["name"]
6599
6758
  };
6600
6759
  function normalizeValue(v) {
6601
6760
  if (v === true || v === "true" || v === "yes") return "true";
@@ -6641,6 +6800,24 @@ function computeRestorePlan(path, beforeRaw, currentRaw) {
6641
6800
  `Restored entries for ${path} are re-created at the end of the list \u2014 rule ORDER is not restored. Review ordering manually after rollback.`
6642
6801
  );
6643
6802
  }
6803
+ if (isSingleton(before, current)) {
6804
+ const beforeRecord = before[0];
6805
+ if (beforeRecord === void 0) {
6806
+ warnings.push(
6807
+ `Snapshot for ${path} holds no record for this singleton \u2014 its current settings are left untouched, as a set-menu singleton cannot be removed.`
6808
+ );
6809
+ return { path, toCreate: [], toRemove: [], toUpdate: [], warnings };
6810
+ }
6811
+ const currentRecord = current[0];
6812
+ if (currentRecord !== void 0 && recordsAreEqual(beforeRecord, currentRecord)) {
6813
+ return { path, toCreate: [], toRemove: [], toUpdate: [], warnings };
6814
+ }
6815
+ const { ".id": _id, ...data } = beforeRecord;
6816
+ const toSet = Object.fromEntries(
6817
+ Object.entries(data).map(([k, v]) => [k, String(v)])
6818
+ );
6819
+ return { path, toCreate: [], toRemove: [], toUpdate: [], toSet, warnings };
6820
+ }
6644
6821
  const keys = SEMANTIC_KEYS[path];
6645
6822
  const canUseKeys = keys !== void 0 && !hasDuplicateKeys(before, keys) && !hasDuplicateKeys(current, keys);
6646
6823
  if (!canUseKeys) {
@@ -6670,7 +6847,16 @@ function computeRestorePlan(path, beforeRaw, currentRaw) {
6670
6847
  const toRemove = current.filter((r) => !beforeByKey.has(semanticKey(r, keys))).map((r) => r[".id"]);
6671
6848
  return applyUserRestriction(path, { path, toCreate, toRemove, toUpdate, warnings });
6672
6849
  }
6850
+ function isSingleton(before, current) {
6851
+ if (before.length > 1 || current.length > 1) return false;
6852
+ if (before.length === 0 && current.length === 0) return false;
6853
+ return [...before, ...current].every((r) => r[".id"] === void 0);
6854
+ }
6673
6855
  async function applyRestorePlan(plan, client) {
6856
+ if (plan.toSet !== void 0) {
6857
+ await client.execute(`${plan.path}/set`, plan.toSet);
6858
+ return;
6859
+ }
6674
6860
  for (const id of plan.toRemove) {
6675
6861
  await client.remove(plan.path, id);
6676
6862
  }
@@ -6947,10 +7133,18 @@ function createChangeManagementTools(baseTools2) {
6947
7133
  const snapshotIds = [];
6948
7134
  for (const path of snapshotPathsFor(tool, stepParams)) {
6949
7135
  try {
6950
- const meta = await takeSnapshot(context.deviceClient, context.routerId, path, snapshotDir);
7136
+ const meta = await takeSnapshot(
7137
+ context.deviceClient,
7138
+ context.routerId,
7139
+ path,
7140
+ snapshotDir
7141
+ );
6951
7142
  snapshotIds.push(meta.id);
6952
7143
  } catch (err) {
6953
- log24.warn({ err, path, routerId: context.routerId, step: i }, "apply_plan step snapshot failed \u2014 proceeding without snapshot");
7144
+ log24.warn(
7145
+ { err, path, routerId: context.routerId, step: i },
7146
+ "apply_plan step snapshot failed \u2014 proceeding without snapshot"
7147
+ );
6954
7148
  }
6955
7149
  }
6956
7150
  const stepJournalId = recordAttempt({
@@ -6966,7 +7160,12 @@ function createChangeManagementTools(baseTools2) {
6966
7160
  try {
6967
7161
  const runStep = () => tool.handler(stepParams, context);
6968
7162
  const result = context.circuitBreaker ? await context.circuitBreaker.execute(runStep) : await runStep();
6969
- recordOutcome({ journalPath, journalId: stepJournalId, phase: "success", durationMs: Date.now() - stepStartMs });
7163
+ recordOutcome({
7164
+ journalPath,
7165
+ journalId: stepJournalId,
7166
+ phase: "success",
7167
+ durationMs: Date.now() - stepStartMs
7168
+ });
6970
7169
  results2.push({
6971
7170
  stepIndex: i,
6972
7171
  tool: step.tool,
@@ -6976,7 +7175,13 @@ function createChangeManagementTools(baseTools2) {
6976
7175
  });
6977
7176
  } catch (err) {
6978
7177
  const error = err instanceof MikroMCPError ? err : enrichError(err, { tool: step.tool });
6979
- recordOutcome({ journalPath, journalId: stepJournalId, phase: "failure", outcome: error.code, durationMs: Date.now() - stepStartMs });
7178
+ recordOutcome({
7179
+ journalPath,
7180
+ journalId: stepJournalId,
7181
+ phase: "failure",
7182
+ outcome: error.code,
7183
+ durationMs: Date.now() - stepStartMs
7184
+ });
6980
7185
  log24.error({ err: error, tool: step.tool, step: i }, "apply_plan step failed");
6981
7186
  return {
6982
7187
  content: `Apply failed at step ${i + 1}/${parsed.steps.length} (${step.tool}): ${error.message}. ${i} step(s) completed before failure.`,
@@ -7058,7 +7263,7 @@ function createChangeManagementTools(baseTools2) {
7058
7263
  restorePlans.push(plan);
7059
7264
  }
7060
7265
  const totalOps = restorePlans.reduce(
7061
- (sum, p) => sum + p.toCreate.length + p.toRemove.length + p.toUpdate.length,
7266
+ (sum, p) => sum + p.toCreate.length + p.toRemove.length + p.toUpdate.length + (p.toSet ? 1 : 0),
7062
7267
  0
7063
7268
  );
7064
7269
  const warnings = restorePlans.flatMap((p) => p.warnings);
@@ -7068,7 +7273,12 @@ ${warnings.map((w) => ` - ${w}`).join("\n")}` : "";
7068
7273
  if (parsed.dryRun) {
7069
7274
  return {
7070
7275
  content: `Rollback dry run for journal entry "${parsed.journalId}": ${totalOps} operation(s) would be applied across ${restorePlans.length} path(s).${warningSuffix}`,
7071
- structuredContent: { action: "dry_run", journalId: parsed.journalId, restorePlans, warnings }
7276
+ structuredContent: {
7277
+ action: "dry_run",
7278
+ journalId: parsed.journalId,
7279
+ restorePlans,
7280
+ warnings
7281
+ }
7072
7282
  };
7073
7283
  }
7074
7284
  for (const plan of restorePlans) {
@@ -7076,7 +7286,12 @@ ${warnings.map((w) => ` - ${w}`).join("\n")}` : "";
7076
7286
  }
7077
7287
  return {
7078
7288
  content: `Rolled back journal entry "${parsed.journalId}": ${totalOps} operation(s) applied across ${restorePlans.length} path(s).${warningSuffix}`,
7079
- structuredContent: { action: "rolled_back", journalId: parsed.journalId, restorePlans, warnings }
7289
+ structuredContent: {
7290
+ action: "rolled_back",
7291
+ journalId: parsed.journalId,
7292
+ restorePlans,
7293
+ warnings
7294
+ }
7080
7295
  };
7081
7296
  }
7082
7297
  };
@@ -7173,7 +7388,14 @@ var listIpsecPoliciesTool = {
7173
7388
  policies,
7174
7389
  allPolicies.length,
7175
7390
  0,
7176
- (p) => compactFields(p, ["src-address", "dst-address", "protocol", "action", "level", "disabled"])
7391
+ (p) => compactFields(p, [
7392
+ "src-address",
7393
+ "dst-address",
7394
+ "protocol",
7395
+ "action",
7396
+ "level",
7397
+ "disabled"
7398
+ ])
7177
7399
  ),
7178
7400
  structuredContent: {
7179
7401
  routerId: context.routerId,
@@ -7221,9 +7443,7 @@ var manageIpsecPeerTool = {
7221
7443
  limit: void 0,
7222
7444
  offset: void 0
7223
7445
  });
7224
- const existing = allPeers.find(
7225
- (p) => p.name === parsed.name
7226
- );
7446
+ const existing = allPeers.find((p) => p.name === parsed.name);
7227
7447
  if (parsed.action === "add") {
7228
7448
  if (existing) {
7229
7449
  if (existing.address === parsed.address) {
@@ -7382,9 +7602,8 @@ var manageIpsecPolicyTool = {
7382
7602
  limit: void 0,
7383
7603
  offset: void 0
7384
7604
  });
7385
- const tunnelStr = String(parsed.tunnel);
7386
7605
  const existing = all.find(
7387
- (p) => p["src-address"] === parsed.srcAddress && p["dst-address"] === parsed.dstAddress && p.tunnel === tunnelStr
7606
+ (p) => p["src-address"] === parsed.srcAddress && p["dst-address"] === parsed.dstAddress && isTrue(p.tunnel) === parsed.tunnel
7388
7607
  );
7389
7608
  if (parsed.action === "add") {
7390
7609
  if (existing) {
@@ -7420,7 +7639,7 @@ var manageIpsecPolicyTool = {
7420
7639
  const body = {
7421
7640
  "src-address": parsed.srcAddress,
7422
7641
  "dst-address": parsed.dstAddress,
7423
- tunnel: tunnelStr,
7642
+ tunnel: String(parsed.tunnel),
7424
7643
  action: parsed.ipsecAction,
7425
7644
  level: parsed.level
7426
7645
  };
@@ -7623,7 +7842,7 @@ var manageCertificateTool = {
7623
7842
  };
7624
7843
  }
7625
7844
  if (parsed.action === "trust") {
7626
- if (cert.trusted === "yes") {
7845
+ if (isTrue(cert.trusted)) {
7627
7846
  return {
7628
7847
  content: `Certificate "${parsed.name}" is already trusted. No changes made.`,
7629
7848
  structuredContent: { action: "already_trusted", name: parsed.name }
@@ -7646,7 +7865,7 @@ var manageCertificateTool = {
7646
7865
  structuredContent: { action: "trusted", name: parsed.name }
7647
7866
  };
7648
7867
  }
7649
- if (cert.trusted === "no") {
7868
+ if (cert.trusted !== void 0 && !isTrue(cert.trusted)) {
7650
7869
  return {
7651
7870
  content: `Certificate "${parsed.name}" is already untrusted. No changes made.`,
7652
7871
  structuredContent: { action: "already_untrusted", name: parsed.name }
@@ -7792,14 +8011,16 @@ var manageUserTool = {
7792
8011
  limit: void 0,
7793
8012
  offset: void 0
7794
8013
  });
7795
- const existing2 = allUsers2.find(
7796
- (u) => u.name === parsed.name
7797
- );
8014
+ const existing2 = allUsers2.find((u) => u.name === parsed.name);
7798
8015
  if (existing2) {
7799
8016
  if (existing2.group === parsed.group) {
7800
8017
  return {
7801
8018
  content: `User "${parsed.name}" already exists in group "${parsed.group}". No changes made.`,
7802
- structuredContent: { action: "already_exists", name: parsed.name, group: existing2.group }
8019
+ structuredContent: {
8020
+ action: "already_exists",
8021
+ name: parsed.name,
8022
+ group: existing2.group
8023
+ }
7803
8024
  };
7804
8025
  }
7805
8026
  throw new MikroMCPError({
@@ -7839,7 +8060,12 @@ var manageUserTool = {
7839
8060
  log27.info({ name: parsed.name, id: created[".id"] }, "User added");
7840
8061
  return {
7841
8062
  content: `Added user "${parsed.name}" in group "${parsed.group}".`,
7842
- structuredContent: { action: "created", routerId: context.routerId, name: parsed.name, group: parsed.group }
8063
+ structuredContent: {
8064
+ action: "created",
8065
+ routerId: context.routerId,
8066
+ name: parsed.name,
8067
+ group: parsed.group
8068
+ }
7843
8069
  };
7844
8070
  }
7845
8071
  if (parsed.action === "set-password") {
@@ -7859,9 +8085,7 @@ var manageUserTool = {
7859
8085
  limit: void 0,
7860
8086
  offset: void 0
7861
8087
  });
7862
- const existing2 = allUsers2.find(
7863
- (u) => u.name === parsed.name
7864
- );
8088
+ const existing2 = allUsers2.find((u) => u.name === parsed.name);
7865
8089
  if (!existing2) {
7866
8090
  throw new MikroMCPError({
7867
8091
  category: "NOT_FOUND" /* NOT_FOUND */,
@@ -7890,16 +8114,18 @@ var manageUserTool = {
7890
8114
  log27.info({ name: parsed.name }, "User password updated");
7891
8115
  return {
7892
8116
  content: `Password updated for user "${parsed.name}".`,
7893
- structuredContent: { action: "password_set", routerId: context.routerId, name: parsed.name }
8117
+ structuredContent: {
8118
+ action: "password_set",
8119
+ routerId: context.routerId,
8120
+ name: parsed.name
8121
+ }
7894
8122
  };
7895
8123
  }
7896
8124
  const allUsers = await context.routerClient.get("user", {
7897
8125
  limit: void 0,
7898
8126
  offset: void 0
7899
8127
  });
7900
- const existing = allUsers.find(
7901
- (u) => u.name === parsed.name
7902
- );
8128
+ const existing = allUsers.find((u) => u.name === parsed.name);
7903
8129
  if (parsed.action === "remove") {
7904
8130
  if (!existing) {
7905
8131
  return {
@@ -8053,7 +8279,11 @@ var manageUserGroupTool = {
8053
8279
  if (parsed.policy === void 0 || existing2.policy === parsed.policy) {
8054
8280
  return {
8055
8281
  content: `User group "${parsed.name}" already exists. No changes made.`,
8056
- structuredContent: { action: "already_exists", name: parsed.name, policy: existing2.policy }
8282
+ structuredContent: {
8283
+ action: "already_exists",
8284
+ name: parsed.name,
8285
+ policy: existing2.policy
8286
+ }
8057
8287
  };
8058
8288
  }
8059
8289
  throw new MikroMCPError({
@@ -8270,9 +8500,7 @@ var manageDhcpServerTool = {
8270
8500
  limit: void 0,
8271
8501
  offset: void 0
8272
8502
  });
8273
- const existing = allServers.find(
8274
- (s) => s.name === parsed.name
8275
- );
8503
+ const existing = allServers.find((s) => s.name === parsed.name);
8276
8504
  if (parsed.action === "add") {
8277
8505
  if (!parsed.interface) {
8278
8506
  throw new MikroMCPError({
@@ -8499,7 +8727,10 @@ var manageIpPoolTool = {
8499
8727
  snapshotPaths: ["ip/pool"],
8500
8728
  async handler(params, context) {
8501
8729
  const parsed = manageIpPoolInputSchema.parse(params);
8502
- log30.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP pool");
8730
+ log30.info(
8731
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
8732
+ "Managing IP pool"
8733
+ );
8503
8734
  try {
8504
8735
  const allPools = await context.routerClient.get("ip/pool", {
8505
8736
  limit: void 0,
@@ -8567,7 +8798,10 @@ var manageIpPoolTool = {
8567
8798
  if (parsed.dryRun) {
8568
8799
  return {
8569
8800
  content: `Dry run: Would remove IP pool "${parsed.name}".`,
8570
- structuredContent: { action: "dry_run", diff: [{ property: "name", before: parsed.name, after: null }] }
8801
+ structuredContent: {
8802
+ action: "dry_run",
8803
+ diff: [{ property: "name", before: parsed.name, after: null }]
8804
+ }
8571
8805
  };
8572
8806
  }
8573
8807
  await context.routerClient.remove("ip/pool", existing[".id"]);
@@ -8911,7 +9145,7 @@ var manageVrrpInstanceTool = {
8911
9145
  });
8912
9146
  }
8913
9147
  if (existing) {
8914
- if (existing.interface === parsed.interface && existing.vrid === String(parsed.vrid)) {
9148
+ if (existing.interface === parsed.interface && sameValue(existing.vrid, parsed.vrid)) {
8915
9149
  return {
8916
9150
  content: `VRRP instance "${parsed.name}" already exists with the same interface and VRID. No changes made.`,
8917
9151
  structuredContent: { action: "already_exists", instance: existing }
@@ -9360,7 +9594,14 @@ var listNeighborsTool = {
9360
9594
  neighbors,
9361
9595
  allNeighbors.length,
9362
9596
  0,
9363
- (n) => compactFields(n, ["interface", "address", "mac-address", "identity", "platform", "board"])
9597
+ (n) => compactFields(n, [
9598
+ "interface",
9599
+ "address",
9600
+ "mac-address",
9601
+ "identity",
9602
+ "platform",
9603
+ "board"
9604
+ ])
9364
9605
  ),
9365
9606
  structuredContent: {
9366
9607
  routerId: context.routerId,
@@ -9411,7 +9652,14 @@ var listArpEntriesTool = {
9411
9652
  entries,
9412
9653
  allEntries.length,
9413
9654
  0,
9414
- (e) => compactFields(e, ["address", "mac-address", "interface", "status", "complete", "dynamic"])
9655
+ (e) => compactFields(e, [
9656
+ "address",
9657
+ "mac-address",
9658
+ "interface",
9659
+ "status",
9660
+ "complete",
9661
+ "dynamic"
9662
+ ])
9415
9663
  ),
9416
9664
  structuredContent: {
9417
9665
  routerId: context.routerId,
@@ -9451,12 +9699,15 @@ var manageNtpClientTool = {
9451
9699
  try {
9452
9700
  const results2 = await context.routerClient.get("system/ntp/client");
9453
9701
  const current = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
9454
- const id = current[".id"];
9455
9702
  const changes = {};
9456
9703
  const diff = [];
9457
- if (parsed.enabled !== void 0 && current.enabled !== String(parsed.enabled)) {
9704
+ if (parsed.enabled !== void 0 && isTrue(current.enabled) !== parsed.enabled) {
9458
9705
  changes.enabled = String(parsed.enabled);
9459
- diff.push({ property: "enabled", before: current.enabled, after: String(parsed.enabled) });
9706
+ diff.push({
9707
+ property: "enabled",
9708
+ before: current.enabled === void 0 ? void 0 : String(current.enabled),
9709
+ after: String(parsed.enabled)
9710
+ });
9460
9711
  }
9461
9712
  if (parsed.mode !== void 0 && current.mode !== parsed.mode) {
9462
9713
  changes.mode = parsed.mode;
@@ -9486,7 +9737,7 @@ var manageNtpClientTool = {
9486
9737
  structuredContent: { action: "dry_run", diff }
9487
9738
  };
9488
9739
  }
9489
- await context.routerClient.update("system/ntp/client", id, changes);
9740
+ await context.routerClient.execute("system/ntp/client/set", changes);
9490
9741
  log33.info({ routerId: context.routerId }, "NTP client updated");
9491
9742
  return {
9492
9743
  content: `NTP client on ${context.routerId} updated.`,
@@ -9517,7 +9768,9 @@ function sanitizeComment4(comment) {
9517
9768
  var manageVlanInputSchema = z34.object({
9518
9769
  routerId,
9519
9770
  action: z34.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
9520
- name: z34.string().regex(/^[a-zA-Z0-9_-]+$/).max(15).describe("VLAN interface name \u2014 idempotency key (alphanumeric, hyphens, underscores, max 15 chars)"),
9771
+ name: z34.string().regex(/^[a-zA-Z0-9_-]+$/).max(15).describe(
9772
+ "VLAN interface name \u2014 idempotency key (alphanumeric, hyphens, underscores, max 15 chars)"
9773
+ ),
9521
9774
  vlanId: z34.number().int().min(1).max(4094).optional().describe("VLAN ID (1-4094; required for add)"),
9522
9775
  parentInterface: z34.string().optional().describe("Parent interface name (e.g., ether1, bridge1; required for add)"),
9523
9776
  mtu: z34.number().int().min(68).max(9e3).default(1500).describe("MTU size (add only)"),
@@ -9540,13 +9793,18 @@ var manageVlanTool = {
9540
9793
  async handler(params, context) {
9541
9794
  const parsed = manageVlanInputSchema.parse(params);
9542
9795
  const comment = sanitizeComment4(parsed.comment);
9543
- log34.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing VLAN");
9796
+ log34.info(
9797
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
9798
+ "Managing VLAN"
9799
+ );
9544
9800
  try {
9545
9801
  const allVlans = await context.routerClient.get("interface/vlan", {
9546
9802
  limit: void 0,
9547
9803
  offset: void 0
9548
9804
  });
9549
- const existing = allVlans.find((v) => v.name === parsed.name);
9805
+ const existing = allVlans.find(
9806
+ (v) => sameValue(v.name, parsed.name)
9807
+ );
9550
9808
  if (parsed.action === "add") {
9551
9809
  if (parsed.vlanId === void 0) {
9552
9810
  throw new MikroMCPError({
@@ -9563,11 +9821,14 @@ var manageVlanTool = {
9563
9821
  code: "VLAN_PARENT_REQUIRED",
9564
9822
  message: "parentInterface is required when adding a VLAN.",
9565
9823
  details: { name: parsed.name },
9566
- recoverability: { retryable: false, suggestedAction: "Provide the parent interface name." }
9824
+ recoverability: {
9825
+ retryable: false,
9826
+ suggestedAction: "Provide the parent interface name."
9827
+ }
9567
9828
  });
9568
9829
  }
9569
9830
  if (existing) {
9570
- if (existing["vlan-id"] === String(parsed.vlanId) && existing.interface === parsed.parentInterface) {
9831
+ if (sameValue(existing["vlan-id"], parsed.vlanId) && existing.interface === parsed.parentInterface) {
9571
9832
  log34.info({ name: parsed.name }, "VLAN already exists with matching config");
9572
9833
  return {
9573
9834
  content: `VLAN "${parsed.name}" already exists with VLAN ID ${parsed.vlanId} on ${parsed.parentInterface}. No changes made.`,
@@ -9628,7 +9889,10 @@ var manageVlanTool = {
9628
9889
  if (parsed.dryRun) {
9629
9890
  return {
9630
9891
  content: `Dry run: Would remove VLAN "${parsed.name}".`,
9631
- structuredContent: { action: "dry_run", diff: [{ property: "name", before: parsed.name, after: null }] }
9892
+ structuredContent: {
9893
+ action: "dry_run",
9894
+ diff: [{ property: "name", before: parsed.name, after: null }]
9895
+ }
9632
9896
  };
9633
9897
  }
9634
9898
  await context.routerClient.remove("interface/vlan", existing[".id"]);
@@ -9656,12 +9920,20 @@ var manageVlanTool = {
9656
9920
  content: `Dry run: Would ${parsed.action} VLAN "${parsed.name}".`,
9657
9921
  structuredContent: {
9658
9922
  action: "dry_run",
9659
- diff: [{ property: "disabled", before: existing.disabled, after: parsed.action === "disable" ? "true" : "false" }]
9923
+ diff: [
9924
+ {
9925
+ property: "disabled",
9926
+ before: existing.disabled,
9927
+ after: parsed.action === "disable" ? "true" : "false"
9928
+ }
9929
+ ]
9660
9930
  }
9661
9931
  };
9662
9932
  }
9663
9933
  const disabledValue = parsed.action === "disable" ? "true" : "false";
9664
- await context.routerClient.update("interface/vlan", existing[".id"], { disabled: disabledValue });
9934
+ await context.routerClient.update("interface/vlan", existing[".id"], {
9935
+ disabled: disabledValue
9936
+ });
9665
9937
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
9666
9938
  log34.info({ name: parsed.name, action: resultAction }, "VLAN updated");
9667
9939
  return {
@@ -9759,7 +10031,10 @@ var manageDhcpClientTool = {
9759
10031
  snapshotPaths: ["ip/dhcp-client"],
9760
10032
  async handler(params, context) {
9761
10033
  const parsed = manageDhcpClientInputSchema.parse(params);
9762
- log35.info({ routerId: context.routerId, action: parsed.action, interface: parsed.interface }, "Managing DHCP client");
10034
+ log35.info(
10035
+ { routerId: context.routerId, action: parsed.action, interface: parsed.interface },
10036
+ "Managing DHCP client"
10037
+ );
9763
10038
  try {
9764
10039
  const allClients = await context.routerClient.get("ip/dhcp-client", {
9765
10040
  limit: void 0,
@@ -9780,7 +10055,11 @@ var manageDhcpClientTool = {
9780
10055
  { property: "interface", before: null, after: parsed.interface },
9781
10056
  { property: "use-peer-dns", before: null, after: parsed.usePeerDns ? "yes" : "no" },
9782
10057
  { property: "use-peer-ntp", before: null, after: parsed.usePeerNtp ? "yes" : "no" },
9783
- { property: "add-default-route", before: null, after: parsed.addDefaultRoute ? "yes" : "no" }
10058
+ {
10059
+ property: "add-default-route",
10060
+ before: null,
10061
+ after: parsed.addDefaultRoute ? "yes" : "no"
10062
+ }
9784
10063
  ];
9785
10064
  return {
9786
10065
  content: `Dry run: Would add DHCP client on "${parsed.interface}".`,
@@ -9821,7 +10100,11 @@ var manageDhcpClientTool = {
9821
10100
  log35.info({ interface: parsed.interface }, "DHCP client removed");
9822
10101
  return {
9823
10102
  content: `Removed DHCP client on "${parsed.interface}".`,
9824
- structuredContent: { action: "removed", interface: parsed.interface, id: existing[".id"] }
10103
+ structuredContent: {
10104
+ action: "removed",
10105
+ interface: parsed.interface,
10106
+ id: existing[".id"]
10107
+ }
9825
10108
  };
9826
10109
  }
9827
10110
  if (!existing) {
@@ -9853,12 +10136,18 @@ var manageDhcpClientTool = {
9853
10136
  };
9854
10137
  }
9855
10138
  const disabledValue = parsed.action === "disable" ? "true" : "false";
9856
- await context.routerClient.update("ip/dhcp-client", existing[".id"], { disabled: disabledValue });
10139
+ await context.routerClient.update("ip/dhcp-client", existing[".id"], {
10140
+ disabled: disabledValue
10141
+ });
9857
10142
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
9858
10143
  log35.info({ interface: parsed.interface, action: resultAction }, "DHCP client updated");
9859
10144
  return {
9860
10145
  content: `DHCP client on "${parsed.interface}" ${resultAction}.`,
9861
- structuredContent: { action: resultAction, interface: parsed.interface, id: existing[".id"] }
10146
+ structuredContent: {
10147
+ action: resultAction,
10148
+ interface: parsed.interface,
10149
+ id: existing[".id"]
10150
+ }
9862
10151
  };
9863
10152
  } catch (err) {
9864
10153
  throw toolError(err, context, "manage_dhcp_client");
@@ -9870,7 +10159,16 @@ var dhcpClientTools = [listDhcpClientsTool, manageDhcpClientTool];
9870
10159
  // src/domain/tools/ip-service-tools.ts
9871
10160
  import { z as z36 } from "zod";
9872
10161
  var log36 = createLogger("ip-service-tools");
9873
- var SERVICE_NAMES = ["api", "api-ssl", "ssh", "telnet", "www", "www-ssl", "winbox", "ftp"];
10162
+ var SERVICE_NAMES = [
10163
+ "api",
10164
+ "api-ssl",
10165
+ "ssh",
10166
+ "telnet",
10167
+ "www",
10168
+ "www-ssl",
10169
+ "winbox",
10170
+ "ftp"
10171
+ ];
9874
10172
  var listIpServicesInputSchema = z36.object({
9875
10173
  routerId,
9876
10174
  name: z36.enum(SERVICE_NAMES).optional().describe("Filter by service name (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp)"),
@@ -9946,13 +10244,18 @@ var manageIpServiceTool = {
9946
10244
  },
9947
10245
  async handler(params, context) {
9948
10246
  const parsed = manageIpServiceInputSchema.parse(params);
9949
- log36.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP service");
10247
+ log36.info(
10248
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
10249
+ "Managing IP service"
10250
+ );
9950
10251
  try {
9951
10252
  const allServices = await context.routerClient.get("ip/service", {
9952
10253
  limit: void 0,
9953
10254
  offset: void 0
9954
10255
  });
9955
- const existing = allServices.find((s) => s.name === parsed.name);
10256
+ const existing = allServices.find(
10257
+ (s) => s.name === parsed.name
10258
+ );
9956
10259
  if (!existing) {
9957
10260
  throw new MikroMCPError({
9958
10261
  category: "NOT_FOUND" /* NOT_FOUND */,
@@ -10097,13 +10400,18 @@ var managePppoeClientTool = {
10097
10400
  snapshotPaths: [PPPOE_PATH],
10098
10401
  async handler(params, context) {
10099
10402
  const parsed = managePppoeClientInputSchema.parse(params);
10100
- log37.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing PPPoE client");
10403
+ log37.info(
10404
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
10405
+ "Managing PPPoE client"
10406
+ );
10101
10407
  try {
10102
10408
  const allClients = await context.routerClient.get(PPPOE_PATH, {
10103
10409
  limit: void 0,
10104
10410
  offset: void 0
10105
10411
  });
10106
- const existing = allClients.find((c) => c.name === parsed.name);
10412
+ const existing = allClients.find(
10413
+ (c) => sameValue(c.name, parsed.name)
10414
+ );
10107
10415
  if (parsed.action === "add") {
10108
10416
  if (!parsed.interface) {
10109
10417
  throw new MikroMCPError({
@@ -10163,10 +10471,18 @@ var managePppoeClientTool = {
10163
10471
  diff.push({ property: "service-name", before: null, after: parsed.serviceName });
10164
10472
  }
10165
10473
  if (parsed.addDefaultRoute !== void 0) {
10166
- diff.push({ property: "add-default-route", before: null, after: parsed.addDefaultRoute ? "yes" : "no" });
10474
+ diff.push({
10475
+ property: "add-default-route",
10476
+ before: null,
10477
+ after: parsed.addDefaultRoute ? "yes" : "no"
10478
+ });
10167
10479
  }
10168
10480
  if (parsed.dialOnDemand !== void 0) {
10169
- diff.push({ property: "dial-on-demand", before: null, after: parsed.dialOnDemand ? "yes" : "no" });
10481
+ diff.push({
10482
+ property: "dial-on-demand",
10483
+ before: null,
10484
+ after: parsed.dialOnDemand ? "yes" : "no"
10485
+ });
10170
10486
  }
10171
10487
  if (parsed.dryRun) {
10172
10488
  return {
@@ -10181,8 +10497,10 @@ var managePppoeClientTool = {
10181
10497
  };
10182
10498
  if (parsed.password !== void 0) body.password = parsed.password;
10183
10499
  if (parsed.serviceName !== void 0) body["service-name"] = parsed.serviceName;
10184
- if (parsed.addDefaultRoute !== void 0) body["add-default-route"] = parsed.addDefaultRoute ? "yes" : "no";
10185
- if (parsed.dialOnDemand !== void 0) body["dial-on-demand"] = parsed.dialOnDemand ? "yes" : "no";
10500
+ if (parsed.addDefaultRoute !== void 0)
10501
+ body["add-default-route"] = parsed.addDefaultRoute ? "yes" : "no";
10502
+ if (parsed.dialOnDemand !== void 0)
10503
+ body["dial-on-demand"] = parsed.dialOnDemand ? "yes" : "no";
10186
10504
  const created = await context.routerClient.create(PPPOE_PATH, body);
10187
10505
  log37.info({ name: parsed.name, id: created[".id"] }, "PPPoE client created");
10188
10506
  return {
@@ -10216,21 +10534,29 @@ var managePppoeClientTool = {
10216
10534
  }
10217
10535
  if (parsed.serviceName !== void 0 && existing["service-name"] !== parsed.serviceName) {
10218
10536
  updates["service-name"] = parsed.serviceName;
10219
- diff.push({ property: "service-name", before: existing["service-name"] ?? null, after: parsed.serviceName });
10537
+ diff.push({
10538
+ property: "service-name",
10539
+ before: existing["service-name"] ?? null,
10540
+ after: parsed.serviceName
10541
+ });
10220
10542
  }
10221
- if (parsed.addDefaultRoute !== void 0) {
10543
+ if (parsed.addDefaultRoute !== void 0 && isTrue(existing["add-default-route"]) !== parsed.addDefaultRoute) {
10222
10544
  const desired = parsed.addDefaultRoute ? "yes" : "no";
10223
- if (existing["add-default-route"] !== desired) {
10224
- updates["add-default-route"] = desired;
10225
- diff.push({ property: "add-default-route", before: existing["add-default-route"] ?? null, after: desired });
10226
- }
10545
+ updates["add-default-route"] = desired;
10546
+ diff.push({
10547
+ property: "add-default-route",
10548
+ before: existing["add-default-route"] === void 0 ? null : String(existing["add-default-route"]),
10549
+ after: desired
10550
+ });
10227
10551
  }
10228
- if (parsed.dialOnDemand !== void 0) {
10552
+ if (parsed.dialOnDemand !== void 0 && isTrue(existing["dial-on-demand"]) !== parsed.dialOnDemand) {
10229
10553
  const desired = parsed.dialOnDemand ? "yes" : "no";
10230
- if (existing["dial-on-demand"] !== desired) {
10231
- updates["dial-on-demand"] = desired;
10232
- diff.push({ property: "dial-on-demand", before: existing["dial-on-demand"] ?? null, after: desired });
10233
- }
10554
+ updates["dial-on-demand"] = desired;
10555
+ diff.push({
10556
+ property: "dial-on-demand",
10557
+ before: existing["dial-on-demand"] === void 0 ? null : String(existing["dial-on-demand"]),
10558
+ after: desired
10559
+ });
10234
10560
  }
10235
10561
  if (parsed.password !== void 0) {
10236
10562
  updates.password = parsed.password;
@@ -10363,13 +10689,18 @@ var manageOvpnClientTool = {
10363
10689
  snapshotPaths: [OVPN_CLIENT_PATH],
10364
10690
  async handler(params, context) {
10365
10691
  const parsed = manageOvpnClientInputSchema.parse(params);
10366
- log38.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing OpenVPN client");
10692
+ log38.info(
10693
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
10694
+ "Managing OpenVPN client"
10695
+ );
10367
10696
  try {
10368
10697
  const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
10369
10698
  limit: void 0,
10370
10699
  offset: void 0
10371
10700
  });
10372
- const existing = allClients.find((c) => c.name === parsed.name);
10701
+ const existing = allClients.find(
10702
+ (c) => sameValue(c.name, parsed.name)
10703
+ );
10373
10704
  if (parsed.action === "add") {
10374
10705
  if (!parsed.connectTo) {
10375
10706
  throw new MikroMCPError({
@@ -10452,11 +10783,19 @@ var manageOvpnClientTool = {
10452
10783
  const diff = [];
10453
10784
  if (parsed.connectTo !== void 0 && existing["connect-to"] !== parsed.connectTo) {
10454
10785
  updates["connect-to"] = parsed.connectTo;
10455
- diff.push({ property: "connect-to", before: existing["connect-to"] ?? null, after: parsed.connectTo });
10786
+ diff.push({
10787
+ property: "connect-to",
10788
+ before: existing["connect-to"] ?? null,
10789
+ after: parsed.connectTo
10790
+ });
10456
10791
  }
10457
- if (parsed.port !== void 0 && existing.port !== String(parsed.port)) {
10792
+ if (parsed.port !== void 0 && !sameValue(existing.port, parsed.port)) {
10458
10793
  updates.port = String(parsed.port);
10459
- diff.push({ property: "port", before: existing.port ?? null, after: String(parsed.port) });
10794
+ diff.push({
10795
+ property: "port",
10796
+ before: existing.port === void 0 ? null : String(existing.port),
10797
+ after: String(parsed.port)
10798
+ });
10460
10799
  }
10461
10800
  if (parsed.mode !== void 0 && existing.mode !== parsed.mode) {
10462
10801
  updates.mode = parsed.mode;
@@ -10464,11 +10803,19 @@ var manageOvpnClientTool = {
10464
10803
  }
10465
10804
  if (parsed.protocol !== void 0 && existing.protocol !== parsed.protocol) {
10466
10805
  updates.protocol = parsed.protocol;
10467
- diff.push({ property: "protocol", before: existing.protocol ?? null, after: parsed.protocol });
10806
+ diff.push({
10807
+ property: "protocol",
10808
+ before: existing.protocol ?? null,
10809
+ after: parsed.protocol
10810
+ });
10468
10811
  }
10469
10812
  if (parsed.certificate !== void 0 && existing.certificate !== parsed.certificate) {
10470
10813
  updates.certificate = parsed.certificate;
10471
- diff.push({ property: "certificate", before: existing.certificate ?? null, after: parsed.certificate });
10814
+ diff.push({
10815
+ property: "certificate",
10816
+ before: existing.certificate ?? null,
10817
+ after: parsed.certificate
10818
+ });
10472
10819
  }
10473
10820
  if (parsed.user !== void 0 && existing.user !== parsed.user) {
10474
10821
  updates.user = parsed.user;
@@ -10540,8 +10887,33 @@ function fetchOvpnServer(allRecords, routerId2) {
10540
10887
  }
10541
10888
  });
10542
10889
  }
10890
+ if (allRecords.length > 1) {
10891
+ const names = allRecords.map((r) => r.name ?? "?").join(", ");
10892
+ throw new MikroMCPError({
10893
+ category: "CONFLICT" /* CONFLICT */,
10894
+ code: "OVPN_SERVER_AMBIGUOUS",
10895
+ message: `Multiple OpenVPN server instances exist (${names}); this tool cannot pick one safely.`,
10896
+ details: { routerId: routerId2, instances: names },
10897
+ recoverability: {
10898
+ retryable: false,
10899
+ suggestedAction: "Remove the extra instances so exactly one remains.",
10900
+ alternativeTools: []
10901
+ }
10902
+ });
10903
+ }
10543
10904
  return allRecords[0];
10544
10905
  }
10906
+ async function writeOvpnServer(context, server, changes) {
10907
+ const serverId = server[".id"];
10908
+ if (serverId !== void 0) {
10909
+ await context.routerClient.update(OVPN_SERVER_PATH, serverId, changes);
10910
+ } else {
10911
+ await context.routerClient.execute(`${OVPN_SERVER_PATH}/set`, changes);
10912
+ }
10913
+ }
10914
+ function ovpnServerEnabled(server) {
10915
+ return server.enabled !== void 0 ? isTrue(server.enabled) : !isTrue(server.disabled);
10916
+ }
10545
10917
  var getOvpnServerTool = {
10546
10918
  name: "get_ovpn_server",
10547
10919
  title: "Get OpenVPN Server",
@@ -10563,7 +10935,7 @@ var getOvpnServerTool = {
10563
10935
  });
10564
10936
  const server = fetchOvpnServer(records, context.routerId);
10565
10937
  return {
10566
- content: `OpenVPN server on ${context.routerId}: enabled=${server.enabled}, port=${server.port}, protocol=${server.protocol}`,
10938
+ content: `OpenVPN server on ${context.routerId}: enabled=${ovpnServerEnabled(server)}, port=${server.port}, protocol=${server.protocol}`,
10567
10939
  structuredContent: { routerId: context.routerId, server }
10568
10940
  };
10569
10941
  } catch (err) {
@@ -10578,7 +10950,15 @@ var manageOvpnServerInputSchema = z38.object({
10578
10950
  mode: z38.enum(["ip", "ethernet"]).optional().describe("Tunnel mode (set action only)"),
10579
10951
  protocol: z38.enum(["tcp-server", "udp"]).optional().describe("Transport protocol (set action only)"),
10580
10952
  certificate: z38.string().optional().describe("Server certificate name (set action only)"),
10581
- cipher: z38.enum(["blowfish128", "aes128-cbc", "aes192-cbc", "aes256-cbc", "aes128-gcm", "aes256-gcm", "none"]).optional().describe("Encryption cipher (set action only)"),
10953
+ cipher: z38.enum([
10954
+ "blowfish128",
10955
+ "aes128-cbc",
10956
+ "aes192-cbc",
10957
+ "aes256-cbc",
10958
+ "aes128-gcm",
10959
+ "aes256-gcm",
10960
+ "none"
10961
+ ]).optional().describe("Encryption cipher (set action only)"),
10582
10962
  auth: z38.enum(["md5", "sha1", "sha256", "sha512", "null"]).optional().describe("Authentication algorithm (set action only)"),
10583
10963
  dryRun
10584
10964
  }).strict();
@@ -10603,32 +10983,42 @@ var manageOvpnServerTool = {
10603
10983
  offset: void 0
10604
10984
  });
10605
10985
  const server = fetchOvpnServer(records, context.routerId);
10606
- const serverId = server[".id"];
10607
10986
  if (parsed.action === "enable" || parsed.action === "disable") {
10608
10987
  const desiredEnabled = parsed.action === "enable";
10609
- const currentEnabled = server.enabled === "yes";
10988
+ const legacyDialect = server.enabled !== void 0;
10989
+ const currentEnabled = ovpnServerEnabled(server);
10610
10990
  if (desiredEnabled === currentEnabled) {
10611
10991
  return {
10612
10992
  content: `OpenVPN server on ${context.routerId} is already ${parsed.action}d. No changes made.`,
10613
10993
  structuredContent: { action: "no_change", server }
10614
10994
  };
10615
10995
  }
10996
+ const change = legacyDialect ? {
10997
+ property: "enabled",
10998
+ before: String(server.enabled),
10999
+ after: desiredEnabled ? "yes" : "no"
11000
+ } : {
11001
+ property: "disabled",
11002
+ before: server.disabled === void 0 ? null : String(server.disabled),
11003
+ after: desiredEnabled ? "no" : "yes"
11004
+ };
10616
11005
  if (parsed.dryRun) {
10617
11006
  return {
10618
11007
  content: `Dry run: Would ${parsed.action} OpenVPN server on ${context.routerId}.`,
10619
11008
  structuredContent: {
10620
11009
  action: "dry_run",
10621
- diff: [{ property: "enabled", before: server.enabled, after: desiredEnabled ? "yes" : "no" }]
11010
+ diff: [change]
10622
11011
  }
10623
11012
  };
10624
11013
  }
10625
- await context.routerClient.update(OVPN_SERVER_PATH, serverId, {
10626
- enabled: desiredEnabled ? "yes" : "no"
10627
- });
11014
+ await writeOvpnServer(context, server, { [change.property]: change.after });
10628
11015
  log38.info({ routerId: context.routerId, action: parsed.action }, "OpenVPN server updated");
10629
11016
  return {
10630
11017
  content: `OpenVPN server on ${context.routerId} ${parsed.action}d.`,
10631
- structuredContent: { action: parsed.action === "enable" ? "enabled" : "disabled", server }
11018
+ structuredContent: {
11019
+ action: parsed.action === "enable" ? "enabled" : "disabled",
11020
+ server
11021
+ }
10632
11022
  };
10633
11023
  }
10634
11024
  const hasFields = parsed.port !== void 0 || parsed.mode !== void 0 || parsed.protocol !== void 0 || parsed.certificate !== void 0 || parsed.cipher !== void 0 || parsed.auth !== void 0;
@@ -10647,9 +11037,13 @@ var manageOvpnServerTool = {
10647
11037
  }
10648
11038
  const updates = {};
10649
11039
  const diff = [];
10650
- if (parsed.port !== void 0 && server.port !== String(parsed.port)) {
11040
+ if (parsed.port !== void 0 && !sameValue(server.port, parsed.port)) {
10651
11041
  updates.port = String(parsed.port);
10652
- diff.push({ property: "port", before: server.port ?? null, after: String(parsed.port) });
11042
+ diff.push({
11043
+ property: "port",
11044
+ before: server.port === void 0 ? null : String(server.port),
11045
+ after: String(parsed.port)
11046
+ });
10653
11047
  }
10654
11048
  if (parsed.mode !== void 0 && server.mode !== parsed.mode) {
10655
11049
  updates.mode = parsed.mode;
@@ -10657,11 +11051,19 @@ var manageOvpnServerTool = {
10657
11051
  }
10658
11052
  if (parsed.protocol !== void 0 && server.protocol !== parsed.protocol) {
10659
11053
  updates.protocol = parsed.protocol;
10660
- diff.push({ property: "protocol", before: server.protocol ?? null, after: parsed.protocol });
11054
+ diff.push({
11055
+ property: "protocol",
11056
+ before: server.protocol ?? null,
11057
+ after: parsed.protocol
11058
+ });
10661
11059
  }
10662
11060
  if (parsed.certificate !== void 0 && server.certificate !== parsed.certificate) {
10663
11061
  updates.certificate = parsed.certificate;
10664
- diff.push({ property: "certificate", before: server.certificate ?? null, after: parsed.certificate });
11062
+ diff.push({
11063
+ property: "certificate",
11064
+ before: server.certificate ?? null,
11065
+ after: parsed.certificate
11066
+ });
10665
11067
  }
10666
11068
  if (parsed.cipher !== void 0 && server.cipher !== parsed.cipher) {
10667
11069
  updates.cipher = parsed.cipher;
@@ -10683,7 +11085,7 @@ var manageOvpnServerTool = {
10683
11085
  structuredContent: { action: "dry_run", diff }
10684
11086
  };
10685
11087
  }
10686
- await context.routerClient.update(OVPN_SERVER_PATH, serverId, updates);
11088
+ await writeOvpnServer(context, server, updates);
10687
11089
  log38.info({ routerId: context.routerId }, "OpenVPN server configuration updated");
10688
11090
  return {
10689
11091
  content: `Updated OpenVPN server configuration on ${context.routerId}.`,
@@ -10714,12 +11116,20 @@ var listPppProfilesTool = {
10714
11116
  title: "List PPP Profiles",
10715
11117
  description: "List PPP profiles including the built-in default and default-encryption profiles.",
10716
11118
  inputSchema: listPppProfilesInputSchema,
10717
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
11119
+ annotations: {
11120
+ readOnlyHint: true,
11121
+ destructiveHint: false,
11122
+ idempotentHint: true,
11123
+ openWorldHint: false
11124
+ },
10718
11125
  async handler(params, context) {
10719
11126
  const parsed = listPppProfilesInputSchema.parse(params);
10720
11127
  try {
10721
11128
  log39.info({ routerId: context.routerId }, "Listing PPP profiles");
10722
- const all = await context.routerClient.get("ppp/profile", { limit: void 0, offset: void 0 });
11129
+ const all = await context.routerClient.get("ppp/profile", {
11130
+ limit: void 0,
11131
+ offset: void 0
11132
+ });
10723
11133
  const filtered = parsed.name ? all.filter((p) => p.name === parsed.name) : all;
10724
11134
  const profiles = filtered.slice(0, parsed.limit);
10725
11135
  return {
@@ -10731,7 +11141,12 @@ var listPppProfilesTool = {
10731
11141
  0,
10732
11142
  (p) => compactFields(p, ["name", "local-address", "remote-address", "dns-server", "rate-limit"])
10733
11143
  ),
10734
- structuredContent: { routerId: context.routerId, profiles, total: all.length, returned: profiles.length }
11144
+ structuredContent: {
11145
+ routerId: context.routerId,
11146
+ profiles,
11147
+ total: all.length,
11148
+ returned: profiles.length
11149
+ }
10735
11150
  };
10736
11151
  } catch (err) {
10737
11152
  throw toolError(err, context, "list_ppp_profiles");
@@ -10755,13 +11170,24 @@ var managePppProfileTool = {
10755
11170
  title: "Manage PPP Profile",
10756
11171
  description: "Add, update, or remove a PPP profile. Idempotent by name. update returns no_change when requested values match. Built-in profiles (default, default-encryption) cannot be removed \u2014 RouterOS blocks this and the error is surfaced.",
10757
11172
  inputSchema: managePppProfileInputSchema,
10758
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
11173
+ annotations: {
11174
+ readOnlyHint: false,
11175
+ destructiveHint: false,
11176
+ idempotentHint: true,
11177
+ openWorldHint: false
11178
+ },
10759
11179
  snapshotPaths: ["ppp/profile"],
10760
11180
  async handler(params, context) {
10761
11181
  const parsed = managePppProfileInputSchema.parse(params);
10762
- log39.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing PPP profile");
11182
+ log39.info(
11183
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
11184
+ "Managing PPP profile"
11185
+ );
10763
11186
  try {
10764
- const all = await context.routerClient.get("ppp/profile", { limit: void 0, offset: void 0 });
11187
+ const all = await context.routerClient.get("ppp/profile", {
11188
+ limit: void 0,
11189
+ offset: void 0
11190
+ });
10765
11191
  const existing = all.find((p) => p.name === parsed.name);
10766
11192
  if (parsed.action === "add") {
10767
11193
  if (existing) {
@@ -10774,12 +11200,18 @@ var managePppProfileTool = {
10774
11200
  const diff = [
10775
11201
  { property: "name", before: null, after: parsed.name }
10776
11202
  ];
10777
- if (parsed.localAddress) diff.push({ property: "local-address", before: null, after: parsed.localAddress });
10778
- if (parsed.remoteAddress) diff.push({ property: "remote-address", before: null, after: parsed.remoteAddress });
10779
- if (parsed.dnsServer) diff.push({ property: "dns-server", before: null, after: parsed.dnsServer });
10780
- if (parsed.rateLimit) diff.push({ property: "rate-limit", before: null, after: parsed.rateLimit });
10781
- if (parsed.sessionTimeout) diff.push({ property: "session-timeout", before: null, after: parsed.sessionTimeout });
10782
- if (parsed.comment) diff.push({ property: "comment", before: null, after: parsed.comment });
11203
+ if (parsed.localAddress)
11204
+ diff.push({ property: "local-address", before: null, after: parsed.localAddress });
11205
+ if (parsed.remoteAddress)
11206
+ diff.push({ property: "remote-address", before: null, after: parsed.remoteAddress });
11207
+ if (parsed.dnsServer)
11208
+ diff.push({ property: "dns-server", before: null, after: parsed.dnsServer });
11209
+ if (parsed.rateLimit)
11210
+ diff.push({ property: "rate-limit", before: null, after: parsed.rateLimit });
11211
+ if (parsed.sessionTimeout)
11212
+ diff.push({ property: "session-timeout", before: null, after: parsed.sessionTimeout });
11213
+ if (parsed.comment)
11214
+ diff.push({ property: "comment", before: null, after: parsed.comment });
10783
11215
  return {
10784
11216
  content: `Dry run: Would create PPP profile "${parsed.name}".`,
10785
11217
  structuredContent: { action: "dry_run", diff }
@@ -10806,34 +11238,62 @@ var managePppProfileTool = {
10806
11238
  code: "PPP_PROFILE_NOT_FOUND",
10807
11239
  message: `PPP profile "${parsed.name}" not found.`,
10808
11240
  details: { name: parsed.name },
10809
- recoverability: { retryable: false, suggestedAction: "Verify with list_ppp_profiles.", alternativeTools: ["list_ppp_profiles"] }
11241
+ recoverability: {
11242
+ retryable: false,
11243
+ suggestedAction: "Verify with list_ppp_profiles.",
11244
+ alternativeTools: ["list_ppp_profiles"]
11245
+ }
10810
11246
  });
10811
11247
  }
10812
11248
  const changes = {};
10813
11249
  const diff = [];
10814
11250
  if (parsed.localAddress !== void 0 && existing["local-address"] !== parsed.localAddress) {
10815
11251
  changes["local-address"] = parsed.localAddress;
10816
- diff.push({ property: "local-address", before: existing["local-address"] ?? null, after: parsed.localAddress });
11252
+ diff.push({
11253
+ property: "local-address",
11254
+ before: existing["local-address"] ?? null,
11255
+ after: parsed.localAddress
11256
+ });
10817
11257
  }
10818
11258
  if (parsed.remoteAddress !== void 0 && existing["remote-address"] !== parsed.remoteAddress) {
10819
11259
  changes["remote-address"] = parsed.remoteAddress;
10820
- diff.push({ property: "remote-address", before: existing["remote-address"] ?? null, after: parsed.remoteAddress });
11260
+ diff.push({
11261
+ property: "remote-address",
11262
+ before: existing["remote-address"] ?? null,
11263
+ after: parsed.remoteAddress
11264
+ });
10821
11265
  }
10822
11266
  if (parsed.dnsServer !== void 0 && existing["dns-server"] !== parsed.dnsServer) {
10823
11267
  changes["dns-server"] = parsed.dnsServer;
10824
- diff.push({ property: "dns-server", before: existing["dns-server"] ?? null, after: parsed.dnsServer });
11268
+ diff.push({
11269
+ property: "dns-server",
11270
+ before: existing["dns-server"] ?? null,
11271
+ after: parsed.dnsServer
11272
+ });
10825
11273
  }
10826
11274
  if (parsed.rateLimit !== void 0 && existing["rate-limit"] !== parsed.rateLimit) {
10827
11275
  changes["rate-limit"] = parsed.rateLimit;
10828
- diff.push({ property: "rate-limit", before: existing["rate-limit"] ?? null, after: parsed.rateLimit });
11276
+ diff.push({
11277
+ property: "rate-limit",
11278
+ before: existing["rate-limit"] ?? null,
11279
+ after: parsed.rateLimit
11280
+ });
10829
11281
  }
10830
11282
  if (parsed.sessionTimeout !== void 0 && existing["session-timeout"] !== parsed.sessionTimeout) {
10831
11283
  changes["session-timeout"] = parsed.sessionTimeout;
10832
- diff.push({ property: "session-timeout", before: existing["session-timeout"] ?? null, after: parsed.sessionTimeout });
11284
+ diff.push({
11285
+ property: "session-timeout",
11286
+ before: existing["session-timeout"] ?? null,
11287
+ after: parsed.sessionTimeout
11288
+ });
10833
11289
  }
10834
11290
  if (parsed.comment !== void 0 && existing.comment !== parsed.comment) {
10835
11291
  changes.comment = parsed.comment;
10836
- diff.push({ property: "comment", before: existing.comment ?? null, after: parsed.comment });
11292
+ diff.push({
11293
+ property: "comment",
11294
+ before: existing.comment ?? null,
11295
+ after: parsed.comment
11296
+ });
10837
11297
  }
10838
11298
  if (Object.keys(changes).length === 0) {
10839
11299
  return {
@@ -10863,7 +11323,10 @@ var managePppProfileTool = {
10863
11323
  if (parsed.dryRun) {
10864
11324
  return {
10865
11325
  content: `Dry run: Would remove PPP profile "${parsed.name}".`,
10866
- structuredContent: { action: "dry_run", diff: [{ property: "name", before: parsed.name, after: null }] }
11326
+ structuredContent: {
11327
+ action: "dry_run",
11328
+ diff: [{ property: "name", before: parsed.name, after: null }]
11329
+ }
10867
11330
  };
10868
11331
  }
10869
11332
  await context.routerClient.remove("ppp/profile", existing[".id"]);
@@ -11032,7 +11495,9 @@ var createBackupTool = {
11032
11495
  var exportConfigInputSchema = z41.object({
11033
11496
  routerId,
11034
11497
  compact: z41.boolean().default(false).describe("Export only non-default settings (compact format)"),
11035
- file: z41.string().optional().describe("Save the export to a file on the router (without extension); omit to return inline")
11498
+ file: z41.string().optional().describe(
11499
+ "Save the export to a file on the router (without extension); omit to return inline"
11500
+ )
11036
11501
  }).strict();
11037
11502
  var exportConfigTool = {
11038
11503
  name: "export_config",
@@ -11204,7 +11669,10 @@ var manageLogRuleTool = {
11204
11669
  };
11205
11670
  }
11206
11671
  const created = await context.routerClient.create(LOG_RULES_PATH, body);
11207
- log42.info({ topics: parsed.topics, logAction: parsed.logAction, id: created[".id"] }, "Log rule added");
11672
+ log42.info(
11673
+ { topics: parsed.topics, logAction: parsed.logAction, id: created[".id"] },
11674
+ "Log rule added"
11675
+ );
11208
11676
  return {
11209
11677
  content: `Added log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
11210
11678
  structuredContent: {
@@ -11260,7 +11728,13 @@ var manageLogRuleTool = {
11260
11728
  structuredContent: {
11261
11729
  action: "dry_run",
11262
11730
  id,
11263
- diff: [{ property: "disabled", before: existing.disabled ?? "false", after: String(wantDisabled) }]
11731
+ diff: [
11732
+ {
11733
+ property: "disabled",
11734
+ before: existing.disabled ?? "false",
11735
+ after: String(wantDisabled)
11736
+ }
11737
+ ]
11264
11738
  }
11265
11739
  };
11266
11740
  }
@@ -11492,17 +11966,25 @@ var bandwidthTestTool = {
11492
11966
  // Reaches an external host and saturates the link; a silent retry would
11493
11967
  // double the load, so opt out of auto-retry.
11494
11968
  retryable: false,
11495
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
11496
- async handler(params, context) {
11497
- const parsed = bandwidthTestInputSchema.parse(params);
11498
- log43.info({ routerId: context.routerId, address: parsed.address }, "Running bandwidth test");
11969
+ annotations: {
11970
+ readOnlyHint: true,
11971
+ destructiveHint: false,
11972
+ idempotentHint: false,
11973
+ openWorldHint: true
11974
+ },
11975
+ async handler(params, context) {
11976
+ const parsed = bandwidthTestInputSchema.parse(params);
11977
+ log43.info({ routerId: context.routerId, address: parsed.address }, "Running bandwidth test");
11499
11978
  try {
11500
- const result = await context.routerClient.execute("tool/bandwidth-test", {
11501
- address: parsed.address,
11502
- protocol: parsed.protocol,
11503
- direction: parsed.direction,
11504
- duration: String(parsed.duration)
11505
- });
11979
+ const result = await context.routerClient.execute(
11980
+ "tool/bandwidth-test",
11981
+ {
11982
+ address: parsed.address,
11983
+ protocol: parsed.protocol,
11984
+ direction: parsed.direction,
11985
+ duration: String(parsed.duration)
11986
+ }
11987
+ );
11506
11988
  const txBps = Number(result["tx-current"] ?? 0);
11507
11989
  const rxBps = Number(result["rx-current"] ?? 0);
11508
11990
  const txMbps = Math.round(txBps / 1e6 * 100) / 100;
@@ -11538,12 +12020,23 @@ var fetchUrlTool = {
11538
12020
  inputSchema: fetchUrlInputSchema,
11539
12021
  // Not read-only: a POST has external side effects and outputFile writes a
11540
12022
  // file on the router. openWorld: reaches arbitrary external hosts.
11541
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
12023
+ annotations: {
12024
+ readOnlyHint: false,
12025
+ destructiveHint: false,
12026
+ idempotentHint: false,
12027
+ openWorldHint: true
12028
+ },
11542
12029
  async handler(params, context) {
11543
12030
  const parsed = fetchUrlInputSchema.parse(params);
11544
- log43.info({ routerId: context.routerId, url: parsed.url, method: parsed.method }, "Fetching URL");
12031
+ log43.info(
12032
+ { routerId: context.routerId, url: parsed.url, method: parsed.method },
12033
+ "Fetching URL"
12034
+ );
11545
12035
  try {
11546
- const body = { url: parsed.url, "http-method": parsed.method.toLowerCase() };
12036
+ const body = {
12037
+ url: parsed.url,
12038
+ "http-method": parsed.method.toLowerCase()
12039
+ };
11547
12040
  if (parsed.httpData) body["http-data"] = parsed.httpData;
11548
12041
  if (parsed.outputFile) {
11549
12042
  body.output = "file";
@@ -11551,13 +12044,21 @@ var fetchUrlTool = {
11551
12044
  } else {
11552
12045
  body.output = "user";
11553
12046
  }
11554
- const sections = await context.routerClient.execute("tool/fetch", body);
12047
+ const sections = await context.routerClient.execute(
12048
+ "tool/fetch",
12049
+ body
12050
+ );
11555
12051
  const finished = sections.find((s) => s.status === "finished") ?? sections[sections.length - 1];
11556
12052
  const statusCode = finished?.code ?? null;
11557
12053
  if (parsed.outputFile) {
11558
12054
  return {
11559
12055
  content: `Fetched ${parsed.url} \u2192 saved to ${parsed.outputFile} (status ${statusCode})`,
11560
- structuredContent: { routerId: context.routerId, url: parsed.url, statusCode, outputFile: parsed.outputFile }
12056
+ structuredContent: {
12057
+ routerId: context.routerId,
12058
+ url: parsed.url,
12059
+ statusCode,
12060
+ outputFile: parsed.outputFile
12061
+ }
11561
12062
  };
11562
12063
  }
11563
12064
  let responseBody = finished?.data ?? "";
@@ -11566,7 +12067,12 @@ var fetchUrlTool = {
11566
12067
  }
11567
12068
  return {
11568
12069
  content: `Fetched ${parsed.url}: status=${statusCode} body_length=${responseBody.length}`,
11569
- structuredContent: { routerId: context.routerId, url: parsed.url, statusCode, body: responseBody }
12070
+ structuredContent: {
12071
+ routerId: context.routerId,
12072
+ url: parsed.url,
12073
+ statusCode,
12074
+ body: responseBody
12075
+ }
11570
12076
  };
11571
12077
  } catch (err) {
11572
12078
  throw toolError(err, context, "fetch_url");
@@ -11585,7 +12091,12 @@ var listConnectionsTool = {
11585
12091
  title: "List Connections",
11586
12092
  description: "List active connection tracking entries from the router firewall table. Filters are applied client-side. Useful for diagnosing NAT and firewall behavior.",
11587
12093
  inputSchema: listConnectionsInputSchema,
11588
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
12094
+ annotations: {
12095
+ readOnlyHint: true,
12096
+ destructiveHint: false,
12097
+ idempotentHint: true,
12098
+ openWorldHint: false
12099
+ },
11589
12100
  async handler(params, context) {
11590
12101
  const parsed = listConnectionsInputSchema.parse(params);
11591
12102
  log43.info({ routerId: context.routerId }, "Listing connections");
@@ -11602,7 +12113,11 @@ var listConnectionsTool = {
11602
12113
  limit: void 0,
11603
12114
  offset: void 0
11604
12115
  });
11605
- const filtered = all.filter((c) => parsed.srcAddress ? (c["src-address"] ?? "").includes(parsed.srcAddress) : true).filter((c) => parsed.dstAddress ? (c["dst-address"] ?? "").includes(parsed.dstAddress) : true).filter((c) => parsed.protocol ? c.protocol === parsed.protocol : true);
12116
+ const filtered = all.filter(
12117
+ (c) => parsed.srcAddress ? (c["src-address"] ?? "").includes(parsed.srcAddress) : true
12118
+ ).filter(
12119
+ (c) => parsed.dstAddress ? (c["dst-address"] ?? "").includes(parsed.dstAddress) : true
12120
+ ).filter((c) => parsed.protocol ? c.protocol === parsed.protocol : true);
11606
12121
  const connections = filtered.slice(0, parsed.limit);
11607
12122
  return {
11608
12123
  content: listContent(
@@ -11619,14 +12134,23 @@ var listConnectionsTool = {
11619
12134
  "connection-mark"
11620
12135
  ])
11621
12136
  ),
11622
- structuredContent: { routerId: context.routerId, connections, total: all.length, returned: connections.length }
12137
+ structuredContent: {
12138
+ routerId: context.routerId,
12139
+ connections,
12140
+ total: all.length,
12141
+ returned: connections.length
12142
+ }
11623
12143
  };
11624
12144
  } catch (err) {
11625
12145
  throw toolError(err, context, "list_connections");
11626
12146
  }
11627
12147
  }
11628
12148
  };
11629
- var networkTestTools = [bandwidthTestTool, fetchUrlTool, listConnectionsTool];
12149
+ var networkTestTools = [
12150
+ bandwidthTestTool,
12151
+ fetchUrlTool,
12152
+ listConnectionsTool
12153
+ ];
11630
12154
 
11631
12155
  // src/domain/tools/container-config-tools.ts
11632
12156
  import { z as z44 } from "zod";
@@ -11637,7 +12161,12 @@ var getContainerConfigTool = {
11637
12161
  title: "Get Container Config",
11638
12162
  description: "Read global container configuration: registry URL, RAM high-water mark, and veth interface.",
11639
12163
  inputSchema: getContainerConfigInputSchema,
11640
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
12164
+ annotations: {
12165
+ readOnlyHint: true,
12166
+ destructiveHint: false,
12167
+ idempotentHint: true,
12168
+ openWorldHint: false
12169
+ },
11641
12170
  async handler(params, context) {
11642
12171
  getContainerConfigInputSchema.parse(params);
11643
12172
  log44.info({ routerId: context.routerId }, "Getting container config");
@@ -11670,7 +12199,12 @@ var manageContainerConfigTool = {
11670
12199
  title: "Manage Container Config",
11671
12200
  description: "Update global container settings. Idempotent: returns no_change if nothing differs.",
11672
12201
  inputSchema: manageContainerConfigInputSchema,
11673
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
12202
+ annotations: {
12203
+ readOnlyHint: false,
12204
+ destructiveHint: false,
12205
+ idempotentHint: true,
12206
+ openWorldHint: false
12207
+ },
11674
12208
  snapshotPaths: ["container/config"],
11675
12209
  async handler(params, context) {
11676
12210
  const parsed = manageContainerConfigInputSchema.parse(params);
@@ -11678,23 +12212,34 @@ var manageContainerConfigTool = {
11678
12212
  try {
11679
12213
  const results2 = await context.routerClient.get("container/config");
11680
12214
  const current = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
11681
- const id = current[".id"];
11682
12215
  const changes = {};
11683
12216
  const diff = [];
11684
12217
  if (parsed.registryUrl !== void 0 && current["registry-url"] !== parsed.registryUrl) {
11685
12218
  changes["registry-url"] = parsed.registryUrl;
11686
- diff.push({ property: "registry-url", before: current["registry-url"] ?? null, after: parsed.registryUrl });
12219
+ diff.push({
12220
+ property: "registry-url",
12221
+ before: current["registry-url"] ?? null,
12222
+ after: parsed.registryUrl
12223
+ });
11687
12224
  }
11688
12225
  if (parsed.ramHighMb !== void 0) {
11689
12226
  const next = String(parsed.ramHighMb);
11690
- if (current["ram-high"] !== next) {
12227
+ if (!sameValue(current["ram-high"], next)) {
11691
12228
  changes["ram-high"] = next;
11692
- diff.push({ property: "ram-high", before: current["ram-high"] ?? null, after: next });
12229
+ diff.push({
12230
+ property: "ram-high",
12231
+ before: current["ram-high"] === void 0 ? null : String(current["ram-high"]),
12232
+ after: next
12233
+ });
11693
12234
  }
11694
12235
  }
11695
12236
  if (parsed.vethInterface !== void 0 && current["veth-interface"] !== parsed.vethInterface) {
11696
12237
  changes["veth-interface"] = parsed.vethInterface;
11697
- diff.push({ property: "veth-interface", before: current["veth-interface"] ?? null, after: parsed.vethInterface });
12238
+ diff.push({
12239
+ property: "veth-interface",
12240
+ before: current["veth-interface"] ?? null,
12241
+ after: parsed.vethInterface
12242
+ });
11698
12243
  }
11699
12244
  if (Object.keys(changes).length === 0) {
11700
12245
  return {
@@ -11708,7 +12253,7 @@ var manageContainerConfigTool = {
11708
12253
  structuredContent: { action: "dry_run", diff }
11709
12254
  };
11710
12255
  }
11711
- await context.routerClient.update("container/config", id, changes);
12256
+ await context.routerClient.execute("container/config/set", changes);
11712
12257
  log44.info({ routerId: context.routerId }, "Container config updated");
11713
12258
  return {
11714
12259
  content: `Updated container config on ${context.routerId}.`,
@@ -11729,7 +12274,12 @@ var listContainerEnvsTool = {
11729
12274
  title: "List Container Envs",
11730
12275
  description: "List container environment variable entries, optionally filtered by container name.",
11731
12276
  inputSchema: listContainerEnvsInputSchema,
11732
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
12277
+ annotations: {
12278
+ readOnlyHint: true,
12279
+ destructiveHint: false,
12280
+ idempotentHint: true,
12281
+ openWorldHint: false
12282
+ },
11733
12283
  async handler(params, context) {
11734
12284
  const parsed = listContainerEnvsInputSchema.parse(params);
11735
12285
  log44.info({ routerId: context.routerId }, "Listing container envs");
@@ -11749,7 +12299,12 @@ var listContainerEnvsTool = {
11749
12299
  0,
11750
12300
  (e) => compactFields(e, ["name", "key", "value"])
11751
12301
  ),
11752
- structuredContent: { routerId: context.routerId, envs, total: all.length, returned: envs.length }
12302
+ structuredContent: {
12303
+ routerId: context.routerId,
12304
+ envs,
12305
+ total: all.length,
12306
+ returned: envs.length
12307
+ }
11753
12308
  };
11754
12309
  } catch (err) {
11755
12310
  throw toolError(err, context, "list_container_envs");
@@ -11769,7 +12324,12 @@ var manageContainerEnvTool = {
11769
12324
  title: "Manage Container Env",
11770
12325
  description: "Add or remove a container environment variable. Idempotent by name+key. add returns already_exists if the entry exists with the same value; throws CONFLICT if the value differs.",
11771
12326
  inputSchema: manageContainerEnvInputSchema,
11772
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
12327
+ annotations: {
12328
+ readOnlyHint: false,
12329
+ destructiveHint: false,
12330
+ idempotentHint: true,
12331
+ openWorldHint: false
12332
+ },
11773
12333
  async handler(params, context) {
11774
12334
  const parsed = manageContainerEnvInputSchema.parse(params);
11775
12335
  log44.info(
@@ -11873,7 +12433,12 @@ var listContainerMountsTool = {
11873
12433
  title: "List Container Mounts",
11874
12434
  description: "List container volume mount definitions with source path, destination path, and mount name.",
11875
12435
  inputSchema: listContainerMountsInputSchema,
11876
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
12436
+ annotations: {
12437
+ readOnlyHint: true,
12438
+ destructiveHint: false,
12439
+ idempotentHint: true,
12440
+ openWorldHint: false
12441
+ },
11877
12442
  async handler(params, context) {
11878
12443
  const parsed = listContainerMountsInputSchema.parse(params);
11879
12444
  log44.info({ routerId: context.routerId }, "Listing container mounts");
@@ -11893,7 +12458,12 @@ var listContainerMountsTool = {
11893
12458
  0,
11894
12459
  (m) => compactFields(m, ["name", "src", "dst"])
11895
12460
  ),
11896
- structuredContent: { routerId: context.routerId, mounts, total: all.length, returned: mounts.length }
12461
+ structuredContent: {
12462
+ routerId: context.routerId,
12463
+ mounts,
12464
+ total: all.length,
12465
+ returned: mounts.length
12466
+ }
11897
12467
  };
11898
12468
  } catch (err) {
11899
12469
  throw toolError(err, context, "list_container_mounts");
@@ -11913,10 +12483,18 @@ var manageContainerMountTool = {
11913
12483
  title: "Manage Container Mount",
11914
12484
  description: "Add or remove a container volume mount. Idempotent by name: add returns already_exists if the mount exists with matching src/dst; throws CONFLICT if name exists with different paths.",
11915
12485
  inputSchema: manageContainerMountInputSchema,
11916
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
12486
+ annotations: {
12487
+ readOnlyHint: false,
12488
+ destructiveHint: false,
12489
+ idempotentHint: true,
12490
+ openWorldHint: false
12491
+ },
11917
12492
  async handler(params, context) {
11918
12493
  const parsed = manageContainerMountInputSchema.parse(params);
11919
- log44.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing container mount");
12494
+ log44.info(
12495
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
12496
+ "Managing container mount"
12497
+ );
11920
12498
  try {
11921
12499
  const all = await context.routerClient.get("container/mounts", {
11922
12500
  limit: void 0,
@@ -11951,7 +12529,10 @@ var manageContainerMountTool = {
11951
12529
  category: "VALIDATION" /* VALIDATION */,
11952
12530
  code: "MOUNT_PATHS_REQUIRED",
11953
12531
  message: "src and dst are required for add.",
11954
- recoverability: { retryable: false, suggestedAction: "Provide src and dst parameters." }
12532
+ recoverability: {
12533
+ retryable: false,
12534
+ suggestedAction: "Provide src and dst parameters."
12535
+ }
11955
12536
  });
11956
12537
  }
11957
12538
  if (parsed.dryRun) {
@@ -12021,7 +12602,12 @@ var listInterfaceListsTool = {
12021
12602
  title: "List Interface Lists",
12022
12603
  description: "List all interface lists defined on the router.",
12023
12604
  inputSchema: listInterfaceListsInputSchema,
12024
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
12605
+ annotations: {
12606
+ readOnlyHint: true,
12607
+ destructiveHint: false,
12608
+ idempotentHint: true,
12609
+ openWorldHint: false
12610
+ },
12025
12611
  async handler(params, context) {
12026
12612
  const parsed = listInterfaceListsInputSchema.parse(params);
12027
12613
  log45.info({ routerId: context.routerId }, "Listing interface lists");
@@ -12040,7 +12626,12 @@ var listInterfaceListsTool = {
12040
12626
  0,
12041
12627
  (l) => compactFields(l, ["name", "include", "exclude", "comment"])
12042
12628
  ),
12043
- structuredContent: { routerId: context.routerId, lists, total: all.length, returned: lists.length }
12629
+ structuredContent: {
12630
+ routerId: context.routerId,
12631
+ lists,
12632
+ total: all.length,
12633
+ returned: lists.length
12634
+ }
12044
12635
  };
12045
12636
  } catch (err) {
12046
12637
  throw toolError(err, context, "list_interface_lists");
@@ -12059,10 +12650,18 @@ var manageInterfaceListTool = {
12059
12650
  title: "Manage Interface List",
12060
12651
  description: "Add or remove an interface list. Idempotent by name. Removing a list that has members is blocked by RouterOS \u2014 the error is surfaced as-is.",
12061
12652
  inputSchema: manageInterfaceListInputSchema,
12062
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
12653
+ annotations: {
12654
+ readOnlyHint: false,
12655
+ destructiveHint: true,
12656
+ idempotentHint: true,
12657
+ openWorldHint: false
12658
+ },
12063
12659
  async handler(params, context) {
12064
12660
  const parsed = manageInterfaceListInputSchema.parse(params);
12065
- log45.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing interface list");
12661
+ log45.info(
12662
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
12663
+ "Managing interface list"
12664
+ );
12066
12665
  try {
12067
12666
  const all = await context.routerClient.get("interface/list", {
12068
12667
  limit: void 0,
@@ -12133,11 +12732,21 @@ var manageInterfaceListMemberTool = {
12133
12732
  title: "Manage Interface List Member",
12134
12733
  description: "Add or remove an interface from an interface list. Idempotent by list+interface composite key. add returns already_exists if the membership exists. remove returns not_found gracefully.",
12135
12734
  inputSchema: manageInterfaceListMemberInputSchema,
12136
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
12735
+ annotations: {
12736
+ readOnlyHint: false,
12737
+ destructiveHint: true,
12738
+ idempotentHint: true,
12739
+ openWorldHint: false
12740
+ },
12137
12741
  async handler(params, context) {
12138
12742
  const parsed = manageInterfaceListMemberInputSchema.parse(params);
12139
12743
  log45.info(
12140
- { routerId: context.routerId, action: parsed.action, list: parsed.list, interface: parsed.interface },
12744
+ {
12745
+ routerId: context.routerId,
12746
+ action: parsed.action,
12747
+ list: parsed.list,
12748
+ interface: parsed.interface
12749
+ },
12141
12750
  "Managing interface list member"
12142
12751
  );
12143
12752
  try {
@@ -12152,7 +12761,11 @@ var manageInterfaceListMemberTool = {
12152
12761
  if (existing) {
12153
12762
  return {
12154
12763
  content: `Interface "${parsed.interface}" is already a member of list "${parsed.list}". No changes made.`,
12155
- structuredContent: { action: "already_exists", list: parsed.list, interface: parsed.interface }
12764
+ structuredContent: {
12765
+ action: "already_exists",
12766
+ list: parsed.list,
12767
+ interface: parsed.interface
12768
+ }
12156
12769
  };
12157
12770
  }
12158
12771
  if (parsed.dryRun) {
@@ -12167,16 +12780,28 @@ var manageInterfaceListMemberTool = {
12167
12780
  const body = { list: parsed.list, interface: parsed.interface };
12168
12781
  if (parsed.comment) body.comment = parsed.comment;
12169
12782
  const created = await context.routerClient.create("interface/list/member", body);
12170
- log45.info({ list: parsed.list, interface: parsed.interface, id: created[".id"] }, "Interface list member added");
12783
+ log45.info(
12784
+ { list: parsed.list, interface: parsed.interface, id: created[".id"] },
12785
+ "Interface list member added"
12786
+ );
12171
12787
  return {
12172
12788
  content: `Added "${parsed.interface}" to list "${parsed.list}".`,
12173
- structuredContent: { action: "created", list: parsed.list, interface: parsed.interface, id: created[".id"] }
12789
+ structuredContent: {
12790
+ action: "created",
12791
+ list: parsed.list,
12792
+ interface: parsed.interface,
12793
+ id: created[".id"]
12794
+ }
12174
12795
  };
12175
12796
  }
12176
12797
  if (!existing) {
12177
12798
  return {
12178
12799
  content: `Interface "${parsed.interface}" is not a member of list "${parsed.list}". Nothing to remove.`,
12179
- structuredContent: { action: "not_found", list: parsed.list, interface: parsed.interface }
12800
+ structuredContent: {
12801
+ action: "not_found",
12802
+ list: parsed.list,
12803
+ interface: parsed.interface
12804
+ }
12180
12805
  };
12181
12806
  }
12182
12807
  if (parsed.dryRun) {
@@ -12647,7 +13272,11 @@ function checkAuthz(identity, toolName, routerId2) {
12647
13272
  category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
12648
13273
  code: "TOOL_NOT_ALLOWED",
12649
13274
  message: `Identity "${identity.id}" is not allowed to call tool "${toolName}".`,
12650
- details: { identityId: identity.id, toolName, allowedToolPatterns: identity.allowedToolPatterns },
13275
+ details: {
13276
+ identityId: identity.id,
13277
+ toolName,
13278
+ allowedToolPatterns: identity.allowedToolPatterns
13279
+ },
12651
13280
  recoverability: {
12652
13281
  retryable: false,
12653
13282
  suggestedAction: `Add a matching pattern to allowedToolPatterns for identity "${identity.id}" in identities.yaml.`
@@ -12704,6 +13333,7 @@ function getCredentials(router) {
12704
13333
 
12705
13334
  // src/adapter/ssh-client.ts
12706
13335
  import { createHash as createHash2 } from "crypto";
13336
+ import { readFileSync as readFileSync3 } from "fs";
12707
13337
  import { Client } from "ssh2";
12708
13338
  var SshClient = class {
12709
13339
  constructor(config, credentials, options) {
@@ -12719,6 +13349,7 @@ var SshClient = class {
12719
13349
  async execute(command, overrideTimeoutMs) {
12720
13350
  const timeoutMs = overrideTimeoutMs ?? this.commandTimeoutMs;
12721
13351
  const maxOutputBytes = this.maxOutputBytes;
13352
+ const privateKey = this.config.sshPrivateKeyPath ? readFileSync3(this.config.sshPrivateKeyPath) : void 0;
12722
13353
  return new Promise((resolve, reject) => {
12723
13354
  const conn = new Client();
12724
13355
  const chunks = [];
@@ -12784,10 +13415,11 @@ var SshClient = class {
12784
13415
  const connectOptions = {
12785
13416
  host: this.config.host,
12786
13417
  port: this.config.sshPort ?? 22,
12787
- username: this.credentials.username,
12788
- password: this.credentials.password,
13418
+ username: this.config.sshUsername ?? this.credentials.username,
12789
13419
  readyTimeout: 1e4
12790
13420
  };
13421
+ if (privateKey) connectOptions.privateKey = privateKey;
13422
+ else connectOptions.password = this.credentials.password;
12791
13423
  if (this.config.sshFingerprint) {
12792
13424
  const expected = this.config.sshFingerprint.toLowerCase();
12793
13425
  connectOptions.hostVerifier = (key2) => {
@@ -12849,6 +13481,7 @@ var FtpClient = class {
12849
13481
 
12850
13482
  // src/adapter/sftp-client.ts
12851
13483
  import { createHash as createHash3 } from "crypto";
13484
+ import { readFileSync as readFileSync4 } from "fs";
12852
13485
  import { Client as Client3 } from "ssh2";
12853
13486
  var SftpClient = class {
12854
13487
  constructor(config, credentials) {
@@ -12858,6 +13491,7 @@ var SftpClient = class {
12858
13491
  config;
12859
13492
  credentials;
12860
13493
  async upload(remoteName, content) {
13494
+ const privateKey = this.config.sshPrivateKeyPath ? readFileSync4(this.config.sshPrivateKeyPath) : void 0;
12861
13495
  return new Promise((resolve, reject) => {
12862
13496
  const conn = new Client3();
12863
13497
  let settled = false;
@@ -12883,10 +13517,11 @@ var SftpClient = class {
12883
13517
  const connectOptions = {
12884
13518
  host: this.config.host,
12885
13519
  port: this.config.sshPort ?? 22,
12886
- username: this.credentials.username,
12887
- password: this.credentials.password,
13520
+ username: this.config.sshUsername ?? this.credentials.username,
12888
13521
  readyTimeout: 1e4
12889
13522
  };
13523
+ if (privateKey) connectOptions.privateKey = privateKey;
13524
+ else connectOptions.password = this.credentials.password;
12890
13525
  if (this.config.sshFingerprint) {
12891
13526
  const expected = this.config.sshFingerprint.toLowerCase();
12892
13527
  connectOptions.hostVerifier = (key2) => {
@@ -13465,17 +14100,20 @@ async function executeToolCall(tool, args, deps) {
13465
14100
  }
13466
14101
  const shouldAudit = tool.annotations.destructiveHint || !tool.annotations.readOnlyHint;
13467
14102
  if (shouldAudit) {
13468
- auditLog({
13469
- type: "audit",
13470
- ts: (/* @__PURE__ */ new Date()).toISOString(),
13471
- correlationId,
13472
- identityId: identity.id,
13473
- role: identity.role,
13474
- tool: tool.name,
13475
- routerId: routerId2,
13476
- phase: "attempt",
13477
- params: args
13478
- }, config.auditLogPath);
14103
+ auditLog(
14104
+ {
14105
+ type: "audit",
14106
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
14107
+ correlationId,
14108
+ identityId: identity.id,
14109
+ role: identity.role,
14110
+ tool: tool.name,
14111
+ routerId: routerId2,
14112
+ phase: "attempt",
14113
+ params: args
14114
+ },
14115
+ config.auditLogPath
14116
+ );
13479
14117
  }
13480
14118
  const toolContext = buildRouterToolContext({
13481
14119
  routerConfig,
@@ -13492,7 +14130,12 @@ async function executeToolCall(tool, args, deps) {
13492
14130
  if (!tool.annotations.readOnlyHint) {
13493
14131
  for (const path of snapshotPathsFor(tool, handlerArgs)) {
13494
14132
  try {
13495
- const meta = await takeSnapshot(toolContext.deviceClient, routerId2, path, config.snapshotDir);
14133
+ const meta = await takeSnapshot(
14134
+ toolContext.deviceClient,
14135
+ routerId2,
14136
+ path,
14137
+ config.snapshotDir
14138
+ );
13496
14139
  snapshotIds.push(meta.id);
13497
14140
  log48.debug({ snapshotId: meta.id, path }, "Snapshot taken");
13498
14141
  } catch (err) {
@@ -13524,18 +14167,21 @@ async function executeToolCall(tool, args, deps) {
13524
14167
  });
13525
14168
  }
13526
14169
  if (shouldAudit) {
13527
- auditLog({
13528
- type: "audit",
13529
- ts: (/* @__PURE__ */ new Date()).toISOString(),
13530
- correlationId,
13531
- identityId: identity.id,
13532
- role: identity.role,
13533
- tool: tool.name,
13534
- routerId: routerId2,
13535
- phase: "success",
13536
- params: args,
13537
- durationMs: Date.now() - startMs
13538
- }, config.auditLogPath);
14170
+ auditLog(
14171
+ {
14172
+ type: "audit",
14173
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
14174
+ correlationId,
14175
+ identityId: identity.id,
14176
+ role: identity.role,
14177
+ tool: tool.name,
14178
+ routerId: routerId2,
14179
+ phase: "success",
14180
+ params: args,
14181
+ durationMs: Date.now() - startMs
14182
+ },
14183
+ config.auditLogPath
14184
+ );
13539
14185
  }
13540
14186
  log48.info({ tool: tool.name, routerId: routerId2, correlationId }, "Tool executed successfully");
13541
14187
  recordToolCall(tool.name, "success");
@@ -13565,19 +14211,22 @@ async function executeToolCall(tool, args, deps) {
13565
14211
  if (shouldAudit && error.category !== "APPROVAL_REQUIRED" /* APPROVAL_REQUIRED */) {
13566
14212
  const routerId2 = args.routerId ?? "unknown";
13567
14213
  const identity = getCurrentIdentity() ?? getStdioIdentity(config.stdioIdentity, identityRegistry);
13568
- auditLog({
13569
- type: "audit",
13570
- ts: (/* @__PURE__ */ new Date()).toISOString(),
13571
- correlationId,
13572
- identityId: identity.id,
13573
- role: identity.role,
13574
- tool: tool.name,
13575
- routerId: routerId2,
13576
- phase: "failure",
13577
- params: args,
13578
- outcome: error.code,
13579
- durationMs: Date.now() - startMs
13580
- }, config.auditLogPath);
14214
+ auditLog(
14215
+ {
14216
+ type: "audit",
14217
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
14218
+ correlationId,
14219
+ identityId: identity.id,
14220
+ role: identity.role,
14221
+ tool: tool.name,
14222
+ routerId: routerId2,
14223
+ phase: "failure",
14224
+ params: args,
14225
+ outcome: error.code,
14226
+ durationMs: Date.now() - startMs
14227
+ },
14228
+ config.auditLogPath
14229
+ );
13581
14230
  }
13582
14231
  log48.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
13583
14232
  recordToolCall(tool.name, "error");
@@ -13716,10 +14365,16 @@ var listRoutersTool = {
13716
14365
  const total = rows.length;
13717
14366
  const header = `Routers: ${total === 0 ? "none" : `1-${total} of ${total}`}.`;
13718
14367
  const lines = rows.map(
13719
- (r) => ` ${compactFields(
13720
- { ...r, tags: r.tags.join(",") },
13721
- ["id", "host", "port", "deviceType", "tlsEnabled", "tags", "rosVersion", "isDefault"]
13722
- )}`
14368
+ (r) => ` ${compactFields({ ...r, tags: r.tags.join(",") }, [
14369
+ "id",
14370
+ "host",
14371
+ "port",
14372
+ "deviceType",
14373
+ "tlsEnabled",
14374
+ "tags",
14375
+ "rosVersion",
14376
+ "isDefault"
14377
+ ])}`
13723
14378
  );
13724
14379
  return {
13725
14380
  content: total === 0 ? header : [header, ...lines].join("\n"),
@@ -13808,17 +14463,20 @@ function createFleetTools(baseTools2) {
13808
14463
  { toolName: parsed.toolName, concurrency: parsed.concurrency },
13809
14464
  "bulk_execute invoked"
13810
14465
  );
13811
- auditLog({
13812
- type: "audit",
13813
- ts: (/* @__PURE__ */ new Date()).toISOString(),
13814
- correlationId: context.correlationId,
13815
- identityId: context.identity.id,
13816
- role: context.identity.role,
13817
- tool: "bulk_execute",
13818
- routerId: "(fleet)",
13819
- phase: "attempt",
13820
- params: { toolName: parsed.toolName, routerIds: parsed.routerIds, tags: parsed.tags }
13821
- }, context.appConfig.auditLogPath);
14466
+ auditLog(
14467
+ {
14468
+ type: "audit",
14469
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
14470
+ correlationId: context.correlationId,
14471
+ identityId: context.identity.id,
14472
+ role: context.identity.role,
14473
+ tool: "bulk_execute",
14474
+ routerId: "(fleet)",
14475
+ phase: "attempt",
14476
+ params: { toolName: parsed.toolName, routerIds: parsed.routerIds, tags: parsed.tags }
14477
+ },
14478
+ context.appConfig.auditLogPath
14479
+ );
13822
14480
  if (parsed.toolName === "bulk_execute" || parsed.toolName === "check_router_health") {
13823
14481
  throw new MikroMCPError({
13824
14482
  category: "VALIDATION" /* VALIDATION */,
@@ -13891,17 +14549,20 @@ function createFleetTools(baseTools2) {
13891
14549
  );
13892
14550
  }
13893
14551
  if (routers.length === 0 && preErrors.length === 0) {
13894
- auditLog({
13895
- type: "audit",
13896
- ts: (/* @__PURE__ */ new Date()).toISOString(),
13897
- correlationId: context.correlationId,
13898
- identityId: context.identity.id,
13899
- role: context.identity.role,
13900
- tool: "bulk_execute",
13901
- routerId: "(fleet)",
13902
- phase: "success",
13903
- params: { toolName: parsed.toolName, succeeded: 0, failed: 0 }
13904
- }, context.appConfig.auditLogPath);
14552
+ auditLog(
14553
+ {
14554
+ type: "audit",
14555
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
14556
+ correlationId: context.correlationId,
14557
+ identityId: context.identity.id,
14558
+ role: context.identity.role,
14559
+ tool: "bulk_execute",
14560
+ routerId: "(fleet)",
14561
+ phase: "success",
14562
+ params: { toolName: parsed.toolName, succeeded: 0, failed: 0 }
14563
+ },
14564
+ context.appConfig.auditLogPath
14565
+ );
13905
14566
  return {
13906
14567
  content: `Executed ${parsed.toolName} on 0 routers: 0 succeeded, 0 failed`,
13907
14568
  structuredContent: {
@@ -13934,7 +14595,10 @@ function createFleetTools(baseTools2) {
13934
14595
  const toolParams = { ...parsed.params, routerId: router.id };
13935
14596
  const snapshotIds = [];
13936
14597
  if (isWrite) {
13937
- for (const path of snapshotPathsFor(targetTool, toolParams)) {
14598
+ for (const path of snapshotPathsFor(
14599
+ targetTool,
14600
+ toolParams
14601
+ )) {
13938
14602
  try {
13939
14603
  const meta = await takeSnapshot(
13940
14604
  routerContext.deviceClient,
@@ -13971,40 +14635,57 @@ function createFleetTools(baseTools2) {
13971
14635
  );
13972
14636
  const elapsed = Date.now() - start;
13973
14637
  if (journalId) {
13974
- recordOutcome({ journalPath, journalId, phase: "success", durationMs: elapsed });
14638
+ recordOutcome({
14639
+ journalPath,
14640
+ journalId,
14641
+ phase: "success",
14642
+ durationMs: elapsed
14643
+ });
13975
14644
  }
13976
- auditLog({
13977
- type: "audit",
13978
- ts: (/* @__PURE__ */ new Date()).toISOString(),
13979
- correlationId: context.correlationId,
13980
- identityId: context.identity.id,
13981
- role: context.identity.role,
13982
- tool: parsed.toolName,
13983
- routerId: router.id,
13984
- phase: "success",
13985
- params: parsed.params,
13986
- durationMs: elapsed
13987
- }, context.appConfig.auditLogPath);
14645
+ auditLog(
14646
+ {
14647
+ type: "audit",
14648
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
14649
+ correlationId: context.correlationId,
14650
+ identityId: context.identity.id,
14651
+ role: context.identity.role,
14652
+ tool: parsed.toolName,
14653
+ routerId: router.id,
14654
+ phase: "success",
14655
+ params: parsed.params,
14656
+ durationMs: elapsed
14657
+ },
14658
+ context.appConfig.auditLogPath
14659
+ );
13988
14660
  return { routerId: router.id, status: "ok", result, durationMs: elapsed };
13989
14661
  } catch (err) {
13990
14662
  const elapsed = Date.now() - start;
13991
14663
  const message = err instanceof Error ? err.message : String(err);
13992
14664
  if (journalId) {
13993
- recordOutcome({ journalPath, journalId, phase: "failure", outcome: message, durationMs: elapsed });
14665
+ recordOutcome({
14666
+ journalPath,
14667
+ journalId,
14668
+ phase: "failure",
14669
+ outcome: message,
14670
+ durationMs: elapsed
14671
+ });
13994
14672
  }
13995
- auditLog({
13996
- type: "audit",
13997
- ts: (/* @__PURE__ */ new Date()).toISOString(),
13998
- correlationId: context.correlationId,
13999
- identityId: context.identity.id,
14000
- role: context.identity.role,
14001
- tool: parsed.toolName,
14002
- routerId: router.id,
14003
- phase: "failure",
14004
- params: parsed.params,
14005
- outcome: message,
14006
- durationMs: elapsed
14007
- }, context.appConfig.auditLogPath);
14673
+ auditLog(
14674
+ {
14675
+ type: "audit",
14676
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
14677
+ correlationId: context.correlationId,
14678
+ identityId: context.identity.id,
14679
+ role: context.identity.role,
14680
+ tool: parsed.toolName,
14681
+ routerId: router.id,
14682
+ phase: "failure",
14683
+ params: parsed.params,
14684
+ outcome: message,
14685
+ durationMs: elapsed
14686
+ },
14687
+ context.appConfig.auditLogPath
14688
+ );
14008
14689
  return { routerId: router.id, status: "error", error: message, durationMs: elapsed };
14009
14690
  }
14010
14691
  }
@@ -14016,17 +14697,20 @@ function createFleetTools(baseTools2) {
14016
14697
  }
14017
14698
  const succeeded = results2.filter((r) => r.status === "ok").length;
14018
14699
  const failed = results2.filter((r) => r.status === "error").length;
14019
- auditLog({
14020
- type: "audit",
14021
- ts: (/* @__PURE__ */ new Date()).toISOString(),
14022
- correlationId: context.correlationId,
14023
- identityId: context.identity.id,
14024
- role: context.identity.role,
14025
- tool: "bulk_execute",
14026
- routerId: "(fleet)",
14027
- phase: "success",
14028
- params: { toolName: parsed.toolName, succeeded, failed }
14029
- }, context.appConfig.auditLogPath);
14700
+ auditLog(
14701
+ {
14702
+ type: "audit",
14703
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
14704
+ correlationId: context.correlationId,
14705
+ identityId: context.identity.id,
14706
+ role: context.identity.role,
14707
+ tool: "bulk_execute",
14708
+ routerId: "(fleet)",
14709
+ phase: "success",
14710
+ params: { toolName: parsed.toolName, succeeded, failed }
14711
+ },
14712
+ context.appConfig.auditLogPath
14713
+ );
14030
14714
  return {
14031
14715
  content: `Executed ${parsed.toolName} on ${results2.length} routers: ${succeeded} succeeded, ${failed} failed`,
14032
14716
  structuredContent: {
@@ -14120,7 +14804,8 @@ function registerAllTools(server, registry, pool, circuitBreakers, config, ident
14120
14804
  }
14121
14805
 
14122
14806
  // src/config/router-registry.ts
14123
- import { readFileSync as readFileSync3, existsSync } from "fs";
14807
+ import { readFileSync as readFileSync5, existsSync } from "fs";
14808
+ import { isAbsolute } from "path";
14124
14809
  import { parse } from "yaml";
14125
14810
  import { z as z49 } from "zod";
14126
14811
  var log51 = createLogger("router-registry");
@@ -14147,14 +14832,14 @@ var RouterConfigSchema = z49.object({
14147
14832
  tags: z49.array(z49.string()).default([]),
14148
14833
  rosVersion: z49.string().min(1).optional(),
14149
14834
  sshPort: z49.number().int().min(1).max(65535).optional(),
14835
+ sshUsername: z49.string().min(1).optional(),
14836
+ sshPrivateKeyPath: z49.string().min(1).refine(isAbsolute, "sshPrivateKeyPath must be absolute").optional(),
14150
14837
  sshFingerprint: z49.string().optional(),
14151
14838
  cmdAllow: z49.array(z49.string()).optional(),
14152
14839
  cmdDeny: z49.array(z49.string()).optional(),
14153
14840
  maintenanceWindows: z49.array(
14154
14841
  z49.object({
14155
- days: z49.array(
14156
- z49.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
14157
- ),
14842
+ days: z49.array(z49.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])),
14158
14843
  startTime: z49.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
14159
14844
  endTime: z49.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
14160
14845
  timezone: z49.string().min(1)
@@ -14200,7 +14885,7 @@ var RouterRegistry = class {
14200
14885
  log51.warn({ configPath }, "Router config file not found; starting with empty registry");
14201
14886
  return;
14202
14887
  }
14203
- const raw = readFileSync3(configPath, "utf-8");
14888
+ const raw = readFileSync5(configPath, "utf-8");
14204
14889
  const parsed = parse(raw);
14205
14890
  const result = ConfigFileSchema.safeParse(parsed);
14206
14891
  if (!result.success) {
@@ -14230,7 +14915,18 @@ ${issues}`);
14230
14915
  getRouter(id) {
14231
14916
  const router = this.routers.get(id);
14232
14917
  if (!router) {
14233
- throw new Error(`Router not found: ${id}`);
14918
+ const available = this.routerIds();
14919
+ throw new MikroMCPError({
14920
+ category: "NOT_FOUND" /* NOT_FOUND */,
14921
+ code: "ROUTER_NOT_FOUND",
14922
+ message: available.length > 0 ? `Router not found: ${id}. Available: ${available.join(", ")}.` : `Router not found: ${id}. No routers are configured.`,
14923
+ details: { routerId: id, availableRouters: available },
14924
+ recoverability: {
14925
+ retryable: false,
14926
+ suggestedAction: "Check the routerId, or use list_routers to see the configured routers.",
14927
+ alternativeTools: ["list_routers"]
14928
+ }
14929
+ });
14234
14930
  }
14235
14931
  return router;
14236
14932
  }
@@ -14317,7 +15013,7 @@ var ConnectionPool = class {
14317
15013
  };
14318
15014
 
14319
15015
  // src/version.ts
14320
- var VERSION = "1.9.0";
15016
+ var VERSION = "1.11.0";
14321
15017
 
14322
15018
  // src/mcp/server.ts
14323
15019
  var SERVER_INSTRUCTIONS = [
@@ -14503,7 +15199,18 @@ async function connectHttp(makeServer, config, identityRegistry) {
14503
15199
  }
14504
15200
  const boundIdentity = identity;
14505
15201
  await withIdentity(boundIdentity, async () => {
14506
- await handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, makeServer, streamableSessions, sseTransports, boundIdentity.id);
15202
+ await handleMcpRequest(
15203
+ pathname,
15204
+ req,
15205
+ res,
15206
+ url,
15207
+ port,
15208
+ maxBodyBytes,
15209
+ makeServer,
15210
+ streamableSessions,
15211
+ sseTransports,
15212
+ boundIdentity.id
15213
+ );
14507
15214
  });
14508
15215
  return;
14509
15216
  }
@@ -14613,7 +15320,7 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
14613
15320
  }
14614
15321
 
14615
15322
  // src/config/identity-registry.ts
14616
- import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
15323
+ import { readFileSync as readFileSync6, existsSync as existsSync2 } from "fs";
14617
15324
  import { createHash as createHash7 } from "crypto";
14618
15325
  import { parse as parse2 } from "yaml";
14619
15326
  import { z as z50 } from "zod";
@@ -14641,7 +15348,7 @@ var IdentityRegistry = class {
14641
15348
  );
14642
15349
  return;
14643
15350
  }
14644
- const raw = readFileSync4(configPath, "utf-8");
15351
+ const raw = readFileSync6(configPath, "utf-8");
14645
15352
  const parsed = parse2(raw);
14646
15353
  const result = ConfigFileSchema2.safeParse(parsed);
14647
15354
  if (!result.success) {
@@ -14741,9 +15448,7 @@ async function runServe() {
14741
15448
  void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
14742
15449
  const identityRegistry = new IdentityRegistry(config.identitiesPath);
14743
15450
  if (config.transport === "http") {
14744
- const hasLimitedRoles = identityRegistry.getIdentities().some(
14745
- (i) => i.role === "readonly" || i.role === "operator"
14746
- );
15451
+ const hasLimitedRoles = identityRegistry.getIdentities().some((i) => i.role === "readonly" || i.role === "operator");
14747
15452
  if (hasLimitedRoles && !config.confirmationSecret) {
14748
15453
  log55.error(
14749
15454
  "MIKROMCP_CONFIRMATION_SECRET is required in HTTP mode when identities with role readonly or operator are configured"
@@ -14782,11 +15487,11 @@ async function runServe() {
14782
15487
  }
14783
15488
 
14784
15489
  // src/cli/doctor.ts
14785
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
15490
+ import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
14786
15491
  import { homedir as homedir2 } from "os";
14787
15492
  import * as net from "net";
14788
15493
  import chalk from "chalk";
14789
- var localVersion = "1.9.0";
15494
+ var localVersion = "1.11.0";
14790
15495
  var results = [];
14791
15496
  function ok(msg) {
14792
15497
  console.log(chalk.green("\u2705 ") + msg);
@@ -14883,7 +15588,9 @@ function checkDefaultRouter(defaultRouter, routers) {
14883
15588
  const ids = routers.map((r) => r.id);
14884
15589
  if (defaultRouter) {
14885
15590
  if (ids.includes(defaultRouter)) {
14886
- ok(`Default router: ${defaultRouter} (MIKROMCP_DEFAULT_ROUTER; used when routerId is omitted)`);
15591
+ ok(
15592
+ `Default router: ${defaultRouter} (MIKROMCP_DEFAULT_ROUTER; used when routerId is omitted)`
15593
+ );
14887
15594
  } else {
14888
15595
  fail(
14889
15596
  `MIKROMCP_DEFAULT_ROUTER="${defaultRouter}" does not match any configured router` + (ids.length > 0 ? ` (available: ${ids.join(", ")})` : "")
@@ -14930,7 +15637,9 @@ async function checkRouter(router) {
14930
15637
  indentWarn(`read policy: ip/address returned ${code} \u2014 check user permissions`);
14931
15638
  results.push({ passed: true, warning: true });
14932
15639
  } else {
14933
- indentFail(`read policy: ip/address failed \u2014 ${readErr instanceof Error ? readErr.message : String(readErr)}`);
15640
+ indentFail(
15641
+ `read policy: ip/address failed \u2014 ${readErr instanceof Error ? readErr.message : String(readErr)}`
15642
+ );
14934
15643
  results.push({ passed: false, warning: false });
14935
15644
  }
14936
15645
  }
@@ -14995,7 +15704,7 @@ function checkClaudeDesktop() {
14995
15704
  return;
14996
15705
  }
14997
15706
  try {
14998
- const raw = readFileSync5(configPath, "utf-8");
15707
+ const raw = readFileSync7(configPath, "utf-8");
14999
15708
  const config = JSON.parse(raw);
15000
15709
  const servers = config.mcpServers ?? {};
15001
15710
  if ("mikromcp" in servers || "mikrotik-mcp-server" in servers) {
@@ -15013,7 +15722,9 @@ function checkClaudeDesktop() {
15013
15722
  );
15014
15723
  }
15015
15724
  } catch (err) {
15016
- warn(`Could not parse Claude Desktop config: ${err instanceof Error ? err.message : String(err)}`);
15725
+ warn(
15726
+ `Could not parse Claude Desktop config: ${err instanceof Error ? err.message : String(err)}`
15727
+ );
15017
15728
  }
15018
15729
  }
15019
15730
  function checkSkill() {
@@ -15092,13 +15803,7 @@ async function runDoctor() {
15092
15803
  // src/cli/init.ts
15093
15804
  import { input, confirm, select } from "@inquirer/prompts";
15094
15805
  import { randomBytes as randomBytes2 } from "crypto";
15095
- import {
15096
- existsSync as existsSync4,
15097
- readFileSync as readFileSync6,
15098
- writeFileSync,
15099
- mkdirSync,
15100
- copyFileSync
15101
- } from "fs";
15806
+ import { existsSync as existsSync4, readFileSync as readFileSync8, writeFileSync, mkdirSync, copyFileSync } from "fs";
15102
15807
  import { homedir as homedir3 } from "os";
15103
15808
  import { join as join4 } from "path";
15104
15809
  import { parse as yamlParse, Document, visit } from "yaml";
@@ -15199,7 +15904,18 @@ async function collectRouterInfo() {
15199
15904
  default: "7",
15200
15905
  validate: (v) => v.trim().length > 0 ? true : "Version is required"
15201
15906
  });
15202
- return { routerId: routerId2, host, port, tlsEnabled, rejectUnauthorized, envPrefix, routerUser, routerPass, tags, rosVersion };
15907
+ return {
15908
+ routerId: routerId2,
15909
+ host,
15910
+ port,
15911
+ tlsEnabled,
15912
+ rejectUnauthorized,
15913
+ envPrefix,
15914
+ routerUser,
15915
+ routerPass,
15916
+ tags,
15917
+ rosVersion
15918
+ };
15203
15919
  }
15204
15920
  async function collectDefaultRouterPreference(routerId2) {
15205
15921
  return confirm({
@@ -15266,7 +15982,10 @@ async function collectTransport() {
15266
15982
  const transport2 = await select({
15267
15983
  message: "Transport mode:",
15268
15984
  choices: [
15269
- { value: "stdio", name: "stdio \u2014 launched by Claude Desktop or an MCP client (recommended)" },
15985
+ {
15986
+ value: "stdio",
15987
+ name: "stdio \u2014 launched by Claude Desktop or an MCP client (recommended)"
15988
+ },
15270
15989
  { value: "http", name: "http \u2014 standalone HTTP server with bearer-token auth" }
15271
15990
  ],
15272
15991
  default: "stdio"
@@ -15308,7 +16027,7 @@ function writeRoutersYaml(data) {
15308
16027
  };
15309
16028
  let parsed = { routers: {} };
15310
16029
  if (existsSync4(filePath)) {
15311
- const raw = readFileSync6(filePath, "utf-8");
16030
+ const raw = readFileSync8(filePath, "utf-8");
15312
16031
  parsed = yamlParse(raw) ?? { routers: {} };
15313
16032
  if (!parsed.routers) parsed.routers = {};
15314
16033
  }
@@ -15327,7 +16046,7 @@ function writeIdentitiesYaml(data) {
15327
16046
  };
15328
16047
  let parsed = { identities: {} };
15329
16048
  if (existsSync4(filePath)) {
15330
- const raw = readFileSync6(filePath, "utf-8");
16049
+ const raw = readFileSync8(filePath, "utf-8");
15331
16050
  parsed = yamlParse(raw) ?? { identities: {} };
15332
16051
  if (!parsed.identities) parsed.identities = {};
15333
16052
  }
@@ -15393,7 +16112,7 @@ function registerClaudeDesktop() {
15393
16112
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
15394
16113
  const backupPath = `${configPath}.backup-${timestamp}`;
15395
16114
  copyFileSync(configPath, backupPath);
15396
- const raw = readFileSync6(configPath, "utf-8");
16115
+ const raw = readFileSync8(configPath, "utf-8");
15397
16116
  const config = JSON.parse(raw);
15398
16117
  if (!config.mcpServers || typeof config.mcpServers !== "object") {
15399
16118
  config.mcpServers = {};
@@ -15484,21 +16203,25 @@ async function runInit() {
15484
16203
  }
15485
16204
  if (data.createIdentity && data.rawToken) {
15486
16205
  console.log(
15487
- chalk2.bold.yellow(` \u2705 Bearer token: ${data.rawToken} (save this \u2014 it won't be shown again)`)
16206
+ chalk2.bold.yellow(
16207
+ ` \u2705 Bearer token: ${data.rawToken} (save this \u2014 it won't be shown again)`
16208
+ )
15488
16209
  );
15489
16210
  }
15490
16211
  console.log(chalk2.bold("\nNext steps:"));
15491
16212
  console.log(chalk2.dim(" 1. Run: mikromcp doctor"));
15492
16213
  console.log(chalk2.dim(" 2. Restart Claude Desktop to load the MCP server"));
15493
16214
  console.log(
15494
- chalk2.dim(" 3. (Optional) Install the usage skill so your AI assistant drives MikroMCP safely")
16215
+ chalk2.dim(
16216
+ " 3. (Optional) Install the usage skill so your AI assistant drives MikroMCP safely"
16217
+ )
15495
16218
  );
15496
16219
  console.log(chalk2.dim(" See: docs/wiki/Using-the-Skill.md"));
15497
16220
  console.log();
15498
16221
  }
15499
16222
 
15500
16223
  // src/cli/index.ts
15501
- var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.9.0");
16224
+ var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.11.0");
15502
16225
  program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
15503
16226
  program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
15504
16227
  program.command("init").description("Interactive setup wizard").action(runInit);