@modelcontextprotocol/inspector 2.8.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
@@ -146,6 +146,10 @@ var ModernListSkillsResultSchema = ListSkillsResultSchema.extend({
146
146
  var GetSkillEnvelopeSchema = z.looseObject({
147
147
  skill: SkillEntrySchema
148
148
  });
149
+ var ModernGetSkillEnvelopeSchema = GetSkillEnvelopeSchema.extend({
150
+ ttlMs: z.int().min(0),
151
+ cacheScope: z.enum(["public", "private"])
152
+ });
149
153
  var GetSkillResultSchema = GetSkillEnvelopeSchema.transform(
150
154
  (result) => result.skill
151
155
  );
@@ -1850,12 +1854,31 @@ function redactJsonValue(value) {
1850
1854
  }
1851
1855
  return value;
1852
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
+ }
1853
1876
  function redactBody(body, contentType) {
1854
1877
  if (!body) return body;
1855
1878
  const type = (contentType ?? "").toLowerCase();
1856
1879
  if (type.includes("application/x-www-form-urlencoded")) {
1857
1880
  try {
1858
- const params = new URLSearchParams(body);
1881
+ const params = new URLSearchParams(joinSensitiveContinuations(body));
1859
1882
  let changed = false;
1860
1883
  for (const key of new Set(params.keys())) {
1861
1884
  if (isSensitiveField(key)) {
@@ -2544,1082 +2567,793 @@ function createTransportNode(config, options = {}) {
2544
2567
  // ../../core/auth/requestTimeout.ts
2545
2568
  var OAUTH_TIMEOUT_WIRE_CODE = "oauth_request_timeout";
2546
2569
 
2547
- // ../../core/json/xMcpHeader.ts
2548
- 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
+ }
2549
2582
 
2550
- // ../../core/mcp/import/clientConfig.ts
2551
- function parseJsonObject(raw) {
2552
- let data;
2553
- try {
2554
- data = JSON.parse(raw);
2555
- } catch (err) {
2556
- const detail = err instanceof Error ? err.message : String(err);
2557
- 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
+ }
2558
2598
  }
2559
- if (!data || typeof data !== "object" || Array.isArray(data)) {
2560
- 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
+ }
2561
2606
  }
2562
- return data;
2607
+ return merged;
2563
2608
  }
2564
- function normalizeServerMap(servers) {
2565
- const out = {};
2566
- for (const [id, raw] of Object.entries(servers)) {
2567
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
2568
- const normalized = normalizeServerType(
2569
- raw
2570
- );
2571
- out[id] = normalized;
2572
- }
2573
- return out;
2609
+ function isStringArray(value) {
2610
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
2574
2611
  }
2575
- function parseMcpServersConfig(raw) {
2576
- const data = parseJsonObject(raw);
2577
- const servers = data.mcpServers;
2578
- if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
2579
- throw new Error("No 'mcpServers' object found in config file");
2612
+ function parseOAuthPersistSections(value) {
2613
+ if (!isRecord2(value)) {
2614
+ return null;
2580
2615
  }
2581
- return {
2582
- mcpServers: normalizeServerMap(servers)
2583
- };
2584
- }
2585
- function parseVsCodeConfig(raw) {
2586
- const data = parseJsonObject(raw);
2587
- const servers = data.servers;
2588
- if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
2589
- 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;
2590
2619
  }
2591
- return {
2592
- mcpServers: normalizeServerMap(servers)
2593
- };
2594
- }
2595
-
2596
- // ../../core/mcp/import/strategies.ts
2597
- function join4(...segments) {
2598
- return segments.map(
2599
- (s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")
2600
- ).filter((s) => s.length > 0).join("/");
2601
- }
2602
- function appData(homeDir) {
2603
- return join4(homeDir, "AppData", "Roaming");
2604
- }
2605
- function appSupportDir(platform, homeDir, app) {
2606
- if (platform === "darwin") {
2607
- return join4(homeDir, "Library", "Application Support", app);
2620
+ if ("servers" in value) {
2621
+ if (!isStringArray(value.servers)) return null;
2622
+ sections.servers = value.servers;
2608
2623
  }
2609
- if (platform === "win32") {
2610
- return join4(appData(homeDir), app);
2624
+ if ("idpSessions" in value) {
2625
+ if (!isStringArray(value.idpSessions)) return null;
2626
+ sections.idpSessions = value.idpSessions;
2611
2627
  }
2612
- return join4(homeDir, ".config", app);
2628
+ return sections;
2613
2629
  }
2614
- var claudeDesktop = {
2615
- id: "claude-desktop",
2616
- label: "Claude Desktop",
2617
- defaultPaths: (platform, homeDir) => [
2618
- join4(
2619
- appSupportDir(platform, homeDir, "Claude"),
2620
- "claude_desktop_config.json"
2621
- )
2622
- ],
2623
- parse: parseMcpServersConfig
2624
- };
2625
- var cursor = {
2626
- id: "cursor",
2627
- label: "Cursor",
2628
- defaultPaths: (_platform, homeDir) => [join4(homeDir, ".cursor", "mcp.json")],
2629
- parse: parseMcpServersConfig
2630
- };
2631
- var cline = {
2632
- id: "cline",
2633
- label: "Cline",
2634
- defaultPaths: (platform, homeDir) => [
2635
- // Cline (VS Code extension) global settings live under the editor's
2636
- // globalStorage; the documented cross-platform fallback is ~/Documents.
2637
- join4(
2638
- appSupportDir(platform, homeDir, "Code"),
2639
- "User",
2640
- "globalStorage",
2641
- "saoudrizwan.claude-dev",
2642
- "settings",
2643
- "cline_mcp_settings.json"
2644
- ),
2645
- join4(homeDir, "Documents", "Cline", "MCP", "cline_mcp_settings.json")
2646
- ],
2647
- parse: parseMcpServersConfig
2648
- };
2649
- var vscode = {
2650
- id: "vscode",
2651
- label: "VS Code",
2652
- defaultPaths: (platform, homeDir) => [
2653
- join4(appSupportDir(platform, homeDir, "Code"), "User", "mcp.json")
2654
- ],
2655
- parse: parseVsCodeConfig
2656
- };
2657
- var IMPORT_STRATEGY_LIST = [
2658
- claudeDesktop,
2659
- cursor,
2660
- cline,
2661
- 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"
2662
2639
  ];
2663
- var IMPORT_STRATEGIES = Object.fromEntries(IMPORT_STRATEGY_LIST.map((s) => [s.id, s]));
2664
- function getImportStrategy(id) {
2665
- 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
+ );
2666
2646
  }
2667
-
2668
- // ../../core/mcp/import/resolveSource.ts
2669
- function resolveImportSource(type, platform, homeDir, readFile2) {
2670
- const strategy = getImportStrategy(type);
2671
- if (!strategy) return null;
2672
- const searched = strategy.defaultPaths(platform, homeDir);
2673
- for (const path6 of searched) {
2674
- let raw;
2675
- try {
2676
- raw = readFile2(path6);
2677
- } catch (err) {
2678
- const msg = err instanceof Error ? err.message : String(err);
2679
- return {
2680
- type,
2681
- found: true,
2682
- path: path6,
2683
- error: `Failed to read ${path6}: ${msg}`,
2684
- searched
2685
- };
2686
- }
2687
- if (raw === null) continue;
2688
- try {
2689
- const config = strategy.parse(raw);
2690
- return { type, found: true, path: path6, config, searched };
2691
- } catch (err) {
2692
- const msg = err instanceof Error ? err.message : String(err);
2693
- 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;
2694
2662
  }
2695
2663
  }
2696
- return { type, found: false, searched };
2664
+ return true;
2697
2665
  }
2698
-
2699
- // ../../core/mcp/remote/progressToken.ts
2700
- import { ProgressTokenSchema } from "@modelcontextprotocol/core";
2701
- function progressTokenOf(message) {
2702
- if ("method" in message && message.method === "notifications/progress" && "params" in message && typeof message.params === "object" && message.params !== null) {
2703
- const parsed = ProgressTokenSchema.safeParse(
2704
- message.params.progressToken
2705
- );
2706
- if (parsed.success) {
2707
- 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
+ }
2708
2680
  }
2709
2681
  }
2710
- return void 0;
2682
+ return Object.values(snapshot.idpSessions).every(isValidIdpSession);
2711
2683
  }
2712
-
2713
- // ../../core/mcp/remote/node/remote-session.ts
2714
- var RemoteSession = class _RemoteSession {
2715
- sessionId;
2716
- transport;
2717
- eventQueue = [];
2718
- eventConsumer = null;
2719
- transportDead = false;
2720
- transportError = null;
2721
- activeSendCount = 0;
2722
- /**
2723
- * Suppress duplicate ambient SSE auth echoes after HTTP command-scoped delivery.
2724
- * Stale transport `onerror` may arrive after the send completes.
2725
- */
2726
- authHttpEchoSuppressUntilMs = 0;
2727
- static AUTH_HTTP_ECHO_SUPPRESS_MS = 3e4;
2728
- requestWaits = /* @__PURE__ */ new Map();
2729
- authProviderHandle = null;
2730
- appliedProtocolVersion;
2731
- /** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
2732
- static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
2733
- constructor(sessionId) {
2734
- this.sessionId = sessionId;
2735
- }
2736
- setAuthProviderHandle(handle) {
2737
- this.authProviderHandle = handle;
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;
2738
2697
  }
2739
- setAuthState(authState) {
2740
- if (!this.authProviderHandle) {
2741
- throw new Error("Session has no OAuth auth provider");
2742
- }
2743
- this.authProviderHandle.setAuthState(authState);
2698
+ const parsed = typeof raw === "string" ? raw ? parseStore(raw) : null : raw;
2699
+ if (!isRecord2(parsed)) {
2700
+ return null;
2744
2701
  }
2745
- setTransport(transport) {
2746
- this.transport = transport;
2702
+ if (isRecord2(parsed.state) && "version" in parsed) {
2703
+ return snapshotFromPayload(parsed.state);
2747
2704
  }
2748
- /**
2749
- * Apply the client's negotiated protocol version to the upstream transport
2750
- * (#1935). The browser's SDK Client owns the `initialize` handshake, so the
2751
- * `setProtocolVersion` call it makes lands on the *remote* transport; without
2752
- * this the backend's real HTTP transport never learns the version and drops
2753
- * `Mcp-Protocol-Version` from every subsequent request.
2754
- *
2755
- * Ignores anything that isn't a plausible version token so a client can't
2756
- * push arbitrary bytes into an upstream header, and re-applies only on
2757
- * change (the value rides every `/api/mcp/send`). The value arrives from an
2758
- * unvalidated JSON body, so the type is checked rather than assumed —
2759
- * `RegExp.test` would otherwise coerce a `123` or `true` into a "matching"
2760
- * string and hand the wrong type to the transport.
2761
- */
2762
- applyProtocolVersion(version) {
2763
- if (typeof version !== "string" || version === this.appliedProtocolVersion || !_RemoteSession.PROTOCOL_VERSION_PATTERN.test(version)) {
2764
- return;
2765
- }
2766
- this.appliedProtocolVersion = version;
2767
- this.transport.setProtocolVersion?.(version);
2705
+ if ("servers" in parsed || "idpSessions" in parsed) {
2706
+ return snapshotFromPayload(parsed);
2768
2707
  }
2769
- setEventConsumer(consumer) {
2770
- this.eventConsumer = consumer;
2771
- while (this.eventQueue.length > 0) {
2772
- const ev = this.eventQueue.shift();
2773
- consumer(ev);
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;
2774
2718
  }
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 };
2775
2726
  }
2776
- clearEventConsumer() {
2777
- this.eventConsumer = null;
2778
- return this.transportDead;
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";
2779
2745
  }
2780
- markTransportDead(error) {
2781
- this.transportDead = true;
2782
- this.transportError = error;
2783
- this.pushEvent({
2784
- type: "transport_error",
2785
- data: {
2786
- error,
2787
- code: -32e3
2788
- // MCP error code for connection closed
2789
- }
2790
- });
2791
- }
2792
- isTransportDead() {
2793
- return this.transportDead;
2794
- }
2795
- getTransportError() {
2796
- return this.transportError;
2797
- }
2798
- hasEventConsumer() {
2799
- return this.eventConsumer !== null;
2800
- }
2801
- beginSend() {
2802
- this.activeSendCount++;
2803
- }
2804
- endSend() {
2805
- if (this.activeSendCount > 0) {
2806
- this.activeSendCount--;
2746
+ };
2747
+ var checkKeyringShape = (mod) => {
2748
+ try {
2749
+ if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
2750
+ return { ok: true, mod };
2807
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
+ };
2808
2766
  }
2809
- /** Command-scoped auth was returned on HTTP; suppress async onerror echo on SSE. */
2810
- noteAuthChallengeDeliveredViaHttp() {
2811
- this.extendAuthHttpEchoSuppression();
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";
2812
2788
  }
2813
- extendAuthHttpEchoSuppression() {
2814
- this.authHttpEchoSuppressUntilMs = Date.now() + _RemoteSession.AUTH_HTTP_ECHO_SUPPRESS_MS;
2789
+ };
2790
+ var SecretFileLockHeldError = class extends SecretStoreUnavailableError {
2791
+ constructor(message, options) {
2792
+ super(message, options);
2793
+ this.name = "SecretFileLockHeldError";
2815
2794
  }
2816
- shouldSuppressAuthEcho() {
2817
- return Date.now() < this.authHttpEchoSuppressUntilMs;
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";
2818
2803
  }
2819
- hasActiveSend() {
2820
- return this.activeSendCount > 0;
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.`;
2821
2808
  }
2822
- pushAuthChallenge(challenge) {
2823
- this.pushEvent({ type: "auth_challenge", data: challenge });
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).`;
2824
2811
  }
2825
- /**
2826
- * Streamable HTTP `transport.send()` can return before the JSON-RPC response
2827
- * arrives. The remote send handler awaits this so `/api/mcp/send` returns
2828
- * `auth_challenge` or `ok: true` only after the MCP round-trip (or auth error).
2829
- */
2830
- waitForRequestResponse(requestId, timeoutMs = 6e4) {
2831
- return new Promise((resolve9, reject) => {
2832
- let timer;
2833
- const arm = () => {
2834
- if (timeoutMs <= 0) return;
2835
- timer = setTimeout(() => {
2836
- this.requestWaits.delete(requestId);
2837
- reject(
2838
- new Error(
2839
- `MCP request ${String(requestId)} timed out after ${timeoutMs}ms`
2840
- )
2841
- );
2842
- }, timeoutMs);
2843
- };
2844
- const clear = () => {
2845
- if (timer !== void 0) {
2846
- clearTimeout(timer);
2847
- timer = void 0;
2848
- }
2849
- };
2850
- arm();
2851
- this.requestWaits.set(requestId, {
2852
- resolve: () => {
2853
- clear();
2854
- resolve9();
2855
- },
2856
- reject: (error) => {
2857
- clear();
2858
- reject(error);
2859
- },
2860
- // A progress notification for this request re-arms the full window, so
2861
- // a tool that keeps reporting progress never trips the relay's own
2862
- // deadline — the same guarantee the SDK client's `resetTimeoutOnProgress`
2863
- // gives on a direct transport (#2028).
2864
- resetTimeout: () => {
2865
- if (timer === void 0) return;
2866
- clear();
2867
- arm();
2868
- }
2869
- });
2870
- });
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
+ };
2822
+ }
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
+ };
2871
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 {
2872
2838
  /**
2873
- * Re-arm a pending request's wait timeout. Invoked from {@link onMessage} when
2874
- * a `notifications/progress` carrying this request's `progressToken` arrives.
2875
- * A no-op when no wait is pending for the id.
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.
2876
2842
  */
2877
- noteRequestProgress(requestId) {
2878
- this.requestWaits.get(requestId)?.resetTimeout();
2879
- }
2880
- cancelRequestWait(requestId) {
2881
- const wait = this.requestWaits.get(requestId);
2882
- if (!wait) {
2883
- return;
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);
2884
2855
  }
2885
- this.requestWaits.delete(requestId);
2886
- wait.reject(new Error("MCP request wait cancelled"));
2887
2856
  }
2888
- rejectActiveRequestWaits(error) {
2889
- for (const wait of this.requestWaits.values()) {
2890
- wait.reject(error);
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;
2891
2869
  }
2892
- this.requestWaits.clear();
2893
2870
  }
2894
- /**
2895
- * Auth errors from the MCP transport.
2896
- * - During an active send: command path owns delivery (HTTP); never SSE.
2897
- * - After HTTP already returned auth_challenge: swallow async onerror echo.
2898
- * - Otherwise: ambient SSE (idle session).
2899
- */
2900
- handleTransportAuthError(error) {
2901
- if (!(error instanceof AuthChallengeError)) {
2902
- return false;
2903
- }
2904
- this.rejectActiveRequestWaits(error);
2905
- if (this.hasActiveSend()) {
2906
- this.extendAuthHttpEchoSuppression();
2907
- return true;
2908
- }
2909
- if (this.shouldSuppressAuthEcho()) {
2910
- return true;
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);
2911
2883
  }
2912
- this.pushAuthChallenge(error.authChallenge);
2913
- return true;
2914
2884
  }
2915
- settleRequestWait(requestId) {
2916
- const wait = this.requestWaits.get(requestId);
2917
- if (!wait) {
2918
- return;
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);
2919
2898
  }
2920
- this.requestWaits.delete(requestId);
2921
- wait.resolve();
2922
2899
  }
2923
- pushEvent(event) {
2924
- if (this.eventConsumer) {
2925
- this.eventConsumer(event);
2926
- } else {
2927
- this.eventQueue.push(event);
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);
2928
2909
  }
2929
- }
2930
- onMessage(message) {
2931
- if ("id" in message && message.id !== null && message.id !== void 0 && ("result" in message || "error" in message)) {
2932
- this.settleRequestWait(message.id);
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);
2933
2916
  }
2934
- const progressToken = progressTokenOf(message);
2935
- if (progressToken !== void 0) {
2936
- this.noteRequestProgress(progressToken);
2917
+ }
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);
2937
2938
  }
2938
- this.pushEvent({ type: "message", data: message });
2939
2939
  }
2940
- onFetchRequest(entry) {
2941
- this.pushEvent({
2942
- type: "fetch_request",
2943
- data: {
2944
- ...entry,
2945
- timestamp: entry.timestamp instanceof Date ? entry.timestamp.toISOString() : entry.timestamp
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);
2946
2954
  }
2947
- });
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;
2948
2986
  }
2949
- onFetchResponseBody(id, responseBody) {
2950
- this.pushEvent({
2951
- type: "fetch_request_body_update",
2952
- data: { id, responseBody }
2953
- });
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;
2954
3003
  }
2955
- onFetchStreamUpdate(id, stream) {
2956
- this.pushEvent({
2957
- type: "fetch_stream_update",
2958
- data: {
2959
- id,
2960
- eventCount: stream.eventCount,
2961
- ...stream.closedAt && { closedAt: stream.closedAt.toISOString() }
2962
- }
2963
- });
3004
+ async set(serverId, field, value) {
3005
+ this.map.set(buildAccount(serverId, field), value);
2964
3006
  }
2965
- onStderr(entry) {
2966
- this.pushEvent({
2967
- type: "stdio_log",
2968
- data: {
2969
- timestamp: entry.timestamp.toISOString(),
2970
- message: entry.message
2971
- }
2972
- });
3007
+ async delete(serverId, field) {
3008
+ this.map.delete(buildAccount(serverId, field));
3009
+ }
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
+ }
3015
+ }
3016
+ };
3017
+ var SessionSecretStore = class extends InMemorySecretStore {
3018
+ async isDurable() {
3019
+ return false;
2973
3020
  }
2974
3021
  };
2975
3022
 
2976
- // ../../core/mcp/remote/node/tokenAuthProvider.ts
2977
- var REMOTE_OAUTH_STUB_METADATA = {
2978
- redirect_uris: [],
2979
- 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
2980
3030
  };
2981
- 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
+ };
2982
3074
  return {
2983
- ...state.oauthTokens && {
2984
- 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
+ });
2985
3089
  },
2986
- ...state.oauthClient && {
2987
- 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();
2988
3106
  }
2989
3107
  };
2990
3108
  }
2991
- function createRemoteAuthProvider(initialAuthState) {
2992
- if (!initialAuthState?.oauthTokens && !initialAuthState?.oauthClient) {
2993
- return void 0;
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
+ });
3128
+ }
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;
2994
3137
  }
2995
- let authState = cloneAuthState(initialAuthState);
2996
- const provider = {
2997
- get clientMetadata() {
2998
- return REMOTE_OAUTH_STUB_METADATA;
2999
- },
3000
- get redirectUrl() {
3001
- return "";
3002
- },
3003
- async tokens() {
3004
- return authState.oauthTokens;
3005
- },
3006
- async clientInformation() {
3007
- if (!authState.oauthClient?.client_id) {
3008
- return void 0;
3009
- }
3010
- return {
3011
- client_id: authState.oauthClient.client_id,
3012
- ...authState.oauthClient.client_secret && {
3013
- client_secret: authState.oauthClient.client_secret
3014
- }
3015
- };
3016
- },
3017
- async saveTokens(tokens) {
3018
- authState = {
3019
- ...authState,
3020
- oauthTokens: {
3021
- access_token: tokens.access_token,
3022
- token_type: tokens.token_type,
3023
- expires_in: tokens.expires_in,
3024
- refresh_token: tokens.refresh_token,
3025
- scope: tokens.scope,
3026
- id_token: tokens.id_token
3027
- }
3028
- };
3029
- },
3030
- codeVerifier() {
3031
- return void 0;
3032
- },
3033
- async saveCodeVerifier() {
3034
- },
3035
- clear() {
3036
- authState = {};
3037
- },
3038
- async redirectToAuthorization() {
3039
- throw new Error(
3040
- "OAuth re-authorization must be performed in the Inspector web client (remote server cannot complete OAuth flows)"
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;
3152
+ try {
3153
+ release = await acquire(target, 0, fsShim).catch((err) => {
3154
+ if (!isStuckOrHeld(err, lockPathOf(target))) throw err;
3155
+ return acquire(target, RETRY, fsShim);
3156
+ });
3157
+ } catch (err) {
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.`
3041
3161
  );
3042
- },
3043
- state() {
3044
- return "";
3045
3162
  }
3046
- // This provider implements only the members the remote backend transport
3047
- // exercises (token get/save, client info, non-interactive redirect); the
3048
- // interactive-only members the SDK never calls on the server side are stubbed
3049
- // with narrower shapes (e.g. `codeVerifier()` returns `undefined`, not the
3050
- // interface's `string | Promise<string>`), so it can't be assigned to the
3051
- // full `OAuthClientProvider` without the double cast.
3052
- };
3053
- return {
3054
- provider,
3055
- setAuthState(next) {
3056
- authState = cloneAuthState(next);
3057
- },
3058
- getAuthState() {
3059
- return cloneAuthState(authState);
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;
3167
+ }
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
+ );
3060
3189
  }
3061
3190
  };
3062
3191
  }
3063
-
3064
- // ../../core/auth/node/secret-store.ts
3065
- var SERVICE_NAME = "mcp-inspector";
3066
- var keyringLoad;
3067
- var KeyringModuleShapeError = class extends Error {
3068
- constructor(detail, options) {
3069
- super(`@napi-rs/keyring loaded but ${detail}`, options);
3070
- this.name = "KeyringModuleShapeError";
3071
- }
3072
- };
3073
- var checkKeyringShape = (mod) => {
3192
+ async function withSecretFileLock(filePath, fn) {
3193
+ const release = await openSecretFileLock(filePath);
3194
+ if (release === null) return fn();
3074
3195
  try {
3075
- if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
3076
- return { ok: true, mod };
3077
- }
3078
- return {
3079
- ok: false,
3080
- err: new KeyringModuleShapeError(
3081
- "did not expose AsyncEntry / findCredentialsAsync"
3082
- )
3083
- };
3084
- } catch (err) {
3085
- return {
3086
- ok: false,
3087
- err: new KeyringModuleShapeError(
3088
- `its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
3089
- { cause: err }
3090
- )
3091
- };
3196
+ return await fn();
3197
+ } finally {
3198
+ await release();
3092
3199
  }
3093
- };
3094
- var loadKeyring = () => {
3095
- keyringLoad ??= import("@napi-rs/keyring").then(
3096
- checkKeyringShape,
3097
- (err) => ({ ok: false, err })
3098
- );
3099
- return keyringLoad;
3100
- };
3101
- function parseAccount(account) {
3102
- const idx = account.indexOf(":");
3103
- if (idx <= 0 || idx === account.length - 1) return null;
3104
- return {
3105
- serverId: account.slice(0, idx),
3106
- field: account.slice(idx + 1)
3107
- };
3108
3200
  }
3109
- var buildAccount = (serverId, field) => `${serverId}:${field}`;
3110
- var SecretStoreUnavailableError = class extends Error {
3111
- constructor(message, options) {
3112
- super(message, options);
3113
- this.name = "SecretStoreUnavailableError";
3114
- }
3115
- };
3116
- var KeychainUnavailableError = class extends SecretStoreUnavailableError {
3117
- constructor(cause) {
3118
- const message = cause instanceof Error ? cause.message : String(cause);
3119
- super(
3120
- `OS keychain is not available. ${hintFor(cause, message)} Underlying error: ${message}`
3121
- );
3122
- this.name = "KeychainUnavailableError";
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;
3123
3237
  }
3124
- };
3125
- var hintFor = (cause, message) => {
3126
- if (cause instanceof KeyringModuleShapeError) {
3127
- 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
+ };
3128
3249
  }
3129
- if (message.includes("Cannot find native binding")) {
3130
- 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` };
3131
3254
  }
3132
- return `On Linux, install libsecret / gnome-keyring.`;
3133
- };
3134
- async function probeKeyringAvailable() {
3135
- const keyring = await loadKeyring();
3136
- if (!keyring.ok) {
3137
- const err = keyring.err;
3255
+ const resolved2 = path3.resolve(keyFile);
3256
+ if (secretFilePath !== void 0 && isSameFile(resolved2, secretFilePath)) {
3138
3257
  return {
3139
- available: false,
3140
- 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`
3141
3259
  };
3142
3260
  }
3261
+ let contents;
3143
3262
  try {
3144
- const entry = new keyring.mod.AsyncEntry(SERVICE_NAME, PROBE_ACCOUNT);
3145
- await entry.getPassword();
3146
- return { available: true };
3263
+ contents = readFileSync4(resolved2, "utf-8");
3147
3264
  } catch (err) {
3148
3265
  return {
3149
- available: false,
3150
- 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)}`
3151
3269
  };
3152
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 };
3153
3276
  }
3154
- var PROBE_ACCOUNT = "__inspector:probe";
3155
- var KeyringSecretStore = class {
3156
- /**
3157
- * The intolerant read. Same lookup as {@link get}, minus the catch — a
3158
- * keychain that cannot answer must not be reported as an empty one to a
3159
- * caller that is about to write based on the answer.
3160
- */
3161
- async getStrict(serverId, field) {
3162
- try {
3163
- const keyring = await loadKeyring();
3164
- if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
3165
- const entry = new keyring.mod.AsyncEntry(
3166
- SERVICE_NAME,
3167
- buildAccount(serverId, field)
3168
- );
3169
- return await entry.getPassword() ?? null;
3170
- } catch (err) {
3171
- if (err instanceof KeychainUnavailableError) throw err;
3172
- throw new KeychainUnavailableError(err);
3173
- }
3174
- }
3175
- async get(serverId, field) {
3176
- try {
3177
- const keyring = await loadKeyring();
3178
- if (!keyring.ok) return null;
3179
- const entry = new keyring.mod.AsyncEntry(
3180
- SERVICE_NAME,
3181
- buildAccount(serverId, field)
3182
- );
3183
- const v = await entry.getPassword();
3184
- return v ?? null;
3185
- } catch {
3186
- return null;
3187
- }
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
+ );
3188
3283
  }
3189
- async set(serverId, field, value) {
3190
- try {
3191
- const keyring = await loadKeyring();
3192
- if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
3193
- const entry = new keyring.mod.AsyncEntry(
3194
- SERVICE_NAME,
3195
- buildAccount(serverId, field)
3196
- );
3197
- await entry.setPassword(value);
3198
- } catch (err) {
3199
- if (err instanceof KeychainUnavailableError) throw err;
3200
- throw new KeychainUnavailableError(err);
3201
- }
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
+ );
3202
3290
  }
3203
- async delete(serverId, field) {
3204
- try {
3205
- const keyring = await loadKeyring();
3206
- if (!keyring.ok) return;
3207
- const entry = new keyring.mod.AsyncEntry(
3208
- SERVICE_NAME,
3209
- 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.`
3210
3295
  );
3211
- await entry.deleteCredential();
3212
- } catch {
3213
3296
  }
3214
3297
  }
3215
- async deleteAllForServer(serverId) {
3216
- let creds;
3217
- try {
3218
- const keyring = await loadKeyring();
3219
- if (!keyring.ok) return;
3220
- creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
3221
- } catch {
3222
- return;
3223
- }
3224
- const prefix = `${serverId}:`;
3225
- for (const c of creds) {
3226
- if (!c.account.startsWith(prefix)) continue;
3227
- const parsed = parseAccount(c.account);
3228
- if (!parsed || parsed.serverId !== serverId) continue;
3229
- await this.delete(serverId, parsed.field);
3230
- }
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";
3231
3306
  }
3232
3307
  };
3233
- async function secretStoreGetStrict(store, serverId, field) {
3234
- return store.getStrict ? store.getStrict(serverId, field) : store.get(serverId, field);
3235
- }
3236
- async function secretStoreGetMany(store, requests) {
3237
- if (store.getMany) return store.getMany(requests);
3238
- const out = {};
3239
- await Promise.all(
3240
- requests.map(async ({ serverId, fields }) => {
3241
- const entries = await Promise.all(
3242
- fields.map(
3243
- async (field) => [field, await store.get(serverId, field)]
3244
- )
3245
- );
3246
- const found = {};
3247
- for (const [field, value] of entries) {
3248
- if (value !== null) found[field] = value;
3249
- }
3250
- out[serverId] = found;
3251
- })
3252
- );
3253
- 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
+ };
3254
3318
  }
3255
- async function secretStoreSetMany(store, serverId, values) {
3256
- if (store.setMany) return store.setMany(serverId, values);
3257
- await Promise.all(
3258
- Object.entries(values).map(
3259
- ([field, value]) => store.set(serverId, field, value)
3260
- )
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]
3261
3326
  );
3262
3327
  }
3263
- async function secretStoreIsDurable(store) {
3264
- return store.isDurable ? store.isDurable() : true;
3265
- }
3266
- var InMemorySecretStore = class {
3267
- map = /* @__PURE__ */ new Map();
3268
- async get(serverId, field) {
3269
- 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";
3270
3332
  }
3271
- async set(serverId, field, value) {
3272
- 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}`;
3273
3337
  }
3274
- async delete(serverId, field) {
3275
- 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}`;
3276
3340
  }
3277
- async deleteAllForServer(serverId) {
3278
- const prefix = `${serverId}:`;
3279
- for (const key of [...this.map.keys()]) {
3280
- if (key.startsWith(prefix)) this.map.delete(key);
3281
- }
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";
3282
3346
  }
3283
- };
3284
- var SessionSecretStore = class extends InMemorySecretStore {
3285
- async isDurable() {
3286
- 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`;
3287
3349
  }
3288
- };
3289
-
3290
- // ../../core/auth/node/secret-store-selection.ts
3291
- import { randomUUID } from "crypto";
3292
- import * as fsSync from "fs";
3293
- import * as fs4 from "fs/promises";
3294
- import * as path4 from "path";
3295
-
3296
- // ../../core/auth/node/file-secret-store.ts
3297
- import * as crypto2 from "crypto";
3298
- import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
3299
- import * as fs3 from "fs/promises";
3300
- import * as path3 from "path";
3301
-
3302
- // ../../core/auth/node/file-lock.ts
3303
- import nodeFs, { statSync, rmdirSync } from "fs";
3304
- import * as fs2 from "fs/promises";
3305
- import * as path2 from "path";
3306
- import properLockfile from "proper-lockfile";
3307
- var STALE_MS = 1e4;
3308
- var RETRY = {
3309
- retries: 20,
3310
- factor: 2,
3311
- minTimeout: 20,
3312
- maxTimeout: 1e3
3313
- };
3314
- var RETRY_BUDGET_MS = (() => {
3315
- let total = 0;
3316
- let delay = RETRY.minTimeout;
3317
- for (let i = 0; i < RETRY.retries; i++) {
3318
- total += Math.min(delay, RETRY.maxTimeout);
3319
- delay *= RETRY.factor;
3350
+ if (!isPositiveInt(r) || !isPositiveInt(p)) {
3351
+ return `kdf r/p are ${String(r)}/${String(p)}, which are not positive integers`;
3320
3352
  }
3321
- return total;
3322
- })();
3323
- var warned = /* @__PURE__ */ new Set();
3324
- function warnOnce(message) {
3325
- if (warned.has(message)) return;
3326
- warned.add(message);
3327
- console.warn(`[mcp-inspector] ${message}`);
3328
- }
3329
- function isStuckOrHeld(err, lockPath) {
3330
- return isHeldElsewhere(err) || identifySync(lockPath) !== null;
3331
- }
3332
- var isHeldElsewhere = (err) => err.code === "ELOCKED";
3333
- var describeError = (err) => err instanceof Error ? err.message : String(err);
3334
- var lockPathOf = (target) => `${target}.lock`;
3335
- function identifySync(lockPath) {
3336
- try {
3337
- const stat2 = statSync(lockPath);
3338
- return { ino: stat2.ino, birthtimeMs: stat2.birthtimeMs };
3339
- } catch {
3340
- 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})`;
3341
3355
  }
3342
- }
3343
- function guardedFs(lockPath, owned, onRefused) {
3344
- const mine = () => {
3345
- if (owned.id === null) return true;
3346
- const now = identifySync(lockPath);
3347
- if (now === null) return false;
3348
- return now.ino === owned.id.ino && now.birthtimeMs === owned.id.birthtimeMs;
3349
- };
3350
- const removeIfMine = () => {
3351
- if (!mine()) {
3352
- onRefused();
3353
- return;
3354
- }
3355
- rmdirSync(lockPath);
3356
- };
3357
- return {
3358
- ...nodeFs,
3359
- // **Identity is captured here, not after `lock()` resolves.** `mkdir` is
3360
- // the moment the directory becomes ours, and it is synchronous with
3361
- // respect to this process: recording it in the callback, before yielding,
3362
- // leaves no window. Capturing after the acquire promise settled — as this
3363
- // did originally — spans the library's own `utimes`/`stat` probe, and a
3364
- // waiter replacing our directory inside that span would have us record
3365
- // *the winner's* identity as our own, after which the guard below would
3366
- // cheerfully delete their lock.
3367
- mkdir: (p, cb) => {
3368
- nodeFs.mkdir(p, (err) => {
3369
- if (!err) owned.id = identifySync(lockPath);
3370
- cb(err);
3371
- });
3372
- },
3373
- // Reported as success when refused: the library's bookkeeping should
3374
- // forget this lock either way. Leaving it registered would hand the
3375
- // `signal-exit` handler a record pointing at the winner's directory.
3376
- rmdir: (_p, cb) => {
3377
- try {
3378
- removeIfMine();
3379
- cb(null);
3380
- } catch (err) {
3381
- cb(err);
3382
- }
3383
- },
3384
- /* v8 ignore next 3 -- @preserve: only reachable from proper-lockfile's
3385
- signal-exit handler, i.e. at real process exit, which no in-process
3386
- test can drive. Its logic is `removeIfMine`, covered via `rmdir`. */
3387
- rmdirSync: () => {
3388
- removeIfMine();
3389
- }
3390
- };
3391
- }
3392
- function acquire(target, retries, fsShim) {
3393
- return properLockfile.lock(target, {
3394
- realpath: false,
3395
- stale: STALE_MS,
3396
- retries,
3397
- // Every directory removal the library performs — on release and from its
3398
- // exit handler — routes through here. See `guardedFs`.
3399
- fs: fsShim,
3400
- // The library's default `onCompromised` *throws* — from a timer, with no
3401
- // caller on the stack, so it lands as an uncaught exception and takes the
3402
- // process down. This is also the library's *only* signal for the
3403
- // stale-takeover race described at the top of this file — someone
3404
- // declared our lock stale and replaced it — so it is the one place a user
3405
- // learns the guarantee was lost. Worth saying; not worth killing an
3406
- // Inspector session over.
3407
- onCompromised: (err) => warnOnce(
3408
- `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.`
3409
- )
3410
- });
3411
- }
3412
- async function isFileLockHeld(filePath) {
3413
- try {
3414
- return await properLockfile.check(path2.resolve(filePath), {
3415
- realpath: false,
3416
- stale: STALE_MS
3417
- });
3418
- } catch {
3419
- return false;
3420
- }
3421
- }
3422
- async function openSecretFileLock(filePath) {
3423
- const target = path2.resolve(filePath);
3424
- await fs2.mkdir(path2.dirname(target), { recursive: true }).catch(() => {
3425
- });
3426
- const owned = { id: null };
3427
- const fsShim = guardedFs(
3428
- lockPathOf(target),
3429
- owned,
3430
- () => warnOnce(
3431
- `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.`
3432
- )
3433
- );
3434
- let release;
3435
- try {
3436
- release = await acquire(target, 0, fsShim).catch((err) => {
3437
- if (!isStuckOrHeld(err, lockPathOf(target))) throw err;
3438
- return acquire(target, RETRY, fsShim);
3439
- });
3440
- } catch (err) {
3441
- if (isStuckOrHeld(err, lockPathOf(target))) {
3442
- throw new SecretStoreUnavailableError(
3443
- `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.`
3444
- );
3445
- }
3446
- warnOnce(
3447
- `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.`
3448
- );
3449
- return null;
3450
- }
3451
- return async () => {
3452
- try {
3453
- await release();
3454
- } catch (err) {
3455
- if (err.code === "ERELEASED") return;
3456
- warnOnce(
3457
- // **Conditional, not an instruction.** Two blanket versions of this
3458
- // have now been wrong in opposite directions: "it expires on its own"
3459
- // (false whenever the same `rmdir` that blocked us also blocks stale
3460
- // takeover — `ENOTEMPTY`, `EACCES`, `EPERM`, `EROFS`), and "remove it
3461
- // by hand" (false whenever the failure was transient, since the path
3462
- // may by then hold a *different, live* Inspector's lock, and deleting
3463
- // that destroys the exclusion of a process that did nothing wrong).
3464
- //
3465
- // `proper-lockfile` forwards whatever the filesystem returned, and
3466
- // nothing here can tell a permanent refusal from a passing one. So
3467
- // this reports what happened and states the two conditions the
3468
- // operator can check for themselves, rather than asserting an outcome
3469
- // this code does not know.
3470
- `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.`
3471
- );
3472
- }
3473
- };
3474
- }
3475
- async function withSecretFileLock(filePath, fn) {
3476
- const release = await openSecretFileLock(filePath);
3477
- if (release === null) return fn();
3478
- try {
3479
- return await fn();
3480
- } finally {
3481
- await release();
3482
- }
3483
- }
3484
-
3485
- // ../../core/auth/node/file-secret-store.ts
3486
- var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve9, reject) => {
3487
- crypto2.scrypt(passphrase, salt, keylen, options, (err, key) => {
3488
- if (err) reject(err);
3489
- else resolve9(key);
3490
- });
3491
- });
3492
- var FORMAT_VERSION = 1;
3493
- var CIPHER = "aes-256-gcm";
3494
- var KEY_BYTES = 32;
3495
- var IV_BYTES = 12;
3496
- var GCM_TAG_BYTES = 16;
3497
- var SCRYPT_N = 16384;
3498
- var SCRYPT_R = 8;
3499
- var SCRYPT_P = 1;
3500
- var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
3501
- var SECRET_KEY_FILE_ENV = "MCP_INSPECTOR_SECRET_KEY_FILE";
3502
- function isSameFile(a, b) {
3503
- if (path3.resolve(a) === path3.resolve(b)) return true;
3504
- try {
3505
- const sa = statSync2(a);
3506
- const sb = statSync2(b);
3507
- return sa.dev === sb.dev && sa.ino === sb.ino;
3508
- } catch {
3509
- return false;
3510
- }
3511
- }
3512
- function resolveSecretPassphrase(env = process.env, secretFilePath) {
3513
- const direct = env[SECRET_KEY_ENV];
3514
- const hasDirect = direct !== void 0 && direct.trim() !== "";
3515
- const rawKeyFile = env[SECRET_KEY_FILE_ENV];
3516
- const wantsKeyFile = rawKeyFile !== void 0;
3517
- const keyFile = rawKeyFile?.trim();
3518
- if (hasDirect && wantsKeyFile) {
3519
- return {
3520
- problem: `both ${SECRET_KEY_ENV} and ${SECRET_KEY_FILE_ENV} are set; set only one`
3521
- };
3522
- }
3523
- if (hasDirect) return { passphrase: direct };
3524
- if (!wantsKeyFile) return {};
3525
- if (!keyFile) {
3526
- return { problem: `${SECRET_KEY_FILE_ENV} is set but empty` };
3527
- }
3528
- const resolved2 = path3.resolve(keyFile);
3529
- if (secretFilePath !== void 0 && isSameFile(resolved2, secretFilePath)) {
3530
- return {
3531
- problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is the secrets file itself; point it at a separate key file`
3532
- };
3533
- }
3534
- let contents;
3535
- try {
3536
- contents = readFileSync4(resolved2, "utf-8");
3537
- } catch (err) {
3538
- return {
3539
- // `String(err)` rather than `.message`: `readFileSync` only ever throws
3540
- // an `Error`, so an `instanceof` guard would be a branch no test can take.
3541
- problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) could not be read: ${String(err)}`
3542
- };
3543
- }
3544
- const passphrase = contents.replace(/[\r\n]+$/, "");
3545
- if (passphrase.trim() === "") {
3546
- return { problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is empty` };
3547
- }
3548
- return { passphrase };
3549
- }
3550
- var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
3551
- function asSecretMap(value, filePath) {
3552
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
3553
- throw new SecretStoreUnavailableError(
3554
- `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.`
3555
- );
3556
- }
3557
- for (const [key, entry] of Object.entries(value)) {
3558
- if (typeof entry !== "string") {
3559
- throw new SecretStoreUnavailableError(
3560
- `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.`
3561
- );
3562
- }
3563
- }
3564
- return { ...value };
3565
- }
3566
- var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
3567
- constructor(filePath, hasKey) {
3568
- super(
3569
- 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.`
3570
- );
3571
- this.name = "SecretFileKeyMismatchError";
3572
- }
3573
- };
3574
- var encodeParts = (iv, tag, body) => `${iv.toString("base64")}.${tag.toString("base64")}.${body.toString("base64")}`;
3575
- function decodeParts(payload) {
3576
- const parts = payload.split(".");
3577
- if (parts.length !== 3) return null;
3578
- const [iv, tag, body] = parts;
3579
- return {
3580
- iv: Buffer.from(iv, "base64"),
3581
- tag: Buffer.from(tag, "base64"),
3582
- body: Buffer.from(body, "base64")
3583
- };
3584
- }
3585
- var MAX_WRITE_ATTEMPTS = 5;
3586
- var fileQueues = /* @__PURE__ */ new Map();
3587
- function sameMap(a, b) {
3588
- const ak = Object.keys(a);
3589
- if (ak.length !== Object.keys(b).length) return false;
3590
- return ak.every(
3591
- (k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]
3592
- );
3593
- }
3594
- var isPositiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
3595
- function encryptedEnvelopeProblem(parsed) {
3596
- if (!parsed.kdf || typeof parsed.data !== "string") {
3597
- return "encrypted envelope is missing its kdf or data";
3598
- }
3599
- const parts = decodeParts(parsed.data);
3600
- if (!parts) return "encrypted payload is not iv.tag.ciphertext";
3601
- if (parts.iv.length !== IV_BYTES) {
3602
- return `initialization vector is ${parts.iv.length} bytes, expected ${IV_BYTES}`;
3603
- }
3604
- if (parts.tag.length !== GCM_TAG_BYTES) {
3605
- return `authentication tag is ${parts.tag.length} bytes, expected ${GCM_TAG_BYTES}`;
3606
- }
3607
- if (parts.body.length === 0) return "ciphertext is empty";
3608
- const { algorithm, salt, N, r, p } = parsed.kdf;
3609
- if (algorithm !== "scrypt") return `unsupported kdf "${String(algorithm)}"`;
3610
- if (typeof salt !== "string" || Buffer.from(salt, "base64").length === 0) {
3611
- return "kdf salt is missing or not base64";
3612
- }
3613
- if (!isPositiveInt(N) || N < 2 || (N & N - 1) !== 0) {
3614
- return `kdf N is ${String(N)}, which is not a power of two above 1`;
3615
- }
3616
- if (!isPositiveInt(r) || !isPositiveInt(p)) {
3617
- return `kdf r/p are ${String(r)}/${String(p)}, which are not positive integers`;
3618
- }
3619
- if (N > SCRYPT_N || r > SCRYPT_R || p > SCRYPT_P) {
3620
- return `kdf cost parameters (N=${N}, r=${r}, p=${p}) exceed the supported maximum (N=${SCRYPT_N}, r=${SCRYPT_R}, p=${SCRYPT_P})`;
3621
- }
3622
- return null;
3356
+ return null;
3623
3357
  }
3624
3358
  var FileSecretStore = class {
3625
3359
  filePath;
@@ -3806,7 +3540,9 @@ var FileSecretStore = class {
3806
3540
  let plain;
3807
3541
  try {
3808
3542
  const key = await this.deriveKey(parsed.kdf);
3809
- const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv);
3543
+ const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv, {
3544
+ authTagLength: GCM_TAG_BYTES
3545
+ });
3810
3546
  decipher.setAuthTag(parts.tag);
3811
3547
  plain = Buffer.concat([
3812
3548
  decipher.update(parts.body),
@@ -3840,7 +3576,9 @@ var FileSecretStore = class {
3840
3576
  };
3841
3577
  const key = await this.deriveKey(kdf);
3842
3578
  const iv = crypto2.randomBytes(IV_BYTES);
3843
- const cipher = crypto2.createCipheriv(CIPHER, key, iv);
3579
+ const cipher = crypto2.createCipheriv(CIPHER, key, iv, {
3580
+ authTagLength: GCM_TAG_BYTES
3581
+ });
3844
3582
  const body = Buffer.concat([
3845
3583
  cipher.update(JSON.stringify(map), "utf-8"),
3846
3584
  cipher.final()
@@ -4005,27 +3743,54 @@ var FileSecretStore = class {
4005
3743
  const found = {};
4006
3744
  for (const field of fields) {
4007
3745
  const value = map?.[buildAccount2(serverId, field)];
4008
- if (value !== void 0) found[field] = value;
3746
+ if (value !== void 0) setOwnEntry(found, field, value);
4009
3747
  }
4010
- out[serverId] = found;
3748
+ setOwnEntry(out, serverId, found);
4011
3749
  }
4012
3750
  return out;
4013
3751
  }
4014
3752
  /**
4015
- * The intolerant read — same lookup as {@link get}, minus the catch.
4016
- *
4017
- * Without this, `secretStoreGetStrict` fell back to `get` for the file
4018
- * store and turned every read failure into `null`. Both plaintext
4019
- * migrations *write* on `null` and then strip the disk copy, so a
4020
- * transient read error that cleared before the write let an older
4021
- * on-disk value replace a newer stored one — the same inversion the
4022
- * strict seam was introduced to prevent, still open on the store the
4023
- * seam was introduced for.
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.
4024
3757
  */
4025
- async getStrict(serverId, field) {
3758
+ async getManyStrict(requests) {
3759
+ let map;
4026
3760
  try {
4027
- const map = await this.serialize(() => this.readMap());
4028
- return map?.[buildAccount2(serverId, field)] ?? null;
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);
3774
+ }
3775
+ setOwnEntry(out, serverId, found);
3776
+ }
3777
+ return out;
3778
+ }
3779
+ /**
3780
+ * The intolerant read — same lookup as {@link get}, minus the catch.
3781
+ *
3782
+ * Without this, `secretStoreGetStrict` fell back to `get` for the file
3783
+ * store and turned every read failure into `null`. Both plaintext
3784
+ * migrations *write* on `null` and then strip the disk copy, so a
3785
+ * transient read error that cleared before the write let an older
3786
+ * on-disk value replace a newer stored one — the same inversion the
3787
+ * strict seam was introduced to prevent, still open on the store the
3788
+ * seam was introduced for.
3789
+ */
3790
+ async getStrict(serverId, field) {
3791
+ try {
3792
+ const map = await this.serialize(() => this.readMap());
3793
+ return map?.[buildAccount2(serverId, field)] ?? null;
4029
3794
  } catch (err) {
4030
3795
  if (err instanceof SecretStoreUnavailableError) throw err;
4031
3796
  throw new SecretStoreUnavailableError(
@@ -4051,6 +3816,9 @@ var FileSecretStore = class {
4051
3816
  */
4052
3817
  async setMany(serverId, values) {
4053
3818
  if (Object.keys(values).length === 0) return;
3819
+ for (const [field, value] of Object.entries(values)) {
3820
+ assertSecretString(field, value);
3821
+ }
4054
3822
  await this.serialize(
4055
3823
  () => this.mutate((map) => {
4056
3824
  const next = { ...map };
@@ -4062,6 +3830,7 @@ var FileSecretStore = class {
4062
3830
  );
4063
3831
  }
4064
3832
  async set(serverId, field, value) {
3833
+ assertSecretString(field, value);
4065
3834
  await this.serialize(
4066
3835
  () => this.mutate((map) => ({
4067
3836
  ...map,
@@ -4077,9 +3846,11 @@ var FileSecretStore = class {
4077
3846
  await this.deleteWhere((key) => key.startsWith(prefix));
4078
3847
  }
4079
3848
  /**
4080
- * Shared delete body. Silent on every failure, matching the keyring store:
4081
- * every reason a delete can fail collapses to "the entry isn't there
4082
- * 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.
4083
3854
  */
4084
3855
  async deleteWhere(match) {
4085
3856
  try {
@@ -4092,7 +3863,12 @@ var FileSecretStore = class {
4092
3863
  return next;
4093
3864
  })
4094
3865
  );
4095
- } 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
+ );
4096
3872
  }
4097
3873
  }
4098
3874
  };
@@ -4353,150 +4129,1432 @@ async function handOffStagedSecrets(staged, filePath, keyring) {
4353
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.`
4354
4130
  );
4355
4131
  }
4356
- 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
+ }
4357
5303
  }
4358
- try {
4359
- await fs4.link(staged, filePath);
4360
- await fs4.rm(staged, { force: true });
4361
- } catch {
4362
- console.warn(
4363
- `
4364
- [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.`
4365
- );
5304
+ /** Command-scoped auth was returned on HTTP; suppress async onerror echo on SSE. */
5305
+ noteAuthChallengeDeliveredViaHttp() {
5306
+ this.extendAuthHttpEchoSuppression();
4366
5307
  }
4367
- }
4368
- var isSnapshotName = (name, base) => name.startsWith(`${base}.migrating-`) && !name.endsWith(".lock");
4369
- async function anythingToMigrate(filePath) {
4370
- const base = path4.basename(filePath);
4371
- try {
4372
- return (await fs4.readdir(path4.dirname(filePath))).some(
4373
- (name) => name === base || isSnapshotName(name, base)
4374
- );
4375
- } catch (err) {
4376
- const code = err.code;
4377
- return code !== "ENOENT" && code !== "ENOTDIR";
5308
+ extendAuthHttpEchoSuppression() {
5309
+ this.authHttpEchoSuppressUntilMs = Date.now() + _RemoteSession.AUTH_HTTP_ECHO_SUPPRESS_MS;
4378
5310
  }
4379
- }
4380
- async function recoverOrphanedSnapshots(filePath) {
4381
- const dir = path4.dirname(filePath);
4382
- const base = path4.basename(filePath);
4383
- let names;
4384
- try {
4385
- names = (await fs4.readdir(dir)).filter((n) => isSnapshotName(n, base));
4386
- } catch {
4387
- return;
5311
+ shouldSuppressAuthEcho() {
5312
+ return Date.now() < this.authHttpEchoSuppressUntilMs;
4388
5313
  }
4389
- for (const name of names) {
4390
- const orphan = path4.join(dir, name);
4391
- if (await isFileLockHeld(orphan)) continue;
4392
- try {
4393
- await fs4.link(orphan, filePath);
4394
- await fs4.rm(orphan, { force: true });
4395
- console.warn(
4396
- `
4397
- [mcp-inspector] Recovered secrets left behind by an interrupted migration (${orphan} \u2192 ${filePath}).`
4398
- );
4399
- } catch {
4400
- console.warn(
4401
- `
4402
- [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.`
4403
- );
4404
- }
5314
+ hasActiveSend() {
5315
+ return this.activeSendCount > 0;
4405
5316
  }
4406
- }
4407
- async function handOff(file, filePath, keyring) {
4408
- let entries;
4409
- try {
4410
- entries = await file.readAll();
4411
- } catch (err) {
4412
- console.warn(
4413
- `
4414
- [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)}`
4415
- );
4416
- return false;
5317
+ pushAuthChallenge(challenge) {
5318
+ this.pushEvent({ type: "auth_challenge", data: challenge });
4417
5319
  }
4418
- if (!entries || Object.keys(entries).length === 0) return false;
4419
- let complete = true;
4420
- try {
4421
- for (const [account, value] of Object.entries(entries)) {
4422
- const parsed = parseAccount(account);
4423
- if (!parsed) {
4424
- complete = false;
4425
- continue;
4426
- }
4427
- const existing = await secretStoreGetStrict(
4428
- keyring,
4429
- parsed.serverId,
4430
- parsed.field
4431
- );
4432
- if (existing === null) {
4433
- await keyring.set(parsed.serverId, parsed.field, value);
4434
- }
4435
- }
4436
- } catch (err) {
4437
- console.warn(
4438
- `
4439
- [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)}`
4440
- );
4441
- 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
+ });
4442
5366
  }
4443
- if (!complete) {
4444
- console.warn(
4445
- `
4446
- [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.`
4447
- );
4448
- 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();
4449
5375
  }
4450
- return true;
4451
- }
4452
- async function getSecretStorageInfo() {
4453
- const { store, info } = await resolveSecretStore();
4454
- if (info.kind !== "file" || !(store instanceof FileSecretStore)) return info;
4455
- return describeFileStore(
4456
- store,
4457
- store.filePath,
4458
- "file",
4459
- info.reason,
4460
- info.detail
4461
- );
4462
- }
4463
- var DeferredSecretStore = class {
4464
- async target() {
4465
- 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"));
4466
5383
  }
4467
- async isDurable() {
4468
- 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();
4469
5389
  }
4470
5390
  /**
4471
- * Forwarded, and its absence was a real defect rather than an omission:
4472
- * this is the store production actually uses, so a `secretStoreGetStrict`
4473
- * call against it fell through to the tolerant `get` and the strictness
4474
- * 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).
4475
5395
  */
4476
- async getStrict(serverId, field) {
4477
- 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;
4478
5410
  }
4479
- async getMany(requests) {
4480
- 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();
4481
5418
  }
4482
- async get(serverId, field) {
4483
- 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
+ }
4484
5425
  }
4485
- async set(serverId, field, value) {
4486
- 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 });
4487
5435
  }
4488
- async setMany(serverId, values) {
4489
- 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
+ });
4490
5444
  }
4491
- async delete(serverId, field) {
4492
- 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
+ });
4493
5460
  }
4494
- async deleteAllForServer(serverId) {
4495
- 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
+ });
4496
5469
  }
4497
5470
  };
4498
- function defaultSecretStore() {
4499
- 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
+ };
4500
5558
  }
4501
5559
 
4502
5560
  // ../../core/client/config-parse.ts
@@ -4651,6 +5709,23 @@ function mergeSecretsIntoClientConfig(config, secrets) {
4651
5709
  }
4652
5710
 
4653
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
+ }
4654
5729
  async function readIdpSecretFromKeychain(secretStore) {
4655
5730
  const secret = await secretStore.get(
4656
5731
  CLIENT_KEYCHAIN_ID,
@@ -4660,29 +5735,39 @@ async function readIdpSecretFromKeychain(secretStore) {
4660
5735
  return { [SECRET_FIELD_IDP_CLIENT_SECRET]: secret };
4661
5736
  }
4662
5737
  async function migrateClientPlaintextSecret(filePath, config, secretStore) {
4663
- const { stripped, secrets } = extractSecretsFromClientConfig(config);
4664
- const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
4665
- if (!value) return config;
4666
5738
  try {
4667
- const existing = await secretStoreGetStrict(
4668
- secretStore,
4669
- CLIENT_KEYCHAIN_ID,
4670
- SECRET_FIELD_IDP_CLIENT_SECRET
4671
- );
4672
- if (existing === null) {
4673
- await secretStore.set(
4674
- CLIENT_KEYCHAIN_ID,
4675
- SECRET_FIELD_IDP_CLIENT_SECRET,
4676
- value
4677
- );
4678
- }
4679
- if (!await secretStoreIsDurable(secretStore)) return config;
4680
- await writeStoreFile(filePath, serializeStore(stripped));
4681
- 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
+ });
4682
5769
  } catch (err) {
4683
- if (err instanceof SecretStoreUnavailableError) {
4684
- return config;
4685
- }
5770
+ if (err instanceof SecretFileLockHeldError) return config;
4686
5771
  throw err;
4687
5772
  }
4688
5773
  }
@@ -4702,30 +5787,44 @@ async function writeClientConfigStore(filePath, body, secretStore) {
4702
5787
  const validated = parseClientConfig(body);
4703
5788
  const { stripped, secrets } = extractSecretsFromClientConfig(validated);
4704
5789
  const idpSecret = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
4705
- if (idpSecret) {
4706
- await secretStore.set(
4707
- CLIENT_KEYCHAIN_ID,
4708
- SECRET_FIELD_IDP_CLIENT_SECRET,
4709
- idpSecret
4710
- );
4711
- } else {
4712
- await secretStore.delete(
4713
- CLIENT_KEYCHAIN_ID,
5790
+ await withClientConfigLock(filePath, "save", async () => {
5791
+ const prior = await snapshotSecretFields(secretStore, CLIENT_KEYCHAIN_ID, [
4714
5792
  SECRET_FIELD_IDP_CLIENT_SECRET
4715
- );
4716
- }
4717
- const durable = await secretStoreIsDurable(secretStore);
4718
- await writeStoreFile(
4719
- filePath,
4720
- serializeStore(
4721
- 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(
4722
5809
  filePath,
4723
- validated,
4724
- stripped,
4725
- idpSecret
4726
- )
4727
- )
4728
- );
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
+ });
4729
5828
  }
4730
5829
  async function preserveLegacyPlaintext(filePath, validated, stripped, idpSecret) {
4731
5830
  if (!idpSecret) return stripped;
@@ -4740,8 +5839,25 @@ async function preserveLegacyPlaintext(filePath, validated, stripped, idpSecret)
4740
5839
  }
4741
5840
  }
4742
5841
  async function deleteClientConfigStore(filePath, secretStore) {
4743
- await deleteStoreFile(filePath);
4744
- 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
+ });
4745
5861
  }
4746
5862
 
4747
5863
  // ../../core/mcp/remote/node/server.ts
@@ -4768,6 +5884,7 @@ async function primeAndHoldSseStream(stream, cleanup) {
4768
5884
  var MAX_APP_DOCUMENT_CHARS = 8 * 1024 * 1024;
4769
5885
  var MAX_APP_DOCUMENT_BODY_BYTES = 24 * 1024 * 1024;
4770
5886
  var MAX_APP_CSP_CHARS = 8 * 1024;
5887
+ var MAX_STORAGE_BODY_BYTES = 4 * 1024 * 1024;
4771
5888
  function isHeaderSafeValue(value) {
4772
5889
  return !/[^\t\x20-\x7e]/.test(value);
4773
5890
  }
@@ -5400,6 +6517,10 @@ function createRemoteApp(options) {
5400
6517
  const config = await readClientConfigStore(filePath, secretStore);
5401
6518
  return c.json(config);
5402
6519
  }
6520
+ if (storeId === OAUTH_PERSIST_STORE_ID) {
6521
+ const snapshot = await readOAuthStore(filePath, secretStore);
6522
+ return c.json(snapshot ?? {});
6523
+ }
5403
6524
  const raw = await readStoreFile(filePath);
5404
6525
  if (raw === null) {
5405
6526
  return c.json({}, 200);
@@ -5407,40 +6528,74 @@ function createRemoteApp(options) {
5407
6528
  const store = parseStore(raw);
5408
6529
  return c.json(store);
5409
6530
  } catch (error) {
6531
+ const keychainResp = keychainErrorResponse(c, error);
6532
+ if (keychainResp) return keychainResp;
5410
6533
  const msg = error instanceof Error ? error.message : String(error);
5411
6534
  return c.json({ error: `Failed to read store: ${msg}` }, 500);
5412
6535
  }
5413
6536
  });
5414
- app.post("/api/storage/:storeId", async (c) => {
5415
- const storeId = c.req.param("storeId");
5416
- if (!storeId || !validateStoreId(storeId)) {
5417
- return c.json({ error: "Invalid storeId" }, 400);
5418
- }
5419
- let body;
5420
- try {
5421
- body = await c.req.json();
5422
- } catch {
5423
- return c.json({ error: "Invalid JSON body" }, 400);
5424
- }
5425
- const filePath = getStoreFilePath(storageDir, storeId);
5426
- try {
5427
- if (storeId === "client") {
5428
- await writeClientConfigStore(filePath, body, secretStore);
5429
- 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);
5430
6548
  }
5431
- const jsonData = serializeStore(body);
5432
- await writeStoreFile(filePath, jsonData);
5433
- return c.json({ ok: true });
5434
- } catch (error) {
5435
- if (error instanceof ZodError2) {
5436
- 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);
5437
6596
  }
5438
- const keychainResp = storeId === "client" ? keychainErrorResponse(c, error) : void 0;
5439
- if (keychainResp) return keychainResp;
5440
- const msg = error instanceof Error ? error.message : String(error);
5441
- return c.json({ error: `Failed to write store: ${msg}` }, 500);
5442
6597
  }
5443
- });
6598
+ );
5444
6599
  app.delete("/api/storage/:storeId", async (c) => {
5445
6600
  const storeId = c.req.param("storeId");
5446
6601
  if (!storeId || !validateStoreId(storeId)) {
@@ -5452,9 +6607,15 @@ function createRemoteApp(options) {
5452
6607
  await deleteClientConfigStore(filePath, secretStore);
5453
6608
  return c.json({ ok: true });
5454
6609
  }
6610
+ if (storeId === OAUTH_PERSIST_STORE_ID) {
6611
+ await removeOAuthStore(filePath, secretStore);
6612
+ return c.json({ ok: true });
6613
+ }
5455
6614
  await deleteStoreFile(filePath);
5456
6615
  return c.json({ ok: true });
5457
6616
  } catch (error) {
6617
+ const keychainResp = keychainErrorResponse(c, error);
6618
+ if (keychainResp) return keychainResp;
5458
6619
  const msg = error instanceof Error ? error.message : String(error);
5459
6620
  return c.json({ error: `Failed to delete store: ${msg}` }, 500);
5460
6621
  }
@@ -5525,6 +6686,13 @@ function createRemoteApp(options) {
5525
6686
  const out = {};
5526
6687
  for (const [id, val] of Object.entries(raw)) {
5527
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
+ }
5528
6696
  const valObj = val;
5529
6697
  if ("settings" in valObj) {
5530
6698
  logWarn(
@@ -5952,7 +7120,7 @@ function createRemoteApp(options) {
5952
7120
  return false;
5953
7121
  };
5954
7122
  let warnedSessionPreserved = false;
5955
- const migratePlaintextSecrets = async (config) => {
7123
+ const migratePlaintextSecrets2 = async (config) => {
5956
7124
  let changed = false;
5957
7125
  const next = { mcpServers: {} };
5958
7126
  const durable = await secretStoreIsDurable(secretStore);
@@ -6023,7 +7191,22 @@ function createRemoteApp(options) {
6023
7191
  return out;
6024
7192
  };
6025
7193
  const deleteKeychainFields = async (id, fields) => {
6026
- 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
+ }
6027
7210
  };
6028
7211
  const entryForDisk = async (built, stripped, previous) => {
6029
7212
  if (await secretStoreIsDurable(secretStore)) return stripped;
@@ -6079,7 +7262,7 @@ function createRemoteApp(options) {
6079
7262
  mcpServers: normalizeMcpServers(parsedInside?.mcpServers)
6080
7263
  };
6081
7264
  if (!hasPlaintextSecrets(inside)) return inside;
6082
- const { migrated, changed } = await migratePlaintextSecrets(inside);
7265
+ const { migrated, changed } = await migratePlaintextSecrets2(inside);
6083
7266
  if (changed) {
6084
7267
  await writeMcpAndTrackMtime(serializeStore(migrated));
6085
7268
  }
@@ -6121,7 +7304,7 @@ function createRemoteApp(options) {
6121
7304
  if (typeof body.id !== "string" || !validateStoreId(body.id)) {
6122
7305
  return c.json(
6123
7306
  {
6124
- 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__`"
6125
7308
  },
6126
7309
  400
6127
7310
  );
@@ -6139,15 +7322,29 @@ function createRemoteApp(options) {
6139
7322
  try {
6140
7323
  return await withWriteLock(async () => {
6141
7324
  const current = await readMcpConfig();
6142
- if (id in current.mcpServers) {
7325
+ if (Object.hasOwn(current.mcpServers, id)) {
6143
7326
  return c.json({ error: `Server '${id}' already exists` }, 409);
6144
7327
  }
6145
7328
  const built = buildStoredEntry(id, body.config, postSettings);
6146
7329
  const { stripped, secrets } = extractSecretsFromStored(built);
6147
7330
  await secretStore.deleteAllForServer(id);
6148
- await writeKeychainEntriesFor(id, secrets);
6149
- current.mcpServers[id] = await entryForDisk(built, stripped);
6150
- 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
+ }
6151
7348
  return c.json({ ok: true });
6152
7349
  });
6153
7350
  } catch (error) {
@@ -6237,10 +7434,10 @@ function createRemoteApp(options) {
6237
7434
  try {
6238
7435
  return await withWriteLock(async () => {
6239
7436
  const current = await readMcpConfig();
6240
- if (!(originalId in current.mcpServers)) {
7437
+ if (!Object.hasOwn(current.mcpServers, originalId)) {
6241
7438
  return c.json({ error: `Server '${originalId}' not found` }, 404);
6242
7439
  }
6243
- if (newId !== originalId && newId in current.mcpServers) {
7440
+ if (newId !== originalId && Object.hasOwn(current.mcpServers, newId)) {
6244
7441
  return c.json({ error: `Server '${newId}' already exists` }, 409);
6245
7442
  }
6246
7443
  const existing = current.mcpServers[originalId];
@@ -6297,27 +7494,80 @@ function createRemoteApp(options) {
6297
7494
  }
6298
7495
  if (newId !== originalId) {
6299
7496
  const previousFields = expectedSecretFields(existing);
6300
- const keychainSecrets = await readKeychainEntriesFor(
7497
+ const originalPrior = await snapshotSecretFields(
7498
+ secretStore,
6301
7499
  originalId,
6302
7500
  previousFields
6303
7501
  );
7502
+ const keychainSecrets = {};
7503
+ for (const { field, value } of originalPrior) {
7504
+ if (value !== null) setOwnEntry(keychainSecrets, field, value);
7505
+ }
6304
7506
  const secretsToWrite = mergeRenameKeychainSecrets(
6305
7507
  stripped,
6306
7508
  keychainSecrets,
6307
7509
  secrets
6308
7510
  );
6309
- await writeKeychainEntriesFor(newId, secretsToWrite);
6310
- await writeMcpAndTrackMtime(serializeStore(next));
6311
- 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
+ }
6312
7541
  } else {
6313
7542
  const previousFields = new Set(expectedSecretFields(existing));
6314
7543
  const stillExpected = new Set(expectedSecretFields(built));
6315
7544
  const obsolete = [...previousFields].filter(
6316
7545
  (field) => field === SECRET_FIELD_OAUTH_CLIENT_SECRET ? settingsIntent.kind !== "preserve" && !(field in secrets) : !stillExpected.has(field)
6317
7546
  );
6318
- await writeKeychainEntriesFor(newId, secrets);
6319
- await writeMcpAndTrackMtime(serializeStore(next));
6320
- 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
+ }
6321
7571
  }
6322
7572
  return c.json({ ok: true });
6323
7573
  });
@@ -6338,13 +7588,28 @@ function createRemoteApp(options) {
6338
7588
  try {
6339
7589
  return await withWriteLock(async () => {
6340
7590
  const current = await readMcpConfig();
6341
- if (!(id in current.mcpServers)) {
7591
+ if (!Object.hasOwn(current.mcpServers, id)) {
6342
7592
  await secretStore.deleteAllForServer(id);
6343
7593
  return c.json({ ok: true });
6344
7594
  }
7595
+ const stored = current.mcpServers[id];
6345
7596
  delete current.mcpServers[id];
6346
- await writeMcpAndTrackMtime(serializeStore(current));
6347
- 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
+ }
6348
7613
  return c.json({ ok: true });
6349
7614
  });
6350
7615
  } catch (error) {