@usex/mikrotik-mcp 3.7.0 → 3.9.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 +144 -65
- package/dist/index.js +144 -65
- package/dist/ui/observability.html +46 -46
- package/package.json +1 -1
- package/schemas/README.md +1 -1
- package/schemas/config.schema.json +44 -3
- package/schemas/tool-catalog.json +28 -6
- package/schemas/tools/get_wireguard_status.json +12 -0
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();
|
|
@@ -20002,7 +20038,7 @@ ${details}` : "WireGuard interface created successfully.";
|
|
|
20002
20038
|
name: "list_wireguard_interfaces",
|
|
20003
20039
|
title: "List WireGuard Interfaces",
|
|
20004
20040
|
annotations: READ,
|
|
20005
|
-
description: "
|
|
20041
|
+
description: "`list_wireguard_interfaces` \u2014 READ / list / show / inspect all WireGuard tunnel interfaces" + " (`/interface wireguard print`). The go-to tool to read the current WireGuard state on a" + " device. Returns each interface's name, listen-port, public-key, MTU, and running/disabled" + " status. Filter by name substring (name_filter), disabled-only, or running-only." + " For one interface's full detail use get_wireguard_interface; for the peer table use" + " list_wireguard_peers; for interfaces AND peers in one call use get_wireguard_status.",
|
|
20006
20042
|
inputSchema: {
|
|
20007
20043
|
name_filter: z108.string().optional(),
|
|
20008
20044
|
disabled_only: z108.boolean().default(false),
|
|
@@ -20027,7 +20063,7 @@ ${result}`;
|
|
|
20027
20063
|
name: "get_wireguard_interface",
|
|
20028
20064
|
title: "Get WireGuard Interface Details",
|
|
20029
20065
|
annotations: READ,
|
|
20030
|
-
description: "
|
|
20066
|
+
description: "`get_wireguard_interface` \u2014 READ / get / show the full detail of ONE WireGuard tunnel interface" + " (`/interface wireguard print detail`) looked up by name. Returns listen-port, private-key," + " public-key, MTU, running state, and comment. To list all interfaces use" + " list_wireguard_interfaces; for that interface's peers use list_wireguard_peers; for" + " interfaces AND peers in one call use get_wireguard_status.",
|
|
20031
20067
|
inputSchema: { name: z108.string() },
|
|
20032
20068
|
async handler(a, ctx) {
|
|
20033
20069
|
ctx.info(`Getting WireGuard interface details: name=${a.name}`);
|
|
@@ -20037,6 +20073,31 @@ ${result}`;
|
|
|
20037
20073
|
${result}`;
|
|
20038
20074
|
}
|
|
20039
20075
|
}),
|
|
20076
|
+
defineTool({
|
|
20077
|
+
name: "get_wireguard_status",
|
|
20078
|
+
title: "Get WireGuard Status (Interfaces + Peers)",
|
|
20079
|
+
annotations: READ,
|
|
20080
|
+
description: "`get_wireguard_status` \u2014 READ the current WireGuard state on a device in ONE call: ALL tunnel" + " interfaces AND ALL peers together. Use this FIRST to inspect / show / check WireGuard before" + " changing anything (e.g. to read public keys, see which side initiates, or confirm the tunnel" + " is up). Interfaces report name, listen-port, public-key, MTU and running state; peers report" + " .id, interface, public-key, allowed-address, endpoint/current-endpoint, last-handshake time" + " and rx/tx bytes. Combines list_wireguard_interfaces + list_wireguard_peers; optionally filter" + " both to one interface with interface_filter.",
|
|
20081
|
+
inputSchema: {
|
|
20082
|
+
interface_filter: z108.string().optional().describe("Limit to one interface name, e.g. 'wg-mesh'")
|
|
20083
|
+
},
|
|
20084
|
+
async handler(a, ctx) {
|
|
20085
|
+
ctx.info("Reading WireGuard status (interfaces + peers)");
|
|
20086
|
+
const ifFilters = a.interface_filter ? [`name="${a.interface_filter}"`] : [];
|
|
20087
|
+
const peerFilters = a.interface_filter ? [`interface="${a.interface_filter}"`] : [];
|
|
20088
|
+
const interfaces = await executeMikrotikCommand(`/interface wireguard print${whereClause(ifFilters)}`, ctx);
|
|
20089
|
+
const peers = await executeMikrotikCommand(`/interface wireguard peers print${whereClause(peerFilters)}`, ctx);
|
|
20090
|
+
const ifBlock = isEmpty(interfaces) ? "(none)" : interfaces;
|
|
20091
|
+
const peerBlock = isEmpty(peers) ? "(none)" : peers;
|
|
20092
|
+
return `WIREGUARD INTERFACES:
|
|
20093
|
+
|
|
20094
|
+
${ifBlock}
|
|
20095
|
+
|
|
20096
|
+
WIREGUARD PEERS:
|
|
20097
|
+
|
|
20098
|
+
${peerBlock}`;
|
|
20099
|
+
}
|
|
20100
|
+
}),
|
|
20040
20101
|
defineTool({
|
|
20041
20102
|
name: "update_wireguard_interface",
|
|
20042
20103
|
title: "Update WireGuard Interface",
|
|
@@ -20149,7 +20210,7 @@ ${details}` : "WireGuard peer added successfully.";
|
|
|
20149
20210
|
name: "list_wireguard_peers",
|
|
20150
20211
|
title: "List WireGuard Peers",
|
|
20151
20212
|
annotations: READ,
|
|
20152
|
-
description: "
|
|
20213
|
+
description: "`list_wireguard_peers` \u2014 READ / list / show / inspect all WireGuard peers" + " (`/interface wireguard peers print`). Use this to read which peers are configured and" + " whether the tunnel is up (handshake). Returns each peer's .id, interface, public-key," + " allowed-address, endpoint/current-endpoint, last-handshake time, and rx/tx byte counters." + " Filter by interface name (interface_filter) or disabled-only. For the interface table use" + " list_wireguard_interfaces; for one peer's full detail use get_wireguard_peer; for interfaces" + " AND peers in one call use get_wireguard_status. Use the .id from this output with" + " update_wireguard_peer, remove_wireguard_peer, enable_wireguard_peer, or disable_wireguard_peer.",
|
|
20153
20214
|
inputSchema: {
|
|
20154
20215
|
interface_filter: z108.string().optional(),
|
|
20155
20216
|
disabled_only: z108.boolean().default(false)
|
|
@@ -20171,7 +20232,7 @@ ${result}`;
|
|
|
20171
20232
|
name: "get_wireguard_peer",
|
|
20172
20233
|
title: "Get WireGuard Peer Details",
|
|
20173
20234
|
annotations: READ,
|
|
20174
|
-
description: "
|
|
20235
|
+
description: "`get_wireguard_peer` \u2014 READ / get / show the full detail of ONE WireGuard peer" + " (`/interface wireguard peers print detail`) by its .id. Returns public-key, allowed-address," + " endpoint/current-endpoint, preshared-key presence, last-handshake time, and rx/tx bytes." + " To list all peers use list_wireguard_peers; for interfaces AND peers in one call use" + ` get_wireguard_status.
|
|
20175
20236
|
|
|
20176
20237
|
` + `Notes:
|
|
20177
20238
|
` + ' peer_id: the .id from list_wireguard_peers, format "*N" or "N" e.g. "*2"',
|
|
@@ -21611,6 +21672,23 @@ var moduleCatalog = [
|
|
|
21611
21672
|
}
|
|
21612
21673
|
];
|
|
21613
21674
|
var allToolModules = moduleCatalog.map((m) => m.tools);
|
|
21675
|
+
function selectToolModules(filter = {}, catalog = moduleCatalog) {
|
|
21676
|
+
const lc = (xs) => new Set((xs ?? []).map((s) => s.toLowerCase()));
|
|
21677
|
+
const enabledModules = lc(filter.enabledModules);
|
|
21678
|
+
const disabledModules = lc(filter.disabledModules);
|
|
21679
|
+
const enabledGroups = lc(filter.enabledGroups);
|
|
21680
|
+
const disabledGroups = lc(filter.disabledGroups);
|
|
21681
|
+
const hasAllow = enabledModules.size > 0 || enabledGroups.size > 0;
|
|
21682
|
+
return catalog.filter((m) => {
|
|
21683
|
+
const slug = m.slug.toLowerCase();
|
|
21684
|
+
const group = m.group.toLowerCase();
|
|
21685
|
+
if (disabledModules.has(slug) || disabledGroups.has(group))
|
|
21686
|
+
return false;
|
|
21687
|
+
if (hasAllow && !(enabledModules.has(slug) || enabledGroups.has(group)))
|
|
21688
|
+
return false;
|
|
21689
|
+
return true;
|
|
21690
|
+
}).map((m) => m.tools);
|
|
21691
|
+
}
|
|
21614
21692
|
|
|
21615
21693
|
// src/observability/dashboard.ts
|
|
21616
21694
|
import { readFileSync as readFileSync6 } from "fs";
|
|
@@ -23223,7 +23301,7 @@ function registerPrompts(server) {
|
|
|
23223
23301
|
// package.json
|
|
23224
23302
|
var package_default = {
|
|
23225
23303
|
name: "@usex/mikrotik-mcp",
|
|
23226
|
-
version: "3.
|
|
23304
|
+
version: "3.9.0",
|
|
23227
23305
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
23228
23306
|
keywords: [
|
|
23229
23307
|
"ai",
|
|
@@ -23415,7 +23493,8 @@ function createServer(opts = {}) {
|
|
|
23415
23493
|
server.server.sendLoggingMessage({ level, data: message }).catch(() => {});
|
|
23416
23494
|
} catch {}
|
|
23417
23495
|
});
|
|
23418
|
-
const
|
|
23496
|
+
const toolModules = selectToolModules(getConfig().tools);
|
|
23497
|
+
const toolCount = registerTools(server, toolModules, {
|
|
23419
23498
|
sendLog,
|
|
23420
23499
|
deviceNames: names,
|
|
23421
23500
|
readOnly
|