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/README.md +5 -1
- package/config/routers.example.yaml +2 -0
- package/dist/main.js +1105 -382
- package/package.json +3 -1
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 {
|
|
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 = {
|
|
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 !==
|
|
821
|
+
if (isTrue(rec.disabled) !== parsed.disabled) {
|
|
813
822
|
changes.disabled = disabledStr;
|
|
814
|
-
diff.push({
|
|
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: {
|
|
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, [
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
if (parsed.routingTable !== void 0)
|
|
1713
|
-
const output = await context.sshClient.execute(
|
|
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:
|
|
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
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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:
|
|
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:
|
|
1799
|
-
dstAddress:
|
|
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
|
|
1833
|
-
if (parsed.srcAddress !== void 0)
|
|
1834
|
-
if (parsed.dstAddress !== void 0)
|
|
1835
|
-
const raw = await context.sshClient.execute(
|
|
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(
|
|
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
|
|
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, [
|
|
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 {
|
|
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
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
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
|
|
2822
|
+
if (parsed.ssid !== void 0 && !sameValue(rec.ssid, parsed.ssid)) {
|
|
2736
2823
|
changes.ssid = parsed.ssid;
|
|
2737
|
-
diff.push({
|
|
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 {
|
|
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, [
|
|
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"] !==
|
|
3475
|
+
if (isTrue(current["allow-remote-requests"]) !== parsed.allowRemoteRequests) {
|
|
3379
3476
|
changes["allow-remote-requests"] = next;
|
|
3380
|
-
diff.push({
|
|
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"]
|
|
3486
|
+
if (!sameValue(current["max-udp-packet-size"], next)) {
|
|
3386
3487
|
changes["max-udp-packet-size"] = next;
|
|
3387
|
-
diff.push({
|
|
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({
|
|
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"]
|
|
3505
|
+
if (!sameValue(current["cache-size"], next)) {
|
|
3397
3506
|
changes["cache-size"] = next;
|
|
3398
|
-
diff.push({
|
|
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.
|
|
3414
|
-
log12.info(
|
|
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 = [
|
|
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, [
|
|
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: {
|
|
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(
|
|
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: {
|
|
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: {
|
|
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(
|
|
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: {
|
|
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
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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({
|
|
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({
|
|
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: {
|
|
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: {
|
|
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, [
|
|
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 ===
|
|
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:
|
|
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
|
|
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
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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(
|
|
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: {
|
|
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
|
|
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, [
|
|
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, [
|
|
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 !==
|
|
9704
|
+
if (parsed.enabled !== void 0 && isTrue(current.enabled) !== parsed.enabled) {
|
|
9458
9705
|
changes.enabled = String(parsed.enabled);
|
|
9459
|
-
diff.push({
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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: {
|
|
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"]
|
|
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: {
|
|
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: [
|
|
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"], {
|
|
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(
|
|
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
|
-
{
|
|
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: {
|
|
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"], {
|
|
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: {
|
|
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 = [
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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({
|
|
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({
|
|
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)
|
|
10185
|
-
|
|
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({
|
|
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
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
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
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
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(
|
|
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(
|
|
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({
|
|
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
|
|
10792
|
+
if (parsed.port !== void 0 && !sameValue(existing.port, parsed.port)) {
|
|
10458
10793
|
updates.port = String(parsed.port);
|
|
10459
|
-
diff.push({
|
|
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({
|
|
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({
|
|
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
|
|
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([
|
|
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
|
|
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: [
|
|
11010
|
+
diff: [change]
|
|
10622
11011
|
}
|
|
10623
11012
|
};
|
|
10624
11013
|
}
|
|
10625
|
-
await context
|
|
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: {
|
|
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
|
|
11040
|
+
if (parsed.port !== void 0 && !sameValue(server.port, parsed.port)) {
|
|
10651
11041
|
updates.port = String(parsed.port);
|
|
10652
|
-
diff.push({
|
|
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({
|
|
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({
|
|
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
|
|
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: {
|
|
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", {
|
|
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: {
|
|
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: {
|
|
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(
|
|
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", {
|
|
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)
|
|
10778
|
-
|
|
10779
|
-
if (parsed.
|
|
10780
|
-
|
|
10781
|
-
if (parsed.
|
|
10782
|
-
|
|
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: {
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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: {
|
|
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(
|
|
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(
|
|
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: [
|
|
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: {
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
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(
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
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: {
|
|
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(
|
|
12031
|
+
log43.info(
|
|
12032
|
+
{ routerId: context.routerId, url: parsed.url, method: parsed.method },
|
|
12033
|
+
"Fetching URL"
|
|
12034
|
+
);
|
|
11545
12035
|
try {
|
|
11546
|
-
const body = {
|
|
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(
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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(
|
|
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: {
|
|
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 = [
|
|
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: {
|
|
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: {
|
|
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({
|
|
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"]
|
|
12227
|
+
if (!sameValue(current["ram-high"], next)) {
|
|
11691
12228
|
changes["ram-high"] = next;
|
|
11692
|
-
diff.push({
|
|
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({
|
|
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.
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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(
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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(
|
|
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: {
|
|
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
|
-
{
|
|
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: {
|
|
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(
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
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(
|
|
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
|
-
|
|
13529
|
-
|
|
13530
|
-
|
|
13531
|
-
|
|
13532
|
-
|
|
13533
|
-
|
|
13534
|
-
|
|
13535
|
-
|
|
13536
|
-
|
|
13537
|
-
|
|
13538
|
-
|
|
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
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
|
|
13574
|
-
|
|
13575
|
-
|
|
13576
|
-
|
|
13577
|
-
|
|
13578
|
-
|
|
13579
|
-
|
|
13580
|
-
|
|
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
|
-
|
|
13721
|
-
|
|
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
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
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
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
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(
|
|
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({
|
|
14638
|
+
recordOutcome({
|
|
14639
|
+
journalPath,
|
|
14640
|
+
journalId,
|
|
14641
|
+
phase: "success",
|
|
14642
|
+
durationMs: elapsed
|
|
14643
|
+
});
|
|
13975
14644
|
}
|
|
13976
|
-
auditLog(
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
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({
|
|
14665
|
+
recordOutcome({
|
|
14666
|
+
journalPath,
|
|
14667
|
+
journalId,
|
|
14668
|
+
phase: "failure",
|
|
14669
|
+
outcome: message,
|
|
14670
|
+
durationMs: elapsed
|
|
14671
|
+
});
|
|
13994
14672
|
}
|
|
13995
|
-
auditLog(
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14007
|
-
|
|
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
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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 {
|
|
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
|
-
{
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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.
|
|
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);
|