@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 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 640 tools your AI can drive.</strong><br/>
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-640%20tools-1F2937.svg">
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 **640 [Model Context Protocol](https://modelcontextprotocol.io)
18
- tools across 51 modules**, so an AI client (Claude Desktop, Claude Code, any MCP
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** — 640 tools covering the whole device: L2 (bridge, VLAN, wireless,
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 640 tools
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
- **640 tools across 51 modules.** Full, always-current reference (parameters +
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
- return { devices, defaultDevice, s3, dashboard };
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
- if (result.trim()) {
6048
- const trimmed = result.trim();
6049
- if (trimmed.includes("*") || isDigits(trimmed)) {
6050
- const details = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${trimmed}`, ctx);
6051
- return details.trim() ? `Firewall filter rule created successfully:
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}` : `Firewall filter rule created with ID: ${result}`;
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
- if (result.trim()) {
6345
- const trimmed = result.trim();
6346
- if (trimmed.includes("*") || isDigits2(trimmed)) {
6347
- const details = await executeMikrotikCommand(`/ip firewall nat print detail where .id=${trimmed}`, ctx);
6348
- return details.trim() ? `NAT rule created successfully:
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}` : `NAT rule created with ID: ${result}`;
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
- if (result.trim()) {
8017
- const trimmed = result.trim();
8018
- if (trimmed.includes("*") || isDigits3(trimmed)) {
8019
- const details = await executeMikrotikCommand(`/ipv6 firewall filter print detail where .id=${trimmed}`, ctx);
8020
- return details.trim() ? `IPv6 firewall filter rule created successfully:
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}` : `IPv6 firewall filter rule created with ID: ${result}`;
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
- if (result.trim()) {
8278
- const trimmed = result.trim();
8279
- if (trimmed.includes("*") || isDigits4(trimmed)) {
8280
- const details = await executeMikrotikCommand(`/ipv6 firewall nat print detail where .id=${trimmed}`, ctx);
8281
- return details.trim() ? `IPv6 firewall NAT rule created successfully:
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}` : `IPv6 firewall NAT rule created with ID: ${result}`;
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
- if (result.trim()) {
8542
- const trimmed = result.trim();
8543
- if (trimmed.includes("*") || isDigits5(trimmed)) {
8544
- const details = await executeMikrotikCommand(`/ipv6 firewall mangle print detail where .id=${trimmed}`, ctx);
8545
- return details.trim() ? `IPv6 firewall mangle rule created successfully:
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}` : `IPv6 firewall mangle rule created with ID: ${result}`;
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
- if (result.trim()) {
8781
- const trimmed = result.trim();
8782
- if (trimmed.includes("*") || isDigits6(trimmed)) {
8783
- const details = await executeMikrotikCommand(`/ipv6 firewall raw print detail where .id=${trimmed}`, ctx);
8784
- return details.trim() ? `IPv6 firewall raw rule created successfully:
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}` : `IPv6 firewall raw rule created with ID: ${result}`;
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
- if (result.trim()) {
17702
- const trimmed = result.trim();
17703
- if (trimmed.includes("*") || isDigits7(trimmed)) {
17704
- const details = await executeMikrotikCommand(`/interface ethernet switch rule print detail where .id=${trimmed}`, ctx);
17705
- return details.trim() ? `Switch rule added successfully:
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}` : `Switch rule added with ID: ${result}`;
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.7.0",
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 toolCount = registerTools(server, allToolModules, {
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
- return { devices, defaultDevice, s3, dashboard };
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
- if (result.trim()) {
6060
- const trimmed = result.trim();
6061
- if (trimmed.includes("*") || isDigits(trimmed)) {
6062
- const details = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${trimmed}`, ctx);
6063
- return details.trim() ? `Firewall filter rule created successfully:
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}` : `Firewall filter rule created with ID: ${result}`;
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
- if (result.trim()) {
6357
- const trimmed = result.trim();
6358
- if (trimmed.includes("*") || isDigits2(trimmed)) {
6359
- const details = await executeMikrotikCommand(`/ip firewall nat print detail where .id=${trimmed}`, ctx);
6360
- return details.trim() ? `NAT rule created successfully:
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}` : `NAT rule created with ID: ${result}`;
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
- if (result.trim()) {
8029
- const trimmed = result.trim();
8030
- if (trimmed.includes("*") || isDigits3(trimmed)) {
8031
- const details = await executeMikrotikCommand(`/ipv6 firewall filter print detail where .id=${trimmed}`, ctx);
8032
- return details.trim() ? `IPv6 firewall filter rule created successfully:
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}` : `IPv6 firewall filter rule created with ID: ${result}`;
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
- if (result.trim()) {
8290
- const trimmed = result.trim();
8291
- if (trimmed.includes("*") || isDigits4(trimmed)) {
8292
- const details = await executeMikrotikCommand(`/ipv6 firewall nat print detail where .id=${trimmed}`, ctx);
8293
- return details.trim() ? `IPv6 firewall NAT rule created successfully:
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}` : `IPv6 firewall NAT rule created with ID: ${result}`;
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
- if (result.trim()) {
8554
- const trimmed = result.trim();
8555
- if (trimmed.includes("*") || isDigits5(trimmed)) {
8556
- const details = await executeMikrotikCommand(`/ipv6 firewall mangle print detail where .id=${trimmed}`, ctx);
8557
- return details.trim() ? `IPv6 firewall mangle rule created successfully:
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}` : `IPv6 firewall mangle rule created with ID: ${result}`;
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
- if (result.trim()) {
8793
- const trimmed = result.trim();
8794
- if (trimmed.includes("*") || isDigits6(trimmed)) {
8795
- const details = await executeMikrotikCommand(`/ipv6 firewall raw print detail where .id=${trimmed}`, ctx);
8796
- return details.trim() ? `IPv6 firewall raw rule created successfully:
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}` : `IPv6 firewall raw rule created with ID: ${result}`;
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
- if (result.trim()) {
17714
- const trimmed = result.trim();
17715
- if (trimmed.includes("*") || isDigits7(trimmed)) {
17716
- const details = await executeMikrotikCommand(`/interface ethernet switch rule print detail where .id=${trimmed}`, ctx);
17717
- return details.trim() ? `Switch rule added successfully:
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}` : `Switch rule added with ID: ${result}`;
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.7.0",
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 toolCount = registerTools(server, allToolModules, {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usex/mikrotik-mcp",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "MCP server for MikroTik RouterOS — 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
5
5
  "keywords": [
6
6
  "ai",
@@ -161,7 +161,7 @@
161
161
  "enabled": false,
162
162
  "host": "0.0.0.0",
163
163
  "port": 9090,
164
- "dbPath": "./.mikrotik-mcp/events.db",
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": "./.mikrotik-mcp/events.db",
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "generated": "by scripts/gen-schemas.ts — do not edit by hand",
5
5
  "toolCount": 706,
6
6
  "tools": [