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