@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/cli.js +181 -27
- package/dist/index.d.ts +8 -0
- package/dist/index.js +177 -26
- package/dist/ui/observability.html +2 -2
- package/package.json +1 -1
- package/schemas/tool-catalog.json +225 -6
- package/schemas/tools/create_local_backup.json +16 -1
- package/schemas/tools/create_mangle_rule.json +91 -1
- package/schemas/tools/create_nat_rule.json +23 -0
- package/schemas/tools/update_mangle_rule.json +71 -0
- package/schemas/tools/update_nat_rule.json +20 -0
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
|
|
781
|
+
function lastNonEmptyLine(response) {
|
|
783
782
|
const lines = response.replace(/\r/g, "").split(`
|
|
784
783
|
`).map((l) => l.trimEnd()).filter(Boolean);
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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`.
|
|
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
|
|
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
|
|
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, {
|
|
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
|
|
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.
|
|
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",
|