@usex/mikrotik-mcp 3.7.0 → 3.8.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 +7 -7
- package/dist/cli.js +115 -61
- package/dist/index.js +115 -61
- package/package.json +1 -1
- package/schemas/config.schema.json +44 -3
- package/schemas/tool-catalog.json +1 -1
package/README.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<img src="assets/logo.svg" alt="@usex/mikrotik-mcp" width="440" />
|
|
3
|
-
<p><strong>A Bun-native MCP server that turns one or more MikroTik routers into
|
|
3
|
+
<p><strong>A Bun-native MCP server that turns one or more MikroTik routers into 706 tools your AI can drive.</strong><br/>
|
|
4
4
|
Firewall · routing · DHCP/DNS · wireless · QoS · and a complete VPN suite — over SSH, with transactional Safe Mode.</p>
|
|
5
5
|
|
|
6
6
|
<p>
|
|
7
7
|
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-7C3AED.svg"></a>
|
|
8
8
|
<img alt="Runtime: Bun" src="https://img.shields.io/badge/runtime-Bun%20%E2%89%A5%201.3-06B6D4.svg">
|
|
9
9
|
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-strict-6366F1.svg">
|
|
10
|
-
<img alt="MCP" src="https://img.shields.io/badge/MCP-
|
|
10
|
+
<img alt="MCP" src="https://img.shields.io/badge/MCP-706%20tools-1F2937.svg">
|
|
11
11
|
<a href="docs/"><img alt="Docs" src="https://img.shields.io/badge/docs-reference-7C3AED.svg"></a>
|
|
12
12
|
</p>
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
`@usex/mikrotik-mcp` exposes **MikroTik RouterOS** as **
|
|
18
|
-
tools across
|
|
17
|
+
`@usex/mikrotik-mcp` exposes **MikroTik RouterOS** as **706 [Model Context Protocol](https://modelcontextprotocol.io)
|
|
18
|
+
tools across 111 modules**, so an AI client (Claude Desktop, Claude Code, any MCP
|
|
19
19
|
client) can read and configure your router in plain language. It speaks to the
|
|
20
20
|
device over **SSH** — no agent, no API package to install on RouterOS — runs on
|
|
21
21
|
**[Bun](https://bun.sh)**, and validates every tool call against a Zod schema.
|
|
@@ -46,7 +46,7 @@ in memory and auto-reverts if your session drops, so you can't lock yourself out
|
|
|
46
46
|
|
|
47
47
|
## Why it's different
|
|
48
48
|
|
|
49
|
-
- 🧰 **Breadth** —
|
|
49
|
+
- 🧰 **Breadth** — 706 tools covering the whole device: L2 (bridge, VLAN, wireless,
|
|
50
50
|
PoE), L3 (addressing, routing, DHCP, DNS), security (firewall, NAT, address-lists,
|
|
51
51
|
certificates), QoS (queues), and system ops (users, logs, backups, scheduler).
|
|
52
52
|
- 🔐 **A complete VPN suite** — WireGuard, IPsec (IKEv1/IKEv2), L2TP, PPTP, SSTP,
|
|
@@ -87,7 +87,7 @@ mikrotik-mcp serve
|
|
|
87
87
|
against the server (from source):
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
bun run inspect # opens the Inspector UI to browse/run all
|
|
90
|
+
bun run inspect # opens the Inspector UI to browse/run all 706 tools
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
**Prefer SSH keys over a password?** Point the server at a key file instead — and
|
|
@@ -115,7 +115,7 @@ bun run build # bundle to dist/
|
|
|
115
115
|
|
|
116
116
|
## The tool catalog
|
|
117
117
|
|
|
118
|
-
**
|
|
118
|
+
**706 tools across 111 modules.** Full, always-current reference (parameters +
|
|
119
119
|
risk per tool) is generated from source: **[docs/tools-reference.md](docs/tools-reference.md)**.
|
|
120
120
|
|
|
121
121
|
| Group | Tools | Modules |
|
package/dist/cli.js
CHANGED
|
@@ -60,6 +60,12 @@ var DashboardConfigSchema = z.object({
|
|
|
60
60
|
maxBodyBytes: z.coerce.number().int().nonnegative().default(16384),
|
|
61
61
|
token: z.string().optional()
|
|
62
62
|
});
|
|
63
|
+
var ToolFilterSchema = z.object({
|
|
64
|
+
enabledModules: z.array(z.string()).default([]),
|
|
65
|
+
disabledModules: z.array(z.string()).default([]),
|
|
66
|
+
enabledGroups: z.array(z.string()).default([]),
|
|
67
|
+
disabledGroups: z.array(z.string()).default([])
|
|
68
|
+
});
|
|
63
69
|
var MikrotikConfigSchema = z.object({
|
|
64
70
|
devices: z.record(z.string(), DeviceConfigSchema).default(() => ({ default: DeviceConfigSchema.parse({}) })),
|
|
65
71
|
defaultDevice: z.string().default("default"),
|
|
@@ -67,6 +73,7 @@ var MikrotikConfigSchema = z.object({
|
|
|
67
73
|
s3: S3ConfigSchema.optional(),
|
|
68
74
|
dashboard: DashboardConfigSchema.default(() => DashboardConfigSchema.parse({})),
|
|
69
75
|
readOnly: z.boolean().default(false),
|
|
76
|
+
tools: ToolFilterSchema.default(() => ToolFilterSchema.parse({})),
|
|
70
77
|
backupDir: z.string().optional()
|
|
71
78
|
});
|
|
72
79
|
function env(...names) {
|
|
@@ -112,7 +119,8 @@ function parseDevicesSource(raw, fromFile) {
|
|
|
112
119
|
const defaultDevice = typeof obj.defaultDevice === "string" ? obj.defaultDevice : undefined;
|
|
113
120
|
const s3 = structured && obj.s3 && typeof obj.s3 === "object" ? obj.s3 : undefined;
|
|
114
121
|
const dashboard = structured && obj.dashboard && typeof obj.dashboard === "object" ? obj.dashboard : undefined;
|
|
115
|
-
|
|
122
|
+
const tools = structured && obj.tools && typeof obj.tools === "object" ? obj.tools : undefined;
|
|
123
|
+
return { devices, defaultDevice, s3, dashboard, tools };
|
|
116
124
|
}
|
|
117
125
|
var configSource = { path: DEFAULT_CONFIG_FILE, fromFile: false };
|
|
118
126
|
function getConfigSource() {
|
|
@@ -147,6 +155,7 @@ function loadConfig(argv = process.argv.slice(2)) {
|
|
|
147
155
|
const devicesInline = flags.devices ?? env("MIKROTIK_DEVICES");
|
|
148
156
|
let fileS3 = {};
|
|
149
157
|
let fileDashboard = {};
|
|
158
|
+
let fileTools;
|
|
150
159
|
if (configFile || devicesInline) {
|
|
151
160
|
const src = configFile ? parseDevicesSource(configFile, true) : parseDevicesSource(devicesInline, false);
|
|
152
161
|
for (const [name, dc] of Object.entries(src.devices))
|
|
@@ -157,6 +166,7 @@ function loadConfig(argv = process.argv.slice(2)) {
|
|
|
157
166
|
defaultDevice = Object.keys(src.devices)[0];
|
|
158
167
|
fileS3 = src.s3;
|
|
159
168
|
fileDashboard = src.dashboard;
|
|
169
|
+
fileTools = src.tools;
|
|
160
170
|
}
|
|
161
171
|
const s3 = {
|
|
162
172
|
accessKeyId: pick("s3-access-key-id", "S3_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID"),
|
|
@@ -179,6 +189,14 @@ function loadConfig(argv = process.argv.slice(2)) {
|
|
|
179
189
|
};
|
|
180
190
|
const isTruthy = (v) => /^(1|true|yes|on)$/i.test(v ?? "");
|
|
181
191
|
const readOnly = isTruthy(pick("read-only", "MIKROTIK_READ_ONLY"));
|
|
192
|
+
const csv = (v) => v === undefined ? undefined : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
193
|
+
const tools = {
|
|
194
|
+
enabledModules: csv(pick("tools-enabled-modules", "MIKROTIK_TOOLS__ENABLED_MODULES")),
|
|
195
|
+
disabledModules: csv(pick("tools-disabled-modules", "MIKROTIK_TOOLS__DISABLED_MODULES")),
|
|
196
|
+
enabledGroups: csv(pick("tools-enabled-groups", "MIKROTIK_TOOLS__ENABLED_GROUPS")),
|
|
197
|
+
disabledGroups: csv(pick("tools-disabled-groups", "MIKROTIK_TOOLS__DISABLED_GROUPS")),
|
|
198
|
+
...fileTools
|
|
199
|
+
};
|
|
182
200
|
const boolOpt = (v) => v === undefined ? undefined : isTruthy(v);
|
|
183
201
|
const dashboard = {
|
|
184
202
|
enabled: boolOpt(pick("dashboard", "MIKROTIK_DASHBOARD__ENABLED", "MIKROTIK_DASHBOARD")),
|
|
@@ -199,6 +217,7 @@ function loadConfig(argv = process.argv.slice(2)) {
|
|
|
199
217
|
mcp,
|
|
200
218
|
dashboard,
|
|
201
219
|
readOnly,
|
|
220
|
+
tools,
|
|
202
221
|
...hasS3 ? { s3 } : {}
|
|
203
222
|
};
|
|
204
223
|
const pruned = JSON.parse(JSON.stringify(raw));
|
|
@@ -1048,6 +1067,16 @@ function isEmpty(result) {
|
|
|
1048
1067
|
const t = result.trim();
|
|
1049
1068
|
return t === "" || t === "no such item" || t === "no such item (4)";
|
|
1050
1069
|
}
|
|
1070
|
+
function extractCreatedId(output) {
|
|
1071
|
+
const star = output.match(/\*[0-9A-Fa-f]+/);
|
|
1072
|
+
if (star)
|
|
1073
|
+
return star[0];
|
|
1074
|
+
const num = output.trim().match(/^\d+$/);
|
|
1075
|
+
return num ? num[0] : undefined;
|
|
1076
|
+
}
|
|
1077
|
+
function readBackUnavailable(details) {
|
|
1078
|
+
return isEmpty(details) || /no such item/i.test(details) || looksLikeError(details);
|
|
1079
|
+
}
|
|
1051
1080
|
function looksLikeError(result) {
|
|
1052
1081
|
const t = result.toLowerCase();
|
|
1053
1082
|
return t.includes("failure:") || t.includes("syntax error") || t.includes("bad command") || t.includes("bad parameter") || t.includes("expected end of command") || t.includes("invalid value") || t.includes("input does not match") || t.includes("ambiguous value") || t.startsWith("error");
|
|
@@ -6044,16 +6073,17 @@ var firewallFilterTools = [
|
|
|
6044
6073
|
ctx.info(`Creating firewall filter rule: chain=${a.chain}, action=${a.action}`);
|
|
6045
6074
|
const cmd = new Cmd("/ip firewall filter add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("connection-state", a.connection_state).opt("connection-nat-state", a.connection_nat_state).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("limit", a.limit).opt("tcp-flags", a.tcp_flags).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
6046
6075
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6076
|
+
const trimmed = result.trim();
|
|
6077
|
+
if (looksLikeError(trimmed)) {
|
|
6078
|
+
const hint = placeBeforeError(trimmed, a.place_before);
|
|
6079
|
+
return `Failed to create firewall filter rule: ${hint ?? trimmed}`;
|
|
6080
|
+
}
|
|
6081
|
+
const createdId = extractCreatedId(trimmed);
|
|
6082
|
+
if (createdId) {
|
|
6083
|
+
const details = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${createdId}`, ctx);
|
|
6084
|
+
return readBackUnavailable(details) ? `Firewall filter rule created (id ${createdId}).` : `Firewall filter rule created successfully:
|
|
6052
6085
|
|
|
6053
|
-
${details}
|
|
6054
|
-
}
|
|
6055
|
-
const hint = placeBeforeError(result, a.place_before);
|
|
6056
|
-
return `Failed to create firewall filter rule: ${hint ?? result}`;
|
|
6086
|
+
${details}`;
|
|
6057
6087
|
}
|
|
6058
6088
|
const count = await executeMikrotikCommand("/ip firewall filter print detail count-only", ctx);
|
|
6059
6089
|
const c = count.trim();
|
|
@@ -6341,16 +6371,17 @@ var firewallNatTools = [
|
|
|
6341
6371
|
}
|
|
6342
6372
|
const cmd = new Cmd("/ip firewall nat add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("to-addresses", a.to_addresses).opt("to-ports", a.to_ports).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
6343
6373
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6374
|
+
const trimmed = result.trim();
|
|
6375
|
+
if (looksLikeError(trimmed)) {
|
|
6376
|
+
const hint = placeBeforeError(trimmed, a.place_before);
|
|
6377
|
+
return `Failed to create NAT rule: ${hint ?? trimmed}`;
|
|
6378
|
+
}
|
|
6379
|
+
const createdId = extractCreatedId(trimmed);
|
|
6380
|
+
if (createdId) {
|
|
6381
|
+
const details = await executeMikrotikCommand(`/ip firewall nat print detail where .id=${createdId}`, ctx);
|
|
6382
|
+
return readBackUnavailable(details) ? `NAT rule created (id ${createdId}).` : `NAT rule created successfully:
|
|
6349
6383
|
|
|
6350
|
-
${details}
|
|
6351
|
-
}
|
|
6352
|
-
const hint = placeBeforeError(result, a.place_before);
|
|
6353
|
-
return `Failed to create NAT rule: ${hint ?? result}`;
|
|
6384
|
+
${details}`;
|
|
6354
6385
|
}
|
|
6355
6386
|
const count = await executeMikrotikCommand("/ip firewall nat print detail count-only", ctx);
|
|
6356
6387
|
const c = count.trim();
|
|
@@ -8013,15 +8044,16 @@ var ipv6FirewallFilterTools = [
|
|
|
8013
8044
|
ctx.info(`Creating IPv6 firewall filter rule: chain=${a.chain}, action=${a.action}`);
|
|
8014
8045
|
const cmd = new Cmd("/ipv6 firewall filter add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("in-interface-list", a.in_interface_list).opt("out-interface-list", a.out_interface_list).opt("connection-state", a.connection_state).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("hop-limit", a.hop_limit).opt("limit", a.limit).opt("tcp-flags", a.tcp_flags).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
8015
8046
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8047
|
+
const trimmed = result.trim();
|
|
8048
|
+
if (looksLikeError(trimmed)) {
|
|
8049
|
+
return `Failed to create IPv6 firewall filter rule: ${trimmed}`;
|
|
8050
|
+
}
|
|
8051
|
+
const createdId = extractCreatedId(trimmed);
|
|
8052
|
+
if (createdId) {
|
|
8053
|
+
const details = await executeMikrotikCommand(`/ipv6 firewall filter print detail where .id=${createdId}`, ctx);
|
|
8054
|
+
return readBackUnavailable(details) ? `IPv6 firewall filter rule created (id ${createdId}).` : `IPv6 firewall filter rule created successfully:
|
|
8021
8055
|
|
|
8022
|
-
${details}
|
|
8023
|
-
}
|
|
8024
|
-
return `Failed to create IPv6 firewall filter rule: ${result}`;
|
|
8056
|
+
${details}`;
|
|
8025
8057
|
}
|
|
8026
8058
|
const count = await executeMikrotikCommand("/ipv6 firewall filter print detail count-only", ctx);
|
|
8027
8059
|
const c = count.trim();
|
|
@@ -8274,15 +8306,16 @@ var ipv6FirewallNatTools = [
|
|
|
8274
8306
|
ctx.info(`Creating IPv6 firewall NAT rule: chain=${a.chain}, action=${a.action}`);
|
|
8275
8307
|
const cmd = new Cmd("/ipv6 firewall nat add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("to-address", a.to_address).opt("to-ports", a.to_ports).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
8276
8308
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8309
|
+
const trimmed = result.trim();
|
|
8310
|
+
if (looksLikeError(trimmed)) {
|
|
8311
|
+
return `Failed to create IPv6 firewall NAT rule: ${trimmed}`;
|
|
8312
|
+
}
|
|
8313
|
+
const createdId = extractCreatedId(trimmed);
|
|
8314
|
+
if (createdId) {
|
|
8315
|
+
const details = await executeMikrotikCommand(`/ipv6 firewall nat print detail where .id=${createdId}`, ctx);
|
|
8316
|
+
return readBackUnavailable(details) ? `IPv6 firewall NAT rule created (id ${createdId}).` : `IPv6 firewall NAT rule created successfully:
|
|
8282
8317
|
|
|
8283
|
-
${details}
|
|
8284
|
-
}
|
|
8285
|
-
return `Failed to create IPv6 firewall NAT rule: ${result}`;
|
|
8318
|
+
${details}`;
|
|
8286
8319
|
}
|
|
8287
8320
|
const count = await executeMikrotikCommand("/ipv6 firewall nat print detail count-only", ctx);
|
|
8288
8321
|
const c = count.trim();
|
|
@@ -8538,15 +8571,16 @@ var ipv6FirewallMangleTools = [
|
|
|
8538
8571
|
ctx.info(`Creating IPv6 firewall mangle rule: chain=${a.chain}, action=${a.action}`);
|
|
8539
8572
|
const cmd = new Cmd("/ipv6 firewall mangle add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("connection-mark", a.connection_mark).opt("packet-mark", a.packet_mark).opt("routing-mark", a.routing_mark).opt("new-connection-mark", a.new_connection_mark).opt("new-packet-mark", a.new_packet_mark).opt("new-routing-mark", a.new_routing_mark).opt("new-dscp", a.new_dscp).opt("new-hop-limit", a.new_hop_limit).bool("passthrough", a.passthrough).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
8540
8573
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8574
|
+
const trimmed = result.trim();
|
|
8575
|
+
if (looksLikeError(trimmed)) {
|
|
8576
|
+
return `Failed to create IPv6 firewall mangle rule: ${trimmed}`;
|
|
8577
|
+
}
|
|
8578
|
+
const createdId = extractCreatedId(trimmed);
|
|
8579
|
+
if (createdId) {
|
|
8580
|
+
const details = await executeMikrotikCommand(`/ipv6 firewall mangle print detail where .id=${createdId}`, ctx);
|
|
8581
|
+
return readBackUnavailable(details) ? `IPv6 firewall mangle rule created (id ${createdId}).` : `IPv6 firewall mangle rule created successfully:
|
|
8546
8582
|
|
|
8547
|
-
${details}
|
|
8548
|
-
}
|
|
8549
|
-
return `Failed to create IPv6 firewall mangle rule: ${result}`;
|
|
8583
|
+
${details}`;
|
|
8550
8584
|
}
|
|
8551
8585
|
const count = await executeMikrotikCommand("/ipv6 firewall mangle print detail count-only", ctx);
|
|
8552
8586
|
const c = count.trim();
|
|
@@ -8777,15 +8811,16 @@ var ipv6FirewallRawTools = [
|
|
|
8777
8811
|
ctx.info(`Creating IPv6 firewall raw rule: chain=${a.chain}, action=${a.action}`);
|
|
8778
8812
|
const cmd = new Cmd("/ipv6 firewall raw add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
8779
8813
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8814
|
+
const trimmed = result.trim();
|
|
8815
|
+
if (looksLikeError(trimmed)) {
|
|
8816
|
+
return `Failed to create IPv6 firewall raw rule: ${trimmed}`;
|
|
8817
|
+
}
|
|
8818
|
+
const createdId = extractCreatedId(trimmed);
|
|
8819
|
+
if (createdId) {
|
|
8820
|
+
const details = await executeMikrotikCommand(`/ipv6 firewall raw print detail where .id=${createdId}`, ctx);
|
|
8821
|
+
return readBackUnavailable(details) ? `IPv6 firewall raw rule created (id ${createdId}).` : `IPv6 firewall raw rule created successfully:
|
|
8785
8822
|
|
|
8786
|
-
${details}
|
|
8787
|
-
}
|
|
8788
|
-
return `Failed to create IPv6 firewall raw rule: ${result}`;
|
|
8823
|
+
${details}`;
|
|
8789
8824
|
}
|
|
8790
8825
|
const count = await executeMikrotikCommand("/ipv6 firewall raw print detail count-only", ctx);
|
|
8791
8826
|
const c = count.trim();
|
|
@@ -17698,15 +17733,16 @@ var switchRuleTools = [
|
|
|
17698
17733
|
ctx.info(`Adding switch rule: switch=${a.switch}, ports=${a.ports}`);
|
|
17699
17734
|
const cmd = new Cmd("/interface ethernet switch rule add").set("switch", a.switch).set("ports", a.ports).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-mac-address", a.src_mac_address).opt("dst-mac-address", a.dst_mac_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("mac-protocol", a.mac_protocol).opt("vlan-id", a.vlan_id).opt("vlan-priority", a.vlan_priority).opt("dscp", a.dscp).opt("flow-label", a.flow_label).opt("new-dst-ports", a.new_dst_ports).opt("new-vlan-id", a.new_vlan_id).opt("new-vlan-priority", a.new_vlan_priority).bool("redirect-to-cpu", a.redirect_to_cpu).bool("copy-to-cpu", a.copy_to_cpu).bool("mirror", a.mirror).opt("rate", a.rate).opt("comment", a.comment).flag("disabled", a.disabled).build();
|
|
17700
17735
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
17701
|
-
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
|
|
17705
|
-
|
|
17736
|
+
const trimmed = result.trim();
|
|
17737
|
+
if (looksLikeError(trimmed)) {
|
|
17738
|
+
return `Failed to add switch rule: ${trimmed}`;
|
|
17739
|
+
}
|
|
17740
|
+
const createdId = extractCreatedId(trimmed);
|
|
17741
|
+
if (createdId) {
|
|
17742
|
+
const details = await executeMikrotikCommand(`/interface ethernet switch rule print detail where .id=${createdId}`, ctx);
|
|
17743
|
+
return readBackUnavailable(details) ? `Switch rule added (id ${createdId}).` : `Switch rule added successfully:
|
|
17706
17744
|
|
|
17707
|
-
${details}
|
|
17708
|
-
}
|
|
17709
|
-
return `Failed to add switch rule: ${result}`;
|
|
17745
|
+
${details}`;
|
|
17710
17746
|
}
|
|
17711
17747
|
const count = await executeMikrotikCommand("/interface ethernet switch rule print detail count-only", ctx);
|
|
17712
17748
|
const c = count.trim();
|
|
@@ -21611,6 +21647,23 @@ var moduleCatalog = [
|
|
|
21611
21647
|
}
|
|
21612
21648
|
];
|
|
21613
21649
|
var allToolModules = moduleCatalog.map((m) => m.tools);
|
|
21650
|
+
function selectToolModules(filter = {}, catalog = moduleCatalog) {
|
|
21651
|
+
const lc = (xs) => new Set((xs ?? []).map((s) => s.toLowerCase()));
|
|
21652
|
+
const enabledModules = lc(filter.enabledModules);
|
|
21653
|
+
const disabledModules = lc(filter.disabledModules);
|
|
21654
|
+
const enabledGroups = lc(filter.enabledGroups);
|
|
21655
|
+
const disabledGroups = lc(filter.disabledGroups);
|
|
21656
|
+
const hasAllow = enabledModules.size > 0 || enabledGroups.size > 0;
|
|
21657
|
+
return catalog.filter((m) => {
|
|
21658
|
+
const slug = m.slug.toLowerCase();
|
|
21659
|
+
const group = m.group.toLowerCase();
|
|
21660
|
+
if (disabledModules.has(slug) || disabledGroups.has(group))
|
|
21661
|
+
return false;
|
|
21662
|
+
if (hasAllow && !(enabledModules.has(slug) || enabledGroups.has(group)))
|
|
21663
|
+
return false;
|
|
21664
|
+
return true;
|
|
21665
|
+
}).map((m) => m.tools);
|
|
21666
|
+
}
|
|
21614
21667
|
|
|
21615
21668
|
// src/observability/dashboard.ts
|
|
21616
21669
|
import { readFileSync as readFileSync6 } from "fs";
|
|
@@ -23223,7 +23276,7 @@ function registerPrompts(server) {
|
|
|
23223
23276
|
// package.json
|
|
23224
23277
|
var package_default = {
|
|
23225
23278
|
name: "@usex/mikrotik-mcp",
|
|
23226
|
-
version: "3.
|
|
23279
|
+
version: "3.8.0",
|
|
23227
23280
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
23228
23281
|
keywords: [
|
|
23229
23282
|
"ai",
|
|
@@ -23415,7 +23468,8 @@ function createServer(opts = {}) {
|
|
|
23415
23468
|
server.server.sendLoggingMessage({ level, data: message }).catch(() => {});
|
|
23416
23469
|
} catch {}
|
|
23417
23470
|
});
|
|
23418
|
-
const
|
|
23471
|
+
const toolModules = selectToolModules(getConfig().tools);
|
|
23472
|
+
const toolCount = registerTools(server, toolModules, {
|
|
23419
23473
|
sendLog,
|
|
23420
23474
|
deviceNames: names,
|
|
23421
23475
|
readOnly
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,12 @@ var DashboardConfigSchema = z.object({
|
|
|
56
56
|
maxBodyBytes: z.coerce.number().int().nonnegative().default(16384),
|
|
57
57
|
token: z.string().optional()
|
|
58
58
|
});
|
|
59
|
+
var ToolFilterSchema = z.object({
|
|
60
|
+
enabledModules: z.array(z.string()).default([]),
|
|
61
|
+
disabledModules: z.array(z.string()).default([]),
|
|
62
|
+
enabledGroups: z.array(z.string()).default([]),
|
|
63
|
+
disabledGroups: z.array(z.string()).default([])
|
|
64
|
+
});
|
|
59
65
|
var MikrotikConfigSchema = z.object({
|
|
60
66
|
devices: z.record(z.string(), DeviceConfigSchema).default(() => ({ default: DeviceConfigSchema.parse({}) })),
|
|
61
67
|
defaultDevice: z.string().default("default"),
|
|
@@ -63,6 +69,7 @@ var MikrotikConfigSchema = z.object({
|
|
|
63
69
|
s3: S3ConfigSchema.optional(),
|
|
64
70
|
dashboard: DashboardConfigSchema.default(() => DashboardConfigSchema.parse({})),
|
|
65
71
|
readOnly: z.boolean().default(false),
|
|
72
|
+
tools: ToolFilterSchema.default(() => ToolFilterSchema.parse({})),
|
|
66
73
|
backupDir: z.string().optional()
|
|
67
74
|
});
|
|
68
75
|
function env(...names) {
|
|
@@ -108,7 +115,8 @@ function parseDevicesSource(raw, fromFile) {
|
|
|
108
115
|
const defaultDevice = typeof obj.defaultDevice === "string" ? obj.defaultDevice : undefined;
|
|
109
116
|
const s3 = structured && obj.s3 && typeof obj.s3 === "object" ? obj.s3 : undefined;
|
|
110
117
|
const dashboard = structured && obj.dashboard && typeof obj.dashboard === "object" ? obj.dashboard : undefined;
|
|
111
|
-
|
|
118
|
+
const tools = structured && obj.tools && typeof obj.tools === "object" ? obj.tools : undefined;
|
|
119
|
+
return { devices, defaultDevice, s3, dashboard, tools };
|
|
112
120
|
}
|
|
113
121
|
var configSource = { path: DEFAULT_CONFIG_FILE, fromFile: false };
|
|
114
122
|
function loadConfig(argv = process.argv.slice(2)) {
|
|
@@ -140,6 +148,7 @@ function loadConfig(argv = process.argv.slice(2)) {
|
|
|
140
148
|
const devicesInline = flags.devices ?? env("MIKROTIK_DEVICES");
|
|
141
149
|
let fileS3 = {};
|
|
142
150
|
let fileDashboard = {};
|
|
151
|
+
let fileTools;
|
|
143
152
|
if (configFile || devicesInline) {
|
|
144
153
|
const src = configFile ? parseDevicesSource(configFile, true) : parseDevicesSource(devicesInline, false);
|
|
145
154
|
for (const [name, dc] of Object.entries(src.devices))
|
|
@@ -150,6 +159,7 @@ function loadConfig(argv = process.argv.slice(2)) {
|
|
|
150
159
|
defaultDevice = Object.keys(src.devices)[0];
|
|
151
160
|
fileS3 = src.s3;
|
|
152
161
|
fileDashboard = src.dashboard;
|
|
162
|
+
fileTools = src.tools;
|
|
153
163
|
}
|
|
154
164
|
const s3 = {
|
|
155
165
|
accessKeyId: pick("s3-access-key-id", "S3_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID"),
|
|
@@ -172,6 +182,14 @@ function loadConfig(argv = process.argv.slice(2)) {
|
|
|
172
182
|
};
|
|
173
183
|
const isTruthy = (v) => /^(1|true|yes|on)$/i.test(v ?? "");
|
|
174
184
|
const readOnly = isTruthy(pick("read-only", "MIKROTIK_READ_ONLY"));
|
|
185
|
+
const csv = (v) => v === undefined ? undefined : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
186
|
+
const tools = {
|
|
187
|
+
enabledModules: csv(pick("tools-enabled-modules", "MIKROTIK_TOOLS__ENABLED_MODULES")),
|
|
188
|
+
disabledModules: csv(pick("tools-disabled-modules", "MIKROTIK_TOOLS__DISABLED_MODULES")),
|
|
189
|
+
enabledGroups: csv(pick("tools-enabled-groups", "MIKROTIK_TOOLS__ENABLED_GROUPS")),
|
|
190
|
+
disabledGroups: csv(pick("tools-disabled-groups", "MIKROTIK_TOOLS__DISABLED_GROUPS")),
|
|
191
|
+
...fileTools
|
|
192
|
+
};
|
|
175
193
|
const boolOpt = (v) => v === undefined ? undefined : isTruthy(v);
|
|
176
194
|
const dashboard = {
|
|
177
195
|
enabled: boolOpt(pick("dashboard", "MIKROTIK_DASHBOARD__ENABLED", "MIKROTIK_DASHBOARD")),
|
|
@@ -192,6 +210,7 @@ function loadConfig(argv = process.argv.slice(2)) {
|
|
|
192
210
|
mcp,
|
|
193
211
|
dashboard,
|
|
194
212
|
readOnly,
|
|
213
|
+
tools,
|
|
195
214
|
...hasS3 ? { s3 } : {}
|
|
196
215
|
};
|
|
197
216
|
const pruned = JSON.parse(JSON.stringify(raw));
|
|
@@ -1033,6 +1052,16 @@ function isEmpty(result) {
|
|
|
1033
1052
|
const t = result.trim();
|
|
1034
1053
|
return t === "" || t === "no such item" || t === "no such item (4)";
|
|
1035
1054
|
}
|
|
1055
|
+
function extractCreatedId(output) {
|
|
1056
|
+
const star = output.match(/\*[0-9A-Fa-f]+/);
|
|
1057
|
+
if (star)
|
|
1058
|
+
return star[0];
|
|
1059
|
+
const num = output.trim().match(/^\d+$/);
|
|
1060
|
+
return num ? num[0] : undefined;
|
|
1061
|
+
}
|
|
1062
|
+
function readBackUnavailable(details) {
|
|
1063
|
+
return isEmpty(details) || /no such item/i.test(details) || looksLikeError(details);
|
|
1064
|
+
}
|
|
1036
1065
|
function looksLikeError(result) {
|
|
1037
1066
|
const t = result.toLowerCase();
|
|
1038
1067
|
return t.includes("failure:") || t.includes("syntax error") || t.includes("bad command") || t.includes("bad parameter") || t.includes("expected end of command") || t.includes("invalid value") || t.includes("input does not match") || t.includes("ambiguous value") || t.startsWith("error");
|
|
@@ -6056,16 +6085,17 @@ var firewallFilterTools = [
|
|
|
6056
6085
|
ctx.info(`Creating firewall filter rule: chain=${a.chain}, action=${a.action}`);
|
|
6057
6086
|
const cmd = new Cmd("/ip firewall filter add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("connection-state", a.connection_state).opt("connection-nat-state", a.connection_nat_state).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("limit", a.limit).opt("tcp-flags", a.tcp_flags).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
6058
6087
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6088
|
+
const trimmed = result.trim();
|
|
6089
|
+
if (looksLikeError(trimmed)) {
|
|
6090
|
+
const hint = placeBeforeError(trimmed, a.place_before);
|
|
6091
|
+
return `Failed to create firewall filter rule: ${hint ?? trimmed}`;
|
|
6092
|
+
}
|
|
6093
|
+
const createdId = extractCreatedId(trimmed);
|
|
6094
|
+
if (createdId) {
|
|
6095
|
+
const details = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${createdId}`, ctx);
|
|
6096
|
+
return readBackUnavailable(details) ? `Firewall filter rule created (id ${createdId}).` : `Firewall filter rule created successfully:
|
|
6064
6097
|
|
|
6065
|
-
${details}
|
|
6066
|
-
}
|
|
6067
|
-
const hint = placeBeforeError(result, a.place_before);
|
|
6068
|
-
return `Failed to create firewall filter rule: ${hint ?? result}`;
|
|
6098
|
+
${details}`;
|
|
6069
6099
|
}
|
|
6070
6100
|
const count = await executeMikrotikCommand("/ip firewall filter print detail count-only", ctx);
|
|
6071
6101
|
const c = count.trim();
|
|
@@ -6353,16 +6383,17 @@ var firewallNatTools = [
|
|
|
6353
6383
|
}
|
|
6354
6384
|
const cmd = new Cmd("/ip firewall nat add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("to-addresses", a.to_addresses).opt("to-ports", a.to_ports).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
6355
6385
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6386
|
+
const trimmed = result.trim();
|
|
6387
|
+
if (looksLikeError(trimmed)) {
|
|
6388
|
+
const hint = placeBeforeError(trimmed, a.place_before);
|
|
6389
|
+
return `Failed to create NAT rule: ${hint ?? trimmed}`;
|
|
6390
|
+
}
|
|
6391
|
+
const createdId = extractCreatedId(trimmed);
|
|
6392
|
+
if (createdId) {
|
|
6393
|
+
const details = await executeMikrotikCommand(`/ip firewall nat print detail where .id=${createdId}`, ctx);
|
|
6394
|
+
return readBackUnavailable(details) ? `NAT rule created (id ${createdId}).` : `NAT rule created successfully:
|
|
6361
6395
|
|
|
6362
|
-
${details}
|
|
6363
|
-
}
|
|
6364
|
-
const hint = placeBeforeError(result, a.place_before);
|
|
6365
|
-
return `Failed to create NAT rule: ${hint ?? result}`;
|
|
6396
|
+
${details}`;
|
|
6366
6397
|
}
|
|
6367
6398
|
const count = await executeMikrotikCommand("/ip firewall nat print detail count-only", ctx);
|
|
6368
6399
|
const c = count.trim();
|
|
@@ -8025,15 +8056,16 @@ var ipv6FirewallFilterTools = [
|
|
|
8025
8056
|
ctx.info(`Creating IPv6 firewall filter rule: chain=${a.chain}, action=${a.action}`);
|
|
8026
8057
|
const cmd = new Cmd("/ipv6 firewall filter add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("in-interface-list", a.in_interface_list).opt("out-interface-list", a.out_interface_list).opt("connection-state", a.connection_state).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("hop-limit", a.hop_limit).opt("limit", a.limit).opt("tcp-flags", a.tcp_flags).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
8027
8058
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8059
|
+
const trimmed = result.trim();
|
|
8060
|
+
if (looksLikeError(trimmed)) {
|
|
8061
|
+
return `Failed to create IPv6 firewall filter rule: ${trimmed}`;
|
|
8062
|
+
}
|
|
8063
|
+
const createdId = extractCreatedId(trimmed);
|
|
8064
|
+
if (createdId) {
|
|
8065
|
+
const details = await executeMikrotikCommand(`/ipv6 firewall filter print detail where .id=${createdId}`, ctx);
|
|
8066
|
+
return readBackUnavailable(details) ? `IPv6 firewall filter rule created (id ${createdId}).` : `IPv6 firewall filter rule created successfully:
|
|
8033
8067
|
|
|
8034
|
-
${details}
|
|
8035
|
-
}
|
|
8036
|
-
return `Failed to create IPv6 firewall filter rule: ${result}`;
|
|
8068
|
+
${details}`;
|
|
8037
8069
|
}
|
|
8038
8070
|
const count = await executeMikrotikCommand("/ipv6 firewall filter print detail count-only", ctx);
|
|
8039
8071
|
const c = count.trim();
|
|
@@ -8286,15 +8318,16 @@ var ipv6FirewallNatTools = [
|
|
|
8286
8318
|
ctx.info(`Creating IPv6 firewall NAT rule: chain=${a.chain}, action=${a.action}`);
|
|
8287
8319
|
const cmd = new Cmd("/ipv6 firewall nat add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("to-address", a.to_address).opt("to-ports", a.to_ports).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
8288
8320
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8321
|
+
const trimmed = result.trim();
|
|
8322
|
+
if (looksLikeError(trimmed)) {
|
|
8323
|
+
return `Failed to create IPv6 firewall NAT rule: ${trimmed}`;
|
|
8324
|
+
}
|
|
8325
|
+
const createdId = extractCreatedId(trimmed);
|
|
8326
|
+
if (createdId) {
|
|
8327
|
+
const details = await executeMikrotikCommand(`/ipv6 firewall nat print detail where .id=${createdId}`, ctx);
|
|
8328
|
+
return readBackUnavailable(details) ? `IPv6 firewall NAT rule created (id ${createdId}).` : `IPv6 firewall NAT rule created successfully:
|
|
8294
8329
|
|
|
8295
|
-
${details}
|
|
8296
|
-
}
|
|
8297
|
-
return `Failed to create IPv6 firewall NAT rule: ${result}`;
|
|
8330
|
+
${details}`;
|
|
8298
8331
|
}
|
|
8299
8332
|
const count = await executeMikrotikCommand("/ipv6 firewall nat print detail count-only", ctx);
|
|
8300
8333
|
const c = count.trim();
|
|
@@ -8550,15 +8583,16 @@ var ipv6FirewallMangleTools = [
|
|
|
8550
8583
|
ctx.info(`Creating IPv6 firewall mangle rule: chain=${a.chain}, action=${a.action}`);
|
|
8551
8584
|
const cmd = new Cmd("/ipv6 firewall mangle add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("connection-mark", a.connection_mark).opt("packet-mark", a.packet_mark).opt("routing-mark", a.routing_mark).opt("new-connection-mark", a.new_connection_mark).opt("new-packet-mark", a.new_packet_mark).opt("new-routing-mark", a.new_routing_mark).opt("new-dscp", a.new_dscp).opt("new-hop-limit", a.new_hop_limit).bool("passthrough", a.passthrough).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
8552
8585
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8586
|
+
const trimmed = result.trim();
|
|
8587
|
+
if (looksLikeError(trimmed)) {
|
|
8588
|
+
return `Failed to create IPv6 firewall mangle rule: ${trimmed}`;
|
|
8589
|
+
}
|
|
8590
|
+
const createdId = extractCreatedId(trimmed);
|
|
8591
|
+
if (createdId) {
|
|
8592
|
+
const details = await executeMikrotikCommand(`/ipv6 firewall mangle print detail where .id=${createdId}`, ctx);
|
|
8593
|
+
return readBackUnavailable(details) ? `IPv6 firewall mangle rule created (id ${createdId}).` : `IPv6 firewall mangle rule created successfully:
|
|
8558
8594
|
|
|
8559
|
-
${details}
|
|
8560
|
-
}
|
|
8561
|
-
return `Failed to create IPv6 firewall mangle rule: ${result}`;
|
|
8595
|
+
${details}`;
|
|
8562
8596
|
}
|
|
8563
8597
|
const count = await executeMikrotikCommand("/ipv6 firewall mangle print detail count-only", ctx);
|
|
8564
8598
|
const c = count.trim();
|
|
@@ -8789,15 +8823,16 @@ var ipv6FirewallRawTools = [
|
|
|
8789
8823
|
ctx.info(`Creating IPv6 firewall raw rule: chain=${a.chain}, action=${a.action}`);
|
|
8790
8824
|
const cmd = new Cmd("/ipv6 firewall raw add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
8791
8825
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8826
|
+
const trimmed = result.trim();
|
|
8827
|
+
if (looksLikeError(trimmed)) {
|
|
8828
|
+
return `Failed to create IPv6 firewall raw rule: ${trimmed}`;
|
|
8829
|
+
}
|
|
8830
|
+
const createdId = extractCreatedId(trimmed);
|
|
8831
|
+
if (createdId) {
|
|
8832
|
+
const details = await executeMikrotikCommand(`/ipv6 firewall raw print detail where .id=${createdId}`, ctx);
|
|
8833
|
+
return readBackUnavailable(details) ? `IPv6 firewall raw rule created (id ${createdId}).` : `IPv6 firewall raw rule created successfully:
|
|
8797
8834
|
|
|
8798
|
-
${details}
|
|
8799
|
-
}
|
|
8800
|
-
return `Failed to create IPv6 firewall raw rule: ${result}`;
|
|
8835
|
+
${details}`;
|
|
8801
8836
|
}
|
|
8802
8837
|
const count = await executeMikrotikCommand("/ipv6 firewall raw print detail count-only", ctx);
|
|
8803
8838
|
const c = count.trim();
|
|
@@ -17710,15 +17745,16 @@ var switchRuleTools = [
|
|
|
17710
17745
|
ctx.info(`Adding switch rule: switch=${a.switch}, ports=${a.ports}`);
|
|
17711
17746
|
const cmd = new Cmd("/interface ethernet switch rule add").set("switch", a.switch).set("ports", a.ports).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-mac-address", a.src_mac_address).opt("dst-mac-address", a.dst_mac_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("mac-protocol", a.mac_protocol).opt("vlan-id", a.vlan_id).opt("vlan-priority", a.vlan_priority).opt("dscp", a.dscp).opt("flow-label", a.flow_label).opt("new-dst-ports", a.new_dst_ports).opt("new-vlan-id", a.new_vlan_id).opt("new-vlan-priority", a.new_vlan_priority).bool("redirect-to-cpu", a.redirect_to_cpu).bool("copy-to-cpu", a.copy_to_cpu).bool("mirror", a.mirror).opt("rate", a.rate).opt("comment", a.comment).flag("disabled", a.disabled).build();
|
|
17712
17747
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
|
|
17716
|
-
|
|
17717
|
-
|
|
17748
|
+
const trimmed = result.trim();
|
|
17749
|
+
if (looksLikeError(trimmed)) {
|
|
17750
|
+
return `Failed to add switch rule: ${trimmed}`;
|
|
17751
|
+
}
|
|
17752
|
+
const createdId = extractCreatedId(trimmed);
|
|
17753
|
+
if (createdId) {
|
|
17754
|
+
const details = await executeMikrotikCommand(`/interface ethernet switch rule print detail where .id=${createdId}`, ctx);
|
|
17755
|
+
return readBackUnavailable(details) ? `Switch rule added (id ${createdId}).` : `Switch rule added successfully:
|
|
17718
17756
|
|
|
17719
|
-
${details}
|
|
17720
|
-
}
|
|
17721
|
-
return `Failed to add switch rule: ${result}`;
|
|
17757
|
+
${details}`;
|
|
17722
17758
|
}
|
|
17723
17759
|
const count = await executeMikrotikCommand("/interface ethernet switch rule print detail count-only", ctx);
|
|
17724
17760
|
const c = count.trim();
|
|
@@ -21623,10 +21659,27 @@ var moduleCatalog = [
|
|
|
21623
21659
|
}
|
|
21624
21660
|
];
|
|
21625
21661
|
var allToolModules = moduleCatalog.map((m) => m.tools);
|
|
21662
|
+
function selectToolModules(filter = {}, catalog = moduleCatalog) {
|
|
21663
|
+
const lc = (xs) => new Set((xs ?? []).map((s) => s.toLowerCase()));
|
|
21664
|
+
const enabledModules = lc(filter.enabledModules);
|
|
21665
|
+
const disabledModules = lc(filter.disabledModules);
|
|
21666
|
+
const enabledGroups = lc(filter.enabledGroups);
|
|
21667
|
+
const disabledGroups = lc(filter.disabledGroups);
|
|
21668
|
+
const hasAllow = enabledModules.size > 0 || enabledGroups.size > 0;
|
|
21669
|
+
return catalog.filter((m) => {
|
|
21670
|
+
const slug = m.slug.toLowerCase();
|
|
21671
|
+
const group = m.group.toLowerCase();
|
|
21672
|
+
if (disabledModules.has(slug) || disabledGroups.has(group))
|
|
21673
|
+
return false;
|
|
21674
|
+
if (hasAllow && !(enabledModules.has(slug) || enabledGroups.has(group)))
|
|
21675
|
+
return false;
|
|
21676
|
+
return true;
|
|
21677
|
+
}).map((m) => m.tools);
|
|
21678
|
+
}
|
|
21626
21679
|
// package.json
|
|
21627
21680
|
var package_default = {
|
|
21628
21681
|
name: "@usex/mikrotik-mcp",
|
|
21629
|
-
version: "3.
|
|
21682
|
+
version: "3.8.0",
|
|
21630
21683
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
21631
21684
|
keywords: [
|
|
21632
21685
|
"ai",
|
|
@@ -21818,7 +21871,8 @@ function createServer(opts = {}) {
|
|
|
21818
21871
|
server.server.sendLoggingMessage({ level, data: message }).catch(() => {});
|
|
21819
21872
|
} catch {}
|
|
21820
21873
|
});
|
|
21821
|
-
const
|
|
21874
|
+
const toolModules = selectToolModules(getConfig().tools);
|
|
21875
|
+
const toolCount = registerTools(server, toolModules, {
|
|
21822
21876
|
sendLog,
|
|
21823
21877
|
deviceNames: names,
|
|
21824
21878
|
readOnly
|
package/package.json
CHANGED
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"enabled": false,
|
|
162
162
|
"host": "0.0.0.0",
|
|
163
163
|
"port": 9090,
|
|
164
|
-
"dbPath": "
|
|
164
|
+
"dbPath": "/Users/alimaster/.mikrotik-mcp/events.db",
|
|
165
165
|
"maxEvents": 100000,
|
|
166
166
|
"captureBody": true,
|
|
167
167
|
"redactInput": false,
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
"maximum": 9007199254740991
|
|
185
185
|
},
|
|
186
186
|
"dbPath": {
|
|
187
|
-
"default": "
|
|
187
|
+
"default": "/Users/alimaster/.mikrotik-mcp/events.db",
|
|
188
188
|
"type": "string"
|
|
189
189
|
},
|
|
190
190
|
"maxEvents": {
|
|
@@ -227,10 +227,51 @@
|
|
|
227
227
|
"default": false,
|
|
228
228
|
"type": "boolean"
|
|
229
229
|
},
|
|
230
|
+
"tools": {
|
|
231
|
+
"default": {
|
|
232
|
+
"enabledModules": [],
|
|
233
|
+
"disabledModules": [],
|
|
234
|
+
"enabledGroups": [],
|
|
235
|
+
"disabledGroups": []
|
|
236
|
+
},
|
|
237
|
+
"type": "object",
|
|
238
|
+
"properties": {
|
|
239
|
+
"enabledModules": {
|
|
240
|
+
"default": [],
|
|
241
|
+
"type": "array",
|
|
242
|
+
"items": {
|
|
243
|
+
"type": "string"
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
"disabledModules": {
|
|
247
|
+
"default": [],
|
|
248
|
+
"type": "array",
|
|
249
|
+
"items": {
|
|
250
|
+
"type": "string"
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"enabledGroups": {
|
|
254
|
+
"default": [],
|
|
255
|
+
"type": "array",
|
|
256
|
+
"items": {
|
|
257
|
+
"type": "string"
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"disabledGroups": {
|
|
261
|
+
"default": [],
|
|
262
|
+
"type": "array",
|
|
263
|
+
"items": {
|
|
264
|
+
"type": "string"
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
"required": ["enabledModules", "disabledModules", "enabledGroups", "disabledGroups"],
|
|
269
|
+
"additionalProperties": false
|
|
270
|
+
},
|
|
230
271
|
"backupDir": {
|
|
231
272
|
"type": "string"
|
|
232
273
|
}
|
|
233
274
|
},
|
|
234
|
-
"required": ["devices", "defaultDevice", "mcp", "dashboard", "readOnly"],
|
|
275
|
+
"required": ["devices", "defaultDevice", "mcp", "dashboard", "readOnly", "tools"],
|
|
235
276
|
"additionalProperties": false
|
|
236
277
|
}
|