mikromcp 1.2.0 → 1.4.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 +6 -5
  2. package/dist/main.js +1846 -272
  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 z36 } from "zod";
63
+ import { z as z42 } from "zod";
64
64
 
65
65
  // src/domain/tools/system-tools.ts
66
66
  import { z } from "zod";
@@ -497,9 +497,9 @@ var listInterfacesTool = {
497
497
  const paginated = interfaces.slice(parsed.offset, parsed.offset + parsed.limit);
498
498
  const results2 = paginated.map((iface) => {
499
499
  const rec = iface;
500
- const isRunning = rec.running === true || rec.running === "true";
500
+ const isRunning2 = rec.running === true || rec.running === "true";
501
501
  const isDisabled = rec.disabled === true || rec.disabled === "true";
502
- const enriched = { status: isDisabled ? "disabled" : isRunning ? "up" : "down" };
502
+ const enriched = { status: isDisabled ? "disabled" : isRunning2 ? "up" : "down" };
503
503
  for (const [key2, value] of Object.entries(rec)) {
504
504
  if (parsed.includeCounters || !COUNTER_PROPS.includes(key2)) {
505
505
  enriched[key2] = value;
@@ -6057,14 +6057,224 @@ var manageUserTool = {
6057
6057
  };
6058
6058
  var userTools = [listUsersTool, manageUserTool];
6059
6059
 
6060
- // src/domain/tools/dhcp-server-tools.ts
6060
+ // src/domain/tools/user-group-tools.ts
6061
6061
  import { z as z27 } from "zod";
6062
- var log28 = createLogger("dhcp-server-tools");
6063
- var listDhcpServersInputSchema = z27.object({
6062
+ var log28 = createLogger("user-group-tools");
6063
+ var listUserGroupsInputSchema = z27.object({
6064
+ routerId: z27.string().describe("Target router identifier from the router registry"),
6065
+ limit: z27.number().int().min(1).max(500).default(100).describe("Maximum number of groups to return")
6066
+ }).strict();
6067
+ var listUserGroupsTool = {
6068
+ name: "list_user_groups",
6069
+ title: "List User Groups",
6070
+ description: "List local user groups on a MikroTik router.",
6071
+ inputSchema: listUserGroupsInputSchema,
6072
+ annotations: {
6073
+ readOnlyHint: true,
6074
+ destructiveHint: false,
6075
+ idempotentHint: true,
6076
+ openWorldHint: false
6077
+ },
6078
+ async handler(params, context) {
6079
+ const parsed = listUserGroupsInputSchema.parse(params);
6080
+ log28.info({ routerId: context.routerId }, "Listing user groups");
6081
+ try {
6082
+ const all = await context.routerClient.get("user/group", {
6083
+ limit: void 0,
6084
+ offset: void 0
6085
+ });
6086
+ const groups = all.slice(0, parsed.limit);
6087
+ return {
6088
+ content: `User groups on ${context.routerId}: ${groups.length} returned (${all.length} total)`,
6089
+ structuredContent: {
6090
+ routerId: context.routerId,
6091
+ groups,
6092
+ total: all.length,
6093
+ returned: groups.length
6094
+ }
6095
+ };
6096
+ } catch (err) {
6097
+ throw enrichError(err, { routerId: context.routerId, tool: "list_user_groups" });
6098
+ }
6099
+ }
6100
+ };
6101
+ var manageUserGroupInputSchema = z27.object({
6064
6102
  routerId: z27.string().describe("Target router identifier from the router registry"),
6065
- interface: z27.string().optional().describe("Filter by interface name (exact match)"),
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")
6103
+ action: z27.enum(["add", "update", "remove"]).describe("Action to perform"),
6104
+ name: z27.string().describe("Group name \u2014 idempotency key"),
6105
+ policy: z27.string().optional().describe("Comma-separated policy list (e.g. 'read,write,ftp')"),
6106
+ skin: z27.string().optional().describe("Optional skin name for the group"),
6107
+ dryRun: z27.boolean().default(false).describe("Preview changes without applying")
6108
+ }).strict();
6109
+ var manageUserGroupTool = {
6110
+ name: "manage_user_group",
6111
+ title: "Manage User Group",
6112
+ description: "Add, update, or remove a local RouterOS user group. Idempotent by name: add returns already_exists if a group with the same name and policy already exists.",
6113
+ inputSchema: manageUserGroupInputSchema,
6114
+ annotations: {
6115
+ readOnlyHint: false,
6116
+ destructiveHint: false,
6117
+ idempotentHint: true,
6118
+ openWorldHint: false
6119
+ },
6120
+ snapshotPaths: ["user/group"],
6121
+ async handler(params, context) {
6122
+ const parsed = manageUserGroupInputSchema.parse(params);
6123
+ log28.info(
6124
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
6125
+ "Managing user group"
6126
+ );
6127
+ try {
6128
+ if (parsed.action === "add") {
6129
+ const all2 = await context.routerClient.get("user/group", {
6130
+ limit: void 0,
6131
+ offset: void 0
6132
+ });
6133
+ const existing2 = all2.find((g) => g.name === parsed.name);
6134
+ if (existing2) {
6135
+ if (parsed.policy === void 0 || existing2.policy === parsed.policy) {
6136
+ return {
6137
+ content: `User group "${parsed.name}" already exists. No changes made.`,
6138
+ structuredContent: { action: "already_exists", name: parsed.name, policy: existing2.policy }
6139
+ };
6140
+ }
6141
+ throw new MikroMCPError({
6142
+ category: "CONFLICT" /* CONFLICT */,
6143
+ code: "USER_GROUP_CONFLICT",
6144
+ message: `Group "${parsed.name}" exists with a different policy.`,
6145
+ details: { existingPolicy: existing2.policy, requestedPolicy: parsed.policy },
6146
+ recoverability: {
6147
+ retryable: false,
6148
+ suggestedAction: "Remove the existing group first or use a different name.",
6149
+ alternativeTools: ["manage_user_group"]
6150
+ }
6151
+ });
6152
+ }
6153
+ if (parsed.dryRun) {
6154
+ return {
6155
+ content: `Dry run: Would add group "${parsed.name}".`,
6156
+ structuredContent: {
6157
+ action: "dry_run",
6158
+ diff: [
6159
+ { property: "name", before: null, after: parsed.name },
6160
+ { property: "policy", before: null, after: parsed.policy ?? "" }
6161
+ ]
6162
+ }
6163
+ };
6164
+ }
6165
+ const body = { name: parsed.name };
6166
+ if (parsed.policy !== void 0) body.policy = parsed.policy;
6167
+ if (parsed.skin !== void 0) body.skin = parsed.skin;
6168
+ const created = await context.routerClient.create("user/group", body);
6169
+ log28.info({ name: parsed.name, id: created[".id"] }, "User group added");
6170
+ return {
6171
+ content: `Added group "${parsed.name}".`,
6172
+ structuredContent: {
6173
+ action: "created",
6174
+ routerId: context.routerId,
6175
+ name: parsed.name,
6176
+ id: created[".id"]
6177
+ }
6178
+ };
6179
+ }
6180
+ if (parsed.action === "update") {
6181
+ const all2 = await context.routerClient.get("user/group", {
6182
+ limit: void 0,
6183
+ offset: void 0
6184
+ });
6185
+ const existing2 = all2.find((g) => g.name === parsed.name);
6186
+ if (!existing2) {
6187
+ throw new MikroMCPError({
6188
+ category: "NOT_FOUND" /* NOT_FOUND */,
6189
+ code: "USER_GROUP_NOT_FOUND",
6190
+ message: `Group "${parsed.name}" not found.`,
6191
+ details: { name: parsed.name },
6192
+ recoverability: {
6193
+ retryable: false,
6194
+ suggestedAction: "Verify the group name with list_user_groups.",
6195
+ alternativeTools: ["list_user_groups"]
6196
+ }
6197
+ });
6198
+ }
6199
+ if (parsed.policy !== void 0 && existing2.policy === parsed.policy) {
6200
+ return {
6201
+ content: `Group "${parsed.name}" already has the requested policy. No changes made.`,
6202
+ structuredContent: { action: "no_change", name: parsed.name }
6203
+ };
6204
+ }
6205
+ if (parsed.dryRun) {
6206
+ return {
6207
+ content: `Dry run: Would update group "${parsed.name}".`,
6208
+ structuredContent: {
6209
+ action: "dry_run",
6210
+ diff: [
6211
+ {
6212
+ property: "policy",
6213
+ before: existing2.policy ?? null,
6214
+ after: parsed.policy ?? null
6215
+ }
6216
+ ]
6217
+ }
6218
+ };
6219
+ }
6220
+ const changes = {};
6221
+ if (parsed.policy !== void 0) changes.policy = parsed.policy;
6222
+ if (parsed.skin !== void 0) changes.skin = parsed.skin;
6223
+ if (Object.keys(changes).length === 0) {
6224
+ return {
6225
+ content: `User group "${parsed.name}" already has the requested configuration. No changes made.`,
6226
+ structuredContent: { action: "no_change", name: parsed.name }
6227
+ };
6228
+ }
6229
+ await context.routerClient.update("user/group", existing2[".id"], changes);
6230
+ log28.info({ name: parsed.name }, "User group updated");
6231
+ return {
6232
+ content: `Updated group "${parsed.name}".`,
6233
+ structuredContent: { action: "updated", routerId: context.routerId, name: parsed.name }
6234
+ };
6235
+ }
6236
+ const all = await context.routerClient.get("user/group", {
6237
+ limit: void 0,
6238
+ offset: void 0
6239
+ });
6240
+ const existing = all.find((g) => g.name === parsed.name);
6241
+ if (!existing) {
6242
+ return {
6243
+ content: `Group "${parsed.name}" not found. Nothing to remove.`,
6244
+ structuredContent: { action: "not_found", name: parsed.name }
6245
+ };
6246
+ }
6247
+ if (parsed.dryRun) {
6248
+ return {
6249
+ content: `Dry run: Would remove group "${parsed.name}".`,
6250
+ structuredContent: {
6251
+ action: "dry_run",
6252
+ diff: [{ property: "name", before: parsed.name, after: null }]
6253
+ }
6254
+ };
6255
+ }
6256
+ await context.routerClient.remove("user/group", existing[".id"]);
6257
+ log28.info({ name: parsed.name }, "User group removed");
6258
+ return {
6259
+ content: `Removed group "${parsed.name}".`,
6260
+ structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
6261
+ };
6262
+ } catch (err) {
6263
+ if (err instanceof MikroMCPError) throw err;
6264
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_user_group" });
6265
+ }
6266
+ }
6267
+ };
6268
+ var userGroupTools = [listUserGroupsTool, manageUserGroupTool];
6269
+
6270
+ // src/domain/tools/dhcp-server-tools.ts
6271
+ import { z as z28 } from "zod";
6272
+ var log29 = createLogger("dhcp-server-tools");
6273
+ var listDhcpServersInputSchema = z28.object({
6274
+ routerId: z28.string().describe("Target router identifier from the router registry"),
6275
+ interface: z28.string().optional().describe("Filter by interface name (exact match)"),
6276
+ limit: z28.number().int().min(1).max(500).default(100).describe("Maximum number of DHCP servers to return"),
6277
+ offset: z28.number().int().min(0).default(0).describe("Offset for pagination")
6068
6278
  }).strict();
6069
6279
  var listDhcpServersTool = {
6070
6280
  name: "list_dhcp_servers",
@@ -6079,7 +6289,7 @@ var listDhcpServersTool = {
6079
6289
  },
6080
6290
  async handler(params, context) {
6081
6291
  const parsed = listDhcpServersInputSchema.parse(params);
6082
- log28.info({ routerId: context.routerId }, "Listing DHCP servers");
6292
+ log29.info({ routerId: context.routerId }, "Listing DHCP servers");
6083
6293
  try {
6084
6294
  const allServers = await context.routerClient.get("ip/dhcp-server", {
6085
6295
  limit: void 0,
@@ -6105,15 +6315,15 @@ var listDhcpServersTool = {
6105
6315
  }
6106
6316
  }
6107
6317
  };
6108
- var manageDhcpServerInputSchema = z27.object({
6109
- routerId: z27.string().describe("Target router identifier from the router registry"),
6110
- action: z27.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
6111
- name: z27.string().describe("Server name \u2014 idempotency key"),
6112
- interface: z27.string().optional().describe("Interface to serve DHCP on (required for add)"),
6113
- addressPool: z27.string().optional().describe("IP pool name (required for add)"),
6114
- leaseTime: z27.string().optional().describe("Lease duration (e.g. '1d', '12h')"),
6115
- comment: z27.string().optional().describe("Optional comment"),
6116
- dryRun: z27.boolean().default(false).describe("Preview changes without applying")
6318
+ var manageDhcpServerInputSchema = z28.object({
6319
+ routerId: z28.string().describe("Target router identifier from the router registry"),
6320
+ action: z28.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
6321
+ name: z28.string().describe("Server name \u2014 idempotency key"),
6322
+ interface: z28.string().optional().describe("Interface to serve DHCP on (required for add)"),
6323
+ addressPool: z28.string().optional().describe("IP pool name (required for add)"),
6324
+ leaseTime: z28.string().optional().describe("Lease duration (e.g. '1d', '12h')"),
6325
+ comment: z28.string().optional().describe("Optional comment"),
6326
+ dryRun: z28.boolean().default(false).describe("Preview changes without applying")
6117
6327
  }).strict();
6118
6328
  var manageDhcpServerTool = {
6119
6329
  name: "manage_dhcp_server",
@@ -6129,7 +6339,7 @@ var manageDhcpServerTool = {
6129
6339
  snapshotPaths: ["ip/dhcp-server"],
6130
6340
  async handler(params, context) {
6131
6341
  const parsed = manageDhcpServerInputSchema.parse(params);
6132
- log28.info(
6342
+ log29.info(
6133
6343
  { routerId: context.routerId, action: parsed.action, name: parsed.name },
6134
6344
  "Managing DHCP server"
6135
6345
  );
@@ -6215,7 +6425,7 @@ var manageDhcpServerTool = {
6215
6425
  if (parsed.leaseTime) body["lease-time"] = parsed.leaseTime;
6216
6426
  if (parsed.comment) body.comment = parsed.comment;
6217
6427
  const created = await context.routerClient.create("ip/dhcp-server", body);
6218
- log28.info({ name: parsed.name, id: created[".id"] }, "DHCP server added");
6428
+ log29.info({ name: parsed.name, id: created[".id"] }, "DHCP server added");
6219
6429
  return {
6220
6430
  content: `Added DHCP server "${parsed.name}".`,
6221
6431
  structuredContent: { action: "created", server: created }
@@ -6238,7 +6448,7 @@ var manageDhcpServerTool = {
6238
6448
  };
6239
6449
  }
6240
6450
  await context.routerClient.remove("ip/dhcp-server", existing[".id"]);
6241
- log28.info({ name: parsed.name }, "DHCP server removed");
6451
+ log29.info({ name: parsed.name }, "DHCP server removed");
6242
6452
  return {
6243
6453
  content: `Removed DHCP server "${parsed.name}".`,
6244
6454
  structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
@@ -6277,7 +6487,7 @@ var manageDhcpServerTool = {
6277
6487
  disabled: disabledValue
6278
6488
  });
6279
6489
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
6280
- log28.info({ name: parsed.name, action: resultAction }, "DHCP server updated");
6490
+ log29.info({ name: parsed.name, action: resultAction }, "DHCP server updated");
6281
6491
  return {
6282
6492
  content: `DHCP server "${parsed.name}" ${resultAction}.`,
6283
6493
  structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
@@ -6291,13 +6501,13 @@ var manageDhcpServerTool = {
6291
6501
  var dhcpServerTools = [listDhcpServersTool, manageDhcpServerTool];
6292
6502
 
6293
6503
  // 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")
6504
+ import { z as z29 } from "zod";
6505
+ var log30 = createLogger("ip-pool-tools");
6506
+ var listIpPoolsInputSchema = z29.object({
6507
+ routerId: z29.string().describe("Target router identifier from the router registry"),
6508
+ name: z29.string().optional().describe("Filter by pool name (substring match)"),
6509
+ limit: z29.number().int().min(1).max(500).default(100).describe("Maximum number of pools to return"),
6510
+ offset: z29.number().int().min(0).default(0).describe("Offset for pagination")
6301
6511
  }).strict();
6302
6512
  var listIpPoolsTool = {
6303
6513
  name: "list_ip_pools",
@@ -6312,7 +6522,7 @@ var listIpPoolsTool = {
6312
6522
  },
6313
6523
  async handler(params, context) {
6314
6524
  const parsed = listIpPoolsInputSchema.parse(params);
6315
- log29.info({ routerId: context.routerId }, "Listing IP pools");
6525
+ log30.info({ routerId: context.routerId }, "Listing IP pools");
6316
6526
  try {
6317
6527
  const allPools = await context.routerClient.get("ip/pool", {
6318
6528
  limit: void 0,
@@ -6341,13 +6551,13 @@ var listIpPoolsTool = {
6341
6551
  }
6342
6552
  }
6343
6553
  };
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")
6554
+ var manageIpPoolInputSchema = z29.object({
6555
+ routerId: z29.string().describe("Target router identifier from the router registry"),
6556
+ action: z29.enum(["add", "remove"]).describe("Action to perform"),
6557
+ name: z29.string().describe("Pool name \u2014 idempotency key"),
6558
+ ranges: z29.string().optional().describe("IP range (e.g. '192.168.1.100-192.168.1.200'; required for add)"),
6559
+ nextPool: z29.string().optional().describe("Next pool name for overflow"),
6560
+ dryRun: z29.boolean().default(false).describe("Preview changes without applying")
6351
6561
  }).strict();
6352
6562
  var manageIpPoolTool = {
6353
6563
  name: "manage_ip_pool",
@@ -6363,7 +6573,7 @@ var manageIpPoolTool = {
6363
6573
  snapshotPaths: ["ip/pool"],
6364
6574
  async handler(params, context) {
6365
6575
  const parsed = manageIpPoolInputSchema.parse(params);
6366
- log29.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP pool");
6576
+ log30.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP pool");
6367
6577
  try {
6368
6578
  const allPools = await context.routerClient.get("ip/pool", {
6369
6579
  limit: void 0,
@@ -6416,7 +6626,7 @@ var manageIpPoolTool = {
6416
6626
  const body = { name: parsed.name, ranges: parsed.ranges };
6417
6627
  if (parsed.nextPool) body["next-pool"] = parsed.nextPool;
6418
6628
  const created = await context.routerClient.create("ip/pool", body);
6419
- log29.info({ name: parsed.name, id: created[".id"] }, "IP pool added");
6629
+ log30.info({ name: parsed.name, id: created[".id"] }, "IP pool added");
6420
6630
  return {
6421
6631
  content: `Added IP pool "${parsed.name}" with ranges ${parsed.ranges}.`,
6422
6632
  structuredContent: { action: "created", pool: created }
@@ -6435,7 +6645,7 @@ var manageIpPoolTool = {
6435
6645
  };
6436
6646
  }
6437
6647
  await context.routerClient.remove("ip/pool", existing[".id"]);
6438
- log29.info({ name: parsed.name }, "IP pool removed");
6648
+ log30.info({ name: parsed.name }, "IP pool removed");
6439
6649
  return {
6440
6650
  content: `Removed IP pool "${parsed.name}".`,
6441
6651
  structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
@@ -6449,12 +6659,12 @@ var manageIpPoolTool = {
6449
6659
  var ipPoolTools = [listIpPoolsTool, manageIpPoolTool];
6450
6660
 
6451
6661
  // src/domain/tools/queue-tools.ts
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")
6662
+ import { z as z30 } from "zod";
6663
+ var log31 = createLogger("queue-tools");
6664
+ var listQueuesInputSchema = z30.object({
6665
+ routerId: z30.string().describe("Target router identifier from the router registry"),
6666
+ target: z30.string().optional().describe("Filter by target address (substring match)"),
6667
+ limit: z30.number().int().min(1).max(500).default(100).describe("Maximum number of queues to return")
6458
6668
  }).strict();
6459
6669
  var listQueuesTool = {
6460
6670
  name: "list_queues",
@@ -6469,7 +6679,7 @@ var listQueuesTool = {
6469
6679
  },
6470
6680
  async handler(params, context) {
6471
6681
  const parsed = listQueuesInputSchema.parse(params);
6472
- log30.info({ routerId: context.routerId }, "Listing queues");
6682
+ log31.info({ routerId: context.routerId }, "Listing queues");
6473
6683
  try {
6474
6684
  const allQueues = await context.routerClient.get("queue/simple", {
6475
6685
  limit: void 0,
@@ -6493,15 +6703,15 @@ var listQueuesTool = {
6493
6703
  }
6494
6704
  }
6495
6705
  };
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")
6706
+ var manageQueueInputSchema = z30.object({
6707
+ routerId: z30.string().describe("Target router identifier from the router registry"),
6708
+ action: z30.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
6709
+ name: z30.string().describe("Queue name \u2014 idempotency key"),
6710
+ target: z30.string().optional().describe("Target address (required for add; e.g. '192.168.1.0/24')"),
6711
+ maxLimit: z30.string().optional().describe("Max upload/download limit (e.g. '10M/10M')"),
6712
+ limitAt: z30.string().optional().describe("Guaranteed rate (e.g. '1M/1M')"),
6713
+ comment: z30.string().optional().describe("Optional comment"),
6714
+ dryRun: z30.boolean().default(false).describe("Preview changes without applying")
6505
6715
  }).strict();
6506
6716
  var manageQueueTool = {
6507
6717
  name: "manage_queue",
@@ -6517,7 +6727,7 @@ var manageQueueTool = {
6517
6727
  snapshotPaths: ["queue/simple"],
6518
6728
  async handler(params, context) {
6519
6729
  const parsed = manageQueueInputSchema.parse(params);
6520
- log30.info(
6730
+ log31.info(
6521
6731
  { routerId: context.routerId, action: parsed.action, name: parsed.name },
6522
6732
  "Managing queue"
6523
6733
  );
@@ -6579,7 +6789,7 @@ var manageQueueTool = {
6579
6789
  if (parsed.limitAt) body["limit-at"] = parsed.limitAt;
6580
6790
  if (parsed.comment) body.comment = parsed.comment;
6581
6791
  const created = await context.routerClient.create("queue/simple", body);
6582
- log30.info({ name: parsed.name, id: created[".id"] }, "Queue added");
6792
+ log31.info({ name: parsed.name, id: created[".id"] }, "Queue added");
6583
6793
  return {
6584
6794
  content: `Added queue "${parsed.name}".`,
6585
6795
  structuredContent: { action: "created", queue: created }
@@ -6602,7 +6812,7 @@ var manageQueueTool = {
6602
6812
  };
6603
6813
  }
6604
6814
  await context.routerClient.remove("queue/simple", existing[".id"]);
6605
- log30.info({ name: parsed.name }, "Queue removed");
6815
+ log31.info({ name: parsed.name }, "Queue removed");
6606
6816
  return {
6607
6817
  content: `Removed queue "${parsed.name}".`,
6608
6818
  structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
@@ -6641,7 +6851,7 @@ var manageQueueTool = {
6641
6851
  disabled: disabledValue
6642
6852
  });
6643
6853
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
6644
- log30.info({ name: parsed.name, action: resultAction }, "Queue updated");
6854
+ log31.info({ name: parsed.name, action: resultAction }, "Queue updated");
6645
6855
  return {
6646
6856
  content: `Queue "${parsed.name}" ${resultAction}.`,
6647
6857
  structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
@@ -6655,12 +6865,12 @@ var manageQueueTool = {
6655
6865
  var queueTools = [listQueuesTool, manageQueueTool];
6656
6866
 
6657
6867
  // src/domain/tools/vrrp-tools.ts
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")
6868
+ import { z as z31 } from "zod";
6869
+ var log32 = createLogger("vrrp-tools");
6870
+ var listVrrpInstancesInputSchema = z31.object({
6871
+ routerId: z31.string().describe("Target router identifier from the router registry"),
6872
+ interface: z31.string().optional().describe("Filter by master interface name (exact match)"),
6873
+ limit: z31.number().int().min(1).max(500).default(100).describe("Maximum number of instances to return")
6664
6874
  }).strict();
6665
6875
  var listVrrpInstancesTool = {
6666
6876
  name: "list_vrrp_instances",
@@ -6675,7 +6885,7 @@ var listVrrpInstancesTool = {
6675
6885
  },
6676
6886
  async handler(params, context) {
6677
6887
  const parsed = listVrrpInstancesInputSchema.parse(params);
6678
- log31.info({ routerId: context.routerId }, "Listing VRRP instances");
6888
+ log32.info({ routerId: context.routerId }, "Listing VRRP instances");
6679
6889
  try {
6680
6890
  const allInstances = await context.routerClient.get("interface/vrrp", {
6681
6891
  limit: void 0,
@@ -6699,17 +6909,17 @@ var listVrrpInstancesTool = {
6699
6909
  }
6700
6910
  }
6701
6911
  };
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")
6912
+ var manageVrrpInstanceInputSchema = z31.object({
6913
+ routerId: z31.string().describe("Target router identifier from the router registry"),
6914
+ action: z31.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
6915
+ name: z31.string().describe("VRRP interface name \u2014 idempotency key"),
6916
+ interface: z31.string().optional().describe("Master interface (required for add)"),
6917
+ vrid: z31.number().int().min(1).max(255).optional().describe("Virtual router ID (required for add)"),
6918
+ priority: z31.number().int().min(1).max(254).default(100).describe("Router priority (1\u2013254)"),
6919
+ interval: z31.number().int().min(1).optional().describe("Advertisement interval in seconds"),
6920
+ version: z31.enum(["2", "3"]).default("3").describe("VRRP protocol version"),
6921
+ comment: z31.string().optional().describe("Optional comment"),
6922
+ dryRun: z31.boolean().default(false).describe("Preview changes without applying")
6713
6923
  }).strict();
6714
6924
  var manageVrrpInstanceTool = {
6715
6925
  name: "manage_vrrp_instance",
@@ -6725,7 +6935,7 @@ var manageVrrpInstanceTool = {
6725
6935
  snapshotPaths: ["interface/vrrp"],
6726
6936
  async handler(params, context) {
6727
6937
  const parsed = manageVrrpInstanceInputSchema.parse(params);
6728
- log31.info(
6938
+ log32.info(
6729
6939
  { routerId: context.routerId, action: parsed.action, name: parsed.name },
6730
6940
  "Managing VRRP instance"
6731
6941
  );
@@ -6809,7 +7019,7 @@ var manageVrrpInstanceTool = {
6809
7019
  if (parsed.interval) body.interval = String(parsed.interval);
6810
7020
  if (parsed.comment) body.comment = parsed.comment;
6811
7021
  const created = await context.routerClient.create("interface/vrrp", body);
6812
- log31.info({ name: parsed.name, id: created[".id"] }, "VRRP instance added");
7022
+ log32.info({ name: parsed.name, id: created[".id"] }, "VRRP instance added");
6813
7023
  return {
6814
7024
  content: `Added VRRP instance "${parsed.name}".`,
6815
7025
  structuredContent: { action: "created", instance: created }
@@ -6832,7 +7042,7 @@ var manageVrrpInstanceTool = {
6832
7042
  };
6833
7043
  }
6834
7044
  await context.routerClient.remove("interface/vrrp", existing[".id"]);
6835
- log31.info({ name: parsed.name }, "VRRP instance removed");
7045
+ log32.info({ name: parsed.name }, "VRRP instance removed");
6836
7046
  return {
6837
7047
  content: `Removed VRRP instance "${parsed.name}".`,
6838
7048
  structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
@@ -6871,7 +7081,7 @@ var manageVrrpInstanceTool = {
6871
7081
  disabled: disabledValue
6872
7082
  });
6873
7083
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
6874
- log31.info({ name: parsed.name, action: resultAction }, "VRRP instance updated");
7084
+ log32.info({ name: parsed.name, action: resultAction }, "VRRP instance updated");
6875
7085
  return {
6876
7086
  content: `VRRP instance "${parsed.name}" ${resultAction}.`,
6877
7087
  structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
@@ -6885,10 +7095,10 @@ var manageVrrpInstanceTool = {
6885
7095
  var vrrpTools = [listVrrpInstancesTool, manageVrrpInstanceTool];
6886
7096
 
6887
7097
  // src/domain/tools/network-services-tools.ts
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")
7098
+ import { z as z32 } from "zod";
7099
+ var log33 = createLogger("network-services-tools");
7100
+ var getSnmpSettingsInputSchema = z32.object({
7101
+ routerId: z32.string().describe("Target router identifier from the router registry")
6892
7102
  }).strict();
6893
7103
  var getSnmpSettingsTool = {
6894
7104
  name: "get_snmp_settings",
@@ -6903,7 +7113,7 @@ var getSnmpSettingsTool = {
6903
7113
  },
6904
7114
  async handler(params, context) {
6905
7115
  const parsed = getSnmpSettingsInputSchema.parse(params);
6906
- log32.info({ routerId: context.routerId }, "Fetching SNMP settings");
7116
+ log33.info({ routerId: context.routerId }, "Fetching SNMP settings");
6907
7117
  try {
6908
7118
  const results2 = await context.routerClient.get("snmp");
6909
7119
  if (!results2 || results2.length === 0) {
@@ -6932,8 +7142,8 @@ var getSnmpSettingsTool = {
6932
7142
  }
6933
7143
  }
6934
7144
  };
6935
- var getNtpSettingsInputSchema = z31.object({
6936
- routerId: z31.string().describe("Target router identifier from the router registry")
7145
+ var getNtpSettingsInputSchema = z32.object({
7146
+ routerId: z32.string().describe("Target router identifier from the router registry")
6937
7147
  }).strict();
6938
7148
  var getNtpSettingsTool = {
6939
7149
  name: "get_ntp_settings",
@@ -6948,7 +7158,7 @@ var getNtpSettingsTool = {
6948
7158
  },
6949
7159
  async handler(params, context) {
6950
7160
  const parsed = getNtpSettingsInputSchema.parse(params);
6951
- log32.info({ routerId: context.routerId }, "Fetching NTP settings");
7161
+ log33.info({ routerId: context.routerId }, "Fetching NTP settings");
6952
7162
  try {
6953
7163
  const results2 = await context.routerClient.get("system/ntp/client");
6954
7164
  if (!results2 || results2.length === 0) {
@@ -6977,11 +7187,11 @@ var getNtpSettingsTool = {
6977
7187
  }
6978
7188
  }
6979
7189
  };
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")
7190
+ var listNetwatchEntriesInputSchema = z32.object({
7191
+ routerId: z32.string().describe("Target router identifier from the router registry"),
7192
+ host: z32.string().optional().describe("Filter by host (substring match)"),
7193
+ status: z32.enum(["up", "down", "unknown"]).optional().describe("Filter by current status"),
7194
+ limit: z32.number().int().min(1).max(500).default(100).describe("Maximum number of entries to return")
6985
7195
  }).strict();
6986
7196
  var listNetwatchEntriesTool = {
6987
7197
  name: "list_netwatch_entries",
@@ -6996,7 +7206,7 @@ var listNetwatchEntriesTool = {
6996
7206
  },
6997
7207
  async handler(params, context) {
6998
7208
  const parsed = listNetwatchEntriesInputSchema.parse(params);
6999
- log32.info({ routerId: context.routerId }, "Listing Netwatch entries");
7209
+ log33.info({ routerId: context.routerId }, "Listing Netwatch entries");
7000
7210
  try {
7001
7211
  const allEntries = await context.routerClient.get("tool/netwatch", {
7002
7212
  limit: void 0,
@@ -7018,15 +7228,15 @@ var listNetwatchEntriesTool = {
7018
7228
  }
7019
7229
  }
7020
7230
  };
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")
7231
+ var manageNetwatchEntryInputSchema = z32.object({
7232
+ routerId: z32.string().describe("Target router identifier from the router registry"),
7233
+ action: z32.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
7234
+ host: z32.string().describe("Host to monitor \u2014 idempotency key"),
7235
+ port: z32.number().int().min(1).max(65535).optional().describe("TCP port (optional; ICMP if omitted)"),
7236
+ interval: z32.string().optional().describe("Check interval (e.g. '1m'; default '1m')"),
7237
+ timeout: z32.string().optional().describe("Probe timeout (e.g. '500ms')"),
7238
+ comment: z32.string().optional().describe("Optional comment"),
7239
+ dryRun: z32.boolean().default(false).describe("Preview changes without applying")
7030
7240
  }).strict();
7031
7241
  var manageNetwatchEntryTool = {
7032
7242
  name: "manage_netwatch_entry",
@@ -7042,7 +7252,7 @@ var manageNetwatchEntryTool = {
7042
7252
  snapshotPaths: ["tool/netwatch"],
7043
7253
  async handler(params, context) {
7044
7254
  const parsed = manageNetwatchEntryInputSchema.parse(params);
7045
- log32.info(
7255
+ log33.info(
7046
7256
  { routerId: context.routerId, action: parsed.action, host: parsed.host },
7047
7257
  "Managing Netwatch entry"
7048
7258
  );
@@ -7099,7 +7309,7 @@ var manageNetwatchEntryTool = {
7099
7309
  if (parsed.timeout) body.timeout = parsed.timeout;
7100
7310
  if (parsed.comment) body.comment = parsed.comment;
7101
7311
  const created = await context.routerClient.create("tool/netwatch", body);
7102
- log32.info({ host: parsed.host, id: created[".id"] }, "Netwatch entry added");
7312
+ log33.info({ host: parsed.host, id: created[".id"] }, "Netwatch entry added");
7103
7313
  return {
7104
7314
  content: `Added Netwatch entry for "${parsed.host}".`,
7105
7315
  structuredContent: { action: "created", entry: created }
@@ -7123,7 +7333,7 @@ var manageNetwatchEntryTool = {
7123
7333
  };
7124
7334
  }
7125
7335
  await context.routerClient.remove("tool/netwatch", existing2[".id"]);
7126
- log32.info({ host: parsed.host }, "Netwatch entry removed");
7336
+ log33.info({ host: parsed.host }, "Netwatch entry removed");
7127
7337
  return {
7128
7338
  content: `Removed Netwatch entry for "${parsed.host}".`,
7129
7339
  structuredContent: { action: "removed", host: parsed.host, id: existing2[".id"] }
@@ -7163,7 +7373,7 @@ var manageNetwatchEntryTool = {
7163
7373
  disabled: disabledValue
7164
7374
  });
7165
7375
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
7166
- log32.info({ host: parsed.host, action: resultAction }, "Netwatch entry updated");
7376
+ log33.info({ host: parsed.host, action: resultAction }, "Netwatch entry updated");
7167
7377
  return {
7168
7378
  content: `Netwatch entry for "${parsed.host}" ${resultAction}.`,
7169
7379
  structuredContent: { action: resultAction, host: parsed.host, id: existing[".id"] }
@@ -7174,10 +7384,10 @@ var manageNetwatchEntryTool = {
7174
7384
  }
7175
7385
  }
7176
7386
  };
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")
7387
+ var listNeighborsInputSchema = z32.object({
7388
+ routerId: z32.string().describe("Target router identifier from the router registry"),
7389
+ interface: z32.string().optional().describe("Filter by interface name (substring match)"),
7390
+ limit: z32.number().int().min(1).max(500).default(100).describe("Maximum number of neighbors to return")
7181
7391
  }).strict();
7182
7392
  var listNeighborsTool = {
7183
7393
  name: "list_neighbors",
@@ -7192,7 +7402,7 @@ var listNeighborsTool = {
7192
7402
  },
7193
7403
  async handler(params, context) {
7194
7404
  const parsed = listNeighborsInputSchema.parse(params);
7195
- log32.info({ routerId: context.routerId }, "Listing neighbors");
7405
+ log33.info({ routerId: context.routerId }, "Listing neighbors");
7196
7406
  try {
7197
7407
  const allNeighbors = await context.routerClient.get("ip/neighbor", {
7198
7408
  limit: void 0,
@@ -7216,12 +7426,12 @@ var listNeighborsTool = {
7216
7426
  }
7217
7427
  }
7218
7428
  };
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")
7429
+ var listArpEntriesInputSchema = z32.object({
7430
+ routerId: z32.string().describe("Target router identifier from the router registry"),
7431
+ interface: z32.string().optional().describe("Filter by interface (substring match)"),
7432
+ address: z32.string().optional().describe("Filter by IP address (substring match)"),
7433
+ macAddress: z32.string().optional().describe("Filter by MAC address (substring match)"),
7434
+ limit: z32.number().int().min(1).max(500).default(100).describe("Maximum number of ARP entries to return")
7225
7435
  }).strict();
7226
7436
  var listArpEntriesTool = {
7227
7437
  name: "list_arp_entries",
@@ -7236,7 +7446,7 @@ var listArpEntriesTool = {
7236
7446
  },
7237
7447
  async handler(params, context) {
7238
7448
  const parsed = listArpEntriesInputSchema.parse(params);
7239
- log32.info({ routerId: context.routerId }, "Listing ARP entries");
7449
+ log33.info({ routerId: context.routerId }, "Listing ARP entries");
7240
7450
  try {
7241
7451
  const allEntries = await context.routerClient.get("ip/arp", {
7242
7452
  limit: void 0,
@@ -7260,32 +7470,105 @@ var listArpEntriesTool = {
7260
7470
  }
7261
7471
  }
7262
7472
  };
7473
+ var manageNtpClientInputSchema = z32.object({
7474
+ routerId: z32.string().describe("Target router identifier from the router registry"),
7475
+ enabled: z32.boolean().optional().describe("Enable or disable the NTP client"),
7476
+ mode: z32.enum(["unicast", "broadcast", "multicast", "manycast"]).optional().describe("NTP client mode"),
7477
+ servers: z32.string().optional().describe("Comma-separated NTP server addresses"),
7478
+ vlanInterface: z32.string().optional().describe("VLAN interface for NTP communication"),
7479
+ dryRun: z32.boolean().default(false).describe("Preview changes without applying")
7480
+ }).strict();
7481
+ var manageNtpClientTool = {
7482
+ name: "manage_ntp_client",
7483
+ title: "Manage NTP Client",
7484
+ description: "Update NTP client settings on a MikroTik router. Idempotent: returns already_set if no changes are needed.",
7485
+ inputSchema: manageNtpClientInputSchema,
7486
+ annotations: {
7487
+ readOnlyHint: false,
7488
+ destructiveHint: false,
7489
+ idempotentHint: true,
7490
+ openWorldHint: false
7491
+ },
7492
+ snapshotPaths: ["system/ntp/client"],
7493
+ async handler(params, context) {
7494
+ const parsed = manageNtpClientInputSchema.parse(params);
7495
+ log33.info({ routerId: context.routerId }, "Managing NTP client settings");
7496
+ try {
7497
+ const results2 = await context.routerClient.get("system/ntp/client");
7498
+ const current = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
7499
+ const id = current[".id"];
7500
+ const changes = {};
7501
+ const diff = [];
7502
+ if (parsed.enabled !== void 0 && current.enabled !== String(parsed.enabled)) {
7503
+ changes.enabled = String(parsed.enabled);
7504
+ diff.push({ property: "enabled", before: current.enabled, after: String(parsed.enabled) });
7505
+ }
7506
+ if (parsed.mode !== void 0 && current.mode !== parsed.mode) {
7507
+ changes.mode = parsed.mode;
7508
+ diff.push({ property: "mode", before: current.mode, after: parsed.mode });
7509
+ }
7510
+ if (parsed.servers !== void 0 && current.servers !== parsed.servers) {
7511
+ changes.servers = parsed.servers;
7512
+ diff.push({ property: "servers", before: current.servers, after: parsed.servers });
7513
+ }
7514
+ if (parsed.vlanInterface !== void 0 && current["vlan-interface"] !== parsed.vlanInterface) {
7515
+ changes["vlan-interface"] = parsed.vlanInterface;
7516
+ diff.push({
7517
+ property: "vlan-interface",
7518
+ before: current["vlan-interface"],
7519
+ after: parsed.vlanInterface
7520
+ });
7521
+ }
7522
+ if (diff.length === 0) {
7523
+ return {
7524
+ content: `NTP client on ${context.routerId} already matches requested configuration.`,
7525
+ structuredContent: { action: "already_set", routerId: context.routerId }
7526
+ };
7527
+ }
7528
+ if (parsed.dryRun) {
7529
+ return {
7530
+ content: `Dry run: Would update NTP client on ${context.routerId}.`,
7531
+ structuredContent: { action: "dry_run", diff }
7532
+ };
7533
+ }
7534
+ await context.routerClient.update("system/ntp/client", id, changes);
7535
+ log33.info({ routerId: context.routerId }, "NTP client updated");
7536
+ return {
7537
+ content: `NTP client on ${context.routerId} updated.`,
7538
+ structuredContent: { action: "updated", routerId: context.routerId, diff }
7539
+ };
7540
+ } catch (err) {
7541
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ntp_client" });
7542
+ }
7543
+ }
7544
+ };
7263
7545
  var networkServicesTools = [
7264
7546
  getSnmpSettingsTool,
7265
7547
  getNtpSettingsTool,
7266
7548
  listNetwatchEntriesTool,
7267
7549
  manageNetwatchEntryTool,
7268
7550
  listNeighborsTool,
7269
- listArpEntriesTool
7551
+ listArpEntriesTool,
7552
+ manageNtpClientTool
7270
7553
  ];
7271
7554
 
7272
7555
  // src/domain/tools/vlan-tools.ts
7273
- import { z as z32 } from "zod";
7274
- var log33 = createLogger("vlan-tools");
7556
+ import { z as z33 } from "zod";
7557
+ var log34 = createLogger("vlan-tools");
7275
7558
  function sanitizeComment4(comment) {
7276
7559
  if (comment === void 0) return void 0;
7277
7560
  return comment.replace(/[\x00-\x1f\x7f]/g, "");
7278
7561
  }
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")
7562
+ var manageVlanInputSchema = z33.object({
7563
+ routerId: z33.string().describe("Target router identifier from the router registry"),
7564
+ action: z33.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
7565
+ name: z33.string().regex(/^[a-zA-Z0-9_-]+$/).max(15).describe("VLAN interface name \u2014 idempotency key (alphanumeric, hyphens, underscores, max 15 chars)"),
7566
+ vlanId: z33.number().int().min(1).max(4094).optional().describe("VLAN ID (1-4094; required for add)"),
7567
+ parentInterface: z33.string().optional().describe("Parent interface name (e.g., ether1, bridge1; required for add)"),
7568
+ mtu: z33.number().int().min(68).max(9e3).default(1500).describe("MTU size (add only)"),
7569
+ disabled: z33.boolean().default(false).describe("Whether to create the VLAN disabled (add only)"),
7570
+ comment: z33.string().max(255).optional().describe("Optional comment (add only)"),
7571
+ dryRun: z33.boolean().default(false).describe("Preview changes without applying")
7289
7572
  }).strict();
7290
7573
  var manageVlanTool = {
7291
7574
  name: "manage_vlan",
@@ -7302,7 +7585,7 @@ var manageVlanTool = {
7302
7585
  async handler(params, context) {
7303
7586
  const parsed = manageVlanInputSchema.parse(params);
7304
7587
  const comment = sanitizeComment4(parsed.comment);
7305
- log33.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing VLAN");
7588
+ log34.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing VLAN");
7306
7589
  try {
7307
7590
  const allVlans = await context.routerClient.get("interface/vlan", {
7308
7591
  limit: void 0,
@@ -7330,7 +7613,7 @@ var manageVlanTool = {
7330
7613
  }
7331
7614
  if (existing) {
7332
7615
  if (existing["vlan-id"] === String(parsed.vlanId) && existing.interface === parsed.parentInterface) {
7333
- log33.info({ name: parsed.name }, "VLAN already exists with matching config");
7616
+ log34.info({ name: parsed.name }, "VLAN already exists with matching config");
7334
7617
  return {
7335
7618
  content: `VLAN "${parsed.name}" already exists with VLAN ID ${parsed.vlanId} on ${parsed.parentInterface}. No changes made.`,
7336
7619
  structuredContent: { action: "already_exists", vlan: existing }
@@ -7374,7 +7657,7 @@ var manageVlanTool = {
7374
7657
  };
7375
7658
  if (comment) body.comment = comment;
7376
7659
  const created = await context.routerClient.create("interface/vlan", body);
7377
- log33.info({ name: parsed.name, id: created[".id"] }, "VLAN created");
7660
+ log34.info({ name: parsed.name, id: created[".id"] }, "VLAN created");
7378
7661
  return {
7379
7662
  content: `Created VLAN "${parsed.name}" (ID ${parsed.vlanId}) on ${parsed.parentInterface} with MTU ${parsed.mtu}.`,
7380
7663
  structuredContent: { action: "created", vlan: created }
@@ -7394,7 +7677,7 @@ var manageVlanTool = {
7394
7677
  };
7395
7678
  }
7396
7679
  await context.routerClient.remove("interface/vlan", existing[".id"]);
7397
- log33.info({ name: parsed.name }, "VLAN removed");
7680
+ log34.info({ name: parsed.name }, "VLAN removed");
7398
7681
  return {
7399
7682
  content: `Removed VLAN "${parsed.name}".`,
7400
7683
  structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
@@ -7425,7 +7708,7 @@ var manageVlanTool = {
7425
7708
  const disabledValue = parsed.action === "disable" ? "true" : "false";
7426
7709
  await context.routerClient.update("interface/vlan", existing[".id"], { disabled: disabledValue });
7427
7710
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
7428
- log33.info({ name: parsed.name, action: resultAction }, "VLAN updated");
7711
+ log34.info({ name: parsed.name, action: resultAction }, "VLAN updated");
7429
7712
  return {
7430
7713
  content: `VLAN "${parsed.name}" ${resultAction}.`,
7431
7714
  structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
@@ -7439,14 +7722,14 @@ var manageVlanTool = {
7439
7722
  var vlanTools = [manageVlanTool];
7440
7723
 
7441
7724
  // 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")
7725
+ import { z as z34 } from "zod";
7726
+ var log35 = createLogger("dhcp-client-tools");
7727
+ var listDhcpClientsInputSchema = z34.object({
7728
+ routerId: z34.string().describe("Target router identifier from the router registry"),
7729
+ interface: z34.string().optional().describe("Filter by interface name (exact match)"),
7730
+ status: z34.enum(["bound", "searching", "requesting", "init", "all"]).default("all").describe("Filter by DHCP client status"),
7731
+ limit: z34.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
7732
+ offset: z34.number().int().min(0).default(0).describe("Offset for pagination")
7450
7733
  }).strict();
7451
7734
  var listDhcpClientsTool = {
7452
7735
  name: "list_dhcp_clients",
@@ -7461,7 +7744,7 @@ var listDhcpClientsTool = {
7461
7744
  },
7462
7745
  async handler(params, context) {
7463
7746
  const parsed = listDhcpClientsInputSchema.parse(params);
7464
- log34.info({ routerId: context.routerId }, "Listing DHCP clients");
7747
+ log35.info({ routerId: context.routerId }, "Listing DHCP clients");
7465
7748
  try {
7466
7749
  const allClients = await context.routerClient.get("ip/dhcp-client", {
7467
7750
  limit: void 0,
@@ -7504,15 +7787,15 @@ var listDhcpClientsTool = {
7504
7787
  }
7505
7788
  }
7506
7789
  };
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")
7790
+ var manageDhcpClientInputSchema = z34.object({
7791
+ routerId: z34.string().describe("Target router identifier from the router registry"),
7792
+ action: z34.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
7793
+ interface: z34.string().describe("Interface name \u2014 idempotency key (e.g. ether1, ether2)"),
7794
+ usePeerDns: z34.boolean().default(true).describe("Use DNS servers provided by DHCP server (add only)"),
7795
+ usePeerNtp: z34.boolean().default(false).describe("Use NTP servers provided by DHCP server (add only)"),
7796
+ addDefaultRoute: z34.boolean().default(true).describe("Add default route from DHCP (add only)"),
7797
+ comment: z34.string().max(255).optional().describe("Optional comment (add only)"),
7798
+ dryRun: z34.boolean().default(false).describe("Preview changes without applying")
7516
7799
  }).strict();
7517
7800
  var manageDhcpClientTool = {
7518
7801
  name: "manage_dhcp_client",
@@ -7528,7 +7811,7 @@ var manageDhcpClientTool = {
7528
7811
  snapshotPaths: ["ip/dhcp-client"],
7529
7812
  async handler(params, context) {
7530
7813
  const parsed = manageDhcpClientInputSchema.parse(params);
7531
- log34.info({ routerId: context.routerId, action: parsed.action, interface: parsed.interface }, "Managing DHCP client");
7814
+ log35.info({ routerId: context.routerId, action: parsed.action, interface: parsed.interface }, "Managing DHCP client");
7532
7815
  try {
7533
7816
  const allClients = await context.routerClient.get("ip/dhcp-client", {
7534
7817
  limit: void 0,
@@ -7564,7 +7847,7 @@ var manageDhcpClientTool = {
7564
7847
  };
7565
7848
  if (parsed.comment) body.comment = parsed.comment;
7566
7849
  const created = await context.routerClient.create("ip/dhcp-client", body);
7567
- log34.info({ interface: parsed.interface, id: created[".id"] }, "DHCP client added");
7850
+ log35.info({ interface: parsed.interface, id: created[".id"] }, "DHCP client added");
7568
7851
  return {
7569
7852
  content: `Added DHCP client on "${parsed.interface}".`,
7570
7853
  structuredContent: { action: "created", client: created }
@@ -7587,7 +7870,7 @@ var manageDhcpClientTool = {
7587
7870
  };
7588
7871
  }
7589
7872
  await context.routerClient.remove("ip/dhcp-client", existing[".id"]);
7590
- log34.info({ interface: parsed.interface }, "DHCP client removed");
7873
+ log35.info({ interface: parsed.interface }, "DHCP client removed");
7591
7874
  return {
7592
7875
  content: `Removed DHCP client on "${parsed.interface}".`,
7593
7876
  structuredContent: { action: "removed", interface: parsed.interface, id: existing[".id"] }
@@ -7624,7 +7907,7 @@ var manageDhcpClientTool = {
7624
7907
  const disabledValue = parsed.action === "disable" ? "true" : "false";
7625
7908
  await context.routerClient.update("ip/dhcp-client", existing[".id"], { disabled: disabledValue });
7626
7909
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
7627
- log34.info({ interface: parsed.interface, action: resultAction }, "DHCP client updated");
7910
+ log35.info({ interface: parsed.interface, action: resultAction }, "DHCP client updated");
7628
7911
  return {
7629
7912
  content: `DHCP client on "${parsed.interface}" ${resultAction}.`,
7630
7913
  structuredContent: { action: resultAction, interface: parsed.interface, id: existing[".id"] }
@@ -7638,13 +7921,13 @@ var manageDhcpClientTool = {
7638
7921
  var dhcpClientTools = [listDhcpClientsTool, manageDhcpClientTool];
7639
7922
 
7640
7923
  // src/domain/tools/ip-service-tools.ts
7641
- import { z as z34 } from "zod";
7642
- var log35 = createLogger("ip-service-tools");
7924
+ import { z as z35 } from "zod";
7925
+ var log36 = createLogger("ip-service-tools");
7643
7926
  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")
7927
+ var listIpServicesInputSchema = z35.object({
7928
+ routerId: z35.string().describe("Target router identifier from the router registry"),
7929
+ name: z35.enum(SERVICE_NAMES).optional().describe("Filter by service name (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp)"),
7930
+ enabled: z35.boolean().optional().describe("When true, return only enabled services; when false, only disabled services")
7648
7931
  }).strict();
7649
7932
  var listIpServicesTool = {
7650
7933
  name: "list_ip_services",
@@ -7659,7 +7942,7 @@ var listIpServicesTool = {
7659
7942
  },
7660
7943
  async handler(params, context) {
7661
7944
  const parsed = listIpServicesInputSchema.parse(params);
7662
- log35.info({ routerId: context.routerId }, "Listing IP services");
7945
+ log36.info({ routerId: context.routerId }, "Listing IP services");
7663
7946
  try {
7664
7947
  const allServices = await context.routerClient.get("ip/service", {
7665
7948
  limit: void 0,
@@ -7694,13 +7977,13 @@ var listIpServicesTool = {
7694
7977
  }
7695
7978
  }
7696
7979
  };
7697
- var manageIpServiceInputSchema = z34.object({
7698
- routerId: z34.string().describe("Target router identifier from the router registry"),
7699
- action: z34.enum(["enable", "disable"]).describe(
7980
+ var manageIpServiceInputSchema = z35.object({
7981
+ routerId: z35.string().describe("Target router identifier from the router registry"),
7982
+ action: z35.enum(["enable", "disable"]).describe(
7700
7983
  "Action to perform \u2014 only enable/disable to prevent accidental lockout from changing ports"
7701
7984
  ),
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")
7985
+ name: z35.enum(SERVICE_NAMES).describe("Service name to manage (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp)"),
7986
+ dryRun: z35.boolean().default(false).describe("Preview changes without applying")
7704
7987
  }).strict();
7705
7988
  var manageIpServiceTool = {
7706
7989
  name: "manage_ip_service",
@@ -7715,7 +7998,7 @@ var manageIpServiceTool = {
7715
7998
  },
7716
7999
  async handler(params, context) {
7717
8000
  const parsed = manageIpServiceInputSchema.parse(params);
7718
- log35.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP service");
8001
+ log36.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP service");
7719
8002
  try {
7720
8003
  const allServices = await context.routerClient.get("ip/service", {
7721
8004
  limit: void 0,
@@ -7762,7 +8045,7 @@ var manageIpServiceTool = {
7762
8045
  const disabledValue = parsed.action === "disable" ? "true" : "false";
7763
8046
  await context.routerClient.update("ip/service", existing[".id"], { disabled: disabledValue });
7764
8047
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
7765
- log35.info({ name: parsed.name, action: resultAction }, "IP service updated");
8048
+ log36.info({ name: parsed.name, action: resultAction }, "IP service updated");
7766
8049
  return {
7767
8050
  content: `IP service "${parsed.name}" ${resultAction}.`,
7768
8051
  structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
@@ -7775,13 +8058,1298 @@ var manageIpServiceTool = {
7775
8058
  };
7776
8059
  var ipServiceTools = [listIpServicesTool, manageIpServiceTool];
7777
8060
 
7778
- // src/domain/tools/fleet-tools.ts
7779
- import { z as z35 } from "zod";
7780
-
7781
- // src/middleware/authz.ts
7782
- function matchesPattern(name, pattern) {
7783
- if (!pattern.includes("*")) return name === pattern;
7784
- const prefix = pattern.slice(0, pattern.indexOf("*"));
8061
+ // src/domain/tools/pppoe-tools.ts
8062
+ import { z as z36 } from "zod";
8063
+ var log37 = createLogger("pppoe-tools");
8064
+ var PPPOE_PATH = "interface/pppoe-client";
8065
+ var listPppoeClientsInputSchema = z36.object({
8066
+ routerId: z36.string().describe("Target router identifier from the router registry"),
8067
+ interface: z36.string().optional().describe("Filter by parent interface name (exact match)"),
8068
+ status: z36.enum(["connected", "disconnected", "all"]).default("all").describe("Filter by running status"),
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
+ function isRunning(record) {
8073
+ return record.running === true || record.running === "true";
8074
+ }
8075
+ var listPppoeClientsTool = {
8076
+ name: "list_pppoe_clients",
8077
+ title: "List PPPoE Clients",
8078
+ description: "List PPPoE client interfaces on a MikroTik router. Shows name, parent interface, ISP username, and connection status.",
8079
+ inputSchema: listPppoeClientsInputSchema,
8080
+ annotations: {
8081
+ readOnlyHint: true,
8082
+ destructiveHint: false,
8083
+ idempotentHint: true,
8084
+ openWorldHint: false
8085
+ },
8086
+ async handler(params, context) {
8087
+ const parsed = listPppoeClientsInputSchema.parse(params);
8088
+ log37.info({ routerId: context.routerId }, "Listing PPPoE clients");
8089
+ try {
8090
+ const allClients = await context.routerClient.get(PPPOE_PATH, {
8091
+ limit: void 0,
8092
+ offset: void 0
8093
+ });
8094
+ let filtered = allClients;
8095
+ if (parsed.interface) {
8096
+ filtered = filtered.filter((c) => c.interface === parsed.interface);
8097
+ }
8098
+ if (parsed.status === "connected") {
8099
+ filtered = filtered.filter((c) => isRunning(c));
8100
+ } else if (parsed.status === "disconnected") {
8101
+ filtered = filtered.filter((c) => !isRunning(c));
8102
+ }
8103
+ const total = filtered.length;
8104
+ const clients = filtered.slice(parsed.offset, parsed.offset + parsed.limit);
8105
+ const hasMore = parsed.offset + parsed.limit < total;
8106
+ const lines = [
8107
+ `PPPoE clients on ${context.routerId}: ${total} total, showing ${clients.length} (offset ${parsed.offset})`
8108
+ ];
8109
+ for (const c of clients) {
8110
+ const name = c.name ?? "unknown";
8111
+ const iface = c.interface ?? "unknown";
8112
+ const user = c.user ?? "-";
8113
+ const status = isRunning(c) ? "connected" : "disconnected";
8114
+ lines.push(` ${name} [${iface}] user=${user} ${status}`);
8115
+ }
8116
+ return {
8117
+ content: lines.join("\n"),
8118
+ structuredContent: {
8119
+ routerId: context.routerId,
8120
+ clients,
8121
+ total,
8122
+ hasMore,
8123
+ offset: parsed.offset,
8124
+ limit: parsed.limit
8125
+ }
8126
+ };
8127
+ } catch (err) {
8128
+ throw enrichError(err, { routerId: context.routerId, tool: "list_pppoe_clients" });
8129
+ }
8130
+ }
8131
+ };
8132
+ var managePppoeClientInputSchema = z36.object({
8133
+ routerId: z36.string().describe("Target router identifier from the router registry"),
8134
+ action: z36.enum(["add", "update", "remove"]).describe("Action to perform"),
8135
+ name: z36.string().describe("PPPoE client interface name \u2014 idempotency key (e.g. pppoe-wan)"),
8136
+ interface: z36.string().optional().describe("Parent interface (required for add)"),
8137
+ user: z36.string().optional().describe("PPPoE username (required for add)"),
8138
+ password: z36.string().optional().describe("PPPoE password (never logged)"),
8139
+ serviceName: z36.string().optional().describe("PPPoE service name filter (leave empty to match any)"),
8140
+ addDefaultRoute: z36.boolean().optional().describe("Add default route via PPPoE (yes/no)"),
8141
+ dialOnDemand: z36.boolean().optional().describe("Dial on demand instead of always-on (yes/no)"),
8142
+ dryRun: z36.boolean().default(false).describe("Preview changes without applying")
8143
+ }).strict();
8144
+ var managePppoeClientTool = {
8145
+ name: "manage_pppoe_client",
8146
+ title: "Manage PPPoE Client",
8147
+ 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.",
8148
+ inputSchema: managePppoeClientInputSchema,
8149
+ annotations: {
8150
+ readOnlyHint: false,
8151
+ destructiveHint: true,
8152
+ idempotentHint: true,
8153
+ openWorldHint: false
8154
+ },
8155
+ snapshotPaths: [PPPOE_PATH],
8156
+ async handler(params, context) {
8157
+ const parsed = managePppoeClientInputSchema.parse(params);
8158
+ log37.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing PPPoE client");
8159
+ try {
8160
+ const allClients = await context.routerClient.get(PPPOE_PATH, {
8161
+ limit: void 0,
8162
+ offset: void 0
8163
+ });
8164
+ const existing = allClients.find((c) => c.name === parsed.name);
8165
+ if (parsed.action === "add") {
8166
+ if (!parsed.interface) {
8167
+ throw new MikroMCPError({
8168
+ category: "VALIDATION" /* VALIDATION */,
8169
+ code: "PPPOE_CLIENT_INTERFACE_REQUIRED",
8170
+ message: "interface is required for action add.",
8171
+ details: { name: parsed.name },
8172
+ recoverability: {
8173
+ retryable: false,
8174
+ suggestedAction: "Provide the parent interface (e.g. ether1).",
8175
+ alternativeTools: []
8176
+ }
8177
+ });
8178
+ }
8179
+ if (!parsed.user) {
8180
+ throw new MikroMCPError({
8181
+ category: "VALIDATION" /* VALIDATION */,
8182
+ code: "PPPOE_CLIENT_USER_REQUIRED",
8183
+ message: "user is required for action add.",
8184
+ details: { name: parsed.name },
8185
+ recoverability: {
8186
+ retryable: false,
8187
+ suggestedAction: "Provide the PPPoE username.",
8188
+ alternativeTools: []
8189
+ }
8190
+ });
8191
+ }
8192
+ if (existing) {
8193
+ const sameConfig = existing.interface === parsed.interface && existing.user === parsed.user;
8194
+ if (sameConfig) {
8195
+ return {
8196
+ content: `PPPoE client "${parsed.name}" already exists with the same configuration. No changes made.`,
8197
+ structuredContent: { action: "already_exists", client: existing }
8198
+ };
8199
+ }
8200
+ throw new MikroMCPError({
8201
+ category: "CONFLICT" /* CONFLICT */,
8202
+ code: "PPPOE_CLIENT_CONFLICT",
8203
+ message: `PPPoE client "${parsed.name}" already exists with a different configuration.`,
8204
+ details: {
8205
+ existing: { interface: existing.interface, user: existing.user },
8206
+ requested: { interface: parsed.interface, user: parsed.user }
8207
+ },
8208
+ recoverability: {
8209
+ retryable: false,
8210
+ suggestedAction: "Remove the existing client first or use action update.",
8211
+ alternativeTools: ["manage_pppoe_client"]
8212
+ }
8213
+ });
8214
+ }
8215
+ const diff = [
8216
+ { property: "name", before: null, after: parsed.name },
8217
+ { property: "interface", before: null, after: parsed.interface },
8218
+ { property: "user", before: null, after: parsed.user }
8219
+ ];
8220
+ if (parsed.serviceName !== void 0) {
8221
+ diff.push({ property: "service-name", before: null, after: parsed.serviceName });
8222
+ }
8223
+ if (parsed.addDefaultRoute !== void 0) {
8224
+ diff.push({ property: "add-default-route", before: null, after: parsed.addDefaultRoute ? "yes" : "no" });
8225
+ }
8226
+ if (parsed.dialOnDemand !== void 0) {
8227
+ diff.push({ property: "dial-on-demand", before: null, after: parsed.dialOnDemand ? "yes" : "no" });
8228
+ }
8229
+ if (parsed.dryRun) {
8230
+ return {
8231
+ content: `Dry run: Would create PPPoE client "${parsed.name}" on "${parsed.interface}".`,
8232
+ structuredContent: { action: "dry_run", diff }
8233
+ };
8234
+ }
8235
+ const body = {
8236
+ name: parsed.name,
8237
+ interface: parsed.interface,
8238
+ user: parsed.user
8239
+ };
8240
+ if (parsed.password !== void 0) body.password = parsed.password;
8241
+ if (parsed.serviceName !== void 0) body["service-name"] = parsed.serviceName;
8242
+ if (parsed.addDefaultRoute !== void 0) body["add-default-route"] = parsed.addDefaultRoute ? "yes" : "no";
8243
+ if (parsed.dialOnDemand !== void 0) body["dial-on-demand"] = parsed.dialOnDemand ? "yes" : "no";
8244
+ const created = await context.routerClient.create(PPPOE_PATH, body);
8245
+ log37.info({ name: parsed.name, id: created[".id"] }, "PPPoE client created");
8246
+ return {
8247
+ content: `Created PPPoE client "${parsed.name}" on "${parsed.interface}".`,
8248
+ structuredContent: { action: "created", client: created }
8249
+ };
8250
+ }
8251
+ if (parsed.action === "update") {
8252
+ if (!existing) {
8253
+ throw new MikroMCPError({
8254
+ category: "NOT_FOUND" /* NOT_FOUND */,
8255
+ code: "PPPOE_CLIENT_NOT_FOUND",
8256
+ message: `PPPoE client "${parsed.name}" not found.`,
8257
+ details: { name: parsed.name },
8258
+ recoverability: {
8259
+ retryable: false,
8260
+ suggestedAction: "Verify with list_pppoe_clients.",
8261
+ alternativeTools: ["list_pppoe_clients"]
8262
+ }
8263
+ });
8264
+ }
8265
+ const updates = {};
8266
+ const diff = [];
8267
+ if (parsed.interface !== void 0 && existing.interface !== parsed.interface) {
8268
+ updates.interface = parsed.interface;
8269
+ diff.push({ property: "interface", before: existing.interface, after: parsed.interface });
8270
+ }
8271
+ if (parsed.user !== void 0 && existing.user !== parsed.user) {
8272
+ updates.user = parsed.user;
8273
+ diff.push({ property: "user", before: existing.user, after: parsed.user });
8274
+ }
8275
+ if (parsed.serviceName !== void 0 && existing["service-name"] !== parsed.serviceName) {
8276
+ updates["service-name"] = parsed.serviceName;
8277
+ diff.push({ property: "service-name", before: existing["service-name"] ?? null, after: parsed.serviceName });
8278
+ }
8279
+ if (parsed.addDefaultRoute !== void 0) {
8280
+ const desired = parsed.addDefaultRoute ? "yes" : "no";
8281
+ if (existing["add-default-route"] !== desired) {
8282
+ updates["add-default-route"] = desired;
8283
+ diff.push({ property: "add-default-route", before: existing["add-default-route"] ?? null, after: desired });
8284
+ }
8285
+ }
8286
+ if (parsed.dialOnDemand !== void 0) {
8287
+ const desired = parsed.dialOnDemand ? "yes" : "no";
8288
+ if (existing["dial-on-demand"] !== desired) {
8289
+ updates["dial-on-demand"] = desired;
8290
+ diff.push({ property: "dial-on-demand", before: existing["dial-on-demand"] ?? null, after: desired });
8291
+ }
8292
+ }
8293
+ if (parsed.password !== void 0) {
8294
+ updates.password = parsed.password;
8295
+ }
8296
+ const hasUpdates = Object.keys(updates).length > 0;
8297
+ if (!hasUpdates) {
8298
+ return {
8299
+ content: `PPPoE client "${parsed.name}" already matches the requested configuration. No changes made.`,
8300
+ structuredContent: { action: "no_change", client: existing }
8301
+ };
8302
+ }
8303
+ if (parsed.dryRun) {
8304
+ return {
8305
+ content: `Dry run: Would update PPPoE client "${parsed.name}".`,
8306
+ structuredContent: { action: "dry_run", diff }
8307
+ };
8308
+ }
8309
+ await context.routerClient.update(PPPOE_PATH, existing[".id"], updates);
8310
+ log37.info({ name: parsed.name }, "PPPoE client updated");
8311
+ return {
8312
+ content: `Updated PPPoE client "${parsed.name}".`,
8313
+ structuredContent: { action: "updated", name: parsed.name, diff }
8314
+ };
8315
+ }
8316
+ if (!existing) {
8317
+ return {
8318
+ content: `PPPoE client "${parsed.name}" not found. Nothing to remove.`,
8319
+ structuredContent: { action: "not_found", name: parsed.name }
8320
+ };
8321
+ }
8322
+ if (parsed.dryRun) {
8323
+ return {
8324
+ content: `Dry run: Would remove PPPoE client "${parsed.name}".`,
8325
+ structuredContent: {
8326
+ action: "dry_run",
8327
+ diff: [{ property: "name", before: parsed.name, after: null }]
8328
+ }
8329
+ };
8330
+ }
8331
+ await context.routerClient.remove(PPPOE_PATH, existing[".id"]);
8332
+ log37.info({ name: parsed.name }, "PPPoE client removed");
8333
+ return {
8334
+ content: `Removed PPPoE client "${parsed.name}".`,
8335
+ structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
8336
+ };
8337
+ } catch (err) {
8338
+ if (err instanceof MikroMCPError) throw err;
8339
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_pppoe_client" });
8340
+ }
8341
+ }
8342
+ };
8343
+ var pppoeTools = [listPppoeClientsTool, managePppoeClientTool];
8344
+
8345
+ // src/domain/tools/ovpn-tools.ts
8346
+ import { z as z37 } from "zod";
8347
+ var log38 = createLogger("ovpn-tools");
8348
+ var OVPN_CLIENT_PATH = "interface/ovpn-client";
8349
+ var OVPN_SERVER_PATH = "interface/ovpn-server/server";
8350
+ var listOvpnClientsInputSchema = z37.object({
8351
+ routerId: z37.string().describe("Target router identifier from the router registry"),
8352
+ limit: z37.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
8353
+ offset: z37.number().int().min(0).default(0).describe("Offset for pagination")
8354
+ }).strict();
8355
+ var listOvpnClientsTool = {
8356
+ name: "list_ovpn_clients",
8357
+ title: "List OpenVPN Clients",
8358
+ description: "List OpenVPN client interfaces on a MikroTik router. Shows name, remote server, and connection status.",
8359
+ inputSchema: listOvpnClientsInputSchema,
8360
+ annotations: {
8361
+ readOnlyHint: true,
8362
+ destructiveHint: false,
8363
+ idempotentHint: true,
8364
+ openWorldHint: false
8365
+ },
8366
+ async handler(params, context) {
8367
+ const parsed = listOvpnClientsInputSchema.parse(params);
8368
+ log38.info({ routerId: context.routerId }, "Listing OpenVPN clients");
8369
+ try {
8370
+ const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
8371
+ limit: void 0,
8372
+ offset: void 0
8373
+ });
8374
+ const total = allClients.length;
8375
+ const clients = allClients.slice(parsed.offset, parsed.offset + parsed.limit);
8376
+ const hasMore = parsed.offset + parsed.limit < total;
8377
+ return {
8378
+ content: `OpenVPN clients on ${context.routerId}: ${total} total, showing ${clients.length} (offset ${parsed.offset})`,
8379
+ structuredContent: {
8380
+ routerId: context.routerId,
8381
+ clients,
8382
+ total,
8383
+ hasMore,
8384
+ offset: parsed.offset,
8385
+ limit: parsed.limit
8386
+ }
8387
+ };
8388
+ } catch (err) {
8389
+ throw enrichError(err, { routerId: context.routerId, tool: "list_ovpn_clients" });
8390
+ }
8391
+ }
8392
+ };
8393
+ var manageOvpnClientInputSchema = z37.object({
8394
+ routerId: z37.string().describe("Target router identifier from the router registry"),
8395
+ action: z37.enum(["add", "update", "remove"]).describe("Action to perform"),
8396
+ name: z37.string().describe("OpenVPN client interface name \u2014 idempotency key"),
8397
+ connectTo: z37.string().optional().describe("Remote server address (required for add)"),
8398
+ port: z37.number().int().min(1).max(65535).optional().describe("Remote port number"),
8399
+ mode: z37.enum(["ip", "ethernet"]).optional().describe("Tunnel mode"),
8400
+ protocol: z37.enum(["tcp-client", "udp"]).optional().describe("Transport protocol"),
8401
+ certificate: z37.string().optional().describe("Client certificate name"),
8402
+ user: z37.string().optional().describe("VPN username"),
8403
+ password: z37.string().optional().describe("VPN password (never logged)"),
8404
+ dryRun: z37.boolean().default(false).describe("Preview changes without applying")
8405
+ }).strict();
8406
+ var manageOvpnClientTool = {
8407
+ name: "manage_ovpn_client",
8408
+ title: "Manage OpenVPN Client",
8409
+ 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.",
8410
+ inputSchema: manageOvpnClientInputSchema,
8411
+ annotations: {
8412
+ readOnlyHint: false,
8413
+ destructiveHint: true,
8414
+ idempotentHint: true,
8415
+ openWorldHint: false
8416
+ },
8417
+ snapshotPaths: [OVPN_CLIENT_PATH],
8418
+ async handler(params, context) {
8419
+ const parsed = manageOvpnClientInputSchema.parse(params);
8420
+ log38.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing OpenVPN client");
8421
+ try {
8422
+ const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
8423
+ limit: void 0,
8424
+ offset: void 0
8425
+ });
8426
+ const existing = allClients.find((c) => c.name === parsed.name);
8427
+ if (parsed.action === "add") {
8428
+ if (!parsed.connectTo) {
8429
+ throw new MikroMCPError({
8430
+ category: "VALIDATION" /* VALIDATION */,
8431
+ code: "OVPN_CLIENT_CONNECT_TO_REQUIRED",
8432
+ message: "connectTo is required for action add.",
8433
+ details: { name: parsed.name },
8434
+ recoverability: {
8435
+ retryable: false,
8436
+ suggestedAction: "Provide the remote server address.",
8437
+ alternativeTools: []
8438
+ }
8439
+ });
8440
+ }
8441
+ if (existing) {
8442
+ const sameConfig = existing["connect-to"] === parsed.connectTo;
8443
+ if (sameConfig) {
8444
+ return {
8445
+ content: `OpenVPN client "${parsed.name}" already exists with the same configuration. No changes made.`,
8446
+ structuredContent: { action: "already_exists", client: existing }
8447
+ };
8448
+ }
8449
+ throw new MikroMCPError({
8450
+ category: "CONFLICT" /* CONFLICT */,
8451
+ code: "OVPN_CLIENT_CONFLICT",
8452
+ message: `OpenVPN client "${parsed.name}" already exists with a different configuration.`,
8453
+ details: {
8454
+ existing: { "connect-to": existing["connect-to"] },
8455
+ requested: { "connect-to": parsed.connectTo }
8456
+ },
8457
+ recoverability: {
8458
+ retryable: false,
8459
+ suggestedAction: "Remove the existing client first or use action update.",
8460
+ alternativeTools: ["manage_ovpn_client"]
8461
+ }
8462
+ });
8463
+ }
8464
+ const diff = [
8465
+ { property: "name", before: null, after: parsed.name },
8466
+ { property: "connect-to", before: null, after: parsed.connectTo }
8467
+ ];
8468
+ if (parsed.dryRun) {
8469
+ return {
8470
+ content: `Dry run: Would create OpenVPN client "${parsed.name}" connecting to "${parsed.connectTo}".`,
8471
+ structuredContent: { action: "dry_run", diff }
8472
+ };
8473
+ }
8474
+ const body = {
8475
+ name: parsed.name,
8476
+ "connect-to": parsed.connectTo
8477
+ };
8478
+ if (parsed.port !== void 0) body.port = String(parsed.port);
8479
+ if (parsed.mode !== void 0) body.mode = parsed.mode;
8480
+ if (parsed.protocol !== void 0) body.protocol = parsed.protocol;
8481
+ if (parsed.certificate !== void 0) body.certificate = parsed.certificate;
8482
+ if (parsed.user !== void 0) body.user = parsed.user;
8483
+ if (parsed.password !== void 0) body.password = parsed.password;
8484
+ const created = await context.routerClient.create(OVPN_CLIENT_PATH, body);
8485
+ log38.info({ name: parsed.name, id: created[".id"] }, "OpenVPN client created");
8486
+ return {
8487
+ content: `Created OpenVPN client "${parsed.name}" connecting to "${parsed.connectTo}".`,
8488
+ structuredContent: { action: "created", client: created }
8489
+ };
8490
+ }
8491
+ if (parsed.action === "update") {
8492
+ if (!existing) {
8493
+ throw new MikroMCPError({
8494
+ category: "NOT_FOUND" /* NOT_FOUND */,
8495
+ code: "OVPN_CLIENT_NOT_FOUND",
8496
+ message: `OpenVPN client "${parsed.name}" not found.`,
8497
+ details: { name: parsed.name },
8498
+ recoverability: {
8499
+ retryable: false,
8500
+ suggestedAction: "Verify with list_ovpn_clients.",
8501
+ alternativeTools: ["list_ovpn_clients"]
8502
+ }
8503
+ });
8504
+ }
8505
+ const updates = {};
8506
+ const diff = [];
8507
+ if (parsed.connectTo !== void 0 && existing["connect-to"] !== parsed.connectTo) {
8508
+ updates["connect-to"] = parsed.connectTo;
8509
+ diff.push({ property: "connect-to", before: existing["connect-to"] ?? null, after: parsed.connectTo });
8510
+ }
8511
+ if (parsed.port !== void 0 && existing.port !== String(parsed.port)) {
8512
+ updates.port = String(parsed.port);
8513
+ diff.push({ property: "port", before: existing.port ?? null, after: String(parsed.port) });
8514
+ }
8515
+ if (parsed.mode !== void 0 && existing.mode !== parsed.mode) {
8516
+ updates.mode = parsed.mode;
8517
+ diff.push({ property: "mode", before: existing.mode ?? null, after: parsed.mode });
8518
+ }
8519
+ if (parsed.protocol !== void 0 && existing.protocol !== parsed.protocol) {
8520
+ updates.protocol = parsed.protocol;
8521
+ diff.push({ property: "protocol", before: existing.protocol ?? null, after: parsed.protocol });
8522
+ }
8523
+ if (parsed.certificate !== void 0 && existing.certificate !== parsed.certificate) {
8524
+ updates.certificate = parsed.certificate;
8525
+ diff.push({ property: "certificate", before: existing.certificate ?? null, after: parsed.certificate });
8526
+ }
8527
+ if (parsed.user !== void 0 && existing.user !== parsed.user) {
8528
+ updates.user = parsed.user;
8529
+ diff.push({ property: "user", before: existing.user ?? null, after: parsed.user });
8530
+ }
8531
+ if (parsed.password !== void 0) {
8532
+ updates.password = parsed.password;
8533
+ }
8534
+ const hasUpdates = Object.keys(updates).length > 0;
8535
+ if (!hasUpdates) {
8536
+ return {
8537
+ content: `OpenVPN client "${parsed.name}" already matches the requested configuration. No changes made.`,
8538
+ structuredContent: { action: "no_change", client: existing }
8539
+ };
8540
+ }
8541
+ if (parsed.dryRun) {
8542
+ return {
8543
+ content: `Dry run: Would update OpenVPN client "${parsed.name}".`,
8544
+ structuredContent: { action: "dry_run", diff }
8545
+ };
8546
+ }
8547
+ await context.routerClient.update(OVPN_CLIENT_PATH, existing[".id"], updates);
8548
+ log38.info({ name: parsed.name }, "OpenVPN client updated");
8549
+ return {
8550
+ content: `Updated OpenVPN client "${parsed.name}".`,
8551
+ structuredContent: { action: "updated", name: parsed.name, diff }
8552
+ };
8553
+ }
8554
+ if (!existing) {
8555
+ return {
8556
+ content: `OpenVPN client "${parsed.name}" not found. Nothing to remove.`,
8557
+ structuredContent: { action: "not_found", name: parsed.name }
8558
+ };
8559
+ }
8560
+ if (parsed.dryRun) {
8561
+ return {
8562
+ content: `Dry run: Would remove OpenVPN client "${parsed.name}".`,
8563
+ structuredContent: {
8564
+ action: "dry_run",
8565
+ diff: [{ property: "name", before: parsed.name, after: null }]
8566
+ }
8567
+ };
8568
+ }
8569
+ await context.routerClient.remove(OVPN_CLIENT_PATH, existing[".id"]);
8570
+ log38.info({ name: parsed.name }, "OpenVPN client removed");
8571
+ return {
8572
+ content: `Removed OpenVPN client "${parsed.name}".`,
8573
+ structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
8574
+ };
8575
+ } catch (err) {
8576
+ if (err instanceof MikroMCPError) throw err;
8577
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_client" });
8578
+ }
8579
+ }
8580
+ };
8581
+ var getOvpnServerInputSchema = z37.object({
8582
+ routerId: z37.string().describe("Target router identifier from the router registry")
8583
+ }).strict();
8584
+ function fetchOvpnServer(allRecords, routerId) {
8585
+ if (allRecords.length === 0) {
8586
+ throw new MikroMCPError({
8587
+ category: "NOT_FOUND" /* NOT_FOUND */,
8588
+ code: "OVPN_SERVER_NOT_FOUND",
8589
+ message: "OpenVPN server not found. The OpenVPN package may not be installed on this router.",
8590
+ details: { routerId },
8591
+ recoverability: {
8592
+ retryable: false,
8593
+ suggestedAction: "Install the OpenVPN package via manage_package.",
8594
+ alternativeTools: ["list_packages", "manage_package"]
8595
+ }
8596
+ });
8597
+ }
8598
+ return allRecords[0];
8599
+ }
8600
+ var getOvpnServerTool = {
8601
+ name: "get_ovpn_server",
8602
+ title: "Get OpenVPN Server",
8603
+ description: "Get the OpenVPN server configuration on a MikroTik router. Throws NOT_FOUND if the OpenVPN package is not installed.",
8604
+ inputSchema: getOvpnServerInputSchema,
8605
+ annotations: {
8606
+ readOnlyHint: true,
8607
+ destructiveHint: false,
8608
+ idempotentHint: true,
8609
+ openWorldHint: false
8610
+ },
8611
+ async handler(params, context) {
8612
+ const parsed = getOvpnServerInputSchema.parse(params);
8613
+ log38.info({ routerId: context.routerId }, "Getting OpenVPN server configuration");
8614
+ try {
8615
+ const records = await context.routerClient.get(OVPN_SERVER_PATH, {
8616
+ limit: void 0,
8617
+ offset: void 0
8618
+ });
8619
+ const server = fetchOvpnServer(records, parsed.routerId);
8620
+ return {
8621
+ content: `OpenVPN server on ${context.routerId}: enabled=${server.enabled}, port=${server.port}, protocol=${server.protocol}`,
8622
+ structuredContent: { routerId: context.routerId, server }
8623
+ };
8624
+ } catch (err) {
8625
+ if (err instanceof MikroMCPError) throw err;
8626
+ throw enrichError(err, { routerId: context.routerId, tool: "get_ovpn_server" });
8627
+ }
8628
+ }
8629
+ };
8630
+ var manageOvpnServerInputSchema = z37.object({
8631
+ routerId: z37.string().describe("Target router identifier from the router registry"),
8632
+ action: z37.enum(["enable", "disable", "set"]).describe("Action to perform"),
8633
+ port: z37.number().int().min(1).max(65535).optional().describe("Listening port (set action only)"),
8634
+ mode: z37.enum(["ip", "ethernet"]).optional().describe("Tunnel mode (set action only)"),
8635
+ protocol: z37.enum(["tcp-server", "udp"]).optional().describe("Transport protocol (set action only)"),
8636
+ certificate: z37.string().optional().describe("Server certificate name (set action only)"),
8637
+ cipher: z37.enum(["blowfish128", "aes128-cbc", "aes192-cbc", "aes256-cbc", "aes128-gcm", "aes256-gcm", "none"]).optional().describe("Encryption cipher (set action only)"),
8638
+ auth: z37.enum(["md5", "sha1", "sha256", "sha512", "null"]).optional().describe("Authentication algorithm (set action only)"),
8639
+ dryRun: z37.boolean().default(false).describe("Preview changes without applying")
8640
+ }).strict();
8641
+ var manageOvpnServerTool = {
8642
+ name: "manage_ovpn_server",
8643
+ title: "Manage OpenVPN Server",
8644
+ 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.",
8645
+ inputSchema: manageOvpnServerInputSchema,
8646
+ annotations: {
8647
+ readOnlyHint: false,
8648
+ destructiveHint: true,
8649
+ idempotentHint: true,
8650
+ openWorldHint: false
8651
+ },
8652
+ snapshotPaths: [OVPN_SERVER_PATH],
8653
+ async handler(params, context) {
8654
+ const parsed = manageOvpnServerInputSchema.parse(params);
8655
+ log38.info({ routerId: context.routerId, action: parsed.action }, "Managing OpenVPN server");
8656
+ try {
8657
+ const records = await context.routerClient.get(OVPN_SERVER_PATH, {
8658
+ limit: void 0,
8659
+ offset: void 0
8660
+ });
8661
+ const server = fetchOvpnServer(records, parsed.routerId);
8662
+ const serverId = server[".id"];
8663
+ if (parsed.action === "enable" || parsed.action === "disable") {
8664
+ const desiredEnabled = parsed.action === "enable";
8665
+ const currentEnabled = server.enabled === "yes";
8666
+ if (desiredEnabled === currentEnabled) {
8667
+ return {
8668
+ content: `OpenVPN server on ${context.routerId} is already ${parsed.action}d. No changes made.`,
8669
+ structuredContent: { action: "no_change", server }
8670
+ };
8671
+ }
8672
+ if (parsed.dryRun) {
8673
+ return {
8674
+ content: `Dry run: Would ${parsed.action} OpenVPN server on ${context.routerId}.`,
8675
+ structuredContent: {
8676
+ action: "dry_run",
8677
+ diff: [{ property: "enabled", before: server.enabled, after: desiredEnabled ? "yes" : "no" }]
8678
+ }
8679
+ };
8680
+ }
8681
+ await context.routerClient.update(OVPN_SERVER_PATH, serverId, {
8682
+ enabled: desiredEnabled ? "yes" : "no"
8683
+ });
8684
+ log38.info({ routerId: context.routerId, action: parsed.action }, "OpenVPN server updated");
8685
+ return {
8686
+ content: `OpenVPN server on ${context.routerId} ${parsed.action}d.`,
8687
+ structuredContent: { action: parsed.action === "enable" ? "enabled" : "disabled", server }
8688
+ };
8689
+ }
8690
+ 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;
8691
+ if (!hasFields) {
8692
+ throw new MikroMCPError({
8693
+ category: "VALIDATION" /* VALIDATION */,
8694
+ code: "OVPN_SERVER_NO_FIELDS",
8695
+ message: "At least one field must be provided for action set.",
8696
+ details: {},
8697
+ recoverability: {
8698
+ retryable: false,
8699
+ suggestedAction: "Provide at least one of: port, mode, protocol, certificate, cipher, auth.",
8700
+ alternativeTools: []
8701
+ }
8702
+ });
8703
+ }
8704
+ const updates = {};
8705
+ const diff = [];
8706
+ if (parsed.port !== void 0 && server.port !== String(parsed.port)) {
8707
+ updates.port = String(parsed.port);
8708
+ diff.push({ property: "port", before: server.port ?? null, after: String(parsed.port) });
8709
+ }
8710
+ if (parsed.mode !== void 0 && server.mode !== parsed.mode) {
8711
+ updates.mode = parsed.mode;
8712
+ diff.push({ property: "mode", before: server.mode ?? null, after: parsed.mode });
8713
+ }
8714
+ if (parsed.protocol !== void 0 && server.protocol !== parsed.protocol) {
8715
+ updates.protocol = parsed.protocol;
8716
+ diff.push({ property: "protocol", before: server.protocol ?? null, after: parsed.protocol });
8717
+ }
8718
+ if (parsed.certificate !== void 0 && server.certificate !== parsed.certificate) {
8719
+ updates.certificate = parsed.certificate;
8720
+ diff.push({ property: "certificate", before: server.certificate ?? null, after: parsed.certificate });
8721
+ }
8722
+ if (parsed.cipher !== void 0 && server.cipher !== parsed.cipher) {
8723
+ updates.cipher = parsed.cipher;
8724
+ diff.push({ property: "cipher", before: server.cipher ?? null, after: parsed.cipher });
8725
+ }
8726
+ if (parsed.auth !== void 0 && server.auth !== parsed.auth) {
8727
+ updates.auth = parsed.auth;
8728
+ diff.push({ property: "auth", before: server.auth ?? null, after: parsed.auth });
8729
+ }
8730
+ if (Object.keys(updates).length === 0) {
8731
+ return {
8732
+ content: `OpenVPN server on ${context.routerId} already matches the requested configuration. No changes made.`,
8733
+ structuredContent: { action: "no_change", server }
8734
+ };
8735
+ }
8736
+ if (parsed.dryRun) {
8737
+ return {
8738
+ content: `Dry run: Would update OpenVPN server on ${context.routerId}.`,
8739
+ structuredContent: { action: "dry_run", diff }
8740
+ };
8741
+ }
8742
+ await context.routerClient.update(OVPN_SERVER_PATH, serverId, updates);
8743
+ log38.info({ routerId: context.routerId }, "OpenVPN server configuration updated");
8744
+ return {
8745
+ content: `Updated OpenVPN server configuration on ${context.routerId}.`,
8746
+ structuredContent: { action: "updated", diff }
8747
+ };
8748
+ } catch (err) {
8749
+ if (err instanceof MikroMCPError) throw err;
8750
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_server" });
8751
+ }
8752
+ }
8753
+ };
8754
+ var ovpnTools = [
8755
+ listOvpnClientsTool,
8756
+ manageOvpnClientTool,
8757
+ getOvpnServerTool,
8758
+ manageOvpnServerTool
8759
+ ];
8760
+
8761
+ // src/domain/tools/upgrade-tools.ts
8762
+ import { z as z38 } from "zod";
8763
+ var log39 = createLogger("upgrade-tools");
8764
+ var getUpgradeStatusInputSchema = z38.object({
8765
+ routerId: z38.string().describe("Target router identifier from the router registry")
8766
+ }).strict();
8767
+ var getUpgradeStatusTool = {
8768
+ name: "get_upgrade_status",
8769
+ title: "Get Upgrade Status",
8770
+ description: "Read the current RouterOS package upgrade status and routerboard firmware versions. Shows installed version, latest available version, update channel, and firmware upgrade availability.",
8771
+ inputSchema: getUpgradeStatusInputSchema,
8772
+ annotations: {
8773
+ readOnlyHint: true,
8774
+ destructiveHint: false,
8775
+ idempotentHint: true,
8776
+ openWorldHint: false
8777
+ },
8778
+ async handler(params, context) {
8779
+ const parsed = getUpgradeStatusInputSchema.parse(params);
8780
+ log39.info({ routerId: context.routerId }, "Getting upgrade status");
8781
+ try {
8782
+ const [updateResults, routerboardResults] = await Promise.all([
8783
+ context.routerClient.get("system/package/update"),
8784
+ context.routerClient.get("system/routerboard")
8785
+ ]);
8786
+ const update = updateResults[0] ?? {};
8787
+ const routerboard = routerboardResults.length > 0 ? routerboardResults[0] : null;
8788
+ return {
8789
+ content: `Upgrade status on ${parsed.routerId}: installed=${update["installed-version"] ?? "?"} latest=${update["latest-version"] ?? "?"} (${update.status ?? "unknown"})`,
8790
+ structuredContent: {
8791
+ routerId: parsed.routerId,
8792
+ channel: update.channel ?? null,
8793
+ installedVersion: update["installed-version"] ?? null,
8794
+ latestVersion: update["latest-version"] ?? null,
8795
+ status: update.status ?? null,
8796
+ routerboard: routerboard ? {
8797
+ model: routerboard.model ?? null,
8798
+ currentFirmware: routerboard["current-firmware"] ?? null,
8799
+ upgradeFirmware: routerboard["upgrade-firmware"] ?? null,
8800
+ factoryFirmware: routerboard["factory-firmware"] ?? null
8801
+ } : null
8802
+ }
8803
+ };
8804
+ } catch (err) {
8805
+ throw enrichError(err, { routerId: context.routerId, tool: "get_upgrade_status" });
8806
+ }
8807
+ }
8808
+ };
8809
+ var manageUpgradeInputSchema = z38.object({
8810
+ routerId: z38.string().describe("Target router identifier from the router registry"),
8811
+ action: z38.enum(["check", "install"]).describe(
8812
+ "check \u2014 trigger a check for available updates; install \u2014 download and install the latest update (triggers reboot)"
8813
+ ),
8814
+ dryRun: z38.boolean().default(false).describe("Preview the action without executing")
8815
+ }).strict();
8816
+ var manageUpgradeTool = {
8817
+ name: "manage_upgrade",
8818
+ title: "Manage Upgrade",
8819
+ description: "Trigger a RouterOS package update check or install. 'check' queries the update server for new packages. 'install' downloads and applies the update \u2014 the router will reboot automatically. Supports dry-run.",
8820
+ inputSchema: manageUpgradeInputSchema,
8821
+ annotations: {
8822
+ readOnlyHint: false,
8823
+ destructiveHint: true,
8824
+ idempotentHint: false,
8825
+ openWorldHint: false
8826
+ },
8827
+ async handler(params, context) {
8828
+ const parsed = manageUpgradeInputSchema.parse(params);
8829
+ log39.info({ routerId: context.routerId, action: parsed.action }, "Managing upgrade");
8830
+ try {
8831
+ if (parsed.dryRun) {
8832
+ return {
8833
+ content: `Dry run: Would execute '${parsed.action}' on ${parsed.routerId}.`,
8834
+ structuredContent: {
8835
+ action: "dry_run",
8836
+ requestedAction: parsed.action,
8837
+ routerId: parsed.routerId
8838
+ }
8839
+ };
8840
+ }
8841
+ if (parsed.action === "check") {
8842
+ await context.routerClient.execute("system/package/update/check-for-updates");
8843
+ log39.info({ routerId: context.routerId }, "Update check triggered");
8844
+ return {
8845
+ content: `Update check triggered on ${parsed.routerId}.`,
8846
+ structuredContent: { action: "check_triggered", routerId: parsed.routerId }
8847
+ };
8848
+ }
8849
+ await context.routerClient.execute("system/package/update/install");
8850
+ log39.info({ routerId: context.routerId }, "Package install triggered");
8851
+ return {
8852
+ content: `Package install triggered on ${parsed.routerId}. Router will reboot automatically.`,
8853
+ structuredContent: { action: "install_triggered", routerId: parsed.routerId }
8854
+ };
8855
+ } catch (err) {
8856
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_upgrade" });
8857
+ }
8858
+ }
8859
+ };
8860
+ var upgradeTools = [getUpgradeStatusTool, manageUpgradeTool];
8861
+
8862
+ // src/domain/tools/backup-tools.ts
8863
+ import { z as z39 } from "zod";
8864
+ var log40 = createLogger("backup-tools");
8865
+ var createBackupInputSchema = z39.object({
8866
+ routerId: z39.string().describe("Target router identifier from the router registry"),
8867
+ name: z39.string().default("backup").describe("Backup file name (without extension)"),
8868
+ password: z39.string().optional().describe("Password to encrypt the backup file"),
8869
+ dryRun: z39.boolean().default(false).describe("Preview the backup without creating it")
8870
+ }).strict();
8871
+ var createBackupTool = {
8872
+ name: "create_backup",
8873
+ title: "Create Backup",
8874
+ description: "Create a binary configuration backup on a MikroTik router. The backup is saved as <name>.backup on the router's filesystem. Supports optional encryption via password and dry-run mode.",
8875
+ inputSchema: createBackupInputSchema,
8876
+ annotations: {
8877
+ readOnlyHint: false,
8878
+ destructiveHint: false,
8879
+ idempotentHint: false,
8880
+ openWorldHint: false
8881
+ },
8882
+ async handler(params, context) {
8883
+ const parsed = createBackupInputSchema.parse(params);
8884
+ log40.info({ routerId: context.routerId, name: parsed.name }, "Creating backup");
8885
+ if (parsed.dryRun) {
8886
+ return {
8887
+ content: `Dry run: Would create backup "${parsed.name}.backup" on ${context.routerId}.`,
8888
+ structuredContent: { action: "dry_run", name: parsed.name, routerId: context.routerId }
8889
+ };
8890
+ }
8891
+ try {
8892
+ const body = { name: parsed.name };
8893
+ if (parsed.password !== void 0) {
8894
+ body.password = parsed.password;
8895
+ }
8896
+ await context.routerClient.execute("system/backup/save", body);
8897
+ log40.info({ routerId: context.routerId, name: parsed.name }, "Backup created");
8898
+ return {
8899
+ content: `Backup "${parsed.name}.backup" created on ${context.routerId}.`,
8900
+ structuredContent: {
8901
+ action: "created",
8902
+ routerId: context.routerId,
8903
+ filePath: `${parsed.name}.backup`
8904
+ }
8905
+ };
8906
+ } catch (err) {
8907
+ if (err instanceof MikroMCPError) throw err;
8908
+ throw enrichError(err, { routerId: context.routerId, tool: "create_backup" });
8909
+ }
8910
+ }
8911
+ };
8912
+ var exportConfigInputSchema = z39.object({
8913
+ routerId: z39.string().describe("Target router identifier from the router registry"),
8914
+ compact: z39.boolean().default(false).describe("Export only non-default settings (compact format)"),
8915
+ file: z39.string().optional().describe("Save the export to a file on the router (without extension); omit to return inline")
8916
+ }).strict();
8917
+ var exportConfigTool = {
8918
+ name: "export_config",
8919
+ title: "Export Config",
8920
+ description: "Export the router configuration as a RouterOS script. When no file is specified, returns the script text inline. When a file is specified, saves it as <file>.rsc on the router's filesystem. Supports compact mode to show only non-default values.",
8921
+ inputSchema: exportConfigInputSchema,
8922
+ annotations: {
8923
+ readOnlyHint: true,
8924
+ destructiveHint: false,
8925
+ idempotentHint: true,
8926
+ openWorldHint: false
8927
+ },
8928
+ async handler(params, context) {
8929
+ const parsed = exportConfigInputSchema.parse(params);
8930
+ log40.info({ routerId: context.routerId, compact: parsed.compact }, "Exporting config");
8931
+ try {
8932
+ if (parsed.file !== void 0) {
8933
+ const body = { file: parsed.file };
8934
+ if (parsed.compact) body.compact = "yes";
8935
+ await context.routerClient.execute("system/export", body);
8936
+ return {
8937
+ content: `Config exported to "${parsed.file}.rsc" on ${context.routerId}.`,
8938
+ structuredContent: {
8939
+ routerId: context.routerId,
8940
+ filePath: `${parsed.file}.rsc`
8941
+ }
8942
+ };
8943
+ }
8944
+ const sshCommand = parsed.compact ? "export compact" : "export";
8945
+ const scriptText = await context.sshClient.execute(sshCommand);
8946
+ return {
8947
+ content: `Config export from ${context.routerId}:
8948
+ ${scriptText}`,
8949
+ structuredContent: {
8950
+ routerId: context.routerId,
8951
+ script: scriptText,
8952
+ compact: parsed.compact
8953
+ }
8954
+ };
8955
+ } catch (err) {
8956
+ if (err instanceof MikroMCPError) throw err;
8957
+ throw enrichError(err, { routerId: context.routerId, tool: "export_config" });
8958
+ }
8959
+ }
8960
+ };
8961
+ var backupTools = [createBackupTool, exportConfigTool];
8962
+
8963
+ // src/domain/tools/log-tools.ts
8964
+ import { z as z40 } from "zod";
8965
+ var log41 = createLogger("log-tools");
8966
+ var LOG_RULES_PATH = "system/logging";
8967
+ var LOG_ACTIONS_PATH = "system/logging/action";
8968
+ var logActionTypeEnum = z40.enum(["memory", "disk", "remote", "echo", "email"]);
8969
+ var listLogRulesInputSchema = z40.object({
8970
+ routerId: z40.string().describe("Target router identifier from the router registry"),
8971
+ topics: z40.string().optional().describe("Filter by topics (substring match)"),
8972
+ logAction: z40.string().optional().describe("Filter by log action target (exact match on action field)"),
8973
+ limit: z40.number().int().min(1).max(500).default(100).describe("Maximum number of rules to return (1-500, default 100)")
8974
+ }).strict();
8975
+ var listLogRulesTool = {
8976
+ name: "list_log_rules",
8977
+ title: "List Log Rules",
8978
+ description: "List RouterOS logging rules (system/logging) with optional topic substring and action exact-match filtering.",
8979
+ inputSchema: listLogRulesInputSchema,
8980
+ annotations: {
8981
+ readOnlyHint: true,
8982
+ destructiveHint: false,
8983
+ idempotentHint: true,
8984
+ openWorldHint: false
8985
+ },
8986
+ async handler(params, context) {
8987
+ const parsed = listLogRulesInputSchema.parse(params);
8988
+ log41.info({ routerId: context.routerId }, "Listing log rules");
8989
+ try {
8990
+ const all = await context.routerClient.get(LOG_RULES_PATH, {
8991
+ limit: void 0,
8992
+ offset: void 0
8993
+ });
8994
+ let rules = all;
8995
+ if (parsed.topics !== void 0) {
8996
+ rules = rules.filter((r) => r.topics?.includes(parsed.topics));
8997
+ }
8998
+ if (parsed.logAction !== void 0) {
8999
+ rules = rules.filter((r) => r.action === parsed.logAction);
9000
+ }
9001
+ const returned = rules.slice(0, parsed.limit);
9002
+ return {
9003
+ content: `Log rules on ${context.routerId}: ${returned.length} rule(s) (${all.length} total).`,
9004
+ structuredContent: {
9005
+ routerId: context.routerId,
9006
+ rules: returned,
9007
+ total: all.length,
9008
+ returned: returned.length
9009
+ }
9010
+ };
9011
+ } catch (err) {
9012
+ if (err instanceof MikroMCPError) throw err;
9013
+ throw enrichError(err, { routerId: context.routerId, tool: "list_log_rules" });
9014
+ }
9015
+ }
9016
+ };
9017
+ var manageLogRuleInputSchema = z40.object({
9018
+ routerId: z40.string().describe("Target router identifier from the router registry"),
9019
+ action: z40.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
9020
+ topics: z40.string().describe("Log topics (e.g. firewall, system, info) \u2014 idempotency key"),
9021
+ logAction: z40.string().describe("Log action target name (RouterOS action field) \u2014 idempotency key"),
9022
+ prefix: z40.string().optional().describe("Optional prefix to prepend to log messages"),
9023
+ dryRun: z40.boolean().default(false).describe("Preview changes without applying")
9024
+ }).strict();
9025
+ var manageLogRuleTool = {
9026
+ name: "manage_log_rule",
9027
+ title: "Manage Log Rule",
9028
+ description: "Add, remove, enable, or disable a RouterOS logging rule. Idempotent by topics+logAction pair. add returns already_exists if matching rule found. remove returns not_found gracefully. enable/disable throw NOT_FOUND if rule not found. Supports dry-run.",
9029
+ inputSchema: manageLogRuleInputSchema,
9030
+ annotations: {
9031
+ readOnlyHint: false,
9032
+ destructiveHint: false,
9033
+ idempotentHint: true,
9034
+ openWorldHint: false
9035
+ },
9036
+ snapshotPaths: ["system/logging"],
9037
+ async handler(params, context) {
9038
+ const parsed = manageLogRuleInputSchema.parse(params);
9039
+ log41.info(
9040
+ { routerId: context.routerId, action: parsed.action, topics: parsed.topics },
9041
+ "Managing log rule"
9042
+ );
9043
+ try {
9044
+ const all = await context.routerClient.get(LOG_RULES_PATH, {
9045
+ limit: void 0,
9046
+ offset: void 0
9047
+ });
9048
+ const existing = all.find(
9049
+ (r) => r.topics === parsed.topics && r.action === parsed.logAction
9050
+ );
9051
+ if (parsed.action === "add") {
9052
+ if (existing) {
9053
+ return {
9054
+ content: `Log rule for topics "${parsed.topics}" \u2192 "${parsed.logAction}" already exists.`,
9055
+ structuredContent: {
9056
+ action: "already_exists",
9057
+ routerId: context.routerId,
9058
+ id: existing[".id"]
9059
+ }
9060
+ };
9061
+ }
9062
+ const body = {
9063
+ topics: parsed.topics,
9064
+ action: parsed.logAction
9065
+ };
9066
+ if (parsed.prefix !== void 0) body.prefix = parsed.prefix;
9067
+ if (parsed.dryRun) {
9068
+ return {
9069
+ content: `Dry run: Would add log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9070
+ structuredContent: {
9071
+ action: "dry_run",
9072
+ diff: Object.entries(body).map(([property, after]) => ({
9073
+ property,
9074
+ before: null,
9075
+ after
9076
+ }))
9077
+ }
9078
+ };
9079
+ }
9080
+ const created = await context.routerClient.create(LOG_RULES_PATH, body);
9081
+ log41.info({ topics: parsed.topics, logAction: parsed.logAction, id: created[".id"] }, "Log rule added");
9082
+ return {
9083
+ content: `Added log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9084
+ structuredContent: {
9085
+ action: "created",
9086
+ routerId: context.routerId,
9087
+ id: created[".id"]
9088
+ }
9089
+ };
9090
+ }
9091
+ if (parsed.action === "remove") {
9092
+ if (!existing) {
9093
+ return {
9094
+ content: `Log rule for topics "${parsed.topics}" \u2192 "${parsed.logAction}" not found. No changes made.`,
9095
+ structuredContent: {
9096
+ action: "not_found",
9097
+ routerId: context.routerId
9098
+ }
9099
+ };
9100
+ }
9101
+ const id = existing[".id"];
9102
+ if (parsed.dryRun) {
9103
+ return {
9104
+ content: `Dry run: Would remove log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9105
+ structuredContent: { action: "dry_run", id }
9106
+ };
9107
+ }
9108
+ await context.routerClient.remove(LOG_RULES_PATH, id);
9109
+ log41.info({ topics: parsed.topics, logAction: parsed.logAction, id }, "Log rule removed");
9110
+ return {
9111
+ content: `Removed log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9112
+ structuredContent: { action: "removed", id }
9113
+ };
9114
+ }
9115
+ if (parsed.action === "enable" || parsed.action === "disable") {
9116
+ if (!existing) {
9117
+ throw new MikroMCPError({
9118
+ category: "NOT_FOUND" /* NOT_FOUND */,
9119
+ code: "LOG_RULE_NOT_FOUND",
9120
+ message: `Log rule with topics "${parsed.topics}" and action "${parsed.logAction}" not found.`,
9121
+ details: { topics: parsed.topics, logAction: parsed.logAction },
9122
+ recoverability: {
9123
+ retryable: false,
9124
+ suggestedAction: "Verify with list_log_rules or use action=add to create it.",
9125
+ alternativeTools: ["list_log_rules", "manage_log_rule with action=add"]
9126
+ }
9127
+ });
9128
+ }
9129
+ const id = existing[".id"];
9130
+ const wantDisabled = parsed.action === "disable";
9131
+ if (parsed.dryRun) {
9132
+ return {
9133
+ content: `Dry run: Would ${parsed.action} log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9134
+ structuredContent: {
9135
+ action: "dry_run",
9136
+ id,
9137
+ diff: [{ property: "disabled", before: existing.disabled ?? "false", after: String(wantDisabled) }]
9138
+ }
9139
+ };
9140
+ }
9141
+ await context.routerClient.update(LOG_RULES_PATH, id, {
9142
+ disabled: wantDisabled ? "true" : "false"
9143
+ });
9144
+ const resultAction = wantDisabled ? "disabled" : "enabled";
9145
+ log41.info({ topics: parsed.topics, id, action: resultAction }, "Log rule toggled");
9146
+ return {
9147
+ content: `${wantDisabled ? "Disabled" : "Enabled"} log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9148
+ structuredContent: { action: resultAction, id }
9149
+ };
9150
+ }
9151
+ throw new MikroMCPError({
9152
+ category: "VALIDATION" /* VALIDATION */,
9153
+ code: "INVALID_ACTION",
9154
+ message: `Unknown action: ${parsed.action}`,
9155
+ recoverability: {
9156
+ retryable: false,
9157
+ suggestedAction: "Use one of: add, remove, enable, disable."
9158
+ }
9159
+ });
9160
+ } catch (err) {
9161
+ if (err instanceof MikroMCPError) throw err;
9162
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_log_rule" });
9163
+ }
9164
+ }
9165
+ };
9166
+ var listLogActionsInputSchema = z40.object({
9167
+ routerId: z40.string().describe("Target router identifier from the router registry"),
9168
+ type: logActionTypeEnum.optional().describe("Filter by action type (exact match): memory, disk, remote, echo, email"),
9169
+ limit: z40.number().int().min(1).max(500).default(100).describe("Maximum number of actions to return (1-500, default 100)")
9170
+ }).strict();
9171
+ var listLogActionsTool = {
9172
+ name: "list_log_actions",
9173
+ title: "List Log Actions",
9174
+ description: "List RouterOS logging action targets (system/logging/action) with optional type filter.",
9175
+ inputSchema: listLogActionsInputSchema,
9176
+ annotations: {
9177
+ readOnlyHint: true,
9178
+ destructiveHint: false,
9179
+ idempotentHint: true,
9180
+ openWorldHint: false
9181
+ },
9182
+ async handler(params, context) {
9183
+ const parsed = listLogActionsInputSchema.parse(params);
9184
+ log41.info({ routerId: context.routerId }, "Listing log actions");
9185
+ try {
9186
+ const all = await context.routerClient.get(LOG_ACTIONS_PATH, {
9187
+ limit: void 0,
9188
+ offset: void 0
9189
+ });
9190
+ let actions = all;
9191
+ if (parsed.type !== void 0) {
9192
+ actions = actions.filter((a) => a.type === parsed.type);
9193
+ }
9194
+ const returned = actions.slice(0, parsed.limit);
9195
+ return {
9196
+ content: `Log actions on ${context.routerId}: ${returned.length} action(s) (${all.length} total).`,
9197
+ structuredContent: {
9198
+ routerId: context.routerId,
9199
+ actions: returned,
9200
+ total: all.length,
9201
+ returned: returned.length
9202
+ }
9203
+ };
9204
+ } catch (err) {
9205
+ if (err instanceof MikroMCPError) throw err;
9206
+ throw enrichError(err, { routerId: context.routerId, tool: "list_log_actions" });
9207
+ }
9208
+ }
9209
+ };
9210
+ var manageLogActionInputSchema = z40.object({
9211
+ routerId: z40.string().describe("Target router identifier from the router registry"),
9212
+ action: z40.enum(["add", "remove"]).describe("Action to perform"),
9213
+ name: z40.string().describe("Log action name \u2014 idempotency key"),
9214
+ type: logActionTypeEnum.optional().describe("Action type: memory, disk, remote, echo, email (required on add)"),
9215
+ remote: z40.string().optional().describe("Remote syslog server address (for type=remote)"),
9216
+ remotePort: z40.number().int().min(1).max(65535).optional().describe("Remote syslog server port (for type=remote)"),
9217
+ diskFileName: z40.string().optional().describe("Disk log file name without extension (for type=disk)"),
9218
+ dryRun: z40.boolean().default(false).describe("Preview changes without applying")
9219
+ }).strict();
9220
+ var manageLogActionTool = {
9221
+ name: "manage_log_action",
9222
+ title: "Manage Log Action",
9223
+ description: "Add or remove a RouterOS logging action target. Idempotent by name. add throws VALIDATION if type is missing; returns already_exists if name found. remove returns not_found gracefully. Supports dry-run.",
9224
+ inputSchema: manageLogActionInputSchema,
9225
+ annotations: {
9226
+ readOnlyHint: false,
9227
+ destructiveHint: false,
9228
+ idempotentHint: true,
9229
+ openWorldHint: false
9230
+ },
9231
+ snapshotPaths: ["system/logging/action"],
9232
+ async handler(params, context) {
9233
+ const parsed = manageLogActionInputSchema.parse(params);
9234
+ log41.info(
9235
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
9236
+ "Managing log action"
9237
+ );
9238
+ try {
9239
+ const all = await context.routerClient.get(LOG_ACTIONS_PATH, {
9240
+ limit: void 0,
9241
+ offset: void 0
9242
+ });
9243
+ const existing = all.find((a) => a.name === parsed.name);
9244
+ if (parsed.action === "add") {
9245
+ if (parsed.type === void 0) {
9246
+ throw new MikroMCPError({
9247
+ category: "VALIDATION" /* VALIDATION */,
9248
+ code: "LOG_ACTION_TYPE_REQUIRED",
9249
+ message: "type is required when action is add",
9250
+ recoverability: {
9251
+ retryable: false,
9252
+ suggestedAction: "Provide the type field: memory, disk, remote, echo, or email."
9253
+ }
9254
+ });
9255
+ }
9256
+ if (existing) {
9257
+ return {
9258
+ content: `Log action "${parsed.name}" already exists.`,
9259
+ structuredContent: {
9260
+ action: "already_exists",
9261
+ routerId: context.routerId,
9262
+ name: parsed.name,
9263
+ id: existing[".id"]
9264
+ }
9265
+ };
9266
+ }
9267
+ const body = {
9268
+ name: parsed.name,
9269
+ type: parsed.type
9270
+ };
9271
+ if (parsed.remote !== void 0) body.remote = parsed.remote;
9272
+ if (parsed.remotePort !== void 0) body["remote-port"] = String(parsed.remotePort);
9273
+ if (parsed.diskFileName !== void 0) body["disk-file-name"] = parsed.diskFileName;
9274
+ if (parsed.dryRun) {
9275
+ return {
9276
+ content: `Dry run: Would add log action "${parsed.name}" (type=${parsed.type}).`,
9277
+ structuredContent: {
9278
+ action: "dry_run",
9279
+ diff: Object.entries(body).map(([property, after]) => ({
9280
+ property,
9281
+ before: null,
9282
+ after
9283
+ }))
9284
+ }
9285
+ };
9286
+ }
9287
+ const created = await context.routerClient.create(LOG_ACTIONS_PATH, body);
9288
+ log41.info({ name: parsed.name, type: parsed.type, id: created[".id"] }, "Log action added");
9289
+ return {
9290
+ content: `Added log action "${parsed.name}" (type=${parsed.type}).`,
9291
+ structuredContent: {
9292
+ action: "created",
9293
+ routerId: context.routerId,
9294
+ name: parsed.name,
9295
+ id: created[".id"]
9296
+ }
9297
+ };
9298
+ }
9299
+ if (parsed.action === "remove") {
9300
+ if (!existing) {
9301
+ return {
9302
+ content: `Log action "${parsed.name}" not found. No changes made.`,
9303
+ structuredContent: {
9304
+ action: "not_found",
9305
+ routerId: context.routerId,
9306
+ name: parsed.name
9307
+ }
9308
+ };
9309
+ }
9310
+ const id = existing[".id"];
9311
+ if (parsed.dryRun) {
9312
+ return {
9313
+ content: `Dry run: Would remove log action "${parsed.name}".`,
9314
+ structuredContent: { action: "dry_run", name: parsed.name, id }
9315
+ };
9316
+ }
9317
+ await context.routerClient.remove(LOG_ACTIONS_PATH, id);
9318
+ log41.info({ name: parsed.name, id }, "Log action removed");
9319
+ return {
9320
+ content: `Removed log action "${parsed.name}".`,
9321
+ structuredContent: { action: "removed", name: parsed.name, id }
9322
+ };
9323
+ }
9324
+ throw new MikroMCPError({
9325
+ category: "VALIDATION" /* VALIDATION */,
9326
+ code: "INVALID_ACTION",
9327
+ message: `Unknown action: ${parsed.action}`,
9328
+ recoverability: {
9329
+ retryable: false,
9330
+ suggestedAction: "Use one of: add, remove."
9331
+ }
9332
+ });
9333
+ } catch (err) {
9334
+ if (err instanceof MikroMCPError) throw err;
9335
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_log_action" });
9336
+ }
9337
+ }
9338
+ };
9339
+ var logTools = [
9340
+ listLogRulesTool,
9341
+ manageLogRuleTool,
9342
+ listLogActionsTool,
9343
+ manageLogActionTool
9344
+ ];
9345
+
9346
+ // src/domain/tools/fleet-tools.ts
9347
+ import { z as z41 } from "zod";
9348
+
9349
+ // src/middleware/authz.ts
9350
+ function matchesPattern(name, pattern) {
9351
+ if (!pattern.includes("*")) return name === pattern;
9352
+ const prefix = pattern.slice(0, pattern.indexOf("*"));
7785
9353
  return name.startsWith(prefix);
7786
9354
  }
7787
9355
  function checkAuthz(identity, toolName, routerId) {
@@ -7944,7 +9512,7 @@ var SshClient = class {
7944
9512
  // src/adapter/ftp-client.ts
7945
9513
  import { Client as Client2 } from "basic-ftp";
7946
9514
  import { Readable } from "stream";
7947
- var log36 = createLogger("ftp-client");
9515
+ var log42 = createLogger("ftp-client");
7948
9516
  var FtpClient = class {
7949
9517
  #host;
7950
9518
  #port;
@@ -7957,7 +9525,7 @@ var FtpClient = class {
7957
9525
  this.#password = password;
7958
9526
  }
7959
9527
  async upload(remoteName, content) {
7960
- log36.info({ host: this.#host, remoteName }, "Uploading file via FTP");
9528
+ log42.info({ host: this.#host, remoteName }, "Uploading file via FTP");
7961
9529
  const client = new Client2();
7962
9530
  try {
7963
9531
  await client.access({
@@ -7967,7 +9535,7 @@ var FtpClient = class {
7967
9535
  password: this.#password
7968
9536
  });
7969
9537
  await client.uploadFrom(Readable.from(Buffer.from(content, "utf-8")), remoteName);
7970
- log36.info({ host: this.#host, remoteName }, "FTP upload complete");
9538
+ log42.info({ host: this.#host, remoteName }, "FTP upload complete");
7971
9539
  } finally {
7972
9540
  client.close();
7973
9541
  }
@@ -8075,17 +9643,17 @@ function checkFleetConfirmation(args, secret) {
8075
9643
  }
8076
9644
 
8077
9645
  // src/domain/tools/fleet-tools.ts
8078
- var log37 = createLogger("fleet-tools");
8079
- var checkHealthInputSchema = z35.object({
8080
- routerId: z35.string().describe("Target router to health-check")
9646
+ var log43 = createLogger("fleet-tools");
9647
+ var checkHealthInputSchema = z41.object({
9648
+ routerId: z41.string().describe("Target router to health-check")
8081
9649
  }).strict();
8082
- var bulkExecuteInputSchema = z35.object({
8083
- toolName: z35.string().describe("Name of the tool to fan out (must be a single-router tool)"),
8084
- routerIds: z35.array(z35.string()).optional().describe("Explicit list of router IDs to target"),
8085
- tags: z35.array(z35.string()).optional().describe("Target all routers with ALL of these tags (mutually exclusive with routerIds)"),
8086
- params: z35.record(z35.string(), z35.unknown()).describe("Params to pass to the tool (omit routerId \u2014 injected per router)"),
8087
- concurrency: z35.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls"),
8088
- confirmationToken: z35.string().optional().describe(
9650
+ var bulkExecuteInputSchema = z41.object({
9651
+ toolName: z41.string().describe("Name of the tool to fan out (must be a single-router tool)"),
9652
+ routerIds: z41.array(z41.string()).optional().describe("Explicit list of router IDs to target"),
9653
+ tags: z41.array(z41.string()).optional().describe("Target all routers with ALL of these tags (mutually exclusive with routerIds)"),
9654
+ params: z41.record(z41.string(), z41.unknown()).describe("Params to pass to the tool (omit routerId \u2014 injected per router)"),
9655
+ concurrency: z41.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls"),
9656
+ confirmationToken: z41.string().optional().describe(
8089
9657
  "Fleet confirmation token from a prior APPROVAL_REQUIRED response. Required to fan out a destructive tool."
8090
9658
  )
8091
9659
  }).strict();
@@ -8104,7 +9672,7 @@ function createFleetTools(baseTools2) {
8104
9672
  },
8105
9673
  async handler(params, context) {
8106
9674
  checkHealthInputSchema.parse(params);
8107
- log37.info({ routerId: context.routerId }, "Checking router health");
9675
+ log43.info({ routerId: context.routerId }, "Checking router health");
8108
9676
  const startMs = Date.now();
8109
9677
  try {
8110
9678
  const resources = await context.routerClient.get("system/resource");
@@ -8166,7 +9734,7 @@ function createFleetTools(baseTools2) {
8166
9734
  }
8167
9735
  });
8168
9736
  }
8169
- log37.info(
9737
+ log43.info(
8170
9738
  { toolName: parsed.toolName, concurrency: parsed.concurrency },
8171
9739
  "bulk_execute invoked"
8172
9740
  );
@@ -8213,7 +9781,7 @@ function createFleetTools(baseTools2) {
8213
9781
  try {
8214
9782
  resolved.push(context.routerRegistry.getRouter(id));
8215
9783
  } catch {
8216
- log37.warn({ routerId: id }, "bulk_execute: router not found");
9784
+ log43.warn({ routerId: id }, "bulk_execute: router not found");
8217
9785
  preErrors.push({
8218
9786
  routerId: id,
8219
9787
  status: "error",
@@ -8384,6 +9952,7 @@ var baseTools = [
8384
9952
  ...ipsecTools,
8385
9953
  ...certificateTools,
8386
9954
  ...userTools,
9955
+ ...userGroupTools,
8387
9956
  ...dhcpServerTools,
8388
9957
  ...ipPoolTools,
8389
9958
  ...queueTools,
@@ -8391,7 +9960,12 @@ var baseTools = [
8391
9960
  ...networkServicesTools,
8392
9961
  ...vlanTools,
8393
9962
  ...dhcpClientTools,
8394
- ...ipServiceTools
9963
+ ...ipServiceTools,
9964
+ ...pppoeTools,
9965
+ ...ovpnTools,
9966
+ ...upgradeTools,
9967
+ ...backupTools,
9968
+ ...logTools
8395
9969
  ];
8396
9970
  var allTools = [
8397
9971
  ...baseTools,
@@ -8790,7 +10364,7 @@ var CircuitBreaker = class {
8790
10364
  };
8791
10365
 
8792
10366
  // src/mcp/tool-executor.ts
8793
- var log38 = createLogger("tool-executor");
10367
+ var log44 = createLogger("tool-executor");
8794
10368
  async function executeToolCall(tool, args, deps) {
8795
10369
  const { registry, pool, circuitBreakers, config, identityRegistry } = deps;
8796
10370
  const correlationId = nanoid4();
@@ -8886,9 +10460,9 @@ async function executeToolCall(tool, args, deps) {
8886
10460
  try {
8887
10461
  const meta = await takeSnapshot(toolContext.routerClient, routerId, path, config.snapshotDir);
8888
10462
  snapshotIds.push(meta.id);
8889
- log38.debug({ snapshotId: meta.id, path }, "Snapshot taken");
10463
+ log44.debug({ snapshotId: meta.id, path }, "Snapshot taken");
8890
10464
  } catch (err) {
8891
- log38.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
10465
+ log44.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
8892
10466
  }
8893
10467
  }
8894
10468
  }
@@ -8930,7 +10504,7 @@ async function executeToolCall(tool, args, deps) {
8930
10504
  durationMs: Date.now() - startMs
8931
10505
  }, config.auditLogPath);
8932
10506
  }
8933
- log38.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
10507
+ log44.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
8934
10508
  recordToolCall(tool.name, "success");
8935
10509
  return formatToolResult(result);
8936
10510
  } catch (err) {
@@ -8939,7 +10513,7 @@ async function executeToolCall(tool, args, deps) {
8939
10513
  const failedRouterId = args.routerId;
8940
10514
  if (failedRouterId) {
8941
10515
  pool.removeClient(failedRouterId);
8942
- log38.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
10516
+ log44.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
8943
10517
  }
8944
10518
  }
8945
10519
  if (journalId) {
@@ -8969,7 +10543,7 @@ async function executeToolCall(tool, args, deps) {
8969
10543
  durationMs: Date.now() - startMs
8970
10544
  }, config.auditLogPath);
8971
10545
  }
8972
- log38.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
10546
+ log44.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
8973
10547
  recordToolCall(tool.name, "error");
8974
10548
  return formatError(error);
8975
10549
  }
@@ -8977,12 +10551,12 @@ async function executeToolCall(tool, args, deps) {
8977
10551
  }
8978
10552
 
8979
10553
  // src/mcp/tool-registry.ts
8980
- var log39 = createLogger("tool-registry");
10554
+ var log45 = createLogger("tool-registry");
8981
10555
  function registerAllTools(server, registry, pool, circuitBreakers, config, identityRegistry) {
8982
10556
  const deps = { registry, pool, circuitBreakers, config, identityRegistry };
8983
10557
  for (const tool of allTools) {
8984
10558
  const registrationSchema = tool.annotations.destructiveHint ? tool.inputSchema.extend({
8985
- confirmationToken: z36.string().optional().describe(
10559
+ confirmationToken: z42.string().optional().describe(
8986
10560
  "Token from a prior APPROVAL_REQUIRED response. Re-submit the identical call with this token to confirm the destructive action."
8987
10561
  )
8988
10562
  }) : tool.inputSchema;
@@ -8996,56 +10570,56 @@ function registerAllTools(server, registry, pool, circuitBreakers, config, ident
8996
10570
  },
8997
10571
  (args) => executeToolCall(tool, args, deps)
8998
10572
  );
8999
- log39.debug({ tool: tool.name }, "Registered tool");
10573
+ log45.debug({ tool: tool.name }, "Registered tool");
9000
10574
  }
9001
- log39.info({ count: allTools.length }, "All tools registered");
10575
+ log45.info({ count: allTools.length }, "All tools registered");
9002
10576
  }
9003
10577
 
9004
10578
  // src/config/router-registry.ts
9005
10579
  import { readFileSync as readFileSync2, existsSync } from "fs";
9006
10580
  import { parse } from "yaml";
9007
- import { z as z37 } from "zod";
9008
- var log40 = createLogger("router-registry");
9009
- var RouterConfigSchema = z37.object({
9010
- host: z37.string().min(1, "host is required"),
9011
- port: z37.number().int().min(1).max(65535),
9012
- tls: z37.object({
9013
- enabled: z37.boolean(),
9014
- rejectUnauthorized: z37.boolean(),
9015
- ca: z37.string().optional(),
9016
- fingerprint: z37.string().optional()
10581
+ import { z as z43 } from "zod";
10582
+ var log46 = createLogger("router-registry");
10583
+ var RouterConfigSchema = z43.object({
10584
+ host: z43.string().min(1, "host is required"),
10585
+ port: z43.number().int().min(1).max(65535),
10586
+ tls: z43.object({
10587
+ enabled: z43.boolean(),
10588
+ rejectUnauthorized: z43.boolean(),
10589
+ ca: z43.string().optional(),
10590
+ fingerprint: z43.string().optional()
9017
10591
  }).strict(),
9018
- credentials: z37.object({
9019
- source: z37.enum(["env", "vault"]),
9020
- envPrefix: z37.string().optional(),
9021
- vaultPath: z37.string().optional()
10592
+ credentials: z43.object({
10593
+ source: z43.enum(["env", "vault"]),
10594
+ envPrefix: z43.string().optional(),
10595
+ vaultPath: z43.string().optional()
9022
10596
  }).strict(),
9023
- tags: z37.array(z37.string()).default([]),
9024
- rosVersion: z37.string().min(1),
9025
- sshPort: z37.number().int().min(1).max(65535).optional(),
9026
- sshFingerprint: z37.string().optional(),
9027
- cmdAllow: z37.array(z37.string()).optional(),
9028
- cmdDeny: z37.array(z37.string()).optional(),
9029
- maintenanceWindows: z37.array(
9030
- z37.object({
9031
- days: z37.array(
9032
- z37.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
10597
+ tags: z43.array(z43.string()).default([]),
10598
+ rosVersion: z43.string().min(1),
10599
+ sshPort: z43.number().int().min(1).max(65535).optional(),
10600
+ sshFingerprint: z43.string().optional(),
10601
+ cmdAllow: z43.array(z43.string()).optional(),
10602
+ cmdDeny: z43.array(z43.string()).optional(),
10603
+ maintenanceWindows: z43.array(
10604
+ z43.object({
10605
+ days: z43.array(
10606
+ z43.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
9033
10607
  ),
9034
- startTime: z37.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
9035
- endTime: z37.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
9036
- timezone: z37.string().min(1)
10608
+ startTime: z43.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
10609
+ endTime: z43.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
10610
+ timezone: z43.string().min(1)
9037
10611
  }).strict()
9038
10612
  ).optional()
9039
10613
  }).strict();
9040
- var ConfigFileSchema = z37.object({
9041
- routers: z37.record(z37.string(), RouterConfigSchema)
10614
+ var ConfigFileSchema = z43.object({
10615
+ routers: z43.record(z43.string(), RouterConfigSchema)
9042
10616
  }).strict();
9043
10617
  var RouterRegistry = class {
9044
10618
  routers;
9045
10619
  constructor(configPath) {
9046
10620
  this.routers = /* @__PURE__ */ new Map();
9047
10621
  if (!existsSync(configPath)) {
9048
- log40.warn({ configPath }, "Router config file not found; starting with empty registry");
10622
+ log46.warn({ configPath }, "Router config file not found; starting with empty registry");
9049
10623
  return;
9050
10624
  }
9051
10625
  const raw = readFileSync2(configPath, "utf-8");
@@ -9059,14 +10633,14 @@ ${issues}`);
9059
10633
  for (const [id, config] of Object.entries(result.data.routers)) {
9060
10634
  const routerConfig = { ...config, id };
9061
10635
  if (routerConfig.tls.enabled && !routerConfig.tls.rejectUnauthorized) {
9062
- log40.warn(
10636
+ log46.warn(
9063
10637
  { routerId: id },
9064
10638
  "TLS certificate validation is DISABLED (rejectUnauthorized=false). Set tls.fingerprint to pin the server certificate instead."
9065
10639
  );
9066
10640
  }
9067
10641
  this.routers.set(id, routerConfig);
9068
10642
  }
9069
- log40.info({ count: this.routers.size }, "Loaded routers from config");
10643
+ log46.info({ count: this.routers.size }, "Loaded routers from config");
9070
10644
  }
9071
10645
  getRouter(id) {
9072
10646
  const router = this.routers.get(id);
@@ -9351,7 +10925,7 @@ var ConnectionPool = class {
9351
10925
  };
9352
10926
 
9353
10927
  // src/version.ts
9354
- var VERSION = "1.2.0";
10928
+ var VERSION = "1.4.0";
9355
10929
 
9356
10930
  // src/mcp/server.ts
9357
10931
  function createServerFactory(config, identityRegistry) {
@@ -9381,7 +10955,7 @@ import { randomUUID } from "crypto";
9381
10955
  import { createServer } from "http";
9382
10956
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9383
10957
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
9384
- var log41 = createLogger("transport-http");
10958
+ var log47 = createLogger("transport-http");
9385
10959
  var BodyTooLargeError = class extends Error {
9386
10960
  constructor() {
9387
10961
  super("Request body exceeds maximum allowed size");
@@ -9473,7 +11047,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
9473
11047
  return;
9474
11048
  }
9475
11049
  if (!checkRateLimit(ip)) {
9476
- log41.warn({ ip }, "Rate limit exceeded");
11050
+ log47.warn({ ip }, "Rate limit exceeded");
9477
11051
  res.writeHead(429, { "Content-Type": "application/json" });
9478
11052
  res.end(JSON.stringify({ error: "Too many requests. Retry after 60 seconds." }));
9479
11053
  return;
@@ -9514,7 +11088,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
9514
11088
  }
9515
11089
  return;
9516
11090
  }
9517
- log41.error({ err }, "HTTP transport error");
11091
+ log47.error({ err }, "HTTP transport error");
9518
11092
  if (!res.headersSent) {
9519
11093
  res.writeHead(500, { "Content-Type": "application/json" });
9520
11094
  res.end(JSON.stringify({ error: "Internal server error" }));
@@ -9524,7 +11098,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
9524
11098
  httpServer.on("close", () => clearInterval(sweepTimer));
9525
11099
  await new Promise((resolve, reject) => {
9526
11100
  httpServer.listen(port, bindHost, () => {
9527
- log41.info(
11101
+ log47.info(
9528
11102
  { port, bindHost },
9529
11103
  "HTTP transport listening (POST /mcp for Streamable HTTP, GET /sse for legacy SSE)"
9530
11104
  );
@@ -9548,13 +11122,13 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
9548
11122
  sessionIdGenerator: () => randomUUID(),
9549
11123
  onsessioninitialized: (id) => {
9550
11124
  streamableSessions.set(id, transport2);
9551
- log41.info({ sessionId: id }, "Streamable HTTP session created");
11125
+ log47.info({ sessionId: id }, "Streamable HTTP session created");
9552
11126
  }
9553
11127
  });
9554
11128
  transport2.onclose = () => {
9555
11129
  if (transport2.sessionId) {
9556
11130
  streamableSessions.delete(transport2.sessionId);
9557
- log41.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
11131
+ log47.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
9558
11132
  }
9559
11133
  };
9560
11134
  await makeServer().connect(transport2);
@@ -9571,7 +11145,7 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
9571
11145
  sseTransport.onclose = () => {
9572
11146
  sseTransports.delete(sseTransport.sessionId);
9573
11147
  };
9574
- log41.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
11148
+ log47.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
9575
11149
  await sseTransport.start();
9576
11150
  return;
9577
11151
  }
@@ -9592,23 +11166,23 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
9592
11166
  // src/config/identity-registry.ts
9593
11167
  import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
9594
11168
  import { parse as parse2 } from "yaml";
9595
- import { z as z38 } from "zod";
11169
+ import { z as z44 } from "zod";
9596
11170
  import bcrypt from "bcryptjs";
9597
- var log42 = createLogger("identity-registry");
9598
- var IdentityConfigSchema = z38.object({
9599
- token: z38.string().min(1, "token hash is required"),
9600
- role: z38.enum(["readonly", "operator", "admin", "superadmin"]),
9601
- allowedRouters: z38.array(z38.string()).default([]),
9602
- allowedToolPatterns: z38.array(z38.string()).default([])
11171
+ var log48 = createLogger("identity-registry");
11172
+ var IdentityConfigSchema = z44.object({
11173
+ token: z44.string().min(1, "token hash is required"),
11174
+ role: z44.enum(["readonly", "operator", "admin", "superadmin"]),
11175
+ allowedRouters: z44.array(z44.string()).default([]),
11176
+ allowedToolPatterns: z44.array(z44.string()).default([])
9603
11177
  }).strict();
9604
- var ConfigFileSchema2 = z38.object({
9605
- identities: z38.record(z38.string(), IdentityConfigSchema)
11178
+ var ConfigFileSchema2 = z44.object({
11179
+ identities: z44.record(z44.string(), IdentityConfigSchema)
9606
11180
  }).strict();
9607
11181
  var IdentityRegistry = class {
9608
11182
  identities = [];
9609
11183
  constructor(configPath) {
9610
11184
  if (!existsSync2(configPath)) {
9611
- log42.warn(
11185
+ log48.warn(
9612
11186
  { configPath },
9613
11187
  "Identity config file not found; starting with empty registry (stdio callers do not require it)"
9614
11188
  );
@@ -9631,7 +11205,7 @@ ${issues}`);
9631
11205
  allowedToolPatterns: config.allowedToolPatterns
9632
11206
  });
9633
11207
  }
9634
- log42.info({ count: this.identities.length }, "Loaded identities from config");
11208
+ log48.info({ count: this.identities.length }, "Loaded identities from config");
9635
11209
  }
9636
11210
  getIdentities() {
9637
11211
  return this.identities.map(({ id, role, allowedRouters, allowedToolPatterns }) => ({
@@ -9660,7 +11234,7 @@ ${issues}`);
9660
11234
  // src/domain/snapshot/retention.ts
9661
11235
  import { readdir, stat, unlink } from "fs/promises";
9662
11236
  import { join as join3 } from "path";
9663
- var log43 = createLogger("snapshot-retention");
11237
+ var log49 = createLogger("snapshot-retention");
9664
11238
  async function pruneSnapshots(snapshotDir, maxAgeDays) {
9665
11239
  const cutoffMs = Date.now() - maxAgeDays * 864e5;
9666
11240
  let removed = 0;
@@ -9687,21 +11261,21 @@ async function pruneSnapshots(snapshotDir, maxAgeDays) {
9687
11261
  removed++;
9688
11262
  }
9689
11263
  } catch (err) {
9690
- log43.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
11264
+ log49.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
9691
11265
  }
9692
11266
  }
9693
11267
  }
9694
11268
  if (removed > 0) {
9695
- log43.info({ removed, maxAgeDays }, "Pruned expired snapshots");
11269
+ log49.info({ removed, maxAgeDays }, "Pruned expired snapshots");
9696
11270
  }
9697
11271
  return removed;
9698
11272
  }
9699
11273
 
9700
11274
  // src/cli/serve.ts
9701
- var log44 = createLogger("main");
11275
+ var log50 = createLogger("main");
9702
11276
  async function runServe() {
9703
11277
  const config = loadAppConfig();
9704
- log44.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
11278
+ log50.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
9705
11279
  void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
9706
11280
  const identityRegistry = new IdentityRegistry(config.identitiesPath);
9707
11281
  if (config.transport === "http") {
@@ -9709,7 +11283,7 @@ async function runServe() {
9709
11283
  (i) => i.role === "readonly" || i.role === "operator"
9710
11284
  );
9711
11285
  if (hasLimitedRoles && !config.confirmationSecret) {
9712
- log44.error(
11286
+ log50.error(
9713
11287
  "MIKROMCP_CONFIRMATION_SECRET is required in HTTP mode when identities with role readonly or operator are configured"
9714
11288
  );
9715
11289
  process.exit(1);
@@ -9719,7 +11293,7 @@ async function runServe() {
9719
11293
  if (config.transport === "stdio") {
9720
11294
  const stdioIdentity = getStdioIdentity(config.stdioIdentity, identityRegistry);
9721
11295
  await withIdentity(stdioIdentity, () => connectStdio(makeServer()));
9722
- log44.info("MikroMCP server running via stdio");
11296
+ log50.info("MikroMCP server running via stdio");
9723
11297
  } else if (config.transport === "http") {
9724
11298
  await connectHttp(
9725
11299
  makeServer,
@@ -9731,13 +11305,13 @@ async function runServe() {
9731
11305
  },
9732
11306
  identityRegistry
9733
11307
  );
9734
- log44.info(
11308
+ log50.info(
9735
11309
  { port: config.port, bindHost: config.bindHost },
9736
11310
  "MikroMCP server running via HTTP/SSE"
9737
11311
  );
9738
11312
  }
9739
11313
  const shutdown = () => {
9740
- log44.info("Shutting down MikroMCP server");
11314
+ log50.info("Shutting down MikroMCP server");
9741
11315
  pool.closeAll();
9742
11316
  process.exit(0);
9743
11317
  };
@@ -9750,7 +11324,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
9750
11324
  import { homedir as homedir2 } from "os";
9751
11325
  import * as net from "net";
9752
11326
  import chalk from "chalk";
9753
- var localVersion = "1.2.0";
11327
+ var localVersion = "1.4.0";
9754
11328
  var results = [];
9755
11329
  function ok(msg) {
9756
11330
  console.log(chalk.green("\u2705 ") + msg);
@@ -10402,7 +11976,7 @@ async function runInit() {
10402
11976
  }
10403
11977
 
10404
11978
  // src/cli/index.ts
10405
- var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.2.0");
11979
+ var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.4.0");
10406
11980
  program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
10407
11981
  program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
10408
11982
  program.command("init").description("Interactive setup wizard").action(runInit);