@usex/mikrotik-mcp 3.20.0 → 3.21.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/dist/cli.js CHANGED
@@ -617,7 +617,7 @@ class MikroTikMacTelnetClient {
617
617
  return false;
618
618
  }
619
619
  }
620
- async run(command) {
620
+ async run(command, _opts = {}) {
621
621
  if (!this.console || !this.console.isReady) {
622
622
  throw new Error("Not connected to MikroTik device (MAC-Telnet)");
623
623
  }
@@ -694,7 +694,7 @@ class MikroTikSSHClient {
694
694
  }).connect(cfg);
695
695
  });
696
696
  }
697
- run(command) {
697
+ run(command, opts = {}) {
698
698
  if (!this.client) {
699
699
  return Promise.reject(new Error("Not connected to MikroTik device"));
700
700
  }
@@ -707,11 +707,30 @@ class MikroTikSSHClient {
707
707
  }
708
708
  const stdout = [];
709
709
  const stderrBuf = [];
710
- stream.on("close", () => {
710
+ let settled = false;
711
+ let timer;
712
+ const finish = () => {
713
+ if (settled)
714
+ return;
715
+ settled = true;
716
+ if (timer)
717
+ clearTimeout(timer);
711
718
  const out = decodeOutput(Buffer.concat(stdout));
712
719
  const error = decodeOutput(Buffer.concat(stderrBuf));
713
720
  resolve2(error && !out ? error : out);
714
- }).on("data", (d) => stdout.push(d)).stderr.on("data", (d) => stderrBuf.push(d));
721
+ };
722
+ if (opts.maxMs && opts.maxMs > 0) {
723
+ timer = setTimeout(() => {
724
+ try {
725
+ stream.signal("INT");
726
+ } catch {}
727
+ try {
728
+ stream.close();
729
+ } catch {}
730
+ finish();
731
+ }, opts.maxMs);
732
+ }
733
+ stream.on("close", finish).on("data", (d) => stdout.push(d)).stderr.on("data", (d) => stderrBuf.push(d));
715
734
  });
716
735
  });
717
736
  }
@@ -1010,7 +1029,7 @@ function getSafeModeManager(deviceName) {
1010
1029
  }
1011
1030
 
1012
1031
  // src/core/connector.ts
1013
- async function runOnce(command, deviceName) {
1032
+ async function runOnce(command, deviceName, opts) {
1014
1033
  const name = resolveDeviceName(deviceName);
1015
1034
  const dc = getDevice(deviceName);
1016
1035
  const client = createDeviceClient(dc);
@@ -1018,12 +1037,12 @@ async function runOnce(command, deviceName) {
1018
1037
  if (!await client.connect()) {
1019
1038
  throw new Error(connectErrorMessage(name, dc, client.lastError));
1020
1039
  }
1021
- return await client.run(command);
1040
+ return await client.run(command, opts);
1022
1041
  } finally {
1023
1042
  client.disconnect();
1024
1043
  }
1025
1044
  }
1026
- async function executeMikrotikCommand(command, ctx) {
1045
+ async function executeMikrotikCommand(command, ctx, opts) {
1027
1046
  const deviceName = resolveDeviceName(ctx.device);
1028
1047
  const safe = getSafeModeManager(deviceName);
1029
1048
  if (safe.isActive) {
@@ -1031,7 +1050,7 @@ async function executeMikrotikCommand(command, ctx) {
1031
1050
  return safe.execute(command);
1032
1051
  }
1033
1052
  ctx.info(`[${deviceName}] Executing MikroTik command: ${command}`);
1034
- return runOnce(command, ctx.device);
1053
+ return runOnce(command, ctx.device, opts);
1035
1054
  }
1036
1055
 
1037
1056
  // src/core/registry.ts
@@ -1124,6 +1143,16 @@ function isEmpty(result) {
1124
1143
  const t = result.trim();
1125
1144
  return t === "" || t === "no such item" || t === "no such item (4)";
1126
1145
  }
1146
+ function flattenLiveOutput(text) {
1147
+ return text.split(`
1148
+ `).map((line) => {
1149
+ const segs = line.split("\r").filter((s) => s.trim() !== "");
1150
+ return segs.length ? segs[segs.length - 1] : "";
1151
+ }).join(`
1152
+ `).replace(/\n{3,}/g, `
1153
+
1154
+ `).trim();
1155
+ }
1127
1156
  function extractCreatedId(output) {
1128
1157
  const star = output.match(/\*[0-9A-Fa-f]+/);
1129
1158
  if (star)
@@ -11406,7 +11435,7 @@ var networkToolTools = [
11406
11435
  async handler(a, ctx) {
11407
11436
  ctx.info(`Pinging ${a.address} (count=${a.count})`);
11408
11437
  const cmd = new Cmd(`/ping ${a.address}`).set("count", a.count).opt("interface", a.interface).opt("src-address", a.src_address).opt("size", a.size).build();
11409
- const result = await executeMikrotikCommand(cmd, ctx);
11438
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.count * 1500 + 6000 }));
11410
11439
  if (looksLikeError(result))
11411
11440
  return `Failed to ping ${a.address}: ${result}`;
11412
11441
  return isEmpty(result) ? `No response from ${a.address}.` : `PING ${a.address}:
@@ -11427,7 +11456,7 @@ ${result}`;
11427
11456
  async handler(a, ctx) {
11428
11457
  ctx.info(`Tracerouting ${a.address} (count=${a.count})`);
11429
11458
  const cmd = new Cmd(`/tool traceroute ${a.address}`).set("count", a.count).flag("use-dns", a.use_dns).build();
11430
- const result = await executeMikrotikCommand(cmd, ctx);
11459
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: 30000 + a.count * 3000 }));
11431
11460
  if (looksLikeError(result))
11432
11461
  return `Failed to traceroute ${a.address}: ${result}`;
11433
11462
  return isEmpty(result) ? `No route information for ${a.address}.` : `TRACEROUTE ${a.address}:
@@ -11451,7 +11480,7 @@ ${result}`;
11451
11480
  async handler(a, ctx) {
11452
11481
  ctx.info(`Bandwidth test to ${a.address} (duration=${a.duration}s, direction=${a.direction})`);
11453
11482
  const cmd = new Cmd(`/tool bandwidth-test ${a.address}`).set("duration", `${a.duration}s`).set("direction", a.direction).set("protocol", a.protocol).opt("user", a.user).opt("password", a.password).build();
11454
- const result = await executeMikrotikCommand(cmd, ctx);
11483
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.duration * 1000 + 12000 }));
11455
11484
  if (looksLikeError(result))
11456
11485
  return `Failed to run bandwidth test to ${a.address}: ${result}`;
11457
11486
  return isEmpty(result) ? `No bandwidth test results for ${a.address}.` : `BANDWIDTH TEST:
@@ -12103,7 +12132,7 @@ var floodPingTools = [
12103
12132
  async handler(a, ctx) {
12104
12133
  ctx.info(`Flood-pinging ${a.address} (count=${a.count})`);
12105
12134
  const cmd = new Cmd(`/tool flood-ping ${a.address}`).set("count", a.count).opt("size", a.size).opt("interface", a.interface).opt("src-address", a.src_address).build();
12106
- const result = await executeMikrotikCommand(cmd, ctx);
12135
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: 30000 }));
12107
12136
  if (looksLikeError(result))
12108
12137
  return `Failed to flood-ping ${a.address}: ${result}`;
12109
12138
  return isEmpty(result) ? `No response from ${a.address}.` : `FLOOD PING ${a.address}:
@@ -12767,7 +12796,7 @@ var speedTestTools = [
12767
12796
  async handler(a, ctx) {
12768
12797
  ctx.info(`Speed test to ${a.address} (duration=${a.duration}s, direction=${a.direction})`);
12769
12798
  const cmd = new Cmd(`/tool speed-test address=${a.address}`).set("duration", `${a.duration}s`).set("direction", a.direction).opt("tcp-connection-count", a.tcp_connection_count).opt("user", a.user).opt("password", a.password).build();
12770
- const result = await executeMikrotikCommand(cmd, ctx);
12799
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.duration * 1000 + 15000 }));
12771
12800
  if (looksLikeError(result))
12772
12801
  return `Failed to run speed test to ${a.address}: ${result}`;
12773
12802
  return isEmpty(result) ? `No speed-test results for ${a.address}.` : `SPEED TEST:
@@ -23922,7 +23951,7 @@ function registerPrompts(server) {
23922
23951
  // package.json
23923
23952
  var package_default = {
23924
23953
  name: "@usex/mikrotik-mcp",
23925
- version: "3.20.0",
23954
+ version: "3.21.0",
23926
23955
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
23927
23956
  keywords: [
23928
23957
  "ai",
package/dist/index.d.ts CHANGED
@@ -26,8 +26,12 @@ type SendLog = (level: "info" | "error", message: string) => void;
26
26
  *
27
27
  * @param command Fully-formed RouterOS CLI command (e.g. `/ip address print`).
28
28
  * @param ctx Per-call context carrying the target device.
29
+ * @param opts `maxMs` caps the one-shot read for interactive/streaming
30
+ * commands (ping, bandwidth-test) so they can't hang the tool.
29
31
  */
30
- declare function executeMikrotikCommand(command: string, ctx: ToolContext): Promise<string>;
32
+ declare function executeMikrotikCommand(command: string, ctx: ToolContext, opts?: {
33
+ maxMs?: number;
34
+ }): Promise<string>;
31
35
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
32
36
  import { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
33
37
  import { ZodRawShape } from "zod";
@@ -147,7 +151,9 @@ declare class MikroTikSSHClient {
147
151
  /** Establish the SSH connection. Resolves `true` on success, `false` on failure. */
148
152
  connect(): Promise<boolean>;
149
153
  /** Run a single command on a fresh SSH channel and return its decoded output. */
150
- run(command: string): Promise<string>;
154
+ run(command: string, opts?: {
155
+ maxMs?: number;
156
+ }): Promise<string>;
151
157
  /** Open a persistent interactive shell channel (used by Safe Mode). */
152
158
  shell(opts?: {
153
159
  term?: string;
package/dist/index.js CHANGED
@@ -609,7 +609,7 @@ class MikroTikMacTelnetClient {
609
609
  return false;
610
610
  }
611
611
  }
612
- async run(command) {
612
+ async run(command, _opts = {}) {
613
613
  if (!this.console || !this.console.isReady) {
614
614
  throw new Error("Not connected to MikroTik device (MAC-Telnet)");
615
615
  }
@@ -686,7 +686,7 @@ class MikroTikSSHClient {
686
686
  }).connect(cfg);
687
687
  });
688
688
  }
689
- run(command) {
689
+ run(command, opts = {}) {
690
690
  if (!this.client) {
691
691
  return Promise.reject(new Error("Not connected to MikroTik device"));
692
692
  }
@@ -699,11 +699,30 @@ class MikroTikSSHClient {
699
699
  }
700
700
  const stdout = [];
701
701
  const stderrBuf = [];
702
- stream.on("close", () => {
702
+ let settled = false;
703
+ let timer;
704
+ const finish = () => {
705
+ if (settled)
706
+ return;
707
+ settled = true;
708
+ if (timer)
709
+ clearTimeout(timer);
703
710
  const out = decodeOutput(Buffer.concat(stdout));
704
711
  const error = decodeOutput(Buffer.concat(stderrBuf));
705
712
  resolve2(error && !out ? error : out);
706
- }).on("data", (d) => stdout.push(d)).stderr.on("data", (d) => stderrBuf.push(d));
713
+ };
714
+ if (opts.maxMs && opts.maxMs > 0) {
715
+ timer = setTimeout(() => {
716
+ try {
717
+ stream.signal("INT");
718
+ } catch {}
719
+ try {
720
+ stream.close();
721
+ } catch {}
722
+ finish();
723
+ }, opts.maxMs);
724
+ }
725
+ stream.on("close", finish).on("data", (d) => stdout.push(d)).stderr.on("data", (d) => stderrBuf.push(d));
707
726
  });
708
727
  });
709
728
  }
@@ -996,7 +1015,7 @@ function getSafeModeManager(deviceName) {
996
1015
  }
997
1016
 
998
1017
  // src/core/connector.ts
999
- async function runOnce(command, deviceName) {
1018
+ async function runOnce(command, deviceName, opts) {
1000
1019
  const name = resolveDeviceName(deviceName);
1001
1020
  const dc = getDevice(deviceName);
1002
1021
  const client = createDeviceClient(dc);
@@ -1004,12 +1023,12 @@ async function runOnce(command, deviceName) {
1004
1023
  if (!await client.connect()) {
1005
1024
  throw new Error(connectErrorMessage(name, dc, client.lastError));
1006
1025
  }
1007
- return await client.run(command);
1026
+ return await client.run(command, opts);
1008
1027
  } finally {
1009
1028
  client.disconnect();
1010
1029
  }
1011
1030
  }
1012
- async function executeMikrotikCommand(command, ctx) {
1031
+ async function executeMikrotikCommand(command, ctx, opts) {
1013
1032
  const deviceName = resolveDeviceName(ctx.device);
1014
1033
  const safe = getSafeModeManager(deviceName);
1015
1034
  if (safe.isActive) {
@@ -1017,7 +1036,7 @@ async function executeMikrotikCommand(command, ctx) {
1017
1036
  return safe.execute(command);
1018
1037
  }
1019
1038
  ctx.info(`[${deviceName}] Executing MikroTik command: ${command}`);
1020
- return runOnce(command, ctx.device);
1039
+ return runOnce(command, ctx.device, opts);
1021
1040
  }
1022
1041
  // src/core/registry.ts
1023
1042
  import { z as z2 } from "zod";
@@ -1109,6 +1128,16 @@ function isEmpty(result) {
1109
1128
  const t = result.trim();
1110
1129
  return t === "" || t === "no such item" || t === "no such item (4)";
1111
1130
  }
1131
+ function flattenLiveOutput(text) {
1132
+ return text.split(`
1133
+ `).map((line) => {
1134
+ const segs = line.split("\r").filter((s) => s.trim() !== "");
1135
+ return segs.length ? segs[segs.length - 1] : "";
1136
+ }).join(`
1137
+ `).replace(/\n{3,}/g, `
1138
+
1139
+ `).trim();
1140
+ }
1112
1141
  function extractCreatedId(output) {
1113
1142
  const star = output.match(/\*[0-9A-Fa-f]+/);
1114
1143
  if (star)
@@ -11418,7 +11447,7 @@ var networkToolTools = [
11418
11447
  async handler(a, ctx) {
11419
11448
  ctx.info(`Pinging ${a.address} (count=${a.count})`);
11420
11449
  const cmd = new Cmd(`/ping ${a.address}`).set("count", a.count).opt("interface", a.interface).opt("src-address", a.src_address).opt("size", a.size).build();
11421
- const result = await executeMikrotikCommand(cmd, ctx);
11450
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.count * 1500 + 6000 }));
11422
11451
  if (looksLikeError(result))
11423
11452
  return `Failed to ping ${a.address}: ${result}`;
11424
11453
  return isEmpty(result) ? `No response from ${a.address}.` : `PING ${a.address}:
@@ -11439,7 +11468,7 @@ ${result}`;
11439
11468
  async handler(a, ctx) {
11440
11469
  ctx.info(`Tracerouting ${a.address} (count=${a.count})`);
11441
11470
  const cmd = new Cmd(`/tool traceroute ${a.address}`).set("count", a.count).flag("use-dns", a.use_dns).build();
11442
- const result = await executeMikrotikCommand(cmd, ctx);
11471
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: 30000 + a.count * 3000 }));
11443
11472
  if (looksLikeError(result))
11444
11473
  return `Failed to traceroute ${a.address}: ${result}`;
11445
11474
  return isEmpty(result) ? `No route information for ${a.address}.` : `TRACEROUTE ${a.address}:
@@ -11463,7 +11492,7 @@ ${result}`;
11463
11492
  async handler(a, ctx) {
11464
11493
  ctx.info(`Bandwidth test to ${a.address} (duration=${a.duration}s, direction=${a.direction})`);
11465
11494
  const cmd = new Cmd(`/tool bandwidth-test ${a.address}`).set("duration", `${a.duration}s`).set("direction", a.direction).set("protocol", a.protocol).opt("user", a.user).opt("password", a.password).build();
11466
- const result = await executeMikrotikCommand(cmd, ctx);
11495
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.duration * 1000 + 12000 }));
11467
11496
  if (looksLikeError(result))
11468
11497
  return `Failed to run bandwidth test to ${a.address}: ${result}`;
11469
11498
  return isEmpty(result) ? `No bandwidth test results for ${a.address}.` : `BANDWIDTH TEST:
@@ -12115,7 +12144,7 @@ var floodPingTools = [
12115
12144
  async handler(a, ctx) {
12116
12145
  ctx.info(`Flood-pinging ${a.address} (count=${a.count})`);
12117
12146
  const cmd = new Cmd(`/tool flood-ping ${a.address}`).set("count", a.count).opt("size", a.size).opt("interface", a.interface).opt("src-address", a.src_address).build();
12118
- const result = await executeMikrotikCommand(cmd, ctx);
12147
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: 30000 }));
12119
12148
  if (looksLikeError(result))
12120
12149
  return `Failed to flood-ping ${a.address}: ${result}`;
12121
12150
  return isEmpty(result) ? `No response from ${a.address}.` : `FLOOD PING ${a.address}:
@@ -12779,7 +12808,7 @@ var speedTestTools = [
12779
12808
  async handler(a, ctx) {
12780
12809
  ctx.info(`Speed test to ${a.address} (duration=${a.duration}s, direction=${a.direction})`);
12781
12810
  const cmd = new Cmd(`/tool speed-test address=${a.address}`).set("duration", `${a.duration}s`).set("direction", a.direction).opt("tcp-connection-count", a.tcp_connection_count).opt("user", a.user).opt("password", a.password).build();
12782
- const result = await executeMikrotikCommand(cmd, ctx);
12811
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.duration * 1000 + 15000 }));
12783
12812
  if (looksLikeError(result))
12784
12813
  return `Failed to run speed test to ${a.address}: ${result}`;
12785
12814
  return isEmpty(result) ? `No speed-test results for ${a.address}.` : `SPEED TEST:
@@ -22322,7 +22351,7 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
22322
22351
  // package.json
22323
22352
  var package_default = {
22324
22353
  name: "@usex/mikrotik-mcp",
22325
- version: "3.20.0",
22354
+ version: "3.21.0",
22326
22355
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
22327
22356
  keywords: [
22328
22357
  "ai",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usex/mikrotik-mcp",
3
- "version": "3.20.0",
3
+ "version": "3.21.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",