mikromcp 1.0.9 → 1.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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/main.js +888 -592
  3. package/package.json +3 -2
package/dist/main.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/main.ts
4
4
  import { config as loadDotenv } from "dotenv";
5
5
  import { join as join5 } from "path";
6
- import { homedir as homedir5 } from "os";
6
+ import { homedir as homedir4 } from "os";
7
7
 
8
8
  // src/cli/index.ts
9
9
  import { Command } from "commander";
@@ -50,9 +50,8 @@ function loadAppConfig() {
50
50
  failureThreshold: 5,
51
51
  cooldownMs: 3e4
52
52
  },
53
- pagination: {
54
- defaultLimit: 100,
55
- maxLimit: 500
53
+ retention: {
54
+ snapshotMaxAgeDays: parseInt(env.MIKROMCP_SNAPSHOT_RETENTION_DAYS ?? "30", 10)
56
55
  }
57
56
  };
58
57
  }
@@ -371,8 +370,8 @@ function buildTextSummary(sections) {
371
370
  if (sections.health) {
372
371
  const h = sections.health;
373
372
  lines.push(`Health:`);
374
- for (const [key, value] of Object.entries(h)) {
375
- if (key !== ".id") lines.push(` ${key}: ${value}`);
373
+ for (const [key2, value] of Object.entries(h)) {
374
+ if (key2 !== ".id") lines.push(` ${key2}: ${value}`);
376
375
  }
377
376
  }
378
377
  if (sections.clock) {
@@ -493,9 +492,9 @@ var listInterfacesTool = {
493
492
  const paginated = interfaces.slice(parsed.offset, parsed.offset + parsed.limit);
494
493
  const results2 = parsed.includeCounters ? paginated : paginated.map((iface) => {
495
494
  const cleaned = {};
496
- for (const [key, value] of Object.entries(iface)) {
497
- if (!COUNTER_PROPS.includes(key)) {
498
- cleaned[key] = value;
495
+ for (const [key2, value] of Object.entries(iface)) {
496
+ if (!COUNTER_PROPS.includes(key2)) {
497
+ cleaned[key2] = value;
499
498
  }
500
499
  }
501
500
  return cleaned;
@@ -2112,8 +2111,8 @@ var runCommandTool = {
2112
2111
  async handler(params, context) {
2113
2112
  const parsed = runCommandInputSchema.parse(params);
2114
2113
  log8.info({ routerId: context.routerId, command: parsed.command }, "Running command");
2115
- const globalAllow = (process.env.MIKROMCP_CMD_ALLOW ?? "").split(",").map((s) => s.trim()).filter(Boolean);
2116
- const globalDeny = (process.env.MIKROMCP_CMD_DENY ?? "").split(",").map((s) => s.trim()).filter(Boolean);
2114
+ const globalAllow = context.appConfig.cmdAllow;
2115
+ const globalDeny = context.appConfig.cmdDeny;
2117
2116
  const policy = resolveCommandPolicy(context.routerConfig, globalAllow, globalDeny);
2118
2117
  checkCommand(parsed.command, policy);
2119
2118
  if (parsed.dryRun) {
@@ -2711,9 +2710,9 @@ var listPeersTool = {
2711
2710
  const lines = [`WireGuard peers on ${context.routerId}: ${total} total`];
2712
2711
  for (const peer of peers) {
2713
2712
  const rec = peer;
2714
- const key = rec["public-key"] ? rec["public-key"].slice(0, 8) + "\u2026" : "unknown";
2713
+ const key2 = rec["public-key"] ? rec["public-key"].slice(0, 8) + "\u2026" : "unknown";
2715
2714
  lines.push(
2716
- ` [${rec.interface ?? "?"}] ${key} last-handshake=${rec["last-handshake"] ?? "never"}`
2715
+ ` [${rec.interface ?? "?"}] ${key2} last-handshake=${rec["last-handshake"] ?? "never"}`
2717
2716
  );
2718
2717
  }
2719
2718
  return {
@@ -4905,12 +4904,10 @@ var containerTools = [listContainersTool, manageContainerTool];
4905
4904
 
4906
4905
  // src/domain/tools/change-management-tools.ts
4907
4906
  import { z as z23 } from "zod";
4908
- import { readFileSync as readFileSync2 } from "fs";
4909
- import { join as join3 } from "path";
4910
- import { homedir as homedir2 } from "os";
4907
+ import { readFileSync } from "fs";
4911
4908
 
4912
4909
  // src/domain/snapshot/snapshot-engine.ts
4913
- import { mkdirSync, writeFileSync, readFileSync } from "fs";
4910
+ import { mkdir, writeFile, readFile } from "fs/promises";
4914
4911
  import { join as join2 } from "path";
4915
4912
  import { nanoid } from "nanoid";
4916
4913
  function pathToSlug(rosPath) {
@@ -4925,13 +4922,13 @@ async function takeSnapshot(client, routerId, path, snapshotDir) {
4925
4922
  const dir = join2(snapshotDir, routerId);
4926
4923
  const filePath = join2(dir, `${id}.json`);
4927
4924
  const ts = (/* @__PURE__ */ new Date()).toISOString();
4928
- mkdirSync(dir, { recursive: true });
4925
+ await mkdir(dir, { recursive: true });
4929
4926
  const stored = { id, routerId, path, ts, records };
4930
- writeFileSync(filePath, JSON.stringify(stored, null, 2));
4927
+ await writeFile(filePath, JSON.stringify(stored, null, 2));
4931
4928
  return { id, routerId, path, ts, filePath, recordCount: records.length };
4932
4929
  }
4933
- function loadSnapshot(filePath) {
4934
- return JSON.parse(readFileSync(filePath, "utf-8"));
4930
+ async function loadSnapshot(filePath) {
4931
+ return JSON.parse(await readFile(filePath, "utf-8"));
4935
4932
  }
4936
4933
 
4937
4934
  // src/domain/snapshot/diff-engine.ts
@@ -4951,7 +4948,16 @@ var SEMANTIC_KEYS = {
4951
4948
  "system/scheduler": ["name"],
4952
4949
  "system/script": ["name"],
4953
4950
  "system/package": ["name"],
4954
- "container": ["name"]
4951
+ "container": ["name"],
4952
+ "certificate": ["name"],
4953
+ "file": ["name"],
4954
+ "interface/vrrp": ["name"],
4955
+ "ip/dhcp-server": ["name"],
4956
+ "ip/ipsec/peer": ["name"],
4957
+ "ip/pool": ["name"],
4958
+ "queue/simple": ["name"],
4959
+ "tool/netwatch": ["host"],
4960
+ "user": ["name"]
4955
4961
  };
4956
4962
  function normalizeValue(v) {
4957
4963
  if (v === "true" || v === "yes") return "true";
@@ -4986,8 +4992,8 @@ function computeRestorePlan(path, before, current) {
4986
4992
  const beforeByKey = new Map(before.map((r) => [semanticKey(r, keys), r]));
4987
4993
  const toCreate = [];
4988
4994
  const toUpdate = [];
4989
- for (const [key, beforeRecord] of beforeByKey) {
4990
- const currentRecord = currentByKey.get(key);
4995
+ for (const [key2, beforeRecord] of beforeByKey) {
4996
+ const currentRecord = currentByKey.get(key2);
4991
4997
  if (!currentRecord) {
4992
4998
  toCreate.push(beforeRecord);
4993
4999
  } else if (!recordsAreEqual(beforeRecord, currentRecord)) {
@@ -5012,52 +5018,63 @@ async function applyRestorePlan(plan, client) {
5012
5018
  }
5013
5019
 
5014
5020
  // src/domain/snapshot/write-journal.ts
5015
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2 } from "fs";
5021
+ import { appendFile as appendFile2, mkdir as mkdir2 } from "fs/promises";
5016
5022
  import { dirname } from "path";
5017
5023
  import { nanoid as nanoid2 } from "nanoid";
5018
5024
 
5019
5025
  // src/observability/audit-log.ts
5020
- import { appendFileSync } from "fs";
5026
+ import { appendFile } from "fs/promises";
5021
5027
  var log22 = createLogger("audit");
5022
- var SENSITIVE_KEYS = /* @__PURE__ */ new Set(["password", "token", "secret", "authorization", "credentials"]);
5028
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
5029
+ "password",
5030
+ "pass",
5031
+ "token",
5032
+ "secret",
5033
+ "authorization",
5034
+ "credentials",
5035
+ "privatekey",
5036
+ "presharedkey",
5037
+ "psk",
5038
+ "passphrase",
5039
+ "community",
5040
+ "apikey",
5041
+ "confirmationsecret"
5042
+ ]);
5023
5043
  function redactParams(params) {
5024
5044
  const out = {};
5025
- for (const [key, value] of Object.entries(params)) {
5026
- if (SENSITIVE_KEYS.has(key)) continue;
5045
+ for (const [key2, value] of Object.entries(params)) {
5046
+ if (SENSITIVE_KEYS.has(key2.toLowerCase())) continue;
5027
5047
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
5028
- out[key] = redactParams(value);
5048
+ out[key2] = redactParams(value);
5029
5049
  } else {
5030
- out[key] = value;
5050
+ out[key2] = value;
5031
5051
  }
5032
5052
  }
5033
5053
  return out;
5034
5054
  }
5035
- var auditFilePath = process.env.MIKROMCP_AUDIT_LOG_PATH || void 0;
5036
- function auditLog(event) {
5055
+ function auditLog(event, auditLogPath) {
5037
5056
  const safeEvent = { ...event, params: redactParams(event.params) };
5038
5057
  log22.info(safeEvent, "audit");
5039
- if (auditFilePath) {
5040
- try {
5041
- appendFileSync(auditFilePath, JSON.stringify(safeEvent) + "\n");
5042
- } catch (err) {
5043
- log22.error({ err, auditFilePath }, "Failed to write audit event to file");
5044
- }
5058
+ if (auditLogPath) {
5059
+ appendFile(auditLogPath, JSON.stringify(safeEvent) + "\n").catch((err) => {
5060
+ log22.error({ err, auditLogPath }, "Failed to write audit event to file");
5061
+ });
5045
5062
  }
5046
5063
  }
5047
5064
 
5048
5065
  // src/domain/snapshot/write-journal.ts
5049
5066
  var log23 = createLogger("write-journal");
5050
- function appendLine(filePath, obj) {
5067
+ async function appendLine(filePath, obj) {
5051
5068
  try {
5052
- mkdirSync2(dirname(filePath), { recursive: true });
5053
- appendFileSync2(filePath, JSON.stringify(obj) + "\n");
5069
+ await mkdir2(dirname(filePath), { recursive: true });
5070
+ await appendFile2(filePath, JSON.stringify(obj) + "\n");
5054
5071
  } catch (err) {
5055
5072
  log23.error({ err, filePath }, "Failed to write to write journal");
5056
5073
  }
5057
5074
  }
5058
5075
  function recordAttempt(args) {
5059
5076
  const id = nanoid2();
5060
- appendLine(args.journalPath, {
5077
+ void appendLine(args.journalPath, {
5061
5078
  id,
5062
5079
  ts: (/* @__PURE__ */ new Date()).toISOString(),
5063
5080
  identityId: args.identityId,
@@ -5071,7 +5088,7 @@ function recordAttempt(args) {
5071
5088
  return id;
5072
5089
  }
5073
5090
  function recordOutcome(args) {
5074
- appendLine(args.journalPath, {
5091
+ void appendLine(args.journalPath, {
5075
5092
  id: args.journalId,
5076
5093
  ts: (/* @__PURE__ */ new Date()).toISOString(),
5077
5094
  phase: args.phase,
@@ -5185,9 +5202,8 @@ function createChangeManagementTools(baseTools2) {
5185
5202
  async handler(params, context) {
5186
5203
  const parsed = applyPlanInputSchema.parse(params);
5187
5204
  log24.info({ routerId: context.routerId, stepCount: parsed.steps.length }, "Applying plan");
5188
- const dataDir = process.env.MIKROMCP_DATA_DIR ?? join3(homedir2(), ".mikromcp", "data");
5189
- const journalPath = `${dataDir}/write-journal.ndjson`;
5190
- const snapshotDir = `${dataDir}/snapshots`;
5205
+ const journalPath = context.appConfig.journalPath;
5206
+ const snapshotDir = context.appConfig.snapshotDir;
5191
5207
  const results2 = [];
5192
5208
  for (const [i, step] of parsed.steps.entries()) {
5193
5209
  const tool = requireTool(step.tool);
@@ -5212,9 +5228,11 @@ function createChangeManagementTools(baseTools2) {
5212
5228
  params: stepParams,
5213
5229
  snapshotIds
5214
5230
  });
5231
+ const stepStartMs = Date.now();
5215
5232
  try {
5216
- const result = await tool.handler(stepParams, context);
5217
- recordOutcome({ journalPath, journalId: stepJournalId, phase: "success", durationMs: 0 });
5233
+ const runStep = () => tool.handler(stepParams, context);
5234
+ const result = context.circuitBreaker ? await context.circuitBreaker.execute(runStep) : await runStep();
5235
+ recordOutcome({ journalPath, journalId: stepJournalId, phase: "success", durationMs: Date.now() - stepStartMs });
5218
5236
  results2.push({
5219
5237
  stepIndex: i,
5220
5238
  tool: step.tool,
@@ -5224,7 +5242,7 @@ function createChangeManagementTools(baseTools2) {
5224
5242
  });
5225
5243
  } catch (err) {
5226
5244
  const error = err instanceof MikroMCPError ? err : enrichError(err, { tool: step.tool });
5227
- recordOutcome({ journalPath, journalId: stepJournalId, phase: "failure", outcome: error.code, durationMs: 0 });
5245
+ recordOutcome({ journalPath, journalId: stepJournalId, phase: "failure", outcome: error.code, durationMs: Date.now() - stepStartMs });
5228
5246
  log24.error({ err: error, tool: step.tool, step: i }, "apply_plan step failed");
5229
5247
  return {
5230
5248
  content: `Apply failed at step ${i + 1}/${parsed.steps.length} (${step.tool}): ${error.message}. ${i} step(s) completed before failure.`,
@@ -5250,7 +5268,7 @@ function createChangeManagementTools(baseTools2) {
5250
5268
  }
5251
5269
  };
5252
5270
  function findJournalEntry(journalPath, journalId) {
5253
- const raw = readFileSync2(journalPath, "utf-8");
5271
+ const raw = readFileSync(journalPath, "utf-8");
5254
5272
  const lines = raw.split("\n").filter(Boolean);
5255
5273
  const entry = lines.map((l) => JSON.parse(l)).find((e) => e.id === journalId && e.phase === "attempt");
5256
5274
  if (!entry) {
@@ -5279,9 +5297,8 @@ function createChangeManagementTools(baseTools2) {
5279
5297
  },
5280
5298
  async handler(params, context) {
5281
5299
  const parsed = rollbackChangeInputSchema.parse(params);
5282
- const dataDir = process.env.MIKROMCP_DATA_DIR ?? join3(homedir2(), ".mikromcp", "data");
5283
- const journalPath = `${dataDir}/write-journal.ndjson`;
5284
- const snapshotDir = `${dataDir}/snapshots`;
5300
+ const journalPath = context.appConfig.journalPath;
5301
+ const snapshotDir = context.appConfig.snapshotDir;
5285
5302
  log24.info({ routerId: context.routerId, journalId: parsed.journalId }, "Rolling back change");
5286
5303
  const { snapshotIds } = findJournalEntry(journalPath, parsed.journalId);
5287
5304
  if (snapshotIds.length === 0) {
@@ -5293,7 +5310,7 @@ function createChangeManagementTools(baseTools2) {
5293
5310
  const restorePlans = [];
5294
5311
  for (const snapshotId of snapshotIds) {
5295
5312
  const filePath = `${snapshotDir}/${context.routerId}/${snapshotId}.json`;
5296
- const stored = loadSnapshot(filePath);
5313
+ const stored = await loadSnapshot(filePath);
5297
5314
  const current = await context.routerClient.get(stored.path, {});
5298
5315
  const plan = computeRestorePlan(stored.path, stored.records, current);
5299
5316
  restorePlans.push(plan);
@@ -7284,6 +7301,51 @@ function checkAuthz(identity, toolName, routerId) {
7284
7301
  }
7285
7302
  }
7286
7303
 
7304
+ // src/config/secrets.ts
7305
+ function getCredentials(router) {
7306
+ const { source, envPrefix, vaultPath } = router.credentials;
7307
+ if (source === "vault") {
7308
+ throw new MikroMCPError({
7309
+ category: "CONFIGURATION" /* CONFIGURATION */,
7310
+ code: "VAULT_NOT_SUPPORTED",
7311
+ message: `Vault credential source is not supported in v0.1 (vaultPath=${vaultPath ?? "n/a"})`,
7312
+ recoverability: {
7313
+ retryable: false,
7314
+ suggestedAction: 'Switch to source="env" and provide credentials via environment variables.'
7315
+ }
7316
+ });
7317
+ }
7318
+ if (!envPrefix) {
7319
+ throw new MikroMCPError({
7320
+ category: "CONFIGURATION" /* CONFIGURATION */,
7321
+ code: "MISSING_ENV_PREFIX",
7322
+ message: `Router "${router.id}" uses source="env" but no envPrefix is configured`,
7323
+ recoverability: {
7324
+ retryable: false,
7325
+ suggestedAction: "Set envPrefix in the router configuration."
7326
+ }
7327
+ });
7328
+ }
7329
+ const userKey = `${envPrefix}_USER`;
7330
+ const passKey = `${envPrefix}_PASS`;
7331
+ const username = process.env[userKey];
7332
+ const password = process.env[passKey];
7333
+ if (!username || !password) {
7334
+ const missing = [!username ? userKey : null, !password ? passKey : null].filter(Boolean);
7335
+ throw new MikroMCPError({
7336
+ category: "CONFIGURATION" /* CONFIGURATION */,
7337
+ code: "MISSING_CREDENTIALS",
7338
+ message: `Missing environment variable(s) for router "${router.id}": ${missing.join(", ")}`,
7339
+ details: { missing },
7340
+ recoverability: {
7341
+ retryable: false,
7342
+ suggestedAction: `Set the ${missing.join(" and ")} environment variable(s).`
7343
+ }
7344
+ });
7345
+ }
7346
+ return { username, password };
7347
+ }
7348
+
7287
7349
  // src/adapter/ssh-client.ts
7288
7350
  import { createHash } from "crypto";
7289
7351
  import { Client } from "ssh2";
@@ -7356,8 +7418,8 @@ var SshClient = class {
7356
7418
  };
7357
7419
  if (this.config.sshFingerprint) {
7358
7420
  const expected = this.config.sshFingerprint.toLowerCase();
7359
- connectOptions.hostVerifier = (key) => {
7360
- const actual = createHash("sha256").update(key).digest("hex");
7421
+ connectOptions.hostVerifier = (key2) => {
7422
+ const actual = createHash("sha256").update(key2).digest("hex");
7361
7423
  return actual === expected;
7362
7424
  };
7363
7425
  }
@@ -7413,51 +7475,6 @@ var FtpClient = class {
7413
7475
  }
7414
7476
  };
7415
7477
 
7416
- // src/config/secrets.ts
7417
- function getCredentials(router) {
7418
- const { source, envPrefix, vaultPath } = router.credentials;
7419
- if (source === "vault") {
7420
- throw new MikroMCPError({
7421
- category: "CONFIGURATION" /* CONFIGURATION */,
7422
- code: "VAULT_NOT_SUPPORTED",
7423
- message: `Vault credential source is not supported in v0.1 (vaultPath=${vaultPath ?? "n/a"})`,
7424
- recoverability: {
7425
- retryable: false,
7426
- suggestedAction: 'Switch to source="env" and provide credentials via environment variables.'
7427
- }
7428
- });
7429
- }
7430
- if (!envPrefix) {
7431
- throw new MikroMCPError({
7432
- category: "CONFIGURATION" /* CONFIGURATION */,
7433
- code: "MISSING_ENV_PREFIX",
7434
- message: `Router "${router.id}" uses source="env" but no envPrefix is configured`,
7435
- recoverability: {
7436
- retryable: false,
7437
- suggestedAction: "Set envPrefix in the router configuration."
7438
- }
7439
- });
7440
- }
7441
- const userKey = `${envPrefix}_USER`;
7442
- const passKey = `${envPrefix}_PASS`;
7443
- const username = process.env[userKey];
7444
- const password = process.env[passKey];
7445
- if (!username || !password) {
7446
- const missing = [!username ? userKey : null, !password ? passKey : null].filter(Boolean);
7447
- throw new MikroMCPError({
7448
- category: "CONFIGURATION" /* CONFIGURATION */,
7449
- code: "MISSING_CREDENTIALS",
7450
- message: `Missing environment variable(s) for router "${router.id}": ${missing.join(", ")}`,
7451
- details: { missing },
7452
- recoverability: {
7453
- retryable: false,
7454
- suggestedAction: `Set the ${missing.join(" and ")} environment variable(s).`
7455
- }
7456
- });
7457
- }
7458
- return { username, password };
7459
- }
7460
-
7461
7478
  // src/adapter/adapter-factory.ts
7462
7479
  function createSshClient(routerConfig, options) {
7463
7480
  const creds = getCredentials(routerConfig);
@@ -7468,6 +7485,82 @@ function createFtpClient(routerConfig) {
7468
7485
  return new FtpClient(routerConfig.host, 21, creds.username, creds.password);
7469
7486
  }
7470
7487
 
7488
+ // src/mcp/tool-context.ts
7489
+ function buildRouterToolContext(args) {
7490
+ const { routerConfig, correlationId, identity, pool, config, registry } = args;
7491
+ const credentials = getCredentials(routerConfig);
7492
+ return {
7493
+ routerClient: pool.getClient(routerConfig, credentials),
7494
+ routerId: routerConfig.id,
7495
+ correlationId,
7496
+ routerConfig,
7497
+ sshClient: createSshClient(routerConfig, config.ssh),
7498
+ ftpClient: createFtpClient(routerConfig),
7499
+ identity,
7500
+ routerRegistry: registry,
7501
+ connectionPool: pool,
7502
+ appConfig: config
7503
+ };
7504
+ }
7505
+
7506
+ // src/middleware/fleet-confirmation.ts
7507
+ import { createHmac, createHash as createHash2 } from "crypto";
7508
+ var CONFIRMATION_TTL_MS = 5 * 60 * 1e3;
7509
+ var pending = /* @__PURE__ */ new Map();
7510
+ function fingerprint(args) {
7511
+ const sortedRouters = [...args.routerIds].sort();
7512
+ const payload = JSON.stringify({
7513
+ toolName: args.toolName,
7514
+ routerIds: sortedRouters,
7515
+ params: args.params,
7516
+ identityId: args.identityId
7517
+ });
7518
+ return createHash2("sha256").update(payload).digest("hex");
7519
+ }
7520
+ function evictExpired() {
7521
+ const now = /* @__PURE__ */ new Date();
7522
+ for (const [token, entry] of pending) {
7523
+ if (entry.expiresAt <= now) pending.delete(token);
7524
+ }
7525
+ }
7526
+ function checkFleetConfirmation(args, secret) {
7527
+ evictExpired();
7528
+ const fp = fingerprint(args);
7529
+ if (args.submittedToken) {
7530
+ const entry = pending.get(args.submittedToken);
7531
+ if (!entry || entry.expiresAt <= /* @__PURE__ */ new Date() || entry.fingerprint !== fp) {
7532
+ throw new MikroMCPError({
7533
+ category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
7534
+ code: "FLEET_CONFIRMATION_MISMATCH",
7535
+ message: "Fleet confirmation token is expired, already used, or does not match the current toolName/routerIds/params.",
7536
+ recoverability: {
7537
+ retryable: true,
7538
+ suggestedAction: "Re-submit bulk_execute without confirmationToken to get a fresh token."
7539
+ }
7540
+ });
7541
+ }
7542
+ pending.delete(args.submittedToken);
7543
+ return;
7544
+ }
7545
+ const expiresAt = new Date(Date.now() + CONFIRMATION_TTL_MS);
7546
+ const token = createHmac("sha256", secret).update(`${fp}:${expiresAt.toISOString()}`).digest("hex");
7547
+ pending.set(token, { expiresAt, fingerprint: fp });
7548
+ throw new MikroMCPError({
7549
+ category: "APPROVAL_REQUIRED" /* APPROVAL_REQUIRED */,
7550
+ code: "FLEET_CONFIRMATION_REQUIRED",
7551
+ message: `This will run destructive tool "${args.toolName}" across ${args.routerIds.length} router(s). Re-submit with confirmationToken to proceed.`,
7552
+ details: {
7553
+ confirmationToken: token,
7554
+ expiresAt: expiresAt.toISOString(),
7555
+ routerCount: args.routerIds.length
7556
+ },
7557
+ recoverability: {
7558
+ retryable: true,
7559
+ suggestedAction: "Re-submit the identical bulk_execute call with details.confirmationToken."
7560
+ }
7561
+ });
7562
+ }
7563
+
7471
7564
  // src/domain/tools/fleet-tools.ts
7472
7565
  var log33 = createLogger("fleet-tools");
7473
7566
  var checkHealthInputSchema = z31.object({
@@ -7478,7 +7571,10 @@ var bulkExecuteInputSchema = z31.object({
7478
7571
  routerIds: z31.array(z31.string()).optional().describe("Explicit list of router IDs to target"),
7479
7572
  tags: z31.array(z31.string()).optional().describe("Target all routers with ALL of these tags (mutually exclusive with routerIds)"),
7480
7573
  params: z31.record(z31.string(), z31.unknown()).describe("Params to pass to the tool (omit routerId \u2014 injected per router)"),
7481
- concurrency: z31.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls")
7574
+ concurrency: z31.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls"),
7575
+ confirmationToken: z31.string().optional().describe(
7576
+ "Fleet confirmation token from a prior APPROVAL_REQUIRED response. Required to fan out a destructive tool."
7577
+ )
7482
7578
  }).strict();
7483
7579
  function createFleetTools(baseTools2) {
7484
7580
  const toolMap = new Map(baseTools2.map((t) => [t.name, t]));
@@ -7533,7 +7629,7 @@ function createFleetTools(baseTools2) {
7533
7629
  const bulkExecuteTool = {
7534
7630
  name: "bulk_execute",
7535
7631
  title: "Bulk Execute",
7536
- description: "Fan out a single-router tool to multiple routers in parallel (up to `concurrency` at a time). Target routers via explicit routerIds or by tag. Destructive tools are not allowed. Returns per-router results with succeeded/failed counts.",
7632
+ description: "Fan out a single-router tool to multiple routers in parallel (up to `concurrency` at a time). Target routers via explicit routerIds or by tag. Non-destructive tools fan out immediately. Destructive tools require a two-step confirmation: call without `confirmationToken` to receive a fleet token (requires `MIKROMCP_CONFIRMATION_SECRET`), then re-call with that token to proceed. Returns per-router results with succeeded/failed counts.",
7537
7633
  inputSchema: bulkExecuteInputSchema,
7538
7634
  annotations: {
7539
7635
  readOnlyHint: false,
@@ -7561,6 +7657,17 @@ function createFleetTools(baseTools2) {
7561
7657
  { toolName: parsed.toolName, concurrency: parsed.concurrency },
7562
7658
  "bulk_execute invoked"
7563
7659
  );
7660
+ auditLog({
7661
+ type: "audit",
7662
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7663
+ correlationId: context.correlationId,
7664
+ identityId: context.identity.id,
7665
+ role: context.identity.role,
7666
+ tool: "bulk_execute",
7667
+ routerId: "(fleet)",
7668
+ phase: "attempt",
7669
+ params: { toolName: parsed.toolName, routerIds: parsed.routerIds, tags: parsed.tags }
7670
+ }, context.appConfig.auditLogPath);
7564
7671
  if (parsed.toolName === "bulk_execute" || parsed.toolName === "check_router_health") {
7565
7672
  throw new MikroMCPError({
7566
7673
  category: "VALIDATION" /* VALIDATION */,
@@ -7585,17 +7692,6 @@ function createFleetTools(baseTools2) {
7585
7692
  });
7586
7693
  }
7587
7694
  const targetTool = foundTool;
7588
- if (targetTool.annotations.destructiveHint) {
7589
- throw new MikroMCPError({
7590
- category: "VALIDATION" /* VALIDATION */,
7591
- code: "BULK_DESTRUCTIVE_NOT_ALLOWED",
7592
- message: `Tool "${parsed.toolName}" is destructive. Destructive tools require per-router confirmation and cannot be fanned out via bulk_execute.`,
7593
- recoverability: {
7594
- retryable: false,
7595
- suggestedAction: "Call the tool directly on each router to provide the required confirmation token."
7596
- }
7597
- });
7598
- }
7599
7695
  const preErrors = [];
7600
7696
  let routers;
7601
7697
  if (hasRouterIds) {
@@ -7605,14 +7701,55 @@ function createFleetTools(baseTools2) {
7605
7701
  resolved.push(context.routerRegistry.getRouter(id));
7606
7702
  } catch {
7607
7703
  log33.warn({ routerId: id }, "bulk_execute: router not found");
7608
- preErrors.push({ routerId: id, status: "error", error: `Router "${id}" not found in registry`, durationMs: 0 });
7704
+ preErrors.push({
7705
+ routerId: id,
7706
+ status: "error",
7707
+ error: `Router "${id}" not found in registry`,
7708
+ durationMs: 0
7709
+ });
7609
7710
  }
7610
7711
  }
7611
7712
  routers = resolved;
7612
7713
  } else {
7613
7714
  routers = context.routerRegistry.listRouters(parsed.tags);
7614
7715
  }
7716
+ if (targetTool.annotations.destructiveHint) {
7717
+ const secret = context.appConfig.confirmationSecret;
7718
+ if (!secret) {
7719
+ throw new MikroMCPError({
7720
+ category: "CONFIGURATION" /* CONFIGURATION */,
7721
+ code: "FLEET_CONFIRMATION_UNAVAILABLE",
7722
+ message: "Fanning out a destructive tool requires MIKROMCP_CONFIRMATION_SECRET to be configured.",
7723
+ recoverability: {
7724
+ retryable: false,
7725
+ suggestedAction: "Set MIKROMCP_CONFIRMATION_SECRET, or call the tool per-router."
7726
+ }
7727
+ });
7728
+ }
7729
+ const routerIdList = routers.map((r) => r.id);
7730
+ checkFleetConfirmation(
7731
+ {
7732
+ toolName: parsed.toolName,
7733
+ routerIds: routerIdList,
7734
+ params: parsed.params,
7735
+ identityId: context.identity.id,
7736
+ submittedToken: parsed.confirmationToken
7737
+ },
7738
+ secret
7739
+ );
7740
+ }
7615
7741
  if (routers.length === 0 && preErrors.length === 0) {
7742
+ auditLog({
7743
+ type: "audit",
7744
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7745
+ correlationId: context.correlationId,
7746
+ identityId: context.identity.id,
7747
+ role: context.identity.role,
7748
+ tool: "bulk_execute",
7749
+ routerId: "(fleet)",
7750
+ phase: "success",
7751
+ params: { toolName: parsed.toolName, succeeded: 0, failed: 0 }
7752
+ }, context.appConfig.auditLogPath);
7616
7753
  return {
7617
7754
  content: `Executed ${parsed.toolName} on 0 routers: 0 succeeded, 0 failed`,
7618
7755
  structuredContent: {
@@ -7628,28 +7765,50 @@ function createFleetTools(baseTools2) {
7628
7765
  const start = Date.now();
7629
7766
  try {
7630
7767
  checkAuthz(context.identity, parsed.toolName, router.id);
7631
- const credentials = getCredentials(router);
7632
- const client = context.connectionPool.getClient(router, credentials);
7633
- const sshClient = createSshClient(router, {});
7634
- const ftpClient = createFtpClient(router);
7635
- const routerContext = {
7636
- routerClient: client,
7637
- routerId: router.id,
7638
- correlationId: context.correlationId,
7768
+ const routerContext = buildRouterToolContext({
7639
7769
  routerConfig: router,
7640
- sshClient,
7641
- ftpClient,
7642
- identity: context.identity
7643
- };
7770
+ correlationId: context.correlationId,
7771
+ identity: context.identity,
7772
+ pool: context.connectionPool,
7773
+ config: context.appConfig,
7774
+ registry: context.routerRegistry
7775
+ });
7644
7776
  const toolParams = { ...parsed.params, routerId: router.id };
7645
7777
  const result = await targetTool.handler(
7646
7778
  toolParams,
7647
7779
  routerContext
7648
7780
  );
7649
- return { routerId: router.id, status: "ok", result, durationMs: Date.now() - start };
7781
+ const elapsed = Date.now() - start;
7782
+ auditLog({
7783
+ type: "audit",
7784
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7785
+ correlationId: context.correlationId,
7786
+ identityId: context.identity.id,
7787
+ role: context.identity.role,
7788
+ tool: parsed.toolName,
7789
+ routerId: router.id,
7790
+ phase: "success",
7791
+ params: parsed.params,
7792
+ durationMs: elapsed
7793
+ }, context.appConfig.auditLogPath);
7794
+ return { routerId: router.id, status: "ok", result, durationMs: elapsed };
7650
7795
  } catch (err) {
7796
+ const elapsed = Date.now() - start;
7651
7797
  const message = err instanceof Error ? err.message : String(err);
7652
- return { routerId: router.id, status: "error", error: message, durationMs: Date.now() - start };
7798
+ auditLog({
7799
+ type: "audit",
7800
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7801
+ correlationId: context.correlationId,
7802
+ identityId: context.identity.id,
7803
+ role: context.identity.role,
7804
+ tool: parsed.toolName,
7805
+ routerId: router.id,
7806
+ phase: "failure",
7807
+ params: parsed.params,
7808
+ outcome: message,
7809
+ durationMs: elapsed
7810
+ }, context.appConfig.auditLogPath);
7811
+ return { routerId: router.id, status: "error", error: message, durationMs: elapsed };
7653
7812
  }
7654
7813
  }
7655
7814
  const results2 = [...preErrors];
@@ -7660,6 +7819,17 @@ function createFleetTools(baseTools2) {
7660
7819
  }
7661
7820
  const succeeded = results2.filter((r) => r.status === "ok").length;
7662
7821
  const failed = results2.filter((r) => r.status === "error").length;
7822
+ auditLog({
7823
+ type: "audit",
7824
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7825
+ correlationId: context.correlationId,
7826
+ identityId: context.identity.id,
7827
+ role: context.identity.role,
7828
+ tool: "bulk_execute",
7829
+ routerId: "(fleet)",
7830
+ phase: "success",
7831
+ params: { toolName: parsed.toolName, succeeded, failed }
7832
+ }, context.appConfig.auditLogPath);
7663
7833
  return {
7664
7834
  content: `Executed ${parsed.toolName} on ${results2.length} routers: ${succeeded} succeeded, ${failed} failed`,
7665
7835
  structuredContent: {
@@ -7712,217 +7882,63 @@ var allTools = [
7712
7882
  ...createFleetTools(baseTools)
7713
7883
  ];
7714
7884
 
7715
- // src/adapter/retry-engine.ts
7716
- var DEFAULTS = {
7717
- maxRetries: 3,
7718
- baseDelayMs: 200,
7719
- maxDelayMs: 5e3
7720
- };
7721
- var RETRYABLE_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "ENOTFOUND"]);
7722
- function isRetryable(error) {
7723
- if (error instanceof Error) {
7724
- const code = error.code;
7725
- if (code && RETRYABLE_CODES.has(code)) {
7726
- return true;
7727
- }
7728
- const statusCode = error.statusCode;
7729
- if (typeof statusCode === "number") {
7730
- return statusCode >= 500;
7731
- }
7732
- }
7733
- return false;
7885
+ // src/mcp/tool-executor.ts
7886
+ import { nanoid as nanoid4 } from "nanoid";
7887
+
7888
+ // src/observability/correlation.ts
7889
+ import { AsyncLocalStorage } from "async_hooks";
7890
+ import { nanoid as nanoid3 } from "nanoid";
7891
+ var requestContext = new AsyncLocalStorage();
7892
+ function withContext(ctx, fn) {
7893
+ const fullContext = {
7894
+ correlationId: ctx.correlationId ?? nanoid3(),
7895
+ routerId: ctx.routerId,
7896
+ tool: ctx.tool,
7897
+ identity: ctx.identity
7898
+ };
7899
+ return requestContext.run(fullContext, fn);
7734
7900
  }
7735
- async function withRetry(fn, options) {
7736
- const opts = { ...DEFAULTS, ...options };
7737
- let lastError;
7738
- for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
7739
- try {
7740
- return await fn();
7741
- } catch (error) {
7742
- lastError = error;
7743
- if (attempt >= opts.maxRetries || !isRetryable(error)) {
7744
- throw error;
7901
+
7902
+ // src/middleware/auth.ts
7903
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
7904
+ var identityStorage = new AsyncLocalStorage2();
7905
+ function getCurrentIdentity() {
7906
+ return identityStorage.getStore();
7907
+ }
7908
+ function withIdentity(identity, fn) {
7909
+ return identityStorage.run(identity, fn);
7910
+ }
7911
+ function extractBearerToken(authHeader) {
7912
+ if (!authHeader) return null;
7913
+ const match = /^Bearer (.+)$/i.exec(authHeader);
7914
+ return match ? match[1] : null;
7915
+ }
7916
+ async function authenticateHttp(req, registry) {
7917
+ const token = extractBearerToken(req.headers.authorization);
7918
+ if (!token) {
7919
+ throw new MikroMCPError({
7920
+ category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
7921
+ code: "INVALID_TOKEN",
7922
+ message: "Missing or malformed Authorization header. Use: Authorization: Bearer <token>",
7923
+ recoverability: {
7924
+ retryable: false,
7925
+ suggestedAction: "Provide a valid bearer token in the Authorization header."
7745
7926
  }
7746
- const exponentialDelay = opts.baseDelayMs * 2 ** attempt;
7747
- const jitter = Math.random() * opts.baseDelayMs;
7748
- const delay = Math.min(exponentialDelay + jitter, opts.maxDelayMs);
7749
- await new Promise((resolve) => setTimeout(resolve, delay));
7750
- }
7927
+ });
7751
7928
  }
7752
- throw lastError;
7753
- }
7754
-
7755
- // src/adapter/circuit-breaker.ts
7756
- var TRANSIENT_CATEGORIES = /* @__PURE__ */ new Set([
7757
- "ROUTER_UNREACHABLE" /* ROUTER_UNREACHABLE */,
7758
- "ROUTER_TIMEOUT" /* ROUTER_TIMEOUT */,
7759
- "ROUTER_ERROR" /* ROUTER_ERROR */,
7760
- "ROUTER_BUSY" /* ROUTER_BUSY */
7761
- ]);
7762
- function isTransientFailure(error) {
7763
- if (!(error instanceof MikroMCPError)) {
7764
- return false;
7765
- }
7766
- return TRANSIENT_CATEGORIES.has(error.category);
7767
- }
7768
- var DEFAULTS2 = {
7769
- failureThreshold: 5,
7770
- cooldownMs: 3e4
7771
- };
7772
- var CircuitBreaker = class {
7773
- constructor(routerId, options) {
7774
- this.routerId = routerId;
7775
- this.options = { ...DEFAULTS2, ...options };
7776
- }
7777
- routerId;
7778
- options;
7779
- currentState = "closed";
7780
- consecutiveFailures = 0;
7781
- lastFailureTime = 0;
7782
- get state() {
7783
- if (this.currentState === "open") {
7784
- const elapsed = Date.now() - this.lastFailureTime;
7785
- if (elapsed >= this.options.cooldownMs) {
7786
- this.currentState = "half-open";
7787
- }
7788
- }
7789
- return this.currentState;
7790
- }
7791
- /**
7792
- * Execute `fn` through the circuit breaker.
7793
- *
7794
- * - **closed**: execute normally; track consecutive failures.
7795
- * - **open**: reject immediately with `ROUTER_BUSY`.
7796
- * - **half-open**: execute as a probe; success closes, failure reopens.
7797
- */
7798
- async execute(fn) {
7799
- const currentState = this.state;
7800
- if (currentState === "open") {
7801
- throw new MikroMCPError({
7802
- category: "ROUTER_BUSY" /* ROUTER_BUSY */,
7803
- code: "CIRCUIT_OPEN",
7804
- message: `Circuit breaker is open for router "${this.routerId}". Try again after cooldown.`,
7805
- details: {
7806
- routerId: this.routerId,
7807
- consecutiveFailures: this.consecutiveFailures,
7808
- cooldownMs: this.options.cooldownMs
7809
- },
7810
- recoverability: {
7811
- retryable: true,
7812
- retryAfterMs: this.options.cooldownMs,
7813
- suggestedAction: "Wait for the circuit breaker cooldown period to expire, then retry."
7814
- }
7815
- });
7816
- }
7817
- try {
7818
- const result = await fn();
7819
- this.onSuccess();
7820
- return result;
7821
- } catch (error) {
7822
- if (isTransientFailure(error)) {
7823
- this.onFailure();
7824
- }
7825
- throw error;
7826
- }
7827
- }
7828
- /** Force the circuit back to the closed state. */
7829
- reset() {
7830
- this.currentState = "closed";
7831
- this.consecutiveFailures = 0;
7832
- this.lastFailureTime = 0;
7833
- }
7834
- // ---------- internal helpers ----------
7835
- onSuccess() {
7836
- this.consecutiveFailures = 0;
7837
- this.currentState = "closed";
7838
- }
7839
- onFailure() {
7840
- this.consecutiveFailures++;
7841
- this.lastFailureTime = Date.now();
7842
- if (this.currentState === "half-open") {
7843
- this.currentState = "open";
7844
- } else if (this.consecutiveFailures >= this.options.failureThreshold) {
7845
- this.currentState = "open";
7846
- }
7847
- }
7848
- };
7849
-
7850
- // src/mcp/response-formatter.ts
7851
- function formatToolResult(result) {
7852
- return {
7853
- content: [{ type: "text", text: result.content }],
7854
- structuredContent: result.structuredContent,
7855
- ...result.isError ? { isError: true } : {}
7856
- };
7857
- }
7858
- function formatError(error) {
7859
- const hint = error.recoverability.suggestedAction;
7860
- const text = `Error [${error.category}]: ${error.message}${hint ? `
7861
- Suggested action: ${hint}` : ""}`;
7862
- return {
7863
- content: [{ type: "text", text }],
7864
- structuredContent: error.toJSON(),
7865
- isError: true
7866
- };
7867
- }
7868
-
7869
- // src/observability/correlation.ts
7870
- import { AsyncLocalStorage } from "async_hooks";
7871
- import { nanoid as nanoid3 } from "nanoid";
7872
- var requestContext = new AsyncLocalStorage();
7873
- function withContext(ctx, fn) {
7874
- const fullContext = {
7875
- correlationId: ctx.correlationId ?? nanoid3(),
7876
- routerId: ctx.routerId,
7877
- tool: ctx.tool,
7878
- identity: ctx.identity
7879
- };
7880
- return requestContext.run(fullContext, fn);
7881
- }
7882
-
7883
- // src/mcp/tool-registry.ts
7884
- import { nanoid as nanoid4 } from "nanoid";
7885
-
7886
- // src/middleware/auth.ts
7887
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
7888
- var identityStorage = new AsyncLocalStorage2();
7889
- function getCurrentIdentity() {
7890
- return identityStorage.getStore();
7891
- }
7892
- function withIdentity(identity, fn) {
7893
- return identityStorage.run(identity, fn);
7894
- }
7895
- function extractBearerToken(authHeader) {
7896
- if (!authHeader) return null;
7897
- const match = /^Bearer (.+)$/i.exec(authHeader);
7898
- return match ? match[1] : null;
7899
- }
7900
- async function authenticateHttp(req, registry) {
7901
- const token = extractBearerToken(req.headers.authorization);
7902
- if (!token) {
7903
- throw new MikroMCPError({
7904
- category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
7905
- code: "INVALID_TOKEN",
7906
- message: "Missing or malformed Authorization header. Use: Authorization: Bearer <token>",
7907
- recoverability: {
7908
- retryable: false,
7909
- suggestedAction: "Provide a valid bearer token in the Authorization header."
7910
- }
7911
- });
7912
- }
7913
- const identity = await registry.findIdentityByToken(token);
7914
- if (!identity) {
7915
- throw new MikroMCPError({
7916
- category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
7917
- code: "INVALID_TOKEN",
7918
- message: "Token does not match any configured identity.",
7919
- recoverability: {
7920
- retryable: false,
7921
- suggestedAction: "Verify the bearer token matches a configured identity in identities.yaml."
7922
- }
7923
- });
7924
- }
7925
- return identity;
7929
+ const identity = await registry.findIdentityByToken(token);
7930
+ if (!identity) {
7931
+ throw new MikroMCPError({
7932
+ category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
7933
+ code: "INVALID_TOKEN",
7934
+ message: "Token does not match any configured identity.",
7935
+ recoverability: {
7936
+ retryable: false,
7937
+ suggestedAction: "Verify the bearer token matches a configured identity in identities.yaml."
7938
+ }
7939
+ });
7940
+ }
7941
+ return identity;
7926
7942
  }
7927
7943
  var BUILTIN_SUPERADMIN = {
7928
7944
  id: "superadmin-builtin",
@@ -7951,19 +7967,19 @@ function getStdioIdentity(stdioIdentityName, registry) {
7951
7967
  }
7952
7968
 
7953
7969
  // src/middleware/confirmation.ts
7954
- import { createHmac, createHash as createHash2 } from "crypto";
7955
- var CONFIRMATION_TTL_MS = 5 * 60 * 1e3;
7970
+ import { createHmac as createHmac2, createHash as createHash3 } from "crypto";
7971
+ var CONFIRMATION_TTL_MS2 = 5 * 60 * 1e3;
7956
7972
  var BYPASS_ROLES = ["admin", "superadmin"];
7957
7973
  var pendingConfirmations = /* @__PURE__ */ new Map();
7958
7974
  function computeParamHash(params) {
7959
7975
  const { confirmationToken: _, ...rest } = params;
7960
- return createHash2("sha256").update(JSON.stringify(rest)).digest("hex");
7976
+ return createHash3("sha256").update(JSON.stringify(rest)).digest("hex");
7961
7977
  }
7962
7978
  function computeToken(tool, routerId, identityId, paramHash, expiresAt, secret) {
7963
7979
  const payload = JSON.stringify({ tool, routerId, paramHash, identityId, expiresAt });
7964
- return createHmac("sha256", secret).update(payload).digest("hex");
7980
+ return createHmac2("sha256", secret).update(payload).digest("hex");
7965
7981
  }
7966
- function evictExpired() {
7982
+ function evictExpired2() {
7967
7983
  const now = /* @__PURE__ */ new Date();
7968
7984
  for (const [token, entry] of pendingConfirmations) {
7969
7985
  if (entry.expiresAt <= now) pendingConfirmations.delete(token);
@@ -7971,7 +7987,7 @@ function evictExpired() {
7971
7987
  }
7972
7988
  async function checkConfirmation(toolName, routerId, params, identity, secret) {
7973
7989
  if (BYPASS_ROLES.includes(identity.role)) return;
7974
- evictExpired();
7990
+ evictExpired2();
7975
7991
  const submittedToken = typeof params.confirmationToken === "string" ? params.confirmationToken : null;
7976
7992
  if (submittedToken) {
7977
7993
  const entry = pendingConfirmations.get(submittedToken);
@@ -8001,7 +8017,7 @@ async function checkConfirmation(toolName, routerId, params, identity, secret) {
8001
8017
  pendingConfirmations.delete(submittedToken);
8002
8018
  return;
8003
8019
  }
8004
- const expiresAt = new Date(Date.now() + CONFIRMATION_TTL_MS).toISOString();
8020
+ const expiresAt = new Date(Date.now() + CONFIRMATION_TTL_MS2).toISOString();
8005
8021
  const paramHash = computeParamHash(params);
8006
8022
  const token = computeToken(toolName, routerId, identity.id, paramHash, expiresAt, secret);
8007
8023
  pendingConfirmations.set(token, {
@@ -8028,6 +8044,49 @@ async function checkConfirmation(toolName, routerId, params, identity, secret) {
8028
8044
  });
8029
8045
  }
8030
8046
 
8047
+ // src/adapter/retry-engine.ts
8048
+ var DEFAULTS = {
8049
+ maxRetries: 3,
8050
+ baseDelayMs: 200,
8051
+ maxDelayMs: 5e3
8052
+ };
8053
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "ENOTFOUND"]);
8054
+ function isRetryable(error) {
8055
+ if (error instanceof MikroMCPError) {
8056
+ return error.recoverability.retryable;
8057
+ }
8058
+ if (error instanceof Error) {
8059
+ const code = error.code;
8060
+ if (code && RETRYABLE_CODES.has(code)) {
8061
+ return true;
8062
+ }
8063
+ const statusCode = error.statusCode;
8064
+ if (typeof statusCode === "number") {
8065
+ return statusCode >= 500;
8066
+ }
8067
+ }
8068
+ return false;
8069
+ }
8070
+ async function withRetry(fn, options) {
8071
+ const opts = { ...DEFAULTS, ...options };
8072
+ let lastError;
8073
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
8074
+ try {
8075
+ return await fn();
8076
+ } catch (error) {
8077
+ lastError = error;
8078
+ if (attempt >= opts.maxRetries || !isRetryable(error)) {
8079
+ throw error;
8080
+ }
8081
+ const exponentialDelay = opts.baseDelayMs * 2 ** attempt;
8082
+ const jitter = Math.random() * opts.baseDelayMs;
8083
+ const delay = Math.min(exponentialDelay + jitter, opts.maxDelayMs);
8084
+ await new Promise((resolve) => setTimeout(resolve, delay));
8085
+ }
8086
+ }
8087
+ throw lastError;
8088
+ }
8089
+
8031
8090
  // src/config/maintenance-window.ts
8032
8091
  var DAY_ABBREVS = {
8033
8092
  Sun: "Sun",
@@ -8058,9 +8117,352 @@ function isWithinMaintenanceWindow(windows, now) {
8058
8117
  });
8059
8118
  }
8060
8119
 
8120
+ // src/mcp/response-formatter.ts
8121
+ function formatToolResult(result) {
8122
+ return {
8123
+ content: [{ type: "text", text: result.content }],
8124
+ structuredContent: result.structuredContent,
8125
+ ...result.isError ? { isError: true } : {}
8126
+ };
8127
+ }
8128
+ function formatError(error) {
8129
+ const hint = error.recoverability.suggestedAction;
8130
+ const text = `Error [${error.category}]: ${error.message}${hint ? `
8131
+ Suggested action: ${hint}` : ""}`;
8132
+ return {
8133
+ content: [{ type: "text", text }],
8134
+ structuredContent: error.toJSON(),
8135
+ isError: true
8136
+ };
8137
+ }
8138
+
8139
+ // src/observability/metrics.ts
8140
+ var toolCalls = /* @__PURE__ */ new Map();
8141
+ function key(tool, status) {
8142
+ return `${tool}\0${status}`;
8143
+ }
8144
+ function recordToolCall(tool, status) {
8145
+ const k = key(tool, status);
8146
+ toolCalls.set(k, (toolCalls.get(k) ?? 0) + 1);
8147
+ }
8148
+ function renderPrometheus() {
8149
+ const lines = [
8150
+ "# HELP mikromcp_tool_calls_total Total MCP tool calls by tool and status.",
8151
+ "# TYPE mikromcp_tool_calls_total counter"
8152
+ ];
8153
+ for (const [k, count] of toolCalls) {
8154
+ const [tool, status] = k.split("\0");
8155
+ lines.push(`mikromcp_tool_calls_total{tool="${tool}",status="${status}"} ${count}`);
8156
+ }
8157
+ return lines.join("\n") + "\n";
8158
+ }
8159
+
8160
+ // src/adapter/circuit-breaker.ts
8161
+ var TRANSIENT_CATEGORIES = /* @__PURE__ */ new Set([
8162
+ "ROUTER_UNREACHABLE" /* ROUTER_UNREACHABLE */,
8163
+ "ROUTER_TIMEOUT" /* ROUTER_TIMEOUT */,
8164
+ "ROUTER_ERROR" /* ROUTER_ERROR */,
8165
+ "ROUTER_BUSY" /* ROUTER_BUSY */
8166
+ ]);
8167
+ function isTransientFailure(error) {
8168
+ if (!(error instanceof MikroMCPError)) {
8169
+ return false;
8170
+ }
8171
+ return TRANSIENT_CATEGORIES.has(error.category);
8172
+ }
8173
+ var DEFAULTS2 = {
8174
+ failureThreshold: 5,
8175
+ cooldownMs: 3e4
8176
+ };
8177
+ var CircuitBreaker = class {
8178
+ constructor(routerId, options) {
8179
+ this.routerId = routerId;
8180
+ this.options = { ...DEFAULTS2, ...options };
8181
+ }
8182
+ routerId;
8183
+ options;
8184
+ currentState = "closed";
8185
+ consecutiveFailures = 0;
8186
+ lastFailureTime = 0;
8187
+ probeInFlight = false;
8188
+ get state() {
8189
+ if (this.currentState === "open") {
8190
+ const elapsed = Date.now() - this.lastFailureTime;
8191
+ if (elapsed >= this.options.cooldownMs) {
8192
+ this.currentState = "half-open";
8193
+ }
8194
+ }
8195
+ return this.currentState;
8196
+ }
8197
+ /**
8198
+ * Execute `fn` through the circuit breaker.
8199
+ *
8200
+ * - **closed**: execute normally; track consecutive failures.
8201
+ * - **open**: reject immediately with `ROUTER_BUSY`.
8202
+ * - **half-open**: execute as a probe; success closes, failure reopens.
8203
+ */
8204
+ async execute(fn) {
8205
+ const currentState = this.state;
8206
+ if (currentState === "open") {
8207
+ throw new MikroMCPError({
8208
+ category: "ROUTER_BUSY" /* ROUTER_BUSY */,
8209
+ code: "CIRCUIT_OPEN",
8210
+ message: `Circuit breaker is open for router "${this.routerId}". Try again after cooldown.`,
8211
+ details: {
8212
+ routerId: this.routerId,
8213
+ consecutiveFailures: this.consecutiveFailures,
8214
+ cooldownMs: this.options.cooldownMs
8215
+ },
8216
+ recoverability: {
8217
+ retryable: true,
8218
+ retryAfterMs: this.options.cooldownMs,
8219
+ suggestedAction: "Wait for the circuit breaker cooldown period to expire, then retry."
8220
+ }
8221
+ });
8222
+ }
8223
+ if (currentState === "half-open") {
8224
+ if (this.probeInFlight) {
8225
+ throw new MikroMCPError({
8226
+ category: "ROUTER_BUSY" /* ROUTER_BUSY */,
8227
+ code: "CIRCUIT_PROBE_IN_FLIGHT",
8228
+ message: `Circuit breaker for router "${this.routerId}" is half-open and a recovery probe is already in flight.`,
8229
+ details: { routerId: this.routerId },
8230
+ recoverability: {
8231
+ retryable: true,
8232
+ retryAfterMs: 1e3,
8233
+ suggestedAction: "Wait for the in-flight probe to complete, then retry."
8234
+ }
8235
+ });
8236
+ }
8237
+ this.probeInFlight = true;
8238
+ }
8239
+ try {
8240
+ const result = await fn();
8241
+ this.onSuccess();
8242
+ return result;
8243
+ } catch (error) {
8244
+ if (isTransientFailure(error)) {
8245
+ this.onFailure();
8246
+ }
8247
+ throw error;
8248
+ } finally {
8249
+ this.probeInFlight = false;
8250
+ }
8251
+ }
8252
+ /** Force the circuit back to the closed state. */
8253
+ reset() {
8254
+ this.currentState = "closed";
8255
+ this.consecutiveFailures = 0;
8256
+ this.lastFailureTime = 0;
8257
+ this.probeInFlight = false;
8258
+ }
8259
+ // ---------- internal helpers ----------
8260
+ onSuccess() {
8261
+ this.consecutiveFailures = 0;
8262
+ this.currentState = "closed";
8263
+ }
8264
+ onFailure() {
8265
+ this.consecutiveFailures++;
8266
+ this.lastFailureTime = Date.now();
8267
+ if (this.currentState === "half-open") {
8268
+ this.currentState = "open";
8269
+ } else if (this.consecutiveFailures >= this.options.failureThreshold) {
8270
+ this.currentState = "open";
8271
+ }
8272
+ }
8273
+ };
8274
+
8275
+ // src/mcp/tool-executor.ts
8276
+ var log34 = createLogger("tool-executor");
8277
+ async function executeToolCall(tool, args, deps) {
8278
+ const { registry, pool, circuitBreakers, config, identityRegistry } = deps;
8279
+ const correlationId = nanoid4();
8280
+ return withContext({ correlationId, tool: tool.name }, async () => {
8281
+ const startMs = Date.now();
8282
+ let journalId;
8283
+ try {
8284
+ const identity = getCurrentIdentity() ?? getStdioIdentity(config.stdioIdentity, identityRegistry);
8285
+ if (tool.skipRouterContext) {
8286
+ const fleetContext = {
8287
+ routerClient: null,
8288
+ routerId: "",
8289
+ correlationId,
8290
+ routerConfig: null,
8291
+ sshClient: null,
8292
+ ftpClient: null,
8293
+ identity,
8294
+ routerRegistry: registry,
8295
+ connectionPool: pool,
8296
+ appConfig: config
8297
+ };
8298
+ const fleetResult = await tool.handler(args, fleetContext);
8299
+ recordToolCall(tool.name, "success");
8300
+ return formatToolResult(fleetResult);
8301
+ }
8302
+ const routerId = args.routerId;
8303
+ if (!routerId) {
8304
+ throw new MikroMCPError({
8305
+ category: "VALIDATION" /* VALIDATION */,
8306
+ code: "MISSING_ROUTER_ID",
8307
+ message: "routerId is required",
8308
+ recoverability: {
8309
+ retryable: false,
8310
+ suggestedAction: "Provide a valid routerId parameter."
8311
+ }
8312
+ });
8313
+ }
8314
+ checkAuthz(identity, tool.name, routerId);
8315
+ const routerConfig = registry.getRouter(routerId);
8316
+ if (tool.annotations.destructiveHint) {
8317
+ const windows = routerConfig.maintenanceWindows;
8318
+ if (windows && windows.length > 0 && !isWithinMaintenanceWindow(windows, /* @__PURE__ */ new Date())) {
8319
+ throw new MikroMCPError({
8320
+ category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
8321
+ code: "OUTSIDE_MAINTENANCE_WINDOW",
8322
+ message: `Router "${routerId}" has maintenance windows configured. Destructive operations are only permitted during scheduled windows.`,
8323
+ details: { maintenanceWindows: windows },
8324
+ recoverability: {
8325
+ retryable: true,
8326
+ suggestedAction: "Wait for a scheduled maintenance window, or remove maintenanceWindows from routers.yaml to allow unrestricted access."
8327
+ }
8328
+ });
8329
+ }
8330
+ }
8331
+ if (tool.annotations.destructiveHint && config.confirmationSecret) {
8332
+ await checkConfirmation(tool.name, routerId, args, identity, config.confirmationSecret);
8333
+ }
8334
+ const shouldAudit = tool.annotations.destructiveHint || !tool.annotations.readOnlyHint;
8335
+ if (shouldAudit) {
8336
+ auditLog({
8337
+ type: "audit",
8338
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
8339
+ correlationId,
8340
+ identityId: identity.id,
8341
+ role: identity.role,
8342
+ tool: tool.name,
8343
+ routerId,
8344
+ phase: "attempt",
8345
+ params: args
8346
+ }, config.auditLogPath);
8347
+ }
8348
+ const toolContext = buildRouterToolContext({
8349
+ routerConfig,
8350
+ correlationId,
8351
+ identity,
8352
+ pool,
8353
+ config,
8354
+ registry
8355
+ });
8356
+ let cb = circuitBreakers.get(routerId);
8357
+ if (!cb) {
8358
+ cb = new CircuitBreaker(routerId, {
8359
+ failureThreshold: config.circuitBreaker.failureThreshold,
8360
+ cooldownMs: config.circuitBreaker.cooldownMs
8361
+ });
8362
+ circuitBreakers.set(routerId, cb);
8363
+ }
8364
+ toolContext.circuitBreaker = cb;
8365
+ const { confirmationToken: _ct, ...handlerArgs } = args;
8366
+ const snapshotIds = [];
8367
+ if (tool.snapshotPaths && tool.snapshotPaths.length > 0 && !tool.annotations.readOnlyHint) {
8368
+ for (const path of tool.snapshotPaths) {
8369
+ try {
8370
+ const meta = await takeSnapshot(toolContext.routerClient, routerId, path, config.snapshotDir);
8371
+ snapshotIds.push(meta.id);
8372
+ log34.debug({ snapshotId: meta.id, path }, "Snapshot taken");
8373
+ } catch (err) {
8374
+ log34.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
8375
+ }
8376
+ }
8377
+ }
8378
+ if (!tool.annotations.readOnlyHint && config.journalPath) {
8379
+ journalId = recordAttempt({
8380
+ journalPath: config.journalPath,
8381
+ identityId: identity.id,
8382
+ role: identity.role,
8383
+ tool: tool.name,
8384
+ routerId,
8385
+ params: handlerArgs,
8386
+ snapshotIds
8387
+ });
8388
+ }
8389
+ const runHandler = () => tool.handler(handlerArgs, toolContext);
8390
+ const executeHandler = () => cb.execute(
8391
+ tool.annotations.readOnlyHint ? () => withRetry(runHandler, config.retry) : runHandler
8392
+ );
8393
+ const result = await executeHandler();
8394
+ if (journalId) {
8395
+ recordOutcome({
8396
+ journalPath: config.journalPath,
8397
+ journalId,
8398
+ phase: "success",
8399
+ durationMs: Date.now() - startMs
8400
+ });
8401
+ }
8402
+ if (shouldAudit) {
8403
+ auditLog({
8404
+ type: "audit",
8405
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
8406
+ correlationId,
8407
+ identityId: identity.id,
8408
+ role: identity.role,
8409
+ tool: tool.name,
8410
+ routerId,
8411
+ phase: "success",
8412
+ params: args,
8413
+ durationMs: Date.now() - startMs
8414
+ }, config.auditLogPath);
8415
+ }
8416
+ log34.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
8417
+ recordToolCall(tool.name, "success");
8418
+ return formatToolResult(result);
8419
+ } catch (err) {
8420
+ const error = err instanceof MikroMCPError ? err : enrichError(err, { tool: tool.name });
8421
+ if (error.category === "ROUTER_AUTH_FAILED" /* ROUTER_AUTH_FAILED */) {
8422
+ const failedRouterId = args.routerId;
8423
+ if (failedRouterId) {
8424
+ pool.removeClient(failedRouterId);
8425
+ log34.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
8426
+ }
8427
+ }
8428
+ if (journalId) {
8429
+ recordOutcome({
8430
+ journalPath: config.journalPath,
8431
+ journalId,
8432
+ phase: "failure",
8433
+ outcome: error.code,
8434
+ durationMs: Date.now() - startMs
8435
+ });
8436
+ }
8437
+ const shouldAudit = tool.annotations.destructiveHint || !tool.annotations.readOnlyHint;
8438
+ if (shouldAudit && error.category !== "APPROVAL_REQUIRED" /* APPROVAL_REQUIRED */) {
8439
+ const routerId = args.routerId ?? "unknown";
8440
+ const identity = getCurrentIdentity() ?? getStdioIdentity(config.stdioIdentity, identityRegistry);
8441
+ auditLog({
8442
+ type: "audit",
8443
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
8444
+ correlationId,
8445
+ identityId: identity.id,
8446
+ role: identity.role,
8447
+ tool: tool.name,
8448
+ routerId,
8449
+ phase: "failure",
8450
+ params: args,
8451
+ outcome: error.code,
8452
+ durationMs: Date.now() - startMs
8453
+ }, config.auditLogPath);
8454
+ }
8455
+ log34.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
8456
+ recordToolCall(tool.name, "error");
8457
+ return formatError(error);
8458
+ }
8459
+ });
8460
+ }
8461
+
8061
8462
  // src/mcp/tool-registry.ts
8062
- var log34 = createLogger("tool-registry");
8463
+ var log35 = createLogger("tool-registry");
8063
8464
  function registerAllTools(server, registry, pool, circuitBreakers, config, identityRegistry) {
8465
+ const deps = { registry, pool, circuitBreakers, config, identityRegistry };
8064
8466
  for (const tool of allTools) {
8065
8467
  const registrationSchema = tool.annotations.destructiveHint ? tool.inputSchema.extend({
8066
8468
  confirmationToken: z32.string().optional().describe(
@@ -8075,193 +8477,18 @@ function registerAllTools(server, registry, pool, circuitBreakers, config, ident
8075
8477
  inputSchema: registrationSchema,
8076
8478
  annotations: tool.annotations
8077
8479
  },
8078
- async (args) => {
8079
- const correlationId = nanoid4();
8080
- return withContext({ correlationId, tool: tool.name }, async () => {
8081
- const startMs = Date.now();
8082
- let journalId;
8083
- try {
8084
- const identity = getCurrentIdentity() ?? getStdioIdentity(config.stdioIdentity, identityRegistry);
8085
- if (tool.skipRouterContext) {
8086
- const fleetContext = {
8087
- routerClient: null,
8088
- routerId: "",
8089
- correlationId,
8090
- routerConfig: null,
8091
- sshClient: null,
8092
- ftpClient: null,
8093
- identity,
8094
- routerRegistry: registry,
8095
- connectionPool: pool
8096
- };
8097
- return formatToolResult(await tool.handler(args, fleetContext));
8098
- }
8099
- const routerId = args.routerId;
8100
- if (!routerId) {
8101
- throw new MikroMCPError({
8102
- category: "VALIDATION" /* VALIDATION */,
8103
- code: "MISSING_ROUTER_ID",
8104
- message: "routerId is required",
8105
- recoverability: {
8106
- retryable: false,
8107
- suggestedAction: "Provide a valid routerId parameter."
8108
- }
8109
- });
8110
- }
8111
- checkAuthz(identity, tool.name, routerId);
8112
- const routerConfig = registry.getRouter(routerId);
8113
- if (tool.annotations.destructiveHint) {
8114
- const windows = routerConfig.maintenanceWindows;
8115
- if (windows && windows.length > 0 && !isWithinMaintenanceWindow(windows, /* @__PURE__ */ new Date())) {
8116
- throw new MikroMCPError({
8117
- category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
8118
- code: "OUTSIDE_MAINTENANCE_WINDOW",
8119
- message: `Router "${routerId}" has maintenance windows configured. Destructive operations are only permitted during scheduled windows.`,
8120
- details: { maintenanceWindows: windows },
8121
- recoverability: {
8122
- retryable: true,
8123
- suggestedAction: "Wait for a scheduled maintenance window, or remove maintenanceWindows from routers.yaml to allow unrestricted access."
8124
- }
8125
- });
8126
- }
8127
- }
8128
- if (tool.annotations.destructiveHint && config.confirmationSecret) {
8129
- await checkConfirmation(tool.name, routerId, args, identity, config.confirmationSecret);
8130
- }
8131
- const shouldAudit = tool.annotations.destructiveHint || !tool.annotations.readOnlyHint;
8132
- if (shouldAudit) {
8133
- auditLog({
8134
- type: "audit",
8135
- ts: (/* @__PURE__ */ new Date()).toISOString(),
8136
- correlationId,
8137
- identityId: identity.id,
8138
- role: identity.role,
8139
- tool: tool.name,
8140
- routerId,
8141
- phase: "attempt",
8142
- params: args
8143
- });
8144
- }
8145
- const credentials = getCredentials(routerConfig);
8146
- const client = pool.getClient(routerConfig, credentials);
8147
- const sshClient = createSshClient(routerConfig, config.ssh);
8148
- const ftpClient = createFtpClient(routerConfig);
8149
- let cb = circuitBreakers.get(routerId);
8150
- if (!cb) {
8151
- cb = new CircuitBreaker(routerId, {
8152
- failureThreshold: config.circuitBreaker.failureThreshold,
8153
- cooldownMs: config.circuitBreaker.cooldownMs
8154
- });
8155
- circuitBreakers.set(routerId, cb);
8156
- }
8157
- const { confirmationToken: _ct, ...handlerArgs } = args;
8158
- const snapshotIds = [];
8159
- if (tool.snapshotPaths && tool.snapshotPaths.length > 0 && !tool.annotations.readOnlyHint) {
8160
- for (const path of tool.snapshotPaths) {
8161
- try {
8162
- const meta = await takeSnapshot(client, routerId, path, config.snapshotDir);
8163
- snapshotIds.push(meta.id);
8164
- log34.debug({ snapshotId: meta.id, path }, "Snapshot taken");
8165
- } catch (err) {
8166
- log34.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
8167
- }
8168
- }
8169
- }
8170
- if (!tool.annotations.readOnlyHint && config.journalPath) {
8171
- journalId = recordAttempt({
8172
- journalPath: config.journalPath,
8173
- identityId: identity.id,
8174
- role: identity.role,
8175
- tool: tool.name,
8176
- routerId,
8177
- params: handlerArgs,
8178
- snapshotIds
8179
- });
8180
- }
8181
- const runHandler = () => tool.handler(handlerArgs, {
8182
- routerClient: client,
8183
- routerId,
8184
- correlationId,
8185
- routerConfig,
8186
- sshClient,
8187
- ftpClient,
8188
- identity,
8189
- routerRegistry: registry,
8190
- connectionPool: pool
8191
- });
8192
- const executeHandler = () => cb.execute(
8193
- tool.annotations.readOnlyHint ? () => withRetry(runHandler, config.retry) : runHandler
8194
- );
8195
- const result = await executeHandler();
8196
- if (journalId) {
8197
- recordOutcome({
8198
- journalPath: config.journalPath,
8199
- journalId,
8200
- phase: "success",
8201
- durationMs: Date.now() - startMs
8202
- });
8203
- }
8204
- if (shouldAudit) {
8205
- auditLog({
8206
- type: "audit",
8207
- ts: (/* @__PURE__ */ new Date()).toISOString(),
8208
- correlationId,
8209
- identityId: identity.id,
8210
- role: identity.role,
8211
- tool: tool.name,
8212
- routerId,
8213
- phase: "success",
8214
- params: args,
8215
- durationMs: Date.now() - startMs
8216
- });
8217
- }
8218
- log34.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
8219
- return formatToolResult(result);
8220
- } catch (err) {
8221
- const error = err instanceof MikroMCPError ? err : enrichError(err, { tool: tool.name });
8222
- if (journalId) {
8223
- recordOutcome({
8224
- journalPath: config.journalPath,
8225
- journalId,
8226
- phase: "failure",
8227
- outcome: error.code,
8228
- durationMs: Date.now() - startMs
8229
- });
8230
- }
8231
- const shouldAudit = tool.annotations.destructiveHint || !tool.annotations.readOnlyHint;
8232
- if (shouldAudit && error.category !== "APPROVAL_REQUIRED" /* APPROVAL_REQUIRED */) {
8233
- const routerId = args.routerId ?? "unknown";
8234
- const identity = getCurrentIdentity() ?? getStdioIdentity(config.stdioIdentity, identityRegistry);
8235
- auditLog({
8236
- type: "audit",
8237
- ts: (/* @__PURE__ */ new Date()).toISOString(),
8238
- correlationId,
8239
- identityId: identity.id,
8240
- role: identity.role,
8241
- tool: tool.name,
8242
- routerId,
8243
- phase: "failure",
8244
- params: args,
8245
- outcome: error.code,
8246
- durationMs: Date.now() - startMs
8247
- });
8248
- }
8249
- log34.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
8250
- return formatError(error);
8251
- }
8252
- });
8253
- }
8480
+ (args) => executeToolCall(tool, args, deps)
8254
8481
  );
8255
- log34.debug({ tool: tool.name }, "Registered tool");
8482
+ log35.debug({ tool: tool.name }, "Registered tool");
8256
8483
  }
8257
- log34.info({ count: allTools.length }, "All tools registered");
8484
+ log35.info({ count: allTools.length }, "All tools registered");
8258
8485
  }
8259
8486
 
8260
8487
  // src/config/router-registry.ts
8261
- import { readFileSync as readFileSync3, existsSync } from "fs";
8488
+ import { readFileSync as readFileSync2, existsSync } from "fs";
8262
8489
  import { parse } from "yaml";
8263
8490
  import { z as z33 } from "zod";
8264
- var log35 = createLogger("router-registry");
8491
+ var log36 = createLogger("router-registry");
8265
8492
  var RouterConfigSchema = z33.object({
8266
8493
  host: z33.string().min(1, "host is required"),
8267
8494
  port: z33.number().int().min(1).max(65535),
@@ -8301,10 +8528,10 @@ var RouterRegistry = class {
8301
8528
  constructor(configPath) {
8302
8529
  this.routers = /* @__PURE__ */ new Map();
8303
8530
  if (!existsSync(configPath)) {
8304
- log35.warn({ configPath }, "Router config file not found; starting with empty registry");
8531
+ log36.warn({ configPath }, "Router config file not found; starting with empty registry");
8305
8532
  return;
8306
8533
  }
8307
- const raw = readFileSync3(configPath, "utf-8");
8534
+ const raw = readFileSync2(configPath, "utf-8");
8308
8535
  const parsed = parse(raw);
8309
8536
  const result = ConfigFileSchema.safeParse(parsed);
8310
8537
  if (!result.success) {
@@ -8315,14 +8542,14 @@ ${issues}`);
8315
8542
  for (const [id, config] of Object.entries(result.data.routers)) {
8316
8543
  const routerConfig = { ...config, id };
8317
8544
  if (routerConfig.tls.enabled && !routerConfig.tls.rejectUnauthorized) {
8318
- log35.warn(
8545
+ log36.warn(
8319
8546
  { routerId: id },
8320
8547
  "TLS certificate validation is DISABLED (rejectUnauthorized=false). Set tls.fingerprint to pin the server certificate instead."
8321
8548
  );
8322
8549
  }
8323
8550
  this.routers.set(id, routerConfig);
8324
8551
  }
8325
- log35.info({ count: this.routers.size }, "Loaded routers from config");
8552
+ log36.info({ count: this.routers.size }, "Loaded routers from config");
8326
8553
  }
8327
8554
  getRouter(id) {
8328
8555
  const router = this.routers.get(id);
@@ -8345,7 +8572,7 @@ ${issues}`);
8345
8572
  import { request, Agent } from "undici";
8346
8573
 
8347
8574
  // src/adapter/tls-manager.ts
8348
- import { readFileSync as readFileSync4 } from "fs";
8575
+ import { readFileSync as readFileSync3 } from "fs";
8349
8576
  function buildAgentOptions(tls) {
8350
8577
  if (!tls.enabled) {
8351
8578
  return {};
@@ -8354,7 +8581,7 @@ function buildAgentOptions(tls) {
8354
8581
  rejectUnauthorized: tls.rejectUnauthorized
8355
8582
  };
8356
8583
  if (tls.ca) {
8357
- connectOptions.ca = readFileSync4(tls.ca, "utf-8");
8584
+ connectOptions.ca = readFileSync3(tls.ca, "utf-8");
8358
8585
  }
8359
8586
  if (tls.fingerprint) {
8360
8587
  const expected = tls.fingerprint.replace(/:/g, "").toLowerCase();
@@ -8376,8 +8603,8 @@ function buildAgentOptions(tls) {
8376
8603
  // src/adapter/response-parser.ts
8377
8604
  var NUMERIC_RE = /^-?\d+(\.\d+)?$/;
8378
8605
  var DURATION_RE = /^\d+[wdhms](\d+[wdhms])*$/;
8379
- function parseRouterOSValue(key, value) {
8380
- if (key === ".id") {
8606
+ function parseRouterOSValue(key2, value) {
8607
+ if (key2 === ".id") {
8381
8608
  return value;
8382
8609
  }
8383
8610
  if (value === "true") return true;
@@ -8395,8 +8622,8 @@ function parseRouterOSValue(key, value) {
8395
8622
  }
8396
8623
  function parseRecord(raw) {
8397
8624
  const parsed = {};
8398
- for (const [key, value] of Object.entries(raw)) {
8399
- parsed[key] = parseRouterOSValue(key, value);
8625
+ for (const [key2, value] of Object.entries(raw)) {
8626
+ parsed[key2] = parseRouterOSValue(key2, value);
8400
8627
  }
8401
8628
  return parsed;
8402
8629
  }
@@ -8418,8 +8645,8 @@ function buildListQuery(options) {
8418
8645
  }
8419
8646
  if (hasFilter) {
8420
8647
  const queryParts = [];
8421
- for (const [key, value] of Object.entries(options.filter)) {
8422
- queryParts.push(`${key}=${value}`);
8648
+ for (const [key2, value] of Object.entries(options.filter)) {
8649
+ queryParts.push(`${key2}=${value}`);
8423
8650
  }
8424
8651
  body[".query"] = queryParts;
8425
8652
  }
@@ -8427,8 +8654,8 @@ function buildListQuery(options) {
8427
8654
  }
8428
8655
  if (hasFilter) {
8429
8656
  const queryParams = {};
8430
- for (const [key, value] of Object.entries(options.filter)) {
8431
- queryParams[key] = value;
8657
+ for (const [key2, value] of Object.entries(options.filter)) {
8658
+ queryParams[key2] = value;
8432
8659
  }
8433
8660
  return { method: "GET", queryParams };
8434
8661
  }
@@ -8606,6 +8833,9 @@ var ConnectionPool = class {
8606
8833
  }
8607
8834
  };
8608
8835
 
8836
+ // src/version.ts
8837
+ var VERSION = "1.1.0";
8838
+
8609
8839
  // src/mcp/server.ts
8610
8840
  function createServerFactory(config, identityRegistry) {
8611
8841
  const registry = new RouterRegistry(config.configPath);
@@ -8614,7 +8844,7 @@ function createServerFactory(config, identityRegistry) {
8614
8844
  const makeServer = () => {
8615
8845
  const server = new McpServer({
8616
8846
  name: "mikromcp",
8617
- version: "1.0.9"
8847
+ version: VERSION
8618
8848
  });
8619
8849
  registerAllTools(server, registry, pool, circuitBreakers, config, identityRegistry);
8620
8850
  return server;
@@ -8634,17 +8864,17 @@ import { randomUUID } from "crypto";
8634
8864
  import { createServer } from "http";
8635
8865
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8636
8866
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
8637
- var log36 = createLogger("transport-http");
8867
+ var log37 = createLogger("transport-http");
8638
8868
  var BodyTooLargeError = class extends Error {
8639
8869
  constructor() {
8640
8870
  super("Request body exceeds maximum allowed size");
8641
8871
  }
8642
8872
  };
8643
8873
  function createRateLimiter(rpm) {
8644
- if (rpm === 0) return () => true;
8645
8874
  const WINDOW_MS = 6e4;
8646
8875
  const windows = /* @__PURE__ */ new Map();
8647
- return (ip) => {
8876
+ const check = (ip) => {
8877
+ if (rpm === 0) return true;
8648
8878
  const now = Date.now();
8649
8879
  const entry = windows.get(ip);
8650
8880
  if (!entry || now - entry.windowStart >= WINDOW_MS) {
@@ -8657,6 +8887,17 @@ function createRateLimiter(rpm) {
8657
8887
  entry.count++;
8658
8888
  return true;
8659
8889
  };
8890
+ const limiter = check;
8891
+ limiter.sweep = () => {
8892
+ const now = Date.now();
8893
+ for (const [ip, entry] of windows) {
8894
+ if (now - entry.windowStart >= WINDOW_MS) {
8895
+ windows.delete(ip);
8896
+ }
8897
+ }
8898
+ };
8899
+ limiter.size = () => windows.size;
8900
+ return limiter;
8660
8901
  }
8661
8902
  async function readBody(req, maxBytes) {
8662
8903
  return new Promise((resolve, reject) => {
@@ -8696,18 +8937,30 @@ async function readBody(req, maxBytes) {
8696
8937
  async function connectHttp(makeServer, config, identityRegistry) {
8697
8938
  const { port, bindHost, maxBodyBytes, rateLimitRpm } = config;
8698
8939
  const checkRateLimit = createRateLimiter(rateLimitRpm);
8940
+ const sweepTimer = setInterval(() => checkRateLimit.sweep(), 6e4);
8941
+ sweepTimer.unref();
8699
8942
  const streamableSessions = /* @__PURE__ */ new Map();
8700
8943
  const sseTransports = /* @__PURE__ */ new Map();
8701
8944
  const httpServer = createServer(async (req, res) => {
8702
8945
  const ip = req.socket.remoteAddress ?? "unknown";
8946
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
8947
+ const { pathname } = url;
8948
+ if (pathname === "/healthz") {
8949
+ res.writeHead(200, { "Content-Type": "application/json" });
8950
+ res.end(JSON.stringify({ status: "ok" }));
8951
+ return;
8952
+ }
8953
+ if (pathname === "/metrics") {
8954
+ res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4; charset=utf-8" });
8955
+ res.end(renderPrometheus());
8956
+ return;
8957
+ }
8703
8958
  if (!checkRateLimit(ip)) {
8704
- log36.warn({ ip }, "Rate limit exceeded");
8959
+ log37.warn({ ip }, "Rate limit exceeded");
8705
8960
  res.writeHead(429, { "Content-Type": "application/json" });
8706
8961
  res.end(JSON.stringify({ error: "Too many requests. Retry after 60 seconds." }));
8707
8962
  return;
8708
8963
  }
8709
- const url = new URL(req.url ?? "/", `http://localhost:${port}`);
8710
- const { pathname } = url;
8711
8964
  try {
8712
8965
  if (pathname === "/mcp" || pathname === "/sse" || pathname === "/messages") {
8713
8966
  let identity;
@@ -8744,16 +8997,17 @@ async function connectHttp(makeServer, config, identityRegistry) {
8744
8997
  }
8745
8998
  return;
8746
8999
  }
8747
- log36.error({ err }, "HTTP transport error");
9000
+ log37.error({ err }, "HTTP transport error");
8748
9001
  if (!res.headersSent) {
8749
9002
  res.writeHead(500, { "Content-Type": "application/json" });
8750
9003
  res.end(JSON.stringify({ error: "Internal server error" }));
8751
9004
  }
8752
9005
  }
8753
9006
  });
9007
+ httpServer.on("close", () => clearInterval(sweepTimer));
8754
9008
  await new Promise((resolve, reject) => {
8755
9009
  httpServer.listen(port, bindHost, () => {
8756
- log36.info(
9010
+ log37.info(
8757
9011
  { port, bindHost },
8758
9012
  "HTTP transport listening (POST /mcp for Streamable HTTP, GET /sse for legacy SSE)"
8759
9013
  );
@@ -8761,6 +9015,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
8761
9015
  });
8762
9016
  httpServer.on("error", reject);
8763
9017
  });
9018
+ return httpServer;
8764
9019
  }
8765
9020
  async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, makeServer, streamableSessions, sseTransports) {
8766
9021
  if (pathname === "/mcp") {
@@ -8776,13 +9031,13 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
8776
9031
  sessionIdGenerator: () => randomUUID(),
8777
9032
  onsessioninitialized: (id) => {
8778
9033
  streamableSessions.set(id, transport2);
8779
- log36.info({ sessionId: id }, "Streamable HTTP session created");
9034
+ log37.info({ sessionId: id }, "Streamable HTTP session created");
8780
9035
  }
8781
9036
  });
8782
9037
  transport2.onclose = () => {
8783
9038
  if (transport2.sessionId) {
8784
9039
  streamableSessions.delete(transport2.sessionId);
8785
- log36.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
9040
+ log37.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
8786
9041
  }
8787
9042
  };
8788
9043
  await makeServer().connect(transport2);
@@ -8799,7 +9054,7 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
8799
9054
  sseTransport.onclose = () => {
8800
9055
  sseTransports.delete(sseTransport.sessionId);
8801
9056
  };
8802
- log36.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
9057
+ log37.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
8803
9058
  await sseTransport.start();
8804
9059
  return;
8805
9060
  }
@@ -8818,11 +9073,11 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
8818
9073
  }
8819
9074
 
8820
9075
  // src/config/identity-registry.ts
8821
- import { readFileSync as readFileSync5, existsSync as existsSync2 } from "fs";
9076
+ import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
8822
9077
  import { parse as parse2 } from "yaml";
8823
9078
  import { z as z34 } from "zod";
8824
9079
  import bcrypt from "bcryptjs";
8825
- var log37 = createLogger("identity-registry");
9080
+ var log38 = createLogger("identity-registry");
8826
9081
  var IdentityConfigSchema = z34.object({
8827
9082
  token: z34.string().min(1, "token hash is required"),
8828
9083
  role: z34.enum(["readonly", "operator", "admin", "superadmin"]),
@@ -8836,13 +9091,13 @@ var IdentityRegistry = class {
8836
9091
  identities = [];
8837
9092
  constructor(configPath) {
8838
9093
  if (!existsSync2(configPath)) {
8839
- log37.warn(
9094
+ log38.warn(
8840
9095
  { configPath },
8841
9096
  "Identity config file not found; starting with empty registry (stdio callers do not require it)"
8842
9097
  );
8843
9098
  return;
8844
9099
  }
8845
- const raw = readFileSync5(configPath, "utf-8");
9100
+ const raw = readFileSync4(configPath, "utf-8");
8846
9101
  const parsed = parse2(raw);
8847
9102
  const result = ConfigFileSchema2.safeParse(parsed);
8848
9103
  if (!result.success) {
@@ -8859,7 +9114,7 @@ ${issues}`);
8859
9114
  allowedToolPatterns: config.allowedToolPatterns
8860
9115
  });
8861
9116
  }
8862
- log37.info({ count: this.identities.length }, "Loaded identities from config");
9117
+ log38.info({ count: this.identities.length }, "Loaded identities from config");
8863
9118
  }
8864
9119
  getIdentities() {
8865
9120
  return this.identities.map(({ id, role, allowedRouters, allowedToolPatterns }) => ({
@@ -8885,18 +9140,59 @@ ${issues}`);
8885
9140
  }
8886
9141
  };
8887
9142
 
9143
+ // src/domain/snapshot/retention.ts
9144
+ import { readdir, stat, unlink } from "fs/promises";
9145
+ import { join as join3 } from "path";
9146
+ var log39 = createLogger("snapshot-retention");
9147
+ async function pruneSnapshots(snapshotDir, maxAgeDays) {
9148
+ const cutoffMs = Date.now() - maxAgeDays * 864e5;
9149
+ let removed = 0;
9150
+ let routerDirs;
9151
+ try {
9152
+ routerDirs = await readdir(snapshotDir);
9153
+ } catch {
9154
+ return 0;
9155
+ }
9156
+ for (const routerDir of routerDirs) {
9157
+ const dirPath = join3(snapshotDir, routerDir);
9158
+ let files;
9159
+ try {
9160
+ files = await readdir(dirPath);
9161
+ } catch {
9162
+ continue;
9163
+ }
9164
+ for (const file of files) {
9165
+ const filePath = join3(dirPath, file);
9166
+ try {
9167
+ const info = await stat(filePath);
9168
+ if (info.isFile() && info.mtimeMs < cutoffMs) {
9169
+ await unlink(filePath);
9170
+ removed++;
9171
+ }
9172
+ } catch (err) {
9173
+ log39.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
9174
+ }
9175
+ }
9176
+ }
9177
+ if (removed > 0) {
9178
+ log39.info({ removed, maxAgeDays }, "Pruned expired snapshots");
9179
+ }
9180
+ return removed;
9181
+ }
9182
+
8888
9183
  // src/cli/serve.ts
8889
- var log38 = createLogger("main");
9184
+ var log40 = createLogger("main");
8890
9185
  async function runServe() {
8891
9186
  const config = loadAppConfig();
8892
- log38.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
9187
+ log40.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
9188
+ void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
8893
9189
  const identityRegistry = new IdentityRegistry(config.identitiesPath);
8894
9190
  if (config.transport === "http") {
8895
9191
  const hasLimitedRoles = identityRegistry.getIdentities().some(
8896
9192
  (i) => i.role === "readonly" || i.role === "operator"
8897
9193
  );
8898
9194
  if (hasLimitedRoles && !config.confirmationSecret) {
8899
- log38.error(
9195
+ log40.error(
8900
9196
  "MIKROMCP_CONFIRMATION_SECRET is required in HTTP mode when identities with role readonly or operator are configured"
8901
9197
  );
8902
9198
  process.exit(1);
@@ -8906,7 +9202,7 @@ async function runServe() {
8906
9202
  if (config.transport === "stdio") {
8907
9203
  const stdioIdentity = getStdioIdentity(config.stdioIdentity, identityRegistry);
8908
9204
  await withIdentity(stdioIdentity, () => connectStdio(makeServer()));
8909
- log38.info("MikroMCP server running via stdio");
9205
+ log40.info("MikroMCP server running via stdio");
8910
9206
  } else if (config.transport === "http") {
8911
9207
  await connectHttp(
8912
9208
  makeServer,
@@ -8918,13 +9214,13 @@ async function runServe() {
8918
9214
  },
8919
9215
  identityRegistry
8920
9216
  );
8921
- log38.info(
9217
+ log40.info(
8922
9218
  { port: config.port, bindHost: config.bindHost },
8923
9219
  "MikroMCP server running via HTTP/SSE"
8924
9220
  );
8925
9221
  }
8926
9222
  const shutdown = () => {
8927
- log38.info("Shutting down MikroMCP server");
9223
+ log40.info("Shutting down MikroMCP server");
8928
9224
  pool.closeAll();
8929
9225
  process.exit(0);
8930
9226
  };
@@ -8933,11 +9229,11 @@ async function runServe() {
8933
9229
  }
8934
9230
 
8935
9231
  // src/cli/doctor.ts
8936
- import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
8937
- import { homedir as homedir3 } from "os";
9232
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
9233
+ import { homedir as homedir2 } from "os";
8938
9234
  import * as net from "net";
8939
9235
  import chalk from "chalk";
8940
- var localVersion = "1.0.9";
9236
+ var localVersion = "1.1.0";
8941
9237
  var results = [];
8942
9238
  function ok(msg) {
8943
9239
  console.log(chalk.green("\u2705 ") + msg);
@@ -9117,8 +9413,8 @@ function checkIdentities(identitiesPath, transport2) {
9117
9413
  }
9118
9414
  function checkClaudeDesktop() {
9119
9415
  const candidatePaths = [
9120
- `${homedir3()}/Library/Application Support/Claude/claude_desktop_config.json`,
9121
- `${homedir3()}/.config/Claude/claude_desktop_config.json`
9416
+ `${homedir2()}/Library/Application Support/Claude/claude_desktop_config.json`,
9417
+ `${homedir2()}/.config/Claude/claude_desktop_config.json`
9122
9418
  ];
9123
9419
  const configPath = candidatePaths.find((p) => existsSync3(p));
9124
9420
  if (!configPath) {
@@ -9126,7 +9422,7 @@ function checkClaudeDesktop() {
9126
9422
  return;
9127
9423
  }
9128
9424
  try {
9129
- const raw = readFileSync6(configPath, "utf-8");
9425
+ const raw = readFileSync5(configPath, "utf-8");
9130
9426
  const config = JSON.parse(raw);
9131
9427
  const servers = config.mcpServers ?? {};
9132
9428
  if ("mikromcp" in servers || "mikrotik-mcp-server" in servers) {
@@ -9211,18 +9507,18 @@ import { input, confirm, select } from "@inquirer/prompts";
9211
9507
  import { randomBytes } from "crypto";
9212
9508
  import {
9213
9509
  existsSync as existsSync4,
9214
- readFileSync as readFileSync7,
9215
- writeFileSync as writeFileSync2,
9216
- mkdirSync as mkdirSync3,
9510
+ readFileSync as readFileSync6,
9511
+ writeFileSync,
9512
+ mkdirSync,
9217
9513
  copyFileSync
9218
9514
  } from "fs";
9219
- import { homedir as homedir4 } from "os";
9515
+ import { homedir as homedir3 } from "os";
9220
9516
  import { join as join4 } from "path";
9221
9517
  import { parse as yamlParse, Document, visit } from "yaml";
9222
9518
  import bcrypt2 from "bcryptjs";
9223
9519
  import chalk2 from "chalk";
9224
9520
  function mikromcpDir2() {
9225
- return join4(homedir4(), ".mikromcp");
9521
+ return join4(homedir3(), ".mikromcp");
9226
9522
  }
9227
9523
  function defaultAuditLogPath() {
9228
9524
  return join4(mikromcpDir2(), "audit.ndjson");
@@ -9230,8 +9526,8 @@ function defaultAuditLogPath() {
9230
9526
  function toYaml(obj) {
9231
9527
  const doc = new Document(obj);
9232
9528
  visit(doc, {
9233
- Scalar(key, node) {
9234
- if (key !== "key" && typeof node.value === "string") {
9529
+ Scalar(key2, node) {
9530
+ if (key2 !== "key" && typeof node.value === "string") {
9235
9531
  node.type = "QUOTE_DOUBLE";
9236
9532
  }
9237
9533
  },
@@ -9253,8 +9549,8 @@ function splitTags(val) {
9253
9549
  }
9254
9550
  function findClaudeDesktopConfig() {
9255
9551
  const candidates = [
9256
- join4(homedir4(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
9257
- join4(homedir4(), ".config", "Claude", "claude_desktop_config.json")
9552
+ join4(homedir3(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
9553
+ join4(homedir3(), ".config", "Claude", "claude_desktop_config.json")
9258
9554
  ];
9259
9555
  return candidates.find((p) => existsSync4(p)) ?? null;
9260
9556
  }
@@ -9419,13 +9715,13 @@ function writeRoutersYaml(data) {
9419
9715
  };
9420
9716
  let parsed = { routers: {} };
9421
9717
  if (existsSync4(filePath)) {
9422
- const raw = readFileSync7(filePath, "utf-8");
9718
+ const raw = readFileSync6(filePath, "utf-8");
9423
9719
  parsed = yamlParse(raw) ?? { routers: {} };
9424
9720
  if (!parsed.routers) parsed.routers = {};
9425
9721
  }
9426
9722
  parsed.routers[data.routerId] = newEntry;
9427
- mkdirSync3(data.configDir, { recursive: true });
9428
- writeFileSync2(filePath, toYaml(parsed));
9723
+ mkdirSync(data.configDir, { recursive: true });
9724
+ writeFileSync(filePath, toYaml(parsed));
9429
9725
  return true;
9430
9726
  }
9431
9727
  function writeIdentitiesYaml(data) {
@@ -9433,18 +9729,18 @@ function writeIdentitiesYaml(data) {
9433
9729
  const newEntry = {
9434
9730
  token: data.tokenHash,
9435
9731
  role: data.role,
9436
- allowedRouters: data.allowedRouters === "*" ? ["*"] : data.allowedRouters.split(",").map((s) => s.trim()),
9437
- allowedToolPatterns: data.allowedToolPatterns === "*" ? ["*"] : data.allowedToolPatterns.split(",").map((s) => s.trim())
9732
+ allowedRouters: data.allowedRouters === "*" ? [] : data.allowedRouters.split(",").map((s) => s.trim()),
9733
+ allowedToolPatterns: data.allowedToolPatterns === "*" ? [] : data.allowedToolPatterns.split(",").map((s) => s.trim())
9438
9734
  };
9439
9735
  let parsed = { identities: {} };
9440
9736
  if (existsSync4(filePath)) {
9441
- const raw = readFileSync7(filePath, "utf-8");
9737
+ const raw = readFileSync6(filePath, "utf-8");
9442
9738
  parsed = yamlParse(raw) ?? { identities: {} };
9443
9739
  if (!parsed.identities) parsed.identities = {};
9444
9740
  }
9445
9741
  parsed.identities[data.identityId] = newEntry;
9446
- mkdirSync3(data.configDir, { recursive: true });
9447
- writeFileSync2(filePath, toYaml(parsed));
9742
+ mkdirSync(data.configDir, { recursive: true });
9743
+ writeFileSync(filePath, toYaml(parsed));
9448
9744
  return true;
9449
9745
  }
9450
9746
  function writeDotEnv(data) {
@@ -9478,8 +9774,8 @@ function writeDotEnv(data) {
9478
9774
  "# \u2500\u2500 RBAC (optional, for stdio with identity enforcement) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
9479
9775
  "# MIKROMCP_STDIO_IDENTITY="
9480
9776
  ];
9481
- mkdirSync3(data.configDir, { recursive: true });
9482
- writeFileSync2(data.envPath, lines.join("\n") + "\n");
9777
+ mkdirSync(data.configDir, { recursive: true });
9778
+ writeFileSync(data.envPath, lines.join("\n") + "\n");
9483
9779
  }
9484
9780
  function registerClaudeDesktop() {
9485
9781
  const configPath = findClaudeDesktopConfig();
@@ -9500,13 +9796,13 @@ function registerClaudeDesktop() {
9500
9796
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9501
9797
  const backupPath = `${configPath}.backup-${timestamp}`;
9502
9798
  copyFileSync(configPath, backupPath);
9503
- const raw = readFileSync7(configPath, "utf-8");
9799
+ const raw = readFileSync6(configPath, "utf-8");
9504
9800
  const config = JSON.parse(raw);
9505
9801
  if (!config.mcpServers || typeof config.mcpServers !== "object") {
9506
9802
  config.mcpServers = {};
9507
9803
  }
9508
9804
  config.mcpServers["mikromcp"] = entry;
9509
- writeFileSync2(configPath, JSON.stringify(config, null, 2));
9805
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
9510
9806
  console.log(chalk2.dim(` Backed up to ${backupPath}`));
9511
9807
  console.log(chalk2.dim(" Restart Claude to apply."));
9512
9808
  return { registered: true };
@@ -9589,7 +9885,7 @@ async function runInit() {
9589
9885
  }
9590
9886
 
9591
9887
  // src/cli/index.ts
9592
- var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.0.9");
9888
+ var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.1.0");
9593
9889
  program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
9594
9890
  program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
9595
9891
  program.command("init").description("Interactive setup wizard").action(runInit);
@@ -9599,6 +9895,6 @@ program.command("update").description("Update mikromcp to the latest version via
9599
9895
  });
9600
9896
 
9601
9897
  // src/main.ts
9602
- loadDotenv({ path: join5(homedir5(), ".mikromcp", ".env"), override: false, quiet: true });
9898
+ loadDotenv({ path: join5(homedir4(), ".mikromcp", ".env"), override: false, quiet: true });
9603
9899
  program.parse();
9604
9900
  //# sourceMappingURL=main.js.map