@usex/mikrotik-mcp 3.21.0 → 3.22.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 +56 -46
- package/dist/index.d.ts +23 -8
- package/dist/index.js +56 -46
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -894,31 +894,39 @@ class SafeModeManager {
|
|
|
894
894
|
}
|
|
895
895
|
commit() {
|
|
896
896
|
return this.lock(async () => {
|
|
897
|
-
if (!this.active || !this.channel)
|
|
898
|
-
return "Safe mode is not active. Nothing to commit.";
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const probe = await this.readSettledPrompt();
|
|
902
|
-
const before = classifyPrompt(probe);
|
|
897
|
+
if (!this.active || !this.channel) {
|
|
898
|
+
return { ok: true, message: "Safe mode is not active. Nothing to commit." };
|
|
899
|
+
}
|
|
900
|
+
const before = await this.probeMode();
|
|
903
901
|
if (before === "released") {
|
|
904
902
|
this.cleanup();
|
|
905
|
-
return
|
|
903
|
+
return {
|
|
904
|
+
ok: true,
|
|
905
|
+
message: "Safe mode already exited \u2014 your changes are committed. Safe mode DISABLED."
|
|
906
|
+
};
|
|
906
907
|
}
|
|
907
908
|
if (before === "unknown") {
|
|
908
|
-
return
|
|
909
|
+
return {
|
|
910
|
+
ok: false,
|
|
911
|
+
message: "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."
|
|
912
|
+
};
|
|
909
913
|
}
|
|
910
914
|
this.channel.write(CTRL_X);
|
|
911
|
-
this.
|
|
912
|
-
|
|
913
|
-
const after = await this.readSettledPrompt();
|
|
914
|
-
switch (classifyPrompt(after)) {
|
|
915
|
+
const after = await this.probeMode();
|
|
916
|
+
switch (after) {
|
|
915
917
|
case "released":
|
|
916
918
|
this.cleanup();
|
|
917
|
-
return "Changes committed successfully. Safe mode DISABLED.";
|
|
919
|
+
return { ok: true, message: "Changes committed successfully. Safe mode DISABLED." };
|
|
918
920
|
case "safe":
|
|
919
|
-
return
|
|
921
|
+
return {
|
|
922
|
+
ok: false,
|
|
923
|
+
message: "Commit NOT completed \u2014 the device is still in Safe Mode (the Ctrl+X commit did not " + "take). Your changes remain held in memory and are NOT yet saved; call commit_safe_mode " + "again to retry, or rollback_safe_mode to discard them. If retries keep failing, this " + "RouterOS build may not accept an interactive Safe Mode commit over SSH \u2014 apply the " + "change without Safe Mode instead."
|
|
924
|
+
};
|
|
920
925
|
default:
|
|
921
|
-
return
|
|
926
|
+
return {
|
|
927
|
+
ok: false,
|
|
928
|
+
message: "Commit status unclear \u2014 no prompt seen after the commit. The session is left open so " + "nothing is reverted; verify with get_safe_mode_status or retry commit_safe_mode."
|
|
929
|
+
};
|
|
922
930
|
}
|
|
923
931
|
});
|
|
924
932
|
}
|
|
@@ -955,32 +963,17 @@ class SafeModeManager {
|
|
|
955
963
|
channel.on("data", onData);
|
|
956
964
|
});
|
|
957
965
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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);
|
|
966
|
+
async probeMode() {
|
|
967
|
+
if (!this.channel)
|
|
968
|
+
return "unknown";
|
|
969
|
+
const token = "__MCP_SAFEMODE_PROBE__";
|
|
970
|
+
this.channel.write(`:put "${token}"
|
|
971
|
+
`);
|
|
972
|
+
const out = await this.readUntilPrompt(8000, (cleaned) => {
|
|
973
|
+
const i = cleaned.lastIndexOf(token);
|
|
974
|
+
return i >= 0 && PROMPT_RE.test(cleaned.slice(i));
|
|
983
975
|
});
|
|
976
|
+
return classifyPrompt(out);
|
|
984
977
|
}
|
|
985
978
|
extractOutput(raw, command) {
|
|
986
979
|
const text = raw.replace(/\r\n/g, `
|
|
@@ -2632,11 +2625,19 @@ async function restoreLocalBackup(device, name, confirm) {
|
|
|
2632
2625
|
return fail("device stopped responding after applying \u2014 rolled back to avoid a lock-out", applied);
|
|
2633
2626
|
}
|
|
2634
2627
|
const committed = await safe.commit();
|
|
2628
|
+
if (!committed.ok) {
|
|
2629
|
+
return {
|
|
2630
|
+
ok: false,
|
|
2631
|
+
applied,
|
|
2632
|
+
committed: false,
|
|
2633
|
+
message: `applied ${applied} command(s) but COMMIT FAILED \u2014 not saved: ${committed.message}`
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2635
2636
|
return {
|
|
2636
2637
|
ok: true,
|
|
2637
2638
|
applied,
|
|
2638
2639
|
committed: true,
|
|
2639
|
-
message: `committed ${applied} command(s). ${committed}`
|
|
2640
|
+
message: `committed ${applied} command(s). ${committed.message}`
|
|
2640
2641
|
};
|
|
2641
2642
|
} catch (e) {
|
|
2642
2643
|
await safe.rollback();
|
|
@@ -3870,9 +3871,14 @@ DRY-RUN: all changes rolled back. Re-run with confirm=true to commit.`;
|
|
|
3870
3871
|
ABORTED: the device stopped responding after applying \u2014 changes rolled back to avoid a lock-out.`;
|
|
3871
3872
|
}
|
|
3872
3873
|
const committed = await safe.commit();
|
|
3874
|
+
if (!committed.ok) {
|
|
3875
|
+
return `${header}
|
|
3876
|
+
|
|
3877
|
+
COMMIT FAILED \u2014 changes are NOT saved (still pending in Safe Mode). ` + `${committed.message}`;
|
|
3878
|
+
}
|
|
3873
3879
|
return `${header}
|
|
3874
3880
|
|
|
3875
|
-
COMMITTED: changes are now permanent. ${committed}`;
|
|
3881
|
+
COMMITTED: changes are now permanent. ${committed.message}`;
|
|
3876
3882
|
} catch (e) {
|
|
3877
3883
|
await safe.rollback();
|
|
3878
3884
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -5935,8 +5941,12 @@ ${useSafe ? "Safe Mode rolled back \u2014 no changes kept." : "Partial rules rem
|
|
|
5935
5941
|
}
|
|
5936
5942
|
done.push(cmd);
|
|
5937
5943
|
}
|
|
5938
|
-
if (useSafe)
|
|
5939
|
-
await mgr.commit();
|
|
5944
|
+
if (useSafe) {
|
|
5945
|
+
const c = await mgr.commit();
|
|
5946
|
+
if (!c.ok) {
|
|
5947
|
+
return `Applied ${done.length} rule(s) but Safe Mode COMMIT FAILED \u2014 changes are NOT saved ` + `and will revert: ${c.message}`;
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5940
5950
|
return `Security Shield applied \u2014 ${done.length} rule(s) across ${groups.length} protection(s)${useSafe ? " (committed via Safe Mode)" : ""}. Audit with audit_firewall_hardening; undo with remove_firewall_hardening.`;
|
|
5941
5951
|
}
|
|
5942
5952
|
}),
|
|
@@ -17583,7 +17593,7 @@ var safeModeTools = [
|
|
|
17583
17593
|
async handler(_a, ctx) {
|
|
17584
17594
|
const device = resolveDeviceName(ctx.device);
|
|
17585
17595
|
ctx.info(`[${device}] Committing safe mode changes`);
|
|
17586
|
-
return getSafeModeManager(device).commit();
|
|
17596
|
+
return (await getSafeModeManager(device).commit()).message;
|
|
17587
17597
|
}
|
|
17588
17598
|
}),
|
|
17589
17599
|
defineTool({
|
|
@@ -23951,7 +23961,7 @@ function registerPrompts(server) {
|
|
|
23951
23961
|
// package.json
|
|
23952
23962
|
var package_default = {
|
|
23953
23963
|
name: "@usex/mikrotik-mcp",
|
|
23954
|
-
version: "3.
|
|
23964
|
+
version: "3.22.0",
|
|
23955
23965
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
23956
23966
|
keywords: [
|
|
23957
23967
|
"ai",
|
package/dist/index.d.ts
CHANGED
|
@@ -163,6 +163,16 @@ declare class MikroTikSSHClient {
|
|
|
163
163
|
/** Close the SSH connection. Safe to call multiple times. */
|
|
164
164
|
disconnect(): void;
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Outcome of {@link SafeModeManager.commit}. `ok` is true ONLY when Safe Mode is
|
|
168
|
+
* confirmed exited (changes persisted) — callers must check it before telling
|
|
169
|
+
* the user anything was committed, so a failed commit can never masquerade as a
|
|
170
|
+
* success that then silently reverts.
|
|
171
|
+
*/
|
|
172
|
+
interface CommitResult {
|
|
173
|
+
ok: boolean;
|
|
174
|
+
message: string;
|
|
175
|
+
}
|
|
166
176
|
declare class SafeModeManager {
|
|
167
177
|
private readonly deviceName;
|
|
168
178
|
private ssh;
|
|
@@ -179,8 +189,12 @@ declare class SafeModeManager {
|
|
|
179
189
|
enable(): Promise<string>;
|
|
180
190
|
/** Execute a command through the safe-mode persistent shell session. */
|
|
181
191
|
execute(command: string): Promise<string>;
|
|
182
|
-
/**
|
|
183
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Send Ctrl+X again to exit Safe Mode and persist all changes. Returns a
|
|
194
|
+
* structured result so callers NEVER report "committed" on a failed commit —
|
|
195
|
+
* `ok` is true only when Safe Mode is confirmed exited (changes saved).
|
|
196
|
+
*/
|
|
197
|
+
commit(): Promise<CommitResult>;
|
|
184
198
|
/** Close the session to trigger MikroTik's automatic safe-mode revert. */
|
|
185
199
|
rollback(): Promise<string>;
|
|
186
200
|
status(): string;
|
|
@@ -192,13 +206,14 @@ declare class SafeModeManager {
|
|
|
192
206
|
*/
|
|
193
207
|
private readUntilPrompt;
|
|
194
208
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* a
|
|
209
|
+
* Determine the shell's CURRENT mode definitively by round-tripping a sentinel
|
|
210
|
+
* command (`:put "<token>"`) and classifying the prompt that follows ITS
|
|
211
|
+
* output. A real command — not a bare Enter — forces RouterOS to fully process
|
|
212
|
+
* any pending Ctrl+X and render a prompt that reflects the true mode, so we
|
|
213
|
+
* never settle on a transient `<SAFE>`→normal redraw and report a commit that
|
|
214
|
+
* did not actually take. Returns `safe`, `released`, or `unknown` (no prompt).
|
|
200
215
|
*/
|
|
201
|
-
private
|
|
216
|
+
private probeMode;
|
|
202
217
|
private extractOutput;
|
|
203
218
|
private cleanup;
|
|
204
219
|
}
|
package/dist/index.js
CHANGED
|
@@ -880,31 +880,39 @@ class SafeModeManager {
|
|
|
880
880
|
}
|
|
881
881
|
commit() {
|
|
882
882
|
return this.lock(async () => {
|
|
883
|
-
if (!this.active || !this.channel)
|
|
884
|
-
return "Safe mode is not active. Nothing to commit.";
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const probe = await this.readSettledPrompt();
|
|
888
|
-
const before = classifyPrompt(probe);
|
|
883
|
+
if (!this.active || !this.channel) {
|
|
884
|
+
return { ok: true, message: "Safe mode is not active. Nothing to commit." };
|
|
885
|
+
}
|
|
886
|
+
const before = await this.probeMode();
|
|
889
887
|
if (before === "released") {
|
|
890
888
|
this.cleanup();
|
|
891
|
-
return
|
|
889
|
+
return {
|
|
890
|
+
ok: true,
|
|
891
|
+
message: "Safe mode already exited \u2014 your changes are committed. Safe mode DISABLED."
|
|
892
|
+
};
|
|
892
893
|
}
|
|
893
894
|
if (before === "unknown") {
|
|
894
|
-
return
|
|
895
|
+
return {
|
|
896
|
+
ok: false,
|
|
897
|
+
message: "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."
|
|
898
|
+
};
|
|
895
899
|
}
|
|
896
900
|
this.channel.write(CTRL_X);
|
|
897
|
-
this.
|
|
898
|
-
|
|
899
|
-
const after = await this.readSettledPrompt();
|
|
900
|
-
switch (classifyPrompt(after)) {
|
|
901
|
+
const after = await this.probeMode();
|
|
902
|
+
switch (after) {
|
|
901
903
|
case "released":
|
|
902
904
|
this.cleanup();
|
|
903
|
-
return "Changes committed successfully. Safe mode DISABLED.";
|
|
905
|
+
return { ok: true, message: "Changes committed successfully. Safe mode DISABLED." };
|
|
904
906
|
case "safe":
|
|
905
|
-
return
|
|
907
|
+
return {
|
|
908
|
+
ok: false,
|
|
909
|
+
message: "Commit NOT completed \u2014 the device is still in Safe Mode (the Ctrl+X commit did not " + "take). Your changes remain held in memory and are NOT yet saved; call commit_safe_mode " + "again to retry, or rollback_safe_mode to discard them. If retries keep failing, this " + "RouterOS build may not accept an interactive Safe Mode commit over SSH \u2014 apply the " + "change without Safe Mode instead."
|
|
910
|
+
};
|
|
906
911
|
default:
|
|
907
|
-
return
|
|
912
|
+
return {
|
|
913
|
+
ok: false,
|
|
914
|
+
message: "Commit status unclear \u2014 no prompt seen after the commit. The session is left open so " + "nothing is reverted; verify with get_safe_mode_status or retry commit_safe_mode."
|
|
915
|
+
};
|
|
908
916
|
}
|
|
909
917
|
});
|
|
910
918
|
}
|
|
@@ -941,32 +949,17 @@ class SafeModeManager {
|
|
|
941
949
|
channel.on("data", onData);
|
|
942
950
|
});
|
|
943
951
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
clearTimeout(hard);
|
|
954
|
-
if (quiet)
|
|
955
|
-
clearTimeout(quiet);
|
|
956
|
-
channel.removeListener("data", onData);
|
|
957
|
-
resolve2(stripAnsi(buf));
|
|
958
|
-
}
|
|
959
|
-
function onData(chunk) {
|
|
960
|
-
buf += decodeOutput(chunk);
|
|
961
|
-
if (PROMPT_RE.test(lastNonEmptyLine(stripAnsi(buf)))) {
|
|
962
|
-
if (quiet)
|
|
963
|
-
clearTimeout(quiet);
|
|
964
|
-
quiet = setTimeout(done, quietMs);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
hard = setTimeout(done, maxMs);
|
|
968
|
-
channel.on("data", onData);
|
|
952
|
+
async probeMode() {
|
|
953
|
+
if (!this.channel)
|
|
954
|
+
return "unknown";
|
|
955
|
+
const token = "__MCP_SAFEMODE_PROBE__";
|
|
956
|
+
this.channel.write(`:put "${token}"
|
|
957
|
+
`);
|
|
958
|
+
const out = await this.readUntilPrompt(8000, (cleaned) => {
|
|
959
|
+
const i = cleaned.lastIndexOf(token);
|
|
960
|
+
return i >= 0 && PROMPT_RE.test(cleaned.slice(i));
|
|
969
961
|
});
|
|
962
|
+
return classifyPrompt(out);
|
|
970
963
|
}
|
|
971
964
|
extractOutput(raw, command) {
|
|
972
965
|
const text = raw.replace(/\r\n/g, `
|
|
@@ -2644,11 +2637,19 @@ async function restoreLocalBackup(device, name, confirm) {
|
|
|
2644
2637
|
return fail("device stopped responding after applying \u2014 rolled back to avoid a lock-out", applied);
|
|
2645
2638
|
}
|
|
2646
2639
|
const committed = await safe.commit();
|
|
2640
|
+
if (!committed.ok) {
|
|
2641
|
+
return {
|
|
2642
|
+
ok: false,
|
|
2643
|
+
applied,
|
|
2644
|
+
committed: false,
|
|
2645
|
+
message: `applied ${applied} command(s) but COMMIT FAILED \u2014 not saved: ${committed.message}`
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2647
2648
|
return {
|
|
2648
2649
|
ok: true,
|
|
2649
2650
|
applied,
|
|
2650
2651
|
committed: true,
|
|
2651
|
-
message: `committed ${applied} command(s). ${committed}`
|
|
2652
|
+
message: `committed ${applied} command(s). ${committed.message}`
|
|
2652
2653
|
};
|
|
2653
2654
|
} catch (e) {
|
|
2654
2655
|
await safe.rollback();
|
|
@@ -3882,9 +3883,14 @@ DRY-RUN: all changes rolled back. Re-run with confirm=true to commit.`;
|
|
|
3882
3883
|
ABORTED: the device stopped responding after applying \u2014 changes rolled back to avoid a lock-out.`;
|
|
3883
3884
|
}
|
|
3884
3885
|
const committed = await safe.commit();
|
|
3886
|
+
if (!committed.ok) {
|
|
3887
|
+
return `${header}
|
|
3888
|
+
|
|
3889
|
+
COMMIT FAILED \u2014 changes are NOT saved (still pending in Safe Mode). ` + `${committed.message}`;
|
|
3890
|
+
}
|
|
3885
3891
|
return `${header}
|
|
3886
3892
|
|
|
3887
|
-
COMMITTED: changes are now permanent. ${committed}`;
|
|
3893
|
+
COMMITTED: changes are now permanent. ${committed.message}`;
|
|
3888
3894
|
} catch (e) {
|
|
3889
3895
|
await safe.rollback();
|
|
3890
3896
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -5947,8 +5953,12 @@ ${useSafe ? "Safe Mode rolled back \u2014 no changes kept." : "Partial rules rem
|
|
|
5947
5953
|
}
|
|
5948
5954
|
done.push(cmd);
|
|
5949
5955
|
}
|
|
5950
|
-
if (useSafe)
|
|
5951
|
-
await mgr.commit();
|
|
5956
|
+
if (useSafe) {
|
|
5957
|
+
const c = await mgr.commit();
|
|
5958
|
+
if (!c.ok) {
|
|
5959
|
+
return `Applied ${done.length} rule(s) but Safe Mode COMMIT FAILED \u2014 changes are NOT saved ` + `and will revert: ${c.message}`;
|
|
5960
|
+
}
|
|
5961
|
+
}
|
|
5952
5962
|
return `Security Shield applied \u2014 ${done.length} rule(s) across ${groups.length} protection(s)${useSafe ? " (committed via Safe Mode)" : ""}. Audit with audit_firewall_hardening; undo with remove_firewall_hardening.`;
|
|
5953
5963
|
}
|
|
5954
5964
|
}),
|
|
@@ -17595,7 +17605,7 @@ var safeModeTools = [
|
|
|
17595
17605
|
async handler(_a, ctx) {
|
|
17596
17606
|
const device = resolveDeviceName(ctx.device);
|
|
17597
17607
|
ctx.info(`[${device}] Committing safe mode changes`);
|
|
17598
|
-
return getSafeModeManager(device).commit();
|
|
17608
|
+
return (await getSafeModeManager(device).commit()).message;
|
|
17599
17609
|
}
|
|
17600
17610
|
}),
|
|
17601
17611
|
defineTool({
|
|
@@ -22351,7 +22361,7 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
|
|
|
22351
22361
|
// package.json
|
|
22352
22362
|
var package_default = {
|
|
22353
22363
|
name: "@usex/mikrotik-mcp",
|
|
22354
|
-
version: "3.
|
|
22364
|
+
version: "3.22.0",
|
|
22355
22365
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
22356
22366
|
keywords: [
|
|
22357
22367
|
"ai",
|
package/package.json
CHANGED