@usex/mikrotik-mcp 3.30.0 → 3.31.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.
Files changed (3) hide show
  1. package/dist/cli.js +45 -11
  2. package/dist/index.js +45 -11
  3. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -626,10 +626,10 @@ class MikroTikMacTelnetClient {
626
626
  const explicitSourceMac = this.opts.sourceMac ? parseMac(this.opts.sourceMac) : undefined;
627
627
  const host = this.opts.host ?? DEFAULT_MAC_TELNET_BROADCAST;
628
628
  const askedDiscovery = host === DEFAULT_MAC_TELNET_BROADCAST && !explicitSourceMac;
629
- if (askedDiscovery) {
630
- const candidates = listBroadcastInterfaces();
631
- logger.info(`[mac-telnet] discovery candidates: ${candidates.length ? candidates.map((i) => `${i.name}(${formatMac(i.mac)}\u2192${i.broadcast})`).join(", ") : "NONE \u2014 no usable non-internal IPv4 interface found"}`);
632
- }
629
+ const candidates = askedDiscovery ? listBroadcastInterfaces() : [];
630
+ const candidatesText = candidates.length ? candidates.map((i) => `${i.name}(${formatMac(i.mac)}\u2192${i.broadcast})`).join(", ") : "NONE \u2014 no usable non-internal IPv4 interface found (is this host on the device's LAN?)";
631
+ if (askedDiscovery)
632
+ logger.info(`[mac-telnet] discovery candidates: ${candidatesText}`);
633
633
  logger.info(`[mac-telnet] resolving route to ${this.opts.mac}\u2026`);
634
634
  const route = await resolveMacTelnetRoute({
635
635
  destinationMac,
@@ -639,7 +639,7 @@ class MikroTikMacTelnetClient {
639
639
  explicitSourceMac
640
640
  });
641
641
  const discoveryFailed = askedDiscovery && route.host === DEFAULT_MAC_TELNET_BROADCAST;
642
- this.routeHint = discoveryFailed ? "no local interface got a reply from the device during discovery \u2014 it is likely on a " + "different Layer-2 segment, mac-server is disabled on the facing interface, or a host " + "firewall is dropping the UDP 20561 reply. Pin the segment by setting `macHost` to the " + "device subnet's broadcast (e.g. 192.168.88.255) and, if needed, `sourceMac`." : undefined;
642
+ this.routeHint = discoveryFailed ? "no local interface got a reply from the device during discovery \u2014 MAC-Telnet is Layer-2 " + "only, so the machine running this MCP server MUST be on the SAME Ethernet/Wi-Fi segment as " + "the device (it cannot cross any router or the internet). Likely: this host is not on that " + "segment, mac-server is disabled on the device's facing interface (`/tool mac-server`), or a " + "host firewall is dropping the UDP 20561 reply. " + `Interfaces tried: ${candidatesText}. ` + "To pin it, set the device's `macHost` to the LAN's directed broadcast (e.g. 192.168.88.255) " + "and `sourceMac` to the real MAC of the interface on that LAN." : undefined;
643
643
  logger.info(`[mac-telnet] route: source ${formatMac(route.sourceMac)} \u2192 ${route.host} ` + `(${explicitSourceMac ? "explicit" : discoveryFailed ? "DISCOVERY FAILED \u2014 limited broadcast" : "discovered"})`);
644
644
  const transport = createUdpMacTelnetTransport({
645
645
  host: route.host,
@@ -692,6 +692,7 @@ class MikroTikMacTelnetClient {
692
692
  // src/ssh/client.ts
693
693
  import { readFileSync as readFileSync2 } from "fs";
694
694
  import { Client } from "ssh2";
695
+ var RUN_IDLE_TIMEOUT_MS = 60000;
695
696
  function decodeOutput(data) {
696
697
  if (!data || data.length === 0)
697
698
  return "";
@@ -762,16 +763,43 @@ class MikroTikSSHClient {
762
763
  const stderrBuf = [];
763
764
  let settled = false;
764
765
  let timer;
766
+ let idleTimer;
767
+ const clearTimers = () => {
768
+ if (timer)
769
+ clearTimeout(timer);
770
+ if (idleTimer)
771
+ clearTimeout(idleTimer);
772
+ };
765
773
  const finish = () => {
766
774
  if (settled)
767
775
  return;
768
776
  settled = true;
769
- if (timer)
770
- clearTimeout(timer);
777
+ clearTimers();
771
778
  const out = decodeOutput(Buffer.concat(stdout));
772
779
  const error = decodeOutput(Buffer.concat(stderrBuf));
773
780
  resolve2(error && !out ? error : out);
774
781
  };
782
+ const fail = (e) => {
783
+ if (settled)
784
+ return;
785
+ settled = true;
786
+ clearTimers();
787
+ try {
788
+ stream.close();
789
+ } catch {}
790
+ reject(e);
791
+ };
792
+ const armIdle = () => {
793
+ if (idleTimer)
794
+ clearTimeout(idleTimer);
795
+ idleTimer = setTimeout(() => {
796
+ try {
797
+ stream.signal("INT");
798
+ } catch {}
799
+ fail(new Error(`MikroTik command produced no output for ${RUN_IDLE_TIMEOUT_MS / 1000}s and the SSH ` + "channel never closed \u2014 it appears wedged (often a malformed or unbalanced command). " + `Aborted to avoid hanging the connection. Command: ${command.slice(0, 120)}`));
800
+ }, RUN_IDLE_TIMEOUT_MS);
801
+ };
802
+ armIdle();
775
803
  if (opts.maxMs && opts.maxMs > 0) {
776
804
  timer = setTimeout(() => {
777
805
  try {
@@ -783,7 +811,13 @@ class MikroTikSSHClient {
783
811
  finish();
784
812
  }, opts.maxMs);
785
813
  }
786
- stream.on("close", finish).on("data", (d) => stdout.push(d)).stderr.on("data", (d) => stderrBuf.push(d));
814
+ stream.on("close", finish).on("data", (d) => {
815
+ stdout.push(d);
816
+ armIdle();
817
+ }).stderr.on("data", (d) => {
818
+ stderrBuf.push(d);
819
+ armIdle();
820
+ });
787
821
  });
788
822
  });
789
823
  }
@@ -27075,7 +27109,7 @@ function registerPrompts(server) {
27075
27109
  // package.json
27076
27110
  var package_default = {
27077
27111
  name: "@usex/mikrotik-mcp",
27078
- version: "3.30.0",
27112
+ version: "3.31.0",
27079
27113
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
27080
27114
  keywords: [
27081
27115
  "ai",
@@ -27168,11 +27202,11 @@ var package_default = {
27168
27202
  "class-variance-authority": "^0.7.1",
27169
27203
  clsx: "^2.1.1",
27170
27204
  gsap: "^3.15.0",
27171
- prettier: "^3.8.4",
27205
+ prettier: "^3.9.1",
27172
27206
  react: "^19.2.7",
27173
27207
  "react-dom": "^19.2.7",
27174
27208
  recharts: "^3.9.0",
27175
- "release-it": "^20.2.0",
27209
+ "release-it": "^20.2.1",
27176
27210
  "tailwind-merge": "^3.6.0",
27177
27211
  tailwindcss: "^4.3.1",
27178
27212
  vite: "npm:@voidzero-dev/vite-plus-core@latest",
package/dist/index.js CHANGED
@@ -618,10 +618,10 @@ class MikroTikMacTelnetClient {
618
618
  const explicitSourceMac = this.opts.sourceMac ? parseMac(this.opts.sourceMac) : undefined;
619
619
  const host = this.opts.host ?? DEFAULT_MAC_TELNET_BROADCAST;
620
620
  const askedDiscovery = host === DEFAULT_MAC_TELNET_BROADCAST && !explicitSourceMac;
621
- if (askedDiscovery) {
622
- const candidates = listBroadcastInterfaces();
623
- logger.info(`[mac-telnet] discovery candidates: ${candidates.length ? candidates.map((i) => `${i.name}(${formatMac(i.mac)}\u2192${i.broadcast})`).join(", ") : "NONE \u2014 no usable non-internal IPv4 interface found"}`);
624
- }
621
+ const candidates = askedDiscovery ? listBroadcastInterfaces() : [];
622
+ const candidatesText = candidates.length ? candidates.map((i) => `${i.name}(${formatMac(i.mac)}\u2192${i.broadcast})`).join(", ") : "NONE \u2014 no usable non-internal IPv4 interface found (is this host on the device's LAN?)";
623
+ if (askedDiscovery)
624
+ logger.info(`[mac-telnet] discovery candidates: ${candidatesText}`);
625
625
  logger.info(`[mac-telnet] resolving route to ${this.opts.mac}\u2026`);
626
626
  const route = await resolveMacTelnetRoute({
627
627
  destinationMac,
@@ -631,7 +631,7 @@ class MikroTikMacTelnetClient {
631
631
  explicitSourceMac
632
632
  });
633
633
  const discoveryFailed = askedDiscovery && route.host === DEFAULT_MAC_TELNET_BROADCAST;
634
- this.routeHint = discoveryFailed ? "no local interface got a reply from the device during discovery \u2014 it is likely on a " + "different Layer-2 segment, mac-server is disabled on the facing interface, or a host " + "firewall is dropping the UDP 20561 reply. Pin the segment by setting `macHost` to the " + "device subnet's broadcast (e.g. 192.168.88.255) and, if needed, `sourceMac`." : undefined;
634
+ this.routeHint = discoveryFailed ? "no local interface got a reply from the device during discovery \u2014 MAC-Telnet is Layer-2 " + "only, so the machine running this MCP server MUST be on the SAME Ethernet/Wi-Fi segment as " + "the device (it cannot cross any router or the internet). Likely: this host is not on that " + "segment, mac-server is disabled on the device's facing interface (`/tool mac-server`), or a " + "host firewall is dropping the UDP 20561 reply. " + `Interfaces tried: ${candidatesText}. ` + "To pin it, set the device's `macHost` to the LAN's directed broadcast (e.g. 192.168.88.255) " + "and `sourceMac` to the real MAC of the interface on that LAN." : undefined;
635
635
  logger.info(`[mac-telnet] route: source ${formatMac(route.sourceMac)} \u2192 ${route.host} ` + `(${explicitSourceMac ? "explicit" : discoveryFailed ? "DISCOVERY FAILED \u2014 limited broadcast" : "discovered"})`);
636
636
  const transport = createUdpMacTelnetTransport({
637
637
  host: route.host,
@@ -684,6 +684,7 @@ class MikroTikMacTelnetClient {
684
684
  // src/ssh/client.ts
685
685
  import { readFileSync as readFileSync2 } from "fs";
686
686
  import { Client } from "ssh2";
687
+ var RUN_IDLE_TIMEOUT_MS = 60000;
687
688
  function decodeOutput(data) {
688
689
  if (!data || data.length === 0)
689
690
  return "";
@@ -754,16 +755,43 @@ class MikroTikSSHClient {
754
755
  const stderrBuf = [];
755
756
  let settled = false;
756
757
  let timer;
758
+ let idleTimer;
759
+ const clearTimers = () => {
760
+ if (timer)
761
+ clearTimeout(timer);
762
+ if (idleTimer)
763
+ clearTimeout(idleTimer);
764
+ };
757
765
  const finish = () => {
758
766
  if (settled)
759
767
  return;
760
768
  settled = true;
761
- if (timer)
762
- clearTimeout(timer);
769
+ clearTimers();
763
770
  const out = decodeOutput(Buffer.concat(stdout));
764
771
  const error = decodeOutput(Buffer.concat(stderrBuf));
765
772
  resolve2(error && !out ? error : out);
766
773
  };
774
+ const fail = (e) => {
775
+ if (settled)
776
+ return;
777
+ settled = true;
778
+ clearTimers();
779
+ try {
780
+ stream.close();
781
+ } catch {}
782
+ reject(e);
783
+ };
784
+ const armIdle = () => {
785
+ if (idleTimer)
786
+ clearTimeout(idleTimer);
787
+ idleTimer = setTimeout(() => {
788
+ try {
789
+ stream.signal("INT");
790
+ } catch {}
791
+ fail(new Error(`MikroTik command produced no output for ${RUN_IDLE_TIMEOUT_MS / 1000}s and the SSH ` + "channel never closed \u2014 it appears wedged (often a malformed or unbalanced command). " + `Aborted to avoid hanging the connection. Command: ${command.slice(0, 120)}`));
792
+ }, RUN_IDLE_TIMEOUT_MS);
793
+ };
794
+ armIdle();
767
795
  if (opts.maxMs && opts.maxMs > 0) {
768
796
  timer = setTimeout(() => {
769
797
  try {
@@ -775,7 +803,13 @@ class MikroTikSSHClient {
775
803
  finish();
776
804
  }, opts.maxMs);
777
805
  }
778
- stream.on("close", finish).on("data", (d) => stdout.push(d)).stderr.on("data", (d) => stderrBuf.push(d));
806
+ stream.on("close", finish).on("data", (d) => {
807
+ stdout.push(d);
808
+ armIdle();
809
+ }).stderr.on("data", (d) => {
810
+ stderrBuf.push(d);
811
+ armIdle();
812
+ });
779
813
  });
780
814
  });
781
815
  }
@@ -25053,7 +25087,7 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
25053
25087
  // package.json
25054
25088
  var package_default = {
25055
25089
  name: "@usex/mikrotik-mcp",
25056
- version: "3.30.0",
25090
+ version: "3.31.0",
25057
25091
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
25058
25092
  keywords: [
25059
25093
  "ai",
@@ -25146,11 +25180,11 @@ var package_default = {
25146
25180
  "class-variance-authority": "^0.7.1",
25147
25181
  clsx: "^2.1.1",
25148
25182
  gsap: "^3.15.0",
25149
- prettier: "^3.8.4",
25183
+ prettier: "^3.9.1",
25150
25184
  react: "^19.2.7",
25151
25185
  "react-dom": "^19.2.7",
25152
25186
  recharts: "^3.9.0",
25153
- "release-it": "^20.2.0",
25187
+ "release-it": "^20.2.1",
25154
25188
  "tailwind-merge": "^3.6.0",
25155
25189
  tailwindcss: "^4.3.1",
25156
25190
  vite: "npm:@voidzero-dev/vite-plus-core@latest",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usex/mikrotik-mcp",
3
- "version": "3.30.0",
3
+ "version": "3.31.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",
@@ -93,11 +93,11 @@
93
93
  "class-variance-authority": "^0.7.1",
94
94
  "clsx": "^2.1.1",
95
95
  "gsap": "^3.15.0",
96
- "prettier": "^3.8.4",
96
+ "prettier": "^3.9.1",
97
97
  "react": "^19.2.7",
98
98
  "react-dom": "^19.2.7",
99
99
  "recharts": "^3.9.0",
100
- "release-it": "^20.2.0",
100
+ "release-it": "^20.2.1",
101
101
  "tailwind-merge": "^3.6.0",
102
102
  "tailwindcss": "^4.3.1",
103
103
  "vite": "npm:@voidzero-dev/vite-plus-core@latest",