@usex/mikrotik-mcp 3.42.0 → 3.44.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 +51 -15
- package/dist/cli.js +81 -25
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -2
- package/dist/shared/{cli-rptenaf7.js → cli-1e5sj65w.js} +1 -1
- package/dist/shared/{cli-sq4trv31.js → cli-k2ghtqd0.js} +1063 -894
- package/dist/shared/{library-z22txcv0.js → library-98nh7hz6.js} +1 -1
- package/dist/shared/{library-5cjmn0n7.js → library-nb4wss02.js} +1001 -867
- package/dist/ui/observability.html +15 -15
- package/package.json +1 -1
- package/schemas/config.schema.json +29 -1
- package/schemas/tool-catalog.json +9 -15
- package/schemas/tools/check_route_path.json +2 -4
- package/schemas/tools/download_file.json +2 -6
package/README.md
CHANGED
|
@@ -66,6 +66,9 @@ in memory and auto-reverts if your session drops, so you can't lock yourself out
|
|
|
66
66
|
- 🪜 **SSH jump hosts** — reach a router with no exposed port by tunnelling
|
|
67
67
|
through another via `jumpVia` (ProxyJump/bastion) — commands, Safe Mode and
|
|
68
68
|
file upload all ride the hop. No new WAN port.
|
|
69
|
+
- ⚡ **Connection pooling** — one persistent SSH session per device, reused
|
|
70
|
+
across tool calls. Saves ~200-500 ms handshake per command (double through
|
|
71
|
+
jump hosts). Idle connections auto-close after 30 s.
|
|
69
72
|
- 🤖 **Guided prompts** — 9 built-in workflows (harden, diagnose, guest Wi-Fi, VPNs,
|
|
70
73
|
cross-device tunnels, backup & document) that turn an intent into tool calls.
|
|
71
74
|
|
|
@@ -217,6 +220,37 @@ HTTP transports expose `POST /mcp` and a `GET /health` check, with DNS-rebinding
|
|
|
217
220
|
protection that reconciles with your bind host automatically. See
|
|
218
221
|
**[docs/transports.md](docs/transports.md)**.
|
|
219
222
|
|
|
223
|
+
## SSH connection pooling
|
|
224
|
+
|
|
225
|
+
By default the server keeps **one persistent SSH connection per device** and
|
|
226
|
+
opens a fresh exec channel for each tool call — eliminating the ~200-500 ms
|
|
227
|
+
handshake overhead that a one-shot connection incurs on every command. Through
|
|
228
|
+
a jump host the savings double (two handshakes avoided). Idle connections are
|
|
229
|
+
closed automatically after 30 s.
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Disable pooling (revert to one-shot per tool call)
|
|
233
|
+
MIKROTIK_SSH__KEEP_ALIVE=false mikrotik-mcp serve
|
|
234
|
+
|
|
235
|
+
# Tune the idle timeout (ms)
|
|
236
|
+
mikrotik-mcp serve --ssh-idle-timeout 60000
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
In a JSON config file:
|
|
240
|
+
|
|
241
|
+
```jsonc
|
|
242
|
+
{
|
|
243
|
+
"ssh": {
|
|
244
|
+
"keepAlive": true, // default — set false to disable
|
|
245
|
+
"keepAliveInterval": 10000, // SSH keepalive packet interval (ms)
|
|
246
|
+
"idleTimeout": 30000, // close idle connections after (ms)
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Connection pooling is SSH-only; MAC-Telnet devices always use one-shot
|
|
252
|
+
connections. Safe Mode still uses its own dedicated persistent session.
|
|
253
|
+
|
|
220
254
|
## Safe Mode
|
|
221
255
|
|
|
222
256
|
```text
|
|
@@ -234,21 +268,23 @@ See **[docs/safe-mode.md](docs/safe-mode.md)**.
|
|
|
234
268
|
Connection and transport settings come from `MIKROTIK_*` env vars or matching CLI
|
|
235
269
|
flags (highest precedence last: defaults → env → flags).
|
|
236
270
|
|
|
237
|
-
| Variable | Flag
|
|
238
|
-
| ----------------------------- |
|
|
239
|
-
| `MIKROTIK_HOST` | `--host`
|
|
240
|
-
| `MIKROTIK_USERNAME` | `--username`
|
|
241
|
-
| `MIKROTIK_PORT` | `--port`
|
|
242
|
-
| `MIKROTIK_PASSWORD` | `--password`
|
|
243
|
-
| `MIKROTIK_KEY_FILENAME` | `--key-filename`
|
|
244
|
-
| `MIKROTIK_PRIVATE_KEY` | `--private-key`
|
|
245
|
-
| `MIKROTIK_KEY_PASSPHRASE` | `--key-passphrase`
|
|
246
|
-
| `MIKROTIK_JUMP_HOST` | `--jump-host`
|
|
247
|
-
| `MIKROTIK_CONFIG_FILE` | `--config`
|
|
248
|
-
| `MIKROTIK_DEVICES` | `--devices`
|
|
249
|
-
| `MIKROTIK_MCP__TRANSPORT` | `--transport`
|
|
250
|
-
| `MIKROTIK_MCP__PORT` | `--mcp-port`
|
|
251
|
-
| `
|
|
271
|
+
| Variable | Flag | Default | Purpose |
|
|
272
|
+
| ----------------------------- | -------------------- | ----------- | ---------------------------------------------------------------------------------------------------- |
|
|
273
|
+
| `MIKROTIK_HOST` | `--host` | `127.0.0.1` | RouterOS host |
|
|
274
|
+
| `MIKROTIK_USERNAME` | `--username` | `admin` | SSH user |
|
|
275
|
+
| `MIKROTIK_PORT` | `--port` | `22` | SSH port |
|
|
276
|
+
| `MIKROTIK_PASSWORD` | `--password` | — | SSH password _(or use a key →)_ |
|
|
277
|
+
| `MIKROTIK_KEY_FILENAME` | `--key-filename` | — | SSH private-key file path |
|
|
278
|
+
| `MIKROTIK_PRIVATE_KEY` | `--private-key` | — | Inline private key (PEM) |
|
|
279
|
+
| `MIKROTIK_KEY_PASSPHRASE` | `--key-passphrase` | — | Passphrase for an encrypted key |
|
|
280
|
+
| `MIKROTIK_JUMP_HOST` | `--jump-host` | — | SSH bastion to tunnel through ([jump hosts](docs/multi-device.md#ssh-jump-hosts-bastion--proxyjump)) |
|
|
281
|
+
| `MIKROTIK_CONFIG_FILE` | `--config` | — | JSON file of named devices ([multi-device](docs/multi-device.md)) |
|
|
282
|
+
| `MIKROTIK_DEVICES` | `--devices` | — | Inline JSON of named devices |
|
|
283
|
+
| `MIKROTIK_MCP__TRANSPORT` | `--transport` | `stdio` | `stdio` / `streamable-http` / `sse` |
|
|
284
|
+
| `MIKROTIK_MCP__PORT` | `--mcp-port` | `8000` | HTTP bind port |
|
|
285
|
+
| `MIKROTIK_SSH__KEEP_ALIVE` | `--ssh-keep-alive` | `true` | SSH connection pooling (reuse connections across tool calls) |
|
|
286
|
+
| `MIKROTIK_SSH__IDLE_TIMEOUT` | `--ssh-idle-timeout` | `30000` | Close idle pooled connections after (ms) |
|
|
287
|
+
| `MIKROTIK_DASHBOARD__ENABLED` | `--dashboard` | `false` | Real-time observability dashboard ([docs](docs/observability.md)) |
|
|
252
288
|
|
|
253
289
|
Full table (incl. HTTP host, allow-lists, timeouts, `MIKROTIK_LOG_LEVEL`):
|
|
254
290
|
**[docs/configuration.md](docs/configuration.md)**.
|
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
blockDevice,
|
|
21
21
|
buildChangePlan,
|
|
22
22
|
capture,
|
|
23
|
+
closeAll,
|
|
23
24
|
commandUnsupported,
|
|
24
25
|
configureRecorder,
|
|
25
26
|
createContext,
|
|
@@ -40,6 +41,8 @@ import {
|
|
|
40
41
|
getS3Client,
|
|
41
42
|
getUmSettings,
|
|
42
43
|
isEmpty,
|
|
44
|
+
isMacTelnetDevice,
|
|
45
|
+
isPoolEnabled,
|
|
43
46
|
isS3Configured,
|
|
44
47
|
listAaaEntity,
|
|
45
48
|
listBackups,
|
|
@@ -57,6 +60,7 @@ import {
|
|
|
57
60
|
parseRouterosDate,
|
|
58
61
|
parseSize,
|
|
59
62
|
parseSystemResource,
|
|
63
|
+
poolStatus,
|
|
60
64
|
presignExpiresIn,
|
|
61
65
|
readBackup,
|
|
62
66
|
redact,
|
|
@@ -70,6 +74,7 @@ import {
|
|
|
70
74
|
resolveDeviceName,
|
|
71
75
|
restoreLocalBackup,
|
|
72
76
|
s3Target,
|
|
77
|
+
sampleAllTraffic,
|
|
73
78
|
sampleDeviceTraffic,
|
|
74
79
|
selectToolModules,
|
|
75
80
|
setConfig,
|
|
@@ -84,7 +89,7 @@ import {
|
|
|
84
89
|
toggleAaaEntity,
|
|
85
90
|
updateAaaEntity,
|
|
86
91
|
writeBackup
|
|
87
|
-
} from "./shared/cli-
|
|
92
|
+
} from "./shared/cli-k2ghtqd0.js";
|
|
88
93
|
|
|
89
94
|
// src/cli.ts
|
|
90
95
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -1209,28 +1214,41 @@ function deviceActivity(store) {
|
|
|
1209
1214
|
function devicesPayload(store) {
|
|
1210
1215
|
const cfg = getConfig();
|
|
1211
1216
|
const activity = deviceActivity(store);
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1217
|
+
const poolEnabled = isPoolEnabled();
|
|
1218
|
+
const poolMap = new Map(poolStatus().map((p) => [p.device, p]));
|
|
1219
|
+
const devices = Object.entries(cfg.devices).map(([name, dc]) => {
|
|
1220
|
+
const isMac = isMacTelnetDevice(dc);
|
|
1221
|
+
const ps = poolMap.get(name);
|
|
1222
|
+
return {
|
|
1223
|
+
name,
|
|
1224
|
+
host: dc.host,
|
|
1225
|
+
port: dc.port,
|
|
1226
|
+
mac: dc.mac,
|
|
1227
|
+
transport: dc.mac ? "mac-telnet" : "ssh",
|
|
1228
|
+
address: dc.mac ? dc.mac : `${dc.host}:${dc.port}`,
|
|
1229
|
+
username: dc.username,
|
|
1230
|
+
authMode: dc.mac ? "mac-telnet" : dc.keyFilename || dc.privateKey ? "key" : dc.password ? "password" : "none",
|
|
1231
|
+
isDefault: name === cfg.defaultDevice,
|
|
1232
|
+
description: dc.description,
|
|
1233
|
+
jumpVia: dc.jumpVia,
|
|
1234
|
+
jumpHost: dc.jumpHost ? { host: dc.jumpHost.host, port: dc.jumpHost.port } : undefined,
|
|
1235
|
+
status: getDeviceStatus(name),
|
|
1236
|
+
history: getDeviceHistory(name),
|
|
1237
|
+
activity: activity.get(name) ?? {
|
|
1238
|
+
calls: 0,
|
|
1239
|
+
errors: 0,
|
|
1240
|
+
lastSeen: 0,
|
|
1241
|
+
avgMs: 0
|
|
1242
|
+
},
|
|
1243
|
+
pool: isMac || !poolEnabled ? null : {
|
|
1244
|
+
device: name,
|
|
1245
|
+
pooled: !!ps,
|
|
1246
|
+
inflight: ps?.inflight ?? 0,
|
|
1247
|
+
idle: ps?.idle ?? false,
|
|
1248
|
+
dead: ps?.dead ?? false
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
});
|
|
1234
1252
|
return { server: SERVER_TAG2, defaultDevice: cfg.defaultDevice, devices };
|
|
1235
1253
|
}
|
|
1236
1254
|
function topologyPayload() {
|
|
@@ -1544,6 +1562,10 @@ async function clientsRoutes(req, url) {
|
|
|
1544
1562
|
const ctx = createContext(undefined, deviceFromQuery());
|
|
1545
1563
|
return json(await sampleDeviceTraffic(ctx, ip));
|
|
1546
1564
|
}
|
|
1565
|
+
if (p === "/api/clients/traffic-bulk" && req.method === "GET") {
|
|
1566
|
+
const ctx = createContext(undefined, deviceFromQuery());
|
|
1567
|
+
return json(await sampleAllTraffic(ctx));
|
|
1568
|
+
}
|
|
1547
1569
|
if (req.method === "POST") {
|
|
1548
1570
|
const b = await readJson(req);
|
|
1549
1571
|
const ctx = createContext(undefined, b?.device);
|
|
@@ -1990,6 +2012,29 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
1990
2012
|
const removed = body.all === true ? db.clear() : db.delete(ids);
|
|
1991
2013
|
return json({ removed, total: db.total() });
|
|
1992
2014
|
}
|
|
2015
|
+
if (url.pathname === "/api/ssh-pool") {
|
|
2016
|
+
const cfg2 = getConfig();
|
|
2017
|
+
const enabled = isPoolEnabled();
|
|
2018
|
+
const ps = poolStatus();
|
|
2019
|
+
const totalInflight = ps.reduce((s, p) => s + p.inflight, 0);
|
|
2020
|
+
const totalIdle = ps.filter((p) => p.idle).length;
|
|
2021
|
+
const totalBusy = ps.filter((p) => p.inflight > 0).length;
|
|
2022
|
+
return json({
|
|
2023
|
+
enabled,
|
|
2024
|
+
config: {
|
|
2025
|
+
keepAlive: cfg2.ssh.keepAlive,
|
|
2026
|
+
keepAliveInterval: cfg2.ssh.keepAliveInterval,
|
|
2027
|
+
idleTimeout: cfg2.ssh.idleTimeout
|
|
2028
|
+
},
|
|
2029
|
+
aggregate: {
|
|
2030
|
+
totalConnections: ps.length,
|
|
2031
|
+
totalInflight,
|
|
2032
|
+
totalIdle,
|
|
2033
|
+
totalBusy
|
|
2034
|
+
},
|
|
2035
|
+
devices: ps
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
1993
2038
|
if (url.pathname === "/api/devices") {
|
|
1994
2039
|
return json(devicesPayload(db));
|
|
1995
2040
|
}
|
|
@@ -2242,7 +2287,7 @@ function registerPrompts(server) {
|
|
|
2242
2287
|
// package.json
|
|
2243
2288
|
var package_default = {
|
|
2244
2289
|
name: "@usex/mikrotik-mcp",
|
|
2245
|
-
version: "3.
|
|
2290
|
+
version: "3.44.0",
|
|
2246
2291
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
2247
2292
|
keywords: [
|
|
2248
2293
|
"ai",
|
|
@@ -2615,6 +2660,11 @@ TRANSPORT OPTIONS
|
|
|
2615
2660
|
--mcp-port HTTP bind port (MIKROTIK_MCP__PORT)
|
|
2616
2661
|
--mcp-allowed-hosts Host header allow-list (DNS-rebinding protection)
|
|
2617
2662
|
|
|
2663
|
+
SSH CONNECTION POOLING (persistent connections, on by default)
|
|
2664
|
+
--ssh-keep-alive Enable/disable pooling (MIKROTIK_SSH__KEEP_ALIVE, default true)
|
|
2665
|
+
--ssh-keepalive-interval Keepalive packet interval (ms) (MIKROTIK_SSH__KEEPALIVE_INTERVAL)
|
|
2666
|
+
--ssh-idle-timeout Close idle connections after (ms) (MIKROTIK_SSH__IDLE_TIMEOUT)
|
|
2667
|
+
|
|
2618
2668
|
OBSERVABILITY DASHBOARD (optional; real-time feed + analytics of every tool call)
|
|
2619
2669
|
--dashboard Enable the dashboard (MIKROTIK_DASHBOARD__ENABLED)
|
|
2620
2670
|
--dashboard-host Bind host (default 0.0.0.0=LAN) (MIKROTIK_DASHBOARD__HOST)
|
|
@@ -2717,7 +2767,13 @@ async function main() {
|
|
|
2717
2767
|
setConfig(cfg);
|
|
2718
2768
|
warnIfPlaintextPasswordInContainer(Object.values(cfg.devices).some((d) => !!d.password));
|
|
2719
2769
|
const deviceNames = Object.keys(cfg.devices);
|
|
2720
|
-
logger.info(`Starting ${SERVER_NAME} v${VERSION} (transport=${cfg.mcp.transport}, ` + `devices=${deviceNames.length === 1 ? deviceNames[0] : deviceNames.join("/")})`);
|
|
2770
|
+
logger.info(`Starting ${SERVER_NAME} v${VERSION} (transport=${cfg.mcp.transport}, ` + `devices=${deviceNames.length === 1 ? deviceNames[0] : deviceNames.join("/")}, ` + `ssh-pool=${cfg.ssh.keepAlive ? "on" : "off"})`);
|
|
2771
|
+
const cleanup = () => {
|
|
2772
|
+
closeAll();
|
|
2773
|
+
};
|
|
2774
|
+
process.on("SIGINT", cleanup);
|
|
2775
|
+
process.on("SIGTERM", cleanup);
|
|
2776
|
+
process.on("exit", cleanup);
|
|
2721
2777
|
const startDashboard = async () => {
|
|
2722
2778
|
if (!cfg.dashboard.enabled)
|
|
2723
2779
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -192,6 +192,10 @@ interface SSHClientOptions {
|
|
|
192
192
|
* carry its own `jump` for a multi-hop chain (A → B → target).
|
|
193
193
|
*/
|
|
194
194
|
jump?: SSHClientOptions;
|
|
195
|
+
/** Interval (ms) for SSH keepalive packets. 0 disables (default). */
|
|
196
|
+
keepAliveInterval?: number;
|
|
197
|
+
/** Max consecutive keepalive failures before disconnect. Default: 3. */
|
|
198
|
+
keepAliveCountMax?: number;
|
|
195
199
|
}
|
|
196
200
|
declare class MikroTikSSHClient {
|
|
197
201
|
private client;
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
resolveDeviceName,
|
|
22
22
|
selectToolModules,
|
|
23
23
|
setConfig
|
|
24
|
-
} from "./shared/library-
|
|
24
|
+
} from "./shared/library-nb4wss02.js";
|
|
25
25
|
// src/server.ts
|
|
26
26
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
27
27
|
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -131,7 +131,7 @@ function registerPrompts(server) {
|
|
|
131
131
|
// package.json
|
|
132
132
|
var package_default = {
|
|
133
133
|
name: "@usex/mikrotik-mcp",
|
|
134
|
-
version: "3.
|
|
134
|
+
version: "3.44.0",
|
|
135
135
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
136
136
|
keywords: [
|
|
137
137
|
"ai",
|