mikromcp 1.3.0 → 1.5.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 +12 -11
  2. package/dist/main.js +2850 -666
  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 z38 } from "zod";
63
+ import { z as z46 } from "zod";
64
64
 
65
65
  // src/domain/tools/system-tools.ts
66
66
  import { z } from "zod";
@@ -2842,7 +2842,148 @@ var managePeerTool = {
2842
2842
  }
2843
2843
  }
2844
2844
  };
2845
- var wireguardTools = [listWgTool, listPeersTool, managePeerTool];
2845
+ var manageWgIfaceInputSchema = z12.object({
2846
+ routerId: z12.string().describe("Target router identifier from the router registry"),
2847
+ action: z12.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
2848
+ name: z12.string().describe("Interface name \u2014 idempotency key (e.g. wg0)"),
2849
+ listenPort: z12.number().int().min(1).max(65535).optional().describe("UDP listen port (RouterOS picks one if omitted)"),
2850
+ mtu: z12.number().int().min(1280).max(65535).default(1420).describe("MTU (default 1420)"),
2851
+ comment: z12.string().optional().describe("Optional comment"),
2852
+ dryRun: z12.boolean().default(false).describe("Preview changes without applying")
2853
+ }).strict();
2854
+ var manageWgIfaceTool = {
2855
+ name: "manage_wireguard_interface",
2856
+ title: "Manage WireGuard Interface",
2857
+ description: "Add, remove, enable, or disable a WireGuard interface. Idempotent by name. RouterOS generates the private key on create \u2014 it is never passed in. The public key is returned after creation.",
2858
+ inputSchema: manageWgIfaceInputSchema,
2859
+ annotations: {
2860
+ readOnlyHint: false,
2861
+ destructiveHint: false,
2862
+ idempotentHint: true,
2863
+ openWorldHint: false
2864
+ },
2865
+ snapshotPaths: ["interface/wireguard"],
2866
+ async handler(params, context) {
2867
+ const parsed = manageWgIfaceInputSchema.parse(params);
2868
+ log11.info(
2869
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
2870
+ "Managing WireGuard interface"
2871
+ );
2872
+ try {
2873
+ const all = await context.routerClient.get("interface/wireguard", {
2874
+ limit: void 0,
2875
+ offset: void 0
2876
+ });
2877
+ const existing = all.find((i) => i.name === parsed.name);
2878
+ if (parsed.action === "add") {
2879
+ if (existing) {
2880
+ return {
2881
+ content: `WireGuard interface "${parsed.name}" already exists. No changes made.`,
2882
+ structuredContent: {
2883
+ action: "already_exists",
2884
+ name: parsed.name,
2885
+ publicKey: existing["public-key"] ?? null
2886
+ }
2887
+ };
2888
+ }
2889
+ if (parsed.dryRun) {
2890
+ return {
2891
+ content: `Dry run: Would create WireGuard interface "${parsed.name}".`,
2892
+ structuredContent: {
2893
+ action: "dry_run",
2894
+ diff: [{ property: "name", before: null, after: parsed.name }]
2895
+ }
2896
+ };
2897
+ }
2898
+ const body = { name: parsed.name, mtu: String(parsed.mtu) };
2899
+ if (parsed.listenPort !== void 0) body["listen-port"] = String(parsed.listenPort);
2900
+ if (parsed.comment) body.comment = parsed.comment;
2901
+ const created = await context.routerClient.create(
2902
+ "interface/wireguard",
2903
+ body
2904
+ );
2905
+ log11.info({ name: parsed.name, id: created[".id"] }, "WireGuard interface created");
2906
+ return {
2907
+ content: `Created WireGuard interface "${parsed.name}". Public key: ${created["public-key"] ?? "unknown"}`,
2908
+ structuredContent: {
2909
+ action: "created",
2910
+ name: parsed.name,
2911
+ id: created[".id"],
2912
+ publicKey: created["public-key"] ?? null
2913
+ }
2914
+ };
2915
+ }
2916
+ if (parsed.action === "remove") {
2917
+ if (!existing) {
2918
+ return {
2919
+ content: `WireGuard interface "${parsed.name}" not found. Nothing to remove.`,
2920
+ structuredContent: { action: "not_found", name: parsed.name }
2921
+ };
2922
+ }
2923
+ if (parsed.dryRun) {
2924
+ return {
2925
+ content: `Dry run: Would remove WireGuard interface "${parsed.name}".`,
2926
+ structuredContent: {
2927
+ action: "dry_run",
2928
+ diff: [{ property: "name", before: parsed.name, after: null }]
2929
+ }
2930
+ };
2931
+ }
2932
+ await context.routerClient.remove("interface/wireguard", existing[".id"]);
2933
+ log11.info({ name: parsed.name }, "WireGuard interface removed");
2934
+ return {
2935
+ content: `Removed WireGuard interface "${parsed.name}".`,
2936
+ structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
2937
+ };
2938
+ }
2939
+ if (!existing) {
2940
+ throw new MikroMCPError({
2941
+ category: "NOT_FOUND" /* NOT_FOUND */,
2942
+ code: "WIREGUARD_IFACE_NOT_FOUND",
2943
+ message: `WireGuard interface "${parsed.name}" not found.`,
2944
+ recoverability: {
2945
+ retryable: false,
2946
+ suggestedAction: "Verify with list_wireguard_interfaces.",
2947
+ alternativeTools: ["list_wireguard_interfaces"]
2948
+ }
2949
+ });
2950
+ }
2951
+ if (parsed.dryRun) {
2952
+ return {
2953
+ content: `Dry run: Would ${parsed.action} WireGuard interface "${parsed.name}".`,
2954
+ structuredContent: {
2955
+ action: "dry_run",
2956
+ diff: [
2957
+ {
2958
+ property: "disabled",
2959
+ before: existing.disabled,
2960
+ after: parsed.action === "disable" ? "true" : "false"
2961
+ }
2962
+ ]
2963
+ }
2964
+ };
2965
+ }
2966
+ await context.routerClient.update("interface/wireguard", existing[".id"], {
2967
+ disabled: parsed.action === "disable" ? "true" : "false"
2968
+ });
2969
+ const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
2970
+ log11.info({ name: parsed.name, action: resultAction }, "WireGuard interface updated");
2971
+ return {
2972
+ content: `WireGuard interface "${parsed.name}" ${resultAction}.`,
2973
+ structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
2974
+ };
2975
+ } catch (err) {
2976
+ if (err instanceof MikroMCPError) throw err;
2977
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_wireguard_interface" });
2978
+ }
2979
+ }
2980
+ };
2981
+ var wireguardTools = [
2982
+ listWgTool,
2983
+ listPeersTool,
2984
+ managePeerTool,
2985
+ manageWgIfaceTool
2986
+ ];
2846
2987
 
2847
2988
  // src/domain/tools/dns-tools.ts
2848
2989
  import { z as z13 } from "zod";
@@ -3081,7 +3222,89 @@ var getDnsSettingsTool = {
3081
3222
  }
3082
3223
  }
3083
3224
  };
3084
- var dnsTools = [listDnsTool, manageDnsTool, getDnsSettingsTool];
3225
+ var manageDnsSettingsInputSchema = z13.object({
3226
+ routerId: z13.string().describe("Target router identifier from the router registry"),
3227
+ servers: z13.string().optional().describe("Comma-separated upstream DNS server IPs (e.g. '8.8.8.8,1.1.1.1')"),
3228
+ allowRemoteRequests: z13.boolean().optional().describe("Allow router to answer DNS queries from the network"),
3229
+ maxUdpPacketSize: z13.number().int().min(512).max(65535).optional().describe("Maximum UDP packet size in bytes"),
3230
+ cacheMaxTtl: z13.string().optional().describe("Maximum cache TTL (e.g. '1d', '00:30:00')"),
3231
+ cacheSize: z13.number().int().min(1).optional().describe("DNS cache size in KiB"),
3232
+ dryRun: z13.boolean().default(false).describe("Preview changes without applying")
3233
+ }).strict();
3234
+ var manageDnsSettingsTool = {
3235
+ name: "manage_dns_settings",
3236
+ title: "Manage DNS Settings",
3237
+ description: "Update DNS resolver settings (upstream servers, cache size, cache TTL, allow-remote-requests). Idempotent: returns no_change if nothing differs.",
3238
+ inputSchema: manageDnsSettingsInputSchema,
3239
+ annotations: {
3240
+ readOnlyHint: false,
3241
+ destructiveHint: false,
3242
+ idempotentHint: true,
3243
+ openWorldHint: false
3244
+ },
3245
+ snapshotPaths: ["ip/dns"],
3246
+ async handler(params, context) {
3247
+ const parsed = manageDnsSettingsInputSchema.parse(params);
3248
+ log12.info({ routerId: context.routerId }, "Managing DNS settings");
3249
+ try {
3250
+ const results2 = await context.routerClient.get("ip/dns");
3251
+ const current = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
3252
+ const id = current[".id"];
3253
+ const changes = {};
3254
+ const diff = [];
3255
+ if (parsed.servers !== void 0 && current.servers !== parsed.servers) {
3256
+ changes.servers = parsed.servers;
3257
+ diff.push({ property: "servers", before: current.servers ?? null, after: parsed.servers });
3258
+ }
3259
+ if (parsed.allowRemoteRequests !== void 0) {
3260
+ const next = String(parsed.allowRemoteRequests);
3261
+ if (current["allow-remote-requests"] !== next) {
3262
+ changes["allow-remote-requests"] = next;
3263
+ diff.push({ property: "allow-remote-requests", before: current["allow-remote-requests"] ?? null, after: next });
3264
+ }
3265
+ }
3266
+ if (parsed.maxUdpPacketSize !== void 0) {
3267
+ const next = String(parsed.maxUdpPacketSize);
3268
+ if (current["max-udp-packet-size"] !== next) {
3269
+ changes["max-udp-packet-size"] = next;
3270
+ diff.push({ property: "max-udp-packet-size", before: current["max-udp-packet-size"] ?? null, after: next });
3271
+ }
3272
+ }
3273
+ if (parsed.cacheMaxTtl !== void 0 && current["cache-max-ttl"] !== parsed.cacheMaxTtl) {
3274
+ changes["cache-max-ttl"] = parsed.cacheMaxTtl;
3275
+ diff.push({ property: "cache-max-ttl", before: current["cache-max-ttl"] ?? null, after: parsed.cacheMaxTtl });
3276
+ }
3277
+ if (parsed.cacheSize !== void 0) {
3278
+ const next = String(parsed.cacheSize);
3279
+ if (current["cache-size"] !== next) {
3280
+ changes["cache-size"] = next;
3281
+ diff.push({ property: "cache-size", before: current["cache-size"] ?? null, after: next });
3282
+ }
3283
+ }
3284
+ if (Object.keys(changes).length === 0) {
3285
+ return {
3286
+ content: "DNS settings already match requested values. No changes made.",
3287
+ structuredContent: { action: "no_change", routerId: context.routerId }
3288
+ };
3289
+ }
3290
+ if (parsed.dryRun) {
3291
+ return {
3292
+ content: `Dry run: Would update DNS settings on ${context.routerId}.`,
3293
+ structuredContent: { action: "dry_run", diff }
3294
+ };
3295
+ }
3296
+ await context.routerClient.update("ip/dns", id, changes);
3297
+ log12.info({ routerId: context.routerId, changes: Object.keys(changes) }, "DNS settings updated");
3298
+ return {
3299
+ content: `Updated DNS settings on ${context.routerId}.`,
3300
+ structuredContent: { action: "updated", routerId: context.routerId, diff }
3301
+ };
3302
+ } catch (err) {
3303
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_dns_settings" });
3304
+ }
3305
+ }
3306
+ };
3307
+ var dnsTools = [listDnsTool, manageDnsTool, getDnsSettingsTool, manageDnsSettingsTool];
3085
3308
 
3086
3309
  // src/domain/tools/mangle-tools.ts
3087
3310
  import { z as z14 } from "zod";
@@ -4689,7 +4912,71 @@ var uploadFileTool = {
4689
4912
  }
4690
4913
  }
4691
4914
  };
4692
- var filesTools = [listFilesTool, getFileContentTool, uploadFileTool];
4915
+ var deleteFileInputSchema = z21.object({
4916
+ routerId: z21.string().describe("Target router identifier from the router registry"),
4917
+ name: z21.string().describe("Exact file name on the router (e.g. flash/backup.backup)"),
4918
+ dryRun: z21.boolean().default(false).describe("Preview deletion without removing the file")
4919
+ }).strict();
4920
+ var deleteFileTool = {
4921
+ name: "delete_file",
4922
+ title: "Delete File",
4923
+ description: "Delete a file from the router filesystem by name. Idempotent: returns not_found gracefully if the file does not exist.",
4924
+ inputSchema: deleteFileInputSchema,
4925
+ annotations: {
4926
+ readOnlyHint: false,
4927
+ destructiveHint: true,
4928
+ idempotentHint: true,
4929
+ openWorldHint: false
4930
+ },
4931
+ async handler(params, context) {
4932
+ const parsed = deleteFileInputSchema.parse(params);
4933
+ log20.info({ routerId: context.routerId, name: parsed.name }, "Deleting file");
4934
+ try {
4935
+ const all = await context.routerClient.get(FILE_PATH, {
4936
+ limit: void 0,
4937
+ offset: void 0
4938
+ });
4939
+ const file = all.find((f) => f.name === parsed.name);
4940
+ if (!file) {
4941
+ return {
4942
+ content: `File "${parsed.name}" not found on ${context.routerId}. Nothing to delete.`,
4943
+ structuredContent: { action: "not_found", name: parsed.name, routerId: context.routerId }
4944
+ };
4945
+ }
4946
+ if (parsed.dryRun) {
4947
+ return {
4948
+ content: `Dry run: Would delete "${parsed.name}" from ${context.routerId}.`,
4949
+ structuredContent: {
4950
+ action: "dry_run",
4951
+ name: parsed.name,
4952
+ id: file[".id"],
4953
+ routerId: context.routerId
4954
+ }
4955
+ };
4956
+ }
4957
+ await context.routerClient.remove(FILE_PATH, file[".id"]);
4958
+ log20.info({ name: parsed.name, id: file[".id"] }, "File deleted");
4959
+ return {
4960
+ content: `Deleted "${parsed.name}" from ${context.routerId}.`,
4961
+ structuredContent: {
4962
+ action: "deleted",
4963
+ name: parsed.name,
4964
+ id: file[".id"],
4965
+ routerId: context.routerId
4966
+ }
4967
+ };
4968
+ } catch (err) {
4969
+ if (err instanceof MikroMCPError) throw err;
4970
+ throw enrichError(err, { routerId: context.routerId, tool: "delete_file" });
4971
+ }
4972
+ }
4973
+ };
4974
+ var filesTools = [
4975
+ listFilesTool,
4976
+ getFileContentTool,
4977
+ uploadFileTool,
4978
+ deleteFileTool
4979
+ ];
4693
4980
 
4694
4981
  // src/domain/tools/container-tools.ts
4695
4982
  import { z as z22 } from "zod";
@@ -5603,91 +5890,244 @@ var manageIpsecPeerTool = {
5603
5890
  }
5604
5891
  }
5605
5892
  };
5606
- var ipsecTools = [
5607
- listIpsecPeersTool,
5608
- listIpsecPoliciesTool,
5609
- manageIpsecPeerTool
5610
- ];
5611
-
5612
- // src/domain/tools/certificate-tools.ts
5613
- import { z as z25 } from "zod";
5614
- var log26 = createLogger("certificate-tools");
5615
- var listCertificatesInputSchema = z25.object({
5616
- routerId: z25.string().describe("Target router identifier from the router registry"),
5617
- name: z25.string().optional().describe("Filter by certificate name (substring match)"),
5618
- expired: z25.boolean().optional().describe("Filter by expiry status; omit to return all"),
5619
- limit: z25.number().int().min(1).max(500).default(100).describe("Maximum number of certificates to return")
5893
+ var manageIpsecPolicyInputSchema = z24.object({
5894
+ routerId: z24.string().describe("Target router identifier from the router registry"),
5895
+ action: z24.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
5896
+ srcAddress: z24.string().describe("Source CIDR \u2014 part of composite idempotency key"),
5897
+ dstAddress: z24.string().describe("Destination CIDR \u2014 part of composite idempotency key"),
5898
+ tunnel: z24.boolean().default(false).describe("Tunnel mode \u2014 part of composite idempotency key"),
5899
+ ipsecAction: z24.enum(["encrypt", "discard", "none"]).optional().describe("IPSec action (required for add)"),
5900
+ level: z24.enum(["require", "use", "unique"]).default("require").describe("SA level"),
5901
+ saSourceAddress: z24.string().optional().describe("SA source IP for tunnel mode"),
5902
+ saDstAddress: z24.string().optional().describe("SA destination IP for tunnel mode"),
5903
+ dryRun: z24.boolean().default(false).describe("Preview changes without applying")
5620
5904
  }).strict();
5621
- var listCertificatesTool = {
5622
- name: "list_certificates",
5623
- title: "List Certificates",
5624
- description: "List certificates on a MikroTik router.",
5625
- inputSchema: listCertificatesInputSchema,
5905
+ var manageIpsecPolicyTool = {
5906
+ name: "manage_ipsec_policy",
5907
+ title: "Manage IPSec Policy",
5908
+ description: "Add, remove, enable, or disable an IPSec policy. Idempotent by composite key (srcAddress + dstAddress + tunnel).",
5909
+ inputSchema: manageIpsecPolicyInputSchema,
5626
5910
  annotations: {
5627
- readOnlyHint: true,
5911
+ readOnlyHint: false,
5628
5912
  destructiveHint: false,
5629
5913
  idempotentHint: true,
5630
5914
  openWorldHint: false
5631
5915
  },
5916
+ snapshotPaths: ["ip/ipsec/policy"],
5632
5917
  async handler(params, context) {
5633
- const parsed = listCertificatesInputSchema.parse(params);
5634
- log26.info({ routerId: context.routerId }, "Listing certificates");
5918
+ const parsed = manageIpsecPolicyInputSchema.parse(params);
5919
+ log25.info({ routerId: context.routerId, action: parsed.action }, "Managing IPSec policy");
5635
5920
  try {
5636
- const allCerts = await context.routerClient.get("certificate", {
5921
+ const all = await context.routerClient.get("ip/ipsec/policy", {
5637
5922
  limit: void 0,
5638
5923
  offset: void 0
5639
5924
  });
5640
- const now = /* @__PURE__ */ new Date();
5641
- const filtered = allCerts.filter((c) => parsed.name ? (c.name ?? "").includes(parsed.name) : true).filter((c) => {
5642
- if (parsed.expired === void 0) return true;
5643
- const invalidAfter = c["invalid-after"];
5644
- if (!invalidAfter) {
5645
- return parsed.expired === true;
5925
+ const tunnelStr = String(parsed.tunnel);
5926
+ const existing = all.find(
5927
+ (p) => p["src-address"] === parsed.srcAddress && p["dst-address"] === parsed.dstAddress && p.tunnel === tunnelStr
5928
+ );
5929
+ if (parsed.action === "add") {
5930
+ if (existing) {
5931
+ return {
5932
+ content: `IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress} already exists. No changes made.`,
5933
+ structuredContent: { action: "already_exists", policy: existing }
5934
+ };
5646
5935
  }
5647
- const expiry = new Date(invalidAfter);
5648
- return parsed.expired ? expiry < now : expiry >= now;
5649
- });
5650
- const certs = filtered.slice(0, parsed.limit);
5651
- return {
5652
- content: `Certificates on ${context.routerId}: ${certs.length} returned (${allCerts.length} total)`,
5653
- structuredContent: {
5654
- routerId: context.routerId,
5655
- certificates: certs,
5656
- total: allCerts.length,
5657
- returned: certs.length
5936
+ if (!parsed.ipsecAction) {
5937
+ throw new MikroMCPError({
5938
+ category: "VALIDATION" /* VALIDATION */,
5939
+ code: "IPSEC_POLICY_ACTION_REQUIRED",
5940
+ message: "ipsecAction is required when adding an IPSec policy.",
5941
+ recoverability: {
5942
+ retryable: false,
5943
+ suggestedAction: "Provide ipsecAction: encrypt, discard, or none."
5944
+ }
5945
+ });
5658
5946
  }
5659
- };
5660
- } catch (err) {
5661
- throw enrichError(err, { routerId: context.routerId, tool: "list_certificates" });
5662
- }
5663
- }
5664
- };
5665
- var manageCertificateInputSchema = z25.object({
5666
- routerId: z25.string().describe("Target router identifier from the router registry"),
5667
- action: z25.enum(["remove", "trust", "untrust"]).describe("Action to perform"),
5668
- name: z25.string().describe("Certificate name \u2014 idempotency key"),
5669
- dryRun: z25.boolean().default(false).describe("Preview changes without applying")
5670
- }).strict();
5671
- var manageCertificateTool = {
5672
- name: "manage_certificate",
5673
- title: "Manage Certificate",
5674
- description: "Remove, trust, or untrust a certificate. Idempotent: trust/untrust return early if already in the target state.",
5675
- inputSchema: manageCertificateInputSchema,
5676
- annotations: {
5677
- readOnlyHint: false,
5678
- destructiveHint: true,
5679
- idempotentHint: false,
5680
- openWorldHint: false
5681
- },
5682
- snapshotPaths: ["certificate"],
5683
- async handler(params, context) {
5684
- const parsed = manageCertificateInputSchema.parse(params);
5685
- log26.info(
5686
- { routerId: context.routerId, action: parsed.action, name: parsed.name },
5687
- "Managing certificate"
5688
- );
5689
- try {
5690
- const allCerts = await context.routerClient.get("certificate");
5947
+ if (parsed.dryRun) {
5948
+ return {
5949
+ content: `Dry run: Would add IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress}.`,
5950
+ structuredContent: {
5951
+ action: "dry_run",
5952
+ diff: [
5953
+ { property: "src-address", before: null, after: parsed.srcAddress },
5954
+ { property: "dst-address", before: null, after: parsed.dstAddress },
5955
+ { property: "action", before: null, after: parsed.ipsecAction }
5956
+ ]
5957
+ }
5958
+ };
5959
+ }
5960
+ const body = {
5961
+ "src-address": parsed.srcAddress,
5962
+ "dst-address": parsed.dstAddress,
5963
+ tunnel: tunnelStr,
5964
+ action: parsed.ipsecAction,
5965
+ level: parsed.level
5966
+ };
5967
+ if (parsed.saSourceAddress) body["sa-src-address"] = parsed.saSourceAddress;
5968
+ if (parsed.saDstAddress) body["sa-dst-address"] = parsed.saDstAddress;
5969
+ const created = await context.routerClient.create("ip/ipsec/policy", body);
5970
+ log25.info({ id: created[".id"] }, "IPSec policy added");
5971
+ return {
5972
+ content: `Added IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress}.`,
5973
+ structuredContent: { action: "created", policy: created }
5974
+ };
5975
+ }
5976
+ if (parsed.action === "remove") {
5977
+ if (!existing) {
5978
+ return {
5979
+ content: `IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress} not found. Nothing to remove.`,
5980
+ structuredContent: {
5981
+ action: "not_found",
5982
+ srcAddress: parsed.srcAddress,
5983
+ dstAddress: parsed.dstAddress
5984
+ }
5985
+ };
5986
+ }
5987
+ if (parsed.dryRun) {
5988
+ return {
5989
+ content: `Dry run: Would remove IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress}.`,
5990
+ structuredContent: {
5991
+ action: "dry_run",
5992
+ diff: [{ property: "src-address", before: parsed.srcAddress, after: null }]
5993
+ }
5994
+ };
5995
+ }
5996
+ await context.routerClient.remove("ip/ipsec/policy", existing[".id"]);
5997
+ log25.info({ id: existing[".id"] }, "IPSec policy removed");
5998
+ return {
5999
+ content: `Removed IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress}.`,
6000
+ structuredContent: { action: "removed", id: existing[".id"] }
6001
+ };
6002
+ }
6003
+ if (!existing) {
6004
+ throw new MikroMCPError({
6005
+ category: "NOT_FOUND" /* NOT_FOUND */,
6006
+ code: "IPSEC_POLICY_NOT_FOUND",
6007
+ message: `IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress} not found.`,
6008
+ recoverability: {
6009
+ retryable: false,
6010
+ suggestedAction: "Verify with list_ipsec_policies.",
6011
+ alternativeTools: ["list_ipsec_policies"]
6012
+ }
6013
+ });
6014
+ }
6015
+ if (parsed.dryRun) {
6016
+ return {
6017
+ content: `Dry run: Would ${parsed.action} IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress}.`,
6018
+ structuredContent: {
6019
+ action: "dry_run",
6020
+ diff: [
6021
+ {
6022
+ property: "disabled",
6023
+ before: existing.disabled,
6024
+ after: parsed.action === "disable" ? "true" : "false"
6025
+ }
6026
+ ]
6027
+ }
6028
+ };
6029
+ }
6030
+ await context.routerClient.update("ip/ipsec/policy", existing[".id"], {
6031
+ disabled: parsed.action === "disable" ? "true" : "false"
6032
+ });
6033
+ const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
6034
+ log25.info({ id: existing[".id"], action: resultAction }, "IPSec policy updated");
6035
+ return {
6036
+ content: `IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress} ${resultAction}.`,
6037
+ structuredContent: { action: resultAction, id: existing[".id"] }
6038
+ };
6039
+ } catch (err) {
6040
+ if (err instanceof MikroMCPError) throw err;
6041
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ipsec_policy" });
6042
+ }
6043
+ }
6044
+ };
6045
+ var ipsecTools = [
6046
+ listIpsecPeersTool,
6047
+ listIpsecPoliciesTool,
6048
+ manageIpsecPeerTool,
6049
+ manageIpsecPolicyTool
6050
+ ];
6051
+
6052
+ // src/domain/tools/certificate-tools.ts
6053
+ import { z as z25 } from "zod";
6054
+ var log26 = createLogger("certificate-tools");
6055
+ var listCertificatesInputSchema = z25.object({
6056
+ routerId: z25.string().describe("Target router identifier from the router registry"),
6057
+ name: z25.string().optional().describe("Filter by certificate name (substring match)"),
6058
+ expired: z25.boolean().optional().describe("Filter by expiry status; omit to return all"),
6059
+ limit: z25.number().int().min(1).max(500).default(100).describe("Maximum number of certificates to return")
6060
+ }).strict();
6061
+ var listCertificatesTool = {
6062
+ name: "list_certificates",
6063
+ title: "List Certificates",
6064
+ description: "List certificates on a MikroTik router.",
6065
+ inputSchema: listCertificatesInputSchema,
6066
+ annotations: {
6067
+ readOnlyHint: true,
6068
+ destructiveHint: false,
6069
+ idempotentHint: true,
6070
+ openWorldHint: false
6071
+ },
6072
+ async handler(params, context) {
6073
+ const parsed = listCertificatesInputSchema.parse(params);
6074
+ log26.info({ routerId: context.routerId }, "Listing certificates");
6075
+ try {
6076
+ const allCerts = await context.routerClient.get("certificate", {
6077
+ limit: void 0,
6078
+ offset: void 0
6079
+ });
6080
+ const now = /* @__PURE__ */ new Date();
6081
+ const filtered = allCerts.filter((c) => parsed.name ? (c.name ?? "").includes(parsed.name) : true).filter((c) => {
6082
+ if (parsed.expired === void 0) return true;
6083
+ const invalidAfter = c["invalid-after"];
6084
+ if (!invalidAfter) {
6085
+ return parsed.expired === true;
6086
+ }
6087
+ const expiry = new Date(invalidAfter);
6088
+ return parsed.expired ? expiry < now : expiry >= now;
6089
+ });
6090
+ const certs = filtered.slice(0, parsed.limit);
6091
+ return {
6092
+ content: `Certificates on ${context.routerId}: ${certs.length} returned (${allCerts.length} total)`,
6093
+ structuredContent: {
6094
+ routerId: context.routerId,
6095
+ certificates: certs,
6096
+ total: allCerts.length,
6097
+ returned: certs.length
6098
+ }
6099
+ };
6100
+ } catch (err) {
6101
+ throw enrichError(err, { routerId: context.routerId, tool: "list_certificates" });
6102
+ }
6103
+ }
6104
+ };
6105
+ var manageCertificateInputSchema = z25.object({
6106
+ routerId: z25.string().describe("Target router identifier from the router registry"),
6107
+ action: z25.enum(["remove", "trust", "untrust"]).describe("Action to perform"),
6108
+ name: z25.string().describe("Certificate name \u2014 idempotency key"),
6109
+ dryRun: z25.boolean().default(false).describe("Preview changes without applying")
6110
+ }).strict();
6111
+ var manageCertificateTool = {
6112
+ name: "manage_certificate",
6113
+ title: "Manage Certificate",
6114
+ description: "Remove, trust, or untrust a certificate. Idempotent: trust/untrust return early if already in the target state.",
6115
+ inputSchema: manageCertificateInputSchema,
6116
+ annotations: {
6117
+ readOnlyHint: false,
6118
+ destructiveHint: true,
6119
+ idempotentHint: false,
6120
+ openWorldHint: false
6121
+ },
6122
+ snapshotPaths: ["certificate"],
6123
+ async handler(params, context) {
6124
+ const parsed = manageCertificateInputSchema.parse(params);
6125
+ log26.info(
6126
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
6127
+ "Managing certificate"
6128
+ );
6129
+ try {
6130
+ const allCerts = await context.routerClient.get("certificate");
5691
6131
  const cert = allCerts.find((c) => c.name === parsed.name);
5692
6132
  if (!cert) {
5693
6133
  throw new MikroMCPError({
@@ -6057,20 +6497,18 @@ var manageUserTool = {
6057
6497
  };
6058
6498
  var userTools = [listUsersTool, manageUserTool];
6059
6499
 
6060
- // src/domain/tools/dhcp-server-tools.ts
6500
+ // src/domain/tools/user-group-tools.ts
6061
6501
  import { z as z27 } from "zod";
6062
- var log28 = createLogger("dhcp-server-tools");
6063
- var listDhcpServersInputSchema = z27.object({
6502
+ var log28 = createLogger("user-group-tools");
6503
+ var listUserGroupsInputSchema = z27.object({
6064
6504
  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")
6505
+ limit: z27.number().int().min(1).max(500).default(100).describe("Maximum number of groups to return")
6068
6506
  }).strict();
6069
- var listDhcpServersTool = {
6070
- name: "list_dhcp_servers",
6071
- title: "List DHCP Servers",
6072
- description: "List DHCP servers on a MikroTik router.",
6073
- inputSchema: listDhcpServersInputSchema,
6507
+ var listUserGroupsTool = {
6508
+ name: "list_user_groups",
6509
+ title: "List User Groups",
6510
+ description: "List local user groups on a MikroTik router.",
6511
+ inputSchema: listUserGroupsInputSchema,
6074
6512
  annotations: {
6075
6513
  readOnlyHint: true,
6076
6514
  destructiveHint: false,
@@ -6078,130 +6516,342 @@ var listDhcpServersTool = {
6078
6516
  openWorldHint: false
6079
6517
  },
6080
6518
  async handler(params, context) {
6081
- const parsed = listDhcpServersInputSchema.parse(params);
6082
- log28.info({ routerId: context.routerId }, "Listing DHCP servers");
6519
+ const parsed = listUserGroupsInputSchema.parse(params);
6520
+ log28.info({ routerId: context.routerId }, "Listing user groups");
6083
6521
  try {
6084
- const allServers = await context.routerClient.get("ip/dhcp-server", {
6522
+ const all = await context.routerClient.get("user/group", {
6085
6523
  limit: void 0,
6086
6524
  offset: void 0
6087
6525
  });
6088
- let filtered = parsed.interface ? allServers.filter((s) => s.interface === parsed.interface) : allServers;
6089
- const total = filtered.length;
6090
- const servers = filtered.slice(parsed.offset, parsed.offset + parsed.limit);
6091
- const hasMore = parsed.offset + parsed.limit < total;
6526
+ const groups = all.slice(0, parsed.limit);
6092
6527
  return {
6093
- content: `DHCP servers on ${context.routerId}: ${total} total, showing ${servers.length} (offset ${parsed.offset})`,
6528
+ content: `User groups on ${context.routerId}: ${groups.length} returned (${all.length} total)`,
6094
6529
  structuredContent: {
6095
6530
  routerId: context.routerId,
6096
- servers,
6097
- total,
6098
- hasMore,
6099
- offset: parsed.offset,
6100
- limit: parsed.limit
6531
+ groups,
6532
+ total: all.length,
6533
+ returned: groups.length
6101
6534
  }
6102
6535
  };
6103
6536
  } catch (err) {
6104
- throw enrichError(err, { routerId: context.routerId, tool: "list_dhcp_servers" });
6537
+ throw enrichError(err, { routerId: context.routerId, tool: "list_user_groups" });
6105
6538
  }
6106
6539
  }
6107
6540
  };
6108
- var manageDhcpServerInputSchema = z27.object({
6541
+ var manageUserGroupInputSchema = z27.object({
6109
6542
  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"),
6543
+ action: z27.enum(["add", "update", "remove"]).describe("Action to perform"),
6544
+ name: z27.string().describe("Group name \u2014 idempotency key"),
6545
+ policy: z27.string().optional().describe("Comma-separated policy list (e.g. 'read,write,ftp')"),
6546
+ skin: z27.string().optional().describe("Optional skin name for the group"),
6116
6547
  dryRun: z27.boolean().default(false).describe("Preview changes without applying")
6117
6548
  }).strict();
6118
- var manageDhcpServerTool = {
6119
- name: "manage_dhcp_server",
6120
- title: "Manage DHCP Server",
6121
- description: "Add, remove, enable, or disable a DHCP server. Idempotent by name: add returns already_exists if a server with the same name, interface, and address pool already exists.",
6122
- inputSchema: manageDhcpServerInputSchema,
6549
+ var manageUserGroupTool = {
6550
+ name: "manage_user_group",
6551
+ title: "Manage User Group",
6552
+ 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.",
6553
+ inputSchema: manageUserGroupInputSchema,
6123
6554
  annotations: {
6124
6555
  readOnlyHint: false,
6125
6556
  destructiveHint: false,
6126
6557
  idempotentHint: true,
6127
6558
  openWorldHint: false
6128
6559
  },
6129
- snapshotPaths: ["ip/dhcp-server"],
6560
+ snapshotPaths: ["user/group"],
6130
6561
  async handler(params, context) {
6131
- const parsed = manageDhcpServerInputSchema.parse(params);
6562
+ const parsed = manageUserGroupInputSchema.parse(params);
6132
6563
  log28.info(
6133
6564
  { routerId: context.routerId, action: parsed.action, name: parsed.name },
6134
- "Managing DHCP server"
6565
+ "Managing user group"
6135
6566
  );
6136
6567
  try {
6137
- const allServers = await context.routerClient.get("ip/dhcp-server", {
6138
- limit: void 0,
6139
- offset: void 0
6140
- });
6141
- const existing = allServers.find(
6142
- (s) => s.name === parsed.name
6143
- );
6144
6568
  if (parsed.action === "add") {
6145
- if (!parsed.interface) {
6569
+ const all2 = await context.routerClient.get("user/group", {
6570
+ limit: void 0,
6571
+ offset: void 0
6572
+ });
6573
+ const existing2 = all2.find((g) => g.name === parsed.name);
6574
+ if (existing2) {
6575
+ if (parsed.policy === void 0 || existing2.policy === parsed.policy) {
6576
+ return {
6577
+ content: `User group "${parsed.name}" already exists. No changes made.`,
6578
+ structuredContent: { action: "already_exists", name: parsed.name, policy: existing2.policy }
6579
+ };
6580
+ }
6146
6581
  throw new MikroMCPError({
6147
- category: "VALIDATION" /* VALIDATION */,
6148
- code: "DHCP_SERVER_INTERFACE_REQUIRED",
6149
- message: "interface is required when adding a DHCP server.",
6150
- details: { name: parsed.name },
6582
+ category: "CONFLICT" /* CONFLICT */,
6583
+ code: "USER_GROUP_CONFLICT",
6584
+ message: `Group "${parsed.name}" exists with a different policy.`,
6585
+ details: { existingPolicy: existing2.policy, requestedPolicy: parsed.policy },
6151
6586
  recoverability: {
6152
6587
  retryable: false,
6153
- suggestedAction: "Provide the interface name to serve DHCP on."
6588
+ suggestedAction: "Remove the existing group first or use a different name.",
6589
+ alternativeTools: ["manage_user_group"]
6154
6590
  }
6155
6591
  });
6156
6592
  }
6157
- if (!parsed.addressPool) {
6158
- throw new MikroMCPError({
6159
- category: "VALIDATION" /* VALIDATION */,
6160
- code: "DHCP_SERVER_POOL_REQUIRED",
6161
- message: "addressPool is required when adding a DHCP server.",
6162
- details: { name: parsed.name },
6163
- recoverability: {
6164
- retryable: false,
6165
- suggestedAction: "Provide the address pool name."
6593
+ if (parsed.dryRun) {
6594
+ return {
6595
+ content: `Dry run: Would add group "${parsed.name}".`,
6596
+ structuredContent: {
6597
+ action: "dry_run",
6598
+ diff: [
6599
+ { property: "name", before: null, after: parsed.name },
6600
+ { property: "policy", before: null, after: parsed.policy ?? "" }
6601
+ ]
6166
6602
  }
6167
- });
6603
+ };
6168
6604
  }
6169
- if (existing) {
6170
- if (existing.interface === parsed.interface && existing["address-pool"] === parsed.addressPool) {
6171
- return {
6172
- content: `DHCP server "${parsed.name}" already exists with the same interface and pool. No changes made.`,
6173
- structuredContent: { action: "already_exists", server: existing }
6174
- };
6605
+ const body = { name: parsed.name };
6606
+ if (parsed.policy !== void 0) body.policy = parsed.policy;
6607
+ if (parsed.skin !== void 0) body.skin = parsed.skin;
6608
+ const created = await context.routerClient.create("user/group", body);
6609
+ log28.info({ name: parsed.name, id: created[".id"] }, "User group added");
6610
+ return {
6611
+ content: `Added group "${parsed.name}".`,
6612
+ structuredContent: {
6613
+ action: "created",
6614
+ routerId: context.routerId,
6615
+ name: parsed.name,
6616
+ id: created[".id"]
6175
6617
  }
6618
+ };
6619
+ }
6620
+ if (parsed.action === "update") {
6621
+ const all2 = await context.routerClient.get("user/group", {
6622
+ limit: void 0,
6623
+ offset: void 0
6624
+ });
6625
+ const existing2 = all2.find((g) => g.name === parsed.name);
6626
+ if (!existing2) {
6176
6627
  throw new MikroMCPError({
6177
- category: "CONFLICT" /* CONFLICT */,
6178
- code: "DHCP_SERVER_CONFLICT",
6179
- message: `DHCP server "${parsed.name}" exists with different configuration.`,
6180
- details: {
6181
- existing: {
6182
- interface: existing.interface,
6183
- addressPool: existing["address-pool"]
6184
- },
6185
- requested: {
6186
- interface: parsed.interface,
6187
- addressPool: parsed.addressPool
6188
- }
6189
- },
6628
+ category: "NOT_FOUND" /* NOT_FOUND */,
6629
+ code: "USER_GROUP_NOT_FOUND",
6630
+ message: `Group "${parsed.name}" not found.`,
6631
+ details: { name: parsed.name },
6190
6632
  recoverability: {
6191
6633
  retryable: false,
6192
- suggestedAction: "Remove the existing server first or use a different name.",
6193
- alternativeTools: ["manage_dhcp_server"]
6634
+ suggestedAction: "Verify the group name with list_user_groups.",
6635
+ alternativeTools: ["list_user_groups"]
6194
6636
  }
6195
6637
  });
6196
6638
  }
6197
- if (parsed.dryRun) {
6198
- const diff = [
6199
- { property: "name", before: null, after: parsed.name },
6200
- { property: "interface", before: null, after: parsed.interface },
6201
- { property: "address-pool", before: null, after: parsed.addressPool },
6202
- ...parsed.leaseTime ? [{ property: "lease-time", before: null, after: parsed.leaseTime }] : [],
6203
- ...parsed.comment ? [{ property: "comment", before: null, after: parsed.comment }] : []
6204
- ];
6639
+ if (parsed.policy !== void 0 && existing2.policy === parsed.policy) {
6640
+ return {
6641
+ content: `Group "${parsed.name}" already has the requested policy. No changes made.`,
6642
+ structuredContent: { action: "no_change", name: parsed.name }
6643
+ };
6644
+ }
6645
+ if (parsed.dryRun) {
6646
+ return {
6647
+ content: `Dry run: Would update group "${parsed.name}".`,
6648
+ structuredContent: {
6649
+ action: "dry_run",
6650
+ diff: [
6651
+ {
6652
+ property: "policy",
6653
+ before: existing2.policy ?? null,
6654
+ after: parsed.policy ?? null
6655
+ }
6656
+ ]
6657
+ }
6658
+ };
6659
+ }
6660
+ const changes = {};
6661
+ if (parsed.policy !== void 0) changes.policy = parsed.policy;
6662
+ if (parsed.skin !== void 0) changes.skin = parsed.skin;
6663
+ if (Object.keys(changes).length === 0) {
6664
+ return {
6665
+ content: `User group "${parsed.name}" already has the requested configuration. No changes made.`,
6666
+ structuredContent: { action: "no_change", name: parsed.name }
6667
+ };
6668
+ }
6669
+ await context.routerClient.update("user/group", existing2[".id"], changes);
6670
+ log28.info({ name: parsed.name }, "User group updated");
6671
+ return {
6672
+ content: `Updated group "${parsed.name}".`,
6673
+ structuredContent: { action: "updated", routerId: context.routerId, name: parsed.name }
6674
+ };
6675
+ }
6676
+ const all = await context.routerClient.get("user/group", {
6677
+ limit: void 0,
6678
+ offset: void 0
6679
+ });
6680
+ const existing = all.find((g) => g.name === parsed.name);
6681
+ if (!existing) {
6682
+ return {
6683
+ content: `Group "${parsed.name}" not found. Nothing to remove.`,
6684
+ structuredContent: { action: "not_found", name: parsed.name }
6685
+ };
6686
+ }
6687
+ if (parsed.dryRun) {
6688
+ return {
6689
+ content: `Dry run: Would remove group "${parsed.name}".`,
6690
+ structuredContent: {
6691
+ action: "dry_run",
6692
+ diff: [{ property: "name", before: parsed.name, after: null }]
6693
+ }
6694
+ };
6695
+ }
6696
+ await context.routerClient.remove("user/group", existing[".id"]);
6697
+ log28.info({ name: parsed.name }, "User group removed");
6698
+ return {
6699
+ content: `Removed group "${parsed.name}".`,
6700
+ structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
6701
+ };
6702
+ } catch (err) {
6703
+ if (err instanceof MikroMCPError) throw err;
6704
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_user_group" });
6705
+ }
6706
+ }
6707
+ };
6708
+ var userGroupTools = [listUserGroupsTool, manageUserGroupTool];
6709
+
6710
+ // src/domain/tools/dhcp-server-tools.ts
6711
+ import { z as z28 } from "zod";
6712
+ var log29 = createLogger("dhcp-server-tools");
6713
+ var listDhcpServersInputSchema = z28.object({
6714
+ routerId: z28.string().describe("Target router identifier from the router registry"),
6715
+ interface: z28.string().optional().describe("Filter by interface name (exact match)"),
6716
+ limit: z28.number().int().min(1).max(500).default(100).describe("Maximum number of DHCP servers to return"),
6717
+ offset: z28.number().int().min(0).default(0).describe("Offset for pagination")
6718
+ }).strict();
6719
+ var listDhcpServersTool = {
6720
+ name: "list_dhcp_servers",
6721
+ title: "List DHCP Servers",
6722
+ description: "List DHCP servers on a MikroTik router.",
6723
+ inputSchema: listDhcpServersInputSchema,
6724
+ annotations: {
6725
+ readOnlyHint: true,
6726
+ destructiveHint: false,
6727
+ idempotentHint: true,
6728
+ openWorldHint: false
6729
+ },
6730
+ async handler(params, context) {
6731
+ const parsed = listDhcpServersInputSchema.parse(params);
6732
+ log29.info({ routerId: context.routerId }, "Listing DHCP servers");
6733
+ try {
6734
+ const allServers = await context.routerClient.get("ip/dhcp-server", {
6735
+ limit: void 0,
6736
+ offset: void 0
6737
+ });
6738
+ let filtered = parsed.interface ? allServers.filter((s) => s.interface === parsed.interface) : allServers;
6739
+ const total = filtered.length;
6740
+ const servers = filtered.slice(parsed.offset, parsed.offset + parsed.limit);
6741
+ const hasMore = parsed.offset + parsed.limit < total;
6742
+ return {
6743
+ content: `DHCP servers on ${context.routerId}: ${total} total, showing ${servers.length} (offset ${parsed.offset})`,
6744
+ structuredContent: {
6745
+ routerId: context.routerId,
6746
+ servers,
6747
+ total,
6748
+ hasMore,
6749
+ offset: parsed.offset,
6750
+ limit: parsed.limit
6751
+ }
6752
+ };
6753
+ } catch (err) {
6754
+ throw enrichError(err, { routerId: context.routerId, tool: "list_dhcp_servers" });
6755
+ }
6756
+ }
6757
+ };
6758
+ var manageDhcpServerInputSchema = z28.object({
6759
+ routerId: z28.string().describe("Target router identifier from the router registry"),
6760
+ action: z28.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
6761
+ name: z28.string().describe("Server name \u2014 idempotency key"),
6762
+ interface: z28.string().optional().describe("Interface to serve DHCP on (required for add)"),
6763
+ addressPool: z28.string().optional().describe("IP pool name (required for add)"),
6764
+ leaseTime: z28.string().optional().describe("Lease duration (e.g. '1d', '12h')"),
6765
+ comment: z28.string().optional().describe("Optional comment"),
6766
+ dryRun: z28.boolean().default(false).describe("Preview changes without applying")
6767
+ }).strict();
6768
+ var manageDhcpServerTool = {
6769
+ name: "manage_dhcp_server",
6770
+ title: "Manage DHCP Server",
6771
+ description: "Add, remove, enable, or disable a DHCP server. Idempotent by name: add returns already_exists if a server with the same name, interface, and address pool already exists.",
6772
+ inputSchema: manageDhcpServerInputSchema,
6773
+ annotations: {
6774
+ readOnlyHint: false,
6775
+ destructiveHint: false,
6776
+ idempotentHint: true,
6777
+ openWorldHint: false
6778
+ },
6779
+ snapshotPaths: ["ip/dhcp-server"],
6780
+ async handler(params, context) {
6781
+ const parsed = manageDhcpServerInputSchema.parse(params);
6782
+ log29.info(
6783
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
6784
+ "Managing DHCP server"
6785
+ );
6786
+ try {
6787
+ const allServers = await context.routerClient.get("ip/dhcp-server", {
6788
+ limit: void 0,
6789
+ offset: void 0
6790
+ });
6791
+ const existing = allServers.find(
6792
+ (s) => s.name === parsed.name
6793
+ );
6794
+ if (parsed.action === "add") {
6795
+ if (!parsed.interface) {
6796
+ throw new MikroMCPError({
6797
+ category: "VALIDATION" /* VALIDATION */,
6798
+ code: "DHCP_SERVER_INTERFACE_REQUIRED",
6799
+ message: "interface is required when adding a DHCP server.",
6800
+ details: { name: parsed.name },
6801
+ recoverability: {
6802
+ retryable: false,
6803
+ suggestedAction: "Provide the interface name to serve DHCP on."
6804
+ }
6805
+ });
6806
+ }
6807
+ if (!parsed.addressPool) {
6808
+ throw new MikroMCPError({
6809
+ category: "VALIDATION" /* VALIDATION */,
6810
+ code: "DHCP_SERVER_POOL_REQUIRED",
6811
+ message: "addressPool is required when adding a DHCP server.",
6812
+ details: { name: parsed.name },
6813
+ recoverability: {
6814
+ retryable: false,
6815
+ suggestedAction: "Provide the address pool name."
6816
+ }
6817
+ });
6818
+ }
6819
+ if (existing) {
6820
+ if (existing.interface === parsed.interface && existing["address-pool"] === parsed.addressPool) {
6821
+ return {
6822
+ content: `DHCP server "${parsed.name}" already exists with the same interface and pool. No changes made.`,
6823
+ structuredContent: { action: "already_exists", server: existing }
6824
+ };
6825
+ }
6826
+ throw new MikroMCPError({
6827
+ category: "CONFLICT" /* CONFLICT */,
6828
+ code: "DHCP_SERVER_CONFLICT",
6829
+ message: `DHCP server "${parsed.name}" exists with different configuration.`,
6830
+ details: {
6831
+ existing: {
6832
+ interface: existing.interface,
6833
+ addressPool: existing["address-pool"]
6834
+ },
6835
+ requested: {
6836
+ interface: parsed.interface,
6837
+ addressPool: parsed.addressPool
6838
+ }
6839
+ },
6840
+ recoverability: {
6841
+ retryable: false,
6842
+ suggestedAction: "Remove the existing server first or use a different name.",
6843
+ alternativeTools: ["manage_dhcp_server"]
6844
+ }
6845
+ });
6846
+ }
6847
+ if (parsed.dryRun) {
6848
+ const diff = [
6849
+ { property: "name", before: null, after: parsed.name },
6850
+ { property: "interface", before: null, after: parsed.interface },
6851
+ { property: "address-pool", before: null, after: parsed.addressPool },
6852
+ ...parsed.leaseTime ? [{ property: "lease-time", before: null, after: parsed.leaseTime }] : [],
6853
+ ...parsed.comment ? [{ property: "comment", before: null, after: parsed.comment }] : []
6854
+ ];
6205
6855
  return {
6206
6856
  content: `Dry run: Would add DHCP server "${parsed.name}".`,
6207
6857
  structuredContent: { action: "dry_run", diff }
@@ -6215,7 +6865,7 @@ var manageDhcpServerTool = {
6215
6865
  if (parsed.leaseTime) body["lease-time"] = parsed.leaseTime;
6216
6866
  if (parsed.comment) body.comment = parsed.comment;
6217
6867
  const created = await context.routerClient.create("ip/dhcp-server", body);
6218
- log28.info({ name: parsed.name, id: created[".id"] }, "DHCP server added");
6868
+ log29.info({ name: parsed.name, id: created[".id"] }, "DHCP server added");
6219
6869
  return {
6220
6870
  content: `Added DHCP server "${parsed.name}".`,
6221
6871
  structuredContent: { action: "created", server: created }
@@ -6238,7 +6888,7 @@ var manageDhcpServerTool = {
6238
6888
  };
6239
6889
  }
6240
6890
  await context.routerClient.remove("ip/dhcp-server", existing[".id"]);
6241
- log28.info({ name: parsed.name }, "DHCP server removed");
6891
+ log29.info({ name: parsed.name }, "DHCP server removed");
6242
6892
  return {
6243
6893
  content: `Removed DHCP server "${parsed.name}".`,
6244
6894
  structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
@@ -6277,7 +6927,7 @@ var manageDhcpServerTool = {
6277
6927
  disabled: disabledValue
6278
6928
  });
6279
6929
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
6280
- log28.info({ name: parsed.name, action: resultAction }, "DHCP server updated");
6930
+ log29.info({ name: parsed.name, action: resultAction }, "DHCP server updated");
6281
6931
  return {
6282
6932
  content: `DHCP server "${parsed.name}" ${resultAction}.`,
6283
6933
  structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
@@ -6291,13 +6941,13 @@ var manageDhcpServerTool = {
6291
6941
  var dhcpServerTools = [listDhcpServersTool, manageDhcpServerTool];
6292
6942
 
6293
6943
  // 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")
6944
+ import { z as z29 } from "zod";
6945
+ var log30 = createLogger("ip-pool-tools");
6946
+ var listIpPoolsInputSchema = z29.object({
6947
+ routerId: z29.string().describe("Target router identifier from the router registry"),
6948
+ name: z29.string().optional().describe("Filter by pool name (substring match)"),
6949
+ limit: z29.number().int().min(1).max(500).default(100).describe("Maximum number of pools to return"),
6950
+ offset: z29.number().int().min(0).default(0).describe("Offset for pagination")
6301
6951
  }).strict();
6302
6952
  var listIpPoolsTool = {
6303
6953
  name: "list_ip_pools",
@@ -6312,7 +6962,7 @@ var listIpPoolsTool = {
6312
6962
  },
6313
6963
  async handler(params, context) {
6314
6964
  const parsed = listIpPoolsInputSchema.parse(params);
6315
- log29.info({ routerId: context.routerId }, "Listing IP pools");
6965
+ log30.info({ routerId: context.routerId }, "Listing IP pools");
6316
6966
  try {
6317
6967
  const allPools = await context.routerClient.get("ip/pool", {
6318
6968
  limit: void 0,
@@ -6341,13 +6991,13 @@ var listIpPoolsTool = {
6341
6991
  }
6342
6992
  }
6343
6993
  };
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")
6994
+ var manageIpPoolInputSchema = z29.object({
6995
+ routerId: z29.string().describe("Target router identifier from the router registry"),
6996
+ action: z29.enum(["add", "remove"]).describe("Action to perform"),
6997
+ name: z29.string().describe("Pool name \u2014 idempotency key"),
6998
+ ranges: z29.string().optional().describe("IP range (e.g. '192.168.1.100-192.168.1.200'; required for add)"),
6999
+ nextPool: z29.string().optional().describe("Next pool name for overflow"),
7000
+ dryRun: z29.boolean().default(false).describe("Preview changes without applying")
6351
7001
  }).strict();
6352
7002
  var manageIpPoolTool = {
6353
7003
  name: "manage_ip_pool",
@@ -6363,7 +7013,7 @@ var manageIpPoolTool = {
6363
7013
  snapshotPaths: ["ip/pool"],
6364
7014
  async handler(params, context) {
6365
7015
  const parsed = manageIpPoolInputSchema.parse(params);
6366
- log29.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP pool");
7016
+ log30.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP pool");
6367
7017
  try {
6368
7018
  const allPools = await context.routerClient.get("ip/pool", {
6369
7019
  limit: void 0,
@@ -6416,7 +7066,7 @@ var manageIpPoolTool = {
6416
7066
  const body = { name: parsed.name, ranges: parsed.ranges };
6417
7067
  if (parsed.nextPool) body["next-pool"] = parsed.nextPool;
6418
7068
  const created = await context.routerClient.create("ip/pool", body);
6419
- log29.info({ name: parsed.name, id: created[".id"] }, "IP pool added");
7069
+ log30.info({ name: parsed.name, id: created[".id"] }, "IP pool added");
6420
7070
  return {
6421
7071
  content: `Added IP pool "${parsed.name}" with ranges ${parsed.ranges}.`,
6422
7072
  structuredContent: { action: "created", pool: created }
@@ -6435,7 +7085,7 @@ var manageIpPoolTool = {
6435
7085
  };
6436
7086
  }
6437
7087
  await context.routerClient.remove("ip/pool", existing[".id"]);
6438
- log29.info({ name: parsed.name }, "IP pool removed");
7088
+ log30.info({ name: parsed.name }, "IP pool removed");
6439
7089
  return {
6440
7090
  content: `Removed IP pool "${parsed.name}".`,
6441
7091
  structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
@@ -6449,12 +7099,12 @@ var manageIpPoolTool = {
6449
7099
  var ipPoolTools = [listIpPoolsTool, manageIpPoolTool];
6450
7100
 
6451
7101
  // 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")
7102
+ import { z as z30 } from "zod";
7103
+ var log31 = createLogger("queue-tools");
7104
+ var listQueuesInputSchema = z30.object({
7105
+ routerId: z30.string().describe("Target router identifier from the router registry"),
7106
+ target: z30.string().optional().describe("Filter by target address (substring match)"),
7107
+ limit: z30.number().int().min(1).max(500).default(100).describe("Maximum number of queues to return")
6458
7108
  }).strict();
6459
7109
  var listQueuesTool = {
6460
7110
  name: "list_queues",
@@ -6469,7 +7119,7 @@ var listQueuesTool = {
6469
7119
  },
6470
7120
  async handler(params, context) {
6471
7121
  const parsed = listQueuesInputSchema.parse(params);
6472
- log30.info({ routerId: context.routerId }, "Listing queues");
7122
+ log31.info({ routerId: context.routerId }, "Listing queues");
6473
7123
  try {
6474
7124
  const allQueues = await context.routerClient.get("queue/simple", {
6475
7125
  limit: void 0,
@@ -6493,15 +7143,15 @@ var listQueuesTool = {
6493
7143
  }
6494
7144
  }
6495
7145
  };
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")
7146
+ var manageQueueInputSchema = z30.object({
7147
+ routerId: z30.string().describe("Target router identifier from the router registry"),
7148
+ action: z30.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
7149
+ name: z30.string().describe("Queue name \u2014 idempotency key"),
7150
+ target: z30.string().optional().describe("Target address (required for add; e.g. '192.168.1.0/24')"),
7151
+ maxLimit: z30.string().optional().describe("Max upload/download limit (e.g. '10M/10M')"),
7152
+ limitAt: z30.string().optional().describe("Guaranteed rate (e.g. '1M/1M')"),
7153
+ comment: z30.string().optional().describe("Optional comment"),
7154
+ dryRun: z30.boolean().default(false).describe("Preview changes without applying")
6505
7155
  }).strict();
6506
7156
  var manageQueueTool = {
6507
7157
  name: "manage_queue",
@@ -6517,7 +7167,7 @@ var manageQueueTool = {
6517
7167
  snapshotPaths: ["queue/simple"],
6518
7168
  async handler(params, context) {
6519
7169
  const parsed = manageQueueInputSchema.parse(params);
6520
- log30.info(
7170
+ log31.info(
6521
7171
  { routerId: context.routerId, action: parsed.action, name: parsed.name },
6522
7172
  "Managing queue"
6523
7173
  );
@@ -6579,7 +7229,7 @@ var manageQueueTool = {
6579
7229
  if (parsed.limitAt) body["limit-at"] = parsed.limitAt;
6580
7230
  if (parsed.comment) body.comment = parsed.comment;
6581
7231
  const created = await context.routerClient.create("queue/simple", body);
6582
- log30.info({ name: parsed.name, id: created[".id"] }, "Queue added");
7232
+ log31.info({ name: parsed.name, id: created[".id"] }, "Queue added");
6583
7233
  return {
6584
7234
  content: `Added queue "${parsed.name}".`,
6585
7235
  structuredContent: { action: "created", queue: created }
@@ -6602,7 +7252,7 @@ var manageQueueTool = {
6602
7252
  };
6603
7253
  }
6604
7254
  await context.routerClient.remove("queue/simple", existing[".id"]);
6605
- log30.info({ name: parsed.name }, "Queue removed");
7255
+ log31.info({ name: parsed.name }, "Queue removed");
6606
7256
  return {
6607
7257
  content: `Removed queue "${parsed.name}".`,
6608
7258
  structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
@@ -6641,7 +7291,7 @@ var manageQueueTool = {
6641
7291
  disabled: disabledValue
6642
7292
  });
6643
7293
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
6644
- log30.info({ name: parsed.name, action: resultAction }, "Queue updated");
7294
+ log31.info({ name: parsed.name, action: resultAction }, "Queue updated");
6645
7295
  return {
6646
7296
  content: `Queue "${parsed.name}" ${resultAction}.`,
6647
7297
  structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
@@ -6655,12 +7305,12 @@ var manageQueueTool = {
6655
7305
  var queueTools = [listQueuesTool, manageQueueTool];
6656
7306
 
6657
7307
  // 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")
7308
+ import { z as z31 } from "zod";
7309
+ var log32 = createLogger("vrrp-tools");
7310
+ var listVrrpInstancesInputSchema = z31.object({
7311
+ routerId: z31.string().describe("Target router identifier from the router registry"),
7312
+ interface: z31.string().optional().describe("Filter by master interface name (exact match)"),
7313
+ limit: z31.number().int().min(1).max(500).default(100).describe("Maximum number of instances to return")
6664
7314
  }).strict();
6665
7315
  var listVrrpInstancesTool = {
6666
7316
  name: "list_vrrp_instances",
@@ -6675,7 +7325,7 @@ var listVrrpInstancesTool = {
6675
7325
  },
6676
7326
  async handler(params, context) {
6677
7327
  const parsed = listVrrpInstancesInputSchema.parse(params);
6678
- log31.info({ routerId: context.routerId }, "Listing VRRP instances");
7328
+ log32.info({ routerId: context.routerId }, "Listing VRRP instances");
6679
7329
  try {
6680
7330
  const allInstances = await context.routerClient.get("interface/vrrp", {
6681
7331
  limit: void 0,
@@ -6699,17 +7349,17 @@ var listVrrpInstancesTool = {
6699
7349
  }
6700
7350
  }
6701
7351
  };
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")
7352
+ var manageVrrpInstanceInputSchema = z31.object({
7353
+ routerId: z31.string().describe("Target router identifier from the router registry"),
7354
+ action: z31.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
7355
+ name: z31.string().describe("VRRP interface name \u2014 idempotency key"),
7356
+ interface: z31.string().optional().describe("Master interface (required for add)"),
7357
+ vrid: z31.number().int().min(1).max(255).optional().describe("Virtual router ID (required for add)"),
7358
+ priority: z31.number().int().min(1).max(254).default(100).describe("Router priority (1\u2013254)"),
7359
+ interval: z31.number().int().min(1).optional().describe("Advertisement interval in seconds"),
7360
+ version: z31.enum(["2", "3"]).default("3").describe("VRRP protocol version"),
7361
+ comment: z31.string().optional().describe("Optional comment"),
7362
+ dryRun: z31.boolean().default(false).describe("Preview changes without applying")
6713
7363
  }).strict();
6714
7364
  var manageVrrpInstanceTool = {
6715
7365
  name: "manage_vrrp_instance",
@@ -6725,7 +7375,7 @@ var manageVrrpInstanceTool = {
6725
7375
  snapshotPaths: ["interface/vrrp"],
6726
7376
  async handler(params, context) {
6727
7377
  const parsed = manageVrrpInstanceInputSchema.parse(params);
6728
- log31.info(
7378
+ log32.info(
6729
7379
  { routerId: context.routerId, action: parsed.action, name: parsed.name },
6730
7380
  "Managing VRRP instance"
6731
7381
  );
@@ -6809,7 +7459,7 @@ var manageVrrpInstanceTool = {
6809
7459
  if (parsed.interval) body.interval = String(parsed.interval);
6810
7460
  if (parsed.comment) body.comment = parsed.comment;
6811
7461
  const created = await context.routerClient.create("interface/vrrp", body);
6812
- log31.info({ name: parsed.name, id: created[".id"] }, "VRRP instance added");
7462
+ log32.info({ name: parsed.name, id: created[".id"] }, "VRRP instance added");
6813
7463
  return {
6814
7464
  content: `Added VRRP instance "${parsed.name}".`,
6815
7465
  structuredContent: { action: "created", instance: created }
@@ -6832,7 +7482,7 @@ var manageVrrpInstanceTool = {
6832
7482
  };
6833
7483
  }
6834
7484
  await context.routerClient.remove("interface/vrrp", existing[".id"]);
6835
- log31.info({ name: parsed.name }, "VRRP instance removed");
7485
+ log32.info({ name: parsed.name }, "VRRP instance removed");
6836
7486
  return {
6837
7487
  content: `Removed VRRP instance "${parsed.name}".`,
6838
7488
  structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
@@ -6871,7 +7521,7 @@ var manageVrrpInstanceTool = {
6871
7521
  disabled: disabledValue
6872
7522
  });
6873
7523
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
6874
- log31.info({ name: parsed.name, action: resultAction }, "VRRP instance updated");
7524
+ log32.info({ name: parsed.name, action: resultAction }, "VRRP instance updated");
6875
7525
  return {
6876
7526
  content: `VRRP instance "${parsed.name}" ${resultAction}.`,
6877
7527
  structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
@@ -6885,10 +7535,10 @@ var manageVrrpInstanceTool = {
6885
7535
  var vrrpTools = [listVrrpInstancesTool, manageVrrpInstanceTool];
6886
7536
 
6887
7537
  // 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")
7538
+ import { z as z32 } from "zod";
7539
+ var log33 = createLogger("network-services-tools");
7540
+ var getSnmpSettingsInputSchema = z32.object({
7541
+ routerId: z32.string().describe("Target router identifier from the router registry")
6892
7542
  }).strict();
6893
7543
  var getSnmpSettingsTool = {
6894
7544
  name: "get_snmp_settings",
@@ -6903,7 +7553,7 @@ var getSnmpSettingsTool = {
6903
7553
  },
6904
7554
  async handler(params, context) {
6905
7555
  const parsed = getSnmpSettingsInputSchema.parse(params);
6906
- log32.info({ routerId: context.routerId }, "Fetching SNMP settings");
7556
+ log33.info({ routerId: context.routerId }, "Fetching SNMP settings");
6907
7557
  try {
6908
7558
  const results2 = await context.routerClient.get("snmp");
6909
7559
  if (!results2 || results2.length === 0) {
@@ -6932,8 +7582,8 @@ var getSnmpSettingsTool = {
6932
7582
  }
6933
7583
  }
6934
7584
  };
6935
- var getNtpSettingsInputSchema = z31.object({
6936
- routerId: z31.string().describe("Target router identifier from the router registry")
7585
+ var getNtpSettingsInputSchema = z32.object({
7586
+ routerId: z32.string().describe("Target router identifier from the router registry")
6937
7587
  }).strict();
6938
7588
  var getNtpSettingsTool = {
6939
7589
  name: "get_ntp_settings",
@@ -6948,7 +7598,7 @@ var getNtpSettingsTool = {
6948
7598
  },
6949
7599
  async handler(params, context) {
6950
7600
  const parsed = getNtpSettingsInputSchema.parse(params);
6951
- log32.info({ routerId: context.routerId }, "Fetching NTP settings");
7601
+ log33.info({ routerId: context.routerId }, "Fetching NTP settings");
6952
7602
  try {
6953
7603
  const results2 = await context.routerClient.get("system/ntp/client");
6954
7604
  if (!results2 || results2.length === 0) {
@@ -6977,11 +7627,11 @@ var getNtpSettingsTool = {
6977
7627
  }
6978
7628
  }
6979
7629
  };
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")
7630
+ var listNetwatchEntriesInputSchema = z32.object({
7631
+ routerId: z32.string().describe("Target router identifier from the router registry"),
7632
+ host: z32.string().optional().describe("Filter by host (substring match)"),
7633
+ status: z32.enum(["up", "down", "unknown"]).optional().describe("Filter by current status"),
7634
+ limit: z32.number().int().min(1).max(500).default(100).describe("Maximum number of entries to return")
6985
7635
  }).strict();
6986
7636
  var listNetwatchEntriesTool = {
6987
7637
  name: "list_netwatch_entries",
@@ -6996,7 +7646,7 @@ var listNetwatchEntriesTool = {
6996
7646
  },
6997
7647
  async handler(params, context) {
6998
7648
  const parsed = listNetwatchEntriesInputSchema.parse(params);
6999
- log32.info({ routerId: context.routerId }, "Listing Netwatch entries");
7649
+ log33.info({ routerId: context.routerId }, "Listing Netwatch entries");
7000
7650
  try {
7001
7651
  const allEntries = await context.routerClient.get("tool/netwatch", {
7002
7652
  limit: void 0,
@@ -7018,15 +7668,15 @@ var listNetwatchEntriesTool = {
7018
7668
  }
7019
7669
  }
7020
7670
  };
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")
7671
+ var manageNetwatchEntryInputSchema = z32.object({
7672
+ routerId: z32.string().describe("Target router identifier from the router registry"),
7673
+ action: z32.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
7674
+ host: z32.string().describe("Host to monitor \u2014 idempotency key"),
7675
+ port: z32.number().int().min(1).max(65535).optional().describe("TCP port (optional; ICMP if omitted)"),
7676
+ interval: z32.string().optional().describe("Check interval (e.g. '1m'; default '1m')"),
7677
+ timeout: z32.string().optional().describe("Probe timeout (e.g. '500ms')"),
7678
+ comment: z32.string().optional().describe("Optional comment"),
7679
+ dryRun: z32.boolean().default(false).describe("Preview changes without applying")
7030
7680
  }).strict();
7031
7681
  var manageNetwatchEntryTool = {
7032
7682
  name: "manage_netwatch_entry",
@@ -7042,7 +7692,7 @@ var manageNetwatchEntryTool = {
7042
7692
  snapshotPaths: ["tool/netwatch"],
7043
7693
  async handler(params, context) {
7044
7694
  const parsed = manageNetwatchEntryInputSchema.parse(params);
7045
- log32.info(
7695
+ log33.info(
7046
7696
  { routerId: context.routerId, action: parsed.action, host: parsed.host },
7047
7697
  "Managing Netwatch entry"
7048
7698
  );
@@ -7099,7 +7749,7 @@ var manageNetwatchEntryTool = {
7099
7749
  if (parsed.timeout) body.timeout = parsed.timeout;
7100
7750
  if (parsed.comment) body.comment = parsed.comment;
7101
7751
  const created = await context.routerClient.create("tool/netwatch", body);
7102
- log32.info({ host: parsed.host, id: created[".id"] }, "Netwatch entry added");
7752
+ log33.info({ host: parsed.host, id: created[".id"] }, "Netwatch entry added");
7103
7753
  return {
7104
7754
  content: `Added Netwatch entry for "${parsed.host}".`,
7105
7755
  structuredContent: { action: "created", entry: created }
@@ -7123,7 +7773,7 @@ var manageNetwatchEntryTool = {
7123
7773
  };
7124
7774
  }
7125
7775
  await context.routerClient.remove("tool/netwatch", existing2[".id"]);
7126
- log32.info({ host: parsed.host }, "Netwatch entry removed");
7776
+ log33.info({ host: parsed.host }, "Netwatch entry removed");
7127
7777
  return {
7128
7778
  content: `Removed Netwatch entry for "${parsed.host}".`,
7129
7779
  structuredContent: { action: "removed", host: parsed.host, id: existing2[".id"] }
@@ -7163,7 +7813,7 @@ var manageNetwatchEntryTool = {
7163
7813
  disabled: disabledValue
7164
7814
  });
7165
7815
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
7166
- log32.info({ host: parsed.host, action: resultAction }, "Netwatch entry updated");
7816
+ log33.info({ host: parsed.host, action: resultAction }, "Netwatch entry updated");
7167
7817
  return {
7168
7818
  content: `Netwatch entry for "${parsed.host}" ${resultAction}.`,
7169
7819
  structuredContent: { action: resultAction, host: parsed.host, id: existing[".id"] }
@@ -7174,10 +7824,10 @@ var manageNetwatchEntryTool = {
7174
7824
  }
7175
7825
  }
7176
7826
  };
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")
7827
+ var listNeighborsInputSchema = z32.object({
7828
+ routerId: z32.string().describe("Target router identifier from the router registry"),
7829
+ interface: z32.string().optional().describe("Filter by interface name (substring match)"),
7830
+ limit: z32.number().int().min(1).max(500).default(100).describe("Maximum number of neighbors to return")
7181
7831
  }).strict();
7182
7832
  var listNeighborsTool = {
7183
7833
  name: "list_neighbors",
@@ -7192,7 +7842,7 @@ var listNeighborsTool = {
7192
7842
  },
7193
7843
  async handler(params, context) {
7194
7844
  const parsed = listNeighborsInputSchema.parse(params);
7195
- log32.info({ routerId: context.routerId }, "Listing neighbors");
7845
+ log33.info({ routerId: context.routerId }, "Listing neighbors");
7196
7846
  try {
7197
7847
  const allNeighbors = await context.routerClient.get("ip/neighbor", {
7198
7848
  limit: void 0,
@@ -7216,12 +7866,12 @@ var listNeighborsTool = {
7216
7866
  }
7217
7867
  }
7218
7868
  };
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")
7869
+ var listArpEntriesInputSchema = z32.object({
7870
+ routerId: z32.string().describe("Target router identifier from the router registry"),
7871
+ interface: z32.string().optional().describe("Filter by interface (substring match)"),
7872
+ address: z32.string().optional().describe("Filter by IP address (substring match)"),
7873
+ macAddress: z32.string().optional().describe("Filter by MAC address (substring match)"),
7874
+ limit: z32.number().int().min(1).max(500).default(100).describe("Maximum number of ARP entries to return")
7225
7875
  }).strict();
7226
7876
  var listArpEntriesTool = {
7227
7877
  name: "list_arp_entries",
@@ -7236,7 +7886,7 @@ var listArpEntriesTool = {
7236
7886
  },
7237
7887
  async handler(params, context) {
7238
7888
  const parsed = listArpEntriesInputSchema.parse(params);
7239
- log32.info({ routerId: context.routerId }, "Listing ARP entries");
7889
+ log33.info({ routerId: context.routerId }, "Listing ARP entries");
7240
7890
  try {
7241
7891
  const allEntries = await context.routerClient.get("ip/arp", {
7242
7892
  limit: void 0,
@@ -7260,54 +7910,127 @@ var listArpEntriesTool = {
7260
7910
  }
7261
7911
  }
7262
7912
  };
7263
- var networkServicesTools = [
7264
- getSnmpSettingsTool,
7265
- getNtpSettingsTool,
7266
- listNetwatchEntriesTool,
7267
- manageNetwatchEntryTool,
7268
- listNeighborsTool,
7269
- listArpEntriesTool
7270
- ];
7271
-
7272
- // src/domain/tools/vlan-tools.ts
7273
- import { z as z32 } from "zod";
7274
- var log33 = createLogger("vlan-tools");
7275
- function sanitizeComment4(comment) {
7276
- if (comment === void 0) return void 0;
7277
- return comment.replace(/[\x00-\x1f\x7f]/g, "");
7278
- }
7279
- var manageVlanInputSchema = z32.object({
7913
+ var manageNtpClientInputSchema = z32.object({
7280
7914
  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)"),
7915
+ enabled: z32.boolean().optional().describe("Enable or disable the NTP client"),
7916
+ mode: z32.enum(["unicast", "broadcast", "multicast", "manycast"]).optional().describe("NTP client mode"),
7917
+ servers: z32.string().optional().describe("Comma-separated NTP server addresses"),
7918
+ vlanInterface: z32.string().optional().describe("VLAN interface for NTP communication"),
7288
7919
  dryRun: z32.boolean().default(false).describe("Preview changes without applying")
7289
7920
  }).strict();
7290
- var manageVlanTool = {
7291
- name: "manage_vlan",
7292
- title: "Manage VLAN Interface",
7293
- description: "Add, remove, enable, or disable a VLAN interface. Idempotent by name: add returns already_exists when a VLAN with matching name, vlan-id, and parent interface exists. Supports dry-run mode.",
7294
- inputSchema: manageVlanInputSchema,
7921
+ var manageNtpClientTool = {
7922
+ name: "manage_ntp_client",
7923
+ title: "Manage NTP Client",
7924
+ description: "Update NTP client settings on a MikroTik router. Idempotent: returns already_set if no changes are needed.",
7925
+ inputSchema: manageNtpClientInputSchema,
7295
7926
  annotations: {
7296
7927
  readOnlyHint: false,
7297
- destructiveHint: true,
7928
+ destructiveHint: false,
7298
7929
  idempotentHint: true,
7299
7930
  openWorldHint: false
7300
7931
  },
7301
- snapshotPaths: ["interface/vlan"],
7932
+ snapshotPaths: ["system/ntp/client"],
7302
7933
  async handler(params, context) {
7303
- const parsed = manageVlanInputSchema.parse(params);
7304
- const comment = sanitizeComment4(parsed.comment);
7305
- log33.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing VLAN");
7934
+ const parsed = manageNtpClientInputSchema.parse(params);
7935
+ log33.info({ routerId: context.routerId }, "Managing NTP client settings");
7306
7936
  try {
7307
- const allVlans = await context.routerClient.get("interface/vlan", {
7308
- limit: void 0,
7309
- offset: void 0
7310
- });
7937
+ const results2 = await context.routerClient.get("system/ntp/client");
7938
+ const current = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
7939
+ const id = current[".id"];
7940
+ const changes = {};
7941
+ const diff = [];
7942
+ if (parsed.enabled !== void 0 && current.enabled !== String(parsed.enabled)) {
7943
+ changes.enabled = String(parsed.enabled);
7944
+ diff.push({ property: "enabled", before: current.enabled, after: String(parsed.enabled) });
7945
+ }
7946
+ if (parsed.mode !== void 0 && current.mode !== parsed.mode) {
7947
+ changes.mode = parsed.mode;
7948
+ diff.push({ property: "mode", before: current.mode, after: parsed.mode });
7949
+ }
7950
+ if (parsed.servers !== void 0 && current.servers !== parsed.servers) {
7951
+ changes.servers = parsed.servers;
7952
+ diff.push({ property: "servers", before: current.servers, after: parsed.servers });
7953
+ }
7954
+ if (parsed.vlanInterface !== void 0 && current["vlan-interface"] !== parsed.vlanInterface) {
7955
+ changes["vlan-interface"] = parsed.vlanInterface;
7956
+ diff.push({
7957
+ property: "vlan-interface",
7958
+ before: current["vlan-interface"],
7959
+ after: parsed.vlanInterface
7960
+ });
7961
+ }
7962
+ if (diff.length === 0) {
7963
+ return {
7964
+ content: `NTP client on ${context.routerId} already matches requested configuration.`,
7965
+ structuredContent: { action: "already_set", routerId: context.routerId }
7966
+ };
7967
+ }
7968
+ if (parsed.dryRun) {
7969
+ return {
7970
+ content: `Dry run: Would update NTP client on ${context.routerId}.`,
7971
+ structuredContent: { action: "dry_run", diff }
7972
+ };
7973
+ }
7974
+ await context.routerClient.update("system/ntp/client", id, changes);
7975
+ log33.info({ routerId: context.routerId }, "NTP client updated");
7976
+ return {
7977
+ content: `NTP client on ${context.routerId} updated.`,
7978
+ structuredContent: { action: "updated", routerId: context.routerId, diff }
7979
+ };
7980
+ } catch (err) {
7981
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ntp_client" });
7982
+ }
7983
+ }
7984
+ };
7985
+ var networkServicesTools = [
7986
+ getSnmpSettingsTool,
7987
+ getNtpSettingsTool,
7988
+ listNetwatchEntriesTool,
7989
+ manageNetwatchEntryTool,
7990
+ listNeighborsTool,
7991
+ listArpEntriesTool,
7992
+ manageNtpClientTool
7993
+ ];
7994
+
7995
+ // src/domain/tools/vlan-tools.ts
7996
+ import { z as z33 } from "zod";
7997
+ var log34 = createLogger("vlan-tools");
7998
+ function sanitizeComment4(comment) {
7999
+ if (comment === void 0) return void 0;
8000
+ return comment.replace(/[\x00-\x1f\x7f]/g, "");
8001
+ }
8002
+ var manageVlanInputSchema = z33.object({
8003
+ routerId: z33.string().describe("Target router identifier from the router registry"),
8004
+ action: z33.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
8005
+ name: z33.string().regex(/^[a-zA-Z0-9_-]+$/).max(15).describe("VLAN interface name \u2014 idempotency key (alphanumeric, hyphens, underscores, max 15 chars)"),
8006
+ vlanId: z33.number().int().min(1).max(4094).optional().describe("VLAN ID (1-4094; required for add)"),
8007
+ parentInterface: z33.string().optional().describe("Parent interface name (e.g., ether1, bridge1; required for add)"),
8008
+ mtu: z33.number().int().min(68).max(9e3).default(1500).describe("MTU size (add only)"),
8009
+ disabled: z33.boolean().default(false).describe("Whether to create the VLAN disabled (add only)"),
8010
+ comment: z33.string().max(255).optional().describe("Optional comment (add only)"),
8011
+ dryRun: z33.boolean().default(false).describe("Preview changes without applying")
8012
+ }).strict();
8013
+ var manageVlanTool = {
8014
+ name: "manage_vlan",
8015
+ title: "Manage VLAN Interface",
8016
+ description: "Add, remove, enable, or disable a VLAN interface. Idempotent by name: add returns already_exists when a VLAN with matching name, vlan-id, and parent interface exists. Supports dry-run mode.",
8017
+ inputSchema: manageVlanInputSchema,
8018
+ annotations: {
8019
+ readOnlyHint: false,
8020
+ destructiveHint: true,
8021
+ idempotentHint: true,
8022
+ openWorldHint: false
8023
+ },
8024
+ snapshotPaths: ["interface/vlan"],
8025
+ async handler(params, context) {
8026
+ const parsed = manageVlanInputSchema.parse(params);
8027
+ const comment = sanitizeComment4(parsed.comment);
8028
+ log34.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing VLAN");
8029
+ try {
8030
+ const allVlans = await context.routerClient.get("interface/vlan", {
8031
+ limit: void 0,
8032
+ offset: void 0
8033
+ });
7311
8034
  const existing = allVlans.find((v) => v.name === parsed.name);
7312
8035
  if (parsed.action === "add") {
7313
8036
  if (parsed.vlanId === void 0) {
@@ -7330,7 +8053,7 @@ var manageVlanTool = {
7330
8053
  }
7331
8054
  if (existing) {
7332
8055
  if (existing["vlan-id"] === String(parsed.vlanId) && existing.interface === parsed.parentInterface) {
7333
- log33.info({ name: parsed.name }, "VLAN already exists with matching config");
8056
+ log34.info({ name: parsed.name }, "VLAN already exists with matching config");
7334
8057
  return {
7335
8058
  content: `VLAN "${parsed.name}" already exists with VLAN ID ${parsed.vlanId} on ${parsed.parentInterface}. No changes made.`,
7336
8059
  structuredContent: { action: "already_exists", vlan: existing }
@@ -7374,7 +8097,7 @@ var manageVlanTool = {
7374
8097
  };
7375
8098
  if (comment) body.comment = comment;
7376
8099
  const created = await context.routerClient.create("interface/vlan", body);
7377
- log33.info({ name: parsed.name, id: created[".id"] }, "VLAN created");
8100
+ log34.info({ name: parsed.name, id: created[".id"] }, "VLAN created");
7378
8101
  return {
7379
8102
  content: `Created VLAN "${parsed.name}" (ID ${parsed.vlanId}) on ${parsed.parentInterface} with MTU ${parsed.mtu}.`,
7380
8103
  structuredContent: { action: "created", vlan: created }
@@ -7394,7 +8117,7 @@ var manageVlanTool = {
7394
8117
  };
7395
8118
  }
7396
8119
  await context.routerClient.remove("interface/vlan", existing[".id"]);
7397
- log33.info({ name: parsed.name }, "VLAN removed");
8120
+ log34.info({ name: parsed.name }, "VLAN removed");
7398
8121
  return {
7399
8122
  content: `Removed VLAN "${parsed.name}".`,
7400
8123
  structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
@@ -7425,7 +8148,7 @@ var manageVlanTool = {
7425
8148
  const disabledValue = parsed.action === "disable" ? "true" : "false";
7426
8149
  await context.routerClient.update("interface/vlan", existing[".id"], { disabled: disabledValue });
7427
8150
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
7428
- log33.info({ name: parsed.name, action: resultAction }, "VLAN updated");
8151
+ log34.info({ name: parsed.name, action: resultAction }, "VLAN updated");
7429
8152
  return {
7430
8153
  content: `VLAN "${parsed.name}" ${resultAction}.`,
7431
8154
  structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
@@ -7439,14 +8162,14 @@ var manageVlanTool = {
7439
8162
  var vlanTools = [manageVlanTool];
7440
8163
 
7441
8164
  // 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")
8165
+ import { z as z34 } from "zod";
8166
+ var log35 = createLogger("dhcp-client-tools");
8167
+ var listDhcpClientsInputSchema = z34.object({
8168
+ routerId: z34.string().describe("Target router identifier from the router registry"),
8169
+ interface: z34.string().optional().describe("Filter by interface name (exact match)"),
8170
+ status: z34.enum(["bound", "searching", "requesting", "init", "all"]).default("all").describe("Filter by DHCP client status"),
8171
+ limit: z34.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
8172
+ offset: z34.number().int().min(0).default(0).describe("Offset for pagination")
7450
8173
  }).strict();
7451
8174
  var listDhcpClientsTool = {
7452
8175
  name: "list_dhcp_clients",
@@ -7461,7 +8184,7 @@ var listDhcpClientsTool = {
7461
8184
  },
7462
8185
  async handler(params, context) {
7463
8186
  const parsed = listDhcpClientsInputSchema.parse(params);
7464
- log34.info({ routerId: context.routerId }, "Listing DHCP clients");
8187
+ log35.info({ routerId: context.routerId }, "Listing DHCP clients");
7465
8188
  try {
7466
8189
  const allClients = await context.routerClient.get("ip/dhcp-client", {
7467
8190
  limit: void 0,
@@ -7504,15 +8227,15 @@ var listDhcpClientsTool = {
7504
8227
  }
7505
8228
  }
7506
8229
  };
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")
8230
+ var manageDhcpClientInputSchema = z34.object({
8231
+ routerId: z34.string().describe("Target router identifier from the router registry"),
8232
+ action: z34.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
8233
+ interface: z34.string().describe("Interface name \u2014 idempotency key (e.g. ether1, ether2)"),
8234
+ usePeerDns: z34.boolean().default(true).describe("Use DNS servers provided by DHCP server (add only)"),
8235
+ usePeerNtp: z34.boolean().default(false).describe("Use NTP servers provided by DHCP server (add only)"),
8236
+ addDefaultRoute: z34.boolean().default(true).describe("Add default route from DHCP (add only)"),
8237
+ comment: z34.string().max(255).optional().describe("Optional comment (add only)"),
8238
+ dryRun: z34.boolean().default(false).describe("Preview changes without applying")
7516
8239
  }).strict();
7517
8240
  var manageDhcpClientTool = {
7518
8241
  name: "manage_dhcp_client",
@@ -7528,7 +8251,7 @@ var manageDhcpClientTool = {
7528
8251
  snapshotPaths: ["ip/dhcp-client"],
7529
8252
  async handler(params, context) {
7530
8253
  const parsed = manageDhcpClientInputSchema.parse(params);
7531
- log34.info({ routerId: context.routerId, action: parsed.action, interface: parsed.interface }, "Managing DHCP client");
8254
+ log35.info({ routerId: context.routerId, action: parsed.action, interface: parsed.interface }, "Managing DHCP client");
7532
8255
  try {
7533
8256
  const allClients = await context.routerClient.get("ip/dhcp-client", {
7534
8257
  limit: void 0,
@@ -7564,7 +8287,7 @@ var manageDhcpClientTool = {
7564
8287
  };
7565
8288
  if (parsed.comment) body.comment = parsed.comment;
7566
8289
  const created = await context.routerClient.create("ip/dhcp-client", body);
7567
- log34.info({ interface: parsed.interface, id: created[".id"] }, "DHCP client added");
8290
+ log35.info({ interface: parsed.interface, id: created[".id"] }, "DHCP client added");
7568
8291
  return {
7569
8292
  content: `Added DHCP client on "${parsed.interface}".`,
7570
8293
  structuredContent: { action: "created", client: created }
@@ -7587,7 +8310,7 @@ var manageDhcpClientTool = {
7587
8310
  };
7588
8311
  }
7589
8312
  await context.routerClient.remove("ip/dhcp-client", existing[".id"]);
7590
- log34.info({ interface: parsed.interface }, "DHCP client removed");
8313
+ log35.info({ interface: parsed.interface }, "DHCP client removed");
7591
8314
  return {
7592
8315
  content: `Removed DHCP client on "${parsed.interface}".`,
7593
8316
  structuredContent: { action: "removed", interface: parsed.interface, id: existing[".id"] }
@@ -7624,7 +8347,7 @@ var manageDhcpClientTool = {
7624
8347
  const disabledValue = parsed.action === "disable" ? "true" : "false";
7625
8348
  await context.routerClient.update("ip/dhcp-client", existing[".id"], { disabled: disabledValue });
7626
8349
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
7627
- log34.info({ interface: parsed.interface, action: resultAction }, "DHCP client updated");
8350
+ log35.info({ interface: parsed.interface, action: resultAction }, "DHCP client updated");
7628
8351
  return {
7629
8352
  content: `DHCP client on "${parsed.interface}" ${resultAction}.`,
7630
8353
  structuredContent: { action: resultAction, interface: parsed.interface, id: existing[".id"] }
@@ -7638,13 +8361,13 @@ var manageDhcpClientTool = {
7638
8361
  var dhcpClientTools = [listDhcpClientsTool, manageDhcpClientTool];
7639
8362
 
7640
8363
  // src/domain/tools/ip-service-tools.ts
7641
- import { z as z34 } from "zod";
7642
- var log35 = createLogger("ip-service-tools");
8364
+ import { z as z35 } from "zod";
8365
+ var log36 = createLogger("ip-service-tools");
7643
8366
  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")
8367
+ var listIpServicesInputSchema = z35.object({
8368
+ routerId: z35.string().describe("Target router identifier from the router registry"),
8369
+ name: z35.enum(SERVICE_NAMES).optional().describe("Filter by service name (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp)"),
8370
+ enabled: z35.boolean().optional().describe("When true, return only enabled services; when false, only disabled services")
7648
8371
  }).strict();
7649
8372
  var listIpServicesTool = {
7650
8373
  name: "list_ip_services",
@@ -7659,7 +8382,7 @@ var listIpServicesTool = {
7659
8382
  },
7660
8383
  async handler(params, context) {
7661
8384
  const parsed = listIpServicesInputSchema.parse(params);
7662
- log35.info({ routerId: context.routerId }, "Listing IP services");
8385
+ log36.info({ routerId: context.routerId }, "Listing IP services");
7663
8386
  try {
7664
8387
  const allServices = await context.routerClient.get("ip/service", {
7665
8388
  limit: void 0,
@@ -7694,13 +8417,13 @@ var listIpServicesTool = {
7694
8417
  }
7695
8418
  }
7696
8419
  };
7697
- var manageIpServiceInputSchema = z34.object({
7698
- routerId: z34.string().describe("Target router identifier from the router registry"),
7699
- action: z34.enum(["enable", "disable"]).describe(
8420
+ var manageIpServiceInputSchema = z35.object({
8421
+ routerId: z35.string().describe("Target router identifier from the router registry"),
8422
+ action: z35.enum(["enable", "disable"]).describe(
7700
8423
  "Action to perform \u2014 only enable/disable to prevent accidental lockout from changing ports"
7701
8424
  ),
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")
8425
+ name: z35.enum(SERVICE_NAMES).describe("Service name to manage (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp)"),
8426
+ dryRun: z35.boolean().default(false).describe("Preview changes without applying")
7704
8427
  }).strict();
7705
8428
  var manageIpServiceTool = {
7706
8429
  name: "manage_ip_service",
@@ -7715,7 +8438,7 @@ var manageIpServiceTool = {
7715
8438
  },
7716
8439
  async handler(params, context) {
7717
8440
  const parsed = manageIpServiceInputSchema.parse(params);
7718
- log35.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP service");
8441
+ log36.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing IP service");
7719
8442
  try {
7720
8443
  const allServices = await context.routerClient.get("ip/service", {
7721
8444
  limit: void 0,
@@ -7762,7 +8485,7 @@ var manageIpServiceTool = {
7762
8485
  const disabledValue = parsed.action === "disable" ? "true" : "false";
7763
8486
  await context.routerClient.update("ip/service", existing[".id"], { disabled: disabledValue });
7764
8487
  const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
7765
- log35.info({ name: parsed.name, action: resultAction }, "IP service updated");
8488
+ log36.info({ name: parsed.name, action: resultAction }, "IP service updated");
7766
8489
  return {
7767
8490
  content: `IP service "${parsed.name}" ${resultAction}.`,
7768
8491
  structuredContent: { action: resultAction, name: parsed.name, id: existing[".id"] }
@@ -7776,15 +8499,15 @@ var manageIpServiceTool = {
7776
8499
  var ipServiceTools = [listIpServicesTool, manageIpServiceTool];
7777
8500
 
7778
8501
  // src/domain/tools/pppoe-tools.ts
7779
- import { z as z35 } from "zod";
7780
- var log36 = createLogger("pppoe-tools");
8502
+ import { z as z36 } from "zod";
8503
+ var log37 = createLogger("pppoe-tools");
7781
8504
  var PPPOE_PATH = "interface/pppoe-client";
7782
- var listPppoeClientsInputSchema = z35.object({
7783
- routerId: z35.string().describe("Target router identifier from the router registry"),
7784
- interface: z35.string().optional().describe("Filter by parent interface name (exact match)"),
7785
- status: z35.enum(["connected", "disconnected", "all"]).default("all").describe("Filter by running status"),
7786
- limit: z35.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
7787
- offset: z35.number().int().min(0).default(0).describe("Offset for pagination")
8505
+ var listPppoeClientsInputSchema = z36.object({
8506
+ routerId: z36.string().describe("Target router identifier from the router registry"),
8507
+ interface: z36.string().optional().describe("Filter by parent interface name (exact match)"),
8508
+ status: z36.enum(["connected", "disconnected", "all"]).default("all").describe("Filter by running status"),
8509
+ limit: z36.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
8510
+ offset: z36.number().int().min(0).default(0).describe("Offset for pagination")
7788
8511
  }).strict();
7789
8512
  function isRunning(record) {
7790
8513
  return record.running === true || record.running === "true";
@@ -7802,7 +8525,7 @@ var listPppoeClientsTool = {
7802
8525
  },
7803
8526
  async handler(params, context) {
7804
8527
  const parsed = listPppoeClientsInputSchema.parse(params);
7805
- log36.info({ routerId: context.routerId }, "Listing PPPoE clients");
8528
+ log37.info({ routerId: context.routerId }, "Listing PPPoE clients");
7806
8529
  try {
7807
8530
  const allClients = await context.routerClient.get(PPPOE_PATH, {
7808
8531
  limit: void 0,
@@ -7846,17 +8569,17 @@ var listPppoeClientsTool = {
7846
8569
  }
7847
8570
  }
7848
8571
  };
7849
- var managePppoeClientInputSchema = z35.object({
7850
- routerId: z35.string().describe("Target router identifier from the router registry"),
7851
- action: z35.enum(["add", "update", "remove"]).describe("Action to perform"),
7852
- name: z35.string().describe("PPPoE client interface name \u2014 idempotency key (e.g. pppoe-wan)"),
7853
- interface: z35.string().optional().describe("Parent interface (required for add)"),
7854
- user: z35.string().optional().describe("PPPoE username (required for add)"),
7855
- password: z35.string().optional().describe("PPPoE password (never logged)"),
7856
- serviceName: z35.string().optional().describe("PPPoE service name filter (leave empty to match any)"),
7857
- addDefaultRoute: z35.boolean().optional().describe("Add default route via PPPoE (yes/no)"),
7858
- dialOnDemand: z35.boolean().optional().describe("Dial on demand instead of always-on (yes/no)"),
7859
- dryRun: z35.boolean().default(false).describe("Preview changes without applying")
8572
+ var managePppoeClientInputSchema = z36.object({
8573
+ routerId: z36.string().describe("Target router identifier from the router registry"),
8574
+ action: z36.enum(["add", "update", "remove"]).describe("Action to perform"),
8575
+ name: z36.string().describe("PPPoE client interface name \u2014 idempotency key (e.g. pppoe-wan)"),
8576
+ interface: z36.string().optional().describe("Parent interface (required for add)"),
8577
+ user: z36.string().optional().describe("PPPoE username (required for add)"),
8578
+ password: z36.string().optional().describe("PPPoE password (never logged)"),
8579
+ serviceName: z36.string().optional().describe("PPPoE service name filter (leave empty to match any)"),
8580
+ addDefaultRoute: z36.boolean().optional().describe("Add default route via PPPoE (yes/no)"),
8581
+ dialOnDemand: z36.boolean().optional().describe("Dial on demand instead of always-on (yes/no)"),
8582
+ dryRun: z36.boolean().default(false).describe("Preview changes without applying")
7860
8583
  }).strict();
7861
8584
  var managePppoeClientTool = {
7862
8585
  name: "manage_pppoe_client",
@@ -7872,7 +8595,7 @@ var managePppoeClientTool = {
7872
8595
  snapshotPaths: [PPPOE_PATH],
7873
8596
  async handler(params, context) {
7874
8597
  const parsed = managePppoeClientInputSchema.parse(params);
7875
- log36.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing PPPoE client");
8598
+ log37.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing PPPoE client");
7876
8599
  try {
7877
8600
  const allClients = await context.routerClient.get(PPPOE_PATH, {
7878
8601
  limit: void 0,
@@ -7959,7 +8682,7 @@ var managePppoeClientTool = {
7959
8682
  if (parsed.addDefaultRoute !== void 0) body["add-default-route"] = parsed.addDefaultRoute ? "yes" : "no";
7960
8683
  if (parsed.dialOnDemand !== void 0) body["dial-on-demand"] = parsed.dialOnDemand ? "yes" : "no";
7961
8684
  const created = await context.routerClient.create(PPPOE_PATH, body);
7962
- log36.info({ name: parsed.name, id: created[".id"] }, "PPPoE client created");
8685
+ log37.info({ name: parsed.name, id: created[".id"] }, "PPPoE client created");
7963
8686
  return {
7964
8687
  content: `Created PPPoE client "${parsed.name}" on "${parsed.interface}".`,
7965
8688
  structuredContent: { action: "created", client: created }
@@ -8024,7 +8747,7 @@ var managePppoeClientTool = {
8024
8747
  };
8025
8748
  }
8026
8749
  await context.routerClient.update(PPPOE_PATH, existing[".id"], updates);
8027
- log36.info({ name: parsed.name }, "PPPoE client updated");
8750
+ log37.info({ name: parsed.name }, "PPPoE client updated");
8028
8751
  return {
8029
8752
  content: `Updated PPPoE client "${parsed.name}".`,
8030
8753
  structuredContent: { action: "updated", name: parsed.name, diff }
@@ -8046,7 +8769,7 @@ var managePppoeClientTool = {
8046
8769
  };
8047
8770
  }
8048
8771
  await context.routerClient.remove(PPPOE_PATH, existing[".id"]);
8049
- log36.info({ name: parsed.name }, "PPPoE client removed");
8772
+ log37.info({ name: parsed.name }, "PPPoE client removed");
8050
8773
  return {
8051
8774
  content: `Removed PPPoE client "${parsed.name}".`,
8052
8775
  structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
@@ -8060,14 +8783,14 @@ var managePppoeClientTool = {
8060
8783
  var pppoeTools = [listPppoeClientsTool, managePppoeClientTool];
8061
8784
 
8062
8785
  // src/domain/tools/ovpn-tools.ts
8063
- import { z as z36 } from "zod";
8064
- var log37 = createLogger("ovpn-tools");
8786
+ import { z as z37 } from "zod";
8787
+ var log38 = createLogger("ovpn-tools");
8065
8788
  var OVPN_CLIENT_PATH = "interface/ovpn-client";
8066
8789
  var OVPN_SERVER_PATH = "interface/ovpn-server/server";
8067
- var listOvpnClientsInputSchema = z36.object({
8068
- routerId: z36.string().describe("Target router identifier from the router registry"),
8069
- limit: z36.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
8070
- offset: z36.number().int().min(0).default(0).describe("Offset for pagination")
8790
+ var listOvpnClientsInputSchema = z37.object({
8791
+ routerId: z37.string().describe("Target router identifier from the router registry"),
8792
+ limit: z37.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
8793
+ offset: z37.number().int().min(0).default(0).describe("Offset for pagination")
8071
8794
  }).strict();
8072
8795
  var listOvpnClientsTool = {
8073
8796
  name: "list_ovpn_clients",
@@ -8082,7 +8805,7 @@ var listOvpnClientsTool = {
8082
8805
  },
8083
8806
  async handler(params, context) {
8084
8807
  const parsed = listOvpnClientsInputSchema.parse(params);
8085
- log37.info({ routerId: context.routerId }, "Listing OpenVPN clients");
8808
+ log38.info({ routerId: context.routerId }, "Listing OpenVPN clients");
8086
8809
  try {
8087
8810
  const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
8088
8811
  limit: void 0,
@@ -8107,18 +8830,18 @@ var listOvpnClientsTool = {
8107
8830
  }
8108
8831
  }
8109
8832
  };
8110
- var manageOvpnClientInputSchema = z36.object({
8111
- routerId: z36.string().describe("Target router identifier from the router registry"),
8112
- action: z36.enum(["add", "update", "remove"]).describe("Action to perform"),
8113
- name: z36.string().describe("OpenVPN client interface name \u2014 idempotency key"),
8114
- connectTo: z36.string().optional().describe("Remote server address (required for add)"),
8115
- port: z36.number().int().min(1).max(65535).optional().describe("Remote port number"),
8116
- mode: z36.enum(["ip", "ethernet"]).optional().describe("Tunnel mode"),
8117
- protocol: z36.enum(["tcp-client", "udp"]).optional().describe("Transport protocol"),
8118
- certificate: z36.string().optional().describe("Client certificate name"),
8119
- user: z36.string().optional().describe("VPN username"),
8120
- password: z36.string().optional().describe("VPN password (never logged)"),
8121
- dryRun: z36.boolean().default(false).describe("Preview changes without applying")
8833
+ var manageOvpnClientInputSchema = z37.object({
8834
+ routerId: z37.string().describe("Target router identifier from the router registry"),
8835
+ action: z37.enum(["add", "update", "remove"]).describe("Action to perform"),
8836
+ name: z37.string().describe("OpenVPN client interface name \u2014 idempotency key"),
8837
+ connectTo: z37.string().optional().describe("Remote server address (required for add)"),
8838
+ port: z37.number().int().min(1).max(65535).optional().describe("Remote port number"),
8839
+ mode: z37.enum(["ip", "ethernet"]).optional().describe("Tunnel mode"),
8840
+ protocol: z37.enum(["tcp-client", "udp"]).optional().describe("Transport protocol"),
8841
+ certificate: z37.string().optional().describe("Client certificate name"),
8842
+ user: z37.string().optional().describe("VPN username"),
8843
+ password: z37.string().optional().describe("VPN password (never logged)"),
8844
+ dryRun: z37.boolean().default(false).describe("Preview changes without applying")
8122
8845
  }).strict();
8123
8846
  var manageOvpnClientTool = {
8124
8847
  name: "manage_ovpn_client",
@@ -8134,7 +8857,7 @@ var manageOvpnClientTool = {
8134
8857
  snapshotPaths: [OVPN_CLIENT_PATH],
8135
8858
  async handler(params, context) {
8136
8859
  const parsed = manageOvpnClientInputSchema.parse(params);
8137
- log37.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing OpenVPN client");
8860
+ log38.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing OpenVPN client");
8138
8861
  try {
8139
8862
  const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
8140
8863
  limit: void 0,
@@ -8199,7 +8922,7 @@ var manageOvpnClientTool = {
8199
8922
  if (parsed.user !== void 0) body.user = parsed.user;
8200
8923
  if (parsed.password !== void 0) body.password = parsed.password;
8201
8924
  const created = await context.routerClient.create(OVPN_CLIENT_PATH, body);
8202
- log37.info({ name: parsed.name, id: created[".id"] }, "OpenVPN client created");
8925
+ log38.info({ name: parsed.name, id: created[".id"] }, "OpenVPN client created");
8203
8926
  return {
8204
8927
  content: `Created OpenVPN client "${parsed.name}" connecting to "${parsed.connectTo}".`,
8205
8928
  structuredContent: { action: "created", client: created }
@@ -8208,275 +8931,1728 @@ var manageOvpnClientTool = {
8208
8931
  if (parsed.action === "update") {
8209
8932
  if (!existing) {
8210
8933
  throw new MikroMCPError({
8211
- category: "NOT_FOUND" /* NOT_FOUND */,
8212
- code: "OVPN_CLIENT_NOT_FOUND",
8213
- message: `OpenVPN client "${parsed.name}" not found.`,
8214
- details: { name: parsed.name },
8934
+ category: "NOT_FOUND" /* NOT_FOUND */,
8935
+ code: "OVPN_CLIENT_NOT_FOUND",
8936
+ message: `OpenVPN client "${parsed.name}" not found.`,
8937
+ details: { name: parsed.name },
8938
+ recoverability: {
8939
+ retryable: false,
8940
+ suggestedAction: "Verify with list_ovpn_clients.",
8941
+ alternativeTools: ["list_ovpn_clients"]
8942
+ }
8943
+ });
8944
+ }
8945
+ const updates = {};
8946
+ const diff = [];
8947
+ if (parsed.connectTo !== void 0 && existing["connect-to"] !== parsed.connectTo) {
8948
+ updates["connect-to"] = parsed.connectTo;
8949
+ diff.push({ property: "connect-to", before: existing["connect-to"] ?? null, after: parsed.connectTo });
8950
+ }
8951
+ if (parsed.port !== void 0 && existing.port !== String(parsed.port)) {
8952
+ updates.port = String(parsed.port);
8953
+ diff.push({ property: "port", before: existing.port ?? null, after: String(parsed.port) });
8954
+ }
8955
+ if (parsed.mode !== void 0 && existing.mode !== parsed.mode) {
8956
+ updates.mode = parsed.mode;
8957
+ diff.push({ property: "mode", before: existing.mode ?? null, after: parsed.mode });
8958
+ }
8959
+ if (parsed.protocol !== void 0 && existing.protocol !== parsed.protocol) {
8960
+ updates.protocol = parsed.protocol;
8961
+ diff.push({ property: "protocol", before: existing.protocol ?? null, after: parsed.protocol });
8962
+ }
8963
+ if (parsed.certificate !== void 0 && existing.certificate !== parsed.certificate) {
8964
+ updates.certificate = parsed.certificate;
8965
+ diff.push({ property: "certificate", before: existing.certificate ?? null, after: parsed.certificate });
8966
+ }
8967
+ if (parsed.user !== void 0 && existing.user !== parsed.user) {
8968
+ updates.user = parsed.user;
8969
+ diff.push({ property: "user", before: existing.user ?? null, after: parsed.user });
8970
+ }
8971
+ if (parsed.password !== void 0) {
8972
+ updates.password = parsed.password;
8973
+ }
8974
+ const hasUpdates = Object.keys(updates).length > 0;
8975
+ if (!hasUpdates) {
8976
+ return {
8977
+ content: `OpenVPN client "${parsed.name}" already matches the requested configuration. No changes made.`,
8978
+ structuredContent: { action: "no_change", client: existing }
8979
+ };
8980
+ }
8981
+ if (parsed.dryRun) {
8982
+ return {
8983
+ content: `Dry run: Would update OpenVPN client "${parsed.name}".`,
8984
+ structuredContent: { action: "dry_run", diff }
8985
+ };
8986
+ }
8987
+ await context.routerClient.update(OVPN_CLIENT_PATH, existing[".id"], updates);
8988
+ log38.info({ name: parsed.name }, "OpenVPN client updated");
8989
+ return {
8990
+ content: `Updated OpenVPN client "${parsed.name}".`,
8991
+ structuredContent: { action: "updated", name: parsed.name, diff }
8992
+ };
8993
+ }
8994
+ if (!existing) {
8995
+ return {
8996
+ content: `OpenVPN client "${parsed.name}" not found. Nothing to remove.`,
8997
+ structuredContent: { action: "not_found", name: parsed.name }
8998
+ };
8999
+ }
9000
+ if (parsed.dryRun) {
9001
+ return {
9002
+ content: `Dry run: Would remove OpenVPN client "${parsed.name}".`,
9003
+ structuredContent: {
9004
+ action: "dry_run",
9005
+ diff: [{ property: "name", before: parsed.name, after: null }]
9006
+ }
9007
+ };
9008
+ }
9009
+ await context.routerClient.remove(OVPN_CLIENT_PATH, existing[".id"]);
9010
+ log38.info({ name: parsed.name }, "OpenVPN client removed");
9011
+ return {
9012
+ content: `Removed OpenVPN client "${parsed.name}".`,
9013
+ structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
9014
+ };
9015
+ } catch (err) {
9016
+ if (err instanceof MikroMCPError) throw err;
9017
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_client" });
9018
+ }
9019
+ }
9020
+ };
9021
+ var getOvpnServerInputSchema = z37.object({
9022
+ routerId: z37.string().describe("Target router identifier from the router registry")
9023
+ }).strict();
9024
+ function fetchOvpnServer(allRecords, routerId) {
9025
+ if (allRecords.length === 0) {
9026
+ throw new MikroMCPError({
9027
+ category: "NOT_FOUND" /* NOT_FOUND */,
9028
+ code: "OVPN_SERVER_NOT_FOUND",
9029
+ message: "OpenVPN server not found. The OpenVPN package may not be installed on this router.",
9030
+ details: { routerId },
9031
+ recoverability: {
9032
+ retryable: false,
9033
+ suggestedAction: "Install the OpenVPN package via manage_package.",
9034
+ alternativeTools: ["list_packages", "manage_package"]
9035
+ }
9036
+ });
9037
+ }
9038
+ return allRecords[0];
9039
+ }
9040
+ var getOvpnServerTool = {
9041
+ name: "get_ovpn_server",
9042
+ title: "Get OpenVPN Server",
9043
+ description: "Get the OpenVPN server configuration on a MikroTik router. Throws NOT_FOUND if the OpenVPN package is not installed.",
9044
+ inputSchema: getOvpnServerInputSchema,
9045
+ annotations: {
9046
+ readOnlyHint: true,
9047
+ destructiveHint: false,
9048
+ idempotentHint: true,
9049
+ openWorldHint: false
9050
+ },
9051
+ async handler(params, context) {
9052
+ const parsed = getOvpnServerInputSchema.parse(params);
9053
+ log38.info({ routerId: context.routerId }, "Getting OpenVPN server configuration");
9054
+ try {
9055
+ const records = await context.routerClient.get(OVPN_SERVER_PATH, {
9056
+ limit: void 0,
9057
+ offset: void 0
9058
+ });
9059
+ const server = fetchOvpnServer(records, parsed.routerId);
9060
+ return {
9061
+ content: `OpenVPN server on ${context.routerId}: enabled=${server.enabled}, port=${server.port}, protocol=${server.protocol}`,
9062
+ structuredContent: { routerId: context.routerId, server }
9063
+ };
9064
+ } catch (err) {
9065
+ if (err instanceof MikroMCPError) throw err;
9066
+ throw enrichError(err, { routerId: context.routerId, tool: "get_ovpn_server" });
9067
+ }
9068
+ }
9069
+ };
9070
+ var manageOvpnServerInputSchema = z37.object({
9071
+ routerId: z37.string().describe("Target router identifier from the router registry"),
9072
+ action: z37.enum(["enable", "disable", "set"]).describe("Action to perform"),
9073
+ port: z37.number().int().min(1).max(65535).optional().describe("Listening port (set action only)"),
9074
+ mode: z37.enum(["ip", "ethernet"]).optional().describe("Tunnel mode (set action only)"),
9075
+ protocol: z37.enum(["tcp-server", "udp"]).optional().describe("Transport protocol (set action only)"),
9076
+ certificate: z37.string().optional().describe("Server certificate name (set action only)"),
9077
+ cipher: z37.enum(["blowfish128", "aes128-cbc", "aes192-cbc", "aes256-cbc", "aes128-gcm", "aes256-gcm", "none"]).optional().describe("Encryption cipher (set action only)"),
9078
+ auth: z37.enum(["md5", "sha1", "sha256", "sha512", "null"]).optional().describe("Authentication algorithm (set action only)"),
9079
+ dryRun: z37.boolean().default(false).describe("Preview changes without applying")
9080
+ }).strict();
9081
+ var manageOvpnServerTool = {
9082
+ name: "manage_ovpn_server",
9083
+ title: "Manage OpenVPN Server",
9084
+ 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.",
9085
+ inputSchema: manageOvpnServerInputSchema,
9086
+ annotations: {
9087
+ readOnlyHint: false,
9088
+ destructiveHint: true,
9089
+ idempotentHint: true,
9090
+ openWorldHint: false
9091
+ },
9092
+ snapshotPaths: [OVPN_SERVER_PATH],
9093
+ async handler(params, context) {
9094
+ const parsed = manageOvpnServerInputSchema.parse(params);
9095
+ log38.info({ routerId: context.routerId, action: parsed.action }, "Managing OpenVPN server");
9096
+ try {
9097
+ const records = await context.routerClient.get(OVPN_SERVER_PATH, {
9098
+ limit: void 0,
9099
+ offset: void 0
9100
+ });
9101
+ const server = fetchOvpnServer(records, parsed.routerId);
9102
+ const serverId = server[".id"];
9103
+ if (parsed.action === "enable" || parsed.action === "disable") {
9104
+ const desiredEnabled = parsed.action === "enable";
9105
+ const currentEnabled = server.enabled === "yes";
9106
+ if (desiredEnabled === currentEnabled) {
9107
+ return {
9108
+ content: `OpenVPN server on ${context.routerId} is already ${parsed.action}d. No changes made.`,
9109
+ structuredContent: { action: "no_change", server }
9110
+ };
9111
+ }
9112
+ if (parsed.dryRun) {
9113
+ return {
9114
+ content: `Dry run: Would ${parsed.action} OpenVPN server on ${context.routerId}.`,
9115
+ structuredContent: {
9116
+ action: "dry_run",
9117
+ diff: [{ property: "enabled", before: server.enabled, after: desiredEnabled ? "yes" : "no" }]
9118
+ }
9119
+ };
9120
+ }
9121
+ await context.routerClient.update(OVPN_SERVER_PATH, serverId, {
9122
+ enabled: desiredEnabled ? "yes" : "no"
9123
+ });
9124
+ log38.info({ routerId: context.routerId, action: parsed.action }, "OpenVPN server updated");
9125
+ return {
9126
+ content: `OpenVPN server on ${context.routerId} ${parsed.action}d.`,
9127
+ structuredContent: { action: parsed.action === "enable" ? "enabled" : "disabled", server }
9128
+ };
9129
+ }
9130
+ 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;
9131
+ if (!hasFields) {
9132
+ throw new MikroMCPError({
9133
+ category: "VALIDATION" /* VALIDATION */,
9134
+ code: "OVPN_SERVER_NO_FIELDS",
9135
+ message: "At least one field must be provided for action set.",
9136
+ details: {},
9137
+ recoverability: {
9138
+ retryable: false,
9139
+ suggestedAction: "Provide at least one of: port, mode, protocol, certificate, cipher, auth.",
9140
+ alternativeTools: []
9141
+ }
9142
+ });
9143
+ }
9144
+ const updates = {};
9145
+ const diff = [];
9146
+ if (parsed.port !== void 0 && server.port !== String(parsed.port)) {
9147
+ updates.port = String(parsed.port);
9148
+ diff.push({ property: "port", before: server.port ?? null, after: String(parsed.port) });
9149
+ }
9150
+ if (parsed.mode !== void 0 && server.mode !== parsed.mode) {
9151
+ updates.mode = parsed.mode;
9152
+ diff.push({ property: "mode", before: server.mode ?? null, after: parsed.mode });
9153
+ }
9154
+ if (parsed.protocol !== void 0 && server.protocol !== parsed.protocol) {
9155
+ updates.protocol = parsed.protocol;
9156
+ diff.push({ property: "protocol", before: server.protocol ?? null, after: parsed.protocol });
9157
+ }
9158
+ if (parsed.certificate !== void 0 && server.certificate !== parsed.certificate) {
9159
+ updates.certificate = parsed.certificate;
9160
+ diff.push({ property: "certificate", before: server.certificate ?? null, after: parsed.certificate });
9161
+ }
9162
+ if (parsed.cipher !== void 0 && server.cipher !== parsed.cipher) {
9163
+ updates.cipher = parsed.cipher;
9164
+ diff.push({ property: "cipher", before: server.cipher ?? null, after: parsed.cipher });
9165
+ }
9166
+ if (parsed.auth !== void 0 && server.auth !== parsed.auth) {
9167
+ updates.auth = parsed.auth;
9168
+ diff.push({ property: "auth", before: server.auth ?? null, after: parsed.auth });
9169
+ }
9170
+ if (Object.keys(updates).length === 0) {
9171
+ return {
9172
+ content: `OpenVPN server on ${context.routerId} already matches the requested configuration. No changes made.`,
9173
+ structuredContent: { action: "no_change", server }
9174
+ };
9175
+ }
9176
+ if (parsed.dryRun) {
9177
+ return {
9178
+ content: `Dry run: Would update OpenVPN server on ${context.routerId}.`,
9179
+ structuredContent: { action: "dry_run", diff }
9180
+ };
9181
+ }
9182
+ await context.routerClient.update(OVPN_SERVER_PATH, serverId, updates);
9183
+ log38.info({ routerId: context.routerId }, "OpenVPN server configuration updated");
9184
+ return {
9185
+ content: `Updated OpenVPN server configuration on ${context.routerId}.`,
9186
+ structuredContent: { action: "updated", diff }
9187
+ };
9188
+ } catch (err) {
9189
+ if (err instanceof MikroMCPError) throw err;
9190
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_server" });
9191
+ }
9192
+ }
9193
+ };
9194
+ var ovpnTools = [
9195
+ listOvpnClientsTool,
9196
+ manageOvpnClientTool,
9197
+ getOvpnServerTool,
9198
+ manageOvpnServerTool
9199
+ ];
9200
+
9201
+ // src/domain/tools/ppp-tools.ts
9202
+ import { z as z38 } from "zod";
9203
+ var log39 = createLogger("ppp-tools");
9204
+ var listPppProfilesInputSchema = z38.object({
9205
+ routerId: z38.string().describe("Target router identifier from the router registry"),
9206
+ name: z38.string().optional().describe("Filter by profile name (exact match)"),
9207
+ limit: z38.number().int().min(1).max(500).default(100).describe("Maximum profiles to return")
9208
+ }).strict();
9209
+ var listPppProfilesTool = {
9210
+ name: "list_ppp_profiles",
9211
+ title: "List PPP Profiles",
9212
+ description: "List PPP profiles including the built-in default and default-encryption profiles.",
9213
+ inputSchema: listPppProfilesInputSchema,
9214
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
9215
+ async handler(params, context) {
9216
+ const parsed = listPppProfilesInputSchema.parse(params);
9217
+ try {
9218
+ log39.info({ routerId: context.routerId }, "Listing PPP profiles");
9219
+ const all = await context.routerClient.get("ppp/profile", { limit: void 0, offset: void 0 });
9220
+ const filtered = parsed.name ? all.filter((p) => p.name === parsed.name) : all;
9221
+ const profiles = filtered.slice(0, parsed.limit);
9222
+ return {
9223
+ content: `PPP profiles on ${context.routerId}: ${profiles.length} returned`,
9224
+ structuredContent: { routerId: context.routerId, profiles, total: all.length, returned: profiles.length }
9225
+ };
9226
+ } catch (err) {
9227
+ if (err instanceof MikroMCPError) throw err;
9228
+ throw enrichError(err, { routerId: context.routerId, tool: "list_ppp_profiles" });
9229
+ }
9230
+ }
9231
+ };
9232
+ var managePppProfileInputSchema = z38.object({
9233
+ routerId: z38.string().describe("Target router identifier from the router registry"),
9234
+ action: z38.enum(["add", "update", "remove"]).describe("Action to perform"),
9235
+ name: z38.string().describe("Profile name \u2014 idempotency key"),
9236
+ localAddress: z38.string().optional().describe("Local IP assigned to router end of PPP link"),
9237
+ remoteAddress: z38.string().optional().describe("IP or pool name assigned to client"),
9238
+ dnsServer: z38.string().optional().describe("DNS server IP pushed to client"),
9239
+ rateLimit: z38.string().optional().describe("Rate limit string (e.g. '10M/10M')"),
9240
+ sessionTimeout: z38.string().optional().describe("Session timeout duration string (e.g. '1h')"),
9241
+ comment: z38.string().optional().describe("Optional comment"),
9242
+ dryRun: z38.boolean().default(false).describe("Preview changes without applying")
9243
+ }).strict();
9244
+ var managePppProfileTool = {
9245
+ name: "manage_ppp_profile",
9246
+ title: "Manage PPP Profile",
9247
+ description: "Add, update, or remove a PPP profile. Idempotent by name. update returns no_change when requested values match. Built-in profiles (default, default-encryption) cannot be removed \u2014 RouterOS blocks this and the error is surfaced.",
9248
+ inputSchema: managePppProfileInputSchema,
9249
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
9250
+ snapshotPaths: ["ppp/profile"],
9251
+ async handler(params, context) {
9252
+ const parsed = managePppProfileInputSchema.parse(params);
9253
+ log39.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing PPP profile");
9254
+ try {
9255
+ const all = await context.routerClient.get("ppp/profile", { limit: void 0, offset: void 0 });
9256
+ const existing = all.find((p) => p.name === parsed.name);
9257
+ if (parsed.action === "add") {
9258
+ if (existing) {
9259
+ return {
9260
+ content: `PPP profile "${parsed.name}" already exists. No changes made.`,
9261
+ structuredContent: { action: "already_exists", name: parsed.name }
9262
+ };
9263
+ }
9264
+ if (parsed.dryRun) {
9265
+ const diff = [
9266
+ { property: "name", before: null, after: parsed.name }
9267
+ ];
9268
+ if (parsed.localAddress) diff.push({ property: "local-address", before: null, after: parsed.localAddress });
9269
+ if (parsed.remoteAddress) diff.push({ property: "remote-address", before: null, after: parsed.remoteAddress });
9270
+ if (parsed.dnsServer) diff.push({ property: "dns-server", before: null, after: parsed.dnsServer });
9271
+ if (parsed.rateLimit) diff.push({ property: "rate-limit", before: null, after: parsed.rateLimit });
9272
+ if (parsed.sessionTimeout) diff.push({ property: "session-timeout", before: null, after: parsed.sessionTimeout });
9273
+ if (parsed.comment) diff.push({ property: "comment", before: null, after: parsed.comment });
9274
+ return {
9275
+ content: `Dry run: Would create PPP profile "${parsed.name}".`,
9276
+ structuredContent: { action: "dry_run", diff }
9277
+ };
9278
+ }
9279
+ const body = { name: parsed.name };
9280
+ if (parsed.localAddress) body["local-address"] = parsed.localAddress;
9281
+ if (parsed.remoteAddress) body["remote-address"] = parsed.remoteAddress;
9282
+ if (parsed.dnsServer) body["dns-server"] = parsed.dnsServer;
9283
+ if (parsed.rateLimit) body["rate-limit"] = parsed.rateLimit;
9284
+ if (parsed.sessionTimeout) body["session-timeout"] = parsed.sessionTimeout;
9285
+ if (parsed.comment) body.comment = parsed.comment;
9286
+ const created = await context.routerClient.create("ppp/profile", body);
9287
+ log39.info({ name: parsed.name, id: created[".id"] }, "PPP profile created");
9288
+ return {
9289
+ content: `Created PPP profile "${parsed.name}".`,
9290
+ structuredContent: { action: "created", name: parsed.name, id: created[".id"] }
9291
+ };
9292
+ }
9293
+ if (parsed.action === "update") {
9294
+ if (!existing) {
9295
+ throw new MikroMCPError({
9296
+ category: "NOT_FOUND" /* NOT_FOUND */,
9297
+ code: "PPP_PROFILE_NOT_FOUND",
9298
+ message: `PPP profile "${parsed.name}" not found.`,
9299
+ details: { name: parsed.name },
9300
+ recoverability: { retryable: false, suggestedAction: "Verify with list_ppp_profiles.", alternativeTools: ["list_ppp_profiles"] }
9301
+ });
9302
+ }
9303
+ const changes = {};
9304
+ const diff = [];
9305
+ if (parsed.localAddress !== void 0 && existing["local-address"] !== parsed.localAddress) {
9306
+ changes["local-address"] = parsed.localAddress;
9307
+ diff.push({ property: "local-address", before: existing["local-address"] ?? null, after: parsed.localAddress });
9308
+ }
9309
+ if (parsed.remoteAddress !== void 0 && existing["remote-address"] !== parsed.remoteAddress) {
9310
+ changes["remote-address"] = parsed.remoteAddress;
9311
+ diff.push({ property: "remote-address", before: existing["remote-address"] ?? null, after: parsed.remoteAddress });
9312
+ }
9313
+ if (parsed.dnsServer !== void 0 && existing["dns-server"] !== parsed.dnsServer) {
9314
+ changes["dns-server"] = parsed.dnsServer;
9315
+ diff.push({ property: "dns-server", before: existing["dns-server"] ?? null, after: parsed.dnsServer });
9316
+ }
9317
+ if (parsed.rateLimit !== void 0 && existing["rate-limit"] !== parsed.rateLimit) {
9318
+ changes["rate-limit"] = parsed.rateLimit;
9319
+ diff.push({ property: "rate-limit", before: existing["rate-limit"] ?? null, after: parsed.rateLimit });
9320
+ }
9321
+ if (parsed.sessionTimeout !== void 0 && existing["session-timeout"] !== parsed.sessionTimeout) {
9322
+ changes["session-timeout"] = parsed.sessionTimeout;
9323
+ diff.push({ property: "session-timeout", before: existing["session-timeout"] ?? null, after: parsed.sessionTimeout });
9324
+ }
9325
+ if (parsed.comment !== void 0 && existing.comment !== parsed.comment) {
9326
+ changes.comment = parsed.comment;
9327
+ diff.push({ property: "comment", before: existing.comment ?? null, after: parsed.comment });
9328
+ }
9329
+ if (Object.keys(changes).length === 0) {
9330
+ return {
9331
+ content: `PPP profile "${parsed.name}" already matches requested values. No changes made.`,
9332
+ structuredContent: { action: "no_change", name: parsed.name }
9333
+ };
9334
+ }
9335
+ if (parsed.dryRun) {
9336
+ return {
9337
+ content: `Dry run: Would update PPP profile "${parsed.name}".`,
9338
+ structuredContent: { action: "dry_run", diff }
9339
+ };
9340
+ }
9341
+ await context.routerClient.update("ppp/profile", existing[".id"], changes);
9342
+ log39.info({ name: parsed.name, id: existing[".id"] }, "PPP profile updated");
9343
+ return {
9344
+ content: `Updated PPP profile "${parsed.name}".`,
9345
+ structuredContent: { action: "updated", name: parsed.name, id: existing[".id"], diff }
9346
+ };
9347
+ }
9348
+ if (!existing) {
9349
+ return {
9350
+ content: `PPP profile "${parsed.name}" not found. Nothing to remove.`,
9351
+ structuredContent: { action: "not_found", name: parsed.name }
9352
+ };
9353
+ }
9354
+ if (parsed.dryRun) {
9355
+ return {
9356
+ content: `Dry run: Would remove PPP profile "${parsed.name}".`,
9357
+ structuredContent: { action: "dry_run", diff: [{ property: "name", before: parsed.name, after: null }] }
9358
+ };
9359
+ }
9360
+ await context.routerClient.remove("ppp/profile", existing[".id"]);
9361
+ log39.info({ name: parsed.name }, "PPP profile removed");
9362
+ return {
9363
+ content: `Removed PPP profile "${parsed.name}".`,
9364
+ structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
9365
+ };
9366
+ } catch (err) {
9367
+ if (err instanceof MikroMCPError) throw err;
9368
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_ppp_profile" });
9369
+ }
9370
+ }
9371
+ };
9372
+ var pppTools = [listPppProfilesTool, managePppProfileTool];
9373
+
9374
+ // src/domain/tools/upgrade-tools.ts
9375
+ import { z as z39 } from "zod";
9376
+ var log40 = createLogger("upgrade-tools");
9377
+ var getUpgradeStatusInputSchema = z39.object({
9378
+ routerId: z39.string().describe("Target router identifier from the router registry")
9379
+ }).strict();
9380
+ var getUpgradeStatusTool = {
9381
+ name: "get_upgrade_status",
9382
+ title: "Get Upgrade Status",
9383
+ description: "Read the current RouterOS package upgrade status and routerboard firmware versions. Shows installed version, latest available version, update channel, and firmware upgrade availability.",
9384
+ inputSchema: getUpgradeStatusInputSchema,
9385
+ annotations: {
9386
+ readOnlyHint: true,
9387
+ destructiveHint: false,
9388
+ idempotentHint: true,
9389
+ openWorldHint: false
9390
+ },
9391
+ async handler(params, context) {
9392
+ const parsed = getUpgradeStatusInputSchema.parse(params);
9393
+ log40.info({ routerId: context.routerId }, "Getting upgrade status");
9394
+ try {
9395
+ const [updateResults, routerboardResults] = await Promise.all([
9396
+ context.routerClient.get("system/package/update"),
9397
+ context.routerClient.get("system/routerboard")
9398
+ ]);
9399
+ const update = updateResults[0] ?? {};
9400
+ const routerboard = routerboardResults.length > 0 ? routerboardResults[0] : null;
9401
+ return {
9402
+ content: `Upgrade status on ${parsed.routerId}: installed=${update["installed-version"] ?? "?"} latest=${update["latest-version"] ?? "?"} (${update.status ?? "unknown"})`,
9403
+ structuredContent: {
9404
+ routerId: parsed.routerId,
9405
+ channel: update.channel ?? null,
9406
+ installedVersion: update["installed-version"] ?? null,
9407
+ latestVersion: update["latest-version"] ?? null,
9408
+ status: update.status ?? null,
9409
+ routerboard: routerboard ? {
9410
+ model: routerboard.model ?? null,
9411
+ currentFirmware: routerboard["current-firmware"] ?? null,
9412
+ upgradeFirmware: routerboard["upgrade-firmware"] ?? null,
9413
+ factoryFirmware: routerboard["factory-firmware"] ?? null
9414
+ } : null
9415
+ }
9416
+ };
9417
+ } catch (err) {
9418
+ throw enrichError(err, { routerId: context.routerId, tool: "get_upgrade_status" });
9419
+ }
9420
+ }
9421
+ };
9422
+ var manageUpgradeInputSchema = z39.object({
9423
+ routerId: z39.string().describe("Target router identifier from the router registry"),
9424
+ action: z39.enum(["check", "install"]).describe(
9425
+ "check \u2014 trigger a check for available updates; install \u2014 download and install the latest update (triggers reboot)"
9426
+ ),
9427
+ dryRun: z39.boolean().default(false).describe("Preview the action without executing")
9428
+ }).strict();
9429
+ var manageUpgradeTool = {
9430
+ name: "manage_upgrade",
9431
+ title: "Manage Upgrade",
9432
+ 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.",
9433
+ inputSchema: manageUpgradeInputSchema,
9434
+ annotations: {
9435
+ readOnlyHint: false,
9436
+ destructiveHint: true,
9437
+ idempotentHint: false,
9438
+ openWorldHint: false
9439
+ },
9440
+ async handler(params, context) {
9441
+ const parsed = manageUpgradeInputSchema.parse(params);
9442
+ log40.info({ routerId: context.routerId, action: parsed.action }, "Managing upgrade");
9443
+ try {
9444
+ if (parsed.dryRun) {
9445
+ return {
9446
+ content: `Dry run: Would execute '${parsed.action}' on ${parsed.routerId}.`,
9447
+ structuredContent: {
9448
+ action: "dry_run",
9449
+ requestedAction: parsed.action,
9450
+ routerId: parsed.routerId
9451
+ }
9452
+ };
9453
+ }
9454
+ if (parsed.action === "check") {
9455
+ await context.routerClient.execute("system/package/update/check-for-updates");
9456
+ log40.info({ routerId: context.routerId }, "Update check triggered");
9457
+ return {
9458
+ content: `Update check triggered on ${parsed.routerId}.`,
9459
+ structuredContent: { action: "check_triggered", routerId: parsed.routerId }
9460
+ };
9461
+ }
9462
+ await context.routerClient.execute("system/package/update/install");
9463
+ log40.info({ routerId: context.routerId }, "Package install triggered");
9464
+ return {
9465
+ content: `Package install triggered on ${parsed.routerId}. Router will reboot automatically.`,
9466
+ structuredContent: { action: "install_triggered", routerId: parsed.routerId }
9467
+ };
9468
+ } catch (err) {
9469
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_upgrade" });
9470
+ }
9471
+ }
9472
+ };
9473
+ var upgradeTools = [getUpgradeStatusTool, manageUpgradeTool];
9474
+
9475
+ // src/domain/tools/backup-tools.ts
9476
+ import { z as z40 } from "zod";
9477
+ var log41 = createLogger("backup-tools");
9478
+ var createBackupInputSchema = z40.object({
9479
+ routerId: z40.string().describe("Target router identifier from the router registry"),
9480
+ name: z40.string().default("backup").describe("Backup file name (without extension)"),
9481
+ password: z40.string().optional().describe("Password to encrypt the backup file"),
9482
+ dryRun: z40.boolean().default(false).describe("Preview the backup without creating it")
9483
+ }).strict();
9484
+ var createBackupTool = {
9485
+ name: "create_backup",
9486
+ title: "Create Backup",
9487
+ 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.",
9488
+ inputSchema: createBackupInputSchema,
9489
+ annotations: {
9490
+ readOnlyHint: false,
9491
+ destructiveHint: false,
9492
+ idempotentHint: false,
9493
+ openWorldHint: false
9494
+ },
9495
+ async handler(params, context) {
9496
+ const parsed = createBackupInputSchema.parse(params);
9497
+ log41.info({ routerId: context.routerId, name: parsed.name }, "Creating backup");
9498
+ if (parsed.dryRun) {
9499
+ return {
9500
+ content: `Dry run: Would create backup "${parsed.name}.backup" on ${context.routerId}.`,
9501
+ structuredContent: { action: "dry_run", name: parsed.name, routerId: context.routerId }
9502
+ };
9503
+ }
9504
+ try {
9505
+ const body = { name: parsed.name };
9506
+ if (parsed.password !== void 0) {
9507
+ body.password = parsed.password;
9508
+ }
9509
+ await context.routerClient.execute("system/backup/save", body);
9510
+ log41.info({ routerId: context.routerId, name: parsed.name }, "Backup created");
9511
+ return {
9512
+ content: `Backup "${parsed.name}.backup" created on ${context.routerId}.`,
9513
+ structuredContent: {
9514
+ action: "created",
9515
+ routerId: context.routerId,
9516
+ filePath: `${parsed.name}.backup`
9517
+ }
9518
+ };
9519
+ } catch (err) {
9520
+ if (err instanceof MikroMCPError) throw err;
9521
+ throw enrichError(err, { routerId: context.routerId, tool: "create_backup" });
9522
+ }
9523
+ }
9524
+ };
9525
+ var exportConfigInputSchema = z40.object({
9526
+ routerId: z40.string().describe("Target router identifier from the router registry"),
9527
+ compact: z40.boolean().default(false).describe("Export only non-default settings (compact format)"),
9528
+ file: z40.string().optional().describe("Save the export to a file on the router (without extension); omit to return inline")
9529
+ }).strict();
9530
+ var exportConfigTool = {
9531
+ name: "export_config",
9532
+ title: "Export Config",
9533
+ 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.",
9534
+ inputSchema: exportConfigInputSchema,
9535
+ annotations: {
9536
+ readOnlyHint: true,
9537
+ destructiveHint: false,
9538
+ idempotentHint: true,
9539
+ openWorldHint: false
9540
+ },
9541
+ async handler(params, context) {
9542
+ const parsed = exportConfigInputSchema.parse(params);
9543
+ log41.info({ routerId: context.routerId, compact: parsed.compact }, "Exporting config");
9544
+ try {
9545
+ if (parsed.file !== void 0) {
9546
+ const body = { file: parsed.file };
9547
+ if (parsed.compact) body.compact = "yes";
9548
+ await context.routerClient.execute("system/export", body);
9549
+ return {
9550
+ content: `Config exported to "${parsed.file}.rsc" on ${context.routerId}.`,
9551
+ structuredContent: {
9552
+ routerId: context.routerId,
9553
+ filePath: `${parsed.file}.rsc`
9554
+ }
9555
+ };
9556
+ }
9557
+ const sshCommand = parsed.compact ? "export compact" : "export";
9558
+ const scriptText = await context.sshClient.execute(sshCommand);
9559
+ return {
9560
+ content: `Config export from ${context.routerId}:
9561
+ ${scriptText}`,
9562
+ structuredContent: {
9563
+ routerId: context.routerId,
9564
+ script: scriptText,
9565
+ compact: parsed.compact
9566
+ }
9567
+ };
9568
+ } catch (err) {
9569
+ if (err instanceof MikroMCPError) throw err;
9570
+ throw enrichError(err, { routerId: context.routerId, tool: "export_config" });
9571
+ }
9572
+ }
9573
+ };
9574
+ var backupTools = [createBackupTool, exportConfigTool];
9575
+
9576
+ // src/domain/tools/log-tools.ts
9577
+ import { z as z41 } from "zod";
9578
+ var log42 = createLogger("log-tools");
9579
+ var LOG_RULES_PATH = "system/logging";
9580
+ var LOG_ACTIONS_PATH = "system/logging/action";
9581
+ var logActionTypeEnum = z41.enum(["memory", "disk", "remote", "echo", "email"]);
9582
+ var listLogRulesInputSchema = z41.object({
9583
+ routerId: z41.string().describe("Target router identifier from the router registry"),
9584
+ topics: z41.string().optional().describe("Filter by topics (substring match)"),
9585
+ logAction: z41.string().optional().describe("Filter by log action target (exact match on action field)"),
9586
+ limit: z41.number().int().min(1).max(500).default(100).describe("Maximum number of rules to return (1-500, default 100)")
9587
+ }).strict();
9588
+ var listLogRulesTool = {
9589
+ name: "list_log_rules",
9590
+ title: "List Log Rules",
9591
+ description: "List RouterOS logging rules (system/logging) with optional topic substring and action exact-match filtering.",
9592
+ inputSchema: listLogRulesInputSchema,
9593
+ annotations: {
9594
+ readOnlyHint: true,
9595
+ destructiveHint: false,
9596
+ idempotentHint: true,
9597
+ openWorldHint: false
9598
+ },
9599
+ async handler(params, context) {
9600
+ const parsed = listLogRulesInputSchema.parse(params);
9601
+ log42.info({ routerId: context.routerId }, "Listing log rules");
9602
+ try {
9603
+ const all = await context.routerClient.get(LOG_RULES_PATH, {
9604
+ limit: void 0,
9605
+ offset: void 0
9606
+ });
9607
+ let rules = all;
9608
+ if (parsed.topics !== void 0) {
9609
+ rules = rules.filter((r) => r.topics?.includes(parsed.topics));
9610
+ }
9611
+ if (parsed.logAction !== void 0) {
9612
+ rules = rules.filter((r) => r.action === parsed.logAction);
9613
+ }
9614
+ const returned = rules.slice(0, parsed.limit);
9615
+ return {
9616
+ content: `Log rules on ${context.routerId}: ${returned.length} rule(s) (${all.length} total).`,
9617
+ structuredContent: {
9618
+ routerId: context.routerId,
9619
+ rules: returned,
9620
+ total: all.length,
9621
+ returned: returned.length
9622
+ }
9623
+ };
9624
+ } catch (err) {
9625
+ if (err instanceof MikroMCPError) throw err;
9626
+ throw enrichError(err, { routerId: context.routerId, tool: "list_log_rules" });
9627
+ }
9628
+ }
9629
+ };
9630
+ var manageLogRuleInputSchema = z41.object({
9631
+ routerId: z41.string().describe("Target router identifier from the router registry"),
9632
+ action: z41.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
9633
+ topics: z41.string().describe("Log topics (e.g. firewall, system, info) \u2014 idempotency key"),
9634
+ logAction: z41.string().describe("Log action target name (RouterOS action field) \u2014 idempotency key"),
9635
+ prefix: z41.string().optional().describe("Optional prefix to prepend to log messages"),
9636
+ dryRun: z41.boolean().default(false).describe("Preview changes without applying")
9637
+ }).strict();
9638
+ var manageLogRuleTool = {
9639
+ name: "manage_log_rule",
9640
+ title: "Manage Log Rule",
9641
+ 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.",
9642
+ inputSchema: manageLogRuleInputSchema,
9643
+ annotations: {
9644
+ readOnlyHint: false,
9645
+ destructiveHint: false,
9646
+ idempotentHint: true,
9647
+ openWorldHint: false
9648
+ },
9649
+ snapshotPaths: ["system/logging"],
9650
+ async handler(params, context) {
9651
+ const parsed = manageLogRuleInputSchema.parse(params);
9652
+ log42.info(
9653
+ { routerId: context.routerId, action: parsed.action, topics: parsed.topics },
9654
+ "Managing log rule"
9655
+ );
9656
+ try {
9657
+ const all = await context.routerClient.get(LOG_RULES_PATH, {
9658
+ limit: void 0,
9659
+ offset: void 0
9660
+ });
9661
+ const existing = all.find(
9662
+ (r) => r.topics === parsed.topics && r.action === parsed.logAction
9663
+ );
9664
+ if (parsed.action === "add") {
9665
+ if (existing) {
9666
+ return {
9667
+ content: `Log rule for topics "${parsed.topics}" \u2192 "${parsed.logAction}" already exists.`,
9668
+ structuredContent: {
9669
+ action: "already_exists",
9670
+ routerId: context.routerId,
9671
+ id: existing[".id"]
9672
+ }
9673
+ };
9674
+ }
9675
+ const body = {
9676
+ topics: parsed.topics,
9677
+ action: parsed.logAction
9678
+ };
9679
+ if (parsed.prefix !== void 0) body.prefix = parsed.prefix;
9680
+ if (parsed.dryRun) {
9681
+ return {
9682
+ content: `Dry run: Would add log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9683
+ structuredContent: {
9684
+ action: "dry_run",
9685
+ diff: Object.entries(body).map(([property, after]) => ({
9686
+ property,
9687
+ before: null,
9688
+ after
9689
+ }))
9690
+ }
9691
+ };
9692
+ }
9693
+ const created = await context.routerClient.create(LOG_RULES_PATH, body);
9694
+ log42.info({ topics: parsed.topics, logAction: parsed.logAction, id: created[".id"] }, "Log rule added");
9695
+ return {
9696
+ content: `Added log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9697
+ structuredContent: {
9698
+ action: "created",
9699
+ routerId: context.routerId,
9700
+ id: created[".id"]
9701
+ }
9702
+ };
9703
+ }
9704
+ if (parsed.action === "remove") {
9705
+ if (!existing) {
9706
+ return {
9707
+ content: `Log rule for topics "${parsed.topics}" \u2192 "${parsed.logAction}" not found. No changes made.`,
9708
+ structuredContent: {
9709
+ action: "not_found",
9710
+ routerId: context.routerId
9711
+ }
9712
+ };
9713
+ }
9714
+ const id = existing[".id"];
9715
+ if (parsed.dryRun) {
9716
+ return {
9717
+ content: `Dry run: Would remove log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9718
+ structuredContent: { action: "dry_run", id }
9719
+ };
9720
+ }
9721
+ await context.routerClient.remove(LOG_RULES_PATH, id);
9722
+ log42.info({ topics: parsed.topics, logAction: parsed.logAction, id }, "Log rule removed");
9723
+ return {
9724
+ content: `Removed log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9725
+ structuredContent: { action: "removed", id }
9726
+ };
9727
+ }
9728
+ if (parsed.action === "enable" || parsed.action === "disable") {
9729
+ if (!existing) {
9730
+ throw new MikroMCPError({
9731
+ category: "NOT_FOUND" /* NOT_FOUND */,
9732
+ code: "LOG_RULE_NOT_FOUND",
9733
+ message: `Log rule with topics "${parsed.topics}" and action "${parsed.logAction}" not found.`,
9734
+ details: { topics: parsed.topics, logAction: parsed.logAction },
9735
+ recoverability: {
9736
+ retryable: false,
9737
+ suggestedAction: "Verify with list_log_rules or use action=add to create it.",
9738
+ alternativeTools: ["list_log_rules", "manage_log_rule with action=add"]
9739
+ }
9740
+ });
9741
+ }
9742
+ const id = existing[".id"];
9743
+ const wantDisabled = parsed.action === "disable";
9744
+ if (parsed.dryRun) {
9745
+ return {
9746
+ content: `Dry run: Would ${parsed.action} log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9747
+ structuredContent: {
9748
+ action: "dry_run",
9749
+ id,
9750
+ diff: [{ property: "disabled", before: existing.disabled ?? "false", after: String(wantDisabled) }]
9751
+ }
9752
+ };
9753
+ }
9754
+ await context.routerClient.update(LOG_RULES_PATH, id, {
9755
+ disabled: wantDisabled ? "true" : "false"
9756
+ });
9757
+ const resultAction = wantDisabled ? "disabled" : "enabled";
9758
+ log42.info({ topics: parsed.topics, id, action: resultAction }, "Log rule toggled");
9759
+ return {
9760
+ content: `${wantDisabled ? "Disabled" : "Enabled"} log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9761
+ structuredContent: { action: resultAction, id }
9762
+ };
9763
+ }
9764
+ throw new MikroMCPError({
9765
+ category: "VALIDATION" /* VALIDATION */,
9766
+ code: "INVALID_ACTION",
9767
+ message: `Unknown action: ${parsed.action}`,
9768
+ recoverability: {
9769
+ retryable: false,
9770
+ suggestedAction: "Use one of: add, remove, enable, disable."
9771
+ }
9772
+ });
9773
+ } catch (err) {
9774
+ if (err instanceof MikroMCPError) throw err;
9775
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_log_rule" });
9776
+ }
9777
+ }
9778
+ };
9779
+ var listLogActionsInputSchema = z41.object({
9780
+ routerId: z41.string().describe("Target router identifier from the router registry"),
9781
+ type: logActionTypeEnum.optional().describe("Filter by action type (exact match): memory, disk, remote, echo, email"),
9782
+ limit: z41.number().int().min(1).max(500).default(100).describe("Maximum number of actions to return (1-500, default 100)")
9783
+ }).strict();
9784
+ var listLogActionsTool = {
9785
+ name: "list_log_actions",
9786
+ title: "List Log Actions",
9787
+ description: "List RouterOS logging action targets (system/logging/action) with optional type filter.",
9788
+ inputSchema: listLogActionsInputSchema,
9789
+ annotations: {
9790
+ readOnlyHint: true,
9791
+ destructiveHint: false,
9792
+ idempotentHint: true,
9793
+ openWorldHint: false
9794
+ },
9795
+ async handler(params, context) {
9796
+ const parsed = listLogActionsInputSchema.parse(params);
9797
+ log42.info({ routerId: context.routerId }, "Listing log actions");
9798
+ try {
9799
+ const all = await context.routerClient.get(LOG_ACTIONS_PATH, {
9800
+ limit: void 0,
9801
+ offset: void 0
9802
+ });
9803
+ let actions = all;
9804
+ if (parsed.type !== void 0) {
9805
+ actions = actions.filter((a) => a.type === parsed.type);
9806
+ }
9807
+ const returned = actions.slice(0, parsed.limit);
9808
+ return {
9809
+ content: `Log actions on ${context.routerId}: ${returned.length} action(s) (${all.length} total).`,
9810
+ structuredContent: {
9811
+ routerId: context.routerId,
9812
+ actions: returned,
9813
+ total: all.length,
9814
+ returned: returned.length
9815
+ }
9816
+ };
9817
+ } catch (err) {
9818
+ if (err instanceof MikroMCPError) throw err;
9819
+ throw enrichError(err, { routerId: context.routerId, tool: "list_log_actions" });
9820
+ }
9821
+ }
9822
+ };
9823
+ var manageLogActionInputSchema = z41.object({
9824
+ routerId: z41.string().describe("Target router identifier from the router registry"),
9825
+ action: z41.enum(["add", "remove"]).describe("Action to perform"),
9826
+ name: z41.string().describe("Log action name \u2014 idempotency key"),
9827
+ type: logActionTypeEnum.optional().describe("Action type: memory, disk, remote, echo, email (required on add)"),
9828
+ remote: z41.string().optional().describe("Remote syslog server address (for type=remote)"),
9829
+ remotePort: z41.number().int().min(1).max(65535).optional().describe("Remote syslog server port (for type=remote)"),
9830
+ diskFileName: z41.string().optional().describe("Disk log file name without extension (for type=disk)"),
9831
+ dryRun: z41.boolean().default(false).describe("Preview changes without applying")
9832
+ }).strict();
9833
+ var manageLogActionTool = {
9834
+ name: "manage_log_action",
9835
+ title: "Manage Log Action",
9836
+ 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.",
9837
+ inputSchema: manageLogActionInputSchema,
9838
+ annotations: {
9839
+ readOnlyHint: false,
9840
+ destructiveHint: false,
9841
+ idempotentHint: true,
9842
+ openWorldHint: false
9843
+ },
9844
+ snapshotPaths: ["system/logging/action"],
9845
+ async handler(params, context) {
9846
+ const parsed = manageLogActionInputSchema.parse(params);
9847
+ log42.info(
9848
+ { routerId: context.routerId, action: parsed.action, name: parsed.name },
9849
+ "Managing log action"
9850
+ );
9851
+ try {
9852
+ const all = await context.routerClient.get(LOG_ACTIONS_PATH, {
9853
+ limit: void 0,
9854
+ offset: void 0
9855
+ });
9856
+ const existing = all.find((a) => a.name === parsed.name);
9857
+ if (parsed.action === "add") {
9858
+ if (parsed.type === void 0) {
9859
+ throw new MikroMCPError({
9860
+ category: "VALIDATION" /* VALIDATION */,
9861
+ code: "LOG_ACTION_TYPE_REQUIRED",
9862
+ message: "type is required when action is add",
9863
+ recoverability: {
9864
+ retryable: false,
9865
+ suggestedAction: "Provide the type field: memory, disk, remote, echo, or email."
9866
+ }
9867
+ });
9868
+ }
9869
+ if (existing) {
9870
+ return {
9871
+ content: `Log action "${parsed.name}" already exists.`,
9872
+ structuredContent: {
9873
+ action: "already_exists",
9874
+ routerId: context.routerId,
9875
+ name: parsed.name,
9876
+ id: existing[".id"]
9877
+ }
9878
+ };
9879
+ }
9880
+ const body = {
9881
+ name: parsed.name,
9882
+ type: parsed.type
9883
+ };
9884
+ if (parsed.remote !== void 0) body.remote = parsed.remote;
9885
+ if (parsed.remotePort !== void 0) body["remote-port"] = String(parsed.remotePort);
9886
+ if (parsed.diskFileName !== void 0) body["disk-file-name"] = parsed.diskFileName;
9887
+ if (parsed.dryRun) {
9888
+ return {
9889
+ content: `Dry run: Would add log action "${parsed.name}" (type=${parsed.type}).`,
9890
+ structuredContent: {
9891
+ action: "dry_run",
9892
+ diff: Object.entries(body).map(([property, after]) => ({
9893
+ property,
9894
+ before: null,
9895
+ after
9896
+ }))
9897
+ }
9898
+ };
9899
+ }
9900
+ const created = await context.routerClient.create(LOG_ACTIONS_PATH, body);
9901
+ log42.info({ name: parsed.name, type: parsed.type, id: created[".id"] }, "Log action added");
9902
+ return {
9903
+ content: `Added log action "${parsed.name}" (type=${parsed.type}).`,
9904
+ structuredContent: {
9905
+ action: "created",
9906
+ routerId: context.routerId,
9907
+ name: parsed.name,
9908
+ id: created[".id"]
9909
+ }
9910
+ };
9911
+ }
9912
+ if (parsed.action === "remove") {
9913
+ if (!existing) {
9914
+ return {
9915
+ content: `Log action "${parsed.name}" not found. No changes made.`,
9916
+ structuredContent: {
9917
+ action: "not_found",
9918
+ routerId: context.routerId,
9919
+ name: parsed.name
9920
+ }
9921
+ };
9922
+ }
9923
+ const id = existing[".id"];
9924
+ if (parsed.dryRun) {
9925
+ return {
9926
+ content: `Dry run: Would remove log action "${parsed.name}".`,
9927
+ structuredContent: { action: "dry_run", name: parsed.name, id }
9928
+ };
9929
+ }
9930
+ await context.routerClient.remove(LOG_ACTIONS_PATH, id);
9931
+ log42.info({ name: parsed.name, id }, "Log action removed");
9932
+ return {
9933
+ content: `Removed log action "${parsed.name}".`,
9934
+ structuredContent: { action: "removed", name: parsed.name, id }
9935
+ };
9936
+ }
9937
+ throw new MikroMCPError({
9938
+ category: "VALIDATION" /* VALIDATION */,
9939
+ code: "INVALID_ACTION",
9940
+ message: `Unknown action: ${parsed.action}`,
9941
+ recoverability: {
9942
+ retryable: false,
9943
+ suggestedAction: "Use one of: add, remove."
9944
+ }
9945
+ });
9946
+ } catch (err) {
9947
+ if (err instanceof MikroMCPError) throw err;
9948
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_log_action" });
9949
+ }
9950
+ }
9951
+ };
9952
+ var logTools = [
9953
+ listLogRulesTool,
9954
+ manageLogRuleTool,
9955
+ listLogActionsTool,
9956
+ manageLogActionTool
9957
+ ];
9958
+
9959
+ // src/domain/tools/network-test-tools.ts
9960
+ import { z as z42 } from "zod";
9961
+ var log43 = createLogger("network-test-tools");
9962
+ var BODY_CAP = 65536;
9963
+ var bandwidthTestInputSchema = z42.object({
9964
+ routerId: z42.string().describe("Target router identifier from the router registry"),
9965
+ address: z42.string().describe("Remote host running RouterOS btest server"),
9966
+ protocol: z42.enum(["tcp", "udp"]).default("tcp").describe("Test protocol"),
9967
+ direction: z42.enum(["send", "receive", "both"]).default("both").describe("Test direction"),
9968
+ duration: z42.number().int().min(1).max(30).default(5).describe("Test duration in seconds (max 30)")
9969
+ }).strict();
9970
+ var bandwidthTestTool = {
9971
+ name: "bandwidth_test",
9972
+ title: "Bandwidth Test",
9973
+ description: "Run a RouterOS bandwidth test from the router to a remote host running a RouterOS btest server. Returns TX and RX throughput in Mbps. Duration capped at 30 seconds.",
9974
+ inputSchema: bandwidthTestInputSchema,
9975
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
9976
+ async handler(params, context) {
9977
+ const parsed = bandwidthTestInputSchema.parse(params);
9978
+ log43.info({ routerId: context.routerId, address: parsed.address }, "Running bandwidth test");
9979
+ try {
9980
+ const result = await context.routerClient.execute("tool/bandwidth-test", {
9981
+ address: parsed.address,
9982
+ protocol: parsed.protocol,
9983
+ direction: parsed.direction,
9984
+ duration: String(parsed.duration)
9985
+ });
9986
+ const txBps = Number(result["tx-current"] ?? 0);
9987
+ const rxBps = Number(result["rx-current"] ?? 0);
9988
+ const txMbps = Math.round(txBps / 1e6 * 100) / 100;
9989
+ const rxMbps = Math.round(rxBps / 1e6 * 100) / 100;
9990
+ return {
9991
+ content: `Bandwidth test to ${parsed.address}: TX=${txMbps} Mbps RX=${rxMbps} Mbps`,
9992
+ structuredContent: {
9993
+ routerId: context.routerId,
9994
+ address: parsed.address,
9995
+ protocol: parsed.protocol,
9996
+ direction: parsed.direction,
9997
+ txMbps,
9998
+ rxMbps,
9999
+ lostPackets: result["lost-packets"] ?? null
10000
+ }
10001
+ };
10002
+ } catch (err) {
10003
+ throw enrichError(err, { routerId: context.routerId, tool: "bandwidth_test" });
10004
+ }
10005
+ }
10006
+ };
10007
+ var fetchUrlInputSchema = z42.object({
10008
+ routerId: z42.string().describe("Target router identifier from the router registry"),
10009
+ url: z42.string().url().describe("URL to fetch from the router"),
10010
+ method: z42.enum(["GET", "POST"]).default("GET").describe("HTTP method"),
10011
+ httpData: z42.string().optional().describe("Request body for POST"),
10012
+ outputFile: z42.string().optional().describe("Save response body to this router file path instead of returning inline")
10013
+ }).strict();
10014
+ var fetchUrlTool = {
10015
+ name: "fetch_url",
10016
+ title: "Fetch URL",
10017
+ description: "Send an HTTP/HTTPS request from the router using /tool/fetch. Response body is returned inline (capped at 64 KB with [TRUNCATED] marker). Use outputFile to save to router filesystem instead.",
10018
+ inputSchema: fetchUrlInputSchema,
10019
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
10020
+ async handler(params, context) {
10021
+ const parsed = fetchUrlInputSchema.parse(params);
10022
+ log43.info({ routerId: context.routerId, url: parsed.url, method: parsed.method }, "Fetching URL");
10023
+ try {
10024
+ const body = { url: parsed.url, "http-method": parsed.method.toLowerCase() };
10025
+ if (parsed.httpData) body["http-data"] = parsed.httpData;
10026
+ if (parsed.outputFile) {
10027
+ body.output = "file";
10028
+ body["dst-path"] = parsed.outputFile;
10029
+ } else {
10030
+ body.output = "user";
10031
+ }
10032
+ const sections = await context.routerClient.execute("tool/fetch", body);
10033
+ const finished = sections.find((s) => s.status === "finished") ?? sections[sections.length - 1];
10034
+ const statusCode = finished?.code ?? null;
10035
+ if (parsed.outputFile) {
10036
+ return {
10037
+ content: `Fetched ${parsed.url} \u2192 saved to ${parsed.outputFile} (status ${statusCode})`,
10038
+ structuredContent: { routerId: context.routerId, url: parsed.url, statusCode, outputFile: parsed.outputFile }
10039
+ };
10040
+ }
10041
+ let responseBody = finished?.data ?? "";
10042
+ if (responseBody.length > BODY_CAP) {
10043
+ responseBody = responseBody.slice(0, BODY_CAP) + "[TRUNCATED]";
10044
+ }
10045
+ return {
10046
+ content: `Fetched ${parsed.url}: status=${statusCode} body_length=${responseBody.length}`,
10047
+ structuredContent: { routerId: context.routerId, url: parsed.url, statusCode, body: responseBody }
10048
+ };
10049
+ } catch (err) {
10050
+ throw enrichError(err, { routerId: context.routerId, tool: "fetch_url" });
10051
+ }
10052
+ }
10053
+ };
10054
+ var listConnectionsInputSchema = z42.object({
10055
+ routerId: z42.string().describe("Target router identifier from the router registry"),
10056
+ srcAddress: z42.string().optional().describe("Filter by source address (substring match)"),
10057
+ dstAddress: z42.string().optional().describe("Filter by destination address (substring match)"),
10058
+ protocol: z42.string().optional().describe("Filter by protocol (exact match, e.g. tcp, udp, icmp)"),
10059
+ limit: z42.number().int().min(1).max(500).default(100).describe("Maximum connections to return")
10060
+ }).strict();
10061
+ var listConnectionsTool = {
10062
+ name: "list_connections",
10063
+ title: "List Connections",
10064
+ description: "List active connection tracking entries from the router firewall table. Filters are applied client-side. Useful for diagnosing NAT and firewall behavior.",
10065
+ inputSchema: listConnectionsInputSchema,
10066
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
10067
+ async handler(params, context) {
10068
+ const parsed = listConnectionsInputSchema.parse(params);
10069
+ log43.info({ routerId: context.routerId }, "Listing connections");
10070
+ try {
10071
+ const all = await context.routerClient.get("ip/firewall/connection", {
10072
+ limit: void 0,
10073
+ offset: void 0
10074
+ });
10075
+ const filtered = all.filter((c) => parsed.srcAddress ? (c["src-address"] ?? "").includes(parsed.srcAddress) : true).filter((c) => parsed.dstAddress ? (c["dst-address"] ?? "").includes(parsed.dstAddress) : true).filter((c) => parsed.protocol ? c.protocol === parsed.protocol : true);
10076
+ const connections = filtered.slice(0, parsed.limit);
10077
+ return {
10078
+ content: `Active connections on ${context.routerId}: ${connections.length} returned (${all.length} total)`,
10079
+ structuredContent: { routerId: context.routerId, connections, total: all.length, returned: connections.length }
10080
+ };
10081
+ } catch (err) {
10082
+ throw enrichError(err, { routerId: context.routerId, tool: "list_connections" });
10083
+ }
10084
+ }
10085
+ };
10086
+ var networkTestTools = [bandwidthTestTool, fetchUrlTool, listConnectionsTool];
10087
+
10088
+ // src/domain/tools/container-config-tools.ts
10089
+ import { z as z43 } from "zod";
10090
+ var log44 = createLogger("container-config-tools");
10091
+ var getContainerConfigInputSchema = z43.object({ routerId: z43.string().describe("Target router identifier from the router registry") }).strict();
10092
+ var getContainerConfigTool = {
10093
+ name: "get_container_config",
10094
+ title: "Get Container Config",
10095
+ description: "Read global container configuration: registry URL, RAM high-water mark, and veth interface.",
10096
+ inputSchema: getContainerConfigInputSchema,
10097
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
10098
+ async handler(params, context) {
10099
+ getContainerConfigInputSchema.parse(params);
10100
+ log44.info({ routerId: context.routerId }, "Getting container config");
10101
+ try {
10102
+ const results2 = await context.routerClient.get("container/config");
10103
+ const cfg = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
10104
+ return {
10105
+ content: `Container config on ${context.routerId}: registry=${cfg["registry-url"] ?? "none"} ram-high=${cfg["ram-high"] ?? "?"}MB veth=${cfg["veth-interface"] ?? "none"}`,
10106
+ structuredContent: {
10107
+ routerId: context.routerId,
10108
+ registryUrl: cfg["registry-url"] ?? null,
10109
+ ramHighMb: cfg["ram-high"] ?? null,
10110
+ vethInterface: cfg["veth-interface"] ?? null
10111
+ }
10112
+ };
10113
+ } catch (err) {
10114
+ throw enrichError(err, { routerId: context.routerId, tool: "get_container_config" });
10115
+ }
10116
+ }
10117
+ };
10118
+ var manageContainerConfigInputSchema = z43.object({
10119
+ routerId: z43.string().describe("Target router identifier from the router registry"),
10120
+ registryUrl: z43.string().optional().describe("Container registry URL"),
10121
+ ramHighMb: z43.number().int().min(1).optional().describe("RAM high-water mark in MB"),
10122
+ vethInterface: z43.string().optional().describe("Veth interface name for container networking"),
10123
+ dryRun: z43.boolean().default(false).describe("Preview changes without applying")
10124
+ }).strict();
10125
+ var manageContainerConfigTool = {
10126
+ name: "manage_container_config",
10127
+ title: "Manage Container Config",
10128
+ description: "Update global container settings. Idempotent: returns no_change if nothing differs.",
10129
+ inputSchema: manageContainerConfigInputSchema,
10130
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
10131
+ snapshotPaths: ["container/config"],
10132
+ async handler(params, context) {
10133
+ const parsed = manageContainerConfigInputSchema.parse(params);
10134
+ log44.info({ routerId: context.routerId }, "Managing container config");
10135
+ try {
10136
+ const results2 = await context.routerClient.get("container/config");
10137
+ const current = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
10138
+ const id = current[".id"];
10139
+ const changes = {};
10140
+ const diff = [];
10141
+ if (parsed.registryUrl !== void 0 && current["registry-url"] !== parsed.registryUrl) {
10142
+ changes["registry-url"] = parsed.registryUrl;
10143
+ diff.push({ property: "registry-url", before: current["registry-url"] ?? null, after: parsed.registryUrl });
10144
+ }
10145
+ if (parsed.ramHighMb !== void 0) {
10146
+ const next = String(parsed.ramHighMb);
10147
+ if (current["ram-high"] !== next) {
10148
+ changes["ram-high"] = next;
10149
+ diff.push({ property: "ram-high", before: current["ram-high"] ?? null, after: next });
10150
+ }
10151
+ }
10152
+ if (parsed.vethInterface !== void 0 && current["veth-interface"] !== parsed.vethInterface) {
10153
+ changes["veth-interface"] = parsed.vethInterface;
10154
+ diff.push({ property: "veth-interface", before: current["veth-interface"] ?? null, after: parsed.vethInterface });
10155
+ }
10156
+ if (Object.keys(changes).length === 0) {
10157
+ return {
10158
+ content: "Container config already matches. No changes made.",
10159
+ structuredContent: { action: "no_change", routerId: context.routerId }
10160
+ };
10161
+ }
10162
+ if (parsed.dryRun) {
10163
+ return {
10164
+ content: `Dry run: Would update container config on ${context.routerId}.`,
10165
+ structuredContent: { action: "dry_run", diff }
10166
+ };
10167
+ }
10168
+ await context.routerClient.update("container/config", id, changes);
10169
+ log44.info({ routerId: context.routerId }, "Container config updated");
10170
+ return {
10171
+ content: `Updated container config on ${context.routerId}.`,
10172
+ structuredContent: { action: "updated", routerId: context.routerId, diff }
10173
+ };
10174
+ } catch (err) {
10175
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_container_config" });
10176
+ }
10177
+ }
10178
+ };
10179
+ var listContainerEnvsInputSchema = z43.object({
10180
+ routerId: z43.string().describe("Target router identifier from the router registry"),
10181
+ name: z43.string().optional().describe("Filter by container name (exact match)"),
10182
+ limit: z43.number().int().min(1).max(500).default(100).describe("Maximum entries to return")
10183
+ }).strict();
10184
+ var listContainerEnvsTool = {
10185
+ name: "list_container_envs",
10186
+ title: "List Container Envs",
10187
+ description: "List container environment variable entries, optionally filtered by container name.",
10188
+ inputSchema: listContainerEnvsInputSchema,
10189
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
10190
+ async handler(params, context) {
10191
+ const parsed = listContainerEnvsInputSchema.parse(params);
10192
+ log44.info({ routerId: context.routerId }, "Listing container envs");
10193
+ try {
10194
+ const all = await context.routerClient.get("container/envs", {
10195
+ limit: void 0,
10196
+ offset: void 0
10197
+ });
10198
+ const filtered = parsed.name ? all.filter((e) => e.name === parsed.name) : all;
10199
+ const envs = filtered.slice(0, parsed.limit);
10200
+ return {
10201
+ content: `Container envs on ${context.routerId}: ${envs.length} returned`,
10202
+ structuredContent: { routerId: context.routerId, envs, total: all.length, returned: envs.length }
10203
+ };
10204
+ } catch (err) {
10205
+ throw enrichError(err, { routerId: context.routerId, tool: "list_container_envs" });
10206
+ }
10207
+ }
10208
+ };
10209
+ var manageContainerEnvInputSchema = z43.object({
10210
+ routerId: z43.string().describe("Target router identifier from the router registry"),
10211
+ action: z43.enum(["add", "remove"]).describe("Action to perform"),
10212
+ name: z43.string().describe("Container name"),
10213
+ key: z43.string().describe("Environment variable name \u2014 part of idempotency key"),
10214
+ value: z43.string().optional().describe("Environment variable value (required for add)"),
10215
+ dryRun: z43.boolean().default(false).describe("Preview changes without applying")
10216
+ }).strict();
10217
+ var manageContainerEnvTool = {
10218
+ name: "manage_container_env",
10219
+ title: "Manage Container Env",
10220
+ description: "Add or remove a container environment variable. Idempotent by name+key. add returns already_exists if the entry exists with the same value; throws CONFLICT if the value differs.",
10221
+ inputSchema: manageContainerEnvInputSchema,
10222
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
10223
+ async handler(params, context) {
10224
+ const parsed = manageContainerEnvInputSchema.parse(params);
10225
+ log44.info(
10226
+ { routerId: context.routerId, action: parsed.action, name: parsed.name, key: parsed.key },
10227
+ "Managing container env"
10228
+ );
10229
+ try {
10230
+ const all = await context.routerClient.get("container/envs", {
10231
+ limit: void 0,
10232
+ offset: void 0
10233
+ });
10234
+ const existing = all.find(
10235
+ (e) => e.name === parsed.name && e.key === parsed.key
10236
+ );
10237
+ if (parsed.action === "add") {
10238
+ if (existing) {
10239
+ if (existing.value === parsed.value) {
10240
+ return {
10241
+ content: `Env ${parsed.name}.${parsed.key} already exists with same value.`,
10242
+ structuredContent: { action: "already_exists", name: parsed.name, key: parsed.key }
10243
+ };
10244
+ }
10245
+ throw new MikroMCPError({
10246
+ category: "CONFLICT" /* CONFLICT */,
10247
+ code: "CONTAINER_ENV_CONFLICT",
10248
+ message: `Env ${parsed.name}.${parsed.key} exists with a different value.`,
10249
+ details: { existingValue: existing.value, requestedValue: parsed.value },
10250
+ recoverability: {
10251
+ retryable: false,
10252
+ suggestedAction: "Remove the existing entry first.",
10253
+ alternativeTools: ["manage_container_env"]
10254
+ }
10255
+ });
10256
+ }
10257
+ if (parsed.value === void 0) {
10258
+ throw new MikroMCPError({
10259
+ category: "VALIDATION" /* VALIDATION */,
10260
+ code: "ENV_VALUE_REQUIRED",
10261
+ message: "value is required for add.",
10262
+ recoverability: { retryable: false, suggestedAction: "Provide the value parameter." }
10263
+ });
10264
+ }
10265
+ if (parsed.dryRun) {
10266
+ return {
10267
+ content: `Dry run: Would add env ${parsed.name}.${parsed.key}=${parsed.value}.`,
10268
+ structuredContent: {
10269
+ action: "dry_run",
10270
+ diff: [{ property: "value", before: null, after: parsed.value }]
10271
+ }
10272
+ };
10273
+ }
10274
+ const created = await context.routerClient.create("container/envs", {
10275
+ name: parsed.name,
10276
+ key: parsed.key,
10277
+ value: parsed.value
10278
+ });
10279
+ log44.info({ name: parsed.name, key: parsed.key, id: created[".id"] }, "Container env added");
10280
+ return {
10281
+ content: `Added env ${parsed.name}.${parsed.key}.`,
10282
+ structuredContent: {
10283
+ action: "created",
10284
+ name: parsed.name,
10285
+ key: parsed.key,
10286
+ id: created[".id"]
10287
+ }
10288
+ };
10289
+ }
10290
+ if (!existing) {
10291
+ return {
10292
+ content: `Env ${parsed.name}.${parsed.key} not found. Nothing to remove.`,
10293
+ structuredContent: { action: "not_found", name: parsed.name, key: parsed.key }
10294
+ };
10295
+ }
10296
+ if (parsed.dryRun) {
10297
+ return {
10298
+ content: `Dry run: Would remove env ${parsed.name}.${parsed.key}.`,
10299
+ structuredContent: {
10300
+ action: "dry_run",
10301
+ diff: [{ property: "key", before: parsed.key, after: null }]
10302
+ }
10303
+ };
10304
+ }
10305
+ await context.routerClient.remove("container/envs", existing[".id"]);
10306
+ log44.info({ name: parsed.name, key: parsed.key }, "Container env removed");
10307
+ return {
10308
+ content: `Removed env ${parsed.name}.${parsed.key}.`,
10309
+ structuredContent: { action: "removed", name: parsed.name, key: parsed.key }
10310
+ };
10311
+ } catch (err) {
10312
+ if (err instanceof MikroMCPError) throw err;
10313
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_container_env" });
10314
+ }
10315
+ }
10316
+ };
10317
+ var listContainerMountsInputSchema = z43.object({
10318
+ routerId: z43.string().describe("Target router identifier from the router registry"),
10319
+ name: z43.string().optional().describe("Filter by mount name (exact match)"),
10320
+ limit: z43.number().int().min(1).max(500).default(100).describe("Maximum entries to return")
10321
+ }).strict();
10322
+ var listContainerMountsTool = {
10323
+ name: "list_container_mounts",
10324
+ title: "List Container Mounts",
10325
+ description: "List container volume mount definitions with source path, destination path, and mount name.",
10326
+ inputSchema: listContainerMountsInputSchema,
10327
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
10328
+ async handler(params, context) {
10329
+ const parsed = listContainerMountsInputSchema.parse(params);
10330
+ log44.info({ routerId: context.routerId }, "Listing container mounts");
10331
+ try {
10332
+ const all = await context.routerClient.get("container/mounts", {
10333
+ limit: void 0,
10334
+ offset: void 0
10335
+ });
10336
+ const filtered = parsed.name ? all.filter((m) => m.name === parsed.name) : all;
10337
+ const mounts = filtered.slice(0, parsed.limit);
10338
+ return {
10339
+ content: `Container mounts on ${context.routerId}: ${mounts.length} returned`,
10340
+ structuredContent: { routerId: context.routerId, mounts, total: all.length, returned: mounts.length }
10341
+ };
10342
+ } catch (err) {
10343
+ throw enrichError(err, { routerId: context.routerId, tool: "list_container_mounts" });
10344
+ }
10345
+ }
10346
+ };
10347
+ var manageContainerMountInputSchema = z43.object({
10348
+ routerId: z43.string().describe("Target router identifier from the router registry"),
10349
+ action: z43.enum(["add", "remove"]).describe("Action to perform"),
10350
+ name: z43.string().describe("Mount name \u2014 idempotency key"),
10351
+ src: z43.string().optional().describe("Host source path (required for add)"),
10352
+ dst: z43.string().optional().describe("Container destination path (required for add)"),
10353
+ dryRun: z43.boolean().default(false).describe("Preview changes without applying")
10354
+ }).strict();
10355
+ var manageContainerMountTool = {
10356
+ name: "manage_container_mount",
10357
+ title: "Manage Container Mount",
10358
+ description: "Add or remove a container volume mount. Idempotent by name: add returns already_exists if the mount exists with matching src/dst; throws CONFLICT if name exists with different paths.",
10359
+ inputSchema: manageContainerMountInputSchema,
10360
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
10361
+ async handler(params, context) {
10362
+ const parsed = manageContainerMountInputSchema.parse(params);
10363
+ log44.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing container mount");
10364
+ try {
10365
+ const all = await context.routerClient.get("container/mounts", {
10366
+ limit: void 0,
10367
+ offset: void 0
10368
+ });
10369
+ const existing = all.find((m) => m.name === parsed.name);
10370
+ if (parsed.action === "add") {
10371
+ if (existing) {
10372
+ if (existing.src === parsed.src && existing.dst === parsed.dst) {
10373
+ return {
10374
+ content: `Mount "${parsed.name}" already exists with same paths.`,
10375
+ structuredContent: { action: "already_exists", name: parsed.name }
10376
+ };
10377
+ }
10378
+ throw new MikroMCPError({
10379
+ category: "CONFLICT" /* CONFLICT */,
10380
+ code: "CONTAINER_MOUNT_CONFLICT",
10381
+ message: `Mount "${parsed.name}" exists with different src/dst paths.`,
10382
+ details: {
10383
+ existing: { src: existing.src, dst: existing.dst },
10384
+ requested: { src: parsed.src, dst: parsed.dst }
10385
+ },
8215
10386
  recoverability: {
8216
10387
  retryable: false,
8217
- suggestedAction: "Verify with list_ovpn_clients.",
8218
- alternativeTools: ["list_ovpn_clients"]
10388
+ suggestedAction: "Remove the existing mount first.",
10389
+ alternativeTools: ["manage_container_mount"]
8219
10390
  }
8220
10391
  });
8221
10392
  }
8222
- const updates = {};
8223
- const diff = [];
8224
- if (parsed.connectTo !== void 0 && existing["connect-to"] !== parsed.connectTo) {
8225
- updates["connect-to"] = parsed.connectTo;
8226
- diff.push({ property: "connect-to", before: existing["connect-to"] ?? null, after: parsed.connectTo });
8227
- }
8228
- if (parsed.port !== void 0 && existing.port !== String(parsed.port)) {
8229
- updates.port = String(parsed.port);
8230
- diff.push({ property: "port", before: existing.port ?? null, after: String(parsed.port) });
8231
- }
8232
- if (parsed.mode !== void 0 && existing.mode !== parsed.mode) {
8233
- updates.mode = parsed.mode;
8234
- diff.push({ property: "mode", before: existing.mode ?? null, after: parsed.mode });
8235
- }
8236
- if (parsed.protocol !== void 0 && existing.protocol !== parsed.protocol) {
8237
- updates.protocol = parsed.protocol;
8238
- diff.push({ property: "protocol", before: existing.protocol ?? null, after: parsed.protocol });
8239
- }
8240
- if (parsed.certificate !== void 0 && existing.certificate !== parsed.certificate) {
8241
- updates.certificate = parsed.certificate;
8242
- diff.push({ property: "certificate", before: existing.certificate ?? null, after: parsed.certificate });
8243
- }
8244
- if (parsed.user !== void 0 && existing.user !== parsed.user) {
8245
- updates.user = parsed.user;
8246
- diff.push({ property: "user", before: existing.user ?? null, after: parsed.user });
8247
- }
8248
- if (parsed.password !== void 0) {
8249
- updates.password = parsed.password;
8250
- }
8251
- const hasUpdates = Object.keys(updates).length > 0;
8252
- if (!hasUpdates) {
8253
- return {
8254
- content: `OpenVPN client "${parsed.name}" already matches the requested configuration. No changes made.`,
8255
- structuredContent: { action: "no_change", client: existing }
8256
- };
10393
+ if (!parsed.src || !parsed.dst) {
10394
+ throw new MikroMCPError({
10395
+ category: "VALIDATION" /* VALIDATION */,
10396
+ code: "MOUNT_PATHS_REQUIRED",
10397
+ message: "src and dst are required for add.",
10398
+ recoverability: { retryable: false, suggestedAction: "Provide src and dst parameters." }
10399
+ });
8257
10400
  }
8258
10401
  if (parsed.dryRun) {
8259
10402
  return {
8260
- content: `Dry run: Would update OpenVPN client "${parsed.name}".`,
8261
- structuredContent: { action: "dry_run", diff }
10403
+ content: `Dry run: Would add mount "${parsed.name}" ${parsed.src} \u2192 ${parsed.dst}.`,
10404
+ structuredContent: {
10405
+ action: "dry_run",
10406
+ diff: [{ property: "name", before: null, after: parsed.name }]
10407
+ }
8262
10408
  };
8263
10409
  }
8264
- await context.routerClient.update(OVPN_CLIENT_PATH, existing[".id"], updates);
8265
- log37.info({ name: parsed.name }, "OpenVPN client updated");
10410
+ const created = await context.routerClient.create("container/mounts", {
10411
+ name: parsed.name,
10412
+ src: parsed.src,
10413
+ dst: parsed.dst
10414
+ });
10415
+ log44.info({ name: parsed.name, id: created[".id"] }, "Container mount added");
8266
10416
  return {
8267
- content: `Updated OpenVPN client "${parsed.name}".`,
8268
- structuredContent: { action: "updated", name: parsed.name, diff }
10417
+ content: `Added mount "${parsed.name}".`,
10418
+ structuredContent: { action: "created", name: parsed.name, id: created[".id"] }
8269
10419
  };
8270
10420
  }
8271
10421
  if (!existing) {
8272
10422
  return {
8273
- content: `OpenVPN client "${parsed.name}" not found. Nothing to remove.`,
10423
+ content: `Mount "${parsed.name}" not found. Nothing to remove.`,
8274
10424
  structuredContent: { action: "not_found", name: parsed.name }
8275
10425
  };
8276
10426
  }
8277
10427
  if (parsed.dryRun) {
8278
10428
  return {
8279
- content: `Dry run: Would remove OpenVPN client "${parsed.name}".`,
10429
+ content: `Dry run: Would remove mount "${parsed.name}".`,
8280
10430
  structuredContent: {
8281
10431
  action: "dry_run",
8282
10432
  diff: [{ property: "name", before: parsed.name, after: null }]
8283
10433
  }
8284
10434
  };
8285
10435
  }
8286
- await context.routerClient.remove(OVPN_CLIENT_PATH, existing[".id"]);
8287
- log37.info({ name: parsed.name }, "OpenVPN client removed");
10436
+ await context.routerClient.remove("container/mounts", existing[".id"]);
10437
+ log44.info({ name: parsed.name }, "Container mount removed");
8288
10438
  return {
8289
- content: `Removed OpenVPN client "${parsed.name}".`,
8290
- structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
10439
+ content: `Removed mount "${parsed.name}".`,
10440
+ structuredContent: { action: "removed", name: parsed.name }
8291
10441
  };
8292
10442
  } catch (err) {
8293
10443
  if (err instanceof MikroMCPError) throw err;
8294
- throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_client" });
10444
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_container_mount" });
8295
10445
  }
8296
10446
  }
8297
10447
  };
8298
- var getOvpnServerInputSchema = z36.object({
8299
- routerId: z36.string().describe("Target router identifier from the router registry")
10448
+ var containerConfigTools = [
10449
+ getContainerConfigTool,
10450
+ manageContainerConfigTool,
10451
+ listContainerEnvsTool,
10452
+ manageContainerEnvTool,
10453
+ listContainerMountsTool,
10454
+ manageContainerMountTool
10455
+ ];
10456
+
10457
+ // src/domain/tools/interface-list-tools.ts
10458
+ import { z as z44 } from "zod";
10459
+ var log45 = createLogger("interface-list-tools");
10460
+ var listInterfaceListsInputSchema = z44.object({
10461
+ routerId: z44.string().describe("Target router identifier from the router registry"),
10462
+ limit: z44.number().int().min(1).max(500).default(100).describe("Maximum lists to return")
8300
10463
  }).strict();
8301
- function fetchOvpnServer(allRecords, routerId) {
8302
- if (allRecords.length === 0) {
8303
- throw new MikroMCPError({
8304
- category: "NOT_FOUND" /* NOT_FOUND */,
8305
- code: "OVPN_SERVER_NOT_FOUND",
8306
- message: "OpenVPN server not found. The OpenVPN package may not be installed on this router.",
8307
- details: { routerId },
8308
- recoverability: {
8309
- retryable: false,
8310
- suggestedAction: "Install the OpenVPN package via manage_package.",
8311
- alternativeTools: ["list_packages", "manage_package"]
8312
- }
8313
- });
8314
- }
8315
- return allRecords[0];
8316
- }
8317
- var getOvpnServerTool = {
8318
- name: "get_ovpn_server",
8319
- title: "Get OpenVPN Server",
8320
- description: "Get the OpenVPN server configuration on a MikroTik router. Throws NOT_FOUND if the OpenVPN package is not installed.",
8321
- inputSchema: getOvpnServerInputSchema,
8322
- annotations: {
8323
- readOnlyHint: true,
8324
- destructiveHint: false,
8325
- idempotentHint: true,
8326
- openWorldHint: false
8327
- },
10464
+ var listInterfaceListsTool = {
10465
+ name: "list_interface_lists",
10466
+ title: "List Interface Lists",
10467
+ description: "List all interface lists defined on the router.",
10468
+ inputSchema: listInterfaceListsInputSchema,
10469
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
8328
10470
  async handler(params, context) {
8329
- const parsed = getOvpnServerInputSchema.parse(params);
8330
- log37.info({ routerId: context.routerId }, "Getting OpenVPN server configuration");
10471
+ const parsed = listInterfaceListsInputSchema.parse(params);
10472
+ log45.info({ routerId: context.routerId }, "Listing interface lists");
8331
10473
  try {
8332
- const records = await context.routerClient.get(OVPN_SERVER_PATH, {
10474
+ const all = await context.routerClient.get("interface/list", {
8333
10475
  limit: void 0,
8334
10476
  offset: void 0
8335
10477
  });
8336
- const server = fetchOvpnServer(records, parsed.routerId);
10478
+ const lists = all.slice(0, parsed.limit);
8337
10479
  return {
8338
- content: `OpenVPN server on ${context.routerId}: enabled=${server.enabled}, port=${server.port}, protocol=${server.protocol}`,
8339
- structuredContent: { routerId: context.routerId, server }
10480
+ content: `Interface lists on ${context.routerId}: ${lists.length} returned`,
10481
+ structuredContent: { routerId: context.routerId, lists, total: all.length, returned: lists.length }
8340
10482
  };
8341
10483
  } catch (err) {
8342
10484
  if (err instanceof MikroMCPError) throw err;
8343
- throw enrichError(err, { routerId: context.routerId, tool: "get_ovpn_server" });
10485
+ throw enrichError(err, { routerId: context.routerId, tool: "list_interface_lists" });
8344
10486
  }
8345
10487
  }
8346
10488
  };
8347
- var manageOvpnServerInputSchema = z36.object({
8348
- routerId: z36.string().describe("Target router identifier from the router registry"),
8349
- action: z36.enum(["enable", "disable", "set"]).describe("Action to perform"),
8350
- port: z36.number().int().min(1).max(65535).optional().describe("Listening port (set action only)"),
8351
- mode: z36.enum(["ip", "ethernet"]).optional().describe("Tunnel mode (set action only)"),
8352
- protocol: z36.enum(["tcp-server", "udp"]).optional().describe("Transport protocol (set action only)"),
8353
- certificate: z36.string().optional().describe("Server certificate name (set action only)"),
8354
- cipher: z36.enum(["blowfish128", "aes128-cbc", "aes192-cbc", "aes256-cbc", "aes128-gcm", "aes256-gcm", "none"]).optional().describe("Encryption cipher (set action only)"),
8355
- auth: z36.enum(["md5", "sha1", "sha256", "sha512", "null"]).optional().describe("Authentication algorithm (set action only)"),
8356
- dryRun: z36.boolean().default(false).describe("Preview changes without applying")
10489
+ var manageInterfaceListInputSchema = z44.object({
10490
+ routerId: z44.string().describe("Target router identifier from the router registry"),
10491
+ action: z44.enum(["add", "remove"]).describe("Action to perform"),
10492
+ name: z44.string().describe("Interface list name \u2014 idempotency key"),
10493
+ comment: z44.string().optional().describe("Optional comment"),
10494
+ dryRun: z44.boolean().default(false).describe("Preview changes without applying")
8357
10495
  }).strict();
8358
- var manageOvpnServerTool = {
8359
- name: "manage_ovpn_server",
8360
- title: "Manage OpenVPN Server",
8361
- description: "Enable, disable, or configure the OpenVPN server on a MikroTik router. The server is a singleton \u2014 there is only one per router. Throws NOT_FOUND if the OpenVPN package is not installed. For set action, at least one configuration field must be provided.",
8362
- inputSchema: manageOvpnServerInputSchema,
8363
- annotations: {
8364
- readOnlyHint: false,
8365
- destructiveHint: true,
8366
- idempotentHint: true,
8367
- openWorldHint: false
8368
- },
8369
- snapshotPaths: [OVPN_SERVER_PATH],
10496
+ var manageInterfaceListTool = {
10497
+ name: "manage_interface_list",
10498
+ title: "Manage Interface List",
10499
+ description: "Add or remove an interface list. Idempotent by name. Removing a list that has members is blocked by RouterOS \u2014 the error is surfaced as-is.",
10500
+ inputSchema: manageInterfaceListInputSchema,
10501
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
8370
10502
  async handler(params, context) {
8371
- const parsed = manageOvpnServerInputSchema.parse(params);
8372
- log37.info({ routerId: context.routerId, action: parsed.action }, "Managing OpenVPN server");
10503
+ const parsed = manageInterfaceListInputSchema.parse(params);
10504
+ log45.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing interface list");
8373
10505
  try {
8374
- const records = await context.routerClient.get(OVPN_SERVER_PATH, {
10506
+ const all = await context.routerClient.get("interface/list", {
8375
10507
  limit: void 0,
8376
10508
  offset: void 0
8377
10509
  });
8378
- const server = fetchOvpnServer(records, parsed.routerId);
8379
- const serverId = server[".id"];
8380
- if (parsed.action === "enable" || parsed.action === "disable") {
8381
- const desiredEnabled = parsed.action === "enable";
8382
- const currentEnabled = server.enabled === "yes";
8383
- if (desiredEnabled === currentEnabled) {
10510
+ const existing = all.find((l) => l.name === parsed.name);
10511
+ if (parsed.action === "add") {
10512
+ if (existing) {
8384
10513
  return {
8385
- content: `OpenVPN server on ${context.routerId} is already ${parsed.action}d. No changes made.`,
8386
- structuredContent: { action: "no_change", server }
10514
+ content: `Interface list "${parsed.name}" already exists. No changes made.`,
10515
+ structuredContent: { action: "already_exists", name: parsed.name }
8387
10516
  };
8388
10517
  }
8389
10518
  if (parsed.dryRun) {
8390
10519
  return {
8391
- content: `Dry run: Would ${parsed.action} OpenVPN server on ${context.routerId}.`,
10520
+ content: `Dry run: Would create interface list "${parsed.name}".`,
8392
10521
  structuredContent: {
8393
10522
  action: "dry_run",
8394
- diff: [{ property: "enabled", before: server.enabled, after: desiredEnabled ? "yes" : "no" }]
10523
+ diff: [{ property: "name", before: null, after: parsed.name }]
8395
10524
  }
8396
10525
  };
8397
10526
  }
8398
- await context.routerClient.update(OVPN_SERVER_PATH, serverId, {
8399
- enabled: desiredEnabled ? "yes" : "no"
8400
- });
8401
- log37.info({ routerId: context.routerId, action: parsed.action }, "OpenVPN server updated");
10527
+ const body = { name: parsed.name };
10528
+ if (parsed.comment) body.comment = parsed.comment;
10529
+ const created = await context.routerClient.create("interface/list", body);
10530
+ log45.info({ name: parsed.name, id: created[".id"] }, "Interface list created");
8402
10531
  return {
8403
- content: `OpenVPN server on ${context.routerId} ${parsed.action}d.`,
8404
- structuredContent: { action: parsed.action === "enable" ? "enabled" : "disabled", server }
10532
+ content: `Created interface list "${parsed.name}".`,
10533
+ structuredContent: { action: "created", name: parsed.name, id: created[".id"] }
8405
10534
  };
8406
10535
  }
8407
- const hasFields = parsed.port !== void 0 || parsed.mode !== void 0 || parsed.protocol !== void 0 || parsed.certificate !== void 0 || parsed.cipher !== void 0 || parsed.auth !== void 0;
8408
- if (!hasFields) {
8409
- throw new MikroMCPError({
8410
- category: "VALIDATION" /* VALIDATION */,
8411
- code: "OVPN_SERVER_NO_FIELDS",
8412
- message: "At least one field must be provided for action set.",
8413
- details: {},
8414
- recoverability: {
8415
- retryable: false,
8416
- suggestedAction: "Provide at least one of: port, mode, protocol, certificate, cipher, auth.",
8417
- alternativeTools: []
8418
- }
8419
- });
8420
- }
8421
- const updates = {};
8422
- const diff = [];
8423
- if (parsed.port !== void 0 && server.port !== String(parsed.port)) {
8424
- updates.port = String(parsed.port);
8425
- diff.push({ property: "port", before: server.port ?? null, after: String(parsed.port) });
8426
- }
8427
- if (parsed.mode !== void 0 && server.mode !== parsed.mode) {
8428
- updates.mode = parsed.mode;
8429
- diff.push({ property: "mode", before: server.mode ?? null, after: parsed.mode });
8430
- }
8431
- if (parsed.protocol !== void 0 && server.protocol !== parsed.protocol) {
8432
- updates.protocol = parsed.protocol;
8433
- diff.push({ property: "protocol", before: server.protocol ?? null, after: parsed.protocol });
8434
- }
8435
- if (parsed.certificate !== void 0 && server.certificate !== parsed.certificate) {
8436
- updates.certificate = parsed.certificate;
8437
- diff.push({ property: "certificate", before: server.certificate ?? null, after: parsed.certificate });
10536
+ if (!existing) {
10537
+ return {
10538
+ content: `Interface list "${parsed.name}" not found. Nothing to remove.`,
10539
+ structuredContent: { action: "not_found", name: parsed.name }
10540
+ };
8438
10541
  }
8439
- if (parsed.cipher !== void 0 && server.cipher !== parsed.cipher) {
8440
- updates.cipher = parsed.cipher;
8441
- diff.push({ property: "cipher", before: server.cipher ?? null, after: parsed.cipher });
10542
+ if (parsed.dryRun) {
10543
+ return {
10544
+ content: `Dry run: Would remove interface list "${parsed.name}".`,
10545
+ structuredContent: {
10546
+ action: "dry_run",
10547
+ diff: [{ property: "name", before: parsed.name, after: null }]
10548
+ }
10549
+ };
8442
10550
  }
8443
- if (parsed.auth !== void 0 && server.auth !== parsed.auth) {
8444
- updates.auth = parsed.auth;
8445
- diff.push({ property: "auth", before: server.auth ?? null, after: parsed.auth });
10551
+ await context.routerClient.remove("interface/list", existing[".id"]);
10552
+ log45.info({ name: parsed.name }, "Interface list removed");
10553
+ return {
10554
+ content: `Removed interface list "${parsed.name}".`,
10555
+ structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
10556
+ };
10557
+ } catch (err) {
10558
+ if (err instanceof MikroMCPError) throw err;
10559
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_interface_list" });
10560
+ }
10561
+ }
10562
+ };
10563
+ var manageInterfaceListMemberInputSchema = z44.object({
10564
+ routerId: z44.string().describe("Target router identifier from the router registry"),
10565
+ action: z44.enum(["add", "remove"]).describe("Action to perform"),
10566
+ list: z44.string().describe("Interface list name \u2014 part of composite idempotency key"),
10567
+ interface: z44.string().describe("Interface name to add/remove \u2014 part of composite idempotency key"),
10568
+ comment: z44.string().optional().describe("Optional comment"),
10569
+ dryRun: z44.boolean().default(false).describe("Preview changes without applying")
10570
+ }).strict();
10571
+ var manageInterfaceListMemberTool = {
10572
+ name: "manage_interface_list_member",
10573
+ title: "Manage Interface List Member",
10574
+ description: "Add or remove an interface from an interface list. Idempotent by list+interface composite key. add returns already_exists if the membership exists. remove returns not_found gracefully.",
10575
+ inputSchema: manageInterfaceListMemberInputSchema,
10576
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
10577
+ async handler(params, context) {
10578
+ const parsed = manageInterfaceListMemberInputSchema.parse(params);
10579
+ log45.info(
10580
+ { routerId: context.routerId, action: parsed.action, list: parsed.list, interface: parsed.interface },
10581
+ "Managing interface list member"
10582
+ );
10583
+ try {
10584
+ const all = await context.routerClient.get("interface/list/member", {
10585
+ limit: void 0,
10586
+ offset: void 0
10587
+ });
10588
+ const existing = all.find(
10589
+ (m) => m.list === parsed.list && m.interface === parsed.interface
10590
+ );
10591
+ if (parsed.action === "add") {
10592
+ if (existing) {
10593
+ return {
10594
+ content: `Interface "${parsed.interface}" is already a member of list "${parsed.list}". No changes made.`,
10595
+ structuredContent: { action: "already_exists", list: parsed.list, interface: parsed.interface }
10596
+ };
10597
+ }
10598
+ if (parsed.dryRun) {
10599
+ return {
10600
+ content: `Dry run: Would add "${parsed.interface}" to list "${parsed.list}".`,
10601
+ structuredContent: {
10602
+ action: "dry_run",
10603
+ diff: [{ property: "interface", before: null, after: parsed.interface }]
10604
+ }
10605
+ };
10606
+ }
10607
+ const body = { list: parsed.list, interface: parsed.interface };
10608
+ if (parsed.comment) body.comment = parsed.comment;
10609
+ const created = await context.routerClient.create("interface/list/member", body);
10610
+ log45.info({ list: parsed.list, interface: parsed.interface, id: created[".id"] }, "Interface list member added");
10611
+ return {
10612
+ content: `Added "${parsed.interface}" to list "${parsed.list}".`,
10613
+ structuredContent: { action: "created", list: parsed.list, interface: parsed.interface, id: created[".id"] }
10614
+ };
8446
10615
  }
8447
- if (Object.keys(updates).length === 0) {
10616
+ if (!existing) {
8448
10617
  return {
8449
- content: `OpenVPN server on ${context.routerId} already matches the requested configuration. No changes made.`,
8450
- structuredContent: { action: "no_change", server }
10618
+ content: `Interface "${parsed.interface}" is not a member of list "${parsed.list}". Nothing to remove.`,
10619
+ structuredContent: { action: "not_found", list: parsed.list, interface: parsed.interface }
8451
10620
  };
8452
10621
  }
8453
10622
  if (parsed.dryRun) {
8454
10623
  return {
8455
- content: `Dry run: Would update OpenVPN server on ${context.routerId}.`,
8456
- structuredContent: { action: "dry_run", diff }
10624
+ content: `Dry run: Would remove "${parsed.interface}" from list "${parsed.list}".`,
10625
+ structuredContent: {
10626
+ action: "dry_run",
10627
+ diff: [{ property: "interface", before: parsed.interface, after: null }]
10628
+ }
8457
10629
  };
8458
10630
  }
8459
- await context.routerClient.update(OVPN_SERVER_PATH, serverId, updates);
8460
- log37.info({ routerId: context.routerId }, "OpenVPN server configuration updated");
10631
+ await context.routerClient.remove("interface/list/member", existing[".id"]);
10632
+ log45.info({ list: parsed.list, interface: parsed.interface }, "Interface list member removed");
8461
10633
  return {
8462
- content: `Updated OpenVPN server configuration on ${context.routerId}.`,
8463
- structuredContent: { action: "updated", diff }
10634
+ content: `Removed "${parsed.interface}" from list "${parsed.list}".`,
10635
+ structuredContent: {
10636
+ action: "removed",
10637
+ list: parsed.list,
10638
+ interface: parsed.interface,
10639
+ id: existing[".id"]
10640
+ }
8464
10641
  };
8465
10642
  } catch (err) {
8466
10643
  if (err instanceof MikroMCPError) throw err;
8467
- throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_server" });
10644
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_interface_list_member" });
8468
10645
  }
8469
10646
  }
8470
10647
  };
8471
- var ovpnTools = [
8472
- listOvpnClientsTool,
8473
- manageOvpnClientTool,
8474
- getOvpnServerTool,
8475
- manageOvpnServerTool
10648
+ var interfaceListTools = [
10649
+ listInterfaceListsTool,
10650
+ manageInterfaceListTool,
10651
+ manageInterfaceListMemberTool
8476
10652
  ];
8477
10653
 
8478
10654
  // src/domain/tools/fleet-tools.ts
8479
- import { z as z37 } from "zod";
10655
+ import { z as z45 } from "zod";
8480
10656
 
8481
10657
  // src/middleware/authz.ts
8482
10658
  function matchesPattern(name, pattern) {
@@ -8644,7 +10820,7 @@ var SshClient = class {
8644
10820
  // src/adapter/ftp-client.ts
8645
10821
  import { Client as Client2 } from "basic-ftp";
8646
10822
  import { Readable } from "stream";
8647
- var log38 = createLogger("ftp-client");
10823
+ var log46 = createLogger("ftp-client");
8648
10824
  var FtpClient = class {
8649
10825
  #host;
8650
10826
  #port;
@@ -8657,7 +10833,7 @@ var FtpClient = class {
8657
10833
  this.#password = password;
8658
10834
  }
8659
10835
  async upload(remoteName, content) {
8660
- log38.info({ host: this.#host, remoteName }, "Uploading file via FTP");
10836
+ log46.info({ host: this.#host, remoteName }, "Uploading file via FTP");
8661
10837
  const client = new Client2();
8662
10838
  try {
8663
10839
  await client.access({
@@ -8667,7 +10843,7 @@ var FtpClient = class {
8667
10843
  password: this.#password
8668
10844
  });
8669
10845
  await client.uploadFrom(Readable.from(Buffer.from(content, "utf-8")), remoteName);
8670
- log38.info({ host: this.#host, remoteName }, "FTP upload complete");
10846
+ log46.info({ host: this.#host, remoteName }, "FTP upload complete");
8671
10847
  } finally {
8672
10848
  client.close();
8673
10849
  }
@@ -8775,17 +10951,17 @@ function checkFleetConfirmation(args, secret) {
8775
10951
  }
8776
10952
 
8777
10953
  // src/domain/tools/fleet-tools.ts
8778
- var log39 = createLogger("fleet-tools");
8779
- var checkHealthInputSchema = z37.object({
8780
- routerId: z37.string().describe("Target router to health-check")
10954
+ var log47 = createLogger("fleet-tools");
10955
+ var checkHealthInputSchema = z45.object({
10956
+ routerId: z45.string().describe("Target router to health-check")
8781
10957
  }).strict();
8782
- var bulkExecuteInputSchema = z37.object({
8783
- toolName: z37.string().describe("Name of the tool to fan out (must be a single-router tool)"),
8784
- routerIds: z37.array(z37.string()).optional().describe("Explicit list of router IDs to target"),
8785
- tags: z37.array(z37.string()).optional().describe("Target all routers with ALL of these tags (mutually exclusive with routerIds)"),
8786
- params: z37.record(z37.string(), z37.unknown()).describe("Params to pass to the tool (omit routerId \u2014 injected per router)"),
8787
- concurrency: z37.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls"),
8788
- confirmationToken: z37.string().optional().describe(
10958
+ var bulkExecuteInputSchema = z45.object({
10959
+ toolName: z45.string().describe("Name of the tool to fan out (must be a single-router tool)"),
10960
+ routerIds: z45.array(z45.string()).optional().describe("Explicit list of router IDs to target"),
10961
+ tags: z45.array(z45.string()).optional().describe("Target all routers with ALL of these tags (mutually exclusive with routerIds)"),
10962
+ params: z45.record(z45.string(), z45.unknown()).describe("Params to pass to the tool (omit routerId \u2014 injected per router)"),
10963
+ concurrency: z45.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls"),
10964
+ confirmationToken: z45.string().optional().describe(
8789
10965
  "Fleet confirmation token from a prior APPROVAL_REQUIRED response. Required to fan out a destructive tool."
8790
10966
  )
8791
10967
  }).strict();
@@ -8804,7 +10980,7 @@ function createFleetTools(baseTools2) {
8804
10980
  },
8805
10981
  async handler(params, context) {
8806
10982
  checkHealthInputSchema.parse(params);
8807
- log39.info({ routerId: context.routerId }, "Checking router health");
10983
+ log47.info({ routerId: context.routerId }, "Checking router health");
8808
10984
  const startMs = Date.now();
8809
10985
  try {
8810
10986
  const resources = await context.routerClient.get("system/resource");
@@ -8866,7 +11042,7 @@ function createFleetTools(baseTools2) {
8866
11042
  }
8867
11043
  });
8868
11044
  }
8869
- log39.info(
11045
+ log47.info(
8870
11046
  { toolName: parsed.toolName, concurrency: parsed.concurrency },
8871
11047
  "bulk_execute invoked"
8872
11048
  );
@@ -8913,7 +11089,7 @@ function createFleetTools(baseTools2) {
8913
11089
  try {
8914
11090
  resolved.push(context.routerRegistry.getRouter(id));
8915
11091
  } catch {
8916
- log39.warn({ routerId: id }, "bulk_execute: router not found");
11092
+ log47.warn({ routerId: id }, "bulk_execute: router not found");
8917
11093
  preErrors.push({
8918
11094
  routerId: id,
8919
11095
  status: "error",
@@ -9084,6 +11260,7 @@ var baseTools = [
9084
11260
  ...ipsecTools,
9085
11261
  ...certificateTools,
9086
11262
  ...userTools,
11263
+ ...userGroupTools,
9087
11264
  ...dhcpServerTools,
9088
11265
  ...ipPoolTools,
9089
11266
  ...queueTools,
@@ -9093,7 +11270,14 @@ var baseTools = [
9093
11270
  ...dhcpClientTools,
9094
11271
  ...ipServiceTools,
9095
11272
  ...pppoeTools,
9096
- ...ovpnTools
11273
+ ...ovpnTools,
11274
+ ...pppTools,
11275
+ ...upgradeTools,
11276
+ ...backupTools,
11277
+ ...logTools,
11278
+ ...networkTestTools,
11279
+ ...containerConfigTools,
11280
+ ...interfaceListTools
9097
11281
  ];
9098
11282
  var allTools = [
9099
11283
  ...baseTools,
@@ -9492,7 +11676,7 @@ var CircuitBreaker = class {
9492
11676
  };
9493
11677
 
9494
11678
  // src/mcp/tool-executor.ts
9495
- var log40 = createLogger("tool-executor");
11679
+ var log48 = createLogger("tool-executor");
9496
11680
  async function executeToolCall(tool, args, deps) {
9497
11681
  const { registry, pool, circuitBreakers, config, identityRegistry } = deps;
9498
11682
  const correlationId = nanoid4();
@@ -9588,9 +11772,9 @@ async function executeToolCall(tool, args, deps) {
9588
11772
  try {
9589
11773
  const meta = await takeSnapshot(toolContext.routerClient, routerId, path, config.snapshotDir);
9590
11774
  snapshotIds.push(meta.id);
9591
- log40.debug({ snapshotId: meta.id, path }, "Snapshot taken");
11775
+ log48.debug({ snapshotId: meta.id, path }, "Snapshot taken");
9592
11776
  } catch (err) {
9593
- log40.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
11777
+ log48.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
9594
11778
  }
9595
11779
  }
9596
11780
  }
@@ -9632,7 +11816,7 @@ async function executeToolCall(tool, args, deps) {
9632
11816
  durationMs: Date.now() - startMs
9633
11817
  }, config.auditLogPath);
9634
11818
  }
9635
- log40.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
11819
+ log48.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
9636
11820
  recordToolCall(tool.name, "success");
9637
11821
  return formatToolResult(result);
9638
11822
  } catch (err) {
@@ -9641,7 +11825,7 @@ async function executeToolCall(tool, args, deps) {
9641
11825
  const failedRouterId = args.routerId;
9642
11826
  if (failedRouterId) {
9643
11827
  pool.removeClient(failedRouterId);
9644
- log40.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
11828
+ log48.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
9645
11829
  }
9646
11830
  }
9647
11831
  if (journalId) {
@@ -9671,7 +11855,7 @@ async function executeToolCall(tool, args, deps) {
9671
11855
  durationMs: Date.now() - startMs
9672
11856
  }, config.auditLogPath);
9673
11857
  }
9674
- log40.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
11858
+ log48.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
9675
11859
  recordToolCall(tool.name, "error");
9676
11860
  return formatError(error);
9677
11861
  }
@@ -9679,12 +11863,12 @@ async function executeToolCall(tool, args, deps) {
9679
11863
  }
9680
11864
 
9681
11865
  // src/mcp/tool-registry.ts
9682
- var log41 = createLogger("tool-registry");
11866
+ var log49 = createLogger("tool-registry");
9683
11867
  function registerAllTools(server, registry, pool, circuitBreakers, config, identityRegistry) {
9684
11868
  const deps = { registry, pool, circuitBreakers, config, identityRegistry };
9685
11869
  for (const tool of allTools) {
9686
11870
  const registrationSchema = tool.annotations.destructiveHint ? tool.inputSchema.extend({
9687
- confirmationToken: z38.string().optional().describe(
11871
+ confirmationToken: z46.string().optional().describe(
9688
11872
  "Token from a prior APPROVAL_REQUIRED response. Re-submit the identical call with this token to confirm the destructive action."
9689
11873
  )
9690
11874
  }) : tool.inputSchema;
@@ -9698,56 +11882,56 @@ function registerAllTools(server, registry, pool, circuitBreakers, config, ident
9698
11882
  },
9699
11883
  (args) => executeToolCall(tool, args, deps)
9700
11884
  );
9701
- log41.debug({ tool: tool.name }, "Registered tool");
11885
+ log49.debug({ tool: tool.name }, "Registered tool");
9702
11886
  }
9703
- log41.info({ count: allTools.length }, "All tools registered");
11887
+ log49.info({ count: allTools.length }, "All tools registered");
9704
11888
  }
9705
11889
 
9706
11890
  // src/config/router-registry.ts
9707
11891
  import { readFileSync as readFileSync2, existsSync } from "fs";
9708
11892
  import { parse } from "yaml";
9709
- import { z as z39 } from "zod";
9710
- var log42 = createLogger("router-registry");
9711
- var RouterConfigSchema = z39.object({
9712
- host: z39.string().min(1, "host is required"),
9713
- port: z39.number().int().min(1).max(65535),
9714
- tls: z39.object({
9715
- enabled: z39.boolean(),
9716
- rejectUnauthorized: z39.boolean(),
9717
- ca: z39.string().optional(),
9718
- fingerprint: z39.string().optional()
11893
+ import { z as z47 } from "zod";
11894
+ var log50 = createLogger("router-registry");
11895
+ var RouterConfigSchema = z47.object({
11896
+ host: z47.string().min(1, "host is required"),
11897
+ port: z47.number().int().min(1).max(65535),
11898
+ tls: z47.object({
11899
+ enabled: z47.boolean(),
11900
+ rejectUnauthorized: z47.boolean(),
11901
+ ca: z47.string().optional(),
11902
+ fingerprint: z47.string().optional()
9719
11903
  }).strict(),
9720
- credentials: z39.object({
9721
- source: z39.enum(["env", "vault"]),
9722
- envPrefix: z39.string().optional(),
9723
- vaultPath: z39.string().optional()
11904
+ credentials: z47.object({
11905
+ source: z47.enum(["env", "vault"]),
11906
+ envPrefix: z47.string().optional(),
11907
+ vaultPath: z47.string().optional()
9724
11908
  }).strict(),
9725
- tags: z39.array(z39.string()).default([]),
9726
- rosVersion: z39.string().min(1),
9727
- sshPort: z39.number().int().min(1).max(65535).optional(),
9728
- sshFingerprint: z39.string().optional(),
9729
- cmdAllow: z39.array(z39.string()).optional(),
9730
- cmdDeny: z39.array(z39.string()).optional(),
9731
- maintenanceWindows: z39.array(
9732
- z39.object({
9733
- days: z39.array(
9734
- z39.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
11909
+ tags: z47.array(z47.string()).default([]),
11910
+ rosVersion: z47.string().min(1),
11911
+ sshPort: z47.number().int().min(1).max(65535).optional(),
11912
+ sshFingerprint: z47.string().optional(),
11913
+ cmdAllow: z47.array(z47.string()).optional(),
11914
+ cmdDeny: z47.array(z47.string()).optional(),
11915
+ maintenanceWindows: z47.array(
11916
+ z47.object({
11917
+ days: z47.array(
11918
+ z47.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
9735
11919
  ),
9736
- startTime: z39.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
9737
- endTime: z39.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
9738
- timezone: z39.string().min(1)
11920
+ startTime: z47.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
11921
+ endTime: z47.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
11922
+ timezone: z47.string().min(1)
9739
11923
  }).strict()
9740
11924
  ).optional()
9741
11925
  }).strict();
9742
- var ConfigFileSchema = z39.object({
9743
- routers: z39.record(z39.string(), RouterConfigSchema)
11926
+ var ConfigFileSchema = z47.object({
11927
+ routers: z47.record(z47.string(), RouterConfigSchema)
9744
11928
  }).strict();
9745
11929
  var RouterRegistry = class {
9746
11930
  routers;
9747
11931
  constructor(configPath) {
9748
11932
  this.routers = /* @__PURE__ */ new Map();
9749
11933
  if (!existsSync(configPath)) {
9750
- log42.warn({ configPath }, "Router config file not found; starting with empty registry");
11934
+ log50.warn({ configPath }, "Router config file not found; starting with empty registry");
9751
11935
  return;
9752
11936
  }
9753
11937
  const raw = readFileSync2(configPath, "utf-8");
@@ -9761,14 +11945,14 @@ ${issues}`);
9761
11945
  for (const [id, config] of Object.entries(result.data.routers)) {
9762
11946
  const routerConfig = { ...config, id };
9763
11947
  if (routerConfig.tls.enabled && !routerConfig.tls.rejectUnauthorized) {
9764
- log42.warn(
11948
+ log50.warn(
9765
11949
  { routerId: id },
9766
11950
  "TLS certificate validation is DISABLED (rejectUnauthorized=false). Set tls.fingerprint to pin the server certificate instead."
9767
11951
  );
9768
11952
  }
9769
11953
  this.routers.set(id, routerConfig);
9770
11954
  }
9771
- log42.info({ count: this.routers.size }, "Loaded routers from config");
11955
+ log50.info({ count: this.routers.size }, "Loaded routers from config");
9772
11956
  }
9773
11957
  getRouter(id) {
9774
11958
  const router = this.routers.get(id);
@@ -10053,7 +12237,7 @@ var ConnectionPool = class {
10053
12237
  };
10054
12238
 
10055
12239
  // src/version.ts
10056
- var VERSION = "1.3.0";
12240
+ var VERSION = "1.5.0";
10057
12241
 
10058
12242
  // src/mcp/server.ts
10059
12243
  function createServerFactory(config, identityRegistry) {
@@ -10083,7 +12267,7 @@ import { randomUUID } from "crypto";
10083
12267
  import { createServer } from "http";
10084
12268
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10085
12269
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
10086
- var log43 = createLogger("transport-http");
12270
+ var log51 = createLogger("transport-http");
10087
12271
  var BodyTooLargeError = class extends Error {
10088
12272
  constructor() {
10089
12273
  super("Request body exceeds maximum allowed size");
@@ -10175,7 +12359,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
10175
12359
  return;
10176
12360
  }
10177
12361
  if (!checkRateLimit(ip)) {
10178
- log43.warn({ ip }, "Rate limit exceeded");
12362
+ log51.warn({ ip }, "Rate limit exceeded");
10179
12363
  res.writeHead(429, { "Content-Type": "application/json" });
10180
12364
  res.end(JSON.stringify({ error: "Too many requests. Retry after 60 seconds." }));
10181
12365
  return;
@@ -10216,7 +12400,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
10216
12400
  }
10217
12401
  return;
10218
12402
  }
10219
- log43.error({ err }, "HTTP transport error");
12403
+ log51.error({ err }, "HTTP transport error");
10220
12404
  if (!res.headersSent) {
10221
12405
  res.writeHead(500, { "Content-Type": "application/json" });
10222
12406
  res.end(JSON.stringify({ error: "Internal server error" }));
@@ -10226,7 +12410,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
10226
12410
  httpServer.on("close", () => clearInterval(sweepTimer));
10227
12411
  await new Promise((resolve, reject) => {
10228
12412
  httpServer.listen(port, bindHost, () => {
10229
- log43.info(
12413
+ log51.info(
10230
12414
  { port, bindHost },
10231
12415
  "HTTP transport listening (POST /mcp for Streamable HTTP, GET /sse for legacy SSE)"
10232
12416
  );
@@ -10250,13 +12434,13 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
10250
12434
  sessionIdGenerator: () => randomUUID(),
10251
12435
  onsessioninitialized: (id) => {
10252
12436
  streamableSessions.set(id, transport2);
10253
- log43.info({ sessionId: id }, "Streamable HTTP session created");
12437
+ log51.info({ sessionId: id }, "Streamable HTTP session created");
10254
12438
  }
10255
12439
  });
10256
12440
  transport2.onclose = () => {
10257
12441
  if (transport2.sessionId) {
10258
12442
  streamableSessions.delete(transport2.sessionId);
10259
- log43.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
12443
+ log51.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
10260
12444
  }
10261
12445
  };
10262
12446
  await makeServer().connect(transport2);
@@ -10273,7 +12457,7 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
10273
12457
  sseTransport.onclose = () => {
10274
12458
  sseTransports.delete(sseTransport.sessionId);
10275
12459
  };
10276
- log43.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
12460
+ log51.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
10277
12461
  await sseTransport.start();
10278
12462
  return;
10279
12463
  }
@@ -10294,23 +12478,23 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
10294
12478
  // src/config/identity-registry.ts
10295
12479
  import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
10296
12480
  import { parse as parse2 } from "yaml";
10297
- import { z as z40 } from "zod";
12481
+ import { z as z48 } from "zod";
10298
12482
  import bcrypt from "bcryptjs";
10299
- var log44 = createLogger("identity-registry");
10300
- var IdentityConfigSchema = z40.object({
10301
- token: z40.string().min(1, "token hash is required"),
10302
- role: z40.enum(["readonly", "operator", "admin", "superadmin"]),
10303
- allowedRouters: z40.array(z40.string()).default([]),
10304
- allowedToolPatterns: z40.array(z40.string()).default([])
12483
+ var log52 = createLogger("identity-registry");
12484
+ var IdentityConfigSchema = z48.object({
12485
+ token: z48.string().min(1, "token hash is required"),
12486
+ role: z48.enum(["readonly", "operator", "admin", "superadmin"]),
12487
+ allowedRouters: z48.array(z48.string()).default([]),
12488
+ allowedToolPatterns: z48.array(z48.string()).default([])
10305
12489
  }).strict();
10306
- var ConfigFileSchema2 = z40.object({
10307
- identities: z40.record(z40.string(), IdentityConfigSchema)
12490
+ var ConfigFileSchema2 = z48.object({
12491
+ identities: z48.record(z48.string(), IdentityConfigSchema)
10308
12492
  }).strict();
10309
12493
  var IdentityRegistry = class {
10310
12494
  identities = [];
10311
12495
  constructor(configPath) {
10312
12496
  if (!existsSync2(configPath)) {
10313
- log44.warn(
12497
+ log52.warn(
10314
12498
  { configPath },
10315
12499
  "Identity config file not found; starting with empty registry (stdio callers do not require it)"
10316
12500
  );
@@ -10333,7 +12517,7 @@ ${issues}`);
10333
12517
  allowedToolPatterns: config.allowedToolPatterns
10334
12518
  });
10335
12519
  }
10336
- log44.info({ count: this.identities.length }, "Loaded identities from config");
12520
+ log52.info({ count: this.identities.length }, "Loaded identities from config");
10337
12521
  }
10338
12522
  getIdentities() {
10339
12523
  return this.identities.map(({ id, role, allowedRouters, allowedToolPatterns }) => ({
@@ -10362,7 +12546,7 @@ ${issues}`);
10362
12546
  // src/domain/snapshot/retention.ts
10363
12547
  import { readdir, stat, unlink } from "fs/promises";
10364
12548
  import { join as join3 } from "path";
10365
- var log45 = createLogger("snapshot-retention");
12549
+ var log53 = createLogger("snapshot-retention");
10366
12550
  async function pruneSnapshots(snapshotDir, maxAgeDays) {
10367
12551
  const cutoffMs = Date.now() - maxAgeDays * 864e5;
10368
12552
  let removed = 0;
@@ -10389,21 +12573,21 @@ async function pruneSnapshots(snapshotDir, maxAgeDays) {
10389
12573
  removed++;
10390
12574
  }
10391
12575
  } catch (err) {
10392
- log45.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
12576
+ log53.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
10393
12577
  }
10394
12578
  }
10395
12579
  }
10396
12580
  if (removed > 0) {
10397
- log45.info({ removed, maxAgeDays }, "Pruned expired snapshots");
12581
+ log53.info({ removed, maxAgeDays }, "Pruned expired snapshots");
10398
12582
  }
10399
12583
  return removed;
10400
12584
  }
10401
12585
 
10402
12586
  // src/cli/serve.ts
10403
- var log46 = createLogger("main");
12587
+ var log54 = createLogger("main");
10404
12588
  async function runServe() {
10405
12589
  const config = loadAppConfig();
10406
- log46.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
12590
+ log54.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
10407
12591
  void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
10408
12592
  const identityRegistry = new IdentityRegistry(config.identitiesPath);
10409
12593
  if (config.transport === "http") {
@@ -10411,7 +12595,7 @@ async function runServe() {
10411
12595
  (i) => i.role === "readonly" || i.role === "operator"
10412
12596
  );
10413
12597
  if (hasLimitedRoles && !config.confirmationSecret) {
10414
- log46.error(
12598
+ log54.error(
10415
12599
  "MIKROMCP_CONFIRMATION_SECRET is required in HTTP mode when identities with role readonly or operator are configured"
10416
12600
  );
10417
12601
  process.exit(1);
@@ -10421,7 +12605,7 @@ async function runServe() {
10421
12605
  if (config.transport === "stdio") {
10422
12606
  const stdioIdentity = getStdioIdentity(config.stdioIdentity, identityRegistry);
10423
12607
  await withIdentity(stdioIdentity, () => connectStdio(makeServer()));
10424
- log46.info("MikroMCP server running via stdio");
12608
+ log54.info("MikroMCP server running via stdio");
10425
12609
  } else if (config.transport === "http") {
10426
12610
  await connectHttp(
10427
12611
  makeServer,
@@ -10433,13 +12617,13 @@ async function runServe() {
10433
12617
  },
10434
12618
  identityRegistry
10435
12619
  );
10436
- log46.info(
12620
+ log54.info(
10437
12621
  { port: config.port, bindHost: config.bindHost },
10438
12622
  "MikroMCP server running via HTTP/SSE"
10439
12623
  );
10440
12624
  }
10441
12625
  const shutdown = () => {
10442
- log46.info("Shutting down MikroMCP server");
12626
+ log54.info("Shutting down MikroMCP server");
10443
12627
  pool.closeAll();
10444
12628
  process.exit(0);
10445
12629
  };
@@ -10452,7 +12636,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
10452
12636
  import { homedir as homedir2 } from "os";
10453
12637
  import * as net from "net";
10454
12638
  import chalk from "chalk";
10455
- var localVersion = "1.3.0";
12639
+ var localVersion = "1.5.0";
10456
12640
  var results = [];
10457
12641
  function ok(msg) {
10458
12642
  console.log(chalk.green("\u2705 ") + msg);
@@ -11104,7 +13288,7 @@ async function runInit() {
11104
13288
  }
11105
13289
 
11106
13290
  // src/cli/index.ts
11107
- var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.3.0");
13291
+ var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.5.0");
11108
13292
  program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
11109
13293
  program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
11110
13294
  program.command("init").description("Interactive setup wizard").action(runInit);