mikromcp 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +15 -8
  2. package/dist/main.js +1634 -415
  3. 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 z32 } from "zod";
63
+ import { z as z38 } from "zod";
64
64
 
65
65
  // src/domain/tools/system-tools.ts
66
66
  import { z } from "zod";
@@ -446,7 +446,12 @@ var COUNTER_PROPS = [
446
446
  "rx-drop",
447
447
  "tx-error",
448
448
  "rx-error",
449
- "tx-queue-drop"
449
+ "tx-queue-drop",
450
+ "fp-tx-byte",
451
+ "fp-rx-byte",
452
+ "fp-tx-packet",
453
+ "fp-rx-packet",
454
+ "fp-rps-drop"
450
455
  ];
451
456
  var listInterfacesTool = {
452
457
  name: "list_interfaces",
@@ -490,14 +495,17 @@ var listInterfacesTool = {
490
495
  }
491
496
  const total = interfaces.length;
492
497
  const paginated = interfaces.slice(parsed.offset, parsed.offset + parsed.limit);
493
- const results2 = parsed.includeCounters ? paginated : paginated.map((iface) => {
494
- const cleaned = {};
495
- for (const [key2, value] of Object.entries(iface)) {
496
- if (!COUNTER_PROPS.includes(key2)) {
497
- cleaned[key2] = value;
498
+ const results2 = paginated.map((iface) => {
499
+ const rec = iface;
500
+ const isRunning2 = rec.running === true || rec.running === "true";
501
+ const isDisabled = rec.disabled === true || rec.disabled === "true";
502
+ const enriched = { status: isDisabled ? "disabled" : isRunning2 ? "up" : "down" };
503
+ for (const [key2, value] of Object.entries(rec)) {
504
+ if (parsed.includeCounters || !COUNTER_PROPS.includes(key2)) {
505
+ enriched[key2] = value;
498
506
  }
499
507
  }
500
- return cleaned;
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 running = rec.running === "true" ? "UP" : "DOWN";
511
- lines.push(` ${name} [${type}] ${running}`);
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 createVlanInputSchema = z2.object({
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 sanitizeComment2(comment) {
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 = sanitizeComment2(parsed.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 dhcpTools = [listDhcpLeasesTool];
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 sanitizeComment3(comment) {
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 = sanitizeComment3(parsed.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 sanitizeComment4(comment) {
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 = sanitizeComment4(parsed.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
- const filtered = parsed.interface ? allServers.filter(
6076
- (s) => s.interface === parsed.interface
6077
- ) : allServers;
6078
- const servers = filtered.slice(0, parsed.limit);
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} returned (${allServers.length} total)`,
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: allServers.length,
6085
- returned: servers.length
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 listDhcpPoolsInputSchema = z27.object({
6277
- routerId: z27.string().describe("Target router identifier from the router registry"),
6278
- name: z27.string().optional().describe("Filter by pool name (substring match)"),
6279
- limit: z27.number().int().min(1).max(500).default(100).describe("Maximum number of pools to return")
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 listDhcpPoolsTool = {
6282
- name: "list_dhcp_pools",
6283
- title: "List DHCP Pools",
6284
- description: "List IP pools on a MikroTik router.",
6285
- inputSchema: listDhcpPoolsInputSchema,
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 = listDhcpPoolsInputSchema.parse(params);
6294
- log28.info({ routerId: context.routerId }, "Listing DHCP pools");
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
- const filtered = parsed.name ? allPools.filter(
6301
- (p) => (p.name ?? "").includes(parsed.name)
6302
- ) : allPools;
6303
- const pools = filtered.slice(0, parsed.limit);
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: `DHCP pools on ${context.routerId}: ${pools.length} returned (${allPools.length} total)`,
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: allPools.length,
6310
- returned: pools.length
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: "list_dhcp_pools" });
6340
+ throw enrichError(err, { routerId: context.routerId, tool: "list_ip_pools" });
6315
6341
  }
6316
6342
  }
6317
6343
  };
6318
- var manageDhcpPoolInputSchema = z27.object({
6319
- routerId: z27.string().describe("Target router identifier from the router registry"),
6320
- action: z27.enum(["add", "remove"]).describe("Action to perform"),
6321
- name: z27.string().describe("Pool name \u2014 idempotency key"),
6322
- ranges: z27.string().optional().describe("IP range (e.g. '192.168.1.100-192.168.1.200'; required for add)"),
6323
- nextPool: z27.string().optional().describe("Next pool name for overflow"),
6324
- dryRun: z27.boolean().default(false).describe("Preview changes without applying")
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 manageDhcpPoolTool = {
6327
- name: "manage_dhcp_pool",
6328
- title: "Manage DHCP Pool",
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: manageDhcpPoolInputSchema,
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 = manageDhcpPoolInputSchema.parse(params);
6340
- log28.info(
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: "DHCP_POOL_RANGES_REQUIRED",
6357
- message: "ranges is required when adding a DHCP pool.",
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: `DHCP pool "${parsed.name}" already exists with the same ranges. No changes made.`,
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: "DHCP_POOL_CONFLICT",
6375
- message: `DHCP pool "${parsed.name}" exists with different ranges.`,
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: ["manage_dhcp_pool"]
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 DHCP pool "${parsed.name}".`,
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
- log28.info({ name: parsed.name, id: created[".id"] }, "DHCP pool added");
6419
+ log29.info({ name: parsed.name, id: created[".id"] }, "IP pool added");
6405
6420
  return {
6406
- content: `Added DHCP pool "${parsed.name}".`,
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: `DHCP pool "${parsed.name}" not found. Nothing to remove.`,
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 DHCP pool "${parsed.name}".`,
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
- log28.info({ name: parsed.name }, "DHCP pool removed");
6438
+ log29.info({ name: parsed.name }, "IP pool removed");
6427
6439
  return {
6428
- content: `Removed DHCP pool "${parsed.name}".`,
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: "manage_dhcp_pool" });
6445
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ip_pool" });
6434
6446
  }
6435
6447
  }
6436
6448
  };
6437
- var dhcpServerTools = [
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 z28 } from "zod";
6446
- var log29 = createLogger("queue-tools");
6447
- var listQueuesInputSchema = z28.object({
6448
- routerId: z28.string().describe("Target router identifier from the router registry"),
6449
- target: z28.string().optional().describe("Filter by target address (substring match)"),
6450
- limit: z28.number().int().min(1).max(500).default(100).describe("Maximum number of queues to return")
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
- log29.info({ routerId: context.routerId }, "Listing queues");
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 = z28.object({
6490
- routerId: z28.string().describe("Target router identifier from the router registry"),
6491
- action: z28.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
6492
- name: z28.string().describe("Queue name \u2014 idempotency key"),
6493
- target: z28.string().optional().describe("Target address (required for add; e.g. '192.168.1.0/24')"),
6494
- maxLimit: z28.string().optional().describe("Max upload/download limit (e.g. '10M/10M')"),
6495
- limitAt: z28.string().optional().describe("Guaranteed rate (e.g. '1M/1M')"),
6496
- comment: z28.string().optional().describe("Optional comment"),
6497
- dryRun: z28.boolean().default(false).describe("Preview changes without applying")
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
- log29.info(
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
- log29.info({ name: parsed.name, id: created[".id"] }, "Queue added");
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
- log29.info({ name: parsed.name }, "Queue removed");
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
- log29.info({ name: parsed.name, action: resultAction }, "Queue updated");
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 z29 } from "zod";
6652
- var log30 = createLogger("vrrp-tools");
6653
- var listVrrpInstancesInputSchema = z29.object({
6654
- routerId: z29.string().describe("Target router identifier from the router registry"),
6655
- interface: z29.string().optional().describe("Filter by master interface name (exact match)"),
6656
- limit: z29.number().int().min(1).max(500).default(100).describe("Maximum number of instances to return")
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
- log30.info({ routerId: context.routerId }, "Listing VRRP instances");
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 = z29.object({
6696
- routerId: z29.string().describe("Target router identifier from the router registry"),
6697
- action: z29.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
6698
- name: z29.string().describe("VRRP interface name \u2014 idempotency key"),
6699
- interface: z29.string().optional().describe("Master interface (required for add)"),
6700
- vrid: z29.number().int().min(1).max(255).optional().describe("Virtual router ID (required for add)"),
6701
- priority: z29.number().int().min(1).max(254).default(100).describe("Router priority (1\u2013254)"),
6702
- interval: z29.number().int().min(1).optional().describe("Advertisement interval in seconds"),
6703
- version: z29.enum(["2", "3"]).default("3").describe("VRRP protocol version"),
6704
- comment: z29.string().optional().describe("Optional comment"),
6705
- dryRun: z29.boolean().default(false).describe("Preview changes without applying")
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
- log30.info(
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
- log30.info({ name: parsed.name, id: created[".id"] }, "VRRP instance added");
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
- log30.info({ name: parsed.name }, "VRRP instance removed");
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
- log30.info({ name: parsed.name, action: resultAction }, "VRRP instance updated");
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 z30 } from "zod";
6882
- var log31 = createLogger("network-services-tools");
6883
- var getSnmpSettingsInputSchema = z30.object({
6884
- routerId: z30.string().describe("Target router identifier from the router registry")
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
- log31.info({ routerId: context.routerId }, "Fetching SNMP settings");
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 = z30.object({
6929
- routerId: z30.string().describe("Target router identifier from the router registry")
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
- log31.info({ routerId: context.routerId }, "Fetching NTP settings");
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 = z30.object({
6974
- routerId: z30.string().describe("Target router identifier from the router registry"),
6975
- host: z30.string().optional().describe("Filter by host (substring match)"),
6976
- status: z30.enum(["up", "down", "unknown"]).optional().describe("Filter by current status"),
6977
- limit: z30.number().int().min(1).max(500).default(100).describe("Maximum number of entries to return")
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
- log31.info({ routerId: context.routerId }, "Listing Netwatch entries");
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 = z30.object({
7015
- routerId: z30.string().describe("Target router identifier from the router registry"),
7016
- action: z30.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
7017
- host: z30.string().describe("Host to monitor \u2014 idempotency key"),
7018
- port: z30.number().int().min(1).max(65535).optional().describe("TCP port (optional; ICMP if omitted)"),
7019
- interval: z30.string().optional().describe("Check interval (e.g. '1m'; default '1m')"),
7020
- timeout: z30.string().optional().describe("Probe timeout (e.g. '500ms')"),
7021
- comment: z30.string().optional().describe("Optional comment"),
7022
- dryRun: z30.boolean().default(false).describe("Preview changes without applying")
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
- log31.info(
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
- log31.info({ host: parsed.host, id: created[".id"] }, "Netwatch entry added");
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
- log31.info({ host: parsed.host }, "Netwatch entry removed");
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
- log31.info({ host: parsed.host, action: resultAction }, "Netwatch entry updated");
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 = z30.object({
7171
- routerId: z30.string().describe("Target router identifier from the router registry"),
7172
- interface: z30.string().optional().describe("Filter by interface name (substring match)"),
7173
- limit: z30.number().int().min(1).max(500).default(100).describe("Maximum number of neighbors to return")
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
- log31.info({ routerId: context.routerId }, "Listing neighbors");
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 = z30.object({
7213
- routerId: z30.string().describe("Target router identifier from the router registry"),
7214
- interface: z30.string().optional().describe("Filter by interface (substring match)"),
7215
- address: z30.string().optional().describe("Filter by IP address (substring match)"),
7216
- macAddress: z30.string().optional().describe("Filter by MAC address (substring match)"),
7217
- limit: z30.number().int().min(1).max(500).default(100).describe("Maximum number of ARP entries to return")
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
- log31.info({ routerId: context.routerId }, "Listing ARP entries");
7239
+ log32.info({ routerId: context.routerId }, "Listing ARP entries");
7233
7240
  try {
7234
7241
  const allEntries = await context.routerClient.get("ip/arp", {
7235
7242
  limit: void 0,
@@ -7262,47 +7269,1253 @@ var networkServicesTools = [
7262
7269
  listArpEntriesTool
7263
7270
  ];
7264
7271
 
7265
- // src/domain/tools/fleet-tools.ts
7266
- import { z as z31 } from "zod";
7267
-
7268
- // src/middleware/authz.ts
7269
- function matchesPattern(name, pattern) {
7270
- if (!pattern.includes("*")) return name === pattern;
7271
- const prefix = pattern.slice(0, pattern.indexOf("*"));
7272
- return name.startsWith(prefix);
7272
+ // src/domain/tools/vlan-tools.ts
7273
+ import { z as z32 } from "zod";
7274
+ var log33 = createLogger("vlan-tools");
7275
+ function sanitizeComment4(comment) {
7276
+ if (comment === void 0) return void 0;
7277
+ return comment.replace(/[\x00-\x1f\x7f]/g, "");
7273
7278
  }
7274
- function checkAuthz(identity, toolName, routerId) {
7275
- if (identity.allowedRouters.length > 0 && !identity.allowedRouters.includes(routerId)) {
7276
- throw new MikroMCPError({
7277
- category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
7278
- code: "ROUTER_NOT_ALLOWED",
7279
- message: `Identity "${identity.id}" is not allowed to access router "${routerId}".`,
7280
- details: { identityId: identity.id, routerId, allowedRouters: identity.allowedRouters },
7281
- recoverability: {
7282
- retryable: false,
7283
- suggestedAction: `Add "${routerId}" to allowedRouters for identity "${identity.id}" in identities.yaml.`
7284
- }
7285
- });
7286
- }
7287
- if (identity.allowedToolPatterns.length > 0) {
7288
- const allowed = identity.allowedToolPatterns.some((p) => matchesPattern(toolName, p));
7289
- if (!allowed) {
7290
- throw new MikroMCPError({
7291
- category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
7292
- code: "TOOL_NOT_ALLOWED",
7293
- message: `Identity "${identity.id}" is not allowed to call tool "${toolName}".`,
7294
- details: { identityId: identity.id, toolName, allowedToolPatterns: identity.allowedToolPatterns },
7295
- recoverability: {
7296
- retryable: false,
7297
- suggestedAction: `Add a matching pattern to allowedToolPatterns for identity "${identity.id}" in identities.yaml.`
7298
- }
7279
+ var manageVlanInputSchema = z32.object({
7280
+ routerId: z32.string().describe("Target router identifier from the router registry"),
7281
+ action: z32.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
7282
+ name: z32.string().regex(/^[a-zA-Z0-9_-]+$/).max(15).describe("VLAN interface name \u2014 idempotency key (alphanumeric, hyphens, underscores, max 15 chars)"),
7283
+ vlanId: z32.number().int().min(1).max(4094).optional().describe("VLAN ID (1-4094; required for add)"),
7284
+ parentInterface: z32.string().optional().describe("Parent interface name (e.g., ether1, bridge1; required for add)"),
7285
+ mtu: z32.number().int().min(68).max(9e3).default(1500).describe("MTU size (add only)"),
7286
+ disabled: z32.boolean().default(false).describe("Whether to create the VLAN disabled (add only)"),
7287
+ comment: z32.string().max(255).optional().describe("Optional comment (add only)"),
7288
+ dryRun: z32.boolean().default(false).describe("Preview changes without applying")
7289
+ }).strict();
7290
+ var manageVlanTool = {
7291
+ name: "manage_vlan",
7292
+ title: "Manage VLAN Interface",
7293
+ description: "Add, remove, enable, or disable a VLAN interface. Idempotent by name: add returns already_exists when a VLAN with matching name, vlan-id, and parent interface exists. Supports dry-run mode.",
7294
+ inputSchema: manageVlanInputSchema,
7295
+ annotations: {
7296
+ readOnlyHint: false,
7297
+ destructiveHint: true,
7298
+ idempotentHint: true,
7299
+ openWorldHint: false
7300
+ },
7301
+ snapshotPaths: ["interface/vlan"],
7302
+ async handler(params, context) {
7303
+ const parsed = manageVlanInputSchema.parse(params);
7304
+ const comment = sanitizeComment4(parsed.comment);
7305
+ log33.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing VLAN");
7306
+ try {
7307
+ const allVlans = await context.routerClient.get("interface/vlan", {
7308
+ limit: void 0,
7309
+ offset: void 0
7299
7310
  });
7300
- }
7301
- }
7302
- }
7303
-
7304
- // src/config/secrets.ts
7305
- function getCredentials(router) {
7311
+ const existing = allVlans.find((v) => v.name === parsed.name);
7312
+ if (parsed.action === "add") {
7313
+ if (parsed.vlanId === void 0) {
7314
+ throw new MikroMCPError({
7315
+ category: "VALIDATION" /* VALIDATION */,
7316
+ code: "VLAN_ID_REQUIRED",
7317
+ message: "vlanId is required when adding a VLAN.",
7318
+ details: { name: parsed.name },
7319
+ recoverability: { retryable: false, suggestedAction: "Provide vlanId (1-4094)." }
7320
+ });
7321
+ }
7322
+ if (!parsed.parentInterface) {
7323
+ throw new MikroMCPError({
7324
+ category: "VALIDATION" /* VALIDATION */,
7325
+ code: "VLAN_PARENT_REQUIRED",
7326
+ message: "parentInterface is required when adding a VLAN.",
7327
+ details: { name: parsed.name },
7328
+ recoverability: { retryable: false, suggestedAction: "Provide the parent interface name." }
7329
+ });
7330
+ }
7331
+ if (existing) {
7332
+ if (existing["vlan-id"] === String(parsed.vlanId) && existing.interface === parsed.parentInterface) {
7333
+ log33.info({ name: parsed.name }, "VLAN already exists with matching config");
7334
+ return {
7335
+ content: `VLAN "${parsed.name}" already exists with VLAN ID ${parsed.vlanId} on ${parsed.parentInterface}. No changes made.`,
7336
+ structuredContent: { action: "already_exists", vlan: existing }
7337
+ };
7338
+ }
7339
+ throw new MikroMCPError({
7340
+ category: "CONFLICT" /* CONFLICT */,
7341
+ code: "VLAN_NAME_CONFLICT",
7342
+ message: `VLAN "${parsed.name}" already exists but with different configuration (vlan-id=${existing["vlan-id"]}, interface=${existing.interface}).`,
7343
+ details: {
7344
+ existing: { vlanId: existing["vlan-id"], interface: existing.interface },
7345
+ requested: { vlanId: parsed.vlanId, interface: parsed.parentInterface }
7346
+ },
7347
+ recoverability: {
7348
+ retryable: false,
7349
+ suggestedAction: "Use a different name or remove the existing VLAN first.",
7350
+ alternativeTools: ["manage_vlan"]
7351
+ }
7352
+ });
7353
+ }
7354
+ if (parsed.dryRun) {
7355
+ const diff = [
7356
+ { property: "name", before: null, after: parsed.name },
7357
+ { property: "vlan-id", before: null, after: String(parsed.vlanId) },
7358
+ { property: "interface", before: null, after: parsed.parentInterface },
7359
+ { property: "mtu", before: null, after: String(parsed.mtu) },
7360
+ { property: "disabled", before: null, after: parsed.disabled ? "true" : "false" },
7361
+ ...comment ? [{ property: "comment", before: null, after: comment }] : []
7362
+ ];
7363
+ return {
7364
+ content: `Dry run: Would create VLAN "${parsed.name}" (ID ${parsed.vlanId}) on ${parsed.parentInterface} with MTU ${parsed.mtu}.`,
7365
+ structuredContent: { action: "dry_run", diff }
7366
+ };
7367
+ }
7368
+ const body = {
7369
+ name: parsed.name,
7370
+ "vlan-id": String(parsed.vlanId),
7371
+ interface: parsed.parentInterface,
7372
+ mtu: String(parsed.mtu),
7373
+ disabled: parsed.disabled ? "true" : "false"
7374
+ };
7375
+ if (comment) body.comment = comment;
7376
+ const created = await context.routerClient.create("interface/vlan", body);
7377
+ log33.info({ name: parsed.name, id: created[".id"] }, "VLAN created");
7378
+ return {
7379
+ content: `Created VLAN "${parsed.name}" (ID ${parsed.vlanId}) on ${parsed.parentInterface} with MTU ${parsed.mtu}.`,
7380
+ structuredContent: { action: "created", vlan: created }
7381
+ };
7382
+ }
7383
+ if (parsed.action === "remove") {
7384
+ if (!existing) {
7385
+ return {
7386
+ content: `VLAN "${parsed.name}" not found. Nothing to remove.`,
7387
+ structuredContent: { action: "not_found", name: parsed.name }
7388
+ };
7389
+ }
7390
+ if (parsed.dryRun) {
7391
+ return {
7392
+ content: `Dry run: Would remove VLAN "${parsed.name}".`,
7393
+ structuredContent: { action: "dry_run", diff: [{ property: "name", before: parsed.name, after: null }] }
7394
+ };
7395
+ }
7396
+ await context.routerClient.remove("interface/vlan", existing[".id"]);
7397
+ log33.info({ name: parsed.name }, "VLAN removed");
7398
+ return {
7399
+ content: `Removed VLAN "${parsed.name}".`,
7400
+ structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
7401
+ };
7402
+ }
7403
+ if (!existing) {
7404
+ throw new MikroMCPError({
7405
+ category: "NOT_FOUND" /* NOT_FOUND */,
7406
+ code: "VLAN_NOT_FOUND",
7407
+ message: `VLAN "${parsed.name}" not found.`,
7408
+ details: { name: parsed.name },
7409
+ recoverability: {
7410
+ retryable: false,
7411
+ suggestedAction: "Verify the VLAN name with list_interfaces.",
7412
+ alternativeTools: ["list_interfaces"]
7413
+ }
7414
+ });
7415
+ }
7416
+ if (parsed.dryRun) {
7417
+ return {
7418
+ content: `Dry run: Would ${parsed.action} VLAN "${parsed.name}".`,
7419
+ structuredContent: {
7420
+ action: "dry_run",
7421
+ diff: [{ property: "disabled", before: existing.disabled, after: parsed.action === "disable" ? "true" : "false" }]
7422
+ }
7423
+ };
7424
+ }
7425
+ const disabledValue = parsed.action === "disable" ? "true" : "false";
7426
+ await context.routerClient.update("interface/vlan", existing[".id"], { disabled: disabledValue });
7427
+ const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
7428
+ log33.info({ name: parsed.name, action: resultAction }, "VLAN updated");
7429
+ return {
7430
+ content: `VLAN "${parsed.name}" ${resultAction}.`,
7431
+ structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
7432
+ };
7433
+ } catch (err) {
7434
+ if (err instanceof MikroMCPError) throw err;
7435
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_vlan" });
7436
+ }
7437
+ }
7438
+ };
7439
+ var vlanTools = [manageVlanTool];
7440
+
7441
+ // src/domain/tools/dhcp-client-tools.ts
7442
+ import { z as z33 } from "zod";
7443
+ var log34 = createLogger("dhcp-client-tools");
7444
+ var listDhcpClientsInputSchema = z33.object({
7445
+ routerId: z33.string().describe("Target router identifier from the router registry"),
7446
+ interface: z33.string().optional().describe("Filter by interface name (exact match)"),
7447
+ status: z33.enum(["bound", "searching", "requesting", "init", "all"]).default("all").describe("Filter by DHCP client status"),
7448
+ limit: z33.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
7449
+ offset: z33.number().int().min(0).default(0).describe("Offset for pagination")
7450
+ }).strict();
7451
+ var listDhcpClientsTool = {
7452
+ name: "list_dhcp_clients",
7453
+ title: "List DHCP Clients",
7454
+ description: "List DHCP client configurations on a MikroTik router. Shows which interfaces obtain their IP via DHCP, current status, and assigned address.",
7455
+ inputSchema: listDhcpClientsInputSchema,
7456
+ annotations: {
7457
+ readOnlyHint: true,
7458
+ destructiveHint: false,
7459
+ idempotentHint: true,
7460
+ openWorldHint: false
7461
+ },
7462
+ async handler(params, context) {
7463
+ const parsed = listDhcpClientsInputSchema.parse(params);
7464
+ log34.info({ routerId: context.routerId }, "Listing DHCP clients");
7465
+ try {
7466
+ const allClients = await context.routerClient.get("ip/dhcp-client", {
7467
+ limit: void 0,
7468
+ offset: void 0
7469
+ });
7470
+ let filtered = allClients;
7471
+ if (parsed.interface) {
7472
+ filtered = filtered.filter((c) => c.interface === parsed.interface);
7473
+ }
7474
+ if (parsed.status !== "all") {
7475
+ filtered = filtered.filter((c) => c.status === parsed.status);
7476
+ }
7477
+ const total = filtered.length;
7478
+ const clients = filtered.slice(parsed.offset, parsed.offset + parsed.limit);
7479
+ const hasMore = parsed.offset + parsed.limit < total;
7480
+ const lines = [
7481
+ `DHCP clients on ${context.routerId}: ${total} total, showing ${clients.length} (offset ${parsed.offset})`
7482
+ ];
7483
+ for (const c of clients) {
7484
+ const rec = c;
7485
+ const iface = rec.interface ?? "unknown";
7486
+ const status = rec.status ?? "unknown";
7487
+ const address = rec.address ?? rec["active-address"] ?? "-";
7488
+ const isDisabled = rec.disabled === true || rec.disabled === "true";
7489
+ lines.push(` ${iface} [${status}] ${address}${isDisabled ? " DISABLED" : ""}`);
7490
+ }
7491
+ return {
7492
+ content: lines.join("\n"),
7493
+ structuredContent: {
7494
+ routerId: context.routerId,
7495
+ clients,
7496
+ total,
7497
+ hasMore,
7498
+ offset: parsed.offset,
7499
+ limit: parsed.limit
7500
+ }
7501
+ };
7502
+ } catch (err) {
7503
+ throw enrichError(err, { routerId: context.routerId, tool: "list_dhcp_clients" });
7504
+ }
7505
+ }
7506
+ };
7507
+ var manageDhcpClientInputSchema = z33.object({
7508
+ routerId: z33.string().describe("Target router identifier from the router registry"),
7509
+ action: z33.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
7510
+ interface: z33.string().describe("Interface name \u2014 idempotency key (e.g. ether1, ether2)"),
7511
+ usePeerDns: z33.boolean().default(true).describe("Use DNS servers provided by DHCP server (add only)"),
7512
+ usePeerNtp: z33.boolean().default(false).describe("Use NTP servers provided by DHCP server (add only)"),
7513
+ addDefaultRoute: z33.boolean().default(true).describe("Add default route from DHCP (add only)"),
7514
+ comment: z33.string().max(255).optional().describe("Optional comment (add only)"),
7515
+ dryRun: z33.boolean().default(false).describe("Preview changes without applying")
7516
+ }).strict();
7517
+ var manageDhcpClientTool = {
7518
+ name: "manage_dhcp_client",
7519
+ title: "Manage DHCP Client",
7520
+ description: "Add, remove, enable, or disable a DHCP client on an interface. Idempotent by interface name: add returns already_exists if a DHCP client is already configured on the same interface.",
7521
+ inputSchema: manageDhcpClientInputSchema,
7522
+ annotations: {
7523
+ readOnlyHint: false,
7524
+ destructiveHint: true,
7525
+ idempotentHint: true,
7526
+ openWorldHint: false
7527
+ },
7528
+ snapshotPaths: ["ip/dhcp-client"],
7529
+ async handler(params, context) {
7530
+ const parsed = manageDhcpClientInputSchema.parse(params);
7531
+ log34.info({ routerId: context.routerId, action: parsed.action, interface: parsed.interface }, "Managing DHCP client");
7532
+ try {
7533
+ const allClients = await context.routerClient.get("ip/dhcp-client", {
7534
+ limit: void 0,
7535
+ offset: void 0
7536
+ });
7537
+ const existing = allClients.find(
7538
+ (c) => c.interface === parsed.interface
7539
+ );
7540
+ if (parsed.action === "add") {
7541
+ if (existing) {
7542
+ return {
7543
+ content: `DHCP client already configured on "${parsed.interface}". No changes made.`,
7544
+ structuredContent: { action: "already_exists", client: existing }
7545
+ };
7546
+ }
7547
+ if (parsed.dryRun) {
7548
+ const diff = [
7549
+ { property: "interface", before: null, after: parsed.interface },
7550
+ { property: "use-peer-dns", before: null, after: parsed.usePeerDns ? "yes" : "no" },
7551
+ { property: "use-peer-ntp", before: null, after: parsed.usePeerNtp ? "yes" : "no" },
7552
+ { property: "add-default-route", before: null, after: parsed.addDefaultRoute ? "yes" : "no" }
7553
+ ];
7554
+ return {
7555
+ content: `Dry run: Would add DHCP client on "${parsed.interface}".`,
7556
+ structuredContent: { action: "dry_run", diff }
7557
+ };
7558
+ }
7559
+ const body = {
7560
+ interface: parsed.interface,
7561
+ "use-peer-dns": parsed.usePeerDns ? "yes" : "no",
7562
+ "use-peer-ntp": parsed.usePeerNtp ? "yes" : "no",
7563
+ "add-default-route": parsed.addDefaultRoute ? "yes" : "no"
7564
+ };
7565
+ if (parsed.comment) body.comment = parsed.comment;
7566
+ const created = await context.routerClient.create("ip/dhcp-client", body);
7567
+ log34.info({ interface: parsed.interface, id: created[".id"] }, "DHCP client added");
7568
+ return {
7569
+ content: `Added DHCP client on "${parsed.interface}".`,
7570
+ structuredContent: { action: "created", client: created }
7571
+ };
7572
+ }
7573
+ if (parsed.action === "remove") {
7574
+ if (!existing) {
7575
+ return {
7576
+ content: `No DHCP client found on "${parsed.interface}". Nothing to remove.`,
7577
+ structuredContent: { action: "not_found", interface: parsed.interface }
7578
+ };
7579
+ }
7580
+ if (parsed.dryRun) {
7581
+ return {
7582
+ content: `Dry run: Would remove DHCP client on "${parsed.interface}".`,
7583
+ structuredContent: {
7584
+ action: "dry_run",
7585
+ diff: [{ property: "interface", before: parsed.interface, after: null }]
7586
+ }
7587
+ };
7588
+ }
7589
+ await context.routerClient.remove("ip/dhcp-client", existing[".id"]);
7590
+ log34.info({ interface: parsed.interface }, "DHCP client removed");
7591
+ return {
7592
+ content: `Removed DHCP client on "${parsed.interface}".`,
7593
+ structuredContent: { action: "removed", interface: parsed.interface, id: existing[".id"] }
7594
+ };
7595
+ }
7596
+ if (!existing) {
7597
+ throw new MikroMCPError({
7598
+ category: "NOT_FOUND" /* NOT_FOUND */,
7599
+ code: "DHCP_CLIENT_NOT_FOUND",
7600
+ message: `No DHCP client found on interface "${parsed.interface}".`,
7601
+ details: { interface: parsed.interface },
7602
+ recoverability: {
7603
+ retryable: false,
7604
+ suggestedAction: "Verify with list_dhcp_clients.",
7605
+ alternativeTools: ["list_dhcp_clients"]
7606
+ }
7607
+ });
7608
+ }
7609
+ if (parsed.dryRun) {
7610
+ return {
7611
+ content: `Dry run: Would ${parsed.action} DHCP client on "${parsed.interface}".`,
7612
+ structuredContent: {
7613
+ action: "dry_run",
7614
+ diff: [
7615
+ {
7616
+ property: "disabled",
7617
+ before: existing.disabled,
7618
+ after: parsed.action === "disable" ? "true" : "false"
7619
+ }
7620
+ ]
7621
+ }
7622
+ };
7623
+ }
7624
+ const disabledValue = parsed.action === "disable" ? "true" : "false";
7625
+ await context.routerClient.update("ip/dhcp-client", existing[".id"], { disabled: disabledValue });
7626
+ const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
7627
+ log34.info({ interface: parsed.interface, action: resultAction }, "DHCP client updated");
7628
+ return {
7629
+ content: `DHCP client on "${parsed.interface}" ${resultAction}.`,
7630
+ structuredContent: { action: resultAction, interface: parsed.interface, id: existing[".id"] }
7631
+ };
7632
+ } catch (err) {
7633
+ if (err instanceof MikroMCPError) throw err;
7634
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_dhcp_client" });
7635
+ }
7636
+ }
7637
+ };
7638
+ var dhcpClientTools = [listDhcpClientsTool, manageDhcpClientTool];
7639
+
7640
+ // src/domain/tools/ip-service-tools.ts
7641
+ import { z as z34 } from "zod";
7642
+ var log35 = createLogger("ip-service-tools");
7643
+ var SERVICE_NAMES = ["api", "api-ssl", "ssh", "telnet", "www", "www-ssl", "winbox", "ftp"];
7644
+ var listIpServicesInputSchema = z34.object({
7645
+ routerId: z34.string().describe("Target router identifier from the router registry"),
7646
+ name: z34.enum(SERVICE_NAMES).optional().describe("Filter by service name (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp)"),
7647
+ enabled: z34.boolean().optional().describe("When true, return only enabled services; when false, only disabled services")
7648
+ }).strict();
7649
+ var listIpServicesTool = {
7650
+ name: "list_ip_services",
7651
+ title: "List IP Services",
7652
+ description: "List IP services on a MikroTik router (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp) with their port numbers and enabled/disabled status.",
7653
+ inputSchema: listIpServicesInputSchema,
7654
+ annotations: {
7655
+ readOnlyHint: true,
7656
+ destructiveHint: false,
7657
+ idempotentHint: true,
7658
+ openWorldHint: false
7659
+ },
7660
+ async handler(params, context) {
7661
+ const parsed = listIpServicesInputSchema.parse(params);
7662
+ log35.info({ routerId: context.routerId }, "Listing IP services");
7663
+ try {
7664
+ const allServices = await context.routerClient.get("ip/service", {
7665
+ limit: void 0,
7666
+ offset: void 0
7667
+ });
7668
+ let filtered = allServices;
7669
+ if (parsed.name) {
7670
+ filtered = filtered.filter((s) => s.name === parsed.name);
7671
+ }
7672
+ if (parsed.enabled !== void 0) {
7673
+ filtered = filtered.filter((s) => {
7674
+ const isDisabled = s.disabled === true || s.disabled === "true";
7675
+ return parsed.enabled ? !isDisabled : isDisabled;
7676
+ });
7677
+ }
7678
+ const lines = [`IP services on ${context.routerId}: ${filtered.length} returned`];
7679
+ for (const s of filtered) {
7680
+ const rec = s;
7681
+ const isDisabled = rec.disabled === true || rec.disabled === "true";
7682
+ lines.push(` ${rec.name} port:${rec.port ?? "-"} [${isDisabled ? "DISABLED" : "enabled"}]`);
7683
+ }
7684
+ return {
7685
+ content: lines.join("\n"),
7686
+ structuredContent: {
7687
+ routerId: context.routerId,
7688
+ services: filtered,
7689
+ total: filtered.length
7690
+ }
7691
+ };
7692
+ } catch (err) {
7693
+ throw enrichError(err, { routerId: context.routerId, tool: "list_ip_services" });
7694
+ }
7695
+ }
7696
+ };
7697
+ var manageIpServiceInputSchema = z34.object({
7698
+ routerId: z34.string().describe("Target router identifier from the router registry"),
7699
+ action: z34.enum(["enable", "disable"]).describe(
7700
+ "Action to perform \u2014 only enable/disable to prevent accidental lockout from changing ports"
7701
+ ),
7702
+ name: z34.enum(SERVICE_NAMES).describe("Service name to manage (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp)"),
7703
+ dryRun: z34.boolean().default(false).describe("Preview changes without applying")
7704
+ }).strict();
7705
+ var manageIpServiceTool = {
7706
+ name: "manage_ip_service",
7707
+ title: "Manage IP Service",
7708
+ description: "Enable or disable a RouterOS IP service (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp). Port number changes are intentionally not supported to prevent accidental lockout.",
7709
+ inputSchema: manageIpServiceInputSchema,
7710
+ annotations: {
7711
+ readOnlyHint: false,
7712
+ destructiveHint: true,
7713
+ idempotentHint: true,
7714
+ openWorldHint: false
7715
+ },
7716
+ async handler(params, context) {
7717
+ const parsed = manageIpServiceInputSchema.parse(params);
7718
+ log35.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP service");
7719
+ try {
7720
+ const allServices = await context.routerClient.get("ip/service", {
7721
+ limit: void 0,
7722
+ offset: void 0
7723
+ });
7724
+ const existing = allServices.find((s) => s.name === parsed.name);
7725
+ if (!existing) {
7726
+ throw new MikroMCPError({
7727
+ category: "NOT_FOUND" /* NOT_FOUND */,
7728
+ code: "IP_SERVICE_NOT_FOUND",
7729
+ message: `IP service "${parsed.name}" not found on router.`,
7730
+ details: { name: parsed.name },
7731
+ recoverability: {
7732
+ retryable: false,
7733
+ suggestedAction: "Verify service name with list_ip_services.",
7734
+ alternativeTools: ["list_ip_services"]
7735
+ }
7736
+ });
7737
+ }
7738
+ const isCurrentlyDisabled = existing.disabled === "true" || existing.disabled === true;
7739
+ const wouldBeDisabled = parsed.action === "disable";
7740
+ if (isCurrentlyDisabled === wouldBeDisabled) {
7741
+ const alreadyState = parsed.action === "disable" ? "disabled" : "enabled";
7742
+ return {
7743
+ content: `IP service "${parsed.name}" is already ${alreadyState}. No changes made.`,
7744
+ structuredContent: { action: "no_change", name: parsed.name, id: existing[".id"] }
7745
+ };
7746
+ }
7747
+ if (parsed.dryRun) {
7748
+ return {
7749
+ content: `Dry run: Would ${parsed.action} IP service "${parsed.name}".`,
7750
+ structuredContent: {
7751
+ action: "dry_run",
7752
+ diff: [
7753
+ {
7754
+ property: "disabled",
7755
+ before: existing.disabled,
7756
+ after: parsed.action === "disable" ? "true" : "false"
7757
+ }
7758
+ ]
7759
+ }
7760
+ };
7761
+ }
7762
+ const disabledValue = parsed.action === "disable" ? "true" : "false";
7763
+ await context.routerClient.update("ip/service", existing[".id"], { disabled: disabledValue });
7764
+ const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
7765
+ log35.info({ name: parsed.name, action: resultAction }, "IP service updated");
7766
+ return {
7767
+ content: `IP service "${parsed.name}" ${resultAction}.`,
7768
+ structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
7769
+ };
7770
+ } catch (err) {
7771
+ if (err instanceof MikroMCPError) throw err;
7772
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ip_service" });
7773
+ }
7774
+ }
7775
+ };
7776
+ var ipServiceTools = [listIpServicesTool, manageIpServiceTool];
7777
+
7778
+ // src/domain/tools/pppoe-tools.ts
7779
+ import { z as z35 } from "zod";
7780
+ var log36 = createLogger("pppoe-tools");
7781
+ var PPPOE_PATH = "interface/pppoe-client";
7782
+ var listPppoeClientsInputSchema = z35.object({
7783
+ routerId: z35.string().describe("Target router identifier from the router registry"),
7784
+ interface: z35.string().optional().describe("Filter by parent interface name (exact match)"),
7785
+ status: z35.enum(["connected", "disconnected", "all"]).default("all").describe("Filter by running status"),
7786
+ limit: z35.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
7787
+ offset: z35.number().int().min(0).default(0).describe("Offset for pagination")
7788
+ }).strict();
7789
+ function isRunning(record) {
7790
+ return record.running === true || record.running === "true";
7791
+ }
7792
+ var listPppoeClientsTool = {
7793
+ name: "list_pppoe_clients",
7794
+ title: "List PPPoE Clients",
7795
+ description: "List PPPoE client interfaces on a MikroTik router. Shows name, parent interface, ISP username, and connection status.",
7796
+ inputSchema: listPppoeClientsInputSchema,
7797
+ annotations: {
7798
+ readOnlyHint: true,
7799
+ destructiveHint: false,
7800
+ idempotentHint: true,
7801
+ openWorldHint: false
7802
+ },
7803
+ async handler(params, context) {
7804
+ const parsed = listPppoeClientsInputSchema.parse(params);
7805
+ log36.info({ routerId: context.routerId }, "Listing PPPoE clients");
7806
+ try {
7807
+ const allClients = await context.routerClient.get(PPPOE_PATH, {
7808
+ limit: void 0,
7809
+ offset: void 0
7810
+ });
7811
+ let filtered = allClients;
7812
+ if (parsed.interface) {
7813
+ filtered = filtered.filter((c) => c.interface === parsed.interface);
7814
+ }
7815
+ if (parsed.status === "connected") {
7816
+ filtered = filtered.filter((c) => isRunning(c));
7817
+ } else if (parsed.status === "disconnected") {
7818
+ filtered = filtered.filter((c) => !isRunning(c));
7819
+ }
7820
+ const total = filtered.length;
7821
+ const clients = filtered.slice(parsed.offset, parsed.offset + parsed.limit);
7822
+ const hasMore = parsed.offset + parsed.limit < total;
7823
+ const lines = [
7824
+ `PPPoE clients on ${context.routerId}: ${total} total, showing ${clients.length} (offset ${parsed.offset})`
7825
+ ];
7826
+ for (const c of clients) {
7827
+ const name = c.name ?? "unknown";
7828
+ const iface = c.interface ?? "unknown";
7829
+ const user = c.user ?? "-";
7830
+ const status = isRunning(c) ? "connected" : "disconnected";
7831
+ lines.push(` ${name} [${iface}] user=${user} ${status}`);
7832
+ }
7833
+ return {
7834
+ content: lines.join("\n"),
7835
+ structuredContent: {
7836
+ routerId: context.routerId,
7837
+ clients,
7838
+ total,
7839
+ hasMore,
7840
+ offset: parsed.offset,
7841
+ limit: parsed.limit
7842
+ }
7843
+ };
7844
+ } catch (err) {
7845
+ throw enrichError(err, { routerId: context.routerId, tool: "list_pppoe_clients" });
7846
+ }
7847
+ }
7848
+ };
7849
+ var managePppoeClientInputSchema = z35.object({
7850
+ routerId: z35.string().describe("Target router identifier from the router registry"),
7851
+ action: z35.enum(["add", "update", "remove"]).describe("Action to perform"),
7852
+ name: z35.string().describe("PPPoE client interface name \u2014 idempotency key (e.g. pppoe-wan)"),
7853
+ interface: z35.string().optional().describe("Parent interface (required for add)"),
7854
+ user: z35.string().optional().describe("PPPoE username (required for add)"),
7855
+ password: z35.string().optional().describe("PPPoE password (never logged)"),
7856
+ serviceName: z35.string().optional().describe("PPPoE service name filter (leave empty to match any)"),
7857
+ addDefaultRoute: z35.boolean().optional().describe("Add default route via PPPoE (yes/no)"),
7858
+ dialOnDemand: z35.boolean().optional().describe("Dial on demand instead of always-on (yes/no)"),
7859
+ dryRun: z35.boolean().default(false).describe("Preview changes without applying")
7860
+ }).strict();
7861
+ var managePppoeClientTool = {
7862
+ name: "manage_pppoe_client",
7863
+ title: "Manage PPPoE Client",
7864
+ description: "Add, update, or remove a PPPoE client interface. Idempotent by name: add returns already_exists if same name+interface+user exists; throws CONFLICT if same name exists with different config. Update builds a diff of changed fields and returns no_change when nothing differs. Password is always written when provided because RouterOS does not expose it in GET.",
7865
+ inputSchema: managePppoeClientInputSchema,
7866
+ annotations: {
7867
+ readOnlyHint: false,
7868
+ destructiveHint: true,
7869
+ idempotentHint: true,
7870
+ openWorldHint: false
7871
+ },
7872
+ snapshotPaths: [PPPOE_PATH],
7873
+ async handler(params, context) {
7874
+ const parsed = managePppoeClientInputSchema.parse(params);
7875
+ log36.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing PPPoE client");
7876
+ try {
7877
+ const allClients = await context.routerClient.get(PPPOE_PATH, {
7878
+ limit: void 0,
7879
+ offset: void 0
7880
+ });
7881
+ const existing = allClients.find((c) => c.name === parsed.name);
7882
+ if (parsed.action === "add") {
7883
+ if (!parsed.interface) {
7884
+ throw new MikroMCPError({
7885
+ category: "VALIDATION" /* VALIDATION */,
7886
+ code: "PPPOE_CLIENT_INTERFACE_REQUIRED",
7887
+ message: "interface is required for action add.",
7888
+ details: { name: parsed.name },
7889
+ recoverability: {
7890
+ retryable: false,
7891
+ suggestedAction: "Provide the parent interface (e.g. ether1).",
7892
+ alternativeTools: []
7893
+ }
7894
+ });
7895
+ }
7896
+ if (!parsed.user) {
7897
+ throw new MikroMCPError({
7898
+ category: "VALIDATION" /* VALIDATION */,
7899
+ code: "PPPOE_CLIENT_USER_REQUIRED",
7900
+ message: "user is required for action add.",
7901
+ details: { name: parsed.name },
7902
+ recoverability: {
7903
+ retryable: false,
7904
+ suggestedAction: "Provide the PPPoE username.",
7905
+ alternativeTools: []
7906
+ }
7907
+ });
7908
+ }
7909
+ if (existing) {
7910
+ const sameConfig = existing.interface === parsed.interface && existing.user === parsed.user;
7911
+ if (sameConfig) {
7912
+ return {
7913
+ content: `PPPoE client "${parsed.name}" already exists with the same configuration. No changes made.`,
7914
+ structuredContent: { action: "already_exists", client: existing }
7915
+ };
7916
+ }
7917
+ throw new MikroMCPError({
7918
+ category: "CONFLICT" /* CONFLICT */,
7919
+ code: "PPPOE_CLIENT_CONFLICT",
7920
+ message: `PPPoE client "${parsed.name}" already exists with a different configuration.`,
7921
+ details: {
7922
+ existing: { interface: existing.interface, user: existing.user },
7923
+ requested: { interface: parsed.interface, user: parsed.user }
7924
+ },
7925
+ recoverability: {
7926
+ retryable: false,
7927
+ suggestedAction: "Remove the existing client first or use action update.",
7928
+ alternativeTools: ["manage_pppoe_client"]
7929
+ }
7930
+ });
7931
+ }
7932
+ const diff = [
7933
+ { property: "name", before: null, after: parsed.name },
7934
+ { property: "interface", before: null, after: parsed.interface },
7935
+ { property: "user", before: null, after: parsed.user }
7936
+ ];
7937
+ if (parsed.serviceName !== void 0) {
7938
+ diff.push({ property: "service-name", before: null, after: parsed.serviceName });
7939
+ }
7940
+ if (parsed.addDefaultRoute !== void 0) {
7941
+ diff.push({ property: "add-default-route", before: null, after: parsed.addDefaultRoute ? "yes" : "no" });
7942
+ }
7943
+ if (parsed.dialOnDemand !== void 0) {
7944
+ diff.push({ property: "dial-on-demand", before: null, after: parsed.dialOnDemand ? "yes" : "no" });
7945
+ }
7946
+ if (parsed.dryRun) {
7947
+ return {
7948
+ content: `Dry run: Would create PPPoE client "${parsed.name}" on "${parsed.interface}".`,
7949
+ structuredContent: { action: "dry_run", diff }
7950
+ };
7951
+ }
7952
+ const body = {
7953
+ name: parsed.name,
7954
+ interface: parsed.interface,
7955
+ user: parsed.user
7956
+ };
7957
+ if (parsed.password !== void 0) body.password = parsed.password;
7958
+ if (parsed.serviceName !== void 0) body["service-name"] = parsed.serviceName;
7959
+ if (parsed.addDefaultRoute !== void 0) body["add-default-route"] = parsed.addDefaultRoute ? "yes" : "no";
7960
+ if (parsed.dialOnDemand !== void 0) body["dial-on-demand"] = parsed.dialOnDemand ? "yes" : "no";
7961
+ const created = await context.routerClient.create(PPPOE_PATH, body);
7962
+ log36.info({ name: parsed.name, id: created[".id"] }, "PPPoE client created");
7963
+ return {
7964
+ content: `Created PPPoE client "${parsed.name}" on "${parsed.interface}".`,
7965
+ structuredContent: { action: "created", client: created }
7966
+ };
7967
+ }
7968
+ if (parsed.action === "update") {
7969
+ if (!existing) {
7970
+ throw new MikroMCPError({
7971
+ category: "NOT_FOUND" /* NOT_FOUND */,
7972
+ code: "PPPOE_CLIENT_NOT_FOUND",
7973
+ message: `PPPoE client "${parsed.name}" not found.`,
7974
+ details: { name: parsed.name },
7975
+ recoverability: {
7976
+ retryable: false,
7977
+ suggestedAction: "Verify with list_pppoe_clients.",
7978
+ alternativeTools: ["list_pppoe_clients"]
7979
+ }
7980
+ });
7981
+ }
7982
+ const updates = {};
7983
+ const diff = [];
7984
+ if (parsed.interface !== void 0 && existing.interface !== parsed.interface) {
7985
+ updates.interface = parsed.interface;
7986
+ diff.push({ property: "interface", before: existing.interface, after: parsed.interface });
7987
+ }
7988
+ if (parsed.user !== void 0 && existing.user !== parsed.user) {
7989
+ updates.user = parsed.user;
7990
+ diff.push({ property: "user", before: existing.user, after: parsed.user });
7991
+ }
7992
+ if (parsed.serviceName !== void 0 && existing["service-name"] !== parsed.serviceName) {
7993
+ updates["service-name"] = parsed.serviceName;
7994
+ diff.push({ property: "service-name", before: existing["service-name"] ?? null, after: parsed.serviceName });
7995
+ }
7996
+ if (parsed.addDefaultRoute !== void 0) {
7997
+ const desired = parsed.addDefaultRoute ? "yes" : "no";
7998
+ if (existing["add-default-route"] !== desired) {
7999
+ updates["add-default-route"] = desired;
8000
+ diff.push({ property: "add-default-route", before: existing["add-default-route"] ?? null, after: desired });
8001
+ }
8002
+ }
8003
+ if (parsed.dialOnDemand !== void 0) {
8004
+ const desired = parsed.dialOnDemand ? "yes" : "no";
8005
+ if (existing["dial-on-demand"] !== desired) {
8006
+ updates["dial-on-demand"] = desired;
8007
+ diff.push({ property: "dial-on-demand", before: existing["dial-on-demand"] ?? null, after: desired });
8008
+ }
8009
+ }
8010
+ if (parsed.password !== void 0) {
8011
+ updates.password = parsed.password;
8012
+ }
8013
+ const hasUpdates = Object.keys(updates).length > 0;
8014
+ if (!hasUpdates) {
8015
+ return {
8016
+ content: `PPPoE client "${parsed.name}" already matches the requested configuration. No changes made.`,
8017
+ structuredContent: { action: "no_change", client: existing }
8018
+ };
8019
+ }
8020
+ if (parsed.dryRun) {
8021
+ return {
8022
+ content: `Dry run: Would update PPPoE client "${parsed.name}".`,
8023
+ structuredContent: { action: "dry_run", diff }
8024
+ };
8025
+ }
8026
+ await context.routerClient.update(PPPOE_PATH, existing[".id"], updates);
8027
+ log36.info({ name: parsed.name }, "PPPoE client updated");
8028
+ return {
8029
+ content: `Updated PPPoE client "${parsed.name}".`,
8030
+ structuredContent: { action: "updated", name: parsed.name, diff }
8031
+ };
8032
+ }
8033
+ if (!existing) {
8034
+ return {
8035
+ content: `PPPoE client "${parsed.name}" not found. Nothing to remove.`,
8036
+ structuredContent: { action: "not_found", name: parsed.name }
8037
+ };
8038
+ }
8039
+ if (parsed.dryRun) {
8040
+ return {
8041
+ content: `Dry run: Would remove PPPoE client "${parsed.name}".`,
8042
+ structuredContent: {
8043
+ action: "dry_run",
8044
+ diff: [{ property: "name", before: parsed.name, after: null }]
8045
+ }
8046
+ };
8047
+ }
8048
+ await context.routerClient.remove(PPPOE_PATH, existing[".id"]);
8049
+ log36.info({ name: parsed.name }, "PPPoE client removed");
8050
+ return {
8051
+ content: `Removed PPPoE client "${parsed.name}".`,
8052
+ structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
8053
+ };
8054
+ } catch (err) {
8055
+ if (err instanceof MikroMCPError) throw err;
8056
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_pppoe_client" });
8057
+ }
8058
+ }
8059
+ };
8060
+ var pppoeTools = [listPppoeClientsTool, managePppoeClientTool];
8061
+
8062
+ // src/domain/tools/ovpn-tools.ts
8063
+ import { z as z36 } from "zod";
8064
+ var log37 = createLogger("ovpn-tools");
8065
+ var OVPN_CLIENT_PATH = "interface/ovpn-client";
8066
+ var OVPN_SERVER_PATH = "interface/ovpn-server/server";
8067
+ var listOvpnClientsInputSchema = z36.object({
8068
+ routerId: z36.string().describe("Target router identifier from the router registry"),
8069
+ limit: z36.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
8070
+ offset: z36.number().int().min(0).default(0).describe("Offset for pagination")
8071
+ }).strict();
8072
+ var listOvpnClientsTool = {
8073
+ name: "list_ovpn_clients",
8074
+ title: "List OpenVPN Clients",
8075
+ description: "List OpenVPN client interfaces on a MikroTik router. Shows name, remote server, and connection status.",
8076
+ inputSchema: listOvpnClientsInputSchema,
8077
+ annotations: {
8078
+ readOnlyHint: true,
8079
+ destructiveHint: false,
8080
+ idempotentHint: true,
8081
+ openWorldHint: false
8082
+ },
8083
+ async handler(params, context) {
8084
+ const parsed = listOvpnClientsInputSchema.parse(params);
8085
+ log37.info({ routerId: context.routerId }, "Listing OpenVPN clients");
8086
+ try {
8087
+ const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
8088
+ limit: void 0,
8089
+ offset: void 0
8090
+ });
8091
+ const total = allClients.length;
8092
+ const clients = allClients.slice(parsed.offset, parsed.offset + parsed.limit);
8093
+ const hasMore = parsed.offset + parsed.limit < total;
8094
+ return {
8095
+ content: `OpenVPN clients on ${context.routerId}: ${total} total, showing ${clients.length} (offset ${parsed.offset})`,
8096
+ structuredContent: {
8097
+ routerId: context.routerId,
8098
+ clients,
8099
+ total,
8100
+ hasMore,
8101
+ offset: parsed.offset,
8102
+ limit: parsed.limit
8103
+ }
8104
+ };
8105
+ } catch (err) {
8106
+ throw enrichError(err, { routerId: context.routerId, tool: "list_ovpn_clients" });
8107
+ }
8108
+ }
8109
+ };
8110
+ var manageOvpnClientInputSchema = z36.object({
8111
+ routerId: z36.string().describe("Target router identifier from the router registry"),
8112
+ action: z36.enum(["add", "update", "remove"]).describe("Action to perform"),
8113
+ name: z36.string().describe("OpenVPN client interface name \u2014 idempotency key"),
8114
+ connectTo: z36.string().optional().describe("Remote server address (required for add)"),
8115
+ port: z36.number().int().min(1).max(65535).optional().describe("Remote port number"),
8116
+ mode: z36.enum(["ip", "ethernet"]).optional().describe("Tunnel mode"),
8117
+ protocol: z36.enum(["tcp-client", "udp"]).optional().describe("Transport protocol"),
8118
+ certificate: z36.string().optional().describe("Client certificate name"),
8119
+ user: z36.string().optional().describe("VPN username"),
8120
+ password: z36.string().optional().describe("VPN password (never logged)"),
8121
+ dryRun: z36.boolean().default(false).describe("Preview changes without applying")
8122
+ }).strict();
8123
+ var manageOvpnClientTool = {
8124
+ name: "manage_ovpn_client",
8125
+ title: "Manage OpenVPN Client",
8126
+ description: "Add, update, or remove an OpenVPN client interface. Idempotent by name: add returns already_exists if same name+connectTo exists; throws CONFLICT if same name exists with different connectTo. Update builds a diff of changed fields and returns no_change when nothing differs. Password is always written when provided because RouterOS does not expose it in GET.",
8127
+ inputSchema: manageOvpnClientInputSchema,
8128
+ annotations: {
8129
+ readOnlyHint: false,
8130
+ destructiveHint: true,
8131
+ idempotentHint: true,
8132
+ openWorldHint: false
8133
+ },
8134
+ snapshotPaths: [OVPN_CLIENT_PATH],
8135
+ async handler(params, context) {
8136
+ const parsed = manageOvpnClientInputSchema.parse(params);
8137
+ log37.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing OpenVPN client");
8138
+ try {
8139
+ const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
8140
+ limit: void 0,
8141
+ offset: void 0
8142
+ });
8143
+ const existing = allClients.find((c) => c.name === parsed.name);
8144
+ if (parsed.action === "add") {
8145
+ if (!parsed.connectTo) {
8146
+ throw new MikroMCPError({
8147
+ category: "VALIDATION" /* VALIDATION */,
8148
+ code: "OVPN_CLIENT_CONNECT_TO_REQUIRED",
8149
+ message: "connectTo is required for action add.",
8150
+ details: { name: parsed.name },
8151
+ recoverability: {
8152
+ retryable: false,
8153
+ suggestedAction: "Provide the remote server address.",
8154
+ alternativeTools: []
8155
+ }
8156
+ });
8157
+ }
8158
+ if (existing) {
8159
+ const sameConfig = existing["connect-to"] === parsed.connectTo;
8160
+ if (sameConfig) {
8161
+ return {
8162
+ content: `OpenVPN client "${parsed.name}" already exists with the same configuration. No changes made.`,
8163
+ structuredContent: { action: "already_exists", client: existing }
8164
+ };
8165
+ }
8166
+ throw new MikroMCPError({
8167
+ category: "CONFLICT" /* CONFLICT */,
8168
+ code: "OVPN_CLIENT_CONFLICT",
8169
+ message: `OpenVPN client "${parsed.name}" already exists with a different configuration.`,
8170
+ details: {
8171
+ existing: { "connect-to": existing["connect-to"] },
8172
+ requested: { "connect-to": parsed.connectTo }
8173
+ },
8174
+ recoverability: {
8175
+ retryable: false,
8176
+ suggestedAction: "Remove the existing client first or use action update.",
8177
+ alternativeTools: ["manage_ovpn_client"]
8178
+ }
8179
+ });
8180
+ }
8181
+ const diff = [
8182
+ { property: "name", before: null, after: parsed.name },
8183
+ { property: "connect-to", before: null, after: parsed.connectTo }
8184
+ ];
8185
+ if (parsed.dryRun) {
8186
+ return {
8187
+ content: `Dry run: Would create OpenVPN client "${parsed.name}" connecting to "${parsed.connectTo}".`,
8188
+ structuredContent: { action: "dry_run", diff }
8189
+ };
8190
+ }
8191
+ const body = {
8192
+ name: parsed.name,
8193
+ "connect-to": parsed.connectTo
8194
+ };
8195
+ if (parsed.port !== void 0) body.port = String(parsed.port);
8196
+ if (parsed.mode !== void 0) body.mode = parsed.mode;
8197
+ if (parsed.protocol !== void 0) body.protocol = parsed.protocol;
8198
+ if (parsed.certificate !== void 0) body.certificate = parsed.certificate;
8199
+ if (parsed.user !== void 0) body.user = parsed.user;
8200
+ if (parsed.password !== void 0) body.password = parsed.password;
8201
+ const created = await context.routerClient.create(OVPN_CLIENT_PATH, body);
8202
+ log37.info({ name: parsed.name, id: created[".id"] }, "OpenVPN client created");
8203
+ return {
8204
+ content: `Created OpenVPN client "${parsed.name}" connecting to "${parsed.connectTo}".`,
8205
+ structuredContent: { action: "created", client: created }
8206
+ };
8207
+ }
8208
+ if (parsed.action === "update") {
8209
+ if (!existing) {
8210
+ throw new MikroMCPError({
8211
+ category: "NOT_FOUND" /* NOT_FOUND */,
8212
+ code: "OVPN_CLIENT_NOT_FOUND",
8213
+ message: `OpenVPN client "${parsed.name}" not found.`,
8214
+ details: { name: parsed.name },
8215
+ recoverability: {
8216
+ retryable: false,
8217
+ suggestedAction: "Verify with list_ovpn_clients.",
8218
+ alternativeTools: ["list_ovpn_clients"]
8219
+ }
8220
+ });
8221
+ }
8222
+ const updates = {};
8223
+ const diff = [];
8224
+ if (parsed.connectTo !== void 0 && existing["connect-to"] !== parsed.connectTo) {
8225
+ updates["connect-to"] = parsed.connectTo;
8226
+ diff.push({ property: "connect-to", before: existing["connect-to"] ?? null, after: parsed.connectTo });
8227
+ }
8228
+ if (parsed.port !== void 0 && existing.port !== String(parsed.port)) {
8229
+ updates.port = String(parsed.port);
8230
+ diff.push({ property: "port", before: existing.port ?? null, after: String(parsed.port) });
8231
+ }
8232
+ if (parsed.mode !== void 0 && existing.mode !== parsed.mode) {
8233
+ updates.mode = parsed.mode;
8234
+ diff.push({ property: "mode", before: existing.mode ?? null, after: parsed.mode });
8235
+ }
8236
+ if (parsed.protocol !== void 0 && existing.protocol !== parsed.protocol) {
8237
+ updates.protocol = parsed.protocol;
8238
+ diff.push({ property: "protocol", before: existing.protocol ?? null, after: parsed.protocol });
8239
+ }
8240
+ if (parsed.certificate !== void 0 && existing.certificate !== parsed.certificate) {
8241
+ updates.certificate = parsed.certificate;
8242
+ diff.push({ property: "certificate", before: existing.certificate ?? null, after: parsed.certificate });
8243
+ }
8244
+ if (parsed.user !== void 0 && existing.user !== parsed.user) {
8245
+ updates.user = parsed.user;
8246
+ diff.push({ property: "user", before: existing.user ?? null, after: parsed.user });
8247
+ }
8248
+ if (parsed.password !== void 0) {
8249
+ updates.password = parsed.password;
8250
+ }
8251
+ const hasUpdates = Object.keys(updates).length > 0;
8252
+ if (!hasUpdates) {
8253
+ return {
8254
+ content: `OpenVPN client "${parsed.name}" already matches the requested configuration. No changes made.`,
8255
+ structuredContent: { action: "no_change", client: existing }
8256
+ };
8257
+ }
8258
+ if (parsed.dryRun) {
8259
+ return {
8260
+ content: `Dry run: Would update OpenVPN client "${parsed.name}".`,
8261
+ structuredContent: { action: "dry_run", diff }
8262
+ };
8263
+ }
8264
+ await context.routerClient.update(OVPN_CLIENT_PATH, existing[".id"], updates);
8265
+ log37.info({ name: parsed.name }, "OpenVPN client updated");
8266
+ return {
8267
+ content: `Updated OpenVPN client "${parsed.name}".`,
8268
+ structuredContent: { action: "updated", name: parsed.name, diff }
8269
+ };
8270
+ }
8271
+ if (!existing) {
8272
+ return {
8273
+ content: `OpenVPN client "${parsed.name}" not found. Nothing to remove.`,
8274
+ structuredContent: { action: "not_found", name: parsed.name }
8275
+ };
8276
+ }
8277
+ if (parsed.dryRun) {
8278
+ return {
8279
+ content: `Dry run: Would remove OpenVPN client "${parsed.name}".`,
8280
+ structuredContent: {
8281
+ action: "dry_run",
8282
+ diff: [{ property: "name", before: parsed.name, after: null }]
8283
+ }
8284
+ };
8285
+ }
8286
+ await context.routerClient.remove(OVPN_CLIENT_PATH, existing[".id"]);
8287
+ log37.info({ name: parsed.name }, "OpenVPN client removed");
8288
+ return {
8289
+ content: `Removed OpenVPN client "${parsed.name}".`,
8290
+ structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
8291
+ };
8292
+ } catch (err) {
8293
+ if (err instanceof MikroMCPError) throw err;
8294
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_client" });
8295
+ }
8296
+ }
8297
+ };
8298
+ var getOvpnServerInputSchema = z36.object({
8299
+ routerId: z36.string().describe("Target router identifier from the router registry")
8300
+ }).strict();
8301
+ function fetchOvpnServer(allRecords, routerId) {
8302
+ if (allRecords.length === 0) {
8303
+ throw new MikroMCPError({
8304
+ category: "NOT_FOUND" /* NOT_FOUND */,
8305
+ code: "OVPN_SERVER_NOT_FOUND",
8306
+ message: "OpenVPN server not found. The OpenVPN package may not be installed on this router.",
8307
+ details: { routerId },
8308
+ recoverability: {
8309
+ retryable: false,
8310
+ suggestedAction: "Install the OpenVPN package via manage_package.",
8311
+ alternativeTools: ["list_packages", "manage_package"]
8312
+ }
8313
+ });
8314
+ }
8315
+ return allRecords[0];
8316
+ }
8317
+ var getOvpnServerTool = {
8318
+ name: "get_ovpn_server",
8319
+ title: "Get OpenVPN Server",
8320
+ description: "Get the OpenVPN server configuration on a MikroTik router. Throws NOT_FOUND if the OpenVPN package is not installed.",
8321
+ inputSchema: getOvpnServerInputSchema,
8322
+ annotations: {
8323
+ readOnlyHint: true,
8324
+ destructiveHint: false,
8325
+ idempotentHint: true,
8326
+ openWorldHint: false
8327
+ },
8328
+ async handler(params, context) {
8329
+ const parsed = getOvpnServerInputSchema.parse(params);
8330
+ log37.info({ routerId: context.routerId }, "Getting OpenVPN server configuration");
8331
+ try {
8332
+ const records = await context.routerClient.get(OVPN_SERVER_PATH, {
8333
+ limit: void 0,
8334
+ offset: void 0
8335
+ });
8336
+ const server = fetchOvpnServer(records, parsed.routerId);
8337
+ return {
8338
+ content: `OpenVPN server on ${context.routerId}: enabled=${server.enabled}, port=${server.port}, protocol=${server.protocol}`,
8339
+ structuredContent: { routerId: context.routerId, server }
8340
+ };
8341
+ } catch (err) {
8342
+ if (err instanceof MikroMCPError) throw err;
8343
+ throw enrichError(err, { routerId: context.routerId, tool: "get_ovpn_server" });
8344
+ }
8345
+ }
8346
+ };
8347
+ var manageOvpnServerInputSchema = z36.object({
8348
+ routerId: z36.string().describe("Target router identifier from the router registry"),
8349
+ action: z36.enum(["enable", "disable", "set"]).describe("Action to perform"),
8350
+ port: z36.number().int().min(1).max(65535).optional().describe("Listening port (set action only)"),
8351
+ mode: z36.enum(["ip", "ethernet"]).optional().describe("Tunnel mode (set action only)"),
8352
+ protocol: z36.enum(["tcp-server", "udp"]).optional().describe("Transport protocol (set action only)"),
8353
+ certificate: z36.string().optional().describe("Server certificate name (set action only)"),
8354
+ cipher: z36.enum(["blowfish128", "aes128-cbc", "aes192-cbc", "aes256-cbc", "aes128-gcm", "aes256-gcm", "none"]).optional().describe("Encryption cipher (set action only)"),
8355
+ auth: z36.enum(["md5", "sha1", "sha256", "sha512", "null"]).optional().describe("Authentication algorithm (set action only)"),
8356
+ dryRun: z36.boolean().default(false).describe("Preview changes without applying")
8357
+ }).strict();
8358
+ var manageOvpnServerTool = {
8359
+ name: "manage_ovpn_server",
8360
+ title: "Manage OpenVPN Server",
8361
+ description: "Enable, disable, or configure the OpenVPN server on a MikroTik router. The server is a singleton \u2014 there is only one per router. Throws NOT_FOUND if the OpenVPN package is not installed. For set action, at least one configuration field must be provided.",
8362
+ inputSchema: manageOvpnServerInputSchema,
8363
+ annotations: {
8364
+ readOnlyHint: false,
8365
+ destructiveHint: true,
8366
+ idempotentHint: true,
8367
+ openWorldHint: false
8368
+ },
8369
+ snapshotPaths: [OVPN_SERVER_PATH],
8370
+ async handler(params, context) {
8371
+ const parsed = manageOvpnServerInputSchema.parse(params);
8372
+ log37.info({ routerId: context.routerId, action: parsed.action }, "Managing OpenVPN server");
8373
+ try {
8374
+ const records = await context.routerClient.get(OVPN_SERVER_PATH, {
8375
+ limit: void 0,
8376
+ offset: void 0
8377
+ });
8378
+ const server = fetchOvpnServer(records, parsed.routerId);
8379
+ const serverId = server[".id"];
8380
+ if (parsed.action === "enable" || parsed.action === "disable") {
8381
+ const desiredEnabled = parsed.action === "enable";
8382
+ const currentEnabled = server.enabled === "yes";
8383
+ if (desiredEnabled === currentEnabled) {
8384
+ return {
8385
+ content: `OpenVPN server on ${context.routerId} is already ${parsed.action}d. No changes made.`,
8386
+ structuredContent: { action: "no_change", server }
8387
+ };
8388
+ }
8389
+ if (parsed.dryRun) {
8390
+ return {
8391
+ content: `Dry run: Would ${parsed.action} OpenVPN server on ${context.routerId}.`,
8392
+ structuredContent: {
8393
+ action: "dry_run",
8394
+ diff: [{ property: "enabled", before: server.enabled, after: desiredEnabled ? "yes" : "no" }]
8395
+ }
8396
+ };
8397
+ }
8398
+ await context.routerClient.update(OVPN_SERVER_PATH, serverId, {
8399
+ enabled: desiredEnabled ? "yes" : "no"
8400
+ });
8401
+ log37.info({ routerId: context.routerId, action: parsed.action }, "OpenVPN server updated");
8402
+ return {
8403
+ content: `OpenVPN server on ${context.routerId} ${parsed.action}d.`,
8404
+ structuredContent: { action: parsed.action === "enable" ? "enabled" : "disabled", server }
8405
+ };
8406
+ }
8407
+ const hasFields = parsed.port !== void 0 || parsed.mode !== void 0 || parsed.protocol !== void 0 || parsed.certificate !== void 0 || parsed.cipher !== void 0 || parsed.auth !== void 0;
8408
+ if (!hasFields) {
8409
+ throw new MikroMCPError({
8410
+ category: "VALIDATION" /* VALIDATION */,
8411
+ code: "OVPN_SERVER_NO_FIELDS",
8412
+ message: "At least one field must be provided for action set.",
8413
+ details: {},
8414
+ recoverability: {
8415
+ retryable: false,
8416
+ suggestedAction: "Provide at least one of: port, mode, protocol, certificate, cipher, auth.",
8417
+ alternativeTools: []
8418
+ }
8419
+ });
8420
+ }
8421
+ const updates = {};
8422
+ const diff = [];
8423
+ if (parsed.port !== void 0 && server.port !== String(parsed.port)) {
8424
+ updates.port = String(parsed.port);
8425
+ diff.push({ property: "port", before: server.port ?? null, after: String(parsed.port) });
8426
+ }
8427
+ if (parsed.mode !== void 0 && server.mode !== parsed.mode) {
8428
+ updates.mode = parsed.mode;
8429
+ diff.push({ property: "mode", before: server.mode ?? null, after: parsed.mode });
8430
+ }
8431
+ if (parsed.protocol !== void 0 && server.protocol !== parsed.protocol) {
8432
+ updates.protocol = parsed.protocol;
8433
+ diff.push({ property: "protocol", before: server.protocol ?? null, after: parsed.protocol });
8434
+ }
8435
+ if (parsed.certificate !== void 0 && server.certificate !== parsed.certificate) {
8436
+ updates.certificate = parsed.certificate;
8437
+ diff.push({ property: "certificate", before: server.certificate ?? null, after: parsed.certificate });
8438
+ }
8439
+ if (parsed.cipher !== void 0 && server.cipher !== parsed.cipher) {
8440
+ updates.cipher = parsed.cipher;
8441
+ diff.push({ property: "cipher", before: server.cipher ?? null, after: parsed.cipher });
8442
+ }
8443
+ if (parsed.auth !== void 0 && server.auth !== parsed.auth) {
8444
+ updates.auth = parsed.auth;
8445
+ diff.push({ property: "auth", before: server.auth ?? null, after: parsed.auth });
8446
+ }
8447
+ if (Object.keys(updates).length === 0) {
8448
+ return {
8449
+ content: `OpenVPN server on ${context.routerId} already matches the requested configuration. No changes made.`,
8450
+ structuredContent: { action: "no_change", server }
8451
+ };
8452
+ }
8453
+ if (parsed.dryRun) {
8454
+ return {
8455
+ content: `Dry run: Would update OpenVPN server on ${context.routerId}.`,
8456
+ structuredContent: { action: "dry_run", diff }
8457
+ };
8458
+ }
8459
+ await context.routerClient.update(OVPN_SERVER_PATH, serverId, updates);
8460
+ log37.info({ routerId: context.routerId }, "OpenVPN server configuration updated");
8461
+ return {
8462
+ content: `Updated OpenVPN server configuration on ${context.routerId}.`,
8463
+ structuredContent: { action: "updated", diff }
8464
+ };
8465
+ } catch (err) {
8466
+ if (err instanceof MikroMCPError) throw err;
8467
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_server" });
8468
+ }
8469
+ }
8470
+ };
8471
+ var ovpnTools = [
8472
+ listOvpnClientsTool,
8473
+ manageOvpnClientTool,
8474
+ getOvpnServerTool,
8475
+ manageOvpnServerTool
8476
+ ];
8477
+
8478
+ // src/domain/tools/fleet-tools.ts
8479
+ import { z as z37 } from "zod";
8480
+
8481
+ // src/middleware/authz.ts
8482
+ function matchesPattern(name, pattern) {
8483
+ if (!pattern.includes("*")) return name === pattern;
8484
+ const prefix = pattern.slice(0, pattern.indexOf("*"));
8485
+ return name.startsWith(prefix);
8486
+ }
8487
+ function checkAuthz(identity, toolName, routerId) {
8488
+ if (identity.allowedRouters.length > 0 && !identity.allowedRouters.includes(routerId)) {
8489
+ throw new MikroMCPError({
8490
+ category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
8491
+ code: "ROUTER_NOT_ALLOWED",
8492
+ message: `Identity "${identity.id}" is not allowed to access router "${routerId}".`,
8493
+ details: { identityId: identity.id, routerId, allowedRouters: identity.allowedRouters },
8494
+ recoverability: {
8495
+ retryable: false,
8496
+ suggestedAction: `Add "${routerId}" to allowedRouters for identity "${identity.id}" in identities.yaml.`
8497
+ }
8498
+ });
8499
+ }
8500
+ if (identity.allowedToolPatterns.length > 0) {
8501
+ const allowed = identity.allowedToolPatterns.some((p) => matchesPattern(toolName, p));
8502
+ if (!allowed) {
8503
+ throw new MikroMCPError({
8504
+ category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
8505
+ code: "TOOL_NOT_ALLOWED",
8506
+ message: `Identity "${identity.id}" is not allowed to call tool "${toolName}".`,
8507
+ details: { identityId: identity.id, toolName, allowedToolPatterns: identity.allowedToolPatterns },
8508
+ recoverability: {
8509
+ retryable: false,
8510
+ suggestedAction: `Add a matching pattern to allowedToolPatterns for identity "${identity.id}" in identities.yaml.`
8511
+ }
8512
+ });
8513
+ }
8514
+ }
8515
+ }
8516
+
8517
+ // src/config/secrets.ts
8518
+ function getCredentials(router) {
7306
8519
  const { source, envPrefix, vaultPath } = router.credentials;
7307
8520
  if (source === "vault") {
7308
8521
  throw new MikroMCPError({
@@ -7431,7 +8644,7 @@ var SshClient = class {
7431
8644
  // src/adapter/ftp-client.ts
7432
8645
  import { Client as Client2 } from "basic-ftp";
7433
8646
  import { Readable } from "stream";
7434
- var log32 = createLogger("ftp-client");
8647
+ var log38 = createLogger("ftp-client");
7435
8648
  var FtpClient = class {
7436
8649
  #host;
7437
8650
  #port;
@@ -7444,7 +8657,7 @@ var FtpClient = class {
7444
8657
  this.#password = password;
7445
8658
  }
7446
8659
  async upload(remoteName, content) {
7447
- log32.info({ host: this.#host, remoteName }, "Uploading file via FTP");
8660
+ log38.info({ host: this.#host, remoteName }, "Uploading file via FTP");
7448
8661
  const client = new Client2();
7449
8662
  try {
7450
8663
  await client.access({
@@ -7454,7 +8667,7 @@ var FtpClient = class {
7454
8667
  password: this.#password
7455
8668
  });
7456
8669
  await client.uploadFrom(Readable.from(Buffer.from(content, "utf-8")), remoteName);
7457
- log32.info({ host: this.#host, remoteName }, "FTP upload complete");
8670
+ log38.info({ host: this.#host, remoteName }, "FTP upload complete");
7458
8671
  } finally {
7459
8672
  client.close();
7460
8673
  }
@@ -7562,17 +8775,17 @@ function checkFleetConfirmation(args, secret) {
7562
8775
  }
7563
8776
 
7564
8777
  // src/domain/tools/fleet-tools.ts
7565
- var log33 = createLogger("fleet-tools");
7566
- var checkHealthInputSchema = z31.object({
7567
- routerId: z31.string().describe("Target router to health-check")
8778
+ var log39 = createLogger("fleet-tools");
8779
+ var checkHealthInputSchema = z37.object({
8780
+ routerId: z37.string().describe("Target router to health-check")
7568
8781
  }).strict();
7569
- var bulkExecuteInputSchema = z31.object({
7570
- toolName: z31.string().describe("Name of the tool to fan out (must be a single-router tool)"),
7571
- routerIds: z31.array(z31.string()).optional().describe("Explicit list of router IDs to target"),
7572
- tags: z31.array(z31.string()).optional().describe("Target all routers with ALL of these tags (mutually exclusive with routerIds)"),
7573
- params: z31.record(z31.string(), z31.unknown()).describe("Params to pass to the tool (omit routerId \u2014 injected per router)"),
7574
- concurrency: z31.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls"),
7575
- confirmationToken: z31.string().optional().describe(
8782
+ var bulkExecuteInputSchema = z37.object({
8783
+ toolName: z37.string().describe("Name of the tool to fan out (must be a single-router tool)"),
8784
+ routerIds: z37.array(z37.string()).optional().describe("Explicit list of router IDs to target"),
8785
+ tags: z37.array(z37.string()).optional().describe("Target all routers with ALL of these tags (mutually exclusive with routerIds)"),
8786
+ params: z37.record(z37.string(), z37.unknown()).describe("Params to pass to the tool (omit routerId \u2014 injected per router)"),
8787
+ concurrency: z37.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls"),
8788
+ confirmationToken: z37.string().optional().describe(
7576
8789
  "Fleet confirmation token from a prior APPROVAL_REQUIRED response. Required to fan out a destructive tool."
7577
8790
  )
7578
8791
  }).strict();
@@ -7591,7 +8804,7 @@ function createFleetTools(baseTools2) {
7591
8804
  },
7592
8805
  async handler(params, context) {
7593
8806
  checkHealthInputSchema.parse(params);
7594
- log33.info({ routerId: context.routerId }, "Checking router health");
8807
+ log39.info({ routerId: context.routerId }, "Checking router health");
7595
8808
  const startMs = Date.now();
7596
8809
  try {
7597
8810
  const resources = await context.routerClient.get("system/resource");
@@ -7653,7 +8866,7 @@ function createFleetTools(baseTools2) {
7653
8866
  }
7654
8867
  });
7655
8868
  }
7656
- log33.info(
8869
+ log39.info(
7657
8870
  { toolName: parsed.toolName, concurrency: parsed.concurrency },
7658
8871
  "bulk_execute invoked"
7659
8872
  );
@@ -7700,7 +8913,7 @@ function createFleetTools(baseTools2) {
7700
8913
  try {
7701
8914
  resolved.push(context.routerRegistry.getRouter(id));
7702
8915
  } catch {
7703
- log33.warn({ routerId: id }, "bulk_execute: router not found");
8916
+ log39.warn({ routerId: id }, "bulk_execute: router not found");
7704
8917
  preErrors.push({
7705
8918
  routerId: id,
7706
8919
  status: "error",
@@ -7872,9 +9085,15 @@ var baseTools = [
7872
9085
  ...certificateTools,
7873
9086
  ...userTools,
7874
9087
  ...dhcpServerTools,
9088
+ ...ipPoolTools,
7875
9089
  ...queueTools,
7876
9090
  ...vrrpTools,
7877
- ...networkServicesTools
9091
+ ...networkServicesTools,
9092
+ ...vlanTools,
9093
+ ...dhcpClientTools,
9094
+ ...ipServiceTools,
9095
+ ...pppoeTools,
9096
+ ...ovpnTools
7878
9097
  ];
7879
9098
  var allTools = [
7880
9099
  ...baseTools,
@@ -8273,7 +9492,7 @@ var CircuitBreaker = class {
8273
9492
  };
8274
9493
 
8275
9494
  // src/mcp/tool-executor.ts
8276
- var log34 = createLogger("tool-executor");
9495
+ var log40 = createLogger("tool-executor");
8277
9496
  async function executeToolCall(tool, args, deps) {
8278
9497
  const { registry, pool, circuitBreakers, config, identityRegistry } = deps;
8279
9498
  const correlationId = nanoid4();
@@ -8369,9 +9588,9 @@ async function executeToolCall(tool, args, deps) {
8369
9588
  try {
8370
9589
  const meta = await takeSnapshot(toolContext.routerClient, routerId, path, config.snapshotDir);
8371
9590
  snapshotIds.push(meta.id);
8372
- log34.debug({ snapshotId: meta.id, path }, "Snapshot taken");
9591
+ log40.debug({ snapshotId: meta.id, path }, "Snapshot taken");
8373
9592
  } catch (err) {
8374
- log34.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
9593
+ log40.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
8375
9594
  }
8376
9595
  }
8377
9596
  }
@@ -8413,7 +9632,7 @@ async function executeToolCall(tool, args, deps) {
8413
9632
  durationMs: Date.now() - startMs
8414
9633
  }, config.auditLogPath);
8415
9634
  }
8416
- log34.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
9635
+ log40.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
8417
9636
  recordToolCall(tool.name, "success");
8418
9637
  return formatToolResult(result);
8419
9638
  } catch (err) {
@@ -8422,7 +9641,7 @@ async function executeToolCall(tool, args, deps) {
8422
9641
  const failedRouterId = args.routerId;
8423
9642
  if (failedRouterId) {
8424
9643
  pool.removeClient(failedRouterId);
8425
- log34.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
9644
+ log40.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
8426
9645
  }
8427
9646
  }
8428
9647
  if (journalId) {
@@ -8452,7 +9671,7 @@ async function executeToolCall(tool, args, deps) {
8452
9671
  durationMs: Date.now() - startMs
8453
9672
  }, config.auditLogPath);
8454
9673
  }
8455
- log34.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
9674
+ log40.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
8456
9675
  recordToolCall(tool.name, "error");
8457
9676
  return formatError(error);
8458
9677
  }
@@ -8460,12 +9679,12 @@ async function executeToolCall(tool, args, deps) {
8460
9679
  }
8461
9680
 
8462
9681
  // src/mcp/tool-registry.ts
8463
- var log35 = createLogger("tool-registry");
9682
+ var log41 = createLogger("tool-registry");
8464
9683
  function registerAllTools(server, registry, pool, circuitBreakers, config, identityRegistry) {
8465
9684
  const deps = { registry, pool, circuitBreakers, config, identityRegistry };
8466
9685
  for (const tool of allTools) {
8467
9686
  const registrationSchema = tool.annotations.destructiveHint ? tool.inputSchema.extend({
8468
- confirmationToken: z32.string().optional().describe(
9687
+ confirmationToken: z38.string().optional().describe(
8469
9688
  "Token from a prior APPROVAL_REQUIRED response. Re-submit the identical call with this token to confirm the destructive action."
8470
9689
  )
8471
9690
  }) : tool.inputSchema;
@@ -8479,56 +9698,56 @@ function registerAllTools(server, registry, pool, circuitBreakers, config, ident
8479
9698
  },
8480
9699
  (args) => executeToolCall(tool, args, deps)
8481
9700
  );
8482
- log35.debug({ tool: tool.name }, "Registered tool");
9701
+ log41.debug({ tool: tool.name }, "Registered tool");
8483
9702
  }
8484
- log35.info({ count: allTools.length }, "All tools registered");
9703
+ log41.info({ count: allTools.length }, "All tools registered");
8485
9704
  }
8486
9705
 
8487
9706
  // src/config/router-registry.ts
8488
9707
  import { readFileSync as readFileSync2, existsSync } from "fs";
8489
9708
  import { parse } from "yaml";
8490
- import { z as z33 } from "zod";
8491
- var log36 = createLogger("router-registry");
8492
- var RouterConfigSchema = z33.object({
8493
- host: z33.string().min(1, "host is required"),
8494
- port: z33.number().int().min(1).max(65535),
8495
- tls: z33.object({
8496
- enabled: z33.boolean(),
8497
- rejectUnauthorized: z33.boolean(),
8498
- ca: z33.string().optional(),
8499
- fingerprint: z33.string().optional()
9709
+ import { z as z39 } from "zod";
9710
+ var log42 = createLogger("router-registry");
9711
+ var RouterConfigSchema = z39.object({
9712
+ host: z39.string().min(1, "host is required"),
9713
+ port: z39.number().int().min(1).max(65535),
9714
+ tls: z39.object({
9715
+ enabled: z39.boolean(),
9716
+ rejectUnauthorized: z39.boolean(),
9717
+ ca: z39.string().optional(),
9718
+ fingerprint: z39.string().optional()
8500
9719
  }).strict(),
8501
- credentials: z33.object({
8502
- source: z33.enum(["env", "vault"]),
8503
- envPrefix: z33.string().optional(),
8504
- vaultPath: z33.string().optional()
9720
+ credentials: z39.object({
9721
+ source: z39.enum(["env", "vault"]),
9722
+ envPrefix: z39.string().optional(),
9723
+ vaultPath: z39.string().optional()
8505
9724
  }).strict(),
8506
- tags: z33.array(z33.string()).default([]),
8507
- rosVersion: z33.string().min(1),
8508
- sshPort: z33.number().int().min(1).max(65535).optional(),
8509
- sshFingerprint: z33.string().optional(),
8510
- cmdAllow: z33.array(z33.string()).optional(),
8511
- cmdDeny: z33.array(z33.string()).optional(),
8512
- maintenanceWindows: z33.array(
8513
- z33.object({
8514
- days: z33.array(
8515
- z33.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
9725
+ tags: z39.array(z39.string()).default([]),
9726
+ rosVersion: z39.string().min(1),
9727
+ sshPort: z39.number().int().min(1).max(65535).optional(),
9728
+ sshFingerprint: z39.string().optional(),
9729
+ cmdAllow: z39.array(z39.string()).optional(),
9730
+ cmdDeny: z39.array(z39.string()).optional(),
9731
+ maintenanceWindows: z39.array(
9732
+ z39.object({
9733
+ days: z39.array(
9734
+ z39.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
8516
9735
  ),
8517
- startTime: z33.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
8518
- endTime: z33.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
8519
- timezone: z33.string().min(1)
9736
+ startTime: z39.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
9737
+ endTime: z39.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
9738
+ timezone: z39.string().min(1)
8520
9739
  }).strict()
8521
9740
  ).optional()
8522
9741
  }).strict();
8523
- var ConfigFileSchema = z33.object({
8524
- routers: z33.record(z33.string(), RouterConfigSchema)
9742
+ var ConfigFileSchema = z39.object({
9743
+ routers: z39.record(z39.string(), RouterConfigSchema)
8525
9744
  }).strict();
8526
9745
  var RouterRegistry = class {
8527
9746
  routers;
8528
9747
  constructor(configPath) {
8529
9748
  this.routers = /* @__PURE__ */ new Map();
8530
9749
  if (!existsSync(configPath)) {
8531
- log36.warn({ configPath }, "Router config file not found; starting with empty registry");
9750
+ log42.warn({ configPath }, "Router config file not found; starting with empty registry");
8532
9751
  return;
8533
9752
  }
8534
9753
  const raw = readFileSync2(configPath, "utf-8");
@@ -8542,14 +9761,14 @@ ${issues}`);
8542
9761
  for (const [id, config] of Object.entries(result.data.routers)) {
8543
9762
  const routerConfig = { ...config, id };
8544
9763
  if (routerConfig.tls.enabled && !routerConfig.tls.rejectUnauthorized) {
8545
- log36.warn(
9764
+ log42.warn(
8546
9765
  { routerId: id },
8547
9766
  "TLS certificate validation is DISABLED (rejectUnauthorized=false). Set tls.fingerprint to pin the server certificate instead."
8548
9767
  );
8549
9768
  }
8550
9769
  this.routers.set(id, routerConfig);
8551
9770
  }
8552
- log36.info({ count: this.routers.size }, "Loaded routers from config");
9771
+ log42.info({ count: this.routers.size }, "Loaded routers from config");
8553
9772
  }
8554
9773
  getRouter(id) {
8555
9774
  const router = this.routers.get(id);
@@ -8834,7 +10053,7 @@ var ConnectionPool = class {
8834
10053
  };
8835
10054
 
8836
10055
  // src/version.ts
8837
- var VERSION = "1.1.0";
10056
+ var VERSION = "1.3.0";
8838
10057
 
8839
10058
  // src/mcp/server.ts
8840
10059
  function createServerFactory(config, identityRegistry) {
@@ -8864,7 +10083,7 @@ import { randomUUID } from "crypto";
8864
10083
  import { createServer } from "http";
8865
10084
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8866
10085
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
8867
- var log37 = createLogger("transport-http");
10086
+ var log43 = createLogger("transport-http");
8868
10087
  var BodyTooLargeError = class extends Error {
8869
10088
  constructor() {
8870
10089
  super("Request body exceeds maximum allowed size");
@@ -8956,7 +10175,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
8956
10175
  return;
8957
10176
  }
8958
10177
  if (!checkRateLimit(ip)) {
8959
- log37.warn({ ip }, "Rate limit exceeded");
10178
+ log43.warn({ ip }, "Rate limit exceeded");
8960
10179
  res.writeHead(429, { "Content-Type": "application/json" });
8961
10180
  res.end(JSON.stringify({ error: "Too many requests. Retry after 60 seconds." }));
8962
10181
  return;
@@ -8997,7 +10216,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
8997
10216
  }
8998
10217
  return;
8999
10218
  }
9000
- log37.error({ err }, "HTTP transport error");
10219
+ log43.error({ err }, "HTTP transport error");
9001
10220
  if (!res.headersSent) {
9002
10221
  res.writeHead(500, { "Content-Type": "application/json" });
9003
10222
  res.end(JSON.stringify({ error: "Internal server error" }));
@@ -9007,7 +10226,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
9007
10226
  httpServer.on("close", () => clearInterval(sweepTimer));
9008
10227
  await new Promise((resolve, reject) => {
9009
10228
  httpServer.listen(port, bindHost, () => {
9010
- log37.info(
10229
+ log43.info(
9011
10230
  { port, bindHost },
9012
10231
  "HTTP transport listening (POST /mcp for Streamable HTTP, GET /sse for legacy SSE)"
9013
10232
  );
@@ -9031,13 +10250,13 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
9031
10250
  sessionIdGenerator: () => randomUUID(),
9032
10251
  onsessioninitialized: (id) => {
9033
10252
  streamableSessions.set(id, transport2);
9034
- log37.info({ sessionId: id }, "Streamable HTTP session created");
10253
+ log43.info({ sessionId: id }, "Streamable HTTP session created");
9035
10254
  }
9036
10255
  });
9037
10256
  transport2.onclose = () => {
9038
10257
  if (transport2.sessionId) {
9039
10258
  streamableSessions.delete(transport2.sessionId);
9040
- log37.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
10259
+ log43.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
9041
10260
  }
9042
10261
  };
9043
10262
  await makeServer().connect(transport2);
@@ -9054,7 +10273,7 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
9054
10273
  sseTransport.onclose = () => {
9055
10274
  sseTransports.delete(sseTransport.sessionId);
9056
10275
  };
9057
- log37.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
10276
+ log43.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
9058
10277
  await sseTransport.start();
9059
10278
  return;
9060
10279
  }
@@ -9075,23 +10294,23 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
9075
10294
  // src/config/identity-registry.ts
9076
10295
  import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
9077
10296
  import { parse as parse2 } from "yaml";
9078
- import { z as z34 } from "zod";
10297
+ import { z as z40 } from "zod";
9079
10298
  import bcrypt from "bcryptjs";
9080
- var log38 = createLogger("identity-registry");
9081
- var IdentityConfigSchema = z34.object({
9082
- token: z34.string().min(1, "token hash is required"),
9083
- role: z34.enum(["readonly", "operator", "admin", "superadmin"]),
9084
- allowedRouters: z34.array(z34.string()).default([]),
9085
- allowedToolPatterns: z34.array(z34.string()).default([])
10299
+ var log44 = createLogger("identity-registry");
10300
+ var IdentityConfigSchema = z40.object({
10301
+ token: z40.string().min(1, "token hash is required"),
10302
+ role: z40.enum(["readonly", "operator", "admin", "superadmin"]),
10303
+ allowedRouters: z40.array(z40.string()).default([]),
10304
+ allowedToolPatterns: z40.array(z40.string()).default([])
9086
10305
  }).strict();
9087
- var ConfigFileSchema2 = z34.object({
9088
- identities: z34.record(z34.string(), IdentityConfigSchema)
10306
+ var ConfigFileSchema2 = z40.object({
10307
+ identities: z40.record(z40.string(), IdentityConfigSchema)
9089
10308
  }).strict();
9090
10309
  var IdentityRegistry = class {
9091
10310
  identities = [];
9092
10311
  constructor(configPath) {
9093
10312
  if (!existsSync2(configPath)) {
9094
- log38.warn(
10313
+ log44.warn(
9095
10314
  { configPath },
9096
10315
  "Identity config file not found; starting with empty registry (stdio callers do not require it)"
9097
10316
  );
@@ -9114,7 +10333,7 @@ ${issues}`);
9114
10333
  allowedToolPatterns: config.allowedToolPatterns
9115
10334
  });
9116
10335
  }
9117
- log38.info({ count: this.identities.length }, "Loaded identities from config");
10336
+ log44.info({ count: this.identities.length }, "Loaded identities from config");
9118
10337
  }
9119
10338
  getIdentities() {
9120
10339
  return this.identities.map(({ id, role, allowedRouters, allowedToolPatterns }) => ({
@@ -9143,7 +10362,7 @@ ${issues}`);
9143
10362
  // src/domain/snapshot/retention.ts
9144
10363
  import { readdir, stat, unlink } from "fs/promises";
9145
10364
  import { join as join3 } from "path";
9146
- var log39 = createLogger("snapshot-retention");
10365
+ var log45 = createLogger("snapshot-retention");
9147
10366
  async function pruneSnapshots(snapshotDir, maxAgeDays) {
9148
10367
  const cutoffMs = Date.now() - maxAgeDays * 864e5;
9149
10368
  let removed = 0;
@@ -9170,21 +10389,21 @@ async function pruneSnapshots(snapshotDir, maxAgeDays) {
9170
10389
  removed++;
9171
10390
  }
9172
10391
  } catch (err) {
9173
- log39.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
10392
+ log45.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
9174
10393
  }
9175
10394
  }
9176
10395
  }
9177
10396
  if (removed > 0) {
9178
- log39.info({ removed, maxAgeDays }, "Pruned expired snapshots");
10397
+ log45.info({ removed, maxAgeDays }, "Pruned expired snapshots");
9179
10398
  }
9180
10399
  return removed;
9181
10400
  }
9182
10401
 
9183
10402
  // src/cli/serve.ts
9184
- var log40 = createLogger("main");
10403
+ var log46 = createLogger("main");
9185
10404
  async function runServe() {
9186
10405
  const config = loadAppConfig();
9187
- log40.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
10406
+ log46.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
9188
10407
  void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
9189
10408
  const identityRegistry = new IdentityRegistry(config.identitiesPath);
9190
10409
  if (config.transport === "http") {
@@ -9192,7 +10411,7 @@ async function runServe() {
9192
10411
  (i) => i.role === "readonly" || i.role === "operator"
9193
10412
  );
9194
10413
  if (hasLimitedRoles && !config.confirmationSecret) {
9195
- log40.error(
10414
+ log46.error(
9196
10415
  "MIKROMCP_CONFIRMATION_SECRET is required in HTTP mode when identities with role readonly or operator are configured"
9197
10416
  );
9198
10417
  process.exit(1);
@@ -9202,7 +10421,7 @@ async function runServe() {
9202
10421
  if (config.transport === "stdio") {
9203
10422
  const stdioIdentity = getStdioIdentity(config.stdioIdentity, identityRegistry);
9204
10423
  await withIdentity(stdioIdentity, () => connectStdio(makeServer()));
9205
- log40.info("MikroMCP server running via stdio");
10424
+ log46.info("MikroMCP server running via stdio");
9206
10425
  } else if (config.transport === "http") {
9207
10426
  await connectHttp(
9208
10427
  makeServer,
@@ -9214,13 +10433,13 @@ async function runServe() {
9214
10433
  },
9215
10434
  identityRegistry
9216
10435
  );
9217
- log40.info(
10436
+ log46.info(
9218
10437
  { port: config.port, bindHost: config.bindHost },
9219
10438
  "MikroMCP server running via HTTP/SSE"
9220
10439
  );
9221
10440
  }
9222
10441
  const shutdown = () => {
9223
- log40.info("Shutting down MikroMCP server");
10442
+ log46.info("Shutting down MikroMCP server");
9224
10443
  pool.closeAll();
9225
10444
  process.exit(0);
9226
10445
  };
@@ -9233,7 +10452,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
9233
10452
  import { homedir as homedir2 } from "os";
9234
10453
  import * as net from "net";
9235
10454
  import chalk from "chalk";
9236
- var localVersion = "1.1.0";
10455
+ var localVersion = "1.3.0";
9237
10456
  var results = [];
9238
10457
  function ok(msg) {
9239
10458
  console.log(chalk.green("\u2705 ") + msg);
@@ -9885,7 +11104,7 @@ async function runInit() {
9885
11104
  }
9886
11105
 
9887
11106
  // src/cli/index.ts
9888
- var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.1.0");
11107
+ var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.3.0");
9889
11108
  program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
9890
11109
  program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
9891
11110
  program.command("init").description("Interactive setup wizard").action(runInit);