@usex/mikrotik-mcp 3.19.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
  }
@@ -782,7 +801,6 @@ import { z as z3 } from "zod";
782
801
 
783
802
  // src/ssh/safe-mode.ts
784
803
  var PROMPT_RE = /\[.+?@.+?\] (?:<SAFE> )?> ?$/m;
785
- var NORMAL_PROMPT_RE = /\[.+?@.+?\] > ?$/m;
786
804
  var ANSI_RE = /\x1B(?:\[[0-9;]*[mA-HJ-MSTfhilnprsu]|[()][0-9A-Za-z]|\[?\?\d+[hl])/g;
787
805
  function stripAnsi(text) {
788
806
  return text.replace(ANSI_RE, "");
@@ -793,13 +811,17 @@ function isSafeModeActivated(response) {
793
811
  return true;
794
812
  return /safe mode[^\n]*\b(?:success|taken|enabled|active)\b/i.test(response);
795
813
  }
796
- function isSafeModeReleased(response) {
814
+ function lastNonEmptyLine(response) {
797
815
  const lines = response.replace(/\r/g, "").split(`
798
816
  `).map((l) => l.trimEnd()).filter(Boolean);
799
- const last = lines.at(-1) ?? "";
800
- return NORMAL_PROMPT_RE.test(last) && !last.includes("<SAFE>");
817
+ return lines.at(-1) ?? "";
818
+ }
819
+ function classifyPrompt(response) {
820
+ const last = lastNonEmptyLine(response);
821
+ if (!PROMPT_RE.test(last))
822
+ return "unknown";
823
+ return last.includes("<SAFE>") ? "safe" : "released";
801
824
  }
802
-
803
825
  class SafeModeManager {
804
826
  deviceName;
805
827
  ssh = null;
@@ -874,13 +896,30 @@ class SafeModeManager {
874
896
  return this.lock(async () => {
875
897
  if (!this.active || !this.channel)
876
898
  return "Safe mode is not active. Nothing to commit.";
899
+ this.channel.write(`
900
+ `);
901
+ const probe = await this.readSettledPrompt();
902
+ const before = classifyPrompt(probe);
903
+ if (before === "released") {
904
+ this.cleanup();
905
+ return "Safe mode already exited \u2014 your changes are committed. Safe mode DISABLED.";
906
+ }
907
+ if (before === "unknown") {
908
+ return "Could not read a prompt to determine Safe Mode state. The session is left open so " + "nothing is reverted \u2014 call get_safe_mode_status, retry commit_safe_mode, or " + `rollback_safe_mode. Last output: ${probe.slice(-160)}`;
909
+ }
877
910
  this.channel.write(CTRL_X);
878
- const response = await this.readUntilPrompt(15000, (c) => isSafeModeReleased(c));
879
- if (!isSafeModeReleased(response)) {
880
- return "Commit may not have completed: the device did not return to a normal prompt " + `after exiting Safe Mode. Session left open \u2014 retry commit_safe_mode. Response: ${response.slice(-200)}`;
911
+ this.channel.write(`
912
+ `);
913
+ const after = await this.readSettledPrompt();
914
+ switch (classifyPrompt(after)) {
915
+ case "released":
916
+ this.cleanup();
917
+ return "Changes committed successfully. Safe mode DISABLED.";
918
+ case "safe":
919
+ return "Commit not completed \u2014 the device is still in Safe Mode. Your changes remain held " + "in memory (not reverted); call commit_safe_mode again to retry, or rollback_safe_mode " + "to discard them.";
920
+ default:
921
+ return "Commit status unclear \u2014 no prompt seen after exiting Safe Mode. The session is left " + "open so nothing is reverted; verify with get_safe_mode_status or retry commit_safe_mode. " + `Last output: ${after.slice(-160)}`;
881
922
  }
882
- this.cleanup();
883
- return "Changes committed successfully. Safe mode DISABLED.";
884
923
  });
885
924
  }
886
925
  rollback() {
@@ -916,6 +955,33 @@ class SafeModeManager {
916
955
  channel.on("data", onData);
917
956
  });
918
957
  }
958
+ readSettledPrompt(quietMs = 450, maxMs = 8000) {
959
+ const channel = this.channel;
960
+ if (!channel)
961
+ return Promise.resolve("");
962
+ return new Promise((resolve2) => {
963
+ let buf = "";
964
+ let quiet;
965
+ let hard;
966
+ function done() {
967
+ clearTimeout(hard);
968
+ if (quiet)
969
+ clearTimeout(quiet);
970
+ channel.removeListener("data", onData);
971
+ resolve2(stripAnsi(buf));
972
+ }
973
+ function onData(chunk) {
974
+ buf += decodeOutput(chunk);
975
+ if (PROMPT_RE.test(lastNonEmptyLine(stripAnsi(buf)))) {
976
+ if (quiet)
977
+ clearTimeout(quiet);
978
+ quiet = setTimeout(done, quietMs);
979
+ }
980
+ }
981
+ hard = setTimeout(done, maxMs);
982
+ channel.on("data", onData);
983
+ });
984
+ }
919
985
  extractOutput(raw, command) {
920
986
  const text = raw.replace(/\r\n/g, `
921
987
  `).replace(/\r/g, `
@@ -963,7 +1029,7 @@ function getSafeModeManager(deviceName) {
963
1029
  }
964
1030
 
965
1031
  // src/core/connector.ts
966
- async function runOnce(command, deviceName) {
1032
+ async function runOnce(command, deviceName, opts) {
967
1033
  const name = resolveDeviceName(deviceName);
968
1034
  const dc = getDevice(deviceName);
969
1035
  const client = createDeviceClient(dc);
@@ -971,12 +1037,12 @@ async function runOnce(command, deviceName) {
971
1037
  if (!await client.connect()) {
972
1038
  throw new Error(connectErrorMessage(name, dc, client.lastError));
973
1039
  }
974
- return await client.run(command);
1040
+ return await client.run(command, opts);
975
1041
  } finally {
976
1042
  client.disconnect();
977
1043
  }
978
1044
  }
979
- async function executeMikrotikCommand(command, ctx) {
1045
+ async function executeMikrotikCommand(command, ctx, opts) {
980
1046
  const deviceName = resolveDeviceName(ctx.device);
981
1047
  const safe = getSafeModeManager(deviceName);
982
1048
  if (safe.isActive) {
@@ -984,7 +1050,7 @@ async function executeMikrotikCommand(command, ctx) {
984
1050
  return safe.execute(command);
985
1051
  }
986
1052
  ctx.info(`[${deviceName}] Executing MikroTik command: ${command}`);
987
- return runOnce(command, ctx.device);
1053
+ return runOnce(command, ctx.device, opts);
988
1054
  }
989
1055
 
990
1056
  // src/core/registry.ts
@@ -1006,7 +1072,7 @@ function createContext(sendLog, device) {
1006
1072
  }
1007
1073
 
1008
1074
  // src/core/routeros.ts
1009
- var BARE_SAFE = /^[\w.\-:/,*@]+$/;
1075
+ var BARE_SAFE = /^[\w.\-:/,*@!]+$/;
1010
1076
  function quoteValue(value) {
1011
1077
  if (typeof value === "number" || typeof value === "boolean")
1012
1078
  return String(value);
@@ -1077,6 +1143,16 @@ function isEmpty(result) {
1077
1143
  const t = result.trim();
1078
1144
  return t === "" || t === "no such item" || t === "no such item (4)";
1079
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
+ }
1080
1156
  function extractCreatedId(output) {
1081
1157
  const star = output.match(/\*[0-9A-Fa-f]+/);
1082
1158
  if (star)
@@ -6317,6 +6393,12 @@ async function updateNatRule(a, ctx) {
6317
6393
  put("protocol", a.protocol);
6318
6394
  put("in-interface", a.in_interface);
6319
6395
  put("out-interface", a.out_interface);
6396
+ put("in-interface-list", a.in_interface_list);
6397
+ put("out-interface-list", a.out_interface_list);
6398
+ put("src-address-list", a.src_address_list);
6399
+ put("dst-address-list", a.dst_address_list);
6400
+ put("connection-mark", a.connection_mark);
6401
+ put("connection-nat-state", a.connection_nat_state);
6320
6402
  put("to-addresses", a.to_addresses);
6321
6403
  put("to-ports", a.to_ports);
6322
6404
  if (a.comment !== undefined)
@@ -6353,8 +6435,14 @@ var firewallNatTools = [
6353
6435
  src_port: z23.string().optional(),
6354
6436
  dst_port: z23.string().optional(),
6355
6437
  protocol: z23.string().optional(),
6356
- in_interface: z23.string().optional(),
6438
+ in_interface: z23.string().optional().describe('Negatable, e.g. "!ether1"'),
6357
6439
  out_interface: z23.string().optional(),
6440
+ in_interface_list: z23.string().optional().describe('Interface list, e.g. "WAN" or "!LAN"'),
6441
+ out_interface_list: z23.string().optional(),
6442
+ src_address_list: z23.string().optional().describe('Match src in a named list; negate "!name"'),
6443
+ dst_address_list: z23.string().optional().describe('Match dst in a named list; negate "!name"'),
6444
+ connection_mark: z23.string().optional(),
6445
+ connection_nat_state: z23.string().optional().describe('"srcnat" / "dstnat" / "!dstnat"'),
6358
6446
  to_addresses: z23.string().optional().describe('Single IP or range e.g. "10.0.0.1" or "10.0.0.1-10.0.0.10"'),
6359
6447
  to_ports: z23.string().optional().describe('Single port or range e.g. "8080" or "8080-8090"'),
6360
6448
  comment: z23.string().optional(),
@@ -6394,7 +6482,7 @@ var firewallNatTools = [
6394
6482
  } else if (a.chain === "dstnat" && !dstnatActions.includes(a.action)) {
6395
6483
  return `Error: Invalid action '${a.action}' for dstnat. Must be one of: ${dstnatActions.join(", ")}`;
6396
6484
  }
6397
- const cmd = new Cmd("/ip firewall nat add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("to-addresses", a.to_addresses).opt("to-ports", a.to_ports).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
6485
+ const cmd = new Cmd("/ip firewall nat add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("in-interface-list", a.in_interface_list).opt("out-interface-list", a.out_interface_list).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("connection-mark", a.connection_mark).opt("connection-nat-state", a.connection_nat_state).opt("to-addresses", a.to_addresses).opt("to-ports", a.to_ports).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
6398
6486
  const result = await executeMikrotikCommand(cmd, ctx);
6399
6487
  const trimmed = result.trim();
6400
6488
  if (looksLikeError(trimmed)) {
@@ -6492,6 +6580,12 @@ ${result}`;
6492
6580
  protocol: z23.string().optional(),
6493
6581
  in_interface: z23.string().optional(),
6494
6582
  out_interface: z23.string().optional(),
6583
+ in_interface_list: z23.string().optional(),
6584
+ out_interface_list: z23.string().optional(),
6585
+ src_address_list: z23.string().optional().describe('Negate with "!name"'),
6586
+ dst_address_list: z23.string().optional().describe('Negate with "!name"'),
6587
+ connection_mark: z23.string().optional(),
6588
+ connection_nat_state: z23.string().optional(),
6495
6589
  to_addresses: z23.string().optional(),
6496
6590
  to_ports: z23.string().optional(),
6497
6591
  comment: z23.string().optional(),
@@ -6584,10 +6678,33 @@ async function updateMangleRule(a, ctx) {
6584
6678
  put("protocol", a.protocol);
6585
6679
  put("in-interface", a.in_interface);
6586
6680
  put("out-interface", a.out_interface);
6681
+ put("in-interface-list", a.in_interface_list);
6682
+ put("out-interface-list", a.out_interface_list);
6683
+ put("src-address-list", a.src_address_list);
6684
+ put("dst-address-list", a.dst_address_list);
6685
+ put("src-address-type", a.src_address_type);
6686
+ put("dst-address-type", a.dst_address_type);
6587
6687
  put("connection-mark", a.connection_mark);
6588
6688
  put("packet-mark", a.packet_mark);
6589
6689
  put("routing-mark", a.routing_mark);
6590
6690
  put("connection-state", a.connection_state);
6691
+ put("connection-nat-state", a.connection_nat_state);
6692
+ put("connection-type", a.connection_type);
6693
+ put("connection-bytes", a.connection_bytes);
6694
+ put("connection-limit", a.connection_limit);
6695
+ put("connection-rate", a.connection_rate);
6696
+ put("per-connection-classifier", a.per_connection_classifier);
6697
+ put("tcp-flags", a.tcp_flags);
6698
+ put("dscp", a.dscp);
6699
+ put("priority", a.priority);
6700
+ put("packet-size", a.packet_size);
6701
+ put("layer7-protocol", a.layer7_protocol);
6702
+ put("ipsec-policy", a.ipsec_policy);
6703
+ put("nth", a.nth);
6704
+ put("random", a.random);
6705
+ put("time", a.time);
6706
+ put("hotspot", a.hotspot);
6707
+ put("p2p", a.p2p);
6591
6708
  put("new-connection-mark", a.new_connection_mark);
6592
6709
  put("new-packet-mark", a.new_packet_mark);
6593
6710
  put("new-routing-mark", a.new_routing_mark);
@@ -6624,7 +6741,7 @@ var firewallMangleTools = [
6624
6741
  name: "create_mangle_rule",
6625
6742
  title: "Create IPv4 Firewall Mangle Rule",
6626
6743
  annotations: WRITE,
6627
- description: "Creates an IPv4 mangle rule (`/ip firewall mangle add`) \u2014 the packet-marking and header-modification table, " + "used to mark connections/packets/routing (for policy routing, QoS and per-connection classification) or to change DSCP/TTL/MSS. " + "For accept/drop decisions use create_filter_rule; for address translation use create_nat_rule; for IPv6 mangle use create_ipv6_mangle_rule. " + "chain: prerouting/input/forward/output/postrouting. " + "action: mark-connection/mark-packet/mark-routing/change-dscp/change-ttl/change-mss/add-src-to-address-list/add-dst-to-address-list/fasttrack-connection/route/set-priority/accept/etc. " + "Set the matching new-*-mark field for mark-* actions and keep passthrough=true so later rules can also match the same packet; " + "for add-*-to-address-list set address_list (and optionally address_list_timeout). " + "place_before accepts a rule number or ID (*N) to control insertion position. " + "Returns the created rule's detail including its `.id`.",
6744
+ description: "Creates an IPv4 mangle rule (`/ip firewall mangle add`) \u2014 the packet-marking and header-modification table, " + "used to mark connections/packets/routing (for policy routing, QoS and per-connection classification) or to change DSCP/TTL/MSS. " + "For accept/drop decisions use create_filter_rule; for address translation use create_nat_rule; for IPv6 mangle use create_ipv6_mangle_rule. " + "chain: prerouting/input/forward/output/postrouting. " + "action: mark-connection/mark-packet/mark-routing/change-dscp/change-ttl/change-mss/add-src-to-address-list/add-dst-to-address-list/fasttrack-connection/route/set-priority/accept/etc. " + "Set the matching new-*-mark field for mark-* actions and keep passthrough=true so later rules can also match the same packet; " + "for add-*-to-address-list set address_list (and optionally address_list_timeout). " + "Full match surface: address-lists (src_address_list/dst_address_list \u2014 negate with a leading '!', e.g. dst_address_list='!IR' to route traffic NOT bound for a list), " + "interface-lists, per_connection_classifier (PCC load-balancing), connection_nat_state, tcp_flags, dscp, layer7_protocol, time, and more. " + "Typical policy-routing rule: chain=prerouting, src_address=<lan>, dst_address_list='!IR', action=mark-routing, new_routing_mark=<table>, then a routing rule/route uses that table. " + "place_before accepts a rule number or ID (*N) to control insertion position. " + "Returns the created rule's detail including its `.id`.",
6628
6745
  inputSchema: {
6629
6746
  chain: z24.enum(["prerouting", "input", "forward", "output", "postrouting"]),
6630
6747
  action: z24.enum([
@@ -6654,12 +6771,35 @@ var firewallMangleTools = [
6654
6771
  src_port: z24.string().optional(),
6655
6772
  dst_port: z24.string().optional(),
6656
6773
  protocol: z24.string().optional(),
6657
- in_interface: z24.string().optional(),
6774
+ in_interface: z24.string().optional().describe('Negatable, e.g. "!ether1"'),
6658
6775
  out_interface: z24.string().optional(),
6776
+ in_interface_list: z24.string().optional().describe('Interface list, negatable e.g. "!WAN"'),
6777
+ out_interface_list: z24.string().optional(),
6778
+ src_address_list: z24.string().optional().describe('Match src in a named list; negate "!name"'),
6779
+ dst_address_list: z24.string().optional().describe('Match dst in a named list; negate "!name" (e.g. "!IR" routes foreign traffic)'),
6780
+ src_address_type: z24.string().optional().describe('e.g. "local", "unicast", "!local"'),
6781
+ dst_address_type: z24.string().optional(),
6659
6782
  connection_mark: z24.string().optional(),
6660
6783
  packet_mark: z24.string().optional(),
6661
6784
  routing_mark: z24.string().optional(),
6662
- connection_state: z24.string().optional().describe('e.g. "new", "established,related", "invalid"'),
6785
+ connection_state: z24.string().optional().describe('e.g. "new", "established,related", "!invalid"'),
6786
+ connection_nat_state: z24.string().optional().describe('"srcnat" / "dstnat" / "!dstnat"'),
6787
+ connection_type: z24.string().optional().describe('Helper, e.g. "sip", "ftp"'),
6788
+ connection_bytes: z24.string().optional().describe('e.g. "1000000-0" (>1 MB connections)'),
6789
+ connection_limit: z24.string().optional().describe('e.g. "100,32"'),
6790
+ connection_rate: z24.string().optional().describe('e.g. "100k-1M"'),
6791
+ per_connection_classifier: z24.string().optional().describe('PCC for load balancing, e.g. "both-addresses:2/0"'),
6792
+ tcp_flags: z24.string().optional().describe('RouterOS flag expression e.g. "syn,!ack"'),
6793
+ dscp: z24.string().optional().describe("Match incoming DSCP (0-63)"),
6794
+ priority: z24.string().optional().describe("Match packet/queue priority (0-63)"),
6795
+ packet_size: z24.string().optional().describe('e.g. "1500" or "0-500"'),
6796
+ layer7_protocol: z24.string().optional().describe("Name of an /ip firewall layer7-protocol regex"),
6797
+ ipsec_policy: z24.string().optional().describe('e.g. "in,ipsec" or "out,none"'),
6798
+ nth: z24.string().optional().describe('e.g. "2,1" \u2014 every 2nd packet'),
6799
+ random: z24.string().optional().describe("Match a random N% of packets (1-99)"),
6800
+ time: z24.string().optional().describe('e.g. "8h-16h,mon,tue,wed,thu,fri"'),
6801
+ hotspot: z24.string().optional().describe('e.g. "auth", "!auth", "from-client"'),
6802
+ p2p: z24.string().optional(),
6663
6803
  new_connection_mark: z24.string().optional(),
6664
6804
  new_packet_mark: z24.string().optional(),
6665
6805
  new_routing_mark: z24.string().optional(),
@@ -6678,7 +6818,7 @@ var firewallMangleTools = [
6678
6818
  },
6679
6819
  async handler(a, ctx) {
6680
6820
  ctx.info(`Creating mangle rule: chain=${a.chain}, action=${a.action}`);
6681
- const cmd = new Cmd("/ip firewall mangle add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("connection-mark", a.connection_mark).opt("packet-mark", a.packet_mark).opt("routing-mark", a.routing_mark).opt("connection-state", a.connection_state).opt("new-connection-mark", a.new_connection_mark).opt("new-packet-mark", a.new_packet_mark).opt("new-routing-mark", a.new_routing_mark).opt("new-dscp", a.new_dscp).opt("new-ttl", a.new_ttl).opt("new-mss", a.new_mss).opt("routing-table", a.routing_table).opt("address-list", a.address_list).opt("address-list-timeout", a.address_list_timeout).bool("passthrough", a.passthrough).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
6821
+ const cmd = new Cmd("/ip firewall mangle add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("in-interface-list", a.in_interface_list).opt("out-interface-list", a.out_interface_list).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("src-address-type", a.src_address_type).opt("dst-address-type", a.dst_address_type).opt("connection-mark", a.connection_mark).opt("packet-mark", a.packet_mark).opt("routing-mark", a.routing_mark).opt("connection-state", a.connection_state).opt("connection-nat-state", a.connection_nat_state).opt("connection-type", a.connection_type).opt("connection-bytes", a.connection_bytes).opt("connection-limit", a.connection_limit).opt("connection-rate", a.connection_rate).opt("per-connection-classifier", a.per_connection_classifier).opt("tcp-flags", a.tcp_flags).opt("dscp", a.dscp).opt("priority", a.priority).opt("packet-size", a.packet_size).opt("layer7-protocol", a.layer7_protocol).opt("ipsec-policy", a.ipsec_policy).opt("nth", a.nth).opt("random", a.random).opt("time", a.time).opt("hotspot", a.hotspot).opt("p2p", a.p2p).opt("new-connection-mark", a.new_connection_mark).opt("new-packet-mark", a.new_packet_mark).opt("new-routing-mark", a.new_routing_mark).opt("new-dscp", a.new_dscp).opt("new-ttl", a.new_ttl).opt("new-mss", a.new_mss).opt("routing-table", a.routing_table).opt("address-list", a.address_list).opt("address-list-timeout", a.address_list_timeout).bool("passthrough", a.passthrough).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
6682
6822
  const result = await executeMikrotikCommand(cmd, ctx);
6683
6823
  const trimmed = result.trim();
6684
6824
  if (looksLikeError(trimmed)) {
@@ -6772,10 +6912,33 @@ ${result}`;
6772
6912
  protocol: z24.string().optional(),
6773
6913
  in_interface: z24.string().optional(),
6774
6914
  out_interface: z24.string().optional(),
6915
+ in_interface_list: z24.string().optional(),
6916
+ out_interface_list: z24.string().optional(),
6917
+ src_address_list: z24.string().optional().describe('Negate with "!name" (e.g. "!IR")'),
6918
+ dst_address_list: z24.string().optional().describe('Negate with "!name" (e.g. "!IR")'),
6919
+ src_address_type: z24.string().optional(),
6920
+ dst_address_type: z24.string().optional(),
6775
6921
  connection_mark: z24.string().optional(),
6776
6922
  packet_mark: z24.string().optional(),
6777
6923
  routing_mark: z24.string().optional(),
6778
6924
  connection_state: z24.string().optional(),
6925
+ connection_nat_state: z24.string().optional(),
6926
+ connection_type: z24.string().optional(),
6927
+ connection_bytes: z24.string().optional(),
6928
+ connection_limit: z24.string().optional(),
6929
+ connection_rate: z24.string().optional(),
6930
+ per_connection_classifier: z24.string().optional(),
6931
+ tcp_flags: z24.string().optional(),
6932
+ dscp: z24.string().optional(),
6933
+ priority: z24.string().optional(),
6934
+ packet_size: z24.string().optional(),
6935
+ layer7_protocol: z24.string().optional(),
6936
+ ipsec_policy: z24.string().optional(),
6937
+ nth: z24.string().optional(),
6938
+ random: z24.string().optional(),
6939
+ time: z24.string().optional(),
6940
+ hotspot: z24.string().optional(),
6941
+ p2p: z24.string().optional(),
6779
6942
  new_connection_mark: z24.string().optional(),
6780
6943
  new_packet_mark: z24.string().optional(),
6781
6944
  new_routing_mark: z24.string().optional(),
@@ -11272,7 +11435,7 @@ var networkToolTools = [
11272
11435
  async handler(a, ctx) {
11273
11436
  ctx.info(`Pinging ${a.address} (count=${a.count})`);
11274
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();
11275
- const result = await executeMikrotikCommand(cmd, ctx);
11438
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.count * 1500 + 6000 }));
11276
11439
  if (looksLikeError(result))
11277
11440
  return `Failed to ping ${a.address}: ${result}`;
11278
11441
  return isEmpty(result) ? `No response from ${a.address}.` : `PING ${a.address}:
@@ -11293,7 +11456,7 @@ ${result}`;
11293
11456
  async handler(a, ctx) {
11294
11457
  ctx.info(`Tracerouting ${a.address} (count=${a.count})`);
11295
11458
  const cmd = new Cmd(`/tool traceroute ${a.address}`).set("count", a.count).flag("use-dns", a.use_dns).build();
11296
- const result = await executeMikrotikCommand(cmd, ctx);
11459
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: 30000 + a.count * 3000 }));
11297
11460
  if (looksLikeError(result))
11298
11461
  return `Failed to traceroute ${a.address}: ${result}`;
11299
11462
  return isEmpty(result) ? `No route information for ${a.address}.` : `TRACEROUTE ${a.address}:
@@ -11317,7 +11480,7 @@ ${result}`;
11317
11480
  async handler(a, ctx) {
11318
11481
  ctx.info(`Bandwidth test to ${a.address} (duration=${a.duration}s, direction=${a.direction})`);
11319
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();
11320
- const result = await executeMikrotikCommand(cmd, ctx);
11483
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.duration * 1000 + 12000 }));
11321
11484
  if (looksLikeError(result))
11322
11485
  return `Failed to run bandwidth test to ${a.address}: ${result}`;
11323
11486
  return isEmpty(result) ? `No bandwidth test results for ${a.address}.` : `BANDWIDTH TEST:
@@ -11969,7 +12132,7 @@ var floodPingTools = [
11969
12132
  async handler(a, ctx) {
11970
12133
  ctx.info(`Flood-pinging ${a.address} (count=${a.count})`);
11971
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();
11972
- const result = await executeMikrotikCommand(cmd, ctx);
12135
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: 30000 }));
11973
12136
  if (looksLikeError(result))
11974
12137
  return `Failed to flood-ping ${a.address}: ${result}`;
11975
12138
  return isEmpty(result) ? `No response from ${a.address}.` : `FLOOD PING ${a.address}:
@@ -12633,7 +12796,7 @@ var speedTestTools = [
12633
12796
  async handler(a, ctx) {
12634
12797
  ctx.info(`Speed test to ${a.address} (duration=${a.duration}s, direction=${a.direction})`);
12635
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();
12636
- const result = await executeMikrotikCommand(cmd, ctx);
12799
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.duration * 1000 + 15000 }));
12637
12800
  if (looksLikeError(result))
12638
12801
  return `Failed to run speed test to ${a.address}: ${result}`;
12639
12802
  return isEmpty(result) ? `No speed-test results for ${a.address}.` : `SPEED TEST:
@@ -23788,7 +23951,7 @@ function registerPrompts(server) {
23788
23951
  // package.json
23789
23952
  var package_default = {
23790
23953
  name: "@usex/mikrotik-mcp",
23791
- version: "3.19.0",
23954
+ version: "3.21.0",
23792
23955
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
23793
23956
  keywords: [
23794
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;
@@ -185,6 +191,14 @@ declare class SafeModeManager {
185
191
  * (safe-mode marker present/absent), not merely the first prompt-shaped line.
186
192
  */
187
193
  private readUntilPrompt;
194
+ /**
195
+ * Read until the output SETTLES: once any prompt is visible, wait for a quiet
196
+ * gap (`quietMs` with no new bytes) before resolving, or give up at `maxMs`.
197
+ * Used by commit, where Ctrl+X + Enter can emit a transient `<SAFE>` redraw
198
+ * followed by the real post-commit prompt — settling on the LAST prompt after
199
+ * a quiet period is what makes mode detection reliable.
200
+ */
201
+ private readSettledPrompt;
188
202
  private extractOutput;
189
203
  private cleanup;
190
204
  }