mikromcp 1.1.0 → 1.2.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 +14 -8
- package/dist/main.js +894 -377
- 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 z36 } 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 isRunning = rec.running === true || rec.running === "true";
|
|
501
|
+
const isDisabled = rec.disabled === true || rec.disabled === "true";
|
|
502
|
+
const enriched = { status: isDisabled ? "disabled" : isRunning ? "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,8 +7269,514 @@ var networkServicesTools = [
|
|
|
7262
7269
|
listArpEntriesTool
|
|
7263
7270
|
];
|
|
7264
7271
|
|
|
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, "");
|
|
7278
|
+
}
|
|
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
|
|
7310
|
+
});
|
|
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
|
+
|
|
7265
7778
|
// src/domain/tools/fleet-tools.ts
|
|
7266
|
-
import { z as
|
|
7779
|
+
import { z as z35 } from "zod";
|
|
7267
7780
|
|
|
7268
7781
|
// src/middleware/authz.ts
|
|
7269
7782
|
function matchesPattern(name, pattern) {
|
|
@@ -7431,7 +7944,7 @@ var SshClient = class {
|
|
|
7431
7944
|
// src/adapter/ftp-client.ts
|
|
7432
7945
|
import { Client as Client2 } from "basic-ftp";
|
|
7433
7946
|
import { Readable } from "stream";
|
|
7434
|
-
var
|
|
7947
|
+
var log36 = createLogger("ftp-client");
|
|
7435
7948
|
var FtpClient = class {
|
|
7436
7949
|
#host;
|
|
7437
7950
|
#port;
|
|
@@ -7444,7 +7957,7 @@ var FtpClient = class {
|
|
|
7444
7957
|
this.#password = password;
|
|
7445
7958
|
}
|
|
7446
7959
|
async upload(remoteName, content) {
|
|
7447
|
-
|
|
7960
|
+
log36.info({ host: this.#host, remoteName }, "Uploading file via FTP");
|
|
7448
7961
|
const client = new Client2();
|
|
7449
7962
|
try {
|
|
7450
7963
|
await client.access({
|
|
@@ -7454,7 +7967,7 @@ var FtpClient = class {
|
|
|
7454
7967
|
password: this.#password
|
|
7455
7968
|
});
|
|
7456
7969
|
await client.uploadFrom(Readable.from(Buffer.from(content, "utf-8")), remoteName);
|
|
7457
|
-
|
|
7970
|
+
log36.info({ host: this.#host, remoteName }, "FTP upload complete");
|
|
7458
7971
|
} finally {
|
|
7459
7972
|
client.close();
|
|
7460
7973
|
}
|
|
@@ -7562,17 +8075,17 @@ function checkFleetConfirmation(args, secret) {
|
|
|
7562
8075
|
}
|
|
7563
8076
|
|
|
7564
8077
|
// src/domain/tools/fleet-tools.ts
|
|
7565
|
-
var
|
|
7566
|
-
var checkHealthInputSchema =
|
|
7567
|
-
routerId:
|
|
8078
|
+
var log37 = createLogger("fleet-tools");
|
|
8079
|
+
var checkHealthInputSchema = z35.object({
|
|
8080
|
+
routerId: z35.string().describe("Target router to health-check")
|
|
7568
8081
|
}).strict();
|
|
7569
|
-
var bulkExecuteInputSchema =
|
|
7570
|
-
toolName:
|
|
7571
|
-
routerIds:
|
|
7572
|
-
tags:
|
|
7573
|
-
params:
|
|
7574
|
-
concurrency:
|
|
7575
|
-
confirmationToken:
|
|
8082
|
+
var bulkExecuteInputSchema = z35.object({
|
|
8083
|
+
toolName: z35.string().describe("Name of the tool to fan out (must be a single-router tool)"),
|
|
8084
|
+
routerIds: z35.array(z35.string()).optional().describe("Explicit list of router IDs to target"),
|
|
8085
|
+
tags: z35.array(z35.string()).optional().describe("Target all routers with ALL of these tags (mutually exclusive with routerIds)"),
|
|
8086
|
+
params: z35.record(z35.string(), z35.unknown()).describe("Params to pass to the tool (omit routerId \u2014 injected per router)"),
|
|
8087
|
+
concurrency: z35.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls"),
|
|
8088
|
+
confirmationToken: z35.string().optional().describe(
|
|
7576
8089
|
"Fleet confirmation token from a prior APPROVAL_REQUIRED response. Required to fan out a destructive tool."
|
|
7577
8090
|
)
|
|
7578
8091
|
}).strict();
|
|
@@ -7591,7 +8104,7 @@ function createFleetTools(baseTools2) {
|
|
|
7591
8104
|
},
|
|
7592
8105
|
async handler(params, context) {
|
|
7593
8106
|
checkHealthInputSchema.parse(params);
|
|
7594
|
-
|
|
8107
|
+
log37.info({ routerId: context.routerId }, "Checking router health");
|
|
7595
8108
|
const startMs = Date.now();
|
|
7596
8109
|
try {
|
|
7597
8110
|
const resources = await context.routerClient.get("system/resource");
|
|
@@ -7653,7 +8166,7 @@ function createFleetTools(baseTools2) {
|
|
|
7653
8166
|
}
|
|
7654
8167
|
});
|
|
7655
8168
|
}
|
|
7656
|
-
|
|
8169
|
+
log37.info(
|
|
7657
8170
|
{ toolName: parsed.toolName, concurrency: parsed.concurrency },
|
|
7658
8171
|
"bulk_execute invoked"
|
|
7659
8172
|
);
|
|
@@ -7700,7 +8213,7 @@ function createFleetTools(baseTools2) {
|
|
|
7700
8213
|
try {
|
|
7701
8214
|
resolved.push(context.routerRegistry.getRouter(id));
|
|
7702
8215
|
} catch {
|
|
7703
|
-
|
|
8216
|
+
log37.warn({ routerId: id }, "bulk_execute: router not found");
|
|
7704
8217
|
preErrors.push({
|
|
7705
8218
|
routerId: id,
|
|
7706
8219
|
status: "error",
|
|
@@ -7872,9 +8385,13 @@ var baseTools = [
|
|
|
7872
8385
|
...certificateTools,
|
|
7873
8386
|
...userTools,
|
|
7874
8387
|
...dhcpServerTools,
|
|
8388
|
+
...ipPoolTools,
|
|
7875
8389
|
...queueTools,
|
|
7876
8390
|
...vrrpTools,
|
|
7877
|
-
...networkServicesTools
|
|
8391
|
+
...networkServicesTools,
|
|
8392
|
+
...vlanTools,
|
|
8393
|
+
...dhcpClientTools,
|
|
8394
|
+
...ipServiceTools
|
|
7878
8395
|
];
|
|
7879
8396
|
var allTools = [
|
|
7880
8397
|
...baseTools,
|
|
@@ -8273,7 +8790,7 @@ var CircuitBreaker = class {
|
|
|
8273
8790
|
};
|
|
8274
8791
|
|
|
8275
8792
|
// src/mcp/tool-executor.ts
|
|
8276
|
-
var
|
|
8793
|
+
var log38 = createLogger("tool-executor");
|
|
8277
8794
|
async function executeToolCall(tool, args, deps) {
|
|
8278
8795
|
const { registry, pool, circuitBreakers, config, identityRegistry } = deps;
|
|
8279
8796
|
const correlationId = nanoid4();
|
|
@@ -8369,9 +8886,9 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8369
8886
|
try {
|
|
8370
8887
|
const meta = await takeSnapshot(toolContext.routerClient, routerId, path, config.snapshotDir);
|
|
8371
8888
|
snapshotIds.push(meta.id);
|
|
8372
|
-
|
|
8889
|
+
log38.debug({ snapshotId: meta.id, path }, "Snapshot taken");
|
|
8373
8890
|
} catch (err) {
|
|
8374
|
-
|
|
8891
|
+
log38.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
|
|
8375
8892
|
}
|
|
8376
8893
|
}
|
|
8377
8894
|
}
|
|
@@ -8413,7 +8930,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8413
8930
|
durationMs: Date.now() - startMs
|
|
8414
8931
|
}, config.auditLogPath);
|
|
8415
8932
|
}
|
|
8416
|
-
|
|
8933
|
+
log38.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
|
|
8417
8934
|
recordToolCall(tool.name, "success");
|
|
8418
8935
|
return formatToolResult(result);
|
|
8419
8936
|
} catch (err) {
|
|
@@ -8422,7 +8939,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8422
8939
|
const failedRouterId = args.routerId;
|
|
8423
8940
|
if (failedRouterId) {
|
|
8424
8941
|
pool.removeClient(failedRouterId);
|
|
8425
|
-
|
|
8942
|
+
log38.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
|
|
8426
8943
|
}
|
|
8427
8944
|
}
|
|
8428
8945
|
if (journalId) {
|
|
@@ -8452,7 +8969,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8452
8969
|
durationMs: Date.now() - startMs
|
|
8453
8970
|
}, config.auditLogPath);
|
|
8454
8971
|
}
|
|
8455
|
-
|
|
8972
|
+
log38.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
|
|
8456
8973
|
recordToolCall(tool.name, "error");
|
|
8457
8974
|
return formatError(error);
|
|
8458
8975
|
}
|
|
@@ -8460,12 +8977,12 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8460
8977
|
}
|
|
8461
8978
|
|
|
8462
8979
|
// src/mcp/tool-registry.ts
|
|
8463
|
-
var
|
|
8980
|
+
var log39 = createLogger("tool-registry");
|
|
8464
8981
|
function registerAllTools(server, registry, pool, circuitBreakers, config, identityRegistry) {
|
|
8465
8982
|
const deps = { registry, pool, circuitBreakers, config, identityRegistry };
|
|
8466
8983
|
for (const tool of allTools) {
|
|
8467
8984
|
const registrationSchema = tool.annotations.destructiveHint ? tool.inputSchema.extend({
|
|
8468
|
-
confirmationToken:
|
|
8985
|
+
confirmationToken: z36.string().optional().describe(
|
|
8469
8986
|
"Token from a prior APPROVAL_REQUIRED response. Re-submit the identical call with this token to confirm the destructive action."
|
|
8470
8987
|
)
|
|
8471
8988
|
}) : tool.inputSchema;
|
|
@@ -8479,56 +8996,56 @@ function registerAllTools(server, registry, pool, circuitBreakers, config, ident
|
|
|
8479
8996
|
},
|
|
8480
8997
|
(args) => executeToolCall(tool, args, deps)
|
|
8481
8998
|
);
|
|
8482
|
-
|
|
8999
|
+
log39.debug({ tool: tool.name }, "Registered tool");
|
|
8483
9000
|
}
|
|
8484
|
-
|
|
9001
|
+
log39.info({ count: allTools.length }, "All tools registered");
|
|
8485
9002
|
}
|
|
8486
9003
|
|
|
8487
9004
|
// src/config/router-registry.ts
|
|
8488
9005
|
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
8489
9006
|
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:
|
|
9007
|
+
import { z as z37 } from "zod";
|
|
9008
|
+
var log40 = createLogger("router-registry");
|
|
9009
|
+
var RouterConfigSchema = z37.object({
|
|
9010
|
+
host: z37.string().min(1, "host is required"),
|
|
9011
|
+
port: z37.number().int().min(1).max(65535),
|
|
9012
|
+
tls: z37.object({
|
|
9013
|
+
enabled: z37.boolean(),
|
|
9014
|
+
rejectUnauthorized: z37.boolean(),
|
|
9015
|
+
ca: z37.string().optional(),
|
|
9016
|
+
fingerprint: z37.string().optional()
|
|
8500
9017
|
}).strict(),
|
|
8501
|
-
credentials:
|
|
8502
|
-
source:
|
|
8503
|
-
envPrefix:
|
|
8504
|
-
vaultPath:
|
|
9018
|
+
credentials: z37.object({
|
|
9019
|
+
source: z37.enum(["env", "vault"]),
|
|
9020
|
+
envPrefix: z37.string().optional(),
|
|
9021
|
+
vaultPath: z37.string().optional()
|
|
8505
9022
|
}).strict(),
|
|
8506
|
-
tags:
|
|
8507
|
-
rosVersion:
|
|
8508
|
-
sshPort:
|
|
8509
|
-
sshFingerprint:
|
|
8510
|
-
cmdAllow:
|
|
8511
|
-
cmdDeny:
|
|
8512
|
-
maintenanceWindows:
|
|
8513
|
-
|
|
8514
|
-
days:
|
|
8515
|
-
|
|
9023
|
+
tags: z37.array(z37.string()).default([]),
|
|
9024
|
+
rosVersion: z37.string().min(1),
|
|
9025
|
+
sshPort: z37.number().int().min(1).max(65535).optional(),
|
|
9026
|
+
sshFingerprint: z37.string().optional(),
|
|
9027
|
+
cmdAllow: z37.array(z37.string()).optional(),
|
|
9028
|
+
cmdDeny: z37.array(z37.string()).optional(),
|
|
9029
|
+
maintenanceWindows: z37.array(
|
|
9030
|
+
z37.object({
|
|
9031
|
+
days: z37.array(
|
|
9032
|
+
z37.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
|
|
8516
9033
|
),
|
|
8517
|
-
startTime:
|
|
8518
|
-
endTime:
|
|
8519
|
-
timezone:
|
|
9034
|
+
startTime: z37.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
|
|
9035
|
+
endTime: z37.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
|
|
9036
|
+
timezone: z37.string().min(1)
|
|
8520
9037
|
}).strict()
|
|
8521
9038
|
).optional()
|
|
8522
9039
|
}).strict();
|
|
8523
|
-
var ConfigFileSchema =
|
|
8524
|
-
routers:
|
|
9040
|
+
var ConfigFileSchema = z37.object({
|
|
9041
|
+
routers: z37.record(z37.string(), RouterConfigSchema)
|
|
8525
9042
|
}).strict();
|
|
8526
9043
|
var RouterRegistry = class {
|
|
8527
9044
|
routers;
|
|
8528
9045
|
constructor(configPath) {
|
|
8529
9046
|
this.routers = /* @__PURE__ */ new Map();
|
|
8530
9047
|
if (!existsSync(configPath)) {
|
|
8531
|
-
|
|
9048
|
+
log40.warn({ configPath }, "Router config file not found; starting with empty registry");
|
|
8532
9049
|
return;
|
|
8533
9050
|
}
|
|
8534
9051
|
const raw = readFileSync2(configPath, "utf-8");
|
|
@@ -8542,14 +9059,14 @@ ${issues}`);
|
|
|
8542
9059
|
for (const [id, config] of Object.entries(result.data.routers)) {
|
|
8543
9060
|
const routerConfig = { ...config, id };
|
|
8544
9061
|
if (routerConfig.tls.enabled && !routerConfig.tls.rejectUnauthorized) {
|
|
8545
|
-
|
|
9062
|
+
log40.warn(
|
|
8546
9063
|
{ routerId: id },
|
|
8547
9064
|
"TLS certificate validation is DISABLED (rejectUnauthorized=false). Set tls.fingerprint to pin the server certificate instead."
|
|
8548
9065
|
);
|
|
8549
9066
|
}
|
|
8550
9067
|
this.routers.set(id, routerConfig);
|
|
8551
9068
|
}
|
|
8552
|
-
|
|
9069
|
+
log40.info({ count: this.routers.size }, "Loaded routers from config");
|
|
8553
9070
|
}
|
|
8554
9071
|
getRouter(id) {
|
|
8555
9072
|
const router = this.routers.get(id);
|
|
@@ -8834,7 +9351,7 @@ var ConnectionPool = class {
|
|
|
8834
9351
|
};
|
|
8835
9352
|
|
|
8836
9353
|
// src/version.ts
|
|
8837
|
-
var VERSION = "1.
|
|
9354
|
+
var VERSION = "1.2.0";
|
|
8838
9355
|
|
|
8839
9356
|
// src/mcp/server.ts
|
|
8840
9357
|
function createServerFactory(config, identityRegistry) {
|
|
@@ -8864,7 +9381,7 @@ import { randomUUID } from "crypto";
|
|
|
8864
9381
|
import { createServer } from "http";
|
|
8865
9382
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8866
9383
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
8867
|
-
var
|
|
9384
|
+
var log41 = createLogger("transport-http");
|
|
8868
9385
|
var BodyTooLargeError = class extends Error {
|
|
8869
9386
|
constructor() {
|
|
8870
9387
|
super("Request body exceeds maximum allowed size");
|
|
@@ -8956,7 +9473,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
8956
9473
|
return;
|
|
8957
9474
|
}
|
|
8958
9475
|
if (!checkRateLimit(ip)) {
|
|
8959
|
-
|
|
9476
|
+
log41.warn({ ip }, "Rate limit exceeded");
|
|
8960
9477
|
res.writeHead(429, { "Content-Type": "application/json" });
|
|
8961
9478
|
res.end(JSON.stringify({ error: "Too many requests. Retry after 60 seconds." }));
|
|
8962
9479
|
return;
|
|
@@ -8997,7 +9514,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
8997
9514
|
}
|
|
8998
9515
|
return;
|
|
8999
9516
|
}
|
|
9000
|
-
|
|
9517
|
+
log41.error({ err }, "HTTP transport error");
|
|
9001
9518
|
if (!res.headersSent) {
|
|
9002
9519
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
9003
9520
|
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
@@ -9007,7 +9524,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
9007
9524
|
httpServer.on("close", () => clearInterval(sweepTimer));
|
|
9008
9525
|
await new Promise((resolve, reject) => {
|
|
9009
9526
|
httpServer.listen(port, bindHost, () => {
|
|
9010
|
-
|
|
9527
|
+
log41.info(
|
|
9011
9528
|
{ port, bindHost },
|
|
9012
9529
|
"HTTP transport listening (POST /mcp for Streamable HTTP, GET /sse for legacy SSE)"
|
|
9013
9530
|
);
|
|
@@ -9031,13 +9548,13 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
9031
9548
|
sessionIdGenerator: () => randomUUID(),
|
|
9032
9549
|
onsessioninitialized: (id) => {
|
|
9033
9550
|
streamableSessions.set(id, transport2);
|
|
9034
|
-
|
|
9551
|
+
log41.info({ sessionId: id }, "Streamable HTTP session created");
|
|
9035
9552
|
}
|
|
9036
9553
|
});
|
|
9037
9554
|
transport2.onclose = () => {
|
|
9038
9555
|
if (transport2.sessionId) {
|
|
9039
9556
|
streamableSessions.delete(transport2.sessionId);
|
|
9040
|
-
|
|
9557
|
+
log41.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
|
|
9041
9558
|
}
|
|
9042
9559
|
};
|
|
9043
9560
|
await makeServer().connect(transport2);
|
|
@@ -9054,7 +9571,7 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
9054
9571
|
sseTransport.onclose = () => {
|
|
9055
9572
|
sseTransports.delete(sseTransport.sessionId);
|
|
9056
9573
|
};
|
|
9057
|
-
|
|
9574
|
+
log41.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
|
|
9058
9575
|
await sseTransport.start();
|
|
9059
9576
|
return;
|
|
9060
9577
|
}
|
|
@@ -9075,23 +9592,23 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
9075
9592
|
// src/config/identity-registry.ts
|
|
9076
9593
|
import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
|
|
9077
9594
|
import { parse as parse2 } from "yaml";
|
|
9078
|
-
import { z as
|
|
9595
|
+
import { z as z38 } from "zod";
|
|
9079
9596
|
import bcrypt from "bcryptjs";
|
|
9080
|
-
var
|
|
9081
|
-
var IdentityConfigSchema =
|
|
9082
|
-
token:
|
|
9083
|
-
role:
|
|
9084
|
-
allowedRouters:
|
|
9085
|
-
allowedToolPatterns:
|
|
9597
|
+
var log42 = createLogger("identity-registry");
|
|
9598
|
+
var IdentityConfigSchema = z38.object({
|
|
9599
|
+
token: z38.string().min(1, "token hash is required"),
|
|
9600
|
+
role: z38.enum(["readonly", "operator", "admin", "superadmin"]),
|
|
9601
|
+
allowedRouters: z38.array(z38.string()).default([]),
|
|
9602
|
+
allowedToolPatterns: z38.array(z38.string()).default([])
|
|
9086
9603
|
}).strict();
|
|
9087
|
-
var ConfigFileSchema2 =
|
|
9088
|
-
identities:
|
|
9604
|
+
var ConfigFileSchema2 = z38.object({
|
|
9605
|
+
identities: z38.record(z38.string(), IdentityConfigSchema)
|
|
9089
9606
|
}).strict();
|
|
9090
9607
|
var IdentityRegistry = class {
|
|
9091
9608
|
identities = [];
|
|
9092
9609
|
constructor(configPath) {
|
|
9093
9610
|
if (!existsSync2(configPath)) {
|
|
9094
|
-
|
|
9611
|
+
log42.warn(
|
|
9095
9612
|
{ configPath },
|
|
9096
9613
|
"Identity config file not found; starting with empty registry (stdio callers do not require it)"
|
|
9097
9614
|
);
|
|
@@ -9114,7 +9631,7 @@ ${issues}`);
|
|
|
9114
9631
|
allowedToolPatterns: config.allowedToolPatterns
|
|
9115
9632
|
});
|
|
9116
9633
|
}
|
|
9117
|
-
|
|
9634
|
+
log42.info({ count: this.identities.length }, "Loaded identities from config");
|
|
9118
9635
|
}
|
|
9119
9636
|
getIdentities() {
|
|
9120
9637
|
return this.identities.map(({ id, role, allowedRouters, allowedToolPatterns }) => ({
|
|
@@ -9143,7 +9660,7 @@ ${issues}`);
|
|
|
9143
9660
|
// src/domain/snapshot/retention.ts
|
|
9144
9661
|
import { readdir, stat, unlink } from "fs/promises";
|
|
9145
9662
|
import { join as join3 } from "path";
|
|
9146
|
-
var
|
|
9663
|
+
var log43 = createLogger("snapshot-retention");
|
|
9147
9664
|
async function pruneSnapshots(snapshotDir, maxAgeDays) {
|
|
9148
9665
|
const cutoffMs = Date.now() - maxAgeDays * 864e5;
|
|
9149
9666
|
let removed = 0;
|
|
@@ -9170,21 +9687,21 @@ async function pruneSnapshots(snapshotDir, maxAgeDays) {
|
|
|
9170
9687
|
removed++;
|
|
9171
9688
|
}
|
|
9172
9689
|
} catch (err) {
|
|
9173
|
-
|
|
9690
|
+
log43.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
|
|
9174
9691
|
}
|
|
9175
9692
|
}
|
|
9176
9693
|
}
|
|
9177
9694
|
if (removed > 0) {
|
|
9178
|
-
|
|
9695
|
+
log43.info({ removed, maxAgeDays }, "Pruned expired snapshots");
|
|
9179
9696
|
}
|
|
9180
9697
|
return removed;
|
|
9181
9698
|
}
|
|
9182
9699
|
|
|
9183
9700
|
// src/cli/serve.ts
|
|
9184
|
-
var
|
|
9701
|
+
var log44 = createLogger("main");
|
|
9185
9702
|
async function runServe() {
|
|
9186
9703
|
const config = loadAppConfig();
|
|
9187
|
-
|
|
9704
|
+
log44.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
|
|
9188
9705
|
void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
|
|
9189
9706
|
const identityRegistry = new IdentityRegistry(config.identitiesPath);
|
|
9190
9707
|
if (config.transport === "http") {
|
|
@@ -9192,7 +9709,7 @@ async function runServe() {
|
|
|
9192
9709
|
(i) => i.role === "readonly" || i.role === "operator"
|
|
9193
9710
|
);
|
|
9194
9711
|
if (hasLimitedRoles && !config.confirmationSecret) {
|
|
9195
|
-
|
|
9712
|
+
log44.error(
|
|
9196
9713
|
"MIKROMCP_CONFIRMATION_SECRET is required in HTTP mode when identities with role readonly or operator are configured"
|
|
9197
9714
|
);
|
|
9198
9715
|
process.exit(1);
|
|
@@ -9202,7 +9719,7 @@ async function runServe() {
|
|
|
9202
9719
|
if (config.transport === "stdio") {
|
|
9203
9720
|
const stdioIdentity = getStdioIdentity(config.stdioIdentity, identityRegistry);
|
|
9204
9721
|
await withIdentity(stdioIdentity, () => connectStdio(makeServer()));
|
|
9205
|
-
|
|
9722
|
+
log44.info("MikroMCP server running via stdio");
|
|
9206
9723
|
} else if (config.transport === "http") {
|
|
9207
9724
|
await connectHttp(
|
|
9208
9725
|
makeServer,
|
|
@@ -9214,13 +9731,13 @@ async function runServe() {
|
|
|
9214
9731
|
},
|
|
9215
9732
|
identityRegistry
|
|
9216
9733
|
);
|
|
9217
|
-
|
|
9734
|
+
log44.info(
|
|
9218
9735
|
{ port: config.port, bindHost: config.bindHost },
|
|
9219
9736
|
"MikroMCP server running via HTTP/SSE"
|
|
9220
9737
|
);
|
|
9221
9738
|
}
|
|
9222
9739
|
const shutdown = () => {
|
|
9223
|
-
|
|
9740
|
+
log44.info("Shutting down MikroMCP server");
|
|
9224
9741
|
pool.closeAll();
|
|
9225
9742
|
process.exit(0);
|
|
9226
9743
|
};
|
|
@@ -9233,7 +9750,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
|
9233
9750
|
import { homedir as homedir2 } from "os";
|
|
9234
9751
|
import * as net from "net";
|
|
9235
9752
|
import chalk from "chalk";
|
|
9236
|
-
var localVersion = "1.
|
|
9753
|
+
var localVersion = "1.2.0";
|
|
9237
9754
|
var results = [];
|
|
9238
9755
|
function ok(msg) {
|
|
9239
9756
|
console.log(chalk.green("\u2705 ") + msg);
|
|
@@ -9885,7 +10402,7 @@ async function runInit() {
|
|
|
9885
10402
|
}
|
|
9886
10403
|
|
|
9887
10404
|
// src/cli/index.ts
|
|
9888
|
-
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.
|
|
10405
|
+
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.2.0");
|
|
9889
10406
|
program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
|
|
9890
10407
|
program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
|
|
9891
10408
|
program.command("init").description("Interactive setup wizard").action(runInit);
|