@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 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 | Default | Purpose |
238
- | ----------------------------- | ------------------ | ----------- | ---------------------------------------------------------------------------------------------------- |
239
- | `MIKROTIK_HOST` | `--host` | `127.0.0.1` | RouterOS host |
240
- | `MIKROTIK_USERNAME` | `--username` | `admin` | SSH user |
241
- | `MIKROTIK_PORT` | `--port` | `22` | SSH port |
242
- | `MIKROTIK_PASSWORD` | `--password` | — | SSH password _(or use a key →)_ |
243
- | `MIKROTIK_KEY_FILENAME` | `--key-filename` | — | SSH private-key file path |
244
- | `MIKROTIK_PRIVATE_KEY` | `--private-key` | — | Inline private key (PEM) |
245
- | `MIKROTIK_KEY_PASSPHRASE` | `--key-passphrase` | — | Passphrase for an encrypted key |
246
- | `MIKROTIK_JUMP_HOST` | `--jump-host` | — | SSH bastion to tunnel through ([jump hosts](docs/multi-device.md#ssh-jump-hosts-bastion--proxyjump)) |
247
- | `MIKROTIK_CONFIG_FILE` | `--config` | — | JSON file of named devices ([multi-device](docs/multi-device.md)) |
248
- | `MIKROTIK_DEVICES` | `--devices` | — | Inline JSON of named devices |
249
- | `MIKROTIK_MCP__TRANSPORT` | `--transport` | `stdio` | `stdio` / `streamable-http` / `sse` |
250
- | `MIKROTIK_MCP__PORT` | `--mcp-port` | `8000` | HTTP bind port |
251
- | `MIKROTIK_DASHBOARD__ENABLED` | `--dashboard` | `false` | Real-time observability dashboard ([docs](docs/observability.md)) |
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-sq4trv31.js";
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 devices = Object.entries(cfg.devices).map(([name, dc]) => ({
1213
- name,
1214
- host: dc.host,
1215
- port: dc.port,
1216
- mac: dc.mac,
1217
- transport: dc.mac ? "mac-telnet" : "ssh",
1218
- address: dc.mac ? dc.mac : `${dc.host}:${dc.port}`,
1219
- username: dc.username,
1220
- authMode: dc.mac ? "mac-telnet" : dc.keyFilename || dc.privateKey ? "key" : dc.password ? "password" : "none",
1221
- isDefault: name === cfg.defaultDevice,
1222
- description: dc.description,
1223
- jumpVia: dc.jumpVia,
1224
- jumpHost: dc.jumpHost ? { host: dc.jumpHost.host, port: dc.jumpHost.port } : undefined,
1225
- status: getDeviceStatus(name),
1226
- history: getDeviceHistory(name),
1227
- activity: activity.get(name) ?? {
1228
- calls: 0,
1229
- errors: 0,
1230
- lastSeen: 0,
1231
- avgMs: 0
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.42.0",
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-5cjmn0n7.js";
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.42.0",
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",
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./cli-sq4trv31.js";
7
+ } from "./cli-k2ghtqd0.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,