@modelcontextprotocol/inspector 2.10.1 → 2.12.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.
@@ -159,6 +159,12 @@ var DirectoryReadResultSchema = z.looseObject({
159
159
  resources: z.array(DirectoryChildSchema),
160
160
  nextCursor: z.string().optional()
161
161
  });
162
+ var ModernDirectoryReadResultSchema = DirectoryReadResultSchema.extend(
163
+ {
164
+ ttlMs: z.int().min(0),
165
+ cacheScope: z.enum(["public", "private"])
166
+ }
167
+ );
162
168
 
163
169
  // ../../core/mcp/sha256.ts
164
170
  var K = new Uint32Array([
@@ -821,16 +827,30 @@ function buildConfigFromOptions(options) {
821
827
  const target = options.target ?? [];
822
828
  const first = target[0];
823
829
  const rest = target.slice(1);
830
+ if (first && options.serverUrl) {
831
+ throw new Error(
832
+ `Cannot combine a positional target (${first}) with --server-url (${options.serverUrl}). Pass one or the other.`
833
+ );
834
+ }
824
835
  const urlFromTarget = first && (first.startsWith("http://") || first.startsWith("https://")) ? first : null;
825
836
  const url = urlFromTarget ?? options.serverUrl ?? null;
826
837
  if (url) {
827
- if (rest.length > 0 && urlFromTarget) {
838
+ if (rest.length > 0) {
828
839
  throw new Error("Arguments cannot be passed to a URL-based MCP server.");
829
840
  }
841
+ if (options.transport === "stdio") {
842
+ throw new Error(
843
+ `stdio transport cannot be used with a URL (${url}). Use --transport http or --transport sse, or pass a local command instead.`
844
+ );
845
+ }
830
846
  let transportType;
831
847
  const t = options.transport === "http" ? "streamable-http" : options.transport;
832
848
  if (t === "sse" || t === "streamable-http") {
833
849
  transportType = t;
850
+ } else if (t) {
851
+ throw new Error(
852
+ `Invalid transport type: ${t}. Valid types are: sse, http, stdio`
853
+ );
834
854
  } else {
835
855
  const u = new URL(url);
836
856
  if (u.pathname.endsWith("/mcp")) {
@@ -1047,14 +1067,25 @@ function readInspectorVersionSafe(callerUrl) {
1047
1067
  // ../../core/node/openUrl.ts
1048
1068
  import open from "open";
1049
1069
  var OPEN_URL_TIMEOUT_MS = 5e3;
1050
- function launched(child) {
1051
- return new Promise((resolve10, reject) => {
1052
- child.on("error", reject);
1053
- child.once("spawn", () => resolve10());
1070
+ var OPENABLE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
1071
+ function toOpenableHref(href) {
1072
+ let parsed;
1073
+ try {
1074
+ parsed = new URL(href);
1075
+ } catch {
1076
+ throw new Error("refusing to open an invalid URL");
1077
+ }
1078
+ if (!OPENABLE_PROTOCOLS.has(parsed.protocol)) {
1079
+ throw new Error(`refusing to open a non-http(s) URL (${parsed.protocol})`);
1080
+ }
1081
+ return parsed.href;
1082
+ }
1083
+ function absorbLateErrors(child) {
1084
+ child.on("error", () => {
1054
1085
  });
1055
1086
  }
1056
1087
  async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
1057
- const href = typeof url === "string" ? url : url.href;
1088
+ const href = toOpenableHref(typeof url === "string" ? url : url.href);
1058
1089
  let timer;
1059
1090
  const timeout = new Promise((_, reject) => {
1060
1091
  timer = setTimeout(
@@ -1063,8 +1094,7 @@ async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
1063
1094
  );
1064
1095
  });
1065
1096
  try {
1066
- const opened = Promise.resolve().then(() => open(href)).then(launched);
1067
- await Promise.race([opened, timeout]);
1097
+ await Promise.race([open(href).then(absorbLateErrors), timeout]);
1068
1098
  } finally {
1069
1099
  clearTimeout(timer);
1070
1100
  }
@@ -1575,6 +1605,153 @@ function resolveAppOriginPublicOrigin(raw, allowedOrigins, sandboxUrl) {
1575
1605
  return url.origin;
1576
1606
  }
1577
1607
 
1608
+ // server/tls.ts
1609
+ import { readFileSync as readFileSync4 } from "fs";
1610
+ import { createSecureContext } from "tls";
1611
+ var TLS_KEY_PATH_ENV = "INSPECTOR_TLS_KEY_PATH";
1612
+ var TLS_CERT_PATH_ENV = "INSPECTOR_TLS_CERT_PATH";
1613
+ function webScheme(tls) {
1614
+ return tls ? "https" : "http";
1615
+ }
1616
+ function readPem(env, path6) {
1617
+ try {
1618
+ return readFileSync4(path6, "utf-8");
1619
+ } catch (err) {
1620
+ const { code } = err;
1621
+ throw new Error(
1622
+ `Cannot read ${env}="${path6}" (${code}): check that the file exists and is readable.`,
1623
+ { cause: err }
1624
+ );
1625
+ }
1626
+ }
1627
+ function resolveTlsConfig(env = process.env) {
1628
+ const keyPath = env[TLS_KEY_PATH_ENV]?.trim();
1629
+ const certPath = env[TLS_CERT_PATH_ENV]?.trim();
1630
+ if (!keyPath && !certPath) return void 0;
1631
+ if (!keyPath || !certPath) {
1632
+ const missing = keyPath ? TLS_CERT_PATH_ENV : TLS_KEY_PATH_ENV;
1633
+ throw new Error(
1634
+ `TLS needs both ${TLS_KEY_PATH_ENV} and ${TLS_CERT_PATH_ENV}; ${missing} is not set. Set both to enable https, or neither for plain http.`
1635
+ );
1636
+ }
1637
+ const key = readPem(TLS_KEY_PATH_ENV, keyPath);
1638
+ const cert = readPem(TLS_CERT_PATH_ENV, certPath);
1639
+ try {
1640
+ if (!key.trim() || !cert.trim()) throw new Error("empty PEM file");
1641
+ createSecureContext({ key, cert });
1642
+ } catch (err) {
1643
+ const reason = err.message;
1644
+ throw new Error(
1645
+ `${TLS_KEY_PATH_ENV}="${keyPath}" and ${TLS_CERT_PATH_ENV}="${certPath}" are not a usable PEM key/certificate pair (${reason}).`,
1646
+ { cause: err }
1647
+ );
1648
+ }
1649
+ return { key, cert };
1650
+ }
1651
+
1652
+ // server/host-allow-list.ts
1653
+ import { isIPv4, isIPv6 as isIPv62 } from "net";
1654
+ import { domainToASCII } from "url";
1655
+ var ALLOWED_HOSTS_ENV = "MCP_INSPECTOR_ALLOWED_HOSTS";
1656
+ var HOSTNAME_RE = /^[a-z0-9._-]+$/;
1657
+ function normalizeHostname(hostname) {
1658
+ return hostname.trim().toLowerCase().replace(/\.$/, "");
1659
+ }
1660
+ function parseHostHeader(value) {
1661
+ const raw = value.trim().toLowerCase();
1662
+ let hostname;
1663
+ let port;
1664
+ if (raw.startsWith("[")) {
1665
+ const end = raw.indexOf("]");
1666
+ if (end < 0) return null;
1667
+ hostname = raw.slice(0, end + 1);
1668
+ if (!isIPv62(hostname.slice(1, -1))) return null;
1669
+ const rest = raw.slice(end + 1);
1670
+ if (rest !== "") {
1671
+ if (!rest.startsWith(":")) return null;
1672
+ port = rest.slice(1);
1673
+ }
1674
+ } else {
1675
+ const colon = raw.indexOf(":");
1676
+ hostname = colon < 0 ? raw : raw.slice(0, colon);
1677
+ port = colon < 0 ? void 0 : raw.slice(colon + 1);
1678
+ }
1679
+ if (port !== void 0) {
1680
+ if (!/^\d{1,5}$/.test(port) || Number(port) > 65535) return null;
1681
+ }
1682
+ if (hostname.startsWith("[")) return hostname;
1683
+ const name = normalizeHostname(hostname);
1684
+ return HOSTNAME_RE.test(name) ? name : null;
1685
+ }
1686
+ function parseAllowedHostsEnv(value) {
1687
+ if (!value) return [];
1688
+ const out = [];
1689
+ for (const entry of value.split(",")) {
1690
+ const trimmed = entry.trim();
1691
+ if (trimmed === "") continue;
1692
+ const domain = trimmed.startsWith(".") ? "." : "";
1693
+ const ascii = /[\s/:*@]/.test(trimmed) ? "" : domainToASCII(normalizeHostname(trimmed.slice(domain.length)));
1694
+ if (!HOSTNAME_RE.test(ascii)) {
1695
+ console.warn(
1696
+ `Ignoring invalid ${ALLOWED_HOSTS_ENV} entry: ${trimmed} (list bare hostnames, e.g. inspector.example.com or .example.com)`
1697
+ );
1698
+ continue;
1699
+ }
1700
+ out.push(domain + ascii);
1701
+ }
1702
+ return out;
1703
+ }
1704
+ function configuredAllowedHosts(options) {
1705
+ const hosts = /* @__PURE__ */ new Set();
1706
+ if (!isAllInterfacesHost(options.bindHost)) {
1707
+ hosts.add(normalizeHostname(canonicalUrlHost(options.bindHost)));
1708
+ }
1709
+ for (const origin of options.allowedOrigins) {
1710
+ try {
1711
+ const { hostname } = new URL(origin);
1712
+ if (hostname !== "") hosts.add(normalizeHostname(hostname));
1713
+ } catch {
1714
+ }
1715
+ }
1716
+ for (const extra of options.extraHosts ?? []) {
1717
+ hosts.add(normalizeHostname(extra));
1718
+ }
1719
+ return [...hosts];
1720
+ }
1721
+ function createHostAllowList(options) {
1722
+ const configured = configuredAllowedHosts(options);
1723
+ return (hostHeader) => {
1724
+ if (hostHeader === void 0) return false;
1725
+ const hostname = parseHostHeader(hostHeader);
1726
+ if (hostname === null) return false;
1727
+ if (hostname.startsWith("[")) return true;
1728
+ if (isIPv4(hostname)) return true;
1729
+ if (hostname === "localhost" || hostname.endsWith(".localhost")) {
1730
+ return true;
1731
+ }
1732
+ for (const allowed of configured) {
1733
+ if (allowed === hostname) return true;
1734
+ if (allowed.startsWith(".") && (allowed.slice(1) === hostname || hostname.endsWith(allowed))) {
1735
+ return true;
1736
+ }
1737
+ }
1738
+ return false;
1739
+ };
1740
+ }
1741
+ function hostAllowListMiddleware(options) {
1742
+ const isAllowed = createHostAllowList(options);
1743
+ return async (c, next) => {
1744
+ const host = c.req.header("host");
1745
+ if (!isAllowed(host)) {
1746
+ return c.text(
1747
+ `Forbidden: Host "${host ?? ""}" is not allowed. To serve the Inspector under this name, add it to ${ALLOWED_HOSTS_ENV} (or to ALLOWED_ORIGINS).`,
1748
+ 403
1749
+ );
1750
+ }
1751
+ await next();
1752
+ };
1753
+ }
1754
+
1578
1755
  // server/web-server-config.ts
1579
1756
  var inspectorVersion = readInspectorVersionSafe(import.meta.url);
1580
1757
  function defaultEnvironmentFromProcess(extra) {
@@ -1656,7 +1833,7 @@ function transportDefaults(config) {
1656
1833
  function printServerBanner(config, actualPort, resolvedToken, sandboxUrl, secretStorage) {
1657
1834
  const h = canonicalUrlHost(config.hostname);
1658
1835
  const bannerHost = isAllInterfacesHost(h) ? "localhost" : h;
1659
- const baseUrl = httpOrigin(bannerHost, actualPort);
1836
+ const baseUrl = webOrigin(webScheme(config.tls), bannerHost, actualPort);
1660
1837
  const url = config.dangerouslyOmitAuth || !resolvedToken ? baseUrl : `${baseUrl}?${API_SERVER_ENV_VARS.AUTH_TOKEN}=${resolvedToken}`;
1661
1838
  console.log(`
1662
1839
  MCP Inspector Web is up and running at:
@@ -1681,35 +1858,42 @@ MCP Inspector Web is up and running at:
1681
1858
  }
1682
1859
  return url;
1683
1860
  }
1861
+ function portInUseMessage(config) {
1862
+ return `MCP Inspector PORT IS IN USE at ${webScheme(config.tls)}://${formatHostForUrl(config.hostname)}:${config.port}`;
1863
+ }
1684
1864
  function openBrowser(url) {
1685
1865
  openUrl(url).catch((err) => {
1686
- console.warn("Could not open a browser automatically:", err);
1866
+ const reason = err instanceof Error ? err.message : String(err);
1867
+ console.warn(
1868
+ `Could not open a browser automatically (${reason}). Open the URL above manually.`
1869
+ );
1687
1870
  });
1688
1871
  }
1689
1872
  var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1690
- function httpOrigin(host, port) {
1691
- return port === 80 ? `http://${host}` : `http://${host}:${port}`;
1873
+ function webOrigin(scheme, host, port) {
1874
+ const defaultPort = scheme === "https" ? 443 : 80;
1875
+ return port === defaultPort ? `${scheme}://${host}` : `${scheme}://${host}:${port}`;
1692
1876
  }
1693
- function loopbackOrigins(port) {
1877
+ function loopbackOrigins(scheme, port) {
1694
1878
  return [
1695
- httpOrigin("localhost", port),
1696
- httpOrigin("127.0.0.1", port),
1697
- httpOrigin("[::1]", port)
1879
+ webOrigin(scheme, "localhost", port),
1880
+ webOrigin(scheme, "127.0.0.1", port),
1881
+ webOrigin(scheme, "[::1]", port)
1698
1882
  ];
1699
1883
  }
1700
- function defaultAllowedOrigins(hostname, port) {
1884
+ function defaultAllowedOrigins(hostname, port, scheme = "http") {
1701
1885
  const h = canonicalUrlHost(hostname);
1702
1886
  if (LOOPBACK_HOSTNAMES.has(h)) {
1703
- return loopbackOrigins(port);
1887
+ return loopbackOrigins(scheme, port);
1704
1888
  }
1705
1889
  if (isAllInterfacesHost(h)) {
1706
1890
  return [
1707
- ...loopbackOrigins(port),
1708
- httpOrigin("0.0.0.0", port),
1709
- httpOrigin("[::]", port)
1891
+ ...loopbackOrigins(scheme, port),
1892
+ webOrigin(scheme, "0.0.0.0", port),
1893
+ webOrigin(scheme, "[::]", port)
1710
1894
  ];
1711
1895
  }
1712
- return [httpOrigin(h, port)];
1896
+ return [webOrigin(scheme, h, port)];
1713
1897
  }
1714
1898
  function buildWebServerConfig(options = {}) {
1715
1899
  const {
@@ -1726,6 +1910,7 @@ function buildWebServerConfig(options = {}) {
1726
1910
  );
1727
1911
  }
1728
1912
  const hostname = resolveBindHostname();
1913
+ const tls = resolveTlsConfig();
1729
1914
  const dangerouslyOmitAuth = isEnvFlagEnabled(
1730
1915
  process.env.DANGEROUSLY_OMIT_AUTH
1731
1916
  );
@@ -1761,7 +1946,7 @@ function buildWebServerConfig(options = {}) {
1761
1946
  return null;
1762
1947
  }
1763
1948
  }).filter((o) => o !== null);
1764
- const allowedOrigins = configuredOrigins?.length ? configuredOrigins : defaultAllowedOrigins(hostname, port);
1949
+ const allowedOrigins = configuredOrigins?.length ? configuredOrigins : defaultAllowedOrigins(hostname, port, webScheme(tls));
1765
1950
  const sandboxPublicUrl = resolveSandboxPublicUrl(
1766
1951
  process.env[SANDBOX_FULL_ADDRESS_ENV],
1767
1952
  allowedOrigins
@@ -1782,13 +1967,15 @@ function buildWebServerConfig(options = {}) {
1782
1967
  initialServers,
1783
1968
  storageDir: process.env.MCP_STORAGE_DIR,
1784
1969
  allowedOrigins,
1970
+ allowedHosts: parseAllowedHostsEnv(process.env[ALLOWED_HOSTS_ENV]),
1785
1971
  sandboxPort,
1786
1972
  sandboxHost: hostname,
1787
1973
  appOriginPort,
1788
1974
  sandboxPublicUrl,
1789
1975
  appOriginPublicOrigin,
1790
1976
  logger,
1791
- autoOpen: resolveAutoOpen()
1977
+ autoOpen: resolveAutoOpen(),
1978
+ ...tls ? { tls } : {}
1792
1979
  };
1793
1980
  }
1794
1981
  function resolveAutoOpen() {
@@ -1806,7 +1993,7 @@ import react from "@vitejs/plugin-react";
1806
1993
 
1807
1994
  // ../../core/mcp/remote/node/server.ts
1808
1995
  import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1809
- import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
1996
+ import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
1810
1997
  import { stat as fsStat } from "fs/promises";
1811
1998
  import { homedir } from "os";
1812
1999
  import { Hono } from "hono";
@@ -2291,7 +2478,10 @@ function parseAuthChallengeFromResponse(response, context) {
2291
2478
  if (status !== 401 && status !== 403) {
2292
2479
  return void 0;
2293
2480
  }
2294
- const wwwAuthenticate = response.headers.get("WWW-Authenticate") ?? void 0;
2481
+ const wwwAuthenticate = response.headers.get("WWW-Authenticate") || void 0;
2482
+ if (status === 403 && !wwwAuthenticate) {
2483
+ return void 0;
2484
+ }
2295
2485
  const bearer = wwwAuthenticate ? parseWwwAuthenticateBearer(wwwAuthenticate) : {};
2296
2486
  const requiredScopes = parseScopeString(bearer.scope);
2297
2487
  return {
@@ -2620,9 +2810,96 @@ function createTransportNode(config, options = {}) {
2620
2810
  }
2621
2811
  }
2622
2812
 
2813
+ // ../../core/mcp/remote/node/cappedBody.ts
2814
+ async function readCappedText(body, maxBytes) {
2815
+ const reader = body.getReader();
2816
+ const decoder = new TextDecoder();
2817
+ let text = "";
2818
+ let bytes = 0;
2819
+ for (; ; ) {
2820
+ const next = await reader.read();
2821
+ if (next.done) return { kind: "complete", text: text + decoder.decode() };
2822
+ bytes += next.value.byteLength;
2823
+ if (bytes > maxBytes) {
2824
+ void reader.cancel().catch(() => {
2825
+ });
2826
+ return { kind: "overflow" };
2827
+ }
2828
+ text += decoder.decode(next.value, { stream: true });
2829
+ }
2830
+ }
2831
+ function declaresOversizeBody(contentLength, maxBytes) {
2832
+ if (contentLength === null || !/^\s*\d+\s*$/.test(contentLength)) {
2833
+ return false;
2834
+ }
2835
+ return Number(contentLength) > maxBytes;
2836
+ }
2837
+
2623
2838
  // ../../core/auth/requestTimeout.ts
2624
2839
  var OAUTH_TIMEOUT_WIRE_CODE = "oauth_request_timeout";
2625
2840
 
2841
+ // ../../core/mcp/rateLimit.ts
2842
+ import {
2843
+ SdkHttpError
2844
+ } from "@modelcontextprotocol/client";
2845
+
2846
+ // ../../core/mcp/connectionDiagnostics.ts
2847
+ import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
2848
+
2849
+ // ../../core/mcp/rateLimit.ts
2850
+ var HTTP_TOO_MANY_REQUESTS = 429;
2851
+ function requestIdsOf(body) {
2852
+ if (body === void 0) return [];
2853
+ let parsed;
2854
+ try {
2855
+ parsed = JSON.parse(body);
2856
+ } catch {
2857
+ return [];
2858
+ }
2859
+ const messages = Array.isArray(parsed) ? parsed : [parsed];
2860
+ const ids = [];
2861
+ for (const message of messages) {
2862
+ if (typeof message !== "object" || message === null) continue;
2863
+ if (!("method" in message) || !("id" in message)) continue;
2864
+ const { id } = message;
2865
+ if (typeof id === "string" || typeof id === "number") ids.push(id);
2866
+ }
2867
+ return ids;
2868
+ }
2869
+ var MAX_UNCLAIMED_RATE_LIMITS = 100;
2870
+ var RateLimitLedger = class {
2871
+ // `undefined` is a real value here: a 429 that carried no header.
2872
+ byRequestId = /* @__PURE__ */ new Map();
2873
+ /** File a tracked fetch, if it was a 429. Anything else is ignored. */
2874
+ record(entry) {
2875
+ if (entry.responseStatus !== HTTP_TOO_MANY_REQUESTS) return;
2876
+ const retryAfter = findHeader(entry.responseHeaders, "retry-after");
2877
+ for (const id of requestIdsOf(entry.requestBody)) {
2878
+ this.byRequestId.delete(id);
2879
+ this.byRequestId.set(id, retryAfter);
2880
+ if (this.byRequestId.size > MAX_UNCLAIMED_RATE_LIMITS) {
2881
+ const [oldest] = this.byRequestId.keys();
2882
+ if (oldest !== void 0) this.byRequestId.delete(oldest);
2883
+ }
2884
+ }
2885
+ }
2886
+ /** The `Retry-After` filed for `id`, removed as it is read. */
2887
+ take(id) {
2888
+ const retryAfter = this.byRequestId.get(id);
2889
+ this.byRequestId.delete(id);
2890
+ return retryAfter;
2891
+ }
2892
+ clear() {
2893
+ this.byRequestId.clear();
2894
+ }
2895
+ };
2896
+ function isRateLimitError(err) {
2897
+ return SdkHttpError.isInstance(err) && err.status === HTTP_TOO_MANY_REQUESTS;
2898
+ }
2899
+
2900
+ // ../../core/mcp/remote/node/server.ts
2901
+ import { SdkHttpError as SdkHttpError2 } from "@modelcontextprotocol/client";
2902
+
2626
2903
  // ../../core/auth/oauth-persist.ts
2627
2904
  import { OAuthTokensSchema } from "@modelcontextprotocol/core";
2628
2905
  var OAUTH_PERSIST_STORE_ID = "oauth";
@@ -3250,7 +3527,7 @@ import * as path4 from "path";
3250
3527
 
3251
3528
  // ../../core/auth/node/file-secret-store.ts
3252
3529
  import * as crypto2 from "crypto";
3253
- import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
3530
+ import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
3254
3531
  import * as fs3 from "fs/promises";
3255
3532
  import * as path3 from "path";
3256
3533
  var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve10, reject) => {
@@ -3303,7 +3580,7 @@ function resolveSecretPassphrase(env = process.env, secretFilePath) {
3303
3580
  }
3304
3581
  let contents;
3305
3582
  try {
3306
- contents = readFileSync4(resolved2, "utf-8");
3583
+ contents = readFileSync5(resolved2, "utf-8");
3307
3584
  } catch (err) {
3308
3585
  return {
3309
3586
  // `String(err)` rather than `.message`: `readFileSync` only ever throws
@@ -4333,7 +4610,7 @@ async function resolveConcreteSecretStore(store) {
4333
4610
  import { resolve as resolve6 } from "path";
4334
4611
 
4335
4612
  // ../../core/auth/node/oauth-secrets.ts
4336
- import { randomUUID as randomUUID2 } from "crypto";
4613
+ import { createHmac, randomUUID as randomUUID2 } from "crypto";
4337
4614
  import { OAuthTokensSchema as OAuthTokensSchema2 } from "@modelcontextprotocol/core";
4338
4615
  var PERSIST_TOKENS_ENV = "MCP_INSPECTOR_PERSIST_TOKENS";
4339
4616
  var warnedPolicyValues = /* @__PURE__ */ new Set();
@@ -4366,6 +4643,29 @@ var LEGACY_REGISTRATION_TOKEN_FIELD = "registration-token";
4366
4643
  var PREREG_CLIENT_SECRET_FIELD = "prereg-client-secret";
4367
4644
  var PREREG_REGISTRATION_TOKEN_FIELD = "prereg-registration-token";
4368
4645
  var IDP_SESSION_FIELD = "idp-session";
4646
+ var clientCredentialStampField = (field) => `${field}#owner`;
4647
+ var credentialMac = (clientId, value) => createHmac("sha256", value).update(clientId).digest("hex");
4648
+ function clientCredentialStamp(clientId, value) {
4649
+ return JSON.stringify({
4650
+ client_id: clientId,
4651
+ mac: credentialMac(clientId, value)
4652
+ });
4653
+ }
4654
+ function stampedOwner(stamp, value) {
4655
+ if (stamp === void 0) return void 0;
4656
+ let parsed;
4657
+ try {
4658
+ parsed = JSON.parse(stamp);
4659
+ } catch {
4660
+ return void 0;
4661
+ }
4662
+ if (typeof parsed !== "object" || parsed === null) return void 0;
4663
+ const { client_id: clientId, mac } = parsed;
4664
+ if (typeof clientId !== "string" || mac !== credentialMac(clientId, value)) {
4665
+ return void 0;
4666
+ }
4667
+ return clientId;
4668
+ }
4369
4669
  function applyTokensPolicy(tokens, policy) {
4370
4670
  if (policy === "none") return void 0;
4371
4671
  if (policy === "access" && tokens.refresh_token !== void 0) {
@@ -4394,12 +4694,18 @@ function splitClientInformation(info, fields, secrets) {
4394
4694
  if (client_secret === void 0 && registration_access_token === void 0) {
4395
4695
  return info;
4396
4696
  }
4397
- if (client_secret !== void 0) {
4398
- secrets[fields.clientSecret] = client_secret;
4399
- }
4400
- if (registration_access_token !== void 0) {
4401
- secrets[fields.registrationToken] = registration_access_token;
4402
- }
4697
+ const put = (field, value) => {
4698
+ if (value === void 0) return;
4699
+ secrets[field] = value;
4700
+ if (fields.stamped && typeof info.client_id === "string") {
4701
+ secrets[clientCredentialStampField(field)] = clientCredentialStamp(
4702
+ info.client_id,
4703
+ value
4704
+ );
4705
+ }
4706
+ };
4707
+ put(fields.clientSecret, client_secret);
4708
+ put(fields.registrationToken, registration_access_token);
4403
4709
  return publicInfo;
4404
4710
  }
4405
4711
  function joinClientInformation(info, fields, secrets) {
@@ -4418,15 +4724,18 @@ function joinClientInformation(info, fields, secrets) {
4418
4724
  }
4419
4725
  var issuerClientInfoFields = (issuer) => ({
4420
4726
  clientSecret: issuerClientSecretField(issuer),
4421
- registrationToken: issuerRegistrationTokenField(issuer)
4727
+ registrationToken: issuerRegistrationTokenField(issuer),
4728
+ stamped: true
4422
4729
  });
4423
4730
  var LEGACY_CLIENT_INFO_FIELDS = {
4424
4731
  clientSecret: LEGACY_CLIENT_SECRET_FIELD,
4425
- registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD
4732
+ registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD,
4733
+ stamped: true
4426
4734
  };
4427
4735
  var PREREG_CLIENT_INFO_FIELDS = {
4428
4736
  clientSecret: PREREG_CLIENT_SECRET_FIELD,
4429
- registrationToken: PREREG_REGISTRATION_TOKEN_FIELD
4737
+ registrationToken: PREREG_REGISTRATION_TOKEN_FIELD,
4738
+ stamped: false
4430
4739
  };
4431
4740
  function splitServerOAuthState(state, policy) {
4432
4741
  const secrets = {};
@@ -4576,20 +4885,54 @@ function joinIdpSession(residue, secrets) {
4576
4885
  return stored ? { ...residue, ...stored } : { ...residue };
4577
4886
  }
4578
4887
  function serverSecretFields(state) {
4888
+ const stamped = (field) => [
4889
+ field,
4890
+ clientCredentialStampField(field)
4891
+ ];
4579
4892
  const fields = /* @__PURE__ */ new Set([
4580
4893
  LEGACY_TOKENS_FIELD,
4581
- LEGACY_CLIENT_SECRET_FIELD,
4582
- LEGACY_REGISTRATION_TOKEN_FIELD,
4894
+ ...stamped(LEGACY_CLIENT_SECRET_FIELD),
4895
+ ...stamped(LEGACY_REGISTRATION_TOKEN_FIELD),
4583
4896
  PREREG_CLIENT_SECRET_FIELD,
4584
4897
  PREREG_REGISTRATION_TOKEN_FIELD
4585
4898
  ]);
4586
4899
  for (const issuer of Object.keys(state?.byIssuer ?? {})) {
4587
4900
  fields.add(issuerTokensField(issuer));
4588
- fields.add(issuerClientSecretField(issuer));
4589
- fields.add(issuerRegistrationTokenField(issuer));
4901
+ for (const field of stamped(issuerClientSecretField(issuer))) {
4902
+ fields.add(field);
4903
+ }
4904
+ for (const field of stamped(issuerRegistrationTokenField(issuer))) {
4905
+ fields.add(field);
4906
+ }
4590
4907
  }
4591
4908
  return [...fields];
4592
4909
  }
4910
+ function clientCredentialOwners(state) {
4911
+ const owners = /* @__PURE__ */ new Map();
4912
+ const add = (info, fields) => {
4913
+ if (typeof info?.client_id !== "string") return;
4914
+ owners.set(fields.clientSecret, info.client_id);
4915
+ owners.set(fields.registrationToken, info.client_id);
4916
+ };
4917
+ add(state.clientInformation, LEGACY_CLIENT_INFO_FIELDS);
4918
+ for (const [issuer, slot] of Object.entries(state.byIssuer ?? {})) {
4919
+ add(slot.clientInformation, issuerClientInfoFields(issuer));
4920
+ }
4921
+ return owners;
4922
+ }
4923
+ function restampClientCredentials(state, secrets) {
4924
+ const out = { ...secrets };
4925
+ for (const [field, clientId] of clientCredentialOwners(state)) {
4926
+ const stampField = clientCredentialStampField(field);
4927
+ const value = Object.hasOwn(out, field) ? out[field] : void 0;
4928
+ if (value === void 0) {
4929
+ delete out[stampField];
4930
+ } else if (stampedOwner(out[stampField], value) === void 0) {
4931
+ setOwnEntry(out, stampField, clientCredentialStamp(clientId, value));
4932
+ }
4933
+ }
4934
+ return out;
4935
+ }
4593
4936
  function clientInfoHasPlaintext(info) {
4594
4937
  return info !== void 0 && (info.client_secret !== void 0 || info.registration_access_token !== void 0);
4595
4938
  }
@@ -4630,6 +4973,44 @@ function coveredLocations(location, byLocation) {
4630
4973
  ([recorded]) => recorded === location || location === KEYRING_LOCATION && recorded.startsWith(FILE_LOCATION_PREFIX)
4631
4974
  ).map(([recorded, keys]) => ({ location: recorded, keys }));
4632
4975
  }
4976
+ var emptyKeys = () => ({
4977
+ servers: /* @__PURE__ */ new Set(),
4978
+ idpSessions: /* @__PURE__ */ new Set(),
4979
+ owners: /* @__PURE__ */ new Map()
4980
+ });
4981
+ function candidateFields(location, byLocation) {
4982
+ const fields = /* @__PURE__ */ new Map();
4983
+ for (const { keys } of coveredLocations(location, byLocation)) {
4984
+ for (const [url, owned] of keys.owners) {
4985
+ const kept = fields.get(url) ?? /* @__PURE__ */ new Set();
4986
+ for (const field of owned.keys()) kept.add(field);
4987
+ fields.set(url, kept);
4988
+ }
4989
+ }
4990
+ return fields;
4991
+ }
4992
+ function addMissingOwners(target, source) {
4993
+ for (const [url, fields] of source) {
4994
+ const kept = target.get(url) ?? /* @__PURE__ */ new Map();
4995
+ for (const [field, clientId] of fields) {
4996
+ if (!kept.has(field)) kept.set(field, clientId);
4997
+ }
4998
+ if (kept.size > 0) target.set(url, kept);
4999
+ }
5000
+ }
5001
+ function mergeOwners(target, source) {
5002
+ let changed = false;
5003
+ for (const [url, fields] of source) {
5004
+ const kept = target.get(url) ?? /* @__PURE__ */ new Map();
5005
+ for (const [field, clientId] of fields) {
5006
+ if (kept.get(field) === clientId) continue;
5007
+ kept.set(field, clientId);
5008
+ changed = true;
5009
+ }
5010
+ if (kept.size > 0) target.set(url, kept);
5011
+ }
5012
+ return changed;
5013
+ }
4633
5014
  var warnedLedgerFailures = /* @__PURE__ */ new Set();
4634
5015
  function warnLedgerFailure(what, error) {
4635
5016
  const reason = error instanceof Error ? error.message : String(error);
@@ -4643,6 +5024,19 @@ function warnLedgerFailure(what, error) {
4643
5024
  function isRecord3(value) {
4644
5025
  return typeof value === "object" && value !== null && !Array.isArray(value);
4645
5026
  }
5027
+ function parseOwners(value) {
5028
+ const owners = /* @__PURE__ */ new Map();
5029
+ if (!isRecord3(value)) return owners;
5030
+ for (const [url, fields] of Object.entries(value)) {
5031
+ if (!isRecord3(fields)) continue;
5032
+ const byField = /* @__PURE__ */ new Map();
5033
+ for (const [field, clientId] of Object.entries(fields)) {
5034
+ if (typeof clientId === "string") byField.set(field, clientId);
5035
+ }
5036
+ if (byField.size > 0) owners.set(url, byField);
5037
+ }
5038
+ return owners;
5039
+ }
4646
5040
  function stringsOf(value) {
4647
5041
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
4648
5042
  }
@@ -4664,7 +5058,9 @@ function parseLedger(raw) {
4664
5058
  if (!isRecord3(keys)) continue;
4665
5059
  byLocation.set(location, {
4666
5060
  servers: new Set(stringsOf(keys.servers)),
4667
- idpSessions: new Set(stringsOf(keys.idpSessions))
5061
+ idpSessions: new Set(stringsOf(keys.idpSessions)),
5062
+ owners: parseOwners(keys.owners),
5063
+ ...isValidSecretsNamespace(keys.owedTo) && { owedTo: keys.owedTo }
4668
5064
  });
4669
5065
  }
4670
5066
  if (byLocation.size > 0) ledger.set(namespace, byLocation);
@@ -4678,7 +5074,19 @@ function serializeLedger(ledger) {
4678
5074
  Object.fromEntries(
4679
5075
  [...byLocation].map(([location, keys]) => [
4680
5076
  location,
4681
- { servers: [...keys.servers], idpSessions: [...keys.idpSessions] }
5077
+ {
5078
+ servers: [...keys.servers],
5079
+ idpSessions: [...keys.idpSessions],
5080
+ ...keys.owners.size > 0 && {
5081
+ owners: Object.fromEntries(
5082
+ [...keys.owners].map(([url, fields]) => [
5083
+ url,
5084
+ Object.fromEntries(fields)
5085
+ ])
5086
+ )
5087
+ },
5088
+ ...keys.owedTo !== void 0 && { owedTo: keys.owedTo }
5089
+ }
4682
5090
  ])
4683
5091
  )
4684
5092
  ])
@@ -4690,7 +5098,7 @@ async function writeLedger(stateFilePath, ledger) {
4690
5098
  if (ledger.size === 0) await deleteStoreFile(path6);
4691
5099
  else await writeStoreFile(path6, serializeLedger(ledger));
4692
5100
  }
4693
- async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions) {
5101
+ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions, owners = /* @__PURE__ */ new Map()) {
4694
5102
  try {
4695
5103
  const ledger = parseLedger(
4696
5104
  await readStoreFile(namespaceLedgerPath(stateFilePath))
@@ -4704,7 +5112,7 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
4704
5112
  let keys = byLocation.get(location);
4705
5113
  let changed = false;
4706
5114
  if (!keys) {
4707
- keys = { servers: /* @__PURE__ */ new Set(), idpSessions: /* @__PURE__ */ new Set() };
5115
+ keys = emptyKeys();
4708
5116
  byLocation.set(location, keys);
4709
5117
  changed = true;
4710
5118
  }
@@ -4718,12 +5126,105 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
4718
5126
  keys.idpSessions.add(issuer);
4719
5127
  changed = true;
4720
5128
  }
5129
+ if (mergeOwners(keys.owners, owners)) changed = true;
4721
5130
  if (changed) await writeLedger(stateFilePath, ledger);
4722
5131
  } catch (error) {
4723
5132
  warnLedgerFailure("update", error);
4724
5133
  }
4725
5134
  }
4726
- async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
5135
+ async function markUncarriedNamespaces(stateFilePath, secretStore, into) {
5136
+ const ledger = parseLedger(
5137
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
5138
+ );
5139
+ const location = await secretStoreLocation(secretStore);
5140
+ let changed = false;
5141
+ for (const [namespace, byLocation] of ledger) {
5142
+ if (namespace === into) continue;
5143
+ for (const [recorded, keys] of byLocation) {
5144
+ if (recorded === location || keys.owners.size === 0) continue;
5145
+ if (keys.owedTo === into) continue;
5146
+ keys.owedTo = into;
5147
+ changed = true;
5148
+ }
5149
+ }
5150
+ if (changed) await writeLedger(stateFilePath, ledger);
5151
+ }
5152
+ async function pendingCarries(stateFilePath, secretStore, current) {
5153
+ const ledger = parseLedger(
5154
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
5155
+ );
5156
+ const location = await secretStoreLocation(secretStore);
5157
+ const carries = [];
5158
+ for (const [namespace, byLocation] of ledger) {
5159
+ const owed = coveredLocations(location, byLocation).some(
5160
+ ({ keys }) => keys.owedTo === current
5161
+ );
5162
+ if (owed) {
5163
+ carries.push({
5164
+ namespace,
5165
+ fields: candidateFields(location, byLocation)
5166
+ });
5167
+ }
5168
+ }
5169
+ return carries.reverse();
5170
+ }
5171
+ async function carryOwed(stateFilePath, ledger, location, secretStore, namespace, current, recorded, now) {
5172
+ let carried = true;
5173
+ for (const [url, fields] of recorded) {
5174
+ for (const field of fields) {
5175
+ const clientId = now.get(url)?.get(field);
5176
+ if (clientId === void 0) continue;
5177
+ try {
5178
+ const from = oauthSecretServerId(url, namespace);
5179
+ const to = oauthSecretServerId(url, current);
5180
+ const stampField = clientCredentialStampField(field);
5181
+ const value = await secretStoreGetStrict(secretStore, from, field);
5182
+ if (value === null) continue;
5183
+ const stamp = await secretStoreGetStrict(secretStore, from, stampField);
5184
+ if (stamp === null || stampedOwner(stamp, value) !== clientId) {
5185
+ continue;
5186
+ }
5187
+ if (await secretStoreGetStrict(secretStore, to, field) !== null) {
5188
+ continue;
5189
+ }
5190
+ let byLocation = ledger.get(current);
5191
+ if (!byLocation) {
5192
+ byLocation = /* @__PURE__ */ new Map();
5193
+ ledger.set(current, byLocation);
5194
+ }
5195
+ const target = byLocation.get(location) ?? emptyKeys();
5196
+ byLocation.set(location, target);
5197
+ const isNew = !target.servers.has(url);
5198
+ target.servers.add(url);
5199
+ const owner = /* @__PURE__ */ new Map([[url, /* @__PURE__ */ new Map([[field, clientId]])]]);
5200
+ if (mergeOwners(target.owners, owner) || isNew) {
5201
+ await writeLedger(stateFilePath, ledger);
5202
+ }
5203
+ await secretStore.set(to, stampField, stamp);
5204
+ await secretStore.set(to, field, value);
5205
+ } catch (error) {
5206
+ carried = false;
5207
+ warnLedgerFailure(
5208
+ "carry over a superseded namespace recorded in",
5209
+ error
5210
+ );
5211
+ }
5212
+ }
5213
+ }
5214
+ return carried;
5215
+ }
5216
+ async function recordedNamespaces(stateFilePath) {
5217
+ try {
5218
+ const ledger = parseLedger(
5219
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
5220
+ );
5221
+ return [...ledger.keys()].reverse();
5222
+ } catch (error) {
5223
+ warnLedgerFailure("read", error);
5224
+ return [];
5225
+ }
5226
+ }
5227
+ async function purgeSupersededNamespaces(stateFilePath, secretStore, current, owners = /* @__PURE__ */ new Map()) {
4727
5228
  let ledger;
4728
5229
  let location;
4729
5230
  try {
@@ -4736,8 +5237,22 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
4736
5237
  return;
4737
5238
  }
4738
5239
  let changed = false;
4739
- for (const [namespace, byLocation] of ledger) {
5240
+ for (const [namespace, byLocation] of [...ledger].reverse()) {
4740
5241
  if (namespace === current) continue;
5242
+ if (current !== void 0 && coveredLocations(location, byLocation).some(
5243
+ ({ keys }) => keys.owedTo === current
5244
+ ) && !await carryOwed(
5245
+ stateFilePath,
5246
+ ledger,
5247
+ location,
5248
+ secretStore,
5249
+ namespace,
5250
+ current,
5251
+ candidateFields(location, byLocation),
5252
+ owners
5253
+ )) {
5254
+ break;
5255
+ }
4741
5256
  for (const { location: recorded, keys } of coveredLocations(
4742
5257
  location,
4743
5258
  byLocation
@@ -4762,12 +5277,11 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
4762
5277
  if (!purged || recorded !== location) continue;
4763
5278
  byLocation.delete(recorded);
4764
5279
  if (recorded.startsWith(FILE_LOCATION_PREFIX)) {
4765
- const keyring = byLocation.get(KEYRING_LOCATION) ?? {
4766
- servers: /* @__PURE__ */ new Set(),
4767
- idpSessions: /* @__PURE__ */ new Set()
4768
- };
5280
+ const keyring = byLocation.get(KEYRING_LOCATION) ?? emptyKeys();
5281
+ addMissingOwners(keyring.owners, keys.owners);
4769
5282
  for (const url of keys.servers) keyring.servers.add(url);
4770
5283
  for (const issuer of keys.idpSessions) keyring.idpSessions.add(issuer);
5284
+ keyring.owedTo ??= keys.owedTo;
4771
5285
  byLocation.set(KEYRING_LOCATION, keyring);
4772
5286
  }
4773
5287
  changed = true;
@@ -4878,7 +5392,12 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4878
5392
  const existing = parseSecretsNamespace(raw);
4879
5393
  if (existing !== void 0) {
4880
5394
  if (locked) {
4881
- await purgeSupersededNamespaces(filePath, secretStore, existing);
5395
+ await purgeSupersededNamespaces(
5396
+ filePath,
5397
+ secretStore,
5398
+ existing,
5399
+ snapshotClientOwners(parseOAuthPersistBlob(raw))
5400
+ );
4882
5401
  }
4883
5402
  return existing;
4884
5403
  }
@@ -4886,42 +5405,53 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4886
5405
  if (raw !== null && snapshot === null) {
4887
5406
  throw new OAuthStateFileUnrecognizedError(filePath, "save");
4888
5407
  }
4889
- if (locked) {
4890
- await purgeSupersededNamespaces(filePath, secretStore, void 0);
4891
- }
4892
5408
  const namespace = newSecretsNamespace();
4893
- if (snapshot === null) return namespace;
4894
- const moves = [
4895
- ...Object.entries(snapshot.servers).map(([url, state]) => ({
4896
- legacyId: oauthSecretServerId(url),
4897
- scopedId: oauthSecretServerId(url, namespace),
4898
- fields: serverSecretFields(state)
4899
- })),
4900
- ...Object.keys(snapshot.idpSessions).map((issuer) => ({
4901
- legacyId: oauthIdpSecretServerId(issuer),
4902
- scopedId: oauthIdpSecretServerId(issuer, namespace),
4903
- fields: [IDP_SESSION_FIELD]
4904
- }))
4905
- ];
4906
- if (moves.length === 0) return namespace;
5409
+ if (snapshot === null || Object.keys(snapshot.servers).length === 0 && Object.keys(snapshot.idpSessions).length === 0) {
5410
+ if (locked) {
5411
+ await purgeSupersededNamespaces(filePath, secretStore, void 0);
5412
+ }
5413
+ return namespace;
5414
+ }
4907
5415
  if (!locked) {
4908
5416
  throw new SecretStoreUnavailableError(
4909
5417
  `Could not save OAuth state: ${filePath} predates per-state-file secret namespaces, and migrating its secret-store entries needs the file lock, which is unavailable here (see the lock warning above). Migrating without it could lose credentials if two processes migrate at once. Nothing was changed; make the lock directory writable and retry \u2014 or, if you cannot, clear this file's stored OAuth state and re-authorize (clearing does not migrate, and the fresh state file mints its namespace without the lock).`
4910
5418
  );
4911
5419
  }
5420
+ const values = await readEntrySecrets(
5421
+ snapshot,
5422
+ secretStore,
5423
+ void 0,
5424
+ await recordedNamespaces(filePath)
5425
+ );
5426
+ const owners = snapshotClientOwners(snapshot);
4912
5427
  await recordNamespaceKeys(
4913
5428
  filePath,
4914
5429
  secretStore,
4915
5430
  namespace,
4916
5431
  Object.keys(snapshot.servers),
4917
- Object.keys(snapshot.idpSessions)
5432
+ Object.keys(snapshot.idpSessions),
5433
+ writtenClientOwners(snapshot, (url) => values.servers.get(url) ?? {})
4918
5434
  );
5435
+ await markUncarriedNamespaces(filePath, secretStore, namespace);
4919
5436
  const touched = [];
4920
5437
  try {
4921
- for (const { legacyId, scopedId, fields } of moves) {
4922
- for (const field of fields) {
4923
- const value = await secretStoreGetStrict(secretStore, legacyId, field);
4924
- if (value === null) continue;
5438
+ const copies = [
5439
+ // Restamped: a value read from the legacy ids predates stamps, and a
5440
+ // fallback's stamp may belong to a value read from elsewhere (#2734).
5441
+ ...[...values.servers].map(([url, secrets]) => ({
5442
+ scopedId: oauthSecretServerId(url, namespace),
5443
+ secrets: restampClientCredentials(
5444
+ getOwnEntry(snapshot.servers, url) ?? {},
5445
+ secrets
5446
+ )
5447
+ })),
5448
+ ...[...values.idpSessions].map(([issuer, secrets]) => ({
5449
+ scopedId: oauthIdpSecretServerId(issuer, namespace),
5450
+ secrets
5451
+ }))
5452
+ ];
5453
+ for (const { scopedId, secrets } of copies) {
5454
+ for (const [field, value] of Object.entries(secrets)) {
4925
5455
  touched.push({ serverId: scopedId, field, value: null });
4926
5456
  await secretStore.set(scopedId, field, value);
4927
5457
  }
@@ -4931,7 +5461,14 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4931
5461
  await restoreSecretFields(secretStore, touched, warnRestoreFailure);
4932
5462
  throw error;
4933
5463
  }
4934
- for (const { legacyId } of moves) {
5464
+ await purgeSupersededNamespaces(filePath, secretStore, namespace, owners);
5465
+ const legacyIds = [
5466
+ ...Object.keys(snapshot.servers).map((url) => oauthSecretServerId(url)),
5467
+ ...Object.keys(snapshot.idpSessions).map(
5468
+ (issuer) => oauthIdpSecretServerId(issuer)
5469
+ )
5470
+ ];
5471
+ for (const legacyId of legacyIds) {
4935
5472
  try {
4936
5473
  await secretStore.deleteAllForServer(legacyId);
4937
5474
  } catch (error) {
@@ -4984,6 +5521,14 @@ function preserveNonDurableSecrets(diskSecrets, secrets) {
4984
5521
  return keep;
4985
5522
  }
4986
5523
  async function writeOAuthSections(filePath, snapshot, sections, secretStore = defaultSecretStore()) {
5524
+ await writeOAuthSectionsFrom(
5525
+ filePath,
5526
+ async () => snapshot,
5527
+ sections,
5528
+ secretStore
5529
+ );
5530
+ }
5531
+ async function writeOAuthSectionsFrom(filePath, resolveSnapshot, sections, secretStore) {
4987
5532
  const policy = getPersistTokensPolicy();
4988
5533
  const durable = await secretStoreIsDurable(secretStore);
4989
5534
  await withOAuthStateLock(filePath, "save", async (locked) => {
@@ -5067,6 +5612,7 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
5067
5612
  unconfirmed = null;
5068
5613
  namespace = diskNamespace;
5069
5614
  }
5615
+ const snapshot = await resolveSnapshot(disk, namespace);
5070
5616
  const effective = sections ? {
5071
5617
  servers: sections.servers && [...new Set(sections.servers)],
5072
5618
  idpSessions: sections.idpSessions && [
@@ -5092,7 +5638,22 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
5092
5638
  secretStore,
5093
5639
  namespace,
5094
5640
  Object.keys(merged.servers),
5095
- Object.keys(merged.idpSessions)
5641
+ Object.keys(merged.idpSessions),
5642
+ // Only the entries this save writes: another entry's secret in
5643
+ // this store was written for whatever client it was written for,
5644
+ // and relabeling it from the file would misattribute it (#2672).
5645
+ writtenClientOwners(
5646
+ {
5647
+ servers: Object.fromEntries(
5648
+ (effective.servers ?? []).flatMap((url) => {
5649
+ const entry = getOwnEntry(merged.servers, url);
5650
+ return entry === void 0 ? [] : [[url, entry]];
5651
+ })
5652
+ ),
5653
+ idpSessions: {}
5654
+ },
5655
+ splitSecretsOf
5656
+ )
5096
5657
  );
5097
5658
  for (const url of effective.servers ?? []) {
5098
5659
  const serverId = oauthSecretServerId(url, namespace);
@@ -5100,7 +5661,8 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
5100
5661
  const candidates = [
5101
5662
  .../* @__PURE__ */ new Set([
5102
5663
  ...serverSecretFields(getOwnEntry(disk?.servers, url)),
5103
- ...serverSecretFields(next)
5664
+ ...serverSecretFields(next),
5665
+ ...[...restoreBaseline.values()].filter((prior) => prior.serverId === serverId).map(({ field }) => field)
5104
5666
  ])
5105
5667
  ];
5106
5668
  const entryPrior = await snapshotSecretFields(
@@ -5109,8 +5671,12 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
5109
5671
  candidates
5110
5672
  );
5111
5673
  priorSecrets.push(...entryPrior);
5674
+ const entryPriorValue = new Map(
5675
+ entryPrior.map(({ field, value }) => [field, value])
5676
+ );
5112
5677
  if (next === void 0) {
5113
5678
  for (const field of candidates) {
5679
+ if (entryPriorValue.get(field) === null) continue;
5114
5680
  ourWrites.set(priorKey({ serverId, field }), null);
5115
5681
  }
5116
5682
  await secretStore.deleteAllForServer(serverId);
@@ -5125,10 +5691,9 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
5125
5691
  }
5126
5692
  const { residue, secrets } = splitServerOAuthState(next, policy);
5127
5693
  for (const field of candidates) {
5128
- ourWrites.set(
5129
- priorKey({ serverId, field }),
5130
- secrets[field] ?? null
5131
- );
5694
+ const value = secrets[field] ?? null;
5695
+ if (value === entryPriorValue.get(field)) continue;
5696
+ ourWrites.set(priorKey({ serverId, field }), value);
5132
5697
  }
5133
5698
  setOwnEntry(merged.servers, url, residue);
5134
5699
  if (!durable) {
@@ -5271,26 +5836,109 @@ function secretRequestsFor(snapshot, namespace) {
5271
5836
  }
5272
5837
  return requests;
5273
5838
  }
5274
- async function joinSnapshot(snapshot, secretStore, namespace) {
5275
- const requests = secretRequestsFor(snapshot, namespace);
5276
- const values = requests.length > 0 ? await secretStoreGetManyStrict(secretStore, requests) : {};
5839
+ function fillMissingSecrets(target, source) {
5840
+ for (const [field, value] of Object.entries(source ?? {})) {
5841
+ if (!Object.hasOwn(target, field)) setOwnEntry(target, field, value);
5842
+ }
5843
+ }
5844
+ function writtenClientOwners(snapshot, secretsOf) {
5845
+ const owners = /* @__PURE__ */ new Map();
5846
+ for (const [url, state] of Object.entries(snapshot.servers)) {
5847
+ const secrets = secretsOf(url, state);
5848
+ const fields = new Map(
5849
+ [...clientCredentialOwners(state)].filter(
5850
+ ([field]) => Object.hasOwn(secrets, field)
5851
+ )
5852
+ );
5853
+ if (fields.size > 0) owners.set(url, fields);
5854
+ }
5855
+ return owners;
5856
+ }
5857
+ var splitSecretsOf = (_url, state) => splitServerOAuthState(state, "all").secrets;
5858
+ function snapshotClientOwners(snapshot) {
5859
+ const owners = /* @__PURE__ */ new Map();
5860
+ for (const [url, state] of Object.entries(snapshot?.servers ?? {})) {
5861
+ const fields = clientCredentialOwners(state);
5862
+ if (fields.size > 0) owners.set(url, fields);
5863
+ }
5864
+ return owners;
5865
+ }
5866
+ async function readEntrySecrets(snapshot, secretStore, namespace, fallbacks, carries = []) {
5867
+ const servers = /* @__PURE__ */ new Map();
5868
+ const idpSessions = /* @__PURE__ */ new Map();
5869
+ for (const url of Object.keys(snapshot.servers)) servers.set(url, {});
5870
+ for (const issuer of Object.keys(snapshot.idpSessions)) {
5871
+ idpSessions.set(issuer, {});
5872
+ }
5873
+ for (const source of [namespace, ...fallbacks]) {
5874
+ const requests = secretRequestsFor(snapshot, source);
5875
+ if (requests.length === 0) break;
5876
+ const values = await secretStoreGetManyStrict(secretStore, requests);
5877
+ for (const [url, target] of servers) {
5878
+ fillMissingSecrets(target, values[oauthSecretServerId(url, source)]);
5879
+ }
5880
+ for (const [issuer, target] of idpSessions) {
5881
+ fillMissingSecrets(
5882
+ target,
5883
+ values[oauthIdpSecretServerId(issuer, source)]
5884
+ );
5885
+ }
5886
+ }
5887
+ const owners = snapshotClientOwners(snapshot);
5888
+ for (const { namespace: from, fields: recorded } of carries) {
5889
+ const requests = [];
5890
+ for (const [url, fields] of recorded) {
5891
+ const target = servers.get(url);
5892
+ if (!target) continue;
5893
+ const wanted = [...fields].filter(
5894
+ (field) => owners.get(url)?.has(field) === true && !Object.hasOwn(target, field)
5895
+ );
5896
+ if (wanted.length > 0) {
5897
+ requests.push({
5898
+ serverId: oauthSecretServerId(url, from),
5899
+ fields: wanted.flatMap((field) => [
5900
+ field,
5901
+ clientCredentialStampField(field)
5902
+ ])
5903
+ });
5904
+ }
5905
+ }
5906
+ if (requests.length === 0) continue;
5907
+ const values = await secretStoreGetManyStrict(secretStore, requests);
5908
+ for (const [url, target] of servers) {
5909
+ const found = values[oauthSecretServerId(url, from)] ?? {};
5910
+ for (const [field, clientId] of owners.get(url) ?? []) {
5911
+ const value = Object.hasOwn(found, field) ? found[field] : void 0;
5912
+ const stampField = clientCredentialStampField(field);
5913
+ if (value === void 0 || Object.hasOwn(target, field) || stampedOwner(found[stampField], value) !== clientId) {
5914
+ continue;
5915
+ }
5916
+ setOwnEntry(target, field, value);
5917
+ setOwnEntry(target, stampField, found[stampField]);
5918
+ }
5919
+ }
5920
+ }
5921
+ return { servers, idpSessions };
5922
+ }
5923
+ async function joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries = []) {
5924
+ const values = await readEntrySecrets(
5925
+ snapshot,
5926
+ secretStore,
5927
+ namespace,
5928
+ fallbacks,
5929
+ carries
5930
+ );
5277
5931
  return {
5278
5932
  servers: Object.fromEntries(
5279
5933
  Object.entries(snapshot.servers).map(([url, state]) => [
5280
5934
  url,
5281
- joinServerOAuthState(
5282
- state,
5283
- values[oauthSecretServerId(url, namespace)] ?? {}
5284
- )
5935
+ joinServerOAuthState(state, values.servers.get(url) ?? {})
5285
5936
  ])
5286
5937
  ),
5287
5938
  idpSessions: Object.fromEntries(
5288
5939
  Object.entries(snapshot.idpSessions).map(([issuer, session]) => [
5289
5940
  issuer,
5290
- joinIdpSession(
5291
- session,
5292
- values[oauthIdpSecretServerId(issuer, namespace)] ?? {}
5293
- )
5941
+ joinIdpSession(session, values.idpSessions.get(issuer) ?? {})
5294
5942
  ])
5295
5943
  )
5296
5944
  };
@@ -5306,7 +5954,8 @@ async function migratePlaintextSecrets(filePath, secretStore) {
5306
5954
  secretStore,
5307
5955
  namespace,
5308
5956
  Object.keys(fresh.servers),
5309
- Object.keys(fresh.idpSessions)
5957
+ Object.keys(fresh.idpSessions),
5958
+ writtenClientOwners(fresh, splitSecretsOf)
5310
5959
  );
5311
5960
  }
5312
5961
  const migrateEntrySecrets = async (serverId, secrets) => {
@@ -5357,7 +6006,10 @@ async function readOAuthStore(filePath, secretStore = defaultSecretStore()) {
5357
6006
  warnMigrationFailure(error);
5358
6007
  }
5359
6008
  }
5360
- return joinSnapshot(snapshot, secretStore, parseSecretsNamespace(raw));
6009
+ const namespace = parseSecretsNamespace(raw);
6010
+ const fallbacks = namespace === void 0 ? await recordedNamespaces(filePath) : [];
6011
+ const carries = namespace === void 0 ? [] : await pendingCarries(filePath, secretStore, namespace);
6012
+ return joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries);
5361
6013
  });
5362
6014
  }
5363
6015
  async function removeOAuthStore(filePath, secretStore = defaultSecretStore()) {
@@ -5597,6 +6249,8 @@ var RemoteSession = class _RemoteSession {
5597
6249
  requestWaits = /* @__PURE__ */ new Map();
5598
6250
  authProviderHandle = null;
5599
6251
  appliedProtocolVersion;
6252
+ /** Each upstream `429`'s `Retry-After`, by the JSON-RPC id that drew it (#2442). */
6253
+ rateLimits = new RateLimitLedger();
5600
6254
  /** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
5601
6255
  static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
5602
6256
  constructor(sessionId) {
@@ -5808,6 +6462,7 @@ var RemoteSession = class _RemoteSession {
5808
6462
  this.pushEvent({ type: "message", data: message });
5809
6463
  }
5810
6464
  onFetchRequest(entry) {
6465
+ this.rateLimits.record(entry);
5811
6466
  this.pushEvent({
5812
6467
  type: "fetch_request",
5813
6468
  data: {
@@ -5816,6 +6471,14 @@ var RemoteSession = class _RemoteSession {
5816
6471
  }
5817
6472
  });
5818
6473
  }
6474
+ /**
6475
+ * The `Retry-After` of the upstream `429` that answered request `id` — read
6476
+ * by the send route, so the header rides back on the send's own error rather
6477
+ * than on a fetch event racing it (#2442).
6478
+ */
6479
+ takeRetryAfter(id) {
6480
+ return this.rateLimits.take(id);
6481
+ }
5819
6482
  onFetchResponseBody(id, responseBody) {
5820
6483
  this.pushEvent({
5821
6484
  type: "fetch_request_body_update",
@@ -6259,6 +6922,17 @@ var MAX_APP_DOCUMENT_CHARS = 8 * 1024 * 1024;
6259
6922
  var MAX_APP_DOCUMENT_BODY_BYTES = 24 * 1024 * 1024;
6260
6923
  var MAX_APP_CSP_CHARS = 8 * 1024;
6261
6924
  var MAX_STORAGE_BODY_BYTES = 4 * 1024 * 1024;
6925
+ var MAX_PROXIED_RESPONSE_BYTES = 4 * 1024 * 1024;
6926
+ var BODY_FRAMING_HEADERS = /* @__PURE__ */ new Set([
6927
+ "content-length",
6928
+ "transfer-encoding",
6929
+ "content-encoding"
6930
+ ]);
6931
+ function withoutBodyFraming(headers) {
6932
+ return Object.fromEntries(
6933
+ Object.entries(headers).filter(([k]) => !BODY_FRAMING_HEADERS.has(k))
6934
+ );
6935
+ }
6262
6936
  function isHeaderSafeValue(value) {
6263
6937
  return !/[^\t\x20-\x7e]/.test(value);
6264
6938
  }
@@ -6714,6 +7388,25 @@ function createRemoteApp(options) {
6714
7388
  });
6715
7389
  }
6716
7390
  const msg = err instanceof Error ? err.message : String(err);
7391
+ if (err instanceof SdkHttpError2) {
7392
+ const text = err.data.text;
7393
+ const sentId = "id" in message ? message.id : void 0;
7394
+ const retryAfter = isRateLimitError(err) && (typeof sentId === "string" || typeof sentId === "number") ? session.takeRetryAfter(sentId) : void 0;
7395
+ return c.json({
7396
+ ok: false,
7397
+ kind: "transport_error",
7398
+ error: msg,
7399
+ http: {
7400
+ code: err.code,
7401
+ status: err.status,
7402
+ ...err.statusText !== void 0 && {
7403
+ statusText: err.statusText
7404
+ },
7405
+ ...typeof text === "string" && { text },
7406
+ ...retryAfter !== void 0 && { retryAfter }
7407
+ }
7408
+ });
7409
+ }
6717
7410
  return c.json({ ok: false, kind: "transport_error", error: msg });
6718
7411
  } finally {
6719
7412
  clientSignal.removeEventListener("abort", onClientDisconnect);
@@ -6843,7 +7536,28 @@ function createRemoteApp(options) {
6843
7536
  const isStream = contentType?.includes("text/event-stream") || contentType?.includes("application/x-ndjson");
6844
7537
  let resBody;
6845
7538
  if (!isStream && res.body) {
6846
- resBody = await res.text();
7539
+ const refuseOversize = () => res.ok ? c.json(
7540
+ {
7541
+ error: `response from ${safeUrl} exceeds the ${MAX_PROXIED_RESPONSE_BYTES}-byte limit for a proxied response`
7542
+ },
7543
+ 502
7544
+ ) : c.json({
7545
+ ok: res.ok,
7546
+ status: res.status,
7547
+ statusText: res.statusText,
7548
+ headers: withoutBodyFraming(resHeaders)
7549
+ });
7550
+ if (declaresOversizeBody(
7551
+ res.headers.get("content-length"),
7552
+ MAX_PROXIED_RESPONSE_BYTES
7553
+ )) {
7554
+ void res.body.cancel().catch(() => {
7555
+ });
7556
+ return refuseOversize();
7557
+ }
7558
+ const read = await readCappedText(res.body, MAX_PROXIED_RESPONSE_BYTES);
7559
+ if (read.kind === "overflow") return refuseOversize();
7560
+ resBody = read.text;
6847
7561
  } else if (isStream) {
6848
7562
  void res.body?.cancel().catch(() => {
6849
7563
  });
@@ -7656,7 +8370,7 @@ function createRemoteApp(options) {
7656
8370
  type,
7657
8371
  process.platform,
7658
8372
  homedir(),
7659
- (path6) => existsSync3(path6) ? readFileSync6(path6, "utf-8") : null
8373
+ (path6) => existsSync3(path6) ? readFileSync7(path6, "utf-8") : null
7660
8374
  );
7661
8375
  if (!result) {
7662
8376
  return c.json(
@@ -8072,6 +8786,30 @@ function handleNodeHealthRequest(req, res) {
8072
8786
  return true;
8073
8787
  }
8074
8788
 
8789
+ // server/node-request-bridge.ts
8790
+ function fetchUrlForNodeRequest(headers, pathname, scheme) {
8791
+ const authority = headers.host ?? headers[":authority"];
8792
+ return `${scheme}://${authority}${pathname}`;
8793
+ }
8794
+ function fetchHeadersFromNode(headers) {
8795
+ const out = new Headers();
8796
+ for (const [key, value] of Object.entries(headers)) {
8797
+ if (!value || key.startsWith(":")) continue;
8798
+ out.set(key, Array.isArray(value) ? value.join(", ") : value);
8799
+ }
8800
+ return out;
8801
+ }
8802
+ var CONNECTION_SPECIFIC_HEADERS = /* @__PURE__ */ new Set([
8803
+ "connection",
8804
+ "keep-alive",
8805
+ "proxy-connection",
8806
+ "transfer-encoding",
8807
+ "upgrade"
8808
+ ]);
8809
+ function isResponseHeaderAllowed(name, httpVersionMajor) {
8810
+ return httpVersionMajor < 2 || !CONNECTION_SPECIFIC_HEADERS.has(name.toLowerCase());
8811
+ }
8812
+
8075
8813
  // server/vite-hono-plugin.ts
8076
8814
  function honoMiddlewarePlugin(config) {
8077
8815
  let resolvedAuthToken = "";
@@ -8179,18 +8917,12 @@ function honoMiddlewarePlugin(config) {
8179
8917
  if (!pathname.startsWith("/api")) {
8180
8918
  return next();
8181
8919
  }
8182
- const url = `http://${req.headers.host}${pathname}`;
8183
- const headers = new Headers();
8184
- Object.entries(req.headers).forEach(
8185
- ([key, value]) => {
8186
- if (value) {
8187
- headers.set(
8188
- key,
8189
- Array.isArray(value) ? value.join(", ") : value
8190
- );
8191
- }
8192
- }
8920
+ const url = fetchUrlForNodeRequest(
8921
+ req.headers,
8922
+ pathname,
8923
+ webScheme(config.tls)
8193
8924
  );
8925
+ const headers = fetchHeadersFromNode(req.headers);
8194
8926
  const init = { method: req.method, headers };
8195
8927
  if (req.method !== "GET" && req.method !== "HEAD") {
8196
8928
  const chunks = [];
@@ -8207,13 +8939,17 @@ function honoMiddlewarePlugin(config) {
8207
8939
  const response = await honoApp.fetch(new Request(url, init));
8208
8940
  res.statusCode = response.status;
8209
8941
  response.headers.forEach((value, key) => {
8210
- res.setHeader(key, value);
8942
+ if (isResponseHeaderAllowed(key, req.httpVersionMajor)) {
8943
+ res.setHeader(key, value);
8944
+ }
8211
8945
  });
8212
8946
  const isSSE = response.headers.get("content-type")?.includes("text/event-stream");
8213
8947
  if (isSSE) {
8214
8948
  res.setHeader("X-Accel-Buffering", "no");
8215
8949
  res.setHeader("Cache-Control", "no-cache");
8216
- res.setHeader("Connection", "keep-alive");
8950
+ if (isResponseHeaderAllowed("connection", req.httpVersionMajor)) {
8951
+ res.setHeader("Connection", "keep-alive");
8952
+ }
8217
8953
  }
8218
8954
  if (response.body) {
8219
8955
  res.flushHeaders?.();
@@ -8566,6 +9302,10 @@ async function startViteDevServer(config) {
8566
9302
  // `config.port`, so a drifted bind would 403 every connect and CSP-block
8567
9303
  // the MCP Apps iframe while the banner advertises the unusable port.
8568
9304
  strictPort: true,
9305
+ // The same TLS pair the prod listener uses (#2446), matching
9306
+ // `vite.config.ts`. Vite serves it over HTTP/2 with an HTTP/1.1
9307
+ // fallback; the Hono plugin's /api bridge handles both.
9308
+ https: config.tls,
8569
9309
  // Allow Vite to serve source files from the repo root (core/ lives
8570
9310
  // outside clients/web), matching `vite.config.ts`'s `server.fs.allow`.
8571
9311
  fs: {
@@ -8584,10 +9324,11 @@ async function startViteDevServer(config) {
8584
9324
  }
8585
9325
 
8586
9326
  // server/server.ts
8587
- import { readFileSync as readFileSync7 } from "fs";
9327
+ import { readFileSync as readFileSync8 } from "fs";
8588
9328
  import { join as join8, dirname as dirname8 } from "path";
8589
9329
  import { fileURLToPath as fileURLToPath5 } from "url";
8590
9330
  import { randomBytes as randomBytes4 } from "crypto";
9331
+ import { createServer as createHttpsServer } from "https";
8591
9332
  import { serve } from "@hono/node-server";
8592
9333
  import { serveStatic } from "@hono/node-server/serve-static";
8593
9334
  import { Hono as Hono2 } from "hono";
@@ -8634,12 +9375,20 @@ async function startHonoServer(config) {
8634
9375
  });
8635
9376
  const app = new Hono2();
8636
9377
  app.get(HEALTH_PATH, (c) => healthResponse(c.req.method));
9378
+ app.use(
9379
+ "*",
9380
+ hostAllowListMiddleware({
9381
+ bindHost: config.hostname,
9382
+ allowedOrigins: config.allowedOrigins,
9383
+ extraHosts: config.allowedHosts
9384
+ })
9385
+ );
8637
9386
  app.use("/api/*", async (c) => {
8638
9387
  return apiApp.fetch(c.req.raw);
8639
9388
  });
8640
9389
  const serveIndexHtml = (c) => {
8641
9390
  const indexPath = join8(rootPath, "index.html");
8642
- const html = readFileSync7(indexPath, "utf-8");
9391
+ const html = readFileSync8(indexPath, "utf-8");
8643
9392
  c.header("Cache-Control", "no-store");
8644
9393
  return c.html(injectAuthToken(html, resolvedAuthToken));
8645
9394
  };
@@ -8667,7 +9416,10 @@ async function startHonoServer(config) {
8667
9416
  {
8668
9417
  fetch: app.fetch,
8669
9418
  port: config.port,
8670
- hostname: config.hostname
9419
+ hostname: config.hostname,
9420
+ // https only when both TLS paths were set (#2446); otherwise
9421
+ // @hono/node-server's default plain `http.createServer`.
9422
+ ...config.tls ? { createServer: createHttpsServer, serverOptions: config.tls } : {}
8671
9423
  },
8672
9424
  (info) => {
8673
9425
  const sandboxUrl = sandboxController.getUrl();
@@ -8685,9 +9437,7 @@ async function startHonoServer(config) {
8685
9437
  );
8686
9438
  httpServer.on("error", (err) => {
8687
9439
  if (err.message.includes("EADDRINUSE")) {
8688
- console.error(
8689
- `MCP Inspector PORT IS IN USE at http://${formatHostForUrl(config.hostname)}:${config.port}`
8690
- );
9440
+ console.error(portInUseMessage(config));
8691
9441
  process.exit(1);
8692
9442
  } else {
8693
9443
  throw err;
@@ -8768,7 +9518,11 @@ async function runWeb(argv) {
8768
9518
  preArgs = argv.slice(0, argSeparatorIndex);
8769
9519
  postArgs = argv.slice(argSeparatorIndex + 1);
8770
9520
  }
8771
- program.name("mcp-inspector-web").description("Web UI for MCP Inspector").allowExcessArguments().allowUnknownOption().option(
9521
+ program.name("mcp-inspector-web").description("Web UI for MCP Inspector").version(
9522
+ readInspectorVersionSafe(import.meta.url) ?? "unknown",
9523
+ "-V, --version",
9524
+ "Print the Inspector version"
9525
+ ).allowExcessArguments().allowUnknownOption().option(
8772
9526
  "-e <env>",
8773
9527
  "environment variables in KEY=VALUE format",
8774
9528
  parseKeyValuePair,