mikromcp 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/dist/main.js +1634 -415
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -60,7 +60,7 @@ function loadAppConfig() {
|
|
|
60
60
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
61
61
|
|
|
62
62
|
// src/mcp/tool-registry.ts
|
|
63
|
-
import { z as
|
|
63
|
+
import { z as z38 } from "zod";
|
|
64
64
|
|
|
65
65
|
// src/domain/tools/system-tools.ts
|
|
66
66
|
import { z } from "zod";
|
|
@@ -446,7 +446,12 @@ var COUNTER_PROPS = [
|
|
|
446
446
|
"rx-drop",
|
|
447
447
|
"tx-error",
|
|
448
448
|
"rx-error",
|
|
449
|
-
"tx-queue-drop"
|
|
449
|
+
"tx-queue-drop",
|
|
450
|
+
"fp-tx-byte",
|
|
451
|
+
"fp-rx-byte",
|
|
452
|
+
"fp-tx-packet",
|
|
453
|
+
"fp-rx-packet",
|
|
454
|
+
"fp-rps-drop"
|
|
450
455
|
];
|
|
451
456
|
var listInterfacesTool = {
|
|
452
457
|
name: "list_interfaces",
|
|
@@ -490,14 +495,17 @@ var listInterfacesTool = {
|
|
|
490
495
|
}
|
|
491
496
|
const total = interfaces.length;
|
|
492
497
|
const paginated = interfaces.slice(parsed.offset, parsed.offset + parsed.limit);
|
|
493
|
-
const results2 =
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
+
const results2 = paginated.map((iface) => {
|
|
499
|
+
const rec = iface;
|
|
500
|
+
const isRunning2 = rec.running === true || rec.running === "true";
|
|
501
|
+
const isDisabled = rec.disabled === true || rec.disabled === "true";
|
|
502
|
+
const enriched = { status: isDisabled ? "disabled" : isRunning2 ? "up" : "down" };
|
|
503
|
+
for (const [key2, value] of Object.entries(rec)) {
|
|
504
|
+
if (parsed.includeCounters || !COUNTER_PROPS.includes(key2)) {
|
|
505
|
+
enriched[key2] = value;
|
|
498
506
|
}
|
|
499
507
|
}
|
|
500
|
-
return
|
|
508
|
+
return enriched;
|
|
501
509
|
});
|
|
502
510
|
const hasMore = parsed.offset + parsed.limit < total;
|
|
503
511
|
const lines = [
|
|
@@ -507,8 +515,8 @@ var listInterfacesTool = {
|
|
|
507
515
|
const rec = iface;
|
|
508
516
|
const name = rec.name ?? rec[".id"] ?? "unknown";
|
|
509
517
|
const type = rec.type ?? parsed.type;
|
|
510
|
-
const
|
|
511
|
-
lines.push(` ${name} [${type}] ${
|
|
518
|
+
const status = String(rec.status).toUpperCase();
|
|
519
|
+
lines.push(` ${name} [${type}] ${status}`);
|
|
512
520
|
}
|
|
513
521
|
return {
|
|
514
522
|
content: lines.join("\n"),
|
|
@@ -526,115 +534,7 @@ var listInterfacesTool = {
|
|
|
526
534
|
}
|
|
527
535
|
}
|
|
528
536
|
};
|
|
529
|
-
var
|
|
530
|
-
routerId: z2.string().describe("Target router identifier from the router registry"),
|
|
531
|
-
name: z2.string().regex(/^[a-zA-Z0-9_-]+$/).max(15).describe("VLAN interface name (alphanumeric, hyphens, underscores, max 15 chars)"),
|
|
532
|
-
vlanId: z2.number().int().min(1).max(4094).describe("VLAN ID (1-4094)"),
|
|
533
|
-
parentInterface: z2.string().describe("Parent interface name (e.g., ether1, bridge1)"),
|
|
534
|
-
mtu: z2.number().int().min(68).max(9e3).default(1500).describe("MTU size"),
|
|
535
|
-
disabled: z2.boolean().default(false).describe("Whether the VLAN interface should be disabled"),
|
|
536
|
-
comment: z2.string().max(255).optional().describe("Optional comment for the VLAN interface"),
|
|
537
|
-
dryRun: z2.boolean().default(false).describe("If true, validate and return planned changes without applying")
|
|
538
|
-
}).strict();
|
|
539
|
-
function sanitizeComment(comment) {
|
|
540
|
-
if (comment === void 0) return void 0;
|
|
541
|
-
return comment.replace(/[\x00-\x1f\x7f]/g, "");
|
|
542
|
-
}
|
|
543
|
-
var createVlanTool = {
|
|
544
|
-
name: "create_vlan",
|
|
545
|
-
title: "Create VLAN Interface",
|
|
546
|
-
description: "Create a new VLAN interface on a MikroTik router. Performs idempotency checks: if a VLAN with the same name already exists with matching configuration, returns success without changes. Supports dry-run mode.",
|
|
547
|
-
inputSchema: createVlanInputSchema,
|
|
548
|
-
annotations: {
|
|
549
|
-
readOnlyHint: false,
|
|
550
|
-
destructiveHint: false,
|
|
551
|
-
idempotentHint: true,
|
|
552
|
-
openWorldHint: false
|
|
553
|
-
},
|
|
554
|
-
async handler(params, context) {
|
|
555
|
-
const parsed = createVlanInputSchema.parse(params);
|
|
556
|
-
const comment = sanitizeComment(parsed.comment);
|
|
557
|
-
log2.info(
|
|
558
|
-
{ routerId: context.routerId, name: parsed.name, vlanId: parsed.vlanId },
|
|
559
|
-
"Creating VLAN interface"
|
|
560
|
-
);
|
|
561
|
-
try {
|
|
562
|
-
const existing = await context.routerClient.get("interface/vlan", {
|
|
563
|
-
filter: { name: parsed.name }
|
|
564
|
-
});
|
|
565
|
-
if (existing.length > 0) {
|
|
566
|
-
const found = existing[0];
|
|
567
|
-
const existingVlanId = found["vlan-id"];
|
|
568
|
-
const existingInterface = found.interface;
|
|
569
|
-
if (existingVlanId === String(parsed.vlanId) && existingInterface === parsed.parentInterface) {
|
|
570
|
-
log2.info({ name: parsed.name }, "VLAN already exists with matching config");
|
|
571
|
-
return {
|
|
572
|
-
content: `VLAN "${parsed.name}" already exists with VLAN ID ${parsed.vlanId} on ${parsed.parentInterface}. No changes made.`,
|
|
573
|
-
structuredContent: {
|
|
574
|
-
action: "already_exists",
|
|
575
|
-
vlan: found
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
throw new MikroMCPError({
|
|
580
|
-
category: "CONFLICT" /* CONFLICT */,
|
|
581
|
-
code: "VLAN_NAME_CONFLICT",
|
|
582
|
-
message: `VLAN "${parsed.name}" already exists but with different configuration (vlan-id=${existingVlanId}, interface=${existingInterface}). Expected vlan-id=${parsed.vlanId}, interface=${parsed.parentInterface}.`,
|
|
583
|
-
details: {
|
|
584
|
-
existingVlanId,
|
|
585
|
-
existingInterface,
|
|
586
|
-
requestedVlanId: parsed.vlanId,
|
|
587
|
-
requestedInterface: parsed.parentInterface
|
|
588
|
-
},
|
|
589
|
-
recoverability: {
|
|
590
|
-
retryable: false,
|
|
591
|
-
suggestedAction: "Use a different name or update the existing VLAN."
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
if (parsed.dryRun) {
|
|
596
|
-
const diff = [
|
|
597
|
-
{ property: "name", before: null, after: parsed.name },
|
|
598
|
-
{ property: "vlan-id", before: null, after: String(parsed.vlanId) },
|
|
599
|
-
{ property: "interface", before: null, after: parsed.parentInterface },
|
|
600
|
-
{ property: "mtu", before: null, after: String(parsed.mtu) },
|
|
601
|
-
{ property: "disabled", before: null, after: parsed.disabled ? "true" : "false" },
|
|
602
|
-
...comment ? [{ property: "comment", before: null, after: comment }] : []
|
|
603
|
-
];
|
|
604
|
-
return {
|
|
605
|
-
content: `Dry run: Would create VLAN "${parsed.name}" (ID ${parsed.vlanId}) on ${parsed.parentInterface} with MTU ${parsed.mtu}.`,
|
|
606
|
-
structuredContent: {
|
|
607
|
-
action: "dry_run",
|
|
608
|
-
diff
|
|
609
|
-
}
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
const body = {
|
|
613
|
-
name: parsed.name,
|
|
614
|
-
"vlan-id": String(parsed.vlanId),
|
|
615
|
-
interface: parsed.parentInterface,
|
|
616
|
-
mtu: String(parsed.mtu),
|
|
617
|
-
disabled: parsed.disabled ? "true" : "false"
|
|
618
|
-
};
|
|
619
|
-
if (comment) {
|
|
620
|
-
body.comment = comment;
|
|
621
|
-
}
|
|
622
|
-
const created = await context.routerClient.create("interface/vlan", body);
|
|
623
|
-
log2.info({ name: parsed.name, id: created[".id"] }, "VLAN created successfully");
|
|
624
|
-
return {
|
|
625
|
-
content: `Created VLAN "${parsed.name}" (ID ${parsed.vlanId}) on ${parsed.parentInterface} with MTU ${parsed.mtu}.`,
|
|
626
|
-
structuredContent: {
|
|
627
|
-
action: "created",
|
|
628
|
-
vlan: created
|
|
629
|
-
}
|
|
630
|
-
};
|
|
631
|
-
} catch (err) {
|
|
632
|
-
if (err instanceof MikroMCPError) throw err;
|
|
633
|
-
throw enrichError(err, { routerId: context.routerId, tool: "create_vlan" });
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
};
|
|
637
|
-
var interfaceTools = [listInterfacesTool, createVlanTool];
|
|
537
|
+
var interfaceTools = [listInterfacesTool];
|
|
638
538
|
|
|
639
539
|
// src/domain/tools/ip-tools.ts
|
|
640
540
|
import { z as z4 } from "zod";
|
|
@@ -685,7 +585,7 @@ var inputSchema2 = z4.object({
|
|
|
685
585
|
disabled: z4.boolean().default(false).describe("Whether the IP address should be disabled"),
|
|
686
586
|
dryRun: z4.boolean().default(false).describe("If true, validate and return planned changes without applying")
|
|
687
587
|
}).strict();
|
|
688
|
-
function
|
|
588
|
+
function sanitizeComment(comment) {
|
|
689
589
|
if (comment === void 0) return void 0;
|
|
690
590
|
return comment.replace(/[\x00-\x1f\x7f]/g, "");
|
|
691
591
|
}
|
|
@@ -709,7 +609,7 @@ var manageIpAddressTool = {
|
|
|
709
609
|
snapshotPaths: ["ip/address"],
|
|
710
610
|
async handler(params, context) {
|
|
711
611
|
const parsed = inputSchema2.parse(params);
|
|
712
|
-
const comment =
|
|
612
|
+
const comment = sanitizeComment(parsed.comment);
|
|
713
613
|
log3.info(
|
|
714
614
|
{
|
|
715
615
|
routerId: context.routerId,
|
|
@@ -898,13 +798,14 @@ var listDhcpLeasesInputSchema = z5.object({
|
|
|
898
798
|
server: z5.string().optional().describe("Filter by DHCP server name"),
|
|
899
799
|
status: z5.enum(["bound", "waiting", "offered", "blocked", "all"]).default("all").describe("Filter by lease status"),
|
|
900
800
|
macAddress: z5.string().optional().describe("Filter by MAC address (exact match, case-insensitive)"),
|
|
801
|
+
leaseType: z5.enum(["dynamic", "static", "all"]).default("all").describe("Filter by lease type (dynamic or static)"),
|
|
901
802
|
limit: z5.number().int().min(1).max(500).default(100).describe("Maximum number of leases to return"),
|
|
902
803
|
offset: z5.number().int().min(0).default(0).describe("Offset for pagination")
|
|
903
804
|
}).strict();
|
|
904
805
|
var listDhcpLeasesTool = {
|
|
905
806
|
name: "list_dhcp_leases",
|
|
906
807
|
title: "List DHCP Leases",
|
|
907
|
-
description: "List DHCP leases on a MikroTik router with optional filtering by server, status, and MAC address. Supports pagination.",
|
|
808
|
+
description: "List DHCP leases on a MikroTik router with optional filtering by server, status, lease type (dynamic/static), and MAC address. Supports pagination.",
|
|
908
809
|
inputSchema: listDhcpLeasesInputSchema,
|
|
909
810
|
annotations: {
|
|
910
811
|
readOnlyHint: true,
|
|
@@ -948,6 +849,14 @@ var listDhcpLeasesTool = {
|
|
|
948
849
|
return leaseMAC === upperMac;
|
|
949
850
|
});
|
|
950
851
|
}
|
|
852
|
+
if (parsed.leaseType !== "all") {
|
|
853
|
+
leases = leases.filter((lease) => {
|
|
854
|
+
const rec = lease;
|
|
855
|
+
const isDynamic = rec.dynamic === true || rec.dynamic === "true";
|
|
856
|
+
if (parsed.leaseType === "dynamic") return isDynamic;
|
|
857
|
+
return !isDynamic;
|
|
858
|
+
});
|
|
859
|
+
}
|
|
951
860
|
const total = leases.length;
|
|
952
861
|
const paginated = leases.slice(parsed.offset, parsed.offset + parsed.limit);
|
|
953
862
|
const hasMore = parsed.offset + parsed.limit < total;
|
|
@@ -979,7 +888,110 @@ var listDhcpLeasesTool = {
|
|
|
979
888
|
}
|
|
980
889
|
}
|
|
981
890
|
};
|
|
982
|
-
var
|
|
891
|
+
var manageDhcpLeaseInputSchema = z5.object({
|
|
892
|
+
routerId: z5.string().describe("Target router identifier from the router registry"),
|
|
893
|
+
action: z5.enum(["make-static", "remove"]).describe(
|
|
894
|
+
"Action to perform: make-static converts a dynamic lease to static; remove deletes the lease"
|
|
895
|
+
),
|
|
896
|
+
macAddress: z5.string().describe("MAC address of the lease \u2014 idempotency key (case-insensitive)"),
|
|
897
|
+
dryRun: z5.boolean().default(false).describe("Preview changes without applying")
|
|
898
|
+
}).strict();
|
|
899
|
+
var manageDhcpLeaseTool = {
|
|
900
|
+
name: "manage_dhcp_lease",
|
|
901
|
+
title: "Manage DHCP Lease",
|
|
902
|
+
description: "Convert a dynamic DHCP lease to static (make-static) or remove a lease. Idempotent by MAC address.",
|
|
903
|
+
inputSchema: manageDhcpLeaseInputSchema,
|
|
904
|
+
annotations: {
|
|
905
|
+
readOnlyHint: false,
|
|
906
|
+
destructiveHint: false,
|
|
907
|
+
idempotentHint: true,
|
|
908
|
+
openWorldHint: false
|
|
909
|
+
},
|
|
910
|
+
snapshotPaths: ["ip/dhcp-server/lease"],
|
|
911
|
+
async handler(params, context) {
|
|
912
|
+
const parsed = manageDhcpLeaseInputSchema.parse(params);
|
|
913
|
+
const upperMac = parsed.macAddress.toUpperCase();
|
|
914
|
+
log4.info(
|
|
915
|
+
{ routerId: context.routerId, action: parsed.action, macAddress: upperMac },
|
|
916
|
+
"Managing DHCP lease"
|
|
917
|
+
);
|
|
918
|
+
try {
|
|
919
|
+
const allLeases = await context.routerClient.get("ip/dhcp-server/lease", {
|
|
920
|
+
limit: void 0,
|
|
921
|
+
offset: void 0
|
|
922
|
+
});
|
|
923
|
+
const existing = allLeases.find(
|
|
924
|
+
(l) => (l["mac-address"] ?? "").toUpperCase() === upperMac
|
|
925
|
+
);
|
|
926
|
+
if (parsed.action === "make-static") {
|
|
927
|
+
if (!existing) {
|
|
928
|
+
throw new MikroMCPError({
|
|
929
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
930
|
+
code: "DHCP_LEASE_NOT_FOUND",
|
|
931
|
+
message: `No DHCP lease found for MAC address ${upperMac}.`,
|
|
932
|
+
details: { macAddress: upperMac },
|
|
933
|
+
recoverability: {
|
|
934
|
+
retryable: false,
|
|
935
|
+
suggestedAction: "Verify the MAC address with list_dhcp_leases.",
|
|
936
|
+
alternativeTools: ["list_dhcp_leases"]
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
const existingDynamic = existing.dynamic;
|
|
941
|
+
const isAlreadyStatic = existingDynamic === "false" || existingDynamic === false;
|
|
942
|
+
if (isAlreadyStatic) {
|
|
943
|
+
return {
|
|
944
|
+
content: `Lease for ${upperMac} (${existing.address ?? "unknown"}) is already static. No changes made.`,
|
|
945
|
+
structuredContent: { action: "already_static", macAddress: upperMac, id: existing[".id"] }
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
if (parsed.dryRun) {
|
|
949
|
+
return {
|
|
950
|
+
content: `Dry run: Would convert lease for ${upperMac} (${existing.address ?? "unknown"}) to static.`,
|
|
951
|
+
structuredContent: {
|
|
952
|
+
action: "dry_run",
|
|
953
|
+
diff: [{ property: "dynamic", before: "true", after: "false" }],
|
|
954
|
+
lease: existing
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
await context.routerClient.update("ip/dhcp-server/lease", existing[".id"], {
|
|
959
|
+
dynamic: "false"
|
|
960
|
+
});
|
|
961
|
+
log4.info({ macAddress: upperMac, id: existing[".id"] }, "DHCP lease converted to static");
|
|
962
|
+
return {
|
|
963
|
+
content: `Lease for ${upperMac} (${existing.address ?? "unknown"}) converted to static.`,
|
|
964
|
+
structuredContent: { action: "made-static", macAddress: upperMac, id: existing[".id"] }
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
if (!existing) {
|
|
968
|
+
return {
|
|
969
|
+
content: `No DHCP lease found for MAC address ${upperMac}. Nothing to remove.`,
|
|
970
|
+
structuredContent: { action: "not_found", macAddress: upperMac }
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
if (parsed.dryRun) {
|
|
974
|
+
return {
|
|
975
|
+
content: `Dry run: Would remove lease for ${upperMac}.`,
|
|
976
|
+
structuredContent: {
|
|
977
|
+
action: "dry_run",
|
|
978
|
+
diff: [{ property: "mac-address", before: upperMac, after: null }]
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
await context.routerClient.remove("ip/dhcp-server/lease", existing[".id"]);
|
|
983
|
+
log4.info({ macAddress: upperMac }, "DHCP lease removed");
|
|
984
|
+
return {
|
|
985
|
+
content: `Removed DHCP lease for ${upperMac}.`,
|
|
986
|
+
structuredContent: { action: "removed", macAddress: upperMac, id: existing[".id"] }
|
|
987
|
+
};
|
|
988
|
+
} catch (err) {
|
|
989
|
+
if (err instanceof MikroMCPError) throw err;
|
|
990
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_dhcp_lease" });
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
var dhcpTools = [listDhcpLeasesTool, manageDhcpLeaseTool];
|
|
983
995
|
|
|
984
996
|
// src/domain/tools/route-tools.ts
|
|
985
997
|
import { z as z6 } from "zod";
|
|
@@ -1083,7 +1095,7 @@ var manageRouteInputSchema = z6.object({
|
|
|
1083
1095
|
disabled: z6.boolean().default(false).describe("Whether the route should be disabled"),
|
|
1084
1096
|
dryRun: z6.boolean().default(false).describe("If true, validate and return planned changes without applying")
|
|
1085
1097
|
}).strict();
|
|
1086
|
-
function
|
|
1098
|
+
function sanitizeComment2(comment) {
|
|
1087
1099
|
if (comment === void 0) return void 0;
|
|
1088
1100
|
return comment.replace(/[\x00-\x1f\x7f]/g, "");
|
|
1089
1101
|
}
|
|
@@ -1111,7 +1123,7 @@ var manageRouteTool = {
|
|
|
1111
1123
|
snapshotPaths: ["ip/route"],
|
|
1112
1124
|
async handler(params, context) {
|
|
1113
1125
|
const parsed = manageRouteInputSchema.parse(params);
|
|
1114
|
-
const comment =
|
|
1126
|
+
const comment = sanitizeComment2(parsed.comment);
|
|
1115
1127
|
log5.info(
|
|
1116
1128
|
{
|
|
1117
1129
|
routerId: context.routerId,
|
|
@@ -1252,7 +1264,7 @@ var log6 = createLogger("firewall-tools");
|
|
|
1252
1264
|
function tableToPath(table) {
|
|
1253
1265
|
return table === "filter" ? "ip/firewall/filter" : "ip/firewall/nat";
|
|
1254
1266
|
}
|
|
1255
|
-
function
|
|
1267
|
+
function sanitizeComment3(comment) {
|
|
1256
1268
|
if (comment === void 0) return void 0;
|
|
1257
1269
|
return comment.replace(/[\x00-\x1f\x7f]/g, "");
|
|
1258
1270
|
}
|
|
@@ -1391,7 +1403,7 @@ var manageFirewallRuleTool = {
|
|
|
1391
1403
|
snapshotPaths: ["ip/firewall/filter", "ip/firewall/nat"],
|
|
1392
1404
|
async handler(params, context) {
|
|
1393
1405
|
const parsed = manageFirewallRuleInputSchema.parse(params);
|
|
1394
|
-
const comment =
|
|
1406
|
+
const comment = sanitizeComment3(parsed.comment);
|
|
1395
1407
|
log6.info(
|
|
1396
1408
|
{
|
|
1397
1409
|
routerId: context.routerId,
|
|
@@ -6051,7 +6063,8 @@ var log28 = createLogger("dhcp-server-tools");
|
|
|
6051
6063
|
var listDhcpServersInputSchema = z27.object({
|
|
6052
6064
|
routerId: z27.string().describe("Target router identifier from the router registry"),
|
|
6053
6065
|
interface: z27.string().optional().describe("Filter by interface name (exact match)"),
|
|
6054
|
-
limit: z27.number().int().min(1).max(500).default(100).describe("Maximum number of DHCP servers to return")
|
|
6066
|
+
limit: z27.number().int().min(1).max(500).default(100).describe("Maximum number of DHCP servers to return"),
|
|
6067
|
+
offset: z27.number().int().min(0).default(0).describe("Offset for pagination")
|
|
6055
6068
|
}).strict();
|
|
6056
6069
|
var listDhcpServersTool = {
|
|
6057
6070
|
name: "list_dhcp_servers",
|
|
@@ -6072,17 +6085,19 @@ var listDhcpServersTool = {
|
|
|
6072
6085
|
limit: void 0,
|
|
6073
6086
|
offset: void 0
|
|
6074
6087
|
});
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
const
|
|
6088
|
+
let filtered = parsed.interface ? allServers.filter((s) => s.interface === parsed.interface) : allServers;
|
|
6089
|
+
const total = filtered.length;
|
|
6090
|
+
const servers = filtered.slice(parsed.offset, parsed.offset + parsed.limit);
|
|
6091
|
+
const hasMore = parsed.offset + parsed.limit < total;
|
|
6079
6092
|
return {
|
|
6080
|
-
content: `DHCP servers on ${context.routerId}: ${servers.length}
|
|
6093
|
+
content: `DHCP servers on ${context.routerId}: ${total} total, showing ${servers.length} (offset ${parsed.offset})`,
|
|
6081
6094
|
structuredContent: {
|
|
6082
6095
|
routerId: context.routerId,
|
|
6083
6096
|
servers,
|
|
6084
|
-
total
|
|
6085
|
-
|
|
6097
|
+
total,
|
|
6098
|
+
hasMore,
|
|
6099
|
+
offset: parsed.offset,
|
|
6100
|
+
limit: parsed.limit
|
|
6086
6101
|
}
|
|
6087
6102
|
};
|
|
6088
6103
|
} catch (err) {
|
|
@@ -6273,16 +6288,22 @@ var manageDhcpServerTool = {
|
|
|
6273
6288
|
}
|
|
6274
6289
|
}
|
|
6275
6290
|
};
|
|
6276
|
-
var
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6291
|
+
var dhcpServerTools = [listDhcpServersTool, manageDhcpServerTool];
|
|
6292
|
+
|
|
6293
|
+
// src/domain/tools/ip-pool-tools.ts
|
|
6294
|
+
import { z as z28 } from "zod";
|
|
6295
|
+
var log29 = createLogger("ip-pool-tools");
|
|
6296
|
+
var listIpPoolsInputSchema = z28.object({
|
|
6297
|
+
routerId: z28.string().describe("Target router identifier from the router registry"),
|
|
6298
|
+
name: z28.string().optional().describe("Filter by pool name (substring match)"),
|
|
6299
|
+
limit: z28.number().int().min(1).max(500).default(100).describe("Maximum number of pools to return"),
|
|
6300
|
+
offset: z28.number().int().min(0).default(0).describe("Offset for pagination")
|
|
6280
6301
|
}).strict();
|
|
6281
|
-
var
|
|
6282
|
-
name: "
|
|
6283
|
-
title: "List
|
|
6284
|
-
description: "List IP pools on a MikroTik router.",
|
|
6285
|
-
inputSchema:
|
|
6302
|
+
var listIpPoolsTool = {
|
|
6303
|
+
name: "list_ip_pools",
|
|
6304
|
+
title: "List IP Pools",
|
|
6305
|
+
description: "List IP address pools on a MikroTik router. Supports filtering by name and pagination.",
|
|
6306
|
+
inputSchema: listIpPoolsInputSchema,
|
|
6286
6307
|
annotations: {
|
|
6287
6308
|
readOnlyHint: true,
|
|
6288
6309
|
destructiveHint: false,
|
|
@@ -6290,44 +6311,49 @@ var listDhcpPoolsTool = {
|
|
|
6290
6311
|
openWorldHint: false
|
|
6291
6312
|
},
|
|
6292
6313
|
async handler(params, context) {
|
|
6293
|
-
const parsed =
|
|
6294
|
-
|
|
6314
|
+
const parsed = listIpPoolsInputSchema.parse(params);
|
|
6315
|
+
log29.info({ routerId: context.routerId }, "Listing IP pools");
|
|
6295
6316
|
try {
|
|
6296
6317
|
const allPools = await context.routerClient.get("ip/pool", {
|
|
6297
6318
|
limit: void 0,
|
|
6298
6319
|
offset: void 0
|
|
6299
6320
|
});
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6321
|
+
let filtered = allPools;
|
|
6322
|
+
if (parsed.name) {
|
|
6323
|
+
filtered = filtered.filter((p) => (p.name ?? "").includes(parsed.name));
|
|
6324
|
+
}
|
|
6325
|
+
const total = filtered.length;
|
|
6326
|
+
const pools = filtered.slice(parsed.offset, parsed.offset + parsed.limit);
|
|
6327
|
+
const hasMore = parsed.offset + parsed.limit < total;
|
|
6304
6328
|
return {
|
|
6305
|
-
content: `
|
|
6329
|
+
content: `IP pools on ${context.routerId}: ${total} total, showing ${pools.length} (offset ${parsed.offset})`,
|
|
6306
6330
|
structuredContent: {
|
|
6307
6331
|
routerId: context.routerId,
|
|
6308
6332
|
pools,
|
|
6309
|
-
total
|
|
6310
|
-
|
|
6333
|
+
total,
|
|
6334
|
+
hasMore,
|
|
6335
|
+
offset: parsed.offset,
|
|
6336
|
+
limit: parsed.limit
|
|
6311
6337
|
}
|
|
6312
6338
|
};
|
|
6313
6339
|
} catch (err) {
|
|
6314
|
-
throw enrichError(err, { routerId: context.routerId, tool: "
|
|
6340
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_ip_pools" });
|
|
6315
6341
|
}
|
|
6316
6342
|
}
|
|
6317
6343
|
};
|
|
6318
|
-
var
|
|
6319
|
-
routerId:
|
|
6320
|
-
action:
|
|
6321
|
-
name:
|
|
6322
|
-
ranges:
|
|
6323
|
-
nextPool:
|
|
6324
|
-
dryRun:
|
|
6344
|
+
var manageIpPoolInputSchema = z28.object({
|
|
6345
|
+
routerId: z28.string().describe("Target router identifier from the router registry"),
|
|
6346
|
+
action: z28.enum(["add", "remove"]).describe("Action to perform"),
|
|
6347
|
+
name: z28.string().describe("Pool name \u2014 idempotency key"),
|
|
6348
|
+
ranges: z28.string().optional().describe("IP range (e.g. '192.168.1.100-192.168.1.200'; required for add)"),
|
|
6349
|
+
nextPool: z28.string().optional().describe("Next pool name for overflow"),
|
|
6350
|
+
dryRun: z28.boolean().default(false).describe("Preview changes without applying")
|
|
6325
6351
|
}).strict();
|
|
6326
|
-
var
|
|
6327
|
-
name: "
|
|
6328
|
-
title: "Manage
|
|
6329
|
-
description: "Add or remove an IP pool. Idempotent by name: add returns already_exists if a pool with the same name and ranges already exists.",
|
|
6330
|
-
inputSchema:
|
|
6352
|
+
var manageIpPoolTool = {
|
|
6353
|
+
name: "manage_ip_pool",
|
|
6354
|
+
title: "Manage IP Pool",
|
|
6355
|
+
description: "Add or remove an IP address pool. Idempotent by name: add returns already_exists if a pool with the same name and ranges already exists.",
|
|
6356
|
+
inputSchema: manageIpPoolInputSchema,
|
|
6331
6357
|
annotations: {
|
|
6332
6358
|
readOnlyHint: false,
|
|
6333
6359
|
destructiveHint: false,
|
|
@@ -6336,25 +6362,20 @@ var manageDhcpPoolTool = {
|
|
|
6336
6362
|
},
|
|
6337
6363
|
snapshotPaths: ["ip/pool"],
|
|
6338
6364
|
async handler(params, context) {
|
|
6339
|
-
const parsed =
|
|
6340
|
-
|
|
6341
|
-
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
6342
|
-
"Managing DHCP pool"
|
|
6343
|
-
);
|
|
6365
|
+
const parsed = manageIpPoolInputSchema.parse(params);
|
|
6366
|
+
log29.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP pool");
|
|
6344
6367
|
try {
|
|
6345
6368
|
const allPools = await context.routerClient.get("ip/pool", {
|
|
6346
6369
|
limit: void 0,
|
|
6347
6370
|
offset: void 0
|
|
6348
6371
|
});
|
|
6349
|
-
const existing = allPools.find(
|
|
6350
|
-
(p) => p.name === parsed.name
|
|
6351
|
-
);
|
|
6372
|
+
const existing = allPools.find((p) => p.name === parsed.name);
|
|
6352
6373
|
if (parsed.action === "add") {
|
|
6353
6374
|
if (!parsed.ranges) {
|
|
6354
6375
|
throw new MikroMCPError({
|
|
6355
6376
|
category: "VALIDATION" /* VALIDATION */,
|
|
6356
|
-
code: "
|
|
6357
|
-
message: "ranges is required when adding
|
|
6377
|
+
code: "IP_POOL_RANGES_REQUIRED",
|
|
6378
|
+
message: "ranges is required when adding an IP pool.",
|
|
6358
6379
|
details: { name: parsed.name },
|
|
6359
6380
|
recoverability: {
|
|
6360
6381
|
retryable: false,
|
|
@@ -6365,22 +6386,19 @@ var manageDhcpPoolTool = {
|
|
|
6365
6386
|
if (existing) {
|
|
6366
6387
|
if (existing.ranges === parsed.ranges) {
|
|
6367
6388
|
return {
|
|
6368
|
-
content: `
|
|
6389
|
+
content: `IP pool "${parsed.name}" already exists with the same ranges. No changes made.`,
|
|
6369
6390
|
structuredContent: { action: "already_exists", pool: existing }
|
|
6370
6391
|
};
|
|
6371
6392
|
}
|
|
6372
6393
|
throw new MikroMCPError({
|
|
6373
6394
|
category: "CONFLICT" /* CONFLICT */,
|
|
6374
|
-
code: "
|
|
6375
|
-
message: `
|
|
6376
|
-
details: {
|
|
6377
|
-
existing: existing.ranges,
|
|
6378
|
-
requested: parsed.ranges
|
|
6379
|
-
},
|
|
6395
|
+
code: "IP_POOL_CONFLICT",
|
|
6396
|
+
message: `IP pool "${parsed.name}" exists with different ranges.`,
|
|
6397
|
+
details: { existing: existing.ranges, requested: parsed.ranges },
|
|
6380
6398
|
recoverability: {
|
|
6381
6399
|
retryable: false,
|
|
6382
6400
|
suggestedAction: "Remove the existing pool first or use a different name.",
|
|
6383
|
-
alternativeTools: ["
|
|
6401
|
+
alternativeTools: ["manage_ip_pool"]
|
|
6384
6402
|
}
|
|
6385
6403
|
});
|
|
6386
6404
|
}
|
|
@@ -6391,63 +6409,52 @@ var manageDhcpPoolTool = {
|
|
|
6391
6409
|
...parsed.nextPool ? [{ property: "next-pool", before: null, after: parsed.nextPool }] : []
|
|
6392
6410
|
];
|
|
6393
6411
|
return {
|
|
6394
|
-
content: `Dry run: Would add
|
|
6412
|
+
content: `Dry run: Would add IP pool "${parsed.name}" with ranges ${parsed.ranges}.`,
|
|
6395
6413
|
structuredContent: { action: "dry_run", diff }
|
|
6396
6414
|
};
|
|
6397
6415
|
}
|
|
6398
|
-
const body = {
|
|
6399
|
-
name: parsed.name,
|
|
6400
|
-
ranges: parsed.ranges
|
|
6401
|
-
};
|
|
6416
|
+
const body = { name: parsed.name, ranges: parsed.ranges };
|
|
6402
6417
|
if (parsed.nextPool) body["next-pool"] = parsed.nextPool;
|
|
6403
6418
|
const created = await context.routerClient.create("ip/pool", body);
|
|
6404
|
-
|
|
6419
|
+
log29.info({ name: parsed.name, id: created[".id"] }, "IP pool added");
|
|
6405
6420
|
return {
|
|
6406
|
-
content: `Added
|
|
6421
|
+
content: `Added IP pool "${parsed.name}" with ranges ${parsed.ranges}.`,
|
|
6407
6422
|
structuredContent: { action: "created", pool: created }
|
|
6408
6423
|
};
|
|
6409
6424
|
}
|
|
6410
6425
|
if (!existing) {
|
|
6411
6426
|
return {
|
|
6412
|
-
content: `
|
|
6427
|
+
content: `IP pool "${parsed.name}" not found. Nothing to remove.`,
|
|
6413
6428
|
structuredContent: { action: "not_found", name: parsed.name }
|
|
6414
6429
|
};
|
|
6415
6430
|
}
|
|
6416
6431
|
if (parsed.dryRun) {
|
|
6417
6432
|
return {
|
|
6418
|
-
content: `Dry run: Would remove
|
|
6419
|
-
structuredContent: {
|
|
6420
|
-
action: "dry_run",
|
|
6421
|
-
diff: [{ property: "name", before: parsed.name, after: null }]
|
|
6422
|
-
}
|
|
6433
|
+
content: `Dry run: Would remove IP pool "${parsed.name}".`,
|
|
6434
|
+
structuredContent: { action: "dry_run", diff: [{ property: "name", before: parsed.name, after: null }] }
|
|
6423
6435
|
};
|
|
6424
6436
|
}
|
|
6425
6437
|
await context.routerClient.remove("ip/pool", existing[".id"]);
|
|
6426
|
-
|
|
6438
|
+
log29.info({ name: parsed.name }, "IP pool removed");
|
|
6427
6439
|
return {
|
|
6428
|
-
content: `Removed
|
|
6440
|
+
content: `Removed IP pool "${parsed.name}".`,
|
|
6429
6441
|
structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
|
|
6430
6442
|
};
|
|
6431
6443
|
} catch (err) {
|
|
6432
6444
|
if (err instanceof MikroMCPError) throw err;
|
|
6433
|
-
throw enrichError(err, { routerId: context.routerId, tool: "
|
|
6445
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_ip_pool" });
|
|
6434
6446
|
}
|
|
6435
6447
|
}
|
|
6436
6448
|
};
|
|
6437
|
-
var
|
|
6438
|
-
listDhcpServersTool,
|
|
6439
|
-
manageDhcpServerTool,
|
|
6440
|
-
listDhcpPoolsTool,
|
|
6441
|
-
manageDhcpPoolTool
|
|
6442
|
-
];
|
|
6449
|
+
var ipPoolTools = [listIpPoolsTool, manageIpPoolTool];
|
|
6443
6450
|
|
|
6444
6451
|
// src/domain/tools/queue-tools.ts
|
|
6445
|
-
import { z as
|
|
6446
|
-
var
|
|
6447
|
-
var listQueuesInputSchema =
|
|
6448
|
-
routerId:
|
|
6449
|
-
target:
|
|
6450
|
-
limit:
|
|
6452
|
+
import { z as z29 } from "zod";
|
|
6453
|
+
var log30 = createLogger("queue-tools");
|
|
6454
|
+
var listQueuesInputSchema = z29.object({
|
|
6455
|
+
routerId: z29.string().describe("Target router identifier from the router registry"),
|
|
6456
|
+
target: z29.string().optional().describe("Filter by target address (substring match)"),
|
|
6457
|
+
limit: z29.number().int().min(1).max(500).default(100).describe("Maximum number of queues to return")
|
|
6451
6458
|
}).strict();
|
|
6452
6459
|
var listQueuesTool = {
|
|
6453
6460
|
name: "list_queues",
|
|
@@ -6462,7 +6469,7 @@ var listQueuesTool = {
|
|
|
6462
6469
|
},
|
|
6463
6470
|
async handler(params, context) {
|
|
6464
6471
|
const parsed = listQueuesInputSchema.parse(params);
|
|
6465
|
-
|
|
6472
|
+
log30.info({ routerId: context.routerId }, "Listing queues");
|
|
6466
6473
|
try {
|
|
6467
6474
|
const allQueues = await context.routerClient.get("queue/simple", {
|
|
6468
6475
|
limit: void 0,
|
|
@@ -6486,15 +6493,15 @@ var listQueuesTool = {
|
|
|
6486
6493
|
}
|
|
6487
6494
|
}
|
|
6488
6495
|
};
|
|
6489
|
-
var manageQueueInputSchema =
|
|
6490
|
-
routerId:
|
|
6491
|
-
action:
|
|
6492
|
-
name:
|
|
6493
|
-
target:
|
|
6494
|
-
maxLimit:
|
|
6495
|
-
limitAt:
|
|
6496
|
-
comment:
|
|
6497
|
-
dryRun:
|
|
6496
|
+
var manageQueueInputSchema = z29.object({
|
|
6497
|
+
routerId: z29.string().describe("Target router identifier from the router registry"),
|
|
6498
|
+
action: z29.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
|
|
6499
|
+
name: z29.string().describe("Queue name \u2014 idempotency key"),
|
|
6500
|
+
target: z29.string().optional().describe("Target address (required for add; e.g. '192.168.1.0/24')"),
|
|
6501
|
+
maxLimit: z29.string().optional().describe("Max upload/download limit (e.g. '10M/10M')"),
|
|
6502
|
+
limitAt: z29.string().optional().describe("Guaranteed rate (e.g. '1M/1M')"),
|
|
6503
|
+
comment: z29.string().optional().describe("Optional comment"),
|
|
6504
|
+
dryRun: z29.boolean().default(false).describe("Preview changes without applying")
|
|
6498
6505
|
}).strict();
|
|
6499
6506
|
var manageQueueTool = {
|
|
6500
6507
|
name: "manage_queue",
|
|
@@ -6510,7 +6517,7 @@ var manageQueueTool = {
|
|
|
6510
6517
|
snapshotPaths: ["queue/simple"],
|
|
6511
6518
|
async handler(params, context) {
|
|
6512
6519
|
const parsed = manageQueueInputSchema.parse(params);
|
|
6513
|
-
|
|
6520
|
+
log30.info(
|
|
6514
6521
|
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
6515
6522
|
"Managing queue"
|
|
6516
6523
|
);
|
|
@@ -6572,7 +6579,7 @@ var manageQueueTool = {
|
|
|
6572
6579
|
if (parsed.limitAt) body["limit-at"] = parsed.limitAt;
|
|
6573
6580
|
if (parsed.comment) body.comment = parsed.comment;
|
|
6574
6581
|
const created = await context.routerClient.create("queue/simple", body);
|
|
6575
|
-
|
|
6582
|
+
log30.info({ name: parsed.name, id: created[".id"] }, "Queue added");
|
|
6576
6583
|
return {
|
|
6577
6584
|
content: `Added queue "${parsed.name}".`,
|
|
6578
6585
|
structuredContent: { action: "created", queue: created }
|
|
@@ -6595,7 +6602,7 @@ var manageQueueTool = {
|
|
|
6595
6602
|
};
|
|
6596
6603
|
}
|
|
6597
6604
|
await context.routerClient.remove("queue/simple", existing[".id"]);
|
|
6598
|
-
|
|
6605
|
+
log30.info({ name: parsed.name }, "Queue removed");
|
|
6599
6606
|
return {
|
|
6600
6607
|
content: `Removed queue "${parsed.name}".`,
|
|
6601
6608
|
structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
|
|
@@ -6634,7 +6641,7 @@ var manageQueueTool = {
|
|
|
6634
6641
|
disabled: disabledValue
|
|
6635
6642
|
});
|
|
6636
6643
|
const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
|
|
6637
|
-
|
|
6644
|
+
log30.info({ name: parsed.name, action: resultAction }, "Queue updated");
|
|
6638
6645
|
return {
|
|
6639
6646
|
content: `Queue "${parsed.name}" ${resultAction}.`,
|
|
6640
6647
|
structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
|
|
@@ -6648,12 +6655,12 @@ var manageQueueTool = {
|
|
|
6648
6655
|
var queueTools = [listQueuesTool, manageQueueTool];
|
|
6649
6656
|
|
|
6650
6657
|
// src/domain/tools/vrrp-tools.ts
|
|
6651
|
-
import { z as
|
|
6652
|
-
var
|
|
6653
|
-
var listVrrpInstancesInputSchema =
|
|
6654
|
-
routerId:
|
|
6655
|
-
interface:
|
|
6656
|
-
limit:
|
|
6658
|
+
import { z as z30 } from "zod";
|
|
6659
|
+
var log31 = createLogger("vrrp-tools");
|
|
6660
|
+
var listVrrpInstancesInputSchema = z30.object({
|
|
6661
|
+
routerId: z30.string().describe("Target router identifier from the router registry"),
|
|
6662
|
+
interface: z30.string().optional().describe("Filter by master interface name (exact match)"),
|
|
6663
|
+
limit: z30.number().int().min(1).max(500).default(100).describe("Maximum number of instances to return")
|
|
6657
6664
|
}).strict();
|
|
6658
6665
|
var listVrrpInstancesTool = {
|
|
6659
6666
|
name: "list_vrrp_instances",
|
|
@@ -6668,7 +6675,7 @@ var listVrrpInstancesTool = {
|
|
|
6668
6675
|
},
|
|
6669
6676
|
async handler(params, context) {
|
|
6670
6677
|
const parsed = listVrrpInstancesInputSchema.parse(params);
|
|
6671
|
-
|
|
6678
|
+
log31.info({ routerId: context.routerId }, "Listing VRRP instances");
|
|
6672
6679
|
try {
|
|
6673
6680
|
const allInstances = await context.routerClient.get("interface/vrrp", {
|
|
6674
6681
|
limit: void 0,
|
|
@@ -6692,17 +6699,17 @@ var listVrrpInstancesTool = {
|
|
|
6692
6699
|
}
|
|
6693
6700
|
}
|
|
6694
6701
|
};
|
|
6695
|
-
var manageVrrpInstanceInputSchema =
|
|
6696
|
-
routerId:
|
|
6697
|
-
action:
|
|
6698
|
-
name:
|
|
6699
|
-
interface:
|
|
6700
|
-
vrid:
|
|
6701
|
-
priority:
|
|
6702
|
-
interval:
|
|
6703
|
-
version:
|
|
6704
|
-
comment:
|
|
6705
|
-
dryRun:
|
|
6702
|
+
var manageVrrpInstanceInputSchema = z30.object({
|
|
6703
|
+
routerId: z30.string().describe("Target router identifier from the router registry"),
|
|
6704
|
+
action: z30.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
|
|
6705
|
+
name: z30.string().describe("VRRP interface name \u2014 idempotency key"),
|
|
6706
|
+
interface: z30.string().optional().describe("Master interface (required for add)"),
|
|
6707
|
+
vrid: z30.number().int().min(1).max(255).optional().describe("Virtual router ID (required for add)"),
|
|
6708
|
+
priority: z30.number().int().min(1).max(254).default(100).describe("Router priority (1\u2013254)"),
|
|
6709
|
+
interval: z30.number().int().min(1).optional().describe("Advertisement interval in seconds"),
|
|
6710
|
+
version: z30.enum(["2", "3"]).default("3").describe("VRRP protocol version"),
|
|
6711
|
+
comment: z30.string().optional().describe("Optional comment"),
|
|
6712
|
+
dryRun: z30.boolean().default(false).describe("Preview changes without applying")
|
|
6706
6713
|
}).strict();
|
|
6707
6714
|
var manageVrrpInstanceTool = {
|
|
6708
6715
|
name: "manage_vrrp_instance",
|
|
@@ -6718,7 +6725,7 @@ var manageVrrpInstanceTool = {
|
|
|
6718
6725
|
snapshotPaths: ["interface/vrrp"],
|
|
6719
6726
|
async handler(params, context) {
|
|
6720
6727
|
const parsed = manageVrrpInstanceInputSchema.parse(params);
|
|
6721
|
-
|
|
6728
|
+
log31.info(
|
|
6722
6729
|
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
6723
6730
|
"Managing VRRP instance"
|
|
6724
6731
|
);
|
|
@@ -6802,7 +6809,7 @@ var manageVrrpInstanceTool = {
|
|
|
6802
6809
|
if (parsed.interval) body.interval = String(parsed.interval);
|
|
6803
6810
|
if (parsed.comment) body.comment = parsed.comment;
|
|
6804
6811
|
const created = await context.routerClient.create("interface/vrrp", body);
|
|
6805
|
-
|
|
6812
|
+
log31.info({ name: parsed.name, id: created[".id"] }, "VRRP instance added");
|
|
6806
6813
|
return {
|
|
6807
6814
|
content: `Added VRRP instance "${parsed.name}".`,
|
|
6808
6815
|
structuredContent: { action: "created", instance: created }
|
|
@@ -6825,7 +6832,7 @@ var manageVrrpInstanceTool = {
|
|
|
6825
6832
|
};
|
|
6826
6833
|
}
|
|
6827
6834
|
await context.routerClient.remove("interface/vrrp", existing[".id"]);
|
|
6828
|
-
|
|
6835
|
+
log31.info({ name: parsed.name }, "VRRP instance removed");
|
|
6829
6836
|
return {
|
|
6830
6837
|
content: `Removed VRRP instance "${parsed.name}".`,
|
|
6831
6838
|
structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
|
|
@@ -6864,7 +6871,7 @@ var manageVrrpInstanceTool = {
|
|
|
6864
6871
|
disabled: disabledValue
|
|
6865
6872
|
});
|
|
6866
6873
|
const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
|
|
6867
|
-
|
|
6874
|
+
log31.info({ name: parsed.name, action: resultAction }, "VRRP instance updated");
|
|
6868
6875
|
return {
|
|
6869
6876
|
content: `VRRP instance "${parsed.name}" ${resultAction}.`,
|
|
6870
6877
|
structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
|
|
@@ -6878,10 +6885,10 @@ var manageVrrpInstanceTool = {
|
|
|
6878
6885
|
var vrrpTools = [listVrrpInstancesTool, manageVrrpInstanceTool];
|
|
6879
6886
|
|
|
6880
6887
|
// src/domain/tools/network-services-tools.ts
|
|
6881
|
-
import { z as
|
|
6882
|
-
var
|
|
6883
|
-
var getSnmpSettingsInputSchema =
|
|
6884
|
-
routerId:
|
|
6888
|
+
import { z as z31 } from "zod";
|
|
6889
|
+
var log32 = createLogger("network-services-tools");
|
|
6890
|
+
var getSnmpSettingsInputSchema = z31.object({
|
|
6891
|
+
routerId: z31.string().describe("Target router identifier from the router registry")
|
|
6885
6892
|
}).strict();
|
|
6886
6893
|
var getSnmpSettingsTool = {
|
|
6887
6894
|
name: "get_snmp_settings",
|
|
@@ -6896,7 +6903,7 @@ var getSnmpSettingsTool = {
|
|
|
6896
6903
|
},
|
|
6897
6904
|
async handler(params, context) {
|
|
6898
6905
|
const parsed = getSnmpSettingsInputSchema.parse(params);
|
|
6899
|
-
|
|
6906
|
+
log32.info({ routerId: context.routerId }, "Fetching SNMP settings");
|
|
6900
6907
|
try {
|
|
6901
6908
|
const results2 = await context.routerClient.get("snmp");
|
|
6902
6909
|
if (!results2 || results2.length === 0) {
|
|
@@ -6925,8 +6932,8 @@ var getSnmpSettingsTool = {
|
|
|
6925
6932
|
}
|
|
6926
6933
|
}
|
|
6927
6934
|
};
|
|
6928
|
-
var getNtpSettingsInputSchema =
|
|
6929
|
-
routerId:
|
|
6935
|
+
var getNtpSettingsInputSchema = z31.object({
|
|
6936
|
+
routerId: z31.string().describe("Target router identifier from the router registry")
|
|
6930
6937
|
}).strict();
|
|
6931
6938
|
var getNtpSettingsTool = {
|
|
6932
6939
|
name: "get_ntp_settings",
|
|
@@ -6941,7 +6948,7 @@ var getNtpSettingsTool = {
|
|
|
6941
6948
|
},
|
|
6942
6949
|
async handler(params, context) {
|
|
6943
6950
|
const parsed = getNtpSettingsInputSchema.parse(params);
|
|
6944
|
-
|
|
6951
|
+
log32.info({ routerId: context.routerId }, "Fetching NTP settings");
|
|
6945
6952
|
try {
|
|
6946
6953
|
const results2 = await context.routerClient.get("system/ntp/client");
|
|
6947
6954
|
if (!results2 || results2.length === 0) {
|
|
@@ -6970,11 +6977,11 @@ var getNtpSettingsTool = {
|
|
|
6970
6977
|
}
|
|
6971
6978
|
}
|
|
6972
6979
|
};
|
|
6973
|
-
var listNetwatchEntriesInputSchema =
|
|
6974
|
-
routerId:
|
|
6975
|
-
host:
|
|
6976
|
-
status:
|
|
6977
|
-
limit:
|
|
6980
|
+
var listNetwatchEntriesInputSchema = z31.object({
|
|
6981
|
+
routerId: z31.string().describe("Target router identifier from the router registry"),
|
|
6982
|
+
host: z31.string().optional().describe("Filter by host (substring match)"),
|
|
6983
|
+
status: z31.enum(["up", "down", "unknown"]).optional().describe("Filter by current status"),
|
|
6984
|
+
limit: z31.number().int().min(1).max(500).default(100).describe("Maximum number of entries to return")
|
|
6978
6985
|
}).strict();
|
|
6979
6986
|
var listNetwatchEntriesTool = {
|
|
6980
6987
|
name: "list_netwatch_entries",
|
|
@@ -6989,7 +6996,7 @@ var listNetwatchEntriesTool = {
|
|
|
6989
6996
|
},
|
|
6990
6997
|
async handler(params, context) {
|
|
6991
6998
|
const parsed = listNetwatchEntriesInputSchema.parse(params);
|
|
6992
|
-
|
|
6999
|
+
log32.info({ routerId: context.routerId }, "Listing Netwatch entries");
|
|
6993
7000
|
try {
|
|
6994
7001
|
const allEntries = await context.routerClient.get("tool/netwatch", {
|
|
6995
7002
|
limit: void 0,
|
|
@@ -7011,15 +7018,15 @@ var listNetwatchEntriesTool = {
|
|
|
7011
7018
|
}
|
|
7012
7019
|
}
|
|
7013
7020
|
};
|
|
7014
|
-
var manageNetwatchEntryInputSchema =
|
|
7015
|
-
routerId:
|
|
7016
|
-
action:
|
|
7017
|
-
host:
|
|
7018
|
-
port:
|
|
7019
|
-
interval:
|
|
7020
|
-
timeout:
|
|
7021
|
-
comment:
|
|
7022
|
-
dryRun:
|
|
7021
|
+
var manageNetwatchEntryInputSchema = z31.object({
|
|
7022
|
+
routerId: z31.string().describe("Target router identifier from the router registry"),
|
|
7023
|
+
action: z31.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
|
|
7024
|
+
host: z31.string().describe("Host to monitor \u2014 idempotency key"),
|
|
7025
|
+
port: z31.number().int().min(1).max(65535).optional().describe("TCP port (optional; ICMP if omitted)"),
|
|
7026
|
+
interval: z31.string().optional().describe("Check interval (e.g. '1m'; default '1m')"),
|
|
7027
|
+
timeout: z31.string().optional().describe("Probe timeout (e.g. '500ms')"),
|
|
7028
|
+
comment: z31.string().optional().describe("Optional comment"),
|
|
7029
|
+
dryRun: z31.boolean().default(false).describe("Preview changes without applying")
|
|
7023
7030
|
}).strict();
|
|
7024
7031
|
var manageNetwatchEntryTool = {
|
|
7025
7032
|
name: "manage_netwatch_entry",
|
|
@@ -7035,7 +7042,7 @@ var manageNetwatchEntryTool = {
|
|
|
7035
7042
|
snapshotPaths: ["tool/netwatch"],
|
|
7036
7043
|
async handler(params, context) {
|
|
7037
7044
|
const parsed = manageNetwatchEntryInputSchema.parse(params);
|
|
7038
|
-
|
|
7045
|
+
log32.info(
|
|
7039
7046
|
{ routerId: context.routerId, action: parsed.action, host: parsed.host },
|
|
7040
7047
|
"Managing Netwatch entry"
|
|
7041
7048
|
);
|
|
@@ -7092,7 +7099,7 @@ var manageNetwatchEntryTool = {
|
|
|
7092
7099
|
if (parsed.timeout) body.timeout = parsed.timeout;
|
|
7093
7100
|
if (parsed.comment) body.comment = parsed.comment;
|
|
7094
7101
|
const created = await context.routerClient.create("tool/netwatch", body);
|
|
7095
|
-
|
|
7102
|
+
log32.info({ host: parsed.host, id: created[".id"] }, "Netwatch entry added");
|
|
7096
7103
|
return {
|
|
7097
7104
|
content: `Added Netwatch entry for "${parsed.host}".`,
|
|
7098
7105
|
structuredContent: { action: "created", entry: created }
|
|
@@ -7116,7 +7123,7 @@ var manageNetwatchEntryTool = {
|
|
|
7116
7123
|
};
|
|
7117
7124
|
}
|
|
7118
7125
|
await context.routerClient.remove("tool/netwatch", existing2[".id"]);
|
|
7119
|
-
|
|
7126
|
+
log32.info({ host: parsed.host }, "Netwatch entry removed");
|
|
7120
7127
|
return {
|
|
7121
7128
|
content: `Removed Netwatch entry for "${parsed.host}".`,
|
|
7122
7129
|
structuredContent: { action: "removed", host: parsed.host, id: existing2[".id"] }
|
|
@@ -7156,7 +7163,7 @@ var manageNetwatchEntryTool = {
|
|
|
7156
7163
|
disabled: disabledValue
|
|
7157
7164
|
});
|
|
7158
7165
|
const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
|
|
7159
|
-
|
|
7166
|
+
log32.info({ host: parsed.host, action: resultAction }, "Netwatch entry updated");
|
|
7160
7167
|
return {
|
|
7161
7168
|
content: `Netwatch entry for "${parsed.host}" ${resultAction}.`,
|
|
7162
7169
|
structuredContent: { action: resultAction, host: parsed.host, id: existing[".id"] }
|
|
@@ -7167,10 +7174,10 @@ var manageNetwatchEntryTool = {
|
|
|
7167
7174
|
}
|
|
7168
7175
|
}
|
|
7169
7176
|
};
|
|
7170
|
-
var listNeighborsInputSchema =
|
|
7171
|
-
routerId:
|
|
7172
|
-
interface:
|
|
7173
|
-
limit:
|
|
7177
|
+
var listNeighborsInputSchema = z31.object({
|
|
7178
|
+
routerId: z31.string().describe("Target router identifier from the router registry"),
|
|
7179
|
+
interface: z31.string().optional().describe("Filter by interface name (substring match)"),
|
|
7180
|
+
limit: z31.number().int().min(1).max(500).default(100).describe("Maximum number of neighbors to return")
|
|
7174
7181
|
}).strict();
|
|
7175
7182
|
var listNeighborsTool = {
|
|
7176
7183
|
name: "list_neighbors",
|
|
@@ -7185,7 +7192,7 @@ var listNeighborsTool = {
|
|
|
7185
7192
|
},
|
|
7186
7193
|
async handler(params, context) {
|
|
7187
7194
|
const parsed = listNeighborsInputSchema.parse(params);
|
|
7188
|
-
|
|
7195
|
+
log32.info({ routerId: context.routerId }, "Listing neighbors");
|
|
7189
7196
|
try {
|
|
7190
7197
|
const allNeighbors = await context.routerClient.get("ip/neighbor", {
|
|
7191
7198
|
limit: void 0,
|
|
@@ -7209,12 +7216,12 @@ var listNeighborsTool = {
|
|
|
7209
7216
|
}
|
|
7210
7217
|
}
|
|
7211
7218
|
};
|
|
7212
|
-
var listArpEntriesInputSchema =
|
|
7213
|
-
routerId:
|
|
7214
|
-
interface:
|
|
7215
|
-
address:
|
|
7216
|
-
macAddress:
|
|
7217
|
-
limit:
|
|
7219
|
+
var listArpEntriesInputSchema = z31.object({
|
|
7220
|
+
routerId: z31.string().describe("Target router identifier from the router registry"),
|
|
7221
|
+
interface: z31.string().optional().describe("Filter by interface (substring match)"),
|
|
7222
|
+
address: z31.string().optional().describe("Filter by IP address (substring match)"),
|
|
7223
|
+
macAddress: z31.string().optional().describe("Filter by MAC address (substring match)"),
|
|
7224
|
+
limit: z31.number().int().min(1).max(500).default(100).describe("Maximum number of ARP entries to return")
|
|
7218
7225
|
}).strict();
|
|
7219
7226
|
var listArpEntriesTool = {
|
|
7220
7227
|
name: "list_arp_entries",
|
|
@@ -7229,7 +7236,7 @@ var listArpEntriesTool = {
|
|
|
7229
7236
|
},
|
|
7230
7237
|
async handler(params, context) {
|
|
7231
7238
|
const parsed = listArpEntriesInputSchema.parse(params);
|
|
7232
|
-
|
|
7239
|
+
log32.info({ routerId: context.routerId }, "Listing ARP entries");
|
|
7233
7240
|
try {
|
|
7234
7241
|
const allEntries = await context.routerClient.get("ip/arp", {
|
|
7235
7242
|
limit: void 0,
|
|
@@ -7262,47 +7269,1253 @@ var networkServicesTools = [
|
|
|
7262
7269
|
listArpEntriesTool
|
|
7263
7270
|
];
|
|
7264
7271
|
|
|
7265
|
-
// src/domain/tools/
|
|
7266
|
-
import { z as
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
const prefix = pattern.slice(0, pattern.indexOf("*"));
|
|
7272
|
-
return name.startsWith(prefix);
|
|
7272
|
+
// src/domain/tools/vlan-tools.ts
|
|
7273
|
+
import { z as z32 } from "zod";
|
|
7274
|
+
var log33 = createLogger("vlan-tools");
|
|
7275
|
+
function sanitizeComment4(comment) {
|
|
7276
|
+
if (comment === void 0) return void 0;
|
|
7277
|
+
return comment.replace(/[\x00-\x1f\x7f]/g, "");
|
|
7273
7278
|
}
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7279
|
+
var manageVlanInputSchema = z32.object({
|
|
7280
|
+
routerId: z32.string().describe("Target router identifier from the router registry"),
|
|
7281
|
+
action: z32.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
|
|
7282
|
+
name: z32.string().regex(/^[a-zA-Z0-9_-]+$/).max(15).describe("VLAN interface name \u2014 idempotency key (alphanumeric, hyphens, underscores, max 15 chars)"),
|
|
7283
|
+
vlanId: z32.number().int().min(1).max(4094).optional().describe("VLAN ID (1-4094; required for add)"),
|
|
7284
|
+
parentInterface: z32.string().optional().describe("Parent interface name (e.g., ether1, bridge1; required for add)"),
|
|
7285
|
+
mtu: z32.number().int().min(68).max(9e3).default(1500).describe("MTU size (add only)"),
|
|
7286
|
+
disabled: z32.boolean().default(false).describe("Whether to create the VLAN disabled (add only)"),
|
|
7287
|
+
comment: z32.string().max(255).optional().describe("Optional comment (add only)"),
|
|
7288
|
+
dryRun: z32.boolean().default(false).describe("Preview changes without applying")
|
|
7289
|
+
}).strict();
|
|
7290
|
+
var manageVlanTool = {
|
|
7291
|
+
name: "manage_vlan",
|
|
7292
|
+
title: "Manage VLAN Interface",
|
|
7293
|
+
description: "Add, remove, enable, or disable a VLAN interface. Idempotent by name: add returns already_exists when a VLAN with matching name, vlan-id, and parent interface exists. Supports dry-run mode.",
|
|
7294
|
+
inputSchema: manageVlanInputSchema,
|
|
7295
|
+
annotations: {
|
|
7296
|
+
readOnlyHint: false,
|
|
7297
|
+
destructiveHint: true,
|
|
7298
|
+
idempotentHint: true,
|
|
7299
|
+
openWorldHint: false
|
|
7300
|
+
},
|
|
7301
|
+
snapshotPaths: ["interface/vlan"],
|
|
7302
|
+
async handler(params, context) {
|
|
7303
|
+
const parsed = manageVlanInputSchema.parse(params);
|
|
7304
|
+
const comment = sanitizeComment4(parsed.comment);
|
|
7305
|
+
log33.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing VLAN");
|
|
7306
|
+
try {
|
|
7307
|
+
const allVlans = await context.routerClient.get("interface/vlan", {
|
|
7308
|
+
limit: void 0,
|
|
7309
|
+
offset: void 0
|
|
7299
7310
|
});
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7311
|
+
const existing = allVlans.find((v) => v.name === parsed.name);
|
|
7312
|
+
if (parsed.action === "add") {
|
|
7313
|
+
if (parsed.vlanId === void 0) {
|
|
7314
|
+
throw new MikroMCPError({
|
|
7315
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
7316
|
+
code: "VLAN_ID_REQUIRED",
|
|
7317
|
+
message: "vlanId is required when adding a VLAN.",
|
|
7318
|
+
details: { name: parsed.name },
|
|
7319
|
+
recoverability: { retryable: false, suggestedAction: "Provide vlanId (1-4094)." }
|
|
7320
|
+
});
|
|
7321
|
+
}
|
|
7322
|
+
if (!parsed.parentInterface) {
|
|
7323
|
+
throw new MikroMCPError({
|
|
7324
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
7325
|
+
code: "VLAN_PARENT_REQUIRED",
|
|
7326
|
+
message: "parentInterface is required when adding a VLAN.",
|
|
7327
|
+
details: { name: parsed.name },
|
|
7328
|
+
recoverability: { retryable: false, suggestedAction: "Provide the parent interface name." }
|
|
7329
|
+
});
|
|
7330
|
+
}
|
|
7331
|
+
if (existing) {
|
|
7332
|
+
if (existing["vlan-id"] === String(parsed.vlanId) && existing.interface === parsed.parentInterface) {
|
|
7333
|
+
log33.info({ name: parsed.name }, "VLAN already exists with matching config");
|
|
7334
|
+
return {
|
|
7335
|
+
content: `VLAN "${parsed.name}" already exists with VLAN ID ${parsed.vlanId} on ${parsed.parentInterface}. No changes made.`,
|
|
7336
|
+
structuredContent: { action: "already_exists", vlan: existing }
|
|
7337
|
+
};
|
|
7338
|
+
}
|
|
7339
|
+
throw new MikroMCPError({
|
|
7340
|
+
category: "CONFLICT" /* CONFLICT */,
|
|
7341
|
+
code: "VLAN_NAME_CONFLICT",
|
|
7342
|
+
message: `VLAN "${parsed.name}" already exists but with different configuration (vlan-id=${existing["vlan-id"]}, interface=${existing.interface}).`,
|
|
7343
|
+
details: {
|
|
7344
|
+
existing: { vlanId: existing["vlan-id"], interface: existing.interface },
|
|
7345
|
+
requested: { vlanId: parsed.vlanId, interface: parsed.parentInterface }
|
|
7346
|
+
},
|
|
7347
|
+
recoverability: {
|
|
7348
|
+
retryable: false,
|
|
7349
|
+
suggestedAction: "Use a different name or remove the existing VLAN first.",
|
|
7350
|
+
alternativeTools: ["manage_vlan"]
|
|
7351
|
+
}
|
|
7352
|
+
});
|
|
7353
|
+
}
|
|
7354
|
+
if (parsed.dryRun) {
|
|
7355
|
+
const diff = [
|
|
7356
|
+
{ property: "name", before: null, after: parsed.name },
|
|
7357
|
+
{ property: "vlan-id", before: null, after: String(parsed.vlanId) },
|
|
7358
|
+
{ property: "interface", before: null, after: parsed.parentInterface },
|
|
7359
|
+
{ property: "mtu", before: null, after: String(parsed.mtu) },
|
|
7360
|
+
{ property: "disabled", before: null, after: parsed.disabled ? "true" : "false" },
|
|
7361
|
+
...comment ? [{ property: "comment", before: null, after: comment }] : []
|
|
7362
|
+
];
|
|
7363
|
+
return {
|
|
7364
|
+
content: `Dry run: Would create VLAN "${parsed.name}" (ID ${parsed.vlanId}) on ${parsed.parentInterface} with MTU ${parsed.mtu}.`,
|
|
7365
|
+
structuredContent: { action: "dry_run", diff }
|
|
7366
|
+
};
|
|
7367
|
+
}
|
|
7368
|
+
const body = {
|
|
7369
|
+
name: parsed.name,
|
|
7370
|
+
"vlan-id": String(parsed.vlanId),
|
|
7371
|
+
interface: parsed.parentInterface,
|
|
7372
|
+
mtu: String(parsed.mtu),
|
|
7373
|
+
disabled: parsed.disabled ? "true" : "false"
|
|
7374
|
+
};
|
|
7375
|
+
if (comment) body.comment = comment;
|
|
7376
|
+
const created = await context.routerClient.create("interface/vlan", body);
|
|
7377
|
+
log33.info({ name: parsed.name, id: created[".id"] }, "VLAN created");
|
|
7378
|
+
return {
|
|
7379
|
+
content: `Created VLAN "${parsed.name}" (ID ${parsed.vlanId}) on ${parsed.parentInterface} with MTU ${parsed.mtu}.`,
|
|
7380
|
+
structuredContent: { action: "created", vlan: created }
|
|
7381
|
+
};
|
|
7382
|
+
}
|
|
7383
|
+
if (parsed.action === "remove") {
|
|
7384
|
+
if (!existing) {
|
|
7385
|
+
return {
|
|
7386
|
+
content: `VLAN "${parsed.name}" not found. Nothing to remove.`,
|
|
7387
|
+
structuredContent: { action: "not_found", name: parsed.name }
|
|
7388
|
+
};
|
|
7389
|
+
}
|
|
7390
|
+
if (parsed.dryRun) {
|
|
7391
|
+
return {
|
|
7392
|
+
content: `Dry run: Would remove VLAN "${parsed.name}".`,
|
|
7393
|
+
structuredContent: { action: "dry_run", diff: [{ property: "name", before: parsed.name, after: null }] }
|
|
7394
|
+
};
|
|
7395
|
+
}
|
|
7396
|
+
await context.routerClient.remove("interface/vlan", existing[".id"]);
|
|
7397
|
+
log33.info({ name: parsed.name }, "VLAN removed");
|
|
7398
|
+
return {
|
|
7399
|
+
content: `Removed VLAN "${parsed.name}".`,
|
|
7400
|
+
structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
|
|
7401
|
+
};
|
|
7402
|
+
}
|
|
7403
|
+
if (!existing) {
|
|
7404
|
+
throw new MikroMCPError({
|
|
7405
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
7406
|
+
code: "VLAN_NOT_FOUND",
|
|
7407
|
+
message: `VLAN "${parsed.name}" not found.`,
|
|
7408
|
+
details: { name: parsed.name },
|
|
7409
|
+
recoverability: {
|
|
7410
|
+
retryable: false,
|
|
7411
|
+
suggestedAction: "Verify the VLAN name with list_interfaces.",
|
|
7412
|
+
alternativeTools: ["list_interfaces"]
|
|
7413
|
+
}
|
|
7414
|
+
});
|
|
7415
|
+
}
|
|
7416
|
+
if (parsed.dryRun) {
|
|
7417
|
+
return {
|
|
7418
|
+
content: `Dry run: Would ${parsed.action} VLAN "${parsed.name}".`,
|
|
7419
|
+
structuredContent: {
|
|
7420
|
+
action: "dry_run",
|
|
7421
|
+
diff: [{ property: "disabled", before: existing.disabled, after: parsed.action === "disable" ? "true" : "false" }]
|
|
7422
|
+
}
|
|
7423
|
+
};
|
|
7424
|
+
}
|
|
7425
|
+
const disabledValue = parsed.action === "disable" ? "true" : "false";
|
|
7426
|
+
await context.routerClient.update("interface/vlan", existing[".id"], { disabled: disabledValue });
|
|
7427
|
+
const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
|
|
7428
|
+
log33.info({ name: parsed.name, action: resultAction }, "VLAN updated");
|
|
7429
|
+
return {
|
|
7430
|
+
content: `VLAN "${parsed.name}" ${resultAction}.`,
|
|
7431
|
+
structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
|
|
7432
|
+
};
|
|
7433
|
+
} catch (err) {
|
|
7434
|
+
if (err instanceof MikroMCPError) throw err;
|
|
7435
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_vlan" });
|
|
7436
|
+
}
|
|
7437
|
+
}
|
|
7438
|
+
};
|
|
7439
|
+
var vlanTools = [manageVlanTool];
|
|
7440
|
+
|
|
7441
|
+
// src/domain/tools/dhcp-client-tools.ts
|
|
7442
|
+
import { z as z33 } from "zod";
|
|
7443
|
+
var log34 = createLogger("dhcp-client-tools");
|
|
7444
|
+
var listDhcpClientsInputSchema = z33.object({
|
|
7445
|
+
routerId: z33.string().describe("Target router identifier from the router registry"),
|
|
7446
|
+
interface: z33.string().optional().describe("Filter by interface name (exact match)"),
|
|
7447
|
+
status: z33.enum(["bound", "searching", "requesting", "init", "all"]).default("all").describe("Filter by DHCP client status"),
|
|
7448
|
+
limit: z33.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
|
|
7449
|
+
offset: z33.number().int().min(0).default(0).describe("Offset for pagination")
|
|
7450
|
+
}).strict();
|
|
7451
|
+
var listDhcpClientsTool = {
|
|
7452
|
+
name: "list_dhcp_clients",
|
|
7453
|
+
title: "List DHCP Clients",
|
|
7454
|
+
description: "List DHCP client configurations on a MikroTik router. Shows which interfaces obtain their IP via DHCP, current status, and assigned address.",
|
|
7455
|
+
inputSchema: listDhcpClientsInputSchema,
|
|
7456
|
+
annotations: {
|
|
7457
|
+
readOnlyHint: true,
|
|
7458
|
+
destructiveHint: false,
|
|
7459
|
+
idempotentHint: true,
|
|
7460
|
+
openWorldHint: false
|
|
7461
|
+
},
|
|
7462
|
+
async handler(params, context) {
|
|
7463
|
+
const parsed = listDhcpClientsInputSchema.parse(params);
|
|
7464
|
+
log34.info({ routerId: context.routerId }, "Listing DHCP clients");
|
|
7465
|
+
try {
|
|
7466
|
+
const allClients = await context.routerClient.get("ip/dhcp-client", {
|
|
7467
|
+
limit: void 0,
|
|
7468
|
+
offset: void 0
|
|
7469
|
+
});
|
|
7470
|
+
let filtered = allClients;
|
|
7471
|
+
if (parsed.interface) {
|
|
7472
|
+
filtered = filtered.filter((c) => c.interface === parsed.interface);
|
|
7473
|
+
}
|
|
7474
|
+
if (parsed.status !== "all") {
|
|
7475
|
+
filtered = filtered.filter((c) => c.status === parsed.status);
|
|
7476
|
+
}
|
|
7477
|
+
const total = filtered.length;
|
|
7478
|
+
const clients = filtered.slice(parsed.offset, parsed.offset + parsed.limit);
|
|
7479
|
+
const hasMore = parsed.offset + parsed.limit < total;
|
|
7480
|
+
const lines = [
|
|
7481
|
+
`DHCP clients on ${context.routerId}: ${total} total, showing ${clients.length} (offset ${parsed.offset})`
|
|
7482
|
+
];
|
|
7483
|
+
for (const c of clients) {
|
|
7484
|
+
const rec = c;
|
|
7485
|
+
const iface = rec.interface ?? "unknown";
|
|
7486
|
+
const status = rec.status ?? "unknown";
|
|
7487
|
+
const address = rec.address ?? rec["active-address"] ?? "-";
|
|
7488
|
+
const isDisabled = rec.disabled === true || rec.disabled === "true";
|
|
7489
|
+
lines.push(` ${iface} [${status}] ${address}${isDisabled ? " DISABLED" : ""}`);
|
|
7490
|
+
}
|
|
7491
|
+
return {
|
|
7492
|
+
content: lines.join("\n"),
|
|
7493
|
+
structuredContent: {
|
|
7494
|
+
routerId: context.routerId,
|
|
7495
|
+
clients,
|
|
7496
|
+
total,
|
|
7497
|
+
hasMore,
|
|
7498
|
+
offset: parsed.offset,
|
|
7499
|
+
limit: parsed.limit
|
|
7500
|
+
}
|
|
7501
|
+
};
|
|
7502
|
+
} catch (err) {
|
|
7503
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_dhcp_clients" });
|
|
7504
|
+
}
|
|
7505
|
+
}
|
|
7506
|
+
};
|
|
7507
|
+
var manageDhcpClientInputSchema = z33.object({
|
|
7508
|
+
routerId: z33.string().describe("Target router identifier from the router registry"),
|
|
7509
|
+
action: z33.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
|
|
7510
|
+
interface: z33.string().describe("Interface name \u2014 idempotency key (e.g. ether1, ether2)"),
|
|
7511
|
+
usePeerDns: z33.boolean().default(true).describe("Use DNS servers provided by DHCP server (add only)"),
|
|
7512
|
+
usePeerNtp: z33.boolean().default(false).describe("Use NTP servers provided by DHCP server (add only)"),
|
|
7513
|
+
addDefaultRoute: z33.boolean().default(true).describe("Add default route from DHCP (add only)"),
|
|
7514
|
+
comment: z33.string().max(255).optional().describe("Optional comment (add only)"),
|
|
7515
|
+
dryRun: z33.boolean().default(false).describe("Preview changes without applying")
|
|
7516
|
+
}).strict();
|
|
7517
|
+
var manageDhcpClientTool = {
|
|
7518
|
+
name: "manage_dhcp_client",
|
|
7519
|
+
title: "Manage DHCP Client",
|
|
7520
|
+
description: "Add, remove, enable, or disable a DHCP client on an interface. Idempotent by interface name: add returns already_exists if a DHCP client is already configured on the same interface.",
|
|
7521
|
+
inputSchema: manageDhcpClientInputSchema,
|
|
7522
|
+
annotations: {
|
|
7523
|
+
readOnlyHint: false,
|
|
7524
|
+
destructiveHint: true,
|
|
7525
|
+
idempotentHint: true,
|
|
7526
|
+
openWorldHint: false
|
|
7527
|
+
},
|
|
7528
|
+
snapshotPaths: ["ip/dhcp-client"],
|
|
7529
|
+
async handler(params, context) {
|
|
7530
|
+
const parsed = manageDhcpClientInputSchema.parse(params);
|
|
7531
|
+
log34.info({ routerId: context.routerId, action: parsed.action, interface: parsed.interface }, "Managing DHCP client");
|
|
7532
|
+
try {
|
|
7533
|
+
const allClients = await context.routerClient.get("ip/dhcp-client", {
|
|
7534
|
+
limit: void 0,
|
|
7535
|
+
offset: void 0
|
|
7536
|
+
});
|
|
7537
|
+
const existing = allClients.find(
|
|
7538
|
+
(c) => c.interface === parsed.interface
|
|
7539
|
+
);
|
|
7540
|
+
if (parsed.action === "add") {
|
|
7541
|
+
if (existing) {
|
|
7542
|
+
return {
|
|
7543
|
+
content: `DHCP client already configured on "${parsed.interface}". No changes made.`,
|
|
7544
|
+
structuredContent: { action: "already_exists", client: existing }
|
|
7545
|
+
};
|
|
7546
|
+
}
|
|
7547
|
+
if (parsed.dryRun) {
|
|
7548
|
+
const diff = [
|
|
7549
|
+
{ property: "interface", before: null, after: parsed.interface },
|
|
7550
|
+
{ property: "use-peer-dns", before: null, after: parsed.usePeerDns ? "yes" : "no" },
|
|
7551
|
+
{ property: "use-peer-ntp", before: null, after: parsed.usePeerNtp ? "yes" : "no" },
|
|
7552
|
+
{ property: "add-default-route", before: null, after: parsed.addDefaultRoute ? "yes" : "no" }
|
|
7553
|
+
];
|
|
7554
|
+
return {
|
|
7555
|
+
content: `Dry run: Would add DHCP client on "${parsed.interface}".`,
|
|
7556
|
+
structuredContent: { action: "dry_run", diff }
|
|
7557
|
+
};
|
|
7558
|
+
}
|
|
7559
|
+
const body = {
|
|
7560
|
+
interface: parsed.interface,
|
|
7561
|
+
"use-peer-dns": parsed.usePeerDns ? "yes" : "no",
|
|
7562
|
+
"use-peer-ntp": parsed.usePeerNtp ? "yes" : "no",
|
|
7563
|
+
"add-default-route": parsed.addDefaultRoute ? "yes" : "no"
|
|
7564
|
+
};
|
|
7565
|
+
if (parsed.comment) body.comment = parsed.comment;
|
|
7566
|
+
const created = await context.routerClient.create("ip/dhcp-client", body);
|
|
7567
|
+
log34.info({ interface: parsed.interface, id: created[".id"] }, "DHCP client added");
|
|
7568
|
+
return {
|
|
7569
|
+
content: `Added DHCP client on "${parsed.interface}".`,
|
|
7570
|
+
structuredContent: { action: "created", client: created }
|
|
7571
|
+
};
|
|
7572
|
+
}
|
|
7573
|
+
if (parsed.action === "remove") {
|
|
7574
|
+
if (!existing) {
|
|
7575
|
+
return {
|
|
7576
|
+
content: `No DHCP client found on "${parsed.interface}". Nothing to remove.`,
|
|
7577
|
+
structuredContent: { action: "not_found", interface: parsed.interface }
|
|
7578
|
+
};
|
|
7579
|
+
}
|
|
7580
|
+
if (parsed.dryRun) {
|
|
7581
|
+
return {
|
|
7582
|
+
content: `Dry run: Would remove DHCP client on "${parsed.interface}".`,
|
|
7583
|
+
structuredContent: {
|
|
7584
|
+
action: "dry_run",
|
|
7585
|
+
diff: [{ property: "interface", before: parsed.interface, after: null }]
|
|
7586
|
+
}
|
|
7587
|
+
};
|
|
7588
|
+
}
|
|
7589
|
+
await context.routerClient.remove("ip/dhcp-client", existing[".id"]);
|
|
7590
|
+
log34.info({ interface: parsed.interface }, "DHCP client removed");
|
|
7591
|
+
return {
|
|
7592
|
+
content: `Removed DHCP client on "${parsed.interface}".`,
|
|
7593
|
+
structuredContent: { action: "removed", interface: parsed.interface, id: existing[".id"] }
|
|
7594
|
+
};
|
|
7595
|
+
}
|
|
7596
|
+
if (!existing) {
|
|
7597
|
+
throw new MikroMCPError({
|
|
7598
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
7599
|
+
code: "DHCP_CLIENT_NOT_FOUND",
|
|
7600
|
+
message: `No DHCP client found on interface "${parsed.interface}".`,
|
|
7601
|
+
details: { interface: parsed.interface },
|
|
7602
|
+
recoverability: {
|
|
7603
|
+
retryable: false,
|
|
7604
|
+
suggestedAction: "Verify with list_dhcp_clients.",
|
|
7605
|
+
alternativeTools: ["list_dhcp_clients"]
|
|
7606
|
+
}
|
|
7607
|
+
});
|
|
7608
|
+
}
|
|
7609
|
+
if (parsed.dryRun) {
|
|
7610
|
+
return {
|
|
7611
|
+
content: `Dry run: Would ${parsed.action} DHCP client on "${parsed.interface}".`,
|
|
7612
|
+
structuredContent: {
|
|
7613
|
+
action: "dry_run",
|
|
7614
|
+
diff: [
|
|
7615
|
+
{
|
|
7616
|
+
property: "disabled",
|
|
7617
|
+
before: existing.disabled,
|
|
7618
|
+
after: parsed.action === "disable" ? "true" : "false"
|
|
7619
|
+
}
|
|
7620
|
+
]
|
|
7621
|
+
}
|
|
7622
|
+
};
|
|
7623
|
+
}
|
|
7624
|
+
const disabledValue = parsed.action === "disable" ? "true" : "false";
|
|
7625
|
+
await context.routerClient.update("ip/dhcp-client", existing[".id"], { disabled: disabledValue });
|
|
7626
|
+
const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
|
|
7627
|
+
log34.info({ interface: parsed.interface, action: resultAction }, "DHCP client updated");
|
|
7628
|
+
return {
|
|
7629
|
+
content: `DHCP client on "${parsed.interface}" ${resultAction}.`,
|
|
7630
|
+
structuredContent: { action: resultAction, interface: parsed.interface, id: existing[".id"] }
|
|
7631
|
+
};
|
|
7632
|
+
} catch (err) {
|
|
7633
|
+
if (err instanceof MikroMCPError) throw err;
|
|
7634
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_dhcp_client" });
|
|
7635
|
+
}
|
|
7636
|
+
}
|
|
7637
|
+
};
|
|
7638
|
+
var dhcpClientTools = [listDhcpClientsTool, manageDhcpClientTool];
|
|
7639
|
+
|
|
7640
|
+
// src/domain/tools/ip-service-tools.ts
|
|
7641
|
+
import { z as z34 } from "zod";
|
|
7642
|
+
var log35 = createLogger("ip-service-tools");
|
|
7643
|
+
var SERVICE_NAMES = ["api", "api-ssl", "ssh", "telnet", "www", "www-ssl", "winbox", "ftp"];
|
|
7644
|
+
var listIpServicesInputSchema = z34.object({
|
|
7645
|
+
routerId: z34.string().describe("Target router identifier from the router registry"),
|
|
7646
|
+
name: z34.enum(SERVICE_NAMES).optional().describe("Filter by service name (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp)"),
|
|
7647
|
+
enabled: z34.boolean().optional().describe("When true, return only enabled services; when false, only disabled services")
|
|
7648
|
+
}).strict();
|
|
7649
|
+
var listIpServicesTool = {
|
|
7650
|
+
name: "list_ip_services",
|
|
7651
|
+
title: "List IP Services",
|
|
7652
|
+
description: "List IP services on a MikroTik router (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp) with their port numbers and enabled/disabled status.",
|
|
7653
|
+
inputSchema: listIpServicesInputSchema,
|
|
7654
|
+
annotations: {
|
|
7655
|
+
readOnlyHint: true,
|
|
7656
|
+
destructiveHint: false,
|
|
7657
|
+
idempotentHint: true,
|
|
7658
|
+
openWorldHint: false
|
|
7659
|
+
},
|
|
7660
|
+
async handler(params, context) {
|
|
7661
|
+
const parsed = listIpServicesInputSchema.parse(params);
|
|
7662
|
+
log35.info({ routerId: context.routerId }, "Listing IP services");
|
|
7663
|
+
try {
|
|
7664
|
+
const allServices = await context.routerClient.get("ip/service", {
|
|
7665
|
+
limit: void 0,
|
|
7666
|
+
offset: void 0
|
|
7667
|
+
});
|
|
7668
|
+
let filtered = allServices;
|
|
7669
|
+
if (parsed.name) {
|
|
7670
|
+
filtered = filtered.filter((s) => s.name === parsed.name);
|
|
7671
|
+
}
|
|
7672
|
+
if (parsed.enabled !== void 0) {
|
|
7673
|
+
filtered = filtered.filter((s) => {
|
|
7674
|
+
const isDisabled = s.disabled === true || s.disabled === "true";
|
|
7675
|
+
return parsed.enabled ? !isDisabled : isDisabled;
|
|
7676
|
+
});
|
|
7677
|
+
}
|
|
7678
|
+
const lines = [`IP services on ${context.routerId}: ${filtered.length} returned`];
|
|
7679
|
+
for (const s of filtered) {
|
|
7680
|
+
const rec = s;
|
|
7681
|
+
const isDisabled = rec.disabled === true || rec.disabled === "true";
|
|
7682
|
+
lines.push(` ${rec.name} port:${rec.port ?? "-"} [${isDisabled ? "DISABLED" : "enabled"}]`);
|
|
7683
|
+
}
|
|
7684
|
+
return {
|
|
7685
|
+
content: lines.join("\n"),
|
|
7686
|
+
structuredContent: {
|
|
7687
|
+
routerId: context.routerId,
|
|
7688
|
+
services: filtered,
|
|
7689
|
+
total: filtered.length
|
|
7690
|
+
}
|
|
7691
|
+
};
|
|
7692
|
+
} catch (err) {
|
|
7693
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_ip_services" });
|
|
7694
|
+
}
|
|
7695
|
+
}
|
|
7696
|
+
};
|
|
7697
|
+
var manageIpServiceInputSchema = z34.object({
|
|
7698
|
+
routerId: z34.string().describe("Target router identifier from the router registry"),
|
|
7699
|
+
action: z34.enum(["enable", "disable"]).describe(
|
|
7700
|
+
"Action to perform \u2014 only enable/disable to prevent accidental lockout from changing ports"
|
|
7701
|
+
),
|
|
7702
|
+
name: z34.enum(SERVICE_NAMES).describe("Service name to manage (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp)"),
|
|
7703
|
+
dryRun: z34.boolean().default(false).describe("Preview changes without applying")
|
|
7704
|
+
}).strict();
|
|
7705
|
+
var manageIpServiceTool = {
|
|
7706
|
+
name: "manage_ip_service",
|
|
7707
|
+
title: "Manage IP Service",
|
|
7708
|
+
description: "Enable or disable a RouterOS IP service (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp). Port number changes are intentionally not supported to prevent accidental lockout.",
|
|
7709
|
+
inputSchema: manageIpServiceInputSchema,
|
|
7710
|
+
annotations: {
|
|
7711
|
+
readOnlyHint: false,
|
|
7712
|
+
destructiveHint: true,
|
|
7713
|
+
idempotentHint: true,
|
|
7714
|
+
openWorldHint: false
|
|
7715
|
+
},
|
|
7716
|
+
async handler(params, context) {
|
|
7717
|
+
const parsed = manageIpServiceInputSchema.parse(params);
|
|
7718
|
+
log35.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP service");
|
|
7719
|
+
try {
|
|
7720
|
+
const allServices = await context.routerClient.get("ip/service", {
|
|
7721
|
+
limit: void 0,
|
|
7722
|
+
offset: void 0
|
|
7723
|
+
});
|
|
7724
|
+
const existing = allServices.find((s) => s.name === parsed.name);
|
|
7725
|
+
if (!existing) {
|
|
7726
|
+
throw new MikroMCPError({
|
|
7727
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
7728
|
+
code: "IP_SERVICE_NOT_FOUND",
|
|
7729
|
+
message: `IP service "${parsed.name}" not found on router.`,
|
|
7730
|
+
details: { name: parsed.name },
|
|
7731
|
+
recoverability: {
|
|
7732
|
+
retryable: false,
|
|
7733
|
+
suggestedAction: "Verify service name with list_ip_services.",
|
|
7734
|
+
alternativeTools: ["list_ip_services"]
|
|
7735
|
+
}
|
|
7736
|
+
});
|
|
7737
|
+
}
|
|
7738
|
+
const isCurrentlyDisabled = existing.disabled === "true" || existing.disabled === true;
|
|
7739
|
+
const wouldBeDisabled = parsed.action === "disable";
|
|
7740
|
+
if (isCurrentlyDisabled === wouldBeDisabled) {
|
|
7741
|
+
const alreadyState = parsed.action === "disable" ? "disabled" : "enabled";
|
|
7742
|
+
return {
|
|
7743
|
+
content: `IP service "${parsed.name}" is already ${alreadyState}. No changes made.`,
|
|
7744
|
+
structuredContent: { action: "no_change", name: parsed.name, id: existing[".id"] }
|
|
7745
|
+
};
|
|
7746
|
+
}
|
|
7747
|
+
if (parsed.dryRun) {
|
|
7748
|
+
return {
|
|
7749
|
+
content: `Dry run: Would ${parsed.action} IP service "${parsed.name}".`,
|
|
7750
|
+
structuredContent: {
|
|
7751
|
+
action: "dry_run",
|
|
7752
|
+
diff: [
|
|
7753
|
+
{
|
|
7754
|
+
property: "disabled",
|
|
7755
|
+
before: existing.disabled,
|
|
7756
|
+
after: parsed.action === "disable" ? "true" : "false"
|
|
7757
|
+
}
|
|
7758
|
+
]
|
|
7759
|
+
}
|
|
7760
|
+
};
|
|
7761
|
+
}
|
|
7762
|
+
const disabledValue = parsed.action === "disable" ? "true" : "false";
|
|
7763
|
+
await context.routerClient.update("ip/service", existing[".id"], { disabled: disabledValue });
|
|
7764
|
+
const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
|
|
7765
|
+
log35.info({ name: parsed.name, action: resultAction }, "IP service updated");
|
|
7766
|
+
return {
|
|
7767
|
+
content: `IP service "${parsed.name}" ${resultAction}.`,
|
|
7768
|
+
structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
|
|
7769
|
+
};
|
|
7770
|
+
} catch (err) {
|
|
7771
|
+
if (err instanceof MikroMCPError) throw err;
|
|
7772
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_ip_service" });
|
|
7773
|
+
}
|
|
7774
|
+
}
|
|
7775
|
+
};
|
|
7776
|
+
var ipServiceTools = [listIpServicesTool, manageIpServiceTool];
|
|
7777
|
+
|
|
7778
|
+
// src/domain/tools/pppoe-tools.ts
|
|
7779
|
+
import { z as z35 } from "zod";
|
|
7780
|
+
var log36 = createLogger("pppoe-tools");
|
|
7781
|
+
var PPPOE_PATH = "interface/pppoe-client";
|
|
7782
|
+
var listPppoeClientsInputSchema = z35.object({
|
|
7783
|
+
routerId: z35.string().describe("Target router identifier from the router registry"),
|
|
7784
|
+
interface: z35.string().optional().describe("Filter by parent interface name (exact match)"),
|
|
7785
|
+
status: z35.enum(["connected", "disconnected", "all"]).default("all").describe("Filter by running status"),
|
|
7786
|
+
limit: z35.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
|
|
7787
|
+
offset: z35.number().int().min(0).default(0).describe("Offset for pagination")
|
|
7788
|
+
}).strict();
|
|
7789
|
+
function isRunning(record) {
|
|
7790
|
+
return record.running === true || record.running === "true";
|
|
7791
|
+
}
|
|
7792
|
+
var listPppoeClientsTool = {
|
|
7793
|
+
name: "list_pppoe_clients",
|
|
7794
|
+
title: "List PPPoE Clients",
|
|
7795
|
+
description: "List PPPoE client interfaces on a MikroTik router. Shows name, parent interface, ISP username, and connection status.",
|
|
7796
|
+
inputSchema: listPppoeClientsInputSchema,
|
|
7797
|
+
annotations: {
|
|
7798
|
+
readOnlyHint: true,
|
|
7799
|
+
destructiveHint: false,
|
|
7800
|
+
idempotentHint: true,
|
|
7801
|
+
openWorldHint: false
|
|
7802
|
+
},
|
|
7803
|
+
async handler(params, context) {
|
|
7804
|
+
const parsed = listPppoeClientsInputSchema.parse(params);
|
|
7805
|
+
log36.info({ routerId: context.routerId }, "Listing PPPoE clients");
|
|
7806
|
+
try {
|
|
7807
|
+
const allClients = await context.routerClient.get(PPPOE_PATH, {
|
|
7808
|
+
limit: void 0,
|
|
7809
|
+
offset: void 0
|
|
7810
|
+
});
|
|
7811
|
+
let filtered = allClients;
|
|
7812
|
+
if (parsed.interface) {
|
|
7813
|
+
filtered = filtered.filter((c) => c.interface === parsed.interface);
|
|
7814
|
+
}
|
|
7815
|
+
if (parsed.status === "connected") {
|
|
7816
|
+
filtered = filtered.filter((c) => isRunning(c));
|
|
7817
|
+
} else if (parsed.status === "disconnected") {
|
|
7818
|
+
filtered = filtered.filter((c) => !isRunning(c));
|
|
7819
|
+
}
|
|
7820
|
+
const total = filtered.length;
|
|
7821
|
+
const clients = filtered.slice(parsed.offset, parsed.offset + parsed.limit);
|
|
7822
|
+
const hasMore = parsed.offset + parsed.limit < total;
|
|
7823
|
+
const lines = [
|
|
7824
|
+
`PPPoE clients on ${context.routerId}: ${total} total, showing ${clients.length} (offset ${parsed.offset})`
|
|
7825
|
+
];
|
|
7826
|
+
for (const c of clients) {
|
|
7827
|
+
const name = c.name ?? "unknown";
|
|
7828
|
+
const iface = c.interface ?? "unknown";
|
|
7829
|
+
const user = c.user ?? "-";
|
|
7830
|
+
const status = isRunning(c) ? "connected" : "disconnected";
|
|
7831
|
+
lines.push(` ${name} [${iface}] user=${user} ${status}`);
|
|
7832
|
+
}
|
|
7833
|
+
return {
|
|
7834
|
+
content: lines.join("\n"),
|
|
7835
|
+
structuredContent: {
|
|
7836
|
+
routerId: context.routerId,
|
|
7837
|
+
clients,
|
|
7838
|
+
total,
|
|
7839
|
+
hasMore,
|
|
7840
|
+
offset: parsed.offset,
|
|
7841
|
+
limit: parsed.limit
|
|
7842
|
+
}
|
|
7843
|
+
};
|
|
7844
|
+
} catch (err) {
|
|
7845
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_pppoe_clients" });
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
7848
|
+
};
|
|
7849
|
+
var managePppoeClientInputSchema = z35.object({
|
|
7850
|
+
routerId: z35.string().describe("Target router identifier from the router registry"),
|
|
7851
|
+
action: z35.enum(["add", "update", "remove"]).describe("Action to perform"),
|
|
7852
|
+
name: z35.string().describe("PPPoE client interface name \u2014 idempotency key (e.g. pppoe-wan)"),
|
|
7853
|
+
interface: z35.string().optional().describe("Parent interface (required for add)"),
|
|
7854
|
+
user: z35.string().optional().describe("PPPoE username (required for add)"),
|
|
7855
|
+
password: z35.string().optional().describe("PPPoE password (never logged)"),
|
|
7856
|
+
serviceName: z35.string().optional().describe("PPPoE service name filter (leave empty to match any)"),
|
|
7857
|
+
addDefaultRoute: z35.boolean().optional().describe("Add default route via PPPoE (yes/no)"),
|
|
7858
|
+
dialOnDemand: z35.boolean().optional().describe("Dial on demand instead of always-on (yes/no)"),
|
|
7859
|
+
dryRun: z35.boolean().default(false).describe("Preview changes without applying")
|
|
7860
|
+
}).strict();
|
|
7861
|
+
var managePppoeClientTool = {
|
|
7862
|
+
name: "manage_pppoe_client",
|
|
7863
|
+
title: "Manage PPPoE Client",
|
|
7864
|
+
description: "Add, update, or remove a PPPoE client interface. Idempotent by name: add returns already_exists if same name+interface+user exists; throws CONFLICT if same name exists with different config. Update builds a diff of changed fields and returns no_change when nothing differs. Password is always written when provided because RouterOS does not expose it in GET.",
|
|
7865
|
+
inputSchema: managePppoeClientInputSchema,
|
|
7866
|
+
annotations: {
|
|
7867
|
+
readOnlyHint: false,
|
|
7868
|
+
destructiveHint: true,
|
|
7869
|
+
idempotentHint: true,
|
|
7870
|
+
openWorldHint: false
|
|
7871
|
+
},
|
|
7872
|
+
snapshotPaths: [PPPOE_PATH],
|
|
7873
|
+
async handler(params, context) {
|
|
7874
|
+
const parsed = managePppoeClientInputSchema.parse(params);
|
|
7875
|
+
log36.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing PPPoE client");
|
|
7876
|
+
try {
|
|
7877
|
+
const allClients = await context.routerClient.get(PPPOE_PATH, {
|
|
7878
|
+
limit: void 0,
|
|
7879
|
+
offset: void 0
|
|
7880
|
+
});
|
|
7881
|
+
const existing = allClients.find((c) => c.name === parsed.name);
|
|
7882
|
+
if (parsed.action === "add") {
|
|
7883
|
+
if (!parsed.interface) {
|
|
7884
|
+
throw new MikroMCPError({
|
|
7885
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
7886
|
+
code: "PPPOE_CLIENT_INTERFACE_REQUIRED",
|
|
7887
|
+
message: "interface is required for action add.",
|
|
7888
|
+
details: { name: parsed.name },
|
|
7889
|
+
recoverability: {
|
|
7890
|
+
retryable: false,
|
|
7891
|
+
suggestedAction: "Provide the parent interface (e.g. ether1).",
|
|
7892
|
+
alternativeTools: []
|
|
7893
|
+
}
|
|
7894
|
+
});
|
|
7895
|
+
}
|
|
7896
|
+
if (!parsed.user) {
|
|
7897
|
+
throw new MikroMCPError({
|
|
7898
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
7899
|
+
code: "PPPOE_CLIENT_USER_REQUIRED",
|
|
7900
|
+
message: "user is required for action add.",
|
|
7901
|
+
details: { name: parsed.name },
|
|
7902
|
+
recoverability: {
|
|
7903
|
+
retryable: false,
|
|
7904
|
+
suggestedAction: "Provide the PPPoE username.",
|
|
7905
|
+
alternativeTools: []
|
|
7906
|
+
}
|
|
7907
|
+
});
|
|
7908
|
+
}
|
|
7909
|
+
if (existing) {
|
|
7910
|
+
const sameConfig = existing.interface === parsed.interface && existing.user === parsed.user;
|
|
7911
|
+
if (sameConfig) {
|
|
7912
|
+
return {
|
|
7913
|
+
content: `PPPoE client "${parsed.name}" already exists with the same configuration. No changes made.`,
|
|
7914
|
+
structuredContent: { action: "already_exists", client: existing }
|
|
7915
|
+
};
|
|
7916
|
+
}
|
|
7917
|
+
throw new MikroMCPError({
|
|
7918
|
+
category: "CONFLICT" /* CONFLICT */,
|
|
7919
|
+
code: "PPPOE_CLIENT_CONFLICT",
|
|
7920
|
+
message: `PPPoE client "${parsed.name}" already exists with a different configuration.`,
|
|
7921
|
+
details: {
|
|
7922
|
+
existing: { interface: existing.interface, user: existing.user },
|
|
7923
|
+
requested: { interface: parsed.interface, user: parsed.user }
|
|
7924
|
+
},
|
|
7925
|
+
recoverability: {
|
|
7926
|
+
retryable: false,
|
|
7927
|
+
suggestedAction: "Remove the existing client first or use action update.",
|
|
7928
|
+
alternativeTools: ["manage_pppoe_client"]
|
|
7929
|
+
}
|
|
7930
|
+
});
|
|
7931
|
+
}
|
|
7932
|
+
const diff = [
|
|
7933
|
+
{ property: "name", before: null, after: parsed.name },
|
|
7934
|
+
{ property: "interface", before: null, after: parsed.interface },
|
|
7935
|
+
{ property: "user", before: null, after: parsed.user }
|
|
7936
|
+
];
|
|
7937
|
+
if (parsed.serviceName !== void 0) {
|
|
7938
|
+
diff.push({ property: "service-name", before: null, after: parsed.serviceName });
|
|
7939
|
+
}
|
|
7940
|
+
if (parsed.addDefaultRoute !== void 0) {
|
|
7941
|
+
diff.push({ property: "add-default-route", before: null, after: parsed.addDefaultRoute ? "yes" : "no" });
|
|
7942
|
+
}
|
|
7943
|
+
if (parsed.dialOnDemand !== void 0) {
|
|
7944
|
+
diff.push({ property: "dial-on-demand", before: null, after: parsed.dialOnDemand ? "yes" : "no" });
|
|
7945
|
+
}
|
|
7946
|
+
if (parsed.dryRun) {
|
|
7947
|
+
return {
|
|
7948
|
+
content: `Dry run: Would create PPPoE client "${parsed.name}" on "${parsed.interface}".`,
|
|
7949
|
+
structuredContent: { action: "dry_run", diff }
|
|
7950
|
+
};
|
|
7951
|
+
}
|
|
7952
|
+
const body = {
|
|
7953
|
+
name: parsed.name,
|
|
7954
|
+
interface: parsed.interface,
|
|
7955
|
+
user: parsed.user
|
|
7956
|
+
};
|
|
7957
|
+
if (parsed.password !== void 0) body.password = parsed.password;
|
|
7958
|
+
if (parsed.serviceName !== void 0) body["service-name"] = parsed.serviceName;
|
|
7959
|
+
if (parsed.addDefaultRoute !== void 0) body["add-default-route"] = parsed.addDefaultRoute ? "yes" : "no";
|
|
7960
|
+
if (parsed.dialOnDemand !== void 0) body["dial-on-demand"] = parsed.dialOnDemand ? "yes" : "no";
|
|
7961
|
+
const created = await context.routerClient.create(PPPOE_PATH, body);
|
|
7962
|
+
log36.info({ name: parsed.name, id: created[".id"] }, "PPPoE client created");
|
|
7963
|
+
return {
|
|
7964
|
+
content: `Created PPPoE client "${parsed.name}" on "${parsed.interface}".`,
|
|
7965
|
+
structuredContent: { action: "created", client: created }
|
|
7966
|
+
};
|
|
7967
|
+
}
|
|
7968
|
+
if (parsed.action === "update") {
|
|
7969
|
+
if (!existing) {
|
|
7970
|
+
throw new MikroMCPError({
|
|
7971
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
7972
|
+
code: "PPPOE_CLIENT_NOT_FOUND",
|
|
7973
|
+
message: `PPPoE client "${parsed.name}" not found.`,
|
|
7974
|
+
details: { name: parsed.name },
|
|
7975
|
+
recoverability: {
|
|
7976
|
+
retryable: false,
|
|
7977
|
+
suggestedAction: "Verify with list_pppoe_clients.",
|
|
7978
|
+
alternativeTools: ["list_pppoe_clients"]
|
|
7979
|
+
}
|
|
7980
|
+
});
|
|
7981
|
+
}
|
|
7982
|
+
const updates = {};
|
|
7983
|
+
const diff = [];
|
|
7984
|
+
if (parsed.interface !== void 0 && existing.interface !== parsed.interface) {
|
|
7985
|
+
updates.interface = parsed.interface;
|
|
7986
|
+
diff.push({ property: "interface", before: existing.interface, after: parsed.interface });
|
|
7987
|
+
}
|
|
7988
|
+
if (parsed.user !== void 0 && existing.user !== parsed.user) {
|
|
7989
|
+
updates.user = parsed.user;
|
|
7990
|
+
diff.push({ property: "user", before: existing.user, after: parsed.user });
|
|
7991
|
+
}
|
|
7992
|
+
if (parsed.serviceName !== void 0 && existing["service-name"] !== parsed.serviceName) {
|
|
7993
|
+
updates["service-name"] = parsed.serviceName;
|
|
7994
|
+
diff.push({ property: "service-name", before: existing["service-name"] ?? null, after: parsed.serviceName });
|
|
7995
|
+
}
|
|
7996
|
+
if (parsed.addDefaultRoute !== void 0) {
|
|
7997
|
+
const desired = parsed.addDefaultRoute ? "yes" : "no";
|
|
7998
|
+
if (existing["add-default-route"] !== desired) {
|
|
7999
|
+
updates["add-default-route"] = desired;
|
|
8000
|
+
diff.push({ property: "add-default-route", before: existing["add-default-route"] ?? null, after: desired });
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
8003
|
+
if (parsed.dialOnDemand !== void 0) {
|
|
8004
|
+
const desired = parsed.dialOnDemand ? "yes" : "no";
|
|
8005
|
+
if (existing["dial-on-demand"] !== desired) {
|
|
8006
|
+
updates["dial-on-demand"] = desired;
|
|
8007
|
+
diff.push({ property: "dial-on-demand", before: existing["dial-on-demand"] ?? null, after: desired });
|
|
8008
|
+
}
|
|
8009
|
+
}
|
|
8010
|
+
if (parsed.password !== void 0) {
|
|
8011
|
+
updates.password = parsed.password;
|
|
8012
|
+
}
|
|
8013
|
+
const hasUpdates = Object.keys(updates).length > 0;
|
|
8014
|
+
if (!hasUpdates) {
|
|
8015
|
+
return {
|
|
8016
|
+
content: `PPPoE client "${parsed.name}" already matches the requested configuration. No changes made.`,
|
|
8017
|
+
structuredContent: { action: "no_change", client: existing }
|
|
8018
|
+
};
|
|
8019
|
+
}
|
|
8020
|
+
if (parsed.dryRun) {
|
|
8021
|
+
return {
|
|
8022
|
+
content: `Dry run: Would update PPPoE client "${parsed.name}".`,
|
|
8023
|
+
structuredContent: { action: "dry_run", diff }
|
|
8024
|
+
};
|
|
8025
|
+
}
|
|
8026
|
+
await context.routerClient.update(PPPOE_PATH, existing[".id"], updates);
|
|
8027
|
+
log36.info({ name: parsed.name }, "PPPoE client updated");
|
|
8028
|
+
return {
|
|
8029
|
+
content: `Updated PPPoE client "${parsed.name}".`,
|
|
8030
|
+
structuredContent: { action: "updated", name: parsed.name, diff }
|
|
8031
|
+
};
|
|
8032
|
+
}
|
|
8033
|
+
if (!existing) {
|
|
8034
|
+
return {
|
|
8035
|
+
content: `PPPoE client "${parsed.name}" not found. Nothing to remove.`,
|
|
8036
|
+
structuredContent: { action: "not_found", name: parsed.name }
|
|
8037
|
+
};
|
|
8038
|
+
}
|
|
8039
|
+
if (parsed.dryRun) {
|
|
8040
|
+
return {
|
|
8041
|
+
content: `Dry run: Would remove PPPoE client "${parsed.name}".`,
|
|
8042
|
+
structuredContent: {
|
|
8043
|
+
action: "dry_run",
|
|
8044
|
+
diff: [{ property: "name", before: parsed.name, after: null }]
|
|
8045
|
+
}
|
|
8046
|
+
};
|
|
8047
|
+
}
|
|
8048
|
+
await context.routerClient.remove(PPPOE_PATH, existing[".id"]);
|
|
8049
|
+
log36.info({ name: parsed.name }, "PPPoE client removed");
|
|
8050
|
+
return {
|
|
8051
|
+
content: `Removed PPPoE client "${parsed.name}".`,
|
|
8052
|
+
structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
|
|
8053
|
+
};
|
|
8054
|
+
} catch (err) {
|
|
8055
|
+
if (err instanceof MikroMCPError) throw err;
|
|
8056
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_pppoe_client" });
|
|
8057
|
+
}
|
|
8058
|
+
}
|
|
8059
|
+
};
|
|
8060
|
+
var pppoeTools = [listPppoeClientsTool, managePppoeClientTool];
|
|
8061
|
+
|
|
8062
|
+
// src/domain/tools/ovpn-tools.ts
|
|
8063
|
+
import { z as z36 } from "zod";
|
|
8064
|
+
var log37 = createLogger("ovpn-tools");
|
|
8065
|
+
var OVPN_CLIENT_PATH = "interface/ovpn-client";
|
|
8066
|
+
var OVPN_SERVER_PATH = "interface/ovpn-server/server";
|
|
8067
|
+
var listOvpnClientsInputSchema = z36.object({
|
|
8068
|
+
routerId: z36.string().describe("Target router identifier from the router registry"),
|
|
8069
|
+
limit: z36.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
|
|
8070
|
+
offset: z36.number().int().min(0).default(0).describe("Offset for pagination")
|
|
8071
|
+
}).strict();
|
|
8072
|
+
var listOvpnClientsTool = {
|
|
8073
|
+
name: "list_ovpn_clients",
|
|
8074
|
+
title: "List OpenVPN Clients",
|
|
8075
|
+
description: "List OpenVPN client interfaces on a MikroTik router. Shows name, remote server, and connection status.",
|
|
8076
|
+
inputSchema: listOvpnClientsInputSchema,
|
|
8077
|
+
annotations: {
|
|
8078
|
+
readOnlyHint: true,
|
|
8079
|
+
destructiveHint: false,
|
|
8080
|
+
idempotentHint: true,
|
|
8081
|
+
openWorldHint: false
|
|
8082
|
+
},
|
|
8083
|
+
async handler(params, context) {
|
|
8084
|
+
const parsed = listOvpnClientsInputSchema.parse(params);
|
|
8085
|
+
log37.info({ routerId: context.routerId }, "Listing OpenVPN clients");
|
|
8086
|
+
try {
|
|
8087
|
+
const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
|
|
8088
|
+
limit: void 0,
|
|
8089
|
+
offset: void 0
|
|
8090
|
+
});
|
|
8091
|
+
const total = allClients.length;
|
|
8092
|
+
const clients = allClients.slice(parsed.offset, parsed.offset + parsed.limit);
|
|
8093
|
+
const hasMore = parsed.offset + parsed.limit < total;
|
|
8094
|
+
return {
|
|
8095
|
+
content: `OpenVPN clients on ${context.routerId}: ${total} total, showing ${clients.length} (offset ${parsed.offset})`,
|
|
8096
|
+
structuredContent: {
|
|
8097
|
+
routerId: context.routerId,
|
|
8098
|
+
clients,
|
|
8099
|
+
total,
|
|
8100
|
+
hasMore,
|
|
8101
|
+
offset: parsed.offset,
|
|
8102
|
+
limit: parsed.limit
|
|
8103
|
+
}
|
|
8104
|
+
};
|
|
8105
|
+
} catch (err) {
|
|
8106
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_ovpn_clients" });
|
|
8107
|
+
}
|
|
8108
|
+
}
|
|
8109
|
+
};
|
|
8110
|
+
var manageOvpnClientInputSchema = z36.object({
|
|
8111
|
+
routerId: z36.string().describe("Target router identifier from the router registry"),
|
|
8112
|
+
action: z36.enum(["add", "update", "remove"]).describe("Action to perform"),
|
|
8113
|
+
name: z36.string().describe("OpenVPN client interface name \u2014 idempotency key"),
|
|
8114
|
+
connectTo: z36.string().optional().describe("Remote server address (required for add)"),
|
|
8115
|
+
port: z36.number().int().min(1).max(65535).optional().describe("Remote port number"),
|
|
8116
|
+
mode: z36.enum(["ip", "ethernet"]).optional().describe("Tunnel mode"),
|
|
8117
|
+
protocol: z36.enum(["tcp-client", "udp"]).optional().describe("Transport protocol"),
|
|
8118
|
+
certificate: z36.string().optional().describe("Client certificate name"),
|
|
8119
|
+
user: z36.string().optional().describe("VPN username"),
|
|
8120
|
+
password: z36.string().optional().describe("VPN password (never logged)"),
|
|
8121
|
+
dryRun: z36.boolean().default(false).describe("Preview changes without applying")
|
|
8122
|
+
}).strict();
|
|
8123
|
+
var manageOvpnClientTool = {
|
|
8124
|
+
name: "manage_ovpn_client",
|
|
8125
|
+
title: "Manage OpenVPN Client",
|
|
8126
|
+
description: "Add, update, or remove an OpenVPN client interface. Idempotent by name: add returns already_exists if same name+connectTo exists; throws CONFLICT if same name exists with different connectTo. Update builds a diff of changed fields and returns no_change when nothing differs. Password is always written when provided because RouterOS does not expose it in GET.",
|
|
8127
|
+
inputSchema: manageOvpnClientInputSchema,
|
|
8128
|
+
annotations: {
|
|
8129
|
+
readOnlyHint: false,
|
|
8130
|
+
destructiveHint: true,
|
|
8131
|
+
idempotentHint: true,
|
|
8132
|
+
openWorldHint: false
|
|
8133
|
+
},
|
|
8134
|
+
snapshotPaths: [OVPN_CLIENT_PATH],
|
|
8135
|
+
async handler(params, context) {
|
|
8136
|
+
const parsed = manageOvpnClientInputSchema.parse(params);
|
|
8137
|
+
log37.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing OpenVPN client");
|
|
8138
|
+
try {
|
|
8139
|
+
const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
|
|
8140
|
+
limit: void 0,
|
|
8141
|
+
offset: void 0
|
|
8142
|
+
});
|
|
8143
|
+
const existing = allClients.find((c) => c.name === parsed.name);
|
|
8144
|
+
if (parsed.action === "add") {
|
|
8145
|
+
if (!parsed.connectTo) {
|
|
8146
|
+
throw new MikroMCPError({
|
|
8147
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
8148
|
+
code: "OVPN_CLIENT_CONNECT_TO_REQUIRED",
|
|
8149
|
+
message: "connectTo is required for action add.",
|
|
8150
|
+
details: { name: parsed.name },
|
|
8151
|
+
recoverability: {
|
|
8152
|
+
retryable: false,
|
|
8153
|
+
suggestedAction: "Provide the remote server address.",
|
|
8154
|
+
alternativeTools: []
|
|
8155
|
+
}
|
|
8156
|
+
});
|
|
8157
|
+
}
|
|
8158
|
+
if (existing) {
|
|
8159
|
+
const sameConfig = existing["connect-to"] === parsed.connectTo;
|
|
8160
|
+
if (sameConfig) {
|
|
8161
|
+
return {
|
|
8162
|
+
content: `OpenVPN client "${parsed.name}" already exists with the same configuration. No changes made.`,
|
|
8163
|
+
structuredContent: { action: "already_exists", client: existing }
|
|
8164
|
+
};
|
|
8165
|
+
}
|
|
8166
|
+
throw new MikroMCPError({
|
|
8167
|
+
category: "CONFLICT" /* CONFLICT */,
|
|
8168
|
+
code: "OVPN_CLIENT_CONFLICT",
|
|
8169
|
+
message: `OpenVPN client "${parsed.name}" already exists with a different configuration.`,
|
|
8170
|
+
details: {
|
|
8171
|
+
existing: { "connect-to": existing["connect-to"] },
|
|
8172
|
+
requested: { "connect-to": parsed.connectTo }
|
|
8173
|
+
},
|
|
8174
|
+
recoverability: {
|
|
8175
|
+
retryable: false,
|
|
8176
|
+
suggestedAction: "Remove the existing client first or use action update.",
|
|
8177
|
+
alternativeTools: ["manage_ovpn_client"]
|
|
8178
|
+
}
|
|
8179
|
+
});
|
|
8180
|
+
}
|
|
8181
|
+
const diff = [
|
|
8182
|
+
{ property: "name", before: null, after: parsed.name },
|
|
8183
|
+
{ property: "connect-to", before: null, after: parsed.connectTo }
|
|
8184
|
+
];
|
|
8185
|
+
if (parsed.dryRun) {
|
|
8186
|
+
return {
|
|
8187
|
+
content: `Dry run: Would create OpenVPN client "${parsed.name}" connecting to "${parsed.connectTo}".`,
|
|
8188
|
+
structuredContent: { action: "dry_run", diff }
|
|
8189
|
+
};
|
|
8190
|
+
}
|
|
8191
|
+
const body = {
|
|
8192
|
+
name: parsed.name,
|
|
8193
|
+
"connect-to": parsed.connectTo
|
|
8194
|
+
};
|
|
8195
|
+
if (parsed.port !== void 0) body.port = String(parsed.port);
|
|
8196
|
+
if (parsed.mode !== void 0) body.mode = parsed.mode;
|
|
8197
|
+
if (parsed.protocol !== void 0) body.protocol = parsed.protocol;
|
|
8198
|
+
if (parsed.certificate !== void 0) body.certificate = parsed.certificate;
|
|
8199
|
+
if (parsed.user !== void 0) body.user = parsed.user;
|
|
8200
|
+
if (parsed.password !== void 0) body.password = parsed.password;
|
|
8201
|
+
const created = await context.routerClient.create(OVPN_CLIENT_PATH, body);
|
|
8202
|
+
log37.info({ name: parsed.name, id: created[".id"] }, "OpenVPN client created");
|
|
8203
|
+
return {
|
|
8204
|
+
content: `Created OpenVPN client "${parsed.name}" connecting to "${parsed.connectTo}".`,
|
|
8205
|
+
structuredContent: { action: "created", client: created }
|
|
8206
|
+
};
|
|
8207
|
+
}
|
|
8208
|
+
if (parsed.action === "update") {
|
|
8209
|
+
if (!existing) {
|
|
8210
|
+
throw new MikroMCPError({
|
|
8211
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
8212
|
+
code: "OVPN_CLIENT_NOT_FOUND",
|
|
8213
|
+
message: `OpenVPN client "${parsed.name}" not found.`,
|
|
8214
|
+
details: { name: parsed.name },
|
|
8215
|
+
recoverability: {
|
|
8216
|
+
retryable: false,
|
|
8217
|
+
suggestedAction: "Verify with list_ovpn_clients.",
|
|
8218
|
+
alternativeTools: ["list_ovpn_clients"]
|
|
8219
|
+
}
|
|
8220
|
+
});
|
|
8221
|
+
}
|
|
8222
|
+
const updates = {};
|
|
8223
|
+
const diff = [];
|
|
8224
|
+
if (parsed.connectTo !== void 0 && existing["connect-to"] !== parsed.connectTo) {
|
|
8225
|
+
updates["connect-to"] = parsed.connectTo;
|
|
8226
|
+
diff.push({ property: "connect-to", before: existing["connect-to"] ?? null, after: parsed.connectTo });
|
|
8227
|
+
}
|
|
8228
|
+
if (parsed.port !== void 0 && existing.port !== String(parsed.port)) {
|
|
8229
|
+
updates.port = String(parsed.port);
|
|
8230
|
+
diff.push({ property: "port", before: existing.port ?? null, after: String(parsed.port) });
|
|
8231
|
+
}
|
|
8232
|
+
if (parsed.mode !== void 0 && existing.mode !== parsed.mode) {
|
|
8233
|
+
updates.mode = parsed.mode;
|
|
8234
|
+
diff.push({ property: "mode", before: existing.mode ?? null, after: parsed.mode });
|
|
8235
|
+
}
|
|
8236
|
+
if (parsed.protocol !== void 0 && existing.protocol !== parsed.protocol) {
|
|
8237
|
+
updates.protocol = parsed.protocol;
|
|
8238
|
+
diff.push({ property: "protocol", before: existing.protocol ?? null, after: parsed.protocol });
|
|
8239
|
+
}
|
|
8240
|
+
if (parsed.certificate !== void 0 && existing.certificate !== parsed.certificate) {
|
|
8241
|
+
updates.certificate = parsed.certificate;
|
|
8242
|
+
diff.push({ property: "certificate", before: existing.certificate ?? null, after: parsed.certificate });
|
|
8243
|
+
}
|
|
8244
|
+
if (parsed.user !== void 0 && existing.user !== parsed.user) {
|
|
8245
|
+
updates.user = parsed.user;
|
|
8246
|
+
diff.push({ property: "user", before: existing.user ?? null, after: parsed.user });
|
|
8247
|
+
}
|
|
8248
|
+
if (parsed.password !== void 0) {
|
|
8249
|
+
updates.password = parsed.password;
|
|
8250
|
+
}
|
|
8251
|
+
const hasUpdates = Object.keys(updates).length > 0;
|
|
8252
|
+
if (!hasUpdates) {
|
|
8253
|
+
return {
|
|
8254
|
+
content: `OpenVPN client "${parsed.name}" already matches the requested configuration. No changes made.`,
|
|
8255
|
+
structuredContent: { action: "no_change", client: existing }
|
|
8256
|
+
};
|
|
8257
|
+
}
|
|
8258
|
+
if (parsed.dryRun) {
|
|
8259
|
+
return {
|
|
8260
|
+
content: `Dry run: Would update OpenVPN client "${parsed.name}".`,
|
|
8261
|
+
structuredContent: { action: "dry_run", diff }
|
|
8262
|
+
};
|
|
8263
|
+
}
|
|
8264
|
+
await context.routerClient.update(OVPN_CLIENT_PATH, existing[".id"], updates);
|
|
8265
|
+
log37.info({ name: parsed.name }, "OpenVPN client updated");
|
|
8266
|
+
return {
|
|
8267
|
+
content: `Updated OpenVPN client "${parsed.name}".`,
|
|
8268
|
+
structuredContent: { action: "updated", name: parsed.name, diff }
|
|
8269
|
+
};
|
|
8270
|
+
}
|
|
8271
|
+
if (!existing) {
|
|
8272
|
+
return {
|
|
8273
|
+
content: `OpenVPN client "${parsed.name}" not found. Nothing to remove.`,
|
|
8274
|
+
structuredContent: { action: "not_found", name: parsed.name }
|
|
8275
|
+
};
|
|
8276
|
+
}
|
|
8277
|
+
if (parsed.dryRun) {
|
|
8278
|
+
return {
|
|
8279
|
+
content: `Dry run: Would remove OpenVPN client "${parsed.name}".`,
|
|
8280
|
+
structuredContent: {
|
|
8281
|
+
action: "dry_run",
|
|
8282
|
+
diff: [{ property: "name", before: parsed.name, after: null }]
|
|
8283
|
+
}
|
|
8284
|
+
};
|
|
8285
|
+
}
|
|
8286
|
+
await context.routerClient.remove(OVPN_CLIENT_PATH, existing[".id"]);
|
|
8287
|
+
log37.info({ name: parsed.name }, "OpenVPN client removed");
|
|
8288
|
+
return {
|
|
8289
|
+
content: `Removed OpenVPN client "${parsed.name}".`,
|
|
8290
|
+
structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
|
|
8291
|
+
};
|
|
8292
|
+
} catch (err) {
|
|
8293
|
+
if (err instanceof MikroMCPError) throw err;
|
|
8294
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_client" });
|
|
8295
|
+
}
|
|
8296
|
+
}
|
|
8297
|
+
};
|
|
8298
|
+
var getOvpnServerInputSchema = z36.object({
|
|
8299
|
+
routerId: z36.string().describe("Target router identifier from the router registry")
|
|
8300
|
+
}).strict();
|
|
8301
|
+
function fetchOvpnServer(allRecords, routerId) {
|
|
8302
|
+
if (allRecords.length === 0) {
|
|
8303
|
+
throw new MikroMCPError({
|
|
8304
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
8305
|
+
code: "OVPN_SERVER_NOT_FOUND",
|
|
8306
|
+
message: "OpenVPN server not found. The OpenVPN package may not be installed on this router.",
|
|
8307
|
+
details: { routerId },
|
|
8308
|
+
recoverability: {
|
|
8309
|
+
retryable: false,
|
|
8310
|
+
suggestedAction: "Install the OpenVPN package via manage_package.",
|
|
8311
|
+
alternativeTools: ["list_packages", "manage_package"]
|
|
8312
|
+
}
|
|
8313
|
+
});
|
|
8314
|
+
}
|
|
8315
|
+
return allRecords[0];
|
|
8316
|
+
}
|
|
8317
|
+
var getOvpnServerTool = {
|
|
8318
|
+
name: "get_ovpn_server",
|
|
8319
|
+
title: "Get OpenVPN Server",
|
|
8320
|
+
description: "Get the OpenVPN server configuration on a MikroTik router. Throws NOT_FOUND if the OpenVPN package is not installed.",
|
|
8321
|
+
inputSchema: getOvpnServerInputSchema,
|
|
8322
|
+
annotations: {
|
|
8323
|
+
readOnlyHint: true,
|
|
8324
|
+
destructiveHint: false,
|
|
8325
|
+
idempotentHint: true,
|
|
8326
|
+
openWorldHint: false
|
|
8327
|
+
},
|
|
8328
|
+
async handler(params, context) {
|
|
8329
|
+
const parsed = getOvpnServerInputSchema.parse(params);
|
|
8330
|
+
log37.info({ routerId: context.routerId }, "Getting OpenVPN server configuration");
|
|
8331
|
+
try {
|
|
8332
|
+
const records = await context.routerClient.get(OVPN_SERVER_PATH, {
|
|
8333
|
+
limit: void 0,
|
|
8334
|
+
offset: void 0
|
|
8335
|
+
});
|
|
8336
|
+
const server = fetchOvpnServer(records, parsed.routerId);
|
|
8337
|
+
return {
|
|
8338
|
+
content: `OpenVPN server on ${context.routerId}: enabled=${server.enabled}, port=${server.port}, protocol=${server.protocol}`,
|
|
8339
|
+
structuredContent: { routerId: context.routerId, server }
|
|
8340
|
+
};
|
|
8341
|
+
} catch (err) {
|
|
8342
|
+
if (err instanceof MikroMCPError) throw err;
|
|
8343
|
+
throw enrichError(err, { routerId: context.routerId, tool: "get_ovpn_server" });
|
|
8344
|
+
}
|
|
8345
|
+
}
|
|
8346
|
+
};
|
|
8347
|
+
var manageOvpnServerInputSchema = z36.object({
|
|
8348
|
+
routerId: z36.string().describe("Target router identifier from the router registry"),
|
|
8349
|
+
action: z36.enum(["enable", "disable", "set"]).describe("Action to perform"),
|
|
8350
|
+
port: z36.number().int().min(1).max(65535).optional().describe("Listening port (set action only)"),
|
|
8351
|
+
mode: z36.enum(["ip", "ethernet"]).optional().describe("Tunnel mode (set action only)"),
|
|
8352
|
+
protocol: z36.enum(["tcp-server", "udp"]).optional().describe("Transport protocol (set action only)"),
|
|
8353
|
+
certificate: z36.string().optional().describe("Server certificate name (set action only)"),
|
|
8354
|
+
cipher: z36.enum(["blowfish128", "aes128-cbc", "aes192-cbc", "aes256-cbc", "aes128-gcm", "aes256-gcm", "none"]).optional().describe("Encryption cipher (set action only)"),
|
|
8355
|
+
auth: z36.enum(["md5", "sha1", "sha256", "sha512", "null"]).optional().describe("Authentication algorithm (set action only)"),
|
|
8356
|
+
dryRun: z36.boolean().default(false).describe("Preview changes without applying")
|
|
8357
|
+
}).strict();
|
|
8358
|
+
var manageOvpnServerTool = {
|
|
8359
|
+
name: "manage_ovpn_server",
|
|
8360
|
+
title: "Manage OpenVPN Server",
|
|
8361
|
+
description: "Enable, disable, or configure the OpenVPN server on a MikroTik router. The server is a singleton \u2014 there is only one per router. Throws NOT_FOUND if the OpenVPN package is not installed. For set action, at least one configuration field must be provided.",
|
|
8362
|
+
inputSchema: manageOvpnServerInputSchema,
|
|
8363
|
+
annotations: {
|
|
8364
|
+
readOnlyHint: false,
|
|
8365
|
+
destructiveHint: true,
|
|
8366
|
+
idempotentHint: true,
|
|
8367
|
+
openWorldHint: false
|
|
8368
|
+
},
|
|
8369
|
+
snapshotPaths: [OVPN_SERVER_PATH],
|
|
8370
|
+
async handler(params, context) {
|
|
8371
|
+
const parsed = manageOvpnServerInputSchema.parse(params);
|
|
8372
|
+
log37.info({ routerId: context.routerId, action: parsed.action }, "Managing OpenVPN server");
|
|
8373
|
+
try {
|
|
8374
|
+
const records = await context.routerClient.get(OVPN_SERVER_PATH, {
|
|
8375
|
+
limit: void 0,
|
|
8376
|
+
offset: void 0
|
|
8377
|
+
});
|
|
8378
|
+
const server = fetchOvpnServer(records, parsed.routerId);
|
|
8379
|
+
const serverId = server[".id"];
|
|
8380
|
+
if (parsed.action === "enable" || parsed.action === "disable") {
|
|
8381
|
+
const desiredEnabled = parsed.action === "enable";
|
|
8382
|
+
const currentEnabled = server.enabled === "yes";
|
|
8383
|
+
if (desiredEnabled === currentEnabled) {
|
|
8384
|
+
return {
|
|
8385
|
+
content: `OpenVPN server on ${context.routerId} is already ${parsed.action}d. No changes made.`,
|
|
8386
|
+
structuredContent: { action: "no_change", server }
|
|
8387
|
+
};
|
|
8388
|
+
}
|
|
8389
|
+
if (parsed.dryRun) {
|
|
8390
|
+
return {
|
|
8391
|
+
content: `Dry run: Would ${parsed.action} OpenVPN server on ${context.routerId}.`,
|
|
8392
|
+
structuredContent: {
|
|
8393
|
+
action: "dry_run",
|
|
8394
|
+
diff: [{ property: "enabled", before: server.enabled, after: desiredEnabled ? "yes" : "no" }]
|
|
8395
|
+
}
|
|
8396
|
+
};
|
|
8397
|
+
}
|
|
8398
|
+
await context.routerClient.update(OVPN_SERVER_PATH, serverId, {
|
|
8399
|
+
enabled: desiredEnabled ? "yes" : "no"
|
|
8400
|
+
});
|
|
8401
|
+
log37.info({ routerId: context.routerId, action: parsed.action }, "OpenVPN server updated");
|
|
8402
|
+
return {
|
|
8403
|
+
content: `OpenVPN server on ${context.routerId} ${parsed.action}d.`,
|
|
8404
|
+
structuredContent: { action: parsed.action === "enable" ? "enabled" : "disabled", server }
|
|
8405
|
+
};
|
|
8406
|
+
}
|
|
8407
|
+
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;
|
|
8408
|
+
if (!hasFields) {
|
|
8409
|
+
throw new MikroMCPError({
|
|
8410
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
8411
|
+
code: "OVPN_SERVER_NO_FIELDS",
|
|
8412
|
+
message: "At least one field must be provided for action set.",
|
|
8413
|
+
details: {},
|
|
8414
|
+
recoverability: {
|
|
8415
|
+
retryable: false,
|
|
8416
|
+
suggestedAction: "Provide at least one of: port, mode, protocol, certificate, cipher, auth.",
|
|
8417
|
+
alternativeTools: []
|
|
8418
|
+
}
|
|
8419
|
+
});
|
|
8420
|
+
}
|
|
8421
|
+
const updates = {};
|
|
8422
|
+
const diff = [];
|
|
8423
|
+
if (parsed.port !== void 0 && server.port !== String(parsed.port)) {
|
|
8424
|
+
updates.port = String(parsed.port);
|
|
8425
|
+
diff.push({ property: "port", before: server.port ?? null, after: String(parsed.port) });
|
|
8426
|
+
}
|
|
8427
|
+
if (parsed.mode !== void 0 && server.mode !== parsed.mode) {
|
|
8428
|
+
updates.mode = parsed.mode;
|
|
8429
|
+
diff.push({ property: "mode", before: server.mode ?? null, after: parsed.mode });
|
|
8430
|
+
}
|
|
8431
|
+
if (parsed.protocol !== void 0 && server.protocol !== parsed.protocol) {
|
|
8432
|
+
updates.protocol = parsed.protocol;
|
|
8433
|
+
diff.push({ property: "protocol", before: server.protocol ?? null, after: parsed.protocol });
|
|
8434
|
+
}
|
|
8435
|
+
if (parsed.certificate !== void 0 && server.certificate !== parsed.certificate) {
|
|
8436
|
+
updates.certificate = parsed.certificate;
|
|
8437
|
+
diff.push({ property: "certificate", before: server.certificate ?? null, after: parsed.certificate });
|
|
8438
|
+
}
|
|
8439
|
+
if (parsed.cipher !== void 0 && server.cipher !== parsed.cipher) {
|
|
8440
|
+
updates.cipher = parsed.cipher;
|
|
8441
|
+
diff.push({ property: "cipher", before: server.cipher ?? null, after: parsed.cipher });
|
|
8442
|
+
}
|
|
8443
|
+
if (parsed.auth !== void 0 && server.auth !== parsed.auth) {
|
|
8444
|
+
updates.auth = parsed.auth;
|
|
8445
|
+
diff.push({ property: "auth", before: server.auth ?? null, after: parsed.auth });
|
|
8446
|
+
}
|
|
8447
|
+
if (Object.keys(updates).length === 0) {
|
|
8448
|
+
return {
|
|
8449
|
+
content: `OpenVPN server on ${context.routerId} already matches the requested configuration. No changes made.`,
|
|
8450
|
+
structuredContent: { action: "no_change", server }
|
|
8451
|
+
};
|
|
8452
|
+
}
|
|
8453
|
+
if (parsed.dryRun) {
|
|
8454
|
+
return {
|
|
8455
|
+
content: `Dry run: Would update OpenVPN server on ${context.routerId}.`,
|
|
8456
|
+
structuredContent: { action: "dry_run", diff }
|
|
8457
|
+
};
|
|
8458
|
+
}
|
|
8459
|
+
await context.routerClient.update(OVPN_SERVER_PATH, serverId, updates);
|
|
8460
|
+
log37.info({ routerId: context.routerId }, "OpenVPN server configuration updated");
|
|
8461
|
+
return {
|
|
8462
|
+
content: `Updated OpenVPN server configuration on ${context.routerId}.`,
|
|
8463
|
+
structuredContent: { action: "updated", diff }
|
|
8464
|
+
};
|
|
8465
|
+
} catch (err) {
|
|
8466
|
+
if (err instanceof MikroMCPError) throw err;
|
|
8467
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_server" });
|
|
8468
|
+
}
|
|
8469
|
+
}
|
|
8470
|
+
};
|
|
8471
|
+
var ovpnTools = [
|
|
8472
|
+
listOvpnClientsTool,
|
|
8473
|
+
manageOvpnClientTool,
|
|
8474
|
+
getOvpnServerTool,
|
|
8475
|
+
manageOvpnServerTool
|
|
8476
|
+
];
|
|
8477
|
+
|
|
8478
|
+
// src/domain/tools/fleet-tools.ts
|
|
8479
|
+
import { z as z37 } from "zod";
|
|
8480
|
+
|
|
8481
|
+
// src/middleware/authz.ts
|
|
8482
|
+
function matchesPattern(name, pattern) {
|
|
8483
|
+
if (!pattern.includes("*")) return name === pattern;
|
|
8484
|
+
const prefix = pattern.slice(0, pattern.indexOf("*"));
|
|
8485
|
+
return name.startsWith(prefix);
|
|
8486
|
+
}
|
|
8487
|
+
function checkAuthz(identity, toolName, routerId) {
|
|
8488
|
+
if (identity.allowedRouters.length > 0 && !identity.allowedRouters.includes(routerId)) {
|
|
8489
|
+
throw new MikroMCPError({
|
|
8490
|
+
category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
|
|
8491
|
+
code: "ROUTER_NOT_ALLOWED",
|
|
8492
|
+
message: `Identity "${identity.id}" is not allowed to access router "${routerId}".`,
|
|
8493
|
+
details: { identityId: identity.id, routerId, allowedRouters: identity.allowedRouters },
|
|
8494
|
+
recoverability: {
|
|
8495
|
+
retryable: false,
|
|
8496
|
+
suggestedAction: `Add "${routerId}" to allowedRouters for identity "${identity.id}" in identities.yaml.`
|
|
8497
|
+
}
|
|
8498
|
+
});
|
|
8499
|
+
}
|
|
8500
|
+
if (identity.allowedToolPatterns.length > 0) {
|
|
8501
|
+
const allowed = identity.allowedToolPatterns.some((p) => matchesPattern(toolName, p));
|
|
8502
|
+
if (!allowed) {
|
|
8503
|
+
throw new MikroMCPError({
|
|
8504
|
+
category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
|
|
8505
|
+
code: "TOOL_NOT_ALLOWED",
|
|
8506
|
+
message: `Identity "${identity.id}" is not allowed to call tool "${toolName}".`,
|
|
8507
|
+
details: { identityId: identity.id, toolName, allowedToolPatterns: identity.allowedToolPatterns },
|
|
8508
|
+
recoverability: {
|
|
8509
|
+
retryable: false,
|
|
8510
|
+
suggestedAction: `Add a matching pattern to allowedToolPatterns for identity "${identity.id}" in identities.yaml.`
|
|
8511
|
+
}
|
|
8512
|
+
});
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8516
|
+
|
|
8517
|
+
// src/config/secrets.ts
|
|
8518
|
+
function getCredentials(router) {
|
|
7306
8519
|
const { source, envPrefix, vaultPath } = router.credentials;
|
|
7307
8520
|
if (source === "vault") {
|
|
7308
8521
|
throw new MikroMCPError({
|
|
@@ -7431,7 +8644,7 @@ var SshClient = class {
|
|
|
7431
8644
|
// src/adapter/ftp-client.ts
|
|
7432
8645
|
import { Client as Client2 } from "basic-ftp";
|
|
7433
8646
|
import { Readable } from "stream";
|
|
7434
|
-
var
|
|
8647
|
+
var log38 = createLogger("ftp-client");
|
|
7435
8648
|
var FtpClient = class {
|
|
7436
8649
|
#host;
|
|
7437
8650
|
#port;
|
|
@@ -7444,7 +8657,7 @@ var FtpClient = class {
|
|
|
7444
8657
|
this.#password = password;
|
|
7445
8658
|
}
|
|
7446
8659
|
async upload(remoteName, content) {
|
|
7447
|
-
|
|
8660
|
+
log38.info({ host: this.#host, remoteName }, "Uploading file via FTP");
|
|
7448
8661
|
const client = new Client2();
|
|
7449
8662
|
try {
|
|
7450
8663
|
await client.access({
|
|
@@ -7454,7 +8667,7 @@ var FtpClient = class {
|
|
|
7454
8667
|
password: this.#password
|
|
7455
8668
|
});
|
|
7456
8669
|
await client.uploadFrom(Readable.from(Buffer.from(content, "utf-8")), remoteName);
|
|
7457
|
-
|
|
8670
|
+
log38.info({ host: this.#host, remoteName }, "FTP upload complete");
|
|
7458
8671
|
} finally {
|
|
7459
8672
|
client.close();
|
|
7460
8673
|
}
|
|
@@ -7562,17 +8775,17 @@ function checkFleetConfirmation(args, secret) {
|
|
|
7562
8775
|
}
|
|
7563
8776
|
|
|
7564
8777
|
// src/domain/tools/fleet-tools.ts
|
|
7565
|
-
var
|
|
7566
|
-
var checkHealthInputSchema =
|
|
7567
|
-
routerId:
|
|
8778
|
+
var log39 = createLogger("fleet-tools");
|
|
8779
|
+
var checkHealthInputSchema = z37.object({
|
|
8780
|
+
routerId: z37.string().describe("Target router to health-check")
|
|
7568
8781
|
}).strict();
|
|
7569
|
-
var bulkExecuteInputSchema =
|
|
7570
|
-
toolName:
|
|
7571
|
-
routerIds:
|
|
7572
|
-
tags:
|
|
7573
|
-
params:
|
|
7574
|
-
concurrency:
|
|
7575
|
-
confirmationToken:
|
|
8782
|
+
var bulkExecuteInputSchema = z37.object({
|
|
8783
|
+
toolName: z37.string().describe("Name of the tool to fan out (must be a single-router tool)"),
|
|
8784
|
+
routerIds: z37.array(z37.string()).optional().describe("Explicit list of router IDs to target"),
|
|
8785
|
+
tags: z37.array(z37.string()).optional().describe("Target all routers with ALL of these tags (mutually exclusive with routerIds)"),
|
|
8786
|
+
params: z37.record(z37.string(), z37.unknown()).describe("Params to pass to the tool (omit routerId \u2014 injected per router)"),
|
|
8787
|
+
concurrency: z37.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls"),
|
|
8788
|
+
confirmationToken: z37.string().optional().describe(
|
|
7576
8789
|
"Fleet confirmation token from a prior APPROVAL_REQUIRED response. Required to fan out a destructive tool."
|
|
7577
8790
|
)
|
|
7578
8791
|
}).strict();
|
|
@@ -7591,7 +8804,7 @@ function createFleetTools(baseTools2) {
|
|
|
7591
8804
|
},
|
|
7592
8805
|
async handler(params, context) {
|
|
7593
8806
|
checkHealthInputSchema.parse(params);
|
|
7594
|
-
|
|
8807
|
+
log39.info({ routerId: context.routerId }, "Checking router health");
|
|
7595
8808
|
const startMs = Date.now();
|
|
7596
8809
|
try {
|
|
7597
8810
|
const resources = await context.routerClient.get("system/resource");
|
|
@@ -7653,7 +8866,7 @@ function createFleetTools(baseTools2) {
|
|
|
7653
8866
|
}
|
|
7654
8867
|
});
|
|
7655
8868
|
}
|
|
7656
|
-
|
|
8869
|
+
log39.info(
|
|
7657
8870
|
{ toolName: parsed.toolName, concurrency: parsed.concurrency },
|
|
7658
8871
|
"bulk_execute invoked"
|
|
7659
8872
|
);
|
|
@@ -7700,7 +8913,7 @@ function createFleetTools(baseTools2) {
|
|
|
7700
8913
|
try {
|
|
7701
8914
|
resolved.push(context.routerRegistry.getRouter(id));
|
|
7702
8915
|
} catch {
|
|
7703
|
-
|
|
8916
|
+
log39.warn({ routerId: id }, "bulk_execute: router not found");
|
|
7704
8917
|
preErrors.push({
|
|
7705
8918
|
routerId: id,
|
|
7706
8919
|
status: "error",
|
|
@@ -7872,9 +9085,15 @@ var baseTools = [
|
|
|
7872
9085
|
...certificateTools,
|
|
7873
9086
|
...userTools,
|
|
7874
9087
|
...dhcpServerTools,
|
|
9088
|
+
...ipPoolTools,
|
|
7875
9089
|
...queueTools,
|
|
7876
9090
|
...vrrpTools,
|
|
7877
|
-
...networkServicesTools
|
|
9091
|
+
...networkServicesTools,
|
|
9092
|
+
...vlanTools,
|
|
9093
|
+
...dhcpClientTools,
|
|
9094
|
+
...ipServiceTools,
|
|
9095
|
+
...pppoeTools,
|
|
9096
|
+
...ovpnTools
|
|
7878
9097
|
];
|
|
7879
9098
|
var allTools = [
|
|
7880
9099
|
...baseTools,
|
|
@@ -8273,7 +9492,7 @@ var CircuitBreaker = class {
|
|
|
8273
9492
|
};
|
|
8274
9493
|
|
|
8275
9494
|
// src/mcp/tool-executor.ts
|
|
8276
|
-
var
|
|
9495
|
+
var log40 = createLogger("tool-executor");
|
|
8277
9496
|
async function executeToolCall(tool, args, deps) {
|
|
8278
9497
|
const { registry, pool, circuitBreakers, config, identityRegistry } = deps;
|
|
8279
9498
|
const correlationId = nanoid4();
|
|
@@ -8369,9 +9588,9 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8369
9588
|
try {
|
|
8370
9589
|
const meta = await takeSnapshot(toolContext.routerClient, routerId, path, config.snapshotDir);
|
|
8371
9590
|
snapshotIds.push(meta.id);
|
|
8372
|
-
|
|
9591
|
+
log40.debug({ snapshotId: meta.id, path }, "Snapshot taken");
|
|
8373
9592
|
} catch (err) {
|
|
8374
|
-
|
|
9593
|
+
log40.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
|
|
8375
9594
|
}
|
|
8376
9595
|
}
|
|
8377
9596
|
}
|
|
@@ -8413,7 +9632,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8413
9632
|
durationMs: Date.now() - startMs
|
|
8414
9633
|
}, config.auditLogPath);
|
|
8415
9634
|
}
|
|
8416
|
-
|
|
9635
|
+
log40.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
|
|
8417
9636
|
recordToolCall(tool.name, "success");
|
|
8418
9637
|
return formatToolResult(result);
|
|
8419
9638
|
} catch (err) {
|
|
@@ -8422,7 +9641,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8422
9641
|
const failedRouterId = args.routerId;
|
|
8423
9642
|
if (failedRouterId) {
|
|
8424
9643
|
pool.removeClient(failedRouterId);
|
|
8425
|
-
|
|
9644
|
+
log40.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
|
|
8426
9645
|
}
|
|
8427
9646
|
}
|
|
8428
9647
|
if (journalId) {
|
|
@@ -8452,7 +9671,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8452
9671
|
durationMs: Date.now() - startMs
|
|
8453
9672
|
}, config.auditLogPath);
|
|
8454
9673
|
}
|
|
8455
|
-
|
|
9674
|
+
log40.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
|
|
8456
9675
|
recordToolCall(tool.name, "error");
|
|
8457
9676
|
return formatError(error);
|
|
8458
9677
|
}
|
|
@@ -8460,12 +9679,12 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8460
9679
|
}
|
|
8461
9680
|
|
|
8462
9681
|
// src/mcp/tool-registry.ts
|
|
8463
|
-
var
|
|
9682
|
+
var log41 = createLogger("tool-registry");
|
|
8464
9683
|
function registerAllTools(server, registry, pool, circuitBreakers, config, identityRegistry) {
|
|
8465
9684
|
const deps = { registry, pool, circuitBreakers, config, identityRegistry };
|
|
8466
9685
|
for (const tool of allTools) {
|
|
8467
9686
|
const registrationSchema = tool.annotations.destructiveHint ? tool.inputSchema.extend({
|
|
8468
|
-
confirmationToken:
|
|
9687
|
+
confirmationToken: z38.string().optional().describe(
|
|
8469
9688
|
"Token from a prior APPROVAL_REQUIRED response. Re-submit the identical call with this token to confirm the destructive action."
|
|
8470
9689
|
)
|
|
8471
9690
|
}) : tool.inputSchema;
|
|
@@ -8479,56 +9698,56 @@ function registerAllTools(server, registry, pool, circuitBreakers, config, ident
|
|
|
8479
9698
|
},
|
|
8480
9699
|
(args) => executeToolCall(tool, args, deps)
|
|
8481
9700
|
);
|
|
8482
|
-
|
|
9701
|
+
log41.debug({ tool: tool.name }, "Registered tool");
|
|
8483
9702
|
}
|
|
8484
|
-
|
|
9703
|
+
log41.info({ count: allTools.length }, "All tools registered");
|
|
8485
9704
|
}
|
|
8486
9705
|
|
|
8487
9706
|
// src/config/router-registry.ts
|
|
8488
9707
|
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
8489
9708
|
import { parse } from "yaml";
|
|
8490
|
-
import { z as
|
|
8491
|
-
var
|
|
8492
|
-
var RouterConfigSchema =
|
|
8493
|
-
host:
|
|
8494
|
-
port:
|
|
8495
|
-
tls:
|
|
8496
|
-
enabled:
|
|
8497
|
-
rejectUnauthorized:
|
|
8498
|
-
ca:
|
|
8499
|
-
fingerprint:
|
|
9709
|
+
import { z as z39 } from "zod";
|
|
9710
|
+
var log42 = createLogger("router-registry");
|
|
9711
|
+
var RouterConfigSchema = z39.object({
|
|
9712
|
+
host: z39.string().min(1, "host is required"),
|
|
9713
|
+
port: z39.number().int().min(1).max(65535),
|
|
9714
|
+
tls: z39.object({
|
|
9715
|
+
enabled: z39.boolean(),
|
|
9716
|
+
rejectUnauthorized: z39.boolean(),
|
|
9717
|
+
ca: z39.string().optional(),
|
|
9718
|
+
fingerprint: z39.string().optional()
|
|
8500
9719
|
}).strict(),
|
|
8501
|
-
credentials:
|
|
8502
|
-
source:
|
|
8503
|
-
envPrefix:
|
|
8504
|
-
vaultPath:
|
|
9720
|
+
credentials: z39.object({
|
|
9721
|
+
source: z39.enum(["env", "vault"]),
|
|
9722
|
+
envPrefix: z39.string().optional(),
|
|
9723
|
+
vaultPath: z39.string().optional()
|
|
8505
9724
|
}).strict(),
|
|
8506
|
-
tags:
|
|
8507
|
-
rosVersion:
|
|
8508
|
-
sshPort:
|
|
8509
|
-
sshFingerprint:
|
|
8510
|
-
cmdAllow:
|
|
8511
|
-
cmdDeny:
|
|
8512
|
-
maintenanceWindows:
|
|
8513
|
-
|
|
8514
|
-
days:
|
|
8515
|
-
|
|
9725
|
+
tags: z39.array(z39.string()).default([]),
|
|
9726
|
+
rosVersion: z39.string().min(1),
|
|
9727
|
+
sshPort: z39.number().int().min(1).max(65535).optional(),
|
|
9728
|
+
sshFingerprint: z39.string().optional(),
|
|
9729
|
+
cmdAllow: z39.array(z39.string()).optional(),
|
|
9730
|
+
cmdDeny: z39.array(z39.string()).optional(),
|
|
9731
|
+
maintenanceWindows: z39.array(
|
|
9732
|
+
z39.object({
|
|
9733
|
+
days: z39.array(
|
|
9734
|
+
z39.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
|
|
8516
9735
|
),
|
|
8517
|
-
startTime:
|
|
8518
|
-
endTime:
|
|
8519
|
-
timezone:
|
|
9736
|
+
startTime: z39.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
|
|
9737
|
+
endTime: z39.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
|
|
9738
|
+
timezone: z39.string().min(1)
|
|
8520
9739
|
}).strict()
|
|
8521
9740
|
).optional()
|
|
8522
9741
|
}).strict();
|
|
8523
|
-
var ConfigFileSchema =
|
|
8524
|
-
routers:
|
|
9742
|
+
var ConfigFileSchema = z39.object({
|
|
9743
|
+
routers: z39.record(z39.string(), RouterConfigSchema)
|
|
8525
9744
|
}).strict();
|
|
8526
9745
|
var RouterRegistry = class {
|
|
8527
9746
|
routers;
|
|
8528
9747
|
constructor(configPath) {
|
|
8529
9748
|
this.routers = /* @__PURE__ */ new Map();
|
|
8530
9749
|
if (!existsSync(configPath)) {
|
|
8531
|
-
|
|
9750
|
+
log42.warn({ configPath }, "Router config file not found; starting with empty registry");
|
|
8532
9751
|
return;
|
|
8533
9752
|
}
|
|
8534
9753
|
const raw = readFileSync2(configPath, "utf-8");
|
|
@@ -8542,14 +9761,14 @@ ${issues}`);
|
|
|
8542
9761
|
for (const [id, config] of Object.entries(result.data.routers)) {
|
|
8543
9762
|
const routerConfig = { ...config, id };
|
|
8544
9763
|
if (routerConfig.tls.enabled && !routerConfig.tls.rejectUnauthorized) {
|
|
8545
|
-
|
|
9764
|
+
log42.warn(
|
|
8546
9765
|
{ routerId: id },
|
|
8547
9766
|
"TLS certificate validation is DISABLED (rejectUnauthorized=false). Set tls.fingerprint to pin the server certificate instead."
|
|
8548
9767
|
);
|
|
8549
9768
|
}
|
|
8550
9769
|
this.routers.set(id, routerConfig);
|
|
8551
9770
|
}
|
|
8552
|
-
|
|
9771
|
+
log42.info({ count: this.routers.size }, "Loaded routers from config");
|
|
8553
9772
|
}
|
|
8554
9773
|
getRouter(id) {
|
|
8555
9774
|
const router = this.routers.get(id);
|
|
@@ -8834,7 +10053,7 @@ var ConnectionPool = class {
|
|
|
8834
10053
|
};
|
|
8835
10054
|
|
|
8836
10055
|
// src/version.ts
|
|
8837
|
-
var VERSION = "1.
|
|
10056
|
+
var VERSION = "1.3.0";
|
|
8838
10057
|
|
|
8839
10058
|
// src/mcp/server.ts
|
|
8840
10059
|
function createServerFactory(config, identityRegistry) {
|
|
@@ -8864,7 +10083,7 @@ import { randomUUID } from "crypto";
|
|
|
8864
10083
|
import { createServer } from "http";
|
|
8865
10084
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8866
10085
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
8867
|
-
var
|
|
10086
|
+
var log43 = createLogger("transport-http");
|
|
8868
10087
|
var BodyTooLargeError = class extends Error {
|
|
8869
10088
|
constructor() {
|
|
8870
10089
|
super("Request body exceeds maximum allowed size");
|
|
@@ -8956,7 +10175,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
8956
10175
|
return;
|
|
8957
10176
|
}
|
|
8958
10177
|
if (!checkRateLimit(ip)) {
|
|
8959
|
-
|
|
10178
|
+
log43.warn({ ip }, "Rate limit exceeded");
|
|
8960
10179
|
res.writeHead(429, { "Content-Type": "application/json" });
|
|
8961
10180
|
res.end(JSON.stringify({ error: "Too many requests. Retry after 60 seconds." }));
|
|
8962
10181
|
return;
|
|
@@ -8997,7 +10216,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
8997
10216
|
}
|
|
8998
10217
|
return;
|
|
8999
10218
|
}
|
|
9000
|
-
|
|
10219
|
+
log43.error({ err }, "HTTP transport error");
|
|
9001
10220
|
if (!res.headersSent) {
|
|
9002
10221
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
9003
10222
|
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
@@ -9007,7 +10226,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
9007
10226
|
httpServer.on("close", () => clearInterval(sweepTimer));
|
|
9008
10227
|
await new Promise((resolve, reject) => {
|
|
9009
10228
|
httpServer.listen(port, bindHost, () => {
|
|
9010
|
-
|
|
10229
|
+
log43.info(
|
|
9011
10230
|
{ port, bindHost },
|
|
9012
10231
|
"HTTP transport listening (POST /mcp for Streamable HTTP, GET /sse for legacy SSE)"
|
|
9013
10232
|
);
|
|
@@ -9031,13 +10250,13 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
9031
10250
|
sessionIdGenerator: () => randomUUID(),
|
|
9032
10251
|
onsessioninitialized: (id) => {
|
|
9033
10252
|
streamableSessions.set(id, transport2);
|
|
9034
|
-
|
|
10253
|
+
log43.info({ sessionId: id }, "Streamable HTTP session created");
|
|
9035
10254
|
}
|
|
9036
10255
|
});
|
|
9037
10256
|
transport2.onclose = () => {
|
|
9038
10257
|
if (transport2.sessionId) {
|
|
9039
10258
|
streamableSessions.delete(transport2.sessionId);
|
|
9040
|
-
|
|
10259
|
+
log43.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
|
|
9041
10260
|
}
|
|
9042
10261
|
};
|
|
9043
10262
|
await makeServer().connect(transport2);
|
|
@@ -9054,7 +10273,7 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
9054
10273
|
sseTransport.onclose = () => {
|
|
9055
10274
|
sseTransports.delete(sseTransport.sessionId);
|
|
9056
10275
|
};
|
|
9057
|
-
|
|
10276
|
+
log43.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
|
|
9058
10277
|
await sseTransport.start();
|
|
9059
10278
|
return;
|
|
9060
10279
|
}
|
|
@@ -9075,23 +10294,23 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
9075
10294
|
// src/config/identity-registry.ts
|
|
9076
10295
|
import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
|
|
9077
10296
|
import { parse as parse2 } from "yaml";
|
|
9078
|
-
import { z as
|
|
10297
|
+
import { z as z40 } from "zod";
|
|
9079
10298
|
import bcrypt from "bcryptjs";
|
|
9080
|
-
var
|
|
9081
|
-
var IdentityConfigSchema =
|
|
9082
|
-
token:
|
|
9083
|
-
role:
|
|
9084
|
-
allowedRouters:
|
|
9085
|
-
allowedToolPatterns:
|
|
10299
|
+
var log44 = createLogger("identity-registry");
|
|
10300
|
+
var IdentityConfigSchema = z40.object({
|
|
10301
|
+
token: z40.string().min(1, "token hash is required"),
|
|
10302
|
+
role: z40.enum(["readonly", "operator", "admin", "superadmin"]),
|
|
10303
|
+
allowedRouters: z40.array(z40.string()).default([]),
|
|
10304
|
+
allowedToolPatterns: z40.array(z40.string()).default([])
|
|
9086
10305
|
}).strict();
|
|
9087
|
-
var ConfigFileSchema2 =
|
|
9088
|
-
identities:
|
|
10306
|
+
var ConfigFileSchema2 = z40.object({
|
|
10307
|
+
identities: z40.record(z40.string(), IdentityConfigSchema)
|
|
9089
10308
|
}).strict();
|
|
9090
10309
|
var IdentityRegistry = class {
|
|
9091
10310
|
identities = [];
|
|
9092
10311
|
constructor(configPath) {
|
|
9093
10312
|
if (!existsSync2(configPath)) {
|
|
9094
|
-
|
|
10313
|
+
log44.warn(
|
|
9095
10314
|
{ configPath },
|
|
9096
10315
|
"Identity config file not found; starting with empty registry (stdio callers do not require it)"
|
|
9097
10316
|
);
|
|
@@ -9114,7 +10333,7 @@ ${issues}`);
|
|
|
9114
10333
|
allowedToolPatterns: config.allowedToolPatterns
|
|
9115
10334
|
});
|
|
9116
10335
|
}
|
|
9117
|
-
|
|
10336
|
+
log44.info({ count: this.identities.length }, "Loaded identities from config");
|
|
9118
10337
|
}
|
|
9119
10338
|
getIdentities() {
|
|
9120
10339
|
return this.identities.map(({ id, role, allowedRouters, allowedToolPatterns }) => ({
|
|
@@ -9143,7 +10362,7 @@ ${issues}`);
|
|
|
9143
10362
|
// src/domain/snapshot/retention.ts
|
|
9144
10363
|
import { readdir, stat, unlink } from "fs/promises";
|
|
9145
10364
|
import { join as join3 } from "path";
|
|
9146
|
-
var
|
|
10365
|
+
var log45 = createLogger("snapshot-retention");
|
|
9147
10366
|
async function pruneSnapshots(snapshotDir, maxAgeDays) {
|
|
9148
10367
|
const cutoffMs = Date.now() - maxAgeDays * 864e5;
|
|
9149
10368
|
let removed = 0;
|
|
@@ -9170,21 +10389,21 @@ async function pruneSnapshots(snapshotDir, maxAgeDays) {
|
|
|
9170
10389
|
removed++;
|
|
9171
10390
|
}
|
|
9172
10391
|
} catch (err) {
|
|
9173
|
-
|
|
10392
|
+
log45.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
|
|
9174
10393
|
}
|
|
9175
10394
|
}
|
|
9176
10395
|
}
|
|
9177
10396
|
if (removed > 0) {
|
|
9178
|
-
|
|
10397
|
+
log45.info({ removed, maxAgeDays }, "Pruned expired snapshots");
|
|
9179
10398
|
}
|
|
9180
10399
|
return removed;
|
|
9181
10400
|
}
|
|
9182
10401
|
|
|
9183
10402
|
// src/cli/serve.ts
|
|
9184
|
-
var
|
|
10403
|
+
var log46 = createLogger("main");
|
|
9185
10404
|
async function runServe() {
|
|
9186
10405
|
const config = loadAppConfig();
|
|
9187
|
-
|
|
10406
|
+
log46.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
|
|
9188
10407
|
void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
|
|
9189
10408
|
const identityRegistry = new IdentityRegistry(config.identitiesPath);
|
|
9190
10409
|
if (config.transport === "http") {
|
|
@@ -9192,7 +10411,7 @@ async function runServe() {
|
|
|
9192
10411
|
(i) => i.role === "readonly" || i.role === "operator"
|
|
9193
10412
|
);
|
|
9194
10413
|
if (hasLimitedRoles && !config.confirmationSecret) {
|
|
9195
|
-
|
|
10414
|
+
log46.error(
|
|
9196
10415
|
"MIKROMCP_CONFIRMATION_SECRET is required in HTTP mode when identities with role readonly or operator are configured"
|
|
9197
10416
|
);
|
|
9198
10417
|
process.exit(1);
|
|
@@ -9202,7 +10421,7 @@ async function runServe() {
|
|
|
9202
10421
|
if (config.transport === "stdio") {
|
|
9203
10422
|
const stdioIdentity = getStdioIdentity(config.stdioIdentity, identityRegistry);
|
|
9204
10423
|
await withIdentity(stdioIdentity, () => connectStdio(makeServer()));
|
|
9205
|
-
|
|
10424
|
+
log46.info("MikroMCP server running via stdio");
|
|
9206
10425
|
} else if (config.transport === "http") {
|
|
9207
10426
|
await connectHttp(
|
|
9208
10427
|
makeServer,
|
|
@@ -9214,13 +10433,13 @@ async function runServe() {
|
|
|
9214
10433
|
},
|
|
9215
10434
|
identityRegistry
|
|
9216
10435
|
);
|
|
9217
|
-
|
|
10436
|
+
log46.info(
|
|
9218
10437
|
{ port: config.port, bindHost: config.bindHost },
|
|
9219
10438
|
"MikroMCP server running via HTTP/SSE"
|
|
9220
10439
|
);
|
|
9221
10440
|
}
|
|
9222
10441
|
const shutdown = () => {
|
|
9223
|
-
|
|
10442
|
+
log46.info("Shutting down MikroMCP server");
|
|
9224
10443
|
pool.closeAll();
|
|
9225
10444
|
process.exit(0);
|
|
9226
10445
|
};
|
|
@@ -9233,7 +10452,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
|
9233
10452
|
import { homedir as homedir2 } from "os";
|
|
9234
10453
|
import * as net from "net";
|
|
9235
10454
|
import chalk from "chalk";
|
|
9236
|
-
var localVersion = "1.
|
|
10455
|
+
var localVersion = "1.3.0";
|
|
9237
10456
|
var results = [];
|
|
9238
10457
|
function ok(msg) {
|
|
9239
10458
|
console.log(chalk.green("\u2705 ") + msg);
|
|
@@ -9885,7 +11104,7 @@ async function runInit() {
|
|
|
9885
11104
|
}
|
|
9886
11105
|
|
|
9887
11106
|
// src/cli/index.ts
|
|
9888
|
-
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.
|
|
11107
|
+
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.3.0");
|
|
9889
11108
|
program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
|
|
9890
11109
|
program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
|
|
9891
11110
|
program.command("init").description("Interactive setup wizard").action(runInit);
|