@usex/mikrotik-mcp 3.37.0 → 3.39.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
@@ -84,7 +84,7 @@ import {
84
84
  toggleAaaEntity,
85
85
  updateAaaEntity,
86
86
  writeBackup
87
- } from "./shared/cli-9p1cxztb.js";
87
+ } from "./shared/cli-ksdjs84m.js";
88
88
 
89
89
  // src/cli.ts
90
90
  import { existsSync as existsSync2 } from "fs";
@@ -1948,6 +1948,90 @@ async function runDashboard(cfg, transportLabel) {
1948
1948
  const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
1949
1949
  return bearer === cfg.token || url.searchParams.get("token") === cfg.token;
1950
1950
  };
1951
+ async function dashboardRoute(req, url, srv, ca, tl) {
1952
+ if (url.pathname === "/api/stream") {
1953
+ if (srv.upgrade(req, { data: {} }))
1954
+ return;
1955
+ return new Response("WebSocket upgrade failed", { status: 400 });
1956
+ }
1957
+ if (url.pathname === "/api/sse") {
1958
+ return sseResponse(tl);
1959
+ }
1960
+ const configResp = await configRoutes(req, url, ca);
1961
+ if (configResp)
1962
+ return configResp;
1963
+ const modulesResp = await modulesRoutes(req, url);
1964
+ if (modulesResp)
1965
+ return modulesResp;
1966
+ const captureResp = await captureRoutes(req, url);
1967
+ if (captureResp)
1968
+ return captureResp;
1969
+ const clientsResp = await clientsRoutes(req, url);
1970
+ if (clientsResp)
1971
+ return clientsResp;
1972
+ const aaaResp = await aaaRoutes(req, url);
1973
+ if (aaaResp)
1974
+ return aaaResp;
1975
+ const usageResp = await usageRoutes(req, url);
1976
+ if (usageResp)
1977
+ return usageResp;
1978
+ const featureResp = await featureRoutes(req, url);
1979
+ if (featureResp)
1980
+ return featureResp;
1981
+ const db = getEventStore();
1982
+ if (!db)
1983
+ return json({ error: "recorder not active" }, 503);
1984
+ if (url.pathname === "/api/events" && req.method === "DELETE") {
1985
+ let body = {};
1986
+ try {
1987
+ body = await req.json();
1988
+ } catch {}
1989
+ const ids = Array.isArray(body.ids) ? body.ids.filter((x) => typeof x === "string") : [];
1990
+ const removed = body.all === true ? db.clear() : db.delete(ids);
1991
+ return json({ removed, total: db.total() });
1992
+ }
1993
+ if (url.pathname === "/api/devices") {
1994
+ return json(devicesPayload(db));
1995
+ }
1996
+ if (url.pathname === "/api/topology") {
1997
+ return json(topologyPayload());
1998
+ }
1999
+ if (url.pathname === "/api/config") {
2000
+ return json(configPayload());
2001
+ }
2002
+ if (url.pathname === "/" || url.pathname === "/index.html") {
2003
+ return new Response(dashboardHtml(), {
2004
+ headers: { "content-type": "text/html; charset=utf-8" }
2005
+ });
2006
+ }
2007
+ if (url.pathname === "/api/meta") {
2008
+ const f = facets(db);
2009
+ return json({
2010
+ ...f,
2011
+ risks: ["READ", "WRITE", "WRITE_IDEMPOTENT", "DESTRUCTIVE", "DANGEROUS"],
2012
+ total: db.total(),
2013
+ liveClients: subscriberCount(),
2014
+ transport: tl
2015
+ });
2016
+ }
2017
+ if (url.pathname === "/api/events") {
2018
+ const filter = filterFromQuery(url);
2019
+ return json({ events: db.query(filter), total: db.total() });
2020
+ }
2021
+ const eventMatch = url.pathname.match(/^\/api\/event\/(.+)$/);
2022
+ if (eventMatch) {
2023
+ const e = db.get(decodeURIComponent(eventMatch[1]));
2024
+ return e ? json(e) : json({ error: "not found" }, 404);
2025
+ }
2026
+ if (url.pathname === "/api/stats") {
2027
+ const now = Date.now();
2028
+ const windowMs = Number(url.searchParams.get("window") ?? 3600000);
2029
+ const buckets = Number(url.searchParams.get("buckets") ?? 60);
2030
+ const events = db.query({ since: now - windowMs, limit: 5000 });
2031
+ return json(computeStats(events, { now, windowMs, buckets }));
2032
+ }
2033
+ return new Response("Not Found", { status: 404 });
2034
+ }
1951
2035
  const server = serve({
1952
2036
  hostname: cfg.host,
1953
2037
  port: cfg.port,
@@ -1959,88 +2043,13 @@ async function runDashboard(cfg, transportLabel) {
1959
2043
  if (!tokenOk(req, url)) {
1960
2044
  return new Response("Unauthorized", { status: 401 });
1961
2045
  }
1962
- if (url.pathname === "/api/stream") {
1963
- if (srv.upgrade(req, { data: {} }))
1964
- return;
1965
- return new Response("WebSocket upgrade failed", { status: 400 });
1966
- }
1967
- if (url.pathname === "/api/sse") {
1968
- return sseResponse(transportLabel);
1969
- }
1970
- const configResp = await configRoutes(req, url, configAdmin);
1971
- if (configResp)
1972
- return configResp;
1973
- const modulesResp = await modulesRoutes(req, url);
1974
- if (modulesResp)
1975
- return modulesResp;
1976
- const captureResp = await captureRoutes(req, url);
1977
- if (captureResp)
1978
- return captureResp;
1979
- const clientsResp = await clientsRoutes(req, url);
1980
- if (clientsResp)
1981
- return clientsResp;
1982
- const aaaResp = await aaaRoutes(req, url);
1983
- if (aaaResp)
1984
- return aaaResp;
1985
- const usageResp = await usageRoutes(req, url);
1986
- if (usageResp)
1987
- return usageResp;
1988
- const featureResp = await featureRoutes(req, url);
1989
- if (featureResp)
1990
- return featureResp;
1991
- const db = getEventStore();
1992
- if (!db)
1993
- return json({ error: "recorder not active" }, 503);
1994
- if (url.pathname === "/api/events" && req.method === "DELETE") {
1995
- let body = {};
1996
- try {
1997
- body = await req.json();
1998
- } catch {}
1999
- const ids = Array.isArray(body.ids) ? body.ids.filter((x) => typeof x === "string") : [];
2000
- const removed = body.all === true ? db.clear() : db.delete(ids);
2001
- return json({ removed, total: db.total() });
2002
- }
2003
- if (url.pathname === "/api/devices") {
2004
- return json(devicesPayload(db));
2005
- }
2006
- if (url.pathname === "/api/topology") {
2007
- return json(topologyPayload());
2008
- }
2009
- if (url.pathname === "/api/config") {
2010
- return json(configPayload());
2011
- }
2012
- if (url.pathname === "/" || url.pathname === "/index.html") {
2013
- return new Response(dashboardHtml(), {
2014
- headers: { "content-type": "text/html; charset=utf-8" }
2015
- });
2016
- }
2017
- if (url.pathname === "/api/meta") {
2018
- const f = facets(db);
2019
- return json({
2020
- ...f,
2021
- risks: ["READ", "WRITE", "WRITE_IDEMPOTENT", "DESTRUCTIVE", "DANGEROUS"],
2022
- total: db.total(),
2023
- liveClients: subscriberCount(),
2024
- transport: transportLabel
2025
- });
2026
- }
2027
- if (url.pathname === "/api/events") {
2028
- const filter = filterFromQuery(url);
2029
- return json({ events: db.query(filter), total: db.total() });
2030
- }
2031
- const eventMatch = url.pathname.match(/^\/api\/event\/(.+)$/);
2032
- if (eventMatch) {
2033
- const e = db.get(decodeURIComponent(eventMatch[1]));
2034
- return e ? json(e) : json({ error: "not found" }, 404);
2035
- }
2036
- if (url.pathname === "/api/stats") {
2037
- const now = Date.now();
2038
- const windowMs = Number(url.searchParams.get("window") ?? 3600000);
2039
- const buckets = Number(url.searchParams.get("buckets") ?? 60);
2040
- const events = db.query({ since: now - windowMs, limit: 5000 });
2041
- return json(computeStats(events, { now, windowMs, buckets }));
2046
+ try {
2047
+ return await dashboardRoute(req, url, srv, configAdmin, transportLabel);
2048
+ } catch (e) {
2049
+ const msg = e instanceof Error ? e.message : String(e);
2050
+ logger.error(`Dashboard request failed (${url.pathname}): ${msg}`);
2051
+ return json({ error: msg }, 502);
2042
2052
  }
2043
- return new Response("Not Found", { status: 404 });
2044
2053
  },
2045
2054
  websocket: {
2046
2055
  open(ws) {
@@ -2233,7 +2242,7 @@ function registerPrompts(server) {
2233
2242
  // package.json
2234
2243
  var package_default = {
2235
2244
  name: "@usex/mikrotik-mcp",
2236
- version: "3.37.0",
2245
+ version: "3.39.0",
2237
2246
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
2238
2247
  keywords: [
2239
2248
  "ai",
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  resolveDeviceName,
22
22
  selectToolModules,
23
23
  setConfig
24
- } from "./shared/library-9pt0yeqy.js";
24
+ } from "./shared/library-pvb7kh40.js";
25
25
  // src/server.ts
26
26
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
27
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -131,7 +131,7 @@ function registerPrompts(server) {
131
131
  // package.json
132
132
  var package_default = {
133
133
  name: "@usex/mikrotik-mcp",
134
- version: "3.37.0",
134
+ version: "3.39.0",
135
135
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
136
136
  keywords: [
137
137
  "ai",
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./cli-9p1cxztb.js";
7
+ } from "./cli-ksdjs84m.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -3075,7 +3075,7 @@ var backupTools = [
3075
3075
  await executeMikrotikCommand(`/file remove ${deviceFile}`, ctx);
3076
3076
  return `[LOW DISK \u2014 ${disk.usedPct?.toFixed(0)}% used] Backup '${deviceFile}' created, downloaded to ` + `local vault as '${vaultName}' (${data.length} bytes) at ${backupDir()}, and removed from ` + "the device to free disk space.";
3077
3077
  } catch (e) {
3078
- return `Backup '${name}.backup' created on device, but the automatic download-and-cleanup ` + `failed: ${e instanceof Error ? e.message : String(e)}. The file is still on the device.`;
3078
+ return `Backup '${name}.backup' created on device, but the automatic download-and-cleanup ` + `failed: ${e instanceof Error ? e.message : String(e)}. The file is still on the device. ` + "Ensure the SSH user has the 'read' and 'sensitive' policies for SFTP downloads to work.";
3079
3079
  }
3080
3080
  }
3081
3081
  const fileDetails = await executeMikrotikCommand(`/file print detail where name=${name}.backup`, ctx);
@@ -3212,24 +3212,24 @@ ${fileDetails}` : `Section export '${name}.rsc' created successfully.`;
3212
3212
  }),
3213
3213
  defineTool({
3214
3214
  name: "download_file",
3215
- title: "Download File as Base64",
3215
+ title: "Download File from Device",
3216
3216
  annotations: READ,
3217
- description: "Attempts to read a file from the device filesystem via `/file print file=<filename>` and" + " returns the RouterOS API text response base64-encoded as `FILE_CONTENT_BASE64:<data>`." + " NOTE: this is a simplified implementation. RouterOS has no file-content-read API over SSH;" + " `/file print file=<name>` saves the directory-listing output to a file named `<name>` rather" + " than streaming an existing file's bytes. The returned base64 payload is the RouterOS text" + " response to that command \u2014 not the actual file contents. Binary `.backup` files cannot be" + " reliably retrieved this way. Verifies file existence first" + " (`/file print count-only where name=<filename>`); returns a not-found message if absent." + " To list available files use `list_backups`; for file metadata only use `backup_info`.",
3217
+ description: "Downloads a file from the device filesystem over SFTP and returns its raw bytes base64-encoded" + " as `FILE_CONTENT_BASE64:<data>`. Works for any file type \u2014 `.backup`, `.rsc`, `.p12`" + " certificates, keys, etc. Verifies file existence first (`/file print count-only`). The SSH" + " user MUST have the `read` policy (and `sensitive` for `.backup` / certificate files) on" + " RouterOS for SFTP downloads to work. NOT available on MAC-Telnet devices (Layer-2 has no" + " file transfer). To list available files use `list_backups`; for file metadata use" + " `backup_info`; to push a file onto the device use `upload_file`.",
3218
3218
  inputSchema: {
3219
- filename: z5.string(),
3220
- file_type: z5.enum(["backup", "export"]).default("backup")
3219
+ filename: z5.string().describe("Name of the file on the device, e.g. 'cert.p12' or 'backup.backup'")
3221
3220
  },
3222
3221
  async handler(a, ctx) {
3223
- ctx.info(`Downloading file: filename=${a.filename}, type=${a.file_type}`);
3222
+ ctx.info(`Downloading file: filename=${a.filename}`);
3224
3223
  const count = await executeMikrotikCommand(`/file print count-only where name=${a.filename}`, ctx);
3225
3224
  if (count.trim() === "0")
3226
- return `File '${a.filename}' not found.`;
3227
- const content = await executeMikrotikCommand(`/file print file=${a.filename}`, ctx);
3228
- if (content) {
3229
- const encoded = Buffer.from(content, "utf8").toString("base64");
3225
+ return `File '${a.filename}' not found on the device.`;
3226
+ try {
3227
+ const data = await downloadFileFromDevice(ctx.device, a.filename);
3228
+ const encoded = data.toString("base64");
3230
3229
  return `FILE_CONTENT_BASE64:${encoded}`;
3230
+ } catch (e) {
3231
+ return `Failed to download '${a.filename}': ${e instanceof Error ? e.message : String(e)}. ` + "Ensure the SSH user has the 'read' and 'sensitive' policies on RouterOS.";
3231
3232
  }
3232
- return `Failed to download file '${a.filename}'.`;
3233
3233
  }
3234
3234
  }),
3235
3235
  defineTool({
@@ -6141,7 +6141,7 @@ var cache = null;
6141
6141
  async function gateway() {
6142
6142
  if (cache)
6143
6143
  return cache;
6144
- const { moduleCatalog } = await import("./cli-fpfchpjm.js");
6144
+ const { moduleCatalog } = await import("./cli-17657c2v.js");
6145
6145
  const forIndex = [];
6146
6146
  const byName = new Map;
6147
6147
  for (const mod of moduleCatalog) {
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./library-9pt0yeqy.js";
7
+ } from "./library-pvb7kh40.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -3051,7 +3051,7 @@ var backupTools = [
3051
3051
  await executeMikrotikCommand(`/file remove ${deviceFile}`, ctx);
3052
3052
  return `[LOW DISK \u2014 ${disk.usedPct?.toFixed(0)}% used] Backup '${deviceFile}' created, downloaded to ` + `local vault as '${vaultName}' (${data.length} bytes) at ${backupDir()}, and removed from ` + "the device to free disk space.";
3053
3053
  } catch (e) {
3054
- return `Backup '${name}.backup' created on device, but the automatic download-and-cleanup ` + `failed: ${e instanceof Error ? e.message : String(e)}. The file is still on the device.`;
3054
+ return `Backup '${name}.backup' created on device, but the automatic download-and-cleanup ` + `failed: ${e instanceof Error ? e.message : String(e)}. The file is still on the device. ` + "Ensure the SSH user has the 'read' and 'sensitive' policies for SFTP downloads to work.";
3055
3055
  }
3056
3056
  }
3057
3057
  const fileDetails = await executeMikrotikCommand(`/file print detail where name=${name}.backup`, ctx);
@@ -3188,24 +3188,24 @@ ${fileDetails}` : `Section export '${name}.rsc' created successfully.`;
3188
3188
  }),
3189
3189
  defineTool({
3190
3190
  name: "download_file",
3191
- title: "Download File as Base64",
3191
+ title: "Download File from Device",
3192
3192
  annotations: READ,
3193
- description: "Attempts to read a file from the device filesystem via `/file print file=<filename>` and" + " returns the RouterOS API text response base64-encoded as `FILE_CONTENT_BASE64:<data>`." + " NOTE: this is a simplified implementation. RouterOS has no file-content-read API over SSH;" + " `/file print file=<name>` saves the directory-listing output to a file named `<name>` rather" + " than streaming an existing file's bytes. The returned base64 payload is the RouterOS text" + " response to that command \u2014 not the actual file contents. Binary `.backup` files cannot be" + " reliably retrieved this way. Verifies file existence first" + " (`/file print count-only where name=<filename>`); returns a not-found message if absent." + " To list available files use `list_backups`; for file metadata only use `backup_info`.",
3193
+ description: "Downloads a file from the device filesystem over SFTP and returns its raw bytes base64-encoded" + " as `FILE_CONTENT_BASE64:<data>`. Works for any file type \u2014 `.backup`, `.rsc`, `.p12`" + " certificates, keys, etc. Verifies file existence first (`/file print count-only`). The SSH" + " user MUST have the `read` policy (and `sensitive` for `.backup` / certificate files) on" + " RouterOS for SFTP downloads to work. NOT available on MAC-Telnet devices (Layer-2 has no" + " file transfer). To list available files use `list_backups`; for file metadata use" + " `backup_info`; to push a file onto the device use `upload_file`.",
3194
3194
  inputSchema: {
3195
- filename: z5.string(),
3196
- file_type: z5.enum(["backup", "export"]).default("backup")
3195
+ filename: z5.string().describe("Name of the file on the device, e.g. 'cert.p12' or 'backup.backup'")
3197
3196
  },
3198
3197
  async handler(a, ctx) {
3199
- ctx.info(`Downloading file: filename=${a.filename}, type=${a.file_type}`);
3198
+ ctx.info(`Downloading file: filename=${a.filename}`);
3200
3199
  const count = await executeMikrotikCommand(`/file print count-only where name=${a.filename}`, ctx);
3201
3200
  if (count.trim() === "0")
3202
- return `File '${a.filename}' not found.`;
3203
- const content = await executeMikrotikCommand(`/file print file=${a.filename}`, ctx);
3204
- if (content) {
3205
- const encoded = Buffer.from(content, "utf8").toString("base64");
3201
+ return `File '${a.filename}' not found on the device.`;
3202
+ try {
3203
+ const data = await downloadFileFromDevice(ctx.device, a.filename);
3204
+ const encoded = data.toString("base64");
3206
3205
  return `FILE_CONTENT_BASE64:${encoded}`;
3206
+ } catch (e) {
3207
+ return `Failed to download '${a.filename}': ${e instanceof Error ? e.message : String(e)}. ` + "Ensure the SSH user has the 'read' and 'sensitive' policies on RouterOS.";
3207
3208
  }
3208
- return `Failed to download file '${a.filename}'.`;
3209
3209
  }
3210
3210
  }),
3211
3211
  defineTool({
@@ -6117,7 +6117,7 @@ var cache = null;
6117
6117
  async function gateway() {
6118
6118
  if (cache)
6119
6119
  return cache;
6120
- const { moduleCatalog } = await import("./library-r6dx5hw2.js");
6120
+ const { moduleCatalog } = await import("./library-dhx14y24.js");
6121
6121
  const forIndex = [];
6122
6122
  const byName = new Map;
6123
6123
  for (const mod of moduleCatalog) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usex/mikrotik-mcp",
3
- "version": "3.37.0",
3
+ "version": "3.39.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",