@usex/mikrotik-mcp 3.60.0 → 4.1.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.
@@ -436,12 +436,34 @@ function parseDevicesSource(raw, fromFile) {
436
436
  const mcp = structured && obj.mcp && typeof obj.mcp === "object" ? obj.mcp : undefined;
437
437
  const ssh = structured && obj.ssh && typeof obj.ssh === "object" ? obj.ssh : undefined;
438
438
  const memory = structured && obj.memory && typeof obj.memory === "object" ? obj.memory : undefined;
439
- return { devices, defaultDevice, s3, dashboard, tools, mcp, ssh, memory };
439
+ const readOnly = structured && typeof obj.readOnly === "boolean" ? obj.readOnly : undefined;
440
+ const disableUpdateCheck = structured && typeof obj.disableUpdateCheck === "boolean" ? obj.disableUpdateCheck : undefined;
441
+ const backupDir = structured && typeof obj.backupDir === "string" ? obj.backupDir : undefined;
442
+ return {
443
+ devices,
444
+ defaultDevice,
445
+ s3,
446
+ dashboard,
447
+ tools,
448
+ mcp,
449
+ ssh,
450
+ memory,
451
+ readOnly,
452
+ disableUpdateCheck,
453
+ backupDir
454
+ };
440
455
  }
441
456
  var configSource = { path: DEFAULT_CONFIG_FILE, fromFile: false };
442
457
  function loadConfig(argv = process.argv.slice(2)) {
443
458
  const flags = parseFlags(argv);
444
459
  const pick = (flag, ...envNames) => flags[flag] ?? env(...envNames);
460
+ const overlay = (file, over) => {
461
+ const out = { ...file };
462
+ for (const [k, v] of Object.entries(over))
463
+ if (v !== undefined)
464
+ out[k] = v;
465
+ return out;
466
+ };
445
467
  const jumpHostName = pick("jump-host", "MIKROTIK_JUMP_HOST");
446
468
  const jumpHost = jumpHostName ? {
447
469
  host: jumpHostName,
@@ -482,6 +504,9 @@ function loadConfig(argv = process.argv.slice(2)) {
482
504
  let fileMcp;
483
505
  let fileSsh;
484
506
  let fileMemory;
507
+ let fileReadOnly;
508
+ let fileDisableUpdateCheck;
509
+ let fileBackupDir;
485
510
  if (configFile || devicesInline) {
486
511
  const src = configFile ? parseDevicesSource(configFile, true) : parseDevicesSource(devicesInline, false);
487
512
  for (const [name, dc] of Object.entries(src.devices))
@@ -496,8 +521,11 @@ function loadConfig(argv = process.argv.slice(2)) {
496
521
  fileMcp = src.mcp;
497
522
  fileSsh = src.ssh;
498
523
  fileMemory = src.memory;
524
+ fileReadOnly = src.readOnly;
525
+ fileDisableUpdateCheck = src.disableUpdateCheck;
526
+ fileBackupDir = src.backupDir;
499
527
  }
500
- const s3 = {
528
+ const s3 = overlay(fileS3, {
501
529
  accessKeyId: pick("s3-access-key-id", "S3_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID"),
502
530
  secretAccessKey: pick("s3-secret-access-key", "S3_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY"),
503
531
  sessionToken: pick("s3-session-token", "S3_SESSION_TOKEN", "AWS_SESSION_TOKEN"),
@@ -505,12 +533,11 @@ function loadConfig(argv = process.argv.slice(2)) {
505
533
  endpoint: pick("s3-endpoint", "S3_ENDPOINT", "AWS_ENDPOINT"),
506
534
  bucket: pick("s3-bucket", "S3_BUCKET", "AWS_BUCKET"),
507
535
  prefix: pick("s3-prefix", "MIKROTIK_S3_PREFIX"),
508
- presignExpiresIn: pick("s3-presign-expires-in", "MIKROTIK_S3_PRESIGN_EXPIRES_IN"),
509
- ...fileS3
510
- };
536
+ presignExpiresIn: pick("s3-presign-expires-in", "MIKROTIK_S3_PRESIGN_EXPIRES_IN")
537
+ });
511
538
  const appViewsRaw = pick("app-views", "MIKROTIK_MCP__APP_VIEWS");
512
539
  const appViewsEnv = appViewsRaw === undefined ? undefined : !/^(0|false|no|off)$/i.test(appViewsRaw);
513
- const mcp = {
540
+ const mcp = overlay(fileMcp, {
514
541
  transport: pick("transport", "MIKROTIK_MCP__TRANSPORT", "MCP_TRANSPORT"),
515
542
  host: pick("mcp-host", "MIKROTIK_MCP__HOST"),
516
543
  port: pick("mcp-port", "MIKROTIK_MCP__PORT"),
@@ -518,22 +545,22 @@ function loadConfig(argv = process.argv.slice(2)) {
518
545
  allowedOrigins: pick("mcp-allowed-origins", "MIKROTIK_MCP__ALLOWED_ORIGINS"),
519
546
  corsOrigins: pick("mcp-cors-origins", "MIKROTIK_MCP__CORS_ORIGINS"),
520
547
  toolPageSize: pick("tool-page-size", "MIKROTIK_MCP__TOOL_PAGE_SIZE"),
521
- appViews: appViewsEnv,
522
- ...fileMcp
523
- };
548
+ appViews: appViewsEnv
549
+ });
524
550
  const isTruthy = (v) => /^(1|true|yes|on)$/i.test(v ?? "");
525
- const readOnly = isTruthy(pick("read-only", "MIKROTIK_READ_ONLY"));
526
- const disableUpdateCheck = isTruthy(pick("disable-update-check", "MIKROTIK_DISABLE_UPDATE_CHECK"));
551
+ const boolFileFlag = (flagVal, fileVal) => flagVal !== undefined ? isTruthy(flagVal) : fileVal;
552
+ const readOnly = boolFileFlag(pick("read-only", "MIKROTIK_READ_ONLY"), fileReadOnly);
553
+ const disableUpdateCheck = boolFileFlag(pick("disable-update-check", "MIKROTIK_DISABLE_UPDATE_CHECK"), fileDisableUpdateCheck);
554
+ const backupDir = pick("backup-dir", "MIKROTIK_BACKUP_DIR") ?? fileBackupDir;
527
555
  const csv = (v) => v === undefined ? undefined : v.split(",").map((s) => s.trim()).filter(Boolean);
528
- const tools = {
556
+ const tools = overlay(fileTools, {
529
557
  enabledModules: csv(pick("tools-enabled-modules", "MIKROTIK_TOOLS__ENABLED_MODULES")),
530
558
  disabledModules: csv(pick("tools-disabled-modules", "MIKROTIK_TOOLS__DISABLED_MODULES")),
531
559
  enabledGroups: csv(pick("tools-enabled-groups", "MIKROTIK_TOOLS__ENABLED_GROUPS")),
532
- disabledGroups: csv(pick("tools-disabled-groups", "MIKROTIK_TOOLS__DISABLED_GROUPS")),
533
- ...fileTools
534
- };
560
+ disabledGroups: csv(pick("tools-disabled-groups", "MIKROTIK_TOOLS__DISABLED_GROUPS"))
561
+ });
535
562
  const boolOpt = (v) => v === undefined ? undefined : isTruthy(v);
536
- const dashboard = {
563
+ const dashboard = overlay(fileDashboard, {
537
564
  enabled: boolOpt(pick("dashboard", "MIKROTIK_DASHBOARD__ENABLED", "MIKROTIK_DASHBOARD")),
538
565
  host: pick("dashboard-host", "MIKROTIK_DASHBOARD__HOST"),
539
566
  port: pick("dashboard-port", "MIKROTIK_DASHBOARD__PORT"),
@@ -542,20 +569,17 @@ function loadConfig(argv = process.argv.slice(2)) {
542
569
  captureBody: boolOpt(pick("dashboard-capture-body", "MIKROTIK_DASHBOARD__CAPTURE_BODY")),
543
570
  redactInput: boolOpt(pick("dashboard-redact-input", "MIKROTIK_DASHBOARD__REDACT_INPUT")),
544
571
  maxBodyBytes: pick("dashboard-max-body-bytes", "MIKROTIK_DASHBOARD__MAX_BODY_BYTES"),
545
- token: pick("dashboard-token", "MIKROTIK_DASHBOARD__TOKEN"),
546
- ...fileDashboard
547
- };
548
- const ssh = {
572
+ token: pick("dashboard-token", "MIKROTIK_DASHBOARD__TOKEN")
573
+ });
574
+ const ssh = overlay(fileSsh, {
549
575
  keepAlive: boolOpt(pick("ssh-keep-alive", "MIKROTIK_SSH__KEEP_ALIVE")),
550
576
  keepAliveInterval: pick("ssh-keepalive-interval", "MIKROTIK_SSH__KEEPALIVE_INTERVAL"),
551
- idleTimeout: pick("ssh-idle-timeout", "MIKROTIK_SSH__IDLE_TIMEOUT"),
552
- ...fileSsh
553
- };
554
- const memory = {
577
+ idleTimeout: pick("ssh-idle-timeout", "MIKROTIK_SSH__IDLE_TIMEOUT")
578
+ });
579
+ const memory = overlay(fileMemory, {
555
580
  enabled: boolOpt(pick("memory-enabled", "MIKROTIK_MEMORY__ENABLED")),
556
- dbPath: pick("memory-db", "MIKROTIK_MEMORY__DB_PATH"),
557
- ...fileMemory
558
- };
581
+ dbPath: pick("memory-db", "MIKROTIK_MEMORY__DB_PATH")
582
+ });
559
583
  const hasS3 = !!(s3.accessKeyId || s3.bucket || s3.endpoint);
560
584
  const raw = {
561
585
  devices: Object.keys(devices).length ? devices : { default: {} },
@@ -564,6 +588,7 @@ function loadConfig(argv = process.argv.slice(2)) {
564
588
  dashboard,
565
589
  readOnly,
566
590
  disableUpdateCheck,
591
+ backupDir,
567
592
  tools,
568
593
  ssh,
569
594
  memory,
@@ -1189,6 +1214,8 @@ function stripAnsi(text) {
1189
1214
  return text.replace(ANSI_RE, "");
1190
1215
  }
1191
1216
  var CTRL_X = "\x18";
1217
+ var IDLE_TIMEOUT_MS = 15000;
1218
+ var HARD_CAP_MS = 120000;
1192
1219
  function isSafeModeActivated(response) {
1193
1220
  if (response.includes("<SAFE>"))
1194
1221
  return true;
@@ -1264,7 +1291,7 @@ class SafeModeManager {
1264
1291
  `);
1265
1292
  const { text, timedOut } = await this.readUntilPrompt();
1266
1293
  if (timedOut) {
1267
- 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).");
1294
+ throw new Error(`Safe Mode shell went silent for ${IDLE_TIMEOUT_MS / 1000}s (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).");
1268
1295
  }
1269
1296
  return this.extractOutput(text, command);
1270
1297
  });
@@ -1318,25 +1345,32 @@ class SafeModeManager {
1318
1345
  status() {
1319
1346
  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.";
1320
1347
  }
1321
- readUntilPrompt(timeoutMs = 15000, isDone = (c) => PROMPT_RE.test(c)) {
1348
+ readUntilPrompt(idleMs = IDLE_TIMEOUT_MS, isDone = (c) => PROMPT_RE.test(c), hardCapMs = HARD_CAP_MS) {
1322
1349
  const channel = this.channel;
1323
1350
  if (!channel)
1324
1351
  return Promise.resolve({ text: "", timedOut: false });
1325
1352
  return new Promise((resolve2) => {
1326
1353
  let buf = "";
1327
- let timer;
1354
+ let idleTimer;
1355
+ const hardTimer = setTimeout(() => finish(stripAnsi(buf), true), hardCapMs);
1356
+ function armIdle2() {
1357
+ clearTimeout(idleTimer);
1358
+ idleTimer = setTimeout(() => finish(stripAnsi(buf), true), idleMs);
1359
+ }
1328
1360
  function onData(chunk) {
1329
1361
  buf += decodeOutput(chunk);
1362
+ armIdle2();
1330
1363
  const cleaned = stripAnsi(buf);
1331
1364
  if (isDone(cleaned))
1332
1365
  finish(cleaned, false);
1333
1366
  }
1334
1367
  function finish(result, timedOut) {
1335
- clearTimeout(timer);
1368
+ clearTimeout(idleTimer);
1369
+ clearTimeout(hardTimer);
1336
1370
  channel.removeListener("data", onData);
1337
1371
  resolve2({ text: result, timedOut });
1338
1372
  }
1339
- timer = setTimeout(() => finish(stripAnsi(buf), true), timeoutMs);
1373
+ armIdle2();
1340
1374
  channel.on("data", onData);
1341
1375
  });
1342
1376
  }
@@ -5194,7 +5228,7 @@ var changePlanTools = [
5194
5228
  const enabled2 = await safe.enable();
5195
5229
  if (enabled2.startsWith("Error"))
5196
5230
  throw new Error(enabled2);
5197
- const APPLY_BUDGET_MS = 90000;
5231
+ const APPLY_BUDGET_MS = 180000;
5198
5232
  const startedAt = Date.now();
5199
5233
  const overBudget = () => Date.now() - startedAt > APPLY_BUDGET_MS;
5200
5234
  const applyAndDiff = async () => {
@@ -8260,7 +8294,7 @@ var cache = null;
8260
8294
  async function gateway() {
8261
8295
  if (cache)
8262
8296
  return cache;
8263
- const { moduleCatalog } = await import("./library-1hs18tmh.js");
8297
+ const { moduleCatalog } = await import("./library-0d02nsc7.js");
8264
8298
  const forIndex = [];
8265
8299
  const byName = new Map;
8266
8300
  for (const mod of moduleCatalog) {
@@ -22111,7 +22145,7 @@ ${result}`;
22111
22145
  name: "check_route_path",
22112
22146
  title: "Check IPv4 Route Path",
22113
22147
  annotations: READ,
22114
- description: "Resolves which nexthop RouterOS would use for a given IPv4 destination " + '\u2014 answers "which gateway will this packet take?" without sending any traffic. ' + "Version-aware: uses `/ip route check` on v6, falls back to " + "`/ip route print where dst-address in <dest> active=yes` on v7+ where the check " + "command was removed. Optionally scoped by `routing_table` (v7) / `routing_mark` (v6) " + "for policy-routing table lookups. " + "For listing all known routes use list_routes; for a named-table view use get_routing_table. " + "Returns the resolved nexthop and interface detail.",
22148
+ description: "Resolves which nexthop RouterOS would use for a given IPv4 destination " + '\u2014 answers "which gateway will this packet take?" without sending any traffic. ' + "Version-aware: uses `/ip route check` on v6, falls back to " + "`/ip route print where <dest> in dst-address active=yes` on v7+ where the check " + "command was removed. Optionally scoped by `routing_table` (v7) / `routing_mark` (v6) " + "for policy-routing table lookups. " + "For listing all known routes use list_routes; for a named-table view use get_routing_table. " + "Returns the resolved nexthop and interface detail.",
22115
22149
  inputSchema: {
22116
22150
  destination: z88.string(),
22117
22151
  routing_table: z88.string().optional().describe('Policy-routing table name (v7) or routing-mark (v6), e.g. "VPN"')
@@ -22126,7 +22160,7 @@ ${result}`;
22126
22160
 
22127
22161
  ${v6Result}`;
22128
22162
  }
22129
- const where = [`dst-address in ${a.destination}`, "active=yes"];
22163
+ const where = [`${a.destination} in dst-address`, "active=yes"];
22130
22164
  if (table && table !== "main")
22131
22165
  where.push(`routing-table=${quoteValue(table)}`);
22132
22166
  const v7Cmd = `/ip route print detail where ${where.join(" ")}`;
@@ -24862,7 +24896,7 @@ var SERVER_DESCRIPTION = pkg.description ?? "";
24862
24896
  var SERVER_NAME = "mikrotik-mcp";
24863
24897
 
24864
24898
  // src/core/update-check.ts
24865
- var GITHUB_API = "https://api.github.com/repos/ali-master/mikrotik-mcp/releases/latest";
24899
+ var GITHUB_API = "https://api.github.com/repos/mikrotik-mcp/mikrotik-mcp/releases/latest";
24866
24900
  var MEMORY_CACHE_TTL = 15 * 60 * 1000;
24867
24901
  var FILE_CACHE_TTL = 6 * 60 * 60 * 1000;
24868
24902
  var CACHE_PATH = join6(homedir2(), ".mikrotik-mcp", "update-check.json");
@@ -24920,13 +24954,15 @@ async function fetchLatestRelease() {
24920
24954
  throw new Error(`GitHub API ${res.status}`);
24921
24955
  const gh = await res.json();
24922
24956
  const latestVersion = gh.tag_name.replace(/^v/, "");
24957
+ const cmp = compareVersions(latestVersion, VERSION);
24923
24958
  const data = {
24924
24959
  version: latestVersion,
24925
24960
  name: gh.name || `v${latestVersion}`,
24926
24961
  body: gh.body || "",
24927
24962
  publishedAt: gh.published_at,
24928
24963
  url: gh.html_url,
24929
- isNewer: compareVersions(latestVersion, VERSION) > 0,
24964
+ isNewer: cmp > 0,
24965
+ isAhead: cmp < 0,
24930
24966
  currentVersion: VERSION
24931
24967
  };
24932
24968
  memoryCache = { data, fetchedAt: Date.now() };
@@ -25034,14 +25070,18 @@ var serverPulseTools = [
25034
25070
  const sep = "\u2500".repeat(50);
25035
25071
  sections.push(`SERVER PULSE \u2014 ${SERVER_TITLE} v${VERSION}`, sep, "Package: @usex/mikrotik-mcp", `Version: ${VERSION}`, `Uptime: ${formatUptime(uptime)}`, `Website: ${WEBSITE_URL}`);
25036
25072
  if (result.release) {
25037
- const freshness = assessFreshness(VERSION, result.release.version);
25038
- const label = freshnessLabel(freshness);
25039
25073
  const age = timeAgo(result.release.publishedAt);
25040
- sections.push("", `UPDATE STATUS: ${label}`, sep, `Current: v${VERSION}`, `Latest: v${result.release.version} (${result.release.name})`, `Published: ${age}`, `Freshness: ${freshness.toUpperCase()}`);
25041
- if (result.release.isNewer) {
25042
- sections.push("", ">>> A newer version is available! <<<");
25074
+ if (result.release.isAhead) {
25075
+ sections.push("", "UPDATE STATUS: AHEAD OF LATEST RELEASE", sep, `Current: v${VERSION}`, `Published: v${result.release.version} (${result.release.name})`, `Released: ${age}`, `Freshness: DEV BUILD`, "", "You are running ahead of the latest published release.");
25043
25076
  } else {
25044
- sections.push("", "You are running the latest version.");
25077
+ const freshness = assessFreshness(VERSION, result.release.version);
25078
+ const label = freshnessLabel(freshness);
25079
+ sections.push("", `UPDATE STATUS: ${label}`, sep, `Current: v${VERSION}`, `Latest: v${result.release.version} (${result.release.name})`, `Published: ${age}`, `Freshness: ${freshness.toUpperCase()}`);
25080
+ if (result.release.isNewer) {
25081
+ sections.push("", ">>> A newer version is available! <<<");
25082
+ } else {
25083
+ sections.push("", "You are running the latest version.");
25084
+ }
25045
25085
  }
25046
25086
  if (result.release.isNewer) {
25047
25087
  sections.push("", "UPGRADE", sep);