mikromcp 1.9.0 → 1.10.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/dist/main.js +1012 -342
- 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 {
|
|
@@ -1927,7 +1951,14 @@ function parseRouterClock(dateStr, timeStr) {
|
|
|
1927
1951
|
const ss = parseInt(time[3], 10);
|
|
1928
1952
|
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr);
|
|
1929
1953
|
if (iso) {
|
|
1930
|
-
return new Date(
|
|
1954
|
+
return new Date(
|
|
1955
|
+
parseInt(iso[1], 10),
|
|
1956
|
+
parseInt(iso[2], 10) - 1,
|
|
1957
|
+
parseInt(iso[3], 10),
|
|
1958
|
+
hh,
|
|
1959
|
+
mm,
|
|
1960
|
+
ss
|
|
1961
|
+
);
|
|
1931
1962
|
}
|
|
1932
1963
|
const named = /^([a-z]{3})\/(\d{2})\/(\d{4})$/i.exec(dateStr);
|
|
1933
1964
|
if (named) {
|
|
@@ -1963,7 +1994,8 @@ var getLogTool = {
|
|
|
1963
1994
|
const clock = Array.isArray(clockResult) ? clockResult[0] : clockResult;
|
|
1964
1995
|
const routerNow = parseRouterClock(clock?.date, clock?.time);
|
|
1965
1996
|
if (routerNow !== null) now = routerNow;
|
|
1966
|
-
else
|
|
1997
|
+
else
|
|
1998
|
+
log7.warn({ routerId: context.routerId }, "Unparseable router clock; using host clock");
|
|
1967
1999
|
} catch {
|
|
1968
2000
|
log7.warn({ routerId: context.routerId }, "Router clock unavailable; using host clock");
|
|
1969
2001
|
}
|
|
@@ -2077,9 +2109,7 @@ function checkCommand(command, policy) {
|
|
|
2077
2109
|
}
|
|
2078
2110
|
if (policy.allow.length > 0) {
|
|
2079
2111
|
const normAllow = policy.allow.map((p) => normalizeCommand(p));
|
|
2080
|
-
const everySegmentAllowed = normSegs.every(
|
|
2081
|
-
(seg) => normAllow.some((p) => globMatch(p, seg))
|
|
2082
|
-
);
|
|
2112
|
+
const everySegmentAllowed = normSegs.every((seg) => normAllow.some((p) => globMatch(p, seg)));
|
|
2083
2113
|
if (!everySegmentAllowed) {
|
|
2084
2114
|
throw new MikroMCPError({
|
|
2085
2115
|
category: "VALIDATION" /* VALIDATION */,
|
|
@@ -2350,7 +2380,14 @@ var listBridgesTool = {
|
|
|
2350
2380
|
paginated,
|
|
2351
2381
|
total,
|
|
2352
2382
|
parsed.offset,
|
|
2353
|
-
(b) => compactFields(b, [
|
|
2383
|
+
(b) => compactFields(b, [
|
|
2384
|
+
"name",
|
|
2385
|
+
"protocol-mode",
|
|
2386
|
+
"vlan-filtering",
|
|
2387
|
+
"mtu",
|
|
2388
|
+
"disabled",
|
|
2389
|
+
"comment"
|
|
2390
|
+
])
|
|
2354
2391
|
),
|
|
2355
2392
|
structuredContent: {
|
|
2356
2393
|
routerId: context.routerId,
|
|
@@ -2601,7 +2638,11 @@ var listWifiTool = {
|
|
|
2601
2638
|
limit: void 0,
|
|
2602
2639
|
offset: void 0
|
|
2603
2640
|
});
|
|
2604
|
-
const {
|
|
2641
|
+
const {
|
|
2642
|
+
items: paginated,
|
|
2643
|
+
total,
|
|
2644
|
+
hasMore
|
|
2645
|
+
} = paginate(interfaces, parsed.offset, parsed.limit);
|
|
2605
2646
|
return {
|
|
2606
2647
|
content: listContent(
|
|
2607
2648
|
"WiFi interfaces",
|
|
@@ -2725,16 +2766,22 @@ var manageWifiTool = {
|
|
|
2725
2766
|
const rec = existing[0];
|
|
2726
2767
|
const changes = {};
|
|
2727
2768
|
const diff = [];
|
|
2728
|
-
if (parsed.disabled !== void 0) {
|
|
2769
|
+
if (parsed.disabled !== void 0 && isTrue(rec.disabled) !== parsed.disabled) {
|
|
2729
2770
|
const desired = parsed.disabled ? "true" : "false";
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2771
|
+
changes.disabled = desired;
|
|
2772
|
+
diff.push({
|
|
2773
|
+
property: "disabled",
|
|
2774
|
+
before: rec.disabled === void 0 ? null : String(rec.disabled),
|
|
2775
|
+
after: desired
|
|
2776
|
+
});
|
|
2734
2777
|
}
|
|
2735
|
-
if (parsed.ssid !== void 0 && rec.ssid
|
|
2778
|
+
if (parsed.ssid !== void 0 && !sameValue(rec.ssid, parsed.ssid)) {
|
|
2736
2779
|
changes.ssid = parsed.ssid;
|
|
2737
|
-
diff.push({
|
|
2780
|
+
diff.push({
|
|
2781
|
+
property: "ssid",
|
|
2782
|
+
before: rec.ssid === void 0 ? null : String(rec.ssid),
|
|
2783
|
+
after: parsed.ssid
|
|
2784
|
+
});
|
|
2738
2785
|
}
|
|
2739
2786
|
if (diff.length === 0) {
|
|
2740
2787
|
return {
|
|
@@ -2788,7 +2835,11 @@ var listWgTool = {
|
|
|
2788
2835
|
limit: void 0,
|
|
2789
2836
|
offset: void 0
|
|
2790
2837
|
});
|
|
2791
|
-
const {
|
|
2838
|
+
const {
|
|
2839
|
+
items: paginated,
|
|
2840
|
+
total,
|
|
2841
|
+
hasMore
|
|
2842
|
+
} = paginate(interfaces, parsed.offset, parsed.limit);
|
|
2792
2843
|
return {
|
|
2793
2844
|
content: listContent(
|
|
2794
2845
|
"WireGuard interfaces",
|
|
@@ -2848,7 +2899,13 @@ var listPeersTool = {
|
|
|
2848
2899
|
peers,
|
|
2849
2900
|
total,
|
|
2850
2901
|
parsed.offset,
|
|
2851
|
-
(p) => compactFields(p, [
|
|
2902
|
+
(p) => compactFields(p, [
|
|
2903
|
+
"interface",
|
|
2904
|
+
"public-key",
|
|
2905
|
+
"endpoint-address",
|
|
2906
|
+
"allowed-address",
|
|
2907
|
+
"disabled"
|
|
2908
|
+
])
|
|
2852
2909
|
),
|
|
2853
2910
|
structuredContent: {
|
|
2854
2911
|
routerId: context.routerId,
|
|
@@ -3018,10 +3075,7 @@ var manageWgIfaceTool = {
|
|
|
3018
3075
|
const body = { name: parsed.name, mtu: String(parsed.mtu) };
|
|
3019
3076
|
if (parsed.listenPort !== void 0) body["listen-port"] = String(parsed.listenPort);
|
|
3020
3077
|
if (parsed.comment) body.comment = parsed.comment;
|
|
3021
|
-
const created = await context.routerClient.create(
|
|
3022
|
-
"interface/wireguard",
|
|
3023
|
-
body
|
|
3024
|
-
);
|
|
3078
|
+
const created = await context.routerClient.create("interface/wireguard", body);
|
|
3025
3079
|
log11.info({ name: parsed.name, id: created[".id"] }, "WireGuard interface created");
|
|
3026
3080
|
return {
|
|
3027
3081
|
content: `Created WireGuard interface "${parsed.name}". Public key: ${created["public-key"] ?? "unknown"}`,
|
|
@@ -3366,7 +3420,6 @@ var manageDnsSettingsTool = {
|
|
|
3366
3420
|
try {
|
|
3367
3421
|
const results2 = await context.routerClient.get("ip/dns");
|
|
3368
3422
|
const current = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
|
|
3369
|
-
const id = current[".id"];
|
|
3370
3423
|
const changes = {};
|
|
3371
3424
|
const diff = [];
|
|
3372
3425
|
if (parsed.servers !== void 0 && current.servers !== parsed.servers) {
|
|
@@ -3375,27 +3428,43 @@ var manageDnsSettingsTool = {
|
|
|
3375
3428
|
}
|
|
3376
3429
|
if (parsed.allowRemoteRequests !== void 0) {
|
|
3377
3430
|
const next = String(parsed.allowRemoteRequests);
|
|
3378
|
-
if (current["allow-remote-requests"] !==
|
|
3431
|
+
if (isTrue(current["allow-remote-requests"]) !== parsed.allowRemoteRequests) {
|
|
3379
3432
|
changes["allow-remote-requests"] = next;
|
|
3380
|
-
diff.push({
|
|
3433
|
+
diff.push({
|
|
3434
|
+
property: "allow-remote-requests",
|
|
3435
|
+
before: current["allow-remote-requests"] === void 0 ? null : String(current["allow-remote-requests"]),
|
|
3436
|
+
after: next
|
|
3437
|
+
});
|
|
3381
3438
|
}
|
|
3382
3439
|
}
|
|
3383
3440
|
if (parsed.maxUdpPacketSize !== void 0) {
|
|
3384
3441
|
const next = String(parsed.maxUdpPacketSize);
|
|
3385
|
-
if (current["max-udp-packet-size"]
|
|
3442
|
+
if (!sameValue(current["max-udp-packet-size"], next)) {
|
|
3386
3443
|
changes["max-udp-packet-size"] = next;
|
|
3387
|
-
diff.push({
|
|
3444
|
+
diff.push({
|
|
3445
|
+
property: "max-udp-packet-size",
|
|
3446
|
+
before: current["max-udp-packet-size"] === void 0 ? null : String(current["max-udp-packet-size"]),
|
|
3447
|
+
after: next
|
|
3448
|
+
});
|
|
3388
3449
|
}
|
|
3389
3450
|
}
|
|
3390
3451
|
if (parsed.cacheMaxTtl !== void 0 && current["cache-max-ttl"] !== parsed.cacheMaxTtl) {
|
|
3391
3452
|
changes["cache-max-ttl"] = parsed.cacheMaxTtl;
|
|
3392
|
-
diff.push({
|
|
3453
|
+
diff.push({
|
|
3454
|
+
property: "cache-max-ttl",
|
|
3455
|
+
before: current["cache-max-ttl"] ?? null,
|
|
3456
|
+
after: parsed.cacheMaxTtl
|
|
3457
|
+
});
|
|
3393
3458
|
}
|
|
3394
3459
|
if (parsed.cacheSize !== void 0) {
|
|
3395
3460
|
const next = String(parsed.cacheSize);
|
|
3396
|
-
if (current["cache-size"]
|
|
3461
|
+
if (!sameValue(current["cache-size"], next)) {
|
|
3397
3462
|
changes["cache-size"] = next;
|
|
3398
|
-
diff.push({
|
|
3463
|
+
diff.push({
|
|
3464
|
+
property: "cache-size",
|
|
3465
|
+
before: current["cache-size"] === void 0 ? null : String(current["cache-size"]),
|
|
3466
|
+
after: next
|
|
3467
|
+
});
|
|
3399
3468
|
}
|
|
3400
3469
|
}
|
|
3401
3470
|
if (Object.keys(changes).length === 0) {
|
|
@@ -3410,8 +3479,11 @@ var manageDnsSettingsTool = {
|
|
|
3410
3479
|
structuredContent: { action: "dry_run", diff }
|
|
3411
3480
|
};
|
|
3412
3481
|
}
|
|
3413
|
-
await context.routerClient.
|
|
3414
|
-
log12.info(
|
|
3482
|
+
await context.routerClient.execute("ip/dns/set", changes);
|
|
3483
|
+
log12.info(
|
|
3484
|
+
{ routerId: context.routerId, changes: Object.keys(changes) },
|
|
3485
|
+
"DNS settings updated"
|
|
3486
|
+
);
|
|
3415
3487
|
return {
|
|
3416
3488
|
content: `Updated DNS settings on ${context.routerId}.`,
|
|
3417
3489
|
structuredContent: { action: "updated", routerId: context.routerId, diff }
|
|
@@ -3421,7 +3493,12 @@ var manageDnsSettingsTool = {
|
|
|
3421
3493
|
}
|
|
3422
3494
|
}
|
|
3423
3495
|
};
|
|
3424
|
-
var dnsTools = [
|
|
3496
|
+
var dnsTools = [
|
|
3497
|
+
listDnsTool,
|
|
3498
|
+
manageDnsTool,
|
|
3499
|
+
getDnsSettingsTool,
|
|
3500
|
+
manageDnsSettingsTool
|
|
3501
|
+
];
|
|
3425
3502
|
|
|
3426
3503
|
// src/domain/tools/mangle-tools.ts
|
|
3427
3504
|
import { z as z15 } from "zod";
|
|
@@ -3905,7 +3982,14 @@ var listRoutingRulesTool = {
|
|
|
3905
3982
|
rules,
|
|
3906
3983
|
rules.length,
|
|
3907
3984
|
0,
|
|
3908
|
-
(r) => compactFields(r, [
|
|
3985
|
+
(r) => compactFields(r, [
|
|
3986
|
+
"src-address",
|
|
3987
|
+
"dst-address",
|
|
3988
|
+
"action",
|
|
3989
|
+
"table",
|
|
3990
|
+
"interface",
|
|
3991
|
+
"disabled"
|
|
3992
|
+
])
|
|
3909
3993
|
),
|
|
3910
3994
|
structuredContent: { routerId: context.routerId, rules, total: rules.length }
|
|
3911
3995
|
};
|
|
@@ -5074,14 +5158,27 @@ var uploadFileTool = {
|
|
|
5074
5158
|
await context.sftpClient.upload(`${parsed.name}.mikromcp-probe`, "");
|
|
5075
5159
|
return {
|
|
5076
5160
|
content: `Dry run: SFTP connectivity to ${context.routerId} verified. Would upload "${parsed.name}".`,
|
|
5077
|
-
structuredContent: {
|
|
5161
|
+
structuredContent: {
|
|
5162
|
+
action: "dry_run",
|
|
5163
|
+
transport: "sftp",
|
|
5164
|
+
name: parsed.name,
|
|
5165
|
+
routerId: context.routerId
|
|
5166
|
+
}
|
|
5078
5167
|
};
|
|
5079
5168
|
} catch (sftpErr) {
|
|
5080
|
-
log20.warn(
|
|
5169
|
+
log20.warn(
|
|
5170
|
+
{ err: sftpErr, routerId: context.routerId },
|
|
5171
|
+
"SFTP dry-run probe failed \u2014 falling back to FTP"
|
|
5172
|
+
);
|
|
5081
5173
|
await context.ftpClient.connect();
|
|
5082
5174
|
return {
|
|
5083
5175
|
content: `Dry run: FTP connectivity to ${context.routerId} verified${ftpPlaintextNote}. Would upload "${parsed.name}".`,
|
|
5084
|
-
structuredContent: {
|
|
5176
|
+
structuredContent: {
|
|
5177
|
+
action: "dry_run",
|
|
5178
|
+
transport: "ftp",
|
|
5179
|
+
name: parsed.name,
|
|
5180
|
+
routerId: context.routerId
|
|
5181
|
+
}
|
|
5085
5182
|
};
|
|
5086
5183
|
}
|
|
5087
5184
|
}
|
|
@@ -5090,15 +5187,28 @@ var uploadFileTool = {
|
|
|
5090
5187
|
log20.info({ name: parsed.name }, "File uploaded via SFTP");
|
|
5091
5188
|
return {
|
|
5092
5189
|
content: `Uploaded "${parsed.name}" to ${context.routerId} via SFTP.`,
|
|
5093
|
-
structuredContent: {
|
|
5190
|
+
structuredContent: {
|
|
5191
|
+
action: "uploaded",
|
|
5192
|
+
transport: "sftp",
|
|
5193
|
+
name: parsed.name,
|
|
5194
|
+
routerId: context.routerId
|
|
5195
|
+
}
|
|
5094
5196
|
};
|
|
5095
5197
|
} catch (sftpErr) {
|
|
5096
|
-
log20.warn(
|
|
5198
|
+
log20.warn(
|
|
5199
|
+
{ err: sftpErr, routerId: context.routerId },
|
|
5200
|
+
"SFTP upload failed \u2014 falling back to FTP"
|
|
5201
|
+
);
|
|
5097
5202
|
await context.ftpClient.upload(parsed.name, parsed.content);
|
|
5098
5203
|
log20.info({ name: parsed.name }, "File uploaded via FTP");
|
|
5099
5204
|
return {
|
|
5100
5205
|
content: `Uploaded "${parsed.name}" to ${context.routerId}${ftpPlaintextNote}.`,
|
|
5101
|
-
structuredContent: {
|
|
5206
|
+
structuredContent: {
|
|
5207
|
+
action: "uploaded",
|
|
5208
|
+
transport: "ftp",
|
|
5209
|
+
name: parsed.name,
|
|
5210
|
+
routerId: context.routerId
|
|
5211
|
+
}
|
|
5102
5212
|
};
|
|
5103
5213
|
}
|
|
5104
5214
|
} catch (err) {
|
|
@@ -6235,12 +6345,12 @@ var RouterOSRestClient = class {
|
|
|
6235
6345
|
const query = buildListQuery(options);
|
|
6236
6346
|
let raw;
|
|
6237
6347
|
if (query.method === "POST") {
|
|
6238
|
-
raw = await this.doRequest("POST", `${this.baseUrl}/${path}`, query.body);
|
|
6348
|
+
raw = await this.doRequest("POST", `${this.baseUrl}/${path}/print`, query.body);
|
|
6239
6349
|
} else {
|
|
6240
6350
|
const qs = query.queryParams ? "?" + new URLSearchParams(query.queryParams).toString() : "";
|
|
6241
6351
|
raw = await this.doRequest("GET", `${this.baseUrl}/${path}${qs}`);
|
|
6242
6352
|
}
|
|
6243
|
-
const rawArray = Array.isArray(raw) ? raw : [raw];
|
|
6353
|
+
const rawArray = Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw];
|
|
6244
6354
|
const parsed = parseRecords(rawArray);
|
|
6245
6355
|
if (options?.limit !== void 0 || options?.offset !== void 0) {
|
|
6246
6356
|
const page = applyPagination(parsed, options?.limit, options?.offset);
|
|
@@ -6550,7 +6660,12 @@ var RUNTIME_FIELDS = /* @__PURE__ */ new Set([
|
|
|
6550
6660
|
"tx-byte",
|
|
6551
6661
|
"rx-packet",
|
|
6552
6662
|
"tx-packet",
|
|
6553
|
-
"uptime"
|
|
6663
|
+
"uptime",
|
|
6664
|
+
// Read-only fields on set-menu singletons. Harmless in a normal diff, but a
|
|
6665
|
+
// singleton is restored by writing the whole record back through /set, and
|
|
6666
|
+
// RouterOS rejects the request outright if it carries a read-only parameter.
|
|
6667
|
+
"cache-used",
|
|
6668
|
+
"dynamic-servers"
|
|
6554
6669
|
]);
|
|
6555
6670
|
var ORDER_SENSITIVE_PATHS = /* @__PURE__ */ new Set([
|
|
6556
6671
|
"ip/firewall/filter",
|
|
@@ -6586,16 +6701,16 @@ var SEMANTIC_KEYS = {
|
|
|
6586
6701
|
"system/scheduler": ["name"],
|
|
6587
6702
|
"system/script": ["name"],
|
|
6588
6703
|
"system/package": ["name"],
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6704
|
+
container: ["name"],
|
|
6705
|
+
certificate: ["name"],
|
|
6706
|
+
file: ["name"],
|
|
6592
6707
|
"interface/vrrp": ["name"],
|
|
6593
6708
|
"ip/dhcp-server": ["name"],
|
|
6594
6709
|
"ip/ipsec/peer": ["name"],
|
|
6595
6710
|
"ip/pool": ["name"],
|
|
6596
6711
|
"queue/simple": ["name"],
|
|
6597
6712
|
"tool/netwatch": ["host"],
|
|
6598
|
-
|
|
6713
|
+
user: ["name"]
|
|
6599
6714
|
};
|
|
6600
6715
|
function normalizeValue(v) {
|
|
6601
6716
|
if (v === true || v === "true" || v === "yes") return "true";
|
|
@@ -6641,6 +6756,24 @@ function computeRestorePlan(path, beforeRaw, currentRaw) {
|
|
|
6641
6756
|
`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
6757
|
);
|
|
6643
6758
|
}
|
|
6759
|
+
if (isSingleton(before, current)) {
|
|
6760
|
+
const beforeRecord = before[0];
|
|
6761
|
+
if (beforeRecord === void 0) {
|
|
6762
|
+
warnings.push(
|
|
6763
|
+
`Snapshot for ${path} holds no record for this singleton \u2014 its current settings are left untouched, as a set-menu singleton cannot be removed.`
|
|
6764
|
+
);
|
|
6765
|
+
return { path, toCreate: [], toRemove: [], toUpdate: [], warnings };
|
|
6766
|
+
}
|
|
6767
|
+
const currentRecord = current[0];
|
|
6768
|
+
if (currentRecord !== void 0 && recordsAreEqual(beforeRecord, currentRecord)) {
|
|
6769
|
+
return { path, toCreate: [], toRemove: [], toUpdate: [], warnings };
|
|
6770
|
+
}
|
|
6771
|
+
const { ".id": _id, ...data } = beforeRecord;
|
|
6772
|
+
const toSet = Object.fromEntries(
|
|
6773
|
+
Object.entries(data).map(([k, v]) => [k, String(v)])
|
|
6774
|
+
);
|
|
6775
|
+
return { path, toCreate: [], toRemove: [], toUpdate: [], toSet, warnings };
|
|
6776
|
+
}
|
|
6644
6777
|
const keys = SEMANTIC_KEYS[path];
|
|
6645
6778
|
const canUseKeys = keys !== void 0 && !hasDuplicateKeys(before, keys) && !hasDuplicateKeys(current, keys);
|
|
6646
6779
|
if (!canUseKeys) {
|
|
@@ -6670,7 +6803,16 @@ function computeRestorePlan(path, beforeRaw, currentRaw) {
|
|
|
6670
6803
|
const toRemove = current.filter((r) => !beforeByKey.has(semanticKey(r, keys))).map((r) => r[".id"]);
|
|
6671
6804
|
return applyUserRestriction(path, { path, toCreate, toRemove, toUpdate, warnings });
|
|
6672
6805
|
}
|
|
6806
|
+
function isSingleton(before, current) {
|
|
6807
|
+
if (before.length > 1 || current.length > 1) return false;
|
|
6808
|
+
if (before.length === 0 && current.length === 0) return false;
|
|
6809
|
+
return [...before, ...current].every((r) => r[".id"] === void 0);
|
|
6810
|
+
}
|
|
6673
6811
|
async function applyRestorePlan(plan, client) {
|
|
6812
|
+
if (plan.toSet !== void 0) {
|
|
6813
|
+
await client.execute(`${plan.path}/set`, plan.toSet);
|
|
6814
|
+
return;
|
|
6815
|
+
}
|
|
6674
6816
|
for (const id of plan.toRemove) {
|
|
6675
6817
|
await client.remove(plan.path, id);
|
|
6676
6818
|
}
|
|
@@ -6947,10 +7089,18 @@ function createChangeManagementTools(baseTools2) {
|
|
|
6947
7089
|
const snapshotIds = [];
|
|
6948
7090
|
for (const path of snapshotPathsFor(tool, stepParams)) {
|
|
6949
7091
|
try {
|
|
6950
|
-
const meta = await takeSnapshot(
|
|
7092
|
+
const meta = await takeSnapshot(
|
|
7093
|
+
context.deviceClient,
|
|
7094
|
+
context.routerId,
|
|
7095
|
+
path,
|
|
7096
|
+
snapshotDir
|
|
7097
|
+
);
|
|
6951
7098
|
snapshotIds.push(meta.id);
|
|
6952
7099
|
} catch (err) {
|
|
6953
|
-
log24.warn(
|
|
7100
|
+
log24.warn(
|
|
7101
|
+
{ err, path, routerId: context.routerId, step: i },
|
|
7102
|
+
"apply_plan step snapshot failed \u2014 proceeding without snapshot"
|
|
7103
|
+
);
|
|
6954
7104
|
}
|
|
6955
7105
|
}
|
|
6956
7106
|
const stepJournalId = recordAttempt({
|
|
@@ -6966,7 +7116,12 @@ function createChangeManagementTools(baseTools2) {
|
|
|
6966
7116
|
try {
|
|
6967
7117
|
const runStep = () => tool.handler(stepParams, context);
|
|
6968
7118
|
const result = context.circuitBreaker ? await context.circuitBreaker.execute(runStep) : await runStep();
|
|
6969
|
-
recordOutcome({
|
|
7119
|
+
recordOutcome({
|
|
7120
|
+
journalPath,
|
|
7121
|
+
journalId: stepJournalId,
|
|
7122
|
+
phase: "success",
|
|
7123
|
+
durationMs: Date.now() - stepStartMs
|
|
7124
|
+
});
|
|
6970
7125
|
results2.push({
|
|
6971
7126
|
stepIndex: i,
|
|
6972
7127
|
tool: step.tool,
|
|
@@ -6976,7 +7131,13 @@ function createChangeManagementTools(baseTools2) {
|
|
|
6976
7131
|
});
|
|
6977
7132
|
} catch (err) {
|
|
6978
7133
|
const error = err instanceof MikroMCPError ? err : enrichError(err, { tool: step.tool });
|
|
6979
|
-
recordOutcome({
|
|
7134
|
+
recordOutcome({
|
|
7135
|
+
journalPath,
|
|
7136
|
+
journalId: stepJournalId,
|
|
7137
|
+
phase: "failure",
|
|
7138
|
+
outcome: error.code,
|
|
7139
|
+
durationMs: Date.now() - stepStartMs
|
|
7140
|
+
});
|
|
6980
7141
|
log24.error({ err: error, tool: step.tool, step: i }, "apply_plan step failed");
|
|
6981
7142
|
return {
|
|
6982
7143
|
content: `Apply failed at step ${i + 1}/${parsed.steps.length} (${step.tool}): ${error.message}. ${i} step(s) completed before failure.`,
|
|
@@ -7058,7 +7219,7 @@ function createChangeManagementTools(baseTools2) {
|
|
|
7058
7219
|
restorePlans.push(plan);
|
|
7059
7220
|
}
|
|
7060
7221
|
const totalOps = restorePlans.reduce(
|
|
7061
|
-
(sum, p) => sum + p.toCreate.length + p.toRemove.length + p.toUpdate.length,
|
|
7222
|
+
(sum, p) => sum + p.toCreate.length + p.toRemove.length + p.toUpdate.length + (p.toSet ? 1 : 0),
|
|
7062
7223
|
0
|
|
7063
7224
|
);
|
|
7064
7225
|
const warnings = restorePlans.flatMap((p) => p.warnings);
|
|
@@ -7068,7 +7229,12 @@ ${warnings.map((w) => ` - ${w}`).join("\n")}` : "";
|
|
|
7068
7229
|
if (parsed.dryRun) {
|
|
7069
7230
|
return {
|
|
7070
7231
|
content: `Rollback dry run for journal entry "${parsed.journalId}": ${totalOps} operation(s) would be applied across ${restorePlans.length} path(s).${warningSuffix}`,
|
|
7071
|
-
structuredContent: {
|
|
7232
|
+
structuredContent: {
|
|
7233
|
+
action: "dry_run",
|
|
7234
|
+
journalId: parsed.journalId,
|
|
7235
|
+
restorePlans,
|
|
7236
|
+
warnings
|
|
7237
|
+
}
|
|
7072
7238
|
};
|
|
7073
7239
|
}
|
|
7074
7240
|
for (const plan of restorePlans) {
|
|
@@ -7076,7 +7242,12 @@ ${warnings.map((w) => ` - ${w}`).join("\n")}` : "";
|
|
|
7076
7242
|
}
|
|
7077
7243
|
return {
|
|
7078
7244
|
content: `Rolled back journal entry "${parsed.journalId}": ${totalOps} operation(s) applied across ${restorePlans.length} path(s).${warningSuffix}`,
|
|
7079
|
-
structuredContent: {
|
|
7245
|
+
structuredContent: {
|
|
7246
|
+
action: "rolled_back",
|
|
7247
|
+
journalId: parsed.journalId,
|
|
7248
|
+
restorePlans,
|
|
7249
|
+
warnings
|
|
7250
|
+
}
|
|
7080
7251
|
};
|
|
7081
7252
|
}
|
|
7082
7253
|
};
|
|
@@ -7173,7 +7344,14 @@ var listIpsecPoliciesTool = {
|
|
|
7173
7344
|
policies,
|
|
7174
7345
|
allPolicies.length,
|
|
7175
7346
|
0,
|
|
7176
|
-
(p) => compactFields(p, [
|
|
7347
|
+
(p) => compactFields(p, [
|
|
7348
|
+
"src-address",
|
|
7349
|
+
"dst-address",
|
|
7350
|
+
"protocol",
|
|
7351
|
+
"action",
|
|
7352
|
+
"level",
|
|
7353
|
+
"disabled"
|
|
7354
|
+
])
|
|
7177
7355
|
),
|
|
7178
7356
|
structuredContent: {
|
|
7179
7357
|
routerId: context.routerId,
|
|
@@ -7221,9 +7399,7 @@ var manageIpsecPeerTool = {
|
|
|
7221
7399
|
limit: void 0,
|
|
7222
7400
|
offset: void 0
|
|
7223
7401
|
});
|
|
7224
|
-
const existing = allPeers.find(
|
|
7225
|
-
(p) => p.name === parsed.name
|
|
7226
|
-
);
|
|
7402
|
+
const existing = allPeers.find((p) => p.name === parsed.name);
|
|
7227
7403
|
if (parsed.action === "add") {
|
|
7228
7404
|
if (existing) {
|
|
7229
7405
|
if (existing.address === parsed.address) {
|
|
@@ -7382,9 +7558,8 @@ var manageIpsecPolicyTool = {
|
|
|
7382
7558
|
limit: void 0,
|
|
7383
7559
|
offset: void 0
|
|
7384
7560
|
});
|
|
7385
|
-
const tunnelStr = String(parsed.tunnel);
|
|
7386
7561
|
const existing = all.find(
|
|
7387
|
-
(p) => p["src-address"] === parsed.srcAddress && p["dst-address"] === parsed.dstAddress && p.tunnel ===
|
|
7562
|
+
(p) => p["src-address"] === parsed.srcAddress && p["dst-address"] === parsed.dstAddress && isTrue(p.tunnel) === parsed.tunnel
|
|
7388
7563
|
);
|
|
7389
7564
|
if (parsed.action === "add") {
|
|
7390
7565
|
if (existing) {
|
|
@@ -7420,7 +7595,7 @@ var manageIpsecPolicyTool = {
|
|
|
7420
7595
|
const body = {
|
|
7421
7596
|
"src-address": parsed.srcAddress,
|
|
7422
7597
|
"dst-address": parsed.dstAddress,
|
|
7423
|
-
tunnel:
|
|
7598
|
+
tunnel: String(parsed.tunnel),
|
|
7424
7599
|
action: parsed.ipsecAction,
|
|
7425
7600
|
level: parsed.level
|
|
7426
7601
|
};
|
|
@@ -7623,7 +7798,7 @@ var manageCertificateTool = {
|
|
|
7623
7798
|
};
|
|
7624
7799
|
}
|
|
7625
7800
|
if (parsed.action === "trust") {
|
|
7626
|
-
if (cert.trusted
|
|
7801
|
+
if (isTrue(cert.trusted)) {
|
|
7627
7802
|
return {
|
|
7628
7803
|
content: `Certificate "${parsed.name}" is already trusted. No changes made.`,
|
|
7629
7804
|
structuredContent: { action: "already_trusted", name: parsed.name }
|
|
@@ -7646,7 +7821,7 @@ var manageCertificateTool = {
|
|
|
7646
7821
|
structuredContent: { action: "trusted", name: parsed.name }
|
|
7647
7822
|
};
|
|
7648
7823
|
}
|
|
7649
|
-
if (cert.trusted
|
|
7824
|
+
if (cert.trusted !== void 0 && !isTrue(cert.trusted)) {
|
|
7650
7825
|
return {
|
|
7651
7826
|
content: `Certificate "${parsed.name}" is already untrusted. No changes made.`,
|
|
7652
7827
|
structuredContent: { action: "already_untrusted", name: parsed.name }
|
|
@@ -7792,14 +7967,16 @@ var manageUserTool = {
|
|
|
7792
7967
|
limit: void 0,
|
|
7793
7968
|
offset: void 0
|
|
7794
7969
|
});
|
|
7795
|
-
const existing2 = allUsers2.find(
|
|
7796
|
-
(u) => u.name === parsed.name
|
|
7797
|
-
);
|
|
7970
|
+
const existing2 = allUsers2.find((u) => u.name === parsed.name);
|
|
7798
7971
|
if (existing2) {
|
|
7799
7972
|
if (existing2.group === parsed.group) {
|
|
7800
7973
|
return {
|
|
7801
7974
|
content: `User "${parsed.name}" already exists in group "${parsed.group}". No changes made.`,
|
|
7802
|
-
structuredContent: {
|
|
7975
|
+
structuredContent: {
|
|
7976
|
+
action: "already_exists",
|
|
7977
|
+
name: parsed.name,
|
|
7978
|
+
group: existing2.group
|
|
7979
|
+
}
|
|
7803
7980
|
};
|
|
7804
7981
|
}
|
|
7805
7982
|
throw new MikroMCPError({
|
|
@@ -7839,7 +8016,12 @@ var manageUserTool = {
|
|
|
7839
8016
|
log27.info({ name: parsed.name, id: created[".id"] }, "User added");
|
|
7840
8017
|
return {
|
|
7841
8018
|
content: `Added user "${parsed.name}" in group "${parsed.group}".`,
|
|
7842
|
-
structuredContent: {
|
|
8019
|
+
structuredContent: {
|
|
8020
|
+
action: "created",
|
|
8021
|
+
routerId: context.routerId,
|
|
8022
|
+
name: parsed.name,
|
|
8023
|
+
group: parsed.group
|
|
8024
|
+
}
|
|
7843
8025
|
};
|
|
7844
8026
|
}
|
|
7845
8027
|
if (parsed.action === "set-password") {
|
|
@@ -7859,9 +8041,7 @@ var manageUserTool = {
|
|
|
7859
8041
|
limit: void 0,
|
|
7860
8042
|
offset: void 0
|
|
7861
8043
|
});
|
|
7862
|
-
const existing2 = allUsers2.find(
|
|
7863
|
-
(u) => u.name === parsed.name
|
|
7864
|
-
);
|
|
8044
|
+
const existing2 = allUsers2.find((u) => u.name === parsed.name);
|
|
7865
8045
|
if (!existing2) {
|
|
7866
8046
|
throw new MikroMCPError({
|
|
7867
8047
|
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
@@ -7890,16 +8070,18 @@ var manageUserTool = {
|
|
|
7890
8070
|
log27.info({ name: parsed.name }, "User password updated");
|
|
7891
8071
|
return {
|
|
7892
8072
|
content: `Password updated for user "${parsed.name}".`,
|
|
7893
|
-
structuredContent: {
|
|
8073
|
+
structuredContent: {
|
|
8074
|
+
action: "password_set",
|
|
8075
|
+
routerId: context.routerId,
|
|
8076
|
+
name: parsed.name
|
|
8077
|
+
}
|
|
7894
8078
|
};
|
|
7895
8079
|
}
|
|
7896
8080
|
const allUsers = await context.routerClient.get("user", {
|
|
7897
8081
|
limit: void 0,
|
|
7898
8082
|
offset: void 0
|
|
7899
8083
|
});
|
|
7900
|
-
const existing = allUsers.find(
|
|
7901
|
-
(u) => u.name === parsed.name
|
|
7902
|
-
);
|
|
8084
|
+
const existing = allUsers.find((u) => u.name === parsed.name);
|
|
7903
8085
|
if (parsed.action === "remove") {
|
|
7904
8086
|
if (!existing) {
|
|
7905
8087
|
return {
|
|
@@ -8053,7 +8235,11 @@ var manageUserGroupTool = {
|
|
|
8053
8235
|
if (parsed.policy === void 0 || existing2.policy === parsed.policy) {
|
|
8054
8236
|
return {
|
|
8055
8237
|
content: `User group "${parsed.name}" already exists. No changes made.`,
|
|
8056
|
-
structuredContent: {
|
|
8238
|
+
structuredContent: {
|
|
8239
|
+
action: "already_exists",
|
|
8240
|
+
name: parsed.name,
|
|
8241
|
+
policy: existing2.policy
|
|
8242
|
+
}
|
|
8057
8243
|
};
|
|
8058
8244
|
}
|
|
8059
8245
|
throw new MikroMCPError({
|
|
@@ -8270,9 +8456,7 @@ var manageDhcpServerTool = {
|
|
|
8270
8456
|
limit: void 0,
|
|
8271
8457
|
offset: void 0
|
|
8272
8458
|
});
|
|
8273
|
-
const existing = allServers.find(
|
|
8274
|
-
(s) => s.name === parsed.name
|
|
8275
|
-
);
|
|
8459
|
+
const existing = allServers.find((s) => s.name === parsed.name);
|
|
8276
8460
|
if (parsed.action === "add") {
|
|
8277
8461
|
if (!parsed.interface) {
|
|
8278
8462
|
throw new MikroMCPError({
|
|
@@ -8499,7 +8683,10 @@ var manageIpPoolTool = {
|
|
|
8499
8683
|
snapshotPaths: ["ip/pool"],
|
|
8500
8684
|
async handler(params, context) {
|
|
8501
8685
|
const parsed = manageIpPoolInputSchema.parse(params);
|
|
8502
|
-
log30.info(
|
|
8686
|
+
log30.info(
|
|
8687
|
+
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
8688
|
+
"Managing IP pool"
|
|
8689
|
+
);
|
|
8503
8690
|
try {
|
|
8504
8691
|
const allPools = await context.routerClient.get("ip/pool", {
|
|
8505
8692
|
limit: void 0,
|
|
@@ -8567,7 +8754,10 @@ var manageIpPoolTool = {
|
|
|
8567
8754
|
if (parsed.dryRun) {
|
|
8568
8755
|
return {
|
|
8569
8756
|
content: `Dry run: Would remove IP pool "${parsed.name}".`,
|
|
8570
|
-
structuredContent: {
|
|
8757
|
+
structuredContent: {
|
|
8758
|
+
action: "dry_run",
|
|
8759
|
+
diff: [{ property: "name", before: parsed.name, after: null }]
|
|
8760
|
+
}
|
|
8571
8761
|
};
|
|
8572
8762
|
}
|
|
8573
8763
|
await context.routerClient.remove("ip/pool", existing[".id"]);
|
|
@@ -8911,7 +9101,7 @@ var manageVrrpInstanceTool = {
|
|
|
8911
9101
|
});
|
|
8912
9102
|
}
|
|
8913
9103
|
if (existing) {
|
|
8914
|
-
if (existing.interface === parsed.interface && existing.vrid
|
|
9104
|
+
if (existing.interface === parsed.interface && sameValue(existing.vrid, parsed.vrid)) {
|
|
8915
9105
|
return {
|
|
8916
9106
|
content: `VRRP instance "${parsed.name}" already exists with the same interface and VRID. No changes made.`,
|
|
8917
9107
|
structuredContent: { action: "already_exists", instance: existing }
|
|
@@ -9360,7 +9550,14 @@ var listNeighborsTool = {
|
|
|
9360
9550
|
neighbors,
|
|
9361
9551
|
allNeighbors.length,
|
|
9362
9552
|
0,
|
|
9363
|
-
(n) => compactFields(n, [
|
|
9553
|
+
(n) => compactFields(n, [
|
|
9554
|
+
"interface",
|
|
9555
|
+
"address",
|
|
9556
|
+
"mac-address",
|
|
9557
|
+
"identity",
|
|
9558
|
+
"platform",
|
|
9559
|
+
"board"
|
|
9560
|
+
])
|
|
9364
9561
|
),
|
|
9365
9562
|
structuredContent: {
|
|
9366
9563
|
routerId: context.routerId,
|
|
@@ -9411,7 +9608,14 @@ var listArpEntriesTool = {
|
|
|
9411
9608
|
entries,
|
|
9412
9609
|
allEntries.length,
|
|
9413
9610
|
0,
|
|
9414
|
-
(e) => compactFields(e, [
|
|
9611
|
+
(e) => compactFields(e, [
|
|
9612
|
+
"address",
|
|
9613
|
+
"mac-address",
|
|
9614
|
+
"interface",
|
|
9615
|
+
"status",
|
|
9616
|
+
"complete",
|
|
9617
|
+
"dynamic"
|
|
9618
|
+
])
|
|
9415
9619
|
),
|
|
9416
9620
|
structuredContent: {
|
|
9417
9621
|
routerId: context.routerId,
|
|
@@ -9451,12 +9655,15 @@ var manageNtpClientTool = {
|
|
|
9451
9655
|
try {
|
|
9452
9656
|
const results2 = await context.routerClient.get("system/ntp/client");
|
|
9453
9657
|
const current = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
|
|
9454
|
-
const id = current[".id"];
|
|
9455
9658
|
const changes = {};
|
|
9456
9659
|
const diff = [];
|
|
9457
|
-
if (parsed.enabled !== void 0 && current.enabled !==
|
|
9660
|
+
if (parsed.enabled !== void 0 && isTrue(current.enabled) !== parsed.enabled) {
|
|
9458
9661
|
changes.enabled = String(parsed.enabled);
|
|
9459
|
-
diff.push({
|
|
9662
|
+
diff.push({
|
|
9663
|
+
property: "enabled",
|
|
9664
|
+
before: current.enabled === void 0 ? void 0 : String(current.enabled),
|
|
9665
|
+
after: String(parsed.enabled)
|
|
9666
|
+
});
|
|
9460
9667
|
}
|
|
9461
9668
|
if (parsed.mode !== void 0 && current.mode !== parsed.mode) {
|
|
9462
9669
|
changes.mode = parsed.mode;
|
|
@@ -9486,7 +9693,7 @@ var manageNtpClientTool = {
|
|
|
9486
9693
|
structuredContent: { action: "dry_run", diff }
|
|
9487
9694
|
};
|
|
9488
9695
|
}
|
|
9489
|
-
await context.routerClient.
|
|
9696
|
+
await context.routerClient.execute("system/ntp/client/set", changes);
|
|
9490
9697
|
log33.info({ routerId: context.routerId }, "NTP client updated");
|
|
9491
9698
|
return {
|
|
9492
9699
|
content: `NTP client on ${context.routerId} updated.`,
|
|
@@ -9517,7 +9724,9 @@ function sanitizeComment4(comment) {
|
|
|
9517
9724
|
var manageVlanInputSchema = z34.object({
|
|
9518
9725
|
routerId,
|
|
9519
9726
|
action: z34.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
|
|
9520
|
-
name: z34.string().regex(/^[a-zA-Z0-9_-]+$/).max(15).describe(
|
|
9727
|
+
name: z34.string().regex(/^[a-zA-Z0-9_-]+$/).max(15).describe(
|
|
9728
|
+
"VLAN interface name \u2014 idempotency key (alphanumeric, hyphens, underscores, max 15 chars)"
|
|
9729
|
+
),
|
|
9521
9730
|
vlanId: z34.number().int().min(1).max(4094).optional().describe("VLAN ID (1-4094; required for add)"),
|
|
9522
9731
|
parentInterface: z34.string().optional().describe("Parent interface name (e.g., ether1, bridge1; required for add)"),
|
|
9523
9732
|
mtu: z34.number().int().min(68).max(9e3).default(1500).describe("MTU size (add only)"),
|
|
@@ -9540,13 +9749,18 @@ var manageVlanTool = {
|
|
|
9540
9749
|
async handler(params, context) {
|
|
9541
9750
|
const parsed = manageVlanInputSchema.parse(params);
|
|
9542
9751
|
const comment = sanitizeComment4(parsed.comment);
|
|
9543
|
-
log34.info(
|
|
9752
|
+
log34.info(
|
|
9753
|
+
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
9754
|
+
"Managing VLAN"
|
|
9755
|
+
);
|
|
9544
9756
|
try {
|
|
9545
9757
|
const allVlans = await context.routerClient.get("interface/vlan", {
|
|
9546
9758
|
limit: void 0,
|
|
9547
9759
|
offset: void 0
|
|
9548
9760
|
});
|
|
9549
|
-
const existing = allVlans.find(
|
|
9761
|
+
const existing = allVlans.find(
|
|
9762
|
+
(v) => sameValue(v.name, parsed.name)
|
|
9763
|
+
);
|
|
9550
9764
|
if (parsed.action === "add") {
|
|
9551
9765
|
if (parsed.vlanId === void 0) {
|
|
9552
9766
|
throw new MikroMCPError({
|
|
@@ -9563,11 +9777,14 @@ var manageVlanTool = {
|
|
|
9563
9777
|
code: "VLAN_PARENT_REQUIRED",
|
|
9564
9778
|
message: "parentInterface is required when adding a VLAN.",
|
|
9565
9779
|
details: { name: parsed.name },
|
|
9566
|
-
recoverability: {
|
|
9780
|
+
recoverability: {
|
|
9781
|
+
retryable: false,
|
|
9782
|
+
suggestedAction: "Provide the parent interface name."
|
|
9783
|
+
}
|
|
9567
9784
|
});
|
|
9568
9785
|
}
|
|
9569
9786
|
if (existing) {
|
|
9570
|
-
if (existing["vlan-id"]
|
|
9787
|
+
if (sameValue(existing["vlan-id"], parsed.vlanId) && existing.interface === parsed.parentInterface) {
|
|
9571
9788
|
log34.info({ name: parsed.name }, "VLAN already exists with matching config");
|
|
9572
9789
|
return {
|
|
9573
9790
|
content: `VLAN "${parsed.name}" already exists with VLAN ID ${parsed.vlanId} on ${parsed.parentInterface}. No changes made.`,
|
|
@@ -9628,7 +9845,10 @@ var manageVlanTool = {
|
|
|
9628
9845
|
if (parsed.dryRun) {
|
|
9629
9846
|
return {
|
|
9630
9847
|
content: `Dry run: Would remove VLAN "${parsed.name}".`,
|
|
9631
|
-
structuredContent: {
|
|
9848
|
+
structuredContent: {
|
|
9849
|
+
action: "dry_run",
|
|
9850
|
+
diff: [{ property: "name", before: parsed.name, after: null }]
|
|
9851
|
+
}
|
|
9632
9852
|
};
|
|
9633
9853
|
}
|
|
9634
9854
|
await context.routerClient.remove("interface/vlan", existing[".id"]);
|
|
@@ -9656,12 +9876,20 @@ var manageVlanTool = {
|
|
|
9656
9876
|
content: `Dry run: Would ${parsed.action} VLAN "${parsed.name}".`,
|
|
9657
9877
|
structuredContent: {
|
|
9658
9878
|
action: "dry_run",
|
|
9659
|
-
diff: [
|
|
9879
|
+
diff: [
|
|
9880
|
+
{
|
|
9881
|
+
property: "disabled",
|
|
9882
|
+
before: existing.disabled,
|
|
9883
|
+
after: parsed.action === "disable" ? "true" : "false"
|
|
9884
|
+
}
|
|
9885
|
+
]
|
|
9660
9886
|
}
|
|
9661
9887
|
};
|
|
9662
9888
|
}
|
|
9663
9889
|
const disabledValue = parsed.action === "disable" ? "true" : "false";
|
|
9664
|
-
await context.routerClient.update("interface/vlan", existing[".id"], {
|
|
9890
|
+
await context.routerClient.update("interface/vlan", existing[".id"], {
|
|
9891
|
+
disabled: disabledValue
|
|
9892
|
+
});
|
|
9665
9893
|
const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
|
|
9666
9894
|
log34.info({ name: parsed.name, action: resultAction }, "VLAN updated");
|
|
9667
9895
|
return {
|
|
@@ -9759,7 +9987,10 @@ var manageDhcpClientTool = {
|
|
|
9759
9987
|
snapshotPaths: ["ip/dhcp-client"],
|
|
9760
9988
|
async handler(params, context) {
|
|
9761
9989
|
const parsed = manageDhcpClientInputSchema.parse(params);
|
|
9762
|
-
log35.info(
|
|
9990
|
+
log35.info(
|
|
9991
|
+
{ routerId: context.routerId, action: parsed.action, interface: parsed.interface },
|
|
9992
|
+
"Managing DHCP client"
|
|
9993
|
+
);
|
|
9763
9994
|
try {
|
|
9764
9995
|
const allClients = await context.routerClient.get("ip/dhcp-client", {
|
|
9765
9996
|
limit: void 0,
|
|
@@ -9780,7 +10011,11 @@ var manageDhcpClientTool = {
|
|
|
9780
10011
|
{ property: "interface", before: null, after: parsed.interface },
|
|
9781
10012
|
{ property: "use-peer-dns", before: null, after: parsed.usePeerDns ? "yes" : "no" },
|
|
9782
10013
|
{ property: "use-peer-ntp", before: null, after: parsed.usePeerNtp ? "yes" : "no" },
|
|
9783
|
-
{
|
|
10014
|
+
{
|
|
10015
|
+
property: "add-default-route",
|
|
10016
|
+
before: null,
|
|
10017
|
+
after: parsed.addDefaultRoute ? "yes" : "no"
|
|
10018
|
+
}
|
|
9784
10019
|
];
|
|
9785
10020
|
return {
|
|
9786
10021
|
content: `Dry run: Would add DHCP client on "${parsed.interface}".`,
|
|
@@ -9821,7 +10056,11 @@ var manageDhcpClientTool = {
|
|
|
9821
10056
|
log35.info({ interface: parsed.interface }, "DHCP client removed");
|
|
9822
10057
|
return {
|
|
9823
10058
|
content: `Removed DHCP client on "${parsed.interface}".`,
|
|
9824
|
-
structuredContent: {
|
|
10059
|
+
structuredContent: {
|
|
10060
|
+
action: "removed",
|
|
10061
|
+
interface: parsed.interface,
|
|
10062
|
+
id: existing[".id"]
|
|
10063
|
+
}
|
|
9825
10064
|
};
|
|
9826
10065
|
}
|
|
9827
10066
|
if (!existing) {
|
|
@@ -9853,12 +10092,18 @@ var manageDhcpClientTool = {
|
|
|
9853
10092
|
};
|
|
9854
10093
|
}
|
|
9855
10094
|
const disabledValue = parsed.action === "disable" ? "true" : "false";
|
|
9856
|
-
await context.routerClient.update("ip/dhcp-client", existing[".id"], {
|
|
10095
|
+
await context.routerClient.update("ip/dhcp-client", existing[".id"], {
|
|
10096
|
+
disabled: disabledValue
|
|
10097
|
+
});
|
|
9857
10098
|
const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
|
|
9858
10099
|
log35.info({ interface: parsed.interface, action: resultAction }, "DHCP client updated");
|
|
9859
10100
|
return {
|
|
9860
10101
|
content: `DHCP client on "${parsed.interface}" ${resultAction}.`,
|
|
9861
|
-
structuredContent: {
|
|
10102
|
+
structuredContent: {
|
|
10103
|
+
action: resultAction,
|
|
10104
|
+
interface: parsed.interface,
|
|
10105
|
+
id: existing[".id"]
|
|
10106
|
+
}
|
|
9862
10107
|
};
|
|
9863
10108
|
} catch (err) {
|
|
9864
10109
|
throw toolError(err, context, "manage_dhcp_client");
|
|
@@ -9870,7 +10115,16 @@ var dhcpClientTools = [listDhcpClientsTool, manageDhcpClientTool];
|
|
|
9870
10115
|
// src/domain/tools/ip-service-tools.ts
|
|
9871
10116
|
import { z as z36 } from "zod";
|
|
9872
10117
|
var log36 = createLogger("ip-service-tools");
|
|
9873
|
-
var SERVICE_NAMES = [
|
|
10118
|
+
var SERVICE_NAMES = [
|
|
10119
|
+
"api",
|
|
10120
|
+
"api-ssl",
|
|
10121
|
+
"ssh",
|
|
10122
|
+
"telnet",
|
|
10123
|
+
"www",
|
|
10124
|
+
"www-ssl",
|
|
10125
|
+
"winbox",
|
|
10126
|
+
"ftp"
|
|
10127
|
+
];
|
|
9874
10128
|
var listIpServicesInputSchema = z36.object({
|
|
9875
10129
|
routerId,
|
|
9876
10130
|
name: z36.enum(SERVICE_NAMES).optional().describe("Filter by service name (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp)"),
|
|
@@ -9946,13 +10200,18 @@ var manageIpServiceTool = {
|
|
|
9946
10200
|
},
|
|
9947
10201
|
async handler(params, context) {
|
|
9948
10202
|
const parsed = manageIpServiceInputSchema.parse(params);
|
|
9949
|
-
log36.info(
|
|
10203
|
+
log36.info(
|
|
10204
|
+
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
10205
|
+
"Managing IP service"
|
|
10206
|
+
);
|
|
9950
10207
|
try {
|
|
9951
10208
|
const allServices = await context.routerClient.get("ip/service", {
|
|
9952
10209
|
limit: void 0,
|
|
9953
10210
|
offset: void 0
|
|
9954
10211
|
});
|
|
9955
|
-
const existing = allServices.find(
|
|
10212
|
+
const existing = allServices.find(
|
|
10213
|
+
(s) => s.name === parsed.name
|
|
10214
|
+
);
|
|
9956
10215
|
if (!existing) {
|
|
9957
10216
|
throw new MikroMCPError({
|
|
9958
10217
|
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
@@ -10097,13 +10356,18 @@ var managePppoeClientTool = {
|
|
|
10097
10356
|
snapshotPaths: [PPPOE_PATH],
|
|
10098
10357
|
async handler(params, context) {
|
|
10099
10358
|
const parsed = managePppoeClientInputSchema.parse(params);
|
|
10100
|
-
log37.info(
|
|
10359
|
+
log37.info(
|
|
10360
|
+
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
10361
|
+
"Managing PPPoE client"
|
|
10362
|
+
);
|
|
10101
10363
|
try {
|
|
10102
10364
|
const allClients = await context.routerClient.get(PPPOE_PATH, {
|
|
10103
10365
|
limit: void 0,
|
|
10104
10366
|
offset: void 0
|
|
10105
10367
|
});
|
|
10106
|
-
const existing = allClients.find(
|
|
10368
|
+
const existing = allClients.find(
|
|
10369
|
+
(c) => sameValue(c.name, parsed.name)
|
|
10370
|
+
);
|
|
10107
10371
|
if (parsed.action === "add") {
|
|
10108
10372
|
if (!parsed.interface) {
|
|
10109
10373
|
throw new MikroMCPError({
|
|
@@ -10163,10 +10427,18 @@ var managePppoeClientTool = {
|
|
|
10163
10427
|
diff.push({ property: "service-name", before: null, after: parsed.serviceName });
|
|
10164
10428
|
}
|
|
10165
10429
|
if (parsed.addDefaultRoute !== void 0) {
|
|
10166
|
-
diff.push({
|
|
10430
|
+
diff.push({
|
|
10431
|
+
property: "add-default-route",
|
|
10432
|
+
before: null,
|
|
10433
|
+
after: parsed.addDefaultRoute ? "yes" : "no"
|
|
10434
|
+
});
|
|
10167
10435
|
}
|
|
10168
10436
|
if (parsed.dialOnDemand !== void 0) {
|
|
10169
|
-
diff.push({
|
|
10437
|
+
diff.push({
|
|
10438
|
+
property: "dial-on-demand",
|
|
10439
|
+
before: null,
|
|
10440
|
+
after: parsed.dialOnDemand ? "yes" : "no"
|
|
10441
|
+
});
|
|
10170
10442
|
}
|
|
10171
10443
|
if (parsed.dryRun) {
|
|
10172
10444
|
return {
|
|
@@ -10181,8 +10453,10 @@ var managePppoeClientTool = {
|
|
|
10181
10453
|
};
|
|
10182
10454
|
if (parsed.password !== void 0) body.password = parsed.password;
|
|
10183
10455
|
if (parsed.serviceName !== void 0) body["service-name"] = parsed.serviceName;
|
|
10184
|
-
if (parsed.addDefaultRoute !== void 0)
|
|
10185
|
-
|
|
10456
|
+
if (parsed.addDefaultRoute !== void 0)
|
|
10457
|
+
body["add-default-route"] = parsed.addDefaultRoute ? "yes" : "no";
|
|
10458
|
+
if (parsed.dialOnDemand !== void 0)
|
|
10459
|
+
body["dial-on-demand"] = parsed.dialOnDemand ? "yes" : "no";
|
|
10186
10460
|
const created = await context.routerClient.create(PPPOE_PATH, body);
|
|
10187
10461
|
log37.info({ name: parsed.name, id: created[".id"] }, "PPPoE client created");
|
|
10188
10462
|
return {
|
|
@@ -10216,21 +10490,29 @@ var managePppoeClientTool = {
|
|
|
10216
10490
|
}
|
|
10217
10491
|
if (parsed.serviceName !== void 0 && existing["service-name"] !== parsed.serviceName) {
|
|
10218
10492
|
updates["service-name"] = parsed.serviceName;
|
|
10219
|
-
diff.push({
|
|
10493
|
+
diff.push({
|
|
10494
|
+
property: "service-name",
|
|
10495
|
+
before: existing["service-name"] ?? null,
|
|
10496
|
+
after: parsed.serviceName
|
|
10497
|
+
});
|
|
10220
10498
|
}
|
|
10221
|
-
if (parsed.addDefaultRoute !== void 0) {
|
|
10499
|
+
if (parsed.addDefaultRoute !== void 0 && isTrue(existing["add-default-route"]) !== parsed.addDefaultRoute) {
|
|
10222
10500
|
const desired = parsed.addDefaultRoute ? "yes" : "no";
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10501
|
+
updates["add-default-route"] = desired;
|
|
10502
|
+
diff.push({
|
|
10503
|
+
property: "add-default-route",
|
|
10504
|
+
before: existing["add-default-route"] === void 0 ? null : String(existing["add-default-route"]),
|
|
10505
|
+
after: desired
|
|
10506
|
+
});
|
|
10227
10507
|
}
|
|
10228
|
-
if (parsed.dialOnDemand !== void 0) {
|
|
10508
|
+
if (parsed.dialOnDemand !== void 0 && isTrue(existing["dial-on-demand"]) !== parsed.dialOnDemand) {
|
|
10229
10509
|
const desired = parsed.dialOnDemand ? "yes" : "no";
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10510
|
+
updates["dial-on-demand"] = desired;
|
|
10511
|
+
diff.push({
|
|
10512
|
+
property: "dial-on-demand",
|
|
10513
|
+
before: existing["dial-on-demand"] === void 0 ? null : String(existing["dial-on-demand"]),
|
|
10514
|
+
after: desired
|
|
10515
|
+
});
|
|
10234
10516
|
}
|
|
10235
10517
|
if (parsed.password !== void 0) {
|
|
10236
10518
|
updates.password = parsed.password;
|
|
@@ -10363,13 +10645,18 @@ var manageOvpnClientTool = {
|
|
|
10363
10645
|
snapshotPaths: [OVPN_CLIENT_PATH],
|
|
10364
10646
|
async handler(params, context) {
|
|
10365
10647
|
const parsed = manageOvpnClientInputSchema.parse(params);
|
|
10366
|
-
log38.info(
|
|
10648
|
+
log38.info(
|
|
10649
|
+
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
10650
|
+
"Managing OpenVPN client"
|
|
10651
|
+
);
|
|
10367
10652
|
try {
|
|
10368
10653
|
const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
|
|
10369
10654
|
limit: void 0,
|
|
10370
10655
|
offset: void 0
|
|
10371
10656
|
});
|
|
10372
|
-
const existing = allClients.find(
|
|
10657
|
+
const existing = allClients.find(
|
|
10658
|
+
(c) => sameValue(c.name, parsed.name)
|
|
10659
|
+
);
|
|
10373
10660
|
if (parsed.action === "add") {
|
|
10374
10661
|
if (!parsed.connectTo) {
|
|
10375
10662
|
throw new MikroMCPError({
|
|
@@ -10452,11 +10739,19 @@ var manageOvpnClientTool = {
|
|
|
10452
10739
|
const diff = [];
|
|
10453
10740
|
if (parsed.connectTo !== void 0 && existing["connect-to"] !== parsed.connectTo) {
|
|
10454
10741
|
updates["connect-to"] = parsed.connectTo;
|
|
10455
|
-
diff.push({
|
|
10742
|
+
diff.push({
|
|
10743
|
+
property: "connect-to",
|
|
10744
|
+
before: existing["connect-to"] ?? null,
|
|
10745
|
+
after: parsed.connectTo
|
|
10746
|
+
});
|
|
10456
10747
|
}
|
|
10457
|
-
if (parsed.port !== void 0 && existing.port
|
|
10748
|
+
if (parsed.port !== void 0 && !sameValue(existing.port, parsed.port)) {
|
|
10458
10749
|
updates.port = String(parsed.port);
|
|
10459
|
-
diff.push({
|
|
10750
|
+
diff.push({
|
|
10751
|
+
property: "port",
|
|
10752
|
+
before: existing.port === void 0 ? null : String(existing.port),
|
|
10753
|
+
after: String(parsed.port)
|
|
10754
|
+
});
|
|
10460
10755
|
}
|
|
10461
10756
|
if (parsed.mode !== void 0 && existing.mode !== parsed.mode) {
|
|
10462
10757
|
updates.mode = parsed.mode;
|
|
@@ -10464,11 +10759,19 @@ var manageOvpnClientTool = {
|
|
|
10464
10759
|
}
|
|
10465
10760
|
if (parsed.protocol !== void 0 && existing.protocol !== parsed.protocol) {
|
|
10466
10761
|
updates.protocol = parsed.protocol;
|
|
10467
|
-
diff.push({
|
|
10762
|
+
diff.push({
|
|
10763
|
+
property: "protocol",
|
|
10764
|
+
before: existing.protocol ?? null,
|
|
10765
|
+
after: parsed.protocol
|
|
10766
|
+
});
|
|
10468
10767
|
}
|
|
10469
10768
|
if (parsed.certificate !== void 0 && existing.certificate !== parsed.certificate) {
|
|
10470
10769
|
updates.certificate = parsed.certificate;
|
|
10471
|
-
diff.push({
|
|
10770
|
+
diff.push({
|
|
10771
|
+
property: "certificate",
|
|
10772
|
+
before: existing.certificate ?? null,
|
|
10773
|
+
after: parsed.certificate
|
|
10774
|
+
});
|
|
10472
10775
|
}
|
|
10473
10776
|
if (parsed.user !== void 0 && existing.user !== parsed.user) {
|
|
10474
10777
|
updates.user = parsed.user;
|
|
@@ -10540,8 +10843,33 @@ function fetchOvpnServer(allRecords, routerId2) {
|
|
|
10540
10843
|
}
|
|
10541
10844
|
});
|
|
10542
10845
|
}
|
|
10846
|
+
if (allRecords.length > 1) {
|
|
10847
|
+
const names = allRecords.map((r) => r.name ?? "?").join(", ");
|
|
10848
|
+
throw new MikroMCPError({
|
|
10849
|
+
category: "CONFLICT" /* CONFLICT */,
|
|
10850
|
+
code: "OVPN_SERVER_AMBIGUOUS",
|
|
10851
|
+
message: `Multiple OpenVPN server instances exist (${names}); this tool cannot pick one safely.`,
|
|
10852
|
+
details: { routerId: routerId2, instances: names },
|
|
10853
|
+
recoverability: {
|
|
10854
|
+
retryable: false,
|
|
10855
|
+
suggestedAction: "Remove the extra instances so exactly one remains.",
|
|
10856
|
+
alternativeTools: []
|
|
10857
|
+
}
|
|
10858
|
+
});
|
|
10859
|
+
}
|
|
10543
10860
|
return allRecords[0];
|
|
10544
10861
|
}
|
|
10862
|
+
async function writeOvpnServer(context, server, changes) {
|
|
10863
|
+
const serverId = server[".id"];
|
|
10864
|
+
if (serverId !== void 0) {
|
|
10865
|
+
await context.routerClient.update(OVPN_SERVER_PATH, serverId, changes);
|
|
10866
|
+
} else {
|
|
10867
|
+
await context.routerClient.execute(`${OVPN_SERVER_PATH}/set`, changes);
|
|
10868
|
+
}
|
|
10869
|
+
}
|
|
10870
|
+
function ovpnServerEnabled(server) {
|
|
10871
|
+
return server.enabled !== void 0 ? isTrue(server.enabled) : !isTrue(server.disabled);
|
|
10872
|
+
}
|
|
10545
10873
|
var getOvpnServerTool = {
|
|
10546
10874
|
name: "get_ovpn_server",
|
|
10547
10875
|
title: "Get OpenVPN Server",
|
|
@@ -10563,7 +10891,7 @@ var getOvpnServerTool = {
|
|
|
10563
10891
|
});
|
|
10564
10892
|
const server = fetchOvpnServer(records, context.routerId);
|
|
10565
10893
|
return {
|
|
10566
|
-
content: `OpenVPN server on ${context.routerId}: enabled=${server
|
|
10894
|
+
content: `OpenVPN server on ${context.routerId}: enabled=${ovpnServerEnabled(server)}, port=${server.port}, protocol=${server.protocol}`,
|
|
10567
10895
|
structuredContent: { routerId: context.routerId, server }
|
|
10568
10896
|
};
|
|
10569
10897
|
} catch (err) {
|
|
@@ -10578,7 +10906,15 @@ var manageOvpnServerInputSchema = z38.object({
|
|
|
10578
10906
|
mode: z38.enum(["ip", "ethernet"]).optional().describe("Tunnel mode (set action only)"),
|
|
10579
10907
|
protocol: z38.enum(["tcp-server", "udp"]).optional().describe("Transport protocol (set action only)"),
|
|
10580
10908
|
certificate: z38.string().optional().describe("Server certificate name (set action only)"),
|
|
10581
|
-
cipher: z38.enum([
|
|
10909
|
+
cipher: z38.enum([
|
|
10910
|
+
"blowfish128",
|
|
10911
|
+
"aes128-cbc",
|
|
10912
|
+
"aes192-cbc",
|
|
10913
|
+
"aes256-cbc",
|
|
10914
|
+
"aes128-gcm",
|
|
10915
|
+
"aes256-gcm",
|
|
10916
|
+
"none"
|
|
10917
|
+
]).optional().describe("Encryption cipher (set action only)"),
|
|
10582
10918
|
auth: z38.enum(["md5", "sha1", "sha256", "sha512", "null"]).optional().describe("Authentication algorithm (set action only)"),
|
|
10583
10919
|
dryRun
|
|
10584
10920
|
}).strict();
|
|
@@ -10603,32 +10939,42 @@ var manageOvpnServerTool = {
|
|
|
10603
10939
|
offset: void 0
|
|
10604
10940
|
});
|
|
10605
10941
|
const server = fetchOvpnServer(records, context.routerId);
|
|
10606
|
-
const serverId = server[".id"];
|
|
10607
10942
|
if (parsed.action === "enable" || parsed.action === "disable") {
|
|
10608
10943
|
const desiredEnabled = parsed.action === "enable";
|
|
10609
|
-
const
|
|
10944
|
+
const legacyDialect = server.enabled !== void 0;
|
|
10945
|
+
const currentEnabled = ovpnServerEnabled(server);
|
|
10610
10946
|
if (desiredEnabled === currentEnabled) {
|
|
10611
10947
|
return {
|
|
10612
10948
|
content: `OpenVPN server on ${context.routerId} is already ${parsed.action}d. No changes made.`,
|
|
10613
10949
|
structuredContent: { action: "no_change", server }
|
|
10614
10950
|
};
|
|
10615
10951
|
}
|
|
10952
|
+
const change = legacyDialect ? {
|
|
10953
|
+
property: "enabled",
|
|
10954
|
+
before: String(server.enabled),
|
|
10955
|
+
after: desiredEnabled ? "yes" : "no"
|
|
10956
|
+
} : {
|
|
10957
|
+
property: "disabled",
|
|
10958
|
+
before: server.disabled === void 0 ? null : String(server.disabled),
|
|
10959
|
+
after: desiredEnabled ? "no" : "yes"
|
|
10960
|
+
};
|
|
10616
10961
|
if (parsed.dryRun) {
|
|
10617
10962
|
return {
|
|
10618
10963
|
content: `Dry run: Would ${parsed.action} OpenVPN server on ${context.routerId}.`,
|
|
10619
10964
|
structuredContent: {
|
|
10620
10965
|
action: "dry_run",
|
|
10621
|
-
diff: [
|
|
10966
|
+
diff: [change]
|
|
10622
10967
|
}
|
|
10623
10968
|
};
|
|
10624
10969
|
}
|
|
10625
|
-
await context
|
|
10626
|
-
enabled: desiredEnabled ? "yes" : "no"
|
|
10627
|
-
});
|
|
10970
|
+
await writeOvpnServer(context, server, { [change.property]: change.after });
|
|
10628
10971
|
log38.info({ routerId: context.routerId, action: parsed.action }, "OpenVPN server updated");
|
|
10629
10972
|
return {
|
|
10630
10973
|
content: `OpenVPN server on ${context.routerId} ${parsed.action}d.`,
|
|
10631
|
-
structuredContent: {
|
|
10974
|
+
structuredContent: {
|
|
10975
|
+
action: parsed.action === "enable" ? "enabled" : "disabled",
|
|
10976
|
+
server
|
|
10977
|
+
}
|
|
10632
10978
|
};
|
|
10633
10979
|
}
|
|
10634
10980
|
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 +10993,13 @@ var manageOvpnServerTool = {
|
|
|
10647
10993
|
}
|
|
10648
10994
|
const updates = {};
|
|
10649
10995
|
const diff = [];
|
|
10650
|
-
if (parsed.port !== void 0 && server.port
|
|
10996
|
+
if (parsed.port !== void 0 && !sameValue(server.port, parsed.port)) {
|
|
10651
10997
|
updates.port = String(parsed.port);
|
|
10652
|
-
diff.push({
|
|
10998
|
+
diff.push({
|
|
10999
|
+
property: "port",
|
|
11000
|
+
before: server.port === void 0 ? null : String(server.port),
|
|
11001
|
+
after: String(parsed.port)
|
|
11002
|
+
});
|
|
10653
11003
|
}
|
|
10654
11004
|
if (parsed.mode !== void 0 && server.mode !== parsed.mode) {
|
|
10655
11005
|
updates.mode = parsed.mode;
|
|
@@ -10657,11 +11007,19 @@ var manageOvpnServerTool = {
|
|
|
10657
11007
|
}
|
|
10658
11008
|
if (parsed.protocol !== void 0 && server.protocol !== parsed.protocol) {
|
|
10659
11009
|
updates.protocol = parsed.protocol;
|
|
10660
|
-
diff.push({
|
|
11010
|
+
diff.push({
|
|
11011
|
+
property: "protocol",
|
|
11012
|
+
before: server.protocol ?? null,
|
|
11013
|
+
after: parsed.protocol
|
|
11014
|
+
});
|
|
10661
11015
|
}
|
|
10662
11016
|
if (parsed.certificate !== void 0 && server.certificate !== parsed.certificate) {
|
|
10663
11017
|
updates.certificate = parsed.certificate;
|
|
10664
|
-
diff.push({
|
|
11018
|
+
diff.push({
|
|
11019
|
+
property: "certificate",
|
|
11020
|
+
before: server.certificate ?? null,
|
|
11021
|
+
after: parsed.certificate
|
|
11022
|
+
});
|
|
10665
11023
|
}
|
|
10666
11024
|
if (parsed.cipher !== void 0 && server.cipher !== parsed.cipher) {
|
|
10667
11025
|
updates.cipher = parsed.cipher;
|
|
@@ -10683,7 +11041,7 @@ var manageOvpnServerTool = {
|
|
|
10683
11041
|
structuredContent: { action: "dry_run", diff }
|
|
10684
11042
|
};
|
|
10685
11043
|
}
|
|
10686
|
-
await context
|
|
11044
|
+
await writeOvpnServer(context, server, updates);
|
|
10687
11045
|
log38.info({ routerId: context.routerId }, "OpenVPN server configuration updated");
|
|
10688
11046
|
return {
|
|
10689
11047
|
content: `Updated OpenVPN server configuration on ${context.routerId}.`,
|
|
@@ -10714,12 +11072,20 @@ var listPppProfilesTool = {
|
|
|
10714
11072
|
title: "List PPP Profiles",
|
|
10715
11073
|
description: "List PPP profiles including the built-in default and default-encryption profiles.",
|
|
10716
11074
|
inputSchema: listPppProfilesInputSchema,
|
|
10717
|
-
annotations: {
|
|
11075
|
+
annotations: {
|
|
11076
|
+
readOnlyHint: true,
|
|
11077
|
+
destructiveHint: false,
|
|
11078
|
+
idempotentHint: true,
|
|
11079
|
+
openWorldHint: false
|
|
11080
|
+
},
|
|
10718
11081
|
async handler(params, context) {
|
|
10719
11082
|
const parsed = listPppProfilesInputSchema.parse(params);
|
|
10720
11083
|
try {
|
|
10721
11084
|
log39.info({ routerId: context.routerId }, "Listing PPP profiles");
|
|
10722
|
-
const all = await context.routerClient.get("ppp/profile", {
|
|
11085
|
+
const all = await context.routerClient.get("ppp/profile", {
|
|
11086
|
+
limit: void 0,
|
|
11087
|
+
offset: void 0
|
|
11088
|
+
});
|
|
10723
11089
|
const filtered = parsed.name ? all.filter((p) => p.name === parsed.name) : all;
|
|
10724
11090
|
const profiles = filtered.slice(0, parsed.limit);
|
|
10725
11091
|
return {
|
|
@@ -10731,7 +11097,12 @@ var listPppProfilesTool = {
|
|
|
10731
11097
|
0,
|
|
10732
11098
|
(p) => compactFields(p, ["name", "local-address", "remote-address", "dns-server", "rate-limit"])
|
|
10733
11099
|
),
|
|
10734
|
-
structuredContent: {
|
|
11100
|
+
structuredContent: {
|
|
11101
|
+
routerId: context.routerId,
|
|
11102
|
+
profiles,
|
|
11103
|
+
total: all.length,
|
|
11104
|
+
returned: profiles.length
|
|
11105
|
+
}
|
|
10735
11106
|
};
|
|
10736
11107
|
} catch (err) {
|
|
10737
11108
|
throw toolError(err, context, "list_ppp_profiles");
|
|
@@ -10755,13 +11126,24 @@ var managePppProfileTool = {
|
|
|
10755
11126
|
title: "Manage PPP Profile",
|
|
10756
11127
|
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
11128
|
inputSchema: managePppProfileInputSchema,
|
|
10758
|
-
annotations: {
|
|
11129
|
+
annotations: {
|
|
11130
|
+
readOnlyHint: false,
|
|
11131
|
+
destructiveHint: false,
|
|
11132
|
+
idempotentHint: true,
|
|
11133
|
+
openWorldHint: false
|
|
11134
|
+
},
|
|
10759
11135
|
snapshotPaths: ["ppp/profile"],
|
|
10760
11136
|
async handler(params, context) {
|
|
10761
11137
|
const parsed = managePppProfileInputSchema.parse(params);
|
|
10762
|
-
log39.info(
|
|
11138
|
+
log39.info(
|
|
11139
|
+
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
11140
|
+
"Managing PPP profile"
|
|
11141
|
+
);
|
|
10763
11142
|
try {
|
|
10764
|
-
const all = await context.routerClient.get("ppp/profile", {
|
|
11143
|
+
const all = await context.routerClient.get("ppp/profile", {
|
|
11144
|
+
limit: void 0,
|
|
11145
|
+
offset: void 0
|
|
11146
|
+
});
|
|
10765
11147
|
const existing = all.find((p) => p.name === parsed.name);
|
|
10766
11148
|
if (parsed.action === "add") {
|
|
10767
11149
|
if (existing) {
|
|
@@ -10774,12 +11156,18 @@ var managePppProfileTool = {
|
|
|
10774
11156
|
const diff = [
|
|
10775
11157
|
{ property: "name", before: null, after: parsed.name }
|
|
10776
11158
|
];
|
|
10777
|
-
if (parsed.localAddress)
|
|
10778
|
-
|
|
10779
|
-
if (parsed.
|
|
10780
|
-
|
|
10781
|
-
if (parsed.
|
|
10782
|
-
|
|
11159
|
+
if (parsed.localAddress)
|
|
11160
|
+
diff.push({ property: "local-address", before: null, after: parsed.localAddress });
|
|
11161
|
+
if (parsed.remoteAddress)
|
|
11162
|
+
diff.push({ property: "remote-address", before: null, after: parsed.remoteAddress });
|
|
11163
|
+
if (parsed.dnsServer)
|
|
11164
|
+
diff.push({ property: "dns-server", before: null, after: parsed.dnsServer });
|
|
11165
|
+
if (parsed.rateLimit)
|
|
11166
|
+
diff.push({ property: "rate-limit", before: null, after: parsed.rateLimit });
|
|
11167
|
+
if (parsed.sessionTimeout)
|
|
11168
|
+
diff.push({ property: "session-timeout", before: null, after: parsed.sessionTimeout });
|
|
11169
|
+
if (parsed.comment)
|
|
11170
|
+
diff.push({ property: "comment", before: null, after: parsed.comment });
|
|
10783
11171
|
return {
|
|
10784
11172
|
content: `Dry run: Would create PPP profile "${parsed.name}".`,
|
|
10785
11173
|
structuredContent: { action: "dry_run", diff }
|
|
@@ -10806,34 +11194,62 @@ var managePppProfileTool = {
|
|
|
10806
11194
|
code: "PPP_PROFILE_NOT_FOUND",
|
|
10807
11195
|
message: `PPP profile "${parsed.name}" not found.`,
|
|
10808
11196
|
details: { name: parsed.name },
|
|
10809
|
-
recoverability: {
|
|
11197
|
+
recoverability: {
|
|
11198
|
+
retryable: false,
|
|
11199
|
+
suggestedAction: "Verify with list_ppp_profiles.",
|
|
11200
|
+
alternativeTools: ["list_ppp_profiles"]
|
|
11201
|
+
}
|
|
10810
11202
|
});
|
|
10811
11203
|
}
|
|
10812
11204
|
const changes = {};
|
|
10813
11205
|
const diff = [];
|
|
10814
11206
|
if (parsed.localAddress !== void 0 && existing["local-address"] !== parsed.localAddress) {
|
|
10815
11207
|
changes["local-address"] = parsed.localAddress;
|
|
10816
|
-
diff.push({
|
|
11208
|
+
diff.push({
|
|
11209
|
+
property: "local-address",
|
|
11210
|
+
before: existing["local-address"] ?? null,
|
|
11211
|
+
after: parsed.localAddress
|
|
11212
|
+
});
|
|
10817
11213
|
}
|
|
10818
11214
|
if (parsed.remoteAddress !== void 0 && existing["remote-address"] !== parsed.remoteAddress) {
|
|
10819
11215
|
changes["remote-address"] = parsed.remoteAddress;
|
|
10820
|
-
diff.push({
|
|
11216
|
+
diff.push({
|
|
11217
|
+
property: "remote-address",
|
|
11218
|
+
before: existing["remote-address"] ?? null,
|
|
11219
|
+
after: parsed.remoteAddress
|
|
11220
|
+
});
|
|
10821
11221
|
}
|
|
10822
11222
|
if (parsed.dnsServer !== void 0 && existing["dns-server"] !== parsed.dnsServer) {
|
|
10823
11223
|
changes["dns-server"] = parsed.dnsServer;
|
|
10824
|
-
diff.push({
|
|
11224
|
+
diff.push({
|
|
11225
|
+
property: "dns-server",
|
|
11226
|
+
before: existing["dns-server"] ?? null,
|
|
11227
|
+
after: parsed.dnsServer
|
|
11228
|
+
});
|
|
10825
11229
|
}
|
|
10826
11230
|
if (parsed.rateLimit !== void 0 && existing["rate-limit"] !== parsed.rateLimit) {
|
|
10827
11231
|
changes["rate-limit"] = parsed.rateLimit;
|
|
10828
|
-
diff.push({
|
|
11232
|
+
diff.push({
|
|
11233
|
+
property: "rate-limit",
|
|
11234
|
+
before: existing["rate-limit"] ?? null,
|
|
11235
|
+
after: parsed.rateLimit
|
|
11236
|
+
});
|
|
10829
11237
|
}
|
|
10830
11238
|
if (parsed.sessionTimeout !== void 0 && existing["session-timeout"] !== parsed.sessionTimeout) {
|
|
10831
11239
|
changes["session-timeout"] = parsed.sessionTimeout;
|
|
10832
|
-
diff.push({
|
|
11240
|
+
diff.push({
|
|
11241
|
+
property: "session-timeout",
|
|
11242
|
+
before: existing["session-timeout"] ?? null,
|
|
11243
|
+
after: parsed.sessionTimeout
|
|
11244
|
+
});
|
|
10833
11245
|
}
|
|
10834
11246
|
if (parsed.comment !== void 0 && existing.comment !== parsed.comment) {
|
|
10835
11247
|
changes.comment = parsed.comment;
|
|
10836
|
-
diff.push({
|
|
11248
|
+
diff.push({
|
|
11249
|
+
property: "comment",
|
|
11250
|
+
before: existing.comment ?? null,
|
|
11251
|
+
after: parsed.comment
|
|
11252
|
+
});
|
|
10837
11253
|
}
|
|
10838
11254
|
if (Object.keys(changes).length === 0) {
|
|
10839
11255
|
return {
|
|
@@ -10863,7 +11279,10 @@ var managePppProfileTool = {
|
|
|
10863
11279
|
if (parsed.dryRun) {
|
|
10864
11280
|
return {
|
|
10865
11281
|
content: `Dry run: Would remove PPP profile "${parsed.name}".`,
|
|
10866
|
-
structuredContent: {
|
|
11282
|
+
structuredContent: {
|
|
11283
|
+
action: "dry_run",
|
|
11284
|
+
diff: [{ property: "name", before: parsed.name, after: null }]
|
|
11285
|
+
}
|
|
10867
11286
|
};
|
|
10868
11287
|
}
|
|
10869
11288
|
await context.routerClient.remove("ppp/profile", existing[".id"]);
|
|
@@ -11032,7 +11451,9 @@ var createBackupTool = {
|
|
|
11032
11451
|
var exportConfigInputSchema = z41.object({
|
|
11033
11452
|
routerId,
|
|
11034
11453
|
compact: z41.boolean().default(false).describe("Export only non-default settings (compact format)"),
|
|
11035
|
-
file: z41.string().optional().describe(
|
|
11454
|
+
file: z41.string().optional().describe(
|
|
11455
|
+
"Save the export to a file on the router (without extension); omit to return inline"
|
|
11456
|
+
)
|
|
11036
11457
|
}).strict();
|
|
11037
11458
|
var exportConfigTool = {
|
|
11038
11459
|
name: "export_config",
|
|
@@ -11204,7 +11625,10 @@ var manageLogRuleTool = {
|
|
|
11204
11625
|
};
|
|
11205
11626
|
}
|
|
11206
11627
|
const created = await context.routerClient.create(LOG_RULES_PATH, body);
|
|
11207
|
-
log42.info(
|
|
11628
|
+
log42.info(
|
|
11629
|
+
{ topics: parsed.topics, logAction: parsed.logAction, id: created[".id"] },
|
|
11630
|
+
"Log rule added"
|
|
11631
|
+
);
|
|
11208
11632
|
return {
|
|
11209
11633
|
content: `Added log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
|
|
11210
11634
|
structuredContent: {
|
|
@@ -11260,7 +11684,13 @@ var manageLogRuleTool = {
|
|
|
11260
11684
|
structuredContent: {
|
|
11261
11685
|
action: "dry_run",
|
|
11262
11686
|
id,
|
|
11263
|
-
diff: [
|
|
11687
|
+
diff: [
|
|
11688
|
+
{
|
|
11689
|
+
property: "disabled",
|
|
11690
|
+
before: existing.disabled ?? "false",
|
|
11691
|
+
after: String(wantDisabled)
|
|
11692
|
+
}
|
|
11693
|
+
]
|
|
11264
11694
|
}
|
|
11265
11695
|
};
|
|
11266
11696
|
}
|
|
@@ -11492,17 +11922,25 @@ var bandwidthTestTool = {
|
|
|
11492
11922
|
// Reaches an external host and saturates the link; a silent retry would
|
|
11493
11923
|
// double the load, so opt out of auto-retry.
|
|
11494
11924
|
retryable: false,
|
|
11495
|
-
annotations: {
|
|
11925
|
+
annotations: {
|
|
11926
|
+
readOnlyHint: true,
|
|
11927
|
+
destructiveHint: false,
|
|
11928
|
+
idempotentHint: false,
|
|
11929
|
+
openWorldHint: true
|
|
11930
|
+
},
|
|
11496
11931
|
async handler(params, context) {
|
|
11497
11932
|
const parsed = bandwidthTestInputSchema.parse(params);
|
|
11498
11933
|
log43.info({ routerId: context.routerId, address: parsed.address }, "Running bandwidth test");
|
|
11499
11934
|
try {
|
|
11500
|
-
const result = await context.routerClient.execute(
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11935
|
+
const result = await context.routerClient.execute(
|
|
11936
|
+
"tool/bandwidth-test",
|
|
11937
|
+
{
|
|
11938
|
+
address: parsed.address,
|
|
11939
|
+
protocol: parsed.protocol,
|
|
11940
|
+
direction: parsed.direction,
|
|
11941
|
+
duration: String(parsed.duration)
|
|
11942
|
+
}
|
|
11943
|
+
);
|
|
11506
11944
|
const txBps = Number(result["tx-current"] ?? 0);
|
|
11507
11945
|
const rxBps = Number(result["rx-current"] ?? 0);
|
|
11508
11946
|
const txMbps = Math.round(txBps / 1e6 * 100) / 100;
|
|
@@ -11538,12 +11976,23 @@ var fetchUrlTool = {
|
|
|
11538
11976
|
inputSchema: fetchUrlInputSchema,
|
|
11539
11977
|
// Not read-only: a POST has external side effects and outputFile writes a
|
|
11540
11978
|
// file on the router. openWorld: reaches arbitrary external hosts.
|
|
11541
|
-
annotations: {
|
|
11979
|
+
annotations: {
|
|
11980
|
+
readOnlyHint: false,
|
|
11981
|
+
destructiveHint: false,
|
|
11982
|
+
idempotentHint: false,
|
|
11983
|
+
openWorldHint: true
|
|
11984
|
+
},
|
|
11542
11985
|
async handler(params, context) {
|
|
11543
11986
|
const parsed = fetchUrlInputSchema.parse(params);
|
|
11544
|
-
log43.info(
|
|
11987
|
+
log43.info(
|
|
11988
|
+
{ routerId: context.routerId, url: parsed.url, method: parsed.method },
|
|
11989
|
+
"Fetching URL"
|
|
11990
|
+
);
|
|
11545
11991
|
try {
|
|
11546
|
-
const body = {
|
|
11992
|
+
const body = {
|
|
11993
|
+
url: parsed.url,
|
|
11994
|
+
"http-method": parsed.method.toLowerCase()
|
|
11995
|
+
};
|
|
11547
11996
|
if (parsed.httpData) body["http-data"] = parsed.httpData;
|
|
11548
11997
|
if (parsed.outputFile) {
|
|
11549
11998
|
body.output = "file";
|
|
@@ -11551,13 +12000,21 @@ var fetchUrlTool = {
|
|
|
11551
12000
|
} else {
|
|
11552
12001
|
body.output = "user";
|
|
11553
12002
|
}
|
|
11554
|
-
const sections = await context.routerClient.execute(
|
|
12003
|
+
const sections = await context.routerClient.execute(
|
|
12004
|
+
"tool/fetch",
|
|
12005
|
+
body
|
|
12006
|
+
);
|
|
11555
12007
|
const finished = sections.find((s) => s.status === "finished") ?? sections[sections.length - 1];
|
|
11556
12008
|
const statusCode = finished?.code ?? null;
|
|
11557
12009
|
if (parsed.outputFile) {
|
|
11558
12010
|
return {
|
|
11559
12011
|
content: `Fetched ${parsed.url} \u2192 saved to ${parsed.outputFile} (status ${statusCode})`,
|
|
11560
|
-
structuredContent: {
|
|
12012
|
+
structuredContent: {
|
|
12013
|
+
routerId: context.routerId,
|
|
12014
|
+
url: parsed.url,
|
|
12015
|
+
statusCode,
|
|
12016
|
+
outputFile: parsed.outputFile
|
|
12017
|
+
}
|
|
11561
12018
|
};
|
|
11562
12019
|
}
|
|
11563
12020
|
let responseBody = finished?.data ?? "";
|
|
@@ -11566,7 +12023,12 @@ var fetchUrlTool = {
|
|
|
11566
12023
|
}
|
|
11567
12024
|
return {
|
|
11568
12025
|
content: `Fetched ${parsed.url}: status=${statusCode} body_length=${responseBody.length}`,
|
|
11569
|
-
structuredContent: {
|
|
12026
|
+
structuredContent: {
|
|
12027
|
+
routerId: context.routerId,
|
|
12028
|
+
url: parsed.url,
|
|
12029
|
+
statusCode,
|
|
12030
|
+
body: responseBody
|
|
12031
|
+
}
|
|
11570
12032
|
};
|
|
11571
12033
|
} catch (err) {
|
|
11572
12034
|
throw toolError(err, context, "fetch_url");
|
|
@@ -11585,7 +12047,12 @@ var listConnectionsTool = {
|
|
|
11585
12047
|
title: "List Connections",
|
|
11586
12048
|
description: "List active connection tracking entries from the router firewall table. Filters are applied client-side. Useful for diagnosing NAT and firewall behavior.",
|
|
11587
12049
|
inputSchema: listConnectionsInputSchema,
|
|
11588
|
-
annotations: {
|
|
12050
|
+
annotations: {
|
|
12051
|
+
readOnlyHint: true,
|
|
12052
|
+
destructiveHint: false,
|
|
12053
|
+
idempotentHint: true,
|
|
12054
|
+
openWorldHint: false
|
|
12055
|
+
},
|
|
11589
12056
|
async handler(params, context) {
|
|
11590
12057
|
const parsed = listConnectionsInputSchema.parse(params);
|
|
11591
12058
|
log43.info({ routerId: context.routerId }, "Listing connections");
|
|
@@ -11602,7 +12069,11 @@ var listConnectionsTool = {
|
|
|
11602
12069
|
limit: void 0,
|
|
11603
12070
|
offset: void 0
|
|
11604
12071
|
});
|
|
11605
|
-
const filtered = all.filter(
|
|
12072
|
+
const filtered = all.filter(
|
|
12073
|
+
(c) => parsed.srcAddress ? (c["src-address"] ?? "").includes(parsed.srcAddress) : true
|
|
12074
|
+
).filter(
|
|
12075
|
+
(c) => parsed.dstAddress ? (c["dst-address"] ?? "").includes(parsed.dstAddress) : true
|
|
12076
|
+
).filter((c) => parsed.protocol ? c.protocol === parsed.protocol : true);
|
|
11606
12077
|
const connections = filtered.slice(0, parsed.limit);
|
|
11607
12078
|
return {
|
|
11608
12079
|
content: listContent(
|
|
@@ -11619,14 +12090,23 @@ var listConnectionsTool = {
|
|
|
11619
12090
|
"connection-mark"
|
|
11620
12091
|
])
|
|
11621
12092
|
),
|
|
11622
|
-
structuredContent: {
|
|
12093
|
+
structuredContent: {
|
|
12094
|
+
routerId: context.routerId,
|
|
12095
|
+
connections,
|
|
12096
|
+
total: all.length,
|
|
12097
|
+
returned: connections.length
|
|
12098
|
+
}
|
|
11623
12099
|
};
|
|
11624
12100
|
} catch (err) {
|
|
11625
12101
|
throw toolError(err, context, "list_connections");
|
|
11626
12102
|
}
|
|
11627
12103
|
}
|
|
11628
12104
|
};
|
|
11629
|
-
var networkTestTools = [
|
|
12105
|
+
var networkTestTools = [
|
|
12106
|
+
bandwidthTestTool,
|
|
12107
|
+
fetchUrlTool,
|
|
12108
|
+
listConnectionsTool
|
|
12109
|
+
];
|
|
11630
12110
|
|
|
11631
12111
|
// src/domain/tools/container-config-tools.ts
|
|
11632
12112
|
import { z as z44 } from "zod";
|
|
@@ -11637,7 +12117,12 @@ var getContainerConfigTool = {
|
|
|
11637
12117
|
title: "Get Container Config",
|
|
11638
12118
|
description: "Read global container configuration: registry URL, RAM high-water mark, and veth interface.",
|
|
11639
12119
|
inputSchema: getContainerConfigInputSchema,
|
|
11640
|
-
annotations: {
|
|
12120
|
+
annotations: {
|
|
12121
|
+
readOnlyHint: true,
|
|
12122
|
+
destructiveHint: false,
|
|
12123
|
+
idempotentHint: true,
|
|
12124
|
+
openWorldHint: false
|
|
12125
|
+
},
|
|
11641
12126
|
async handler(params, context) {
|
|
11642
12127
|
getContainerConfigInputSchema.parse(params);
|
|
11643
12128
|
log44.info({ routerId: context.routerId }, "Getting container config");
|
|
@@ -11670,7 +12155,12 @@ var manageContainerConfigTool = {
|
|
|
11670
12155
|
title: "Manage Container Config",
|
|
11671
12156
|
description: "Update global container settings. Idempotent: returns no_change if nothing differs.",
|
|
11672
12157
|
inputSchema: manageContainerConfigInputSchema,
|
|
11673
|
-
annotations: {
|
|
12158
|
+
annotations: {
|
|
12159
|
+
readOnlyHint: false,
|
|
12160
|
+
destructiveHint: false,
|
|
12161
|
+
idempotentHint: true,
|
|
12162
|
+
openWorldHint: false
|
|
12163
|
+
},
|
|
11674
12164
|
snapshotPaths: ["container/config"],
|
|
11675
12165
|
async handler(params, context) {
|
|
11676
12166
|
const parsed = manageContainerConfigInputSchema.parse(params);
|
|
@@ -11678,23 +12168,34 @@ var manageContainerConfigTool = {
|
|
|
11678
12168
|
try {
|
|
11679
12169
|
const results2 = await context.routerClient.get("container/config");
|
|
11680
12170
|
const current = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
|
|
11681
|
-
const id = current[".id"];
|
|
11682
12171
|
const changes = {};
|
|
11683
12172
|
const diff = [];
|
|
11684
12173
|
if (parsed.registryUrl !== void 0 && current["registry-url"] !== parsed.registryUrl) {
|
|
11685
12174
|
changes["registry-url"] = parsed.registryUrl;
|
|
11686
|
-
diff.push({
|
|
12175
|
+
diff.push({
|
|
12176
|
+
property: "registry-url",
|
|
12177
|
+
before: current["registry-url"] ?? null,
|
|
12178
|
+
after: parsed.registryUrl
|
|
12179
|
+
});
|
|
11687
12180
|
}
|
|
11688
12181
|
if (parsed.ramHighMb !== void 0) {
|
|
11689
12182
|
const next = String(parsed.ramHighMb);
|
|
11690
|
-
if (current["ram-high"]
|
|
12183
|
+
if (!sameValue(current["ram-high"], next)) {
|
|
11691
12184
|
changes["ram-high"] = next;
|
|
11692
|
-
diff.push({
|
|
12185
|
+
diff.push({
|
|
12186
|
+
property: "ram-high",
|
|
12187
|
+
before: current["ram-high"] === void 0 ? null : String(current["ram-high"]),
|
|
12188
|
+
after: next
|
|
12189
|
+
});
|
|
11693
12190
|
}
|
|
11694
12191
|
}
|
|
11695
12192
|
if (parsed.vethInterface !== void 0 && current["veth-interface"] !== parsed.vethInterface) {
|
|
11696
12193
|
changes["veth-interface"] = parsed.vethInterface;
|
|
11697
|
-
diff.push({
|
|
12194
|
+
diff.push({
|
|
12195
|
+
property: "veth-interface",
|
|
12196
|
+
before: current["veth-interface"] ?? null,
|
|
12197
|
+
after: parsed.vethInterface
|
|
12198
|
+
});
|
|
11698
12199
|
}
|
|
11699
12200
|
if (Object.keys(changes).length === 0) {
|
|
11700
12201
|
return {
|
|
@@ -11708,7 +12209,7 @@ var manageContainerConfigTool = {
|
|
|
11708
12209
|
structuredContent: { action: "dry_run", diff }
|
|
11709
12210
|
};
|
|
11710
12211
|
}
|
|
11711
|
-
await context.routerClient.
|
|
12212
|
+
await context.routerClient.execute("container/config/set", changes);
|
|
11712
12213
|
log44.info({ routerId: context.routerId }, "Container config updated");
|
|
11713
12214
|
return {
|
|
11714
12215
|
content: `Updated container config on ${context.routerId}.`,
|
|
@@ -11729,7 +12230,12 @@ var listContainerEnvsTool = {
|
|
|
11729
12230
|
title: "List Container Envs",
|
|
11730
12231
|
description: "List container environment variable entries, optionally filtered by container name.",
|
|
11731
12232
|
inputSchema: listContainerEnvsInputSchema,
|
|
11732
|
-
annotations: {
|
|
12233
|
+
annotations: {
|
|
12234
|
+
readOnlyHint: true,
|
|
12235
|
+
destructiveHint: false,
|
|
12236
|
+
idempotentHint: true,
|
|
12237
|
+
openWorldHint: false
|
|
12238
|
+
},
|
|
11733
12239
|
async handler(params, context) {
|
|
11734
12240
|
const parsed = listContainerEnvsInputSchema.parse(params);
|
|
11735
12241
|
log44.info({ routerId: context.routerId }, "Listing container envs");
|
|
@@ -11749,7 +12255,12 @@ var listContainerEnvsTool = {
|
|
|
11749
12255
|
0,
|
|
11750
12256
|
(e) => compactFields(e, ["name", "key", "value"])
|
|
11751
12257
|
),
|
|
11752
|
-
structuredContent: {
|
|
12258
|
+
structuredContent: {
|
|
12259
|
+
routerId: context.routerId,
|
|
12260
|
+
envs,
|
|
12261
|
+
total: all.length,
|
|
12262
|
+
returned: envs.length
|
|
12263
|
+
}
|
|
11753
12264
|
};
|
|
11754
12265
|
} catch (err) {
|
|
11755
12266
|
throw toolError(err, context, "list_container_envs");
|
|
@@ -11769,7 +12280,12 @@ var manageContainerEnvTool = {
|
|
|
11769
12280
|
title: "Manage Container Env",
|
|
11770
12281
|
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
12282
|
inputSchema: manageContainerEnvInputSchema,
|
|
11772
|
-
annotations: {
|
|
12283
|
+
annotations: {
|
|
12284
|
+
readOnlyHint: false,
|
|
12285
|
+
destructiveHint: false,
|
|
12286
|
+
idempotentHint: true,
|
|
12287
|
+
openWorldHint: false
|
|
12288
|
+
},
|
|
11773
12289
|
async handler(params, context) {
|
|
11774
12290
|
const parsed = manageContainerEnvInputSchema.parse(params);
|
|
11775
12291
|
log44.info(
|
|
@@ -11873,7 +12389,12 @@ var listContainerMountsTool = {
|
|
|
11873
12389
|
title: "List Container Mounts",
|
|
11874
12390
|
description: "List container volume mount definitions with source path, destination path, and mount name.",
|
|
11875
12391
|
inputSchema: listContainerMountsInputSchema,
|
|
11876
|
-
annotations: {
|
|
12392
|
+
annotations: {
|
|
12393
|
+
readOnlyHint: true,
|
|
12394
|
+
destructiveHint: false,
|
|
12395
|
+
idempotentHint: true,
|
|
12396
|
+
openWorldHint: false
|
|
12397
|
+
},
|
|
11877
12398
|
async handler(params, context) {
|
|
11878
12399
|
const parsed = listContainerMountsInputSchema.parse(params);
|
|
11879
12400
|
log44.info({ routerId: context.routerId }, "Listing container mounts");
|
|
@@ -11893,7 +12414,12 @@ var listContainerMountsTool = {
|
|
|
11893
12414
|
0,
|
|
11894
12415
|
(m) => compactFields(m, ["name", "src", "dst"])
|
|
11895
12416
|
),
|
|
11896
|
-
structuredContent: {
|
|
12417
|
+
structuredContent: {
|
|
12418
|
+
routerId: context.routerId,
|
|
12419
|
+
mounts,
|
|
12420
|
+
total: all.length,
|
|
12421
|
+
returned: mounts.length
|
|
12422
|
+
}
|
|
11897
12423
|
};
|
|
11898
12424
|
} catch (err) {
|
|
11899
12425
|
throw toolError(err, context, "list_container_mounts");
|
|
@@ -11913,10 +12439,18 @@ var manageContainerMountTool = {
|
|
|
11913
12439
|
title: "Manage Container Mount",
|
|
11914
12440
|
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
12441
|
inputSchema: manageContainerMountInputSchema,
|
|
11916
|
-
annotations: {
|
|
12442
|
+
annotations: {
|
|
12443
|
+
readOnlyHint: false,
|
|
12444
|
+
destructiveHint: false,
|
|
12445
|
+
idempotentHint: true,
|
|
12446
|
+
openWorldHint: false
|
|
12447
|
+
},
|
|
11917
12448
|
async handler(params, context) {
|
|
11918
12449
|
const parsed = manageContainerMountInputSchema.parse(params);
|
|
11919
|
-
log44.info(
|
|
12450
|
+
log44.info(
|
|
12451
|
+
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
12452
|
+
"Managing container mount"
|
|
12453
|
+
);
|
|
11920
12454
|
try {
|
|
11921
12455
|
const all = await context.routerClient.get("container/mounts", {
|
|
11922
12456
|
limit: void 0,
|
|
@@ -11951,7 +12485,10 @@ var manageContainerMountTool = {
|
|
|
11951
12485
|
category: "VALIDATION" /* VALIDATION */,
|
|
11952
12486
|
code: "MOUNT_PATHS_REQUIRED",
|
|
11953
12487
|
message: "src and dst are required for add.",
|
|
11954
|
-
recoverability: {
|
|
12488
|
+
recoverability: {
|
|
12489
|
+
retryable: false,
|
|
12490
|
+
suggestedAction: "Provide src and dst parameters."
|
|
12491
|
+
}
|
|
11955
12492
|
});
|
|
11956
12493
|
}
|
|
11957
12494
|
if (parsed.dryRun) {
|
|
@@ -12021,7 +12558,12 @@ var listInterfaceListsTool = {
|
|
|
12021
12558
|
title: "List Interface Lists",
|
|
12022
12559
|
description: "List all interface lists defined on the router.",
|
|
12023
12560
|
inputSchema: listInterfaceListsInputSchema,
|
|
12024
|
-
annotations: {
|
|
12561
|
+
annotations: {
|
|
12562
|
+
readOnlyHint: true,
|
|
12563
|
+
destructiveHint: false,
|
|
12564
|
+
idempotentHint: true,
|
|
12565
|
+
openWorldHint: false
|
|
12566
|
+
},
|
|
12025
12567
|
async handler(params, context) {
|
|
12026
12568
|
const parsed = listInterfaceListsInputSchema.parse(params);
|
|
12027
12569
|
log45.info({ routerId: context.routerId }, "Listing interface lists");
|
|
@@ -12040,7 +12582,12 @@ var listInterfaceListsTool = {
|
|
|
12040
12582
|
0,
|
|
12041
12583
|
(l) => compactFields(l, ["name", "include", "exclude", "comment"])
|
|
12042
12584
|
),
|
|
12043
|
-
structuredContent: {
|
|
12585
|
+
structuredContent: {
|
|
12586
|
+
routerId: context.routerId,
|
|
12587
|
+
lists,
|
|
12588
|
+
total: all.length,
|
|
12589
|
+
returned: lists.length
|
|
12590
|
+
}
|
|
12044
12591
|
};
|
|
12045
12592
|
} catch (err) {
|
|
12046
12593
|
throw toolError(err, context, "list_interface_lists");
|
|
@@ -12059,10 +12606,18 @@ var manageInterfaceListTool = {
|
|
|
12059
12606
|
title: "Manage Interface List",
|
|
12060
12607
|
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
12608
|
inputSchema: manageInterfaceListInputSchema,
|
|
12062
|
-
annotations: {
|
|
12609
|
+
annotations: {
|
|
12610
|
+
readOnlyHint: false,
|
|
12611
|
+
destructiveHint: true,
|
|
12612
|
+
idempotentHint: true,
|
|
12613
|
+
openWorldHint: false
|
|
12614
|
+
},
|
|
12063
12615
|
async handler(params, context) {
|
|
12064
12616
|
const parsed = manageInterfaceListInputSchema.parse(params);
|
|
12065
|
-
log45.info(
|
|
12617
|
+
log45.info(
|
|
12618
|
+
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
12619
|
+
"Managing interface list"
|
|
12620
|
+
);
|
|
12066
12621
|
try {
|
|
12067
12622
|
const all = await context.routerClient.get("interface/list", {
|
|
12068
12623
|
limit: void 0,
|
|
@@ -12133,11 +12688,21 @@ var manageInterfaceListMemberTool = {
|
|
|
12133
12688
|
title: "Manage Interface List Member",
|
|
12134
12689
|
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
12690
|
inputSchema: manageInterfaceListMemberInputSchema,
|
|
12136
|
-
annotations: {
|
|
12691
|
+
annotations: {
|
|
12692
|
+
readOnlyHint: false,
|
|
12693
|
+
destructiveHint: true,
|
|
12694
|
+
idempotentHint: true,
|
|
12695
|
+
openWorldHint: false
|
|
12696
|
+
},
|
|
12137
12697
|
async handler(params, context) {
|
|
12138
12698
|
const parsed = manageInterfaceListMemberInputSchema.parse(params);
|
|
12139
12699
|
log45.info(
|
|
12140
|
-
{
|
|
12700
|
+
{
|
|
12701
|
+
routerId: context.routerId,
|
|
12702
|
+
action: parsed.action,
|
|
12703
|
+
list: parsed.list,
|
|
12704
|
+
interface: parsed.interface
|
|
12705
|
+
},
|
|
12141
12706
|
"Managing interface list member"
|
|
12142
12707
|
);
|
|
12143
12708
|
try {
|
|
@@ -12152,7 +12717,11 @@ var manageInterfaceListMemberTool = {
|
|
|
12152
12717
|
if (existing) {
|
|
12153
12718
|
return {
|
|
12154
12719
|
content: `Interface "${parsed.interface}" is already a member of list "${parsed.list}". No changes made.`,
|
|
12155
|
-
structuredContent: {
|
|
12720
|
+
structuredContent: {
|
|
12721
|
+
action: "already_exists",
|
|
12722
|
+
list: parsed.list,
|
|
12723
|
+
interface: parsed.interface
|
|
12724
|
+
}
|
|
12156
12725
|
};
|
|
12157
12726
|
}
|
|
12158
12727
|
if (parsed.dryRun) {
|
|
@@ -12167,16 +12736,28 @@ var manageInterfaceListMemberTool = {
|
|
|
12167
12736
|
const body = { list: parsed.list, interface: parsed.interface };
|
|
12168
12737
|
if (parsed.comment) body.comment = parsed.comment;
|
|
12169
12738
|
const created = await context.routerClient.create("interface/list/member", body);
|
|
12170
|
-
log45.info(
|
|
12739
|
+
log45.info(
|
|
12740
|
+
{ list: parsed.list, interface: parsed.interface, id: created[".id"] },
|
|
12741
|
+
"Interface list member added"
|
|
12742
|
+
);
|
|
12171
12743
|
return {
|
|
12172
12744
|
content: `Added "${parsed.interface}" to list "${parsed.list}".`,
|
|
12173
|
-
structuredContent: {
|
|
12745
|
+
structuredContent: {
|
|
12746
|
+
action: "created",
|
|
12747
|
+
list: parsed.list,
|
|
12748
|
+
interface: parsed.interface,
|
|
12749
|
+
id: created[".id"]
|
|
12750
|
+
}
|
|
12174
12751
|
};
|
|
12175
12752
|
}
|
|
12176
12753
|
if (!existing) {
|
|
12177
12754
|
return {
|
|
12178
12755
|
content: `Interface "${parsed.interface}" is not a member of list "${parsed.list}". Nothing to remove.`,
|
|
12179
|
-
structuredContent: {
|
|
12756
|
+
structuredContent: {
|
|
12757
|
+
action: "not_found",
|
|
12758
|
+
list: parsed.list,
|
|
12759
|
+
interface: parsed.interface
|
|
12760
|
+
}
|
|
12180
12761
|
};
|
|
12181
12762
|
}
|
|
12182
12763
|
if (parsed.dryRun) {
|
|
@@ -12647,7 +13228,11 @@ function checkAuthz(identity, toolName, routerId2) {
|
|
|
12647
13228
|
category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
|
|
12648
13229
|
code: "TOOL_NOT_ALLOWED",
|
|
12649
13230
|
message: `Identity "${identity.id}" is not allowed to call tool "${toolName}".`,
|
|
12650
|
-
details: {
|
|
13231
|
+
details: {
|
|
13232
|
+
identityId: identity.id,
|
|
13233
|
+
toolName,
|
|
13234
|
+
allowedToolPatterns: identity.allowedToolPatterns
|
|
13235
|
+
},
|
|
12651
13236
|
recoverability: {
|
|
12652
13237
|
retryable: false,
|
|
12653
13238
|
suggestedAction: `Add a matching pattern to allowedToolPatterns for identity "${identity.id}" in identities.yaml.`
|
|
@@ -13465,17 +14050,20 @@ async function executeToolCall(tool, args, deps) {
|
|
|
13465
14050
|
}
|
|
13466
14051
|
const shouldAudit = tool.annotations.destructiveHint || !tool.annotations.readOnlyHint;
|
|
13467
14052
|
if (shouldAudit) {
|
|
13468
|
-
auditLog(
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
14053
|
+
auditLog(
|
|
14054
|
+
{
|
|
14055
|
+
type: "audit",
|
|
14056
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14057
|
+
correlationId,
|
|
14058
|
+
identityId: identity.id,
|
|
14059
|
+
role: identity.role,
|
|
14060
|
+
tool: tool.name,
|
|
14061
|
+
routerId: routerId2,
|
|
14062
|
+
phase: "attempt",
|
|
14063
|
+
params: args
|
|
14064
|
+
},
|
|
14065
|
+
config.auditLogPath
|
|
14066
|
+
);
|
|
13479
14067
|
}
|
|
13480
14068
|
const toolContext = buildRouterToolContext({
|
|
13481
14069
|
routerConfig,
|
|
@@ -13492,7 +14080,12 @@ async function executeToolCall(tool, args, deps) {
|
|
|
13492
14080
|
if (!tool.annotations.readOnlyHint) {
|
|
13493
14081
|
for (const path of snapshotPathsFor(tool, handlerArgs)) {
|
|
13494
14082
|
try {
|
|
13495
|
-
const meta = await takeSnapshot(
|
|
14083
|
+
const meta = await takeSnapshot(
|
|
14084
|
+
toolContext.deviceClient,
|
|
14085
|
+
routerId2,
|
|
14086
|
+
path,
|
|
14087
|
+
config.snapshotDir
|
|
14088
|
+
);
|
|
13496
14089
|
snapshotIds.push(meta.id);
|
|
13497
14090
|
log48.debug({ snapshotId: meta.id, path }, "Snapshot taken");
|
|
13498
14091
|
} catch (err) {
|
|
@@ -13524,18 +14117,21 @@ async function executeToolCall(tool, args, deps) {
|
|
|
13524
14117
|
});
|
|
13525
14118
|
}
|
|
13526
14119
|
if (shouldAudit) {
|
|
13527
|
-
auditLog(
|
|
13528
|
-
|
|
13529
|
-
|
|
13530
|
-
|
|
13531
|
-
|
|
13532
|
-
|
|
13533
|
-
|
|
13534
|
-
|
|
13535
|
-
|
|
13536
|
-
|
|
13537
|
-
|
|
13538
|
-
|
|
14120
|
+
auditLog(
|
|
14121
|
+
{
|
|
14122
|
+
type: "audit",
|
|
14123
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14124
|
+
correlationId,
|
|
14125
|
+
identityId: identity.id,
|
|
14126
|
+
role: identity.role,
|
|
14127
|
+
tool: tool.name,
|
|
14128
|
+
routerId: routerId2,
|
|
14129
|
+
phase: "success",
|
|
14130
|
+
params: args,
|
|
14131
|
+
durationMs: Date.now() - startMs
|
|
14132
|
+
},
|
|
14133
|
+
config.auditLogPath
|
|
14134
|
+
);
|
|
13539
14135
|
}
|
|
13540
14136
|
log48.info({ tool: tool.name, routerId: routerId2, correlationId }, "Tool executed successfully");
|
|
13541
14137
|
recordToolCall(tool.name, "success");
|
|
@@ -13565,19 +14161,22 @@ async function executeToolCall(tool, args, deps) {
|
|
|
13565
14161
|
if (shouldAudit && error.category !== "APPROVAL_REQUIRED" /* APPROVAL_REQUIRED */) {
|
|
13566
14162
|
const routerId2 = args.routerId ?? "unknown";
|
|
13567
14163
|
const identity = getCurrentIdentity() ?? getStdioIdentity(config.stdioIdentity, identityRegistry);
|
|
13568
|
-
auditLog(
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
|
|
13574
|
-
|
|
13575
|
-
|
|
13576
|
-
|
|
13577
|
-
|
|
13578
|
-
|
|
13579
|
-
|
|
13580
|
-
|
|
14164
|
+
auditLog(
|
|
14165
|
+
{
|
|
14166
|
+
type: "audit",
|
|
14167
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14168
|
+
correlationId,
|
|
14169
|
+
identityId: identity.id,
|
|
14170
|
+
role: identity.role,
|
|
14171
|
+
tool: tool.name,
|
|
14172
|
+
routerId: routerId2,
|
|
14173
|
+
phase: "failure",
|
|
14174
|
+
params: args,
|
|
14175
|
+
outcome: error.code,
|
|
14176
|
+
durationMs: Date.now() - startMs
|
|
14177
|
+
},
|
|
14178
|
+
config.auditLogPath
|
|
14179
|
+
);
|
|
13581
14180
|
}
|
|
13582
14181
|
log48.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
|
|
13583
14182
|
recordToolCall(tool.name, "error");
|
|
@@ -13716,10 +14315,16 @@ var listRoutersTool = {
|
|
|
13716
14315
|
const total = rows.length;
|
|
13717
14316
|
const header = `Routers: ${total === 0 ? "none" : `1-${total} of ${total}`}.`;
|
|
13718
14317
|
const lines = rows.map(
|
|
13719
|
-
(r) => ` ${compactFields(
|
|
13720
|
-
|
|
13721
|
-
|
|
13722
|
-
|
|
14318
|
+
(r) => ` ${compactFields({ ...r, tags: r.tags.join(",") }, [
|
|
14319
|
+
"id",
|
|
14320
|
+
"host",
|
|
14321
|
+
"port",
|
|
14322
|
+
"deviceType",
|
|
14323
|
+
"tlsEnabled",
|
|
14324
|
+
"tags",
|
|
14325
|
+
"rosVersion",
|
|
14326
|
+
"isDefault"
|
|
14327
|
+
])}`
|
|
13723
14328
|
);
|
|
13724
14329
|
return {
|
|
13725
14330
|
content: total === 0 ? header : [header, ...lines].join("\n"),
|
|
@@ -13808,17 +14413,20 @@ function createFleetTools(baseTools2) {
|
|
|
13808
14413
|
{ toolName: parsed.toolName, concurrency: parsed.concurrency },
|
|
13809
14414
|
"bulk_execute invoked"
|
|
13810
14415
|
);
|
|
13811
|
-
auditLog(
|
|
13812
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
14416
|
+
auditLog(
|
|
14417
|
+
{
|
|
14418
|
+
type: "audit",
|
|
14419
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14420
|
+
correlationId: context.correlationId,
|
|
14421
|
+
identityId: context.identity.id,
|
|
14422
|
+
role: context.identity.role,
|
|
14423
|
+
tool: "bulk_execute",
|
|
14424
|
+
routerId: "(fleet)",
|
|
14425
|
+
phase: "attempt",
|
|
14426
|
+
params: { toolName: parsed.toolName, routerIds: parsed.routerIds, tags: parsed.tags }
|
|
14427
|
+
},
|
|
14428
|
+
context.appConfig.auditLogPath
|
|
14429
|
+
);
|
|
13822
14430
|
if (parsed.toolName === "bulk_execute" || parsed.toolName === "check_router_health") {
|
|
13823
14431
|
throw new MikroMCPError({
|
|
13824
14432
|
category: "VALIDATION" /* VALIDATION */,
|
|
@@ -13891,17 +14499,20 @@ function createFleetTools(baseTools2) {
|
|
|
13891
14499
|
);
|
|
13892
14500
|
}
|
|
13893
14501
|
if (routers.length === 0 && preErrors.length === 0) {
|
|
13894
|
-
auditLog(
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
14502
|
+
auditLog(
|
|
14503
|
+
{
|
|
14504
|
+
type: "audit",
|
|
14505
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14506
|
+
correlationId: context.correlationId,
|
|
14507
|
+
identityId: context.identity.id,
|
|
14508
|
+
role: context.identity.role,
|
|
14509
|
+
tool: "bulk_execute",
|
|
14510
|
+
routerId: "(fleet)",
|
|
14511
|
+
phase: "success",
|
|
14512
|
+
params: { toolName: parsed.toolName, succeeded: 0, failed: 0 }
|
|
14513
|
+
},
|
|
14514
|
+
context.appConfig.auditLogPath
|
|
14515
|
+
);
|
|
13905
14516
|
return {
|
|
13906
14517
|
content: `Executed ${parsed.toolName} on 0 routers: 0 succeeded, 0 failed`,
|
|
13907
14518
|
structuredContent: {
|
|
@@ -13934,7 +14545,10 @@ function createFleetTools(baseTools2) {
|
|
|
13934
14545
|
const toolParams = { ...parsed.params, routerId: router.id };
|
|
13935
14546
|
const snapshotIds = [];
|
|
13936
14547
|
if (isWrite) {
|
|
13937
|
-
for (const path of snapshotPathsFor(
|
|
14548
|
+
for (const path of snapshotPathsFor(
|
|
14549
|
+
targetTool,
|
|
14550
|
+
toolParams
|
|
14551
|
+
)) {
|
|
13938
14552
|
try {
|
|
13939
14553
|
const meta = await takeSnapshot(
|
|
13940
14554
|
routerContext.deviceClient,
|
|
@@ -13971,40 +14585,57 @@ function createFleetTools(baseTools2) {
|
|
|
13971
14585
|
);
|
|
13972
14586
|
const elapsed = Date.now() - start;
|
|
13973
14587
|
if (journalId) {
|
|
13974
|
-
recordOutcome({
|
|
14588
|
+
recordOutcome({
|
|
14589
|
+
journalPath,
|
|
14590
|
+
journalId,
|
|
14591
|
+
phase: "success",
|
|
14592
|
+
durationMs: elapsed
|
|
14593
|
+
});
|
|
13975
14594
|
}
|
|
13976
|
-
auditLog(
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
14595
|
+
auditLog(
|
|
14596
|
+
{
|
|
14597
|
+
type: "audit",
|
|
14598
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14599
|
+
correlationId: context.correlationId,
|
|
14600
|
+
identityId: context.identity.id,
|
|
14601
|
+
role: context.identity.role,
|
|
14602
|
+
tool: parsed.toolName,
|
|
14603
|
+
routerId: router.id,
|
|
14604
|
+
phase: "success",
|
|
14605
|
+
params: parsed.params,
|
|
14606
|
+
durationMs: elapsed
|
|
14607
|
+
},
|
|
14608
|
+
context.appConfig.auditLogPath
|
|
14609
|
+
);
|
|
13988
14610
|
return { routerId: router.id, status: "ok", result, durationMs: elapsed };
|
|
13989
14611
|
} catch (err) {
|
|
13990
14612
|
const elapsed = Date.now() - start;
|
|
13991
14613
|
const message = err instanceof Error ? err.message : String(err);
|
|
13992
14614
|
if (journalId) {
|
|
13993
|
-
recordOutcome({
|
|
14615
|
+
recordOutcome({
|
|
14616
|
+
journalPath,
|
|
14617
|
+
journalId,
|
|
14618
|
+
phase: "failure",
|
|
14619
|
+
outcome: message,
|
|
14620
|
+
durationMs: elapsed
|
|
14621
|
+
});
|
|
13994
14622
|
}
|
|
13995
|
-
auditLog(
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14007
|
-
|
|
14623
|
+
auditLog(
|
|
14624
|
+
{
|
|
14625
|
+
type: "audit",
|
|
14626
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14627
|
+
correlationId: context.correlationId,
|
|
14628
|
+
identityId: context.identity.id,
|
|
14629
|
+
role: context.identity.role,
|
|
14630
|
+
tool: parsed.toolName,
|
|
14631
|
+
routerId: router.id,
|
|
14632
|
+
phase: "failure",
|
|
14633
|
+
params: parsed.params,
|
|
14634
|
+
outcome: message,
|
|
14635
|
+
durationMs: elapsed
|
|
14636
|
+
},
|
|
14637
|
+
context.appConfig.auditLogPath
|
|
14638
|
+
);
|
|
14008
14639
|
return { routerId: router.id, status: "error", error: message, durationMs: elapsed };
|
|
14009
14640
|
}
|
|
14010
14641
|
}
|
|
@@ -14016,17 +14647,20 @@ function createFleetTools(baseTools2) {
|
|
|
14016
14647
|
}
|
|
14017
14648
|
const succeeded = results2.filter((r) => r.status === "ok").length;
|
|
14018
14649
|
const failed = results2.filter((r) => r.status === "error").length;
|
|
14019
|
-
auditLog(
|
|
14020
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14650
|
+
auditLog(
|
|
14651
|
+
{
|
|
14652
|
+
type: "audit",
|
|
14653
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14654
|
+
correlationId: context.correlationId,
|
|
14655
|
+
identityId: context.identity.id,
|
|
14656
|
+
role: context.identity.role,
|
|
14657
|
+
tool: "bulk_execute",
|
|
14658
|
+
routerId: "(fleet)",
|
|
14659
|
+
phase: "success",
|
|
14660
|
+
params: { toolName: parsed.toolName, succeeded, failed }
|
|
14661
|
+
},
|
|
14662
|
+
context.appConfig.auditLogPath
|
|
14663
|
+
);
|
|
14030
14664
|
return {
|
|
14031
14665
|
content: `Executed ${parsed.toolName} on ${results2.length} routers: ${succeeded} succeeded, ${failed} failed`,
|
|
14032
14666
|
structuredContent: {
|
|
@@ -14152,9 +14786,7 @@ var RouterConfigSchema = z49.object({
|
|
|
14152
14786
|
cmdDeny: z49.array(z49.string()).optional(),
|
|
14153
14787
|
maintenanceWindows: z49.array(
|
|
14154
14788
|
z49.object({
|
|
14155
|
-
days: z49.array(
|
|
14156
|
-
z49.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
|
|
14157
|
-
),
|
|
14789
|
+
days: z49.array(z49.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])),
|
|
14158
14790
|
startTime: z49.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
|
|
14159
14791
|
endTime: z49.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
|
|
14160
14792
|
timezone: z49.string().min(1)
|
|
@@ -14230,7 +14862,18 @@ ${issues}`);
|
|
|
14230
14862
|
getRouter(id) {
|
|
14231
14863
|
const router = this.routers.get(id);
|
|
14232
14864
|
if (!router) {
|
|
14233
|
-
|
|
14865
|
+
const available = this.routerIds();
|
|
14866
|
+
throw new MikroMCPError({
|
|
14867
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
14868
|
+
code: "ROUTER_NOT_FOUND",
|
|
14869
|
+
message: available.length > 0 ? `Router not found: ${id}. Available: ${available.join(", ")}.` : `Router not found: ${id}. No routers are configured.`,
|
|
14870
|
+
details: { routerId: id, availableRouters: available },
|
|
14871
|
+
recoverability: {
|
|
14872
|
+
retryable: false,
|
|
14873
|
+
suggestedAction: "Check the routerId, or use list_routers to see the configured routers.",
|
|
14874
|
+
alternativeTools: ["list_routers"]
|
|
14875
|
+
}
|
|
14876
|
+
});
|
|
14234
14877
|
}
|
|
14235
14878
|
return router;
|
|
14236
14879
|
}
|
|
@@ -14317,7 +14960,7 @@ var ConnectionPool = class {
|
|
|
14317
14960
|
};
|
|
14318
14961
|
|
|
14319
14962
|
// src/version.ts
|
|
14320
|
-
var VERSION = "1.
|
|
14963
|
+
var VERSION = "1.10.0";
|
|
14321
14964
|
|
|
14322
14965
|
// src/mcp/server.ts
|
|
14323
14966
|
var SERVER_INSTRUCTIONS = [
|
|
@@ -14503,7 +15146,18 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
14503
15146
|
}
|
|
14504
15147
|
const boundIdentity = identity;
|
|
14505
15148
|
await withIdentity(boundIdentity, async () => {
|
|
14506
|
-
await handleMcpRequest(
|
|
15149
|
+
await handleMcpRequest(
|
|
15150
|
+
pathname,
|
|
15151
|
+
req,
|
|
15152
|
+
res,
|
|
15153
|
+
url,
|
|
15154
|
+
port,
|
|
15155
|
+
maxBodyBytes,
|
|
15156
|
+
makeServer,
|
|
15157
|
+
streamableSessions,
|
|
15158
|
+
sseTransports,
|
|
15159
|
+
boundIdentity.id
|
|
15160
|
+
);
|
|
14507
15161
|
});
|
|
14508
15162
|
return;
|
|
14509
15163
|
}
|
|
@@ -14741,9 +15395,7 @@ async function runServe() {
|
|
|
14741
15395
|
void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
|
|
14742
15396
|
const identityRegistry = new IdentityRegistry(config.identitiesPath);
|
|
14743
15397
|
if (config.transport === "http") {
|
|
14744
|
-
const hasLimitedRoles = identityRegistry.getIdentities().some(
|
|
14745
|
-
(i) => i.role === "readonly" || i.role === "operator"
|
|
14746
|
-
);
|
|
15398
|
+
const hasLimitedRoles = identityRegistry.getIdentities().some((i) => i.role === "readonly" || i.role === "operator");
|
|
14747
15399
|
if (hasLimitedRoles && !config.confirmationSecret) {
|
|
14748
15400
|
log55.error(
|
|
14749
15401
|
"MIKROMCP_CONFIRMATION_SECRET is required in HTTP mode when identities with role readonly or operator are configured"
|
|
@@ -14786,7 +15438,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
|
14786
15438
|
import { homedir as homedir2 } from "os";
|
|
14787
15439
|
import * as net from "net";
|
|
14788
15440
|
import chalk from "chalk";
|
|
14789
|
-
var localVersion = "1.
|
|
15441
|
+
var localVersion = "1.10.0";
|
|
14790
15442
|
var results = [];
|
|
14791
15443
|
function ok(msg) {
|
|
14792
15444
|
console.log(chalk.green("\u2705 ") + msg);
|
|
@@ -14883,7 +15535,9 @@ function checkDefaultRouter(defaultRouter, routers) {
|
|
|
14883
15535
|
const ids = routers.map((r) => r.id);
|
|
14884
15536
|
if (defaultRouter) {
|
|
14885
15537
|
if (ids.includes(defaultRouter)) {
|
|
14886
|
-
ok(
|
|
15538
|
+
ok(
|
|
15539
|
+
`Default router: ${defaultRouter} (MIKROMCP_DEFAULT_ROUTER; used when routerId is omitted)`
|
|
15540
|
+
);
|
|
14887
15541
|
} else {
|
|
14888
15542
|
fail(
|
|
14889
15543
|
`MIKROMCP_DEFAULT_ROUTER="${defaultRouter}" does not match any configured router` + (ids.length > 0 ? ` (available: ${ids.join(", ")})` : "")
|
|
@@ -14930,7 +15584,9 @@ async function checkRouter(router) {
|
|
|
14930
15584
|
indentWarn(`read policy: ip/address returned ${code} \u2014 check user permissions`);
|
|
14931
15585
|
results.push({ passed: true, warning: true });
|
|
14932
15586
|
} else {
|
|
14933
|
-
indentFail(
|
|
15587
|
+
indentFail(
|
|
15588
|
+
`read policy: ip/address failed \u2014 ${readErr instanceof Error ? readErr.message : String(readErr)}`
|
|
15589
|
+
);
|
|
14934
15590
|
results.push({ passed: false, warning: false });
|
|
14935
15591
|
}
|
|
14936
15592
|
}
|
|
@@ -15013,7 +15669,9 @@ function checkClaudeDesktop() {
|
|
|
15013
15669
|
);
|
|
15014
15670
|
}
|
|
15015
15671
|
} catch (err) {
|
|
15016
|
-
warn(
|
|
15672
|
+
warn(
|
|
15673
|
+
`Could not parse Claude Desktop config: ${err instanceof Error ? err.message : String(err)}`
|
|
15674
|
+
);
|
|
15017
15675
|
}
|
|
15018
15676
|
}
|
|
15019
15677
|
function checkSkill() {
|
|
@@ -15092,13 +15750,7 @@ async function runDoctor() {
|
|
|
15092
15750
|
// src/cli/init.ts
|
|
15093
15751
|
import { input, confirm, select } from "@inquirer/prompts";
|
|
15094
15752
|
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";
|
|
15753
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync, mkdirSync, copyFileSync } from "fs";
|
|
15102
15754
|
import { homedir as homedir3 } from "os";
|
|
15103
15755
|
import { join as join4 } from "path";
|
|
15104
15756
|
import { parse as yamlParse, Document, visit } from "yaml";
|
|
@@ -15199,7 +15851,18 @@ async function collectRouterInfo() {
|
|
|
15199
15851
|
default: "7",
|
|
15200
15852
|
validate: (v) => v.trim().length > 0 ? true : "Version is required"
|
|
15201
15853
|
});
|
|
15202
|
-
return {
|
|
15854
|
+
return {
|
|
15855
|
+
routerId: routerId2,
|
|
15856
|
+
host,
|
|
15857
|
+
port,
|
|
15858
|
+
tlsEnabled,
|
|
15859
|
+
rejectUnauthorized,
|
|
15860
|
+
envPrefix,
|
|
15861
|
+
routerUser,
|
|
15862
|
+
routerPass,
|
|
15863
|
+
tags,
|
|
15864
|
+
rosVersion
|
|
15865
|
+
};
|
|
15203
15866
|
}
|
|
15204
15867
|
async function collectDefaultRouterPreference(routerId2) {
|
|
15205
15868
|
return confirm({
|
|
@@ -15266,7 +15929,10 @@ async function collectTransport() {
|
|
|
15266
15929
|
const transport2 = await select({
|
|
15267
15930
|
message: "Transport mode:",
|
|
15268
15931
|
choices: [
|
|
15269
|
-
{
|
|
15932
|
+
{
|
|
15933
|
+
value: "stdio",
|
|
15934
|
+
name: "stdio \u2014 launched by Claude Desktop or an MCP client (recommended)"
|
|
15935
|
+
},
|
|
15270
15936
|
{ value: "http", name: "http \u2014 standalone HTTP server with bearer-token auth" }
|
|
15271
15937
|
],
|
|
15272
15938
|
default: "stdio"
|
|
@@ -15484,21 +16150,25 @@ async function runInit() {
|
|
|
15484
16150
|
}
|
|
15485
16151
|
if (data.createIdentity && data.rawToken) {
|
|
15486
16152
|
console.log(
|
|
15487
|
-
chalk2.bold.yellow(
|
|
16153
|
+
chalk2.bold.yellow(
|
|
16154
|
+
` \u2705 Bearer token: ${data.rawToken} (save this \u2014 it won't be shown again)`
|
|
16155
|
+
)
|
|
15488
16156
|
);
|
|
15489
16157
|
}
|
|
15490
16158
|
console.log(chalk2.bold("\nNext steps:"));
|
|
15491
16159
|
console.log(chalk2.dim(" 1. Run: mikromcp doctor"));
|
|
15492
16160
|
console.log(chalk2.dim(" 2. Restart Claude Desktop to load the MCP server"));
|
|
15493
16161
|
console.log(
|
|
15494
|
-
chalk2.dim(
|
|
16162
|
+
chalk2.dim(
|
|
16163
|
+
" 3. (Optional) Install the usage skill so your AI assistant drives MikroMCP safely"
|
|
16164
|
+
)
|
|
15495
16165
|
);
|
|
15496
16166
|
console.log(chalk2.dim(" See: docs/wiki/Using-the-Skill.md"));
|
|
15497
16167
|
console.log();
|
|
15498
16168
|
}
|
|
15499
16169
|
|
|
15500
16170
|
// src/cli/index.ts
|
|
15501
|
-
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.
|
|
16171
|
+
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.10.0");
|
|
15502
16172
|
program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
|
|
15503
16173
|
program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
|
|
15504
16174
|
program.command("init").description("Interactive setup wizard").action(runInit);
|