mikromcp 1.15.0 → 1.16.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/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  [![CI](https://github.com/AliKarami/MikroMCP/actions/workflows/ci.yml/badge.svg)](https://github.com/AliKarami/MikroMCP/actions/workflows/ci.yml)
14
14
  [![Release](https://github.com/AliKarami/MikroMCP/actions/workflows/release.yml/badge.svg)](https://github.com/AliKarami/MikroMCP/actions/workflows/release.yml)
15
- [![Version](https://img.shields.io/badge/version-v1.15.0-2DD4BF)](package.json)
15
+ [![Version](https://img.shields.io/badge/version-v1.16.0-2DD4BF)](package.json)
16
16
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
17
17
  [![Node.js >= 22](https://img.shields.io/badge/node-%3E%3D22-339933)](package.json)
18
18
  [![RouterOS 7.x](https://img.shields.io/badge/RouterOS-7.x-293239)](https://help.mikrotik.com/docs/display/ROS/REST+API)
@@ -19,6 +19,8 @@
19
19
  #
20
20
  # allowedRouters: [] means all routers are accessible.
21
21
  # allowedToolPatterns: [] means all tools are accessible. Supports * wildcard.
22
+ # Fleet tools (bulk_read, bulk_execute) must match by their own name too;
23
+ # they also check each tool they fan out.
22
24
 
23
25
  identities:
24
26
  # CI pipeline: operator access to specific routers, limited tool set
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/main.ts
4
4
  import { config as loadDotenv } from "dotenv";
5
- import { join as join6 } from "path";
5
+ import { join as join7 } from "path";
6
6
  import { homedir as homedir4 } from "os";
7
7
 
8
8
  // src/cli/index.ts
@@ -6360,7 +6360,6 @@ var containerTools = [listContainersTool, manageContainerTool];
6360
6360
 
6361
6361
  // src/domain/tools/change-management-tools.ts
6362
6362
  import { z as z25 } from "zod";
6363
- import { readFileSync as readFileSync2 } from "fs";
6364
6363
 
6365
6364
  // src/domain/snapshot/snapshot-engine.ts
6366
6365
  import { mkdir, writeFile, readFile } from "fs/promises";
@@ -8081,8 +8080,10 @@ async function loadSnapshot(filePath) {
8081
8080
  }
8082
8081
 
8083
8082
  // src/domain/snapshot/write-journal.ts
8083
+ import { randomBytes as randomBytes2 } from "crypto";
8084
+ import { readdirSync, readFileSync as readFileSync2 } from "fs";
8084
8085
  import { appendFile as appendFile2, mkdir as mkdir2 } from "fs/promises";
8085
- import { dirname } from "path";
8086
+ import { basename, dirname, extname, join as join3 } from "path";
8086
8087
  import { nanoid as nanoid2 } from "nanoid";
8087
8088
 
8088
8089
  // src/observability/audit-log.ts
@@ -8156,6 +8157,72 @@ function recordAttempt(args) {
8156
8157
  });
8157
8158
  return id;
8158
8159
  }
8160
+ function parseJournal(raw) {
8161
+ const entries = [];
8162
+ for (const line of raw.split("\n")) {
8163
+ if (!line.trim()) continue;
8164
+ try {
8165
+ const entry = JSON.parse(line);
8166
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
8167
+ entries.push(entry);
8168
+ }
8169
+ } catch {
8170
+ continue;
8171
+ }
8172
+ }
8173
+ return entries;
8174
+ }
8175
+ var SEGMENT_STAMP = /^\d{8}T\d{6}Z-[0-9a-z]+$/;
8176
+ function journalSegmentName(journalPath, now, suffix = randomBytes2(6).toString("hex")) {
8177
+ const ext = extname(journalPath);
8178
+ const stamp2 = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
8179
+ return join3(dirname(journalPath), `${basename(journalPath, ext)}.${stamp2}-${suffix}${ext}`);
8180
+ }
8181
+ function journalSegmentPaths(journalPath) {
8182
+ const ext = extname(journalPath);
8183
+ const prefix = `${basename(journalPath, ext)}.`;
8184
+ let names;
8185
+ try {
8186
+ names = readdirSync(dirname(journalPath));
8187
+ } catch {
8188
+ return [];
8189
+ }
8190
+ return names.filter(
8191
+ (n) => n.startsWith(prefix) && n.endsWith(ext) && SEGMENT_STAMP.test(n.slice(prefix.length, n.length - ext.length))
8192
+ ).sort().map((n) => join3(dirname(journalPath), n));
8193
+ }
8194
+ function journalSegmentRotatedAt(segmentPath) {
8195
+ const stamp2 = /\.(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z-[0-9a-z]+\.[^.]+$/.exec(
8196
+ segmentPath
8197
+ );
8198
+ if (!stamp2) return NaN;
8199
+ const [, y, mo, d, h, mi, se] = stamp2;
8200
+ return Date.parse(`${y}-${mo}-${d}T${h}:${mi}:${se}Z`);
8201
+ }
8202
+ function readIfExists(path) {
8203
+ try {
8204
+ return readFileSync2(path, "utf-8");
8205
+ } catch (err) {
8206
+ if (err.code === "ENOENT") return "";
8207
+ throw err;
8208
+ }
8209
+ }
8210
+ function readJournalEntries(journalPath) {
8211
+ const current = parseJournal(readIfExists(journalPath));
8212
+ const segments2 = journalSegmentPaths(journalPath).flatMap((p) => parseJournal(readIfExists(p)));
8213
+ const seen = /* @__PURE__ */ new Set();
8214
+ const entries = [...segments2, ...current].filter((e) => {
8215
+ const key2 = `${String(e.id)}\0${String(e.phase)}`;
8216
+ if (seen.has(key2)) return false;
8217
+ seen.add(key2);
8218
+ return true;
8219
+ });
8220
+ const time = (e) => {
8221
+ const t = typeof e.ts === "string" ? Date.parse(e.ts) : NaN;
8222
+ return Number.isFinite(t) ? t : Number.POSITIVE_INFINITY;
8223
+ };
8224
+ return entries.map((e, i) => ({ e, i, t: time(e) })).sort((a, b) => a.t - b.t || a.i - b.i).map(({ e }) => e);
8225
+ }
8159
8226
  function recordOutcome(args) {
8160
8227
  void appendLine(args.journalPath, {
8161
8228
  id: args.journalId,
@@ -9046,47 +9113,6 @@ function unionVlanBlob(currentWire, targetWire) {
9046
9113
  return dumpBlob([...rows.entries()].sort((a, b) => a[0] - b[0]).map(([, row]) => row));
9047
9114
  }
9048
9115
 
9049
- // src/middleware/authz.ts
9050
- function matchesPattern(name, pattern) {
9051
- return globMatch(pattern, name);
9052
- }
9053
- function checkAuthz(identity, toolName, routerId2) {
9054
- if (identity.allowedRouters.length > 0 && !identity.allowedRouters.includes(routerId2)) {
9055
- throw new MikroMCPError({
9056
- category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
9057
- code: "ROUTER_NOT_ALLOWED",
9058
- message: `Identity "${identity.id}" is not allowed to access router "${routerId2}".`,
9059
- details: { identityId: identity.id, routerId: routerId2, allowedRouters: identity.allowedRouters },
9060
- recoverability: {
9061
- retryable: false,
9062
- suggestedAction: `Add "${routerId2}" to allowedRouters for identity "${identity.id}" in identities.yaml.`
9063
- }
9064
- });
9065
- }
9066
- checkToolAllowed(identity, toolName);
9067
- }
9068
- function checkToolAllowed(identity, toolName) {
9069
- if (identity.allowedToolPatterns.length > 0) {
9070
- const allowed = identity.allowedToolPatterns.some((p) => matchesPattern(toolName, p));
9071
- if (!allowed) {
9072
- throw new MikroMCPError({
9073
- category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
9074
- code: "TOOL_NOT_ALLOWED",
9075
- message: `Identity "${identity.id}" is not allowed to call tool "${toolName}".`,
9076
- details: {
9077
- identityId: identity.id,
9078
- toolName,
9079
- allowedToolPatterns: identity.allowedToolPatterns
9080
- },
9081
- recoverability: {
9082
- retryable: false,
9083
- suggestedAction: `Add a matching pattern to allowedToolPatterns for identity "${identity.id}" in identities.yaml.`
9084
- }
9085
- });
9086
- }
9087
- }
9088
- }
9089
-
9090
9116
  // src/domain/tools/change-management-tools.ts
9091
9117
  var log24 = createLogger("change-management");
9092
9118
  var stepSchema = z25.object({
@@ -9125,7 +9151,6 @@ var listJournalTool = {
9125
9151
  skipRouterContext: true,
9126
9152
  async handler(params, context) {
9127
9153
  const parsed = listJournalInputSchema.parse(params);
9128
- checkToolAllowed(context.identity, "list_journal");
9129
9154
  const journalPath = context.appConfig.journalPath;
9130
9155
  if (!journalPath) {
9131
9156
  return {
@@ -9133,35 +9158,24 @@ var listJournalTool = {
9133
9158
  structuredContent: { journalEnabled: false, entries: [], total: 0, returned: 0 }
9134
9159
  };
9135
9160
  }
9136
- let raw;
9161
+ let journal;
9137
9162
  try {
9138
- raw = readFileSync2(journalPath, "utf-8");
9163
+ journal = readJournalEntries(journalPath);
9139
9164
  } catch (err) {
9140
- if (err.code === "ENOENT") {
9141
- raw = "";
9142
- } else {
9143
- throw new MikroMCPError({
9144
- category: "INTERNAL" /* INTERNAL */,
9145
- code: "JOURNAL_READ_FAILED",
9146
- message: `Could not read the write journal: ${err.message}`,
9147
- details: { journalPath },
9148
- recoverability: {
9149
- retryable: false,
9150
- suggestedAction: "Check the file's permissions under the MikroMCP data directory."
9151
- }
9152
- });
9153
- }
9165
+ throw new MikroMCPError({
9166
+ category: "INTERNAL" /* INTERNAL */,
9167
+ code: "JOURNAL_READ_FAILED",
9168
+ message: `Could not read the write journal: ${err.message}`,
9169
+ details: { journalPath },
9170
+ recoverability: {
9171
+ retryable: false,
9172
+ suggestedAction: "Check the file's permissions under the MikroMCP data directory."
9173
+ }
9174
+ });
9154
9175
  }
9155
9176
  const outcomes = /* @__PURE__ */ new Map();
9156
9177
  const attempts = [];
9157
- for (const line of raw.split("\n")) {
9158
- if (!line.trim()) continue;
9159
- let entry;
9160
- try {
9161
- entry = JSON.parse(line);
9162
- } catch {
9163
- continue;
9164
- }
9178
+ for (const entry of journal) {
9165
9179
  if (entry.phase === "attempt") attempts.push(entry);
9166
9180
  else
9167
9181
  outcomes.set(entry.id, {
@@ -9486,9 +9500,9 @@ function createChangeManagementTools(baseTools2) {
9486
9500
  }
9487
9501
  };
9488
9502
  function findJournalEntry(journalPath, journalId) {
9489
- const raw = readFileSync2(journalPath, "utf-8");
9490
- const lines = raw.split("\n").filter(Boolean);
9491
- const entry = lines.map((l) => JSON.parse(l)).find((e) => e.id === journalId && e.phase === "attempt");
9503
+ const entry = readJournalEntries(journalPath).find(
9504
+ (e) => e.id === journalId && e.phase === "attempt"
9505
+ );
9492
9506
  if (!entry) {
9493
9507
  throw new MikroMCPError({
9494
9508
  category: "NOT_FOUND" /* NOT_FOUND */,
@@ -13764,13 +13778,54 @@ var log41 = createLogger("backup-tools");
13764
13778
  var createBackupInputSchema = z42.object({
13765
13779
  routerId,
13766
13780
  name: z42.string().default("backup").describe("Backup file name (without extension)"),
13767
- password: z42.string().optional().describe("Password to encrypt the backup file"),
13781
+ password: z42.string().optional().describe(
13782
+ "Literal password to encrypt the backup. It stays in the conversation history; prefer passwordSecret"
13783
+ ),
13784
+ passwordSecret: z42.string().regex(/^[A-Z0-9_]+$/, "a secret name is the NAME of MIKROMCP_SECRET_<NAME>").optional().describe(
13785
+ "Encrypt with the server's MIKROMCP_SECRET_<NAME> value, bound to this router by its secrets list in routers.yaml. The value never reaches the conversation"
13786
+ ),
13768
13787
  dryRun: z42.boolean().default(false).describe("Preview the backup without creating it")
13769
13788
  }).strict();
13789
+ function backupError(code, message, suggestedAction) {
13790
+ return new MikroMCPError({
13791
+ category: "VALIDATION" /* VALIDATION */,
13792
+ code,
13793
+ message,
13794
+ recoverability: { retryable: false, suggestedAction }
13795
+ });
13796
+ }
13797
+ function resolveBackupPassword(parsed, context) {
13798
+ const name = parsed.passwordSecret;
13799
+ if (name === void 0) return parsed.password;
13800
+ if (parsed.password !== void 0) {
13801
+ throw backupError(
13802
+ "BACKUP_PASSWORD_CONFLICT",
13803
+ "Pass either password or passwordSecret, not both.",
13804
+ "Drop password and keep passwordSecret, so the password stays out of the conversation."
13805
+ );
13806
+ }
13807
+ const bound = context.routerConfig.secrets ?? [];
13808
+ if (!bound.includes(name)) {
13809
+ throw backupError(
13810
+ "BACKUP_SECRET_NOT_BOUND",
13811
+ bound.length ? `Secret ${name} is not bound to router "${context.routerId}" (its routers.yaml secrets list names ${bound.join(", ")}).` : `Router "${context.routerId}" has no secrets list in routers.yaml, so it may use no secret.`,
13812
+ `Add ${name} to the router's \`secrets\` list in routers.yaml if it is meant for it.`
13813
+ );
13814
+ }
13815
+ const value = context.appConfig.configSecrets[name];
13816
+ if (!value) {
13817
+ throw backupError(
13818
+ "BACKUP_SECRET_MISSING",
13819
+ `Secret ${name} is bound to router "${context.routerId}", but MIKROMCP_SECRET_${name} is not set or empty.`,
13820
+ `Set MIKROMCP_SECRET_${name} in the MikroMCP server's environment and restart it.`
13821
+ );
13822
+ }
13823
+ return value;
13824
+ }
13770
13825
  var createBackupTool = {
13771
13826
  name: "create_backup",
13772
13827
  title: "Create Backup",
13773
- description: "Create a binary configuration backup on a MikroTik router. The backup is saved as <name>.backup on the router's filesystem. Supports optional encryption via password and dry-run mode.",
13828
+ description: "Create a binary configuration backup on a MikroTik router. The backup is saved as <name>.backup on the router's filesystem. To encrypt it, prefer passwordSecret (a MIKROMCP_SECRET_<NAME> bound to the router in routers.yaml \u2014 the password never reaches the conversation) over a literal password. Dry run checks that the secret resolves.",
13774
13829
  inputSchema: createBackupInputSchema,
13775
13830
  annotations: {
13776
13831
  readOnlyHint: false,
@@ -13781,25 +13836,34 @@ var createBackupTool = {
13781
13836
  async handler(params, context) {
13782
13837
  const parsed = createBackupInputSchema.parse(params);
13783
13838
  log41.info({ routerId: context.routerId, name: parsed.name }, "Creating backup");
13839
+ const password = resolveBackupPassword(parsed, context);
13840
+ const encrypted = password !== void 0 && password !== "";
13841
+ const how = encrypted ? "encrypted " : "";
13784
13842
  if (parsed.dryRun) {
13785
13843
  return {
13786
- content: `Dry run: Would create backup "${parsed.name}.backup" on ${context.routerId}.`,
13787
- structuredContent: { action: "dry_run", name: parsed.name, routerId: context.routerId }
13844
+ content: `Dry run: Would create ${how}backup "${parsed.name}.backup" on ${context.routerId}.`,
13845
+ structuredContent: {
13846
+ action: "dry_run",
13847
+ name: parsed.name,
13848
+ routerId: context.routerId,
13849
+ encrypted
13850
+ }
13788
13851
  };
13789
13852
  }
13790
13853
  try {
13791
13854
  const body = { name: parsed.name };
13792
- if (parsed.password !== void 0) {
13793
- body.password = parsed.password;
13855
+ if (password !== void 0) {
13856
+ body.password = password;
13794
13857
  }
13795
13858
  await context.routerClient.execute("system/backup/save", body);
13796
- log41.info({ routerId: context.routerId, name: parsed.name }, "Backup created");
13859
+ log41.info({ routerId: context.routerId, name: parsed.name, encrypted }, "Backup created");
13797
13860
  return {
13798
- content: `Backup "${parsed.name}.backup" created on ${context.routerId}.`,
13861
+ content: `Backup "${parsed.name}.backup" created ${how}on ${context.routerId}.`,
13799
13862
  structuredContent: {
13800
13863
  action: "created",
13801
13864
  routerId: context.routerId,
13802
- filePath: `${parsed.name}.backup`
13865
+ filePath: `${parsed.name}.backup`,
13866
+ encrypted
13803
13867
  }
13804
13868
  };
13805
13869
  } catch (err) {
@@ -16100,7 +16164,7 @@ var swosConfigTools = [getSwosConfigTool, applySwosConfigTool];
16100
16164
  import { z as z49 } from "zod";
16101
16165
  import { createHash as createHash2 } from "crypto";
16102
16166
  import { chmod, mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
16103
- import { join as join3, resolve, sep } from "path";
16167
+ import { join as join4, resolve, sep } from "path";
16104
16168
  var log48 = createLogger("swos-maintenance-tools");
16105
16169
  var DOWNLOAD_BASE = "https://download.mikrotik.com/swos";
16106
16170
  var FIRMWARE_MIN_BYTES = 16 * 1024;
@@ -16110,7 +16174,7 @@ var RESTORE_TIMEOUT_MS = 6e4;
16110
16174
  var FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
16111
16175
  var fileName = z49.string().regex(FILE_NAME, "a plain file name inside the server's SwOS folder, no path");
16112
16176
  function swosDir(context) {
16113
- return join3(context.appConfig.dataDir, "swos");
16177
+ return join4(context.appConfig.dataDir, "swos");
16114
16178
  }
16115
16179
  function sha256(data) {
16116
16180
  return createHash2("sha256").update(data).digest("hex");
@@ -16702,6 +16766,47 @@ var swosMaintenanceTools = [
16702
16766
  // src/domain/tools/fleet-tools.ts
16703
16767
  import { z as z50 } from "zod";
16704
16768
 
16769
+ // src/middleware/authz.ts
16770
+ function matchesPattern(name, pattern) {
16771
+ return globMatch(pattern, name);
16772
+ }
16773
+ function checkAuthz(identity, toolName, routerId2) {
16774
+ if (identity.allowedRouters.length > 0 && !identity.allowedRouters.includes(routerId2)) {
16775
+ throw new MikroMCPError({
16776
+ category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
16777
+ code: "ROUTER_NOT_ALLOWED",
16778
+ message: `Identity "${identity.id}" is not allowed to access router "${routerId2}".`,
16779
+ details: { identityId: identity.id, routerId: routerId2, allowedRouters: identity.allowedRouters },
16780
+ recoverability: {
16781
+ retryable: false,
16782
+ suggestedAction: `Add "${routerId2}" to allowedRouters for identity "${identity.id}" in identities.yaml.`
16783
+ }
16784
+ });
16785
+ }
16786
+ checkToolAllowed(identity, toolName);
16787
+ }
16788
+ function checkToolAllowed(identity, toolName) {
16789
+ if (identity.allowedToolPatterns.length > 0) {
16790
+ const allowed = identity.allowedToolPatterns.some((p) => matchesPattern(toolName, p));
16791
+ if (!allowed) {
16792
+ throw new MikroMCPError({
16793
+ category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
16794
+ code: "TOOL_NOT_ALLOWED",
16795
+ message: `Identity "${identity.id}" is not allowed to call tool "${toolName}".`,
16796
+ details: {
16797
+ identityId: identity.id,
16798
+ toolName,
16799
+ allowedToolPatterns: identity.allowedToolPatterns
16800
+ },
16801
+ recoverability: {
16802
+ retryable: false,
16803
+ suggestedAction: `Add a matching pattern to allowedToolPatterns for identity "${identity.id}" in identities.yaml.`
16804
+ }
16805
+ });
16806
+ }
16807
+ }
16808
+ }
16809
+
16705
16810
  // src/config/secrets.ts
16706
16811
  function getCredentials(router) {
16707
16812
  const { source, envPrefix, vaultPath } = router.credentials;
@@ -17517,6 +17622,7 @@ async function executeToolCall(tool, args, deps) {
17517
17622
  try {
17518
17623
  const identity = getCurrentIdentity() ?? getStdioIdentity(config.stdioIdentity, identityRegistry);
17519
17624
  if (tool.skipRouterContext) {
17625
+ checkToolAllowed(identity, tool.name);
17520
17626
  const fleetContext = {
17521
17627
  routerClient: fleetUnavailable("routerClient"),
17522
17628
  deviceClient: fleetUnavailable("deviceClient"),
@@ -17696,9 +17802,9 @@ Journal ID: ${journalId} (pass to rollback_change to undo).`;
17696
17802
  }
17697
17803
 
17698
17804
  // src/middleware/fleet-confirmation.ts
17699
- import { createHmac as createHmac2, createHash as createHash5, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
17805
+ import { createHmac as createHmac2, createHash as createHash5, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
17700
17806
  var CONFIRMATION_TTL_MS2 = 5 * 60 * 1e3;
17701
- var processSecret = randomBytes2(32).toString("hex");
17807
+ var processSecret = randomBytes3(32).toString("hex");
17702
17808
  var usedTokens2 = /* @__PURE__ */ new Map();
17703
17809
  function fingerprint(args) {
17704
17810
  const sortedRouters = [...args.routerIds].sort();
@@ -18391,8 +18497,9 @@ var RouterConfigSchema = z52.object({
18391
18497
  }).optional(),
18392
18498
  cmdAllow: z52.array(z52.string()).optional(),
18393
18499
  cmdDeny: z52.array(z52.string()).optional(),
18394
- // SwOS only: the MIKROMCP_SECRET_<NAME> values a switch config applied to
18395
- // this switch may reference. Without the list it may reference none.
18500
+ // The MIKROMCP_SECRET_<NAME> values this device may use: a switch config
18501
+ // applied to a SwOS switch, create_backup's passwordSecret on RouterOS.
18502
+ // Without the list it may use none.
18396
18503
  secrets: z52.array(
18397
18504
  z52.string().regex(/^[A-Z0-9_]+$/, "a secret name is the NAME of MIKROMCP_SECRET_<NAME>")
18398
18505
  ).optional(),
@@ -18420,13 +18527,6 @@ var RouterConfigSchema = z52.object({
18420
18527
  message: "rosVersion is required for RouterOS devices"
18421
18528
  });
18422
18529
  }
18423
- if (config.secrets) {
18424
- ctx.addIssue({
18425
- code: z52.ZodIssueCode.custom,
18426
- path: ["secrets"],
18427
- message: "secrets binds switch config secrets and applies to SwOS devices only"
18428
- });
18429
- }
18430
18530
  return;
18431
18531
  }
18432
18532
  if (config.tls?.enabled) {
@@ -18599,7 +18699,7 @@ var ConnectionPool = class {
18599
18699
  };
18600
18700
 
18601
18701
  // src/version.ts
18602
- var VERSION2 = "1.15.0";
18702
+ var VERSION2 = "1.16.0";
18603
18703
 
18604
18704
  // src/mcp/server.ts
18605
18705
  var SERVER_INSTRUCTIONS = [
@@ -18987,8 +19087,8 @@ ${issues}`);
18987
19087
  };
18988
19088
 
18989
19089
  // src/domain/snapshot/retention.ts
18990
- import { readdir, stat, unlink } from "fs/promises";
18991
- import { join as join4 } from "path";
19090
+ import { readdir, readFile as readFile3, rename, stat, unlink } from "fs/promises";
19091
+ import { join as join5 } from "path";
18992
19092
  var log56 = createLogger("snapshot-retention");
18993
19093
  async function pruneSnapshots(snapshotDir, maxAgeDays) {
18994
19094
  const cutoffMs = Date.now() - maxAgeDays * 864e5;
@@ -19000,7 +19100,7 @@ async function pruneSnapshots(snapshotDir, maxAgeDays) {
19000
19100
  return 0;
19001
19101
  }
19002
19102
  for (const routerDir of routerDirs) {
19003
- const dirPath = join4(snapshotDir, routerDir);
19103
+ const dirPath = join5(snapshotDir, routerDir);
19004
19104
  let files;
19005
19105
  try {
19006
19106
  files = await readdir(dirPath);
@@ -19008,7 +19108,7 @@ async function pruneSnapshots(snapshotDir, maxAgeDays) {
19008
19108
  continue;
19009
19109
  }
19010
19110
  for (const file of files) {
19011
- const filePath = join4(dirPath, file);
19111
+ const filePath = join5(dirPath, file);
19012
19112
  try {
19013
19113
  const info = await stat(filePath);
19014
19114
  if (info.isFile() && info.mtimeMs < cutoffMs) {
@@ -19025,6 +19125,41 @@ async function pruneSnapshots(snapshotDir, maxAgeDays) {
19025
19125
  }
19026
19126
  return removed;
19027
19127
  }
19128
+ var JOURNAL_ROTATE_AFTER_MS = 864e5;
19129
+ async function rotateAndPruneJournal(journalPath, maxAgeDays, now = /* @__PURE__ */ new Date()) {
19130
+ const cutoffMs = now.getTime() - maxAgeDays * 864e5;
19131
+ let removed = 0;
19132
+ for (const segment of journalSegmentPaths(journalPath)) {
19133
+ try {
19134
+ if (journalSegmentRotatedAt(segment) < cutoffMs && (await stat(segment)).mtimeMs < cutoffMs) {
19135
+ await unlink(segment);
19136
+ removed++;
19137
+ }
19138
+ } catch (err) {
19139
+ if (err.code !== "ENOENT") {
19140
+ log56.warn({ err, segment }, "Failed to prune a write-journal segment");
19141
+ }
19142
+ }
19143
+ }
19144
+ if (removed > 0) {
19145
+ log56.info({ removed, maxAgeDays }, "Pruned expired write-journal segments");
19146
+ }
19147
+ let rotated = false;
19148
+ try {
19149
+ const oldest = parseJournal(await readFile3(journalPath, "utf-8")).find(
19150
+ (e) => typeof e.ts === "string" && Number.isFinite(Date.parse(e.ts))
19151
+ );
19152
+ if (oldest && Date.parse(oldest.ts) <= now.getTime() - JOURNAL_ROTATE_AFTER_MS) {
19153
+ await rename(journalPath, journalSegmentName(journalPath, now));
19154
+ rotated = true;
19155
+ }
19156
+ } catch (err) {
19157
+ if (err.code !== "ENOENT") {
19158
+ log56.warn({ err, journalPath }, "Failed to rotate the write journal");
19159
+ }
19160
+ }
19161
+ return { rotated, removed };
19162
+ }
19028
19163
 
19029
19164
  // src/cli/serve.ts
19030
19165
  var log57 = createLogger("main");
@@ -19032,6 +19167,7 @@ async function runServe() {
19032
19167
  const config = loadAppConfig();
19033
19168
  log57.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
19034
19169
  void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
19170
+ void rotateAndPruneJournal(config.journalPath, config.retention.snapshotMaxAgeDays);
19035
19171
  const identityRegistry = new IdentityRegistry(config.identitiesPath);
19036
19172
  if (config.transport === "http") {
19037
19173
  const hasLimitedRoles = identityRegistry.getIdentities().some((i) => i.role === "readonly" || i.role === "operator");
@@ -19077,7 +19213,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
19077
19213
  import { homedir as homedir2 } from "os";
19078
19214
  import * as net from "net";
19079
19215
  import chalk from "chalk";
19080
- var localVersion = "1.15.0";
19216
+ var localVersion = "1.16.0";
19081
19217
  var results = [];
19082
19218
  function ok(msg) {
19083
19219
  console.log(chalk.green("\u2705 ") + msg);
@@ -19168,7 +19304,16 @@ function checkEnvVars(routers, transport2) {
19168
19304
  }
19169
19305
  const secretNames = Object.keys(process.env).filter((k) => k.startsWith("MIKROMCP_SECRET_"));
19170
19306
  for (const router of routers) {
19171
- if (router.deviceType !== "swos") continue;
19307
+ if (router.deviceType !== "swos") {
19308
+ for (const name of router.secrets ?? []) {
19309
+ if (!process.env[`MIKROMCP_SECRET_${name}`]) {
19310
+ warn(
19311
+ `${router.id} binds secret ${name}, but MIKROMCP_SECRET_${name} is not set or empty \u2014 create_backup with passwordSecret ${name} will be refused`
19312
+ );
19313
+ }
19314
+ }
19315
+ continue;
19316
+ }
19172
19317
  if (router.secrets) {
19173
19318
  for (const name of router.secrets) {
19174
19319
  if (!process.env[`MIKROMCP_SECRET_${name}`]) {
@@ -19474,18 +19619,18 @@ async function runDoctor() {
19474
19619
 
19475
19620
  // src/cli/init.ts
19476
19621
  import { input, confirm, select } from "@inquirer/prompts";
19477
- import { randomBytes as randomBytes3 } from "crypto";
19622
+ import { randomBytes as randomBytes4 } from "crypto";
19478
19623
  import { existsSync as existsSync4, readFileSync as readFileSync8, writeFileSync, mkdirSync, copyFileSync } from "fs";
19479
19624
  import { homedir as homedir3 } from "os";
19480
- import { join as join5 } from "path";
19625
+ import { join as join6 } from "path";
19481
19626
  import { parse as yamlParse, Document, visit } from "yaml";
19482
19627
  import bcrypt2 from "bcryptjs";
19483
19628
  import chalk2 from "chalk";
19484
19629
  function mikromcpDir2() {
19485
- return join5(homedir3(), ".mikromcp");
19630
+ return join6(homedir3(), ".mikromcp");
19486
19631
  }
19487
19632
  function defaultAuditLogPath() {
19488
- return join5(mikromcpDir2(), "audit.ndjson");
19633
+ return join6(mikromcpDir2(), "audit.ndjson");
19489
19634
  }
19490
19635
  function toYaml2(obj) {
19491
19636
  const doc = new Document(obj);
@@ -19513,8 +19658,8 @@ function splitTags(val) {
19513
19658
  }
19514
19659
  function findClaudeDesktopConfig() {
19515
19660
  const candidates = [
19516
- join5(homedir3(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
19517
- join5(homedir3(), ".config", "Claude", "claude_desktop_config.json")
19661
+ join6(homedir3(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
19662
+ join6(homedir3(), ".config", "Claude", "claude_desktop_config.json")
19518
19663
  ];
19519
19664
  return candidates.find((p) => existsSync4(p)) ?? null;
19520
19665
  }
@@ -19634,7 +19779,7 @@ async function collectIdentityInfo() {
19634
19779
  message: "Allowed tool patterns (comma-separated or * for all):",
19635
19780
  default: "*"
19636
19781
  });
19637
- const rawToken = randomBytes3(32).toString("hex");
19782
+ const rawToken = randomBytes4(32).toString("hex");
19638
19783
  console.log(chalk2.green(`
19639
19784
  Generated token: ${chalk2.bold(rawToken)}`));
19640
19785
  console.log(chalk2.yellow(" \u26A0 Save this token \u2014 it will NOT be shown again.\n"));
@@ -19682,7 +19827,7 @@ async function collectClaudeDesktopPreference() {
19682
19827
  return confirm({ message: "Register with Claude Desktop?", default: true });
19683
19828
  }
19684
19829
  function writeRoutersYaml(data) {
19685
- const filePath = join5(data.configDir, "routers.yaml");
19830
+ const filePath = join6(data.configDir, "routers.yaml");
19686
19831
  const newEntry = {
19687
19832
  host: data.host,
19688
19833
  port: data.port,
@@ -19709,7 +19854,7 @@ function writeRoutersYaml(data) {
19709
19854
  return true;
19710
19855
  }
19711
19856
  function writeIdentitiesYaml(data) {
19712
- const filePath = join5(data.configDir, "identities.yaml");
19857
+ const filePath = join6(data.configDir, "identities.yaml");
19713
19858
  const newEntry = {
19714
19859
  token: data.tokenHash,
19715
19860
  role: data.role,
@@ -19731,7 +19876,7 @@ function writeDotEnv(data) {
19731
19876
  const userKey = `${data.envPrefix}_USER`;
19732
19877
  const passKey = `${data.envPrefix}_PASS`;
19733
19878
  const auditLogPath = defaultAuditLogPath();
19734
- const confirmationSecret = data.transport === "http" ? randomBytes3(32).toString("hex") : "";
19879
+ const confirmationSecret = data.transport === "http" ? randomBytes4(32).toString("hex") : "";
19735
19880
  const lines = [
19736
19881
  "# \u2500\u2500 Router credentials \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
19737
19882
  `${userKey}=${data.routerUser}`,
@@ -19743,8 +19888,8 @@ function writeDotEnv(data) {
19743
19888
  `MIKROMCP_TRANSPORT=${data.transport}`,
19744
19889
  "",
19745
19890
  "# \u2500\u2500 Config paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
19746
- `MIKROMCP_CONFIG_PATH=${join5(data.configDir, "routers.yaml")}`,
19747
- `MIKROMCP_IDENTITIES_PATH=${join5(data.configDir, "identities.yaml")}`,
19891
+ `MIKROMCP_CONFIG_PATH=${join6(data.configDir, "routers.yaml")}`,
19892
+ `MIKROMCP_IDENTITIES_PATH=${join6(data.configDir, "identities.yaml")}`,
19748
19893
  "",
19749
19894
  "# \u2500\u2500 Default router \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
19750
19895
  "# Router used when a tool call omits routerId (handy for single-router setups).",
@@ -19798,9 +19943,9 @@ function registerClaudeDesktop() {
19798
19943
  async function runInit() {
19799
19944
  console.log(chalk2.bold.cyan("\n\u{1F527} MikroMCP \u2014 Interactive Setup Wizard\n"));
19800
19945
  const configDir = mikromcpDir2();
19801
- const envPath = join5(configDir, ".env");
19946
+ const envPath = join6(configDir, ".env");
19802
19947
  const existingFiles = [".env", "routers.yaml", "identities.yaml"].filter(
19803
- (f2) => existsSync4(join5(configDir, f2))
19948
+ (f2) => existsSync4(join6(configDir, f2))
19804
19949
  );
19805
19950
  if (existingFiles.length > 0) {
19806
19951
  console.log(chalk2.yellow(` \u26A0 Found existing files in ${configDir}:`));
@@ -19838,12 +19983,12 @@ async function runInit() {
19838
19983
  if (data.writeRoutersYaml) {
19839
19984
  writeRoutersYaml(data);
19840
19985
  summary.push(`routers.yaml written to ${data.configDir}`);
19841
- console.log(chalk2.green(` \u2714 ${join5(data.configDir, "routers.yaml")}`));
19986
+ console.log(chalk2.green(` \u2714 ${join6(data.configDir, "routers.yaml")}`));
19842
19987
  }
19843
19988
  if (data.writeIdentitiesYaml) {
19844
19989
  writeIdentitiesYaml(data);
19845
19990
  summary.push(`identities.yaml written to ${data.configDir}`);
19846
- console.log(chalk2.green(` \u2714 ${join5(data.configDir, "identities.yaml")}`));
19991
+ console.log(chalk2.green(` \u2714 ${join6(data.configDir, "identities.yaml")}`));
19847
19992
  }
19848
19993
  if (data.writeEnv) {
19849
19994
  writeDotEnv(data);
@@ -19893,7 +20038,7 @@ async function runInit() {
19893
20038
  }
19894
20039
 
19895
20040
  // src/cli/index.ts
19896
- var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.15.0");
20041
+ var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.16.0");
19897
20042
  program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
19898
20043
  program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
19899
20044
  program.command("init").description("Interactive setup wizard").action(runInit);
@@ -19903,6 +20048,6 @@ program.command("update").description("Update mikromcp to the latest version via
19903
20048
  });
19904
20049
 
19905
20050
  // src/main.ts
19906
- loadDotenv({ path: join6(homedir4(), ".mikromcp", ".env"), override: false, quiet: true });
20051
+ loadDotenv({ path: join7(homedir4(), ".mikromcp", ".env"), override: false, quiet: true });
19907
20052
  program.parse();
19908
20053
  //# sourceMappingURL=main.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikromcp",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "mcpName": "io.github.AliKarami/mikromcp",
5
5
  "description": "Production-grade MCP server for MikroTik RouterOS routers and SwOS switches",
6
6
  "type": "module",