mikromcp 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -11
- package/dist/main.js +1549 -237
- 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,86 +5890,239 @@ 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
|
-
|
|
5947
|
+
if (parsed.dryRun) {
|
|
5948
|
+
return {
|
|
5949
|
+
content: `Dry run: Would add IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress}.`,
|
|
5950
|
+
structuredContent: {
|
|
5951
|
+
action: "dry_run",
|
|
5952
|
+
diff: [
|
|
5953
|
+
{ property: "src-address", before: null, after: parsed.srcAddress },
|
|
5954
|
+
{ property: "dst-address", before: null, after: parsed.dstAddress },
|
|
5955
|
+
{ property: "action", before: null, after: parsed.ipsecAction }
|
|
5956
|
+
]
|
|
5957
|
+
}
|
|
5958
|
+
};
|
|
5959
|
+
}
|
|
5960
|
+
const body = {
|
|
5961
|
+
"src-address": parsed.srcAddress,
|
|
5962
|
+
"dst-address": parsed.dstAddress,
|
|
5963
|
+
tunnel: tunnelStr,
|
|
5964
|
+
action: parsed.ipsecAction,
|
|
5965
|
+
level: parsed.level
|
|
5966
|
+
};
|
|
5967
|
+
if (parsed.saSourceAddress) body["sa-src-address"] = parsed.saSourceAddress;
|
|
5968
|
+
if (parsed.saDstAddress) body["sa-dst-address"] = parsed.saDstAddress;
|
|
5969
|
+
const created = await context.routerClient.create("ip/ipsec/policy", body);
|
|
5970
|
+
log25.info({ id: created[".id"] }, "IPSec policy added");
|
|
5971
|
+
return {
|
|
5972
|
+
content: `Added IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress}.`,
|
|
5973
|
+
structuredContent: { action: "created", policy: created }
|
|
5974
|
+
};
|
|
5975
|
+
}
|
|
5976
|
+
if (parsed.action === "remove") {
|
|
5977
|
+
if (!existing) {
|
|
5978
|
+
return {
|
|
5979
|
+
content: `IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress} not found. Nothing to remove.`,
|
|
5980
|
+
structuredContent: {
|
|
5981
|
+
action: "not_found",
|
|
5982
|
+
srcAddress: parsed.srcAddress,
|
|
5983
|
+
dstAddress: parsed.dstAddress
|
|
5984
|
+
}
|
|
5985
|
+
};
|
|
5986
|
+
}
|
|
5987
|
+
if (parsed.dryRun) {
|
|
5988
|
+
return {
|
|
5989
|
+
content: `Dry run: Would remove IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress}.`,
|
|
5990
|
+
structuredContent: {
|
|
5991
|
+
action: "dry_run",
|
|
5992
|
+
diff: [{ property: "src-address", before: parsed.srcAddress, after: null }]
|
|
5993
|
+
}
|
|
5994
|
+
};
|
|
5995
|
+
}
|
|
5996
|
+
await context.routerClient.remove("ip/ipsec/policy", existing[".id"]);
|
|
5997
|
+
log25.info({ id: existing[".id"] }, "IPSec policy removed");
|
|
5998
|
+
return {
|
|
5999
|
+
content: `Removed IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress}.`,
|
|
6000
|
+
structuredContent: { action: "removed", id: existing[".id"] }
|
|
6001
|
+
};
|
|
6002
|
+
}
|
|
6003
|
+
if (!existing) {
|
|
6004
|
+
throw new MikroMCPError({
|
|
6005
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
6006
|
+
code: "IPSEC_POLICY_NOT_FOUND",
|
|
6007
|
+
message: `IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress} not found.`,
|
|
6008
|
+
recoverability: {
|
|
6009
|
+
retryable: false,
|
|
6010
|
+
suggestedAction: "Verify with list_ipsec_policies.",
|
|
6011
|
+
alternativeTools: ["list_ipsec_policies"]
|
|
6012
|
+
}
|
|
6013
|
+
});
|
|
6014
|
+
}
|
|
6015
|
+
if (parsed.dryRun) {
|
|
6016
|
+
return {
|
|
6017
|
+
content: `Dry run: Would ${parsed.action} IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress}.`,
|
|
6018
|
+
structuredContent: {
|
|
6019
|
+
action: "dry_run",
|
|
6020
|
+
diff: [
|
|
6021
|
+
{
|
|
6022
|
+
property: "disabled",
|
|
6023
|
+
before: existing.disabled,
|
|
6024
|
+
after: parsed.action === "disable" ? "true" : "false"
|
|
6025
|
+
}
|
|
6026
|
+
]
|
|
6027
|
+
}
|
|
6028
|
+
};
|
|
6029
|
+
}
|
|
6030
|
+
await context.routerClient.update("ip/ipsec/policy", existing[".id"], {
|
|
6031
|
+
disabled: parsed.action === "disable" ? "true" : "false"
|
|
6032
|
+
});
|
|
6033
|
+
const resultAction = parsed.action === "disable" ? "disabled" : "enabled";
|
|
6034
|
+
log25.info({ id: existing[".id"], action: resultAction }, "IPSec policy updated");
|
|
6035
|
+
return {
|
|
6036
|
+
content: `IPSec policy ${parsed.srcAddress} \u2192 ${parsed.dstAddress} ${resultAction}.`,
|
|
6037
|
+
structuredContent: { action: resultAction, id: existing[".id"] }
|
|
6038
|
+
};
|
|
6039
|
+
} catch (err) {
|
|
6040
|
+
if (err instanceof MikroMCPError) throw err;
|
|
6041
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_ipsec_policy" });
|
|
6042
|
+
}
|
|
6043
|
+
}
|
|
6044
|
+
};
|
|
6045
|
+
var ipsecTools = [
|
|
6046
|
+
listIpsecPeersTool,
|
|
6047
|
+
listIpsecPoliciesTool,
|
|
6048
|
+
manageIpsecPeerTool,
|
|
6049
|
+
manageIpsecPolicyTool
|
|
6050
|
+
];
|
|
6051
|
+
|
|
6052
|
+
// src/domain/tools/certificate-tools.ts
|
|
6053
|
+
import { z as z25 } from "zod";
|
|
6054
|
+
var log26 = createLogger("certificate-tools");
|
|
6055
|
+
var listCertificatesInputSchema = z25.object({
|
|
6056
|
+
routerId: z25.string().describe("Target router identifier from the router registry"),
|
|
6057
|
+
name: z25.string().optional().describe("Filter by certificate name (substring match)"),
|
|
6058
|
+
expired: z25.boolean().optional().describe("Filter by expiry status; omit to return all"),
|
|
6059
|
+
limit: z25.number().int().min(1).max(500).default(100).describe("Maximum number of certificates to return")
|
|
6060
|
+
}).strict();
|
|
6061
|
+
var listCertificatesTool = {
|
|
6062
|
+
name: "list_certificates",
|
|
6063
|
+
title: "List Certificates",
|
|
6064
|
+
description: "List certificates on a MikroTik router.",
|
|
6065
|
+
inputSchema: listCertificatesInputSchema,
|
|
6066
|
+
annotations: {
|
|
6067
|
+
readOnlyHint: true,
|
|
6068
|
+
destructiveHint: false,
|
|
6069
|
+
idempotentHint: true,
|
|
6070
|
+
openWorldHint: false
|
|
6071
|
+
},
|
|
6072
|
+
async handler(params, context) {
|
|
6073
|
+
const parsed = listCertificatesInputSchema.parse(params);
|
|
6074
|
+
log26.info({ routerId: context.routerId }, "Listing certificates");
|
|
6075
|
+
try {
|
|
6076
|
+
const allCerts = await context.routerClient.get("certificate", {
|
|
6077
|
+
limit: void 0,
|
|
6078
|
+
offset: void 0
|
|
6079
|
+
});
|
|
6080
|
+
const now = /* @__PURE__ */ new Date();
|
|
6081
|
+
const filtered = allCerts.filter((c) => parsed.name ? (c.name ?? "").includes(parsed.name) : true).filter((c) => {
|
|
6082
|
+
if (parsed.expired === void 0) return true;
|
|
6083
|
+
const invalidAfter = c["invalid-after"];
|
|
6084
|
+
if (!invalidAfter) {
|
|
6085
|
+
return parsed.expired === true;
|
|
6086
|
+
}
|
|
6087
|
+
const expiry = new Date(invalidAfter);
|
|
6088
|
+
return parsed.expired ? expiry < now : expiry >= now;
|
|
6089
|
+
});
|
|
6090
|
+
const certs = filtered.slice(0, parsed.limit);
|
|
6091
|
+
return {
|
|
6092
|
+
content: `Certificates on ${context.routerId}: ${certs.length} returned (${allCerts.length} total)`,
|
|
6093
|
+
structuredContent: {
|
|
6094
|
+
routerId: context.routerId,
|
|
6095
|
+
certificates: certs,
|
|
6096
|
+
total: allCerts.length,
|
|
6097
|
+
returned: certs.length
|
|
6098
|
+
}
|
|
6099
|
+
};
|
|
6100
|
+
} catch (err) {
|
|
6101
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_certificates" });
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
6104
|
+
};
|
|
6105
|
+
var manageCertificateInputSchema = z25.object({
|
|
6106
|
+
routerId: z25.string().describe("Target router identifier from the router registry"),
|
|
6107
|
+
action: z25.enum(["remove", "trust", "untrust"]).describe("Action to perform"),
|
|
6108
|
+
name: z25.string().describe("Certificate name \u2014 idempotency key"),
|
|
6109
|
+
dryRun: z25.boolean().default(false).describe("Preview changes without applying")
|
|
6110
|
+
}).strict();
|
|
6111
|
+
var manageCertificateTool = {
|
|
6112
|
+
name: "manage_certificate",
|
|
6113
|
+
title: "Manage Certificate",
|
|
6114
|
+
description: "Remove, trust, or untrust a certificate. Idempotent: trust/untrust return early if already in the target state.",
|
|
6115
|
+
inputSchema: manageCertificateInputSchema,
|
|
6116
|
+
annotations: {
|
|
6117
|
+
readOnlyHint: false,
|
|
6118
|
+
destructiveHint: true,
|
|
6119
|
+
idempotentHint: false,
|
|
6120
|
+
openWorldHint: false
|
|
6121
|
+
},
|
|
6122
|
+
snapshotPaths: ["certificate"],
|
|
6123
|
+
async handler(params, context) {
|
|
6124
|
+
const parsed = manageCertificateInputSchema.parse(params);
|
|
6125
|
+
log26.info(
|
|
5686
6126
|
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
5687
6127
|
"Managing certificate"
|
|
5688
6128
|
);
|
|
@@ -8758,11 +9198,184 @@ var ovpnTools = [
|
|
|
8758
9198
|
manageOvpnServerTool
|
|
8759
9199
|
];
|
|
8760
9200
|
|
|
8761
|
-
// src/domain/tools/
|
|
9201
|
+
// src/domain/tools/ppp-tools.ts
|
|
8762
9202
|
import { z as z38 } from "zod";
|
|
8763
|
-
var log39 = createLogger("
|
|
8764
|
-
var
|
|
8765
|
-
routerId: z38.string().describe("Target router identifier from the router registry")
|
|
9203
|
+
var log39 = createLogger("ppp-tools");
|
|
9204
|
+
var listPppProfilesInputSchema = z38.object({
|
|
9205
|
+
routerId: z38.string().describe("Target router identifier from the router registry"),
|
|
9206
|
+
name: z38.string().optional().describe("Filter by profile name (exact match)"),
|
|
9207
|
+
limit: z38.number().int().min(1).max(500).default(100).describe("Maximum profiles to return")
|
|
9208
|
+
}).strict();
|
|
9209
|
+
var listPppProfilesTool = {
|
|
9210
|
+
name: "list_ppp_profiles",
|
|
9211
|
+
title: "List PPP Profiles",
|
|
9212
|
+
description: "List PPP profiles including the built-in default and default-encryption profiles.",
|
|
9213
|
+
inputSchema: listPppProfilesInputSchema,
|
|
9214
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
9215
|
+
async handler(params, context) {
|
|
9216
|
+
const parsed = listPppProfilesInputSchema.parse(params);
|
|
9217
|
+
try {
|
|
9218
|
+
log39.info({ routerId: context.routerId }, "Listing PPP profiles");
|
|
9219
|
+
const all = await context.routerClient.get("ppp/profile", { limit: void 0, offset: void 0 });
|
|
9220
|
+
const filtered = parsed.name ? all.filter((p) => p.name === parsed.name) : all;
|
|
9221
|
+
const profiles = filtered.slice(0, parsed.limit);
|
|
9222
|
+
return {
|
|
9223
|
+
content: `PPP profiles on ${context.routerId}: ${profiles.length} returned`,
|
|
9224
|
+
structuredContent: { routerId: context.routerId, profiles, total: all.length, returned: profiles.length }
|
|
9225
|
+
};
|
|
9226
|
+
} catch (err) {
|
|
9227
|
+
if (err instanceof MikroMCPError) throw err;
|
|
9228
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_ppp_profiles" });
|
|
9229
|
+
}
|
|
9230
|
+
}
|
|
9231
|
+
};
|
|
9232
|
+
var managePppProfileInputSchema = z38.object({
|
|
9233
|
+
routerId: z38.string().describe("Target router identifier from the router registry"),
|
|
9234
|
+
action: z38.enum(["add", "update", "remove"]).describe("Action to perform"),
|
|
9235
|
+
name: z38.string().describe("Profile name \u2014 idempotency key"),
|
|
9236
|
+
localAddress: z38.string().optional().describe("Local IP assigned to router end of PPP link"),
|
|
9237
|
+
remoteAddress: z38.string().optional().describe("IP or pool name assigned to client"),
|
|
9238
|
+
dnsServer: z38.string().optional().describe("DNS server IP pushed to client"),
|
|
9239
|
+
rateLimit: z38.string().optional().describe("Rate limit string (e.g. '10M/10M')"),
|
|
9240
|
+
sessionTimeout: z38.string().optional().describe("Session timeout duration string (e.g. '1h')"),
|
|
9241
|
+
comment: z38.string().optional().describe("Optional comment"),
|
|
9242
|
+
dryRun: z38.boolean().default(false).describe("Preview changes without applying")
|
|
9243
|
+
}).strict();
|
|
9244
|
+
var managePppProfileTool = {
|
|
9245
|
+
name: "manage_ppp_profile",
|
|
9246
|
+
title: "Manage PPP Profile",
|
|
9247
|
+
description: "Add, update, or remove a PPP profile. Idempotent by name. update returns no_change when requested values match. Built-in profiles (default, default-encryption) cannot be removed \u2014 RouterOS blocks this and the error is surfaced.",
|
|
9248
|
+
inputSchema: managePppProfileInputSchema,
|
|
9249
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
9250
|
+
snapshotPaths: ["ppp/profile"],
|
|
9251
|
+
async handler(params, context) {
|
|
9252
|
+
const parsed = managePppProfileInputSchema.parse(params);
|
|
9253
|
+
log39.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing PPP profile");
|
|
9254
|
+
try {
|
|
9255
|
+
const all = await context.routerClient.get("ppp/profile", { limit: void 0, offset: void 0 });
|
|
9256
|
+
const existing = all.find((p) => p.name === parsed.name);
|
|
9257
|
+
if (parsed.action === "add") {
|
|
9258
|
+
if (existing) {
|
|
9259
|
+
return {
|
|
9260
|
+
content: `PPP profile "${parsed.name}" already exists. No changes made.`,
|
|
9261
|
+
structuredContent: { action: "already_exists", name: parsed.name }
|
|
9262
|
+
};
|
|
9263
|
+
}
|
|
9264
|
+
if (parsed.dryRun) {
|
|
9265
|
+
const diff = [
|
|
9266
|
+
{ property: "name", before: null, after: parsed.name }
|
|
9267
|
+
];
|
|
9268
|
+
if (parsed.localAddress) diff.push({ property: "local-address", before: null, after: parsed.localAddress });
|
|
9269
|
+
if (parsed.remoteAddress) diff.push({ property: "remote-address", before: null, after: parsed.remoteAddress });
|
|
9270
|
+
if (parsed.dnsServer) diff.push({ property: "dns-server", before: null, after: parsed.dnsServer });
|
|
9271
|
+
if (parsed.rateLimit) diff.push({ property: "rate-limit", before: null, after: parsed.rateLimit });
|
|
9272
|
+
if (parsed.sessionTimeout) diff.push({ property: "session-timeout", before: null, after: parsed.sessionTimeout });
|
|
9273
|
+
if (parsed.comment) diff.push({ property: "comment", before: null, after: parsed.comment });
|
|
9274
|
+
return {
|
|
9275
|
+
content: `Dry run: Would create PPP profile "${parsed.name}".`,
|
|
9276
|
+
structuredContent: { action: "dry_run", diff }
|
|
9277
|
+
};
|
|
9278
|
+
}
|
|
9279
|
+
const body = { name: parsed.name };
|
|
9280
|
+
if (parsed.localAddress) body["local-address"] = parsed.localAddress;
|
|
9281
|
+
if (parsed.remoteAddress) body["remote-address"] = parsed.remoteAddress;
|
|
9282
|
+
if (parsed.dnsServer) body["dns-server"] = parsed.dnsServer;
|
|
9283
|
+
if (parsed.rateLimit) body["rate-limit"] = parsed.rateLimit;
|
|
9284
|
+
if (parsed.sessionTimeout) body["session-timeout"] = parsed.sessionTimeout;
|
|
9285
|
+
if (parsed.comment) body.comment = parsed.comment;
|
|
9286
|
+
const created = await context.routerClient.create("ppp/profile", body);
|
|
9287
|
+
log39.info({ name: parsed.name, id: created[".id"] }, "PPP profile created");
|
|
9288
|
+
return {
|
|
9289
|
+
content: `Created PPP profile "${parsed.name}".`,
|
|
9290
|
+
structuredContent: { action: "created", name: parsed.name, id: created[".id"] }
|
|
9291
|
+
};
|
|
9292
|
+
}
|
|
9293
|
+
if (parsed.action === "update") {
|
|
9294
|
+
if (!existing) {
|
|
9295
|
+
throw new MikroMCPError({
|
|
9296
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
9297
|
+
code: "PPP_PROFILE_NOT_FOUND",
|
|
9298
|
+
message: `PPP profile "${parsed.name}" not found.`,
|
|
9299
|
+
details: { name: parsed.name },
|
|
9300
|
+
recoverability: { retryable: false, suggestedAction: "Verify with list_ppp_profiles.", alternativeTools: ["list_ppp_profiles"] }
|
|
9301
|
+
});
|
|
9302
|
+
}
|
|
9303
|
+
const changes = {};
|
|
9304
|
+
const diff = [];
|
|
9305
|
+
if (parsed.localAddress !== void 0 && existing["local-address"] !== parsed.localAddress) {
|
|
9306
|
+
changes["local-address"] = parsed.localAddress;
|
|
9307
|
+
diff.push({ property: "local-address", before: existing["local-address"] ?? null, after: parsed.localAddress });
|
|
9308
|
+
}
|
|
9309
|
+
if (parsed.remoteAddress !== void 0 && existing["remote-address"] !== parsed.remoteAddress) {
|
|
9310
|
+
changes["remote-address"] = parsed.remoteAddress;
|
|
9311
|
+
diff.push({ property: "remote-address", before: existing["remote-address"] ?? null, after: parsed.remoteAddress });
|
|
9312
|
+
}
|
|
9313
|
+
if (parsed.dnsServer !== void 0 && existing["dns-server"] !== parsed.dnsServer) {
|
|
9314
|
+
changes["dns-server"] = parsed.dnsServer;
|
|
9315
|
+
diff.push({ property: "dns-server", before: existing["dns-server"] ?? null, after: parsed.dnsServer });
|
|
9316
|
+
}
|
|
9317
|
+
if (parsed.rateLimit !== void 0 && existing["rate-limit"] !== parsed.rateLimit) {
|
|
9318
|
+
changes["rate-limit"] = parsed.rateLimit;
|
|
9319
|
+
diff.push({ property: "rate-limit", before: existing["rate-limit"] ?? null, after: parsed.rateLimit });
|
|
9320
|
+
}
|
|
9321
|
+
if (parsed.sessionTimeout !== void 0 && existing["session-timeout"] !== parsed.sessionTimeout) {
|
|
9322
|
+
changes["session-timeout"] = parsed.sessionTimeout;
|
|
9323
|
+
diff.push({ property: "session-timeout", before: existing["session-timeout"] ?? null, after: parsed.sessionTimeout });
|
|
9324
|
+
}
|
|
9325
|
+
if (parsed.comment !== void 0 && existing.comment !== parsed.comment) {
|
|
9326
|
+
changes.comment = parsed.comment;
|
|
9327
|
+
diff.push({ property: "comment", before: existing.comment ?? null, after: parsed.comment });
|
|
9328
|
+
}
|
|
9329
|
+
if (Object.keys(changes).length === 0) {
|
|
9330
|
+
return {
|
|
9331
|
+
content: `PPP profile "${parsed.name}" already matches requested values. No changes made.`,
|
|
9332
|
+
structuredContent: { action: "no_change", name: parsed.name }
|
|
9333
|
+
};
|
|
9334
|
+
}
|
|
9335
|
+
if (parsed.dryRun) {
|
|
9336
|
+
return {
|
|
9337
|
+
content: `Dry run: Would update PPP profile "${parsed.name}".`,
|
|
9338
|
+
structuredContent: { action: "dry_run", diff }
|
|
9339
|
+
};
|
|
9340
|
+
}
|
|
9341
|
+
await context.routerClient.update("ppp/profile", existing[".id"], changes);
|
|
9342
|
+
log39.info({ name: parsed.name, id: existing[".id"] }, "PPP profile updated");
|
|
9343
|
+
return {
|
|
9344
|
+
content: `Updated PPP profile "${parsed.name}".`,
|
|
9345
|
+
structuredContent: { action: "updated", name: parsed.name, id: existing[".id"], diff }
|
|
9346
|
+
};
|
|
9347
|
+
}
|
|
9348
|
+
if (!existing) {
|
|
9349
|
+
return {
|
|
9350
|
+
content: `PPP profile "${parsed.name}" not found. Nothing to remove.`,
|
|
9351
|
+
structuredContent: { action: "not_found", name: parsed.name }
|
|
9352
|
+
};
|
|
9353
|
+
}
|
|
9354
|
+
if (parsed.dryRun) {
|
|
9355
|
+
return {
|
|
9356
|
+
content: `Dry run: Would remove PPP profile "${parsed.name}".`,
|
|
9357
|
+
structuredContent: { action: "dry_run", diff: [{ property: "name", before: parsed.name, after: null }] }
|
|
9358
|
+
};
|
|
9359
|
+
}
|
|
9360
|
+
await context.routerClient.remove("ppp/profile", existing[".id"]);
|
|
9361
|
+
log39.info({ name: parsed.name }, "PPP profile removed");
|
|
9362
|
+
return {
|
|
9363
|
+
content: `Removed PPP profile "${parsed.name}".`,
|
|
9364
|
+
structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
|
|
9365
|
+
};
|
|
9366
|
+
} catch (err) {
|
|
9367
|
+
if (err instanceof MikroMCPError) throw err;
|
|
9368
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_ppp_profile" });
|
|
9369
|
+
}
|
|
9370
|
+
}
|
|
9371
|
+
};
|
|
9372
|
+
var pppTools = [listPppProfilesTool, managePppProfileTool];
|
|
9373
|
+
|
|
9374
|
+
// src/domain/tools/upgrade-tools.ts
|
|
9375
|
+
import { z as z39 } from "zod";
|
|
9376
|
+
var log40 = createLogger("upgrade-tools");
|
|
9377
|
+
var getUpgradeStatusInputSchema = z39.object({
|
|
9378
|
+
routerId: z39.string().describe("Target router identifier from the router registry")
|
|
8766
9379
|
}).strict();
|
|
8767
9380
|
var getUpgradeStatusTool = {
|
|
8768
9381
|
name: "get_upgrade_status",
|
|
@@ -8777,7 +9390,7 @@ var getUpgradeStatusTool = {
|
|
|
8777
9390
|
},
|
|
8778
9391
|
async handler(params, context) {
|
|
8779
9392
|
const parsed = getUpgradeStatusInputSchema.parse(params);
|
|
8780
|
-
|
|
9393
|
+
log40.info({ routerId: context.routerId }, "Getting upgrade status");
|
|
8781
9394
|
try {
|
|
8782
9395
|
const [updateResults, routerboardResults] = await Promise.all([
|
|
8783
9396
|
context.routerClient.get("system/package/update"),
|
|
@@ -8806,12 +9419,12 @@ var getUpgradeStatusTool = {
|
|
|
8806
9419
|
}
|
|
8807
9420
|
}
|
|
8808
9421
|
};
|
|
8809
|
-
var manageUpgradeInputSchema =
|
|
8810
|
-
routerId:
|
|
8811
|
-
action:
|
|
9422
|
+
var manageUpgradeInputSchema = z39.object({
|
|
9423
|
+
routerId: z39.string().describe("Target router identifier from the router registry"),
|
|
9424
|
+
action: z39.enum(["check", "install"]).describe(
|
|
8812
9425
|
"check \u2014 trigger a check for available updates; install \u2014 download and install the latest update (triggers reboot)"
|
|
8813
9426
|
),
|
|
8814
|
-
dryRun:
|
|
9427
|
+
dryRun: z39.boolean().default(false).describe("Preview the action without executing")
|
|
8815
9428
|
}).strict();
|
|
8816
9429
|
var manageUpgradeTool = {
|
|
8817
9430
|
name: "manage_upgrade",
|
|
@@ -8826,7 +9439,7 @@ var manageUpgradeTool = {
|
|
|
8826
9439
|
},
|
|
8827
9440
|
async handler(params, context) {
|
|
8828
9441
|
const parsed = manageUpgradeInputSchema.parse(params);
|
|
8829
|
-
|
|
9442
|
+
log40.info({ routerId: context.routerId, action: parsed.action }, "Managing upgrade");
|
|
8830
9443
|
try {
|
|
8831
9444
|
if (parsed.dryRun) {
|
|
8832
9445
|
return {
|
|
@@ -8840,14 +9453,14 @@ var manageUpgradeTool = {
|
|
|
8840
9453
|
}
|
|
8841
9454
|
if (parsed.action === "check") {
|
|
8842
9455
|
await context.routerClient.execute("system/package/update/check-for-updates");
|
|
8843
|
-
|
|
9456
|
+
log40.info({ routerId: context.routerId }, "Update check triggered");
|
|
8844
9457
|
return {
|
|
8845
9458
|
content: `Update check triggered on ${parsed.routerId}.`,
|
|
8846
9459
|
structuredContent: { action: "check_triggered", routerId: parsed.routerId }
|
|
8847
9460
|
};
|
|
8848
9461
|
}
|
|
8849
9462
|
await context.routerClient.execute("system/package/update/install");
|
|
8850
|
-
|
|
9463
|
+
log40.info({ routerId: context.routerId }, "Package install triggered");
|
|
8851
9464
|
return {
|
|
8852
9465
|
content: `Package install triggered on ${parsed.routerId}. Router will reboot automatically.`,
|
|
8853
9466
|
structuredContent: { action: "install_triggered", routerId: parsed.routerId }
|
|
@@ -8860,13 +9473,13 @@ var manageUpgradeTool = {
|
|
|
8860
9473
|
var upgradeTools = [getUpgradeStatusTool, manageUpgradeTool];
|
|
8861
9474
|
|
|
8862
9475
|
// src/domain/tools/backup-tools.ts
|
|
8863
|
-
import { z as
|
|
8864
|
-
var
|
|
8865
|
-
var createBackupInputSchema =
|
|
8866
|
-
routerId:
|
|
8867
|
-
name:
|
|
8868
|
-
password:
|
|
8869
|
-
dryRun:
|
|
9476
|
+
import { z as z40 } from "zod";
|
|
9477
|
+
var log41 = createLogger("backup-tools");
|
|
9478
|
+
var createBackupInputSchema = z40.object({
|
|
9479
|
+
routerId: z40.string().describe("Target router identifier from the router registry"),
|
|
9480
|
+
name: z40.string().default("backup").describe("Backup file name (without extension)"),
|
|
9481
|
+
password: z40.string().optional().describe("Password to encrypt the backup file"),
|
|
9482
|
+
dryRun: z40.boolean().default(false).describe("Preview the backup without creating it")
|
|
8870
9483
|
}).strict();
|
|
8871
9484
|
var createBackupTool = {
|
|
8872
9485
|
name: "create_backup",
|
|
@@ -8881,7 +9494,7 @@ var createBackupTool = {
|
|
|
8881
9494
|
},
|
|
8882
9495
|
async handler(params, context) {
|
|
8883
9496
|
const parsed = createBackupInputSchema.parse(params);
|
|
8884
|
-
|
|
9497
|
+
log41.info({ routerId: context.routerId, name: parsed.name }, "Creating backup");
|
|
8885
9498
|
if (parsed.dryRun) {
|
|
8886
9499
|
return {
|
|
8887
9500
|
content: `Dry run: Would create backup "${parsed.name}.backup" on ${context.routerId}.`,
|
|
@@ -8894,7 +9507,7 @@ var createBackupTool = {
|
|
|
8894
9507
|
body.password = parsed.password;
|
|
8895
9508
|
}
|
|
8896
9509
|
await context.routerClient.execute("system/backup/save", body);
|
|
8897
|
-
|
|
9510
|
+
log41.info({ routerId: context.routerId, name: parsed.name }, "Backup created");
|
|
8898
9511
|
return {
|
|
8899
9512
|
content: `Backup "${parsed.name}.backup" created on ${context.routerId}.`,
|
|
8900
9513
|
structuredContent: {
|
|
@@ -8909,10 +9522,10 @@ var createBackupTool = {
|
|
|
8909
9522
|
}
|
|
8910
9523
|
}
|
|
8911
9524
|
};
|
|
8912
|
-
var exportConfigInputSchema =
|
|
8913
|
-
routerId:
|
|
8914
|
-
compact:
|
|
8915
|
-
file:
|
|
9525
|
+
var exportConfigInputSchema = z40.object({
|
|
9526
|
+
routerId: z40.string().describe("Target router identifier from the router registry"),
|
|
9527
|
+
compact: z40.boolean().default(false).describe("Export only non-default settings (compact format)"),
|
|
9528
|
+
file: z40.string().optional().describe("Save the export to a file on the router (without extension); omit to return inline")
|
|
8916
9529
|
}).strict();
|
|
8917
9530
|
var exportConfigTool = {
|
|
8918
9531
|
name: "export_config",
|
|
@@ -8927,7 +9540,7 @@ var exportConfigTool = {
|
|
|
8927
9540
|
},
|
|
8928
9541
|
async handler(params, context) {
|
|
8929
9542
|
const parsed = exportConfigInputSchema.parse(params);
|
|
8930
|
-
|
|
9543
|
+
log41.info({ routerId: context.routerId, compact: parsed.compact }, "Exporting config");
|
|
8931
9544
|
try {
|
|
8932
9545
|
if (parsed.file !== void 0) {
|
|
8933
9546
|
const body = { file: parsed.file };
|
|
@@ -8961,16 +9574,16 @@ ${scriptText}`,
|
|
|
8961
9574
|
var backupTools = [createBackupTool, exportConfigTool];
|
|
8962
9575
|
|
|
8963
9576
|
// src/domain/tools/log-tools.ts
|
|
8964
|
-
import { z as
|
|
8965
|
-
var
|
|
9577
|
+
import { z as z41 } from "zod";
|
|
9578
|
+
var log42 = createLogger("log-tools");
|
|
8966
9579
|
var LOG_RULES_PATH = "system/logging";
|
|
8967
9580
|
var LOG_ACTIONS_PATH = "system/logging/action";
|
|
8968
|
-
var logActionTypeEnum =
|
|
8969
|
-
var listLogRulesInputSchema =
|
|
8970
|
-
routerId:
|
|
8971
|
-
topics:
|
|
8972
|
-
logAction:
|
|
8973
|
-
limit:
|
|
9581
|
+
var logActionTypeEnum = z41.enum(["memory", "disk", "remote", "echo", "email"]);
|
|
9582
|
+
var listLogRulesInputSchema = z41.object({
|
|
9583
|
+
routerId: z41.string().describe("Target router identifier from the router registry"),
|
|
9584
|
+
topics: z41.string().optional().describe("Filter by topics (substring match)"),
|
|
9585
|
+
logAction: z41.string().optional().describe("Filter by log action target (exact match on action field)"),
|
|
9586
|
+
limit: z41.number().int().min(1).max(500).default(100).describe("Maximum number of rules to return (1-500, default 100)")
|
|
8974
9587
|
}).strict();
|
|
8975
9588
|
var listLogRulesTool = {
|
|
8976
9589
|
name: "list_log_rules",
|
|
@@ -8985,7 +9598,7 @@ var listLogRulesTool = {
|
|
|
8985
9598
|
},
|
|
8986
9599
|
async handler(params, context) {
|
|
8987
9600
|
const parsed = listLogRulesInputSchema.parse(params);
|
|
8988
|
-
|
|
9601
|
+
log42.info({ routerId: context.routerId }, "Listing log rules");
|
|
8989
9602
|
try {
|
|
8990
9603
|
const all = await context.routerClient.get(LOG_RULES_PATH, {
|
|
8991
9604
|
limit: void 0,
|
|
@@ -9014,13 +9627,13 @@ var listLogRulesTool = {
|
|
|
9014
9627
|
}
|
|
9015
9628
|
}
|
|
9016
9629
|
};
|
|
9017
|
-
var manageLogRuleInputSchema =
|
|
9018
|
-
routerId:
|
|
9019
|
-
action:
|
|
9020
|
-
topics:
|
|
9021
|
-
logAction:
|
|
9022
|
-
prefix:
|
|
9023
|
-
dryRun:
|
|
9630
|
+
var manageLogRuleInputSchema = z41.object({
|
|
9631
|
+
routerId: z41.string().describe("Target router identifier from the router registry"),
|
|
9632
|
+
action: z41.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
|
|
9633
|
+
topics: z41.string().describe("Log topics (e.g. firewall, system, info) \u2014 idempotency key"),
|
|
9634
|
+
logAction: z41.string().describe("Log action target name (RouterOS action field) \u2014 idempotency key"),
|
|
9635
|
+
prefix: z41.string().optional().describe("Optional prefix to prepend to log messages"),
|
|
9636
|
+
dryRun: z41.boolean().default(false).describe("Preview changes without applying")
|
|
9024
9637
|
}).strict();
|
|
9025
9638
|
var manageLogRuleTool = {
|
|
9026
9639
|
name: "manage_log_rule",
|
|
@@ -9036,7 +9649,7 @@ var manageLogRuleTool = {
|
|
|
9036
9649
|
snapshotPaths: ["system/logging"],
|
|
9037
9650
|
async handler(params, context) {
|
|
9038
9651
|
const parsed = manageLogRuleInputSchema.parse(params);
|
|
9039
|
-
|
|
9652
|
+
log42.info(
|
|
9040
9653
|
{ routerId: context.routerId, action: parsed.action, topics: parsed.topics },
|
|
9041
9654
|
"Managing log rule"
|
|
9042
9655
|
);
|
|
@@ -9078,7 +9691,7 @@ var manageLogRuleTool = {
|
|
|
9078
9691
|
};
|
|
9079
9692
|
}
|
|
9080
9693
|
const created = await context.routerClient.create(LOG_RULES_PATH, body);
|
|
9081
|
-
|
|
9694
|
+
log42.info({ topics: parsed.topics, logAction: parsed.logAction, id: created[".id"] }, "Log rule added");
|
|
9082
9695
|
return {
|
|
9083
9696
|
content: `Added log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
|
|
9084
9697
|
structuredContent: {
|
|
@@ -9106,7 +9719,7 @@ var manageLogRuleTool = {
|
|
|
9106
9719
|
};
|
|
9107
9720
|
}
|
|
9108
9721
|
await context.routerClient.remove(LOG_RULES_PATH, id);
|
|
9109
|
-
|
|
9722
|
+
log42.info({ topics: parsed.topics, logAction: parsed.logAction, id }, "Log rule removed");
|
|
9110
9723
|
return {
|
|
9111
9724
|
content: `Removed log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
|
|
9112
9725
|
structuredContent: { action: "removed", id }
|
|
@@ -9142,7 +9755,7 @@ var manageLogRuleTool = {
|
|
|
9142
9755
|
disabled: wantDisabled ? "true" : "false"
|
|
9143
9756
|
});
|
|
9144
9757
|
const resultAction = wantDisabled ? "disabled" : "enabled";
|
|
9145
|
-
|
|
9758
|
+
log42.info({ topics: parsed.topics, id, action: resultAction }, "Log rule toggled");
|
|
9146
9759
|
return {
|
|
9147
9760
|
content: `${wantDisabled ? "Disabled" : "Enabled"} log rule topics="${parsed.topics}" action="${parsed.logAction}".`,
|
|
9148
9761
|
structuredContent: { action: resultAction, id }
|
|
@@ -9163,10 +9776,10 @@ var manageLogRuleTool = {
|
|
|
9163
9776
|
}
|
|
9164
9777
|
}
|
|
9165
9778
|
};
|
|
9166
|
-
var listLogActionsInputSchema =
|
|
9167
|
-
routerId:
|
|
9779
|
+
var listLogActionsInputSchema = z41.object({
|
|
9780
|
+
routerId: z41.string().describe("Target router identifier from the router registry"),
|
|
9168
9781
|
type: logActionTypeEnum.optional().describe("Filter by action type (exact match): memory, disk, remote, echo, email"),
|
|
9169
|
-
limit:
|
|
9782
|
+
limit: z41.number().int().min(1).max(500).default(100).describe("Maximum number of actions to return (1-500, default 100)")
|
|
9170
9783
|
}).strict();
|
|
9171
9784
|
var listLogActionsTool = {
|
|
9172
9785
|
name: "list_log_actions",
|
|
@@ -9181,7 +9794,7 @@ var listLogActionsTool = {
|
|
|
9181
9794
|
},
|
|
9182
9795
|
async handler(params, context) {
|
|
9183
9796
|
const parsed = listLogActionsInputSchema.parse(params);
|
|
9184
|
-
|
|
9797
|
+
log42.info({ routerId: context.routerId }, "Listing log actions");
|
|
9185
9798
|
try {
|
|
9186
9799
|
const all = await context.routerClient.get(LOG_ACTIONS_PATH, {
|
|
9187
9800
|
limit: void 0,
|
|
@@ -9207,15 +9820,15 @@ var listLogActionsTool = {
|
|
|
9207
9820
|
}
|
|
9208
9821
|
}
|
|
9209
9822
|
};
|
|
9210
|
-
var manageLogActionInputSchema =
|
|
9211
|
-
routerId:
|
|
9212
|
-
action:
|
|
9213
|
-
name:
|
|
9823
|
+
var manageLogActionInputSchema = z41.object({
|
|
9824
|
+
routerId: z41.string().describe("Target router identifier from the router registry"),
|
|
9825
|
+
action: z41.enum(["add", "remove"]).describe("Action to perform"),
|
|
9826
|
+
name: z41.string().describe("Log action name \u2014 idempotency key"),
|
|
9214
9827
|
type: logActionTypeEnum.optional().describe("Action type: memory, disk, remote, echo, email (required on add)"),
|
|
9215
|
-
remote:
|
|
9216
|
-
remotePort:
|
|
9217
|
-
diskFileName:
|
|
9218
|
-
dryRun:
|
|
9828
|
+
remote: z41.string().optional().describe("Remote syslog server address (for type=remote)"),
|
|
9829
|
+
remotePort: z41.number().int().min(1).max(65535).optional().describe("Remote syslog server port (for type=remote)"),
|
|
9830
|
+
diskFileName: z41.string().optional().describe("Disk log file name without extension (for type=disk)"),
|
|
9831
|
+
dryRun: z41.boolean().default(false).describe("Preview changes without applying")
|
|
9219
9832
|
}).strict();
|
|
9220
9833
|
var manageLogActionTool = {
|
|
9221
9834
|
name: "manage_log_action",
|
|
@@ -9231,7 +9844,7 @@ var manageLogActionTool = {
|
|
|
9231
9844
|
snapshotPaths: ["system/logging/action"],
|
|
9232
9845
|
async handler(params, context) {
|
|
9233
9846
|
const parsed = manageLogActionInputSchema.parse(params);
|
|
9234
|
-
|
|
9847
|
+
log42.info(
|
|
9235
9848
|
{ routerId: context.routerId, action: parsed.action, name: parsed.name },
|
|
9236
9849
|
"Managing log action"
|
|
9237
9850
|
);
|
|
@@ -9285,7 +9898,7 @@ var manageLogActionTool = {
|
|
|
9285
9898
|
};
|
|
9286
9899
|
}
|
|
9287
9900
|
const created = await context.routerClient.create(LOG_ACTIONS_PATH, body);
|
|
9288
|
-
|
|
9901
|
+
log42.info({ name: parsed.name, type: parsed.type, id: created[".id"] }, "Log action added");
|
|
9289
9902
|
return {
|
|
9290
9903
|
content: `Added log action "${parsed.name}" (type=${parsed.type}).`,
|
|
9291
9904
|
structuredContent: {
|
|
@@ -9315,7 +9928,7 @@ var manageLogActionTool = {
|
|
|
9315
9928
|
};
|
|
9316
9929
|
}
|
|
9317
9930
|
await context.routerClient.remove(LOG_ACTIONS_PATH, id);
|
|
9318
|
-
|
|
9931
|
+
log42.info({ name: parsed.name, id }, "Log action removed");
|
|
9319
9932
|
return {
|
|
9320
9933
|
content: `Removed log action "${parsed.name}".`,
|
|
9321
9934
|
structuredContent: { action: "removed", name: parsed.name, id }
|
|
@@ -9343,22 +9956,717 @@ var logTools = [
|
|
|
9343
9956
|
manageLogActionTool
|
|
9344
9957
|
];
|
|
9345
9958
|
|
|
9346
|
-
// src/domain/tools/
|
|
9347
|
-
import { z as
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9959
|
+
// src/domain/tools/network-test-tools.ts
|
|
9960
|
+
import { z as z42 } from "zod";
|
|
9961
|
+
var log43 = createLogger("network-test-tools");
|
|
9962
|
+
var BODY_CAP = 65536;
|
|
9963
|
+
var bandwidthTestInputSchema = z42.object({
|
|
9964
|
+
routerId: z42.string().describe("Target router identifier from the router registry"),
|
|
9965
|
+
address: z42.string().describe("Remote host running RouterOS btest server"),
|
|
9966
|
+
protocol: z42.enum(["tcp", "udp"]).default("tcp").describe("Test protocol"),
|
|
9967
|
+
direction: z42.enum(["send", "receive", "both"]).default("both").describe("Test direction"),
|
|
9968
|
+
duration: z42.number().int().min(1).max(30).default(5).describe("Test duration in seconds (max 30)")
|
|
9969
|
+
}).strict();
|
|
9970
|
+
var bandwidthTestTool = {
|
|
9971
|
+
name: "bandwidth_test",
|
|
9972
|
+
title: "Bandwidth Test",
|
|
9973
|
+
description: "Run a RouterOS bandwidth test from the router to a remote host running a RouterOS btest server. Returns TX and RX throughput in Mbps. Duration capped at 30 seconds.",
|
|
9974
|
+
inputSchema: bandwidthTestInputSchema,
|
|
9975
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
9976
|
+
async handler(params, context) {
|
|
9977
|
+
const parsed = bandwidthTestInputSchema.parse(params);
|
|
9978
|
+
log43.info({ routerId: context.routerId, address: parsed.address }, "Running bandwidth test");
|
|
9979
|
+
try {
|
|
9980
|
+
const result = await context.routerClient.execute("tool/bandwidth-test", {
|
|
9981
|
+
address: parsed.address,
|
|
9982
|
+
protocol: parsed.protocol,
|
|
9983
|
+
direction: parsed.direction,
|
|
9984
|
+
duration: String(parsed.duration)
|
|
9985
|
+
});
|
|
9986
|
+
const txBps = Number(result["tx-current"] ?? 0);
|
|
9987
|
+
const rxBps = Number(result["rx-current"] ?? 0);
|
|
9988
|
+
const txMbps = Math.round(txBps / 1e6 * 100) / 100;
|
|
9989
|
+
const rxMbps = Math.round(rxBps / 1e6 * 100) / 100;
|
|
9990
|
+
return {
|
|
9991
|
+
content: `Bandwidth test to ${parsed.address}: TX=${txMbps} Mbps RX=${rxMbps} Mbps`,
|
|
9992
|
+
structuredContent: {
|
|
9993
|
+
routerId: context.routerId,
|
|
9994
|
+
address: parsed.address,
|
|
9995
|
+
protocol: parsed.protocol,
|
|
9996
|
+
direction: parsed.direction,
|
|
9997
|
+
txMbps,
|
|
9998
|
+
rxMbps,
|
|
9999
|
+
lostPackets: result["lost-packets"] ?? null
|
|
10000
|
+
}
|
|
10001
|
+
};
|
|
10002
|
+
} catch (err) {
|
|
10003
|
+
throw enrichError(err, { routerId: context.routerId, tool: "bandwidth_test" });
|
|
10004
|
+
}
|
|
10005
|
+
}
|
|
10006
|
+
};
|
|
10007
|
+
var fetchUrlInputSchema = z42.object({
|
|
10008
|
+
routerId: z42.string().describe("Target router identifier from the router registry"),
|
|
10009
|
+
url: z42.string().url().describe("URL to fetch from the router"),
|
|
10010
|
+
method: z42.enum(["GET", "POST"]).default("GET").describe("HTTP method"),
|
|
10011
|
+
httpData: z42.string().optional().describe("Request body for POST"),
|
|
10012
|
+
outputFile: z42.string().optional().describe("Save response body to this router file path instead of returning inline")
|
|
10013
|
+
}).strict();
|
|
10014
|
+
var fetchUrlTool = {
|
|
10015
|
+
name: "fetch_url",
|
|
10016
|
+
title: "Fetch URL",
|
|
10017
|
+
description: "Send an HTTP/HTTPS request from the router using /tool/fetch. Response body is returned inline (capped at 64 KB with [TRUNCATED] marker). Use outputFile to save to router filesystem instead.",
|
|
10018
|
+
inputSchema: fetchUrlInputSchema,
|
|
10019
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
10020
|
+
async handler(params, context) {
|
|
10021
|
+
const parsed = fetchUrlInputSchema.parse(params);
|
|
10022
|
+
log43.info({ routerId: context.routerId, url: parsed.url, method: parsed.method }, "Fetching URL");
|
|
10023
|
+
try {
|
|
10024
|
+
const body = { url: parsed.url, "http-method": parsed.method.toLowerCase() };
|
|
10025
|
+
if (parsed.httpData) body["http-data"] = parsed.httpData;
|
|
10026
|
+
if (parsed.outputFile) {
|
|
10027
|
+
body.output = "file";
|
|
10028
|
+
body["dst-path"] = parsed.outputFile;
|
|
10029
|
+
} else {
|
|
10030
|
+
body.output = "user";
|
|
10031
|
+
}
|
|
10032
|
+
const sections = await context.routerClient.execute("tool/fetch", body);
|
|
10033
|
+
const finished = sections.find((s) => s.status === "finished") ?? sections[sections.length - 1];
|
|
10034
|
+
const statusCode = finished?.code ?? null;
|
|
10035
|
+
if (parsed.outputFile) {
|
|
10036
|
+
return {
|
|
10037
|
+
content: `Fetched ${parsed.url} \u2192 saved to ${parsed.outputFile} (status ${statusCode})`,
|
|
10038
|
+
structuredContent: { routerId: context.routerId, url: parsed.url, statusCode, outputFile: parsed.outputFile }
|
|
10039
|
+
};
|
|
10040
|
+
}
|
|
10041
|
+
let responseBody = finished?.data ?? "";
|
|
10042
|
+
if (responseBody.length > BODY_CAP) {
|
|
10043
|
+
responseBody = responseBody.slice(0, BODY_CAP) + "[TRUNCATED]";
|
|
10044
|
+
}
|
|
10045
|
+
return {
|
|
10046
|
+
content: `Fetched ${parsed.url}: status=${statusCode} body_length=${responseBody.length}`,
|
|
10047
|
+
structuredContent: { routerId: context.routerId, url: parsed.url, statusCode, body: responseBody }
|
|
10048
|
+
};
|
|
10049
|
+
} catch (err) {
|
|
10050
|
+
throw enrichError(err, { routerId: context.routerId, tool: "fetch_url" });
|
|
10051
|
+
}
|
|
10052
|
+
}
|
|
10053
|
+
};
|
|
10054
|
+
var listConnectionsInputSchema = z42.object({
|
|
10055
|
+
routerId: z42.string().describe("Target router identifier from the router registry"),
|
|
10056
|
+
srcAddress: z42.string().optional().describe("Filter by source address (substring match)"),
|
|
10057
|
+
dstAddress: z42.string().optional().describe("Filter by destination address (substring match)"),
|
|
10058
|
+
protocol: z42.string().optional().describe("Filter by protocol (exact match, e.g. tcp, udp, icmp)"),
|
|
10059
|
+
limit: z42.number().int().min(1).max(500).default(100).describe("Maximum connections to return")
|
|
10060
|
+
}).strict();
|
|
10061
|
+
var listConnectionsTool = {
|
|
10062
|
+
name: "list_connections",
|
|
10063
|
+
title: "List Connections",
|
|
10064
|
+
description: "List active connection tracking entries from the router firewall table. Filters are applied client-side. Useful for diagnosing NAT and firewall behavior.",
|
|
10065
|
+
inputSchema: listConnectionsInputSchema,
|
|
10066
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
10067
|
+
async handler(params, context) {
|
|
10068
|
+
const parsed = listConnectionsInputSchema.parse(params);
|
|
10069
|
+
log43.info({ routerId: context.routerId }, "Listing connections");
|
|
10070
|
+
try {
|
|
10071
|
+
const all = await context.routerClient.get("ip/firewall/connection", {
|
|
10072
|
+
limit: void 0,
|
|
10073
|
+
offset: void 0
|
|
10074
|
+
});
|
|
10075
|
+
const filtered = all.filter((c) => parsed.srcAddress ? (c["src-address"] ?? "").includes(parsed.srcAddress) : true).filter((c) => parsed.dstAddress ? (c["dst-address"] ?? "").includes(parsed.dstAddress) : true).filter((c) => parsed.protocol ? c.protocol === parsed.protocol : true);
|
|
10076
|
+
const connections = filtered.slice(0, parsed.limit);
|
|
10077
|
+
return {
|
|
10078
|
+
content: `Active connections on ${context.routerId}: ${connections.length} returned (${all.length} total)`,
|
|
10079
|
+
structuredContent: { routerId: context.routerId, connections, total: all.length, returned: connections.length }
|
|
10080
|
+
};
|
|
10081
|
+
} catch (err) {
|
|
10082
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_connections" });
|
|
10083
|
+
}
|
|
10084
|
+
}
|
|
10085
|
+
};
|
|
10086
|
+
var networkTestTools = [bandwidthTestTool, fetchUrlTool, listConnectionsTool];
|
|
10087
|
+
|
|
10088
|
+
// src/domain/tools/container-config-tools.ts
|
|
10089
|
+
import { z as z43 } from "zod";
|
|
10090
|
+
var log44 = createLogger("container-config-tools");
|
|
10091
|
+
var getContainerConfigInputSchema = z43.object({ routerId: z43.string().describe("Target router identifier from the router registry") }).strict();
|
|
10092
|
+
var getContainerConfigTool = {
|
|
10093
|
+
name: "get_container_config",
|
|
10094
|
+
title: "Get Container Config",
|
|
10095
|
+
description: "Read global container configuration: registry URL, RAM high-water mark, and veth interface.",
|
|
10096
|
+
inputSchema: getContainerConfigInputSchema,
|
|
10097
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
10098
|
+
async handler(params, context) {
|
|
10099
|
+
getContainerConfigInputSchema.parse(params);
|
|
10100
|
+
log44.info({ routerId: context.routerId }, "Getting container config");
|
|
10101
|
+
try {
|
|
10102
|
+
const results2 = await context.routerClient.get("container/config");
|
|
10103
|
+
const cfg = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
|
|
10104
|
+
return {
|
|
10105
|
+
content: `Container config on ${context.routerId}: registry=${cfg["registry-url"] ?? "none"} ram-high=${cfg["ram-high"] ?? "?"}MB veth=${cfg["veth-interface"] ?? "none"}`,
|
|
10106
|
+
structuredContent: {
|
|
10107
|
+
routerId: context.routerId,
|
|
10108
|
+
registryUrl: cfg["registry-url"] ?? null,
|
|
10109
|
+
ramHighMb: cfg["ram-high"] ?? null,
|
|
10110
|
+
vethInterface: cfg["veth-interface"] ?? null
|
|
10111
|
+
}
|
|
10112
|
+
};
|
|
10113
|
+
} catch (err) {
|
|
10114
|
+
throw enrichError(err, { routerId: context.routerId, tool: "get_container_config" });
|
|
10115
|
+
}
|
|
10116
|
+
}
|
|
10117
|
+
};
|
|
10118
|
+
var manageContainerConfigInputSchema = z43.object({
|
|
10119
|
+
routerId: z43.string().describe("Target router identifier from the router registry"),
|
|
10120
|
+
registryUrl: z43.string().optional().describe("Container registry URL"),
|
|
10121
|
+
ramHighMb: z43.number().int().min(1).optional().describe("RAM high-water mark in MB"),
|
|
10122
|
+
vethInterface: z43.string().optional().describe("Veth interface name for container networking"),
|
|
10123
|
+
dryRun: z43.boolean().default(false).describe("Preview changes without applying")
|
|
10124
|
+
}).strict();
|
|
10125
|
+
var manageContainerConfigTool = {
|
|
10126
|
+
name: "manage_container_config",
|
|
10127
|
+
title: "Manage Container Config",
|
|
10128
|
+
description: "Update global container settings. Idempotent: returns no_change if nothing differs.",
|
|
10129
|
+
inputSchema: manageContainerConfigInputSchema,
|
|
10130
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
10131
|
+
snapshotPaths: ["container/config"],
|
|
10132
|
+
async handler(params, context) {
|
|
10133
|
+
const parsed = manageContainerConfigInputSchema.parse(params);
|
|
10134
|
+
log44.info({ routerId: context.routerId }, "Managing container config");
|
|
10135
|
+
try {
|
|
10136
|
+
const results2 = await context.routerClient.get("container/config");
|
|
10137
|
+
const current = Array.isArray(results2) && results2.length > 0 ? results2[0] : results2;
|
|
10138
|
+
const id = current[".id"];
|
|
10139
|
+
const changes = {};
|
|
10140
|
+
const diff = [];
|
|
10141
|
+
if (parsed.registryUrl !== void 0 && current["registry-url"] !== parsed.registryUrl) {
|
|
10142
|
+
changes["registry-url"] = parsed.registryUrl;
|
|
10143
|
+
diff.push({ property: "registry-url", before: current["registry-url"] ?? null, after: parsed.registryUrl });
|
|
10144
|
+
}
|
|
10145
|
+
if (parsed.ramHighMb !== void 0) {
|
|
10146
|
+
const next = String(parsed.ramHighMb);
|
|
10147
|
+
if (current["ram-high"] !== next) {
|
|
10148
|
+
changes["ram-high"] = next;
|
|
10149
|
+
diff.push({ property: "ram-high", before: current["ram-high"] ?? null, after: next });
|
|
10150
|
+
}
|
|
10151
|
+
}
|
|
10152
|
+
if (parsed.vethInterface !== void 0 && current["veth-interface"] !== parsed.vethInterface) {
|
|
10153
|
+
changes["veth-interface"] = parsed.vethInterface;
|
|
10154
|
+
diff.push({ property: "veth-interface", before: current["veth-interface"] ?? null, after: parsed.vethInterface });
|
|
10155
|
+
}
|
|
10156
|
+
if (Object.keys(changes).length === 0) {
|
|
10157
|
+
return {
|
|
10158
|
+
content: "Container config already matches. No changes made.",
|
|
10159
|
+
structuredContent: { action: "no_change", routerId: context.routerId }
|
|
10160
|
+
};
|
|
10161
|
+
}
|
|
10162
|
+
if (parsed.dryRun) {
|
|
10163
|
+
return {
|
|
10164
|
+
content: `Dry run: Would update container config on ${context.routerId}.`,
|
|
10165
|
+
structuredContent: { action: "dry_run", diff }
|
|
10166
|
+
};
|
|
10167
|
+
}
|
|
10168
|
+
await context.routerClient.update("container/config", id, changes);
|
|
10169
|
+
log44.info({ routerId: context.routerId }, "Container config updated");
|
|
10170
|
+
return {
|
|
10171
|
+
content: `Updated container config on ${context.routerId}.`,
|
|
10172
|
+
structuredContent: { action: "updated", routerId: context.routerId, diff }
|
|
10173
|
+
};
|
|
10174
|
+
} catch (err) {
|
|
10175
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_container_config" });
|
|
10176
|
+
}
|
|
10177
|
+
}
|
|
10178
|
+
};
|
|
10179
|
+
var listContainerEnvsInputSchema = z43.object({
|
|
10180
|
+
routerId: z43.string().describe("Target router identifier from the router registry"),
|
|
10181
|
+
name: z43.string().optional().describe("Filter by container name (exact match)"),
|
|
10182
|
+
limit: z43.number().int().min(1).max(500).default(100).describe("Maximum entries to return")
|
|
10183
|
+
}).strict();
|
|
10184
|
+
var listContainerEnvsTool = {
|
|
10185
|
+
name: "list_container_envs",
|
|
10186
|
+
title: "List Container Envs",
|
|
10187
|
+
description: "List container environment variable entries, optionally filtered by container name.",
|
|
10188
|
+
inputSchema: listContainerEnvsInputSchema,
|
|
10189
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
10190
|
+
async handler(params, context) {
|
|
10191
|
+
const parsed = listContainerEnvsInputSchema.parse(params);
|
|
10192
|
+
log44.info({ routerId: context.routerId }, "Listing container envs");
|
|
10193
|
+
try {
|
|
10194
|
+
const all = await context.routerClient.get("container/envs", {
|
|
10195
|
+
limit: void 0,
|
|
10196
|
+
offset: void 0
|
|
10197
|
+
});
|
|
10198
|
+
const filtered = parsed.name ? all.filter((e) => e.name === parsed.name) : all;
|
|
10199
|
+
const envs = filtered.slice(0, parsed.limit);
|
|
10200
|
+
return {
|
|
10201
|
+
content: `Container envs on ${context.routerId}: ${envs.length} returned`,
|
|
10202
|
+
structuredContent: { routerId: context.routerId, envs, total: all.length, returned: envs.length }
|
|
10203
|
+
};
|
|
10204
|
+
} catch (err) {
|
|
10205
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_container_envs" });
|
|
10206
|
+
}
|
|
10207
|
+
}
|
|
10208
|
+
};
|
|
10209
|
+
var manageContainerEnvInputSchema = z43.object({
|
|
10210
|
+
routerId: z43.string().describe("Target router identifier from the router registry"),
|
|
10211
|
+
action: z43.enum(["add", "remove"]).describe("Action to perform"),
|
|
10212
|
+
name: z43.string().describe("Container name"),
|
|
10213
|
+
key: z43.string().describe("Environment variable name \u2014 part of idempotency key"),
|
|
10214
|
+
value: z43.string().optional().describe("Environment variable value (required for add)"),
|
|
10215
|
+
dryRun: z43.boolean().default(false).describe("Preview changes without applying")
|
|
10216
|
+
}).strict();
|
|
10217
|
+
var manageContainerEnvTool = {
|
|
10218
|
+
name: "manage_container_env",
|
|
10219
|
+
title: "Manage Container Env",
|
|
10220
|
+
description: "Add or remove a container environment variable. Idempotent by name+key. add returns already_exists if the entry exists with the same value; throws CONFLICT if the value differs.",
|
|
10221
|
+
inputSchema: manageContainerEnvInputSchema,
|
|
10222
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
10223
|
+
async handler(params, context) {
|
|
10224
|
+
const parsed = manageContainerEnvInputSchema.parse(params);
|
|
10225
|
+
log44.info(
|
|
10226
|
+
{ routerId: context.routerId, action: parsed.action, name: parsed.name, key: parsed.key },
|
|
10227
|
+
"Managing container env"
|
|
10228
|
+
);
|
|
10229
|
+
try {
|
|
10230
|
+
const all = await context.routerClient.get("container/envs", {
|
|
10231
|
+
limit: void 0,
|
|
10232
|
+
offset: void 0
|
|
10233
|
+
});
|
|
10234
|
+
const existing = all.find(
|
|
10235
|
+
(e) => e.name === parsed.name && e.key === parsed.key
|
|
10236
|
+
);
|
|
10237
|
+
if (parsed.action === "add") {
|
|
10238
|
+
if (existing) {
|
|
10239
|
+
if (existing.value === parsed.value) {
|
|
10240
|
+
return {
|
|
10241
|
+
content: `Env ${parsed.name}.${parsed.key} already exists with same value.`,
|
|
10242
|
+
structuredContent: { action: "already_exists", name: parsed.name, key: parsed.key }
|
|
10243
|
+
};
|
|
10244
|
+
}
|
|
10245
|
+
throw new MikroMCPError({
|
|
10246
|
+
category: "CONFLICT" /* CONFLICT */,
|
|
10247
|
+
code: "CONTAINER_ENV_CONFLICT",
|
|
10248
|
+
message: `Env ${parsed.name}.${parsed.key} exists with a different value.`,
|
|
10249
|
+
details: { existingValue: existing.value, requestedValue: parsed.value },
|
|
10250
|
+
recoverability: {
|
|
10251
|
+
retryable: false,
|
|
10252
|
+
suggestedAction: "Remove the existing entry first.",
|
|
10253
|
+
alternativeTools: ["manage_container_env"]
|
|
10254
|
+
}
|
|
10255
|
+
});
|
|
10256
|
+
}
|
|
10257
|
+
if (parsed.value === void 0) {
|
|
10258
|
+
throw new MikroMCPError({
|
|
10259
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
10260
|
+
code: "ENV_VALUE_REQUIRED",
|
|
10261
|
+
message: "value is required for add.",
|
|
10262
|
+
recoverability: { retryable: false, suggestedAction: "Provide the value parameter." }
|
|
10263
|
+
});
|
|
10264
|
+
}
|
|
10265
|
+
if (parsed.dryRun) {
|
|
10266
|
+
return {
|
|
10267
|
+
content: `Dry run: Would add env ${parsed.name}.${parsed.key}=${parsed.value}.`,
|
|
10268
|
+
structuredContent: {
|
|
10269
|
+
action: "dry_run",
|
|
10270
|
+
diff: [{ property: "value", before: null, after: parsed.value }]
|
|
10271
|
+
}
|
|
10272
|
+
};
|
|
10273
|
+
}
|
|
10274
|
+
const created = await context.routerClient.create("container/envs", {
|
|
10275
|
+
name: parsed.name,
|
|
10276
|
+
key: parsed.key,
|
|
10277
|
+
value: parsed.value
|
|
10278
|
+
});
|
|
10279
|
+
log44.info({ name: parsed.name, key: parsed.key, id: created[".id"] }, "Container env added");
|
|
10280
|
+
return {
|
|
10281
|
+
content: `Added env ${parsed.name}.${parsed.key}.`,
|
|
10282
|
+
structuredContent: {
|
|
10283
|
+
action: "created",
|
|
10284
|
+
name: parsed.name,
|
|
10285
|
+
key: parsed.key,
|
|
10286
|
+
id: created[".id"]
|
|
10287
|
+
}
|
|
10288
|
+
};
|
|
10289
|
+
}
|
|
10290
|
+
if (!existing) {
|
|
10291
|
+
return {
|
|
10292
|
+
content: `Env ${parsed.name}.${parsed.key} not found. Nothing to remove.`,
|
|
10293
|
+
structuredContent: { action: "not_found", name: parsed.name, key: parsed.key }
|
|
10294
|
+
};
|
|
10295
|
+
}
|
|
10296
|
+
if (parsed.dryRun) {
|
|
10297
|
+
return {
|
|
10298
|
+
content: `Dry run: Would remove env ${parsed.name}.${parsed.key}.`,
|
|
10299
|
+
structuredContent: {
|
|
10300
|
+
action: "dry_run",
|
|
10301
|
+
diff: [{ property: "key", before: parsed.key, after: null }]
|
|
10302
|
+
}
|
|
10303
|
+
};
|
|
10304
|
+
}
|
|
10305
|
+
await context.routerClient.remove("container/envs", existing[".id"]);
|
|
10306
|
+
log44.info({ name: parsed.name, key: parsed.key }, "Container env removed");
|
|
10307
|
+
return {
|
|
10308
|
+
content: `Removed env ${parsed.name}.${parsed.key}.`,
|
|
10309
|
+
structuredContent: { action: "removed", name: parsed.name, key: parsed.key }
|
|
10310
|
+
};
|
|
10311
|
+
} catch (err) {
|
|
10312
|
+
if (err instanceof MikroMCPError) throw err;
|
|
10313
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_container_env" });
|
|
10314
|
+
}
|
|
10315
|
+
}
|
|
10316
|
+
};
|
|
10317
|
+
var listContainerMountsInputSchema = z43.object({
|
|
10318
|
+
routerId: z43.string().describe("Target router identifier from the router registry"),
|
|
10319
|
+
name: z43.string().optional().describe("Filter by mount name (exact match)"),
|
|
10320
|
+
limit: z43.number().int().min(1).max(500).default(100).describe("Maximum entries to return")
|
|
10321
|
+
}).strict();
|
|
10322
|
+
var listContainerMountsTool = {
|
|
10323
|
+
name: "list_container_mounts",
|
|
10324
|
+
title: "List Container Mounts",
|
|
10325
|
+
description: "List container volume mount definitions with source path, destination path, and mount name.",
|
|
10326
|
+
inputSchema: listContainerMountsInputSchema,
|
|
10327
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
10328
|
+
async handler(params, context) {
|
|
10329
|
+
const parsed = listContainerMountsInputSchema.parse(params);
|
|
10330
|
+
log44.info({ routerId: context.routerId }, "Listing container mounts");
|
|
10331
|
+
try {
|
|
10332
|
+
const all = await context.routerClient.get("container/mounts", {
|
|
10333
|
+
limit: void 0,
|
|
10334
|
+
offset: void 0
|
|
10335
|
+
});
|
|
10336
|
+
const filtered = parsed.name ? all.filter((m) => m.name === parsed.name) : all;
|
|
10337
|
+
const mounts = filtered.slice(0, parsed.limit);
|
|
10338
|
+
return {
|
|
10339
|
+
content: `Container mounts on ${context.routerId}: ${mounts.length} returned`,
|
|
10340
|
+
structuredContent: { routerId: context.routerId, mounts, total: all.length, returned: mounts.length }
|
|
10341
|
+
};
|
|
10342
|
+
} catch (err) {
|
|
10343
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_container_mounts" });
|
|
10344
|
+
}
|
|
10345
|
+
}
|
|
10346
|
+
};
|
|
10347
|
+
var manageContainerMountInputSchema = z43.object({
|
|
10348
|
+
routerId: z43.string().describe("Target router identifier from the router registry"),
|
|
10349
|
+
action: z43.enum(["add", "remove"]).describe("Action to perform"),
|
|
10350
|
+
name: z43.string().describe("Mount name \u2014 idempotency key"),
|
|
10351
|
+
src: z43.string().optional().describe("Host source path (required for add)"),
|
|
10352
|
+
dst: z43.string().optional().describe("Container destination path (required for add)"),
|
|
10353
|
+
dryRun: z43.boolean().default(false).describe("Preview changes without applying")
|
|
10354
|
+
}).strict();
|
|
10355
|
+
var manageContainerMountTool = {
|
|
10356
|
+
name: "manage_container_mount",
|
|
10357
|
+
title: "Manage Container Mount",
|
|
10358
|
+
description: "Add or remove a container volume mount. Idempotent by name: add returns already_exists if the mount exists with matching src/dst; throws CONFLICT if name exists with different paths.",
|
|
10359
|
+
inputSchema: manageContainerMountInputSchema,
|
|
10360
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
10361
|
+
async handler(params, context) {
|
|
10362
|
+
const parsed = manageContainerMountInputSchema.parse(params);
|
|
10363
|
+
log44.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing container mount");
|
|
10364
|
+
try {
|
|
10365
|
+
const all = await context.routerClient.get("container/mounts", {
|
|
10366
|
+
limit: void 0,
|
|
10367
|
+
offset: void 0
|
|
10368
|
+
});
|
|
10369
|
+
const existing = all.find((m) => m.name === parsed.name);
|
|
10370
|
+
if (parsed.action === "add") {
|
|
10371
|
+
if (existing) {
|
|
10372
|
+
if (existing.src === parsed.src && existing.dst === parsed.dst) {
|
|
10373
|
+
return {
|
|
10374
|
+
content: `Mount "${parsed.name}" already exists with same paths.`,
|
|
10375
|
+
structuredContent: { action: "already_exists", name: parsed.name }
|
|
10376
|
+
};
|
|
10377
|
+
}
|
|
10378
|
+
throw new MikroMCPError({
|
|
10379
|
+
category: "CONFLICT" /* CONFLICT */,
|
|
10380
|
+
code: "CONTAINER_MOUNT_CONFLICT",
|
|
10381
|
+
message: `Mount "${parsed.name}" exists with different src/dst paths.`,
|
|
10382
|
+
details: {
|
|
10383
|
+
existing: { src: existing.src, dst: existing.dst },
|
|
10384
|
+
requested: { src: parsed.src, dst: parsed.dst }
|
|
10385
|
+
},
|
|
10386
|
+
recoverability: {
|
|
10387
|
+
retryable: false,
|
|
10388
|
+
suggestedAction: "Remove the existing mount first.",
|
|
10389
|
+
alternativeTools: ["manage_container_mount"]
|
|
10390
|
+
}
|
|
10391
|
+
});
|
|
10392
|
+
}
|
|
10393
|
+
if (!parsed.src || !parsed.dst) {
|
|
10394
|
+
throw new MikroMCPError({
|
|
10395
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
10396
|
+
code: "MOUNT_PATHS_REQUIRED",
|
|
10397
|
+
message: "src and dst are required for add.",
|
|
10398
|
+
recoverability: { retryable: false, suggestedAction: "Provide src and dst parameters." }
|
|
10399
|
+
});
|
|
10400
|
+
}
|
|
10401
|
+
if (parsed.dryRun) {
|
|
10402
|
+
return {
|
|
10403
|
+
content: `Dry run: Would add mount "${parsed.name}" ${parsed.src} \u2192 ${parsed.dst}.`,
|
|
10404
|
+
structuredContent: {
|
|
10405
|
+
action: "dry_run",
|
|
10406
|
+
diff: [{ property: "name", before: null, after: parsed.name }]
|
|
10407
|
+
}
|
|
10408
|
+
};
|
|
10409
|
+
}
|
|
10410
|
+
const created = await context.routerClient.create("container/mounts", {
|
|
10411
|
+
name: parsed.name,
|
|
10412
|
+
src: parsed.src,
|
|
10413
|
+
dst: parsed.dst
|
|
10414
|
+
});
|
|
10415
|
+
log44.info({ name: parsed.name, id: created[".id"] }, "Container mount added");
|
|
10416
|
+
return {
|
|
10417
|
+
content: `Added mount "${parsed.name}".`,
|
|
10418
|
+
structuredContent: { action: "created", name: parsed.name, id: created[".id"] }
|
|
10419
|
+
};
|
|
10420
|
+
}
|
|
10421
|
+
if (!existing) {
|
|
10422
|
+
return {
|
|
10423
|
+
content: `Mount "${parsed.name}" not found. Nothing to remove.`,
|
|
10424
|
+
structuredContent: { action: "not_found", name: parsed.name }
|
|
10425
|
+
};
|
|
10426
|
+
}
|
|
10427
|
+
if (parsed.dryRun) {
|
|
10428
|
+
return {
|
|
10429
|
+
content: `Dry run: Would remove mount "${parsed.name}".`,
|
|
10430
|
+
structuredContent: {
|
|
10431
|
+
action: "dry_run",
|
|
10432
|
+
diff: [{ property: "name", before: parsed.name, after: null }]
|
|
10433
|
+
}
|
|
10434
|
+
};
|
|
10435
|
+
}
|
|
10436
|
+
await context.routerClient.remove("container/mounts", existing[".id"]);
|
|
10437
|
+
log44.info({ name: parsed.name }, "Container mount removed");
|
|
10438
|
+
return {
|
|
10439
|
+
content: `Removed mount "${parsed.name}".`,
|
|
10440
|
+
structuredContent: { action: "removed", name: parsed.name }
|
|
10441
|
+
};
|
|
10442
|
+
} catch (err) {
|
|
10443
|
+
if (err instanceof MikroMCPError) throw err;
|
|
10444
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_container_mount" });
|
|
10445
|
+
}
|
|
10446
|
+
}
|
|
10447
|
+
};
|
|
10448
|
+
var containerConfigTools = [
|
|
10449
|
+
getContainerConfigTool,
|
|
10450
|
+
manageContainerConfigTool,
|
|
10451
|
+
listContainerEnvsTool,
|
|
10452
|
+
manageContainerEnvTool,
|
|
10453
|
+
listContainerMountsTool,
|
|
10454
|
+
manageContainerMountTool
|
|
10455
|
+
];
|
|
10456
|
+
|
|
10457
|
+
// src/domain/tools/interface-list-tools.ts
|
|
10458
|
+
import { z as z44 } from "zod";
|
|
10459
|
+
var log45 = createLogger("interface-list-tools");
|
|
10460
|
+
var listInterfaceListsInputSchema = z44.object({
|
|
10461
|
+
routerId: z44.string().describe("Target router identifier from the router registry"),
|
|
10462
|
+
limit: z44.number().int().min(1).max(500).default(100).describe("Maximum lists to return")
|
|
10463
|
+
}).strict();
|
|
10464
|
+
var listInterfaceListsTool = {
|
|
10465
|
+
name: "list_interface_lists",
|
|
10466
|
+
title: "List Interface Lists",
|
|
10467
|
+
description: "List all interface lists defined on the router.",
|
|
10468
|
+
inputSchema: listInterfaceListsInputSchema,
|
|
10469
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
10470
|
+
async handler(params, context) {
|
|
10471
|
+
const parsed = listInterfaceListsInputSchema.parse(params);
|
|
10472
|
+
log45.info({ routerId: context.routerId }, "Listing interface lists");
|
|
10473
|
+
try {
|
|
10474
|
+
const all = await context.routerClient.get("interface/list", {
|
|
10475
|
+
limit: void 0,
|
|
10476
|
+
offset: void 0
|
|
10477
|
+
});
|
|
10478
|
+
const lists = all.slice(0, parsed.limit);
|
|
10479
|
+
return {
|
|
10480
|
+
content: `Interface lists on ${context.routerId}: ${lists.length} returned`,
|
|
10481
|
+
structuredContent: { routerId: context.routerId, lists, total: all.length, returned: lists.length }
|
|
10482
|
+
};
|
|
10483
|
+
} catch (err) {
|
|
10484
|
+
if (err instanceof MikroMCPError) throw err;
|
|
10485
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_interface_lists" });
|
|
10486
|
+
}
|
|
10487
|
+
}
|
|
10488
|
+
};
|
|
10489
|
+
var manageInterfaceListInputSchema = z44.object({
|
|
10490
|
+
routerId: z44.string().describe("Target router identifier from the router registry"),
|
|
10491
|
+
action: z44.enum(["add", "remove"]).describe("Action to perform"),
|
|
10492
|
+
name: z44.string().describe("Interface list name \u2014 idempotency key"),
|
|
10493
|
+
comment: z44.string().optional().describe("Optional comment"),
|
|
10494
|
+
dryRun: z44.boolean().default(false).describe("Preview changes without applying")
|
|
10495
|
+
}).strict();
|
|
10496
|
+
var manageInterfaceListTool = {
|
|
10497
|
+
name: "manage_interface_list",
|
|
10498
|
+
title: "Manage Interface List",
|
|
10499
|
+
description: "Add or remove an interface list. Idempotent by name. Removing a list that has members is blocked by RouterOS \u2014 the error is surfaced as-is.",
|
|
10500
|
+
inputSchema: manageInterfaceListInputSchema,
|
|
10501
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
10502
|
+
async handler(params, context) {
|
|
10503
|
+
const parsed = manageInterfaceListInputSchema.parse(params);
|
|
10504
|
+
log45.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing interface list");
|
|
10505
|
+
try {
|
|
10506
|
+
const all = await context.routerClient.get("interface/list", {
|
|
10507
|
+
limit: void 0,
|
|
10508
|
+
offset: void 0
|
|
10509
|
+
});
|
|
10510
|
+
const existing = all.find((l) => l.name === parsed.name);
|
|
10511
|
+
if (parsed.action === "add") {
|
|
10512
|
+
if (existing) {
|
|
10513
|
+
return {
|
|
10514
|
+
content: `Interface list "${parsed.name}" already exists. No changes made.`,
|
|
10515
|
+
structuredContent: { action: "already_exists", name: parsed.name }
|
|
10516
|
+
};
|
|
10517
|
+
}
|
|
10518
|
+
if (parsed.dryRun) {
|
|
10519
|
+
return {
|
|
10520
|
+
content: `Dry run: Would create interface list "${parsed.name}".`,
|
|
10521
|
+
structuredContent: {
|
|
10522
|
+
action: "dry_run",
|
|
10523
|
+
diff: [{ property: "name", before: null, after: parsed.name }]
|
|
10524
|
+
}
|
|
10525
|
+
};
|
|
10526
|
+
}
|
|
10527
|
+
const body = { name: parsed.name };
|
|
10528
|
+
if (parsed.comment) body.comment = parsed.comment;
|
|
10529
|
+
const created = await context.routerClient.create("interface/list", body);
|
|
10530
|
+
log45.info({ name: parsed.name, id: created[".id"] }, "Interface list created");
|
|
10531
|
+
return {
|
|
10532
|
+
content: `Created interface list "${parsed.name}".`,
|
|
10533
|
+
structuredContent: { action: "created", name: parsed.name, id: created[".id"] }
|
|
10534
|
+
};
|
|
10535
|
+
}
|
|
10536
|
+
if (!existing) {
|
|
10537
|
+
return {
|
|
10538
|
+
content: `Interface list "${parsed.name}" not found. Nothing to remove.`,
|
|
10539
|
+
structuredContent: { action: "not_found", name: parsed.name }
|
|
10540
|
+
};
|
|
10541
|
+
}
|
|
10542
|
+
if (parsed.dryRun) {
|
|
10543
|
+
return {
|
|
10544
|
+
content: `Dry run: Would remove interface list "${parsed.name}".`,
|
|
10545
|
+
structuredContent: {
|
|
10546
|
+
action: "dry_run",
|
|
10547
|
+
diff: [{ property: "name", before: parsed.name, after: null }]
|
|
10548
|
+
}
|
|
10549
|
+
};
|
|
10550
|
+
}
|
|
10551
|
+
await context.routerClient.remove("interface/list", existing[".id"]);
|
|
10552
|
+
log45.info({ name: parsed.name }, "Interface list removed");
|
|
10553
|
+
return {
|
|
10554
|
+
content: `Removed interface list "${parsed.name}".`,
|
|
10555
|
+
structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
|
|
10556
|
+
};
|
|
10557
|
+
} catch (err) {
|
|
10558
|
+
if (err instanceof MikroMCPError) throw err;
|
|
10559
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_interface_list" });
|
|
10560
|
+
}
|
|
10561
|
+
}
|
|
10562
|
+
};
|
|
10563
|
+
var manageInterfaceListMemberInputSchema = z44.object({
|
|
10564
|
+
routerId: z44.string().describe("Target router identifier from the router registry"),
|
|
10565
|
+
action: z44.enum(["add", "remove"]).describe("Action to perform"),
|
|
10566
|
+
list: z44.string().describe("Interface list name \u2014 part of composite idempotency key"),
|
|
10567
|
+
interface: z44.string().describe("Interface name to add/remove \u2014 part of composite idempotency key"),
|
|
10568
|
+
comment: z44.string().optional().describe("Optional comment"),
|
|
10569
|
+
dryRun: z44.boolean().default(false).describe("Preview changes without applying")
|
|
10570
|
+
}).strict();
|
|
10571
|
+
var manageInterfaceListMemberTool = {
|
|
10572
|
+
name: "manage_interface_list_member",
|
|
10573
|
+
title: "Manage Interface List Member",
|
|
10574
|
+
description: "Add or remove an interface from an interface list. Idempotent by list+interface composite key. add returns already_exists if the membership exists. remove returns not_found gracefully.",
|
|
10575
|
+
inputSchema: manageInterfaceListMemberInputSchema,
|
|
10576
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
10577
|
+
async handler(params, context) {
|
|
10578
|
+
const parsed = manageInterfaceListMemberInputSchema.parse(params);
|
|
10579
|
+
log45.info(
|
|
10580
|
+
{ routerId: context.routerId, action: parsed.action, list: parsed.list, interface: parsed.interface },
|
|
10581
|
+
"Managing interface list member"
|
|
10582
|
+
);
|
|
10583
|
+
try {
|
|
10584
|
+
const all = await context.routerClient.get("interface/list/member", {
|
|
10585
|
+
limit: void 0,
|
|
10586
|
+
offset: void 0
|
|
10587
|
+
});
|
|
10588
|
+
const existing = all.find(
|
|
10589
|
+
(m) => m.list === parsed.list && m.interface === parsed.interface
|
|
10590
|
+
);
|
|
10591
|
+
if (parsed.action === "add") {
|
|
10592
|
+
if (existing) {
|
|
10593
|
+
return {
|
|
10594
|
+
content: `Interface "${parsed.interface}" is already a member of list "${parsed.list}". No changes made.`,
|
|
10595
|
+
structuredContent: { action: "already_exists", list: parsed.list, interface: parsed.interface }
|
|
10596
|
+
};
|
|
10597
|
+
}
|
|
10598
|
+
if (parsed.dryRun) {
|
|
10599
|
+
return {
|
|
10600
|
+
content: `Dry run: Would add "${parsed.interface}" to list "${parsed.list}".`,
|
|
10601
|
+
structuredContent: {
|
|
10602
|
+
action: "dry_run",
|
|
10603
|
+
diff: [{ property: "interface", before: null, after: parsed.interface }]
|
|
10604
|
+
}
|
|
10605
|
+
};
|
|
10606
|
+
}
|
|
10607
|
+
const body = { list: parsed.list, interface: parsed.interface };
|
|
10608
|
+
if (parsed.comment) body.comment = parsed.comment;
|
|
10609
|
+
const created = await context.routerClient.create("interface/list/member", body);
|
|
10610
|
+
log45.info({ list: parsed.list, interface: parsed.interface, id: created[".id"] }, "Interface list member added");
|
|
10611
|
+
return {
|
|
10612
|
+
content: `Added "${parsed.interface}" to list "${parsed.list}".`,
|
|
10613
|
+
structuredContent: { action: "created", list: parsed.list, interface: parsed.interface, id: created[".id"] }
|
|
10614
|
+
};
|
|
10615
|
+
}
|
|
10616
|
+
if (!existing) {
|
|
10617
|
+
return {
|
|
10618
|
+
content: `Interface "${parsed.interface}" is not a member of list "${parsed.list}". Nothing to remove.`,
|
|
10619
|
+
structuredContent: { action: "not_found", list: parsed.list, interface: parsed.interface }
|
|
10620
|
+
};
|
|
10621
|
+
}
|
|
10622
|
+
if (parsed.dryRun) {
|
|
10623
|
+
return {
|
|
10624
|
+
content: `Dry run: Would remove "${parsed.interface}" from list "${parsed.list}".`,
|
|
10625
|
+
structuredContent: {
|
|
10626
|
+
action: "dry_run",
|
|
10627
|
+
diff: [{ property: "interface", before: parsed.interface, after: null }]
|
|
10628
|
+
}
|
|
10629
|
+
};
|
|
10630
|
+
}
|
|
10631
|
+
await context.routerClient.remove("interface/list/member", existing[".id"]);
|
|
10632
|
+
log45.info({ list: parsed.list, interface: parsed.interface }, "Interface list member removed");
|
|
10633
|
+
return {
|
|
10634
|
+
content: `Removed "${parsed.interface}" from list "${parsed.list}".`,
|
|
10635
|
+
structuredContent: {
|
|
10636
|
+
action: "removed",
|
|
10637
|
+
list: parsed.list,
|
|
10638
|
+
interface: parsed.interface,
|
|
10639
|
+
id: existing[".id"]
|
|
10640
|
+
}
|
|
10641
|
+
};
|
|
10642
|
+
} catch (err) {
|
|
10643
|
+
if (err instanceof MikroMCPError) throw err;
|
|
10644
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_interface_list_member" });
|
|
10645
|
+
}
|
|
10646
|
+
}
|
|
10647
|
+
};
|
|
10648
|
+
var interfaceListTools = [
|
|
10649
|
+
listInterfaceListsTool,
|
|
10650
|
+
manageInterfaceListTool,
|
|
10651
|
+
manageInterfaceListMemberTool
|
|
10652
|
+
];
|
|
10653
|
+
|
|
10654
|
+
// src/domain/tools/fleet-tools.ts
|
|
10655
|
+
import { z as z45 } from "zod";
|
|
10656
|
+
|
|
10657
|
+
// src/middleware/authz.ts
|
|
10658
|
+
function matchesPattern(name, pattern) {
|
|
10659
|
+
if (!pattern.includes("*")) return name === pattern;
|
|
10660
|
+
const prefix = pattern.slice(0, pattern.indexOf("*"));
|
|
10661
|
+
return name.startsWith(prefix);
|
|
10662
|
+
}
|
|
10663
|
+
function checkAuthz(identity, toolName, routerId) {
|
|
10664
|
+
if (identity.allowedRouters.length > 0 && !identity.allowedRouters.includes(routerId)) {
|
|
10665
|
+
throw new MikroMCPError({
|
|
10666
|
+
category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
|
|
10667
|
+
code: "ROUTER_NOT_ALLOWED",
|
|
10668
|
+
message: `Identity "${identity.id}" is not allowed to access router "${routerId}".`,
|
|
10669
|
+
details: { identityId: identity.id, routerId, allowedRouters: identity.allowedRouters },
|
|
9362
10670
|
recoverability: {
|
|
9363
10671
|
retryable: false,
|
|
9364
10672
|
suggestedAction: `Add "${routerId}" to allowedRouters for identity "${identity.id}" in identities.yaml.`
|
|
@@ -9512,7 +10820,7 @@ var SshClient = class {
|
|
|
9512
10820
|
// src/adapter/ftp-client.ts
|
|
9513
10821
|
import { Client as Client2 } from "basic-ftp";
|
|
9514
10822
|
import { Readable } from "stream";
|
|
9515
|
-
var
|
|
10823
|
+
var log46 = createLogger("ftp-client");
|
|
9516
10824
|
var FtpClient = class {
|
|
9517
10825
|
#host;
|
|
9518
10826
|
#port;
|
|
@@ -9525,7 +10833,7 @@ var FtpClient = class {
|
|
|
9525
10833
|
this.#password = password;
|
|
9526
10834
|
}
|
|
9527
10835
|
async upload(remoteName, content) {
|
|
9528
|
-
|
|
10836
|
+
log46.info({ host: this.#host, remoteName }, "Uploading file via FTP");
|
|
9529
10837
|
const client = new Client2();
|
|
9530
10838
|
try {
|
|
9531
10839
|
await client.access({
|
|
@@ -9535,7 +10843,7 @@ var FtpClient = class {
|
|
|
9535
10843
|
password: this.#password
|
|
9536
10844
|
});
|
|
9537
10845
|
await client.uploadFrom(Readable.from(Buffer.from(content, "utf-8")), remoteName);
|
|
9538
|
-
|
|
10846
|
+
log46.info({ host: this.#host, remoteName }, "FTP upload complete");
|
|
9539
10847
|
} finally {
|
|
9540
10848
|
client.close();
|
|
9541
10849
|
}
|
|
@@ -9643,17 +10951,17 @@ function checkFleetConfirmation(args, secret) {
|
|
|
9643
10951
|
}
|
|
9644
10952
|
|
|
9645
10953
|
// src/domain/tools/fleet-tools.ts
|
|
9646
|
-
var
|
|
9647
|
-
var checkHealthInputSchema =
|
|
9648
|
-
routerId:
|
|
10954
|
+
var log47 = createLogger("fleet-tools");
|
|
10955
|
+
var checkHealthInputSchema = z45.object({
|
|
10956
|
+
routerId: z45.string().describe("Target router to health-check")
|
|
9649
10957
|
}).strict();
|
|
9650
|
-
var bulkExecuteInputSchema =
|
|
9651
|
-
toolName:
|
|
9652
|
-
routerIds:
|
|
9653
|
-
tags:
|
|
9654
|
-
params:
|
|
9655
|
-
concurrency:
|
|
9656
|
-
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(
|
|
9657
10965
|
"Fleet confirmation token from a prior APPROVAL_REQUIRED response. Required to fan out a destructive tool."
|
|
9658
10966
|
)
|
|
9659
10967
|
}).strict();
|
|
@@ -9672,7 +10980,7 @@ function createFleetTools(baseTools2) {
|
|
|
9672
10980
|
},
|
|
9673
10981
|
async handler(params, context) {
|
|
9674
10982
|
checkHealthInputSchema.parse(params);
|
|
9675
|
-
|
|
10983
|
+
log47.info({ routerId: context.routerId }, "Checking router health");
|
|
9676
10984
|
const startMs = Date.now();
|
|
9677
10985
|
try {
|
|
9678
10986
|
const resources = await context.routerClient.get("system/resource");
|
|
@@ -9734,7 +11042,7 @@ function createFleetTools(baseTools2) {
|
|
|
9734
11042
|
}
|
|
9735
11043
|
});
|
|
9736
11044
|
}
|
|
9737
|
-
|
|
11045
|
+
log47.info(
|
|
9738
11046
|
{ toolName: parsed.toolName, concurrency: parsed.concurrency },
|
|
9739
11047
|
"bulk_execute invoked"
|
|
9740
11048
|
);
|
|
@@ -9781,7 +11089,7 @@ function createFleetTools(baseTools2) {
|
|
|
9781
11089
|
try {
|
|
9782
11090
|
resolved.push(context.routerRegistry.getRouter(id));
|
|
9783
11091
|
} catch {
|
|
9784
|
-
|
|
11092
|
+
log47.warn({ routerId: id }, "bulk_execute: router not found");
|
|
9785
11093
|
preErrors.push({
|
|
9786
11094
|
routerId: id,
|
|
9787
11095
|
status: "error",
|
|
@@ -9963,9 +11271,13 @@ var baseTools = [
|
|
|
9963
11271
|
...ipServiceTools,
|
|
9964
11272
|
...pppoeTools,
|
|
9965
11273
|
...ovpnTools,
|
|
11274
|
+
...pppTools,
|
|
9966
11275
|
...upgradeTools,
|
|
9967
11276
|
...backupTools,
|
|
9968
|
-
...logTools
|
|
11277
|
+
...logTools,
|
|
11278
|
+
...networkTestTools,
|
|
11279
|
+
...containerConfigTools,
|
|
11280
|
+
...interfaceListTools
|
|
9969
11281
|
];
|
|
9970
11282
|
var allTools = [
|
|
9971
11283
|
...baseTools,
|
|
@@ -10364,7 +11676,7 @@ var CircuitBreaker = class {
|
|
|
10364
11676
|
};
|
|
10365
11677
|
|
|
10366
11678
|
// src/mcp/tool-executor.ts
|
|
10367
|
-
var
|
|
11679
|
+
var log48 = createLogger("tool-executor");
|
|
10368
11680
|
async function executeToolCall(tool, args, deps) {
|
|
10369
11681
|
const { registry, pool, circuitBreakers, config, identityRegistry } = deps;
|
|
10370
11682
|
const correlationId = nanoid4();
|
|
@@ -10460,9 +11772,9 @@ async function executeToolCall(tool, args, deps) {
|
|
|
10460
11772
|
try {
|
|
10461
11773
|
const meta = await takeSnapshot(toolContext.routerClient, routerId, path, config.snapshotDir);
|
|
10462
11774
|
snapshotIds.push(meta.id);
|
|
10463
|
-
|
|
11775
|
+
log48.debug({ snapshotId: meta.id, path }, "Snapshot taken");
|
|
10464
11776
|
} catch (err) {
|
|
10465
|
-
|
|
11777
|
+
log48.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
|
|
10466
11778
|
}
|
|
10467
11779
|
}
|
|
10468
11780
|
}
|
|
@@ -10504,7 +11816,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
10504
11816
|
durationMs: Date.now() - startMs
|
|
10505
11817
|
}, config.auditLogPath);
|
|
10506
11818
|
}
|
|
10507
|
-
|
|
11819
|
+
log48.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
|
|
10508
11820
|
recordToolCall(tool.name, "success");
|
|
10509
11821
|
return formatToolResult(result);
|
|
10510
11822
|
} catch (err) {
|
|
@@ -10513,7 +11825,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
10513
11825
|
const failedRouterId = args.routerId;
|
|
10514
11826
|
if (failedRouterId) {
|
|
10515
11827
|
pool.removeClient(failedRouterId);
|
|
10516
|
-
|
|
11828
|
+
log48.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
|
|
10517
11829
|
}
|
|
10518
11830
|
}
|
|
10519
11831
|
if (journalId) {
|
|
@@ -10543,7 +11855,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
10543
11855
|
durationMs: Date.now() - startMs
|
|
10544
11856
|
}, config.auditLogPath);
|
|
10545
11857
|
}
|
|
10546
|
-
|
|
11858
|
+
log48.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
|
|
10547
11859
|
recordToolCall(tool.name, "error");
|
|
10548
11860
|
return formatError(error);
|
|
10549
11861
|
}
|
|
@@ -10551,12 +11863,12 @@ async function executeToolCall(tool, args, deps) {
|
|
|
10551
11863
|
}
|
|
10552
11864
|
|
|
10553
11865
|
// src/mcp/tool-registry.ts
|
|
10554
|
-
var
|
|
11866
|
+
var log49 = createLogger("tool-registry");
|
|
10555
11867
|
function registerAllTools(server, registry, pool, circuitBreakers, config, identityRegistry) {
|
|
10556
11868
|
const deps = { registry, pool, circuitBreakers, config, identityRegistry };
|
|
10557
11869
|
for (const tool of allTools) {
|
|
10558
11870
|
const registrationSchema = tool.annotations.destructiveHint ? tool.inputSchema.extend({
|
|
10559
|
-
confirmationToken:
|
|
11871
|
+
confirmationToken: z46.string().optional().describe(
|
|
10560
11872
|
"Token from a prior APPROVAL_REQUIRED response. Re-submit the identical call with this token to confirm the destructive action."
|
|
10561
11873
|
)
|
|
10562
11874
|
}) : tool.inputSchema;
|
|
@@ -10570,56 +11882,56 @@ function registerAllTools(server, registry, pool, circuitBreakers, config, ident
|
|
|
10570
11882
|
},
|
|
10571
11883
|
(args) => executeToolCall(tool, args, deps)
|
|
10572
11884
|
);
|
|
10573
|
-
|
|
11885
|
+
log49.debug({ tool: tool.name }, "Registered tool");
|
|
10574
11886
|
}
|
|
10575
|
-
|
|
11887
|
+
log49.info({ count: allTools.length }, "All tools registered");
|
|
10576
11888
|
}
|
|
10577
11889
|
|
|
10578
11890
|
// src/config/router-registry.ts
|
|
10579
11891
|
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
10580
11892
|
import { parse } from "yaml";
|
|
10581
|
-
import { z as
|
|
10582
|
-
var
|
|
10583
|
-
var RouterConfigSchema =
|
|
10584
|
-
host:
|
|
10585
|
-
port:
|
|
10586
|
-
tls:
|
|
10587
|
-
enabled:
|
|
10588
|
-
rejectUnauthorized:
|
|
10589
|
-
ca:
|
|
10590
|
-
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()
|
|
10591
11903
|
}).strict(),
|
|
10592
|
-
credentials:
|
|
10593
|
-
source:
|
|
10594
|
-
envPrefix:
|
|
10595
|
-
vaultPath:
|
|
11904
|
+
credentials: z47.object({
|
|
11905
|
+
source: z47.enum(["env", "vault"]),
|
|
11906
|
+
envPrefix: z47.string().optional(),
|
|
11907
|
+
vaultPath: z47.string().optional()
|
|
10596
11908
|
}).strict(),
|
|
10597
|
-
tags:
|
|
10598
|
-
rosVersion:
|
|
10599
|
-
sshPort:
|
|
10600
|
-
sshFingerprint:
|
|
10601
|
-
cmdAllow:
|
|
10602
|
-
cmdDeny:
|
|
10603
|
-
maintenanceWindows:
|
|
10604
|
-
|
|
10605
|
-
days:
|
|
10606
|
-
|
|
11909
|
+
tags: z47.array(z47.string()).default([]),
|
|
11910
|
+
rosVersion: z47.string().min(1),
|
|
11911
|
+
sshPort: z47.number().int().min(1).max(65535).optional(),
|
|
11912
|
+
sshFingerprint: z47.string().optional(),
|
|
11913
|
+
cmdAllow: z47.array(z47.string()).optional(),
|
|
11914
|
+
cmdDeny: z47.array(z47.string()).optional(),
|
|
11915
|
+
maintenanceWindows: z47.array(
|
|
11916
|
+
z47.object({
|
|
11917
|
+
days: z47.array(
|
|
11918
|
+
z47.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
|
|
10607
11919
|
),
|
|
10608
|
-
startTime:
|
|
10609
|
-
endTime:
|
|
10610
|
-
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)
|
|
10611
11923
|
}).strict()
|
|
10612
11924
|
).optional()
|
|
10613
11925
|
}).strict();
|
|
10614
|
-
var ConfigFileSchema =
|
|
10615
|
-
routers:
|
|
11926
|
+
var ConfigFileSchema = z47.object({
|
|
11927
|
+
routers: z47.record(z47.string(), RouterConfigSchema)
|
|
10616
11928
|
}).strict();
|
|
10617
11929
|
var RouterRegistry = class {
|
|
10618
11930
|
routers;
|
|
10619
11931
|
constructor(configPath) {
|
|
10620
11932
|
this.routers = /* @__PURE__ */ new Map();
|
|
10621
11933
|
if (!existsSync(configPath)) {
|
|
10622
|
-
|
|
11934
|
+
log50.warn({ configPath }, "Router config file not found; starting with empty registry");
|
|
10623
11935
|
return;
|
|
10624
11936
|
}
|
|
10625
11937
|
const raw = readFileSync2(configPath, "utf-8");
|
|
@@ -10633,14 +11945,14 @@ ${issues}`);
|
|
|
10633
11945
|
for (const [id, config] of Object.entries(result.data.routers)) {
|
|
10634
11946
|
const routerConfig = { ...config, id };
|
|
10635
11947
|
if (routerConfig.tls.enabled && !routerConfig.tls.rejectUnauthorized) {
|
|
10636
|
-
|
|
11948
|
+
log50.warn(
|
|
10637
11949
|
{ routerId: id },
|
|
10638
11950
|
"TLS certificate validation is DISABLED (rejectUnauthorized=false). Set tls.fingerprint to pin the server certificate instead."
|
|
10639
11951
|
);
|
|
10640
11952
|
}
|
|
10641
11953
|
this.routers.set(id, routerConfig);
|
|
10642
11954
|
}
|
|
10643
|
-
|
|
11955
|
+
log50.info({ count: this.routers.size }, "Loaded routers from config");
|
|
10644
11956
|
}
|
|
10645
11957
|
getRouter(id) {
|
|
10646
11958
|
const router = this.routers.get(id);
|
|
@@ -10925,7 +12237,7 @@ var ConnectionPool = class {
|
|
|
10925
12237
|
};
|
|
10926
12238
|
|
|
10927
12239
|
// src/version.ts
|
|
10928
|
-
var VERSION = "1.
|
|
12240
|
+
var VERSION = "1.5.0";
|
|
10929
12241
|
|
|
10930
12242
|
// src/mcp/server.ts
|
|
10931
12243
|
function createServerFactory(config, identityRegistry) {
|
|
@@ -10955,7 +12267,7 @@ import { randomUUID } from "crypto";
|
|
|
10955
12267
|
import { createServer } from "http";
|
|
10956
12268
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
10957
12269
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
10958
|
-
var
|
|
12270
|
+
var log51 = createLogger("transport-http");
|
|
10959
12271
|
var BodyTooLargeError = class extends Error {
|
|
10960
12272
|
constructor() {
|
|
10961
12273
|
super("Request body exceeds maximum allowed size");
|
|
@@ -11047,7 +12359,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
11047
12359
|
return;
|
|
11048
12360
|
}
|
|
11049
12361
|
if (!checkRateLimit(ip)) {
|
|
11050
|
-
|
|
12362
|
+
log51.warn({ ip }, "Rate limit exceeded");
|
|
11051
12363
|
res.writeHead(429, { "Content-Type": "application/json" });
|
|
11052
12364
|
res.end(JSON.stringify({ error: "Too many requests. Retry after 60 seconds." }));
|
|
11053
12365
|
return;
|
|
@@ -11088,7 +12400,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
11088
12400
|
}
|
|
11089
12401
|
return;
|
|
11090
12402
|
}
|
|
11091
|
-
|
|
12403
|
+
log51.error({ err }, "HTTP transport error");
|
|
11092
12404
|
if (!res.headersSent) {
|
|
11093
12405
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
11094
12406
|
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
@@ -11098,7 +12410,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
11098
12410
|
httpServer.on("close", () => clearInterval(sweepTimer));
|
|
11099
12411
|
await new Promise((resolve, reject) => {
|
|
11100
12412
|
httpServer.listen(port, bindHost, () => {
|
|
11101
|
-
|
|
12413
|
+
log51.info(
|
|
11102
12414
|
{ port, bindHost },
|
|
11103
12415
|
"HTTP transport listening (POST /mcp for Streamable HTTP, GET /sse for legacy SSE)"
|
|
11104
12416
|
);
|
|
@@ -11122,13 +12434,13 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
11122
12434
|
sessionIdGenerator: () => randomUUID(),
|
|
11123
12435
|
onsessioninitialized: (id) => {
|
|
11124
12436
|
streamableSessions.set(id, transport2);
|
|
11125
|
-
|
|
12437
|
+
log51.info({ sessionId: id }, "Streamable HTTP session created");
|
|
11126
12438
|
}
|
|
11127
12439
|
});
|
|
11128
12440
|
transport2.onclose = () => {
|
|
11129
12441
|
if (transport2.sessionId) {
|
|
11130
12442
|
streamableSessions.delete(transport2.sessionId);
|
|
11131
|
-
|
|
12443
|
+
log51.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
|
|
11132
12444
|
}
|
|
11133
12445
|
};
|
|
11134
12446
|
await makeServer().connect(transport2);
|
|
@@ -11145,7 +12457,7 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
11145
12457
|
sseTransport.onclose = () => {
|
|
11146
12458
|
sseTransports.delete(sseTransport.sessionId);
|
|
11147
12459
|
};
|
|
11148
|
-
|
|
12460
|
+
log51.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
|
|
11149
12461
|
await sseTransport.start();
|
|
11150
12462
|
return;
|
|
11151
12463
|
}
|
|
@@ -11166,23 +12478,23 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
11166
12478
|
// src/config/identity-registry.ts
|
|
11167
12479
|
import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
|
|
11168
12480
|
import { parse as parse2 } from "yaml";
|
|
11169
|
-
import { z as
|
|
12481
|
+
import { z as z48 } from "zod";
|
|
11170
12482
|
import bcrypt from "bcryptjs";
|
|
11171
|
-
var
|
|
11172
|
-
var IdentityConfigSchema =
|
|
11173
|
-
token:
|
|
11174
|
-
role:
|
|
11175
|
-
allowedRouters:
|
|
11176
|
-
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([])
|
|
11177
12489
|
}).strict();
|
|
11178
|
-
var ConfigFileSchema2 =
|
|
11179
|
-
identities:
|
|
12490
|
+
var ConfigFileSchema2 = z48.object({
|
|
12491
|
+
identities: z48.record(z48.string(), IdentityConfigSchema)
|
|
11180
12492
|
}).strict();
|
|
11181
12493
|
var IdentityRegistry = class {
|
|
11182
12494
|
identities = [];
|
|
11183
12495
|
constructor(configPath) {
|
|
11184
12496
|
if (!existsSync2(configPath)) {
|
|
11185
|
-
|
|
12497
|
+
log52.warn(
|
|
11186
12498
|
{ configPath },
|
|
11187
12499
|
"Identity config file not found; starting with empty registry (stdio callers do not require it)"
|
|
11188
12500
|
);
|
|
@@ -11205,7 +12517,7 @@ ${issues}`);
|
|
|
11205
12517
|
allowedToolPatterns: config.allowedToolPatterns
|
|
11206
12518
|
});
|
|
11207
12519
|
}
|
|
11208
|
-
|
|
12520
|
+
log52.info({ count: this.identities.length }, "Loaded identities from config");
|
|
11209
12521
|
}
|
|
11210
12522
|
getIdentities() {
|
|
11211
12523
|
return this.identities.map(({ id, role, allowedRouters, allowedToolPatterns }) => ({
|
|
@@ -11234,7 +12546,7 @@ ${issues}`);
|
|
|
11234
12546
|
// src/domain/snapshot/retention.ts
|
|
11235
12547
|
import { readdir, stat, unlink } from "fs/promises";
|
|
11236
12548
|
import { join as join3 } from "path";
|
|
11237
|
-
var
|
|
12549
|
+
var log53 = createLogger("snapshot-retention");
|
|
11238
12550
|
async function pruneSnapshots(snapshotDir, maxAgeDays) {
|
|
11239
12551
|
const cutoffMs = Date.now() - maxAgeDays * 864e5;
|
|
11240
12552
|
let removed = 0;
|
|
@@ -11261,21 +12573,21 @@ async function pruneSnapshots(snapshotDir, maxAgeDays) {
|
|
|
11261
12573
|
removed++;
|
|
11262
12574
|
}
|
|
11263
12575
|
} catch (err) {
|
|
11264
|
-
|
|
12576
|
+
log53.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
|
|
11265
12577
|
}
|
|
11266
12578
|
}
|
|
11267
12579
|
}
|
|
11268
12580
|
if (removed > 0) {
|
|
11269
|
-
|
|
12581
|
+
log53.info({ removed, maxAgeDays }, "Pruned expired snapshots");
|
|
11270
12582
|
}
|
|
11271
12583
|
return removed;
|
|
11272
12584
|
}
|
|
11273
12585
|
|
|
11274
12586
|
// src/cli/serve.ts
|
|
11275
|
-
var
|
|
12587
|
+
var log54 = createLogger("main");
|
|
11276
12588
|
async function runServe() {
|
|
11277
12589
|
const config = loadAppConfig();
|
|
11278
|
-
|
|
12590
|
+
log54.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
|
|
11279
12591
|
void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
|
|
11280
12592
|
const identityRegistry = new IdentityRegistry(config.identitiesPath);
|
|
11281
12593
|
if (config.transport === "http") {
|
|
@@ -11283,7 +12595,7 @@ async function runServe() {
|
|
|
11283
12595
|
(i) => i.role === "readonly" || i.role === "operator"
|
|
11284
12596
|
);
|
|
11285
12597
|
if (hasLimitedRoles && !config.confirmationSecret) {
|
|
11286
|
-
|
|
12598
|
+
log54.error(
|
|
11287
12599
|
"MIKROMCP_CONFIRMATION_SECRET is required in HTTP mode when identities with role readonly or operator are configured"
|
|
11288
12600
|
);
|
|
11289
12601
|
process.exit(1);
|
|
@@ -11293,7 +12605,7 @@ async function runServe() {
|
|
|
11293
12605
|
if (config.transport === "stdio") {
|
|
11294
12606
|
const stdioIdentity = getStdioIdentity(config.stdioIdentity, identityRegistry);
|
|
11295
12607
|
await withIdentity(stdioIdentity, () => connectStdio(makeServer()));
|
|
11296
|
-
|
|
12608
|
+
log54.info("MikroMCP server running via stdio");
|
|
11297
12609
|
} else if (config.transport === "http") {
|
|
11298
12610
|
await connectHttp(
|
|
11299
12611
|
makeServer,
|
|
@@ -11305,13 +12617,13 @@ async function runServe() {
|
|
|
11305
12617
|
},
|
|
11306
12618
|
identityRegistry
|
|
11307
12619
|
);
|
|
11308
|
-
|
|
12620
|
+
log54.info(
|
|
11309
12621
|
{ port: config.port, bindHost: config.bindHost },
|
|
11310
12622
|
"MikroMCP server running via HTTP/SSE"
|
|
11311
12623
|
);
|
|
11312
12624
|
}
|
|
11313
12625
|
const shutdown = () => {
|
|
11314
|
-
|
|
12626
|
+
log54.info("Shutting down MikroMCP server");
|
|
11315
12627
|
pool.closeAll();
|
|
11316
12628
|
process.exit(0);
|
|
11317
12629
|
};
|
|
@@ -11324,7 +12636,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
|
11324
12636
|
import { homedir as homedir2 } from "os";
|
|
11325
12637
|
import * as net from "net";
|
|
11326
12638
|
import chalk from "chalk";
|
|
11327
|
-
var localVersion = "1.
|
|
12639
|
+
var localVersion = "1.5.0";
|
|
11328
12640
|
var results = [];
|
|
11329
12641
|
function ok(msg) {
|
|
11330
12642
|
console.log(chalk.green("\u2705 ") + msg);
|
|
@@ -11976,7 +13288,7 @@ async function runInit() {
|
|
|
11976
13288
|
}
|
|
11977
13289
|
|
|
11978
13290
|
// src/cli/index.ts
|
|
11979
|
-
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.
|
|
13291
|
+
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.5.0");
|
|
11980
13292
|
program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
|
|
11981
13293
|
program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
|
|
11982
13294
|
program.command("init").description("Interactive setup wizard").action(runInit);
|