@usex/mikrotik-mcp 3.19.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +194 -31
- package/dist/index.d.ts +16 -2
- package/dist/index.js +194 -31
- package/package.json +1 -1
- package/schemas/tool-catalog.json +207 -3
- 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
|
@@ -617,7 +617,7 @@ class MikroTikMacTelnetClient {
|
|
|
617
617
|
return false;
|
|
618
618
|
}
|
|
619
619
|
}
|
|
620
|
-
async run(command) {
|
|
620
|
+
async run(command, _opts = {}) {
|
|
621
621
|
if (!this.console || !this.console.isReady) {
|
|
622
622
|
throw new Error("Not connected to MikroTik device (MAC-Telnet)");
|
|
623
623
|
}
|
|
@@ -694,7 +694,7 @@ class MikroTikSSHClient {
|
|
|
694
694
|
}).connect(cfg);
|
|
695
695
|
});
|
|
696
696
|
}
|
|
697
|
-
run(command) {
|
|
697
|
+
run(command, opts = {}) {
|
|
698
698
|
if (!this.client) {
|
|
699
699
|
return Promise.reject(new Error("Not connected to MikroTik device"));
|
|
700
700
|
}
|
|
@@ -707,11 +707,30 @@ class MikroTikSSHClient {
|
|
|
707
707
|
}
|
|
708
708
|
const stdout = [];
|
|
709
709
|
const stderrBuf = [];
|
|
710
|
-
|
|
710
|
+
let settled = false;
|
|
711
|
+
let timer;
|
|
712
|
+
const finish = () => {
|
|
713
|
+
if (settled)
|
|
714
|
+
return;
|
|
715
|
+
settled = true;
|
|
716
|
+
if (timer)
|
|
717
|
+
clearTimeout(timer);
|
|
711
718
|
const out = decodeOutput(Buffer.concat(stdout));
|
|
712
719
|
const error = decodeOutput(Buffer.concat(stderrBuf));
|
|
713
720
|
resolve2(error && !out ? error : out);
|
|
714
|
-
}
|
|
721
|
+
};
|
|
722
|
+
if (opts.maxMs && opts.maxMs > 0) {
|
|
723
|
+
timer = setTimeout(() => {
|
|
724
|
+
try {
|
|
725
|
+
stream.signal("INT");
|
|
726
|
+
} catch {}
|
|
727
|
+
try {
|
|
728
|
+
stream.close();
|
|
729
|
+
} catch {}
|
|
730
|
+
finish();
|
|
731
|
+
}, opts.maxMs);
|
|
732
|
+
}
|
|
733
|
+
stream.on("close", finish).on("data", (d) => stdout.push(d)).stderr.on("data", (d) => stderrBuf.push(d));
|
|
715
734
|
});
|
|
716
735
|
});
|
|
717
736
|
}
|
|
@@ -782,7 +801,6 @@ import { z as z3 } from "zod";
|
|
|
782
801
|
|
|
783
802
|
// src/ssh/safe-mode.ts
|
|
784
803
|
var PROMPT_RE = /\[.+?@.+?\] (?:<SAFE> )?> ?$/m;
|
|
785
|
-
var NORMAL_PROMPT_RE = /\[.+?@.+?\] > ?$/m;
|
|
786
804
|
var ANSI_RE = /\x1B(?:\[[0-9;]*[mA-HJ-MSTfhilnprsu]|[()][0-9A-Za-z]|\[?\?\d+[hl])/g;
|
|
787
805
|
function stripAnsi(text) {
|
|
788
806
|
return text.replace(ANSI_RE, "");
|
|
@@ -793,13 +811,17 @@ function isSafeModeActivated(response) {
|
|
|
793
811
|
return true;
|
|
794
812
|
return /safe mode[^\n]*\b(?:success|taken|enabled|active)\b/i.test(response);
|
|
795
813
|
}
|
|
796
|
-
function
|
|
814
|
+
function lastNonEmptyLine(response) {
|
|
797
815
|
const lines = response.replace(/\r/g, "").split(`
|
|
798
816
|
`).map((l) => l.trimEnd()).filter(Boolean);
|
|
799
|
-
|
|
800
|
-
|
|
817
|
+
return lines.at(-1) ?? "";
|
|
818
|
+
}
|
|
819
|
+
function classifyPrompt(response) {
|
|
820
|
+
const last = lastNonEmptyLine(response);
|
|
821
|
+
if (!PROMPT_RE.test(last))
|
|
822
|
+
return "unknown";
|
|
823
|
+
return last.includes("<SAFE>") ? "safe" : "released";
|
|
801
824
|
}
|
|
802
|
-
|
|
803
825
|
class SafeModeManager {
|
|
804
826
|
deviceName;
|
|
805
827
|
ssh = null;
|
|
@@ -874,13 +896,30 @@ class SafeModeManager {
|
|
|
874
896
|
return this.lock(async () => {
|
|
875
897
|
if (!this.active || !this.channel)
|
|
876
898
|
return "Safe mode is not active. Nothing to commit.";
|
|
899
|
+
this.channel.write(`
|
|
900
|
+
`);
|
|
901
|
+
const probe = await this.readSettledPrompt();
|
|
902
|
+
const before = classifyPrompt(probe);
|
|
903
|
+
if (before === "released") {
|
|
904
|
+
this.cleanup();
|
|
905
|
+
return "Safe mode already exited \u2014 your changes are committed. Safe mode DISABLED.";
|
|
906
|
+
}
|
|
907
|
+
if (before === "unknown") {
|
|
908
|
+
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)}`;
|
|
909
|
+
}
|
|
877
910
|
this.channel.write(CTRL_X);
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
911
|
+
this.channel.write(`
|
|
912
|
+
`);
|
|
913
|
+
const after = await this.readSettledPrompt();
|
|
914
|
+
switch (classifyPrompt(after)) {
|
|
915
|
+
case "released":
|
|
916
|
+
this.cleanup();
|
|
917
|
+
return "Changes committed successfully. Safe mode DISABLED.";
|
|
918
|
+
case "safe":
|
|
919
|
+
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.";
|
|
920
|
+
default:
|
|
921
|
+
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
922
|
}
|
|
882
|
-
this.cleanup();
|
|
883
|
-
return "Changes committed successfully. Safe mode DISABLED.";
|
|
884
923
|
});
|
|
885
924
|
}
|
|
886
925
|
rollback() {
|
|
@@ -916,6 +955,33 @@ class SafeModeManager {
|
|
|
916
955
|
channel.on("data", onData);
|
|
917
956
|
});
|
|
918
957
|
}
|
|
958
|
+
readSettledPrompt(quietMs = 450, maxMs = 8000) {
|
|
959
|
+
const channel = this.channel;
|
|
960
|
+
if (!channel)
|
|
961
|
+
return Promise.resolve("");
|
|
962
|
+
return new Promise((resolve2) => {
|
|
963
|
+
let buf = "";
|
|
964
|
+
let quiet;
|
|
965
|
+
let hard;
|
|
966
|
+
function done() {
|
|
967
|
+
clearTimeout(hard);
|
|
968
|
+
if (quiet)
|
|
969
|
+
clearTimeout(quiet);
|
|
970
|
+
channel.removeListener("data", onData);
|
|
971
|
+
resolve2(stripAnsi(buf));
|
|
972
|
+
}
|
|
973
|
+
function onData(chunk) {
|
|
974
|
+
buf += decodeOutput(chunk);
|
|
975
|
+
if (PROMPT_RE.test(lastNonEmptyLine(stripAnsi(buf)))) {
|
|
976
|
+
if (quiet)
|
|
977
|
+
clearTimeout(quiet);
|
|
978
|
+
quiet = setTimeout(done, quietMs);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
hard = setTimeout(done, maxMs);
|
|
982
|
+
channel.on("data", onData);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
919
985
|
extractOutput(raw, command) {
|
|
920
986
|
const text = raw.replace(/\r\n/g, `
|
|
921
987
|
`).replace(/\r/g, `
|
|
@@ -963,7 +1029,7 @@ function getSafeModeManager(deviceName) {
|
|
|
963
1029
|
}
|
|
964
1030
|
|
|
965
1031
|
// src/core/connector.ts
|
|
966
|
-
async function runOnce(command, deviceName) {
|
|
1032
|
+
async function runOnce(command, deviceName, opts) {
|
|
967
1033
|
const name = resolveDeviceName(deviceName);
|
|
968
1034
|
const dc = getDevice(deviceName);
|
|
969
1035
|
const client = createDeviceClient(dc);
|
|
@@ -971,12 +1037,12 @@ async function runOnce(command, deviceName) {
|
|
|
971
1037
|
if (!await client.connect()) {
|
|
972
1038
|
throw new Error(connectErrorMessage(name, dc, client.lastError));
|
|
973
1039
|
}
|
|
974
|
-
return await client.run(command);
|
|
1040
|
+
return await client.run(command, opts);
|
|
975
1041
|
} finally {
|
|
976
1042
|
client.disconnect();
|
|
977
1043
|
}
|
|
978
1044
|
}
|
|
979
|
-
async function executeMikrotikCommand(command, ctx) {
|
|
1045
|
+
async function executeMikrotikCommand(command, ctx, opts) {
|
|
980
1046
|
const deviceName = resolveDeviceName(ctx.device);
|
|
981
1047
|
const safe = getSafeModeManager(deviceName);
|
|
982
1048
|
if (safe.isActive) {
|
|
@@ -984,7 +1050,7 @@ async function executeMikrotikCommand(command, ctx) {
|
|
|
984
1050
|
return safe.execute(command);
|
|
985
1051
|
}
|
|
986
1052
|
ctx.info(`[${deviceName}] Executing MikroTik command: ${command}`);
|
|
987
|
-
return runOnce(command, ctx.device);
|
|
1053
|
+
return runOnce(command, ctx.device, opts);
|
|
988
1054
|
}
|
|
989
1055
|
|
|
990
1056
|
// src/core/registry.ts
|
|
@@ -1006,7 +1072,7 @@ function createContext(sendLog, device) {
|
|
|
1006
1072
|
}
|
|
1007
1073
|
|
|
1008
1074
|
// src/core/routeros.ts
|
|
1009
|
-
var BARE_SAFE = /^[\w
|
|
1075
|
+
var BARE_SAFE = /^[\w.\-:/,*@!]+$/;
|
|
1010
1076
|
function quoteValue(value) {
|
|
1011
1077
|
if (typeof value === "number" || typeof value === "boolean")
|
|
1012
1078
|
return String(value);
|
|
@@ -1077,6 +1143,16 @@ function isEmpty(result) {
|
|
|
1077
1143
|
const t = result.trim();
|
|
1078
1144
|
return t === "" || t === "no such item" || t === "no such item (4)";
|
|
1079
1145
|
}
|
|
1146
|
+
function flattenLiveOutput(text) {
|
|
1147
|
+
return text.split(`
|
|
1148
|
+
`).map((line) => {
|
|
1149
|
+
const segs = line.split("\r").filter((s) => s.trim() !== "");
|
|
1150
|
+
return segs.length ? segs[segs.length - 1] : "";
|
|
1151
|
+
}).join(`
|
|
1152
|
+
`).replace(/\n{3,}/g, `
|
|
1153
|
+
|
|
1154
|
+
`).trim();
|
|
1155
|
+
}
|
|
1080
1156
|
function extractCreatedId(output) {
|
|
1081
1157
|
const star = output.match(/\*[0-9A-Fa-f]+/);
|
|
1082
1158
|
if (star)
|
|
@@ -6317,6 +6393,12 @@ async function updateNatRule(a, ctx) {
|
|
|
6317
6393
|
put("protocol", a.protocol);
|
|
6318
6394
|
put("in-interface", a.in_interface);
|
|
6319
6395
|
put("out-interface", a.out_interface);
|
|
6396
|
+
put("in-interface-list", a.in_interface_list);
|
|
6397
|
+
put("out-interface-list", a.out_interface_list);
|
|
6398
|
+
put("src-address-list", a.src_address_list);
|
|
6399
|
+
put("dst-address-list", a.dst_address_list);
|
|
6400
|
+
put("connection-mark", a.connection_mark);
|
|
6401
|
+
put("connection-nat-state", a.connection_nat_state);
|
|
6320
6402
|
put("to-addresses", a.to_addresses);
|
|
6321
6403
|
put("to-ports", a.to_ports);
|
|
6322
6404
|
if (a.comment !== undefined)
|
|
@@ -6353,8 +6435,14 @@ var firewallNatTools = [
|
|
|
6353
6435
|
src_port: z23.string().optional(),
|
|
6354
6436
|
dst_port: z23.string().optional(),
|
|
6355
6437
|
protocol: z23.string().optional(),
|
|
6356
|
-
in_interface: z23.string().optional(),
|
|
6438
|
+
in_interface: z23.string().optional().describe('Negatable, e.g. "!ether1"'),
|
|
6357
6439
|
out_interface: z23.string().optional(),
|
|
6440
|
+
in_interface_list: z23.string().optional().describe('Interface list, e.g. "WAN" or "!LAN"'),
|
|
6441
|
+
out_interface_list: z23.string().optional(),
|
|
6442
|
+
src_address_list: z23.string().optional().describe('Match src in a named list; negate "!name"'),
|
|
6443
|
+
dst_address_list: z23.string().optional().describe('Match dst in a named list; negate "!name"'),
|
|
6444
|
+
connection_mark: z23.string().optional(),
|
|
6445
|
+
connection_nat_state: z23.string().optional().describe('"srcnat" / "dstnat" / "!dstnat"'),
|
|
6358
6446
|
to_addresses: z23.string().optional().describe('Single IP or range e.g. "10.0.0.1" or "10.0.0.1-10.0.0.10"'),
|
|
6359
6447
|
to_ports: z23.string().optional().describe('Single port or range e.g. "8080" or "8080-8090"'),
|
|
6360
6448
|
comment: z23.string().optional(),
|
|
@@ -6394,7 +6482,7 @@ var firewallNatTools = [
|
|
|
6394
6482
|
} else if (a.chain === "dstnat" && !dstnatActions.includes(a.action)) {
|
|
6395
6483
|
return `Error: Invalid action '${a.action}' for dstnat. Must be one of: ${dstnatActions.join(", ")}`;
|
|
6396
6484
|
}
|
|
6397
|
-
const cmd = new Cmd("/ip firewall nat add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("to-addresses", a.to_addresses).opt("to-ports", a.to_ports).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
6485
|
+
const cmd = new Cmd("/ip firewall nat add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("in-interface-list", a.in_interface_list).opt("out-interface-list", a.out_interface_list).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("connection-mark", a.connection_mark).opt("connection-nat-state", a.connection_nat_state).opt("to-addresses", a.to_addresses).opt("to-ports", a.to_ports).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
6398
6486
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
6399
6487
|
const trimmed = result.trim();
|
|
6400
6488
|
if (looksLikeError(trimmed)) {
|
|
@@ -6492,6 +6580,12 @@ ${result}`;
|
|
|
6492
6580
|
protocol: z23.string().optional(),
|
|
6493
6581
|
in_interface: z23.string().optional(),
|
|
6494
6582
|
out_interface: z23.string().optional(),
|
|
6583
|
+
in_interface_list: z23.string().optional(),
|
|
6584
|
+
out_interface_list: z23.string().optional(),
|
|
6585
|
+
src_address_list: z23.string().optional().describe('Negate with "!name"'),
|
|
6586
|
+
dst_address_list: z23.string().optional().describe('Negate with "!name"'),
|
|
6587
|
+
connection_mark: z23.string().optional(),
|
|
6588
|
+
connection_nat_state: z23.string().optional(),
|
|
6495
6589
|
to_addresses: z23.string().optional(),
|
|
6496
6590
|
to_ports: z23.string().optional(),
|
|
6497
6591
|
comment: z23.string().optional(),
|
|
@@ -6584,10 +6678,33 @@ async function updateMangleRule(a, ctx) {
|
|
|
6584
6678
|
put("protocol", a.protocol);
|
|
6585
6679
|
put("in-interface", a.in_interface);
|
|
6586
6680
|
put("out-interface", a.out_interface);
|
|
6681
|
+
put("in-interface-list", a.in_interface_list);
|
|
6682
|
+
put("out-interface-list", a.out_interface_list);
|
|
6683
|
+
put("src-address-list", a.src_address_list);
|
|
6684
|
+
put("dst-address-list", a.dst_address_list);
|
|
6685
|
+
put("src-address-type", a.src_address_type);
|
|
6686
|
+
put("dst-address-type", a.dst_address_type);
|
|
6587
6687
|
put("connection-mark", a.connection_mark);
|
|
6588
6688
|
put("packet-mark", a.packet_mark);
|
|
6589
6689
|
put("routing-mark", a.routing_mark);
|
|
6590
6690
|
put("connection-state", a.connection_state);
|
|
6691
|
+
put("connection-nat-state", a.connection_nat_state);
|
|
6692
|
+
put("connection-type", a.connection_type);
|
|
6693
|
+
put("connection-bytes", a.connection_bytes);
|
|
6694
|
+
put("connection-limit", a.connection_limit);
|
|
6695
|
+
put("connection-rate", a.connection_rate);
|
|
6696
|
+
put("per-connection-classifier", a.per_connection_classifier);
|
|
6697
|
+
put("tcp-flags", a.tcp_flags);
|
|
6698
|
+
put("dscp", a.dscp);
|
|
6699
|
+
put("priority", a.priority);
|
|
6700
|
+
put("packet-size", a.packet_size);
|
|
6701
|
+
put("layer7-protocol", a.layer7_protocol);
|
|
6702
|
+
put("ipsec-policy", a.ipsec_policy);
|
|
6703
|
+
put("nth", a.nth);
|
|
6704
|
+
put("random", a.random);
|
|
6705
|
+
put("time", a.time);
|
|
6706
|
+
put("hotspot", a.hotspot);
|
|
6707
|
+
put("p2p", a.p2p);
|
|
6591
6708
|
put("new-connection-mark", a.new_connection_mark);
|
|
6592
6709
|
put("new-packet-mark", a.new_packet_mark);
|
|
6593
6710
|
put("new-routing-mark", a.new_routing_mark);
|
|
@@ -6624,7 +6741,7 @@ var firewallMangleTools = [
|
|
|
6624
6741
|
name: "create_mangle_rule",
|
|
6625
6742
|
title: "Create IPv4 Firewall Mangle Rule",
|
|
6626
6743
|
annotations: WRITE,
|
|
6627
|
-
description: "Creates an IPv4 mangle rule (`/ip firewall mangle add`) \u2014 the packet-marking and header-modification table, " + "used to mark connections/packets/routing (for policy routing, QoS and per-connection classification) or to change DSCP/TTL/MSS. " + "For accept/drop decisions use create_filter_rule; for address translation use create_nat_rule; for IPv6 mangle use create_ipv6_mangle_rule. " + "chain: prerouting/input/forward/output/postrouting. " + "action: mark-connection/mark-packet/mark-routing/change-dscp/change-ttl/change-mss/add-src-to-address-list/add-dst-to-address-list/fasttrack-connection/route/set-priority/accept/etc. " + "Set the matching new-*-mark field for mark-* actions and keep passthrough=true so later rules can also match the same packet; " + "for add-*-to-address-list set address_list (and optionally address_list_timeout). " + "place_before accepts a rule number or ID (*N) to control insertion position. " + "Returns the created rule's detail including its `.id`.",
|
|
6744
|
+
description: "Creates an IPv4 mangle rule (`/ip firewall mangle add`) \u2014 the packet-marking and header-modification table, " + "used to mark connections/packets/routing (for policy routing, QoS and per-connection classification) or to change DSCP/TTL/MSS. " + "For accept/drop decisions use create_filter_rule; for address translation use create_nat_rule; for IPv6 mangle use create_ipv6_mangle_rule. " + "chain: prerouting/input/forward/output/postrouting. " + "action: mark-connection/mark-packet/mark-routing/change-dscp/change-ttl/change-mss/add-src-to-address-list/add-dst-to-address-list/fasttrack-connection/route/set-priority/accept/etc. " + "Set the matching new-*-mark field for mark-* actions and keep passthrough=true so later rules can also match the same packet; " + "for add-*-to-address-list set address_list (and optionally address_list_timeout). " + "Full match surface: address-lists (src_address_list/dst_address_list \u2014 negate with a leading '!', e.g. dst_address_list='!IR' to route traffic NOT bound for a list), " + "interface-lists, per_connection_classifier (PCC load-balancing), connection_nat_state, tcp_flags, dscp, layer7_protocol, time, and more. " + "Typical policy-routing rule: chain=prerouting, src_address=<lan>, dst_address_list='!IR', action=mark-routing, new_routing_mark=<table>, then a routing rule/route uses that table. " + "place_before accepts a rule number or ID (*N) to control insertion position. " + "Returns the created rule's detail including its `.id`.",
|
|
6628
6745
|
inputSchema: {
|
|
6629
6746
|
chain: z24.enum(["prerouting", "input", "forward", "output", "postrouting"]),
|
|
6630
6747
|
action: z24.enum([
|
|
@@ -6654,12 +6771,35 @@ var firewallMangleTools = [
|
|
|
6654
6771
|
src_port: z24.string().optional(),
|
|
6655
6772
|
dst_port: z24.string().optional(),
|
|
6656
6773
|
protocol: z24.string().optional(),
|
|
6657
|
-
in_interface: z24.string().optional(),
|
|
6774
|
+
in_interface: z24.string().optional().describe('Negatable, e.g. "!ether1"'),
|
|
6658
6775
|
out_interface: z24.string().optional(),
|
|
6776
|
+
in_interface_list: z24.string().optional().describe('Interface list, negatable e.g. "!WAN"'),
|
|
6777
|
+
out_interface_list: z24.string().optional(),
|
|
6778
|
+
src_address_list: z24.string().optional().describe('Match src in a named list; negate "!name"'),
|
|
6779
|
+
dst_address_list: z24.string().optional().describe('Match dst in a named list; negate "!name" (e.g. "!IR" routes foreign traffic)'),
|
|
6780
|
+
src_address_type: z24.string().optional().describe('e.g. "local", "unicast", "!local"'),
|
|
6781
|
+
dst_address_type: z24.string().optional(),
|
|
6659
6782
|
connection_mark: z24.string().optional(),
|
|
6660
6783
|
packet_mark: z24.string().optional(),
|
|
6661
6784
|
routing_mark: z24.string().optional(),
|
|
6662
|
-
connection_state: z24.string().optional().describe('e.g. "new", "established,related", "invalid"'),
|
|
6785
|
+
connection_state: z24.string().optional().describe('e.g. "new", "established,related", "!invalid"'),
|
|
6786
|
+
connection_nat_state: z24.string().optional().describe('"srcnat" / "dstnat" / "!dstnat"'),
|
|
6787
|
+
connection_type: z24.string().optional().describe('Helper, e.g. "sip", "ftp"'),
|
|
6788
|
+
connection_bytes: z24.string().optional().describe('e.g. "1000000-0" (>1 MB connections)'),
|
|
6789
|
+
connection_limit: z24.string().optional().describe('e.g. "100,32"'),
|
|
6790
|
+
connection_rate: z24.string().optional().describe('e.g. "100k-1M"'),
|
|
6791
|
+
per_connection_classifier: z24.string().optional().describe('PCC for load balancing, e.g. "both-addresses:2/0"'),
|
|
6792
|
+
tcp_flags: z24.string().optional().describe('RouterOS flag expression e.g. "syn,!ack"'),
|
|
6793
|
+
dscp: z24.string().optional().describe("Match incoming DSCP (0-63)"),
|
|
6794
|
+
priority: z24.string().optional().describe("Match packet/queue priority (0-63)"),
|
|
6795
|
+
packet_size: z24.string().optional().describe('e.g. "1500" or "0-500"'),
|
|
6796
|
+
layer7_protocol: z24.string().optional().describe("Name of an /ip firewall layer7-protocol regex"),
|
|
6797
|
+
ipsec_policy: z24.string().optional().describe('e.g. "in,ipsec" or "out,none"'),
|
|
6798
|
+
nth: z24.string().optional().describe('e.g. "2,1" \u2014 every 2nd packet'),
|
|
6799
|
+
random: z24.string().optional().describe("Match a random N% of packets (1-99)"),
|
|
6800
|
+
time: z24.string().optional().describe('e.g. "8h-16h,mon,tue,wed,thu,fri"'),
|
|
6801
|
+
hotspot: z24.string().optional().describe('e.g. "auth", "!auth", "from-client"'),
|
|
6802
|
+
p2p: z24.string().optional(),
|
|
6663
6803
|
new_connection_mark: z24.string().optional(),
|
|
6664
6804
|
new_packet_mark: z24.string().optional(),
|
|
6665
6805
|
new_routing_mark: z24.string().optional(),
|
|
@@ -6678,7 +6818,7 @@ var firewallMangleTools = [
|
|
|
6678
6818
|
},
|
|
6679
6819
|
async handler(a, ctx) {
|
|
6680
6820
|
ctx.info(`Creating mangle rule: chain=${a.chain}, action=${a.action}`);
|
|
6681
|
-
const cmd = new Cmd("/ip firewall mangle add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("connection-mark", a.connection_mark).opt("packet-mark", a.packet_mark).opt("routing-mark", a.routing_mark).opt("connection-state", a.connection_state).opt("new-connection-mark", a.new_connection_mark).opt("new-packet-mark", a.new_packet_mark).opt("new-routing-mark", a.new_routing_mark).opt("new-dscp", a.new_dscp).opt("new-ttl", a.new_ttl).opt("new-mss", a.new_mss).opt("routing-table", a.routing_table).opt("address-list", a.address_list).opt("address-list-timeout", a.address_list_timeout).bool("passthrough", a.passthrough).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
6821
|
+
const cmd = new Cmd("/ip firewall mangle add").set("chain", a.chain).set("action", a.action).opt("src-address", a.src_address).opt("dst-address", a.dst_address).opt("src-port", a.src_port).opt("dst-port", a.dst_port).opt("protocol", a.protocol).opt("in-interface", a.in_interface).opt("out-interface", a.out_interface).opt("in-interface-list", a.in_interface_list).opt("out-interface-list", a.out_interface_list).opt("src-address-list", a.src_address_list).opt("dst-address-list", a.dst_address_list).opt("src-address-type", a.src_address_type).opt("dst-address-type", a.dst_address_type).opt("connection-mark", a.connection_mark).opt("packet-mark", a.packet_mark).opt("routing-mark", a.routing_mark).opt("connection-state", a.connection_state).opt("connection-nat-state", a.connection_nat_state).opt("connection-type", a.connection_type).opt("connection-bytes", a.connection_bytes).opt("connection-limit", a.connection_limit).opt("connection-rate", a.connection_rate).opt("per-connection-classifier", a.per_connection_classifier).opt("tcp-flags", a.tcp_flags).opt("dscp", a.dscp).opt("priority", a.priority).opt("packet-size", a.packet_size).opt("layer7-protocol", a.layer7_protocol).opt("ipsec-policy", a.ipsec_policy).opt("nth", a.nth).opt("random", a.random).opt("time", a.time).opt("hotspot", a.hotspot).opt("p2p", a.p2p).opt("new-connection-mark", a.new_connection_mark).opt("new-packet-mark", a.new_packet_mark).opt("new-routing-mark", a.new_routing_mark).opt("new-dscp", a.new_dscp).opt("new-ttl", a.new_ttl).opt("new-mss", a.new_mss).opt("routing-table", a.routing_table).opt("address-list", a.address_list).opt("address-list-timeout", a.address_list_timeout).bool("passthrough", a.passthrough).opt("comment", a.comment).flag("disabled", a.disabled).flag("log", a.log).opt("log-prefix", a.log ? a.log_prefix : undefined).opt("place-before", a.place_before).build();
|
|
6682
6822
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
6683
6823
|
const trimmed = result.trim();
|
|
6684
6824
|
if (looksLikeError(trimmed)) {
|
|
@@ -6772,10 +6912,33 @@ ${result}`;
|
|
|
6772
6912
|
protocol: z24.string().optional(),
|
|
6773
6913
|
in_interface: z24.string().optional(),
|
|
6774
6914
|
out_interface: z24.string().optional(),
|
|
6915
|
+
in_interface_list: z24.string().optional(),
|
|
6916
|
+
out_interface_list: z24.string().optional(),
|
|
6917
|
+
src_address_list: z24.string().optional().describe('Negate with "!name" (e.g. "!IR")'),
|
|
6918
|
+
dst_address_list: z24.string().optional().describe('Negate with "!name" (e.g. "!IR")'),
|
|
6919
|
+
src_address_type: z24.string().optional(),
|
|
6920
|
+
dst_address_type: z24.string().optional(),
|
|
6775
6921
|
connection_mark: z24.string().optional(),
|
|
6776
6922
|
packet_mark: z24.string().optional(),
|
|
6777
6923
|
routing_mark: z24.string().optional(),
|
|
6778
6924
|
connection_state: z24.string().optional(),
|
|
6925
|
+
connection_nat_state: z24.string().optional(),
|
|
6926
|
+
connection_type: z24.string().optional(),
|
|
6927
|
+
connection_bytes: z24.string().optional(),
|
|
6928
|
+
connection_limit: z24.string().optional(),
|
|
6929
|
+
connection_rate: z24.string().optional(),
|
|
6930
|
+
per_connection_classifier: z24.string().optional(),
|
|
6931
|
+
tcp_flags: z24.string().optional(),
|
|
6932
|
+
dscp: z24.string().optional(),
|
|
6933
|
+
priority: z24.string().optional(),
|
|
6934
|
+
packet_size: z24.string().optional(),
|
|
6935
|
+
layer7_protocol: z24.string().optional(),
|
|
6936
|
+
ipsec_policy: z24.string().optional(),
|
|
6937
|
+
nth: z24.string().optional(),
|
|
6938
|
+
random: z24.string().optional(),
|
|
6939
|
+
time: z24.string().optional(),
|
|
6940
|
+
hotspot: z24.string().optional(),
|
|
6941
|
+
p2p: z24.string().optional(),
|
|
6779
6942
|
new_connection_mark: z24.string().optional(),
|
|
6780
6943
|
new_packet_mark: z24.string().optional(),
|
|
6781
6944
|
new_routing_mark: z24.string().optional(),
|
|
@@ -11272,7 +11435,7 @@ var networkToolTools = [
|
|
|
11272
11435
|
async handler(a, ctx) {
|
|
11273
11436
|
ctx.info(`Pinging ${a.address} (count=${a.count})`);
|
|
11274
11437
|
const cmd = new Cmd(`/ping ${a.address}`).set("count", a.count).opt("interface", a.interface).opt("src-address", a.src_address).opt("size", a.size).build();
|
|
11275
|
-
const result = await executeMikrotikCommand(cmd, ctx);
|
|
11438
|
+
const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.count * 1500 + 6000 }));
|
|
11276
11439
|
if (looksLikeError(result))
|
|
11277
11440
|
return `Failed to ping ${a.address}: ${result}`;
|
|
11278
11441
|
return isEmpty(result) ? `No response from ${a.address}.` : `PING ${a.address}:
|
|
@@ -11293,7 +11456,7 @@ ${result}`;
|
|
|
11293
11456
|
async handler(a, ctx) {
|
|
11294
11457
|
ctx.info(`Tracerouting ${a.address} (count=${a.count})`);
|
|
11295
11458
|
const cmd = new Cmd(`/tool traceroute ${a.address}`).set("count", a.count).flag("use-dns", a.use_dns).build();
|
|
11296
|
-
const result = await executeMikrotikCommand(cmd, ctx);
|
|
11459
|
+
const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: 30000 + a.count * 3000 }));
|
|
11297
11460
|
if (looksLikeError(result))
|
|
11298
11461
|
return `Failed to traceroute ${a.address}: ${result}`;
|
|
11299
11462
|
return isEmpty(result) ? `No route information for ${a.address}.` : `TRACEROUTE ${a.address}:
|
|
@@ -11317,7 +11480,7 @@ ${result}`;
|
|
|
11317
11480
|
async handler(a, ctx) {
|
|
11318
11481
|
ctx.info(`Bandwidth test to ${a.address} (duration=${a.duration}s, direction=${a.direction})`);
|
|
11319
11482
|
const cmd = new Cmd(`/tool bandwidth-test ${a.address}`).set("duration", `${a.duration}s`).set("direction", a.direction).set("protocol", a.protocol).opt("user", a.user).opt("password", a.password).build();
|
|
11320
|
-
const result = await executeMikrotikCommand(cmd, ctx);
|
|
11483
|
+
const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.duration * 1000 + 12000 }));
|
|
11321
11484
|
if (looksLikeError(result))
|
|
11322
11485
|
return `Failed to run bandwidth test to ${a.address}: ${result}`;
|
|
11323
11486
|
return isEmpty(result) ? `No bandwidth test results for ${a.address}.` : `BANDWIDTH TEST:
|
|
@@ -11969,7 +12132,7 @@ var floodPingTools = [
|
|
|
11969
12132
|
async handler(a, ctx) {
|
|
11970
12133
|
ctx.info(`Flood-pinging ${a.address} (count=${a.count})`);
|
|
11971
12134
|
const cmd = new Cmd(`/tool flood-ping ${a.address}`).set("count", a.count).opt("size", a.size).opt("interface", a.interface).opt("src-address", a.src_address).build();
|
|
11972
|
-
const result = await executeMikrotikCommand(cmd, ctx);
|
|
12135
|
+
const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: 30000 }));
|
|
11973
12136
|
if (looksLikeError(result))
|
|
11974
12137
|
return `Failed to flood-ping ${a.address}: ${result}`;
|
|
11975
12138
|
return isEmpty(result) ? `No response from ${a.address}.` : `FLOOD PING ${a.address}:
|
|
@@ -12633,7 +12796,7 @@ var speedTestTools = [
|
|
|
12633
12796
|
async handler(a, ctx) {
|
|
12634
12797
|
ctx.info(`Speed test to ${a.address} (duration=${a.duration}s, direction=${a.direction})`);
|
|
12635
12798
|
const cmd = new Cmd(`/tool speed-test address=${a.address}`).set("duration", `${a.duration}s`).set("direction", a.direction).opt("tcp-connection-count", a.tcp_connection_count).opt("user", a.user).opt("password", a.password).build();
|
|
12636
|
-
const result = await executeMikrotikCommand(cmd, ctx);
|
|
12799
|
+
const result = flattenLiveOutput(await executeMikrotikCommand(cmd, ctx, { maxMs: a.duration * 1000 + 15000 }));
|
|
12637
12800
|
if (looksLikeError(result))
|
|
12638
12801
|
return `Failed to run speed test to ${a.address}: ${result}`;
|
|
12639
12802
|
return isEmpty(result) ? `No speed-test results for ${a.address}.` : `SPEED TEST:
|
|
@@ -23788,7 +23951,7 @@ function registerPrompts(server) {
|
|
|
23788
23951
|
// package.json
|
|
23789
23952
|
var package_default = {
|
|
23790
23953
|
name: "@usex/mikrotik-mcp",
|
|
23791
|
-
version: "3.
|
|
23954
|
+
version: "3.21.0",
|
|
23792
23955
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
23793
23956
|
keywords: [
|
|
23794
23957
|
"ai",
|
package/dist/index.d.ts
CHANGED
|
@@ -26,8 +26,12 @@ type SendLog = (level: "info" | "error", message: string) => void;
|
|
|
26
26
|
*
|
|
27
27
|
* @param command Fully-formed RouterOS CLI command (e.g. `/ip address print`).
|
|
28
28
|
* @param ctx Per-call context carrying the target device.
|
|
29
|
+
* @param opts `maxMs` caps the one-shot read for interactive/streaming
|
|
30
|
+
* commands (ping, bandwidth-test) so they can't hang the tool.
|
|
29
31
|
*/
|
|
30
|
-
declare function executeMikrotikCommand(command: string, ctx: ToolContext
|
|
32
|
+
declare function executeMikrotikCommand(command: string, ctx: ToolContext, opts?: {
|
|
33
|
+
maxMs?: number;
|
|
34
|
+
}): Promise<string>;
|
|
31
35
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
32
36
|
import { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
33
37
|
import { ZodRawShape } from "zod";
|
|
@@ -147,7 +151,9 @@ declare class MikroTikSSHClient {
|
|
|
147
151
|
/** Establish the SSH connection. Resolves `true` on success, `false` on failure. */
|
|
148
152
|
connect(): Promise<boolean>;
|
|
149
153
|
/** Run a single command on a fresh SSH channel and return its decoded output. */
|
|
150
|
-
run(command: string
|
|
154
|
+
run(command: string, opts?: {
|
|
155
|
+
maxMs?: number;
|
|
156
|
+
}): Promise<string>;
|
|
151
157
|
/** Open a persistent interactive shell channel (used by Safe Mode). */
|
|
152
158
|
shell(opts?: {
|
|
153
159
|
term?: string;
|
|
@@ -185,6 +191,14 @@ declare class SafeModeManager {
|
|
|
185
191
|
* (safe-mode marker present/absent), not merely the first prompt-shaped line.
|
|
186
192
|
*/
|
|
187
193
|
private readUntilPrompt;
|
|
194
|
+
/**
|
|
195
|
+
* Read until the output SETTLES: once any prompt is visible, wait for a quiet
|
|
196
|
+
* gap (`quietMs` with no new bytes) before resolving, or give up at `maxMs`.
|
|
197
|
+
* Used by commit, where Ctrl+X + Enter can emit a transient `<SAFE>` redraw
|
|
198
|
+
* followed by the real post-commit prompt — settling on the LAST prompt after
|
|
199
|
+
* a quiet period is what makes mode detection reliable.
|
|
200
|
+
*/
|
|
201
|
+
private readSettledPrompt;
|
|
188
202
|
private extractOutput;
|
|
189
203
|
private cleanup;
|
|
190
204
|
}
|