@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/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
  }
@@ -768,7 +787,6 @@ function connectErrorMessage(name, dc, lastError) {
768
787
 
769
788
  // src/ssh/safe-mode.ts
770
789
  var PROMPT_RE = /\[.+?@.+?\] (?:<SAFE> )?> ?$/m;
771
- var NORMAL_PROMPT_RE = /\[.+?@.+?\] > ?$/m;
772
790
  var ANSI_RE = /\x1B(?:\[[0-9;]*[mA-HJ-MSTfhilnprsu]|[()][0-9A-Za-z]|\[?\?\d+[hl])/g;
773
791
  function stripAnsi(text) {
774
792
  return text.replace(ANSI_RE, "");
@@ -779,13 +797,17 @@ function isSafeModeActivated(response) {
779
797
  return true;
780
798
  return /safe mode[^\n]*\b(?:success|taken|enabled|active)\b/i.test(response);
781
799
  }
782
- function isSafeModeReleased(response) {
800
+ function lastNonEmptyLine(response) {
783
801
  const lines = response.replace(/\r/g, "").split(`
784
802
  `).map((l) => l.trimEnd()).filter(Boolean);
785
- const last = lines.at(-1) ?? "";
786
- return NORMAL_PROMPT_RE.test(last) && !last.includes("<SAFE>");
803
+ return lines.at(-1) ?? "";
804
+ }
805
+ function classifyPrompt(response) {
806
+ const last = lastNonEmptyLine(response);
807
+ if (!PROMPT_RE.test(last))
808
+ return "unknown";
809
+ return last.includes("<SAFE>") ? "safe" : "released";
787
810
  }
788
-
789
811
  class SafeModeManager {
790
812
  deviceName;
791
813
  ssh = null;
@@ -860,13 +882,30 @@ class SafeModeManager {
860
882
  return this.lock(async () => {
861
883
  if (!this.active || !this.channel)
862
884
  return "Safe mode is not active. Nothing to commit.";
885
+ this.channel.write(`
886
+ `);
887
+ const probe = await this.readSettledPrompt();
888
+ const before = classifyPrompt(probe);
889
+ if (before === "released") {
890
+ this.cleanup();
891
+ return "Safe mode already exited \u2014 your changes are committed. Safe mode DISABLED.";
892
+ }
893
+ if (before === "unknown") {
894
+ 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)}`;
895
+ }
863
896
  this.channel.write(CTRL_X);
864
- const response = await this.readUntilPrompt(15000, (c) => isSafeModeReleased(c));
865
- if (!isSafeModeReleased(response)) {
866
- 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)}`;
897
+ this.channel.write(`
898
+ `);
899
+ const after = await this.readSettledPrompt();
900
+ switch (classifyPrompt(after)) {
901
+ case "released":
902
+ this.cleanup();
903
+ return "Changes committed successfully. Safe mode DISABLED.";
904
+ case "safe":
905
+ 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.";
906
+ default:
907
+ 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)}`;
867
908
  }
868
- this.cleanup();
869
- return "Changes committed successfully. Safe mode DISABLED.";
870
909
  });
871
910
  }
872
911
  rollback() {
@@ -902,6 +941,33 @@ class SafeModeManager {
902
941
  channel.on("data", onData);
903
942
  });
904
943
  }
944
+ readSettledPrompt(quietMs = 450, maxMs = 8000) {
945
+ const channel = this.channel;
946
+ if (!channel)
947
+ return Promise.resolve("");
948
+ return new Promise((resolve2) => {
949
+ let buf = "";
950
+ let quiet;
951
+ let hard;
952
+ function done() {
953
+ clearTimeout(hard);
954
+ if (quiet)
955
+ clearTimeout(quiet);
956
+ channel.removeListener("data", onData);
957
+ resolve2(stripAnsi(buf));
958
+ }
959
+ function onData(chunk) {
960
+ buf += decodeOutput(chunk);
961
+ if (PROMPT_RE.test(lastNonEmptyLine(stripAnsi(buf)))) {
962
+ if (quiet)
963
+ clearTimeout(quiet);
964
+ quiet = setTimeout(done, quietMs);
965
+ }
966
+ }
967
+ hard = setTimeout(done, maxMs);
968
+ channel.on("data", onData);
969
+ });
970
+ }
905
971
  extractOutput(raw, command) {
906
972
  const text = raw.replace(/\r\n/g, `
907
973
  `).replace(/\r/g, `
@@ -949,7 +1015,7 @@ function getSafeModeManager(deviceName) {
949
1015
  }
950
1016
 
951
1017
  // src/core/connector.ts
952
- async function runOnce(command, deviceName) {
1018
+ async function runOnce(command, deviceName, opts) {
953
1019
  const name = resolveDeviceName(deviceName);
954
1020
  const dc = getDevice(deviceName);
955
1021
  const client = createDeviceClient(dc);
@@ -957,12 +1023,12 @@ async function runOnce(command, deviceName) {
957
1023
  if (!await client.connect()) {
958
1024
  throw new Error(connectErrorMessage(name, dc, client.lastError));
959
1025
  }
960
- return await client.run(command);
1026
+ return await client.run(command, opts);
961
1027
  } finally {
962
1028
  client.disconnect();
963
1029
  }
964
1030
  }
965
- async function executeMikrotikCommand(command, ctx) {
1031
+ async function executeMikrotikCommand(command, ctx, opts) {
966
1032
  const deviceName = resolveDeviceName(ctx.device);
967
1033
  const safe = getSafeModeManager(deviceName);
968
1034
  if (safe.isActive) {
@@ -970,7 +1036,7 @@ async function executeMikrotikCommand(command, ctx) {
970
1036
  return safe.execute(command);
971
1037
  }
972
1038
  ctx.info(`[${deviceName}] Executing MikroTik command: ${command}`);
973
- return runOnce(command, ctx.device);
1039
+ return runOnce(command, ctx.device, opts);
974
1040
  }
975
1041
  // src/core/registry.ts
976
1042
  import { z as z2 } from "zod";
@@ -991,7 +1057,7 @@ function createContext(sendLog, device) {
991
1057
  }
992
1058
 
993
1059
  // src/core/routeros.ts
994
- var BARE_SAFE = /^[\w.\-:/,*@]+$/;
1060
+ var BARE_SAFE = /^[\w.\-:/,*@!]+$/;
995
1061
  function quoteValue(value) {
996
1062
  if (typeof value === "number" || typeof value === "boolean")
997
1063
  return String(value);
@@ -1062,6 +1128,16 @@ function isEmpty(result) {
1062
1128
  const t = result.trim();
1063
1129
  return t === "" || t === "no such item" || t === "no such item (4)";
1064
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
+ }
1065
1141
  function extractCreatedId(output) {
1066
1142
  const star = output.match(/\*[0-9A-Fa-f]+/);
1067
1143
  if (star)
@@ -6329,6 +6405,12 @@ async function updateNatRule(a, ctx) {
6329
6405
  put("protocol", a.protocol);
6330
6406
  put("in-interface", a.in_interface);
6331
6407
  put("out-interface", a.out_interface);
6408
+ put("in-interface-list", a.in_interface_list);
6409
+ put("out-interface-list", a.out_interface_list);
6410
+ put("src-address-list", a.src_address_list);
6411
+ put("dst-address-list", a.dst_address_list);
6412
+ put("connection-mark", a.connection_mark);
6413
+ put("connection-nat-state", a.connection_nat_state);
6332
6414
  put("to-addresses", a.to_addresses);
6333
6415
  put("to-ports", a.to_ports);
6334
6416
  if (a.comment !== undefined)
@@ -6365,8 +6447,14 @@ var firewallNatTools = [
6365
6447
  src_port: z24.string().optional(),
6366
6448
  dst_port: z24.string().optional(),
6367
6449
  protocol: z24.string().optional(),
6368
- in_interface: z24.string().optional(),
6450
+ in_interface: z24.string().optional().describe('Negatable, e.g. "!ether1"'),
6369
6451
  out_interface: z24.string().optional(),
6452
+ in_interface_list: z24.string().optional().describe('Interface list, e.g. "WAN" or "!LAN"'),
6453
+ out_interface_list: z24.string().optional(),
6454
+ src_address_list: z24.string().optional().describe('Match src in a named list; negate "!name"'),
6455
+ dst_address_list: z24.string().optional().describe('Match dst in a named list; negate "!name"'),
6456
+ connection_mark: z24.string().optional(),
6457
+ connection_nat_state: z24.string().optional().describe('"srcnat" / "dstnat" / "!dstnat"'),
6370
6458
  to_addresses: z24.string().optional().describe('Single IP or range e.g. "10.0.0.1" or "10.0.0.1-10.0.0.10"'),
6371
6459
  to_ports: z24.string().optional().describe('Single port or range e.g. "8080" or "8080-8090"'),
6372
6460
  comment: z24.string().optional(),
@@ -6406,7 +6494,7 @@ var firewallNatTools = [
6406
6494
  } else if (a.chain === "dstnat" && !dstnatActions.includes(a.action)) {
6407
6495
  return `Error: Invalid action '${a.action}' for dstnat. Must be one of: ${dstnatActions.join(", ")}`;
6408
6496
  }
6409
- 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();
6497
+ 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();
6410
6498
  const result = await executeMikrotikCommand(cmd, ctx);
6411
6499
  const trimmed = result.trim();
6412
6500
  if (looksLikeError(trimmed)) {
@@ -6504,6 +6592,12 @@ ${result}`;
6504
6592
  protocol: z24.string().optional(),
6505
6593
  in_interface: z24.string().optional(),
6506
6594
  out_interface: z24.string().optional(),
6595
+ in_interface_list: z24.string().optional(),
6596
+ out_interface_list: z24.string().optional(),
6597
+ src_address_list: z24.string().optional().describe('Negate with "!name"'),
6598
+ dst_address_list: z24.string().optional().describe('Negate with "!name"'),
6599
+ connection_mark: z24.string().optional(),
6600
+ connection_nat_state: z24.string().optional(),
6507
6601
  to_addresses: z24.string().optional(),
6508
6602
  to_ports: z24.string().optional(),
6509
6603
  comment: z24.string().optional(),
@@ -6596,10 +6690,33 @@ async function updateMangleRule(a, ctx) {
6596
6690
  put("protocol", a.protocol);
6597
6691
  put("in-interface", a.in_interface);
6598
6692
  put("out-interface", a.out_interface);
6693
+ put("in-interface-list", a.in_interface_list);
6694
+ put("out-interface-list", a.out_interface_list);
6695
+ put("src-address-list", a.src_address_list);
6696
+ put("dst-address-list", a.dst_address_list);
6697
+ put("src-address-type", a.src_address_type);
6698
+ put("dst-address-type", a.dst_address_type);
6599
6699
  put("connection-mark", a.connection_mark);
6600
6700
  put("packet-mark", a.packet_mark);
6601
6701
  put("routing-mark", a.routing_mark);
6602
6702
  put("connection-state", a.connection_state);
6703
+ put("connection-nat-state", a.connection_nat_state);
6704
+ put("connection-type", a.connection_type);
6705
+ put("connection-bytes", a.connection_bytes);
6706
+ put("connection-limit", a.connection_limit);
6707
+ put("connection-rate", a.connection_rate);
6708
+ put("per-connection-classifier", a.per_connection_classifier);
6709
+ put("tcp-flags", a.tcp_flags);
6710
+ put("dscp", a.dscp);
6711
+ put("priority", a.priority);
6712
+ put("packet-size", a.packet_size);
6713
+ put("layer7-protocol", a.layer7_protocol);
6714
+ put("ipsec-policy", a.ipsec_policy);
6715
+ put("nth", a.nth);
6716
+ put("random", a.random);
6717
+ put("time", a.time);
6718
+ put("hotspot", a.hotspot);
6719
+ put("p2p", a.p2p);
6603
6720
  put("new-connection-mark", a.new_connection_mark);
6604
6721
  put("new-packet-mark", a.new_packet_mark);
6605
6722
  put("new-routing-mark", a.new_routing_mark);
@@ -6636,7 +6753,7 @@ var firewallMangleTools = [
6636
6753
  name: "create_mangle_rule",
6637
6754
  title: "Create IPv4 Firewall Mangle Rule",
6638
6755
  annotations: WRITE,
6639
- 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`.",
6756
+ 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`.",
6640
6757
  inputSchema: {
6641
6758
  chain: z25.enum(["prerouting", "input", "forward", "output", "postrouting"]),
6642
6759
  action: z25.enum([
@@ -6666,12 +6783,35 @@ var firewallMangleTools = [
6666
6783
  src_port: z25.string().optional(),
6667
6784
  dst_port: z25.string().optional(),
6668
6785
  protocol: z25.string().optional(),
6669
- in_interface: z25.string().optional(),
6786
+ in_interface: z25.string().optional().describe('Negatable, e.g. "!ether1"'),
6670
6787
  out_interface: z25.string().optional(),
6788
+ in_interface_list: z25.string().optional().describe('Interface list, negatable e.g. "!WAN"'),
6789
+ out_interface_list: z25.string().optional(),
6790
+ src_address_list: z25.string().optional().describe('Match src in a named list; negate "!name"'),
6791
+ dst_address_list: z25.string().optional().describe('Match dst in a named list; negate "!name" (e.g. "!IR" routes foreign traffic)'),
6792
+ src_address_type: z25.string().optional().describe('e.g. "local", "unicast", "!local"'),
6793
+ dst_address_type: z25.string().optional(),
6671
6794
  connection_mark: z25.string().optional(),
6672
6795
  packet_mark: z25.string().optional(),
6673
6796
  routing_mark: z25.string().optional(),
6674
- connection_state: z25.string().optional().describe('e.g. "new", "established,related", "invalid"'),
6797
+ connection_state: z25.string().optional().describe('e.g. "new", "established,related", "!invalid"'),
6798
+ connection_nat_state: z25.string().optional().describe('"srcnat" / "dstnat" / "!dstnat"'),
6799
+ connection_type: z25.string().optional().describe('Helper, e.g. "sip", "ftp"'),
6800
+ connection_bytes: z25.string().optional().describe('e.g. "1000000-0" (>1 MB connections)'),
6801
+ connection_limit: z25.string().optional().describe('e.g. "100,32"'),
6802
+ connection_rate: z25.string().optional().describe('e.g. "100k-1M"'),
6803
+ per_connection_classifier: z25.string().optional().describe('PCC for load balancing, e.g. "both-addresses:2/0"'),
6804
+ tcp_flags: z25.string().optional().describe('RouterOS flag expression e.g. "syn,!ack"'),
6805
+ dscp: z25.string().optional().describe("Match incoming DSCP (0-63)"),
6806
+ priority: z25.string().optional().describe("Match packet/queue priority (0-63)"),
6807
+ packet_size: z25.string().optional().describe('e.g. "1500" or "0-500"'),
6808
+ layer7_protocol: z25.string().optional().describe("Name of an /ip firewall layer7-protocol regex"),
6809
+ ipsec_policy: z25.string().optional().describe('e.g. "in,ipsec" or "out,none"'),
6810
+ nth: z25.string().optional().describe('e.g. "2,1" \u2014 every 2nd packet'),
6811
+ random: z25.string().optional().describe("Match a random N% of packets (1-99)"),
6812
+ time: z25.string().optional().describe('e.g. "8h-16h,mon,tue,wed,thu,fri"'),
6813
+ hotspot: z25.string().optional().describe('e.g. "auth", "!auth", "from-client"'),
6814
+ p2p: z25.string().optional(),
6675
6815
  new_connection_mark: z25.string().optional(),
6676
6816
  new_packet_mark: z25.string().optional(),
6677
6817
  new_routing_mark: z25.string().optional(),
@@ -6690,7 +6830,7 @@ var firewallMangleTools = [
6690
6830
  },
6691
6831
  async handler(a, ctx) {
6692
6832
  ctx.info(`Creating mangle rule: chain=${a.chain}, action=${a.action}`);
6693
- 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();
6833
+ 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();
6694
6834
  const result = await executeMikrotikCommand(cmd, ctx);
6695
6835
  const trimmed = result.trim();
6696
6836
  if (looksLikeError(trimmed)) {
@@ -6784,10 +6924,33 @@ ${result}`;
6784
6924
  protocol: z25.string().optional(),
6785
6925
  in_interface: z25.string().optional(),
6786
6926
  out_interface: z25.string().optional(),
6927
+ in_interface_list: z25.string().optional(),
6928
+ out_interface_list: z25.string().optional(),
6929
+ src_address_list: z25.string().optional().describe('Negate with "!name" (e.g. "!IR")'),
6930
+ dst_address_list: z25.string().optional().describe('Negate with "!name" (e.g. "!IR")'),
6931
+ src_address_type: z25.string().optional(),
6932
+ dst_address_type: z25.string().optional(),
6787
6933
  connection_mark: z25.string().optional(),
6788
6934
  packet_mark: z25.string().optional(),
6789
6935
  routing_mark: z25.string().optional(),
6790
6936
  connection_state: z25.string().optional(),
6937
+ connection_nat_state: z25.string().optional(),
6938
+ connection_type: z25.string().optional(),
6939
+ connection_bytes: z25.string().optional(),
6940
+ connection_limit: z25.string().optional(),
6941
+ connection_rate: z25.string().optional(),
6942
+ per_connection_classifier: z25.string().optional(),
6943
+ tcp_flags: z25.string().optional(),
6944
+ dscp: z25.string().optional(),
6945
+ priority: z25.string().optional(),
6946
+ packet_size: z25.string().optional(),
6947
+ layer7_protocol: z25.string().optional(),
6948
+ ipsec_policy: z25.string().optional(),
6949
+ nth: z25.string().optional(),
6950
+ random: z25.string().optional(),
6951
+ time: z25.string().optional(),
6952
+ hotspot: z25.string().optional(),
6953
+ p2p: z25.string().optional(),
6791
6954
  new_connection_mark: z25.string().optional(),
6792
6955
  new_packet_mark: z25.string().optional(),
6793
6956
  new_routing_mark: z25.string().optional(),
@@ -11284,7 +11447,7 @@ var networkToolTools = [
11284
11447
  async handler(a, ctx) {
11285
11448
  ctx.info(`Pinging ${a.address} (count=${a.count})`);
11286
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();
11287
- const result = await executeMikrotikCommand(cmd, ctx);
11450
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.count * 1500 + 6000 }));
11288
11451
  if (looksLikeError(result))
11289
11452
  return `Failed to ping ${a.address}: ${result}`;
11290
11453
  return isEmpty(result) ? `No response from ${a.address}.` : `PING ${a.address}:
@@ -11305,7 +11468,7 @@ ${result}`;
11305
11468
  async handler(a, ctx) {
11306
11469
  ctx.info(`Tracerouting ${a.address} (count=${a.count})`);
11307
11470
  const cmd = new Cmd(`/tool traceroute ${a.address}`).set("count", a.count).flag("use-dns", a.use_dns).build();
11308
- const result = await executeMikrotikCommand(cmd, ctx);
11471
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: 30000 + a.count * 3000 }));
11309
11472
  if (looksLikeError(result))
11310
11473
  return `Failed to traceroute ${a.address}: ${result}`;
11311
11474
  return isEmpty(result) ? `No route information for ${a.address}.` : `TRACEROUTE ${a.address}:
@@ -11329,7 +11492,7 @@ ${result}`;
11329
11492
  async handler(a, ctx) {
11330
11493
  ctx.info(`Bandwidth test to ${a.address} (duration=${a.duration}s, direction=${a.direction})`);
11331
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();
11332
- const result = await executeMikrotikCommand(cmd, ctx);
11495
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.duration * 1000 + 12000 }));
11333
11496
  if (looksLikeError(result))
11334
11497
  return `Failed to run bandwidth test to ${a.address}: ${result}`;
11335
11498
  return isEmpty(result) ? `No bandwidth test results for ${a.address}.` : `BANDWIDTH TEST:
@@ -11981,7 +12144,7 @@ var floodPingTools = [
11981
12144
  async handler(a, ctx) {
11982
12145
  ctx.info(`Flood-pinging ${a.address} (count=${a.count})`);
11983
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();
11984
- const result = await executeMikrotikCommand(cmd, ctx);
12147
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: 30000 }));
11985
12148
  if (looksLikeError(result))
11986
12149
  return `Failed to flood-ping ${a.address}: ${result}`;
11987
12150
  return isEmpty(result) ? `No response from ${a.address}.` : `FLOOD PING ${a.address}:
@@ -12645,7 +12808,7 @@ var speedTestTools = [
12645
12808
  async handler(a, ctx) {
12646
12809
  ctx.info(`Speed test to ${a.address} (duration=${a.duration}s, direction=${a.direction})`);
12647
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();
12648
- const result = await executeMikrotikCommand(cmd, ctx);
12811
+ const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.duration * 1000 + 15000 }));
12649
12812
  if (looksLikeError(result))
12650
12813
  return `Failed to run speed test to ${a.address}: ${result}`;
12651
12814
  return isEmpty(result) ? `No speed-test results for ${a.address}.` : `SPEED TEST:
@@ -22188,7 +22351,7 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
22188
22351
  // package.json
22189
22352
  var package_default = {
22190
22353
  name: "@usex/mikrotik-mcp",
22191
- version: "3.19.0",
22354
+ version: "3.21.0",
22192
22355
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
22193
22356
  keywords: [
22194
22357
  "ai",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usex/mikrotik-mcp",
3
- "version": "3.19.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",