@usex/mikrotik-mcp 3.18.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/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);
@@ -2186,7 +2233,7 @@ ${result}`;
2186
2233
  name: "create_export",
2187
2234
  title: "Create Full Configuration Export",
2188
2235
  annotations: READ,
2189
- description: "Exports the complete device configuration to a `.rsc` plain-text script file (`/export" + " file=<name>`), re-applicable via `import_configuration`. Unlike `create_backup`, the result" + " is human-readable plain text \u2014 not a binary snapshot \u2014 and passwords are hidden by default" + " (`hide_sensitive=true`). `file_format` changes only the file extension used when looking up" + " the saved file \u2014 no `format=` flag is ever sent to RouterOS, so the content is always" + " RouterOS script text regardless of the chosen extension; selecting `json` or `xml` will also" + " cause the post-export file lookup to fail because RouterOS saves the file as `.rsc`. Use the" + " `compact` boolean to omit default values, or `verbose` to include all parameters;" + " `export_type` only controls where the `file=` argument is positioned in the command and" + " does not independently drive compactness or verbosity. For a single subsection only use" + " `export_section`. Returns file details of the created export file.",
2236
+ description: "Exports the complete device configuration to a `.rsc` plain-text script file (`/export" + " file=<name>`), re-applicable via `import_configuration`. Unlike `create_backup`, the result" + " is human-readable plain text \u2014 not a binary snapshot \u2014 and passwords are hidden by default" + " (`hide_sensitive=true`). `file_format` changes only the file extension used when looking up" + " the saved file \u2014 no `format=` flag is ever sent to RouterOS, so the content is always" + " RouterOS script text regardless of the chosen extension; selecting `json` or `xml` will also" + " cause the post-export file lookup to fail because RouterOS saves the file as `.rsc`." + " ALWAYS default to a FULL configuration export: leave `compact` and `verbose` off (the plain" + " full export) unless the user explicitly asks for less or more. Set `compact` ONLY when the" + " user wants a smaller diff that omits default values, or `verbose` ONLY when they want every" + " parameter (including defaults). `export_type` only controls where the `file=` argument is" + " positioned in the command and does not independently drive compactness or verbosity. For a" + " single subsection only use `export_section`. Returns file details of the created export file.",
2190
2237
  inputSchema: {
2191
2238
  name: z5.string().optional(),
2192
2239
  file_format: z5.enum(["rsc", "json", "xml"]).default("rsc"),
@@ -2379,6 +2426,12 @@ ${result}`;
2379
2426
  // src/tools/local-backup.ts
2380
2427
  import { z as z6 } from "zod";
2381
2428
 
2429
+ // src/core/slug.ts
2430
+ function deviceSlug(name) {
2431
+ const s = (name ?? "").replace(/[^A-Za-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
2432
+ return s || "device";
2433
+ }
2434
+
2382
2435
  // src/backups/vault.ts
2383
2436
  import {
2384
2437
  existsSync,
@@ -2499,15 +2552,17 @@ function labelSlug(label) {
2499
2552
  const s = (label ?? "").replace(/[^A-Za-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
2500
2553
  return s ? `_${s}` : "";
2501
2554
  }
2555
+ function buildExportCommand(opts = {}) {
2556
+ return new Cmd("/export").raw(opts.verbose ? "verbose" : opts.compact ? "compact" : undefined).raw(opts.terse ? "terse" : undefined).raw(opts.showSensitive ? "show-sensitive" : undefined).build();
2557
+ }
2502
2558
  async function createLocalBackup(ctx, opts = {}) {
2503
2559
  const device = resolveDeviceName(ctx.device);
2504
- const cmd = new Cmd("/export").raw(opts.showSensitive ? "show-sensitive" : undefined).build();
2505
- const body = await executeMikrotikCommand(cmd, ctx);
2560
+ const body = await executeMikrotikCommand(buildExportCommand(opts), ctx);
2506
2561
  if (isEmpty(body) || looksLikeError(body)) {
2507
2562
  return { ok: false, device, error: body.trim() || "(empty export)" };
2508
2563
  }
2509
2564
  const stamp = await deviceDateStamp(ctx);
2510
- const name = `${device}_${stamp}${labelSlug(opts.label)}.rsc`;
2565
+ const name = `${deviceSlug(device)}_${stamp}${labelSlug(opts.label)}.rsc`;
2511
2566
  return { ok: true, device, name: writeBackup(name, body), bytes: Buffer.byteLength(body) };
2512
2567
  }
2513
2568
 
@@ -2578,13 +2633,22 @@ var localBackupTools = [
2578
2633
  name: "create_local_backup",
2579
2634
  title: "Create Local Config Backup (host vault)",
2580
2635
  annotations: WRITE,
2581
- description: "Captures the device's full configuration with `/export` and saves it as a timestamped " + "plain-text `.rsc` file in the MCP server's LOCAL backup vault (default " + "`~/.mikrotik-mcp/backups/`, override with the `MIKROTIK_BACKUP_DIR` env var) \u2014 NOT on the " + "device and NOT in S3. The filename is `<device>_<date_time>.rsc` stamped in the device's " + "local clock (24-hour; Jalali for the Tehran timezone, Gregorian otherwise). This is a " + "host-side, human-readable, diffable copy you can restore later with restore_local_backup. " + "Compare: create_backup makes a binary `/system backup` file ON the device; " + "capture_config_snapshot stores an export in the local snapshot database; upload_backup_to_s3 " + "pushes to S3. show_sensitive=true includes secrets (keys/passwords) in the export. " + "Returns the saved filename, byte size and vault path.",
2636
+ description: "Captures the device's full configuration with `/export` and saves it as a timestamped " + "plain-text `.rsc` file in the MCP server's LOCAL backup vault (default " + "`~/.mikrotik-mcp/backups/`, override with the `MIKROTIK_BACKUP_DIR` env var) \u2014 NOT on the " + "device and NOT in S3. The filename is `<device-slug>_<date_time>.rsc` \u2014 the device name is " + "slugified (spaces/underscores/etc \u2192 dash) \u2014 stamped in the device's local clock (24-hour; " + "Jalali for the Tehran timezone, Gregorian otherwise). This is a host-side, human-readable, " + "diffable copy you can restore later with restore_local_backup. Compare: create_backup makes " + "a binary `/system backup` file ON the device; capture_config_snapshot stores an export in " + "the local snapshot database; upload_backup_to_s3 pushes to S3. ALWAYS default to a FULL " + "backup of the complete configuration: call this with no option flags (the bare `/export`) " + "unless the user explicitly asks for less or more. Narrow it ONLY on request \u2014 set compact to " + "drop default values, or use export_section for a single subsection. Broaden it ONLY on " + "request \u2014 set verbose to include every parameter (even defaults), show_sensitive to include " + "secrets (keys/passwords), or use create_backup for a binary full-system snapshot. terse just " + "changes the text to one machine-readable line per item. Returns the saved filename, byte size " + "and vault path.",
2582
2637
  inputSchema: {
2583
2638
  label: z6.string().optional().describe('Optional label appended to the filename, e.g. "pre-upgrade".'),
2584
- show_sensitive: z6.boolean().default(false).describe("Include secrets (keys/passwords) in the export. Default false.")
2639
+ show_sensitive: z6.boolean().default(false).describe("Include secrets (keys/passwords) in the export. Default false."),
2640
+ verbose: z6.boolean().default(false).describe("Include every parameter, even defaults (RouterOS `verbose`)."),
2641
+ compact: z6.boolean().default(false).describe("Export only non-default values (RouterOS `compact`; ignored if verbose)."),
2642
+ terse: z6.boolean().default(false).describe("One self-contained, machine-readable line per item (RouterOS `terse`).")
2585
2643
  },
2586
2644
  async handler(a, ctx) {
2587
- const r = await createLocalBackup(ctx, { label: a.label, showSensitive: a.show_sensitive });
2645
+ const r = await createLocalBackup(ctx, {
2646
+ label: a.label,
2647
+ showSensitive: a.show_sensitive,
2648
+ verbose: a.verbose,
2649
+ compact: a.compact,
2650
+ terse: a.terse
2651
+ });
2588
2652
  if (!r.ok)
2589
2653
  return `Failed to capture export for a local backup: ${r.error}`;
2590
2654
  return `Saved local backup '${r.name}' (${r.bytes} bytes) for device '${r.device}' to ` + `${backupDir()}. Restore it with restore_local_backup name=${r.name}.`;
@@ -2720,7 +2784,7 @@ function trimSlashes(s) {
2720
2784
  return s.replace(/^\/+|\/+$/g, "");
2721
2785
  }
2722
2786
  function s3DevicePrefix(device) {
2723
- const segments = [getS3Config()?.prefix ?? "", device ?? ""].map(trimSlashes).filter(Boolean);
2787
+ const segments = [getS3Config()?.prefix ?? "", device ? deviceSlug(device) : ""].map(trimSlashes).filter(Boolean);
2724
2788
  return segments.length ? `${segments.join("/")}/` : "";
2725
2789
  }
2726
2790
  function s3Key(name, device) {
@@ -6312,6 +6376,12 @@ async function updateNatRule(a, ctx) {
6312
6376
  put("protocol", a.protocol);
6313
6377
  put("in-interface", a.in_interface);
6314
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);
6315
6385
  put("to-addresses", a.to_addresses);
6316
6386
  put("to-ports", a.to_ports);
6317
6387
  if (a.comment !== undefined)
@@ -6348,8 +6418,14 @@ var firewallNatTools = [
6348
6418
  src_port: z24.string().optional(),
6349
6419
  dst_port: z24.string().optional(),
6350
6420
  protocol: z24.string().optional(),
6351
- in_interface: z24.string().optional(),
6421
+ in_interface: z24.string().optional().describe('Negatable, e.g. "!ether1"'),
6352
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"'),
6353
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"'),
6354
6430
  to_ports: z24.string().optional().describe('Single port or range e.g. "8080" or "8080-8090"'),
6355
6431
  comment: z24.string().optional(),
@@ -6389,7 +6465,7 @@ var firewallNatTools = [
6389
6465
  } else if (a.chain === "dstnat" && !dstnatActions.includes(a.action)) {
6390
6466
  return `Error: Invalid action '${a.action}' for dstnat. Must be one of: ${dstnatActions.join(", ")}`;
6391
6467
  }
6392
- 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();
6393
6469
  const result = await executeMikrotikCommand(cmd, ctx);
6394
6470
  const trimmed = result.trim();
6395
6471
  if (looksLikeError(trimmed)) {
@@ -6487,6 +6563,12 @@ ${result}`;
6487
6563
  protocol: z24.string().optional(),
6488
6564
  in_interface: z24.string().optional(),
6489
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(),
6490
6572
  to_addresses: z24.string().optional(),
6491
6573
  to_ports: z24.string().optional(),
6492
6574
  comment: z24.string().optional(),
@@ -6579,10 +6661,33 @@ async function updateMangleRule(a, ctx) {
6579
6661
  put("protocol", a.protocol);
6580
6662
  put("in-interface", a.in_interface);
6581
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);
6582
6670
  put("connection-mark", a.connection_mark);
6583
6671
  put("packet-mark", a.packet_mark);
6584
6672
  put("routing-mark", a.routing_mark);
6585
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);
6586
6691
  put("new-connection-mark", a.new_connection_mark);
6587
6692
  put("new-packet-mark", a.new_packet_mark);
6588
6693
  put("new-routing-mark", a.new_routing_mark);
@@ -6619,7 +6724,7 @@ var firewallMangleTools = [
6619
6724
  name: "create_mangle_rule",
6620
6725
  title: "Create IPv4 Firewall Mangle Rule",
6621
6726
  annotations: WRITE,
6622
- 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`.",
6623
6728
  inputSchema: {
6624
6729
  chain: z25.enum(["prerouting", "input", "forward", "output", "postrouting"]),
6625
6730
  action: z25.enum([
@@ -6649,12 +6754,35 @@ var firewallMangleTools = [
6649
6754
  src_port: z25.string().optional(),
6650
6755
  dst_port: z25.string().optional(),
6651
6756
  protocol: z25.string().optional(),
6652
- in_interface: z25.string().optional(),
6757
+ in_interface: z25.string().optional().describe('Negatable, e.g. "!ether1"'),
6653
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(),
6654
6765
  connection_mark: z25.string().optional(),
6655
6766
  packet_mark: z25.string().optional(),
6656
6767
  routing_mark: z25.string().optional(),
6657
- 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(),
6658
6786
  new_connection_mark: z25.string().optional(),
6659
6787
  new_packet_mark: z25.string().optional(),
6660
6788
  new_routing_mark: z25.string().optional(),
@@ -6673,7 +6801,7 @@ var firewallMangleTools = [
6673
6801
  },
6674
6802
  async handler(a, ctx) {
6675
6803
  ctx.info(`Creating mangle rule: chain=${a.chain}, action=${a.action}`);
6676
- 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();
6677
6805
  const result = await executeMikrotikCommand(cmd, ctx);
6678
6806
  const trimmed = result.trim();
6679
6807
  if (looksLikeError(trimmed)) {
@@ -6767,10 +6895,33 @@ ${result}`;
6767
6895
  protocol: z25.string().optional(),
6768
6896
  in_interface: z25.string().optional(),
6769
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(),
6770
6904
  connection_mark: z25.string().optional(),
6771
6905
  packet_mark: z25.string().optional(),
6772
6906
  routing_mark: z25.string().optional(),
6773
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(),
6774
6925
  new_connection_mark: z25.string().optional(),
6775
6926
  new_packet_mark: z25.string().optional(),
6776
6927
  new_routing_mark: z25.string().optional(),
@@ -22171,7 +22322,7 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
22171
22322
  // package.json
22172
22323
  var package_default = {
22173
22324
  name: "@usex/mikrotik-mcp",
22174
- version: "3.18.0",
22325
+ version: "3.20.0",
22175
22326
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
22176
22327
  keywords: [
22177
22328
  "ai",