@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/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
|
|
795
|
+
function lastNonEmptyLine(response) {
|
|
797
796
|
const lines = response.replace(/\r/g, "").split(`
|
|
798
797
|
`).map((l) => l.trimEnd()).filter(Boolean);
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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);
|
|
@@ -2174,7 +2221,7 @@ ${result}`;
|
|
|
2174
2221
|
name: "create_export",
|
|
2175
2222
|
title: "Create Full Configuration Export",
|
|
2176
2223
|
annotations: READ,
|
|
2177
|
-
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`.
|
|
2224
|
+
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.",
|
|
2178
2225
|
inputSchema: {
|
|
2179
2226
|
name: z4.string().optional(),
|
|
2180
2227
|
file_format: z4.enum(["rsc", "json", "xml"]).default("rsc"),
|
|
@@ -2367,6 +2414,12 @@ ${result}`;
|
|
|
2367
2414
|
// src/tools/local-backup.ts
|
|
2368
2415
|
import { z as z5 } from "zod";
|
|
2369
2416
|
|
|
2417
|
+
// src/core/slug.ts
|
|
2418
|
+
function deviceSlug(name) {
|
|
2419
|
+
const s = (name ?? "").replace(/[^A-Za-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2420
|
+
return s || "device";
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2370
2423
|
// src/backups/vault.ts
|
|
2371
2424
|
import {
|
|
2372
2425
|
existsSync,
|
|
@@ -2487,15 +2540,17 @@ function labelSlug(label) {
|
|
|
2487
2540
|
const s = (label ?? "").replace(/[^A-Za-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
2488
2541
|
return s ? `_${s}` : "";
|
|
2489
2542
|
}
|
|
2543
|
+
function buildExportCommand(opts = {}) {
|
|
2544
|
+
return new Cmd("/export").raw(opts.verbose ? "verbose" : opts.compact ? "compact" : undefined).raw(opts.terse ? "terse" : undefined).raw(opts.showSensitive ? "show-sensitive" : undefined).build();
|
|
2545
|
+
}
|
|
2490
2546
|
async function createLocalBackup(ctx, opts = {}) {
|
|
2491
2547
|
const device = resolveDeviceName(ctx.device);
|
|
2492
|
-
const
|
|
2493
|
-
const body = await executeMikrotikCommand(cmd, ctx);
|
|
2548
|
+
const body = await executeMikrotikCommand(buildExportCommand(opts), ctx);
|
|
2494
2549
|
if (isEmpty(body) || looksLikeError(body)) {
|
|
2495
2550
|
return { ok: false, device, error: body.trim() || "(empty export)" };
|
|
2496
2551
|
}
|
|
2497
2552
|
const stamp = await deviceDateStamp(ctx);
|
|
2498
|
-
const name = `${device}_${stamp}${labelSlug(opts.label)}.rsc`;
|
|
2553
|
+
const name = `${deviceSlug(device)}_${stamp}${labelSlug(opts.label)}.rsc`;
|
|
2499
2554
|
return { ok: true, device, name: writeBackup(name, body), bytes: Buffer.byteLength(body) };
|
|
2500
2555
|
}
|
|
2501
2556
|
|
|
@@ -2566,13 +2621,22 @@ var localBackupTools = [
|
|
|
2566
2621
|
name: "create_local_backup",
|
|
2567
2622
|
title: "Create Local Config Backup (host vault)",
|
|
2568
2623
|
annotations: WRITE,
|
|
2569
|
-
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
|
|
2624
|
+
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.",
|
|
2570
2625
|
inputSchema: {
|
|
2571
2626
|
label: z5.string().optional().describe('Optional label appended to the filename, e.g. "pre-upgrade".'),
|
|
2572
|
-
show_sensitive: z5.boolean().default(false).describe("Include secrets (keys/passwords) in the export. Default false.")
|
|
2627
|
+
show_sensitive: z5.boolean().default(false).describe("Include secrets (keys/passwords) in the export. Default false."),
|
|
2628
|
+
verbose: z5.boolean().default(false).describe("Include every parameter, even defaults (RouterOS `verbose`)."),
|
|
2629
|
+
compact: z5.boolean().default(false).describe("Export only non-default values (RouterOS `compact`; ignored if verbose)."),
|
|
2630
|
+
terse: z5.boolean().default(false).describe("One self-contained, machine-readable line per item (RouterOS `terse`).")
|
|
2573
2631
|
},
|
|
2574
2632
|
async handler(a, ctx) {
|
|
2575
|
-
const r = await createLocalBackup(ctx, {
|
|
2633
|
+
const r = await createLocalBackup(ctx, {
|
|
2634
|
+
label: a.label,
|
|
2635
|
+
showSensitive: a.show_sensitive,
|
|
2636
|
+
verbose: a.verbose,
|
|
2637
|
+
compact: a.compact,
|
|
2638
|
+
terse: a.terse
|
|
2639
|
+
});
|
|
2576
2640
|
if (!r.ok)
|
|
2577
2641
|
return `Failed to capture export for a local backup: ${r.error}`;
|
|
2578
2642
|
return `Saved local backup '${r.name}' (${r.bytes} bytes) for device '${r.device}' to ` + `${backupDir()}. Restore it with restore_local_backup name=${r.name}.`;
|
|
@@ -2708,7 +2772,7 @@ function trimSlashes(s) {
|
|
|
2708
2772
|
return s.replace(/^\/+|\/+$/g, "");
|
|
2709
2773
|
}
|
|
2710
2774
|
function s3DevicePrefix(device) {
|
|
2711
|
-
const segments = [getS3Config()?.prefix ?? "", device
|
|
2775
|
+
const segments = [getS3Config()?.prefix ?? "", device ? deviceSlug(device) : ""].map(trimSlashes).filter(Boolean);
|
|
2712
2776
|
return segments.length ? `${segments.join("/")}/` : "";
|
|
2713
2777
|
}
|
|
2714
2778
|
function s3Key(name, device) {
|
|
@@ -6300,6 +6364,12 @@ async function updateNatRule(a, ctx) {
|
|
|
6300
6364
|
put("protocol", a.protocol);
|
|
6301
6365
|
put("in-interface", a.in_interface);
|
|
6302
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);
|
|
6303
6373
|
put("to-addresses", a.to_addresses);
|
|
6304
6374
|
put("to-ports", a.to_ports);
|
|
6305
6375
|
if (a.comment !== undefined)
|
|
@@ -6336,8 +6406,14 @@ var firewallNatTools = [
|
|
|
6336
6406
|
src_port: z23.string().optional(),
|
|
6337
6407
|
dst_port: z23.string().optional(),
|
|
6338
6408
|
protocol: z23.string().optional(),
|
|
6339
|
-
in_interface: z23.string().optional(),
|
|
6409
|
+
in_interface: z23.string().optional().describe('Negatable, e.g. "!ether1"'),
|
|
6340
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"'),
|
|
6341
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"'),
|
|
6342
6418
|
to_ports: z23.string().optional().describe('Single port or range e.g. "8080" or "8080-8090"'),
|
|
6343
6419
|
comment: z23.string().optional(),
|
|
@@ -6377,7 +6453,7 @@ var firewallNatTools = [
|
|
|
6377
6453
|
} else if (a.chain === "dstnat" && !dstnatActions.includes(a.action)) {
|
|
6378
6454
|
return `Error: Invalid action '${a.action}' for dstnat. Must be one of: ${dstnatActions.join(", ")}`;
|
|
6379
6455
|
}
|
|
6380
|
-
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();
|
|
6381
6457
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
6382
6458
|
const trimmed = result.trim();
|
|
6383
6459
|
if (looksLikeError(trimmed)) {
|
|
@@ -6475,6 +6551,12 @@ ${result}`;
|
|
|
6475
6551
|
protocol: z23.string().optional(),
|
|
6476
6552
|
in_interface: z23.string().optional(),
|
|
6477
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(),
|
|
6478
6560
|
to_addresses: z23.string().optional(),
|
|
6479
6561
|
to_ports: z23.string().optional(),
|
|
6480
6562
|
comment: z23.string().optional(),
|
|
@@ -6567,10 +6649,33 @@ async function updateMangleRule(a, ctx) {
|
|
|
6567
6649
|
put("protocol", a.protocol);
|
|
6568
6650
|
put("in-interface", a.in_interface);
|
|
6569
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);
|
|
6570
6658
|
put("connection-mark", a.connection_mark);
|
|
6571
6659
|
put("packet-mark", a.packet_mark);
|
|
6572
6660
|
put("routing-mark", a.routing_mark);
|
|
6573
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);
|
|
6574
6679
|
put("new-connection-mark", a.new_connection_mark);
|
|
6575
6680
|
put("new-packet-mark", a.new_packet_mark);
|
|
6576
6681
|
put("new-routing-mark", a.new_routing_mark);
|
|
@@ -6607,7 +6712,7 @@ var firewallMangleTools = [
|
|
|
6607
6712
|
name: "create_mangle_rule",
|
|
6608
6713
|
title: "Create IPv4 Firewall Mangle Rule",
|
|
6609
6714
|
annotations: WRITE,
|
|
6610
|
-
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`.",
|
|
6611
6716
|
inputSchema: {
|
|
6612
6717
|
chain: z24.enum(["prerouting", "input", "forward", "output", "postrouting"]),
|
|
6613
6718
|
action: z24.enum([
|
|
@@ -6637,12 +6742,35 @@ var firewallMangleTools = [
|
|
|
6637
6742
|
src_port: z24.string().optional(),
|
|
6638
6743
|
dst_port: z24.string().optional(),
|
|
6639
6744
|
protocol: z24.string().optional(),
|
|
6640
|
-
in_interface: z24.string().optional(),
|
|
6745
|
+
in_interface: z24.string().optional().describe('Negatable, e.g. "!ether1"'),
|
|
6641
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(),
|
|
6642
6753
|
connection_mark: z24.string().optional(),
|
|
6643
6754
|
packet_mark: z24.string().optional(),
|
|
6644
6755
|
routing_mark: z24.string().optional(),
|
|
6645
|
-
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(),
|
|
6646
6774
|
new_connection_mark: z24.string().optional(),
|
|
6647
6775
|
new_packet_mark: z24.string().optional(),
|
|
6648
6776
|
new_routing_mark: z24.string().optional(),
|
|
@@ -6661,7 +6789,7 @@ var firewallMangleTools = [
|
|
|
6661
6789
|
},
|
|
6662
6790
|
async handler(a, ctx) {
|
|
6663
6791
|
ctx.info(`Creating mangle rule: chain=${a.chain}, action=${a.action}`);
|
|
6664
|
-
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();
|
|
6665
6793
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
6666
6794
|
const trimmed = result.trim();
|
|
6667
6795
|
if (looksLikeError(trimmed)) {
|
|
@@ -6755,10 +6883,33 @@ ${result}`;
|
|
|
6755
6883
|
protocol: z24.string().optional(),
|
|
6756
6884
|
in_interface: z24.string().optional(),
|
|
6757
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(),
|
|
6758
6892
|
connection_mark: z24.string().optional(),
|
|
6759
6893
|
packet_mark: z24.string().optional(),
|
|
6760
6894
|
routing_mark: z24.string().optional(),
|
|
6761
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(),
|
|
6762
6913
|
new_connection_mark: z24.string().optional(),
|
|
6763
6914
|
new_packet_mark: z24.string().optional(),
|
|
6764
6915
|
new_routing_mark: z24.string().optional(),
|
|
@@ -23449,7 +23600,10 @@ async function featureRoutes(req, url) {
|
|
|
23449
23600
|
const ctx = createContext(undefined, b?.device);
|
|
23450
23601
|
const r = await createLocalBackup(ctx, {
|
|
23451
23602
|
label: b?.label,
|
|
23452
|
-
showSensitive: b?.show_sensitive === true
|
|
23603
|
+
showSensitive: b?.show_sensitive === true,
|
|
23604
|
+
verbose: b?.verbose === true,
|
|
23605
|
+
compact: b?.compact === true,
|
|
23606
|
+
terse: b?.terse === true
|
|
23453
23607
|
});
|
|
23454
23608
|
return r.ok ? json(r) : json({ error: r.error ?? "export failed" }, 502);
|
|
23455
23609
|
}
|
|
@@ -23768,7 +23922,7 @@ function registerPrompts(server) {
|
|
|
23768
23922
|
// package.json
|
|
23769
23923
|
var package_default = {
|
|
23770
23924
|
name: "@usex/mikrotik-mcp",
|
|
23771
|
-
version: "3.
|
|
23925
|
+
version: "3.20.0",
|
|
23772
23926
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
23773
23927
|
keywords: [
|
|
23774
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
|
}
|