@modelcontextprotocol/inspector 2.3.0 → 2.4.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.
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-MCKGQKYU.js";
3
2
 
4
3
  // src/index.ts
5
- import { resolve as resolve5 } from "path";
4
+ import { resolve as resolve8 } from "path";
6
5
  import { fileURLToPath as fileURLToPath7 } from "url";
7
6
 
8
7
  // server/run-web.ts
9
- import { resolve as resolve4, join as join9, dirname as dirname7 } from "path";
8
+ import { resolve as resolve7, join as join10, dirname as dirname9 } from "path";
10
9
  import { fileURLToPath as fileURLToPath6 } from "url";
11
10
  import { Command } from "commander";
12
11
 
@@ -116,6 +115,22 @@ var SECRET_FIELD_IDP_CLIENT_SECRET = "idp-client-secret";
116
115
  var envSecretField = (envKey) => `env:${envKey}`;
117
116
 
118
117
  // ../../core/json/jsonUtils.ts
118
+ function isSerializableJson(value) {
119
+ if (value === null) return true;
120
+ switch (typeof value) {
121
+ case "string":
122
+ case "boolean":
123
+ return true;
124
+ case "number":
125
+ return Number.isFinite(value);
126
+ case "object":
127
+ return Array.isArray(value) ? value.every(isSerializableJson) : Object.values(value).every(
128
+ isSerializableJson
129
+ );
130
+ default:
131
+ return false;
132
+ }
133
+ }
119
134
  function toRecord(value) {
120
135
  return value;
121
136
  }
@@ -180,6 +195,68 @@ function envPairsToRecord(pairs) {
180
195
  }
181
196
  return out;
182
197
  }
198
+ function stdioConfigFieldsFromSettings(settings) {
199
+ const env = envPairsToRecord(settings.env);
200
+ const cwd = settings.cwd?.trim();
201
+ return {
202
+ env: Object.keys(env).length > 0 ? env : void 0,
203
+ cwd: cwd ? cwd : void 0
204
+ };
205
+ }
206
+ function normalizeStoredMetadata(metadata) {
207
+ if (metadata === void 0 || metadata === null) return {};
208
+ if (Array.isArray(metadata)) {
209
+ const out = {};
210
+ for (const entry of metadata) {
211
+ if (typeof entry !== "object" || entry === null || typeof entry.key !== "string") {
212
+ console.warn(
213
+ "Ignoring malformed legacy metadata entry (expected `{ key, value }`)."
214
+ );
215
+ continue;
216
+ }
217
+ const { key, value } = entry;
218
+ if (key.trim() === "") continue;
219
+ const resolved2 = value === void 0 ? "" : value;
220
+ if (!keepSerializable(key, resolved2)) continue;
221
+ defineMetadataKey(out, key, resolved2);
222
+ }
223
+ return out;
224
+ }
225
+ if (typeof metadata === "object") {
226
+ const out = {};
227
+ for (const [key, value] of Object.entries(metadata)) {
228
+ if (keepSerializable(key, value)) defineMetadataKey(out, key, value);
229
+ }
230
+ return out;
231
+ }
232
+ console.warn(
233
+ `Ignoring malformed \`metadata\` (expected a JSON object, got ${describeJsonType(metadata)}).`
234
+ );
235
+ return {};
236
+ }
237
+ function defineMetadataKey(out, key, value) {
238
+ Object.defineProperty(out, key, {
239
+ value,
240
+ writable: true,
241
+ enumerable: true,
242
+ configurable: true
243
+ });
244
+ }
245
+ function keepSerializable(key, value) {
246
+ if (isSerializableJson(value)) return true;
247
+ console.warn(
248
+ `Ignoring metadata key "${key}" \u2014 its value is not JSON that can be sent (got ${describeJsonType(value)}; note \`1e400\` parses to Infinity).`
249
+ );
250
+ return false;
251
+ }
252
+ function describeJsonType(value) {
253
+ if (value === null) return "null";
254
+ if (Array.isArray(value)) return "array";
255
+ if (typeof value === "number" && !Number.isFinite(value)) {
256
+ return Number.isNaN(value) ? "NaN" : "a non-finite number";
257
+ }
258
+ return typeof value;
259
+ }
183
260
  function cleanAuthorizationParams(params) {
184
261
  if (params === void 0) return void 0;
185
262
  if (typeof params !== "object" || params === null || Array.isArray(params)) {
@@ -219,7 +296,7 @@ function storedFieldsToInspectorSettings(stored) {
219
296
  const settings = {
220
297
  headers: headersPairs,
221
298
  env: envRecordToPairs(stored.env),
222
- metadata: stored.metadata ?? [],
299
+ metadata: normalizeStoredMetadata(stored.metadata),
223
300
  connectionTimeout: stored.connectionTimeout ?? 0,
224
301
  requestTimeout: stored.requestTimeout ?? 0,
225
302
  // Unlike the timeouts (0 = "SDK default"), task TTL has a concrete product
@@ -268,6 +345,9 @@ function storedFieldsToInspectorSettings(stored) {
268
345
  if (stored.oauth?.enterpriseManaged === true) {
269
346
  settings.enterpriseManaged = true;
270
347
  }
348
+ if (stored.oauth?.requestRefreshToken === false) {
349
+ settings.oauthRequestRefreshToken = false;
350
+ }
271
351
  if (stored.cwd) settings.cwd = stored.cwd;
272
352
  return settings;
273
353
  }
@@ -281,9 +361,8 @@ function inspectorSettingsToStoredFields(settings) {
281
361
  if (Object.keys(headersRecord).length > 0) {
282
362
  out.headers = headersRecord;
283
363
  }
284
- const metadataFiltered = settings.metadata.filter((m) => m.key.trim() !== "");
285
- if (metadataFiltered.length > 0) {
286
- out.metadata = metadataFiltered;
364
+ if (Object.keys(settings.metadata).length > 0) {
365
+ out.metadata = settings.metadata;
287
366
  }
288
367
  if (settings.connectionTimeout > 0) {
289
368
  out.connectionTimeout = settings.connectionTimeout;
@@ -335,6 +414,9 @@ function inspectorSettingsToStoredFields(settings) {
335
414
  if (settings.enterpriseManaged === true) {
336
415
  oauthFields.enterpriseManaged = true;
337
416
  }
417
+ if (settings.oauthRequestRefreshToken === false) {
418
+ oauthFields.requestRefreshToken = false;
419
+ }
338
420
  if (Object.keys(oauthFields).length > 0) {
339
421
  out.oauth = oauthFields;
340
422
  }
@@ -672,6 +754,49 @@ var API_SERVER_ENV_VARS = {
672
754
  };
673
755
  var INSPECTOR_API_TOKEN_GLOBAL = "__INSPECTOR_API_TOKEN__";
674
756
 
757
+ // ../../core/auth/secret-storage-info.ts
758
+ function secretStorageLabel(info) {
759
+ switch (info.kind) {
760
+ case "keyring":
761
+ return "OS keychain";
762
+ case "file":
763
+ if (info.encryptionUnknown !== void 0) return "File (unreadable)";
764
+ return info.plaintext ? "File (unencrypted)" : "File (encrypted)";
765
+ case "memory":
766
+ return "Memory (this session only)";
767
+ }
768
+ }
769
+ function secretStorageCaveat(info) {
770
+ if (info.kind === "memory") {
771
+ return "Secrets are not written anywhere and are lost on exit.";
772
+ }
773
+ if (info.looseMode !== void 0) {
774
+ const mode = info.looseMode.toString(8).padStart(4, "0");
775
+ if (info.encryptionUnknown !== void 0) {
776
+ return `The secrets file is mode ${mode}, not 0600, and could not be tightened \u2014 others can copy it, and whether that exposes the secrets depends on encryption this build could not determine (${info.encryptionUnknown}).`;
777
+ }
778
+ return info.plaintext === false ? `The secrets file is mode ${mode}, not 0600, and could not be tightened \u2014 others can copy it, and the passphrase is then the only thing protecting its contents.` : `The secrets file is mode ${mode}, not 0600, and could not be tightened \u2014 anyone who can read it can read the secrets in it.`;
779
+ }
780
+ if (info.permissionsUnknown !== void 0) {
781
+ return `The secrets file's permissions could not be checked (${info.permissionsUnknown}), so it is not known whether others can read it.`;
782
+ }
783
+ if (info.encryptionUnknown !== void 0) {
784
+ return `The secrets file could not be read (${info.encryptionUnknown}). Saving a secret will fail rather than overwrite it.`;
785
+ }
786
+ if (info.plaintext) {
787
+ return info.pendingEncryption ? "Existing secrets in this file are still unencrypted (file mode 0600). They are re-encrypted the next time a secret is saved." : "Secrets are stored unencrypted (file mode 0600). Set MCP_INSPECTOR_SECRET_KEY to encrypt them.";
788
+ }
789
+ return void 0;
790
+ }
791
+ function secretStorageSummary(info) {
792
+ const parts = [secretStorageLabel(info)];
793
+ if (info.path) parts.push(`at ${info.path}`);
794
+ if (info.reason === "fallback") {
795
+ parts.push("\u2014 fell back from the OS keychain");
796
+ }
797
+ return parts.join(" ");
798
+ }
799
+
675
800
  // ../../core/node/version.ts
676
801
  import { readFileSync as readFileSync2 } from "fs";
677
802
  import { dirname as dirname3, join as join2, parse } from "path";
@@ -771,8 +896,8 @@ var DEFAULT_BIND_HOST = "127.0.0.1";
771
896
  function resolveBindHostname(env = process.env) {
772
897
  const host = (env.HOST ?? DEFAULT_BIND_HOST).trim();
773
898
  if (isAllInterfacesHost(host) && !isEnabled(env[BIND_ALL_INTERFACES_ENV])) {
774
- const resolved = canonicalUrlHost(host);
775
- const shown = stripBrackets(resolved) === stripBrackets(host.toLowerCase()) ? `HOST="${host}"` : `HOST="${host}" (resolves to ${resolved})`;
899
+ const resolved2 = canonicalUrlHost(host);
900
+ const shown = stripBrackets(resolved2) === stripBrackets(host.toLowerCase()) ? `HOST="${host}"` : `HOST="${host}" (resolves to ${resolved2})`;
776
901
  throw new Error(
777
902
  `Refusing to bind ${shown}: this exposes the MCP Inspector to your entire network, and its backend can spawn local processes and connect to MCP servers on your behalf \u2014 the exposure DNS-rebinding attacks target. Bind a loopback host (localhost / 127.0.0.1) instead. To override \u2014 only inside an isolated container or trusted network \u2014 set ${BIND_ALL_INTERFACES_ENV}=true.`
778
903
  );
@@ -785,7 +910,10 @@ var __dirname = dirname4(fileURLToPath2(import.meta.url));
785
910
  var LOOPBACK_FRAME_ANCESTORS = ["http://127.0.0.1:*", "http://localhost:*"];
786
911
  var CSP_HOST_SOURCE = /^[a-z][a-z0-9+.-]*:\/\/[^\s;,'"[\]*]+$/i;
787
912
  function sandboxFrameAncestors(allowedOrigins) {
788
- const valid = (allowedOrigins ?? []).filter((o) => CSP_HOST_SOURCE.test(o));
913
+ return frameAncestorsDirective(allowedOrigins);
914
+ }
915
+ function frameAncestorsDirective(origins) {
916
+ const valid = (origins ?? []).filter((o) => CSP_HOST_SOURCE.test(o));
789
917
  const sources = valid.length > 0 ? valid : LOOPBACK_FRAME_ANCESTORS;
790
918
  return `frame-ancestors ${sources.join(" ")}`;
791
919
  }
@@ -823,12 +951,12 @@ function createSandboxController(options) {
823
951
  const p = parseInt(new URL(sandboxUrl).port, 10);
824
952
  return { port: p, url: sandboxUrl };
825
953
  }
826
- return new Promise((resolve6) => {
954
+ return new Promise((resolve9) => {
827
955
  let settled = false;
828
956
  const settle = (value) => {
829
957
  if (settled) return;
830
958
  settled = true;
831
- resolve6(value);
959
+ resolve9(value);
832
960
  };
833
961
  server = createServer((req, res) => {
834
962
  if (req.method !== "GET" || req.url !== "/sandbox" && req.url !== "/sandbox/") {
@@ -882,11 +1010,11 @@ function createSandboxController(options) {
882
1010
  },
883
1011
  async close() {
884
1012
  if (!server) return;
885
- return new Promise((resolve6) => {
1013
+ return new Promise((resolve9) => {
886
1014
  server.close(() => {
887
1015
  server = null;
888
1016
  sandboxUrl = null;
889
- resolve6();
1017
+ resolve9();
890
1018
  });
891
1019
  });
892
1020
  },
@@ -896,6 +1024,169 @@ function createSandboxController(options) {
896
1024
  };
897
1025
  }
898
1026
 
1027
+ // server/app-origin-controller.ts
1028
+ import { createServer as createServer2 } from "http";
1029
+ import { randomBytes } from "crypto";
1030
+ var DEFAULT_APP_ORIGIN_PORT = 6278;
1031
+ var MAX_DOCUMENTS = 32;
1032
+ var MAX_RETAINED_CHARS = 64 * 1024 * 1024;
1033
+ var DOCUMENT_TTL_MS = 60 * 60 * 1e3;
1034
+ function appDocumentEmbedders(sandboxUrl, allowedOrigins) {
1035
+ const origins = [];
1036
+ if (sandboxUrl) {
1037
+ try {
1038
+ origins.push(new URL(sandboxUrl).origin);
1039
+ } catch {
1040
+ }
1041
+ }
1042
+ origins.push(...allowedOrigins ?? []);
1043
+ return origins.length > 0 ? origins : void 0;
1044
+ }
1045
+ function parseListenPort2(raw) {
1046
+ const v = raw?.trim();
1047
+ if (!v || !/^\d+$/.test(v)) return void 0;
1048
+ const n = parseInt(v, 10);
1049
+ return n <= 65535 ? n : void 0;
1050
+ }
1051
+ function resolveAppOriginPort() {
1052
+ const parsed = parseListenPort2(process.env.MCP_APP_ORIGIN_PORT);
1053
+ if (parsed === void 0 && process.env.MCP_APP_ORIGIN_PORT?.trim()) {
1054
+ console.warn(
1055
+ `Ignoring invalid MCP_APP_ORIGIN_PORT="${process.env.MCP_APP_ORIGIN_PORT}" (need an integer 0\u201365535); falling back.`
1056
+ );
1057
+ }
1058
+ return parsed ?? DEFAULT_APP_ORIGIN_PORT;
1059
+ }
1060
+ var DOC_PATH_PREFIX = "/app-document/";
1061
+ function parseDocumentId(url) {
1062
+ if (!url || !url.startsWith(DOC_PATH_PREFIX)) return null;
1063
+ const id = url.slice(DOC_PATH_PREFIX.length).split(/[?#]/)[0];
1064
+ return /^[0-9a-f]{32}$/.test(id ?? "") ? id ?? null : null;
1065
+ }
1066
+ function createAppOriginController(options) {
1067
+ const { port, host = DEFAULT_BIND_HOST, embedderOrigins } = options;
1068
+ let server = null;
1069
+ let origin = null;
1070
+ const documents = /* @__PURE__ */ new Map();
1071
+ let retainedChars = 0;
1072
+ function drop(id) {
1073
+ const doc = documents.get(id);
1074
+ if (!doc) return;
1075
+ retainedChars -= doc.size;
1076
+ documents.delete(id);
1077
+ }
1078
+ const FRAME_ANCESTORS = frameAncestorsDirective(embedderOrigins);
1079
+ function evictExpired(now) {
1080
+ for (const [id, doc] of documents) {
1081
+ if (doc.expiresAt <= now) drop(id);
1082
+ }
1083
+ }
1084
+ return {
1085
+ async start() {
1086
+ if (server && origin) {
1087
+ return { port: parseInt(new URL(origin).port, 10), url: origin };
1088
+ }
1089
+ return new Promise((resolve9) => {
1090
+ let settled = false;
1091
+ const settle = (value) => {
1092
+ if (settled) return;
1093
+ settled = true;
1094
+ resolve9(value);
1095
+ };
1096
+ server = createServer2((req, res) => {
1097
+ const id = req.method === "GET" ? parseDocumentId(req.url) : null;
1098
+ const doc = id ? documents.get(id) : void 0;
1099
+ if (!doc || doc.expiresAt <= Date.now()) {
1100
+ if (id) drop(id);
1101
+ res.writeHead(404, { "Content-Type": "text/plain" });
1102
+ res.end("Not Found");
1103
+ return;
1104
+ }
1105
+ res.writeHead(200, {
1106
+ "Content-Type": "text/html; charset=utf-8",
1107
+ "Cache-Control": "no-store, no-cache, must-revalidate",
1108
+ Pragma: "no-cache",
1109
+ // `nosniff` matters more here than on a page we authored: these
1110
+ // bytes are server-supplied and untrusted.
1111
+ "X-Content-Type-Options": "nosniff",
1112
+ // The per-app policy as a real header. It is the same policy baked
1113
+ // into the document's <meta>; delivering both is deliberate —
1114
+ // multiple policies intersect, and two copies of one policy
1115
+ // intersect to itself, so this can only ever be at least as strict.
1116
+ "Content-Security-Policy": doc.csp ? `${doc.csp}; ${FRAME_ANCESTORS}` : FRAME_ANCESTORS
1117
+ });
1118
+ res.end(doc.html);
1119
+ });
1120
+ let retriedDynamic = false;
1121
+ server.on("error", (err) => {
1122
+ if (err.code === "EADDRINUSE") {
1123
+ if (!retriedDynamic && port !== 0) {
1124
+ retriedDynamic = true;
1125
+ console.warn(
1126
+ `App origin: port ${port} in use; falling back to an OS-assigned port. Apps declaring _meta.ui.domain will still render, but the origin they are served from is no longer predictable \u2014 set MCP_APP_ORIGIN_PORT to a free one if your app's backend allowlists it.`
1127
+ );
1128
+ server.listen(0, host);
1129
+ return;
1130
+ }
1131
+ console.error(
1132
+ `App origin: port ${port || "dynamic"} in use. Apps declaring _meta.ui.domain will fall back to an opaque origin.`
1133
+ );
1134
+ } else {
1135
+ console.error("App origin server error:", err);
1136
+ }
1137
+ server = null;
1138
+ origin = null;
1139
+ documents.clear();
1140
+ retainedChars = 0;
1141
+ settle({ port: 0, url: "" });
1142
+ });
1143
+ server.listen(port, host, () => {
1144
+ const addr = server.address();
1145
+ if (typeof addr !== "object" || addr === null) {
1146
+ settle({ port: 0, url: "" });
1147
+ return;
1148
+ }
1149
+ const actualPort = addr.port;
1150
+ const canonicalHost = canonicalUrlHost(host);
1151
+ const urlHost = isAllInterfacesHost(canonicalHost) ? "localhost" : canonicalHost;
1152
+ origin = `http://${urlHost}:${actualPort}`;
1153
+ settle({ port: actualPort, url: origin });
1154
+ });
1155
+ });
1156
+ },
1157
+ async close() {
1158
+ documents.clear();
1159
+ retainedChars = 0;
1160
+ if (!server) return;
1161
+ return new Promise((resolve9) => {
1162
+ server.close(() => {
1163
+ server = null;
1164
+ origin = null;
1165
+ resolve9();
1166
+ });
1167
+ });
1168
+ },
1169
+ getOrigin() {
1170
+ return origin;
1171
+ },
1172
+ publish(doc) {
1173
+ if (!origin) return null;
1174
+ const now = Date.now();
1175
+ evictExpired(now);
1176
+ const size = doc.html.length + (doc.csp?.length ?? 0);
1177
+ while (documents.size >= MAX_DOCUMENTS || documents.size > 0 && retainedChars + size > MAX_RETAINED_CHARS) {
1178
+ const oldest = documents.keys().next();
1179
+ if (oldest.done) break;
1180
+ drop(oldest.value);
1181
+ }
1182
+ const id = randomBytes(16).toString("hex");
1183
+ documents.set(id, { ...doc, expiresAt: now + DOCUMENT_TTL_MS, size });
1184
+ retainedChars += size;
1185
+ return { url: `${origin}${DOC_PATH_PREFIX}${id}` };
1186
+ }
1187
+ };
1188
+ }
1189
+
899
1190
  // server/web-server-config.ts
900
1191
  var inspectorVersion = readInspectorVersionSafe(import.meta.url);
901
1192
  function defaultEnvironmentFromProcess(extra) {
@@ -925,8 +1216,16 @@ function defaultEnvironmentFromProcess(extra) {
925
1216
  }
926
1217
  return out;
927
1218
  }
928
- function webServerConfigToInitialPayload(config) {
929
- return { ...transportDefaults(config), version: inspectorVersion };
1219
+ function webServerConfigToInitialPayload(config, secretStorage) {
1220
+ return {
1221
+ ...transportDefaults(config),
1222
+ version: inspectorVersion,
1223
+ // Omitted rather than defaulted when the caller didn't resolve a store:
1224
+ // the UI treats an absent descriptor as "unknown" and shows nothing,
1225
+ // which is the only honest rendering. A default of "keychain" would put a
1226
+ // confident wrong answer under the field where a secret is typed.
1227
+ ...secretStorage ? { secretStorage } : {}
1228
+ };
930
1229
  }
931
1230
  function transportDefaults(config) {
932
1231
  const mc = config.initialMcpConfig;
@@ -966,7 +1265,7 @@ function transportDefaults(config) {
966
1265
  defaultEnvironment
967
1266
  };
968
1267
  }
969
- function printServerBanner(config, actualPort, resolvedToken, sandboxUrl) {
1268
+ function printServerBanner(config, actualPort, resolvedToken, sandboxUrl, secretStorage) {
970
1269
  const h = canonicalUrlHost(config.hostname);
971
1270
  const bannerHost = isAllInterfacesHost(h) ? "localhost" : h;
972
1271
  const baseUrl = httpOrigin(bannerHost, actualPort);
@@ -983,6 +1282,10 @@ MCP Inspector Web is up and running at:
983
1282
  console.log(" Auth: disabled (DANGEROUSLY_OMIT_AUTH)\n");
984
1283
  } else {
985
1284
  console.log(` Auth token: ${resolvedToken}
1285
+ `);
1286
+ }
1287
+ if (secretStorage) {
1288
+ console.log(` Secrets: ${secretStorageSummary(secretStorage)}
986
1289
  `);
987
1290
  }
988
1291
  if (config.autoOpen) {
@@ -1038,6 +1341,14 @@ function buildWebServerConfig(options = {}) {
1038
1341
  const dangerouslyOmitAuth = !!process.env.DANGEROUSLY_OMIT_AUTH;
1039
1342
  const authToken = dangerouslyOmitAuth ? "" : process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] ?? process.env[LEGACY_AUTH_TOKEN_ENV] ?? "";
1040
1343
  const sandboxPort = resolveSandboxPort();
1344
+ const requestedAppOriginPort = resolveAppOriginPort();
1345
+ const appOriginCollidesWith = requestedAppOriginPort !== 0 && (requestedAppOriginPort === port ? "the web server (CLIENT_PORT)" : requestedAppOriginPort === sandboxPort ? "the MCP Apps sandbox (MCP_SANDBOX_PORT)" : null);
1346
+ if (appOriginCollidesWith) {
1347
+ console.warn(
1348
+ `App origin: port ${requestedAppOriginPort} is already ${appOriginCollidesWith}; using an OS-assigned port instead. Apps declaring _meta.ui.domain will still render, but the origin they are served from is no longer predictable \u2014 set MCP_APP_ORIGIN_PORT to a free one if your app's backend allowlists it.`
1349
+ );
1350
+ }
1351
+ const appOriginPort = appOriginCollidesWith ? 0 : requestedAppOriginPort;
1041
1352
  let logger;
1042
1353
  if (process.env.MCP_LOG_FILE) {
1043
1354
  logger = pino(
@@ -1073,6 +1384,7 @@ function buildWebServerConfig(options = {}) {
1073
1384
  allowedOrigins: configuredOrigins?.length ? configuredOrigins : defaultAllowedOrigins(hostname, port),
1074
1385
  sandboxPort,
1075
1386
  sandboxHost: hostname,
1387
+ appOriginPort,
1076
1388
  logger,
1077
1389
  autoOpen: resolveAutoOpen()
1078
1390
  };
@@ -1085,18 +1397,19 @@ function resolveAutoOpen() {
1085
1397
  }
1086
1398
 
1087
1399
  // server/start-vite-dev-server.ts
1088
- import { join as join6, dirname as dirname5, resolve as resolve3 } from "path";
1400
+ import { join as join7, dirname as dirname7, resolve as resolve6 } from "path";
1089
1401
  import { fileURLToPath as fileURLToPath4 } from "url";
1090
- import { createServer as createServer2 } from "vite";
1402
+ import { createServer as createServer3 } from "vite";
1091
1403
  import react from "@vitejs/plugin-react";
1092
1404
 
1093
1405
  // ../../core/mcp/remote/node/server.ts
1094
- import { randomBytes, timingSafeEqual } from "crypto";
1095
- import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
1406
+ import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1407
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
1096
1408
  import { stat as fsStat } from "fs/promises";
1097
1409
  import { homedir } from "os";
1098
1410
  import { Hono } from "hono";
1099
1411
  import { streamSSE } from "hono/streaming";
1412
+ import { bodyLimit } from "hono/body-limit";
1100
1413
  import { watch as chokidarWatch } from "chokidar";
1101
1414
 
1102
1415
  // ../../core/mcp/config.ts
@@ -1411,6 +1724,7 @@ function parseAuthChallengeFromResponse(response, context) {
1411
1724
  return {
1412
1725
  reason: reasonFromHttpResponse(status, bearer),
1413
1726
  ...requiredScopes.length > 0 ? { requiredScopes } : {},
1727
+ ...bearer.resourceMetadata ? { resourceMetadataUrl: bearer.resourceMetadata } : {},
1414
1728
  ...bearer.errorDescription ? { message: bearer.errorDescription } : {},
1415
1729
  ...context ? { context } : {},
1416
1730
  raw: {
@@ -1440,8 +1754,26 @@ function createAuthChallengeInterceptFetch(baseFetch) {
1440
1754
  );
1441
1755
  };
1442
1756
  }
1757
+ function createAuthChallengeObserverFetch(baseFetch, onChallenge) {
1758
+ return async (input, init) => {
1759
+ const response = await baseFetch(input, init);
1760
+ if (response.status === 401 || response.status === 403) {
1761
+ const challenge = parseAuthChallengeFromResponse(response);
1762
+ if (challenge) {
1763
+ onChallenge(challenge);
1764
+ }
1765
+ }
1766
+ return response;
1767
+ };
1768
+ }
1443
1769
 
1444
- // ../../core/mcp/node/transport.ts
1770
+ // ../../core/mcp/node/proxyFetch.ts
1771
+ function asUndiciInit(init) {
1772
+ return init;
1773
+ }
1774
+ function asBodyInit(body) {
1775
+ return body;
1776
+ }
1445
1777
  var PROXY_ENV_VARS = [
1446
1778
  "HTTPS_PROXY",
1447
1779
  "https_proxy",
@@ -1455,27 +1787,95 @@ function readProxyEnv() {
1455
1787
  }
1456
1788
  return void 0;
1457
1789
  }
1458
- function withProxyDispatcher(baseFetch) {
1459
- if (readProxyEnv() === void 0) return baseFetch;
1460
- let dispatcherPromise;
1461
- const getDispatcher = () => {
1462
- dispatcherPromise ??= import("./undici-KNXAVD2F.js").then(
1463
- ({ EnvHttpProxyAgent }) => new EnvHttpProxyAgent(),
1464
- (cause) => {
1465
- throw new Error(
1466
- "HTTPS_PROXY / HTTP_PROXY is set but the `undici` package is not available. Install it (it ships with the CLI client) or unset the proxy env var.",
1467
- { cause }
1468
- );
1469
- }
1790
+ var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
1791
+ 101,
1792
+ 103,
1793
+ 204,
1794
+ 205,
1795
+ 304
1796
+ ]);
1797
+ function toGlobalResponse(res) {
1798
+ const headers = new Headers();
1799
+ for (const [key, value] of res.headers) headers.append(key, value);
1800
+ const out = new Response(
1801
+ NULL_BODY_STATUSES.has(res.status) ? null : asBodyInit(res.body),
1802
+ {
1803
+ status: res.status,
1804
+ statusText: res.statusText,
1805
+ headers
1806
+ }
1807
+ );
1808
+ for (const [key, value] of [
1809
+ ["url", res.url],
1810
+ ["redirected", res.redirected],
1811
+ ["type", res.type]
1812
+ ]) {
1813
+ Object.defineProperty(out, key, {
1814
+ value,
1815
+ configurable: true,
1816
+ enumerable: false
1817
+ });
1818
+ }
1819
+ return out;
1820
+ }
1821
+ var loadedProxiedFetch;
1822
+ async function loadProxiedFetch() {
1823
+ let undici;
1824
+ try {
1825
+ undici = await import("undici");
1826
+ } catch (cause) {
1827
+ throw new Error(
1828
+ "HTTPS_PROXY / HTTP_PROXY is set but the `undici` package could not be loaded, so requests cannot be routed through the proxy. This is a packaging fault rather than something to fix by installing undici; please report it, or unset the proxy env var to fall back to a direct connection.",
1829
+ { cause }
1830
+ );
1831
+ }
1832
+ const dispatcher = new undici.EnvHttpProxyAgent();
1833
+ return async (input, init) => {
1834
+ const [target, requestInit] = toUndiciRequest(input, init);
1835
+ return toGlobalResponse(
1836
+ await undici.fetch(target, { ...asUndiciInit(requestInit), dispatcher })
1470
1837
  );
1471
- return dispatcherPromise;
1472
1838
  };
1473
- const proxiedFetch = async (input, init) => {
1474
- const dispatcher = await getDispatcher();
1475
- return baseFetch(input, { ...init, dispatcher });
1839
+ }
1840
+ function toUndiciRequest(input, init) {
1841
+ if (input instanceof Request) {
1842
+ return [
1843
+ input.url,
1844
+ {
1845
+ method: input.method,
1846
+ headers: input.headers,
1847
+ // A stream body needs `duplex: "half"`; the field is undici's, not in
1848
+ // the DOM `RequestInit`, so it rides along as an extra property.
1849
+ ...input.body ? { body: input.body, duplex: "half" } : {},
1850
+ signal: input.signal,
1851
+ // Everything else the Request carries. Dropping these would silently
1852
+ // change fetch semantics under a proxy — most visibly `redirect`, where
1853
+ // a `new Request(url, { redirect: "manual" })` would start following
1854
+ // redirects instead of surfacing them.
1855
+ redirect: input.redirect,
1856
+ cache: input.cache,
1857
+ credentials: input.credentials,
1858
+ integrity: input.integrity,
1859
+ keepalive: input.keepalive,
1860
+ mode: input.mode,
1861
+ referrer: input.referrer,
1862
+ referrerPolicy: input.referrerPolicy,
1863
+ // An explicit `init` still wins, per the fetch contract.
1864
+ ...init
1865
+ }
1866
+ ];
1867
+ }
1868
+ return [input instanceof URL ? input.href : input, init ?? {}];
1869
+ }
1870
+ function createProxyFetch() {
1871
+ if (readProxyEnv() === void 0) return void 0;
1872
+ return async (input, init) => {
1873
+ loadedProxiedFetch ??= loadProxiedFetch();
1874
+ return (await loadedProxiedFetch)(input, init);
1476
1875
  };
1477
- return proxiedFetch;
1478
1876
  }
1877
+
1878
+ // ../../core/mcp/node/transport.ts
1479
1879
  function headersFromSettings(settings) {
1480
1880
  if (!settings || settings.headers.length === 0) return void 0;
1481
1881
  const out = {};
@@ -1495,10 +1895,12 @@ function createTransportNode(config, options = {}) {
1495
1895
  onFetchResponseBody,
1496
1896
  authProvider,
1497
1897
  settings,
1498
- interceptAuthChallenges = false
1898
+ interceptAuthChallenges = false,
1899
+ onAuthChallengeObserved
1499
1900
  } = options;
1500
- const baseFetch = withProxyDispatcher(optionsFetchFn ?? globalThis.fetch);
1501
- const fetchWithOptionalAuthIntercept = interceptAuthChallenges ? createAuthChallengeInterceptFetch(baseFetch) : baseFetch;
1901
+ const baseFetch = optionsFetchFn ?? createProxyFetch() ?? globalThis.fetch;
1902
+ const withChallengeObserver = (inner) => onAuthChallengeObserved ? createAuthChallengeObserverFetch(inner, onAuthChallengeObserved) : inner;
1903
+ const fetchWithOptionalAuthIntercept = interceptAuthChallenges ? createAuthChallengeInterceptFetch(withChallengeObserver(baseFetch)) : withChallengeObserver(baseFetch);
1502
1904
  if (serverType === "stdio") {
1503
1905
  const stdioConfig = config;
1504
1906
  const transport = new StdioClientTransport({
@@ -1523,7 +1925,8 @@ function createTransportNode(config, options = {}) {
1523
1925
  } else if (serverType === "sse") {
1524
1926
  const sseConfig = config;
1525
1927
  const url = new URL(sseConfig.url);
1526
- const sseFetch = sseConfig.eventSourceInit?.fetch || fetchWithOptionalAuthIntercept;
1928
+ const configuredSseFetch = sseConfig.eventSourceInit?.fetch;
1929
+ const sseFetch = configuredSseFetch ? withChallengeObserver(configuredSseFetch) : fetchWithOptionalAuthIntercept;
1527
1930
  const trackedFetch = onFetchRequest ? createFetchTracker(sseFetch, {
1528
1931
  trackRequest: onFetchRequest,
1529
1932
  updateResponseBody: onFetchResponseBody
@@ -1701,27 +2104,27 @@ function resolveImportSource(type, platform, homeDir, readFile2) {
1701
2104
  const strategy = getImportStrategy(type);
1702
2105
  if (!strategy) return null;
1703
2106
  const searched = strategy.defaultPaths(platform, homeDir);
1704
- for (const path3 of searched) {
2107
+ for (const path6 of searched) {
1705
2108
  let raw;
1706
2109
  try {
1707
- raw = readFile2(path3);
2110
+ raw = readFile2(path6);
1708
2111
  } catch (err) {
1709
2112
  const msg = err instanceof Error ? err.message : String(err);
1710
2113
  return {
1711
2114
  type,
1712
2115
  found: true,
1713
- path: path3,
1714
- error: `Failed to read ${path3}: ${msg}`,
2116
+ path: path6,
2117
+ error: `Failed to read ${path6}: ${msg}`,
1715
2118
  searched
1716
2119
  };
1717
2120
  }
1718
2121
  if (raw === null) continue;
1719
2122
  try {
1720
2123
  const config = strategy.parse(raw);
1721
- return { type, found: true, path: path3, config, searched };
2124
+ return { type, found: true, path: path6, config, searched };
1722
2125
  } catch (err) {
1723
2126
  const msg = err instanceof Error ? err.message : String(err);
1724
- return { type, found: true, path: path3, error: msg, searched };
2127
+ return { type, found: true, path: path6, error: msg, searched };
1725
2128
  }
1726
2129
  }
1727
2130
  return { type, found: false, searched };
@@ -1845,7 +2248,7 @@ var RemoteSession = class _RemoteSession {
1845
2248
  * `auth_challenge` or `ok: true` only after the MCP round-trip (or auth error).
1846
2249
  */
1847
2250
  waitForRequestResponse(requestId, timeoutMs = 6e4) {
1848
- return new Promise((resolve6, reject) => {
2251
+ return new Promise((resolve9, reject) => {
1849
2252
  const timer = timeoutMs > 0 ? setTimeout(() => {
1850
2253
  this.requestWaits.delete(requestId);
1851
2254
  reject(
@@ -1859,7 +2262,7 @@ var RemoteSession = class _RemoteSession {
1859
2262
  if (timer !== void 0) {
1860
2263
  clearTimeout(timer);
1861
2264
  }
1862
- resolve6();
2265
+ resolve9();
1863
2266
  },
1864
2267
  reject: (error) => {
1865
2268
  if (timer !== void 0) {
@@ -2037,132 +2440,1374 @@ function createRemoteAuthProvider(initialAuthState) {
2037
2440
  getAuthState() {
2038
2441
  return cloneAuthState(authState);
2039
2442
  }
2040
- };
2443
+ };
2444
+ }
2445
+
2446
+ // ../../core/auth/node/secret-store.ts
2447
+ var SERVICE_NAME = "mcp-inspector";
2448
+ var keyringLoad;
2449
+ var KeyringModuleShapeError = class extends Error {
2450
+ constructor(detail, options) {
2451
+ super(`@napi-rs/keyring loaded but ${detail}`, options);
2452
+ this.name = "KeyringModuleShapeError";
2453
+ }
2454
+ };
2455
+ var checkKeyringShape = (mod) => {
2456
+ try {
2457
+ if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
2458
+ return { ok: true, mod };
2459
+ }
2460
+ return {
2461
+ ok: false,
2462
+ err: new KeyringModuleShapeError(
2463
+ "did not expose AsyncEntry / findCredentialsAsync"
2464
+ )
2465
+ };
2466
+ } catch (err) {
2467
+ return {
2468
+ ok: false,
2469
+ err: new KeyringModuleShapeError(
2470
+ `its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
2471
+ { cause: err }
2472
+ )
2473
+ };
2474
+ }
2475
+ };
2476
+ var loadKeyring = () => {
2477
+ keyringLoad ??= import("@napi-rs/keyring").then(
2478
+ checkKeyringShape,
2479
+ (err) => ({ ok: false, err })
2480
+ );
2481
+ return keyringLoad;
2482
+ };
2483
+ function parseAccount(account) {
2484
+ const idx = account.indexOf(":");
2485
+ if (idx <= 0 || idx === account.length - 1) return null;
2486
+ return {
2487
+ serverId: account.slice(0, idx),
2488
+ field: account.slice(idx + 1)
2489
+ };
2490
+ }
2491
+ var buildAccount = (serverId, field) => `${serverId}:${field}`;
2492
+ var SecretStoreUnavailableError = class extends Error {
2493
+ constructor(message, options) {
2494
+ super(message, options);
2495
+ this.name = "SecretStoreUnavailableError";
2496
+ }
2497
+ };
2498
+ var KeychainUnavailableError = class extends SecretStoreUnavailableError {
2499
+ constructor(cause) {
2500
+ const message = cause instanceof Error ? cause.message : String(cause);
2501
+ super(
2502
+ `OS keychain is not available. ${hintFor(cause, message)} Underlying error: ${message}`
2503
+ );
2504
+ this.name = "KeychainUnavailableError";
2505
+ }
2506
+ };
2507
+ var hintFor = (cause, message) => {
2508
+ if (cause instanceof KeyringModuleShapeError) {
2509
+ return `The @napi-rs/keyring package loaded but does not expose the API this build expects \u2014 most likely a version or packaging mismatch; reinstall the Inspector, and report this if it persists.`;
2510
+ }
2511
+ if (message.includes("Cannot find native binding")) {
2512
+ return `The @napi-rs/keyring platform package for this OS is missing or unavailable \u2014 reinstall the Inspector (for npx, clear the npx cache under your npm cache directory first).`;
2513
+ }
2514
+ return `On Linux, install libsecret / gnome-keyring.`;
2515
+ };
2516
+ async function probeKeyringAvailable() {
2517
+ const keyring = await loadKeyring();
2518
+ if (!keyring.ok) {
2519
+ const err = keyring.err;
2520
+ return {
2521
+ available: false,
2522
+ detail: err instanceof Error ? err.message : String(err)
2523
+ };
2524
+ }
2525
+ try {
2526
+ const entry = new keyring.mod.AsyncEntry(SERVICE_NAME, PROBE_ACCOUNT);
2527
+ await entry.getPassword();
2528
+ return { available: true };
2529
+ } catch (err) {
2530
+ return {
2531
+ available: false,
2532
+ detail: err instanceof Error ? err.message : String(err)
2533
+ };
2534
+ }
2535
+ }
2536
+ var PROBE_ACCOUNT = "__inspector:probe";
2537
+ var KeyringSecretStore = class {
2538
+ /**
2539
+ * The intolerant read. Same lookup as {@link get}, minus the catch — a
2540
+ * keychain that cannot answer must not be reported as an empty one to a
2541
+ * caller that is about to write based on the answer.
2542
+ */
2543
+ async getStrict(serverId, field) {
2544
+ try {
2545
+ const keyring = await loadKeyring();
2546
+ if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
2547
+ const entry = new keyring.mod.AsyncEntry(
2548
+ SERVICE_NAME,
2549
+ buildAccount(serverId, field)
2550
+ );
2551
+ return await entry.getPassword() ?? null;
2552
+ } catch (err) {
2553
+ if (err instanceof KeychainUnavailableError) throw err;
2554
+ throw new KeychainUnavailableError(err);
2555
+ }
2556
+ }
2557
+ async get(serverId, field) {
2558
+ try {
2559
+ const keyring = await loadKeyring();
2560
+ if (!keyring.ok) return null;
2561
+ const entry = new keyring.mod.AsyncEntry(
2562
+ SERVICE_NAME,
2563
+ buildAccount(serverId, field)
2564
+ );
2565
+ const v = await entry.getPassword();
2566
+ return v ?? null;
2567
+ } catch {
2568
+ return null;
2569
+ }
2570
+ }
2571
+ async set(serverId, field, value) {
2572
+ try {
2573
+ const keyring = await loadKeyring();
2574
+ if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
2575
+ const entry = new keyring.mod.AsyncEntry(
2576
+ SERVICE_NAME,
2577
+ buildAccount(serverId, field)
2578
+ );
2579
+ await entry.setPassword(value);
2580
+ } catch (err) {
2581
+ if (err instanceof KeychainUnavailableError) throw err;
2582
+ throw new KeychainUnavailableError(err);
2583
+ }
2584
+ }
2585
+ async delete(serverId, field) {
2586
+ try {
2587
+ const keyring = await loadKeyring();
2588
+ if (!keyring.ok) return;
2589
+ const entry = new keyring.mod.AsyncEntry(
2590
+ SERVICE_NAME,
2591
+ buildAccount(serverId, field)
2592
+ );
2593
+ await entry.deleteCredential();
2594
+ } catch {
2595
+ }
2596
+ }
2597
+ async deleteAllForServer(serverId) {
2598
+ let creds;
2599
+ try {
2600
+ const keyring = await loadKeyring();
2601
+ if (!keyring.ok) return;
2602
+ creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
2603
+ } catch {
2604
+ return;
2605
+ }
2606
+ const prefix = `${serverId}:`;
2607
+ for (const c of creds) {
2608
+ if (!c.account.startsWith(prefix)) continue;
2609
+ const parsed = parseAccount(c.account);
2610
+ if (!parsed || parsed.serverId !== serverId) continue;
2611
+ await this.delete(serverId, parsed.field);
2612
+ }
2613
+ }
2614
+ };
2615
+ async function secretStoreGetStrict(store, serverId, field) {
2616
+ return store.getStrict ? store.getStrict(serverId, field) : store.get(serverId, field);
2617
+ }
2618
+ async function secretStoreGetMany(store, requests) {
2619
+ if (store.getMany) return store.getMany(requests);
2620
+ const out = {};
2621
+ await Promise.all(
2622
+ requests.map(async ({ serverId, fields }) => {
2623
+ const entries = await Promise.all(
2624
+ fields.map(
2625
+ async (field) => [field, await store.get(serverId, field)]
2626
+ )
2627
+ );
2628
+ const found = {};
2629
+ for (const [field, value] of entries) {
2630
+ if (value !== null) found[field] = value;
2631
+ }
2632
+ out[serverId] = found;
2633
+ })
2634
+ );
2635
+ return out;
2636
+ }
2637
+ async function secretStoreSetMany(store, serverId, values) {
2638
+ if (store.setMany) return store.setMany(serverId, values);
2639
+ await Promise.all(
2640
+ Object.entries(values).map(
2641
+ ([field, value]) => store.set(serverId, field, value)
2642
+ )
2643
+ );
2644
+ }
2645
+ async function secretStoreIsDurable(store) {
2646
+ return store.isDurable ? store.isDurable() : true;
2647
+ }
2648
+ var InMemorySecretStore = class {
2649
+ map = /* @__PURE__ */ new Map();
2650
+ async get(serverId, field) {
2651
+ return this.map.get(buildAccount(serverId, field)) ?? null;
2652
+ }
2653
+ async set(serverId, field, value) {
2654
+ this.map.set(buildAccount(serverId, field), value);
2655
+ }
2656
+ async delete(serverId, field) {
2657
+ this.map.delete(buildAccount(serverId, field));
2658
+ }
2659
+ async deleteAllForServer(serverId) {
2660
+ const prefix = `${serverId}:`;
2661
+ for (const key of [...this.map.keys()]) {
2662
+ if (key.startsWith(prefix)) this.map.delete(key);
2663
+ }
2664
+ }
2665
+ };
2666
+ var SessionSecretStore = class extends InMemorySecretStore {
2667
+ async isDurable() {
2668
+ return false;
2669
+ }
2670
+ };
2671
+
2672
+ // ../../core/auth/node/secret-store-selection.ts
2673
+ import { randomUUID } from "crypto";
2674
+ import * as fsSync from "fs";
2675
+ import * as fs4 from "fs/promises";
2676
+ import * as path4 from "path";
2677
+
2678
+ // ../../core/auth/node/file-secret-store.ts
2679
+ import * as crypto2 from "crypto";
2680
+ import * as fs3 from "fs/promises";
2681
+ import * as path3 from "path";
2682
+
2683
+ // ../../core/auth/node/file-lock.ts
2684
+ import nodeFs, { statSync, rmdirSync } from "fs";
2685
+ import * as fs2 from "fs/promises";
2686
+ import * as path2 from "path";
2687
+ import properLockfile from "proper-lockfile";
2688
+ var STALE_MS = 1e4;
2689
+ var RETRY = {
2690
+ retries: 20,
2691
+ factor: 2,
2692
+ minTimeout: 20,
2693
+ maxTimeout: 1e3
2694
+ };
2695
+ var RETRY_BUDGET_MS = (() => {
2696
+ let total = 0;
2697
+ let delay = RETRY.minTimeout;
2698
+ for (let i = 0; i < RETRY.retries; i++) {
2699
+ total += Math.min(delay, RETRY.maxTimeout);
2700
+ delay *= RETRY.factor;
2701
+ }
2702
+ return total;
2703
+ })();
2704
+ var warned = /* @__PURE__ */ new Set();
2705
+ function warnOnce(message) {
2706
+ if (warned.has(message)) return;
2707
+ warned.add(message);
2708
+ console.warn(`[mcp-inspector] ${message}`);
2709
+ }
2710
+ function isStuckOrHeld(err, lockPath) {
2711
+ return isHeldElsewhere(err) || identifySync(lockPath) !== null;
2712
+ }
2713
+ var isHeldElsewhere = (err) => err.code === "ELOCKED";
2714
+ var describeError = (err) => err instanceof Error ? err.message : String(err);
2715
+ var lockPathOf = (target) => `${target}.lock`;
2716
+ function identifySync(lockPath) {
2717
+ try {
2718
+ const stat2 = statSync(lockPath);
2719
+ return { ino: stat2.ino, birthtimeMs: stat2.birthtimeMs };
2720
+ } catch {
2721
+ return null;
2722
+ }
2723
+ }
2724
+ function guardedFs(lockPath, owned, onRefused) {
2725
+ const mine = () => {
2726
+ if (owned.id === null) return true;
2727
+ const now = identifySync(lockPath);
2728
+ if (now === null) return false;
2729
+ return now.ino === owned.id.ino && now.birthtimeMs === owned.id.birthtimeMs;
2730
+ };
2731
+ const removeIfMine = () => {
2732
+ if (!mine()) {
2733
+ onRefused();
2734
+ return;
2735
+ }
2736
+ rmdirSync(lockPath);
2737
+ };
2738
+ return {
2739
+ ...nodeFs,
2740
+ // **Identity is captured here, not after `lock()` resolves.** `mkdir` is
2741
+ // the moment the directory becomes ours, and it is synchronous with
2742
+ // respect to this process: recording it in the callback, before yielding,
2743
+ // leaves no window. Capturing after the acquire promise settled — as this
2744
+ // did originally — spans the library's own `utimes`/`stat` probe, and a
2745
+ // waiter replacing our directory inside that span would have us record
2746
+ // *the winner's* identity as our own, after which the guard below would
2747
+ // cheerfully delete their lock.
2748
+ mkdir: (p, cb) => {
2749
+ nodeFs.mkdir(p, (err) => {
2750
+ if (!err) owned.id = identifySync(lockPath);
2751
+ cb(err);
2752
+ });
2753
+ },
2754
+ // Reported as success when refused: the library's bookkeeping should
2755
+ // forget this lock either way. Leaving it registered would hand the
2756
+ // `signal-exit` handler a record pointing at the winner's directory.
2757
+ rmdir: (_p, cb) => {
2758
+ try {
2759
+ removeIfMine();
2760
+ cb(null);
2761
+ } catch (err) {
2762
+ cb(err);
2763
+ }
2764
+ },
2765
+ /* v8 ignore next 3 -- @preserve: only reachable from proper-lockfile's
2766
+ signal-exit handler, i.e. at real process exit, which no in-process
2767
+ test can drive. Its logic is `removeIfMine`, covered via `rmdir`. */
2768
+ rmdirSync: () => {
2769
+ removeIfMine();
2770
+ }
2771
+ };
2772
+ }
2773
+ function acquire(target, retries, fsShim) {
2774
+ return properLockfile.lock(target, {
2775
+ realpath: false,
2776
+ stale: STALE_MS,
2777
+ retries,
2778
+ // Every directory removal the library performs — on release and from its
2779
+ // exit handler — routes through here. See `guardedFs`.
2780
+ fs: fsShim,
2781
+ // The library's default `onCompromised` *throws* — from a timer, with no
2782
+ // caller on the stack, so it lands as an uncaught exception and takes the
2783
+ // process down. This is also the library's *only* signal for the
2784
+ // stale-takeover race described at the top of this file — someone
2785
+ // declared our lock stale and replaced it — so it is the one place a user
2786
+ // learns the guarantee was lost. Worth saying; not worth killing an
2787
+ // Inspector session over.
2788
+ onCompromised: (err) => warnOnce(
2789
+ `The lock on the secrets file at ${target} was taken over by another process while a write was in progress (${err.message}). If a secret you just saved is missing, save it again.`
2790
+ )
2791
+ });
2792
+ }
2793
+ async function isFileLockHeld(filePath) {
2794
+ try {
2795
+ return await properLockfile.check(path2.resolve(filePath), {
2796
+ realpath: false,
2797
+ stale: STALE_MS
2798
+ });
2799
+ } catch {
2800
+ return false;
2801
+ }
2802
+ }
2803
+ async function openSecretFileLock(filePath) {
2804
+ const target = path2.resolve(filePath);
2805
+ await fs2.mkdir(path2.dirname(target), { recursive: true }).catch(() => {
2806
+ });
2807
+ const owned = { id: null };
2808
+ const fsShim = guardedFs(
2809
+ lockPathOf(target),
2810
+ owned,
2811
+ () => warnOnce(
2812
+ `The lock on the secrets file at ${target} was taken over by another process while a write was in progress, so it was left alone rather than removed. If a secret you just saved is missing, save it again.`
2813
+ )
2814
+ );
2815
+ let release;
2816
+ try {
2817
+ release = await acquire(target, 0, fsShim).catch((err) => {
2818
+ if (!isStuckOrHeld(err, lockPathOf(target))) throw err;
2819
+ return acquire(target, RETRY, fsShim);
2820
+ });
2821
+ } catch (err) {
2822
+ if (isStuckOrHeld(err, lockPathOf(target))) {
2823
+ throw new SecretStoreUnavailableError(
2824
+ `Could not save to the secrets file at ${target}: its lock (${lockPathOf(target)}) was still held after the ${Math.round(RETRY_BUDGET_MS / 1e3)} seconds this save waited. Its secrets are intact; the value you just entered was not saved. If no other Inspector is running, remove that lock and try again.`
2825
+ );
2826
+ }
2827
+ warnOnce(
2828
+ `Could not take a lock on the secrets file at ${target} (${describeError(err)}), so writes to it are not protected against another process writing at the same moment.`
2829
+ );
2830
+ return null;
2831
+ }
2832
+ return async () => {
2833
+ try {
2834
+ await release();
2835
+ } catch (err) {
2836
+ if (err.code === "ERELEASED") return;
2837
+ warnOnce(
2838
+ // **Conditional, not an instruction.** Two blanket versions of this
2839
+ // have now been wrong in opposite directions: "it expires on its own"
2840
+ // (false whenever the same `rmdir` that blocked us also blocks stale
2841
+ // takeover — `ENOTEMPTY`, `EACCES`, `EPERM`, `EROFS`), and "remove it
2842
+ // by hand" (false whenever the failure was transient, since the path
2843
+ // may by then hold a *different, live* Inspector's lock, and deleting
2844
+ // that destroys the exclusion of a process that did nothing wrong).
2845
+ //
2846
+ // `proper-lockfile` forwards whatever the filesystem returned, and
2847
+ // nothing here can tell a permanent refusal from a passing one. So
2848
+ // this reports what happened and states the two conditions the
2849
+ // operator can check for themselves, rather than asserting an outcome
2850
+ // this code does not know.
2851
+ `Could not release the lock on the secrets file at ${target} (${describeError(err)}). It may clear on its own \u2014 stale takeover reclaims a lock through the same directory removal, so it will not if whatever blocked this persists. If saves keep failing against this file and no other Inspector is running, remove ${lockPathOf(target)} by hand.`
2852
+ );
2853
+ }
2854
+ };
2855
+ }
2856
+ async function withSecretFileLock(filePath, fn) {
2857
+ const release = await openSecretFileLock(filePath);
2858
+ if (release === null) return fn();
2859
+ try {
2860
+ return await fn();
2861
+ } finally {
2862
+ await release();
2863
+ }
2864
+ }
2865
+
2866
+ // ../../core/auth/node/file-secret-store.ts
2867
+ var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve9, reject) => {
2868
+ crypto2.scrypt(passphrase, salt, keylen, options, (err, key) => {
2869
+ if (err) reject(err);
2870
+ else resolve9(key);
2871
+ });
2872
+ });
2873
+ var FORMAT_VERSION = 1;
2874
+ var CIPHER = "aes-256-gcm";
2875
+ var KEY_BYTES = 32;
2876
+ var IV_BYTES = 12;
2877
+ var GCM_TAG_BYTES = 16;
2878
+ var SCRYPT_N = 16384;
2879
+ var SCRYPT_R = 8;
2880
+ var SCRYPT_P = 1;
2881
+ var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
2882
+ var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
2883
+ function asSecretMap(value, filePath) {
2884
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2885
+ throw new SecretStoreUnavailableError(
2886
+ `The secrets file at ${filePath} does not hold a secret map (found ${Array.isArray(value) ? "an array" : typeof value}). Refusing to write, which would replace it. Move the file aside to start over.`
2887
+ );
2888
+ }
2889
+ for (const [key, entry] of Object.entries(value)) {
2890
+ if (typeof entry !== "string") {
2891
+ throw new SecretStoreUnavailableError(
2892
+ `The secrets file at ${filePath} holds a non-string value for "${key}". Refusing to write, which would replace it. Move the file aside to start over.`
2893
+ );
2894
+ }
2895
+ }
2896
+ return { ...value };
2897
+ }
2898
+ var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
2899
+ constructor(filePath, hasKey) {
2900
+ super(
2901
+ hasKey ? `The secrets file at ${filePath} could not be decrypted with the current ${SECRET_KEY_ENV}. Refusing to write, which would overwrite the existing secrets. Restore the original passphrase, or delete the file to start over.` : `The secrets file at ${filePath} is encrypted but ${SECRET_KEY_ENV} is not set. Refusing to write, which would overwrite the existing secrets. Set the passphrase this file was written with, or delete the file to start over.`
2902
+ );
2903
+ this.name = "SecretFileKeyMismatchError";
2904
+ }
2905
+ };
2906
+ var encodeParts = (iv, tag, body) => `${iv.toString("base64")}.${tag.toString("base64")}.${body.toString("base64")}`;
2907
+ function decodeParts(payload) {
2908
+ const parts = payload.split(".");
2909
+ if (parts.length !== 3) return null;
2910
+ const [iv, tag, body] = parts;
2911
+ return {
2912
+ iv: Buffer.from(iv, "base64"),
2913
+ tag: Buffer.from(tag, "base64"),
2914
+ body: Buffer.from(body, "base64")
2915
+ };
2916
+ }
2917
+ var MAX_WRITE_ATTEMPTS = 5;
2918
+ var fileQueues = /* @__PURE__ */ new Map();
2919
+ function sameMap(a, b) {
2920
+ const ak = Object.keys(a);
2921
+ if (ak.length !== Object.keys(b).length) return false;
2922
+ return ak.every(
2923
+ (k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]
2924
+ );
2925
+ }
2926
+ var isPositiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
2927
+ function encryptedEnvelopeProblem(parsed) {
2928
+ if (!parsed.kdf || typeof parsed.data !== "string") {
2929
+ return "encrypted envelope is missing its kdf or data";
2930
+ }
2931
+ const parts = decodeParts(parsed.data);
2932
+ if (!parts) return "encrypted payload is not iv.tag.ciphertext";
2933
+ if (parts.iv.length !== IV_BYTES) {
2934
+ return `initialization vector is ${parts.iv.length} bytes, expected ${IV_BYTES}`;
2935
+ }
2936
+ if (parts.tag.length !== GCM_TAG_BYTES) {
2937
+ return `authentication tag is ${parts.tag.length} bytes, expected ${GCM_TAG_BYTES}`;
2938
+ }
2939
+ if (parts.body.length === 0) return "ciphertext is empty";
2940
+ const { algorithm, salt, N, r, p } = parsed.kdf;
2941
+ if (algorithm !== "scrypt") return `unsupported kdf "${String(algorithm)}"`;
2942
+ if (typeof salt !== "string" || Buffer.from(salt, "base64").length === 0) {
2943
+ return "kdf salt is missing or not base64";
2944
+ }
2945
+ if (!isPositiveInt(N) || N < 2 || (N & N - 1) !== 0) {
2946
+ return `kdf N is ${String(N)}, which is not a power of two above 1`;
2947
+ }
2948
+ if (!isPositiveInt(r) || !isPositiveInt(p)) {
2949
+ return `kdf r/p are ${String(r)}/${String(p)}, which are not positive integers`;
2950
+ }
2951
+ if (N > SCRYPT_N || r > SCRYPT_R || p > SCRYPT_P) {
2952
+ return `kdf cost parameters (N=${N}, r=${r}, p=${p}) exceed the supported maximum (N=${SCRYPT_N}, r=${SCRYPT_R}, p=${SCRYPT_P})`;
2953
+ }
2954
+ return null;
2955
+ }
2956
+ var FileSecretStore = class {
2957
+ filePath;
2958
+ passphrase;
2959
+ constructor(options) {
2960
+ this.filePath = options.filePath;
2961
+ const raw = options.passphrase ?? process.env[SECRET_KEY_ENV];
2962
+ this.passphrase = raw && raw.trim() ? raw : void 0;
2963
+ }
2964
+ /**
2965
+ * True when the *next write* will encrypt. This is the configured policy,
2966
+ * not the state of the file — see {@link readOnDiskEncryption}, which is
2967
+ * what the user-facing descriptor is built from.
2968
+ */
2969
+ get encrypted() {
2970
+ return this.passphrase !== void 0;
2971
+ }
2972
+ /**
2973
+ * What the file on disk is *currently* written as — `null` when there is
2974
+ * no file yet, or when it cannot be read or parsed.
2975
+ *
2976
+ * Separate from {@link encrypted} because the two genuinely disagree for a
2977
+ * whole session: adding `MCP_INSPECTOR_SECRET_KEY` to an install that
2978
+ * already has a plaintext file flips `encrypted` to true immediately,
2979
+ * while the bytes stay readable until the next `set`. A descriptor built
2980
+ * from the policy would tell that user "File (encrypted)" while their
2981
+ * existing secrets sat in the clear — the precise reassurance this whole
2982
+ * subsystem exists not to give.
2983
+ *
2984
+ * Reads the envelope, and for an encrypted one **attempts to open it**.
2985
+ * Structure alone is not access: a well-formed file this passphrase cannot
2986
+ * decrypt answers `null` from every `get` and refuses every `set`, so
2987
+ * reporting it as a healthy "encrypted" store hands the quiet footer to
2988
+ * exactly the state the unreadable one exists to warn about. The
2989
+ * authentication costs one scrypt derivation per call, which is once per
2990
+ * `/api/config`, and the decrypted bytes are discarded.
2991
+ *
2992
+ * `absent` and `unreadable` are kept apart deliberately. Collapsing them
2993
+ * (as a bare `null` did) means an existing file that is corrupt, or
2994
+ * written in an envelope this build does not know, is reported as "no
2995
+ * file yet" — and the descriptor then falls back to the configured write
2996
+ * policy, so a passphrase-configured install claims "Encrypted file"
2997
+ * about a file whose encryption state was never established and which
2998
+ * `set` is in fact about to refuse.
2999
+ */
3000
+ async readOnDiskEncryption() {
3001
+ let raw;
3002
+ try {
3003
+ raw = await readStoreFile(this.filePath);
3004
+ } catch (err) {
3005
+ return {
3006
+ state: "unreadable",
3007
+ detail: err instanceof Error ? err.message : String(err)
3008
+ };
3009
+ }
3010
+ if (raw === null) return { state: "absent" };
3011
+ try {
3012
+ const parsed = JSON.parse(raw);
3013
+ if (parsed.version !== FORMAT_VERSION) {
3014
+ return {
3015
+ state: "unreadable",
3016
+ detail: `format version ${String(parsed.version)}, expected ${FORMAT_VERSION}`
3017
+ };
3018
+ }
3019
+ if (parsed.encryption === "none") {
3020
+ try {
3021
+ asSecretMap(parsed.secrets ?? {}, this.filePath);
3022
+ } catch (err) {
3023
+ return {
3024
+ state: "unreadable",
3025
+ detail: err instanceof Error ? err.message : String(err)
3026
+ };
3027
+ }
3028
+ return { state: "plaintext" };
3029
+ }
3030
+ if (parsed.encryption === CIPHER) {
3031
+ const problem = encryptedEnvelopeProblem(parsed);
3032
+ if (problem) return { state: "unreadable", detail: problem };
3033
+ try {
3034
+ await this.readMap();
3035
+ } catch (err) {
3036
+ return {
3037
+ state: "unreadable",
3038
+ detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV}` : err instanceof Error ? err.message : String(err)
3039
+ };
3040
+ }
3041
+ return { state: "encrypted" };
3042
+ }
3043
+ return {
3044
+ state: "unreadable",
3045
+ detail: `unsupported envelope (encryption="${String(parsed.encryption)}", version=${String(parsed.version)})`
3046
+ };
3047
+ } catch {
3048
+ return { state: "unreadable", detail: "not valid JSON" };
3049
+ }
3050
+ }
3051
+ async deriveKey(kdf) {
3052
+ return scrypt2(
3053
+ // Only reached when the passphrase is set — every caller checks first,
3054
+ // which is what makes the non-null assertion safe here rather than a
3055
+ // widened `string | undefined` threaded through the crypto helpers.
3056
+ this.passphrase,
3057
+ Buffer.from(kdf.salt, "base64"),
3058
+ KEY_BYTES,
3059
+ { N: kdf.N, r: kdf.r, p: kdf.p }
3060
+ );
3061
+ }
3062
+ /**
3063
+ * Read and decode the file into a plain map.
3064
+ *
3065
+ * Distinguishes three outcomes that callers treat differently: `null` for
3066
+ * "no file yet" (a first write should create one), a map for success, and a
3067
+ * throw for "there is a file and we cannot read it" — which must never be
3068
+ * flattened into an empty map, or `set` would cheerfully overwrite it.
3069
+ */
3070
+ /**
3071
+ * Every secret in the file, keyed by `${serverId}:${field}` — `null` when
3072
+ * there is no file.
3073
+ *
3074
+ * Exists for one caller: the hand-off performed when a keychain becomes
3075
+ * available on a box that had been falling back to a file (see
3076
+ * `absorbFileSecretsIntoKeyring`). Nothing in the normal request path
3077
+ * enumerates secrets, and deliberately so — this is the widest possible
3078
+ * read of the store and it should stay a one-purpose seam rather than a
3079
+ * general convenience.
3080
+ *
3081
+ * Throws for the same reasons `get` swallows: an unreadable or
3082
+ * undecryptable file must not present as "no secrets", because the caller
3083
+ * would conclude there was nothing to migrate and move on.
3084
+ */
3085
+ async readAll() {
3086
+ return this.serialize(() => this.readMap());
3087
+ }
3088
+ async readMap() {
3089
+ const raw = await readStoreFile(this.filePath);
3090
+ if (raw === null) return null;
3091
+ let parsed;
3092
+ try {
3093
+ parsed = JSON.parse(raw);
3094
+ } catch (err) {
3095
+ throw new SecretStoreUnavailableError(
3096
+ `The secrets file at ${this.filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
3097
+ );
3098
+ }
3099
+ if (parsed.version !== FORMAT_VERSION) {
3100
+ throw new SecretStoreUnavailableError(
3101
+ `The secrets file at ${this.filePath} declares format version ${String(parsed.version)}, but this Inspector only understands version ${FORMAT_VERSION}. It was probably written by a newer Inspector; upgrade, or move the file aside to start over.`
3102
+ );
3103
+ }
3104
+ if (parsed.encryption === "none") {
3105
+ return asSecretMap(parsed.secrets ?? {}, this.filePath);
3106
+ }
3107
+ if (parsed.encryption !== CIPHER || !parsed.kdf || !parsed.data) {
3108
+ throw new SecretStoreUnavailableError(
3109
+ `The secrets file at ${this.filePath} declares an unsupported format (encryption="${String(parsed.encryption)}", version=${String(parsed.version)}). It may have been written by a newer Inspector.`
3110
+ );
3111
+ }
3112
+ if (!this.passphrase)
3113
+ throw new SecretFileKeyMismatchError(this.filePath, false);
3114
+ const structural = encryptedEnvelopeProblem(parsed);
3115
+ if (structural) {
3116
+ throw new SecretStoreUnavailableError(
3117
+ `The secrets file at ${this.filePath} is not readable: ${structural}. Restoring a passphrase will not repair it; the file itself is malformed.`
3118
+ );
3119
+ }
3120
+ const parts = decodeParts(parsed.data);
3121
+ if (!parts) throw new SecretFileKeyMismatchError(this.filePath, true);
3122
+ let plain;
3123
+ try {
3124
+ const key = await this.deriveKey(parsed.kdf);
3125
+ const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv);
3126
+ decipher.setAuthTag(parts.tag);
3127
+ plain = Buffer.concat([
3128
+ decipher.update(parts.body),
3129
+ decipher.final()
3130
+ ]).toString("utf-8");
3131
+ } catch {
3132
+ throw new SecretFileKeyMismatchError(this.filePath, true);
3133
+ }
3134
+ let payload;
3135
+ try {
3136
+ payload = JSON.parse(plain);
3137
+ } catch (err) {
3138
+ throw new SecretStoreUnavailableError(
3139
+ `The secrets file at ${this.filePath} decrypted successfully but its contents are not valid JSON: ${err instanceof Error ? err.message : String(err)}. The passphrase is correct; the file itself is corrupt.`
3140
+ );
3141
+ }
3142
+ return asSecretMap(payload, this.filePath);
3143
+ }
3144
+ async writeMap(map) {
3145
+ let file;
3146
+ if (this.passphrase) {
3147
+ const kdf = {
3148
+ algorithm: "scrypt",
3149
+ // A fresh salt per write, not per file: rewriting with the same salt
3150
+ // and a fresh IV would still be safe for GCM, but re-salting costs one
3151
+ // scrypt derivation we were doing anyway and removes the question.
3152
+ salt: crypto2.randomBytes(16).toString("base64"),
3153
+ N: SCRYPT_N,
3154
+ r: SCRYPT_R,
3155
+ p: SCRYPT_P
3156
+ };
3157
+ const key = await this.deriveKey(kdf);
3158
+ const iv = crypto2.randomBytes(IV_BYTES);
3159
+ const cipher = crypto2.createCipheriv(CIPHER, key, iv);
3160
+ const body = Buffer.concat([
3161
+ cipher.update(JSON.stringify(map), "utf-8"),
3162
+ cipher.final()
3163
+ ]);
3164
+ file = {
3165
+ version: FORMAT_VERSION,
3166
+ encryption: CIPHER,
3167
+ kdf,
3168
+ data: encodeParts(iv, cipher.getAuthTag(), body)
3169
+ };
3170
+ } else {
3171
+ file = { version: FORMAT_VERSION, encryption: "none", secrets: map };
3172
+ }
3173
+ await writeStoreFile(this.filePath, `${JSON.stringify(file, null, 2)}
3174
+ `);
3175
+ }
3176
+ /**
3177
+ * Apply a mutation to the file, under a cross-process lock, and confirm it
3178
+ * survived.
3179
+ *
3180
+ * **Two mechanisms, and they answer different questions** — this is not
3181
+ * belt-and-braces.
3182
+ *
3183
+ * The lock ({@link withSecretFileLock}, #2082) is the one that provides
3184
+ * mutual exclusion: a durable secrets file is shared state, and a second
3185
+ * Inspector on the same box — a CLI run beside a web session is the
3186
+ * ordinary case, not a contrived one — otherwise reads the same "before"
3187
+ * map and then atomically replaces the file, dropping whatever the first
3188
+ * process had just added. Both writes report success; one secret is simply
3189
+ * gone. Held across the read *and* the write, so no other holder can
3190
+ * observe or replace the map in between.
3191
+ *
3192
+ * The verify covers what a lock structurally cannot. A lock is an advisory
3193
+ * convention between the processes that take it, so it says nothing about
3194
+ * a writer that does not: an editor, a restored backup, a `jq` one-liner,
3195
+ * or an Inspector predating #2082. It is also the fallback when the lock
3196
+ * is unavailable at all — `withSecretFileLock` runs the body anyway on a
3197
+ * read-only or lock-hostile directory rather than failing a `set` on
3198
+ * exactly the deployments this store exists for (#1848, #1905).
3199
+ *
3200
+ * So: read `M0`, apply the mutation to get `M1`, write it, then read back
3201
+ * `M2`. If `M2` equals `M1` nothing interleaved. If it does not, someone
3202
+ * wrote between our write and our read — so re-apply onto what they left
3203
+ * and try again.
3204
+ *
3205
+ * The comparison is over the **whole map**, not just the entry we touched.
3206
+ * Checking only our own key would pass in precisely the case that loses
3207
+ * data: our entry is present, and it is the *other* writer's that is gone.
3208
+ * When the verify does fire, the writer that was clobbered is the one that
3209
+ * repairs it — A writes `MA`, B writes `MB` over it, B verifies `MB`
3210
+ * correctly, A verifies, sees `M2 !== MA`, re-applies onto `MB`.
3211
+ *
3212
+ * **What is left, stated without overselling.** Against an unlocked writer
3213
+ * the verify is not mutual exclusion: it only catches a clobber that has
3214
+ * already landed, so ordering the two as write-A, verify-A, write-B,
3215
+ * verify-B loses A's entry with both reporting success. That residual is
3216
+ * now confined to a writer outside this codebase, or to a directory where
3217
+ * no lock could be taken — and the latter announces itself, once, on the
3218
+ * console.
3219
+ *
3220
+ * Reads deliberately do not participate: `writeStoreFile` is atomic
3221
+ * (write-temp-then-rename), so a reader sees either the old file or the
3222
+ * new one, never a torn one. Taking the lock for them would serialize
3223
+ * every `GET /api/servers` behind whatever else holds it, to prevent
3224
+ * nothing.
3225
+ */
3226
+ async mutate(apply) {
3227
+ await withSecretFileLock(this.filePath, () => this.mutateLocked(apply));
3228
+ }
3229
+ /** {@link mutate}'s body, with the lock already held. */
3230
+ async mutateLocked(apply) {
3231
+ for (let attempt = 0; attempt < MAX_WRITE_ATTEMPTS; attempt++) {
3232
+ let current;
3233
+ try {
3234
+ current = await this.readMap() ?? {};
3235
+ } catch (err) {
3236
+ if (err instanceof SecretStoreUnavailableError) throw err;
3237
+ throw new SecretStoreUnavailableError(
3238
+ `Could not read the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
3239
+ );
3240
+ }
3241
+ const next = apply(current);
3242
+ if (next === null) return;
3243
+ try {
3244
+ await this.writeMap(next);
3245
+ } catch (err) {
3246
+ throw new SecretStoreUnavailableError(
3247
+ `Could not write the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
3248
+ );
3249
+ }
3250
+ let observed;
3251
+ try {
3252
+ observed = await this.readMap() ?? {};
3253
+ } catch {
3254
+ continue;
3255
+ }
3256
+ if (sameMap(next, observed)) return;
3257
+ }
3258
+ throw new SecretStoreUnavailableError(
3259
+ `Could not write the secrets file at ${this.filePath}: another process kept overwriting it (gave up after ${MAX_WRITE_ATTEMPTS} attempts). Its secrets are intact; the value you just entered was not saved.`
3260
+ );
3261
+ }
3262
+ /**
3263
+ * Run `fn` with the file to itself, so concurrent callers can't lose
3264
+ * writes.
3265
+ *
3266
+ * Keyed on the **resolved path, process-wide** rather than on this
3267
+ * instance. Two `FileSecretStore`s pointing at one file are ordinary —
3268
+ * the resolved store holds one and `absorbFileSecretsIntoKeyring`
3269
+ * constructs another — and a per-instance queue does not order them
3270
+ * against each other at all. They then race in-process, which the
3271
+ * optimistic verify is only *supposed* to catch for writers in different
3272
+ * processes: both read the same map, both write, and whichever verifies
3273
+ * before the other's write lands sees its own value and returns happy.
3274
+ * Measured, not theorised — two instances racing `set` lost an entry on
3275
+ * the first run of the convergence test below.
3276
+ *
3277
+ * So the in-process case is made correct by construction here, and
3278
+ * {@link mutate}'s cross-process lock covers what this cannot see: a
3279
+ * second Inspector process.
3280
+ *
3281
+ * The queue is *also* what keeps that lock usable. `proper-lockfile` is
3282
+ * not reentrant — a second `lock()` on a path this process already holds
3283
+ * fails `ELOCKED`, which is indistinguishable from a genuine remote
3284
+ * holder. Serializing here means the lock is only ever contended between
3285
+ * processes, which is the only contention it is asked to arbitrate.
3286
+ */
3287
+ serialize(fn) {
3288
+ const key = path3.resolve(this.filePath);
3289
+ const run = (fileQueues.get(key) ?? Promise.resolve()).then(fn);
3290
+ const settled = run.then(
3291
+ () => {
3292
+ },
3293
+ () => {
3294
+ }
3295
+ );
3296
+ fileQueues.set(key, settled);
3297
+ void settled.then(() => {
3298
+ if (fileQueues.get(key) === settled) fileQueues.delete(key);
3299
+ });
3300
+ return run;
3301
+ }
3302
+ /**
3303
+ * One read and one derivation for an entire rehydration.
3304
+ *
3305
+ * `get` reads and decrypts the whole file, so the cost is per *call*, not
3306
+ * per field — which is why the batch has to span servers. A per-server
3307
+ * version of this shipped first and left a 20-server catalog paying 20
3308
+ * serialized derivations, the same stall it was written to remove.
3309
+ * Tolerant like `get`: an unreadable store yields no fields for anyone
3310
+ * rather than throwing.
3311
+ */
3312
+ async getMany(requests) {
3313
+ let map;
3314
+ try {
3315
+ map = await this.serialize(() => this.readMap());
3316
+ } catch {
3317
+ map = null;
3318
+ }
3319
+ const out = {};
3320
+ for (const { serverId, fields } of requests) {
3321
+ const found = {};
3322
+ for (const field of fields) {
3323
+ const value = map?.[buildAccount2(serverId, field)];
3324
+ if (value !== void 0) found[field] = value;
3325
+ }
3326
+ out[serverId] = found;
3327
+ }
3328
+ return out;
3329
+ }
3330
+ /**
3331
+ * The intolerant read — same lookup as {@link get}, minus the catch.
3332
+ *
3333
+ * Without this, `secretStoreGetStrict` fell back to `get` for the file
3334
+ * store and turned every read failure into `null`. Both plaintext
3335
+ * migrations *write* on `null` and then strip the disk copy, so a
3336
+ * transient read error that cleared before the write let an older
3337
+ * on-disk value replace a newer stored one — the same inversion the
3338
+ * strict seam was introduced to prevent, still open on the store the
3339
+ * seam was introduced for.
3340
+ */
3341
+ async getStrict(serverId, field) {
3342
+ try {
3343
+ const map = await this.serialize(() => this.readMap());
3344
+ return map?.[buildAccount2(serverId, field)] ?? null;
3345
+ } catch (err) {
3346
+ if (err instanceof SecretStoreUnavailableError) throw err;
3347
+ throw new SecretStoreUnavailableError(
3348
+ `Could not read the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
3349
+ );
3350
+ }
3351
+ }
3352
+ async get(serverId, field) {
3353
+ try {
3354
+ const map = await this.serialize(() => this.readMap());
3355
+ return map?.[buildAccount2(serverId, field)] ?? null;
3356
+ } catch {
3357
+ return null;
3358
+ }
3359
+ }
3360
+ /**
3361
+ * One read-modify-write-verify cycle for a whole server's fields.
3362
+ *
3363
+ * `set` pays that cycle — and, encrypted, three scrypt derivations — per
3364
+ * call, serialized behind this store's queue. The settings form resends a
3365
+ * server's full `env` map on any edit, so a 10-variable stdio server spent
3366
+ * ~30 derivations saving an unrelated timeout change.
3367
+ */
3368
+ async setMany(serverId, values) {
3369
+ if (Object.keys(values).length === 0) return;
3370
+ await this.serialize(
3371
+ () => this.mutate((map) => {
3372
+ const next = { ...map };
3373
+ for (const [field, value] of Object.entries(values)) {
3374
+ next[buildAccount2(serverId, field)] = value;
3375
+ }
3376
+ return next;
3377
+ })
3378
+ );
3379
+ }
3380
+ async set(serverId, field, value) {
3381
+ await this.serialize(
3382
+ () => this.mutate((map) => ({
3383
+ ...map,
3384
+ [buildAccount2(serverId, field)]: value
3385
+ }))
3386
+ );
3387
+ }
3388
+ async delete(serverId, field) {
3389
+ await this.deleteWhere((key) => key === buildAccount2(serverId, field));
3390
+ }
3391
+ async deleteAllForServer(serverId) {
3392
+ const prefix = `${serverId}:`;
3393
+ await this.deleteWhere((key) => key.startsWith(prefix));
3394
+ }
3395
+ /**
3396
+ * Shared delete body. Silent on every failure, matching the keyring store:
3397
+ * every reason a delete can fail collapses to "the entry isn't there
3398
+ * anymore", and `set` is the operation that reports a broken store.
3399
+ */
3400
+ async deleteWhere(match) {
3401
+ try {
3402
+ await this.serialize(
3403
+ () => this.mutate((map) => {
3404
+ const keys = Object.keys(map).filter(match);
3405
+ if (keys.length === 0) return null;
3406
+ const next = { ...map };
3407
+ for (const key of keys) delete next[key];
3408
+ return next;
3409
+ })
3410
+ );
3411
+ } catch {
3412
+ }
3413
+ }
3414
+ };
3415
+ async function tightenSecretFilePermissions(filePath) {
3416
+ try {
3417
+ await fs3.chmod(filePath, 384);
3418
+ } catch {
3419
+ }
3420
+ return readSecretFilePermissions(filePath);
3421
+ }
3422
+ async function readSecretFilePermissions(filePath) {
3423
+ try {
3424
+ if (process.platform === "win32") {
3425
+ await fs3.stat(filePath);
3426
+ return { state: "unknown", detail: "not checked on Windows" };
3427
+ }
3428
+ const mode = (await fs3.stat(filePath)).mode & 511;
3429
+ if (mode === 384) return { state: "ok" };
3430
+ return { state: "loose", mode };
3431
+ } catch (err) {
3432
+ const code = err.code;
3433
+ if (code === "ENOENT") {
3434
+ return { state: "absent" };
3435
+ }
3436
+ return { state: "unknown", detail: code ?? "unknown error" };
3437
+ }
2041
3438
  }
2042
3439
 
2043
- // ../../core/auth/node/secret-store.ts
2044
- var SERVICE_NAME = "mcp-inspector";
2045
- var keyringLoad;
2046
- var KeyringModuleShapeError = class extends Error {
2047
- constructor(detail, options) {
2048
- super(`@napi-rs/keyring loaded but ${detail}`, options);
2049
- this.name = "KeyringModuleShapeError";
3440
+ // ../../core/auth/node/secret-store-selection.ts
3441
+ var SECRET_STORE_ENV = "MCP_INSPECTOR_SECRET_STORE";
3442
+ var SECRET_FILE_ENV = "MCP_INSPECTOR_SECRET_FILE";
3443
+ var STORAGE_DIR_ENV = "MCP_STORAGE_DIR";
3444
+ var KINDS = ["keyring", "file", "memory"];
3445
+ function parseSecretStoreEnv(raw) {
3446
+ const value = raw?.trim().toLowerCase();
3447
+ if (!value) return void 0;
3448
+ if (KINDS.includes(value)) return value;
3449
+ console.warn(
3450
+ `[mcp-inspector] Ignoring ${SECRET_STORE_ENV}="${raw}": expected one of ${KINDS.join(", ")}.`
3451
+ );
3452
+ return void 0;
3453
+ }
3454
+ function defaultSecretFilePath() {
3455
+ const override = process.env[SECRET_FILE_ENV]?.trim();
3456
+ if (override) return path4.resolve(override);
3457
+ const storageDir = process.env[STORAGE_DIR_ENV]?.trim();
3458
+ if (storageDir) return path4.resolve(storageDir, "secrets.json");
3459
+ const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
3460
+ return path4.resolve(homeDir, ".mcp-inspector", "secrets.json");
3461
+ }
3462
+ function isContainer() {
3463
+ if (process.env.KUBERNETES_SERVICE_HOST) return true;
3464
+ try {
3465
+ if (fsSync.existsSync("/.dockerenv") || fsSync.existsSync("/run/.containerenv")) {
3466
+ return true;
3467
+ }
3468
+ } catch {
2050
3469
  }
2051
- };
2052
- var checkKeyringShape = (mod) => {
2053
3470
  try {
2054
- if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
2055
- return { ok: true, mod };
3471
+ const cgroup = fsSync.readFileSync("/proc/1/cgroup", "utf-8");
3472
+ return /\b(docker|containerd|kubepods|podman|libpod|lxc)\b/.test(cgroup);
3473
+ } catch {
3474
+ return false;
3475
+ }
3476
+ }
3477
+ function nearestExisting(dir) {
3478
+ let current = path4.resolve(dir);
3479
+ while (!fsSync.existsSync(current)) {
3480
+ const parent = path4.dirname(current);
3481
+ if (parent === current) return null;
3482
+ current = parent;
3483
+ }
3484
+ return current;
3485
+ }
3486
+ function unescapeMountPoint(point) {
3487
+ return point.replace(
3488
+ /\\(\d{3})/g,
3489
+ (_match, octal) => String.fromCharCode(parseInt(octal, 8))
3490
+ );
3491
+ }
3492
+ var EPHEMERAL_FSTYPES = /* @__PURE__ */ new Set(["tmpfs", "ramfs"]);
3493
+ function readMountPoints() {
3494
+ try {
3495
+ return fsSync.readFileSync("/proc/self/mountinfo", "utf-8").split("\n").flatMap((line) => {
3496
+ const point = line.split(" ")[4];
3497
+ if (!point) return [];
3498
+ const after = line.split(" - ")[1];
3499
+ const fstype = after?.trim().split(/\s+/)[0];
3500
+ if (fstype && EPHEMERAL_FSTYPES.has(fstype)) return [];
3501
+ return [unescapeMountPoint(point)];
3502
+ }).filter((point) => point !== "/");
3503
+ } catch {
3504
+ return null;
3505
+ }
3506
+ }
3507
+ function isOnMountPoint(dir) {
3508
+ try {
3509
+ const nearest = nearestExisting(dir);
3510
+ if (nearest === null) return false;
3511
+ let current = nearest;
3512
+ try {
3513
+ current = fsSync.realpathSync(nearest);
3514
+ } catch {
2056
3515
  }
3516
+ const mounts = readMountPoints();
3517
+ if (mounts !== null) {
3518
+ return mounts.some(
3519
+ (point) => current === point || current.startsWith(`${point}/`)
3520
+ );
3521
+ }
3522
+ const parent = path4.dirname(current);
3523
+ if (parent === current) return false;
3524
+ return fsSync.statSync(current).dev !== fsSync.statSync("/").dev;
3525
+ } catch {
3526
+ return false;
3527
+ }
3528
+ }
3529
+ async function buildStore(kind, reason, detail) {
3530
+ if (kind === "keyring") {
2057
3531
  return {
2058
- ok: false,
2059
- err: new KeyringModuleShapeError(
2060
- "did not expose AsyncEntry / findCredentialsAsync"
2061
- )
3532
+ store: new KeyringSecretStore(),
3533
+ info: { kind, reason, durable: true }
2062
3534
  };
2063
- } catch (err) {
3535
+ }
3536
+ if (kind === "memory") {
2064
3537
  return {
2065
- ok: false,
2066
- err: new KeyringModuleShapeError(
2067
- `its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
2068
- { cause: err }
2069
- )
3538
+ store: new SessionSecretStore(),
3539
+ info: { kind, reason, durable: false, detail }
2070
3540
  };
2071
3541
  }
2072
- };
2073
- var loadKeyring = () => {
2074
- keyringLoad ??= import("@napi-rs/keyring").then(
2075
- checkKeyringShape,
2076
- (err) => ({ ok: false, err })
2077
- );
2078
- return keyringLoad;
2079
- };
2080
- function parseAccount(account) {
2081
- const idx = account.indexOf(":");
2082
- if (idx <= 0 || idx === account.length - 1) return null;
3542
+ const filePath = defaultSecretFilePath();
3543
+ const store = new FileSecretStore({ filePath });
3544
+ await tightenSecretFilePermissions(filePath);
2083
3545
  return {
2084
- serverId: account.slice(0, idx),
2085
- field: account.slice(idx + 1)
3546
+ store,
3547
+ info: await describeFileStore(store, filePath, kind, reason, detail)
2086
3548
  };
2087
3549
  }
2088
- var buildAccount = (serverId, field) => `${serverId}:${field}`;
2089
- var KeychainUnavailableError = class extends Error {
2090
- constructor(cause) {
2091
- const message = cause instanceof Error ? cause.message : String(cause);
2092
- super(
2093
- `OS keychain is not available. ${hintFor(cause, message)} Underlying error: ${message}`
3550
+ async function describeFileStore(store, filePath, kind, reason, detail) {
3551
+ const onDisk = await store.readOnDiskEncryption();
3552
+ const unreadable = onDisk.state === "unreadable" ? onDisk.detail : void 0;
3553
+ const plaintext = onDisk.state === "absent" ? !store.encrypted : onDisk.state === "plaintext";
3554
+ const perms = await readSecretFilePermissions(filePath);
3555
+ return {
3556
+ kind,
3557
+ reason,
3558
+ path: filePath,
3559
+ // Omitted entirely when the envelope could not be read, so no consumer
3560
+ // can read a default out of it.
3561
+ ...unreadable === void 0 ? { plaintext } : {},
3562
+ ...unreadable !== void 0 ? { encryptionUnknown: unreadable } : {},
3563
+ ...perms.state === "loose" ? { looseMode: perms.mode } : {},
3564
+ ...perms.state === "unknown" ? { permissionsUnknown: perms.detail } : {},
3565
+ // Only meaningful while the two disagree; omitted otherwise so the
3566
+ // payload does not carry a flag that says nothing.
3567
+ ...unreadable === void 0 && plaintext && store.encrypted ? { pendingEncryption: true } : {},
3568
+ durable: true,
3569
+ detail
3570
+ };
3571
+ }
3572
+ function chooseFallbackKind(opts) {
3573
+ return opts.container && !opts.mounted ? "memory" : "file";
3574
+ }
3575
+ function warnAboutSecretStorage(info) {
3576
+ if (info.reason === "fallback") {
3577
+ console.warn(
3578
+ `
3579
+ [mcp-inspector] The OS keychain is not available, so secrets will be kept in: ${secretStorageSummary(info)}.` + (info.detail ? `
3580
+ [mcp-inspector] Keychain error: ${info.detail}` : "") + `
3581
+ [mcp-inspector] Set ${SECRET_STORE_ENV}=keyring|file|memory to choose explicitly.`
2094
3582
  );
2095
- this.name = "KeychainUnavailableError";
2096
- }
2097
- };
2098
- var hintFor = (cause, message) => {
2099
- if (cause instanceof KeyringModuleShapeError) {
2100
- return `The @napi-rs/keyring package loaded but does not expose the API this build expects \u2014 most likely a version or packaging mismatch; reinstall the Inspector, and report this if it persists.`;
2101
- }
2102
- if (message.includes("Cannot find native binding")) {
2103
- return `The @napi-rs/keyring platform package for this OS is missing or unavailable \u2014 reinstall the Inspector (for npx, clear the npx cache under your npm cache directory first).`;
2104
3583
  }
2105
- return `On Linux, install libsecret / gnome-keyring.`;
2106
- };
2107
- var KeyringSecretStore = class {
2108
- async get(serverId, field) {
3584
+ const caveat = secretStorageCaveat(info);
3585
+ if (caveat) console.warn(`[mcp-inspector] ${caveat}`);
3586
+ }
3587
+ var resolved;
3588
+ function resolveSecretStore() {
3589
+ resolved ??= (async () => {
3590
+ const configured = parseSecretStoreEnv(process.env[SECRET_STORE_ENV]);
3591
+ if (configured) {
3592
+ const result2 = await buildStore(configured, "configured");
3593
+ if (configured === "keyring") {
3594
+ await absorbFileSecretsIntoKeyring(result2.store);
3595
+ }
3596
+ warnAboutSecretStorage(result2.info);
3597
+ return result2;
3598
+ }
3599
+ const probe = await probeKeyringAvailable();
3600
+ if (probe.available) {
3601
+ const result2 = await buildStore("keyring", "default");
3602
+ await absorbFileSecretsIntoKeyring(result2.store);
3603
+ return result2;
3604
+ }
3605
+ const kind = chooseFallbackKind({
3606
+ container: isContainer(),
3607
+ mounted: isOnMountPoint(path4.dirname(defaultSecretFilePath()))
3608
+ });
3609
+ const result = await buildStore(kind, "fallback", probe.detail);
3610
+ warnAboutSecretStorage(result.info);
3611
+ return result;
3612
+ })();
3613
+ return resolved;
3614
+ }
3615
+ async function absorbFileSecretsIntoKeyring(keyring) {
3616
+ const filePath = defaultSecretFilePath();
3617
+ if (!await anythingToMigrate(filePath)) return;
3618
+ const claimed = await withSecretFileLock(filePath, async () => {
3619
+ await recoverOrphanedSnapshots(filePath);
3620
+ if (!fsSync.existsSync(filePath)) return null;
3621
+ const staged = `${filePath}.migrating-${process.pid}-${randomUUID()}`;
2109
3622
  try {
2110
- const keyring = await loadKeyring();
2111
- if (!keyring.ok) return null;
2112
- const entry = new keyring.mod.AsyncEntry(
2113
- SERVICE_NAME,
2114
- buildAccount(serverId, field)
2115
- );
2116
- const v = await entry.getPassword();
2117
- return v ?? null;
2118
- } catch {
3623
+ await fs4.rename(filePath, staged);
3624
+ } catch (err) {
3625
+ const code = err.code;
3626
+ if (code !== "ENOENT") {
3627
+ console.warn(
3628
+ `
3629
+ [mcp-inspector] Could not claim the secrets file at ${filePath} for migration into the OS keychain (${code ?? "unknown error"}), so it has been left in place. Its secrets are not visible to this session.`
3630
+ );
3631
+ }
2119
3632
  return null;
2120
3633
  }
3634
+ const releaseSnapshot = await openSecretFileLock(staged).catch(() => null);
3635
+ return { staged, releaseSnapshot };
3636
+ }).catch((err) => {
3637
+ console.warn(
3638
+ `
3639
+ [mcp-inspector] Could not lock the secrets file at ${filePath} to migrate it into the OS keychain (${err.message}), so it has been left in place. Its secrets are not visible to this session; the next run will try again.`
3640
+ );
3641
+ return null;
3642
+ });
3643
+ if (claimed === null) return;
3644
+ try {
3645
+ await handOffStagedSecrets(claimed.staged, filePath, keyring);
3646
+ } finally {
3647
+ await claimed.releaseSnapshot?.();
2121
3648
  }
2122
- async set(serverId, field, value) {
3649
+ }
3650
+ async function handOffStagedSecrets(staged, filePath, keyring) {
3651
+ const file = new FileSecretStore({ filePath: staged });
3652
+ const absorbed = await handOff(file, staged, keyring);
3653
+ if (absorbed) {
2123
3654
  try {
2124
- const keyring = await loadKeyring();
2125
- if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
2126
- const entry = new keyring.mod.AsyncEntry(
2127
- SERVICE_NAME,
2128
- buildAccount(serverId, field)
3655
+ await fs4.rm(staged, { force: true });
3656
+ console.warn(
3657
+ `
3658
+ [mcp-inspector] The OS keychain is available again. Secrets kept in ${filePath} while it was not have been moved into the keychain, and the file has been removed.`
2129
3659
  );
2130
- await entry.setPassword(value);
2131
3660
  } catch (err) {
2132
- if (err instanceof KeychainUnavailableError) throw err;
2133
- throw new KeychainUnavailableError(err);
3661
+ console.warn(
3662
+ `
3663
+ [mcp-inspector] Secrets were moved into the OS keychain, but the temporary copy at ${staged} could not be removed (${err instanceof Error ? err.message : String(err)}). Delete it once you have confirmed the keychain has your secrets.`
3664
+ );
2134
3665
  }
3666
+ return;
2135
3667
  }
2136
- async delete(serverId, field) {
3668
+ try {
3669
+ await fs4.link(staged, filePath);
3670
+ await fs4.rm(staged, { force: true });
3671
+ } catch {
3672
+ console.warn(
3673
+ `
3674
+ [mcp-inspector] Could not restore ${staged} to ${filePath}; both files exist and the snapshot holds the secrets that were not migrated. Merge it by hand, or delete it once you have confirmed nothing is missing.`
3675
+ );
3676
+ }
3677
+ }
3678
+ var isSnapshotName = (name, base) => name.startsWith(`${base}.migrating-`) && !name.endsWith(".lock");
3679
+ async function anythingToMigrate(filePath) {
3680
+ const base = path4.basename(filePath);
3681
+ try {
3682
+ return (await fs4.readdir(path4.dirname(filePath))).some(
3683
+ (name) => name === base || isSnapshotName(name, base)
3684
+ );
3685
+ } catch (err) {
3686
+ const code = err.code;
3687
+ return code !== "ENOENT" && code !== "ENOTDIR";
3688
+ }
3689
+ }
3690
+ async function recoverOrphanedSnapshots(filePath) {
3691
+ const dir = path4.dirname(filePath);
3692
+ const base = path4.basename(filePath);
3693
+ let names;
3694
+ try {
3695
+ names = (await fs4.readdir(dir)).filter((n) => isSnapshotName(n, base));
3696
+ } catch {
3697
+ return;
3698
+ }
3699
+ for (const name of names) {
3700
+ const orphan = path4.join(dir, name);
3701
+ if (await isFileLockHeld(orphan)) continue;
2137
3702
  try {
2138
- const keyring = await loadKeyring();
2139
- if (!keyring.ok) return;
2140
- const entry = new keyring.mod.AsyncEntry(
2141
- SERVICE_NAME,
2142
- buildAccount(serverId, field)
3703
+ await fs4.link(orphan, filePath);
3704
+ await fs4.rm(orphan, { force: true });
3705
+ console.warn(
3706
+ `
3707
+ [mcp-inspector] Recovered secrets left behind by an interrupted migration (${orphan} \u2192 ${filePath}).`
2143
3708
  );
2144
- await entry.deleteCredential();
2145
3709
  } catch {
3710
+ console.warn(
3711
+ `
3712
+ [mcp-inspector] Found secrets from an interrupted migration at ${orphan}, and a current ${filePath} already exists, so it was left alone. It may hold values the current file does not.`
3713
+ );
2146
3714
  }
2147
3715
  }
2148
- async deleteAllForServer(serverId) {
2149
- let creds;
2150
- try {
2151
- const keyring = await loadKeyring();
2152
- if (!keyring.ok) return;
2153
- creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
2154
- } catch {
2155
- return;
2156
- }
2157
- const prefix = `${serverId}:`;
2158
- for (const c of creds) {
2159
- if (!c.account.startsWith(prefix)) continue;
2160
- const parsed = parseAccount(c.account);
2161
- if (!parsed || parsed.serverId !== serverId) continue;
2162
- await this.delete(serverId, parsed.field);
3716
+ }
3717
+ async function handOff(file, filePath, keyring) {
3718
+ let entries;
3719
+ try {
3720
+ entries = await file.readAll();
3721
+ } catch (err) {
3722
+ console.warn(
3723
+ `
3724
+ [mcp-inspector] The OS keychain is available again, but the secrets file at ${filePath} could not be read, so its contents were left there: ${err instanceof Error ? err.message : String(err)}`
3725
+ );
3726
+ return false;
3727
+ }
3728
+ if (!entries || Object.keys(entries).length === 0) return false;
3729
+ let complete = true;
3730
+ try {
3731
+ for (const [account, value] of Object.entries(entries)) {
3732
+ const parsed = parseAccount(account);
3733
+ if (!parsed) {
3734
+ complete = false;
3735
+ continue;
3736
+ }
3737
+ const existing = await secretStoreGetStrict(
3738
+ keyring,
3739
+ parsed.serverId,
3740
+ parsed.field
3741
+ );
3742
+ if (existing === null) {
3743
+ await keyring.set(parsed.serverId, parsed.field, value);
3744
+ }
2163
3745
  }
3746
+ } catch (err) {
3747
+ console.warn(
3748
+ `
3749
+ [mcp-inspector] Could not move the secrets file at ${filePath} into the OS keychain, so it has been left in place: ${err instanceof Error ? err.message : String(err)}`
3750
+ );
3751
+ return false;
3752
+ }
3753
+ if (!complete) {
3754
+ console.warn(
3755
+ `
3756
+ [mcp-inspector] Secrets from ${filePath} have been copied into the OS keychain, but at least one entry could not be read (its key is not in \`serverId:field\` form). The file has been left in place so nothing is lost.`
3757
+ );
3758
+ return false;
3759
+ }
3760
+ return true;
3761
+ }
3762
+ async function getSecretStorageInfo() {
3763
+ const { store, info } = await resolveSecretStore();
3764
+ if (info.kind !== "file" || !(store instanceof FileSecretStore)) return info;
3765
+ return describeFileStore(
3766
+ store,
3767
+ store.filePath,
3768
+ "file",
3769
+ info.reason,
3770
+ info.detail
3771
+ );
3772
+ }
3773
+ var DeferredSecretStore = class {
3774
+ async target() {
3775
+ return (await resolveSecretStore()).store;
3776
+ }
3777
+ async isDurable() {
3778
+ return secretStoreIsDurable(await this.target());
3779
+ }
3780
+ /**
3781
+ * Forwarded, and its absence was a real defect rather than an omission:
3782
+ * this is the store production actually uses, so a `secretStoreGetStrict`
3783
+ * call against it fell through to the tolerant `get` and the strictness
3784
+ * existed only in tests that injected a concrete store.
3785
+ */
3786
+ async getStrict(serverId, field) {
3787
+ return secretStoreGetStrict(await this.target(), serverId, field);
3788
+ }
3789
+ async getMany(requests) {
3790
+ return secretStoreGetMany(await this.target(), requests);
3791
+ }
3792
+ async get(serverId, field) {
3793
+ return (await this.target()).get(serverId, field);
3794
+ }
3795
+ async set(serverId, field, value) {
3796
+ await (await this.target()).set(serverId, field, value);
3797
+ }
3798
+ async setMany(serverId, values) {
3799
+ await secretStoreSetMany(await this.target(), serverId, values);
3800
+ }
3801
+ async delete(serverId, field) {
3802
+ await (await this.target()).delete(serverId, field);
3803
+ }
3804
+ async deleteAllForServer(serverId) {
3805
+ await (await this.target()).deleteAllForServer(serverId);
2164
3806
  }
2165
3807
  };
3808
+ function defaultSecretStore() {
3809
+ return new DeferredSecretStore();
3810
+ }
2166
3811
 
2167
3812
  // ../../core/client/config-parse.ts
2168
3813
  import { z } from "zod";
@@ -2325,7 +3970,8 @@ async function migrateClientPlaintextSecret(filePath, config, secretStore) {
2325
3970
  const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
2326
3971
  if (!value) return config;
2327
3972
  try {
2328
- const existing = await secretStore.get(
3973
+ const existing = await secretStoreGetStrict(
3974
+ secretStore,
2329
3975
  CLIENT_KEYCHAIN_ID,
2330
3976
  SECRET_FIELD_IDP_CLIENT_SECRET
2331
3977
  );
@@ -2336,10 +3982,11 @@ async function migrateClientPlaintextSecret(filePath, config, secretStore) {
2336
3982
  value
2337
3983
  );
2338
3984
  }
3985
+ if (!await secretStoreIsDurable(secretStore)) return config;
2339
3986
  await writeStoreFile(filePath, serializeStore(stripped));
2340
3987
  return stripped;
2341
3988
  } catch (err) {
2342
- if (err instanceof KeychainUnavailableError) {
3989
+ if (err instanceof SecretStoreUnavailableError) {
2343
3990
  return config;
2344
3991
  }
2345
3992
  throw err;
@@ -2373,7 +4020,30 @@ async function writeClientConfigStore(filePath, body, secretStore) {
2373
4020
  SECRET_FIELD_IDP_CLIENT_SECRET
2374
4021
  );
2375
4022
  }
2376
- await writeStoreFile(filePath, serializeStore(stripped));
4023
+ const durable = await secretStoreIsDurable(secretStore);
4024
+ await writeStoreFile(
4025
+ filePath,
4026
+ serializeStore(
4027
+ durable ? stripped : await preserveLegacyPlaintext(
4028
+ filePath,
4029
+ validated,
4030
+ stripped,
4031
+ idpSecret
4032
+ )
4033
+ )
4034
+ );
4035
+ }
4036
+ async function preserveLegacyPlaintext(filePath, validated, stripped, idpSecret) {
4037
+ if (!idpSecret) return stripped;
4038
+ try {
4039
+ const raw = await readStoreFile(filePath);
4040
+ if (raw === null) return stripped;
4041
+ const prior = parseClientConfig(parseStore(raw));
4042
+ const priorSecret = extractSecretsFromClientConfig(prior).secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
4043
+ return priorSecret === idpSecret ? validated : stripped;
4044
+ } catch {
4045
+ return stripped;
4046
+ }
2377
4047
  }
2378
4048
  async function deleteClientConfigStore(filePath, secretStore) {
2379
4049
  await deleteStoreFile(filePath);
@@ -2389,18 +4059,24 @@ function closeAbortedStream(stream) {
2389
4059
  }
2390
4060
  async function primeAndHoldSseStream(stream, cleanup) {
2391
4061
  let done = false;
2392
- const aborted = new Promise((resolve6) => {
4062
+ const aborted = new Promise((resolve9) => {
2393
4063
  stream.onAbort(() => {
2394
4064
  if (done) return;
2395
4065
  done = true;
2396
4066
  cleanup();
2397
4067
  closeAbortedStream(stream);
2398
- resolve6();
4068
+ resolve9();
2399
4069
  });
2400
4070
  });
2401
4071
  await stream.write(SSE_PRIMING_COMMENT);
2402
4072
  await aborted;
2403
4073
  }
4074
+ var MAX_APP_DOCUMENT_CHARS = 8 * 1024 * 1024;
4075
+ var MAX_APP_DOCUMENT_BODY_BYTES = 24 * 1024 * 1024;
4076
+ var MAX_APP_CSP_CHARS = 8 * 1024;
4077
+ function isHeaderSafeValue(value) {
4078
+ return !/[^\t\x20-\x7e]/.test(value);
4079
+ }
2404
4080
  function createOriginMiddleware(allowedOrigins) {
2405
4081
  return async (c, next) => {
2406
4082
  if (!allowedOrigins || allowedOrigins.length === 0) {
@@ -2535,13 +4211,13 @@ function mcpParamHeadersOnly(headers) {
2535
4211
  }
2536
4212
  function createRemoteApp(options) {
2537
4213
  const dangerouslyOmitAuth = !!options.dangerouslyOmitAuth;
2538
- const authToken = dangerouslyOmitAuth ? "" : options.authToken || process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] || randomBytes(32).toString("hex");
4214
+ const authToken = dangerouslyOmitAuth ? "" : options.authToken || process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] || randomBytes3(32).toString("hex");
2539
4215
  const app = new Hono();
2540
4216
  const sessions = /* @__PURE__ */ new Map();
2541
4217
  const { logger: fileLogger, allowedOrigins } = options;
2542
4218
  const storageDir = options.storageDir ?? getDefaultStorageDir();
2543
4219
  const mcpConfigPath = options.mcpConfigPath ?? getDefaultMcpConfigPath();
2544
- const secretStore = options.secretStore ?? new KeyringSecretStore();
4220
+ const secretStore = options.secretStore ?? defaultSecretStore();
2545
4221
  const writable = options.writable ?? true;
2546
4222
  const inMemoryServers = options.initialServers;
2547
4223
  const watchable = !inMemoryServers;
@@ -2625,14 +4301,66 @@ function createRemoteApp(options) {
2625
4301
  if (!dangerouslyOmitAuth) {
2626
4302
  app.use("*", createAuthMiddleware(authToken));
2627
4303
  }
2628
- app.get("/api/config", (c) => {
4304
+ app.get("/api/config", async (c) => {
4305
+ const secretStorage = options.secretStorageResolver ? await options.secretStorageResolver() : options.initialConfig?.secretStorage;
2629
4306
  const payload = {
2630
4307
  ...options.initialConfig,
2631
4308
  writable,
4309
+ // Assigned unconditionally, not spread-when-truthy. The resolver's
4310
+ // contract is that `undefined` means "not known right now", and a
4311
+ // conditional spread leaves the *startup* descriptor from
4312
+ // `initialConfig` standing in its place — so a store that became
4313
+ // undescribable would keep serving a stale, confident answer. Setting
4314
+ // it to `undefined` is what clears it; `c.json` omits the key.
4315
+ secretStorage,
2632
4316
  ...options.sandboxUrl ? { sandboxUrl: options.sandboxUrl } : {}
2633
4317
  };
2634
4318
  return c.json(payload);
2635
4319
  });
4320
+ app.post(
4321
+ "/api/app-document",
4322
+ // Before the parse, not after: see MAX_APP_DOCUMENT_BODY_BYTES.
4323
+ bodyLimit({
4324
+ maxSize: MAX_APP_DOCUMENT_BODY_BYTES,
4325
+ onError: (c) => c.json({ error: "Document too large" }, 413)
4326
+ }),
4327
+ async (c) => {
4328
+ const publish = options.publishAppDocument;
4329
+ if (!publish) {
4330
+ return c.json({ error: "App-origin hosting is not available" }, 503);
4331
+ }
4332
+ let parsed;
4333
+ try {
4334
+ parsed = await c.req.json();
4335
+ } catch {
4336
+ return c.json({ error: "Invalid JSON body" }, 400);
4337
+ }
4338
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4339
+ return c.json({ error: "Invalid JSON body" }, 400);
4340
+ }
4341
+ const { html, csp } = parsed;
4342
+ if (typeof html !== "string" || html === "") {
4343
+ return c.json({ error: "Missing html" }, 400);
4344
+ }
4345
+ if (csp !== void 0 && typeof csp !== "string") {
4346
+ return c.json({ error: "csp must be a string" }, 400);
4347
+ }
4348
+ if (csp !== void 0 && !isHeaderSafeValue(csp)) {
4349
+ return c.json({ error: "csp is not a valid header value" }, 400);
4350
+ }
4351
+ if (html.length > MAX_APP_DOCUMENT_CHARS) {
4352
+ return c.json({ error: "Document too large" }, 413);
4353
+ }
4354
+ if (csp !== void 0 && csp.length > MAX_APP_CSP_CHARS) {
4355
+ return c.json({ error: "csp too large" }, 413);
4356
+ }
4357
+ const published = publish({ html, csp });
4358
+ if (!published) {
4359
+ return c.json({ error: "App-origin hosting is not available" }, 503);
4360
+ }
4361
+ return c.json(published);
4362
+ }
4363
+ );
2636
4364
  app.post("/api/mcp/connect", async (c) => {
2637
4365
  let body;
2638
4366
  try {
@@ -2807,7 +4535,7 @@ function createRemoteApp(options) {
2807
4535
  });
2808
4536
  });
2809
4537
  if (session.isTransportDead()) {
2810
- await new Promise((resolve6) => setTimeout(resolve6, 0));
4538
+ await new Promise((resolve9) => setTimeout(resolve9, 0));
2811
4539
  session.clearEventConsumer();
2812
4540
  sessions.delete(sessionId);
2813
4541
  return;
@@ -2878,7 +4606,7 @@ function createRemoteApp(options) {
2878
4606
  return c.json({ error: "Missing url" }, 400);
2879
4607
  }
2880
4608
  try {
2881
- const res = await fetch(url, {
4609
+ const res = await (createProxyFetch() ?? fetch)(url, {
2882
4610
  method,
2883
4611
  headers: new Headers(headers),
2884
4612
  body: reqBody
@@ -2989,6 +4717,7 @@ function createRemoteApp(options) {
2989
4717
  console.warn("[mcp.json]", msg, bindings);
2990
4718
  }
2991
4719
  };
4720
+ const isJsonObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
2992
4721
  const isStringRecord = (v) => {
2993
4722
  if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
2994
4723
  for (const val of Object.values(v)) {
@@ -3023,6 +4752,9 @@ function createRemoteApp(options) {
3023
4752
  if (o.enterpriseManaged !== void 0 && typeof o.enterpriseManaged !== "boolean") {
3024
4753
  return false;
3025
4754
  }
4755
+ if (o.requestRefreshToken !== void 0 && typeof o.requestRefreshToken !== "boolean") {
4756
+ return false;
4757
+ }
3026
4758
  return true;
3027
4759
  };
3028
4760
  const isRootArray = (v) => {
@@ -3056,10 +4788,10 @@ function createRemoteApp(options) {
3056
4788
  );
3057
4789
  delete valObj.headers;
3058
4790
  }
3059
- if ("metadata" in valObj && !isKvArray(valObj.metadata)) {
4791
+ if ("metadata" in valObj && !isJsonObject(valObj.metadata) && !isKvArray(valObj.metadata)) {
3060
4792
  logWarn(
3061
4793
  { route: "/api/servers", id, droppedKey: "metadata" },
3062
- "Dropping malformed `metadata` field \u2014 expected `Array<{ key: string, value: string }>`."
4794
+ "Dropping malformed `metadata` field \u2014 expected a JSON object."
3063
4795
  );
3064
4796
  delete valObj.metadata;
3065
4797
  }
@@ -3094,7 +4826,7 @@ function createRemoteApp(options) {
3094
4826
  if ("oauth" in valObj && !isOauthObject(valObj.oauth)) {
3095
4827
  logWarn(
3096
4828
  { route: "/api/servers", id, droppedKey: "oauth" },
3097
- "Dropping malformed `oauth` field \u2014 expected `{ clientId?, clientSecret?, scopes?, authorizationParams?, authorizationUrl?, tokenUrl?, enterpriseManaged?, onInsufficientScope? }`."
4829
+ "Dropping malformed `oauth` field \u2014 expected `{ clientId?, clientSecret?, scopes?, authorizationParams?, authorizationUrl?, tokenUrl?, enterpriseManaged?, onInsufficientScope?, requestRefreshToken? }`."
3098
4830
  );
3099
4831
  delete valObj.oauth;
3100
4832
  }
@@ -3157,16 +4889,15 @@ function createRemoteApp(options) {
3157
4889
  const applyStdioSettingsToConfig = (entry, settings, provided) => {
3158
4890
  if (!(entry.type === "stdio" || entry.type === void 0)) return;
3159
4891
  const stdio = entry;
4892
+ const { env, cwd } = stdioConfigFieldsFromSettings(settings);
3160
4893
  if (provided.env) {
3161
- const env = envPairsToRecord(settings.env);
3162
- if (Object.keys(env).length > 0) {
4894
+ if (env) {
3163
4895
  stdio.env = env;
3164
4896
  } else {
3165
4897
  delete stdio.env;
3166
4898
  }
3167
4899
  }
3168
4900
  if (provided.cwd) {
3169
- const cwd = settings.cwd?.trim();
3170
4901
  if (cwd) {
3171
4902
  stdio.cwd = cwd;
3172
4903
  } else {
@@ -3197,10 +4928,10 @@ function createRemoteApp(options) {
3197
4928
  error: "settings.headers must be an array of { key, value }"
3198
4929
  };
3199
4930
  }
3200
- if (!isKvArray2(obj.metadata)) {
4931
+ if (!isJsonObject(obj.metadata)) {
3201
4932
  return {
3202
4933
  ok: false,
3203
- error: "settings.metadata must be an array of { key, value }"
4934
+ error: "settings.metadata must be a JSON object"
3204
4935
  };
3205
4936
  }
3206
4937
  if (obj.env !== void 0 && !isKvArray2(obj.env)) {
@@ -3273,6 +5004,12 @@ function createRemoteApp(options) {
3273
5004
  error: "settings.enterpriseManaged must be a boolean"
3274
5005
  };
3275
5006
  }
5007
+ if (obj.oauthRequestRefreshToken !== void 0 && typeof obj.oauthRequestRefreshToken !== "boolean") {
5008
+ return {
5009
+ ok: false,
5010
+ error: "settings.oauthRequestRefreshToken must be a boolean"
5011
+ };
5012
+ }
3276
5013
  if (obj.oauthOnInsufficientScope !== void 0 && obj.oauthOnInsufficientScope !== "reauthorize" && obj.oauthOnInsufficientScope !== "throw") {
3277
5014
  return {
3278
5015
  ok: false,
@@ -3352,6 +5089,9 @@ function createRemoteApp(options) {
3352
5089
  if (obj.enterpriseManaged === true) {
3353
5090
  value.enterpriseManaged = true;
3354
5091
  }
5092
+ if (obj.oauthRequestRefreshToken === false) {
5093
+ value.oauthRequestRefreshToken = false;
5094
+ }
3355
5095
  if (obj.oauthOnInsufficientScope === "reauthorize" || obj.oauthOnInsufficientScope === "throw") {
3356
5096
  value.oauthOnInsufficientScope = obj.oauthOnInsufficientScope;
3357
5097
  }
@@ -3396,23 +5136,13 @@ function createRemoteApp(options) {
3396
5136
  return { mcpServers: normalizeMcpServers(parsed?.mcpServers) };
3397
5137
  };
3398
5138
  const writeKeychainEntriesFor = async (id, secrets) => {
3399
- await Promise.all(
3400
- Object.entries(secrets).map(
3401
- ([field, value]) => secretStore.set(id, field, value)
3402
- )
3403
- );
5139
+ await secretStoreSetMany(secretStore, id, secrets);
3404
5140
  };
3405
5141
  const readKeychainEntriesFor = async (id, fields) => {
3406
- const values = await Promise.all(
3407
- fields.map(
3408
- async (field) => [field, await secretStore.get(id, field)]
3409
- )
3410
- );
3411
- const out = {};
3412
- for (const [field, v] of values) {
3413
- if (v !== null) out[field] = v;
3414
- }
3415
- return out;
5142
+ const many = await secretStoreGetMany(secretStore, [
5143
+ { serverId: id, fields }
5144
+ ]);
5145
+ return many[id] ?? {};
3416
5146
  };
3417
5147
  const hasPlaintextSecrets = (config) => {
3418
5148
  for (const stored of Object.values(config.mcpServers)) {
@@ -3421,9 +5151,18 @@ function createRemoteApp(options) {
3421
5151
  }
3422
5152
  return false;
3423
5153
  };
5154
+ let warnedSessionPreserved = false;
3424
5155
  const migratePlaintextSecrets = async (config) => {
3425
5156
  let changed = false;
3426
5157
  const next = { mcpServers: {} };
5158
+ const durable = await secretStoreIsDurable(secretStore);
5159
+ const warnSessionPreserved = () => {
5160
+ if (warnedSessionPreserved || durable || !fileLogger) return;
5161
+ warnedSessionPreserved = true;
5162
+ fileLogger.warn(
5163
+ "Secrets are kept in memory for this session, so plaintext values in mcp.json are left on disk rather than migrated away. They would otherwise be lost when the Inspector exits."
5164
+ );
5165
+ };
3427
5166
  try {
3428
5167
  for (const [id, stored] of Object.entries(config.mcpServers)) {
3429
5168
  const { stripped, secrets } = extractSecretsFromStored(stored);
@@ -3432,20 +5171,25 @@ function createRemoteApp(options) {
3432
5171
  continue;
3433
5172
  }
3434
5173
  for (const [field, value] of Object.entries(secrets)) {
3435
- const existing = await secretStore.get(id, field);
5174
+ const existing = await secretStoreGetStrict(secretStore, id, field);
3436
5175
  if (existing === null) {
3437
5176
  await secretStore.set(id, field, value);
3438
5177
  }
3439
5178
  }
5179
+ if (!durable) {
5180
+ warnSessionPreserved();
5181
+ next.mcpServers[id] = stored;
5182
+ continue;
5183
+ }
3440
5184
  next.mcpServers[id] = stripped;
3441
5185
  changed = true;
3442
5186
  }
3443
5187
  } catch (err) {
3444
- if (err instanceof KeychainUnavailableError) {
5188
+ if (err instanceof SecretStoreUnavailableError) {
3445
5189
  if (fileLogger) {
3446
5190
  fileLogger.warn(
3447
5191
  { err: err.message },
3448
- "Keychain unavailable; skipping plaintext-secret migration on this read. Existing mcp.json plaintext values are preserved."
5192
+ "Secret store unavailable; skipping plaintext-secret migration on this read. Existing mcp.json plaintext values are preserved."
3449
5193
  );
3450
5194
  }
3451
5195
  return { migrated: config, changed: false };
@@ -3455,22 +5199,20 @@ function createRemoteApp(options) {
3455
5199
  return { migrated: next, changed };
3456
5200
  };
3457
5201
  const rehydrateConfig = async (config) => {
5202
+ const entries = Object.entries(config.mcpServers);
5203
+ const secrets = await secretStoreGetMany(
5204
+ secretStore,
5205
+ entries.map(([serverId, stored]) => ({
5206
+ serverId,
5207
+ fields: expectedSecretFields(stored)
5208
+ }))
5209
+ );
3458
5210
  const out = { mcpServers: {} };
3459
- for (const [id, stored] of Object.entries(config.mcpServers)) {
3460
- const fields = expectedSecretFields(stored);
3461
- const secrets = await readKeychainEntriesFor(id, fields);
3462
- out.mcpServers[id] = mergeSecretsIntoStored(stored, secrets);
5211
+ for (const [id, stored] of entries) {
5212
+ out.mcpServers[id] = mergeSecretsIntoStored(stored, secrets[id] ?? {});
3463
5213
  }
3464
5214
  return out;
3465
5215
  };
3466
- const computeObsoleteFields = (previousFields, nextSecrets) => {
3467
- const nextFieldSet = new Set(Object.keys(nextSecrets));
3468
- const obsolete = [];
3469
- for (const field of previousFields) {
3470
- if (!nextFieldSet.has(field)) obsolete.push(field);
3471
- }
3472
- return obsolete;
3473
- };
3474
5216
  const mergeRenameKeychainSecrets = (stripped, keychainSecrets, nextSecrets) => {
3475
5217
  const allowedFields = new Set(expectedSecretFields(stripped));
3476
5218
  const merged = { ...keychainSecrets, ...nextSecrets };
@@ -3483,8 +5225,18 @@ function createRemoteApp(options) {
3483
5225
  const deleteKeychainFields = async (id, fields) => {
3484
5226
  await Promise.all(fields.map((field) => secretStore.delete(id, field)));
3485
5227
  };
5228
+ const entryForDisk = async (built, stripped, previous) => {
5229
+ if (await secretStoreIsDurable(secretStore)) return stripped;
5230
+ const prior = previous ? extractSecretsFromStored(previous).secrets : {};
5231
+ const submitted = extractSecretsFromStored(built).secrets;
5232
+ const carry = {};
5233
+ for (const [field, value] of Object.entries(submitted)) {
5234
+ if (prior[field] === value) carry[field] = value;
5235
+ }
5236
+ return Object.keys(carry).length > 0 ? mergeSecretsIntoStored(stripped, carry) : stripped;
5237
+ };
3486
5238
  const keychainErrorResponse = (c, err) => {
3487
- if (err instanceof KeychainUnavailableError) {
5239
+ if (err instanceof SecretStoreUnavailableError) {
3488
5240
  return c.json({ error: err.message }, 503);
3489
5241
  }
3490
5242
  return void 0;
@@ -3547,7 +5299,7 @@ function createRemoteApp(options) {
3547
5299
  type,
3548
5300
  process.platform,
3549
5301
  homedir(),
3550
- (path3) => existsSync2(path3) ? readFileSync4(path3, "utf-8") : null
5302
+ (path6) => existsSync3(path6) ? readFileSync5(path6, "utf-8") : null
3551
5303
  );
3552
5304
  if (!result) {
3553
5305
  return c.json(
@@ -3594,7 +5346,7 @@ function createRemoteApp(options) {
3594
5346
  const { stripped, secrets } = extractSecretsFromStored(built);
3595
5347
  await secretStore.deleteAllForServer(id);
3596
5348
  await writeKeychainEntriesFor(id, secrets);
3597
- current.mcpServers[id] = stripped;
5349
+ current.mcpServers[id] = await entryForDisk(built, stripped);
3598
5350
  await writeMcpAndTrackMtime(serializeStore(current));
3599
5351
  return c.json({ ok: true });
3600
5352
  });
@@ -3734,10 +5486,11 @@ function createRemoteApp(options) {
3734
5486
  }
3735
5487
  }
3736
5488
  const { stripped, secrets } = extractSecretsFromStored(built);
5489
+ const onDisk = await entryForDisk(built, stripped, existing);
3737
5490
  const next = { mcpServers: {} };
3738
5491
  for (const [key, val] of Object.entries(current.mcpServers)) {
3739
5492
  if (key === originalId) {
3740
- next.mcpServers[newId] = stripped;
5493
+ next.mcpServers[newId] = onDisk;
3741
5494
  } else {
3742
5495
  next.mcpServers[key] = val;
3743
5496
  }
@@ -3758,7 +5511,10 @@ function createRemoteApp(options) {
3758
5511
  await secretStore.deleteAllForServer(originalId);
3759
5512
  } else {
3760
5513
  const previousFields = new Set(expectedSecretFields(existing));
3761
- const obsolete = computeObsoleteFields(previousFields, secrets);
5514
+ const stillExpected = new Set(expectedSecretFields(built));
5515
+ const obsolete = [...previousFields].filter(
5516
+ (field) => field === SECRET_FIELD_OAUTH_CLIENT_SECRET ? settingsIntent.kind !== "preserve" && !(field in secrets) : !stillExpected.has(field)
5517
+ );
3762
5518
  await writeKeychainEntriesFor(newId, secrets);
3763
5519
  await writeMcpAndTrackMtime(serializeStore(next));
3764
5520
  await deleteKeychainFields(newId, obsolete);
@@ -3874,6 +5630,16 @@ function honoMiddlewarePlugin(config) {
3874
5630
  allowedOrigins: config.allowedOrigins
3875
5631
  });
3876
5632
  await sandboxController.start();
5633
+ const appOriginController = createAppOriginController({
5634
+ port: config.appOriginPort,
5635
+ host: config.sandboxHost,
5636
+ embedderOrigins: appDocumentEmbedders(
5637
+ sandboxController.getUrl(),
5638
+ config.allowedOrigins
5639
+ )
5640
+ });
5641
+ await appOriginController.start();
5642
+ const secretStorage = await getSecretStorageInfo();
3877
5643
  const {
3878
5644
  app: honoApp,
3879
5645
  authToken: resolvedToken,
@@ -3887,14 +5653,21 @@ function honoMiddlewarePlugin(config) {
3887
5653
  initialServers: config.initialServers ?? void 0,
3888
5654
  allowedOrigins: config.allowedOrigins,
3889
5655
  sandboxUrl: sandboxController.getUrl() ?? void 0,
5656
+ publishAppDocument: (doc) => appOriginController.publish(doc),
3890
5657
  logger: config.logger,
3891
- initialConfig: webServerConfigToInitialPayload(config)
5658
+ initialConfig: webServerConfigToInitialPayload(config, secretStorage),
5659
+ // The startup value above is what the banner printed; the route
5660
+ // re-resolves per request, because the first write under a newly-set
5661
+ // passphrase encrypts a pre-existing plaintext file and a captured
5662
+ // descriptor would keep reporting the old state until a restart.
5663
+ secretStorageResolver: getSecretStorageInfo
3892
5664
  });
3893
5665
  resolvedAuthToken = config.dangerouslyOmitAuth ? "" : resolvedToken;
3894
5666
  const originalClose = server.close.bind(server);
3895
5667
  server.close = async () => {
3896
5668
  await closeApi();
3897
5669
  await sandboxController.close();
5670
+ await appOriginController.close();
3898
5671
  return originalClose();
3899
5672
  };
3900
5673
  const sandboxUrl = sandboxController.getUrl();
@@ -3905,7 +5678,8 @@ function honoMiddlewarePlugin(config) {
3905
5678
  config,
3906
5679
  actualPort,
3907
5680
  resolvedToken,
3908
- sandboxUrl ?? void 0
5681
+ sandboxUrl ?? void 0,
5682
+ secretStorage
3909
5683
  );
3910
5684
  if (config.autoOpen) {
3911
5685
  openBrowser(url);
@@ -3943,10 +5717,10 @@ function honoMiddlewarePlugin(config) {
3943
5717
  if (req.method !== "GET" && req.method !== "HEAD") {
3944
5718
  const chunks = [];
3945
5719
  req.on("data", (chunk) => chunks.push(chunk));
3946
- await new Promise((resolve6) => {
3947
- req.once("end", () => resolve6());
3948
- req.once("error", () => resolve6());
3949
- req.once("close", () => resolve6());
5720
+ await new Promise((resolve9) => {
5721
+ req.once("end", () => resolve9());
5722
+ req.once("error", () => resolve9());
5723
+ req.once("close", () => resolve9());
3950
5724
  });
3951
5725
  if (chunks.length > 0) {
3952
5726
  init.body = Buffer.concat(chunks);
@@ -3988,10 +5762,10 @@ function honoMiddlewarePlugin(config) {
3988
5762
  while (true) {
3989
5763
  const { done, value } = await reader.read();
3990
5764
  if (done || clientGone || res.destroyed) break;
3991
- await new Promise((resolve6, reject) => {
5765
+ await new Promise((resolve9, reject) => {
3992
5766
  res.write(
3993
5767
  Buffer.from(value),
3994
- (err) => err ? reject(err) : resolve6()
5768
+ (err) => err ? reject(err) : resolve9()
3995
5769
  );
3996
5770
  });
3997
5771
  }
@@ -4025,7 +5799,7 @@ function honoMiddlewarePlugin(config) {
4025
5799
 
4026
5800
  // server/vite-base-config.ts
4027
5801
  import { rmSync } from "fs";
4028
- import { join as join5 } from "path";
5802
+ import { join as join6 } from "path";
4029
5803
  var NODE_ONLY_OPTIMIZE_DEPS_EXCLUDE = [
4030
5804
  "@modelcontextprotocol/client/stdio",
4031
5805
  // `atomically` is reached only through `core/storage/store-io.ts`,
@@ -4048,7 +5822,15 @@ var NODE_ONLY_OPTIMIZE_DEPS_EXCLUDE = [
4048
5822
  // (#1905). It's a native-binding package (no browser code path) so
4049
5823
  // excluding it keeps Vite's dep scanner from chasing into the
4050
5824
  // platform-specific binaries during dev startup.
4051
- "@napi-rs/keyring"
5825
+ "@napi-rs/keyring",
5826
+ // `proper-lockfile` is reached only through `core/auth/node/file-lock.ts`
5827
+ // — the secrets file's cross-process lock (#2082) — which the Hono
5828
+ // `/api/servers` handlers pull in via `core/auth/node/file-secret-store.ts`.
5829
+ // Same node-only import chain as `atomically` above, and the same reason:
5830
+ // it is CJS with a `graceful-fs`/`signal-exit`/`retry` graph that Vite's
5831
+ // dev scanner has no business walking. Note the tsup `external` lists do
5832
+ // **not** cover this — they configure the production bundles, not `vite dev`.
5833
+ "proper-lockfile"
4052
5834
  ];
4053
5835
  function getViteDevOptimizeDeps() {
4054
5836
  return {
@@ -4059,21 +5841,21 @@ function getViteDevOptimizeDeps() {
4059
5841
  };
4060
5842
  }
4061
5843
  function clearViteDepsCache(clientWebRoot) {
4062
- rmSync(join5(clientWebRoot, "node_modules", ".vite"), {
5844
+ rmSync(join6(clientWebRoot, "node_modules", ".vite"), {
4063
5845
  recursive: true,
4064
5846
  force: true
4065
5847
  });
4066
5848
  }
4067
5849
 
4068
5850
  // ../../vitest.shared.mts
4069
- import path2 from "path";
5851
+ import path5 from "path";
4070
5852
  import { fileURLToPath as fileURLToPath3 } from "url";
4071
5853
  function vitestSharedPaths(clientDir) {
4072
- const dirname8 = path2.resolve(clientDir);
4073
- const repoRoot = path2.resolve(dirname8, "../..");
5854
+ const dirname10 = path5.resolve(clientDir);
5855
+ const repoRoot = path5.resolve(dirname10, "../..");
4074
5856
  const sharedAliases = {
4075
- "@inspector/core": path2.resolve(repoRoot, "core"),
4076
- "@modelcontextprotocol/inspector-test-server": path2.resolve(
5857
+ "@inspector/core": path5.resolve(repoRoot, "core"),
5858
+ "@modelcontextprotocol/inspector-test-server": path5.resolve(
4077
5859
  repoRoot,
4078
5860
  "test-servers/build/index.js"
4079
5861
  )
@@ -4087,58 +5869,58 @@ function vitestSharedPaths(clientDir) {
4087
5869
  const nodeModulesAliases = [
4088
5870
  {
4089
5871
  find: /^react$/,
4090
- replacement: path2.resolve(dirname8, "node_modules/react")
5872
+ replacement: path5.resolve(dirname10, "node_modules/react")
4091
5873
  },
4092
5874
  {
4093
5875
  find: /^react\/jsx-runtime$/,
4094
- replacement: path2.resolve(dirname8, "node_modules/react/jsx-runtime.js")
5876
+ replacement: path5.resolve(dirname10, "node_modules/react/jsx-runtime.js")
4095
5877
  },
4096
5878
  {
4097
5879
  find: /^react\/jsx-dev-runtime$/,
4098
- replacement: path2.resolve(
4099
- dirname8,
5880
+ replacement: path5.resolve(
5881
+ dirname10,
4100
5882
  "node_modules/react/jsx-dev-runtime.js"
4101
5883
  )
4102
5884
  },
4103
5885
  {
4104
5886
  find: /^react-dom$/,
4105
- replacement: path2.resolve(dirname8, "node_modules/react-dom")
5887
+ replacement: path5.resolve(dirname10, "node_modules/react-dom")
4106
5888
  },
4107
5889
  {
4108
5890
  find: /^react-dom\/client$/,
4109
- replacement: path2.resolve(dirname8, "node_modules/react-dom/client.js")
5891
+ replacement: path5.resolve(dirname10, "node_modules/react-dom/client.js")
4110
5892
  },
4111
- { find: /^pino$/, replacement: path2.resolve(dirname8, "node_modules/pino") },
5893
+ { find: /^pino$/, replacement: path5.resolve(dirname10, "node_modules/pino") },
4112
5894
  {
4113
5895
  find: /^pino\/browser\.js$/,
4114
- replacement: path2.resolve(dirname8, "node_modules/pino/browser.js")
5896
+ replacement: path5.resolve(dirname10, "node_modules/pino/browser.js")
4115
5897
  },
4116
5898
  {
4117
5899
  find: /^hono$/,
4118
- replacement: path2.resolve(dirname8, "node_modules/hono/dist/index.js")
5900
+ replacement: path5.resolve(dirname10, "node_modules/hono/dist/index.js")
4119
5901
  },
4120
5902
  {
4121
5903
  find: /^hono\/streaming$/,
4122
- replacement: path2.resolve(
4123
- dirname8,
5904
+ replacement: path5.resolve(
5905
+ dirname10,
4124
5906
  "node_modules/hono/dist/helper/streaming/index.js"
4125
5907
  )
4126
5908
  },
4127
5909
  {
4128
5910
  find: /^@hono\/node-server$/,
4129
- replacement: path2.resolve(dirname8, "node_modules/@hono/node-server")
5911
+ replacement: path5.resolve(dirname10, "node_modules/@hono/node-server")
4130
5912
  },
4131
5913
  {
4132
5914
  find: /^atomically$/,
4133
- replacement: path2.resolve(dirname8, "node_modules/atomically")
5915
+ replacement: path5.resolve(dirname10, "node_modules/atomically")
4134
5916
  },
4135
5917
  {
4136
5918
  find: /^chokidar$/,
4137
- replacement: path2.resolve(dirname8, "node_modules/chokidar")
5919
+ replacement: path5.resolve(dirname10, "node_modules/chokidar")
4138
5920
  },
4139
5921
  {
4140
5922
  find: /^@napi-rs\/keyring$/,
4141
- replacement: path2.resolve(dirname8, "node_modules/@napi-rs/keyring")
5923
+ replacement: path5.resolve(dirname10, "node_modules/@napi-rs/keyring")
4142
5924
  },
4143
5925
  // `express` and `yaml` resolve from the **repo root**, unlike every pin
4144
5926
  // above. Both are reached only through `test-servers/src` — express by the
@@ -4153,11 +5935,22 @@ function vitestSharedPaths(clientDir) {
4153
5935
  // failed to resolve it.
4154
5936
  {
4155
5937
  find: /^express$/,
4156
- replacement: path2.resolve(repoRoot, "node_modules/express")
5938
+ replacement: path5.resolve(repoRoot, "node_modules/express")
4157
5939
  },
4158
5940
  {
4159
5941
  find: /^yaml$/,
4160
- replacement: path2.resolve(repoRoot, "node_modules/yaml")
5942
+ replacement: path5.resolve(repoRoot, "node_modules/yaml")
5943
+ },
5944
+ // Same reasoning, one layer in: `proper-lockfile` is reached only through
5945
+ // `core/` (the secrets file's cross-process lock, #2082), which is the
5946
+ // other root-owned tree with no manifest of its own. Resolution finds the
5947
+ // root copy on its own today — nothing declares it in a client — and this
5948
+ // pin is what keeps that from depending on nothing ever arriving as some
5949
+ // client's transitive dependency, which would otherwise give a test two
5950
+ // copies of a module whose whole job is a single registry of held locks.
5951
+ {
5952
+ find: /^proper-lockfile$/,
5953
+ replacement: path5.resolve(repoRoot, "node_modules/proper-lockfile")
4161
5954
  }
4162
5955
  ];
4163
5956
  const projectResolve = {
@@ -4180,9 +5973,9 @@ function vitestSharedPaths(clientDir) {
4180
5973
  }
4181
5974
 
4182
5975
  // server/start-vite-dev-server.ts
4183
- var __dirname2 = dirname5(fileURLToPath4(import.meta.url));
5976
+ var __dirname2 = dirname7(fileURLToPath4(import.meta.url));
4184
5977
  async function startViteDevServer(config) {
4185
- const root = resolve3(join6(__dirname2, ".."));
5978
+ const root = resolve6(join7(__dirname2, ".."));
4186
5979
  clearViteDepsCache(root);
4187
5980
  const { repoRoot, sharedAliases, sharedDedupe, nodeModulesAliases } = vitestSharedPaths(root);
4188
5981
  const inlineConfig = {
@@ -4216,7 +6009,7 @@ async function startViteDevServer(config) {
4216
6009
  },
4217
6010
  plugins: [react(), honoMiddlewarePlugin(config)]
4218
6011
  };
4219
- const server = await createServer2(inlineConfig);
6012
+ const server = await createServer3(inlineConfig);
4220
6013
  await server.listen();
4221
6014
  return {
4222
6015
  async close() {
@@ -4226,15 +6019,15 @@ async function startViteDevServer(config) {
4226
6019
  }
4227
6020
 
4228
6021
  // server/server.ts
4229
- import { readFileSync as readFileSync5 } from "fs";
4230
- import { join as join7, dirname as dirname6 } from "path";
6022
+ import { readFileSync as readFileSync6 } from "fs";
6023
+ import { join as join8, dirname as dirname8 } from "path";
4231
6024
  import { fileURLToPath as fileURLToPath5 } from "url";
4232
- import { randomBytes as randomBytes2 } from "crypto";
6025
+ import { randomBytes as randomBytes4 } from "crypto";
4233
6026
  import { serve } from "@hono/node-server";
4234
6027
  import { serveStatic } from "@hono/node-server/serve-static";
4235
6028
  import { Hono as Hono2 } from "hono";
4236
6029
  var __filename = fileURLToPath5(import.meta.url);
4237
- var __dirname3 = dirname6(__filename);
6030
+ var __dirname3 = dirname8(__filename);
4238
6031
  async function startHonoServer(config) {
4239
6032
  const sandboxController = createSandboxController({
4240
6033
  port: config.sandboxPort,
@@ -4242,8 +6035,18 @@ async function startHonoServer(config) {
4242
6035
  allowedOrigins: config.allowedOrigins
4243
6036
  });
4244
6037
  await sandboxController.start();
4245
- const resolvedAuthToken = config.authToken || (config.dangerouslyOmitAuth ? "" : randomBytes2(32).toString("hex"));
6038
+ const appOriginController = createAppOriginController({
6039
+ port: config.appOriginPort,
6040
+ host: config.sandboxHost,
6041
+ embedderOrigins: appDocumentEmbedders(
6042
+ sandboxController.getUrl(),
6043
+ config.allowedOrigins
6044
+ )
6045
+ });
6046
+ await appOriginController.start();
6047
+ const resolvedAuthToken = config.authToken || (config.dangerouslyOmitAuth ? "" : randomBytes4(32).toString("hex"));
4246
6048
  const rootPath = config.staticRoot ?? __dirname3;
6049
+ const secretStorage = await getSecretStorageInfo();
4247
6050
  const { app: apiApp, close: closeApi } = createRemoteApp({
4248
6051
  authToken: config.dangerouslyOmitAuth ? void 0 : resolvedAuthToken,
4249
6052
  dangerouslyOmitAuth: config.dangerouslyOmitAuth,
@@ -4253,16 +6056,22 @@ async function startHonoServer(config) {
4253
6056
  initialServers: config.initialServers ?? void 0,
4254
6057
  allowedOrigins: config.allowedOrigins,
4255
6058
  sandboxUrl: sandboxController.getUrl() ?? void 0,
6059
+ publishAppDocument: (doc) => appOriginController.publish(doc),
4256
6060
  logger: config.logger,
4257
- initialConfig: webServerConfigToInitialPayload(config)
6061
+ initialConfig: webServerConfigToInitialPayload(config, secretStorage),
6062
+ // The startup value above is what the banner printed; the route
6063
+ // re-resolves per request, because the first write under a newly-set
6064
+ // passphrase encrypts a pre-existing plaintext file and a captured
6065
+ // descriptor would keep reporting the old state until a restart.
6066
+ secretStorageResolver: getSecretStorageInfo
4258
6067
  });
4259
6068
  const app = new Hono2();
4260
6069
  app.use("/api/*", async (c) => {
4261
6070
  return apiApp.fetch(c.req.raw);
4262
6071
  });
4263
6072
  const serveIndexHtml = (c) => {
4264
- const indexPath = join7(rootPath, "index.html");
4265
- const html = readFileSync5(indexPath, "utf-8");
6073
+ const indexPath = join8(rootPath, "index.html");
6074
+ const html = readFileSync6(indexPath, "utf-8");
4266
6075
  c.header("Cache-Control", "no-store");
4267
6076
  return c.html(injectAuthToken(html, resolvedAuthToken));
4268
6077
  };
@@ -4298,7 +6107,8 @@ async function startHonoServer(config) {
4298
6107
  config,
4299
6108
  info.port,
4300
6109
  resolvedAuthToken,
4301
- sandboxUrl ?? void 0
6110
+ sandboxUrl ?? void 0,
6111
+ secretStorage
4302
6112
  );
4303
6113
  if (config.autoOpen) {
4304
6114
  openBrowser(url);
@@ -4319,19 +6129,20 @@ async function startHonoServer(config) {
4319
6129
  async close() {
4320
6130
  await closeApi();
4321
6131
  await sandboxController.close();
6132
+ await appOriginController.close();
4322
6133
  if ("closeAllConnections" in httpServer) {
4323
6134
  httpServer.closeAllConnections();
4324
6135
  }
4325
- await new Promise((resolve6, reject) => {
4326
- httpServer.close((err) => err ? reject(err) : resolve6());
6136
+ await new Promise((resolve9, reject) => {
6137
+ httpServer.close((err) => err ? reject(err) : resolve9());
4327
6138
  });
4328
6139
  }
4329
6140
  };
4330
6141
  }
4331
6142
 
4332
6143
  // server/ensure-web-build.ts
4333
- import { existsSync as existsSync3 } from "fs";
4334
- import { join as join8 } from "path";
6144
+ import { existsSync as existsSync4 } from "fs";
6145
+ import { join as join9 } from "path";
4335
6146
  import { spawnSync } from "child_process";
4336
6147
  function runViteBuild(webRoot, log) {
4337
6148
  const result = spawnSync("npm", ["run", "build:client"], {
@@ -4346,10 +6157,10 @@ function runViteBuild(webRoot, log) {
4346
6157
  return result.status;
4347
6158
  }
4348
6159
  function ensureWebBuild(webRoot, distRoot, deps = {}) {
4349
- const exists = deps.exists ?? existsSync3;
6160
+ const exists = deps.exists ?? existsSync4;
4350
6161
  const build = deps.build ?? runViteBuild;
4351
6162
  const log = deps.log ?? ((message) => console.log(message));
4352
- const indexHtml = join8(distRoot, "index.html");
6163
+ const indexHtml = join9(distRoot, "index.html");
4353
6164
  if (exists(indexHtml)) return;
4354
6165
  log(
4355
6166
  "No production web build found at clients/web/dist. Building the web UI now (first run only; this can take a minute)..."
@@ -4363,10 +6174,10 @@ function ensureWebBuild(webRoot, distRoot, deps = {}) {
4363
6174
  }
4364
6175
 
4365
6176
  // server/run-web.ts
4366
- var __dirname4 = dirname7(fileURLToPath6(import.meta.url));
6177
+ var __dirname4 = dirname9(fileURLToPath6(import.meta.url));
4367
6178
  function ensureStdioCwd(config) {
4368
6179
  if ((config.type === "stdio" || config.type === void 0) && config.command && !config.cwd) {
4369
- return { ...config, cwd: resolve4(process.cwd()) };
6180
+ return { ...config, cwd: resolve7(process.cwd()) };
4370
6181
  }
4371
6182
  return config;
4372
6183
  }
@@ -4424,7 +6235,7 @@ async function runWeb(argv) {
4424
6235
  "Note: --server has no effect on the web UI yet; it lists every server in the catalog."
4425
6236
  );
4426
6237
  }
4427
- mcpConfigPath = resolve4(process.cwd(), catalogPath);
6238
+ mcpConfigPath = resolve7(process.cwd(), catalogPath);
4428
6239
  writable = true;
4429
6240
  } else if (hasConfig) {
4430
6241
  if (opts.server) {
@@ -4432,7 +6243,7 @@ async function runWeb(argv) {
4432
6243
  "Note: --server has no effect on the web UI yet; it lists every server in the file."
4433
6244
  );
4434
6245
  }
4435
- const resolvedConfigPath = resolve4(process.cwd(), opts.config);
6246
+ const resolvedConfigPath = resolve7(process.cwd(), opts.config);
4436
6247
  try {
4437
6248
  resolveServerConfigs({ configPath: resolvedConfigPath }, "multi");
4438
6249
  } catch (err) {
@@ -4453,14 +6264,14 @@ async function runWeb(argv) {
4453
6264
  cwd: opts.cwd,
4454
6265
  env: opts.e
4455
6266
  };
4456
- const resolved = resolveServerConfigs(serverOptions, "single")[0];
4457
- if (!resolved) {
6267
+ const resolved2 = resolveServerConfigs(serverOptions, "single")[0];
6268
+ if (!resolved2) {
4458
6269
  console.error(
4459
6270
  "Error: Could not resolve server config. Pass a command/URL, or use --catalog / --config to serve a file."
4460
6271
  );
4461
6272
  process.exit(1);
4462
6273
  }
4463
- config = ensureStdioCwd(resolved);
6274
+ config = ensureStdioCwd(resolved2);
4464
6275
  } catch (err) {
4465
6276
  const message = err instanceof Error ? err.message : "Could not resolve server config.";
4466
6277
  console.error("Error:", message);
@@ -4500,8 +6311,8 @@ async function runWeb(argv) {
4500
6311
  console.error("Error:", message);
4501
6312
  process.exit(1);
4502
6313
  }
4503
- const webRoot = join9(__dirname4, "..");
4504
- const distRoot = join9(webRoot, "dist");
6314
+ const webRoot = join10(__dirname4, "..");
6315
+ const distRoot = join10(webRoot, "dist");
4505
6316
  if (!isDev) {
4506
6317
  webConfig.staticRoot = distRoot;
4507
6318
  }
@@ -4532,7 +6343,7 @@ async function runWeb(argv) {
4532
6343
 
4533
6344
  // src/index.ts
4534
6345
  var __filename2 = fileURLToPath7(import.meta.url);
4535
- var isMain = process.argv[1] !== void 0 && resolve5(process.argv[1]) === resolve5(__filename2);
6346
+ var isMain = process.argv[1] !== void 0 && resolve8(process.argv[1]) === resolve8(__filename2);
4536
6347
  if (isMain) {
4537
6348
  runWeb(process.argv).then((code) => process.exit(code ?? 0)).catch((e) => {
4538
6349
  console.error(e);
@@ -4542,3 +6353,16 @@ if (isMain) {
4542
6353
  export {
4543
6354
  runWeb
4544
6355
  };
6356
+ /* v8 ignore next 2 -- @preserve: the non-Error arm is unreachable via
6357
+ proper-lockfile, which rejects only with Errors. */
6358
+ /* v8 ignore next -- @preserve: node validates the cost parameters
6359
+ synchronously (an out-of-grammar `N` throws from the call itself,
6360
+ which the corrupt-KDF test exercises), so this asynchronous channel
6361
+ is left for allocation failure — not provokable deterministically. */
6362
+ /* v8 ignore next -- @preserve: unreachable, `encryptedEnvelopeProblem`
6363
+ above returns a reason for anything `decodeParts` would reject. Kept as
6364
+ the type narrowing `parts` needs. */
6365
+ /* v8 ignore next -- @preserve: the walk can only run out of ancestors if
6366
+ the filesystem root itself does not exist, which would mean the process
6367
+ has no cwd to have resolved against. Kept as the loop's termination
6368
+ guard rather than trusting that invariant. */