@usex/mikrotik-mcp 3.42.0 → 3.43.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,
@@ -84,7 +88,7 @@ import {
84
88
  toggleAaaEntity,
85
89
  updateAaaEntity,
86
90
  writeBackup
87
- } from "./shared/cli-sq4trv31.js";
91
+ } from "./shared/cli-vrm1c5fb.js";
88
92
 
89
93
  // src/cli.ts
90
94
  import { existsSync as existsSync2 } from "fs";
@@ -1209,28 +1213,41 @@ function deviceActivity(store) {
1209
1213
  function devicesPayload(store) {
1210
1214
  const cfg = getConfig();
1211
1215
  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
- }));
1216
+ const poolEnabled = isPoolEnabled();
1217
+ const poolMap = new Map(poolStatus().map((p) => [p.device, p]));
1218
+ const devices = Object.entries(cfg.devices).map(([name, dc]) => {
1219
+ const isMac = isMacTelnetDevice(dc);
1220
+ const ps = poolMap.get(name);
1221
+ return {
1222
+ name,
1223
+ host: dc.host,
1224
+ port: dc.port,
1225
+ mac: dc.mac,
1226
+ transport: dc.mac ? "mac-telnet" : "ssh",
1227
+ address: dc.mac ? dc.mac : `${dc.host}:${dc.port}`,
1228
+ username: dc.username,
1229
+ authMode: dc.mac ? "mac-telnet" : dc.keyFilename || dc.privateKey ? "key" : dc.password ? "password" : "none",
1230
+ isDefault: name === cfg.defaultDevice,
1231
+ description: dc.description,
1232
+ jumpVia: dc.jumpVia,
1233
+ jumpHost: dc.jumpHost ? { host: dc.jumpHost.host, port: dc.jumpHost.port } : undefined,
1234
+ status: getDeviceStatus(name),
1235
+ history: getDeviceHistory(name),
1236
+ activity: activity.get(name) ?? {
1237
+ calls: 0,
1238
+ errors: 0,
1239
+ lastSeen: 0,
1240
+ avgMs: 0
1241
+ },
1242
+ pool: isMac || !poolEnabled ? null : {
1243
+ device: name,
1244
+ pooled: !!ps,
1245
+ inflight: ps?.inflight ?? 0,
1246
+ idle: ps?.idle ?? false,
1247
+ dead: ps?.dead ?? false
1248
+ }
1249
+ };
1250
+ });
1234
1251
  return { server: SERVER_TAG2, defaultDevice: cfg.defaultDevice, devices };
1235
1252
  }
1236
1253
  function topologyPayload() {
@@ -1990,6 +2007,29 @@ async function runDashboard(cfg, transportLabel) {
1990
2007
  const removed = body.all === true ? db.clear() : db.delete(ids);
1991
2008
  return json({ removed, total: db.total() });
1992
2009
  }
2010
+ if (url.pathname === "/api/ssh-pool") {
2011
+ const cfg2 = getConfig();
2012
+ const enabled = isPoolEnabled();
2013
+ const ps = poolStatus();
2014
+ const totalInflight = ps.reduce((s, p) => s + p.inflight, 0);
2015
+ const totalIdle = ps.filter((p) => p.idle).length;
2016
+ const totalBusy = ps.filter((p) => p.inflight > 0).length;
2017
+ return json({
2018
+ enabled,
2019
+ config: {
2020
+ keepAlive: cfg2.ssh.keepAlive,
2021
+ keepAliveInterval: cfg2.ssh.keepAliveInterval,
2022
+ idleTimeout: cfg2.ssh.idleTimeout
2023
+ },
2024
+ aggregate: {
2025
+ totalConnections: ps.length,
2026
+ totalInflight,
2027
+ totalIdle,
2028
+ totalBusy
2029
+ },
2030
+ devices: ps
2031
+ });
2032
+ }
1993
2033
  if (url.pathname === "/api/devices") {
1994
2034
  return json(devicesPayload(db));
1995
2035
  }
@@ -2242,7 +2282,7 @@ function registerPrompts(server) {
2242
2282
  // package.json
2243
2283
  var package_default = {
2244
2284
  name: "@usex/mikrotik-mcp",
2245
- version: "3.42.0",
2285
+ version: "3.43.0",
2246
2286
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
2247
2287
  keywords: [
2248
2288
  "ai",
@@ -2615,6 +2655,11 @@ TRANSPORT OPTIONS
2615
2655
  --mcp-port HTTP bind port (MIKROTIK_MCP__PORT)
2616
2656
  --mcp-allowed-hosts Host header allow-list (DNS-rebinding protection)
2617
2657
 
2658
+ SSH CONNECTION POOLING (persistent connections, on by default)
2659
+ --ssh-keep-alive Enable/disable pooling (MIKROTIK_SSH__KEEP_ALIVE, default true)
2660
+ --ssh-keepalive-interval Keepalive packet interval (ms) (MIKROTIK_SSH__KEEPALIVE_INTERVAL)
2661
+ --ssh-idle-timeout Close idle connections after (ms) (MIKROTIK_SSH__IDLE_TIMEOUT)
2662
+
2618
2663
  OBSERVABILITY DASHBOARD (optional; real-time feed + analytics of every tool call)
2619
2664
  --dashboard Enable the dashboard (MIKROTIK_DASHBOARD__ENABLED)
2620
2665
  --dashboard-host Bind host (default 0.0.0.0=LAN) (MIKROTIK_DASHBOARD__HOST)
@@ -2717,7 +2762,13 @@ async function main() {
2717
2762
  setConfig(cfg);
2718
2763
  warnIfPlaintextPasswordInContainer(Object.values(cfg.devices).some((d) => !!d.password));
2719
2764
  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("/")})`);
2765
+ 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"})`);
2766
+ const cleanup = () => {
2767
+ closeAll();
2768
+ };
2769
+ process.on("SIGINT", cleanup);
2770
+ process.on("SIGTERM", cleanup);
2771
+ process.on("exit", cleanup);
2721
2772
  const startDashboard = async () => {
2722
2773
  if (!cfg.dashboard.enabled)
2723
2774
  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-hjde13aa.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.43.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-vrm1c5fb.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,