@usex/mikrotik-mcp 3.19.0 → 3.20.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
@@ -782,7 +782,6 @@ import { z as z3 } from "zod";
782
782
 
783
783
  // src/ssh/safe-mode.ts
784
784
  var PROMPT_RE = /\[.+?@.+?\] (?:<SAFE> )?> ?$/m;
785
- var NORMAL_PROMPT_RE = /\[.+?@.+?\] > ?$/m;
786
785
  var ANSI_RE = /\x1B(?:\[[0-9;]*[mA-HJ-MSTfhilnprsu]|[()][0-9A-Za-z]|\[?\?\d+[hl])/g;
787
786
  function stripAnsi(text) {
788
787
  return text.replace(ANSI_RE, "");
@@ -793,13 +792,17 @@ function isSafeModeActivated(response) {
793
792
  return true;
794
793
  return /safe mode[^\n]*\b(?:success|taken|enabled|active)\b/i.test(response);
795
794
  }
796
- function isSafeModeReleased(response) {
795
+ function lastNonEmptyLine(response) {
797
796
  const lines = response.replace(/\r/g, "").split(`
798
797
  `).map((l) => l.trimEnd()).filter(Boolean);
799
- const last = lines.at(-1) ?? "";
800
- return NORMAL_PROMPT_RE.test(last) && !last.includes("<SAFE>");
798
+ return lines.at(-1) ?? "";
799
+ }
800
+ function classifyPrompt(response) {
801
+ const last = lastNonEmptyLine(response);
802
+ if (!PROMPT_RE.test(last))
803
+ return "unknown";
804
+ return last.includes("<SAFE>") ? "safe" : "released";
801
805
  }
802
-
803
806
  class SafeModeManager {
804
807
  deviceName;
805
808
  ssh = null;
@@ -874,13 +877,30 @@ class SafeModeManager {
874
877
  return this.lock(async () => {
875
878
  if (!this.active || !this.channel)
876
879
  return "Safe mode is not active. Nothing to commit.";
880
+ this.channel.write(`
881
+ `);
882
+ const probe = await this.readSettledPrompt();
883
+ const before = classifyPrompt(probe);
884
+ if (before === "released") {
885
+ this.cleanup();
886
+ return "Safe mode already exited \u2014 your changes are committed. Safe mode DISABLED.";
887
+ }
888
+ if (before === "unknown") {
889
+ 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)}`;
890
+ }
877
891
  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)}`;
892
+ this.channel.write(`
893
+ `);
894
+ const after = await this.readSettledPrompt();
895
+ switch (classifyPrompt(after)) {
896
+ case "released":
897
+ this.cleanup();
898
+ return "Changes committed successfully. Safe mode DISABLED.";
899
+ case "safe":
900
+ 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.";
901
+ default:
902
+ 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
903
  }
882
- this.cleanup();
883
- return "Changes committed successfully. Safe mode DISABLED.";
884
904
  });
885
905
  }
886
906
  rollback() {
@@ -916,6 +936,33 @@ class SafeModeManager {
916
936
  channel.on("data", onData);
917
937
  });
918
938
  }
939
+ readSettledPrompt(quietMs = 450, maxMs = 8000) {
940
+ const channel = this.channel;
941
+ if (!channel)
942
+ return Promise.resolve("");
943
+ return new Promise((resolve2) => {
944
+ let buf = "";
945
+ let quiet;
946
+ let hard;
947
+ function done() {
948
+ clearTimeout(hard);
949
+ if (quiet)
950
+ clearTimeout(quiet);
951
+ channel.removeListener("data", onData);
952
+ resolve2(stripAnsi(buf));
953
+ }
954
+ function onData(chunk) {
955
+ buf += decodeOutput(chunk);
956
+ if (PROMPT_RE.test(lastNonEmptyLine(stripAnsi(buf)))) {
957
+ if (quiet)
958
+ clearTimeout(quiet);
959
+ quiet = setTimeout(done, quietMs);
960
+ }
961
+ }
962
+ hard = setTimeout(done, maxMs);
963
+ channel.on("data", onData);
964
+ });
965
+ }
919
966
  extractOutput(raw, command) {
920
967
  const text = raw.replace(/\r\n/g, `
921
968
  `).replace(/\r/g, `
@@ -1006,7 +1053,7 @@ function createContext(sendLog, device) {
1006
1053
  }
1007
1054
 
1008
1055
  // src/core/routeros.ts
1009
- var BARE_SAFE = /^[\w.\-:/,*@]+$/;
1056
+ var BARE_SAFE = /^[\w.\-:/,*@!]+$/;
1010
1057
  function quoteValue(value) {
1011
1058
  if (typeof value === "number" || typeof value === "boolean")
1012
1059
  return String(value);
@@ -6317,6 +6364,12 @@ async function updateNatRule(a, ctx) {
6317
6364
  put("protocol", a.protocol);
6318
6365
  put("in-interface", a.in_interface);
6319
6366
  put("out-interface", a.out_interface);
6367
+ put("in-interface-list", a.in_interface_list);
6368
+ put("out-interface-list", a.out_interface_list);
6369
+ put("src-address-list", a.src_address_list);
6370
+ put("dst-address-list", a.dst_address_list);
6371
+ put("connection-mark", a.connection_mark);
6372
+ put("connection-nat-state", a.connection_nat_state);
6320
6373
  put("to-addresses", a.to_addresses);
6321
6374
  put("to-ports", a.to_ports);
6322
6375
  if (a.comment !== undefined)
@@ -6353,8 +6406,14 @@ var firewallNatTools = [
6353
6406
  src_port: z23.string().optional(),
6354
6407
  dst_port: z23.string().optional(),
6355
6408
  protocol: z23.string().optional(),
6356
- in_interface: z23.string().optional(),
6409
+ in_interface: z23.string().optional().describe('Negatable, e.g. "!ether1"'),
6357
6410
  out_interface: z23.string().optional(),
6411
+ in_interface_list: z23.string().optional().describe('Interface list, e.g. "WAN" or "!LAN"'),
6412
+ out_interface_list: z23.string().optional(),
6413
+ src_address_list: z23.string().optional().describe('Match src in a named list; negate "!name"'),
6414
+ dst_address_list: z23.string().optional().describe('Match dst in a named list; negate "!name"'),
6415
+ connection_mark: z23.string().optional(),
6416
+ connection_nat_state: z23.string().optional().describe('"srcnat" / "dstnat" / "!dstnat"'),
6358
6417
  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
6418
  to_ports: z23.string().optional().describe('Single port or range e.g. "8080" or "8080-8090"'),
6360
6419
  comment: z23.string().optional(),
@@ -6394,7 +6453,7 @@ var firewallNatTools = [
6394
6453
  } else if (a.chain === "dstnat" && !dstnatActions.includes(a.action)) {
6395
6454
  return `Error: Invalid action '${a.action}' for dstnat. Must be one of: ${dstnatActions.join(", ")}`;
6396
6455
  }
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();
6456
+ 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
6457
  const result = await executeMikrotikCommand(cmd, ctx);
6399
6458
  const trimmed = result.trim();
6400
6459
  if (looksLikeError(trimmed)) {
@@ -6492,6 +6551,12 @@ ${result}`;
6492
6551
  protocol: z23.string().optional(),
6493
6552
  in_interface: z23.string().optional(),
6494
6553
  out_interface: z23.string().optional(),
6554
+ in_interface_list: z23.string().optional(),
6555
+ out_interface_list: z23.string().optional(),
6556
+ src_address_list: z23.string().optional().describe('Negate with "!name"'),
6557
+ dst_address_list: z23.string().optional().describe('Negate with "!name"'),
6558
+ connection_mark: z23.string().optional(),
6559
+ connection_nat_state: z23.string().optional(),
6495
6560
  to_addresses: z23.string().optional(),
6496
6561
  to_ports: z23.string().optional(),
6497
6562
  comment: z23.string().optional(),
@@ -6584,10 +6649,33 @@ async function updateMangleRule(a, ctx) {
6584
6649
  put("protocol", a.protocol);
6585
6650
  put("in-interface", a.in_interface);
6586
6651
  put("out-interface", a.out_interface);
6652
+ put("in-interface-list", a.in_interface_list);
6653
+ put("out-interface-list", a.out_interface_list);
6654
+ put("src-address-list", a.src_address_list);
6655
+ put("dst-address-list", a.dst_address_list);
6656
+ put("src-address-type", a.src_address_type);
6657
+ put("dst-address-type", a.dst_address_type);
6587
6658
  put("connection-mark", a.connection_mark);
6588
6659
  put("packet-mark", a.packet_mark);
6589
6660
  put("routing-mark", a.routing_mark);
6590
6661
  put("connection-state", a.connection_state);
6662
+ put("connection-nat-state", a.connection_nat_state);
6663
+ put("connection-type", a.connection_type);
6664
+ put("connection-bytes", a.connection_bytes);
6665
+ put("connection-limit", a.connection_limit);
6666
+ put("connection-rate", a.connection_rate);
6667
+ put("per-connection-classifier", a.per_connection_classifier);
6668
+ put("tcp-flags", a.tcp_flags);
6669
+ put("dscp", a.dscp);
6670
+ put("priority", a.priority);
6671
+ put("packet-size", a.packet_size);
6672
+ put("layer7-protocol", a.layer7_protocol);
6673
+ put("ipsec-policy", a.ipsec_policy);
6674
+ put("nth", a.nth);
6675
+ put("random", a.random);
6676
+ put("time", a.time);
6677
+ put("hotspot", a.hotspot);
6678
+ put("p2p", a.p2p);
6591
6679
  put("new-connection-mark", a.new_connection_mark);
6592
6680
  put("new-packet-mark", a.new_packet_mark);
6593
6681
  put("new-routing-mark", a.new_routing_mark);
@@ -6624,7 +6712,7 @@ var firewallMangleTools = [
6624
6712
  name: "create_mangle_rule",
6625
6713
  title: "Create IPv4 Firewall Mangle Rule",
6626
6714
  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`.",
6715
+ 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
6716
  inputSchema: {
6629
6717
  chain: z24.enum(["prerouting", "input", "forward", "output", "postrouting"]),
6630
6718
  action: z24.enum([
@@ -6654,12 +6742,35 @@ var firewallMangleTools = [
6654
6742
  src_port: z24.string().optional(),
6655
6743
  dst_port: z24.string().optional(),
6656
6744
  protocol: z24.string().optional(),
6657
- in_interface: z24.string().optional(),
6745
+ in_interface: z24.string().optional().describe('Negatable, e.g. "!ether1"'),
6658
6746
  out_interface: z24.string().optional(),
6747
+ in_interface_list: z24.string().optional().describe('Interface list, negatable e.g. "!WAN"'),
6748
+ out_interface_list: z24.string().optional(),
6749
+ src_address_list: z24.string().optional().describe('Match src in a named list; negate "!name"'),
6750
+ dst_address_list: z24.string().optional().describe('Match dst in a named list; negate "!name" (e.g. "!IR" routes foreign traffic)'),
6751
+ src_address_type: z24.string().optional().describe('e.g. "local", "unicast", "!local"'),
6752
+ dst_address_type: z24.string().optional(),
6659
6753
  connection_mark: z24.string().optional(),
6660
6754
  packet_mark: z24.string().optional(),
6661
6755
  routing_mark: z24.string().optional(),
6662
- connection_state: z24.string().optional().describe('e.g. "new", "established,related", "invalid"'),
6756
+ connection_state: z24.string().optional().describe('e.g. "new", "established,related", "!invalid"'),
6757
+ connection_nat_state: z24.string().optional().describe('"srcnat" / "dstnat" / "!dstnat"'),
6758
+ connection_type: z24.string().optional().describe('Helper, e.g. "sip", "ftp"'),
6759
+ connection_bytes: z24.string().optional().describe('e.g. "1000000-0" (>1 MB connections)'),
6760
+ connection_limit: z24.string().optional().describe('e.g. "100,32"'),
6761
+ connection_rate: z24.string().optional().describe('e.g. "100k-1M"'),
6762
+ per_connection_classifier: z24.string().optional().describe('PCC for load balancing, e.g. "both-addresses:2/0"'),
6763
+ tcp_flags: z24.string().optional().describe('RouterOS flag expression e.g. "syn,!ack"'),
6764
+ dscp: z24.string().optional().describe("Match incoming DSCP (0-63)"),
6765
+ priority: z24.string().optional().describe("Match packet/queue priority (0-63)"),
6766
+ packet_size: z24.string().optional().describe('e.g. "1500" or "0-500"'),
6767
+ layer7_protocol: z24.string().optional().describe("Name of an /ip firewall layer7-protocol regex"),
6768
+ ipsec_policy: z24.string().optional().describe('e.g. "in,ipsec" or "out,none"'),
6769
+ nth: z24.string().optional().describe('e.g. "2,1" \u2014 every 2nd packet'),
6770
+ random: z24.string().optional().describe("Match a random N% of packets (1-99)"),
6771
+ time: z24.string().optional().describe('e.g. "8h-16h,mon,tue,wed,thu,fri"'),
6772
+ hotspot: z24.string().optional().describe('e.g. "auth", "!auth", "from-client"'),
6773
+ p2p: z24.string().optional(),
6663
6774
  new_connection_mark: z24.string().optional(),
6664
6775
  new_packet_mark: z24.string().optional(),
6665
6776
  new_routing_mark: z24.string().optional(),
@@ -6678,7 +6789,7 @@ var firewallMangleTools = [
6678
6789
  },
6679
6790
  async handler(a, ctx) {
6680
6791
  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();
6792
+ 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
6793
  const result = await executeMikrotikCommand(cmd, ctx);
6683
6794
  const trimmed = result.trim();
6684
6795
  if (looksLikeError(trimmed)) {
@@ -6772,10 +6883,33 @@ ${result}`;
6772
6883
  protocol: z24.string().optional(),
6773
6884
  in_interface: z24.string().optional(),
6774
6885
  out_interface: z24.string().optional(),
6886
+ in_interface_list: z24.string().optional(),
6887
+ out_interface_list: z24.string().optional(),
6888
+ src_address_list: z24.string().optional().describe('Negate with "!name" (e.g. "!IR")'),
6889
+ dst_address_list: z24.string().optional().describe('Negate with "!name" (e.g. "!IR")'),
6890
+ src_address_type: z24.string().optional(),
6891
+ dst_address_type: z24.string().optional(),
6775
6892
  connection_mark: z24.string().optional(),
6776
6893
  packet_mark: z24.string().optional(),
6777
6894
  routing_mark: z24.string().optional(),
6778
6895
  connection_state: z24.string().optional(),
6896
+ connection_nat_state: z24.string().optional(),
6897
+ connection_type: z24.string().optional(),
6898
+ connection_bytes: z24.string().optional(),
6899
+ connection_limit: z24.string().optional(),
6900
+ connection_rate: z24.string().optional(),
6901
+ per_connection_classifier: z24.string().optional(),
6902
+ tcp_flags: z24.string().optional(),
6903
+ dscp: z24.string().optional(),
6904
+ priority: z24.string().optional(),
6905
+ packet_size: z24.string().optional(),
6906
+ layer7_protocol: z24.string().optional(),
6907
+ ipsec_policy: z24.string().optional(),
6908
+ nth: z24.string().optional(),
6909
+ random: z24.string().optional(),
6910
+ time: z24.string().optional(),
6911
+ hotspot: z24.string().optional(),
6912
+ p2p: z24.string().optional(),
6779
6913
  new_connection_mark: z24.string().optional(),
6780
6914
  new_packet_mark: z24.string().optional(),
6781
6915
  new_routing_mark: z24.string().optional(),
@@ -23788,7 +23922,7 @@ function registerPrompts(server) {
23788
23922
  // package.json
23789
23923
  var package_default = {
23790
23924
  name: "@usex/mikrotik-mcp",
23791
- version: "3.19.0",
23925
+ version: "3.20.0",
23792
23926
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
23793
23927
  keywords: [
23794
23928
  "ai",
package/dist/index.d.ts CHANGED
@@ -185,6 +185,14 @@ declare class SafeModeManager {
185
185
  * (safe-mode marker present/absent), not merely the first prompt-shaped line.
186
186
  */
187
187
  private readUntilPrompt;
188
+ /**
189
+ * Read until the output SETTLES: once any prompt is visible, wait for a quiet
190
+ * gap (`quietMs` with no new bytes) before resolving, or give up at `maxMs`.
191
+ * Used by commit, where Ctrl+X + Enter can emit a transient `<SAFE>` redraw
192
+ * followed by the real post-commit prompt — settling on the LAST prompt after
193
+ * a quiet period is what makes mode detection reliable.
194
+ */
195
+ private readSettledPrompt;
188
196
  private extractOutput;
189
197
  private cleanup;
190
198
  }
package/dist/index.js CHANGED
@@ -768,7 +768,6 @@ function connectErrorMessage(name, dc, lastError) {
768
768
 
769
769
  // src/ssh/safe-mode.ts
770
770
  var PROMPT_RE = /\[.+?@.+?\] (?:<SAFE> )?> ?$/m;
771
- var NORMAL_PROMPT_RE = /\[.+?@.+?\] > ?$/m;
772
771
  var ANSI_RE = /\x1B(?:\[[0-9;]*[mA-HJ-MSTfhilnprsu]|[()][0-9A-Za-z]|\[?\?\d+[hl])/g;
773
772
  function stripAnsi(text) {
774
773
  return text.replace(ANSI_RE, "");
@@ -779,13 +778,17 @@ function isSafeModeActivated(response) {
779
778
  return true;
780
779
  return /safe mode[^\n]*\b(?:success|taken|enabled|active)\b/i.test(response);
781
780
  }
782
- function isSafeModeReleased(response) {
781
+ function lastNonEmptyLine(response) {
783
782
  const lines = response.replace(/\r/g, "").split(`
784
783
  `).map((l) => l.trimEnd()).filter(Boolean);
785
- const last = lines.at(-1) ?? "";
786
- return NORMAL_PROMPT_RE.test(last) && !last.includes("<SAFE>");
784
+ return lines.at(-1) ?? "";
785
+ }
786
+ function classifyPrompt(response) {
787
+ const last = lastNonEmptyLine(response);
788
+ if (!PROMPT_RE.test(last))
789
+ return "unknown";
790
+ return last.includes("<SAFE>") ? "safe" : "released";
787
791
  }
788
-
789
792
  class SafeModeManager {
790
793
  deviceName;
791
794
  ssh = null;
@@ -860,13 +863,30 @@ class SafeModeManager {
860
863
  return this.lock(async () => {
861
864
  if (!this.active || !this.channel)
862
865
  return "Safe mode is not active. Nothing to commit.";
866
+ this.channel.write(`
867
+ `);
868
+ const probe = await this.readSettledPrompt();
869
+ const before = classifyPrompt(probe);
870
+ if (before === "released") {
871
+ this.cleanup();
872
+ return "Safe mode already exited \u2014 your changes are committed. Safe mode DISABLED.";
873
+ }
874
+ if (before === "unknown") {
875
+ 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)}`;
876
+ }
863
877
  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)}`;
878
+ this.channel.write(`
879
+ `);
880
+ const after = await this.readSettledPrompt();
881
+ switch (classifyPrompt(after)) {
882
+ case "released":
883
+ this.cleanup();
884
+ return "Changes committed successfully. Safe mode DISABLED.";
885
+ case "safe":
886
+ 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.";
887
+ default:
888
+ 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
889
  }
868
- this.cleanup();
869
- return "Changes committed successfully. Safe mode DISABLED.";
870
890
  });
871
891
  }
872
892
  rollback() {
@@ -902,6 +922,33 @@ class SafeModeManager {
902
922
  channel.on("data", onData);
903
923
  });
904
924
  }
925
+ readSettledPrompt(quietMs = 450, maxMs = 8000) {
926
+ const channel = this.channel;
927
+ if (!channel)
928
+ return Promise.resolve("");
929
+ return new Promise((resolve2) => {
930
+ let buf = "";
931
+ let quiet;
932
+ let hard;
933
+ function done() {
934
+ clearTimeout(hard);
935
+ if (quiet)
936
+ clearTimeout(quiet);
937
+ channel.removeListener("data", onData);
938
+ resolve2(stripAnsi(buf));
939
+ }
940
+ function onData(chunk) {
941
+ buf += decodeOutput(chunk);
942
+ if (PROMPT_RE.test(lastNonEmptyLine(stripAnsi(buf)))) {
943
+ if (quiet)
944
+ clearTimeout(quiet);
945
+ quiet = setTimeout(done, quietMs);
946
+ }
947
+ }
948
+ hard = setTimeout(done, maxMs);
949
+ channel.on("data", onData);
950
+ });
951
+ }
905
952
  extractOutput(raw, command) {
906
953
  const text = raw.replace(/\r\n/g, `
907
954
  `).replace(/\r/g, `
@@ -991,7 +1038,7 @@ function createContext(sendLog, device) {
991
1038
  }
992
1039
 
993
1040
  // src/core/routeros.ts
994
- var BARE_SAFE = /^[\w.\-:/,*@]+$/;
1041
+ var BARE_SAFE = /^[\w.\-:/,*@!]+$/;
995
1042
  function quoteValue(value) {
996
1043
  if (typeof value === "number" || typeof value === "boolean")
997
1044
  return String(value);
@@ -6329,6 +6376,12 @@ async function updateNatRule(a, ctx) {
6329
6376
  put("protocol", a.protocol);
6330
6377
  put("in-interface", a.in_interface);
6331
6378
  put("out-interface", a.out_interface);
6379
+ put("in-interface-list", a.in_interface_list);
6380
+ put("out-interface-list", a.out_interface_list);
6381
+ put("src-address-list", a.src_address_list);
6382
+ put("dst-address-list", a.dst_address_list);
6383
+ put("connection-mark", a.connection_mark);
6384
+ put("connection-nat-state", a.connection_nat_state);
6332
6385
  put("to-addresses", a.to_addresses);
6333
6386
  put("to-ports", a.to_ports);
6334
6387
  if (a.comment !== undefined)
@@ -6365,8 +6418,14 @@ var firewallNatTools = [
6365
6418
  src_port: z24.string().optional(),
6366
6419
  dst_port: z24.string().optional(),
6367
6420
  protocol: z24.string().optional(),
6368
- in_interface: z24.string().optional(),
6421
+ in_interface: z24.string().optional().describe('Negatable, e.g. "!ether1"'),
6369
6422
  out_interface: z24.string().optional(),
6423
+ in_interface_list: z24.string().optional().describe('Interface list, e.g. "WAN" or "!LAN"'),
6424
+ out_interface_list: z24.string().optional(),
6425
+ src_address_list: z24.string().optional().describe('Match src in a named list; negate "!name"'),
6426
+ dst_address_list: z24.string().optional().describe('Match dst in a named list; negate "!name"'),
6427
+ connection_mark: z24.string().optional(),
6428
+ connection_nat_state: z24.string().optional().describe('"srcnat" / "dstnat" / "!dstnat"'),
6370
6429
  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
6430
  to_ports: z24.string().optional().describe('Single port or range e.g. "8080" or "8080-8090"'),
6372
6431
  comment: z24.string().optional(),
@@ -6406,7 +6465,7 @@ var firewallNatTools = [
6406
6465
  } else if (a.chain === "dstnat" && !dstnatActions.includes(a.action)) {
6407
6466
  return `Error: Invalid action '${a.action}' for dstnat. Must be one of: ${dstnatActions.join(", ")}`;
6408
6467
  }
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();
6468
+ 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
6469
  const result = await executeMikrotikCommand(cmd, ctx);
6411
6470
  const trimmed = result.trim();
6412
6471
  if (looksLikeError(trimmed)) {
@@ -6504,6 +6563,12 @@ ${result}`;
6504
6563
  protocol: z24.string().optional(),
6505
6564
  in_interface: z24.string().optional(),
6506
6565
  out_interface: z24.string().optional(),
6566
+ in_interface_list: z24.string().optional(),
6567
+ out_interface_list: z24.string().optional(),
6568
+ src_address_list: z24.string().optional().describe('Negate with "!name"'),
6569
+ dst_address_list: z24.string().optional().describe('Negate with "!name"'),
6570
+ connection_mark: z24.string().optional(),
6571
+ connection_nat_state: z24.string().optional(),
6507
6572
  to_addresses: z24.string().optional(),
6508
6573
  to_ports: z24.string().optional(),
6509
6574
  comment: z24.string().optional(),
@@ -6596,10 +6661,33 @@ async function updateMangleRule(a, ctx) {
6596
6661
  put("protocol", a.protocol);
6597
6662
  put("in-interface", a.in_interface);
6598
6663
  put("out-interface", a.out_interface);
6664
+ put("in-interface-list", a.in_interface_list);
6665
+ put("out-interface-list", a.out_interface_list);
6666
+ put("src-address-list", a.src_address_list);
6667
+ put("dst-address-list", a.dst_address_list);
6668
+ put("src-address-type", a.src_address_type);
6669
+ put("dst-address-type", a.dst_address_type);
6599
6670
  put("connection-mark", a.connection_mark);
6600
6671
  put("packet-mark", a.packet_mark);
6601
6672
  put("routing-mark", a.routing_mark);
6602
6673
  put("connection-state", a.connection_state);
6674
+ put("connection-nat-state", a.connection_nat_state);
6675
+ put("connection-type", a.connection_type);
6676
+ put("connection-bytes", a.connection_bytes);
6677
+ put("connection-limit", a.connection_limit);
6678
+ put("connection-rate", a.connection_rate);
6679
+ put("per-connection-classifier", a.per_connection_classifier);
6680
+ put("tcp-flags", a.tcp_flags);
6681
+ put("dscp", a.dscp);
6682
+ put("priority", a.priority);
6683
+ put("packet-size", a.packet_size);
6684
+ put("layer7-protocol", a.layer7_protocol);
6685
+ put("ipsec-policy", a.ipsec_policy);
6686
+ put("nth", a.nth);
6687
+ put("random", a.random);
6688
+ put("time", a.time);
6689
+ put("hotspot", a.hotspot);
6690
+ put("p2p", a.p2p);
6603
6691
  put("new-connection-mark", a.new_connection_mark);
6604
6692
  put("new-packet-mark", a.new_packet_mark);
6605
6693
  put("new-routing-mark", a.new_routing_mark);
@@ -6636,7 +6724,7 @@ var firewallMangleTools = [
6636
6724
  name: "create_mangle_rule",
6637
6725
  title: "Create IPv4 Firewall Mangle Rule",
6638
6726
  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`.",
6727
+ 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
6728
  inputSchema: {
6641
6729
  chain: z25.enum(["prerouting", "input", "forward", "output", "postrouting"]),
6642
6730
  action: z25.enum([
@@ -6666,12 +6754,35 @@ var firewallMangleTools = [
6666
6754
  src_port: z25.string().optional(),
6667
6755
  dst_port: z25.string().optional(),
6668
6756
  protocol: z25.string().optional(),
6669
- in_interface: z25.string().optional(),
6757
+ in_interface: z25.string().optional().describe('Negatable, e.g. "!ether1"'),
6670
6758
  out_interface: z25.string().optional(),
6759
+ in_interface_list: z25.string().optional().describe('Interface list, negatable e.g. "!WAN"'),
6760
+ out_interface_list: z25.string().optional(),
6761
+ src_address_list: z25.string().optional().describe('Match src in a named list; negate "!name"'),
6762
+ dst_address_list: z25.string().optional().describe('Match dst in a named list; negate "!name" (e.g. "!IR" routes foreign traffic)'),
6763
+ src_address_type: z25.string().optional().describe('e.g. "local", "unicast", "!local"'),
6764
+ dst_address_type: z25.string().optional(),
6671
6765
  connection_mark: z25.string().optional(),
6672
6766
  packet_mark: z25.string().optional(),
6673
6767
  routing_mark: z25.string().optional(),
6674
- connection_state: z25.string().optional().describe('e.g. "new", "established,related", "invalid"'),
6768
+ connection_state: z25.string().optional().describe('e.g. "new", "established,related", "!invalid"'),
6769
+ connection_nat_state: z25.string().optional().describe('"srcnat" / "dstnat" / "!dstnat"'),
6770
+ connection_type: z25.string().optional().describe('Helper, e.g. "sip", "ftp"'),
6771
+ connection_bytes: z25.string().optional().describe('e.g. "1000000-0" (>1 MB connections)'),
6772
+ connection_limit: z25.string().optional().describe('e.g. "100,32"'),
6773
+ connection_rate: z25.string().optional().describe('e.g. "100k-1M"'),
6774
+ per_connection_classifier: z25.string().optional().describe('PCC for load balancing, e.g. "both-addresses:2/0"'),
6775
+ tcp_flags: z25.string().optional().describe('RouterOS flag expression e.g. "syn,!ack"'),
6776
+ dscp: z25.string().optional().describe("Match incoming DSCP (0-63)"),
6777
+ priority: z25.string().optional().describe("Match packet/queue priority (0-63)"),
6778
+ packet_size: z25.string().optional().describe('e.g. "1500" or "0-500"'),
6779
+ layer7_protocol: z25.string().optional().describe("Name of an /ip firewall layer7-protocol regex"),
6780
+ ipsec_policy: z25.string().optional().describe('e.g. "in,ipsec" or "out,none"'),
6781
+ nth: z25.string().optional().describe('e.g. "2,1" \u2014 every 2nd packet'),
6782
+ random: z25.string().optional().describe("Match a random N% of packets (1-99)"),
6783
+ time: z25.string().optional().describe('e.g. "8h-16h,mon,tue,wed,thu,fri"'),
6784
+ hotspot: z25.string().optional().describe('e.g. "auth", "!auth", "from-client"'),
6785
+ p2p: z25.string().optional(),
6675
6786
  new_connection_mark: z25.string().optional(),
6676
6787
  new_packet_mark: z25.string().optional(),
6677
6788
  new_routing_mark: z25.string().optional(),
@@ -6690,7 +6801,7 @@ var firewallMangleTools = [
6690
6801
  },
6691
6802
  async handler(a, ctx) {
6692
6803
  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();
6804
+ 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
6805
  const result = await executeMikrotikCommand(cmd, ctx);
6695
6806
  const trimmed = result.trim();
6696
6807
  if (looksLikeError(trimmed)) {
@@ -6784,10 +6895,33 @@ ${result}`;
6784
6895
  protocol: z25.string().optional(),
6785
6896
  in_interface: z25.string().optional(),
6786
6897
  out_interface: z25.string().optional(),
6898
+ in_interface_list: z25.string().optional(),
6899
+ out_interface_list: z25.string().optional(),
6900
+ src_address_list: z25.string().optional().describe('Negate with "!name" (e.g. "!IR")'),
6901
+ dst_address_list: z25.string().optional().describe('Negate with "!name" (e.g. "!IR")'),
6902
+ src_address_type: z25.string().optional(),
6903
+ dst_address_type: z25.string().optional(),
6787
6904
  connection_mark: z25.string().optional(),
6788
6905
  packet_mark: z25.string().optional(),
6789
6906
  routing_mark: z25.string().optional(),
6790
6907
  connection_state: z25.string().optional(),
6908
+ connection_nat_state: z25.string().optional(),
6909
+ connection_type: z25.string().optional(),
6910
+ connection_bytes: z25.string().optional(),
6911
+ connection_limit: z25.string().optional(),
6912
+ connection_rate: z25.string().optional(),
6913
+ per_connection_classifier: z25.string().optional(),
6914
+ tcp_flags: z25.string().optional(),
6915
+ dscp: z25.string().optional(),
6916
+ priority: z25.string().optional(),
6917
+ packet_size: z25.string().optional(),
6918
+ layer7_protocol: z25.string().optional(),
6919
+ ipsec_policy: z25.string().optional(),
6920
+ nth: z25.string().optional(),
6921
+ random: z25.string().optional(),
6922
+ time: z25.string().optional(),
6923
+ hotspot: z25.string().optional(),
6924
+ p2p: z25.string().optional(),
6791
6925
  new_connection_mark: z25.string().optional(),
6792
6926
  new_packet_mark: z25.string().optional(),
6793
6927
  new_routing_mark: z25.string().optional(),
@@ -22188,7 +22322,7 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
22188
22322
  // package.json
22189
22323
  var package_default = {
22190
22324
  name: "@usex/mikrotik-mcp",
22191
- version: "3.19.0",
22325
+ version: "3.20.0",
22192
22326
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
22193
22327
  keywords: [
22194
22328
  "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.20.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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "3.18.0",
3
+ "version": "3.19.0",
4
4
  "generated": "by scripts/gen-schemas.ts — do not edit by hand",
5
5
  "toolCount": 721,
6
6
  "tools": [
@@ -10179,11 +10179,34 @@
10179
10179
  "type": "string"
10180
10180
  },
10181
10181
  "in_interface": {
10182
+ "description": "Negatable, e.g. \"!ether1\"",
10182
10183
  "type": "string"
10183
10184
  },
10184
10185
  "out_interface": {
10185
10186
  "type": "string"
10186
10187
  },
10188
+ "in_interface_list": {
10189
+ "description": "Interface list, e.g. \"WAN\" or \"!LAN\"",
10190
+ "type": "string"
10191
+ },
10192
+ "out_interface_list": {
10193
+ "type": "string"
10194
+ },
10195
+ "src_address_list": {
10196
+ "description": "Match src in a named list; negate \"!name\"",
10197
+ "type": "string"
10198
+ },
10199
+ "dst_address_list": {
10200
+ "description": "Match dst in a named list; negate \"!name\"",
10201
+ "type": "string"
10202
+ },
10203
+ "connection_mark": {
10204
+ "type": "string"
10205
+ },
10206
+ "connection_nat_state": {
10207
+ "description": "\"srcnat\" / \"dstnat\" / \"!dstnat\"",
10208
+ "type": "string"
10209
+ },
10187
10210
  "to_addresses": {
10188
10211
  "description": "Single IP or range e.g. \"10.0.0.1\" or \"10.0.0.1-10.0.0.10\"",
10189
10212
  "type": "string"
@@ -10327,6 +10350,26 @@
10327
10350
  "out_interface": {
10328
10351
  "type": "string"
10329
10352
  },
10353
+ "in_interface_list": {
10354
+ "type": "string"
10355
+ },
10356
+ "out_interface_list": {
10357
+ "type": "string"
10358
+ },
10359
+ "src_address_list": {
10360
+ "description": "Negate with \"!name\"",
10361
+ "type": "string"
10362
+ },
10363
+ "dst_address_list": {
10364
+ "description": "Negate with \"!name\"",
10365
+ "type": "string"
10366
+ },
10367
+ "connection_mark": {
10368
+ "type": "string"
10369
+ },
10370
+ "connection_nat_state": {
10371
+ "type": "string"
10372
+ },
10330
10373
  "to_addresses": {
10331
10374
  "type": "string"
10332
10375
  },
@@ -10452,7 +10495,7 @@
10452
10495
  "destructiveHint": false,
10453
10496
  "openWorldHint": false
10454
10497
  },
10455
- "description": "Creates an IPv4 mangle rule (`/ip firewall mangle add`) — 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`.",
10498
+ "description": "Creates an IPv4 mangle rule (`/ip firewall mangle add`) — 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 — 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`.",
10456
10499
  "inputSchema": {
10457
10500
  "$schema": "https://json-schema.org/draft/2020-12/schema",
10458
10501
  "type": "object",
@@ -10502,11 +10545,34 @@
10502
10545
  "type": "string"
10503
10546
  },
10504
10547
  "in_interface": {
10548
+ "description": "Negatable, e.g. \"!ether1\"",
10505
10549
  "type": "string"
10506
10550
  },
10507
10551
  "out_interface": {
10508
10552
  "type": "string"
10509
10553
  },
10554
+ "in_interface_list": {
10555
+ "description": "Interface list, negatable e.g. \"!WAN\"",
10556
+ "type": "string"
10557
+ },
10558
+ "out_interface_list": {
10559
+ "type": "string"
10560
+ },
10561
+ "src_address_list": {
10562
+ "description": "Match src in a named list; negate \"!name\"",
10563
+ "type": "string"
10564
+ },
10565
+ "dst_address_list": {
10566
+ "description": "Match dst in a named list; negate \"!name\" (e.g. \"!IR\" routes foreign traffic)",
10567
+ "type": "string"
10568
+ },
10569
+ "src_address_type": {
10570
+ "description": "e.g. \"local\", \"unicast\", \"!local\"",
10571
+ "type": "string"
10572
+ },
10573
+ "dst_address_type": {
10574
+ "type": "string"
10575
+ },
10510
10576
  "connection_mark": {
10511
10577
  "type": "string"
10512
10578
  },
@@ -10517,7 +10583,74 @@
10517
10583
  "type": "string"
10518
10584
  },
10519
10585
  "connection_state": {
10520
- "description": "e.g. \"new\", \"established,related\", \"invalid\"",
10586
+ "description": "e.g. \"new\", \"established,related\", \"!invalid\"",
10587
+ "type": "string"
10588
+ },
10589
+ "connection_nat_state": {
10590
+ "description": "\"srcnat\" / \"dstnat\" / \"!dstnat\"",
10591
+ "type": "string"
10592
+ },
10593
+ "connection_type": {
10594
+ "description": "Helper, e.g. \"sip\", \"ftp\"",
10595
+ "type": "string"
10596
+ },
10597
+ "connection_bytes": {
10598
+ "description": "e.g. \"1000000-0\" (>1 MB connections)",
10599
+ "type": "string"
10600
+ },
10601
+ "connection_limit": {
10602
+ "description": "e.g. \"100,32\"",
10603
+ "type": "string"
10604
+ },
10605
+ "connection_rate": {
10606
+ "description": "e.g. \"100k-1M\"",
10607
+ "type": "string"
10608
+ },
10609
+ "per_connection_classifier": {
10610
+ "description": "PCC for load balancing, e.g. \"both-addresses:2/0\"",
10611
+ "type": "string"
10612
+ },
10613
+ "tcp_flags": {
10614
+ "description": "RouterOS flag expression e.g. \"syn,!ack\"",
10615
+ "type": "string"
10616
+ },
10617
+ "dscp": {
10618
+ "description": "Match incoming DSCP (0-63)",
10619
+ "type": "string"
10620
+ },
10621
+ "priority": {
10622
+ "description": "Match packet/queue priority (0-63)",
10623
+ "type": "string"
10624
+ },
10625
+ "packet_size": {
10626
+ "description": "e.g. \"1500\" or \"0-500\"",
10627
+ "type": "string"
10628
+ },
10629
+ "layer7_protocol": {
10630
+ "description": "Name of an /ip firewall layer7-protocol regex",
10631
+ "type": "string"
10632
+ },
10633
+ "ipsec_policy": {
10634
+ "description": "e.g. \"in,ipsec\" or \"out,none\"",
10635
+ "type": "string"
10636
+ },
10637
+ "nth": {
10638
+ "description": "e.g. \"2,1\" — every 2nd packet",
10639
+ "type": "string"
10640
+ },
10641
+ "random": {
10642
+ "description": "Match a random N% of packets (1-99)",
10643
+ "type": "string"
10644
+ },
10645
+ "time": {
10646
+ "description": "e.g. \"8h-16h,mon,tue,wed,thu,fri\"",
10647
+ "type": "string"
10648
+ },
10649
+ "hotspot": {
10650
+ "description": "e.g. \"auth\", \"!auth\", \"from-client\"",
10651
+ "type": "string"
10652
+ },
10653
+ "p2p": {
10521
10654
  "type": "string"
10522
10655
  },
10523
10656
  "new_connection_mark": {
@@ -10689,6 +10822,26 @@
10689
10822
  "out_interface": {
10690
10823
  "type": "string"
10691
10824
  },
10825
+ "in_interface_list": {
10826
+ "type": "string"
10827
+ },
10828
+ "out_interface_list": {
10829
+ "type": "string"
10830
+ },
10831
+ "src_address_list": {
10832
+ "description": "Negate with \"!name\" (e.g. \"!IR\")",
10833
+ "type": "string"
10834
+ },
10835
+ "dst_address_list": {
10836
+ "description": "Negate with \"!name\" (e.g. \"!IR\")",
10837
+ "type": "string"
10838
+ },
10839
+ "src_address_type": {
10840
+ "type": "string"
10841
+ },
10842
+ "dst_address_type": {
10843
+ "type": "string"
10844
+ },
10692
10845
  "connection_mark": {
10693
10846
  "type": "string"
10694
10847
  },
@@ -10701,6 +10854,57 @@
10701
10854
  "connection_state": {
10702
10855
  "type": "string"
10703
10856
  },
10857
+ "connection_nat_state": {
10858
+ "type": "string"
10859
+ },
10860
+ "connection_type": {
10861
+ "type": "string"
10862
+ },
10863
+ "connection_bytes": {
10864
+ "type": "string"
10865
+ },
10866
+ "connection_limit": {
10867
+ "type": "string"
10868
+ },
10869
+ "connection_rate": {
10870
+ "type": "string"
10871
+ },
10872
+ "per_connection_classifier": {
10873
+ "type": "string"
10874
+ },
10875
+ "tcp_flags": {
10876
+ "type": "string"
10877
+ },
10878
+ "dscp": {
10879
+ "type": "string"
10880
+ },
10881
+ "priority": {
10882
+ "type": "string"
10883
+ },
10884
+ "packet_size": {
10885
+ "type": "string"
10886
+ },
10887
+ "layer7_protocol": {
10888
+ "type": "string"
10889
+ },
10890
+ "ipsec_policy": {
10891
+ "type": "string"
10892
+ },
10893
+ "nth": {
10894
+ "type": "string"
10895
+ },
10896
+ "random": {
10897
+ "type": "string"
10898
+ },
10899
+ "time": {
10900
+ "type": "string"
10901
+ },
10902
+ "hotspot": {
10903
+ "type": "string"
10904
+ },
10905
+ "p2p": {
10906
+ "type": "string"
10907
+ },
10704
10908
  "new_connection_mark": {
10705
10909
  "type": "string"
10706
10910
  },
@@ -48,11 +48,34 @@
48
48
  "type": "string"
49
49
  },
50
50
  "in_interface": {
51
+ "description": "Negatable, e.g. \"!ether1\"",
51
52
  "type": "string"
52
53
  },
53
54
  "out_interface": {
54
55
  "type": "string"
55
56
  },
57
+ "in_interface_list": {
58
+ "description": "Interface list, negatable e.g. \"!WAN\"",
59
+ "type": "string"
60
+ },
61
+ "out_interface_list": {
62
+ "type": "string"
63
+ },
64
+ "src_address_list": {
65
+ "description": "Match src in a named list; negate \"!name\"",
66
+ "type": "string"
67
+ },
68
+ "dst_address_list": {
69
+ "description": "Match dst in a named list; negate \"!name\" (e.g. \"!IR\" routes foreign traffic)",
70
+ "type": "string"
71
+ },
72
+ "src_address_type": {
73
+ "description": "e.g. \"local\", \"unicast\", \"!local\"",
74
+ "type": "string"
75
+ },
76
+ "dst_address_type": {
77
+ "type": "string"
78
+ },
56
79
  "connection_mark": {
57
80
  "type": "string"
58
81
  },
@@ -63,7 +86,74 @@
63
86
  "type": "string"
64
87
  },
65
88
  "connection_state": {
66
- "description": "e.g. \"new\", \"established,related\", \"invalid\"",
89
+ "description": "e.g. \"new\", \"established,related\", \"!invalid\"",
90
+ "type": "string"
91
+ },
92
+ "connection_nat_state": {
93
+ "description": "\"srcnat\" / \"dstnat\" / \"!dstnat\"",
94
+ "type": "string"
95
+ },
96
+ "connection_type": {
97
+ "description": "Helper, e.g. \"sip\", \"ftp\"",
98
+ "type": "string"
99
+ },
100
+ "connection_bytes": {
101
+ "description": "e.g. \"1000000-0\" (>1 MB connections)",
102
+ "type": "string"
103
+ },
104
+ "connection_limit": {
105
+ "description": "e.g. \"100,32\"",
106
+ "type": "string"
107
+ },
108
+ "connection_rate": {
109
+ "description": "e.g. \"100k-1M\"",
110
+ "type": "string"
111
+ },
112
+ "per_connection_classifier": {
113
+ "description": "PCC for load balancing, e.g. \"both-addresses:2/0\"",
114
+ "type": "string"
115
+ },
116
+ "tcp_flags": {
117
+ "description": "RouterOS flag expression e.g. \"syn,!ack\"",
118
+ "type": "string"
119
+ },
120
+ "dscp": {
121
+ "description": "Match incoming DSCP (0-63)",
122
+ "type": "string"
123
+ },
124
+ "priority": {
125
+ "description": "Match packet/queue priority (0-63)",
126
+ "type": "string"
127
+ },
128
+ "packet_size": {
129
+ "description": "e.g. \"1500\" or \"0-500\"",
130
+ "type": "string"
131
+ },
132
+ "layer7_protocol": {
133
+ "description": "Name of an /ip firewall layer7-protocol regex",
134
+ "type": "string"
135
+ },
136
+ "ipsec_policy": {
137
+ "description": "e.g. \"in,ipsec\" or \"out,none\"",
138
+ "type": "string"
139
+ },
140
+ "nth": {
141
+ "description": "e.g. \"2,1\" — every 2nd packet",
142
+ "type": "string"
143
+ },
144
+ "random": {
145
+ "description": "Match a random N% of packets (1-99)",
146
+ "type": "string"
147
+ },
148
+ "time": {
149
+ "description": "e.g. \"8h-16h,mon,tue,wed,thu,fri\"",
150
+ "type": "string"
151
+ },
152
+ "hotspot": {
153
+ "description": "e.g. \"auth\", \"!auth\", \"from-client\"",
154
+ "type": "string"
155
+ },
156
+ "p2p": {
67
157
  "type": "string"
68
158
  },
69
159
  "new_connection_mark": {
@@ -26,11 +26,34 @@
26
26
  "type": "string"
27
27
  },
28
28
  "in_interface": {
29
+ "description": "Negatable, e.g. \"!ether1\"",
29
30
  "type": "string"
30
31
  },
31
32
  "out_interface": {
32
33
  "type": "string"
33
34
  },
35
+ "in_interface_list": {
36
+ "description": "Interface list, e.g. \"WAN\" or \"!LAN\"",
37
+ "type": "string"
38
+ },
39
+ "out_interface_list": {
40
+ "type": "string"
41
+ },
42
+ "src_address_list": {
43
+ "description": "Match src in a named list; negate \"!name\"",
44
+ "type": "string"
45
+ },
46
+ "dst_address_list": {
47
+ "description": "Match dst in a named list; negate \"!name\"",
48
+ "type": "string"
49
+ },
50
+ "connection_mark": {
51
+ "type": "string"
52
+ },
53
+ "connection_nat_state": {
54
+ "description": "\"srcnat\" / \"dstnat\" / \"!dstnat\"",
55
+ "type": "string"
56
+ },
34
57
  "to_addresses": {
35
58
  "description": "Single IP or range e.g. \"10.0.0.1\" or \"10.0.0.1-10.0.0.10\"",
36
59
  "type": "string"
@@ -33,6 +33,26 @@
33
33
  "out_interface": {
34
34
  "type": "string"
35
35
  },
36
+ "in_interface_list": {
37
+ "type": "string"
38
+ },
39
+ "out_interface_list": {
40
+ "type": "string"
41
+ },
42
+ "src_address_list": {
43
+ "description": "Negate with \"!name\" (e.g. \"!IR\")",
44
+ "type": "string"
45
+ },
46
+ "dst_address_list": {
47
+ "description": "Negate with \"!name\" (e.g. \"!IR\")",
48
+ "type": "string"
49
+ },
50
+ "src_address_type": {
51
+ "type": "string"
52
+ },
53
+ "dst_address_type": {
54
+ "type": "string"
55
+ },
36
56
  "connection_mark": {
37
57
  "type": "string"
38
58
  },
@@ -45,6 +65,57 @@
45
65
  "connection_state": {
46
66
  "type": "string"
47
67
  },
68
+ "connection_nat_state": {
69
+ "type": "string"
70
+ },
71
+ "connection_type": {
72
+ "type": "string"
73
+ },
74
+ "connection_bytes": {
75
+ "type": "string"
76
+ },
77
+ "connection_limit": {
78
+ "type": "string"
79
+ },
80
+ "connection_rate": {
81
+ "type": "string"
82
+ },
83
+ "per_connection_classifier": {
84
+ "type": "string"
85
+ },
86
+ "tcp_flags": {
87
+ "type": "string"
88
+ },
89
+ "dscp": {
90
+ "type": "string"
91
+ },
92
+ "priority": {
93
+ "type": "string"
94
+ },
95
+ "packet_size": {
96
+ "type": "string"
97
+ },
98
+ "layer7_protocol": {
99
+ "type": "string"
100
+ },
101
+ "ipsec_policy": {
102
+ "type": "string"
103
+ },
104
+ "nth": {
105
+ "type": "string"
106
+ },
107
+ "random": {
108
+ "type": "string"
109
+ },
110
+ "time": {
111
+ "type": "string"
112
+ },
113
+ "hotspot": {
114
+ "type": "string"
115
+ },
116
+ "p2p": {
117
+ "type": "string"
118
+ },
48
119
  "new_connection_mark": {
49
120
  "type": "string"
50
121
  },
@@ -33,6 +33,26 @@
33
33
  "out_interface": {
34
34
  "type": "string"
35
35
  },
36
+ "in_interface_list": {
37
+ "type": "string"
38
+ },
39
+ "out_interface_list": {
40
+ "type": "string"
41
+ },
42
+ "src_address_list": {
43
+ "description": "Negate with \"!name\"",
44
+ "type": "string"
45
+ },
46
+ "dst_address_list": {
47
+ "description": "Negate with \"!name\"",
48
+ "type": "string"
49
+ },
50
+ "connection_mark": {
51
+ "type": "string"
52
+ },
53
+ "connection_nat_state": {
54
+ "type": "string"
55
+ },
36
56
  "to_addresses": {
37
57
  "type": "string"
38
58
  },