@usex/mikrotik-mcp 3.28.0 → 3.29.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 +85 -19
- package/dist/index.d.ts +16 -1
- package/dist/index.js +85 -19
- package/package.json +1 -1
- package/schemas/tool-catalog.json +7 -5
- package/schemas/tools/upload_file.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -779,6 +779,29 @@ class MikroTikSSHClient {
|
|
|
779
779
|
});
|
|
780
780
|
});
|
|
781
781
|
}
|
|
782
|
+
uploadFile(remotePath, data) {
|
|
783
|
+
if (!this.client) {
|
|
784
|
+
return Promise.reject(new Error("Not connected to MikroTik device"));
|
|
785
|
+
}
|
|
786
|
+
const openSftp = this.client.sftp.bind(this.client);
|
|
787
|
+
return new Promise((resolve2, reject) => {
|
|
788
|
+
openSftp((err, sftp) => {
|
|
789
|
+
if (err) {
|
|
790
|
+
reject(new Error(`SFTP subsystem unavailable: ${err.message}`));
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
sftp.writeFile(remotePath, data, (werr) => {
|
|
794
|
+
try {
|
|
795
|
+
sftp.end();
|
|
796
|
+
} catch {}
|
|
797
|
+
if (werr)
|
|
798
|
+
reject(new Error(`SFTP write failed: ${werr.message}`));
|
|
799
|
+
else
|
|
800
|
+
resolve2();
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
}
|
|
782
805
|
shell(opts = {}) {
|
|
783
806
|
if (!this.client) {
|
|
784
807
|
return Promise.reject(new Error("Not connected to MikroTik device"));
|
|
@@ -911,13 +934,13 @@ class SafeModeManager {
|
|
|
911
934
|
}
|
|
912
935
|
this.ssh = ssh;
|
|
913
936
|
this.channel = await ssh.shell({ term: "dumb", cols: 220, rows: 50 });
|
|
914
|
-
const initial = await this.readUntilPrompt(20000);
|
|
937
|
+
const initial = (await this.readUntilPrompt(20000)).text;
|
|
915
938
|
if (!PROMPT_RE.test(initial)) {
|
|
916
939
|
this.cleanup();
|
|
917
940
|
return `Error: Timed out waiting for MikroTik shell prompt. Got: ${JSON.stringify(initial.slice(0, 300))}`;
|
|
918
941
|
}
|
|
919
942
|
this.channel.write(CTRL_X);
|
|
920
|
-
const response = await this.readUntilPrompt(1e4, (c) => isSafeModeActivated(c));
|
|
943
|
+
const response = (await this.readUntilPrompt(1e4, (c) => isSafeModeActivated(c))).text;
|
|
921
944
|
if (!isSafeModeActivated(response)) {
|
|
922
945
|
this.cleanup();
|
|
923
946
|
return `Error: Safe mode did not activate. Response: ${JSON.stringify(response.slice(0, 300))}`;
|
|
@@ -933,8 +956,11 @@ class SafeModeManager {
|
|
|
933
956
|
}
|
|
934
957
|
this.channel.write(`${command}
|
|
935
958
|
`);
|
|
936
|
-
const
|
|
937
|
-
|
|
959
|
+
const { text, timedOut } = await this.readUntilPrompt();
|
|
960
|
+
if (timedOut) {
|
|
961
|
+
throw new Error(`Safe Mode shell did not return a prompt within 15s (command: ${command}). The interactive ` + "session appears wedged \u2014 some RouterOS builds/terminals don't support Safe Mode over SSH. " + "Apply the change with the direct write tools instead (verify each with a read).");
|
|
962
|
+
}
|
|
963
|
+
return this.extractOutput(text, command);
|
|
938
964
|
});
|
|
939
965
|
}
|
|
940
966
|
commit() {
|
|
@@ -989,7 +1015,7 @@ class SafeModeManager {
|
|
|
989
1015
|
readUntilPrompt(timeoutMs = 15000, isDone = (c) => PROMPT_RE.test(c)) {
|
|
990
1016
|
const channel = this.channel;
|
|
991
1017
|
if (!channel)
|
|
992
|
-
return Promise.resolve("");
|
|
1018
|
+
return Promise.resolve({ text: "", timedOut: false });
|
|
993
1019
|
return new Promise((resolve2) => {
|
|
994
1020
|
let buf = "";
|
|
995
1021
|
let timer;
|
|
@@ -997,14 +1023,14 @@ class SafeModeManager {
|
|
|
997
1023
|
buf += decodeOutput(chunk);
|
|
998
1024
|
const cleaned = stripAnsi(buf);
|
|
999
1025
|
if (isDone(cleaned))
|
|
1000
|
-
finish(cleaned);
|
|
1026
|
+
finish(cleaned, false);
|
|
1001
1027
|
}
|
|
1002
|
-
function finish(result) {
|
|
1028
|
+
function finish(result, timedOut) {
|
|
1003
1029
|
clearTimeout(timer);
|
|
1004
1030
|
channel.removeListener("data", onData);
|
|
1005
|
-
resolve2(result);
|
|
1031
|
+
resolve2({ text: result, timedOut });
|
|
1006
1032
|
}
|
|
1007
|
-
timer = setTimeout(() => finish(stripAnsi(buf)), timeoutMs);
|
|
1033
|
+
timer = setTimeout(() => finish(stripAnsi(buf), true), timeoutMs);
|
|
1008
1034
|
channel.on("data", onData);
|
|
1009
1035
|
});
|
|
1010
1036
|
}
|
|
@@ -1014,10 +1040,10 @@ class SafeModeManager {
|
|
|
1014
1040
|
const token = "__MCP_SAFEMODE_PROBE__";
|
|
1015
1041
|
this.channel.write(`:put "${token}"
|
|
1016
1042
|
`);
|
|
1017
|
-
const out = await this.readUntilPrompt(8000, (cleaned) => {
|
|
1043
|
+
const out = (await this.readUntilPrompt(8000, (cleaned) => {
|
|
1018
1044
|
const i = cleaned.lastIndexOf(token);
|
|
1019
1045
|
return i >= 0 && PROMPT_RE.test(cleaned.slice(i));
|
|
1020
|
-
});
|
|
1046
|
+
})).text;
|
|
1021
1047
|
return classifyPrompt(out);
|
|
1022
1048
|
}
|
|
1023
1049
|
extractOutput(raw, command) {
|
|
@@ -1090,6 +1116,31 @@ async function executeMikrotikCommand(command, ctx, opts) {
|
|
|
1090
1116
|
ctx.info(`[${deviceName}] Executing MikroTik command: ${command}`);
|
|
1091
1117
|
return runOnce(command, ctx.device, opts);
|
|
1092
1118
|
}
|
|
1119
|
+
async function uploadFileToDevice(deviceName, remotePath, data) {
|
|
1120
|
+
const name = resolveDeviceName(deviceName);
|
|
1121
|
+
const dc = getDevice(deviceName);
|
|
1122
|
+
if (isMacTelnetDevice(dc)) {
|
|
1123
|
+
throw new Error(`Cannot transfer a file to '${name}': it is reached over Layer-2 MAC-Telnet, which has no file ` + "transfer. Configure SSH (host + credentials) for this device, or have the router pull the file " + "itself with /tool fetch from a URL it can reach.");
|
|
1124
|
+
}
|
|
1125
|
+
const ssh = new MikroTikSSHClient({
|
|
1126
|
+
host: dc.host,
|
|
1127
|
+
username: dc.username,
|
|
1128
|
+
password: dc.password,
|
|
1129
|
+
keyFilename: dc.keyFilename,
|
|
1130
|
+
privateKey: dc.privateKey,
|
|
1131
|
+
keyPassphrase: dc.keyPassphrase,
|
|
1132
|
+
port: dc.port,
|
|
1133
|
+
timeoutMs: dc.timeoutMs
|
|
1134
|
+
});
|
|
1135
|
+
if (!await ssh.connect()) {
|
|
1136
|
+
throw new Error(connectErrorMessage(name, dc, ssh.lastError));
|
|
1137
|
+
}
|
|
1138
|
+
try {
|
|
1139
|
+
await ssh.uploadFile(remotePath, data);
|
|
1140
|
+
} finally {
|
|
1141
|
+
ssh.disconnect();
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1093
1144
|
|
|
1094
1145
|
// src/core/registry.ts
|
|
1095
1146
|
import { z as z2 } from "zod";
|
|
@@ -2803,21 +2854,29 @@ ${fileDetails}` : `Section export '${name}.rsc' created successfully.`;
|
|
|
2803
2854
|
}),
|
|
2804
2855
|
defineTool({
|
|
2805
2856
|
name: "upload_file",
|
|
2806
|
-
title: "Upload File to Device
|
|
2857
|
+
title: "Upload File to Device",
|
|
2807
2858
|
annotations: WRITE,
|
|
2808
|
-
description: "
|
|
2859
|
+
description: "Transfer a file to the device filesystem over SFTP (the file subsystem RouterOS exposes on its SSH" + " server) \u2014 this actually pushes the bytes, then verifies the file appears in `/file`. Use it to put" + " a file on the router before another tool can use it: a `.rsc` config script (then apply with" + " `import_configuration`), a `.backup` file (then `restore_backup`), or a certificate/key (then" + " `import_certificate`). `filename` is the DESTINATION path on the device \u2014 root by default" + " (e.g. `config.rsc`), or an external-disk path (e.g. `disk1/cert.pem`). `content_base64` is the file's" + " raw bytes, base64-encoded (binary-safe \u2014 works for `.backup`/cert files, not just text). Overwrites" + " any existing file of the same name. NOT available on MAC-Telnet devices (Layer-2 has no file" + " transfer) \u2014 there, have the router pull the file itself with `/tool fetch` from a reachable URL.",
|
|
2809
2860
|
inputSchema: {
|
|
2810
|
-
filename: z5.string(),
|
|
2811
|
-
content_base64: z5.string()
|
|
2861
|
+
filename: z5.string().describe("Destination path on the device, e.g. 'config.rsc' or 'disk1/cert.pem'"),
|
|
2862
|
+
content_base64: z5.string().describe("File bytes, base64-encoded (binary-safe)")
|
|
2812
2863
|
},
|
|
2813
2864
|
async handler(a, ctx) {
|
|
2814
2865
|
ctx.info(`Uploading file: filename=${a.filename}`);
|
|
2866
|
+
const data = Buffer.from(a.content_base64, "base64");
|
|
2867
|
+
if (data.length === 0) {
|
|
2868
|
+
return "Nothing to upload: content_base64 is empty or decoded to 0 bytes.";
|
|
2869
|
+
}
|
|
2815
2870
|
try {
|
|
2816
|
-
|
|
2871
|
+
await uploadFileToDevice(ctx.device, a.filename, data);
|
|
2817
2872
|
} catch (e) {
|
|
2818
|
-
return `Failed to
|
|
2873
|
+
return `Failed to upload '${a.filename}': ${e instanceof Error ? e.message : String(e)}`;
|
|
2874
|
+
}
|
|
2875
|
+
const verify = await executeMikrotikCommand(`/file print count-only where name="${a.filename}"`, ctx);
|
|
2876
|
+
if (verify.trim() === "0") {
|
|
2877
|
+
return `File '${a.filename}' was transferred (${data.length} bytes) but did not appear in /file under ` + "that exact name \u2014 RouterOS may have stored it at a slightly different path. Run list_files to " + "confirm, then apply it (import_configuration / restore_backup / import_certificate).";
|
|
2819
2878
|
}
|
|
2820
|
-
return `File '${a.filename}' uploaded successfully (
|
|
2879
|
+
return `File '${a.filename}' uploaded successfully (${data.length} bytes) and is now in /file. Apply it: ` + "import_configuration for a `.rsc`, restore_backup for a `.backup`, or import_certificate for a cert.";
|
|
2821
2880
|
}
|
|
2822
2881
|
}),
|
|
2823
2882
|
defineTool({
|
|
@@ -4383,10 +4442,17 @@ var changePlanTools = [
|
|
|
4383
4442
|
const enabled2 = await safe.enable();
|
|
4384
4443
|
if (enabled2.startsWith("Error"))
|
|
4385
4444
|
return enabled2;
|
|
4445
|
+
const APPLY_BUDGET_MS = 90000;
|
|
4446
|
+
const startedAt = Date.now();
|
|
4447
|
+
const overBudget = () => Date.now() - startedAt > APPLY_BUDGET_MS;
|
|
4386
4448
|
try {
|
|
4387
4449
|
const before = normalizeExport(await safe.execute("/export terse"));
|
|
4388
4450
|
const log = [];
|
|
4389
4451
|
for (const step of plan.steps) {
|
|
4452
|
+
if (overBudget()) {
|
|
4453
|
+
await safe.rollback();
|
|
4454
|
+
return `Aborted after ${Math.round((Date.now() - startedAt) / 1000)}s (time budget exceeded) \u2014 ` + "the plan was ROLLED BACK (nothing committed). Safe Mode is slow/unresponsive on this " + "device; apply the change with the direct write tools instead.";
|
|
4455
|
+
}
|
|
4390
4456
|
const out = await safe.execute(step.command);
|
|
4391
4457
|
if (looksLikeError(out)) {
|
|
4392
4458
|
await safe.rollback();
|
|
@@ -27001,7 +27067,7 @@ function registerPrompts(server) {
|
|
|
27001
27067
|
// package.json
|
|
27002
27068
|
var package_default = {
|
|
27003
27069
|
name: "@usex/mikrotik-mcp",
|
|
27004
|
-
version: "3.
|
|
27070
|
+
version: "3.29.0",
|
|
27005
27071
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
27006
27072
|
keywords: [
|
|
27007
27073
|
"ai",
|
package/dist/index.d.ts
CHANGED
|
@@ -182,6 +182,14 @@ declare class MikroTikSSHClient {
|
|
|
182
182
|
run(command: string, opts?: {
|
|
183
183
|
maxMs?: number;
|
|
184
184
|
}): Promise<string>;
|
|
185
|
+
/**
|
|
186
|
+
* Upload a file's bytes to the device over SFTP — the file-transfer subsystem
|
|
187
|
+
* RouterOS exposes on its SSH server. `remotePath` is relative to the SFTP
|
|
188
|
+
* default directory (the flash root), so `config.rsc` lands at the root and
|
|
189
|
+
* appears in `/file`; a path like `disk1/config.rsc` targets external disk.
|
|
190
|
+
* Resolves on success; rejects with a clear reason on failure.
|
|
191
|
+
*/
|
|
192
|
+
uploadFile(remotePath: string, data: Buffer): Promise<void>;
|
|
185
193
|
/** Open a persistent interactive shell channel (used by Safe Mode). */
|
|
186
194
|
shell(opts?: {
|
|
187
195
|
term?: string;
|
|
@@ -215,7 +223,14 @@ declare class SafeModeManager {
|
|
|
215
223
|
private lock;
|
|
216
224
|
/** Open a persistent SSH shell and activate MikroTik Safe Mode. */
|
|
217
225
|
enable(): Promise<string>;
|
|
218
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* Execute a command through the safe-mode persistent shell session.
|
|
228
|
+
*
|
|
229
|
+
* If the shell never returns a prompt within the timeout, the interactive
|
|
230
|
+
* Safe-Mode session is wedged — this throws immediately so the caller aborts
|
|
231
|
+
* instead of issuing more commands that would each also burn the full timeout
|
|
232
|
+
* (the cumulative effect of which is a multi-minute "hang" to the MCP client).
|
|
233
|
+
*/
|
|
219
234
|
execute(command: string): Promise<string>;
|
|
220
235
|
/**
|
|
221
236
|
* Send Ctrl+X again to exit Safe Mode and persist all changes. Returns a
|
package/dist/index.js
CHANGED
|
@@ -771,6 +771,29 @@ class MikroTikSSHClient {
|
|
|
771
771
|
});
|
|
772
772
|
});
|
|
773
773
|
}
|
|
774
|
+
uploadFile(remotePath, data) {
|
|
775
|
+
if (!this.client) {
|
|
776
|
+
return Promise.reject(new Error("Not connected to MikroTik device"));
|
|
777
|
+
}
|
|
778
|
+
const openSftp = this.client.sftp.bind(this.client);
|
|
779
|
+
return new Promise((resolve2, reject) => {
|
|
780
|
+
openSftp((err, sftp) => {
|
|
781
|
+
if (err) {
|
|
782
|
+
reject(new Error(`SFTP subsystem unavailable: ${err.message}`));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
sftp.writeFile(remotePath, data, (werr) => {
|
|
786
|
+
try {
|
|
787
|
+
sftp.end();
|
|
788
|
+
} catch {}
|
|
789
|
+
if (werr)
|
|
790
|
+
reject(new Error(`SFTP write failed: ${werr.message}`));
|
|
791
|
+
else
|
|
792
|
+
resolve2();
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
}
|
|
774
797
|
shell(opts = {}) {
|
|
775
798
|
if (!this.client) {
|
|
776
799
|
return Promise.reject(new Error("Not connected to MikroTik device"));
|
|
@@ -897,13 +920,13 @@ class SafeModeManager {
|
|
|
897
920
|
}
|
|
898
921
|
this.ssh = ssh;
|
|
899
922
|
this.channel = await ssh.shell({ term: "dumb", cols: 220, rows: 50 });
|
|
900
|
-
const initial = await this.readUntilPrompt(20000);
|
|
923
|
+
const initial = (await this.readUntilPrompt(20000)).text;
|
|
901
924
|
if (!PROMPT_RE.test(initial)) {
|
|
902
925
|
this.cleanup();
|
|
903
926
|
return `Error: Timed out waiting for MikroTik shell prompt. Got: ${JSON.stringify(initial.slice(0, 300))}`;
|
|
904
927
|
}
|
|
905
928
|
this.channel.write(CTRL_X);
|
|
906
|
-
const response = await this.readUntilPrompt(1e4, (c) => isSafeModeActivated(c));
|
|
929
|
+
const response = (await this.readUntilPrompt(1e4, (c) => isSafeModeActivated(c))).text;
|
|
907
930
|
if (!isSafeModeActivated(response)) {
|
|
908
931
|
this.cleanup();
|
|
909
932
|
return `Error: Safe mode did not activate. Response: ${JSON.stringify(response.slice(0, 300))}`;
|
|
@@ -919,8 +942,11 @@ class SafeModeManager {
|
|
|
919
942
|
}
|
|
920
943
|
this.channel.write(`${command}
|
|
921
944
|
`);
|
|
922
|
-
const
|
|
923
|
-
|
|
945
|
+
const { text, timedOut } = await this.readUntilPrompt();
|
|
946
|
+
if (timedOut) {
|
|
947
|
+
throw new Error(`Safe Mode shell did not return a prompt within 15s (command: ${command}). The interactive ` + "session appears wedged \u2014 some RouterOS builds/terminals don't support Safe Mode over SSH. " + "Apply the change with the direct write tools instead (verify each with a read).");
|
|
948
|
+
}
|
|
949
|
+
return this.extractOutput(text, command);
|
|
924
950
|
});
|
|
925
951
|
}
|
|
926
952
|
commit() {
|
|
@@ -975,7 +1001,7 @@ class SafeModeManager {
|
|
|
975
1001
|
readUntilPrompt(timeoutMs = 15000, isDone = (c) => PROMPT_RE.test(c)) {
|
|
976
1002
|
const channel = this.channel;
|
|
977
1003
|
if (!channel)
|
|
978
|
-
return Promise.resolve("");
|
|
1004
|
+
return Promise.resolve({ text: "", timedOut: false });
|
|
979
1005
|
return new Promise((resolve2) => {
|
|
980
1006
|
let buf = "";
|
|
981
1007
|
let timer;
|
|
@@ -983,14 +1009,14 @@ class SafeModeManager {
|
|
|
983
1009
|
buf += decodeOutput(chunk);
|
|
984
1010
|
const cleaned = stripAnsi(buf);
|
|
985
1011
|
if (isDone(cleaned))
|
|
986
|
-
finish(cleaned);
|
|
1012
|
+
finish(cleaned, false);
|
|
987
1013
|
}
|
|
988
|
-
function finish(result) {
|
|
1014
|
+
function finish(result, timedOut) {
|
|
989
1015
|
clearTimeout(timer);
|
|
990
1016
|
channel.removeListener("data", onData);
|
|
991
|
-
resolve2(result);
|
|
1017
|
+
resolve2({ text: result, timedOut });
|
|
992
1018
|
}
|
|
993
|
-
timer = setTimeout(() => finish(stripAnsi(buf)), timeoutMs);
|
|
1019
|
+
timer = setTimeout(() => finish(stripAnsi(buf), true), timeoutMs);
|
|
994
1020
|
channel.on("data", onData);
|
|
995
1021
|
});
|
|
996
1022
|
}
|
|
@@ -1000,10 +1026,10 @@ class SafeModeManager {
|
|
|
1000
1026
|
const token = "__MCP_SAFEMODE_PROBE__";
|
|
1001
1027
|
this.channel.write(`:put "${token}"
|
|
1002
1028
|
`);
|
|
1003
|
-
const out = await this.readUntilPrompt(8000, (cleaned) => {
|
|
1029
|
+
const out = (await this.readUntilPrompt(8000, (cleaned) => {
|
|
1004
1030
|
const i = cleaned.lastIndexOf(token);
|
|
1005
1031
|
return i >= 0 && PROMPT_RE.test(cleaned.slice(i));
|
|
1006
|
-
});
|
|
1032
|
+
})).text;
|
|
1007
1033
|
return classifyPrompt(out);
|
|
1008
1034
|
}
|
|
1009
1035
|
extractOutput(raw, command) {
|
|
@@ -1076,6 +1102,31 @@ async function executeMikrotikCommand(command, ctx, opts) {
|
|
|
1076
1102
|
ctx.info(`[${deviceName}] Executing MikroTik command: ${command}`);
|
|
1077
1103
|
return runOnce(command, ctx.device, opts);
|
|
1078
1104
|
}
|
|
1105
|
+
async function uploadFileToDevice(deviceName, remotePath, data) {
|
|
1106
|
+
const name = resolveDeviceName(deviceName);
|
|
1107
|
+
const dc = getDevice(deviceName);
|
|
1108
|
+
if (isMacTelnetDevice(dc)) {
|
|
1109
|
+
throw new Error(`Cannot transfer a file to '${name}': it is reached over Layer-2 MAC-Telnet, which has no file ` + "transfer. Configure SSH (host + credentials) for this device, or have the router pull the file " + "itself with /tool fetch from a URL it can reach.");
|
|
1110
|
+
}
|
|
1111
|
+
const ssh = new MikroTikSSHClient({
|
|
1112
|
+
host: dc.host,
|
|
1113
|
+
username: dc.username,
|
|
1114
|
+
password: dc.password,
|
|
1115
|
+
keyFilename: dc.keyFilename,
|
|
1116
|
+
privateKey: dc.privateKey,
|
|
1117
|
+
keyPassphrase: dc.keyPassphrase,
|
|
1118
|
+
port: dc.port,
|
|
1119
|
+
timeoutMs: dc.timeoutMs
|
|
1120
|
+
});
|
|
1121
|
+
if (!await ssh.connect()) {
|
|
1122
|
+
throw new Error(connectErrorMessage(name, dc, ssh.lastError));
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
await ssh.uploadFile(remotePath, data);
|
|
1126
|
+
} finally {
|
|
1127
|
+
ssh.disconnect();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1079
1130
|
// src/core/registry.ts
|
|
1080
1131
|
import { z as z2 } from "zod";
|
|
1081
1132
|
|
|
@@ -2816,21 +2867,29 @@ ${fileDetails}` : `Section export '${name}.rsc' created successfully.`;
|
|
|
2816
2867
|
}),
|
|
2817
2868
|
defineTool({
|
|
2818
2869
|
name: "upload_file",
|
|
2819
|
-
title: "Upload File to Device
|
|
2870
|
+
title: "Upload File to Device",
|
|
2820
2871
|
annotations: WRITE,
|
|
2821
|
-
description: "
|
|
2872
|
+
description: "Transfer a file to the device filesystem over SFTP (the file subsystem RouterOS exposes on its SSH" + " server) \u2014 this actually pushes the bytes, then verifies the file appears in `/file`. Use it to put" + " a file on the router before another tool can use it: a `.rsc` config script (then apply with" + " `import_configuration`), a `.backup` file (then `restore_backup`), or a certificate/key (then" + " `import_certificate`). `filename` is the DESTINATION path on the device \u2014 root by default" + " (e.g. `config.rsc`), or an external-disk path (e.g. `disk1/cert.pem`). `content_base64` is the file's" + " raw bytes, base64-encoded (binary-safe \u2014 works for `.backup`/cert files, not just text). Overwrites" + " any existing file of the same name. NOT available on MAC-Telnet devices (Layer-2 has no file" + " transfer) \u2014 there, have the router pull the file itself with `/tool fetch` from a reachable URL.",
|
|
2822
2873
|
inputSchema: {
|
|
2823
|
-
filename: z6.string(),
|
|
2824
|
-
content_base64: z6.string()
|
|
2874
|
+
filename: z6.string().describe("Destination path on the device, e.g. 'config.rsc' or 'disk1/cert.pem'"),
|
|
2875
|
+
content_base64: z6.string().describe("File bytes, base64-encoded (binary-safe)")
|
|
2825
2876
|
},
|
|
2826
2877
|
async handler(a, ctx) {
|
|
2827
2878
|
ctx.info(`Uploading file: filename=${a.filename}`);
|
|
2879
|
+
const data = Buffer.from(a.content_base64, "base64");
|
|
2880
|
+
if (data.length === 0) {
|
|
2881
|
+
return "Nothing to upload: content_base64 is empty or decoded to 0 bytes.";
|
|
2882
|
+
}
|
|
2828
2883
|
try {
|
|
2829
|
-
|
|
2884
|
+
await uploadFileToDevice(ctx.device, a.filename, data);
|
|
2830
2885
|
} catch (e) {
|
|
2831
|
-
return `Failed to
|
|
2886
|
+
return `Failed to upload '${a.filename}': ${e instanceof Error ? e.message : String(e)}`;
|
|
2887
|
+
}
|
|
2888
|
+
const verify = await executeMikrotikCommand(`/file print count-only where name="${a.filename}"`, ctx);
|
|
2889
|
+
if (verify.trim() === "0") {
|
|
2890
|
+
return `File '${a.filename}' was transferred (${data.length} bytes) but did not appear in /file under ` + "that exact name \u2014 RouterOS may have stored it at a slightly different path. Run list_files to " + "confirm, then apply it (import_configuration / restore_backup / import_certificate).";
|
|
2832
2891
|
}
|
|
2833
|
-
return `File '${a.filename}' uploaded successfully (
|
|
2892
|
+
return `File '${a.filename}' uploaded successfully (${data.length} bytes) and is now in /file. Apply it: ` + "import_configuration for a `.rsc`, restore_backup for a `.backup`, or import_certificate for a cert.";
|
|
2834
2893
|
}
|
|
2835
2894
|
}),
|
|
2836
2895
|
defineTool({
|
|
@@ -4396,10 +4455,17 @@ var changePlanTools = [
|
|
|
4396
4455
|
const enabled2 = await safe.enable();
|
|
4397
4456
|
if (enabled2.startsWith("Error"))
|
|
4398
4457
|
return enabled2;
|
|
4458
|
+
const APPLY_BUDGET_MS = 90000;
|
|
4459
|
+
const startedAt = Date.now();
|
|
4460
|
+
const overBudget = () => Date.now() - startedAt > APPLY_BUDGET_MS;
|
|
4399
4461
|
try {
|
|
4400
4462
|
const before = normalizeExport(await safe.execute("/export terse"));
|
|
4401
4463
|
const log = [];
|
|
4402
4464
|
for (const step of plan.steps) {
|
|
4465
|
+
if (overBudget()) {
|
|
4466
|
+
await safe.rollback();
|
|
4467
|
+
return `Aborted after ${Math.round((Date.now() - startedAt) / 1000)}s (time budget exceeded) \u2014 ` + "the plan was ROLLED BACK (nothing committed). Safe Mode is slow/unresponsive on this " + "device; apply the change with the direct write tools instead.";
|
|
4468
|
+
}
|
|
4403
4469
|
const out = await safe.execute(step.command);
|
|
4404
4470
|
if (looksLikeError(out)) {
|
|
4405
4471
|
await safe.rollback();
|
|
@@ -24979,7 +25045,7 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
|
|
|
24979
25045
|
// package.json
|
|
24980
25046
|
var package_default = {
|
|
24981
25047
|
name: "@usex/mikrotik-mcp",
|
|
24982
|
-
version: "3.
|
|
25048
|
+
version: "3.29.0",
|
|
24983
25049
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
24984
25050
|
keywords: [
|
|
24985
25051
|
"ai",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.28.0",
|
|
4
4
|
"generated": "by scripts/gen-schemas.ts — do not edit by hand",
|
|
5
5
|
"toolCount": 758,
|
|
6
6
|
"tools": [
|
|
@@ -25552,22 +25552,24 @@
|
|
|
25552
25552
|
},
|
|
25553
25553
|
{
|
|
25554
25554
|
"name": "upload_file",
|
|
25555
|
-
"title": "Upload File to Device
|
|
25555
|
+
"title": "Upload File to Device",
|
|
25556
25556
|
"risk": "write",
|
|
25557
25557
|
"annotations": {
|
|
25558
25558
|
"destructiveHint": false,
|
|
25559
25559
|
"openWorldHint": false
|
|
25560
25560
|
},
|
|
25561
|
-
"description": "
|
|
25561
|
+
"description": "Transfer a file to the device filesystem over SFTP (the file subsystem RouterOS exposes on its SSH server) — this actually pushes the bytes, then verifies the file appears in `/file`. Use it to put a file on the router before another tool can use it: a `.rsc` config script (then apply with `import_configuration`), a `.backup` file (then `restore_backup`), or a certificate/key (then `import_certificate`). `filename` is the DESTINATION path on the device — root by default (e.g. `config.rsc`), or an external-disk path (e.g. `disk1/cert.pem`). `content_base64` is the file's raw bytes, base64-encoded (binary-safe — works for `.backup`/cert files, not just text). Overwrites any existing file of the same name. NOT available on MAC-Telnet devices (Layer-2 has no file transfer) — there, have the router pull the file itself with `/tool fetch` from a reachable URL.",
|
|
25562
25562
|
"inputSchema": {
|
|
25563
25563
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
25564
25564
|
"type": "object",
|
|
25565
25565
|
"properties": {
|
|
25566
25566
|
"filename": {
|
|
25567
|
-
"type": "string"
|
|
25567
|
+
"type": "string",
|
|
25568
|
+
"description": "Destination path on the device, e.g. 'config.rsc' or 'disk1/cert.pem'"
|
|
25568
25569
|
},
|
|
25569
25570
|
"content_base64": {
|
|
25570
|
-
"type": "string"
|
|
25571
|
+
"type": "string",
|
|
25572
|
+
"description": "File bytes, base64-encoded (binary-safe)"
|
|
25571
25573
|
}
|
|
25572
25574
|
},
|
|
25573
25575
|
"required": ["filename", "content_base64"],
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
6
6
|
"filename": {
|
|
7
|
-
"type": "string"
|
|
7
|
+
"type": "string",
|
|
8
|
+
"description": "Destination path on the device, e.g. 'config.rsc' or 'disk1/cert.pem'"
|
|
8
9
|
},
|
|
9
10
|
"content_base64": {
|
|
10
|
-
"type": "string"
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "File bytes, base64-encoded (binary-safe)"
|
|
11
13
|
}
|
|
12
14
|
},
|
|
13
15
|
"required": ["filename", "content_base64"],
|