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