mikromcp 1.4.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 +1549 -237
  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 z42 } 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,86 +5890,239 @@ 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(
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(
5686
6126
  { routerId: context.routerId, action: parsed.action, name: parsed.name },
5687
6127
  "Managing certificate"
5688
6128
  );
@@ -8758,11 +9198,184 @@ var ovpnTools = [
8758
9198
  manageOvpnServerTool
8759
9199
  ];
8760
9200
 
8761
- // src/domain/tools/upgrade-tools.ts
9201
+ // src/domain/tools/ppp-tools.ts
8762
9202
  import { z as z38 } from "zod";
8763
- var log39 = createLogger("upgrade-tools");
8764
- var getUpgradeStatusInputSchema = z38.object({
8765
- routerId: z38.string().describe("Target router identifier from the router registry")
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")
8766
9379
  }).strict();
8767
9380
  var getUpgradeStatusTool = {
8768
9381
  name: "get_upgrade_status",
@@ -8777,7 +9390,7 @@ var getUpgradeStatusTool = {
8777
9390
  },
8778
9391
  async handler(params, context) {
8779
9392
  const parsed = getUpgradeStatusInputSchema.parse(params);
8780
- log39.info({ routerId: context.routerId }, "Getting upgrade status");
9393
+ log40.info({ routerId: context.routerId }, "Getting upgrade status");
8781
9394
  try {
8782
9395
  const [updateResults, routerboardResults] = await Promise.all([
8783
9396
  context.routerClient.get("system/package/update"),
@@ -8806,12 +9419,12 @@ var getUpgradeStatusTool = {
8806
9419
  }
8807
9420
  }
8808
9421
  };
8809
- var manageUpgradeInputSchema = z38.object({
8810
- routerId: z38.string().describe("Target router identifier from the router registry"),
8811
- action: z38.enum(["check", "install"]).describe(
9422
+ var manageUpgradeInputSchema = z39.object({
9423
+ routerId: z39.string().describe("Target router identifier from the router registry"),
9424
+ action: z39.enum(["check", "install"]).describe(
8812
9425
  "check \u2014 trigger a check for available updates; install \u2014 download and install the latest update (triggers reboot)"
8813
9426
  ),
8814
- dryRun: z38.boolean().default(false).describe("Preview the action without executing")
9427
+ dryRun: z39.boolean().default(false).describe("Preview the action without executing")
8815
9428
  }).strict();
8816
9429
  var manageUpgradeTool = {
8817
9430
  name: "manage_upgrade",
@@ -8826,7 +9439,7 @@ var manageUpgradeTool = {
8826
9439
  },
8827
9440
  async handler(params, context) {
8828
9441
  const parsed = manageUpgradeInputSchema.parse(params);
8829
- log39.info({ routerId: context.routerId, action: parsed.action }, "Managing upgrade");
9442
+ log40.info({ routerId: context.routerId, action: parsed.action }, "Managing upgrade");
8830
9443
  try {
8831
9444
  if (parsed.dryRun) {
8832
9445
  return {
@@ -8840,14 +9453,14 @@ var manageUpgradeTool = {
8840
9453
  }
8841
9454
  if (parsed.action === "check") {
8842
9455
  await context.routerClient.execute("system/package/update/check-for-updates");
8843
- log39.info({ routerId: context.routerId }, "Update check triggered");
9456
+ log40.info({ routerId: context.routerId }, "Update check triggered");
8844
9457
  return {
8845
9458
  content: `Update check triggered on ${parsed.routerId}.`,
8846
9459
  structuredContent: { action: "check_triggered", routerId: parsed.routerId }
8847
9460
  };
8848
9461
  }
8849
9462
  await context.routerClient.execute("system/package/update/install");
8850
- log39.info({ routerId: context.routerId }, "Package install triggered");
9463
+ log40.info({ routerId: context.routerId }, "Package install triggered");
8851
9464
  return {
8852
9465
  content: `Package install triggered on ${parsed.routerId}. Router will reboot automatically.`,
8853
9466
  structuredContent: { action: "install_triggered", routerId: parsed.routerId }
@@ -8860,13 +9473,13 @@ var manageUpgradeTool = {
8860
9473
  var upgradeTools = [getUpgradeStatusTool, manageUpgradeTool];
8861
9474
 
8862
9475
  // src/domain/tools/backup-tools.ts
8863
- import { z as z39 } from "zod";
8864
- var log40 = createLogger("backup-tools");
8865
- var createBackupInputSchema = z39.object({
8866
- routerId: z39.string().describe("Target router identifier from the router registry"),
8867
- name: z39.string().default("backup").describe("Backup file name (without extension)"),
8868
- password: z39.string().optional().describe("Password to encrypt the backup file"),
8869
- dryRun: z39.boolean().default(false).describe("Preview the backup without creating it")
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")
8870
9483
  }).strict();
8871
9484
  var createBackupTool = {
8872
9485
  name: "create_backup",
@@ -8881,7 +9494,7 @@ var createBackupTool = {
8881
9494
  },
8882
9495
  async handler(params, context) {
8883
9496
  const parsed = createBackupInputSchema.parse(params);
8884
- log40.info({ routerId: context.routerId, name: parsed.name }, "Creating backup");
9497
+ log41.info({ routerId: context.routerId, name: parsed.name }, "Creating backup");
8885
9498
  if (parsed.dryRun) {
8886
9499
  return {
8887
9500
  content: `Dry run: Would create backup "${parsed.name}.backup" on ${context.routerId}.`,
@@ -8894,7 +9507,7 @@ var createBackupTool = {
8894
9507
  body.password = parsed.password;
8895
9508
  }
8896
9509
  await context.routerClient.execute("system/backup/save", body);
8897
- log40.info({ routerId: context.routerId, name: parsed.name }, "Backup created");
9510
+ log41.info({ routerId: context.routerId, name: parsed.name }, "Backup created");
8898
9511
  return {
8899
9512
  content: `Backup "${parsed.name}.backup" created on ${context.routerId}.`,
8900
9513
  structuredContent: {
@@ -8909,10 +9522,10 @@ var createBackupTool = {
8909
9522
  }
8910
9523
  }
8911
9524
  };
8912
- var exportConfigInputSchema = z39.object({
8913
- routerId: z39.string().describe("Target router identifier from the router registry"),
8914
- compact: z39.boolean().default(false).describe("Export only non-default settings (compact format)"),
8915
- file: z39.string().optional().describe("Save the export to a file on the router (without extension); omit to return inline")
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")
8916
9529
  }).strict();
8917
9530
  var exportConfigTool = {
8918
9531
  name: "export_config",
@@ -8927,7 +9540,7 @@ var exportConfigTool = {
8927
9540
  },
8928
9541
  async handler(params, context) {
8929
9542
  const parsed = exportConfigInputSchema.parse(params);
8930
- log40.info({ routerId: context.routerId, compact: parsed.compact }, "Exporting config");
9543
+ log41.info({ routerId: context.routerId, compact: parsed.compact }, "Exporting config");
8931
9544
  try {
8932
9545
  if (parsed.file !== void 0) {
8933
9546
  const body = { file: parsed.file };
@@ -8961,16 +9574,16 @@ ${scriptText}`,
8961
9574
  var backupTools = [createBackupTool, exportConfigTool];
8962
9575
 
8963
9576
  // src/domain/tools/log-tools.ts
8964
- import { z as z40 } from "zod";
8965
- var log41 = createLogger("log-tools");
9577
+ import { z as z41 } from "zod";
9578
+ var log42 = createLogger("log-tools");
8966
9579
  var LOG_RULES_PATH = "system/logging";
8967
9580
  var LOG_ACTIONS_PATH = "system/logging/action";
8968
- var logActionTypeEnum = z40.enum(["memory", "disk", "remote", "echo", "email"]);
8969
- var listLogRulesInputSchema = z40.object({
8970
- routerId: z40.string().describe("Target router identifier from the router registry"),
8971
- topics: z40.string().optional().describe("Filter by topics (substring match)"),
8972
- logAction: z40.string().optional().describe("Filter by log action target (exact match on action field)"),
8973
- limit: z40.number().int().min(1).max(500).default(100).describe("Maximum number of rules to return (1-500, default 100)")
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)")
8974
9587
  }).strict();
8975
9588
  var listLogRulesTool = {
8976
9589
  name: "list_log_rules",
@@ -8985,7 +9598,7 @@ var listLogRulesTool = {
8985
9598
  },
8986
9599
  async handler(params, context) {
8987
9600
  const parsed = listLogRulesInputSchema.parse(params);
8988
- log41.info({ routerId: context.routerId }, "Listing log rules");
9601
+ log42.info({ routerId: context.routerId }, "Listing log rules");
8989
9602
  try {
8990
9603
  const all = await context.routerClient.get(LOG_RULES_PATH, {
8991
9604
  limit: void 0,
@@ -9014,13 +9627,13 @@ var listLogRulesTool = {
9014
9627
  }
9015
9628
  }
9016
9629
  };
9017
- var manageLogRuleInputSchema = z40.object({
9018
- routerId: z40.string().describe("Target router identifier from the router registry"),
9019
- action: z40.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
9020
- topics: z40.string().describe("Log topics (e.g. firewall, system, info) \u2014 idempotency key"),
9021
- logAction: z40.string().describe("Log action target name (RouterOS action field) \u2014 idempotency key"),
9022
- prefix: z40.string().optional().describe("Optional prefix to prepend to log messages"),
9023
- dryRun: z40.boolean().default(false).describe("Preview changes without applying")
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")
9024
9637
  }).strict();
9025
9638
  var manageLogRuleTool = {
9026
9639
  name: "manage_log_rule",
@@ -9036,7 +9649,7 @@ var manageLogRuleTool = {
9036
9649
  snapshotPaths: ["system/logging"],
9037
9650
  async handler(params, context) {
9038
9651
  const parsed = manageLogRuleInputSchema.parse(params);
9039
- log41.info(
9652
+ log42.info(
9040
9653
  { routerId: context.routerId, action: parsed.action, topics: parsed.topics },
9041
9654
  "Managing log rule"
9042
9655
  );
@@ -9078,7 +9691,7 @@ var manageLogRuleTool = {
9078
9691
  };
9079
9692
  }
9080
9693
  const created = await context.routerClient.create(LOG_RULES_PATH, body);
9081
- log41.info({ topics: parsed.topics, logAction: parsed.logAction, id: created[".id"] }, "Log rule added");
9694
+ log42.info({ topics: parsed.topics, logAction: parsed.logAction, id: created[".id"] }, "Log rule added");
9082
9695
  return {
9083
9696
  content: `Added log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9084
9697
  structuredContent: {
@@ -9106,7 +9719,7 @@ var manageLogRuleTool = {
9106
9719
  };
9107
9720
  }
9108
9721
  await context.routerClient.remove(LOG_RULES_PATH, id);
9109
- log41.info({ topics: parsed.topics, logAction: parsed.logAction, id }, "Log rule removed");
9722
+ log42.info({ topics: parsed.topics, logAction: parsed.logAction, id }, "Log rule removed");
9110
9723
  return {
9111
9724
  content: `Removed log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9112
9725
  structuredContent: { action: "removed", id }
@@ -9142,7 +9755,7 @@ var manageLogRuleTool = {
9142
9755
  disabled: wantDisabled ? "true" : "false"
9143
9756
  });
9144
9757
  const resultAction = wantDisabled ? "disabled" : "enabled";
9145
- log41.info({ topics: parsed.topics, id, action: resultAction }, "Log rule toggled");
9758
+ log42.info({ topics: parsed.topics, id, action: resultAction }, "Log rule toggled");
9146
9759
  return {
9147
9760
  content: `${wantDisabled ? "Disabled" : "Enabled"} log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
9148
9761
  structuredContent: { action: resultAction, id }
@@ -9163,10 +9776,10 @@ var manageLogRuleTool = {
9163
9776
  }
9164
9777
  }
9165
9778
  };
9166
- var listLogActionsInputSchema = z40.object({
9167
- routerId: z40.string().describe("Target router identifier from the router registry"),
9779
+ var listLogActionsInputSchema = z41.object({
9780
+ routerId: z41.string().describe("Target router identifier from the router registry"),
9168
9781
  type: logActionTypeEnum.optional().describe("Filter by action type (exact match): memory, disk, remote, echo, email"),
9169
- limit: z40.number().int().min(1).max(500).default(100).describe("Maximum number of actions to return (1-500, default 100)")
9782
+ limit: z41.number().int().min(1).max(500).default(100).describe("Maximum number of actions to return (1-500, default 100)")
9170
9783
  }).strict();
9171
9784
  var listLogActionsTool = {
9172
9785
  name: "list_log_actions",
@@ -9181,7 +9794,7 @@ var listLogActionsTool = {
9181
9794
  },
9182
9795
  async handler(params, context) {
9183
9796
  const parsed = listLogActionsInputSchema.parse(params);
9184
- log41.info({ routerId: context.routerId }, "Listing log actions");
9797
+ log42.info({ routerId: context.routerId }, "Listing log actions");
9185
9798
  try {
9186
9799
  const all = await context.routerClient.get(LOG_ACTIONS_PATH, {
9187
9800
  limit: void 0,
@@ -9207,15 +9820,15 @@ var listLogActionsTool = {
9207
9820
  }
9208
9821
  }
9209
9822
  };
9210
- var manageLogActionInputSchema = z40.object({
9211
- routerId: z40.string().describe("Target router identifier from the router registry"),
9212
- action: z40.enum(["add", "remove"]).describe("Action to perform"),
9213
- name: z40.string().describe("Log action name \u2014 idempotency key"),
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"),
9214
9827
  type: logActionTypeEnum.optional().describe("Action type: memory, disk, remote, echo, email (required on add)"),
9215
- remote: z40.string().optional().describe("Remote syslog server address (for type=remote)"),
9216
- remotePort: z40.number().int().min(1).max(65535).optional().describe("Remote syslog server port (for type=remote)"),
9217
- diskFileName: z40.string().optional().describe("Disk log file name without extension (for type=disk)"),
9218
- dryRun: z40.boolean().default(false).describe("Preview changes without applying")
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")
9219
9832
  }).strict();
9220
9833
  var manageLogActionTool = {
9221
9834
  name: "manage_log_action",
@@ -9231,7 +9844,7 @@ var manageLogActionTool = {
9231
9844
  snapshotPaths: ["system/logging/action"],
9232
9845
  async handler(params, context) {
9233
9846
  const parsed = manageLogActionInputSchema.parse(params);
9234
- log41.info(
9847
+ log42.info(
9235
9848
  { routerId: context.routerId, action: parsed.action, name: parsed.name },
9236
9849
  "Managing log action"
9237
9850
  );
@@ -9285,7 +9898,7 @@ var manageLogActionTool = {
9285
9898
  };
9286
9899
  }
9287
9900
  const created = await context.routerClient.create(LOG_ACTIONS_PATH, body);
9288
- log41.info({ name: parsed.name, type: parsed.type, id: created[".id"] }, "Log action added");
9901
+ log42.info({ name: parsed.name, type: parsed.type, id: created[".id"] }, "Log action added");
9289
9902
  return {
9290
9903
  content: `Added log action "${parsed.name}" (type=${parsed.type}).`,
9291
9904
  structuredContent: {
@@ -9315,7 +9928,7 @@ var manageLogActionTool = {
9315
9928
  };
9316
9929
  }
9317
9930
  await context.routerClient.remove(LOG_ACTIONS_PATH, id);
9318
- log41.info({ name: parsed.name, id }, "Log action removed");
9931
+ log42.info({ name: parsed.name, id }, "Log action removed");
9319
9932
  return {
9320
9933
  content: `Removed log action "${parsed.name}".`,
9321
9934
  structuredContent: { action: "removed", name: parsed.name, id }
@@ -9343,22 +9956,717 @@ var logTools = [
9343
9956
  manageLogActionTool
9344
9957
  ];
9345
9958
 
9346
- // src/domain/tools/fleet-tools.ts
9347
- import { z as z41 } from "zod";
9348
-
9349
- // src/middleware/authz.ts
9350
- function matchesPattern(name, pattern) {
9351
- if (!pattern.includes("*")) return name === pattern;
9352
- const prefix = pattern.slice(0, pattern.indexOf("*"));
9353
- return name.startsWith(prefix);
9354
- }
9355
- function checkAuthz(identity, toolName, routerId) {
9356
- if (identity.allowedRouters.length > 0 && !identity.allowedRouters.includes(routerId)) {
9357
- throw new MikroMCPError({
9358
- category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
9359
- code: "ROUTER_NOT_ALLOWED",
9360
- message: `Identity "${identity.id}" is not allowed to access router "${routerId}".`,
9361
- details: { identityId: identity.id, routerId, allowedRouters: identity.allowedRouters },
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
+ },
10386
+ recoverability: {
10387
+ retryable: false,
10388
+ suggestedAction: "Remove the existing mount first.",
10389
+ alternativeTools: ["manage_container_mount"]
10390
+ }
10391
+ });
10392
+ }
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
+ });
10400
+ }
10401
+ if (parsed.dryRun) {
10402
+ return {
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
+ }
10408
+ };
10409
+ }
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");
10416
+ return {
10417
+ content: `Added mount "${parsed.name}".`,
10418
+ structuredContent: { action: "created", name: parsed.name, id: created[".id"] }
10419
+ };
10420
+ }
10421
+ if (!existing) {
10422
+ return {
10423
+ content: `Mount "${parsed.name}" not found. Nothing to remove.`,
10424
+ structuredContent: { action: "not_found", name: parsed.name }
10425
+ };
10426
+ }
10427
+ if (parsed.dryRun) {
10428
+ return {
10429
+ content: `Dry run: Would remove mount "${parsed.name}".`,
10430
+ structuredContent: {
10431
+ action: "dry_run",
10432
+ diff: [{ property: "name", before: parsed.name, after: null }]
10433
+ }
10434
+ };
10435
+ }
10436
+ await context.routerClient.remove("container/mounts", existing[".id"]);
10437
+ log44.info({ name: parsed.name }, "Container mount removed");
10438
+ return {
10439
+ content: `Removed mount "${parsed.name}".`,
10440
+ structuredContent: { action: "removed", name: parsed.name }
10441
+ };
10442
+ } catch (err) {
10443
+ if (err instanceof MikroMCPError) throw err;
10444
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_container_mount" });
10445
+ }
10446
+ }
10447
+ };
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")
10463
+ }).strict();
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 },
10470
+ async handler(params, context) {
10471
+ const parsed = listInterfaceListsInputSchema.parse(params);
10472
+ log45.info({ routerId: context.routerId }, "Listing interface lists");
10473
+ try {
10474
+ const all = await context.routerClient.get("interface/list", {
10475
+ limit: void 0,
10476
+ offset: void 0
10477
+ });
10478
+ const lists = all.slice(0, parsed.limit);
10479
+ return {
10480
+ content: `Interface lists on ${context.routerId}: ${lists.length} returned`,
10481
+ structuredContent: { routerId: context.routerId, lists, total: all.length, returned: lists.length }
10482
+ };
10483
+ } catch (err) {
10484
+ if (err instanceof MikroMCPError) throw err;
10485
+ throw enrichError(err, { routerId: context.routerId, tool: "list_interface_lists" });
10486
+ }
10487
+ }
10488
+ };
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")
10495
+ }).strict();
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 },
10502
+ async handler(params, context) {
10503
+ const parsed = manageInterfaceListInputSchema.parse(params);
10504
+ log45.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing interface list");
10505
+ try {
10506
+ const all = await context.routerClient.get("interface/list", {
10507
+ limit: void 0,
10508
+ offset: void 0
10509
+ });
10510
+ const existing = all.find((l) => l.name === parsed.name);
10511
+ if (parsed.action === "add") {
10512
+ if (existing) {
10513
+ return {
10514
+ content: `Interface list "${parsed.name}" already exists. No changes made.`,
10515
+ structuredContent: { action: "already_exists", name: parsed.name }
10516
+ };
10517
+ }
10518
+ if (parsed.dryRun) {
10519
+ return {
10520
+ content: `Dry run: Would create interface list "${parsed.name}".`,
10521
+ structuredContent: {
10522
+ action: "dry_run",
10523
+ diff: [{ property: "name", before: null, after: parsed.name }]
10524
+ }
10525
+ };
10526
+ }
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");
10531
+ return {
10532
+ content: `Created interface list "${parsed.name}".`,
10533
+ structuredContent: { action: "created", name: parsed.name, id: created[".id"] }
10534
+ };
10535
+ }
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
+ };
10541
+ }
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
+ };
10550
+ }
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
+ };
10615
+ }
10616
+ if (!existing) {
10617
+ return {
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 }
10620
+ };
10621
+ }
10622
+ if (parsed.dryRun) {
10623
+ return {
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
+ }
10629
+ };
10630
+ }
10631
+ await context.routerClient.remove("interface/list/member", existing[".id"]);
10632
+ log45.info({ list: parsed.list, interface: parsed.interface }, "Interface list member removed");
10633
+ return {
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
+ }
10641
+ };
10642
+ } catch (err) {
10643
+ if (err instanceof MikroMCPError) throw err;
10644
+ throw enrichError(err, { routerId: context.routerId, tool: "manage_interface_list_member" });
10645
+ }
10646
+ }
10647
+ };
10648
+ var interfaceListTools = [
10649
+ listInterfaceListsTool,
10650
+ manageInterfaceListTool,
10651
+ manageInterfaceListMemberTool
10652
+ ];
10653
+
10654
+ // src/domain/tools/fleet-tools.ts
10655
+ import { z as z45 } from "zod";
10656
+
10657
+ // src/middleware/authz.ts
10658
+ function matchesPattern(name, pattern) {
10659
+ if (!pattern.includes("*")) return name === pattern;
10660
+ const prefix = pattern.slice(0, pattern.indexOf("*"));
10661
+ return name.startsWith(prefix);
10662
+ }
10663
+ function checkAuthz(identity, toolName, routerId) {
10664
+ if (identity.allowedRouters.length > 0 && !identity.allowedRouters.includes(routerId)) {
10665
+ throw new MikroMCPError({
10666
+ category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
10667
+ code: "ROUTER_NOT_ALLOWED",
10668
+ message: `Identity "${identity.id}" is not allowed to access router "${routerId}".`,
10669
+ details: { identityId: identity.id, routerId, allowedRouters: identity.allowedRouters },
9362
10670
  recoverability: {
9363
10671
  retryable: false,
9364
10672
  suggestedAction: `Add "${routerId}" to allowedRouters for identity "${identity.id}" in identities.yaml.`
@@ -9512,7 +10820,7 @@ var SshClient = class {
9512
10820
  // src/adapter/ftp-client.ts
9513
10821
  import { Client as Client2 } from "basic-ftp";
9514
10822
  import { Readable } from "stream";
9515
- var log42 = createLogger("ftp-client");
10823
+ var log46 = createLogger("ftp-client");
9516
10824
  var FtpClient = class {
9517
10825
  #host;
9518
10826
  #port;
@@ -9525,7 +10833,7 @@ var FtpClient = class {
9525
10833
  this.#password = password;
9526
10834
  }
9527
10835
  async upload(remoteName, content) {
9528
- log42.info({ host: this.#host, remoteName }, "Uploading file via FTP");
10836
+ log46.info({ host: this.#host, remoteName }, "Uploading file via FTP");
9529
10837
  const client = new Client2();
9530
10838
  try {
9531
10839
  await client.access({
@@ -9535,7 +10843,7 @@ var FtpClient = class {
9535
10843
  password: this.#password
9536
10844
  });
9537
10845
  await client.uploadFrom(Readable.from(Buffer.from(content, "utf-8")), remoteName);
9538
- log42.info({ host: this.#host, remoteName }, "FTP upload complete");
10846
+ log46.info({ host: this.#host, remoteName }, "FTP upload complete");
9539
10847
  } finally {
9540
10848
  client.close();
9541
10849
  }
@@ -9643,17 +10951,17 @@ function checkFleetConfirmation(args, secret) {
9643
10951
  }
9644
10952
 
9645
10953
  // src/domain/tools/fleet-tools.ts
9646
- var log43 = createLogger("fleet-tools");
9647
- var checkHealthInputSchema = z41.object({
9648
- routerId: z41.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")
9649
10957
  }).strict();
9650
- var bulkExecuteInputSchema = z41.object({
9651
- toolName: z41.string().describe("Name of the tool to fan out (must be a single-router tool)"),
9652
- routerIds: z41.array(z41.string()).optional().describe("Explicit list of router IDs to target"),
9653
- tags: z41.array(z41.string()).optional().describe("Target all routers with ALL of these tags (mutually exclusive with routerIds)"),
9654
- params: z41.record(z41.string(), z41.unknown()).describe("Params to pass to the tool (omit routerId \u2014 injected per router)"),
9655
- concurrency: z41.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls"),
9656
- confirmationToken: z41.string().optional().describe(
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(
9657
10965
  "Fleet confirmation token from a prior APPROVAL_REQUIRED response. Required to fan out a destructive tool."
9658
10966
  )
9659
10967
  }).strict();
@@ -9672,7 +10980,7 @@ function createFleetTools(baseTools2) {
9672
10980
  },
9673
10981
  async handler(params, context) {
9674
10982
  checkHealthInputSchema.parse(params);
9675
- log43.info({ routerId: context.routerId }, "Checking router health");
10983
+ log47.info({ routerId: context.routerId }, "Checking router health");
9676
10984
  const startMs = Date.now();
9677
10985
  try {
9678
10986
  const resources = await context.routerClient.get("system/resource");
@@ -9734,7 +11042,7 @@ function createFleetTools(baseTools2) {
9734
11042
  }
9735
11043
  });
9736
11044
  }
9737
- log43.info(
11045
+ log47.info(
9738
11046
  { toolName: parsed.toolName, concurrency: parsed.concurrency },
9739
11047
  "bulk_execute invoked"
9740
11048
  );
@@ -9781,7 +11089,7 @@ function createFleetTools(baseTools2) {
9781
11089
  try {
9782
11090
  resolved.push(context.routerRegistry.getRouter(id));
9783
11091
  } catch {
9784
- log43.warn({ routerId: id }, "bulk_execute: router not found");
11092
+ log47.warn({ routerId: id }, "bulk_execute: router not found");
9785
11093
  preErrors.push({
9786
11094
  routerId: id,
9787
11095
  status: "error",
@@ -9963,9 +11271,13 @@ var baseTools = [
9963
11271
  ...ipServiceTools,
9964
11272
  ...pppoeTools,
9965
11273
  ...ovpnTools,
11274
+ ...pppTools,
9966
11275
  ...upgradeTools,
9967
11276
  ...backupTools,
9968
- ...logTools
11277
+ ...logTools,
11278
+ ...networkTestTools,
11279
+ ...containerConfigTools,
11280
+ ...interfaceListTools
9969
11281
  ];
9970
11282
  var allTools = [
9971
11283
  ...baseTools,
@@ -10364,7 +11676,7 @@ var CircuitBreaker = class {
10364
11676
  };
10365
11677
 
10366
11678
  // src/mcp/tool-executor.ts
10367
- var log44 = createLogger("tool-executor");
11679
+ var log48 = createLogger("tool-executor");
10368
11680
  async function executeToolCall(tool, args, deps) {
10369
11681
  const { registry, pool, circuitBreakers, config, identityRegistry } = deps;
10370
11682
  const correlationId = nanoid4();
@@ -10460,9 +11772,9 @@ async function executeToolCall(tool, args, deps) {
10460
11772
  try {
10461
11773
  const meta = await takeSnapshot(toolContext.routerClient, routerId, path, config.snapshotDir);
10462
11774
  snapshotIds.push(meta.id);
10463
- log44.debug({ snapshotId: meta.id, path }, "Snapshot taken");
11775
+ log48.debug({ snapshotId: meta.id, path }, "Snapshot taken");
10464
11776
  } catch (err) {
10465
- log44.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
11777
+ log48.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
10466
11778
  }
10467
11779
  }
10468
11780
  }
@@ -10504,7 +11816,7 @@ async function executeToolCall(tool, args, deps) {
10504
11816
  durationMs: Date.now() - startMs
10505
11817
  }, config.auditLogPath);
10506
11818
  }
10507
- log44.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
11819
+ log48.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
10508
11820
  recordToolCall(tool.name, "success");
10509
11821
  return formatToolResult(result);
10510
11822
  } catch (err) {
@@ -10513,7 +11825,7 @@ async function executeToolCall(tool, args, deps) {
10513
11825
  const failedRouterId = args.routerId;
10514
11826
  if (failedRouterId) {
10515
11827
  pool.removeClient(failedRouterId);
10516
- log44.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
11828
+ log48.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
10517
11829
  }
10518
11830
  }
10519
11831
  if (journalId) {
@@ -10543,7 +11855,7 @@ async function executeToolCall(tool, args, deps) {
10543
11855
  durationMs: Date.now() - startMs
10544
11856
  }, config.auditLogPath);
10545
11857
  }
10546
- log44.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
11858
+ log48.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
10547
11859
  recordToolCall(tool.name, "error");
10548
11860
  return formatError(error);
10549
11861
  }
@@ -10551,12 +11863,12 @@ async function executeToolCall(tool, args, deps) {
10551
11863
  }
10552
11864
 
10553
11865
  // src/mcp/tool-registry.ts
10554
- var log45 = createLogger("tool-registry");
11866
+ var log49 = createLogger("tool-registry");
10555
11867
  function registerAllTools(server, registry, pool, circuitBreakers, config, identityRegistry) {
10556
11868
  const deps = { registry, pool, circuitBreakers, config, identityRegistry };
10557
11869
  for (const tool of allTools) {
10558
11870
  const registrationSchema = tool.annotations.destructiveHint ? tool.inputSchema.extend({
10559
- confirmationToken: z42.string().optional().describe(
11871
+ confirmationToken: z46.string().optional().describe(
10560
11872
  "Token from a prior APPROVAL_REQUIRED response. Re-submit the identical call with this token to confirm the destructive action."
10561
11873
  )
10562
11874
  }) : tool.inputSchema;
@@ -10570,56 +11882,56 @@ function registerAllTools(server, registry, pool, circuitBreakers, config, ident
10570
11882
  },
10571
11883
  (args) => executeToolCall(tool, args, deps)
10572
11884
  );
10573
- log45.debug({ tool: tool.name }, "Registered tool");
11885
+ log49.debug({ tool: tool.name }, "Registered tool");
10574
11886
  }
10575
- log45.info({ count: allTools.length }, "All tools registered");
11887
+ log49.info({ count: allTools.length }, "All tools registered");
10576
11888
  }
10577
11889
 
10578
11890
  // src/config/router-registry.ts
10579
11891
  import { readFileSync as readFileSync2, existsSync } from "fs";
10580
11892
  import { parse } from "yaml";
10581
- import { z as z43 } from "zod";
10582
- var log46 = createLogger("router-registry");
10583
- var RouterConfigSchema = z43.object({
10584
- host: z43.string().min(1, "host is required"),
10585
- port: z43.number().int().min(1).max(65535),
10586
- tls: z43.object({
10587
- enabled: z43.boolean(),
10588
- rejectUnauthorized: z43.boolean(),
10589
- ca: z43.string().optional(),
10590
- fingerprint: z43.string().optional()
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()
10591
11903
  }).strict(),
10592
- credentials: z43.object({
10593
- source: z43.enum(["env", "vault"]),
10594
- envPrefix: z43.string().optional(),
10595
- vaultPath: z43.string().optional()
11904
+ credentials: z47.object({
11905
+ source: z47.enum(["env", "vault"]),
11906
+ envPrefix: z47.string().optional(),
11907
+ vaultPath: z47.string().optional()
10596
11908
  }).strict(),
10597
- tags: z43.array(z43.string()).default([]),
10598
- rosVersion: z43.string().min(1),
10599
- sshPort: z43.number().int().min(1).max(65535).optional(),
10600
- sshFingerprint: z43.string().optional(),
10601
- cmdAllow: z43.array(z43.string()).optional(),
10602
- cmdDeny: z43.array(z43.string()).optional(),
10603
- maintenanceWindows: z43.array(
10604
- z43.object({
10605
- days: z43.array(
10606
- z43.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
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"])
10607
11919
  ),
10608
- startTime: z43.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
10609
- endTime: z43.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
10610
- timezone: z43.string().min(1)
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)
10611
11923
  }).strict()
10612
11924
  ).optional()
10613
11925
  }).strict();
10614
- var ConfigFileSchema = z43.object({
10615
- routers: z43.record(z43.string(), RouterConfigSchema)
11926
+ var ConfigFileSchema = z47.object({
11927
+ routers: z47.record(z47.string(), RouterConfigSchema)
10616
11928
  }).strict();
10617
11929
  var RouterRegistry = class {
10618
11930
  routers;
10619
11931
  constructor(configPath) {
10620
11932
  this.routers = /* @__PURE__ */ new Map();
10621
11933
  if (!existsSync(configPath)) {
10622
- log46.warn({ configPath }, "Router config file not found; starting with empty registry");
11934
+ log50.warn({ configPath }, "Router config file not found; starting with empty registry");
10623
11935
  return;
10624
11936
  }
10625
11937
  const raw = readFileSync2(configPath, "utf-8");
@@ -10633,14 +11945,14 @@ ${issues}`);
10633
11945
  for (const [id, config] of Object.entries(result.data.routers)) {
10634
11946
  const routerConfig = { ...config, id };
10635
11947
  if (routerConfig.tls.enabled && !routerConfig.tls.rejectUnauthorized) {
10636
- log46.warn(
11948
+ log50.warn(
10637
11949
  { routerId: id },
10638
11950
  "TLS certificate validation is DISABLED (rejectUnauthorized=false). Set tls.fingerprint to pin the server certificate instead."
10639
11951
  );
10640
11952
  }
10641
11953
  this.routers.set(id, routerConfig);
10642
11954
  }
10643
- log46.info({ count: this.routers.size }, "Loaded routers from config");
11955
+ log50.info({ count: this.routers.size }, "Loaded routers from config");
10644
11956
  }
10645
11957
  getRouter(id) {
10646
11958
  const router = this.routers.get(id);
@@ -10925,7 +12237,7 @@ var ConnectionPool = class {
10925
12237
  };
10926
12238
 
10927
12239
  // src/version.ts
10928
- var VERSION = "1.4.0";
12240
+ var VERSION = "1.5.0";
10929
12241
 
10930
12242
  // src/mcp/server.ts
10931
12243
  function createServerFactory(config, identityRegistry) {
@@ -10955,7 +12267,7 @@ import { randomUUID } from "crypto";
10955
12267
  import { createServer } from "http";
10956
12268
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10957
12269
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
10958
- var log47 = createLogger("transport-http");
12270
+ var log51 = createLogger("transport-http");
10959
12271
  var BodyTooLargeError = class extends Error {
10960
12272
  constructor() {
10961
12273
  super("Request body exceeds maximum allowed size");
@@ -11047,7 +12359,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
11047
12359
  return;
11048
12360
  }
11049
12361
  if (!checkRateLimit(ip)) {
11050
- log47.warn({ ip }, "Rate limit exceeded");
12362
+ log51.warn({ ip }, "Rate limit exceeded");
11051
12363
  res.writeHead(429, { "Content-Type": "application/json" });
11052
12364
  res.end(JSON.stringify({ error: "Too many requests. Retry after 60 seconds." }));
11053
12365
  return;
@@ -11088,7 +12400,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
11088
12400
  }
11089
12401
  return;
11090
12402
  }
11091
- log47.error({ err }, "HTTP transport error");
12403
+ log51.error({ err }, "HTTP transport error");
11092
12404
  if (!res.headersSent) {
11093
12405
  res.writeHead(500, { "Content-Type": "application/json" });
11094
12406
  res.end(JSON.stringify({ error: "Internal server error" }));
@@ -11098,7 +12410,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
11098
12410
  httpServer.on("close", () => clearInterval(sweepTimer));
11099
12411
  await new Promise((resolve, reject) => {
11100
12412
  httpServer.listen(port, bindHost, () => {
11101
- log47.info(
12413
+ log51.info(
11102
12414
  { port, bindHost },
11103
12415
  "HTTP transport listening (POST /mcp for Streamable HTTP, GET /sse for legacy SSE)"
11104
12416
  );
@@ -11122,13 +12434,13 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
11122
12434
  sessionIdGenerator: () => randomUUID(),
11123
12435
  onsessioninitialized: (id) => {
11124
12436
  streamableSessions.set(id, transport2);
11125
- log47.info({ sessionId: id }, "Streamable HTTP session created");
12437
+ log51.info({ sessionId: id }, "Streamable HTTP session created");
11126
12438
  }
11127
12439
  });
11128
12440
  transport2.onclose = () => {
11129
12441
  if (transport2.sessionId) {
11130
12442
  streamableSessions.delete(transport2.sessionId);
11131
- log47.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
12443
+ log51.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
11132
12444
  }
11133
12445
  };
11134
12446
  await makeServer().connect(transport2);
@@ -11145,7 +12457,7 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
11145
12457
  sseTransport.onclose = () => {
11146
12458
  sseTransports.delete(sseTransport.sessionId);
11147
12459
  };
11148
- log47.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
12460
+ log51.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
11149
12461
  await sseTransport.start();
11150
12462
  return;
11151
12463
  }
@@ -11166,23 +12478,23 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
11166
12478
  // src/config/identity-registry.ts
11167
12479
  import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
11168
12480
  import { parse as parse2 } from "yaml";
11169
- import { z as z44 } from "zod";
12481
+ import { z as z48 } from "zod";
11170
12482
  import bcrypt from "bcryptjs";
11171
- var log48 = createLogger("identity-registry");
11172
- var IdentityConfigSchema = z44.object({
11173
- token: z44.string().min(1, "token hash is required"),
11174
- role: z44.enum(["readonly", "operator", "admin", "superadmin"]),
11175
- allowedRouters: z44.array(z44.string()).default([]),
11176
- allowedToolPatterns: z44.array(z44.string()).default([])
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([])
11177
12489
  }).strict();
11178
- var ConfigFileSchema2 = z44.object({
11179
- identities: z44.record(z44.string(), IdentityConfigSchema)
12490
+ var ConfigFileSchema2 = z48.object({
12491
+ identities: z48.record(z48.string(), IdentityConfigSchema)
11180
12492
  }).strict();
11181
12493
  var IdentityRegistry = class {
11182
12494
  identities = [];
11183
12495
  constructor(configPath) {
11184
12496
  if (!existsSync2(configPath)) {
11185
- log48.warn(
12497
+ log52.warn(
11186
12498
  { configPath },
11187
12499
  "Identity config file not found; starting with empty registry (stdio callers do not require it)"
11188
12500
  );
@@ -11205,7 +12517,7 @@ ${issues}`);
11205
12517
  allowedToolPatterns: config.allowedToolPatterns
11206
12518
  });
11207
12519
  }
11208
- log48.info({ count: this.identities.length }, "Loaded identities from config");
12520
+ log52.info({ count: this.identities.length }, "Loaded identities from config");
11209
12521
  }
11210
12522
  getIdentities() {
11211
12523
  return this.identities.map(({ id, role, allowedRouters, allowedToolPatterns }) => ({
@@ -11234,7 +12546,7 @@ ${issues}`);
11234
12546
  // src/domain/snapshot/retention.ts
11235
12547
  import { readdir, stat, unlink } from "fs/promises";
11236
12548
  import { join as join3 } from "path";
11237
- var log49 = createLogger("snapshot-retention");
12549
+ var log53 = createLogger("snapshot-retention");
11238
12550
  async function pruneSnapshots(snapshotDir, maxAgeDays) {
11239
12551
  const cutoffMs = Date.now() - maxAgeDays * 864e5;
11240
12552
  let removed = 0;
@@ -11261,21 +12573,21 @@ async function pruneSnapshots(snapshotDir, maxAgeDays) {
11261
12573
  removed++;
11262
12574
  }
11263
12575
  } catch (err) {
11264
- log49.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
12576
+ log53.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
11265
12577
  }
11266
12578
  }
11267
12579
  }
11268
12580
  if (removed > 0) {
11269
- log49.info({ removed, maxAgeDays }, "Pruned expired snapshots");
12581
+ log53.info({ removed, maxAgeDays }, "Pruned expired snapshots");
11270
12582
  }
11271
12583
  return removed;
11272
12584
  }
11273
12585
 
11274
12586
  // src/cli/serve.ts
11275
- var log50 = createLogger("main");
12587
+ var log54 = createLogger("main");
11276
12588
  async function runServe() {
11277
12589
  const config = loadAppConfig();
11278
- log50.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
12590
+ log54.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
11279
12591
  void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
11280
12592
  const identityRegistry = new IdentityRegistry(config.identitiesPath);
11281
12593
  if (config.transport === "http") {
@@ -11283,7 +12595,7 @@ async function runServe() {
11283
12595
  (i) => i.role === "readonly" || i.role === "operator"
11284
12596
  );
11285
12597
  if (hasLimitedRoles && !config.confirmationSecret) {
11286
- log50.error(
12598
+ log54.error(
11287
12599
  "MIKROMCP_CONFIRMATION_SECRET is required in HTTP mode when identities with role readonly or operator are configured"
11288
12600
  );
11289
12601
  process.exit(1);
@@ -11293,7 +12605,7 @@ async function runServe() {
11293
12605
  if (config.transport === "stdio") {
11294
12606
  const stdioIdentity = getStdioIdentity(config.stdioIdentity, identityRegistry);
11295
12607
  await withIdentity(stdioIdentity, () => connectStdio(makeServer()));
11296
- log50.info("MikroMCP server running via stdio");
12608
+ log54.info("MikroMCP server running via stdio");
11297
12609
  } else if (config.transport === "http") {
11298
12610
  await connectHttp(
11299
12611
  makeServer,
@@ -11305,13 +12617,13 @@ async function runServe() {
11305
12617
  },
11306
12618
  identityRegistry
11307
12619
  );
11308
- log50.info(
12620
+ log54.info(
11309
12621
  { port: config.port, bindHost: config.bindHost },
11310
12622
  "MikroMCP server running via HTTP/SSE"
11311
12623
  );
11312
12624
  }
11313
12625
  const shutdown = () => {
11314
- log50.info("Shutting down MikroMCP server");
12626
+ log54.info("Shutting down MikroMCP server");
11315
12627
  pool.closeAll();
11316
12628
  process.exit(0);
11317
12629
  };
@@ -11324,7 +12636,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
11324
12636
  import { homedir as homedir2 } from "os";
11325
12637
  import * as net from "net";
11326
12638
  import chalk from "chalk";
11327
- var localVersion = "1.4.0";
12639
+ var localVersion = "1.5.0";
11328
12640
  var results = [];
11329
12641
  function ok(msg) {
11330
12642
  console.log(chalk.green("\u2705 ") + msg);
@@ -11976,7 +13288,7 @@ async function runInit() {
11976
13288
  }
11977
13289
 
11978
13290
  // src/cli/index.ts
11979
- var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.4.0");
13291
+ var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.5.0");
11980
13292
  program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
11981
13293
  program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
11982
13294
  program.command("init").description("Interactive setup wizard").action(runInit);