@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 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 raw = await this.readUntilPrompt();
937
- return this.extractOutput(raw, command);
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 (Simulated)",
2857
+ title: "Upload File to Device",
2807
2858
  annotations: WRITE,
2808
- description: "Accepts a base64-encoded file intended for the device filesystem. NOTE: the current handler" + " only validates the base64 encoding and returns a simulated success \u2014 it does not transfer" + " bytes to the device over SSH. Once a `.backup` file is present on the device apply it with" + " `restore_backup`; for `.rsc` configuration scripts use `import_configuration`.",
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
- Buffer.from(a.content_base64, "base64").toString("utf8");
2871
+ await uploadFileToDevice(ctx.device, a.filename, data);
2817
2872
  } catch (e) {
2818
- return `Failed to decode file content: ${e instanceof Error ? e.message : String(e)}`;
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 (simulated).`;
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.28.0",
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
- /** Execute a command through the safe-mode persistent shell session. */
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 raw = await this.readUntilPrompt();
923
- return this.extractOutput(raw, command);
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 (Simulated)",
2870
+ title: "Upload File to Device",
2820
2871
  annotations: WRITE,
2821
- description: "Accepts a base64-encoded file intended for the device filesystem. NOTE: the current handler" + " only validates the base64 encoding and returns a simulated success \u2014 it does not transfer" + " bytes to the device over SSH. Once a `.backup` file is present on the device apply it with" + " `restore_backup`; for `.rsc` configuration scripts use `import_configuration`.",
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
- Buffer.from(a.content_base64, "base64").toString("utf8");
2884
+ await uploadFileToDevice(ctx.device, a.filename, data);
2830
2885
  } catch (e) {
2831
- return `Failed to decode file content: ${e instanceof Error ? e.message : String(e)}`;
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 (simulated).`;
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.28.0",
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
  "name": "@usex/mikrotik-mcp",
3
- "version": "3.28.0",
3
+ "version": "3.29.0",
4
4
  "description": "MCP server for MikroTik RouterOS — 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "3.27.0",
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 (Simulated)",
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": "Accepts a base64-encoded file intended for the device filesystem. NOTE: the current handler only validates the base64 encoding and returns a simulated success it does not transfer bytes to the device over SSH. Once a `.backup` file is present on the device apply it with `restore_backup`; for `.rsc` configuration scripts use `import_configuration`.",
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"],