@usex/mikrotik-mcp 4.18.0 → 4.20.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
@@ -40,6 +40,7 @@ import {
40
40
  captureSnapshot,
41
41
  checkForUpdate,
42
42
  closeAll,
43
+ closeDevice,
43
44
  closeMemoryStore,
44
45
  commandUnsupported,
45
46
  configureRecorder,
@@ -123,7 +124,7 @@ import {
123
124
  updateAaaEntity,
124
125
  updateSummaryLine,
125
126
  writeBackup
126
- } from "./shared/cli-dv2kmsh2.js";
127
+ } from "./shared/cli-fa29149s.js";
127
128
 
128
129
  // src/cli.ts
129
130
  import { existsSync as existsSync2 } from "fs";
@@ -2997,6 +2998,17 @@ async function runDashboard(cfg, transportLabel) {
2997
2998
  ...devicesPayload(db)
2998
2999
  });
2999
3000
  }
3001
+ if ((url.pathname === "/api/devices/test" || url.pathname === "/api/devices/reconnect") && req.method === "POST") {
3002
+ const b = await readJson(req);
3003
+ const name = typeof b?.device === "string" ? b.device : "";
3004
+ const cfg2 = getConfig();
3005
+ if (!(name in cfg2.devices))
3006
+ return json3({ error: `unknown device: ${name}` }, 404);
3007
+ if (url.pathname === "/api/devices/reconnect")
3008
+ closeDevice(name);
3009
+ const status = await probeDevice(name, cfg2.devices[name]);
3010
+ return json3({ ok: true, status, ...devicesPayload(db) });
3011
+ }
3000
3012
  if (url.pathname === "/api/topology") {
3001
3013
  return json3(topologyPayload());
3002
3014
  }
package/dist/index.d.ts CHANGED
@@ -280,6 +280,14 @@ declare class SafeModeManager {
280
280
  private ssh;
281
281
  private channel;
282
282
  private active;
283
+ /**
284
+ * Set when the persistent shell drops WHILE Safe Mode was active — i.e. the
285
+ * session died before an explicit commit, so RouterOS has already auto-reverted
286
+ * every staged change. Distinguishes "never enabled / cleanly closed" from
287
+ * "died with your changes still pending" so commit/status can report the revert
288
+ * instead of a reassuring false success. Cleared on the next enable().
289
+ */
290
+ private droppedUnexpectedly;
283
291
  /** Serializes channel access so concurrent tool calls don't interleave I/O. */
284
292
  private queue;
285
293
  /** The device this Safe Mode session belongs to (a configured device name). */
@@ -308,6 +316,15 @@ declare class SafeModeManager {
308
316
  rollback(): Promise<string>;
309
317
  status(): string;
310
318
  /**
319
+ * Fired by the persistent shell's `close`/`error` events. Arrow-bound so the
320
+ * same reference is used for add/removeListener. Acts ONLY when we still think
321
+ * Safe Mode is active — a drop then means the session died with staged changes
322
+ * pending, which RouterOS has auto-reverted, so we flag it and tear the zombie
323
+ * handles down. A close during our own cleanup() (which clears `active` first)
324
+ * is a no-op: it is an intentional teardown, not a lost session.
325
+ */
326
+ private readonly handleUnexpectedDrop;
327
+ /**
311
328
  * Read from the channel until `isDone(cleaned)` is satisfied or the shell goes
312
329
  * SILENT. Defaults to "any RouterOS prompt appeared". Mode transitions pass a
313
330
  * stricter predicate so they wait for the prompt that reflects the NEW state
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  selectToolModules,
30
30
  setConfig,
31
31
  updateSummaryLine
32
- } from "./shared/library-7m2c1g1y.js";
32
+ } from "./shared/library-ypdm9bkx.js";
33
33
  // src/server.ts
34
34
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
35
35
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./cli-dv2kmsh2.js";
7
+ } from "./cli-fa29149s.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -1219,6 +1219,9 @@ function closeAll() {
1219
1219
  removeEntry(name);
1220
1220
  }
1221
1221
  }
1222
+ function closeDevice(deviceName) {
1223
+ removeEntry(deviceName);
1224
+ }
1222
1225
  function poolStatus() {
1223
1226
  return Array.from(entries, ([name, e]) => ({
1224
1227
  device: name,
@@ -1258,6 +1261,7 @@ class SafeModeManager {
1258
1261
  ssh = null;
1259
1262
  channel = null;
1260
1263
  active = false;
1264
+ droppedUnexpectedly = false;
1261
1265
  queue = Promise.resolve();
1262
1266
  constructor(deviceName) {
1263
1267
  this.deviceName = deviceName;
@@ -1278,6 +1282,7 @@ class SafeModeManager {
1278
1282
  return this.lock(async () => {
1279
1283
  if (this.active)
1280
1284
  return "Safe mode is already active.";
1285
+ this.droppedUnexpectedly = false;
1281
1286
  const dc = getDevice(this.deviceName);
1282
1287
  if (dc.mac) {
1283
1288
  return "Error: Safe Mode is not supported for a MAC-Telnet device " + `('${this.deviceName}' is reached by MAC ${dc.mac}). ` + "Connect over SSH (configure host/credentials) to use Safe Mode.";
@@ -1288,6 +1293,8 @@ class SafeModeManager {
1288
1293
  }
1289
1294
  this.ssh = ssh;
1290
1295
  this.channel = await ssh.shell({ term: "dumb", cols: 220, rows: 50 });
1296
+ this.channel.on("close", this.handleUnexpectedDrop);
1297
+ this.channel.on("error", this.handleUnexpectedDrop);
1291
1298
  const initial = (await this.readUntilPrompt(20000)).text;
1292
1299
  if (!PROMPT_RE.test(initial)) {
1293
1300
  this.cleanup();
@@ -1305,6 +1312,9 @@ class SafeModeManager {
1305
1312
  }
1306
1313
  execute(command) {
1307
1314
  return this.lock(async () => {
1315
+ if (this.droppedUnexpectedly) {
1316
+ throw new Error("Safe Mode session dropped \u2014 RouterOS auto-reverted every staged change; nothing " + "was saved. Re-enable Safe Mode and re-apply, or apply the change directly.");
1317
+ }
1308
1318
  if (!this.active || !this.channel) {
1309
1319
  throw new Error("Safe mode session is not active.");
1310
1320
  }
@@ -1319,6 +1329,13 @@ class SafeModeManager {
1319
1329
  }
1320
1330
  commit() {
1321
1331
  return this.lock(async () => {
1332
+ if (this.droppedUnexpectedly) {
1333
+ this.droppedUnexpectedly = false;
1334
+ return {
1335
+ ok: false,
1336
+ message: "Commit FAILED \u2014 the Safe Mode session dropped before this commit, so RouterOS " + "automatically reverted ALL staged changes and NOTHING was saved. Re-enable Safe Mode " + "and re-apply the changes (or apply them directly and verify each with a read)."
1337
+ };
1338
+ }
1322
1339
  if (!this.active || !this.channel) {
1323
1340
  return { ok: true, message: "Safe mode is not active. Nothing to commit." };
1324
1341
  }
@@ -1357,6 +1374,10 @@ class SafeModeManager {
1357
1374
  }
1358
1375
  rollback() {
1359
1376
  return this.lock(async () => {
1377
+ if (this.droppedUnexpectedly) {
1378
+ this.droppedUnexpectedly = false;
1379
+ return "Safe Mode session had already dropped \u2014 RouterOS auto-reverted all staged changes; " + "nothing was left to roll back.";
1380
+ }
1360
1381
  if (!this.active)
1361
1382
  return "Safe mode is not active. Nothing to roll back.";
1362
1383
  this.cleanup();
@@ -1364,8 +1385,17 @@ class SafeModeManager {
1364
1385
  });
1365
1386
  }
1366
1387
  status() {
1388
+ if (this.droppedUnexpectedly) {
1389
+ return "Safe mode session DROPPED unexpectedly \u2014 RouterOS auto-reverted all staged changes; they " + "were NOT saved. Re-enable Safe Mode and re-apply, or apply changes directly (verify each with a read).";
1390
+ }
1367
1391
  return this.active ? "Safe mode is ACTIVE. Changes are pending \u2014 they are NOT yet persisted. " + "Call commit_safe_mode to persist or rollback_safe_mode to revert." : "Safe mode is NOT active. Changes take effect and persist immediately.";
1368
1392
  }
1393
+ handleUnexpectedDrop = () => {
1394
+ if (!this.active)
1395
+ return;
1396
+ this.droppedUnexpectedly = true;
1397
+ this.cleanup();
1398
+ };
1369
1399
  readUntilPrompt(idleMs = IDLE_TIMEOUT_MS, isDone = (c) => PROMPT_RE.test(c), hardCapMs = HARD_CAP_MS) {
1370
1400
  const channel = this.channel;
1371
1401
  if (!channel)
@@ -9029,7 +9059,7 @@ var cache = null;
9029
9059
  async function gateway() {
9030
9060
  if (cache)
9031
9061
  return cache;
9032
- const { moduleCatalog } = await import("./cli-7jxnfmp0.js");
9062
+ const { moduleCatalog } = await import("./cli-cdxrs0vz.js");
9033
9063
  const forIndex = [];
9034
9064
  const byName = new Map;
9035
9065
  for (const mod of moduleCatalog) {
@@ -33305,4 +33335,4 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
33305
33335
  }).map((m) => m.tools);
33306
33336
  }
33307
33337
 
33308
- export { __require, DEFAULT_SNAPSHOT_DB, DEFAULT_CONFIG_HISTORY_DIR, DeviceConfigSchema, ToolFilterSchema, MikrotikConfigSchema, getConfigSource, loadConfig, logger, setConfig, getConfig, listDevices, deviceLabels, resolveDeviceName, deviceDirectory, isMacTelnetDevice, createDeviceClient, describeTransport, isPoolEnabled, closeAll, poolStatus, getMemoryStore, closeMemoryStore, reopenMemoryStore, executeMikrotikCommand, createContext, Cmd, isEmpty, looksLikeError, commandUnsupported, parseKeyValues, parseRouterosDate, parseSize, parseSystemResource, parseRecords, parseLeadingNumber, parseDisks, riskOf, REDACTED, redact, configureRecorder, getEventStore, subscribe, subscriberCount, registerTools, fetchDevices, sampleDeviceTraffic, sampleAllTraffic, setDeviceLimits, blockDevice, allowDevice, makeDeviceStatic, setDeviceIp, setDeviceLabel, removeDeviceLease, devicesView, PROMPTS_DIR, UI_DIST_DIR, registerUiResources, backupDir, listBackups, readBackup, writeBackup, deleteBackup, renameBackup, createLocalBackup, restoreLocalBackup, isS3Configured, getS3Client, presignExpiresIn, s3Target, splitCommands, buildChangePlan, renderPlan, diffLines, normalizeExport, analyzeDrift, attributeChanges, openSnapshotStore, applyWritesSafely, captureSnapshot, DEFAULT_TZSP_PORT, capture2 as capture, AAA_ENTITIES, listAaaEntity, addAaaEntity, updateAaaEntity, removeAaaEntity, toggleAaaEntity, getRadiusIncoming, setRadiusIncoming, resetRadiusCounters, getUmSettings, setUmSettings, VERSION, WEBSITE_URL, LOGO_URL, SERVER_TITLE, SERVER_DESCRIPTION, SERVER_NAME, PKG_META, loadFileCacheSync, fetchLatestRelease, checkForUpdate, updateSummaryLine, fetchAllReleases, buildChannelPlanCommands, reportWeakClients, runCapsmanAudit, steerAlreadyPresent, buildSteerCommands, loadBalancePlan, buildLoadBalanceCommands, buildFtCommands, buildHaCommands, haGuidance, capsmanOverview, fetchCapsmanState, moduleCatalog, allToolModules, ALWAYS_ON_MODULES, selectToolModules };
33338
+ export { __require, DEFAULT_SNAPSHOT_DB, DEFAULT_CONFIG_HISTORY_DIR, DeviceConfigSchema, ToolFilterSchema, MikrotikConfigSchema, getConfigSource, loadConfig, logger, setConfig, getConfig, listDevices, deviceLabels, resolveDeviceName, deviceDirectory, isMacTelnetDevice, createDeviceClient, describeTransport, isPoolEnabled, closeAll, closeDevice, poolStatus, getMemoryStore, closeMemoryStore, reopenMemoryStore, executeMikrotikCommand, createContext, Cmd, isEmpty, looksLikeError, commandUnsupported, parseKeyValues, parseRouterosDate, parseSize, parseSystemResource, parseRecords, parseLeadingNumber, parseDisks, riskOf, REDACTED, redact, configureRecorder, getEventStore, subscribe, subscriberCount, registerTools, fetchDevices, sampleDeviceTraffic, sampleAllTraffic, setDeviceLimits, blockDevice, allowDevice, makeDeviceStatic, setDeviceIp, setDeviceLabel, removeDeviceLease, devicesView, PROMPTS_DIR, UI_DIST_DIR, registerUiResources, backupDir, listBackups, readBackup, writeBackup, deleteBackup, renameBackup, createLocalBackup, restoreLocalBackup, isS3Configured, getS3Client, presignExpiresIn, s3Target, splitCommands, buildChangePlan, renderPlan, diffLines, normalizeExport, analyzeDrift, attributeChanges, openSnapshotStore, applyWritesSafely, captureSnapshot, DEFAULT_TZSP_PORT, capture2 as capture, AAA_ENTITIES, listAaaEntity, addAaaEntity, updateAaaEntity, removeAaaEntity, toggleAaaEntity, getRadiusIncoming, setRadiusIncoming, resetRadiusCounters, getUmSettings, setUmSettings, VERSION, WEBSITE_URL, LOGO_URL, SERVER_TITLE, SERVER_DESCRIPTION, SERVER_NAME, PKG_META, loadFileCacheSync, fetchLatestRelease, checkForUpdate, updateSummaryLine, fetchAllReleases, buildChannelPlanCommands, reportWeakClients, runCapsmanAudit, steerAlreadyPresent, buildSteerCommands, loadBalancePlan, buildLoadBalanceCommands, buildFtCommands, buildHaCommands, haGuidance, capsmanOverview, fetchCapsmanState, moduleCatalog, allToolModules, ALWAYS_ON_MODULES, selectToolModules };
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./library-7m2c1g1y.js";
7
+ } from "./library-ypdm9bkx.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -1239,6 +1239,7 @@ class SafeModeManager {
1239
1239
  ssh = null;
1240
1240
  channel = null;
1241
1241
  active = false;
1242
+ droppedUnexpectedly = false;
1242
1243
  queue = Promise.resolve();
1243
1244
  constructor(deviceName) {
1244
1245
  this.deviceName = deviceName;
@@ -1259,6 +1260,7 @@ class SafeModeManager {
1259
1260
  return this.lock(async () => {
1260
1261
  if (this.active)
1261
1262
  return "Safe mode is already active.";
1263
+ this.droppedUnexpectedly = false;
1262
1264
  const dc = getDevice(this.deviceName);
1263
1265
  if (dc.mac) {
1264
1266
  return "Error: Safe Mode is not supported for a MAC-Telnet device " + `('${this.deviceName}' is reached by MAC ${dc.mac}). ` + "Connect over SSH (configure host/credentials) to use Safe Mode.";
@@ -1269,6 +1271,8 @@ class SafeModeManager {
1269
1271
  }
1270
1272
  this.ssh = ssh;
1271
1273
  this.channel = await ssh.shell({ term: "dumb", cols: 220, rows: 50 });
1274
+ this.channel.on("close", this.handleUnexpectedDrop);
1275
+ this.channel.on("error", this.handleUnexpectedDrop);
1272
1276
  const initial = (await this.readUntilPrompt(20000)).text;
1273
1277
  if (!PROMPT_RE.test(initial)) {
1274
1278
  this.cleanup();
@@ -1286,6 +1290,9 @@ class SafeModeManager {
1286
1290
  }
1287
1291
  execute(command) {
1288
1292
  return this.lock(async () => {
1293
+ if (this.droppedUnexpectedly) {
1294
+ throw new Error("Safe Mode session dropped \u2014 RouterOS auto-reverted every staged change; nothing " + "was saved. Re-enable Safe Mode and re-apply, or apply the change directly.");
1295
+ }
1289
1296
  if (!this.active || !this.channel) {
1290
1297
  throw new Error("Safe mode session is not active.");
1291
1298
  }
@@ -1300,6 +1307,13 @@ class SafeModeManager {
1300
1307
  }
1301
1308
  commit() {
1302
1309
  return this.lock(async () => {
1310
+ if (this.droppedUnexpectedly) {
1311
+ this.droppedUnexpectedly = false;
1312
+ return {
1313
+ ok: false,
1314
+ message: "Commit FAILED \u2014 the Safe Mode session dropped before this commit, so RouterOS " + "automatically reverted ALL staged changes and NOTHING was saved. Re-enable Safe Mode " + "and re-apply the changes (or apply them directly and verify each with a read)."
1315
+ };
1316
+ }
1303
1317
  if (!this.active || !this.channel) {
1304
1318
  return { ok: true, message: "Safe mode is not active. Nothing to commit." };
1305
1319
  }
@@ -1338,6 +1352,10 @@ class SafeModeManager {
1338
1352
  }
1339
1353
  rollback() {
1340
1354
  return this.lock(async () => {
1355
+ if (this.droppedUnexpectedly) {
1356
+ this.droppedUnexpectedly = false;
1357
+ return "Safe Mode session had already dropped \u2014 RouterOS auto-reverted all staged changes; " + "nothing was left to roll back.";
1358
+ }
1341
1359
  if (!this.active)
1342
1360
  return "Safe mode is not active. Nothing to roll back.";
1343
1361
  this.cleanup();
@@ -1345,8 +1363,17 @@ class SafeModeManager {
1345
1363
  });
1346
1364
  }
1347
1365
  status() {
1366
+ if (this.droppedUnexpectedly) {
1367
+ return "Safe mode session DROPPED unexpectedly \u2014 RouterOS auto-reverted all staged changes; they " + "were NOT saved. Re-enable Safe Mode and re-apply, or apply changes directly (verify each with a read).";
1368
+ }
1348
1369
  return this.active ? "Safe mode is ACTIVE. Changes are pending \u2014 they are NOT yet persisted. " + "Call commit_safe_mode to persist or rollback_safe_mode to revert." : "Safe mode is NOT active. Changes take effect and persist immediately.";
1349
1370
  }
1371
+ handleUnexpectedDrop = () => {
1372
+ if (!this.active)
1373
+ return;
1374
+ this.droppedUnexpectedly = true;
1375
+ this.cleanup();
1376
+ };
1350
1377
  readUntilPrompt(idleMs = IDLE_TIMEOUT_MS, isDone = (c) => PROMPT_RE.test(c), hardCapMs = HARD_CAP_MS) {
1351
1378
  const channel = this.channel;
1352
1379
  if (!channel)
@@ -8811,7 +8838,7 @@ var cache = null;
8811
8838
  async function gateway() {
8812
8839
  if (cache)
8813
8840
  return cache;
8814
- const { moduleCatalog } = await import("./library-77r1j24d.js");
8841
+ const { moduleCatalog } = await import("./library-g5p25m9e.js");
8815
8842
  const forIndex = [];
8816
8843
  const byName = new Map;
8817
8844
  for (const mod of moduleCatalog) {