@modelcontextprotocol/inspector 2.7.0 → 2.9.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.
@@ -20,7 +20,7 @@ import { readFile, writeFile } from "atomically";
20
20
 
21
21
  // ../../core/storage/store-id.ts
22
22
  function validateStoreId(storeId) {
23
- return /^[a-zA-Z0-9_-]+$/.test(storeId) && storeId.length > 0;
23
+ return /^[a-zA-Z0-9_-]+$/.test(storeId) && storeId !== "__proto__";
24
24
  }
25
25
 
26
26
  // ../../core/storage/store-serialize.ts
@@ -109,6 +109,7 @@ var MODERN_LOG_LEVELS = [
109
109
  function isModernLogLevel(value) {
110
110
  return typeof value === "string" && MODERN_LOG_LEVELS.includes(value);
111
111
  }
112
+ var MODERN_PROTOCOL_VERSION = "2026-07-28";
112
113
 
113
114
  // ../../core/mcp/skillsSchemas.ts
114
115
  import { z } from "zod/v4";
@@ -145,6 +146,10 @@ var ModernListSkillsResultSchema = ListSkillsResultSchema.extend({
145
146
  var GetSkillEnvelopeSchema = z.looseObject({
146
147
  skill: SkillEntrySchema
147
148
  });
149
+ var ModernGetSkillEnvelopeSchema = GetSkillEnvelopeSchema.extend({
150
+ ttlMs: z.int().min(0),
151
+ cacheScope: z.enum(["public", "private"])
152
+ });
148
153
  var GetSkillResultSchema = GetSkillEnvelopeSchema.transform(
149
154
  (result) => result.skill
150
155
  );
@@ -424,7 +429,7 @@ function cleanEndpointOverride(value, field) {
424
429
  return value.trim() || void 0;
425
430
  }
426
431
  function storedFieldsToInspectorSettings(stored) {
427
- const hasAny = stored.headers !== void 0 || stored.metadata !== void 0 || stored.connectionTimeout !== void 0 || stored.requestTimeout !== void 0 || stored.taskTtl !== void 0 || stored.autoRefreshOnListChanged !== void 0 || stored.paginatedLists !== void 0 || stored.advertisedExtensions !== void 0 || stored.maxFetchRequests !== void 0 || stored.skillCatalogMaxSkills !== void 0 || stored.skillCatalogMaxBytes !== void 0 || stored.oauth !== void 0 || stored.roots !== void 0 || stored.protocolEra !== void 0 || stored.modernLogLevel !== void 0 || stored.env !== void 0 || stored.cwd !== void 0;
432
+ const hasAny = stored.headers !== void 0 || stored.metadata !== void 0 || stored.connectionTimeout !== void 0 || stored.requestTimeout !== void 0 || stored.taskTtl !== void 0 || stored.autoRefreshOnListChanged !== void 0 || stored.paginatedLists !== void 0 || stored.suppressNotificationStream !== void 0 || stored.advertisedExtensions !== void 0 || stored.maxFetchRequests !== void 0 || stored.skillCatalogMaxSkills !== void 0 || stored.skillCatalogMaxBytes !== void 0 || stored.oauth !== void 0 || stored.roots !== void 0 || stored.protocolEra !== void 0 || stored.modernLogLevel !== void 0 || stored.env !== void 0 || stored.cwd !== void 0;
428
433
  if (!hasAny) return void 0;
429
434
  const headersPairs = stored.headers ? Object.entries(stored.headers).map(([key, value]) => ({ key, value })) : [];
430
435
  const settings = {
@@ -456,6 +461,9 @@ function storedFieldsToInspectorSettings(stored) {
456
461
  if (isSkillCatalogLimit(stored.skillCatalogMaxBytes)) {
457
462
  settings.skillCatalogMaxBytes = stored.skillCatalogMaxBytes;
458
463
  }
464
+ if (stored.suppressNotificationStream === true) {
465
+ settings.suppressNotificationStream = true;
466
+ }
459
467
  if (isProtocolEra(stored.protocolEra)) {
460
468
  settings.protocolEra = stored.protocolEra;
461
469
  }
@@ -525,6 +533,9 @@ function inspectorSettingsToStoredFields(settings) {
525
533
  if (settings.paginatedLists) {
526
534
  out.paginatedLists = true;
527
535
  }
536
+ if (settings.suppressNotificationStream) {
537
+ out.suppressNotificationStream = true;
538
+ }
528
539
  if (settings.advertisedExtensions && Object.keys(settings.advertisedExtensions).length > 0) {
529
540
  out.advertisedExtensions = { ...settings.advertisedExtensions };
530
541
  }
@@ -591,6 +602,7 @@ var INSPECTOR_FIELD_KEY_MAP = {
591
602
  taskTtl: true,
592
603
  autoRefreshOnListChanged: true,
593
604
  paginatedLists: true,
605
+ suppressNotificationStream: true,
594
606
  advertisedExtensions: true,
595
607
  maxFetchRequests: true,
596
608
  skillCatalogMaxSkills: true,
@@ -954,7 +966,7 @@ function secretStorageCaveat(info) {
954
966
  return `The secrets file could not be read (${info.encryptionUnknown}). Saving a secret will fail rather than overwrite it.`;
955
967
  }
956
968
  if (info.plaintext) {
957
- 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.";
969
+ 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 or MCP_INSPECTOR_SECRET_KEY_FILE to encrypt them.";
958
970
  }
959
971
  return void 0;
960
972
  }
@@ -1058,14 +1070,14 @@ function isAllInterfacesHost(host) {
1058
1070
 
1059
1071
  // server/resolve-bind-host.ts
1060
1072
  var BIND_ALL_INTERFACES_ENV = "DANGEROUSLY_BIND_ALL_INTERFACES";
1061
- function isEnabled(value) {
1073
+ function isEnvFlagEnabled(value) {
1062
1074
  const v = value?.trim().toLowerCase();
1063
1075
  return v === "true" || v === "1";
1064
1076
  }
1065
1077
  var DEFAULT_BIND_HOST = "127.0.0.1";
1066
1078
  function resolveBindHostname(env = process.env) {
1067
1079
  const host = (env.HOST ?? DEFAULT_BIND_HOST).trim();
1068
- if (isAllInterfacesHost(host) && !isEnabled(env[BIND_ALL_INTERFACES_ENV])) {
1080
+ if (isAllInterfacesHost(host) && !isEnvFlagEnabled(env[BIND_ALL_INTERFACES_ENV])) {
1069
1081
  const resolved2 = canonicalUrlHost(host);
1070
1082
  const shown = stripBrackets(resolved2) === stripBrackets(host.toLowerCase()) ? `HOST="${host}"` : `HOST="${host}" (resolves to ${resolved2})`;
1071
1083
  throw new Error(
@@ -1658,7 +1670,9 @@ function buildWebServerConfig(options = {}) {
1658
1670
  );
1659
1671
  }
1660
1672
  const hostname = resolveBindHostname();
1661
- const dangerouslyOmitAuth = !!process.env.DANGEROUSLY_OMIT_AUTH;
1673
+ const dangerouslyOmitAuth = isEnvFlagEnabled(
1674
+ process.env.DANGEROUSLY_OMIT_AUTH
1675
+ );
1662
1676
  const authToken = dangerouslyOmitAuth ? "" : process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] ?? process.env[LEGACY_AUTH_TOKEN_ENV] ?? "";
1663
1677
  const sandboxPort = resolveSandboxPort();
1664
1678
  const requestedAppOriginPort = resolveAppOriginPort();
@@ -1736,7 +1750,7 @@ import react from "@vitejs/plugin-react";
1736
1750
 
1737
1751
  // ../../core/mcp/remote/node/server.ts
1738
1752
  import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1739
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
1753
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
1740
1754
  import { stat as fsStat } from "fs/promises";
1741
1755
  import { homedir } from "os";
1742
1756
  import { Hono } from "hono";
@@ -1840,12 +1854,31 @@ function redactJsonValue(value) {
1840
1854
  }
1841
1855
  return value;
1842
1856
  }
1857
+ function joinSensitiveContinuations(body) {
1858
+ const out = [];
1859
+ let sensitiveAt = -1;
1860
+ for (const pair of body.split("&")) {
1861
+ const eq = pair.indexOf("=");
1862
+ if (eq === -1) {
1863
+ if (sensitiveAt !== -1 && pair.length > 0) {
1864
+ out[sensitiveAt] += `%26${pair}`;
1865
+ } else {
1866
+ out.push(pair);
1867
+ }
1868
+ continue;
1869
+ }
1870
+ const [key] = new URLSearchParams(pair).keys();
1871
+ sensitiveAt = isSensitiveField(key) && eq < pair.length - 1 ? out.length : -1;
1872
+ out.push(pair);
1873
+ }
1874
+ return out.join("&");
1875
+ }
1843
1876
  function redactBody(body, contentType) {
1844
1877
  if (!body) return body;
1845
1878
  const type = (contentType ?? "").toLowerCase();
1846
1879
  if (type.includes("application/x-www-form-urlencoded")) {
1847
1880
  try {
1848
- const params = new URLSearchParams(body);
1881
+ const params = new URLSearchParams(joinSensitiveContinuations(body));
1849
1882
  let changed = false;
1850
1883
  for (const key of new Set(params.keys())) {
1851
1884
  if (isSensitiveField(key)) {
@@ -2359,6 +2392,73 @@ function createProxyFetch() {
2359
2392
  };
2360
2393
  }
2361
2394
 
2395
+ // ../../core/mcp/node/notificationHeadersFetch.ts
2396
+ import { PROTOCOL_VERSION_META_KEY } from "@modelcontextprotocol/client";
2397
+ function createNotificationHeadersFetch(baseFetch) {
2398
+ return (input, init) => {
2399
+ const method = modernNotificationMethod(init);
2400
+ if (method === void 0) return baseFetch(input, init);
2401
+ const headers = new Headers(init?.headers);
2402
+ headers.set("mcp-protocol-version", method.version);
2403
+ headers.set("mcp-method", method.method);
2404
+ return baseFetch(input, { ...init, headers });
2405
+ };
2406
+ }
2407
+ function modernNotificationMethod(init) {
2408
+ if (init?.method?.toUpperCase() !== "POST") return void 0;
2409
+ if (typeof init.body !== "string") return void 0;
2410
+ let message;
2411
+ try {
2412
+ message = JSON.parse(init.body);
2413
+ } catch {
2414
+ return void 0;
2415
+ }
2416
+ if (!isRecord(message) || "id" in message) return void 0;
2417
+ if (typeof message.method !== "string") return void 0;
2418
+ const meta = isRecord(message.params) ? message.params._meta : void 0;
2419
+ const version = isRecord(meta) ? meta[PROTOCOL_VERSION_META_KEY] : void 0;
2420
+ if (typeof version !== "string" || version < MODERN_PROTOCOL_VERSION) {
2421
+ return void 0;
2422
+ }
2423
+ return { method: message.method, version };
2424
+ }
2425
+ function isRecord(value) {
2426
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2427
+ }
2428
+
2429
+ // ../../core/mcp/node/suppressNotificationStreamFetch.ts
2430
+ function createSuppressNotificationStreamFetch(baseFetch, endpoint) {
2431
+ return async (input, init) => {
2432
+ if (isStandaloneStreamRequest(input, init, endpoint)) {
2433
+ return new Response(null, {
2434
+ status: 405,
2435
+ statusText: "Method Not Allowed"
2436
+ });
2437
+ }
2438
+ return baseFetch(input, init);
2439
+ };
2440
+ }
2441
+ function requestUrl(input) {
2442
+ const raw = input instanceof Request ? input.url : input instanceof URL ? input.href : input;
2443
+ try {
2444
+ return new URL(raw);
2445
+ } catch {
2446
+ return void 0;
2447
+ }
2448
+ }
2449
+ function isStandaloneStreamRequest(input, init, endpoint) {
2450
+ const request = input instanceof Request ? input : void 0;
2451
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
2452
+ if (method !== "GET") return false;
2453
+ const url = requestUrl(input);
2454
+ if (!url || url.origin !== endpoint.origin || url.pathname !== endpoint.pathname || url.search !== endpoint.search) {
2455
+ return false;
2456
+ }
2457
+ const headers = new Headers(init?.headers ?? request?.headers);
2458
+ const accept = headers.get("accept")?.toLowerCase() ?? "";
2459
+ return accept.includes("text/event-stream") && !headers.has("last-event-id");
2460
+ }
2461
+
2362
2462
  // ../../core/mcp/node/transport.ts
2363
2463
  function headersFromSettings(settings) {
2364
2464
  if (!settings || settings.headers.length === 0) return void 0;
@@ -2446,10 +2546,14 @@ function createTransportNode(config, options = {}) {
2446
2546
  ...httpConfig.requestInit,
2447
2547
  ...headers && { headers }
2448
2548
  };
2549
+ const stampedFetch = createNotificationHeadersFetch(
2550
+ fetchWithOptionalAuthIntercept
2551
+ );
2552
+ const httpFetch = settings?.suppressNotificationStream === true ? createSuppressNotificationStreamFetch(stampedFetch, url) : stampedFetch;
2449
2553
  const transport = new StreamableHTTPClientTransport(url, {
2450
2554
  authProvider,
2451
2555
  requestInit,
2452
- fetch: fetchWithOptionalAuthIntercept,
2556
+ fetch: httpFetch,
2453
2557
  // SEP-2350: how the transport reacts to a `403 insufficient_scope`
2454
2558
  // challenge. Defaults to the SDK's `reauthorize` when unset.
2455
2559
  ...settings?.oauthOnInsufficientScope && {
@@ -2463,1040 +2567,808 @@ function createTransportNode(config, options = {}) {
2463
2567
  // ../../core/auth/requestTimeout.ts
2464
2568
  var OAUTH_TIMEOUT_WIRE_CODE = "oauth_request_timeout";
2465
2569
 
2466
- // ../../core/json/xMcpHeader.ts
2467
- var MCP_PARAM_HEADER_PREFIX = "Mcp-Param-";
2570
+ // ../../core/storage/own-entry.ts
2571
+ function setOwnEntry(map, key, value) {
2572
+ Object.defineProperty(map, key, {
2573
+ value,
2574
+ enumerable: true,
2575
+ writable: true,
2576
+ configurable: true
2577
+ });
2578
+ }
2579
+ function getOwnEntry(map, key) {
2580
+ return map !== void 0 && Object.hasOwn(map, key) ? map[key] : void 0;
2581
+ }
2468
2582
 
2469
- // ../../core/mcp/import/clientConfig.ts
2470
- function parseJsonObject(raw) {
2471
- let data;
2472
- try {
2473
- data = JSON.parse(raw);
2474
- } catch (err) {
2475
- const detail = err instanceof Error ? err.message : String(err);
2476
- throw new Error(`Invalid JSON: ${detail}`, { cause: err });
2583
+ // ../../core/auth/oauth-persist.ts
2584
+ import { OAuthTokensSchema } from "@modelcontextprotocol/core";
2585
+ var OAUTH_PERSIST_STORE_ID = "oauth";
2586
+ function mergeOAuthSections(disk, snapshot, sections) {
2587
+ const merged = {
2588
+ servers: { ...disk?.servers },
2589
+ idpSessions: { ...disk?.idpSessions }
2590
+ };
2591
+ for (const url of sections.servers ?? []) {
2592
+ const value = getOwnEntry(snapshot.servers, url);
2593
+ if (value === void 0) {
2594
+ delete merged.servers[url];
2595
+ } else {
2596
+ setOwnEntry(merged.servers, url, value);
2597
+ }
2477
2598
  }
2478
- if (!data || typeof data !== "object" || Array.isArray(data)) {
2479
- throw new Error("Expected a JSON object at the top level");
2599
+ for (const issuer of sections.idpSessions ?? []) {
2600
+ const value = getOwnEntry(snapshot.idpSessions, issuer);
2601
+ if (value === void 0) {
2602
+ delete merged.idpSessions[issuer];
2603
+ } else {
2604
+ setOwnEntry(merged.idpSessions, issuer, value);
2605
+ }
2480
2606
  }
2481
- return data;
2607
+ return merged;
2482
2608
  }
2483
- function normalizeServerMap(servers) {
2484
- const out = {};
2485
- for (const [id, raw] of Object.entries(servers)) {
2486
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
2487
- const normalized = normalizeServerType(
2488
- raw
2489
- );
2490
- out[id] = normalized;
2491
- }
2492
- return out;
2609
+ function isStringArray(value) {
2610
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
2493
2611
  }
2494
- function parseMcpServersConfig(raw) {
2495
- const data = parseJsonObject(raw);
2496
- const servers = data.mcpServers;
2497
- if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
2498
- throw new Error("No 'mcpServers' object found in config file");
2612
+ function parseOAuthPersistSections(value) {
2613
+ if (!isRecord2(value)) {
2614
+ return null;
2499
2615
  }
2500
- return {
2501
- mcpServers: normalizeServerMap(servers)
2502
- };
2503
- }
2504
- function parseVsCodeConfig(raw) {
2505
- const data = parseJsonObject(raw);
2506
- const servers = data.servers;
2507
- if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
2508
- throw new Error("No 'servers' object found in VS Code config file");
2616
+ const sections = {};
2617
+ for (const key of Object.keys(value)) {
2618
+ if (key !== "servers" && key !== "idpSessions") return null;
2509
2619
  }
2510
- return {
2511
- mcpServers: normalizeServerMap(servers)
2512
- };
2513
- }
2514
-
2515
- // ../../core/mcp/import/strategies.ts
2516
- function join4(...segments) {
2517
- return segments.map(
2518
- (s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")
2519
- ).filter((s) => s.length > 0).join("/");
2520
- }
2521
- function appData(homeDir) {
2522
- return join4(homeDir, "AppData", "Roaming");
2523
- }
2524
- function appSupportDir(platform, homeDir, app) {
2525
- if (platform === "darwin") {
2526
- return join4(homeDir, "Library", "Application Support", app);
2620
+ if ("servers" in value) {
2621
+ if (!isStringArray(value.servers)) return null;
2622
+ sections.servers = value.servers;
2527
2623
  }
2528
- if (platform === "win32") {
2529
- return join4(appData(homeDir), app);
2624
+ if ("idpSessions" in value) {
2625
+ if (!isStringArray(value.idpSessions)) return null;
2626
+ sections.idpSessions = value.idpSessions;
2530
2627
  }
2531
- return join4(homeDir, ".config", app);
2628
+ return sections;
2532
2629
  }
2533
- var claudeDesktop = {
2534
- id: "claude-desktop",
2535
- label: "Claude Desktop",
2536
- defaultPaths: (platform, homeDir) => [
2537
- join4(
2538
- appSupportDir(platform, homeDir, "Claude"),
2539
- "claude_desktop_config.json"
2540
- )
2541
- ],
2542
- parse: parseMcpServersConfig
2543
- };
2544
- var cursor = {
2545
- id: "cursor",
2546
- label: "Cursor",
2547
- defaultPaths: (_platform, homeDir) => [join4(homeDir, ".cursor", "mcp.json")],
2548
- parse: parseMcpServersConfig
2549
- };
2550
- var cline = {
2551
- id: "cline",
2552
- label: "Cline",
2553
- defaultPaths: (platform, homeDir) => [
2554
- // Cline (VS Code extension) global settings live under the editor's
2555
- // globalStorage; the documented cross-platform fallback is ~/Documents.
2556
- join4(
2557
- appSupportDir(platform, homeDir, "Code"),
2558
- "User",
2559
- "globalStorage",
2560
- "saoudrizwan.claude-dev",
2561
- "settings",
2562
- "cline_mcp_settings.json"
2563
- ),
2564
- join4(homeDir, "Documents", "Cline", "MCP", "cline_mcp_settings.json")
2565
- ],
2566
- parse: parseMcpServersConfig
2567
- };
2568
- var vscode = {
2569
- id: "vscode",
2570
- label: "VS Code",
2571
- defaultPaths: (platform, homeDir) => [
2572
- join4(appSupportDir(platform, homeDir, "Code"), "User", "mcp.json")
2573
- ],
2574
- parse: parseVsCodeConfig
2575
- };
2576
- var IMPORT_STRATEGY_LIST = [
2577
- claudeDesktop,
2578
- cursor,
2579
- cline,
2580
- vscode
2630
+ function isRecord2(value) {
2631
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2632
+ }
2633
+ function isEntryMap(value) {
2634
+ return isRecord2(value) && Object.values(value).every(isRecord2);
2635
+ }
2636
+ var VERBATIM_SECRET_KEYS = [
2637
+ "client_secret",
2638
+ "registration_access_token"
2581
2639
  ];
2582
- var IMPORT_STRATEGIES = Object.fromEntries(IMPORT_STRATEGY_LIST.map((s) => [s.id, s]));
2583
- function getImportStrategy(id) {
2584
- return IMPORT_STRATEGIES[id];
2640
+ function isValidClientInformation(value) {
2641
+ if (value === void 0) return true;
2642
+ if (!isRecord2(value)) return false;
2643
+ return VERBATIM_SECRET_KEYS.every(
2644
+ (key) => !Object.hasOwn(value, key) || typeof value[key] === "string"
2645
+ );
2585
2646
  }
2586
-
2587
- // ../../core/mcp/import/resolveSource.ts
2588
- function resolveImportSource(type, platform, homeDir, readFile2) {
2589
- const strategy = getImportStrategy(type);
2590
- if (!strategy) return null;
2591
- const searched = strategy.defaultPaths(platform, homeDir);
2592
- for (const path6 of searched) {
2593
- let raw;
2594
- try {
2595
- raw = readFile2(path6);
2596
- } catch (err) {
2597
- const msg = err instanceof Error ? err.message : String(err);
2598
- return {
2599
- type,
2600
- found: true,
2601
- path: path6,
2602
- error: `Failed to read ${path6}: ${msg}`,
2603
- searched
2604
- };
2605
- }
2606
- if (raw === null) continue;
2607
- try {
2608
- const config = strategy.parse(raw);
2609
- return { type, found: true, path: path6, config, searched };
2610
- } catch (err) {
2611
- const msg = err instanceof Error ? err.message : String(err);
2612
- return { type, found: true, path: path6, error: msg, searched };
2647
+ var PartialTokensSchema = OAuthTokensSchema.partial();
2648
+ function isValidTokens(value) {
2649
+ if (value === void 0) return true;
2650
+ return PartialTokensSchema.safeParse(value).success;
2651
+ }
2652
+ function isValidServerEntry(entry) {
2653
+ if (!isRecord2(entry)) return false;
2654
+ if (!isValidClientInformation(entry.clientInformation)) return false;
2655
+ if (!isValidClientInformation(entry.preregisteredClientInformation))
2656
+ return false;
2657
+ if (entry.byIssuer !== void 0) {
2658
+ if (!isRecord2(entry.byIssuer)) return false;
2659
+ for (const slot of Object.values(entry.byIssuer)) {
2660
+ if (!isRecord2(slot)) return false;
2661
+ if (!isValidClientInformation(slot.clientInformation)) return false;
2613
2662
  }
2614
2663
  }
2615
- return { type, found: false, searched };
2664
+ return true;
2616
2665
  }
2617
-
2618
- // ../../core/mcp/remote/progressToken.ts
2619
- import { ProgressTokenSchema } from "@modelcontextprotocol/core";
2620
- function progressTokenOf(message) {
2621
- if ("method" in message && message.method === "notifications/progress" && "params" in message && typeof message.params === "object" && message.params !== null) {
2622
- const parsed = ProgressTokenSchema.safeParse(
2623
- message.params.progressToken
2624
- );
2625
- if (parsed.success) {
2626
- return parsed.data;
2666
+ function isValidIdpSession(session) {
2667
+ if (!isRecord2(session)) return false;
2668
+ return ["idToken", "refreshToken"].every(
2669
+ (key) => !Object.hasOwn(session, key) || typeof session[key] === "string"
2670
+ );
2671
+ }
2672
+ function snapshotHasValidSecretPayloads(snapshot) {
2673
+ for (const entry of Object.values(snapshot.servers)) {
2674
+ const record = entry;
2675
+ if (!isValidTokens(record.tokens)) return false;
2676
+ if (isRecord2(record.byIssuer)) {
2677
+ for (const slot of Object.values(record.byIssuer)) {
2678
+ if (isRecord2(slot) && !isValidTokens(slot.tokens)) return false;
2679
+ }
2627
2680
  }
2628
2681
  }
2629
- return void 0;
2682
+ return Object.values(snapshot.idpSessions).every(isValidIdpSession);
2630
2683
  }
2631
-
2632
- // ../../core/mcp/remote/node/remote-session.ts
2633
- var RemoteSession = class _RemoteSession {
2634
- sessionId;
2635
- transport;
2636
- eventQueue = [];
2637
- eventConsumer = null;
2638
- transportDead = false;
2639
- transportError = null;
2640
- activeSendCount = 0;
2641
- /**
2642
- * Suppress duplicate ambient SSE auth echoes after HTTP command-scoped delivery.
2643
- * Stale transport `onerror` may arrive after the send completes.
2644
- */
2645
- authHttpEchoSuppressUntilMs = 0;
2646
- static AUTH_HTTP_ECHO_SUPPRESS_MS = 3e4;
2647
- requestWaits = /* @__PURE__ */ new Map();
2648
- authProviderHandle = null;
2649
- appliedProtocolVersion;
2650
- /** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
2651
- static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
2652
- constructor(sessionId) {
2653
- this.sessionId = sessionId;
2684
+ function snapshotFromPayload(payload) {
2685
+ const servers = payload.servers ?? {};
2686
+ const idpSessions = payload.idpSessions ?? {};
2687
+ if (!isEntryMap(servers) || !isEntryMap(idpSessions)) return null;
2688
+ if (!Object.values(servers).every(isValidServerEntry)) return null;
2689
+ return {
2690
+ servers,
2691
+ idpSessions
2692
+ };
2693
+ }
2694
+ function parseOAuthPersistBlob(raw) {
2695
+ if (raw === null || raw === void 0) {
2696
+ return null;
2654
2697
  }
2655
- setAuthProviderHandle(handle) {
2656
- this.authProviderHandle = handle;
2698
+ const parsed = typeof raw === "string" ? raw ? parseStore(raw) : null : raw;
2699
+ if (!isRecord2(parsed)) {
2700
+ return null;
2657
2701
  }
2658
- setAuthState(authState) {
2659
- if (!this.authProviderHandle) {
2660
- throw new Error("Session has no OAuth auth provider");
2661
- }
2662
- this.authProviderHandle.setAuthState(authState);
2702
+ if (isRecord2(parsed.state) && "version" in parsed) {
2703
+ return snapshotFromPayload(parsed.state);
2663
2704
  }
2664
- setTransport(transport) {
2665
- this.transport = transport;
2705
+ if ("servers" in parsed || "idpSessions" in parsed) {
2706
+ return snapshotFromPayload(parsed);
2666
2707
  }
2667
- /**
2668
- * Apply the client's negotiated protocol version to the upstream transport
2669
- * (#1935). The browser's SDK Client owns the `initialize` handshake, so the
2670
- * `setProtocolVersion` call it makes lands on the *remote* transport; without
2671
- * this the backend's real HTTP transport never learns the version and drops
2672
- * `Mcp-Protocol-Version` from every subsequent request.
2673
- *
2674
- * Ignores anything that isn't a plausible version token so a client can't
2675
- * push arbitrary bytes into an upstream header, and re-applies only on
2676
- * change (the value rides every `/api/mcp/send`). The value arrives from an
2677
- * unvalidated JSON body, so the type is checked rather than assumed —
2678
- * `RegExp.test` would otherwise coerce a `123` or `true` into a "matching"
2679
- * string and hand the wrong type to the transport.
2680
- */
2681
- applyProtocolVersion(version) {
2682
- if (typeof version !== "string" || version === this.appliedProtocolVersion || !_RemoteSession.PROTOCOL_VERSION_PATTERN.test(version)) {
2683
- return;
2708
+ return null;
2709
+ }
2710
+ function serializeOAuthPersistBlob(snapshot) {
2711
+ return serializeStore(snapshot);
2712
+ }
2713
+ function parseOAuthStoreWriteBody(body) {
2714
+ if (!isRecord2(body)) return null;
2715
+ if ("sections" in body) {
2716
+ for (const key of Object.keys(body)) {
2717
+ if (key !== "sections" && key !== "snapshot") return null;
2684
2718
  }
2685
- this.appliedProtocolVersion = version;
2686
- this.transport.setProtocolVersion?.(version);
2687
- }
2688
- setEventConsumer(consumer) {
2689
- this.eventConsumer = consumer;
2690
- while (this.eventQueue.length > 0) {
2691
- const ev = this.eventQueue.shift();
2692
- consumer(ev);
2693
- }
2694
- }
2695
- clearEventConsumer() {
2696
- this.eventConsumer = null;
2697
- return this.transportDead;
2698
- }
2699
- markTransportDead(error) {
2700
- this.transportDead = true;
2701
- this.transportError = error;
2702
- this.pushEvent({
2703
- type: "transport_error",
2704
- data: {
2705
- error,
2706
- code: -32e3
2707
- // MCP error code for connection closed
2708
- }
2709
- });
2710
- }
2711
- isTransportDead() {
2712
- return this.transportDead;
2719
+ const sections = parseOAuthPersistSections(body.sections);
2720
+ if (!sections) return null;
2721
+ const snapshot2 = parseOAuthPersistBlob(
2722
+ "snapshot" in body ? body.snapshot : null
2723
+ );
2724
+ if (!snapshot2 || !snapshotHasValidSecretPayloads(snapshot2)) return null;
2725
+ return { snapshot: snapshot2, sections };
2713
2726
  }
2714
- getTransportError() {
2715
- return this.transportError;
2727
+ const snapshot = parseOAuthPersistBlob(body);
2728
+ if (!snapshot || !snapshotHasValidSecretPayloads(snapshot)) return null;
2729
+ return { snapshot };
2730
+ }
2731
+
2732
+ // ../../core/auth/node/file-lock.ts
2733
+ import nodeFs, { statSync, rmdirSync } from "fs";
2734
+ import * as fs2 from "fs/promises";
2735
+ import * as path2 from "path";
2736
+ import properLockfile from "proper-lockfile";
2737
+
2738
+ // ../../core/auth/node/secret-store.ts
2739
+ var SERVICE_NAME = "mcp-inspector";
2740
+ var keyringLoad;
2741
+ var KeyringModuleShapeError = class extends Error {
2742
+ constructor(detail, options) {
2743
+ super(`@napi-rs/keyring loaded but ${detail}`, options);
2744
+ this.name = "KeyringModuleShapeError";
2716
2745
  }
2717
- hasEventConsumer() {
2718
- return this.eventConsumer !== null;
2746
+ };
2747
+ var checkKeyringShape = (mod) => {
2748
+ try {
2749
+ if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
2750
+ return { ok: true, mod };
2751
+ }
2752
+ return {
2753
+ ok: false,
2754
+ err: new KeyringModuleShapeError(
2755
+ "did not expose AsyncEntry / findCredentialsAsync"
2756
+ )
2757
+ };
2758
+ } catch (err) {
2759
+ return {
2760
+ ok: false,
2761
+ err: new KeyringModuleShapeError(
2762
+ `its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
2763
+ { cause: err }
2764
+ )
2765
+ };
2719
2766
  }
2720
- beginSend() {
2721
- this.activeSendCount++;
2767
+ };
2768
+ var loadKeyring = () => {
2769
+ keyringLoad ??= import("@napi-rs/keyring").then(
2770
+ checkKeyringShape,
2771
+ (err) => ({ ok: false, err })
2772
+ );
2773
+ return keyringLoad;
2774
+ };
2775
+ function parseAccount(account) {
2776
+ const idx = account.indexOf(":");
2777
+ if (idx <= 0 || idx === account.length - 1) return null;
2778
+ return {
2779
+ serverId: account.slice(0, idx),
2780
+ field: account.slice(idx + 1)
2781
+ };
2782
+ }
2783
+ var buildAccount = (serverId, field) => `${serverId}:${field}`;
2784
+ var SecretStoreUnavailableError = class extends Error {
2785
+ constructor(message, options) {
2786
+ super(message, options);
2787
+ this.name = "SecretStoreUnavailableError";
2722
2788
  }
2723
- endSend() {
2724
- if (this.activeSendCount > 0) {
2725
- this.activeSendCount--;
2726
- }
2789
+ };
2790
+ var SecretFileLockHeldError = class extends SecretStoreUnavailableError {
2791
+ constructor(message, options) {
2792
+ super(message, options);
2793
+ this.name = "SecretFileLockHeldError";
2727
2794
  }
2728
- /** Command-scoped auth was returned on HTTP; suppress async onerror echo on SSE. */
2729
- noteAuthChallengeDeliveredViaHttp() {
2730
- this.extendAuthHttpEchoSuppression();
2795
+ };
2796
+ var KeychainUnavailableError = class extends SecretStoreUnavailableError {
2797
+ constructor(cause) {
2798
+ const message = cause instanceof Error ? cause.message : String(cause);
2799
+ super(
2800
+ `OS keychain is not available. ${hintFor(cause, message)} Underlying error: ${message}`
2801
+ );
2802
+ this.name = "KeychainUnavailableError";
2731
2803
  }
2732
- extendAuthHttpEchoSuppression() {
2733
- this.authHttpEchoSuppressUntilMs = Date.now() + _RemoteSession.AUTH_HTTP_ECHO_SUPPRESS_MS;
2804
+ };
2805
+ var hintFor = (cause, message) => {
2806
+ if (cause instanceof KeyringModuleShapeError) {
2807
+ 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.`;
2734
2808
  }
2735
- shouldSuppressAuthEcho() {
2736
- return Date.now() < this.authHttpEchoSuppressUntilMs;
2809
+ if (message.includes("Cannot find native binding")) {
2810
+ 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).`;
2737
2811
  }
2738
- hasActiveSend() {
2739
- return this.activeSendCount > 0;
2812
+ return `On Linux, install libsecret / gnome-keyring.`;
2813
+ };
2814
+ async function probeKeyringAvailable() {
2815
+ const keyring = await loadKeyring();
2816
+ if (!keyring.ok) {
2817
+ const err = keyring.err;
2818
+ return {
2819
+ available: false,
2820
+ detail: err instanceof Error ? err.message : String(err)
2821
+ };
2740
2822
  }
2741
- pushAuthChallenge(challenge) {
2742
- this.pushEvent({ type: "auth_challenge", data: challenge });
2823
+ try {
2824
+ const entry = new keyring.mod.AsyncEntry(SERVICE_NAME, PROBE_ACCOUNT);
2825
+ await entry.getPassword();
2826
+ await keyring.mod.findCredentialsAsync(SERVICE_NAME);
2827
+ return { available: true };
2828
+ } catch (err) {
2829
+ return {
2830
+ available: false,
2831
+ detail: err instanceof Error ? err.message : String(err)
2832
+ };
2743
2833
  }
2834
+ }
2835
+ var PROBE_ACCOUNT = "__inspector:probe";
2836
+ var isNoEntryError = (err) => err instanceof Error && /no (matching )?entry/i.test(err.message);
2837
+ var KeyringSecretStore = class {
2744
2838
  /**
2745
- * Streamable HTTP `transport.send()` can return before the JSON-RPC response
2746
- * arrives. The remote send handler awaits this so `/api/mcp/send` returns
2747
- * `auth_challenge` or `ok: true` only after the MCP round-trip (or auth error).
2839
+ * The intolerant read. Same lookup as {@link get}, minus the catch — a
2840
+ * keychain that cannot answer must not be reported as an empty one to a
2841
+ * caller that is about to write based on the answer.
2748
2842
  */
2749
- waitForRequestResponse(requestId, timeoutMs = 6e4) {
2750
- return new Promise((resolve9, reject) => {
2751
- let timer;
2752
- const arm = () => {
2753
- if (timeoutMs <= 0) return;
2754
- timer = setTimeout(() => {
2755
- this.requestWaits.delete(requestId);
2756
- reject(
2757
- new Error(
2758
- `MCP request ${String(requestId)} timed out after ${timeoutMs}ms`
2759
- )
2760
- );
2761
- }, timeoutMs);
2762
- };
2763
- const clear = () => {
2764
- if (timer !== void 0) {
2765
- clearTimeout(timer);
2766
- timer = void 0;
2767
- }
2768
- };
2769
- arm();
2770
- this.requestWaits.set(requestId, {
2771
- resolve: () => {
2772
- clear();
2773
- resolve9();
2774
- },
2775
- reject: (error) => {
2776
- clear();
2777
- reject(error);
2778
- },
2779
- // A progress notification for this request re-arms the full window, so
2780
- // a tool that keeps reporting progress never trips the relay's own
2781
- // deadline — the same guarantee the SDK client's `resetTimeoutOnProgress`
2782
- // gives on a direct transport (#2028).
2783
- resetTimeout: () => {
2784
- if (timer === void 0) return;
2785
- clear();
2786
- arm();
2787
- }
2788
- });
2789
- });
2843
+ async getStrict(serverId, field) {
2844
+ try {
2845
+ const keyring = await loadKeyring();
2846
+ if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
2847
+ const entry = new keyring.mod.AsyncEntry(
2848
+ SERVICE_NAME,
2849
+ buildAccount(serverId, field)
2850
+ );
2851
+ return await entry.getPassword() ?? null;
2852
+ } catch (err) {
2853
+ if (err instanceof KeychainUnavailableError) throw err;
2854
+ throw new KeychainUnavailableError(err);
2855
+ }
2790
2856
  }
2791
- /**
2792
- * Re-arm a pending request's wait timeout. Invoked from {@link onMessage} when
2793
- * a `notifications/progress` carrying this request's `progressToken` arrives.
2794
- * A no-op when no wait is pending for the id.
2795
- */
2796
- noteRequestProgress(requestId) {
2797
- this.requestWaits.get(requestId)?.resetTimeout();
2857
+ async get(serverId, field) {
2858
+ try {
2859
+ const keyring = await loadKeyring();
2860
+ if (!keyring.ok) return null;
2861
+ const entry = new keyring.mod.AsyncEntry(
2862
+ SERVICE_NAME,
2863
+ buildAccount(serverId, field)
2864
+ );
2865
+ const v = await entry.getPassword();
2866
+ return v ?? null;
2867
+ } catch {
2868
+ return null;
2869
+ }
2798
2870
  }
2799
- cancelRequestWait(requestId) {
2800
- const wait = this.requestWaits.get(requestId);
2801
- if (!wait) {
2802
- return;
2871
+ async set(serverId, field, value) {
2872
+ try {
2873
+ const keyring = await loadKeyring();
2874
+ if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
2875
+ const entry = new keyring.mod.AsyncEntry(
2876
+ SERVICE_NAME,
2877
+ buildAccount(serverId, field)
2878
+ );
2879
+ await entry.setPassword(value);
2880
+ } catch (err) {
2881
+ if (err instanceof KeychainUnavailableError) throw err;
2882
+ throw new KeychainUnavailableError(err);
2803
2883
  }
2804
- this.requestWaits.delete(requestId);
2805
- wait.reject(new Error("MCP request wait cancelled"));
2806
2884
  }
2807
- rejectActiveRequestWaits(error) {
2808
- for (const wait of this.requestWaits.values()) {
2809
- wait.reject(error);
2885
+ async delete(serverId, field) {
2886
+ try {
2887
+ const keyring = await loadKeyring();
2888
+ if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
2889
+ const entry = new keyring.mod.AsyncEntry(
2890
+ SERVICE_NAME,
2891
+ buildAccount(serverId, field)
2892
+ );
2893
+ await entry.deleteCredential();
2894
+ } catch (err) {
2895
+ if (isNoEntryError(err)) return;
2896
+ if (err instanceof KeychainUnavailableError) throw err;
2897
+ throw new KeychainUnavailableError(err);
2810
2898
  }
2811
- this.requestWaits.clear();
2812
2899
  }
2813
- /**
2814
- * Auth errors from the MCP transport.
2815
- * - During an active send: command path owns delivery (HTTP); never SSE.
2816
- * - After HTTP already returned auth_challenge: swallow async onerror echo.
2817
- * - Otherwise: ambient SSE (idle session).
2818
- */
2819
- handleTransportAuthError(error) {
2820
- if (!(error instanceof AuthChallengeError)) {
2821
- return false;
2822
- }
2823
- this.rejectActiveRequestWaits(error);
2824
- if (this.hasActiveSend()) {
2825
- this.extendAuthHttpEchoSuppression();
2826
- return true;
2900
+ async deleteAllForServer(serverId) {
2901
+ let creds;
2902
+ try {
2903
+ const keyring = await loadKeyring();
2904
+ if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
2905
+ creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
2906
+ } catch (err) {
2907
+ if (err instanceof KeychainUnavailableError) throw err;
2908
+ throw new KeychainUnavailableError(err);
2827
2909
  }
2828
- if (this.shouldSuppressAuthEcho()) {
2829
- return true;
2910
+ const prefix = `${serverId}:`;
2911
+ for (const c of creds) {
2912
+ if (!c.account.startsWith(prefix)) continue;
2913
+ const parsed = parseAccount(c.account);
2914
+ if (!parsed || parsed.serverId !== serverId) continue;
2915
+ await this.delete(serverId, parsed.field);
2830
2916
  }
2831
- this.pushAuthChallenge(error.authChallenge);
2832
- return true;
2833
2917
  }
2834
- settleRequestWait(requestId) {
2835
- const wait = this.requestWaits.get(requestId);
2836
- if (!wait) {
2837
- return;
2918
+ };
2919
+ async function secretStoreGetStrict(store, serverId, field) {
2920
+ return store.getStrict ? store.getStrict(serverId, field) : store.get(serverId, field);
2921
+ }
2922
+ async function snapshotSecretFields(store, serverId, fields) {
2923
+ return Promise.all(
2924
+ fields.map(async (field) => ({
2925
+ serverId,
2926
+ field,
2927
+ value: await secretStoreGetStrict(store, serverId, field)
2928
+ }))
2929
+ );
2930
+ }
2931
+ async function restoreSecretFields(store, snapshot, onRestoreFailure) {
2932
+ for (const { serverId, field, value } of snapshot) {
2933
+ try {
2934
+ if (value === null) await store.delete(serverId, field);
2935
+ else await store.set(serverId, field, value);
2936
+ } catch (error) {
2937
+ onRestoreFailure(error);
2838
2938
  }
2839
- this.requestWaits.delete(requestId);
2840
- wait.resolve();
2841
2939
  }
2842
- pushEvent(event) {
2843
- if (this.eventConsumer) {
2844
- this.eventConsumer(event);
2845
- } else {
2846
- this.eventQueue.push(event);
2847
- }
2940
+ }
2941
+ async function secretStoreGetMany(store, requests) {
2942
+ if (store.getMany) return store.getMany(requests);
2943
+ const out = {};
2944
+ await Promise.all(
2945
+ requests.map(async ({ serverId, fields }) => {
2946
+ const entries = await Promise.all(
2947
+ fields.map(
2948
+ async (field) => [field, await store.get(serverId, field)]
2949
+ )
2950
+ );
2951
+ const found = {};
2952
+ for (const [field, value] of entries) {
2953
+ if (value !== null) setOwnEntry(found, field, value);
2954
+ }
2955
+ setOwnEntry(out, serverId, found);
2956
+ })
2957
+ );
2958
+ return out;
2959
+ }
2960
+ async function secretStoreGetManyStrict(store, requests) {
2961
+ if (store.getManyStrict) return store.getManyStrict(requests);
2962
+ const out = {};
2963
+ await Promise.all(
2964
+ requests.map(async ({ serverId, fields }) => {
2965
+ const entries = await Promise.all(
2966
+ fields.map(
2967
+ async (field) => [
2968
+ field,
2969
+ await secretStoreGetStrict(store, serverId, field)
2970
+ ]
2971
+ )
2972
+ );
2973
+ const found = {};
2974
+ for (const [field, value] of entries) {
2975
+ if (value !== null) setOwnEntry(found, field, value);
2976
+ }
2977
+ setOwnEntry(out, serverId, found);
2978
+ })
2979
+ );
2980
+ return out;
2981
+ }
2982
+ async function settleStoreMutations(mutations) {
2983
+ const results = await Promise.allSettled(mutations);
2984
+ for (const result of results) {
2985
+ if (result.status === "rejected") throw result.reason;
2848
2986
  }
2849
- onMessage(message) {
2850
- if ("id" in message && message.id !== null && message.id !== void 0 && ("result" in message || "error" in message)) {
2851
- this.settleRequestWait(message.id);
2852
- }
2853
- const progressToken = progressTokenOf(message);
2854
- if (progressToken !== void 0) {
2855
- this.noteRequestProgress(progressToken);
2856
- }
2857
- this.pushEvent({ type: "message", data: message });
2987
+ }
2988
+ async function secretStoreSetMany(store, serverId, values) {
2989
+ if (store.setMany) return store.setMany(serverId, values);
2990
+ await settleStoreMutations(
2991
+ Object.entries(values).map(
2992
+ ([field, value]) => store.set(serverId, field, value)
2993
+ )
2994
+ );
2995
+ }
2996
+ async function secretStoreIsDurable(store) {
2997
+ return store.isDurable ? store.isDurable() : true;
2998
+ }
2999
+ var InMemorySecretStore = class {
3000
+ map = /* @__PURE__ */ new Map();
3001
+ async get(serverId, field) {
3002
+ return this.map.get(buildAccount(serverId, field)) ?? null;
2858
3003
  }
2859
- onFetchRequest(entry) {
2860
- this.pushEvent({
2861
- type: "fetch_request",
2862
- data: {
2863
- ...entry,
2864
- timestamp: entry.timestamp instanceof Date ? entry.timestamp.toISOString() : entry.timestamp
2865
- }
2866
- });
3004
+ async set(serverId, field, value) {
3005
+ this.map.set(buildAccount(serverId, field), value);
2867
3006
  }
2868
- onFetchResponseBody(id, responseBody) {
2869
- this.pushEvent({
2870
- type: "fetch_request_body_update",
2871
- data: { id, responseBody }
2872
- });
3007
+ async delete(serverId, field) {
3008
+ this.map.delete(buildAccount(serverId, field));
2873
3009
  }
2874
- onFetchStreamUpdate(id, stream) {
2875
- this.pushEvent({
2876
- type: "fetch_stream_update",
2877
- data: {
2878
- id,
2879
- eventCount: stream.eventCount,
2880
- ...stream.closedAt && { closedAt: stream.closedAt.toISOString() }
2881
- }
2882
- });
3010
+ async deleteAllForServer(serverId) {
3011
+ const prefix = `${serverId}:`;
3012
+ for (const key of [...this.map.keys()]) {
3013
+ if (key.startsWith(prefix)) this.map.delete(key);
3014
+ }
2883
3015
  }
2884
- onStderr(entry) {
2885
- this.pushEvent({
2886
- type: "stdio_log",
2887
- data: {
2888
- timestamp: entry.timestamp.toISOString(),
2889
- message: entry.message
2890
- }
2891
- });
3016
+ };
3017
+ var SessionSecretStore = class extends InMemorySecretStore {
3018
+ async isDurable() {
3019
+ return false;
2892
3020
  }
2893
3021
  };
2894
3022
 
2895
- // ../../core/mcp/remote/node/tokenAuthProvider.ts
2896
- var REMOTE_OAUTH_STUB_METADATA = {
2897
- redirect_uris: [],
2898
- scope: ""
3023
+ // ../../core/auth/node/file-lock.ts
3024
+ var STALE_MS = 1e4;
3025
+ var RETRY = {
3026
+ retries: 20,
3027
+ factor: 2,
3028
+ minTimeout: 20,
3029
+ maxTimeout: 1e3
2899
3030
  };
2900
- function cloneAuthState(state) {
3031
+ var RETRY_BUDGET_MS = (() => {
3032
+ let total = 0;
3033
+ let delay = RETRY.minTimeout;
3034
+ for (let i = 0; i < RETRY.retries; i++) {
3035
+ total += Math.min(delay, RETRY.maxTimeout);
3036
+ delay *= RETRY.factor;
3037
+ }
3038
+ return total;
3039
+ })();
3040
+ var warned = /* @__PURE__ */ new Set();
3041
+ function warnOnce(message) {
3042
+ if (warned.has(message)) return;
3043
+ warned.add(message);
3044
+ console.warn(`[mcp-inspector] ${message}`);
3045
+ }
3046
+ function isStuckOrHeld(err, lockPath) {
3047
+ return isHeldElsewhere(err) || identifySync(lockPath) !== null;
3048
+ }
3049
+ var isHeldElsewhere = (err) => err.code === "ELOCKED";
3050
+ var describeError = (err) => err instanceof Error ? err.message : String(err);
3051
+ var lockPathOf = (target) => `${target}.lock`;
3052
+ function identifySync(lockPath) {
3053
+ try {
3054
+ const stat2 = statSync(lockPath);
3055
+ return { ino: stat2.ino, birthtimeMs: stat2.birthtimeMs };
3056
+ } catch {
3057
+ return null;
3058
+ }
3059
+ }
3060
+ function guardedFs(lockPath, owned, onRefused) {
3061
+ const mine = () => {
3062
+ if (owned.id === null) return true;
3063
+ const now = identifySync(lockPath);
3064
+ if (now === null) return false;
3065
+ return now.ino === owned.id.ino && now.birthtimeMs === owned.id.birthtimeMs;
3066
+ };
3067
+ const removeIfMine = () => {
3068
+ if (!mine()) {
3069
+ onRefused();
3070
+ return;
3071
+ }
3072
+ rmdirSync(lockPath);
3073
+ };
2901
3074
  return {
2902
- ...state.oauthTokens && {
2903
- oauthTokens: { ...state.oauthTokens }
3075
+ ...nodeFs,
3076
+ // **Identity is captured here, not after `lock()` resolves.** `mkdir` is
3077
+ // the moment the directory becomes ours, and it is synchronous with
3078
+ // respect to this process: recording it in the callback, before yielding,
3079
+ // leaves no window. Capturing after the acquire promise settled — as this
3080
+ // did originally — spans the library's own `utimes`/`stat` probe, and a
3081
+ // waiter replacing our directory inside that span would have us record
3082
+ // *the winner's* identity as our own, after which the guard below would
3083
+ // cheerfully delete their lock.
3084
+ mkdir: (p, cb) => {
3085
+ nodeFs.mkdir(p, (err) => {
3086
+ if (!err) owned.id = identifySync(lockPath);
3087
+ cb(err);
3088
+ });
2904
3089
  },
2905
- ...state.oauthClient && {
2906
- oauthClient: { ...state.oauthClient }
3090
+ // Reported as success when refused: the library's bookkeeping should
3091
+ // forget this lock either way. Leaving it registered would hand the
3092
+ // `signal-exit` handler a record pointing at the winner's directory.
3093
+ rmdir: (_p, cb) => {
3094
+ try {
3095
+ removeIfMine();
3096
+ cb(null);
3097
+ } catch (err) {
3098
+ cb(err);
3099
+ }
3100
+ },
3101
+ /* v8 ignore next 3 -- @preserve: only reachable from proper-lockfile's
3102
+ signal-exit handler, i.e. at real process exit, which no in-process
3103
+ test can drive. Its logic is `removeIfMine`, covered via `rmdir`. */
3104
+ rmdirSync: () => {
3105
+ removeIfMine();
2907
3106
  }
2908
3107
  };
2909
3108
  }
2910
- function createRemoteAuthProvider(initialAuthState) {
2911
- if (!initialAuthState?.oauthTokens && !initialAuthState?.oauthClient) {
2912
- return void 0;
2913
- }
2914
- let authState = cloneAuthState(initialAuthState);
2915
- const provider = {
2916
- get clientMetadata() {
2917
- return REMOTE_OAUTH_STUB_METADATA;
2918
- },
2919
- get redirectUrl() {
2920
- return "";
2921
- },
2922
- async tokens() {
2923
- return authState.oauthTokens;
2924
- },
2925
- async clientInformation() {
2926
- if (!authState.oauthClient?.client_id) {
2927
- return void 0;
2928
- }
2929
- return {
2930
- client_id: authState.oauthClient.client_id,
2931
- ...authState.oauthClient.client_secret && {
2932
- client_secret: authState.oauthClient.client_secret
2933
- }
2934
- };
2935
- },
2936
- async saveTokens(tokens) {
2937
- authState = {
2938
- ...authState,
2939
- oauthTokens: {
2940
- access_token: tokens.access_token,
2941
- token_type: tokens.token_type,
2942
- expires_in: tokens.expires_in,
2943
- refresh_token: tokens.refresh_token,
2944
- scope: tokens.scope,
2945
- id_token: tokens.id_token
2946
- }
2947
- };
2948
- },
2949
- codeVerifier() {
2950
- return void 0;
2951
- },
2952
- async saveCodeVerifier() {
2953
- },
2954
- clear() {
2955
- authState = {};
2956
- },
2957
- async redirectToAuthorization() {
2958
- throw new Error(
2959
- "OAuth re-authorization must be performed in the Inspector web client (remote server cannot complete OAuth flows)"
2960
- );
2961
- },
2962
- state() {
2963
- return "";
2964
- }
2965
- // This provider implements only the members the remote backend transport
2966
- // exercises (token get/save, client info, non-interactive redirect); the
2967
- // interactive-only members the SDK never calls on the server side are stubbed
2968
- // with narrower shapes (e.g. `codeVerifier()` returns `undefined`, not the
2969
- // interface's `string | Promise<string>`), so it can't be assigned to the
2970
- // full `OAuthClientProvider` without the double cast.
2971
- };
2972
- return {
2973
- provider,
2974
- setAuthState(next) {
2975
- authState = cloneAuthState(next);
2976
- },
2977
- getAuthState() {
2978
- return cloneAuthState(authState);
2979
- }
2980
- };
3109
+ function acquire(target, retries, fsShim) {
3110
+ return properLockfile.lock(target, {
3111
+ realpath: false,
3112
+ stale: STALE_MS,
3113
+ retries,
3114
+ // Every directory removal the library performs — on release and from its
3115
+ // exit handler — routes through here. See `guardedFs`.
3116
+ fs: fsShim,
3117
+ // The library's default `onCompromised` *throws* — from a timer, with no
3118
+ // caller on the stack, so it lands as an uncaught exception and takes the
3119
+ // process down. This is also the library's *only* signal for the
3120
+ // stale-takeover race described at the top of this file — someone
3121
+ // declared our lock stale and replaced it — so it is the one place a user
3122
+ // learns the guarantee was lost. Worth saying; not worth killing an
3123
+ // Inspector session over.
3124
+ onCompromised: (err) => warnOnce(
3125
+ `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.`
3126
+ )
3127
+ });
2981
3128
  }
2982
-
2983
- // ../../core/auth/node/secret-store.ts
2984
- var SERVICE_NAME = "mcp-inspector";
2985
- var keyringLoad;
2986
- var KeyringModuleShapeError = class extends Error {
2987
- constructor(detail, options) {
2988
- super(`@napi-rs/keyring loaded but ${detail}`, options);
2989
- this.name = "KeyringModuleShapeError";
3129
+ async function isFileLockHeld(filePath) {
3130
+ try {
3131
+ return await properLockfile.check(path2.resolve(filePath), {
3132
+ realpath: false,
3133
+ stale: STALE_MS
3134
+ });
3135
+ } catch {
3136
+ return false;
2990
3137
  }
2991
- };
2992
- var checkKeyringShape = (mod) => {
3138
+ }
3139
+ async function openSecretFileLock(filePath) {
3140
+ const target = path2.resolve(filePath);
3141
+ await fs2.mkdir(path2.dirname(target), { recursive: true }).catch(() => {
3142
+ });
3143
+ const owned = { id: null };
3144
+ const fsShim = guardedFs(
3145
+ lockPathOf(target),
3146
+ owned,
3147
+ () => warnOnce(
3148
+ `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.`
3149
+ )
3150
+ );
3151
+ let release;
2993
3152
  try {
2994
- if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
2995
- return { ok: true, mod };
2996
- }
2997
- return {
2998
- ok: false,
2999
- err: new KeyringModuleShapeError(
3000
- "did not expose AsyncEntry / findCredentialsAsync"
3001
- )
3002
- };
3153
+ release = await acquire(target, 0, fsShim).catch((err) => {
3154
+ if (!isStuckOrHeld(err, lockPathOf(target))) throw err;
3155
+ return acquire(target, RETRY, fsShim);
3156
+ });
3003
3157
  } catch (err) {
3004
- return {
3005
- ok: false,
3006
- err: new KeyringModuleShapeError(
3007
- `its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
3008
- { cause: err }
3009
- )
3010
- };
3158
+ if (isStuckOrHeld(err, lockPathOf(target))) {
3159
+ throw new SecretFileLockHeldError(
3160
+ `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.`
3161
+ );
3162
+ }
3163
+ warnOnce(
3164
+ `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.`
3165
+ );
3166
+ return null;
3011
3167
  }
3012
- };
3013
- var loadKeyring = () => {
3014
- keyringLoad ??= import("@napi-rs/keyring").then(
3015
- checkKeyringShape,
3016
- (err) => ({ ok: false, err })
3017
- );
3018
- return keyringLoad;
3019
- };
3020
- function parseAccount(account) {
3021
- const idx = account.indexOf(":");
3022
- if (idx <= 0 || idx === account.length - 1) return null;
3023
- return {
3024
- serverId: account.slice(0, idx),
3025
- field: account.slice(idx + 1)
3168
+ return async () => {
3169
+ try {
3170
+ await release();
3171
+ } catch (err) {
3172
+ if (err.code === "ERELEASED") return;
3173
+ warnOnce(
3174
+ // **Conditional, not an instruction.** Two blanket versions of this
3175
+ // have now been wrong in opposite directions: "it expires on its own"
3176
+ // (false whenever the same `rmdir` that blocked us also blocks stale
3177
+ // takeover — `ENOTEMPTY`, `EACCES`, `EPERM`, `EROFS`), and "remove it
3178
+ // by hand" (false whenever the failure was transient, since the path
3179
+ // may by then hold a *different, live* Inspector's lock, and deleting
3180
+ // that destroys the exclusion of a process that did nothing wrong).
3181
+ //
3182
+ // `proper-lockfile` forwards whatever the filesystem returned, and
3183
+ // nothing here can tell a permanent refusal from a passing one. So
3184
+ // this reports what happened and states the two conditions the
3185
+ // operator can check for themselves, rather than asserting an outcome
3186
+ // this code does not know.
3187
+ `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.`
3188
+ );
3189
+ }
3026
3190
  };
3027
3191
  }
3028
- var buildAccount = (serverId, field) => `${serverId}:${field}`;
3029
- var SecretStoreUnavailableError = class extends Error {
3030
- constructor(message, options) {
3031
- super(message, options);
3032
- this.name = "SecretStoreUnavailableError";
3192
+ async function withSecretFileLock(filePath, fn) {
3193
+ const release = await openSecretFileLock(filePath);
3194
+ if (release === null) return fn();
3195
+ try {
3196
+ return await fn();
3197
+ } finally {
3198
+ await release();
3033
3199
  }
3034
- };
3035
- var KeychainUnavailableError = class extends SecretStoreUnavailableError {
3036
- constructor(cause) {
3037
- const message = cause instanceof Error ? cause.message : String(cause);
3038
- super(
3039
- `OS keychain is not available. ${hintFor(cause, message)} Underlying error: ${message}`
3040
- );
3041
- this.name = "KeychainUnavailableError";
3200
+ }
3201
+
3202
+ // ../../core/auth/node/secret-store-selection.ts
3203
+ import { randomUUID } from "crypto";
3204
+ import * as fsSync from "fs";
3205
+ import * as fs4 from "fs/promises";
3206
+ import * as path4 from "path";
3207
+
3208
+ // ../../core/auth/node/file-secret-store.ts
3209
+ import * as crypto2 from "crypto";
3210
+ import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
3211
+ import * as fs3 from "fs/promises";
3212
+ import * as path3 from "path";
3213
+ var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve9, reject) => {
3214
+ crypto2.scrypt(passphrase, salt, keylen, options, (err, key) => {
3215
+ if (err) reject(err);
3216
+ else resolve9(key);
3217
+ });
3218
+ });
3219
+ var FORMAT_VERSION = 1;
3220
+ var CIPHER = "aes-256-gcm";
3221
+ var KEY_BYTES = 32;
3222
+ var IV_BYTES = 12;
3223
+ var GCM_TAG_BYTES = 16;
3224
+ var SCRYPT_N = 16384;
3225
+ var SCRYPT_R = 8;
3226
+ var SCRYPT_P = 1;
3227
+ var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
3228
+ var SECRET_KEY_FILE_ENV = "MCP_INSPECTOR_SECRET_KEY_FILE";
3229
+ function isSameFile(a, b) {
3230
+ if (path3.resolve(a) === path3.resolve(b)) return true;
3231
+ try {
3232
+ const sa = statSync2(a);
3233
+ const sb = statSync2(b);
3234
+ return sa.dev === sb.dev && sa.ino === sb.ino;
3235
+ } catch {
3236
+ return false;
3042
3237
  }
3043
- };
3044
- var hintFor = (cause, message) => {
3045
- if (cause instanceof KeyringModuleShapeError) {
3046
- 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.`;
3238
+ }
3239
+ function resolveSecretPassphrase(env = process.env, secretFilePath) {
3240
+ const direct = env[SECRET_KEY_ENV];
3241
+ const hasDirect = direct !== void 0 && direct.trim() !== "";
3242
+ const rawKeyFile = env[SECRET_KEY_FILE_ENV];
3243
+ const wantsKeyFile = rawKeyFile !== void 0;
3244
+ const keyFile = rawKeyFile?.trim();
3245
+ if (hasDirect && wantsKeyFile) {
3246
+ return {
3247
+ problem: `both ${SECRET_KEY_ENV} and ${SECRET_KEY_FILE_ENV} are set; set only one`
3248
+ };
3047
3249
  }
3048
- if (message.includes("Cannot find native binding")) {
3049
- 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).`;
3250
+ if (hasDirect) return { passphrase: direct };
3251
+ if (!wantsKeyFile) return {};
3252
+ if (!keyFile) {
3253
+ return { problem: `${SECRET_KEY_FILE_ENV} is set but empty` };
3050
3254
  }
3051
- return `On Linux, install libsecret / gnome-keyring.`;
3052
- };
3053
- async function probeKeyringAvailable() {
3054
- const keyring = await loadKeyring();
3055
- if (!keyring.ok) {
3056
- const err = keyring.err;
3255
+ const resolved2 = path3.resolve(keyFile);
3256
+ if (secretFilePath !== void 0 && isSameFile(resolved2, secretFilePath)) {
3057
3257
  return {
3058
- available: false,
3059
- detail: err instanceof Error ? err.message : String(err)
3258
+ problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is the secrets file itself; point it at a separate key file`
3060
3259
  };
3061
3260
  }
3261
+ let contents;
3062
3262
  try {
3063
- const entry = new keyring.mod.AsyncEntry(SERVICE_NAME, PROBE_ACCOUNT);
3064
- await entry.getPassword();
3065
- return { available: true };
3263
+ contents = readFileSync4(resolved2, "utf-8");
3066
3264
  } catch (err) {
3067
3265
  return {
3068
- available: false,
3069
- detail: err instanceof Error ? err.message : String(err)
3266
+ // `String(err)` rather than `.message`: `readFileSync` only ever throws
3267
+ // an `Error`, so an `instanceof` guard would be a branch no test can take.
3268
+ problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) could not be read: ${String(err)}`
3070
3269
  };
3071
3270
  }
3271
+ const passphrase = contents.replace(/[\r\n]+$/, "");
3272
+ if (passphrase.trim() === "") {
3273
+ return { problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is empty` };
3274
+ }
3275
+ return { passphrase };
3072
3276
  }
3073
- var PROBE_ACCOUNT = "__inspector:probe";
3074
- var KeyringSecretStore = class {
3075
- /**
3076
- * The intolerant read. Same lookup as {@link get}, minus the catch — a
3077
- * keychain that cannot answer must not be reported as an empty one to a
3078
- * caller that is about to write based on the answer.
3079
- */
3080
- async getStrict(serverId, field) {
3081
- try {
3082
- const keyring = await loadKeyring();
3083
- if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
3084
- const entry = new keyring.mod.AsyncEntry(
3085
- SERVICE_NAME,
3086
- buildAccount(serverId, field)
3087
- );
3088
- return await entry.getPassword() ?? null;
3089
- } catch (err) {
3090
- if (err instanceof KeychainUnavailableError) throw err;
3091
- throw new KeychainUnavailableError(err);
3092
- }
3093
- }
3094
- async get(serverId, field) {
3095
- try {
3096
- const keyring = await loadKeyring();
3097
- if (!keyring.ok) return null;
3098
- const entry = new keyring.mod.AsyncEntry(
3099
- SERVICE_NAME,
3100
- buildAccount(serverId, field)
3101
- );
3102
- const v = await entry.getPassword();
3103
- return v ?? null;
3104
- } catch {
3105
- return null;
3106
- }
3277
+ var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
3278
+ function assertSecretString(field, value) {
3279
+ if (typeof value !== "string") {
3280
+ throw new TypeError(
3281
+ `Refusing to store a non-string secret value (${typeof value}) for "${field}": the secrets file holds only strings, and writing this would make the whole file unreadable.`
3282
+ );
3107
3283
  }
3108
- async set(serverId, field, value) {
3109
- try {
3110
- const keyring = await loadKeyring();
3111
- if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
3112
- const entry = new keyring.mod.AsyncEntry(
3113
- SERVICE_NAME,
3114
- buildAccount(serverId, field)
3115
- );
3116
- await entry.setPassword(value);
3117
- } catch (err) {
3118
- if (err instanceof KeychainUnavailableError) throw err;
3119
- throw new KeychainUnavailableError(err);
3120
- }
3284
+ }
3285
+ function asSecretMap(value, filePath) {
3286
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3287
+ throw new SecretStoreUnavailableError(
3288
+ `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.`
3289
+ );
3121
3290
  }
3122
- async delete(serverId, field) {
3123
- try {
3124
- const keyring = await loadKeyring();
3125
- if (!keyring.ok) return;
3126
- const entry = new keyring.mod.AsyncEntry(
3127
- SERVICE_NAME,
3128
- buildAccount(serverId, field)
3291
+ for (const [key, entry] of Object.entries(value)) {
3292
+ if (typeof entry !== "string") {
3293
+ throw new SecretStoreUnavailableError(
3294
+ `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.`
3129
3295
  );
3130
- await entry.deleteCredential();
3131
- } catch {
3132
3296
  }
3133
3297
  }
3134
- async deleteAllForServer(serverId) {
3135
- let creds;
3136
- try {
3137
- const keyring = await loadKeyring();
3138
- if (!keyring.ok) return;
3139
- creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
3140
- } catch {
3141
- return;
3142
- }
3143
- const prefix = `${serverId}:`;
3144
- for (const c of creds) {
3145
- if (!c.account.startsWith(prefix)) continue;
3146
- const parsed = parseAccount(c.account);
3147
- if (!parsed || parsed.serverId !== serverId) continue;
3148
- await this.delete(serverId, parsed.field);
3149
- }
3298
+ return { ...value };
3299
+ }
3300
+ var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
3301
+ constructor(filePath, hasKey) {
3302
+ super(
3303
+ hasKey ? `The secrets file at ${filePath} could not be decrypted with the current ${SECRET_KEY_ENV} (or ${SECRET_KEY_FILE_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 (nor ${SECRET_KEY_FILE_ENV}). Refusing to write, which would overwrite the existing secrets. Set the passphrase this file was written with, or delete the file to start over.`
3304
+ );
3305
+ this.name = "SecretFileKeyMismatchError";
3150
3306
  }
3151
3307
  };
3152
- async function secretStoreGetStrict(store, serverId, field) {
3153
- return store.getStrict ? store.getStrict(serverId, field) : store.get(serverId, field);
3154
- }
3155
- async function secretStoreGetMany(store, requests) {
3156
- if (store.getMany) return store.getMany(requests);
3157
- const out = {};
3158
- await Promise.all(
3159
- requests.map(async ({ serverId, fields }) => {
3160
- const entries = await Promise.all(
3161
- fields.map(
3162
- async (field) => [field, await store.get(serverId, field)]
3163
- )
3164
- );
3165
- const found = {};
3166
- for (const [field, value] of entries) {
3167
- if (value !== null) found[field] = value;
3168
- }
3169
- out[serverId] = found;
3170
- })
3171
- );
3172
- return out;
3308
+ var encodeParts = (iv, tag, body) => `${iv.toString("base64")}.${tag.toString("base64")}.${body.toString("base64")}`;
3309
+ function decodeParts(payload) {
3310
+ const parts = payload.split(".");
3311
+ if (parts.length !== 3) return null;
3312
+ const [iv, tag, body] = parts;
3313
+ return {
3314
+ iv: Buffer.from(iv, "base64"),
3315
+ tag: Buffer.from(tag, "base64"),
3316
+ body: Buffer.from(body, "base64")
3317
+ };
3173
3318
  }
3174
- async function secretStoreSetMany(store, serverId, values) {
3175
- if (store.setMany) return store.setMany(serverId, values);
3176
- await Promise.all(
3177
- Object.entries(values).map(
3178
- ([field, value]) => store.set(serverId, field, value)
3179
- )
3319
+ var MAX_WRITE_ATTEMPTS = 5;
3320
+ var fileQueues = /* @__PURE__ */ new Map();
3321
+ function sameMap(a, b) {
3322
+ const ak = Object.keys(a);
3323
+ if (ak.length !== Object.keys(b).length) return false;
3324
+ return ak.every(
3325
+ (k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]
3180
3326
  );
3181
3327
  }
3182
- async function secretStoreIsDurable(store) {
3183
- return store.isDurable ? store.isDurable() : true;
3184
- }
3185
- var InMemorySecretStore = class {
3186
- map = /* @__PURE__ */ new Map();
3187
- async get(serverId, field) {
3188
- return this.map.get(buildAccount(serverId, field)) ?? null;
3328
+ var isPositiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
3329
+ function encryptedEnvelopeProblem(parsed) {
3330
+ if (!parsed.kdf || typeof parsed.data !== "string") {
3331
+ return "encrypted envelope is missing its kdf or data";
3189
3332
  }
3190
- async set(serverId, field, value) {
3191
- this.map.set(buildAccount(serverId, field), value);
3333
+ const parts = decodeParts(parsed.data);
3334
+ if (!parts) return "encrypted payload is not iv.tag.ciphertext";
3335
+ if (parts.iv.length !== IV_BYTES) {
3336
+ return `initialization vector is ${parts.iv.length} bytes, expected ${IV_BYTES}`;
3192
3337
  }
3193
- async delete(serverId, field) {
3194
- this.map.delete(buildAccount(serverId, field));
3338
+ if (parts.tag.length !== GCM_TAG_BYTES) {
3339
+ return `authentication tag is ${parts.tag.length} bytes, expected ${GCM_TAG_BYTES}`;
3195
3340
  }
3196
- async deleteAllForServer(serverId) {
3197
- const prefix = `${serverId}:`;
3198
- for (const key of [...this.map.keys()]) {
3199
- if (key.startsWith(prefix)) this.map.delete(key);
3200
- }
3341
+ if (parts.body.length === 0) return "ciphertext is empty";
3342
+ const { algorithm, salt, N, r, p } = parsed.kdf;
3343
+ if (algorithm !== "scrypt") return `unsupported kdf "${String(algorithm)}"`;
3344
+ if (typeof salt !== "string" || Buffer.from(salt, "base64").length === 0) {
3345
+ return "kdf salt is missing or not base64";
3201
3346
  }
3202
- };
3203
- var SessionSecretStore = class extends InMemorySecretStore {
3204
- async isDurable() {
3205
- return false;
3347
+ if (!isPositiveInt(N) || N < 2 || (N & N - 1) !== 0) {
3348
+ return `kdf N is ${String(N)}, which is not a power of two above 1`;
3206
3349
  }
3207
- };
3208
-
3209
- // ../../core/auth/node/secret-store-selection.ts
3210
- import { randomUUID } from "crypto";
3211
- import * as fsSync from "fs";
3212
- import * as fs4 from "fs/promises";
3213
- import * as path4 from "path";
3214
-
3215
- // ../../core/auth/node/file-secret-store.ts
3216
- import * as crypto2 from "crypto";
3217
- import * as fs3 from "fs/promises";
3218
- import * as path3 from "path";
3219
-
3220
- // ../../core/auth/node/file-lock.ts
3221
- import nodeFs, { statSync, rmdirSync } from "fs";
3222
- import * as fs2 from "fs/promises";
3223
- import * as path2 from "path";
3224
- import properLockfile from "proper-lockfile";
3225
- var STALE_MS = 1e4;
3226
- var RETRY = {
3227
- retries: 20,
3228
- factor: 2,
3229
- minTimeout: 20,
3230
- maxTimeout: 1e3
3231
- };
3232
- var RETRY_BUDGET_MS = (() => {
3233
- let total = 0;
3234
- let delay = RETRY.minTimeout;
3235
- for (let i = 0; i < RETRY.retries; i++) {
3236
- total += Math.min(delay, RETRY.maxTimeout);
3237
- delay *= RETRY.factor;
3350
+ if (!isPositiveInt(r) || !isPositiveInt(p)) {
3351
+ return `kdf r/p are ${String(r)}/${String(p)}, which are not positive integers`;
3238
3352
  }
3239
- return total;
3240
- })();
3241
- var warned = /* @__PURE__ */ new Set();
3242
- function warnOnce(message) {
3243
- if (warned.has(message)) return;
3244
- warned.add(message);
3245
- console.warn(`[mcp-inspector] ${message}`);
3246
- }
3247
- function isStuckOrHeld(err, lockPath) {
3248
- return isHeldElsewhere(err) || identifySync(lockPath) !== null;
3249
- }
3250
- var isHeldElsewhere = (err) => err.code === "ELOCKED";
3251
- var describeError = (err) => err instanceof Error ? err.message : String(err);
3252
- var lockPathOf = (target) => `${target}.lock`;
3253
- function identifySync(lockPath) {
3254
- try {
3255
- const stat2 = statSync(lockPath);
3256
- return { ino: stat2.ino, birthtimeMs: stat2.birthtimeMs };
3257
- } catch {
3258
- return null;
3353
+ if (N > SCRYPT_N || r > SCRYPT_R || p > SCRYPT_P) {
3354
+ return `kdf cost parameters (N=${N}, r=${r}, p=${p}) exceed the supported maximum (N=${SCRYPT_N}, r=${SCRYPT_R}, p=${SCRYPT_P})`;
3259
3355
  }
3260
- }
3261
- function guardedFs(lockPath, owned, onRefused) {
3262
- const mine = () => {
3263
- if (owned.id === null) return true;
3264
- const now = identifySync(lockPath);
3265
- if (now === null) return false;
3266
- return now.ino === owned.id.ino && now.birthtimeMs === owned.id.birthtimeMs;
3267
- };
3268
- const removeIfMine = () => {
3269
- if (!mine()) {
3270
- onRefused();
3271
- return;
3272
- }
3273
- rmdirSync(lockPath);
3274
- };
3275
- return {
3276
- ...nodeFs,
3277
- // **Identity is captured here, not after `lock()` resolves.** `mkdir` is
3278
- // the moment the directory becomes ours, and it is synchronous with
3279
- // respect to this process: recording it in the callback, before yielding,
3280
- // leaves no window. Capturing after the acquire promise settled — as this
3281
- // did originally — spans the library's own `utimes`/`stat` probe, and a
3282
- // waiter replacing our directory inside that span would have us record
3283
- // *the winner's* identity as our own, after which the guard below would
3284
- // cheerfully delete their lock.
3285
- mkdir: (p, cb) => {
3286
- nodeFs.mkdir(p, (err) => {
3287
- if (!err) owned.id = identifySync(lockPath);
3288
- cb(err);
3289
- });
3290
- },
3291
- // Reported as success when refused: the library's bookkeeping should
3292
- // forget this lock either way. Leaving it registered would hand the
3293
- // `signal-exit` handler a record pointing at the winner's directory.
3294
- rmdir: (_p, cb) => {
3295
- try {
3296
- removeIfMine();
3297
- cb(null);
3298
- } catch (err) {
3299
- cb(err);
3300
- }
3301
- },
3302
- /* v8 ignore next 3 -- @preserve: only reachable from proper-lockfile's
3303
- signal-exit handler, i.e. at real process exit, which no in-process
3304
- test can drive. Its logic is `removeIfMine`, covered via `rmdir`. */
3305
- rmdirSync: () => {
3306
- removeIfMine();
3307
- }
3308
- };
3309
- }
3310
- function acquire(target, retries, fsShim) {
3311
- return properLockfile.lock(target, {
3312
- realpath: false,
3313
- stale: STALE_MS,
3314
- retries,
3315
- // Every directory removal the library performs — on release and from its
3316
- // exit handler — routes through here. See `guardedFs`.
3317
- fs: fsShim,
3318
- // The library's default `onCompromised` *throws* — from a timer, with no
3319
- // caller on the stack, so it lands as an uncaught exception and takes the
3320
- // process down. This is also the library's *only* signal for the
3321
- // stale-takeover race described at the top of this file — someone
3322
- // declared our lock stale and replaced it — so it is the one place a user
3323
- // learns the guarantee was lost. Worth saying; not worth killing an
3324
- // Inspector session over.
3325
- onCompromised: (err) => warnOnce(
3326
- `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.`
3327
- )
3328
- });
3329
- }
3330
- async function isFileLockHeld(filePath) {
3331
- try {
3332
- return await properLockfile.check(path2.resolve(filePath), {
3333
- realpath: false,
3334
- stale: STALE_MS
3335
- });
3336
- } catch {
3337
- return false;
3338
- }
3339
- }
3340
- async function openSecretFileLock(filePath) {
3341
- const target = path2.resolve(filePath);
3342
- await fs2.mkdir(path2.dirname(target), { recursive: true }).catch(() => {
3343
- });
3344
- const owned = { id: null };
3345
- const fsShim = guardedFs(
3346
- lockPathOf(target),
3347
- owned,
3348
- () => warnOnce(
3349
- `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.`
3350
- )
3351
- );
3352
- let release;
3353
- try {
3354
- release = await acquire(target, 0, fsShim).catch((err) => {
3355
- if (!isStuckOrHeld(err, lockPathOf(target))) throw err;
3356
- return acquire(target, RETRY, fsShim);
3357
- });
3358
- } catch (err) {
3359
- if (isStuckOrHeld(err, lockPathOf(target))) {
3360
- throw new SecretStoreUnavailableError(
3361
- `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.`
3362
- );
3363
- }
3364
- warnOnce(
3365
- `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.`
3366
- );
3367
- return null;
3368
- }
3369
- return async () => {
3370
- try {
3371
- await release();
3372
- } catch (err) {
3373
- if (err.code === "ERELEASED") return;
3374
- warnOnce(
3375
- // **Conditional, not an instruction.** Two blanket versions of this
3376
- // have now been wrong in opposite directions: "it expires on its own"
3377
- // (false whenever the same `rmdir` that blocked us also blocks stale
3378
- // takeover — `ENOTEMPTY`, `EACCES`, `EPERM`, `EROFS`), and "remove it
3379
- // by hand" (false whenever the failure was transient, since the path
3380
- // may by then hold a *different, live* Inspector's lock, and deleting
3381
- // that destroys the exclusion of a process that did nothing wrong).
3382
- //
3383
- // `proper-lockfile` forwards whatever the filesystem returned, and
3384
- // nothing here can tell a permanent refusal from a passing one. So
3385
- // this reports what happened and states the two conditions the
3386
- // operator can check for themselves, rather than asserting an outcome
3387
- // this code does not know.
3388
- `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.`
3389
- );
3390
- }
3391
- };
3392
- }
3393
- async function withSecretFileLock(filePath, fn) {
3394
- const release = await openSecretFileLock(filePath);
3395
- if (release === null) return fn();
3396
- try {
3397
- return await fn();
3398
- } finally {
3399
- await release();
3400
- }
3401
- }
3402
-
3403
- // ../../core/auth/node/file-secret-store.ts
3404
- var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve9, reject) => {
3405
- crypto2.scrypt(passphrase, salt, keylen, options, (err, key) => {
3406
- if (err) reject(err);
3407
- else resolve9(key);
3408
- });
3409
- });
3410
- var FORMAT_VERSION = 1;
3411
- var CIPHER = "aes-256-gcm";
3412
- var KEY_BYTES = 32;
3413
- var IV_BYTES = 12;
3414
- var GCM_TAG_BYTES = 16;
3415
- var SCRYPT_N = 16384;
3416
- var SCRYPT_R = 8;
3417
- var SCRYPT_P = 1;
3418
- var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
3419
- var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
3420
- function asSecretMap(value, filePath) {
3421
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
3422
- throw new SecretStoreUnavailableError(
3423
- `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.`
3424
- );
3425
- }
3426
- for (const [key, entry] of Object.entries(value)) {
3427
- if (typeof entry !== "string") {
3428
- throw new SecretStoreUnavailableError(
3429
- `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.`
3430
- );
3431
- }
3432
- }
3433
- return { ...value };
3434
- }
3435
- var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
3436
- constructor(filePath, hasKey) {
3437
- super(
3438
- 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.`
3439
- );
3440
- this.name = "SecretFileKeyMismatchError";
3441
- }
3442
- };
3443
- var encodeParts = (iv, tag, body) => `${iv.toString("base64")}.${tag.toString("base64")}.${body.toString("base64")}`;
3444
- function decodeParts(payload) {
3445
- const parts = payload.split(".");
3446
- if (parts.length !== 3) return null;
3447
- const [iv, tag, body] = parts;
3448
- return {
3449
- iv: Buffer.from(iv, "base64"),
3450
- tag: Buffer.from(tag, "base64"),
3451
- body: Buffer.from(body, "base64")
3452
- };
3453
- }
3454
- var MAX_WRITE_ATTEMPTS = 5;
3455
- var fileQueues = /* @__PURE__ */ new Map();
3456
- function sameMap(a, b) {
3457
- const ak = Object.keys(a);
3458
- if (ak.length !== Object.keys(b).length) return false;
3459
- return ak.every(
3460
- (k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]
3461
- );
3462
- }
3463
- var isPositiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
3464
- function encryptedEnvelopeProblem(parsed) {
3465
- if (!parsed.kdf || typeof parsed.data !== "string") {
3466
- return "encrypted envelope is missing its kdf or data";
3467
- }
3468
- const parts = decodeParts(parsed.data);
3469
- if (!parts) return "encrypted payload is not iv.tag.ciphertext";
3470
- if (parts.iv.length !== IV_BYTES) {
3471
- return `initialization vector is ${parts.iv.length} bytes, expected ${IV_BYTES}`;
3472
- }
3473
- if (parts.tag.length !== GCM_TAG_BYTES) {
3474
- return `authentication tag is ${parts.tag.length} bytes, expected ${GCM_TAG_BYTES}`;
3475
- }
3476
- if (parts.body.length === 0) return "ciphertext is empty";
3477
- const { algorithm, salt, N, r, p } = parsed.kdf;
3478
- if (algorithm !== "scrypt") return `unsupported kdf "${String(algorithm)}"`;
3479
- if (typeof salt !== "string" || Buffer.from(salt, "base64").length === 0) {
3480
- return "kdf salt is missing or not base64";
3481
- }
3482
- if (!isPositiveInt(N) || N < 2 || (N & N - 1) !== 0) {
3483
- return `kdf N is ${String(N)}, which is not a power of two above 1`;
3484
- }
3485
- if (!isPositiveInt(r) || !isPositiveInt(p)) {
3486
- return `kdf r/p are ${String(r)}/${String(p)}, which are not positive integers`;
3487
- }
3488
- if (N > SCRYPT_N || r > SCRYPT_R || p > SCRYPT_P) {
3489
- return `kdf cost parameters (N=${N}, r=${r}, p=${p}) exceed the supported maximum (N=${SCRYPT_N}, r=${SCRYPT_R}, p=${SCRYPT_P})`;
3490
- }
3491
- return null;
3356
+ return null;
3492
3357
  }
3493
3358
  var FileSecretStore = class {
3494
3359
  filePath;
3495
3360
  passphrase;
3361
+ /**
3362
+ * Why a configured key could not be obtained. While set, every read throws
3363
+ * and every write refuses — see {@link SecretPassphrase}.
3364
+ */
3365
+ keyProblem;
3496
3366
  constructor(options) {
3497
3367
  this.filePath = options.filePath;
3498
- const raw = options.passphrase ?? process.env[SECRET_KEY_ENV];
3368
+ const resolved2 = options.passphrase !== void 0 ? { passphrase: options.passphrase } : resolveSecretPassphrase(process.env, this.filePath);
3369
+ const raw = resolved2.passphrase;
3499
3370
  this.passphrase = raw && raw.trim() ? raw : void 0;
3371
+ this.keyProblem = resolved2.problem;
3500
3372
  }
3501
3373
  /**
3502
3374
  * True when the *next write* will encrypt. This is the configured policy,
@@ -3511,7 +3383,8 @@ var FileSecretStore = class {
3511
3383
  * no file yet, or when it cannot be read or parsed.
3512
3384
  *
3513
3385
  * Separate from {@link encrypted} because the two genuinely disagree for a
3514
- * whole session: adding `MCP_INSPECTOR_SECRET_KEY` to an install that
3386
+ * whole session: adding a passphrase (`MCP_INSPECTOR_SECRET_KEY` or
3387
+ * `MCP_INSPECTOR_SECRET_KEY_FILE`) to an install that
3515
3388
  * already has a plaintext file flips `encrypted` to true immediately,
3516
3389
  * while the bytes stay readable until the next `set`. A descriptor built
3517
3390
  * from the policy would tell that user "File (encrypted)" while their
@@ -3535,6 +3408,9 @@ var FileSecretStore = class {
3535
3408
  * `set` is in fact about to refuse.
3536
3409
  */
3537
3410
  async readOnDiskEncryption() {
3411
+ if (this.keyProblem !== void 0) {
3412
+ return { state: "unreadable", detail: this.keyProblem };
3413
+ }
3538
3414
  let raw;
3539
3415
  try {
3540
3416
  raw = await readStoreFile(this.filePath);
@@ -3572,7 +3448,7 @@ var FileSecretStore = class {
3572
3448
  } catch (err) {
3573
3449
  return {
3574
3450
  state: "unreadable",
3575
- detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV}` : err instanceof Error ? err.message : String(err)
3451
+ detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV} (or ${SECRET_KEY_FILE_ENV})` : err instanceof Error ? err.message : String(err)
3576
3452
  };
3577
3453
  }
3578
3454
  return { state: "encrypted" };
@@ -3623,6 +3499,11 @@ var FileSecretStore = class {
3623
3499
  return this.serialize(() => this.readMap());
3624
3500
  }
3625
3501
  async readMap() {
3502
+ if (this.keyProblem !== void 0) {
3503
+ throw new SecretStoreUnavailableError(
3504
+ `The secrets file at ${this.filePath} cannot be used: ${this.keyProblem}. Refusing to read or write it rather than store secrets unencrypted.`
3505
+ );
3506
+ }
3626
3507
  const raw = await readStoreFile(this.filePath);
3627
3508
  if (raw === null) return null;
3628
3509
  let parsed;
@@ -3659,7 +3540,9 @@ var FileSecretStore = class {
3659
3540
  let plain;
3660
3541
  try {
3661
3542
  const key = await this.deriveKey(parsed.kdf);
3662
- const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv);
3543
+ const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv, {
3544
+ authTagLength: GCM_TAG_BYTES
3545
+ });
3663
3546
  decipher.setAuthTag(parts.tag);
3664
3547
  plain = Buffer.concat([
3665
3548
  decipher.update(parts.body),
@@ -3693,7 +3576,9 @@ var FileSecretStore = class {
3693
3576
  };
3694
3577
  const key = await this.deriveKey(kdf);
3695
3578
  const iv = crypto2.randomBytes(IV_BYTES);
3696
- const cipher = crypto2.createCipheriv(CIPHER, key, iv);
3579
+ const cipher = crypto2.createCipheriv(CIPHER, key, iv, {
3580
+ authTagLength: GCM_TAG_BYTES
3581
+ });
3697
3582
  const body = Buffer.concat([
3698
3583
  cipher.update(JSON.stringify(map), "utf-8"),
3699
3584
  cipher.final()
@@ -3858,9 +3743,36 @@ var FileSecretStore = class {
3858
3743
  const found = {};
3859
3744
  for (const field of fields) {
3860
3745
  const value = map?.[buildAccount2(serverId, field)];
3861
- if (value !== void 0) found[field] = value;
3746
+ if (value !== void 0) setOwnEntry(found, field, value);
3747
+ }
3748
+ setOwnEntry(out, serverId, found);
3749
+ }
3750
+ return out;
3751
+ }
3752
+ /**
3753
+ * Bulk twin of {@link getStrict}: the same single-pass read as
3754
+ * {@link getMany}, but an unreadable store throws (with the typed advice
3755
+ * {@link getStrict} attaches) instead of yielding no fields — required by
3756
+ * OAuth read hydration, whose result later drives store deletions.
3757
+ */
3758
+ async getManyStrict(requests) {
3759
+ let map;
3760
+ try {
3761
+ map = await this.serialize(() => this.readMap());
3762
+ } catch (err) {
3763
+ if (err instanceof SecretStoreUnavailableError) throw err;
3764
+ throw new SecretStoreUnavailableError(
3765
+ `Could not read the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
3766
+ );
3767
+ }
3768
+ const out = {};
3769
+ for (const { serverId, fields } of requests) {
3770
+ const found = {};
3771
+ for (const field of fields) {
3772
+ const value = map?.[buildAccount2(serverId, field)];
3773
+ if (value !== void 0) setOwnEntry(found, field, value);
3862
3774
  }
3863
- out[serverId] = found;
3775
+ setOwnEntry(out, serverId, found);
3864
3776
  }
3865
3777
  return out;
3866
3778
  }
@@ -3904,6 +3816,9 @@ var FileSecretStore = class {
3904
3816
  */
3905
3817
  async setMany(serverId, values) {
3906
3818
  if (Object.keys(values).length === 0) return;
3819
+ for (const [field, value] of Object.entries(values)) {
3820
+ assertSecretString(field, value);
3821
+ }
3907
3822
  await this.serialize(
3908
3823
  () => this.mutate((map) => {
3909
3824
  const next = { ...map };
@@ -3915,6 +3830,7 @@ var FileSecretStore = class {
3915
3830
  );
3916
3831
  }
3917
3832
  async set(serverId, field, value) {
3833
+ assertSecretString(field, value);
3918
3834
  await this.serialize(
3919
3835
  () => this.mutate((map) => ({
3920
3836
  ...map,
@@ -3930,9 +3846,11 @@ var FileSecretStore = class {
3930
3846
  await this.deleteWhere((key) => key.startsWith(prefix));
3931
3847
  }
3932
3848
  /**
3933
- * Shared delete body. Silent on every failure, matching the keyring store:
3934
- * every reason a delete can fail collapses to "the entry isn't there
3935
- * anymore", and `set` is the operation that reports a broken store.
3849
+ * Shared delete body. Same confirmed-delete contract as the keyring
3850
+ * store: a deletion that cannot be confirmed (unreadable file, failed
3851
+ * decrypt, failed write) must escape, because reporting success would
3852
+ * let a caller commit state that assumes the entry is gone — once the
3853
+ * file becomes readable again, a later read would resurrect it.
3936
3854
  */
3937
3855
  async deleteWhere(match) {
3938
3856
  try {
@@ -3945,7 +3863,12 @@ var FileSecretStore = class {
3945
3863
  return next;
3946
3864
  })
3947
3865
  );
3948
- } catch {
3866
+ } catch (err) {
3867
+ if (err instanceof SecretStoreUnavailableError) throw err;
3868
+ throw new SecretStoreUnavailableError(
3869
+ `Could not delete from the secrets file: ${err instanceof Error ? err.message : String(err)}`,
3870
+ { cause: err }
3871
+ );
3949
3872
  }
3950
3873
  }
3951
3874
  };
@@ -3978,6 +3901,7 @@ async function readSecretFilePermissions(filePath) {
3978
3901
  var SECRET_STORE_ENV = "MCP_INSPECTOR_SECRET_STORE";
3979
3902
  var SECRET_FILE_ENV = "MCP_INSPECTOR_SECRET_FILE";
3980
3903
  var STORAGE_DIR_ENV = "MCP_STORAGE_DIR";
3904
+ var SECRET_STORAGE_DOCS_URL = "https://github.com/modelcontextprotocol/inspector/blob/main/docs/secret-storage.md";
3981
3905
  var KINDS = ["keyring", "file", "memory"];
3982
3906
  function parseSecretStoreEnv(raw) {
3983
3907
  const value = raw?.trim().toLowerCase();
@@ -4120,6 +4044,11 @@ function warnAboutSecretStorage(info) {
4120
4044
  }
4121
4045
  const caveat = secretStorageCaveat(info);
4122
4046
  if (caveat) console.warn(`[mcp-inspector] ${caveat}`);
4047
+ if (info.reason === "fallback" || caveat) {
4048
+ console.warn(
4049
+ `[mcp-inspector] How the secret store is chosen, and how to secure it: ${SECRET_STORAGE_DOCS_URL}`
4050
+ );
4051
+ }
4123
4052
  }
4124
4053
  var resolved;
4125
4054
  function resolveSecretStore() {
@@ -4200,150 +4129,1432 @@ async function handOffStagedSecrets(staged, filePath, keyring) {
4200
4129
  [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.`
4201
4130
  );
4202
4131
  }
4203
- return;
4132
+ return;
4133
+ }
4134
+ try {
4135
+ await fs4.link(staged, filePath);
4136
+ await fs4.rm(staged, { force: true });
4137
+ } catch {
4138
+ console.warn(
4139
+ `
4140
+ [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.`
4141
+ );
4142
+ }
4143
+ }
4144
+ var isSnapshotName = (name, base) => name.startsWith(`${base}.migrating-`) && !name.endsWith(".lock");
4145
+ async function anythingToMigrate(filePath) {
4146
+ const base = path4.basename(filePath);
4147
+ try {
4148
+ return (await fs4.readdir(path4.dirname(filePath))).some(
4149
+ (name) => name === base || isSnapshotName(name, base)
4150
+ );
4151
+ } catch (err) {
4152
+ const code = err.code;
4153
+ return code !== "ENOENT" && code !== "ENOTDIR";
4154
+ }
4155
+ }
4156
+ async function recoverOrphanedSnapshots(filePath) {
4157
+ const dir = path4.dirname(filePath);
4158
+ const base = path4.basename(filePath);
4159
+ let names;
4160
+ try {
4161
+ names = (await fs4.readdir(dir)).filter((n) => isSnapshotName(n, base));
4162
+ } catch {
4163
+ return;
4164
+ }
4165
+ for (const name of names) {
4166
+ const orphan = path4.join(dir, name);
4167
+ if (await isFileLockHeld(orphan)) continue;
4168
+ try {
4169
+ await fs4.link(orphan, filePath);
4170
+ await fs4.rm(orphan, { force: true });
4171
+ console.warn(
4172
+ `
4173
+ [mcp-inspector] Recovered secrets left behind by an interrupted migration (${orphan} \u2192 ${filePath}).`
4174
+ );
4175
+ } catch {
4176
+ console.warn(
4177
+ `
4178
+ [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.`
4179
+ );
4180
+ }
4181
+ }
4182
+ }
4183
+ async function handOff(file, filePath, keyring) {
4184
+ let entries;
4185
+ try {
4186
+ entries = await file.readAll();
4187
+ } catch (err) {
4188
+ console.warn(
4189
+ `
4190
+ [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)}`
4191
+ );
4192
+ return false;
4193
+ }
4194
+ if (!entries || Object.keys(entries).length === 0) return false;
4195
+ let complete = true;
4196
+ try {
4197
+ for (const [account, value] of Object.entries(entries)) {
4198
+ const parsed = parseAccount(account);
4199
+ if (!parsed) {
4200
+ complete = false;
4201
+ continue;
4202
+ }
4203
+ const existing = await secretStoreGetStrict(
4204
+ keyring,
4205
+ parsed.serverId,
4206
+ parsed.field
4207
+ );
4208
+ if (existing === null) {
4209
+ await keyring.set(parsed.serverId, parsed.field, value);
4210
+ }
4211
+ }
4212
+ } catch (err) {
4213
+ console.warn(
4214
+ `
4215
+ [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)}`
4216
+ );
4217
+ return false;
4218
+ }
4219
+ if (!complete) {
4220
+ console.warn(
4221
+ `
4222
+ [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.`
4223
+ );
4224
+ return false;
4225
+ }
4226
+ return true;
4227
+ }
4228
+ async function getSecretStorageInfo() {
4229
+ const { store, info } = await resolveSecretStore();
4230
+ if (info.kind !== "file" || !(store instanceof FileSecretStore)) return info;
4231
+ return describeFileStore(
4232
+ store,
4233
+ store.filePath,
4234
+ "file",
4235
+ info.reason,
4236
+ info.detail
4237
+ );
4238
+ }
4239
+ var DeferredSecretStore = class {
4240
+ async target() {
4241
+ return (await resolveSecretStore()).store;
4242
+ }
4243
+ async isDurable() {
4244
+ return secretStoreIsDurable(await this.target());
4245
+ }
4246
+ /**
4247
+ * Forwarded, and its absence was a real defect rather than an omission:
4248
+ * this is the store production actually uses, so a `secretStoreGetStrict`
4249
+ * call against it fell through to the tolerant `get` and the strictness
4250
+ * existed only in tests that injected a concrete store.
4251
+ */
4252
+ async getStrict(serverId, field) {
4253
+ return secretStoreGetStrict(await this.target(), serverId, field);
4254
+ }
4255
+ async getMany(requests) {
4256
+ return secretStoreGetMany(await this.target(), requests);
4257
+ }
4258
+ async getManyStrict(requests) {
4259
+ return secretStoreGetManyStrict(await this.target(), requests);
4260
+ }
4261
+ async get(serverId, field) {
4262
+ return (await this.target()).get(serverId, field);
4263
+ }
4264
+ async set(serverId, field, value) {
4265
+ await (await this.target()).set(serverId, field, value);
4266
+ }
4267
+ async setMany(serverId, values) {
4268
+ await secretStoreSetMany(await this.target(), serverId, values);
4269
+ }
4270
+ async delete(serverId, field) {
4271
+ await (await this.target()).delete(serverId, field);
4272
+ }
4273
+ async deleteAllForServer(serverId) {
4274
+ await (await this.target()).deleteAllForServer(serverId);
4275
+ }
4276
+ };
4277
+ function defaultSecretStore() {
4278
+ return new DeferredSecretStore();
4279
+ }
4280
+
4281
+ // ../../core/auth/node/oauth-secrets.ts
4282
+ import { OAuthTokensSchema as OAuthTokensSchema2 } from "@modelcontextprotocol/core";
4283
+ var PERSIST_TOKENS_ENV = "MCP_INSPECTOR_PERSIST_TOKENS";
4284
+ var warnedPolicyValues = /* @__PURE__ */ new Set();
4285
+ function getPersistTokensPolicy(env = process.env) {
4286
+ const raw = env[PERSIST_TOKENS_ENV];
4287
+ if (raw === void 0 || raw === "") return "all";
4288
+ if (raw === "all" || raw === "access" || raw === "none") return raw;
4289
+ if (!warnedPolicyValues.has(raw)) {
4290
+ warnedPolicyValues.add(raw);
4291
+ console.warn(
4292
+ `[mcp-inspector] Ignoring invalid ${PERSIST_TOKENS_ENV}=${JSON.stringify(raw)} (expected "all", "access", or "none"); persisting all tokens.`
4293
+ );
4294
+ }
4295
+ return "all";
4296
+ }
4297
+ var oauthSecretServerId = (serverUrl) => `oauth+${encodeURIComponent(serverUrl)}`;
4298
+ var oauthIdpSecretServerId = (issuer) => `oauth-idp+${encodeURIComponent(issuer)}`;
4299
+ var issuerTokensField = (issuer) => `tokens:${issuer}`;
4300
+ var issuerClientSecretField = (issuer) => `client-secret:${issuer}`;
4301
+ var issuerRegistrationTokenField = (issuer) => `registration-token:${issuer}`;
4302
+ var LEGACY_TOKENS_FIELD = "tokens";
4303
+ var LEGACY_CLIENT_SECRET_FIELD = "client-secret";
4304
+ var LEGACY_REGISTRATION_TOKEN_FIELD = "registration-token";
4305
+ var PREREG_CLIENT_SECRET_FIELD = "prereg-client-secret";
4306
+ var PREREG_REGISTRATION_TOKEN_FIELD = "prereg-registration-token";
4307
+ var IDP_SESSION_FIELD = "idp-session";
4308
+ function applyTokensPolicy(tokens, policy) {
4309
+ if (policy === "none") return void 0;
4310
+ if (policy === "access" && tokens.refresh_token !== void 0) {
4311
+ const { refresh_token: _stripped, ...rest } = tokens;
4312
+ return rest;
4313
+ }
4314
+ return tokens;
4315
+ }
4316
+ var StorableTokensSchema = OAuthTokensSchema2.partial();
4317
+ function hasSecretBearingTokenField(tokens) {
4318
+ return tokens.access_token !== void 0 || tokens.refresh_token !== void 0 || tokens.id_token !== void 0;
4319
+ }
4320
+ function splitTokens(tokens, policy) {
4321
+ const kept = applyTokensPolicy(tokens, policy);
4322
+ if (!kept) return {};
4323
+ if (!StorableTokensSchema.safeParse(kept).success) {
4324
+ return { plaintext: kept };
4325
+ }
4326
+ if (!hasSecretBearingTokenField(kept)) {
4327
+ return {};
4328
+ }
4329
+ return { secret: JSON.stringify(kept) };
4330
+ }
4331
+ function splitClientInformation(info, fields, secrets) {
4332
+ const { client_secret, registration_access_token, ...publicInfo } = info;
4333
+ if (client_secret === void 0 && registration_access_token === void 0) {
4334
+ return info;
4335
+ }
4336
+ if (client_secret !== void 0) {
4337
+ secrets[fields.clientSecret] = client_secret;
4338
+ }
4339
+ if (registration_access_token !== void 0) {
4340
+ secrets[fields.registrationToken] = registration_access_token;
4341
+ }
4342
+ return publicInfo;
4343
+ }
4344
+ function joinClientInformation(info, fields, secrets) {
4345
+ const clientSecret = secrets[fields.clientSecret];
4346
+ const registrationToken = secrets[fields.registrationToken];
4347
+ if (clientSecret === void 0 && registrationToken === void 0) {
4348
+ return info;
4349
+ }
4350
+ return {
4351
+ ...info,
4352
+ ...clientSecret !== void 0 && { client_secret: clientSecret },
4353
+ ...registrationToken !== void 0 && {
4354
+ registration_access_token: registrationToken
4355
+ }
4356
+ };
4357
+ }
4358
+ var issuerClientInfoFields = (issuer) => ({
4359
+ clientSecret: issuerClientSecretField(issuer),
4360
+ registrationToken: issuerRegistrationTokenField(issuer)
4361
+ });
4362
+ var LEGACY_CLIENT_INFO_FIELDS = {
4363
+ clientSecret: LEGACY_CLIENT_SECRET_FIELD,
4364
+ registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD
4365
+ };
4366
+ var PREREG_CLIENT_INFO_FIELDS = {
4367
+ clientSecret: PREREG_CLIENT_SECRET_FIELD,
4368
+ registrationToken: PREREG_REGISTRATION_TOKEN_FIELD
4369
+ };
4370
+ function splitServerOAuthState(state, policy) {
4371
+ const secrets = {};
4372
+ const residue = { ...state };
4373
+ if (state.byIssuer) {
4374
+ const byIssuer = {};
4375
+ for (const [issuer, slot] of Object.entries(state.byIssuer)) {
4376
+ const slotResidue = { ...slot };
4377
+ if (slot.tokens) {
4378
+ delete slotResidue.tokens;
4379
+ const tokenSplit = splitTokens(slot.tokens, policy);
4380
+ if (tokenSplit.secret !== void 0) {
4381
+ secrets[issuerTokensField(issuer)] = tokenSplit.secret;
4382
+ }
4383
+ if (tokenSplit.plaintext) slotResidue.tokens = tokenSplit.plaintext;
4384
+ }
4385
+ if (slot.clientInformation) {
4386
+ slotResidue.clientInformation = splitClientInformation(
4387
+ slot.clientInformation,
4388
+ issuerClientInfoFields(issuer),
4389
+ secrets
4390
+ );
4391
+ }
4392
+ setOwnEntry(byIssuer, issuer, slotResidue);
4393
+ }
4394
+ residue.byIssuer = byIssuer;
4395
+ }
4396
+ if (state.tokens) {
4397
+ delete residue.tokens;
4398
+ const tokenSplit = splitTokens(state.tokens, policy);
4399
+ if (tokenSplit.secret !== void 0) {
4400
+ secrets[LEGACY_TOKENS_FIELD] = tokenSplit.secret;
4401
+ }
4402
+ if (tokenSplit.plaintext) residue.tokens = tokenSplit.plaintext;
4403
+ }
4404
+ if (state.clientInformation) {
4405
+ residue.clientInformation = splitClientInformation(
4406
+ state.clientInformation,
4407
+ LEGACY_CLIENT_INFO_FIELDS,
4408
+ secrets
4409
+ );
4410
+ }
4411
+ if (state.preregisteredClientInformation) {
4412
+ residue.preregisteredClientInformation = splitClientInformation(
4413
+ state.preregisteredClientInformation,
4414
+ PREREG_CLIENT_INFO_FIELDS,
4415
+ secrets
4416
+ );
4417
+ }
4418
+ return { residue, secrets };
4419
+ }
4420
+ function parseStoredTokens(raw) {
4421
+ try {
4422
+ const parsed = JSON.parse(raw);
4423
+ if (StorableTokensSchema.safeParse(parsed).success && hasSecretBearingTokenField(parsed)) {
4424
+ return parsed;
4425
+ }
4426
+ } catch {
4427
+ }
4428
+ return void 0;
4429
+ }
4430
+ function parseStoredIdpSession(raw) {
4431
+ try {
4432
+ const parsed = JSON.parse(raw);
4433
+ if (typeof parsed === "object" && parsed !== null) {
4434
+ const { idToken, refreshToken } = parsed;
4435
+ const usable = {
4436
+ ...typeof idToken === "string" && { idToken },
4437
+ ...typeof refreshToken === "string" && { refreshToken }
4438
+ };
4439
+ if (Object.keys(usable).length > 0) return usable;
4440
+ }
4441
+ } catch {
4442
+ }
4443
+ return void 0;
4444
+ }
4445
+ function isUsableStoredSecret(field, raw) {
4446
+ if (field === LEGACY_TOKENS_FIELD || field.startsWith("tokens:")) {
4447
+ return parseStoredTokens(raw) !== void 0;
4448
+ }
4449
+ if (field === IDP_SESSION_FIELD) {
4450
+ return parseStoredIdpSession(raw) !== void 0;
4451
+ }
4452
+ return true;
4453
+ }
4454
+ function joinServerOAuthState(residue, secrets) {
4455
+ const joined = { ...residue };
4456
+ if (residue.byIssuer) {
4457
+ const byIssuer = {};
4458
+ for (const [issuer, slot] of Object.entries(residue.byIssuer)) {
4459
+ const joinedSlot = { ...slot };
4460
+ const tokensRaw = secrets[issuerTokensField(issuer)];
4461
+ if (tokensRaw !== void 0) {
4462
+ const tokens = parseStoredTokens(tokensRaw);
4463
+ if (tokens) joinedSlot.tokens = tokens;
4464
+ }
4465
+ if (joinedSlot.clientInformation) {
4466
+ joinedSlot.clientInformation = joinClientInformation(
4467
+ joinedSlot.clientInformation,
4468
+ issuerClientInfoFields(issuer),
4469
+ secrets
4470
+ );
4471
+ }
4472
+ setOwnEntry(byIssuer, issuer, joinedSlot);
4473
+ }
4474
+ joined.byIssuer = byIssuer;
4475
+ }
4476
+ const legacyTokensRaw = secrets[LEGACY_TOKENS_FIELD];
4477
+ if (legacyTokensRaw !== void 0) {
4478
+ const tokens = parseStoredTokens(legacyTokensRaw);
4479
+ if (tokens) joined.tokens = tokens;
4480
+ }
4481
+ if (joined.clientInformation) {
4482
+ joined.clientInformation = joinClientInformation(
4483
+ joined.clientInformation,
4484
+ LEGACY_CLIENT_INFO_FIELDS,
4485
+ secrets
4486
+ );
4487
+ }
4488
+ if (joined.preregisteredClientInformation) {
4489
+ joined.preregisteredClientInformation = joinClientInformation(
4490
+ joined.preregisteredClientInformation,
4491
+ PREREG_CLIENT_INFO_FIELDS,
4492
+ secrets
4493
+ );
4494
+ }
4495
+ return joined;
4496
+ }
4497
+ function splitIdpSession(session, policy) {
4498
+ const { idToken, refreshToken, ...residue } = session;
4499
+ const secrets = {};
4500
+ if (policy !== "none" && (idToken !== void 0 || refreshToken !== void 0)) {
4501
+ const kept = {
4502
+ ...typeof idToken === "string" && { idToken },
4503
+ ...policy === "all" && typeof refreshToken === "string" && { refreshToken }
4504
+ };
4505
+ if (Object.keys(kept).length > 0) {
4506
+ secrets[IDP_SESSION_FIELD] = JSON.stringify(kept);
4507
+ }
4508
+ }
4509
+ return { residue, secrets };
4510
+ }
4511
+ function joinIdpSession(residue, secrets) {
4512
+ const raw = secrets[IDP_SESSION_FIELD];
4513
+ if (raw === void 0) return { ...residue };
4514
+ const stored = parseStoredIdpSession(raw);
4515
+ return stored ? { ...residue, ...stored } : { ...residue };
4516
+ }
4517
+ function serverSecretFields(state) {
4518
+ const fields = /* @__PURE__ */ new Set([
4519
+ LEGACY_TOKENS_FIELD,
4520
+ LEGACY_CLIENT_SECRET_FIELD,
4521
+ LEGACY_REGISTRATION_TOKEN_FIELD,
4522
+ PREREG_CLIENT_SECRET_FIELD,
4523
+ PREREG_REGISTRATION_TOKEN_FIELD
4524
+ ]);
4525
+ for (const issuer of Object.keys(state?.byIssuer ?? {})) {
4526
+ fields.add(issuerTokensField(issuer));
4527
+ fields.add(issuerClientSecretField(issuer));
4528
+ fields.add(issuerRegistrationTokenField(issuer));
4529
+ }
4530
+ return [...fields];
4531
+ }
4532
+ function clientInfoHasPlaintext(info) {
4533
+ return info !== void 0 && (info.client_secret !== void 0 || info.registration_access_token !== void 0);
4534
+ }
4535
+ function snapshotHasPlaintextSecrets(snapshot) {
4536
+ for (const state of Object.values(snapshot.servers)) {
4537
+ if (state.tokens) return true;
4538
+ if (clientInfoHasPlaintext(state.clientInformation)) return true;
4539
+ if (clientInfoHasPlaintext(state.preregisteredClientInformation)) {
4540
+ return true;
4541
+ }
4542
+ for (const slot of Object.values(state.byIssuer ?? {})) {
4543
+ if (slot.tokens) return true;
4544
+ if (clientInfoHasPlaintext(slot.clientInformation)) return true;
4545
+ }
4546
+ }
4547
+ for (const session of Object.values(snapshot.idpSessions)) {
4548
+ if (session.idToken !== void 0 || session.refreshToken !== void 0) {
4549
+ return true;
4550
+ }
4551
+ }
4552
+ return false;
4553
+ }
4554
+
4555
+ // ../../core/auth/node/oauth-persist-file.ts
4556
+ var warnedStoreFailures = /* @__PURE__ */ new Set();
4557
+ function warnStoreWriteFailure(error) {
4558
+ const reason = error instanceof Error ? error.message : String(error);
4559
+ if (warnedStoreFailures.has(reason)) return;
4560
+ warnedStoreFailures.add(reason);
4561
+ console.warn(
4562
+ `[mcp-inspector] Could not write OAuth tokens to the secret store (${reason}). Tokens will be kept in memory for this session only and will NOT be persisted; expect to re-authorize after a restart.`
4563
+ );
4564
+ }
4565
+ function warnRestoreFailure(error) {
4566
+ const reason = error instanceof Error ? error.message : String(error);
4567
+ const key = `restore:${reason}`;
4568
+ if (warnedStoreFailures.has(key)) return;
4569
+ warnedStoreFailures.add(key);
4570
+ console.warn(
4571
+ `[mcp-inspector] Could not restore secret-store entries after a failed OAuth state write (${reason}). The secret store may be inconsistent with oauth.json; if a server's stored credentials stop working, re-authorize it.`
4572
+ );
4573
+ }
4574
+ function warnUnconfirmedFile(error) {
4575
+ const reason = error instanceof Error ? error.message : String(error);
4576
+ const key = `unconfirmed:${reason}`;
4577
+ if (warnedStoreFailures.has(key)) return;
4578
+ warnedStoreFailures.add(key);
4579
+ console.warn(
4580
+ `[mcp-inspector] Could not re-read oauth.json after a failed OAuth state save (${reason}). The file may still contain the interrupted save while the secret store was restored to the previous values; if a server's stored credentials stop working, re-authorize it.`
4581
+ );
4582
+ }
4583
+ function warnMigrationFailure(error) {
4584
+ const reason = error instanceof Error ? error.message : String(error);
4585
+ const key = `migration:${reason}`;
4586
+ if (warnedStoreFailures.has(key)) return;
4587
+ warnedStoreFailures.add(key);
4588
+ console.warn(
4589
+ `[mcp-inspector] Could not migrate plaintext OAuth secrets into the secret store (${reason}). The plaintext copy in oauth.json was kept and keeps working; migration will be retried on the next read.`
4590
+ );
4591
+ }
4592
+ function rethrowLockError(filePath, error, action = "save") {
4593
+ if (error instanceof SecretFileLockHeldError) {
4594
+ throw new SecretFileLockHeldError(
4595
+ `Could not ${action} OAuth state: the state file at ${filePath} is locked by another Inspector process and did not become available.`,
4596
+ { cause: error }
4597
+ );
4598
+ }
4599
+ throw error;
4600
+ }
4601
+ async function withOAuthStateLock(filePath, action, body) {
4602
+ let entered = false;
4603
+ try {
4604
+ return await withSecretFileLock(filePath, async () => {
4605
+ entered = true;
4606
+ return body();
4607
+ });
4608
+ } catch (error) {
4609
+ if (!entered) rethrowLockError(filePath, error, action);
4610
+ throw error;
4611
+ }
4612
+ }
4613
+ var OAuthStateFileUnrecognizedError = class extends Error {
4614
+ constructor(filePath, action) {
4615
+ super(
4616
+ `Refusing to ${action} OAuth state: ${filePath} exists but is not a recognized OAuth state file (it may be corrupt, truncated, or written by something else). Its entries are the only index of credentials in the OS keychain / secret store, so overwriting it would strand them. Restore the file from a backup or fix its JSON; deleting it starts fresh but abandons any credentials it indexed.`
4617
+ );
4618
+ this.name = "OAuthStateFileUnrecognizedError";
4619
+ }
4620
+ };
4621
+ async function readDiskForMutation(filePath, action) {
4622
+ const raw = await readStoreFile(filePath);
4623
+ const parsed = parseOAuthPersistBlob(raw);
4624
+ if (raw !== null && parsed === null) {
4625
+ throw new OAuthStateFileUnrecognizedError(filePath, action);
4626
+ }
4627
+ return parsed;
4628
+ }
4629
+ async function persistEntrySecrets(store, serverId, candidates, secrets, prior, allowDegrade) {
4630
+ const priorValue = new Map(prior.map(({ field, value }) => [field, value]));
4631
+ const changed = {};
4632
+ for (const [field, value] of Object.entries(secrets)) {
4633
+ if (priorValue.get(field) !== value) changed[field] = value;
4634
+ }
4635
+ try {
4636
+ if (Object.keys(changed).length > 0) {
4637
+ await secretStoreSetMany(store, serverId, changed);
4638
+ }
4639
+ } catch (error) {
4640
+ if (!allowDegrade) throw error;
4641
+ warnStoreWriteFailure(error);
4642
+ const touched = prior.filter(({ field }) => changed[field] !== void 0);
4643
+ let restoreFailure;
4644
+ await restoreSecretFields(store, touched, (err) => {
4645
+ restoreFailure = err;
4646
+ });
4647
+ if (restoreFailure !== void 0) throw restoreFailure;
4648
+ return false;
4649
+ }
4650
+ await settleStoreMutations(
4651
+ candidates.filter(
4652
+ (field) => secrets[field] === void 0 && priorValue.get(field) !== null
4653
+ ).map((field) => store.delete(serverId, field))
4654
+ );
4655
+ return true;
4656
+ }
4657
+ function revertEntryToDisk(merged, disk, key) {
4658
+ const diskEntry = getOwnEntry(disk, key);
4659
+ if (diskEntry === void 0) {
4660
+ if (Object.hasOwn(merged, key)) delete merged[key];
4661
+ } else {
4662
+ setOwnEntry(merged, key, diskEntry);
4663
+ }
4664
+ }
4665
+ function preserveNonDurableSecrets(diskSecrets, secrets) {
4666
+ const keep = {};
4667
+ for (const [field, value] of Object.entries(secrets)) {
4668
+ if (diskSecrets[field] === value) keep[field] = value;
4669
+ }
4670
+ return keep;
4671
+ }
4672
+ async function writeOAuthSections(filePath, snapshot, sections, secretStore = defaultSecretStore()) {
4673
+ const policy = getPersistTokensPolicy();
4674
+ const durable = await secretStoreIsDurable(secretStore);
4675
+ await withOAuthStateLock(filePath, "save", async () => {
4676
+ const restoreBaseline = /* @__PURE__ */ new Map();
4677
+ const ourWrites = /* @__PURE__ */ new Map();
4678
+ const priorKey = (p) => `${p.serverId}\0${p.field}`;
4679
+ const foldPriors = (priors) => {
4680
+ for (const prior of priors) {
4681
+ const key = priorKey(prior);
4682
+ if (restoreBaseline.has(key) && ourWrites.get(key) !== void 0 && prior.value === ourWrites.get(key)) {
4683
+ continue;
4684
+ }
4685
+ restoreBaseline.set(key, prior);
4686
+ }
4687
+ };
4688
+ const restoreToBaseline = () => restoreSecretFields(
4689
+ secretStore,
4690
+ [...restoreBaseline.values()],
4691
+ warnRestoreFailure
4692
+ );
4693
+ let unconfirmed = null;
4694
+ const reconcileFailure = async () => {
4695
+ if (unconfirmed !== null) {
4696
+ const committed = unconfirmed;
4697
+ let observed = null;
4698
+ let readable = true;
4699
+ try {
4700
+ observed = await readStoreFile(filePath);
4701
+ } catch (error) {
4702
+ readable = false;
4703
+ warnUnconfirmedFile(error);
4704
+ }
4705
+ if (readable && observed === committed.written) {
4706
+ const target = new Map(
4707
+ [...restoreBaseline].map(([key, { serverId, field, value }]) => [
4708
+ key,
4709
+ { serverId, field, value }
4710
+ ])
4711
+ );
4712
+ for (const [key, write] of committed.writes) target.set(key, write);
4713
+ try {
4714
+ await settleStoreMutations(
4715
+ [...target.values()].map(
4716
+ ({ serverId, field, value }) => value === null ? secretStore.delete(serverId, field) : secretStore.set(serverId, field, value)
4717
+ )
4718
+ );
4719
+ return true;
4720
+ } catch (error) {
4721
+ warnRestoreFailure(error);
4722
+ return false;
4723
+ }
4724
+ }
4725
+ }
4726
+ await restoreToBaseline();
4727
+ return false;
4728
+ };
4729
+ for (let attempt = 0; attempt < MAX_WRITE_ATTEMPTS; attempt++) {
4730
+ const priorSecrets = [];
4731
+ const attemptWrites = /* @__PURE__ */ new Map();
4732
+ let written;
4733
+ try {
4734
+ const disk = await readDiskForMutation(filePath, "save");
4735
+ const effective = sections ? {
4736
+ servers: sections.servers && [...new Set(sections.servers)],
4737
+ idpSessions: sections.idpSessions && [
4738
+ ...new Set(sections.idpSessions)
4739
+ ]
4740
+ } : {
4741
+ servers: [
4742
+ .../* @__PURE__ */ new Set([
4743
+ ...Object.keys(disk?.servers ?? {}),
4744
+ ...Object.keys(snapshot.servers)
4745
+ ])
4746
+ ],
4747
+ idpSessions: [
4748
+ .../* @__PURE__ */ new Set([
4749
+ ...Object.keys(disk?.idpSessions ?? {}),
4750
+ ...Object.keys(snapshot.idpSessions)
4751
+ ])
4752
+ ]
4753
+ };
4754
+ const merged = mergeOAuthSections(disk, snapshot, effective);
4755
+ for (const url of effective.servers ?? []) {
4756
+ const serverId = oauthSecretServerId(url);
4757
+ const next = getOwnEntry(snapshot.servers, url);
4758
+ const candidates = [
4759
+ .../* @__PURE__ */ new Set([
4760
+ ...serverSecretFields(getOwnEntry(disk?.servers, url)),
4761
+ ...serverSecretFields(next)
4762
+ ])
4763
+ ];
4764
+ const entryPrior = await snapshotSecretFields(
4765
+ secretStore,
4766
+ serverId,
4767
+ candidates
4768
+ );
4769
+ priorSecrets.push(...entryPrior);
4770
+ if (next === void 0) {
4771
+ for (const field of candidates) {
4772
+ ourWrites.set(priorKey({ serverId, field }), null);
4773
+ }
4774
+ await secretStore.deleteAllForServer(serverId);
4775
+ for (const field of candidates) {
4776
+ attemptWrites.set(priorKey({ serverId, field }), {
4777
+ serverId,
4778
+ field,
4779
+ value: null
4780
+ });
4781
+ }
4782
+ continue;
4783
+ }
4784
+ const { residue, secrets } = splitServerOAuthState(next, policy);
4785
+ for (const field of candidates) {
4786
+ ourWrites.set(
4787
+ priorKey({ serverId, field }),
4788
+ secrets[field] ?? null
4789
+ );
4790
+ }
4791
+ setOwnEntry(merged.servers, url, residue);
4792
+ if (!durable) {
4793
+ const diskEntry = getOwnEntry(disk?.servers, url);
4794
+ const keep = preserveNonDurableSecrets(
4795
+ diskEntry ? splitServerOAuthState(diskEntry, policy).secrets : {},
4796
+ secrets
4797
+ );
4798
+ if (Object.keys(keep).length > 0) {
4799
+ setOwnEntry(
4800
+ merged.servers,
4801
+ url,
4802
+ joinServerOAuthState(residue, keep)
4803
+ );
4804
+ }
4805
+ }
4806
+ const persisted = await persistEntrySecrets(
4807
+ secretStore,
4808
+ serverId,
4809
+ candidates,
4810
+ secrets,
4811
+ entryPrior,
4812
+ // Degrading is only sound when the disk entry is the pre-call
4813
+ // state this attempt's priors pair with; on a retry it may be an
4814
+ // earlier attempt's committed residue, so escalate into the
4815
+ // reconciling exit instead.
4816
+ attempt === 0
4817
+ );
4818
+ if (!persisted) {
4819
+ revertEntryToDisk(merged.servers, disk?.servers, url);
4820
+ continue;
4821
+ }
4822
+ for (const field of candidates) {
4823
+ attemptWrites.set(priorKey({ serverId, field }), {
4824
+ serverId,
4825
+ field,
4826
+ value: secrets[field] ?? null
4827
+ });
4828
+ }
4829
+ }
4830
+ for (const issuer of effective.idpSessions ?? []) {
4831
+ const serverId = oauthIdpSecretServerId(issuer);
4832
+ const next = getOwnEntry(snapshot.idpSessions, issuer);
4833
+ const entryPrior = await snapshotSecretFields(secretStore, serverId, [
4834
+ IDP_SESSION_FIELD
4835
+ ]);
4836
+ priorSecrets.push(...entryPrior);
4837
+ if (next === void 0) {
4838
+ ourWrites.set(
4839
+ priorKey({ serverId, field: IDP_SESSION_FIELD }),
4840
+ null
4841
+ );
4842
+ await secretStore.deleteAllForServer(serverId);
4843
+ attemptWrites.set(
4844
+ priorKey({ serverId, field: IDP_SESSION_FIELD }),
4845
+ {
4846
+ serverId,
4847
+ field: IDP_SESSION_FIELD,
4848
+ value: null
4849
+ }
4850
+ );
4851
+ continue;
4852
+ }
4853
+ const { residue, secrets } = splitIdpSession(next, policy);
4854
+ ourWrites.set(
4855
+ priorKey({ serverId, field: IDP_SESSION_FIELD }),
4856
+ secrets[IDP_SESSION_FIELD] ?? null
4857
+ );
4858
+ setOwnEntry(merged.idpSessions, issuer, residue);
4859
+ if (!durable) {
4860
+ const diskSession = getOwnEntry(disk?.idpSessions, issuer);
4861
+ const keep = preserveNonDurableSecrets(
4862
+ diskSession ? splitIdpSession(diskSession, policy).secrets : {},
4863
+ secrets
4864
+ );
4865
+ if (Object.keys(keep).length > 0) {
4866
+ setOwnEntry(
4867
+ merged.idpSessions,
4868
+ issuer,
4869
+ joinIdpSession(residue, keep)
4870
+ );
4871
+ }
4872
+ }
4873
+ const persisted = await persistEntrySecrets(
4874
+ secretStore,
4875
+ serverId,
4876
+ [IDP_SESSION_FIELD],
4877
+ secrets,
4878
+ entryPrior,
4879
+ // Same as the server loop: degrade on the first attempt only.
4880
+ attempt === 0
4881
+ );
4882
+ if (!persisted) {
4883
+ revertEntryToDisk(merged.idpSessions, disk?.idpSessions, issuer);
4884
+ continue;
4885
+ }
4886
+ attemptWrites.set(priorKey({ serverId, field: IDP_SESSION_FIELD }), {
4887
+ serverId,
4888
+ field: IDP_SESSION_FIELD,
4889
+ value: secrets[IDP_SESSION_FIELD] ?? null
4890
+ });
4891
+ }
4892
+ written = serializeOAuthPersistBlob(merged);
4893
+ await writeStoreFile(filePath, written);
4894
+ unconfirmed = { written, writes: attemptWrites };
4895
+ } catch (error) {
4896
+ foldPriors(priorSecrets);
4897
+ if (await reconcileFailure()) return;
4898
+ throw error;
4899
+ }
4900
+ foldPriors(priorSecrets);
4901
+ let observed;
4902
+ try {
4903
+ observed = await readStoreFile(filePath);
4904
+ } catch {
4905
+ continue;
4906
+ }
4907
+ if (observed === written) return;
4908
+ unconfirmed = null;
4909
+ }
4910
+ if (await reconcileFailure()) return;
4911
+ throw new SecretStoreUnavailableError(
4912
+ `Could not save OAuth state: another process kept overwriting ${filePath} (gave up after ${MAX_WRITE_ATTEMPTS} attempts). Re-run the action to retry.`
4913
+ );
4914
+ });
4915
+ }
4916
+ function secretRequestsFor(snapshot) {
4917
+ const requests = [];
4918
+ for (const [url, state] of Object.entries(snapshot.servers)) {
4919
+ requests.push({
4920
+ serverId: oauthSecretServerId(url),
4921
+ fields: serverSecretFields(state)
4922
+ });
4923
+ }
4924
+ for (const issuer of Object.keys(snapshot.idpSessions)) {
4925
+ requests.push({
4926
+ serverId: oauthIdpSecretServerId(issuer),
4927
+ fields: [IDP_SESSION_FIELD]
4928
+ });
4929
+ }
4930
+ return requests;
4931
+ }
4932
+ async function joinSnapshot(snapshot, secretStore) {
4933
+ const requests = secretRequestsFor(snapshot);
4934
+ const values = requests.length > 0 ? await secretStoreGetManyStrict(secretStore, requests) : {};
4935
+ return {
4936
+ servers: Object.fromEntries(
4937
+ Object.entries(snapshot.servers).map(([url, state]) => [
4938
+ url,
4939
+ joinServerOAuthState(state, values[oauthSecretServerId(url)] ?? {})
4940
+ ])
4941
+ ),
4942
+ idpSessions: Object.fromEntries(
4943
+ Object.entries(snapshot.idpSessions).map(([issuer, session]) => [
4944
+ issuer,
4945
+ joinIdpSession(session, values[oauthIdpSecretServerId(issuer)] ?? {})
4946
+ ])
4947
+ )
4948
+ };
4949
+ }
4950
+ async function migratePlaintextSecrets(filePath, secretStore) {
4951
+ const raw = await readStoreFile(filePath);
4952
+ const fresh = parseOAuthPersistBlob(raw);
4953
+ if (!fresh || !snapshotHasPlaintextSecrets(fresh)) return;
4954
+ const migrateEntrySecrets = async (serverId, secrets) => {
4955
+ const absent = {};
4956
+ for (const [field, value] of Object.entries(secrets)) {
4957
+ const existing = await secretStoreGetStrict(secretStore, serverId, field);
4958
+ if (existing === null || !isUsableStoredSecret(field, existing)) {
4959
+ absent[field] = value;
4960
+ }
4961
+ }
4962
+ if (Object.keys(absent).length > 0) {
4963
+ await secretStoreSetMany(secretStore, serverId, absent);
4964
+ }
4965
+ };
4966
+ const residue = { servers: {}, idpSessions: {} };
4967
+ for (const [url, state] of Object.entries(fresh.servers)) {
4968
+ const split = splitServerOAuthState(state, "all");
4969
+ setOwnEntry(residue.servers, url, split.residue);
4970
+ await migrateEntrySecrets(oauthSecretServerId(url), split.secrets);
4971
+ }
4972
+ for (const [issuer, session] of Object.entries(fresh.idpSessions)) {
4973
+ const split = splitIdpSession(session, "all");
4974
+ setOwnEntry(residue.idpSessions, issuer, split.residue);
4975
+ await migrateEntrySecrets(oauthIdpSecretServerId(issuer), split.secrets);
4976
+ }
4977
+ const stripped = serializeOAuthPersistBlob(residue);
4978
+ if (stripped !== raw) {
4979
+ await writeStoreFile(filePath, stripped);
4980
+ }
4981
+ }
4982
+ async function readOAuthStore(filePath, secretStore = defaultSecretStore()) {
4983
+ return withOAuthStateLock(filePath, "read", async () => {
4984
+ let snapshot = parseOAuthPersistBlob(await readStoreFile(filePath));
4985
+ if (snapshot === null) return null;
4986
+ if (snapshotHasPlaintextSecrets(snapshot) && await secretStoreIsDurable(secretStore)) {
4987
+ try {
4988
+ await migratePlaintextSecrets(filePath, secretStore);
4989
+ snapshot = parseOAuthPersistBlob(await readStoreFile(filePath)) ?? snapshot;
4990
+ } catch (error) {
4991
+ warnMigrationFailure(error);
4992
+ }
4993
+ }
4994
+ return joinSnapshot(snapshot, secretStore);
4995
+ });
4996
+ }
4997
+ async function removeOAuthStore(filePath, secretStore = defaultSecretStore()) {
4998
+ await withOAuthStateLock(filePath, "remove", async () => {
4999
+ const snapshot = await readDiskForMutation(filePath, "remove");
5000
+ if (snapshot) {
5001
+ const targets = [
5002
+ ...Object.entries(snapshot.servers).map(([url, state]) => ({
5003
+ id: oauthSecretServerId(url),
5004
+ fields: serverSecretFields(state)
5005
+ })),
5006
+ ...Object.keys(snapshot.idpSessions).map((issuer) => ({
5007
+ id: oauthIdpSecretServerId(issuer),
5008
+ fields: [IDP_SESSION_FIELD]
5009
+ }))
5010
+ ];
5011
+ const priorSecrets = [];
5012
+ try {
5013
+ for (const { id, fields } of targets) {
5014
+ priorSecrets.push(
5015
+ ...await snapshotSecretFields(secretStore, id, fields)
5016
+ );
5017
+ await secretStore.deleteAllForServer(id);
5018
+ }
5019
+ await deleteStoreFile(filePath);
5020
+ } catch (error) {
5021
+ await restoreSecretFields(
5022
+ secretStore,
5023
+ priorSecrets,
5024
+ warnRestoreFailure
5025
+ );
5026
+ throw error;
5027
+ }
5028
+ } else {
5029
+ await deleteStoreFile(filePath);
5030
+ }
5031
+ });
5032
+ }
5033
+
5034
+ // ../../core/json/xMcpHeader.ts
5035
+ var MCP_PARAM_HEADER_PREFIX = "Mcp-Param-";
5036
+
5037
+ // ../../core/mcp/import/clientConfig.ts
5038
+ function parseJsonObject(raw) {
5039
+ let data;
5040
+ try {
5041
+ data = JSON.parse(raw);
5042
+ } catch (err) {
5043
+ const detail = err instanceof Error ? err.message : String(err);
5044
+ throw new Error(`Invalid JSON: ${detail}`, { cause: err });
5045
+ }
5046
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
5047
+ throw new Error("Expected a JSON object at the top level");
5048
+ }
5049
+ return data;
5050
+ }
5051
+ function normalizeServerMap(servers) {
5052
+ const out = {};
5053
+ for (const [id, raw] of Object.entries(servers)) {
5054
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
5055
+ const normalized = normalizeServerType(
5056
+ raw
5057
+ );
5058
+ out[id] = normalized;
5059
+ }
5060
+ return out;
5061
+ }
5062
+ function parseMcpServersConfig(raw) {
5063
+ const data = parseJsonObject(raw);
5064
+ const servers = data.mcpServers;
5065
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
5066
+ throw new Error("No 'mcpServers' object found in config file");
5067
+ }
5068
+ return {
5069
+ mcpServers: normalizeServerMap(servers)
5070
+ };
5071
+ }
5072
+ function parseVsCodeConfig(raw) {
5073
+ const data = parseJsonObject(raw);
5074
+ const servers = data.servers;
5075
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
5076
+ throw new Error("No 'servers' object found in VS Code config file");
5077
+ }
5078
+ return {
5079
+ mcpServers: normalizeServerMap(servers)
5080
+ };
5081
+ }
5082
+
5083
+ // ../../core/mcp/import/strategies.ts
5084
+ function join5(...segments) {
5085
+ return segments.map(
5086
+ (s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")
5087
+ ).filter((s) => s.length > 0).join("/");
5088
+ }
5089
+ function appData(homeDir) {
5090
+ return join5(homeDir, "AppData", "Roaming");
5091
+ }
5092
+ function appSupportDir(platform, homeDir, app) {
5093
+ if (platform === "darwin") {
5094
+ return join5(homeDir, "Library", "Application Support", app);
5095
+ }
5096
+ if (platform === "win32") {
5097
+ return join5(appData(homeDir), app);
5098
+ }
5099
+ return join5(homeDir, ".config", app);
5100
+ }
5101
+ var claudeDesktop = {
5102
+ id: "claude-desktop",
5103
+ label: "Claude Desktop",
5104
+ defaultPaths: (platform, homeDir) => [
5105
+ join5(
5106
+ appSupportDir(platform, homeDir, "Claude"),
5107
+ "claude_desktop_config.json"
5108
+ )
5109
+ ],
5110
+ parse: parseMcpServersConfig
5111
+ };
5112
+ var cursor = {
5113
+ id: "cursor",
5114
+ label: "Cursor",
5115
+ defaultPaths: (_platform, homeDir) => [join5(homeDir, ".cursor", "mcp.json")],
5116
+ parse: parseMcpServersConfig
5117
+ };
5118
+ var cline = {
5119
+ id: "cline",
5120
+ label: "Cline",
5121
+ defaultPaths: (platform, homeDir) => [
5122
+ // Cline (VS Code extension) global settings live under the editor's
5123
+ // globalStorage; the documented cross-platform fallback is ~/Documents.
5124
+ join5(
5125
+ appSupportDir(platform, homeDir, "Code"),
5126
+ "User",
5127
+ "globalStorage",
5128
+ "saoudrizwan.claude-dev",
5129
+ "settings",
5130
+ "cline_mcp_settings.json"
5131
+ ),
5132
+ join5(homeDir, "Documents", "Cline", "MCP", "cline_mcp_settings.json")
5133
+ ],
5134
+ parse: parseMcpServersConfig
5135
+ };
5136
+ var vscode = {
5137
+ id: "vscode",
5138
+ label: "VS Code",
5139
+ defaultPaths: (platform, homeDir) => [
5140
+ join5(appSupportDir(platform, homeDir, "Code"), "User", "mcp.json")
5141
+ ],
5142
+ parse: parseVsCodeConfig
5143
+ };
5144
+ var IMPORT_STRATEGY_LIST = [
5145
+ claudeDesktop,
5146
+ cursor,
5147
+ cline,
5148
+ vscode
5149
+ ];
5150
+ var IMPORT_STRATEGIES = Object.fromEntries(IMPORT_STRATEGY_LIST.map((s) => [s.id, s]));
5151
+ function getImportStrategy(id) {
5152
+ return IMPORT_STRATEGIES[id];
5153
+ }
5154
+
5155
+ // ../../core/mcp/import/resolveSource.ts
5156
+ function resolveImportSource(type, platform, homeDir, readFile2) {
5157
+ const strategy = getImportStrategy(type);
5158
+ if (!strategy) return null;
5159
+ const searched = strategy.defaultPaths(platform, homeDir);
5160
+ for (const path6 of searched) {
5161
+ let raw;
5162
+ try {
5163
+ raw = readFile2(path6);
5164
+ } catch (err) {
5165
+ const msg = err instanceof Error ? err.message : String(err);
5166
+ return {
5167
+ type,
5168
+ found: true,
5169
+ path: path6,
5170
+ error: `Failed to read ${path6}: ${msg}`,
5171
+ searched
5172
+ };
5173
+ }
5174
+ if (raw === null) continue;
5175
+ try {
5176
+ const config = strategy.parse(raw);
5177
+ return { type, found: true, path: path6, config, searched };
5178
+ } catch (err) {
5179
+ const msg = err instanceof Error ? err.message : String(err);
5180
+ return { type, found: true, path: path6, error: msg, searched };
5181
+ }
5182
+ }
5183
+ return { type, found: false, searched };
5184
+ }
5185
+
5186
+ // ../../core/mcp/remote/progressToken.ts
5187
+ import { ProgressTokenSchema } from "@modelcontextprotocol/core";
5188
+ function progressTokenOf(message) {
5189
+ if ("method" in message && message.method === "notifications/progress" && "params" in message && typeof message.params === "object" && message.params !== null) {
5190
+ const parsed = ProgressTokenSchema.safeParse(
5191
+ message.params.progressToken
5192
+ );
5193
+ if (parsed.success) {
5194
+ return parsed.data;
5195
+ }
5196
+ }
5197
+ return void 0;
5198
+ }
5199
+ function waitForProgressToken(waits, token) {
5200
+ const exact = waits.get(token);
5201
+ if (exact !== void 0 || typeof token === "number") {
5202
+ return exact;
5203
+ }
5204
+ const numeric = Number(token);
5205
+ return Number.isSafeInteger(numeric) ? waits.get(numeric) : void 0;
5206
+ }
5207
+
5208
+ // ../../core/mcp/remote/node/remote-session.ts
5209
+ var RemoteSession = class _RemoteSession {
5210
+ sessionId;
5211
+ transport;
5212
+ eventQueue = [];
5213
+ eventConsumer = null;
5214
+ transportDead = false;
5215
+ transportError = null;
5216
+ activeSendCount = 0;
5217
+ /**
5218
+ * Suppress duplicate ambient SSE auth echoes after HTTP command-scoped delivery.
5219
+ * Stale transport `onerror` may arrive after the send completes.
5220
+ */
5221
+ authHttpEchoSuppressUntilMs = 0;
5222
+ static AUTH_HTTP_ECHO_SUPPRESS_MS = 3e4;
5223
+ requestWaits = /* @__PURE__ */ new Map();
5224
+ authProviderHandle = null;
5225
+ appliedProtocolVersion;
5226
+ /** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
5227
+ static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
5228
+ constructor(sessionId) {
5229
+ this.sessionId = sessionId;
5230
+ }
5231
+ setAuthProviderHandle(handle) {
5232
+ this.authProviderHandle = handle;
5233
+ }
5234
+ setAuthState(authState) {
5235
+ if (!this.authProviderHandle) {
5236
+ throw new Error("Session has no OAuth auth provider");
5237
+ }
5238
+ this.authProviderHandle.setAuthState(authState);
5239
+ }
5240
+ setTransport(transport) {
5241
+ this.transport = transport;
5242
+ }
5243
+ /**
5244
+ * Apply the client's negotiated protocol version to the upstream transport
5245
+ * (#1935). The browser's SDK Client owns the `initialize` handshake, so the
5246
+ * `setProtocolVersion` call it makes lands on the *remote* transport; without
5247
+ * this the backend's real HTTP transport never learns the version and drops
5248
+ * `Mcp-Protocol-Version` from every subsequent request.
5249
+ *
5250
+ * Ignores anything that isn't a plausible version token so a client can't
5251
+ * push arbitrary bytes into an upstream header, and re-applies only on
5252
+ * change (the value rides every `/api/mcp/send`). The value arrives from an
5253
+ * unvalidated JSON body, so the type is checked rather than assumed —
5254
+ * `RegExp.test` would otherwise coerce a `123` or `true` into a "matching"
5255
+ * string and hand the wrong type to the transport.
5256
+ */
5257
+ applyProtocolVersion(version) {
5258
+ if (typeof version !== "string" || version === this.appliedProtocolVersion || !_RemoteSession.PROTOCOL_VERSION_PATTERN.test(version)) {
5259
+ return;
5260
+ }
5261
+ this.appliedProtocolVersion = version;
5262
+ this.transport.setProtocolVersion?.(version);
5263
+ }
5264
+ setEventConsumer(consumer) {
5265
+ this.eventConsumer = consumer;
5266
+ while (this.eventQueue.length > 0) {
5267
+ const ev = this.eventQueue.shift();
5268
+ consumer(ev);
5269
+ }
5270
+ }
5271
+ clearEventConsumer() {
5272
+ this.eventConsumer = null;
5273
+ return this.transportDead;
5274
+ }
5275
+ markTransportDead(error) {
5276
+ this.transportDead = true;
5277
+ this.transportError = error;
5278
+ this.pushEvent({
5279
+ type: "transport_error",
5280
+ data: {
5281
+ error,
5282
+ code: -32e3
5283
+ // MCP error code for connection closed
5284
+ }
5285
+ });
5286
+ }
5287
+ isTransportDead() {
5288
+ return this.transportDead;
5289
+ }
5290
+ getTransportError() {
5291
+ return this.transportError;
5292
+ }
5293
+ hasEventConsumer() {
5294
+ return this.eventConsumer !== null;
5295
+ }
5296
+ beginSend() {
5297
+ this.activeSendCount++;
5298
+ }
5299
+ endSend() {
5300
+ if (this.activeSendCount > 0) {
5301
+ this.activeSendCount--;
5302
+ }
4204
5303
  }
4205
- try {
4206
- await fs4.link(staged, filePath);
4207
- await fs4.rm(staged, { force: true });
4208
- } catch {
4209
- console.warn(
4210
- `
4211
- [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.`
4212
- );
5304
+ /** Command-scoped auth was returned on HTTP; suppress async onerror echo on SSE. */
5305
+ noteAuthChallengeDeliveredViaHttp() {
5306
+ this.extendAuthHttpEchoSuppression();
4213
5307
  }
4214
- }
4215
- var isSnapshotName = (name, base) => name.startsWith(`${base}.migrating-`) && !name.endsWith(".lock");
4216
- async function anythingToMigrate(filePath) {
4217
- const base = path4.basename(filePath);
4218
- try {
4219
- return (await fs4.readdir(path4.dirname(filePath))).some(
4220
- (name) => name === base || isSnapshotName(name, base)
4221
- );
4222
- } catch (err) {
4223
- const code = err.code;
4224
- return code !== "ENOENT" && code !== "ENOTDIR";
5308
+ extendAuthHttpEchoSuppression() {
5309
+ this.authHttpEchoSuppressUntilMs = Date.now() + _RemoteSession.AUTH_HTTP_ECHO_SUPPRESS_MS;
4225
5310
  }
4226
- }
4227
- async function recoverOrphanedSnapshots(filePath) {
4228
- const dir = path4.dirname(filePath);
4229
- const base = path4.basename(filePath);
4230
- let names;
4231
- try {
4232
- names = (await fs4.readdir(dir)).filter((n) => isSnapshotName(n, base));
4233
- } catch {
4234
- return;
5311
+ shouldSuppressAuthEcho() {
5312
+ return Date.now() < this.authHttpEchoSuppressUntilMs;
4235
5313
  }
4236
- for (const name of names) {
4237
- const orphan = path4.join(dir, name);
4238
- if (await isFileLockHeld(orphan)) continue;
4239
- try {
4240
- await fs4.link(orphan, filePath);
4241
- await fs4.rm(orphan, { force: true });
4242
- console.warn(
4243
- `
4244
- [mcp-inspector] Recovered secrets left behind by an interrupted migration (${orphan} \u2192 ${filePath}).`
4245
- );
4246
- } catch {
4247
- console.warn(
4248
- `
4249
- [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.`
4250
- );
4251
- }
5314
+ hasActiveSend() {
5315
+ return this.activeSendCount > 0;
4252
5316
  }
4253
- }
4254
- async function handOff(file, filePath, keyring) {
4255
- let entries;
4256
- try {
4257
- entries = await file.readAll();
4258
- } catch (err) {
4259
- console.warn(
4260
- `
4261
- [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)}`
4262
- );
4263
- return false;
5317
+ pushAuthChallenge(challenge) {
5318
+ this.pushEvent({ type: "auth_challenge", data: challenge });
4264
5319
  }
4265
- if (!entries || Object.keys(entries).length === 0) return false;
4266
- let complete = true;
4267
- try {
4268
- for (const [account, value] of Object.entries(entries)) {
4269
- const parsed = parseAccount(account);
4270
- if (!parsed) {
4271
- complete = false;
4272
- continue;
4273
- }
4274
- const existing = await secretStoreGetStrict(
4275
- keyring,
4276
- parsed.serverId,
4277
- parsed.field
4278
- );
4279
- if (existing === null) {
4280
- await keyring.set(parsed.serverId, parsed.field, value);
4281
- }
4282
- }
4283
- } catch (err) {
4284
- console.warn(
4285
- `
4286
- [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)}`
4287
- );
4288
- return false;
5320
+ /**
5321
+ * Streamable HTTP `transport.send()` can return before the JSON-RPC response
5322
+ * arrives. The remote send handler awaits this so `/api/mcp/send` returns
5323
+ * `auth_challenge` or `ok: true` only after the MCP round-trip (or auth error).
5324
+ */
5325
+ waitForRequestResponse(requestId, timeoutMs = 6e4) {
5326
+ return new Promise((resolve9, reject) => {
5327
+ let timer;
5328
+ const arm = () => {
5329
+ if (timeoutMs <= 0) return;
5330
+ timer = setTimeout(() => {
5331
+ this.requestWaits.delete(requestId);
5332
+ reject(
5333
+ new Error(
5334
+ `MCP request ${String(requestId)} timed out after ${timeoutMs}ms`
5335
+ )
5336
+ );
5337
+ }, timeoutMs);
5338
+ };
5339
+ const clear = () => {
5340
+ if (timer !== void 0) {
5341
+ clearTimeout(timer);
5342
+ timer = void 0;
5343
+ }
5344
+ };
5345
+ arm();
5346
+ this.requestWaits.set(requestId, {
5347
+ resolve: () => {
5348
+ clear();
5349
+ resolve9();
5350
+ },
5351
+ reject: (error) => {
5352
+ clear();
5353
+ reject(error);
5354
+ },
5355
+ // A progress notification for this request re-arms the full window, so
5356
+ // a tool that keeps reporting progress never trips the relay's own
5357
+ // deadline — the same guarantee the SDK client's `resetTimeoutOnProgress`
5358
+ // gives on a direct transport (#2028).
5359
+ resetTimeout: () => {
5360
+ if (timer === void 0) return;
5361
+ clear();
5362
+ arm();
5363
+ }
5364
+ });
5365
+ });
4289
5366
  }
4290
- if (!complete) {
4291
- console.warn(
4292
- `
4293
- [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.`
4294
- );
4295
- return false;
5367
+ /**
5368
+ * Re-arm a pending request's wait timeout. Invoked from {@link onMessage} when
5369
+ * a `notifications/progress` carrying this request's `progressToken` arrives.
5370
+ * A no-op when no wait is pending for the id. A numeric string token (`"4"`)
5371
+ * matches the numeric request id, as it does in the SDK (#2458).
5372
+ */
5373
+ noteRequestProgress(progressToken) {
5374
+ waitForProgressToken(this.requestWaits, progressToken)?.resetTimeout();
4296
5375
  }
4297
- return true;
4298
- }
4299
- async function getSecretStorageInfo() {
4300
- const { store, info } = await resolveSecretStore();
4301
- if (info.kind !== "file" || !(store instanceof FileSecretStore)) return info;
4302
- return describeFileStore(
4303
- store,
4304
- store.filePath,
4305
- "file",
4306
- info.reason,
4307
- info.detail
4308
- );
4309
- }
4310
- var DeferredSecretStore = class {
4311
- async target() {
4312
- return (await resolveSecretStore()).store;
5376
+ cancelRequestWait(requestId) {
5377
+ const wait = this.requestWaits.get(requestId);
5378
+ if (!wait) {
5379
+ return;
5380
+ }
5381
+ this.requestWaits.delete(requestId);
5382
+ wait.reject(new Error("MCP request wait cancelled"));
4313
5383
  }
4314
- async isDurable() {
4315
- return secretStoreIsDurable(await this.target());
5384
+ rejectActiveRequestWaits(error) {
5385
+ for (const wait of this.requestWaits.values()) {
5386
+ wait.reject(error);
5387
+ }
5388
+ this.requestWaits.clear();
4316
5389
  }
4317
5390
  /**
4318
- * Forwarded, and its absence was a real defect rather than an omission:
4319
- * this is the store production actually uses, so a `secretStoreGetStrict`
4320
- * call against it fell through to the tolerant `get` and the strictness
4321
- * existed only in tests that injected a concrete store.
5391
+ * Auth errors from the MCP transport.
5392
+ * - During an active send: command path owns delivery (HTTP); never SSE.
5393
+ * - After HTTP already returned auth_challenge: swallow async onerror echo.
5394
+ * - Otherwise: ambient SSE (idle session).
4322
5395
  */
4323
- async getStrict(serverId, field) {
4324
- return secretStoreGetStrict(await this.target(), serverId, field);
5396
+ handleTransportAuthError(error) {
5397
+ if (!(error instanceof AuthChallengeError)) {
5398
+ return false;
5399
+ }
5400
+ this.rejectActiveRequestWaits(error);
5401
+ if (this.hasActiveSend()) {
5402
+ this.extendAuthHttpEchoSuppression();
5403
+ return true;
5404
+ }
5405
+ if (this.shouldSuppressAuthEcho()) {
5406
+ return true;
5407
+ }
5408
+ this.pushAuthChallenge(error.authChallenge);
5409
+ return true;
4325
5410
  }
4326
- async getMany(requests) {
4327
- return secretStoreGetMany(await this.target(), requests);
5411
+ settleRequestWait(requestId) {
5412
+ const wait = this.requestWaits.get(requestId);
5413
+ if (!wait) {
5414
+ return;
5415
+ }
5416
+ this.requestWaits.delete(requestId);
5417
+ wait.resolve();
4328
5418
  }
4329
- async get(serverId, field) {
4330
- return (await this.target()).get(serverId, field);
5419
+ pushEvent(event) {
5420
+ if (this.eventConsumer) {
5421
+ this.eventConsumer(event);
5422
+ } else {
5423
+ this.eventQueue.push(event);
5424
+ }
4331
5425
  }
4332
- async set(serverId, field, value) {
4333
- await (await this.target()).set(serverId, field, value);
5426
+ onMessage(message) {
5427
+ if ("id" in message && message.id !== null && message.id !== void 0 && ("result" in message || "error" in message)) {
5428
+ this.settleRequestWait(message.id);
5429
+ }
5430
+ const progressToken = progressTokenOf(message);
5431
+ if (progressToken !== void 0) {
5432
+ this.noteRequestProgress(progressToken);
5433
+ }
5434
+ this.pushEvent({ type: "message", data: message });
4334
5435
  }
4335
- async setMany(serverId, values) {
4336
- await secretStoreSetMany(await this.target(), serverId, values);
5436
+ onFetchRequest(entry) {
5437
+ this.pushEvent({
5438
+ type: "fetch_request",
5439
+ data: {
5440
+ ...entry,
5441
+ timestamp: entry.timestamp instanceof Date ? entry.timestamp.toISOString() : entry.timestamp
5442
+ }
5443
+ });
4337
5444
  }
4338
- async delete(serverId, field) {
4339
- await (await this.target()).delete(serverId, field);
5445
+ onFetchResponseBody(id, responseBody) {
5446
+ this.pushEvent({
5447
+ type: "fetch_request_body_update",
5448
+ data: { id, responseBody }
5449
+ });
5450
+ }
5451
+ onFetchStreamUpdate(id, stream) {
5452
+ this.pushEvent({
5453
+ type: "fetch_stream_update",
5454
+ data: {
5455
+ id,
5456
+ eventCount: stream.eventCount,
5457
+ ...stream.closedAt && { closedAt: stream.closedAt.toISOString() }
5458
+ }
5459
+ });
4340
5460
  }
4341
- async deleteAllForServer(serverId) {
4342
- await (await this.target()).deleteAllForServer(serverId);
5461
+ onStderr(entry) {
5462
+ this.pushEvent({
5463
+ type: "stdio_log",
5464
+ data: {
5465
+ timestamp: entry.timestamp.toISOString(),
5466
+ message: entry.message
5467
+ }
5468
+ });
4343
5469
  }
4344
5470
  };
4345
- function defaultSecretStore() {
4346
- return new DeferredSecretStore();
5471
+
5472
+ // ../../core/mcp/remote/node/tokenAuthProvider.ts
5473
+ var REMOTE_OAUTH_STUB_METADATA = {
5474
+ redirect_uris: [],
5475
+ scope: ""
5476
+ };
5477
+ function cloneAuthState(state) {
5478
+ return {
5479
+ ...state.oauthTokens && {
5480
+ oauthTokens: { ...state.oauthTokens }
5481
+ },
5482
+ ...state.oauthClient && {
5483
+ oauthClient: { ...state.oauthClient }
5484
+ }
5485
+ };
5486
+ }
5487
+ function createRemoteAuthProvider(initialAuthState) {
5488
+ if (!initialAuthState?.oauthTokens && !initialAuthState?.oauthClient) {
5489
+ return void 0;
5490
+ }
5491
+ let authState = cloneAuthState(initialAuthState);
5492
+ const provider = {
5493
+ get clientMetadata() {
5494
+ return REMOTE_OAUTH_STUB_METADATA;
5495
+ },
5496
+ get redirectUrl() {
5497
+ return "";
5498
+ },
5499
+ async tokens() {
5500
+ return authState.oauthTokens;
5501
+ },
5502
+ async clientInformation() {
5503
+ if (!authState.oauthClient?.client_id) {
5504
+ return void 0;
5505
+ }
5506
+ return {
5507
+ client_id: authState.oauthClient.client_id,
5508
+ ...authState.oauthClient.client_secret && {
5509
+ client_secret: authState.oauthClient.client_secret
5510
+ }
5511
+ };
5512
+ },
5513
+ async saveTokens(tokens) {
5514
+ authState = {
5515
+ ...authState,
5516
+ oauthTokens: {
5517
+ access_token: tokens.access_token,
5518
+ token_type: tokens.token_type,
5519
+ expires_in: tokens.expires_in,
5520
+ refresh_token: tokens.refresh_token,
5521
+ scope: tokens.scope,
5522
+ id_token: tokens.id_token
5523
+ }
5524
+ };
5525
+ },
5526
+ codeVerifier() {
5527
+ return void 0;
5528
+ },
5529
+ async saveCodeVerifier() {
5530
+ },
5531
+ clear() {
5532
+ authState = {};
5533
+ },
5534
+ async redirectToAuthorization() {
5535
+ throw new Error(
5536
+ "OAuth re-authorization must be performed in the Inspector web client (remote server cannot complete OAuth flows)"
5537
+ );
5538
+ },
5539
+ state() {
5540
+ return "";
5541
+ }
5542
+ // This provider implements only the members the remote backend transport
5543
+ // exercises (token get/save, client info, non-interactive redirect); the
5544
+ // interactive-only members the SDK never calls on the server side are stubbed
5545
+ // with narrower shapes (e.g. `codeVerifier()` returns `undefined`, not the
5546
+ // interface's `string | Promise<string>`), so it can't be assigned to the
5547
+ // full `OAuthClientProvider` without the double cast.
5548
+ };
5549
+ return {
5550
+ provider,
5551
+ setAuthState(next) {
5552
+ authState = cloneAuthState(next);
5553
+ },
5554
+ getAuthState() {
5555
+ return cloneAuthState(authState);
5556
+ }
5557
+ };
4347
5558
  }
4348
5559
 
4349
5560
  // ../../core/client/config-parse.ts
@@ -4498,6 +5709,23 @@ function mergeSecretsIntoClientConfig(config, secrets) {
4498
5709
  }
4499
5710
 
4500
5711
  // ../../core/client/node-persistence.ts
5712
+ async function withClientConfigLock(filePath, action, body) {
5713
+ let entered = false;
5714
+ try {
5715
+ return await withSecretFileLock(filePath, async () => {
5716
+ entered = true;
5717
+ return body();
5718
+ });
5719
+ } catch (error) {
5720
+ if (!entered && error instanceof SecretFileLockHeldError) {
5721
+ throw new SecretFileLockHeldError(
5722
+ `Could not ${action} the client configuration: the file at ${filePath} is locked by another Inspector process and did not become available.`,
5723
+ { cause: error }
5724
+ );
5725
+ }
5726
+ throw error;
5727
+ }
5728
+ }
4501
5729
  async function readIdpSecretFromKeychain(secretStore) {
4502
5730
  const secret = await secretStore.get(
4503
5731
  CLIENT_KEYCHAIN_ID,
@@ -4507,29 +5735,39 @@ async function readIdpSecretFromKeychain(secretStore) {
4507
5735
  return { [SECRET_FIELD_IDP_CLIENT_SECRET]: secret };
4508
5736
  }
4509
5737
  async function migrateClientPlaintextSecret(filePath, config, secretStore) {
4510
- const { stripped, secrets } = extractSecretsFromClientConfig(config);
4511
- const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
4512
- if (!value) return config;
4513
5738
  try {
4514
- const existing = await secretStoreGetStrict(
4515
- secretStore,
4516
- CLIENT_KEYCHAIN_ID,
4517
- SECRET_FIELD_IDP_CLIENT_SECRET
4518
- );
4519
- if (existing === null) {
4520
- await secretStore.set(
4521
- CLIENT_KEYCHAIN_ID,
4522
- SECRET_FIELD_IDP_CLIENT_SECRET,
4523
- value
4524
- );
4525
- }
4526
- if (!await secretStoreIsDurable(secretStore)) return config;
4527
- await writeStoreFile(filePath, serializeStore(stripped));
4528
- return stripped;
5739
+ return await withClientConfigLock(filePath, "migrate", async () => {
5740
+ const raw = await readStoreFile(filePath);
5741
+ if (raw === null) return {};
5742
+ const fresh = parseClientConfig(parseStore(raw));
5743
+ const { stripped, secrets } = extractSecretsFromClientConfig(fresh);
5744
+ const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
5745
+ if (!value) return fresh;
5746
+ try {
5747
+ const existing = await secretStoreGetStrict(
5748
+ secretStore,
5749
+ CLIENT_KEYCHAIN_ID,
5750
+ SECRET_FIELD_IDP_CLIENT_SECRET
5751
+ );
5752
+ if (existing === null) {
5753
+ await secretStore.set(
5754
+ CLIENT_KEYCHAIN_ID,
5755
+ SECRET_FIELD_IDP_CLIENT_SECRET,
5756
+ value
5757
+ );
5758
+ }
5759
+ if (!await secretStoreIsDurable(secretStore)) return fresh;
5760
+ await writeStoreFile(filePath, serializeStore(stripped));
5761
+ return stripped;
5762
+ } catch (err) {
5763
+ if (err instanceof SecretStoreUnavailableError) {
5764
+ return fresh;
5765
+ }
5766
+ throw err;
5767
+ }
5768
+ });
4529
5769
  } catch (err) {
4530
- if (err instanceof SecretStoreUnavailableError) {
4531
- return config;
4532
- }
5770
+ if (err instanceof SecretFileLockHeldError) return config;
4533
5771
  throw err;
4534
5772
  }
4535
5773
  }
@@ -4549,30 +5787,44 @@ async function writeClientConfigStore(filePath, body, secretStore) {
4549
5787
  const validated = parseClientConfig(body);
4550
5788
  const { stripped, secrets } = extractSecretsFromClientConfig(validated);
4551
5789
  const idpSecret = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
4552
- if (idpSecret) {
4553
- await secretStore.set(
4554
- CLIENT_KEYCHAIN_ID,
4555
- SECRET_FIELD_IDP_CLIENT_SECRET,
4556
- idpSecret
4557
- );
4558
- } else {
4559
- await secretStore.delete(
4560
- CLIENT_KEYCHAIN_ID,
5790
+ await withClientConfigLock(filePath, "save", async () => {
5791
+ const prior = await snapshotSecretFields(secretStore, CLIENT_KEYCHAIN_ID, [
4561
5792
  SECRET_FIELD_IDP_CLIENT_SECRET
4562
- );
4563
- }
4564
- const durable = await secretStoreIsDurable(secretStore);
4565
- await writeStoreFile(
4566
- filePath,
4567
- serializeStore(
4568
- durable ? stripped : await preserveLegacyPlaintext(
5793
+ ]);
5794
+ try {
5795
+ if (idpSecret) {
5796
+ await secretStore.set(
5797
+ CLIENT_KEYCHAIN_ID,
5798
+ SECRET_FIELD_IDP_CLIENT_SECRET,
5799
+ idpSecret
5800
+ );
5801
+ } else {
5802
+ await secretStore.delete(
5803
+ CLIENT_KEYCHAIN_ID,
5804
+ SECRET_FIELD_IDP_CLIENT_SECRET
5805
+ );
5806
+ }
5807
+ const durable = await secretStoreIsDurable(secretStore);
5808
+ await writeStoreFile(
4569
5809
  filePath,
4570
- validated,
4571
- stripped,
4572
- idpSecret
4573
- )
4574
- )
4575
- );
5810
+ serializeStore(
5811
+ durable ? stripped : await preserveLegacyPlaintext(
5812
+ filePath,
5813
+ validated,
5814
+ stripped,
5815
+ idpSecret
5816
+ )
5817
+ )
5818
+ );
5819
+ } catch (error) {
5820
+ await restoreSecretFields(secretStore, prior, (restoreError) => {
5821
+ console.warn(
5822
+ `[mcp-inspector] Could not restore the IdP client secret after a failed client.json write: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`
5823
+ );
5824
+ });
5825
+ throw error;
5826
+ }
5827
+ });
4576
5828
  }
4577
5829
  async function preserveLegacyPlaintext(filePath, validated, stripped, idpSecret) {
4578
5830
  if (!idpSecret) return stripped;
@@ -4587,8 +5839,25 @@ async function preserveLegacyPlaintext(filePath, validated, stripped, idpSecret)
4587
5839
  }
4588
5840
  }
4589
5841
  async function deleteClientConfigStore(filePath, secretStore) {
4590
- await deleteStoreFile(filePath);
4591
- await secretStore.delete(CLIENT_KEYCHAIN_ID, SECRET_FIELD_IDP_CLIENT_SECRET);
5842
+ await withClientConfigLock(filePath, "remove", async () => {
5843
+ const prior = await snapshotSecretFields(secretStore, CLIENT_KEYCHAIN_ID, [
5844
+ SECRET_FIELD_IDP_CLIENT_SECRET
5845
+ ]);
5846
+ try {
5847
+ await secretStore.delete(
5848
+ CLIENT_KEYCHAIN_ID,
5849
+ SECRET_FIELD_IDP_CLIENT_SECRET
5850
+ );
5851
+ await deleteStoreFile(filePath);
5852
+ } catch (error) {
5853
+ await restoreSecretFields(secretStore, prior, (restoreError) => {
5854
+ console.warn(
5855
+ `[mcp-inspector] Could not restore the IdP client secret after a failed client.json delete: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`
5856
+ );
5857
+ });
5858
+ throw error;
5859
+ }
5860
+ });
4592
5861
  }
4593
5862
 
4594
5863
  // ../../core/mcp/remote/node/server.ts
@@ -4615,6 +5884,7 @@ async function primeAndHoldSseStream(stream, cleanup) {
4615
5884
  var MAX_APP_DOCUMENT_CHARS = 8 * 1024 * 1024;
4616
5885
  var MAX_APP_DOCUMENT_BODY_BYTES = 24 * 1024 * 1024;
4617
5886
  var MAX_APP_CSP_CHARS = 8 * 1024;
5887
+ var MAX_STORAGE_BODY_BYTES = 4 * 1024 * 1024;
4618
5888
  function isHeaderSafeValue(value) {
4619
5889
  return !/[^\t\x20-\x7e]/.test(value);
4620
5890
  }
@@ -5247,6 +6517,10 @@ function createRemoteApp(options) {
5247
6517
  const config = await readClientConfigStore(filePath, secretStore);
5248
6518
  return c.json(config);
5249
6519
  }
6520
+ if (storeId === OAUTH_PERSIST_STORE_ID) {
6521
+ const snapshot = await readOAuthStore(filePath, secretStore);
6522
+ return c.json(snapshot ?? {});
6523
+ }
5250
6524
  const raw = await readStoreFile(filePath);
5251
6525
  if (raw === null) {
5252
6526
  return c.json({}, 200);
@@ -5254,40 +6528,74 @@ function createRemoteApp(options) {
5254
6528
  const store = parseStore(raw);
5255
6529
  return c.json(store);
5256
6530
  } catch (error) {
6531
+ const keychainResp = keychainErrorResponse(c, error);
6532
+ if (keychainResp) return keychainResp;
5257
6533
  const msg = error instanceof Error ? error.message : String(error);
5258
6534
  return c.json({ error: `Failed to read store: ${msg}` }, 500);
5259
6535
  }
5260
6536
  });
5261
- app.post("/api/storage/:storeId", async (c) => {
5262
- const storeId = c.req.param("storeId");
5263
- if (!storeId || !validateStoreId(storeId)) {
5264
- return c.json({ error: "Invalid storeId" }, 400);
5265
- }
5266
- let body;
5267
- try {
5268
- body = await c.req.json();
5269
- } catch {
5270
- return c.json({ error: "Invalid JSON body" }, 400);
5271
- }
5272
- const filePath = getStoreFilePath(storageDir, storeId);
5273
- try {
5274
- if (storeId === "client") {
5275
- await writeClientConfigStore(filePath, body, secretStore);
5276
- return c.json({ ok: true });
6537
+ app.post(
6538
+ "/api/storage/:storeId",
6539
+ // Before the parse, not after: see MAX_STORAGE_BODY_BYTES.
6540
+ bodyLimit({
6541
+ maxSize: MAX_STORAGE_BODY_BYTES,
6542
+ onError: (c) => c.json({ error: "Storage payload too large" }, 413)
6543
+ }),
6544
+ async (c) => {
6545
+ const storeId = c.req.param("storeId");
6546
+ if (!storeId || !validateStoreId(storeId)) {
6547
+ return c.json({ error: "Invalid storeId" }, 400);
5277
6548
  }
5278
- const jsonData = serializeStore(body);
5279
- await writeStoreFile(filePath, jsonData);
5280
- return c.json({ ok: true });
5281
- } catch (error) {
5282
- if (error instanceof ZodError2) {
5283
- return c.json({ error: formatClientConfigLoadError(error) }, 400);
6549
+ let body;
6550
+ try {
6551
+ body = await c.req.json();
6552
+ } catch {
6553
+ return c.json({ error: "Invalid JSON body" }, 400);
6554
+ }
6555
+ const filePath = getStoreFilePath(storageDir, storeId);
6556
+ try {
6557
+ if (storeId === "client") {
6558
+ await writeClientConfigStore(filePath, body, secretStore);
6559
+ return c.json({ ok: true });
6560
+ }
6561
+ if (c.req.query("sections") !== void 0) {
6562
+ return c.json(
6563
+ {
6564
+ error: "The sections descriptor moved from the ?sections query parameter to the request body"
6565
+ },
6566
+ 400
6567
+ );
6568
+ }
6569
+ if (storeId === OAUTH_PERSIST_STORE_ID) {
6570
+ const write = parseOAuthStoreWriteBody(body);
6571
+ if (!write) {
6572
+ return c.json(
6573
+ { error: "OAuth store writes require an OAuth state body" },
6574
+ 400
6575
+ );
6576
+ }
6577
+ await writeOAuthSections(
6578
+ filePath,
6579
+ write.snapshot,
6580
+ write.sections,
6581
+ secretStore
6582
+ );
6583
+ return c.json({ ok: true });
6584
+ }
6585
+ const jsonData = serializeStore(body);
6586
+ await writeStoreFile(filePath, jsonData);
6587
+ return c.json({ ok: true });
6588
+ } catch (error) {
6589
+ if (error instanceof ZodError2) {
6590
+ return c.json({ error: formatClientConfigLoadError(error) }, 400);
6591
+ }
6592
+ const keychainResp = keychainErrorResponse(c, error);
6593
+ if (keychainResp) return keychainResp;
6594
+ const msg = error instanceof Error ? error.message : String(error);
6595
+ return c.json({ error: `Failed to write store: ${msg}` }, 500);
5284
6596
  }
5285
- const keychainResp = storeId === "client" ? keychainErrorResponse(c, error) : void 0;
5286
- if (keychainResp) return keychainResp;
5287
- const msg = error instanceof Error ? error.message : String(error);
5288
- return c.json({ error: `Failed to write store: ${msg}` }, 500);
5289
6597
  }
5290
- });
6598
+ );
5291
6599
  app.delete("/api/storage/:storeId", async (c) => {
5292
6600
  const storeId = c.req.param("storeId");
5293
6601
  if (!storeId || !validateStoreId(storeId)) {
@@ -5299,9 +6607,15 @@ function createRemoteApp(options) {
5299
6607
  await deleteClientConfigStore(filePath, secretStore);
5300
6608
  return c.json({ ok: true });
5301
6609
  }
6610
+ if (storeId === OAUTH_PERSIST_STORE_ID) {
6611
+ await removeOAuthStore(filePath, secretStore);
6612
+ return c.json({ ok: true });
6613
+ }
5302
6614
  await deleteStoreFile(filePath);
5303
6615
  return c.json({ ok: true });
5304
6616
  } catch (error) {
6617
+ const keychainResp = keychainErrorResponse(c, error);
6618
+ if (keychainResp) return keychainResp;
5305
6619
  const msg = error instanceof Error ? error.message : String(error);
5306
6620
  return c.json({ error: `Failed to delete store: ${msg}` }, 500);
5307
6621
  }
@@ -5372,6 +6686,13 @@ function createRemoteApp(options) {
5372
6686
  const out = {};
5373
6687
  for (const [id, val] of Object.entries(raw)) {
5374
6688
  if (!val || typeof val !== "object") continue;
6689
+ if (id === "__proto__") {
6690
+ logWarn(
6691
+ { route: "/api/servers", id },
6692
+ "Dropping mcp.json entry with reserved id `__proto__` \u2014 rename the server to use it."
6693
+ );
6694
+ continue;
6695
+ }
5375
6696
  const valObj = val;
5376
6697
  if ("settings" in valObj) {
5377
6698
  logWarn(
@@ -5584,6 +6905,12 @@ function createRemoteApp(options) {
5584
6905
  error: "settings.paginatedLists must be a boolean"
5585
6906
  };
5586
6907
  }
6908
+ if (obj.suppressNotificationStream !== void 0 && typeof obj.suppressNotificationStream !== "boolean") {
6909
+ return {
6910
+ ok: false,
6911
+ error: "settings.suppressNotificationStream must be a boolean"
6912
+ };
6913
+ }
5587
6914
  if (obj.maxFetchRequests !== void 0 && (typeof obj.maxFetchRequests !== "number" || obj.maxFetchRequests < 0)) {
5588
6915
  return {
5589
6916
  ok: false,
@@ -5686,6 +7013,10 @@ function createRemoteApp(options) {
5686
7013
  autoRefreshOnListChanged: obj.autoRefreshOnListChanged === true,
5687
7014
  // Absent → false, matching the read side (omit-on-false on the write side).
5688
7015
  paginatedLists: obj.paginatedLists === true,
7016
+ // Absent → unset (off); only an explicit true is carried (#2317).
7017
+ ...obj.suppressNotificationStream === true && {
7018
+ suppressNotificationStream: true
7019
+ },
5689
7020
  // Absent → product default, matching the read side. The default is the
5690
7021
  // omit-sentinel in inspectorSettingsToStoredFields, so a client that
5691
7022
  // didn't send one writes no spurious maxFetchRequests to disk.
@@ -5789,7 +7120,7 @@ function createRemoteApp(options) {
5789
7120
  return false;
5790
7121
  };
5791
7122
  let warnedSessionPreserved = false;
5792
- const migratePlaintextSecrets = async (config) => {
7123
+ const migratePlaintextSecrets2 = async (config) => {
5793
7124
  let changed = false;
5794
7125
  const next = { mcpServers: {} };
5795
7126
  const durable = await secretStoreIsDurable(secretStore);
@@ -5860,7 +7191,22 @@ function createRemoteApp(options) {
5860
7191
  return out;
5861
7192
  };
5862
7193
  const deleteKeychainFields = async (id, fields) => {
5863
- await Promise.all(fields.map((field) => secretStore.delete(id, field)));
7194
+ await settleStoreMutations(
7195
+ fields.map((field) => secretStore.delete(id, field))
7196
+ );
7197
+ };
7198
+ const warnSecretRestoreFailure = (error) => {
7199
+ const msg = error instanceof Error ? error.message : String(error);
7200
+ if (fileLogger) {
7201
+ fileLogger.warn(
7202
+ { err: msg },
7203
+ "Could not restore keychain entries after a failed catalog mutation; keychain and mcp.json may disagree until the next successful save."
7204
+ );
7205
+ } else {
7206
+ console.warn(
7207
+ `[mcp-inspector] Could not restore keychain entries after a failed catalog mutation: ${msg}`
7208
+ );
7209
+ }
5864
7210
  };
5865
7211
  const entryForDisk = async (built, stripped, previous) => {
5866
7212
  if (await secretStoreIsDurable(secretStore)) return stripped;
@@ -5916,7 +7262,7 @@ function createRemoteApp(options) {
5916
7262
  mcpServers: normalizeMcpServers(parsedInside?.mcpServers)
5917
7263
  };
5918
7264
  if (!hasPlaintextSecrets(inside)) return inside;
5919
- const { migrated, changed } = await migratePlaintextSecrets(inside);
7265
+ const { migrated, changed } = await migratePlaintextSecrets2(inside);
5920
7266
  if (changed) {
5921
7267
  await writeMcpAndTrackMtime(serializeStore(migrated));
5922
7268
  }
@@ -5936,7 +7282,7 @@ function createRemoteApp(options) {
5936
7282
  type,
5937
7283
  process.platform,
5938
7284
  homedir(),
5939
- (path6) => existsSync3(path6) ? readFileSync5(path6, "utf-8") : null
7285
+ (path6) => existsSync3(path6) ? readFileSync6(path6, "utf-8") : null
5940
7286
  );
5941
7287
  if (!result) {
5942
7288
  return c.json(
@@ -5958,7 +7304,7 @@ function createRemoteApp(options) {
5958
7304
  if (typeof body.id !== "string" || !validateStoreId(body.id)) {
5959
7305
  return c.json(
5960
7306
  {
5961
- error: "Invalid id: must be non-empty and contain only alphanumeric, hyphen, or underscore"
7307
+ error: "Invalid id: must be non-empty, contain only alphanumeric, hyphen, or underscore, and not be `__proto__`"
5962
7308
  },
5963
7309
  400
5964
7310
  );
@@ -5976,15 +7322,29 @@ function createRemoteApp(options) {
5976
7322
  try {
5977
7323
  return await withWriteLock(async () => {
5978
7324
  const current = await readMcpConfig();
5979
- if (id in current.mcpServers) {
7325
+ if (Object.hasOwn(current.mcpServers, id)) {
5980
7326
  return c.json({ error: `Server '${id}' already exists` }, 409);
5981
7327
  }
5982
7328
  const built = buildStoredEntry(id, body.config, postSettings);
5983
7329
  const { stripped, secrets } = extractSecretsFromStored(built);
5984
7330
  await secretStore.deleteAllForServer(id);
5985
- await writeKeychainEntriesFor(id, secrets);
5986
- current.mcpServers[id] = await entryForDisk(built, stripped);
5987
- await writeMcpAndTrackMtime(serializeStore(current));
7331
+ const prior = await snapshotSecretFields(
7332
+ secretStore,
7333
+ id,
7334
+ Object.keys(secrets)
7335
+ );
7336
+ try {
7337
+ await writeKeychainEntriesFor(id, secrets);
7338
+ current.mcpServers[id] = await entryForDisk(built, stripped);
7339
+ await writeMcpAndTrackMtime(serializeStore(current));
7340
+ } catch (error) {
7341
+ await restoreSecretFields(
7342
+ secretStore,
7343
+ prior,
7344
+ warnSecretRestoreFailure
7345
+ );
7346
+ throw error;
7347
+ }
5988
7348
  return c.json({ ok: true });
5989
7349
  });
5990
7350
  } catch (error) {
@@ -6074,10 +7434,10 @@ function createRemoteApp(options) {
6074
7434
  try {
6075
7435
  return await withWriteLock(async () => {
6076
7436
  const current = await readMcpConfig();
6077
- if (!(originalId in current.mcpServers)) {
7437
+ if (!Object.hasOwn(current.mcpServers, originalId)) {
6078
7438
  return c.json({ error: `Server '${originalId}' not found` }, 404);
6079
7439
  }
6080
- if (newId !== originalId && newId in current.mcpServers) {
7440
+ if (newId !== originalId && Object.hasOwn(current.mcpServers, newId)) {
6081
7441
  return c.json({ error: `Server '${newId}' already exists` }, 409);
6082
7442
  }
6083
7443
  const existing = current.mcpServers[originalId];
@@ -6134,27 +7494,80 @@ function createRemoteApp(options) {
6134
7494
  }
6135
7495
  if (newId !== originalId) {
6136
7496
  const previousFields = expectedSecretFields(existing);
6137
- const keychainSecrets = await readKeychainEntriesFor(
7497
+ const originalPrior = await snapshotSecretFields(
7498
+ secretStore,
6138
7499
  originalId,
6139
7500
  previousFields
6140
7501
  );
7502
+ const keychainSecrets = {};
7503
+ for (const { field, value } of originalPrior) {
7504
+ if (value !== null) setOwnEntry(keychainSecrets, field, value);
7505
+ }
6141
7506
  const secretsToWrite = mergeRenameKeychainSecrets(
6142
7507
  stripped,
6143
7508
  keychainSecrets,
6144
7509
  secrets
6145
7510
  );
6146
- await writeKeychainEntriesFor(newId, secretsToWrite);
6147
- await writeMcpAndTrackMtime(serializeStore(next));
6148
- await secretStore.deleteAllForServer(originalId);
7511
+ await secretStore.deleteAllForServer(newId);
7512
+ const prior = [
7513
+ ...await snapshotSecretFields(
7514
+ secretStore,
7515
+ newId,
7516
+ Object.keys(secretsToWrite)
7517
+ ),
7518
+ ...originalPrior
7519
+ ];
7520
+ let diskCommitted = false;
7521
+ try {
7522
+ await writeKeychainEntriesFor(newId, secretsToWrite);
7523
+ await writeMcpAndTrackMtime(serializeStore(next));
7524
+ diskCommitted = true;
7525
+ await secretStore.deleteAllForServer(originalId);
7526
+ } catch (error) {
7527
+ if (diskCommitted) {
7528
+ try {
7529
+ await writeMcpAndTrackMtime(serializeStore(current));
7530
+ } catch (diskError) {
7531
+ warnSecretRestoreFailure(diskError);
7532
+ }
7533
+ }
7534
+ await restoreSecretFields(
7535
+ secretStore,
7536
+ prior,
7537
+ warnSecretRestoreFailure
7538
+ );
7539
+ throw error;
7540
+ }
6149
7541
  } else {
6150
7542
  const previousFields = new Set(expectedSecretFields(existing));
6151
7543
  const stillExpected = new Set(expectedSecretFields(built));
6152
7544
  const obsolete = [...previousFields].filter(
6153
7545
  (field) => field === SECRET_FIELD_OAUTH_CLIENT_SECRET ? settingsIntent.kind !== "preserve" && !(field in secrets) : !stillExpected.has(field)
6154
7546
  );
6155
- await writeKeychainEntriesFor(newId, secrets);
6156
- await writeMcpAndTrackMtime(serializeStore(next));
6157
- await deleteKeychainFields(newId, obsolete);
7547
+ const prior = await snapshotSecretFields(secretStore, newId, [
7548
+ .../* @__PURE__ */ new Set([...Object.keys(secrets), ...obsolete])
7549
+ ]);
7550
+ let diskCommitted = false;
7551
+ try {
7552
+ await writeKeychainEntriesFor(newId, secrets);
7553
+ await writeMcpAndTrackMtime(serializeStore(next));
7554
+ diskCommitted = true;
7555
+ await deleteKeychainFields(newId, obsolete);
7556
+ } catch (error) {
7557
+ if (diskCommitted) {
7558
+ try {
7559
+ await writeMcpAndTrackMtime(serializeStore(current));
7560
+ } catch (diskError) {
7561
+ warnSecretRestoreFailure(diskError);
7562
+ }
7563
+ }
7564
+ await restoreSecretFields(
7565
+ secretStore,
7566
+ prior,
7567
+ warnSecretRestoreFailure
7568
+ );
7569
+ throw error;
7570
+ }
6158
7571
  }
6159
7572
  return c.json({ ok: true });
6160
7573
  });
@@ -6175,13 +7588,28 @@ function createRemoteApp(options) {
6175
7588
  try {
6176
7589
  return await withWriteLock(async () => {
6177
7590
  const current = await readMcpConfig();
6178
- if (!(id in current.mcpServers)) {
7591
+ if (!Object.hasOwn(current.mcpServers, id)) {
6179
7592
  await secretStore.deleteAllForServer(id);
6180
7593
  return c.json({ ok: true });
6181
7594
  }
7595
+ const stored = current.mcpServers[id];
6182
7596
  delete current.mcpServers[id];
6183
- await writeMcpAndTrackMtime(serializeStore(current));
6184
- await secretStore.deleteAllForServer(id);
7597
+ const prior = await snapshotSecretFields(
7598
+ secretStore,
7599
+ id,
7600
+ expectedSecretFields(stored)
7601
+ );
7602
+ try {
7603
+ await secretStore.deleteAllForServer(id);
7604
+ await writeMcpAndTrackMtime(serializeStore(current));
7605
+ } catch (error) {
7606
+ await restoreSecretFields(
7607
+ secretStore,
7608
+ prior,
7609
+ warnSecretRestoreFailure
7610
+ );
7611
+ throw error;
7612
+ }
6185
7613
  return c.json({ ok: true });
6186
7614
  });
6187
7615
  } catch (error) {
@@ -6745,7 +8173,7 @@ async function startViteDevServer(config) {
6745
8173
  }
6746
8174
 
6747
8175
  // server/server.ts
6748
- import { readFileSync as readFileSync6 } from "fs";
8176
+ import { readFileSync as readFileSync7 } from "fs";
6749
8177
  import { join as join8, dirname as dirname8 } from "path";
6750
8178
  import { fileURLToPath as fileURLToPath5 } from "url";
6751
8179
  import { randomBytes as randomBytes4 } from "crypto";
@@ -6799,7 +8227,7 @@ async function startHonoServer(config) {
6799
8227
  });
6800
8228
  const serveIndexHtml = (c) => {
6801
8229
  const indexPath = join8(rootPath, "index.html");
6802
- const html = readFileSync6(indexPath, "utf-8");
8230
+ const html = readFileSync7(indexPath, "utf-8");
6803
8231
  c.header("Cache-Control", "no-store");
6804
8232
  return c.html(injectAuthToken(html, resolvedAuthToken));
6805
8233
  };