@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.
@@ -177,6 +177,12 @@ var DirectoryReadResultSchema = z.looseObject({
177
177
  resources: z.array(DirectoryChildSchema),
178
178
  nextCursor: z.string().optional()
179
179
  });
180
+ var ModernDirectoryReadResultSchema = DirectoryReadResultSchema.extend(
181
+ {
182
+ ttlMs: z.int().min(0),
183
+ cacheScope: z.enum(["public", "private"])
184
+ }
185
+ );
180
186
 
181
187
  // ../../core/mcp/sha256.ts
182
188
  var K = new Uint32Array([
@@ -2044,16 +2050,30 @@ function buildConfigFromOptions(options) {
2044
2050
  const target = options.target ?? [];
2045
2051
  const first = target[0];
2046
2052
  const rest = target.slice(1);
2053
+ if (first && options.serverUrl) {
2054
+ throw new Error(
2055
+ `Cannot combine a positional target (${first}) with --server-url (${options.serverUrl}). Pass one or the other.`
2056
+ );
2057
+ }
2047
2058
  const urlFromTarget = first && (first.startsWith("http://") || first.startsWith("https://")) ? first : null;
2048
2059
  const url = urlFromTarget ?? options.serverUrl ?? null;
2049
2060
  if (url) {
2050
- if (rest.length > 0 && urlFromTarget) {
2061
+ if (rest.length > 0) {
2051
2062
  throw new Error("Arguments cannot be passed to a URL-based MCP server.");
2052
2063
  }
2064
+ if (options.transport === "stdio") {
2065
+ throw new Error(
2066
+ `stdio transport cannot be used with a URL (${url}). Use --transport http or --transport sse, or pass a local command instead.`
2067
+ );
2068
+ }
2053
2069
  let transportType;
2054
2070
  const t = options.transport === "http" ? "streamable-http" : options.transport;
2055
2071
  if (t === "sse" || t === "streamable-http") {
2056
2072
  transportType = t;
2073
+ } else if (t) {
2074
+ throw new Error(
2075
+ `Invalid transport type: ${t}. Valid types are: sse, http, stdio`
2076
+ );
2057
2077
  } else {
2058
2078
  const u = new URL(url);
2059
2079
  if (u.pathname.endsWith("/mcp")) {
@@ -4552,7 +4572,10 @@ function parseAuthChallengeFromResponse(response, context) {
4552
4572
  if (status !== 401 && status !== 403) {
4553
4573
  return void 0;
4554
4574
  }
4555
- const wwwAuthenticate = response.headers.get("WWW-Authenticate") ?? void 0;
4575
+ const wwwAuthenticate = response.headers.get("WWW-Authenticate") || void 0;
4576
+ if (status === 403 && !wwwAuthenticate) {
4577
+ return void 0;
4578
+ }
4556
4579
  const bearer = wwwAuthenticate ? parseWwwAuthenticateBearer(wwwAuthenticate) : {};
4557
4580
  const requiredScopes = parseScopeString(bearer.scope);
4558
4581
  return {
@@ -5249,6 +5272,34 @@ function buildRunnerClientAuthOptions(clientConfig, savedSettings, cliOverrides)
5249
5272
  };
5250
5273
  }
5251
5274
 
5275
+ // ../../core/node/version.ts
5276
+ import { readFileSync as readFileSync4 } from "fs";
5277
+ import { dirname as dirname5, join as join3, parse } from "path";
5278
+ import { fileURLToPath } from "url";
5279
+ var ROOT_PACKAGE_NAME = "@modelcontextprotocol/inspector";
5280
+ function readInspectorVersion(callerUrl) {
5281
+ const startDir = dirname5(fileURLToPath(callerUrl));
5282
+ const { root } = parse(startDir);
5283
+ let dir = startDir;
5284
+ for (; ; ) {
5285
+ let manifest;
5286
+ try {
5287
+ manifest = JSON.parse(
5288
+ readFileSync4(join3(dir, "package.json"), "utf-8")
5289
+ );
5290
+ } catch {
5291
+ }
5292
+ if (manifest?.name === ROOT_PACKAGE_NAME && manifest.version) {
5293
+ return manifest.version;
5294
+ }
5295
+ if (dir === root) break;
5296
+ dir = dirname5(dir);
5297
+ }
5298
+ throw new Error(
5299
+ `Could not locate the ${ROOT_PACKAGE_NAME} root package.json above ${startDir}`
5300
+ );
5301
+ }
5302
+
5252
5303
  // ../../core/node/hostUrl.ts
5253
5304
  import { isIPv6 } from "net";
5254
5305
  function stripBrackets(host) {
@@ -5531,6 +5582,23 @@ function subscriptionFailureMessage(error) {
5531
5582
  return error instanceof Error ? error.message : String(error);
5532
5583
  }
5533
5584
 
5585
+ // ../../core/mcp/subscriptionRefusal.ts
5586
+ import { SdkErrorCode as SdkErrorCode2, SdkHttpError } from "@modelcontextprotocol/client";
5587
+ var REFUSAL_STATUSES = /* @__PURE__ */ new Set([400, 403, 404, 405]);
5588
+ function subscriptionListenRefusal(error) {
5589
+ if (!SdkHttpError.isInstance(error) || error.code !== SdkErrorCode2.ClientHttpNotImplemented || !REFUSAL_STATUSES.has(error.status)) {
5590
+ return void 0;
5591
+ }
5592
+ return {
5593
+ status: error.status,
5594
+ ...error.statusText ? { statusText: error.statusText } : {}
5595
+ };
5596
+ }
5597
+ function subscriptionListenRefusalMessage(refusal) {
5598
+ const status = refusal.statusText ? `${refusal.status} ${refusal.statusText}` : String(refusal.status);
5599
+ return `The server refused subscriptions/listen (HTTP ${status}). Not retrying \u2014 the server would answer the same way again.`;
5600
+ }
5601
+
5534
5602
  // ../../core/mcp/messageTrackingTransport.ts
5535
5603
  var MessageTrackingTransport = class {
5536
5604
  baseTransport;
@@ -5760,7 +5828,7 @@ var TaskProgressRouter = class {
5760
5828
  };
5761
5829
 
5762
5830
  // ../../core/extension/tasks/rawWireChannel.ts
5763
- import { SdkError as SdkError2, SdkErrorCode as SdkErrorCode2 } from "@modelcontextprotocol/client";
5831
+ import { SdkError as SdkError2, SdkErrorCode as SdkErrorCode3 } from "@modelcontextprotocol/client";
5764
5832
  import { DispatchError as DispatchError2 } from "@modelcontextprotocol/ext-tasks/client";
5765
5833
  var RAW_WIRE_ID_PREFIX = "inspector-ext-";
5766
5834
  var RawWireChannel = class {
@@ -5833,8 +5901,8 @@ var RawWireChannel = class {
5833
5901
  `Raw MCP request "${message.method}" timed out after ${timeoutMs} ms`,
5834
5902
  false,
5835
5903
  {
5836
- cause: this.host.annotateTimeout(
5837
- new SdkError2(SdkErrorCode2.RequestTimeout, "Request timed out", {
5904
+ cause: this.host.annotateError(
5905
+ new SdkError2(SdkErrorCode3.RequestTimeout, "Request timed out", {
5838
5906
  timeout: timeoutMs
5839
5907
  }),
5840
5908
  message.method
@@ -5874,7 +5942,7 @@ var RawWireChannel = class {
5874
5942
  if (!pending) return;
5875
5943
  pending.cleanup();
5876
5944
  reject(
5877
- this.host.annotateTimeout(
5945
+ this.host.annotateError(
5878
5946
  error instanceof Error ? error : new Error(String(error)),
5879
5947
  message.method
5880
5948
  )
@@ -6174,7 +6242,7 @@ function validateToolOutput(provider, tool, result) {
6174
6242
 
6175
6243
  // ../../core/mcp/listSalvage.ts
6176
6244
  import { z as z4 } from "zod/v4";
6177
- import { SdkError as SdkError3, SdkErrorCode as SdkErrorCode3 } from "@modelcontextprotocol/client";
6245
+ import { SdkError as SdkError3, SdkErrorCode as SdkErrorCode4 } from "@modelcontextprotocol/client";
6178
6246
  import { ToolSchema } from "@modelcontextprotocol/core";
6179
6247
  var LIST_MAX_PAGES = 64;
6180
6248
  function listPaginationExceeded(method) {
@@ -6183,10 +6251,10 @@ function listPaginationExceeded(method) {
6183
6251
  );
6184
6252
  }
6185
6253
  function isClientDecodeRejection(err) {
6186
- return SdkError3.isInstance(err) && (err.code === SdkErrorCode3.InvalidResult || err.code === SdkErrorCode3.UnsupportedResultType);
6254
+ return SdkError3.isInstance(err) && (err.code === SdkErrorCode4.InvalidResult || err.code === SdkErrorCode4.UnsupportedResultType);
6187
6255
  }
6188
6256
  function isSalvageableRejection(err) {
6189
- return SdkError3.isInstance(err) && err.code === SdkErrorCode3.InvalidResult;
6257
+ return SdkError3.isInstance(err) && err.code === SdkErrorCode4.InvalidResult;
6190
6258
  }
6191
6259
  function lenientListPageSchema(resultSchema, itemsKey) {
6192
6260
  return resultSchema.extend({ [itemsKey]: z4.array(z4.unknown()) });
@@ -6923,6 +6991,84 @@ function isJsonDocumentResponse(contentType) {
6923
6991
  const mediaType = contentType.split(";")[0].trim().toLowerCase();
6924
6992
  return mediaType === "application/json" || mediaType.endsWith("+json");
6925
6993
  }
6994
+ var SYNTHESIZED_METADATA_HEADER = "x-inspector-oauth-metadata-synthesized";
6995
+ var synthesizedResponses = /* @__PURE__ */ new WeakSet();
6996
+ function isSynthesizedMetadataResponse(response) {
6997
+ return synthesizedResponses.has(response);
6998
+ }
6999
+ var OIDC_WELL_KNOWN = "/.well-known/openid-configuration";
7000
+ var RFC8414_WELL_KNOWN = "/.well-known/oauth-authorization-server";
7001
+ function continuesDiscovery(status) {
7002
+ return status < 500 || status === 502;
7003
+ }
7004
+ function authorizationServerForFinalCandidate(url) {
7005
+ let parsed;
7006
+ try {
7007
+ parsed = new URL(url);
7008
+ } catch {
7009
+ return void 0;
7010
+ }
7011
+ const { pathname } = parsed;
7012
+ if (pathname === OIDC_WELL_KNOWN) return parsed.origin;
7013
+ if (!pathname.endsWith(OIDC_WELL_KNOWN)) return void 0;
7014
+ if (pathname.startsWith(`${RFC8414_WELL_KNOWN}/`) || pathname.startsWith(`${OIDC_WELL_KNOWN}/`)) {
7015
+ return void 0;
7016
+ }
7017
+ return `${parsed.origin}${pathname.slice(0, -OIDC_WELL_KNOWN.length)}`;
7018
+ }
7019
+ function synthesizeAuthorizationServerMetadata(issuer, overrides) {
7020
+ return {
7021
+ issuer,
7022
+ authorization_endpoint: overrides.authorizationUrl ?? new URL("/authorize", issuer).href,
7023
+ token_endpoint: overrides.tokenUrl ?? new URL("/token", issuer).href,
7024
+ registration_endpoint: new URL("/register", issuer).href,
7025
+ response_types_supported: ["code"],
7026
+ code_challenge_methods_supported: ["S256"]
7027
+ };
7028
+ }
7029
+ function requestMethodOf(input, init) {
7030
+ if (init?.method) return init.method.toUpperCase();
7031
+ if (typeof input !== "string" && !(input instanceof URL)) {
7032
+ return input.method.toUpperCase();
7033
+ }
7034
+ return "GET";
7035
+ }
7036
+ function acceptsJson(input, init) {
7037
+ let accept = null;
7038
+ if (init?.headers) {
7039
+ accept = new Headers(init.headers).get("accept");
7040
+ } else if (typeof input !== "string" && !(input instanceof URL)) {
7041
+ accept = input.headers.get("accept");
7042
+ }
7043
+ if (!accept) return false;
7044
+ const mediaTypes = accept.split(",").map((part) => part.split(";")[0].trim().toLowerCase());
7045
+ return mediaTypes.includes("application/json") && !mediaTypes.includes("text/event-stream");
7046
+ }
7047
+ function synthesizedMetadataResponse(input, init, response, overrides) {
7048
+ if (!continuesDiscovery(response.status)) return void 0;
7049
+ if (requestMethodOf(input, init) !== "GET") return void 0;
7050
+ if (!acceptsJson(input, init)) return void 0;
7051
+ const issuer = authorizationServerForFinalCandidate(requestUrlOf(input));
7052
+ if (!issuer) return void 0;
7053
+ void response.body?.cancel().catch(() => {
7054
+ });
7055
+ console.warn(
7056
+ `[oauth] ${issuer} publishes no authorization server metadata; using the configured endpoint override(s) in place of the SDK's /authorize and /token fallbacks.`
7057
+ );
7058
+ const synthesized = new Response(
7059
+ JSON.stringify(synthesizeAuthorizationServerMetadata(issuer, overrides)),
7060
+ {
7061
+ status: 200,
7062
+ statusText: "OK",
7063
+ headers: {
7064
+ "content-type": "application/json",
7065
+ [SYNTHESIZED_METADATA_HEADER]: "endpoint-overrides"
7066
+ }
7067
+ }
7068
+ );
7069
+ synthesizedResponses.add(synthesized);
7070
+ return synthesized;
7071
+ }
6926
7072
  function responseWithBody(response, body) {
6927
7073
  const headers = new Headers(response.headers);
6928
7074
  headers.delete("content-length");
@@ -6950,7 +7096,9 @@ function withOAuthEndpointOverrides(fetchFn, getOverrides) {
6950
7096
  const overrides = resolveOverrides();
6951
7097
  if (!overrides) return response;
6952
7098
  if (!isAuthorizationServerMetadataRequest(input)) return response;
6953
- if (!response.ok) return response;
7099
+ if (!response.ok) {
7100
+ return synthesizedMetadataResponse(input, init, response, overrides) ?? response;
7101
+ }
6954
7102
  if (!isJsonDocumentResponse(response.headers.get("content-type")))
6955
7103
  return response;
6956
7104
  let parsed;
@@ -7126,8 +7274,8 @@ import {
7126
7274
  OAuthMetadataSchema,
7127
7275
  OpenIdProviderDiscoveryMetadataSchema
7128
7276
  } from "@modelcontextprotocol/core";
7129
- var RFC8414_WELL_KNOWN = "/.well-known/oauth-authorization-server";
7130
- var OIDC_WELL_KNOWN = "/.well-known/openid-configuration";
7277
+ var RFC8414_WELL_KNOWN2 = "/.well-known/oauth-authorization-server";
7278
+ var OIDC_WELL_KNOWN2 = "/.well-known/openid-configuration";
7131
7279
  var COMPAT_SOURCE_HEADER = "x-inspector-oauth-metadata-source";
7132
7280
  function oidcDiscoveryCandidates(rfc8414Url) {
7133
7281
  let url;
@@ -7136,16 +7284,16 @@ function oidcDiscoveryCandidates(rfc8414Url) {
7136
7284
  } catch {
7137
7285
  return [];
7138
7286
  }
7139
- if (!url.pathname.startsWith(RFC8414_WELL_KNOWN)) return [];
7140
- const path7 = url.pathname.slice(RFC8414_WELL_KNOWN.length);
7287
+ if (!url.pathname.startsWith(RFC8414_WELL_KNOWN2)) return [];
7288
+ const path7 = url.pathname.slice(RFC8414_WELL_KNOWN2.length);
7141
7289
  if (path7 !== "" && !path7.startsWith("/")) return [];
7142
- if (path7 === "") return [new URL(OIDC_WELL_KNOWN, url.origin).href];
7290
+ if (path7 === "") return [new URL(OIDC_WELL_KNOWN2, url.origin).href];
7143
7291
  return [
7144
- new URL(`${OIDC_WELL_KNOWN}${path7}`, url.origin).href,
7145
- new URL(`${path7}${OIDC_WELL_KNOWN}`, url.origin).href
7292
+ new URL(`${OIDC_WELL_KNOWN2}${path7}`, url.origin).href,
7293
+ new URL(`${path7}${OIDC_WELL_KNOWN2}`, url.origin).href
7146
7294
  ];
7147
7295
  }
7148
- function continuesDiscovery(status) {
7296
+ function continuesDiscovery2(status) {
7149
7297
  return status >= 400 && status < 500 || status === 502;
7150
7298
  }
7151
7299
  async function releaseBody(response) {
@@ -7171,7 +7319,7 @@ function discoveryHeaders(input, init) {
7171
7319
  }
7172
7320
  return new Headers();
7173
7321
  }
7174
- function requestMethodOf(input, init) {
7322
+ function requestMethodOf2(input, init) {
7175
7323
  if (init?.method) return init.method.toUpperCase();
7176
7324
  if (typeof input !== "string" && !(input instanceof URL)) {
7177
7325
  return input.method.toUpperCase();
@@ -7182,8 +7330,8 @@ function withRfc8414OidcCompat(fetchFn) {
7182
7330
  return async (input, init) => {
7183
7331
  const response = await fetchFn(input, init);
7184
7332
  if (response.ok) return response;
7185
- if (!continuesDiscovery(response.status)) return response;
7186
- if (requestMethodOf(input, init) !== "GET") return response;
7333
+ if (!continuesDiscovery2(response.status)) return response;
7334
+ if (requestMethodOf2(input, init) !== "GET") return response;
7187
7335
  const candidates = oidcDiscoveryCandidates(requestUrlOf3(input));
7188
7336
  if (candidates.length === 0) return response;
7189
7337
  const headers = discoveryHeaders(input, init);
@@ -7213,7 +7361,7 @@ function withRfc8414OidcCompat(fetchFn) {
7213
7361
  releaseBodyDetached(response);
7214
7362
  throw callerSignal.reason;
7215
7363
  }
7216
- if (continuesDiscovery(probe.status)) continue;
7364
+ if (continuesDiscovery2(probe.status)) continue;
7217
7365
  return response;
7218
7366
  }
7219
7367
  let parsed;
@@ -7233,6 +7381,16 @@ function withRfc8414OidcCompat(fetchFn) {
7233
7381
  }
7234
7382
  const source = probe.url || candidate;
7235
7383
  releaseBodyDetached(response);
7384
+ if (isSynthesizedMetadataResponse(probe)) {
7385
+ return new Response(body, {
7386
+ status: 200,
7387
+ statusText: "OK",
7388
+ headers: {
7389
+ "content-type": "application/json",
7390
+ [SYNTHESIZED_METADATA_HEADER]: "endpoint-overrides"
7391
+ }
7392
+ });
7393
+ }
7236
7394
  console.warn(
7237
7395
  `[oauth] ${source} returned RFC 8414 OAuth 2.0 authorization server metadata, not an OpenID provider document. The MCP TypeScript SDK validates that path as OpenID Connect Discovery and would reject it (modelcontextprotocol/typescript-sdk#2733), so the Inspector is handing it to discovery as the RFC 8414 document it is.`
7238
7396
  );
@@ -7274,12 +7432,33 @@ function createSilentLogger() {
7274
7432
  var silentLogger = createSilentLogger();
7275
7433
 
7276
7434
  // ../../core/mcp/connectionDiagnostics.ts
7277
- import { SdkError as SdkError4, SdkErrorCode as SdkErrorCode4 } from "@modelcontextprotocol/client";
7435
+ import { SdkError as SdkError4, SdkErrorCode as SdkErrorCode5 } from "@modelcontextprotocol/client";
7278
7436
  function isRequestTimeoutData(value) {
7279
7437
  return typeof value === "object" && value !== null && typeof value.timeout === "number";
7280
7438
  }
7281
7439
  function isRequestTimeoutError(err) {
7282
- return SdkError4.isInstance(err) && err.code === SdkErrorCode4.RequestTimeout && isRequestTimeoutData(err.data);
7440
+ return SdkError4.isInstance(err) && err.code === SdkErrorCode5.RequestTimeout && isRequestTimeoutData(err.data);
7441
+ }
7442
+ function describeSendFailure(err) {
7443
+ const message = err instanceof Error ? err.message : String(err);
7444
+ const status = httpStatusOf(err);
7445
+ if (status === void 0 || message.includes(`HTTP ${status}`)) {
7446
+ return message;
7447
+ }
7448
+ const statusText = httpStatusTextOf(err);
7449
+ return `HTTP ${status}${statusText ? ` ${statusText}` : ""}: ${message}`;
7450
+ }
7451
+ function httpStatusOf(err) {
7452
+ if (typeof err !== "object" || err === null || !("status" in err)) {
7453
+ return void 0;
7454
+ }
7455
+ return typeof err.status === "number" ? err.status : void 0;
7456
+ }
7457
+ function httpStatusTextOf(err) {
7458
+ if (typeof err !== "object" || err === null || !("statusText" in err)) {
7459
+ return void 0;
7460
+ }
7461
+ return typeof err.statusText === "string" ? err.statusText : void 0;
7283
7462
  }
7284
7463
  function formatDuration(ms) {
7285
7464
  const clamped = Math.max(0, ms);
@@ -7341,11 +7520,108 @@ function annotateRequestTimeout(err, method, diagnostics) {
7341
7520
  diagnostics
7342
7521
  };
7343
7522
  const message = `Request timed out after ${formatDuration(err.data.timeout)} (${method}). ${describeConnectionDiagnostics(diagnostics)}`;
7344
- const annotated = new SdkError4(SdkErrorCode4.RequestTimeout, message, data);
7523
+ const annotated = new SdkError4(SdkErrorCode5.RequestTimeout, message, data);
7345
7524
  annotated.cause = err;
7346
7525
  return annotated;
7347
7526
  }
7348
7527
 
7528
+ // ../../core/mcp/rateLimit.ts
7529
+ import {
7530
+ SdkHttpError as SdkHttpError2
7531
+ } from "@modelcontextprotocol/client";
7532
+ var HTTP_TOO_MANY_REQUESTS = 429;
7533
+ function requestIdsOf(body) {
7534
+ if (body === void 0) return [];
7535
+ let parsed;
7536
+ try {
7537
+ parsed = JSON.parse(body);
7538
+ } catch {
7539
+ return [];
7540
+ }
7541
+ const messages = Array.isArray(parsed) ? parsed : [parsed];
7542
+ const ids = [];
7543
+ for (const message of messages) {
7544
+ if (typeof message !== "object" || message === null) continue;
7545
+ if (!("method" in message) || !("id" in message)) continue;
7546
+ const { id } = message;
7547
+ if (typeof id === "string" || typeof id === "number") ids.push(id);
7548
+ }
7549
+ return ids;
7550
+ }
7551
+ var MAX_UNCLAIMED_RATE_LIMITS = 100;
7552
+ var RateLimitLedger = class {
7553
+ // `undefined` is a real value here: a 429 that carried no header.
7554
+ byRequestId = /* @__PURE__ */ new Map();
7555
+ /** File a tracked fetch, if it was a 429. Anything else is ignored. */
7556
+ record(entry) {
7557
+ if (entry.responseStatus !== HTTP_TOO_MANY_REQUESTS) return;
7558
+ const retryAfter = findHeader(entry.responseHeaders, "retry-after");
7559
+ for (const id of requestIdsOf(entry.requestBody)) {
7560
+ this.byRequestId.delete(id);
7561
+ this.byRequestId.set(id, retryAfter);
7562
+ if (this.byRequestId.size > MAX_UNCLAIMED_RATE_LIMITS) {
7563
+ const [oldest] = this.byRequestId.keys();
7564
+ if (oldest !== void 0) this.byRequestId.delete(oldest);
7565
+ }
7566
+ }
7567
+ }
7568
+ /** The `Retry-After` filed for `id`, removed as it is read. */
7569
+ take(id) {
7570
+ const retryAfter = this.byRequestId.get(id);
7571
+ this.byRequestId.delete(id);
7572
+ return retryAfter;
7573
+ }
7574
+ clear() {
7575
+ this.byRequestId.clear();
7576
+ }
7577
+ };
7578
+ var IMF_FIXDATE = /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/;
7579
+ function parseRetryAfter(value, now) {
7580
+ if (value === void 0) return void 0;
7581
+ const trimmed = value.trim();
7582
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
7583
+ if (!IMF_FIXDATE.test(trimmed)) return void 0;
7584
+ const at = Date.parse(trimmed);
7585
+ if (Number.isNaN(at) || new Date(at).toUTCString() !== trimmed) {
7586
+ return void 0;
7587
+ }
7588
+ return Math.max(0, at - now);
7589
+ }
7590
+ function isRateLimitError(err) {
7591
+ return SdkHttpError2.isInstance(err) && err.status === HTTP_TOO_MANY_REQUESTS;
7592
+ }
7593
+ function retryAfterOn(err) {
7594
+ const value = err.data.retryAfter;
7595
+ return typeof value === "string" ? value : void 0;
7596
+ }
7597
+ function attachRetryAfter(err, retryAfter) {
7598
+ if (!isRateLimitError(err) || retryAfter === void 0) return;
7599
+ if (retryAfterOn(err) !== void 0) return;
7600
+ err.data.retryAfter = retryAfter;
7601
+ }
7602
+ function describeRetry(retryAfter, retryAfterMs) {
7603
+ if (retryAfterMs !== void 0) {
7604
+ return `The server asked to retry after ${formatDuration(retryAfterMs)}.`;
7605
+ }
7606
+ if (retryAfter !== void 0) {
7607
+ return `The server sent an unparseable Retry-After (${retryAfter}).`;
7608
+ }
7609
+ return "The server sent no Retry-After header.";
7610
+ }
7611
+ function annotateRateLimit(err, method, now = Date.now()) {
7612
+ if (!isRateLimitError(err)) return err;
7613
+ const header = retryAfterOn(err);
7614
+ const retryAfterMs = parseRetryAfter(header, now);
7615
+ const data = {
7616
+ ...err.data,
7617
+ method,
7618
+ ...header !== void 0 && { retryAfter: header },
7619
+ ...retryAfterMs !== void 0 && { retryAfterMs }
7620
+ };
7621
+ const message = `Rate limited by the server (HTTP 429) on ${method}. ${describeRetry(header, retryAfterMs)} ${err.message}`;
7622
+ return new SdkHttpError2(err.code, message, data, { cause: err });
7623
+ }
7624
+
7349
7625
  // ../../core/auth/authorizationParams.ts
7350
7626
  var RESERVED_AUTHORIZATION_PARAMS = [
7351
7627
  "client_id",
@@ -7463,6 +7739,13 @@ var BaseOAuthClientProvider = class {
7463
7739
  requestRefreshToken;
7464
7740
  /** Server-configured scope, for the #2068 `offline_access` distinction. */
7465
7741
  configuredScope;
7742
+ /** Per-flow verifier store (#2726); absent → the shared storage slot. */
7743
+ flowStore;
7744
+ /**
7745
+ * The authId of the flow this provider is driving: set by {@link state} on the
7746
+ * redirect leg and by {@link resumeAuthorizationFlow} on the callback leg.
7747
+ */
7748
+ flowId;
7466
7749
  constructor(serverUrl, oauthConfig) {
7467
7750
  this.serverUrl = serverUrl;
7468
7751
  this.storage = oauthConfig.storage;
@@ -7472,6 +7755,7 @@ var BaseOAuthClientProvider = class {
7472
7755
  this.authorizationParams = oauthConfig.authorizationParams;
7473
7756
  this.requestRefreshToken = oauthConfig.requestRefreshToken ?? true;
7474
7757
  this.configuredScope = oauthConfig.configuredScope;
7758
+ this.flowStore = oauthConfig.flowStore;
7475
7759
  }
7476
7760
  /**
7477
7761
  * Load persisted scope into {@link cachedScope} before SDK `auth()` (which
@@ -7542,7 +7826,24 @@ var BaseOAuthClientProvider = class {
7542
7826
  return metadata;
7543
7827
  }
7544
7828
  state() {
7545
- return generateOAuthState();
7829
+ const state = generateOAuthState();
7830
+ this.flowId = state;
7831
+ return state;
7832
+ }
7833
+ /**
7834
+ * Bind this provider to the flow a callback's `state` names, so
7835
+ * {@link codeVerifier} reads that flow's verifier and no other (#2726). A
7836
+ * `state` that is not one of ours binds nothing — and, with a flow store, a
7837
+ * flow-less read then fails rather than reaching for the shared slot.
7838
+ */
7839
+ resumeAuthorizationFlow(state) {
7840
+ this.flowId = parseOAuthState(state)?.authId;
7841
+ }
7842
+ /** Drop the bound flow's verifier once its exchange has completed. */
7843
+ async clearAuthorizationFlow() {
7844
+ if (this.flowStore && this.flowId) {
7845
+ await this.flowStore.clearCodeVerifier(this.flowId);
7846
+ }
7546
7847
  }
7547
7848
  async clientInformation(ctx) {
7548
7849
  const preregistered = await this.storage.getClientInformation(
@@ -7687,9 +7988,26 @@ var BaseOAuthClientProvider = class {
7687
7988
  }
7688
7989
  }
7689
7990
  async saveCodeVerifier(codeVerifier) {
7991
+ if (this.flowStore && this.flowId) {
7992
+ await this.flowStore.saveCodeVerifier(
7993
+ this.flowId,
7994
+ this.serverUrl,
7995
+ codeVerifier
7996
+ );
7997
+ return;
7998
+ }
7690
7999
  await this.storage.saveCodeVerifier(this.serverUrl, codeVerifier);
7691
8000
  }
7692
8001
  async codeVerifier() {
8002
+ if (this.flowStore) {
8003
+ const verifier2 = this.flowId ? await this.flowStore.getCodeVerifier(this.flowId, this.serverUrl) : void 0;
8004
+ if (!verifier2) {
8005
+ throw new Error(
8006
+ "No code verifier saved for this authorization flow. It may have been started in another browser tab or already completed; please re-authenticate."
8007
+ );
8008
+ }
8009
+ return verifier2;
8010
+ }
7693
8011
  const verifier = await this.storage.getCodeVerifier(this.serverUrl);
7694
8012
  if (!verifier) {
7695
8013
  throw new Error("No code verifier saved for session");
@@ -7736,6 +8054,10 @@ var BaseOAuthClientProvider = class {
7736
8054
  await this.storage.clearTokens(this.serverUrl);
7737
8055
  return;
7738
8056
  case "verifier":
8057
+ if (this.flowStore) {
8058
+ await this.clearAuthorizationFlow();
8059
+ return;
8060
+ }
7739
8061
  await this.storage.clearCodeVerifier(this.serverUrl);
7740
8062
  return;
7741
8063
  case "discovery":
@@ -8213,10 +8535,10 @@ async function resolveIdpMetadata(issuer, storage, fetchFn) {
8213
8535
  }
8214
8536
  return discoverIdpMetadata(issuer, fetchFn);
8215
8537
  }
8216
- var OIDC_WELL_KNOWN2 = "/.well-known/openid-configuration";
8538
+ var OIDC_WELL_KNOWN3 = "/.well-known/openid-configuration";
8217
8539
  async function fetchOpenIdConfiguration(issuer, issuerUrl, fetchFn) {
8218
8540
  const url = new URL(
8219
- `${issuerUrl.pathname.replace(/\/$/, "")}${OIDC_WELL_KNOWN2}`,
8541
+ `${issuerUrl.pathname.replace(/\/$/, "")}${OIDC_WELL_KNOWN3}`,
8220
8542
  issuerUrl.origin
8221
8543
  );
8222
8544
  try {
@@ -8606,11 +8928,6 @@ async function mintEmaResourceTokens(config, resourceContext) {
8606
8928
  "EMA requires resource authorization server clientId (per-server oauth.clientId)"
8607
8929
  );
8608
8930
  }
8609
- if (!config.resourceClientSecret?.trim()) {
8610
- throw new Error(
8611
- "EMA requires resource authorization server client secret in server OAuth settings (Test Client secret from xaa.dev resource registration)"
8612
- );
8613
- }
8614
8931
  const idToken = await getValidIdToken({
8615
8932
  idp: config.idp,
8616
8933
  storage: config.storage,
@@ -9117,7 +9434,9 @@ var OAuthManager = class {
9117
9434
  requestRefreshToken: this.oauthConfig.requestRefreshToken,
9118
9435
  // #2068: lets the provider tell an `offline_access` the user asked for
9119
9436
  // from one inherited from a previous grant's persisted scope.
9120
- configuredScope: this.oauthConfig.scope
9437
+ configuredScope: this.oauthConfig.scope,
9438
+ // #2726: in-flight PKCE verifiers keyed by flow, not by server URL.
9439
+ flowStore: this.oauthConfig.flowStore
9121
9440
  });
9122
9441
  provider.setEventTarget(this.params.getEventTarget());
9123
9442
  await provider.prepareForAuth();
@@ -9281,7 +9600,12 @@ var OAuthManager = class {
9281
9600
  recordAuthorizationRequestScope(authorizationUrl, requestedScope) {
9282
9601
  this.pendingAuthorizationScope = authorizationUrl.searchParams.get("scope")?.trim() || requestedScope;
9283
9602
  }
9284
- async completeOAuthFlow(authorizationCode, iss) {
9603
+ /**
9604
+ * `state` is the OAuth `state` the callback was redirected back with. It
9605
+ * selects the flow whose PKCE verifier the exchange uses (#2726); without it
9606
+ * a provider with a flow store has no verifier to send.
9607
+ */
9608
+ async completeOAuthFlow(authorizationCode, iss, state) {
9285
9609
  try {
9286
9610
  if (this.isEnterpriseManaged()) {
9287
9611
  const emaConfig = await this.getEmaFlowConfig();
@@ -9305,6 +9629,9 @@ var OAuthManager = class {
9305
9629
  }
9306
9630
  const provider = await this.createOAuthProvider();
9307
9631
  const serverUrl = this.getServerUrl();
9632
+ if (state !== void 0) {
9633
+ provider.resumeAuthorizationFlow(state);
9634
+ }
9308
9635
  const result = await mcpAuth(provider, {
9309
9636
  serverUrl,
9310
9637
  authorizationCode,
@@ -9316,6 +9643,7 @@ var OAuthManager = class {
9316
9643
  `Expected AUTHORIZED after providing authorization code, got: ${result}`
9317
9644
  );
9318
9645
  }
9646
+ await provider.clearAuthorizationFlow();
9319
9647
  const tokens = await provider.tokens();
9320
9648
  if (!tokens) {
9321
9649
  throw new Error("Failed to retrieve tokens after authorization");
@@ -9447,15 +9775,21 @@ var OAuthManager = class {
9447
9775
  * Re-read persisted OAuth state and determine whether `challenge` is already
9448
9776
  * satisfied without an authorization-server round-trip.
9449
9777
  *
9450
- * Returns `true` for `insufficient_scope` when stored + token scope cover the
9451
- * SEP-2350 union. For `token_expired`, returns `true` only when storage holds
9452
- * a *provably* unexpired token — a JWT whose `exp` is still in the future. An
9453
- * opaque token carries no local expiry evidence, so the resource server's
9454
- * verdict stands and re-authorization proceeds. `invalid_token` and
9455
- * `unauthorized` always return `false` — the resource server explicitly
9456
- * rejected the credential.
9778
+ * For `token_expired` and `insufficient_scope`, returns `true` only when
9779
+ * storage holds a *provably* unexpired token — a JWT whose `exp` is still in
9780
+ * the future. An opaque token carries no local expiry evidence, so the
9781
+ * resource server's verdict stands and re-authorization proceeds.
9782
+ * `insufficient_scope` additionally requires stored + token scope to cover
9783
+ * the SEP-2350 union: some resource servers answer an *expired* token with
9784
+ * `insufficient_scope`, and a scope-only check would replay that dead token
9785
+ * instead of refreshing it (#2724, the same gap #2051 closed for
9786
+ * `token_expired`). `invalid_token` and `unauthorized` always return
9787
+ * `false` — the resource server explicitly rejected the credential.
9788
+ *
9789
+ * A caller that has just completed a grant passes `scopeOnly` — see
9790
+ * {@link CheckAuthChallengeSatisfiedOptions}.
9457
9791
  */
9458
- async checkAuthChallengeSatisfied(challenge) {
9792
+ async checkAuthChallengeSatisfied(challenge, options) {
9459
9793
  const storage = this.oauthConfig.storage;
9460
9794
  if (!storage) {
9461
9795
  return false;
@@ -9468,6 +9802,14 @@ var OAuthManager = class {
9468
9802
  if (challenge.reason !== "insufficient_scope") {
9469
9803
  return challenge.reason === "token_expired" && isAccessTokenProvablyUnexpired(tokens);
9470
9804
  }
9805
+ if (!options?.scopeOnly && !isAccessTokenProvablyUnexpired(tokens)) {
9806
+ return false;
9807
+ }
9808
+ return this.isStepUpScopeCovered(challenge, storage, tokens);
9809
+ }
9810
+ /** Stored + token scope cover the challenge's SEP-2350 union. */
9811
+ async isStepUpScopeCovered(challenge, storage, tokens) {
9812
+ const serverUrl = this.getServerUrl();
9471
9813
  const enriched = await this.enrichChallengeWithAuthorizationScopes(
9472
9814
  challenge,
9473
9815
  tokens.scope
@@ -9571,14 +9913,14 @@ var OAuthManager = class {
9571
9913
  }
9572
9914
  async handleEnterpriseManagedAuthChallenge(challenge, options) {
9573
9915
  const enriched = await this.enrichChallengeWithAuthorizationScopes(challenge);
9574
- if (enriched.reason === "insufficient_scope" && !options?.confirmedStepUp) {
9916
+ if (enriched.reason === "insufficient_scope" && !options?.confirmedStepUp && !await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
9575
9917
  return { kind: "step_up_confirm", challenge: enriched };
9576
9918
  }
9577
9919
  const config = await this.emaFlowConfigForChallenge(enriched);
9578
9920
  if (enriched.reason === "insufficient_scope") {
9579
9921
  const silent = await trySilentEmaAuth(config);
9580
9922
  if (silent.status === "success") {
9581
- if (await this.checkAuthChallengeSatisfied(enriched)) {
9923
+ if (await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
9582
9924
  const minted = await this.requireStorage().getTokens(
9583
9925
  this.getServerUrl()
9584
9926
  );
@@ -9656,9 +9998,16 @@ var OAuthManager = class {
9656
9998
  fetchFn: this.params.effectiveAuthFetch,
9657
9999
  resourceMetadataUrl,
9658
10000
  ...enriched.reason === "insufficient_scope" && {
10001
+ // Judge coverage the way checkAuthChallengeSatisfied does: a token
10002
+ // response that omitted `scope` granted what was stored, so a
10003
+ // covered challenge on an expired token refreshes rather than
10004
+ // forcing interactive authorization (#2724).
9659
10005
  forceReauthorization: isStrictScopeSuperset(
9660
10006
  scopeForAuth,
9661
- tokens?.scope
10007
+ resolveEffectiveGrantedScope(
10008
+ await this.oauthConfig.storage?.getScope(serverUrl),
10009
+ tokens?.scope
10010
+ )
9662
10011
  )
9663
10012
  }
9664
10013
  });
@@ -9667,7 +10016,7 @@ var OAuthManager = class {
9667
10016
  }
9668
10017
  if (result === "AUTHORIZED") {
9669
10018
  if (enriched.reason === "insufficient_scope") {
9670
- if (await this.checkAuthChallengeSatisfied(enriched)) {
10019
+ if (await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
9671
10020
  const freshTokens = await provider.tokens();
9672
10021
  const scopeToPersist = resolvePersistedScopeAfterGrant(
9673
10022
  freshTokens?.scope,
@@ -9769,7 +10118,7 @@ var OAuthManager = class {
9769
10118
  if (result !== "AUTHORIZED") {
9770
10119
  return null;
9771
10120
  }
9772
- if (await this.checkAuthChallengeSatisfied(enriched)) {
10121
+ if (await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
9773
10122
  const freshTokens = await provider.tokens();
9774
10123
  const scopeToPersist = resolvePersistedScopeAfterGrant(
9775
10124
  freshTokens?.scope,
@@ -9799,7 +10148,11 @@ var OAuthManager = class {
9799
10148
  };
9800
10149
 
9801
10150
  // ../../core/mcp/remote/remoteClientTransport.ts
9802
- import { SdkError as SdkError5, SdkErrorCode as SdkErrorCode5 } from "@modelcontextprotocol/client";
10151
+ import {
10152
+ SdkError as SdkError5,
10153
+ SdkErrorCode as SdkErrorCode6,
10154
+ SdkHttpError as SdkHttpError3
10155
+ } from "@modelcontextprotocol/client";
9803
10156
 
9804
10157
  // ../../core/mcp/remote/types.ts
9805
10158
  function oauthTokensToRemoteAuthState(tokens) {
@@ -9854,6 +10207,18 @@ function isConnectAuthChallenge(json) {
9854
10207
  function isConnectTransportError(json) {
9855
10208
  return typeof json === "object" && json !== null && "ok" in json && json.ok === false && "kind" in json && json.kind === "transport_error";
9856
10209
  }
10210
+ var SDK_ERROR_CODES = new Set(
10211
+ Object.values(SdkErrorCode6)
10212
+ );
10213
+ function toSdkHttpError(message, http) {
10214
+ const code = SDK_ERROR_CODES.has(http.code) ? http.code : SdkErrorCode6.ClientHttpNotImplemented;
10215
+ return new SdkHttpError3(code, message, {
10216
+ status: http.status,
10217
+ ...http.statusText !== void 0 && { statusText: http.statusText },
10218
+ ...http.text !== void 0 && { text: http.text },
10219
+ ...http.retryAfter !== void 0 && { retryAfter: http.retryAfter }
10220
+ });
10221
+ }
9857
10222
  function legacySessionId(json) {
9858
10223
  if (typeof json === "object" && json !== null && "sessionId" in json && typeof json.sessionId === "string" && !("ok" in json)) {
9859
10224
  return json.sessionId;
@@ -10285,7 +10650,7 @@ var RemoteClientTransport = class {
10285
10650
  timer = setTimeout(() => {
10286
10651
  this.sseResponseWaits.delete(requestId);
10287
10652
  reject(
10288
- new SdkError5(SdkErrorCode5.RequestTimeout, "Request timed out", {
10653
+ new SdkError5(SdkErrorCode6.RequestTimeout, "Request timed out", {
10289
10654
  timeout: this.sseResponseTimeoutMs
10290
10655
  })
10291
10656
  );
@@ -10354,6 +10719,9 @@ var RemoteClientTransport = class {
10354
10719
  });
10355
10720
  return;
10356
10721
  }
10722
+ if (json.http) {
10723
+ throw toSdkHttpError(json.error, json.http);
10724
+ }
10357
10725
  throw new Error(json.error);
10358
10726
  }
10359
10727
  async requestSend(message, options) {
@@ -10426,8 +10794,9 @@ var corePackageJson = {
10426
10794
  version: "0.0.0"
10427
10795
  };
10428
10796
  var MAX_URL_ELICITATION_RETRIES = 5;
10429
- function connectionTimeoutMessage(timeoutMs) {
10430
- return `Connection timed out after ${timeoutMs} ms. To accommodate a slower server, you may increase the timeout value in Server Settings.`;
10797
+ var WEB_CONNECTION_TIMEOUT_REMEDY = "To accommodate a slower server, you may increase the timeout value in Server Settings.";
10798
+ function connectionTimeoutMessage(timeoutMs, remedy = WEB_CONNECTION_TIMEOUT_REMEDY) {
10799
+ return `Connection timed out after ${timeoutMs} ms. ${remedy}`;
10431
10800
  }
10432
10801
  function createPendingAbortError() {
10433
10802
  return new Error("Pending request aborted");
@@ -10493,7 +10862,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10493
10862
  transport: () => this.transport,
10494
10863
  defaultTimeoutMs: () => this.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC,
10495
10864
  resetTimeoutOnProgress: () => this.resetTimeoutOnProgress,
10496
- annotateTimeout: (error, method) => annotateRequestTimeout(error, method, this.getConnectionDiagnostics())
10865
+ annotateError: (error, method) => this.annotateRequestError(error, method)
10497
10866
  });
10498
10867
  dispatchTaskRequest = (request, options) => this.rawWire.dispatch(request, options);
10499
10868
  // Every outbound request still awaiting a response, keyed by JSON-RPC id.
@@ -10513,11 +10882,16 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10513
10882
  // `GET` on Streamable HTTP). Both per-session, reset with the maps above.
10514
10883
  lastResponse;
10515
10884
  notificationStream;
10885
+ // Each tracked `429`'s `Retry-After`, by the JSON-RPC id that drew it, until
10886
+ // that id's failed send claims it (#2442). Per-session, reset with the
10887
+ // diagnostics above.
10888
+ rateLimits = new RateLimitLedger();
10516
10889
  /** True when the cached transport was built with an OAuth authProvider attached. */
10517
10890
  transportHasAuthProvider = false;
10518
10891
  /** Dedupes concurrent ambient auth challenges (reason + scopes). */
10519
10892
  ambientAuthChallengeInFlight = /* @__PURE__ */ new Map();
10520
10893
  pipeStderr;
10894
+ connectionTimeoutRemedy;
10521
10895
  initialLoggingLevel;
10522
10896
  // Modern-era per-request log level (#1629). On 2026-07-28 servers
10523
10897
  // `logging/setLevel` is gone; the client opts into logs per request via the
@@ -10656,6 +11030,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10656
11030
  // site to the two failure handlers. Cleared by any user-initiated refresh (a
10657
11031
  // subscribe/unsubscribe is a fresh attempt, and the server may have changed).
10658
11032
  modernNeverAcknowledged = false;
11033
+ // Set when the last `listen()` was refused at the HTTP layer with a status a
11034
+ // retry cannot change (#2722) — the sibling of `modernNeverAcknowledged`,
11035
+ // carried and cleared the same way, holding the HTTP answer the UI reports.
11036
+ modernListenRefusal = null;
10659
11037
  /** Which in-flight task calls own each progress token (#2316). */
10660
11038
  taskProgress = new TaskProgressRouter();
10661
11039
  // Abort controller for the in-flight ordinary (non-task) tool call. Aborting
@@ -10755,6 +11133,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10755
11133
  this.fetchFn = options.environment.fetch;
10756
11134
  this.logger = options.environment.logger ?? silentLogger;
10757
11135
  this.pipeStderr = options.pipeStderr ?? false;
11136
+ this.connectionTimeoutRemedy = options.connectionTimeoutRemedy ?? WEB_CONNECTION_TIMEOUT_REMEDY;
10758
11137
  this.initialLoggingLevel = options.initialLoggingLevel;
10759
11138
  this.sample = options.sample ?? true;
10760
11139
  this.elicit = options.elicit ?? true;
@@ -10912,7 +11291,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10912
11291
  this.clientInfo,
10913
11292
  Object.keys(clientOptions).length > 0 ? clientOptions : void 0
10914
11293
  );
10915
- this.annotateSdkRequestTimeouts(this.client);
11294
+ this.annotateSdkRequestErrors(this.client);
10916
11295
  if (this.tasksCapabilityAdvertised) {
10917
11296
  this.bindReceiverTasks();
10918
11297
  }
@@ -10920,7 +11299,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10920
11299
  /**
10921
11300
  * Decorate the SDK Client's `request` so a per-request timeout rejects with
10922
11301
  * the connection's state at that moment instead of a bare `Request timed
10923
- * out` (#2318).
11302
+ * out` (#2318), and a `429` rejects as rate limiting with the server's
11303
+ * `Retry-After` instead of a bare `Error POSTing to endpoint` (#2442).
10924
11304
  *
10925
11305
  * `Protocol.request` is the one funnel every SDK verb goes through —
10926
11306
  * `listTools`, `callTool`, `readResource`, the handshake's `initialize`,
@@ -10938,21 +11318,30 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10938
11318
  * implementation signature serves both at runtime; the single cast on the
10939
11319
  * way back is what the overloads cost.
10940
11320
  */
10941
- annotateSdkRequestTimeouts(client) {
11321
+ annotateSdkRequestErrors(client) {
10942
11322
  const sdkRequest = client.request.bind(client);
10943
11323
  const decorated = async (...args) => {
10944
11324
  try {
10945
11325
  return await sdkRequest(...args);
10946
11326
  } catch (err) {
10947
- throw annotateRequestTimeout(
10948
- err,
10949
- args[0].method,
10950
- this.getConnectionDiagnostics()
10951
- );
11327
+ throw this.annotateRequestError(err, args[0].method);
10952
11328
  }
10953
11329
  };
10954
11330
  client.request = decorated;
10955
11331
  }
11332
+ /**
11333
+ * The one place a failed request's error is decorated, shared by the SDK
11334
+ * funnel above and the raw-wire channel that bypasses it (modern-era tool
11335
+ * calls and task operations), so a caller sees the same error whichever
11336
+ * path its request took: a timeout with the connection's state (#2318), a
11337
+ * `429` named as rate limiting (#2442), anything else untouched.
11338
+ */
11339
+ annotateRequestError(err, method) {
11340
+ return annotateRateLimit(
11341
+ annotateRequestTimeout(err, method, this.getConnectionDiagnostics()),
11342
+ method
11343
+ );
11344
+ }
10956
11345
  /**
10957
11346
  * @param base - the composed OAuth-path fetch, deadline innermost (#2319).
10958
11347
  *
@@ -10996,9 +11385,14 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10996
11385
  },
10997
11386
  trackSendFailure: (message, error) => {
10998
11387
  if (isRequestTimeoutError(error)) return;
11388
+ attachRetryAfter(error, this.rateLimits.take(message.id));
10999
11389
  if (this.outstandingRequests.delete(message.id)) {
11000
11390
  this.dispatchConnectionDiagnosticsChange();
11001
11391
  }
11392
+ this.dispatchTypedEvent("requestFailed", {
11393
+ id: message.id,
11394
+ reason: describeSendFailure(error)
11395
+ });
11002
11396
  },
11003
11397
  trackResponse: (message, origin) => {
11004
11398
  const responseId = message.id;
@@ -11064,6 +11458,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11064
11458
  };
11065
11459
  baseTransport.onerror = (error) => {
11066
11460
  if (this.status === "connecting") return;
11461
+ if (subscriptionListenRefusal(error) !== void 0) return;
11067
11462
  this.status = "error";
11068
11463
  this.dispatchTypedEvent("statusChange", this.status);
11069
11464
  this.dispatchTypedEvent("error", error);
@@ -11309,6 +11704,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11309
11704
  this.clearModernReconnectTimer();
11310
11705
  this.modernReconnectAttempts = 0;
11311
11706
  this.modernNeverAcknowledged = false;
11707
+ this.modernListenRefusal = null;
11312
11708
  this.modernSubscription = null;
11313
11709
  this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
11314
11710
  this.dispatchSubscriptionsChange();
@@ -11347,6 +11743,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11347
11743
  this.taskProgress.clear();
11348
11744
  this.lastResponse = void 0;
11349
11745
  this.notificationStream = void 0;
11746
+ this.rateLimits.clear();
11350
11747
  this.dispatchConnectionDiagnosticsChange();
11351
11748
  if (this.malformedListItems.size > 0) {
11352
11749
  this.malformedListItems.clear();
@@ -11449,6 +11846,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11449
11846
  onFetchRequest: (entry) => {
11450
11847
  this.dispatchFetchRequest({ ...entry, category: "transport" });
11451
11848
  this.noteTransportStream(entry);
11849
+ this.rateLimits.record(entry);
11452
11850
  },
11453
11851
  onFetchResponseBody: (id, body) => {
11454
11852
  this.dispatchFetchRequestBodyUpdate(id, body);
@@ -11496,9 +11894,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11496
11894
  // for that upstream gap and should be deleted with it.
11497
11895
  //
11498
11896
  // Known, accepted side effect of turning intercept on with no stored
11499
- // tokens: `parseAuthChallengeFromResponse` treats 403 as a challenge
11500
- // too, so a probe answered 403 for a *non-auth* reason (a gateway
11501
- // rejecting the unknown `server/discover` method, say) now starts OAuth
11897
+ // tokens: `parseAuthChallengeFromResponse` treats a 403 carrying
11898
+ // `WWW-Authenticate` as a challenge too, so a probe answered that way
11899
+ // for a *non-auth* reason (a gateway rejecting the unknown
11900
+ // `server/discover` method, say) now starts OAuth
11502
11901
  // discovery instead of letting "auto" fall back to the legacy
11503
11902
  // `initialize`. The outcome is a surfaced `oauthError`, not a hang, and
11504
11903
  // it goes away with this clause.
@@ -11565,7 +11964,14 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11565
11964
  const timeoutPromise = new Promise((_, reject) => {
11566
11965
  timer = setTimeout(() => {
11567
11966
  connectTimedOut = true;
11568
- reject(new Error(connectionTimeoutMessage(connectTimeoutMs)));
11967
+ reject(
11968
+ new Error(
11969
+ connectionTimeoutMessage(
11970
+ connectTimeoutMs,
11971
+ this.connectionTimeoutRemedy
11972
+ )
11973
+ )
11974
+ );
11569
11975
  }, connectTimeoutMs);
11570
11976
  });
11571
11977
  try {
@@ -13780,11 +14186,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13780
14186
  // for page one.
13781
14187
  ...cursor !== void 0 ? { cursor } : {}
13782
14188
  };
14189
+ const resultSchema = this.isModernEra() ? ModernDirectoryReadResultSchema : DirectoryReadResultSchema;
13783
14190
  try {
13784
14191
  return await this.invokeMcpClient(
13785
14192
  () => this.client.request(
13786
14193
  { method: RESOURCES_DIRECTORY_READ_METHOD, params },
13787
- DirectoryReadResultSchema,
14194
+ resultSchema,
13788
14195
  this.getRequestOptions(this.progressTokenOf(metadata))
13789
14196
  ),
13790
14197
  { method: RESOURCES_DIRECTORY_READ_METHOD }
@@ -14239,6 +14646,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
14239
14646
  this.clearModernReconnectTimer();
14240
14647
  this.modernReconnectAttempts = 0;
14241
14648
  this.modernNeverAcknowledged = false;
14649
+ this.modernListenRefusal = null;
14242
14650
  }
14243
14651
  const generation = ++this.modernListenGeneration;
14244
14652
  const previous = this.modernSubscription;
@@ -14257,8 +14665,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
14257
14665
  this.getRequestOptions()
14258
14666
  );
14259
14667
  } catch (error) {
14260
- if (generation === this.modernListenGeneration && isNeverAcknowledgedSubscriptionClose(error)) {
14261
- this.modernNeverAcknowledged = true;
14668
+ if (generation === this.modernListenGeneration) {
14669
+ if (isNeverAcknowledgedSubscriptionClose(error)) {
14670
+ this.modernNeverAcknowledged = true;
14671
+ }
14672
+ this.modernListenRefusal = subscriptionListenRefusal(error) ?? null;
14262
14673
  }
14263
14674
  throw error;
14264
14675
  }
@@ -14339,6 +14750,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
14339
14750
  * its error either way — the retry is about the subscriptions, not the call.
14340
14751
  */
14341
14752
  reconcileModernStreamStateAfterFailedRefresh() {
14753
+ if (this.modernListenRefusal) {
14754
+ this.endModernStreamRefused(this.modernListenRefusal);
14755
+ return;
14756
+ }
14342
14757
  if (!this.wantsModernStream()) {
14343
14758
  this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
14344
14759
  return;
@@ -14371,6 +14786,23 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
14371
14786
  honoredUris: []
14372
14787
  });
14373
14788
  }
14789
+ /**
14790
+ * Settle the stream on an HTTP refusal of the listen (#2722): the
14791
+ * `"refused"` status carrying the HTTP answer, no reconnect, and a log line
14792
+ * with the sentence the UI shows. Reported whatever `active` says — see
14793
+ * `ResourceSubscriptionStreamState`.
14794
+ */
14795
+ endModernStreamRefused(refusal) {
14796
+ this.clearModernReconnectTimer();
14797
+ const message = subscriptionListenRefusalMessage(refusal);
14798
+ this.logger.warn({ message }, "subscriptions/listen refused over HTTP");
14799
+ this.setModernStreamState({
14800
+ active: this.modernStreamActive(),
14801
+ status: "refused",
14802
+ honoredUris: [],
14803
+ refusal
14804
+ });
14805
+ }
14374
14806
  /**
14375
14807
  * Schedule a reconnect re-listen after the current backoff delay (#1630).
14376
14808
  * `modernReconnectAttempts` reflects the number of *consecutive failed*
@@ -14408,6 +14840,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
14408
14840
  this.endModernStreamNeverAcknowledged();
14409
14841
  return;
14410
14842
  }
14843
+ if (this.modernListenRefusal && !isTerminalStatus(this.status)) {
14844
+ this.endModernStreamRefused(this.modernListenRefusal);
14845
+ return;
14846
+ }
14411
14847
  this.modernReconnectAttempts += 1;
14412
14848
  if (this.modernReconnectAttempts > MODERN_RECONNECT_MAX_ATTEMPTS || isTerminalStatus(this.status) || !this.wantsModernStream()) {
14413
14849
  this.setModernStreamState({
@@ -14562,8 +14998,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
14562
14998
  * Re-read OAuth storage and test whether a challenge is already satisfied.
14563
14999
  * See {@link OAuthManager.checkAuthChallengeSatisfied}.
14564
15000
  */
14565
- async checkAuthChallengeSatisfied(challenge) {
14566
- return this.ensureOAuthManager().checkAuthChallengeSatisfied(challenge);
15001
+ async checkAuthChallengeSatisfied(challenge, options) {
15002
+ return this.ensureOAuthManager().checkAuthChallengeSatisfied(
15003
+ challenge,
15004
+ options
15005
+ );
14567
15006
  }
14568
15007
  /**
14569
15008
  * Push recovered OAuth auth state to the remote backend (same MCP session).
@@ -14727,8 +15166,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
14727
15166
  * Direct transports reconnect after token exchange so the live MCP session
14728
15167
  * picks up the new Bearer token (mirrors silent recovery reconnect).
14729
15168
  */
14730
- async completeOAuthFlow(authorizationCode, iss) {
14731
- await this.ensureOAuthManager().completeOAuthFlow(authorizationCode, iss);
15169
+ async completeOAuthFlow(authorizationCode, iss, state) {
15170
+ await this.ensureOAuthManager().completeOAuthFlow(
15171
+ authorizationCode,
15172
+ iss,
15173
+ state
15174
+ );
14732
15175
  if (this.usesDirectAuthRecovery()) {
14733
15176
  await this.reconnectAfterAuthRecovery();
14734
15177
  }
@@ -14752,7 +15195,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
14752
15195
  * Finish OAuth after a full-page redirect and reconnect (or reattach) the MCP session.
14753
15196
  */
14754
15197
  async resumeAfterOAuth(authorizationCode, options) {
14755
- await this.completeOAuthFlow(authorizationCode, options?.iss);
15198
+ await this.completeOAuthFlow(
15199
+ authorizationCode,
15200
+ options?.iss,
15201
+ options?.state
15202
+ );
14756
15203
  const remoteSessionId = options?.remoteSessionId;
14757
15204
  const transport = this.baseTransport;
14758
15205
  if (remoteSessionId && transport instanceof RemoteClientTransport) {
@@ -14843,6 +15290,11 @@ var ManagedListState = class extends TypedEventTarget {
14843
15290
  // rendered by the list panel instead of vanishing into an unhandled
14844
15291
  // rejection — which read as "this server has no tools" (#1953).
14845
15292
  error = null;
15293
+ // Whether a fetch has committed a list this session. Until it has, the empty
15294
+ // `items` mean "not loaded yet", not "the server has none" — a distinction a
15295
+ // count of the list cannot make from `items` alone (#2607). Set and cleared
15296
+ // just before the `*Change` dispatch, so a reader keyed on that event sees it.
15297
+ loaded = false;
14846
15298
  config;
14847
15299
  // Debounce a burst of `list_changed` notifications into a single
14848
15300
  // refresh (or one indicator light) once it settles.
@@ -14878,6 +15330,7 @@ var ManagedListState = class extends TypedEventTarget {
14878
15330
  this.listChangedTimer = null;
14879
15331
  }
14880
15332
  this.items = [];
15333
+ this.loaded = false;
14881
15334
  this.dispatchChange();
14882
15335
  this.setListChanged(false);
14883
15336
  this.setError(null);
@@ -14936,6 +15389,14 @@ var ManagedListState = class extends TypedEventTarget {
14936
15389
  getItems() {
14937
15390
  return [...this.items];
14938
15391
  }
15392
+ /**
15393
+ * Whether a fetch has committed a list since the last connect. False while
15394
+ * the connect-time load is in flight, when it was deferred (paginated mode),
15395
+ * and after a disconnect. Changes only alongside the `*Change` event (#2607).
15396
+ */
15397
+ isLoaded() {
15398
+ return this.loaded;
15399
+ }
14939
15400
  /** Whether a `list_changed` arrived since the last refresh (indicator on). */
14940
15401
  getListChanged() {
14941
15402
  return this.listChanged;
@@ -15029,6 +15490,7 @@ var ManagedListState = class extends TypedEventTarget {
15029
15490
  /** Commit a fetched list as the current one and notify subscribers. */
15030
15491
  applyItems(items) {
15031
15492
  this.items = items;
15493
+ this.loaded = true;
15032
15494
  this.dispatchChange();
15033
15495
  }
15034
15496
  dispatchChange() {
@@ -15039,6 +15501,7 @@ var ManagedListState = class extends TypedEventTarget {
15039
15501
  this.unsubscribe?.();
15040
15502
  this.unsubscribe = null;
15041
15503
  this.items = [];
15504
+ this.loaded = false;
15042
15505
  }
15043
15506
  };
15044
15507
 
@@ -15122,6 +15585,16 @@ var MessageLogState = class extends TypedEventTarget {
15122
15585
  return;
15123
15586
  }
15124
15587
  };
15588
+ const onRequestFailed = (event) => {
15589
+ const { id, reason } = event.detail;
15590
+ const requestEntry = this.pendingRequestEntries.get(id);
15591
+ if (!requestEntry) return;
15592
+ this.pendingRequestEntries.delete(id);
15593
+ requestEntry.clientError = reason;
15594
+ requestEntry.duration = Date.now() - requestEntry.timestamp.getTime();
15595
+ this.dispatchTypedEvent("message", requestEntry);
15596
+ this.dispatchTypedEvent("messagesChange", this.getMessages());
15597
+ };
15125
15598
  const onStatusChange = () => {
15126
15599
  if (isTerminalStatus(this.client?.getStatus())) {
15127
15600
  this.messages = [];
@@ -15131,11 +15604,13 @@ var MessageLogState = class extends TypedEventTarget {
15131
15604
  };
15132
15605
  this.client.addEventListener("message", onMessage);
15133
15606
  this.client.addEventListener("responseRejected", onResponseRejected);
15607
+ this.client.addEventListener("requestFailed", onRequestFailed);
15134
15608
  this.client.addEventListener("statusChange", onStatusChange);
15135
15609
  this.unsubscribe = () => {
15136
15610
  if (this.client) {
15137
15611
  this.client.removeEventListener("message", onMessage);
15138
15612
  this.client.removeEventListener("responseRejected", onResponseRejected);
15613
+ this.client.removeEventListener("requestFailed", onRequestFailed);
15139
15614
  this.client.removeEventListener("statusChange", onStatusChange);
15140
15615
  }
15141
15616
  this.client = null;
@@ -16087,6 +16562,7 @@ function useListError(state) {
16087
16562
  var NO_TOOLS = [];
16088
16563
  var readTools = (state) => state.getTools();
16089
16564
  var readListChanged = (state) => state.getListChanged();
16565
+ var readLoaded = (state) => state.isLoaded();
16090
16566
  function useManagedTools(client, managedToolsState) {
16091
16567
  const tools = useStoreSnapshot(
16092
16568
  managedToolsState,
@@ -16100,6 +16576,12 @@ function useManagedTools(client, managedToolsState) {
16100
16576
  readListChanged,
16101
16577
  false
16102
16578
  );
16579
+ const loaded = useStoreSnapshot(
16580
+ managedToolsState,
16581
+ "toolsChange",
16582
+ readLoaded,
16583
+ false
16584
+ );
16103
16585
  const error = useListError(managedToolsState);
16104
16586
  const refresh = useCallback4(async () => {
16105
16587
  if (!managedToolsState || !client) return NO_TOOLS;
@@ -16109,7 +16591,7 @@ function useManagedTools(client, managedToolsState) {
16109
16591
  const clearListChanged = useCallback4(() => {
16110
16592
  managedToolsState?.clearListChanged();
16111
16593
  }, [managedToolsState]);
16112
- return { tools, error, listChanged, refresh, clearListChanged };
16594
+ return { tools, error, listChanged, loaded, refresh, clearListChanged };
16113
16595
  }
16114
16596
 
16115
16597
  // ../../core/react/useManagedResources.ts
@@ -16117,6 +16599,7 @@ import { useCallback as useCallback5 } from "react";
16117
16599
  var NO_RESOURCES = [];
16118
16600
  var readResources = (state) => state.getResources();
16119
16601
  var readListChanged2 = (state) => state.getListChanged();
16602
+ var readLoaded2 = (state) => state.isLoaded();
16120
16603
  function useManagedResources(client, managedResourcesState) {
16121
16604
  const resources = useStoreSnapshot(
16122
16605
  managedResourcesState,
@@ -16130,6 +16613,12 @@ function useManagedResources(client, managedResourcesState) {
16130
16613
  readListChanged2,
16131
16614
  false
16132
16615
  );
16616
+ const loaded = useStoreSnapshot(
16617
+ managedResourcesState,
16618
+ "resourcesChange",
16619
+ readLoaded2,
16620
+ false
16621
+ );
16133
16622
  const error = useListError(managedResourcesState);
16134
16623
  const refresh = useCallback5(async () => {
16135
16624
  if (!managedResourcesState || !client) return NO_RESOURCES;
@@ -16139,7 +16628,7 @@ function useManagedResources(client, managedResourcesState) {
16139
16628
  const clearListChanged = useCallback5(() => {
16140
16629
  managedResourcesState?.clearListChanged();
16141
16630
  }, [managedResourcesState]);
16142
- return { resources, error, listChanged, refresh, clearListChanged };
16631
+ return { resources, error, listChanged, loaded, refresh, clearListChanged };
16143
16632
  }
16144
16633
 
16145
16634
  // ../../core/react/useManagedResourceTemplates.ts
@@ -16168,6 +16657,7 @@ import { useCallback as useCallback7 } from "react";
16168
16657
  var NO_PROMPTS = [];
16169
16658
  var readPrompts = (state) => state.getPrompts();
16170
16659
  var readListChanged3 = (state) => state.getListChanged();
16660
+ var readLoaded3 = (state) => state.isLoaded();
16171
16661
  function useManagedPrompts(client, managedPromptsState) {
16172
16662
  const prompts = useStoreSnapshot(
16173
16663
  managedPromptsState,
@@ -16181,6 +16671,12 @@ function useManagedPrompts(client, managedPromptsState) {
16181
16671
  readListChanged3,
16182
16672
  false
16183
16673
  );
16674
+ const loaded = useStoreSnapshot(
16675
+ managedPromptsState,
16676
+ "promptsChange",
16677
+ readLoaded3,
16678
+ false
16679
+ );
16184
16680
  const error = useListError(managedPromptsState);
16185
16681
  const refresh = useCallback7(async () => {
16186
16682
  if (!managedPromptsState || !client) return NO_PROMPTS;
@@ -16190,7 +16686,7 @@ function useManagedPrompts(client, managedPromptsState) {
16190
16686
  const clearListChanged = useCallback7(() => {
16191
16687
  managedPromptsState?.clearListChanged();
16192
16688
  }, [managedPromptsState]);
16193
- return { prompts, error, listChanged, refresh, clearListChanged };
16689
+ return { prompts, error, listChanged, loaded, refresh, clearListChanged };
16194
16690
  }
16195
16691
 
16196
16692
  // ../../core/react/useManagedSkills.ts
@@ -16367,34 +16863,6 @@ function classifyAuthorizationServerMismatch(err) {
16367
16863
  };
16368
16864
  }
16369
16865
 
16370
- // ../../core/node/version.ts
16371
- import { readFileSync as readFileSync4 } from "fs";
16372
- import { dirname as dirname5, join as join3, parse } from "path";
16373
- import { fileURLToPath } from "url";
16374
- var ROOT_PACKAGE_NAME = "@modelcontextprotocol/inspector";
16375
- function readInspectorVersion(callerUrl) {
16376
- const startDir = dirname5(fileURLToPath(callerUrl));
16377
- const { root } = parse(startDir);
16378
- let dir = startDir;
16379
- for (; ; ) {
16380
- let manifest;
16381
- try {
16382
- manifest = JSON.parse(
16383
- readFileSync4(join3(dir, "package.json"), "utf-8")
16384
- );
16385
- } catch {
16386
- }
16387
- if (manifest?.name === ROOT_PACKAGE_NAME && manifest.version) {
16388
- return manifest.version;
16389
- }
16390
- if (dir === root) break;
16391
- dir = dirname5(dir);
16392
- }
16393
- throw new Error(
16394
- `Could not locate the ${ROOT_PACKAGE_NAME} root package.json above ${startDir}`
16395
- );
16396
- }
16397
-
16398
16866
  // ../../core/auth/oauth-storage.ts
16399
16867
  import {
16400
16868
  OAuthClientInformationSchema as OAuthClientInformationSchema2,
@@ -16910,7 +17378,7 @@ function serializeOAuthPersistBlob(snapshot) {
16910
17378
  import { resolve as resolve6 } from "path";
16911
17379
 
16912
17380
  // ../../core/auth/node/oauth-secrets.ts
16913
- import { randomUUID as randomUUID2 } from "crypto";
17381
+ import { createHmac, randomUUID as randomUUID2 } from "crypto";
16914
17382
  import { OAuthTokensSchema as OAuthTokensSchema5 } from "@modelcontextprotocol/core";
16915
17383
  var PERSIST_TOKENS_ENV = "MCP_INSPECTOR_PERSIST_TOKENS";
16916
17384
  var warnedPolicyValues = /* @__PURE__ */ new Set();
@@ -16943,6 +17411,29 @@ var LEGACY_REGISTRATION_TOKEN_FIELD = "registration-token";
16943
17411
  var PREREG_CLIENT_SECRET_FIELD = "prereg-client-secret";
16944
17412
  var PREREG_REGISTRATION_TOKEN_FIELD = "prereg-registration-token";
16945
17413
  var IDP_SESSION_FIELD = "idp-session";
17414
+ var clientCredentialStampField = (field) => `${field}#owner`;
17415
+ var credentialMac = (clientId, value) => createHmac("sha256", value).update(clientId).digest("hex");
17416
+ function clientCredentialStamp(clientId, value) {
17417
+ return JSON.stringify({
17418
+ client_id: clientId,
17419
+ mac: credentialMac(clientId, value)
17420
+ });
17421
+ }
17422
+ function stampedOwner(stamp, value) {
17423
+ if (stamp === void 0) return void 0;
17424
+ let parsed;
17425
+ try {
17426
+ parsed = JSON.parse(stamp);
17427
+ } catch {
17428
+ return void 0;
17429
+ }
17430
+ if (typeof parsed !== "object" || parsed === null) return void 0;
17431
+ const { client_id: clientId, mac } = parsed;
17432
+ if (typeof clientId !== "string" || mac !== credentialMac(clientId, value)) {
17433
+ return void 0;
17434
+ }
17435
+ return clientId;
17436
+ }
16946
17437
  function applyTokensPolicy(tokens, policy) {
16947
17438
  if (policy === "none") return void 0;
16948
17439
  if (policy === "access" && tokens.refresh_token !== void 0) {
@@ -16971,12 +17462,18 @@ function splitClientInformation(info, fields, secrets) {
16971
17462
  if (client_secret === void 0 && registration_access_token === void 0) {
16972
17463
  return info;
16973
17464
  }
16974
- if (client_secret !== void 0) {
16975
- secrets[fields.clientSecret] = client_secret;
16976
- }
16977
- if (registration_access_token !== void 0) {
16978
- secrets[fields.registrationToken] = registration_access_token;
16979
- }
17465
+ const put = (field, value) => {
17466
+ if (value === void 0) return;
17467
+ secrets[field] = value;
17468
+ if (fields.stamped && typeof info.client_id === "string") {
17469
+ secrets[clientCredentialStampField(field)] = clientCredentialStamp(
17470
+ info.client_id,
17471
+ value
17472
+ );
17473
+ }
17474
+ };
17475
+ put(fields.clientSecret, client_secret);
17476
+ put(fields.registrationToken, registration_access_token);
16980
17477
  return publicInfo;
16981
17478
  }
16982
17479
  function joinClientInformation(info, fields, secrets) {
@@ -16995,15 +17492,18 @@ function joinClientInformation(info, fields, secrets) {
16995
17492
  }
16996
17493
  var issuerClientInfoFields = (issuer) => ({
16997
17494
  clientSecret: issuerClientSecretField(issuer),
16998
- registrationToken: issuerRegistrationTokenField(issuer)
17495
+ registrationToken: issuerRegistrationTokenField(issuer),
17496
+ stamped: true
16999
17497
  });
17000
17498
  var LEGACY_CLIENT_INFO_FIELDS = {
17001
17499
  clientSecret: LEGACY_CLIENT_SECRET_FIELD,
17002
- registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD
17500
+ registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD,
17501
+ stamped: true
17003
17502
  };
17004
17503
  var PREREG_CLIENT_INFO_FIELDS = {
17005
17504
  clientSecret: PREREG_CLIENT_SECRET_FIELD,
17006
- registrationToken: PREREG_REGISTRATION_TOKEN_FIELD
17505
+ registrationToken: PREREG_REGISTRATION_TOKEN_FIELD,
17506
+ stamped: false
17007
17507
  };
17008
17508
  function splitServerOAuthState(state, policy) {
17009
17509
  const secrets = {};
@@ -17153,20 +17653,54 @@ function joinIdpSession(residue, secrets) {
17153
17653
  return stored ? { ...residue, ...stored } : { ...residue };
17154
17654
  }
17155
17655
  function serverSecretFields(state) {
17656
+ const stamped = (field) => [
17657
+ field,
17658
+ clientCredentialStampField(field)
17659
+ ];
17156
17660
  const fields = /* @__PURE__ */ new Set([
17157
17661
  LEGACY_TOKENS_FIELD,
17158
- LEGACY_CLIENT_SECRET_FIELD,
17159
- LEGACY_REGISTRATION_TOKEN_FIELD,
17662
+ ...stamped(LEGACY_CLIENT_SECRET_FIELD),
17663
+ ...stamped(LEGACY_REGISTRATION_TOKEN_FIELD),
17160
17664
  PREREG_CLIENT_SECRET_FIELD,
17161
17665
  PREREG_REGISTRATION_TOKEN_FIELD
17162
17666
  ]);
17163
17667
  for (const issuer of Object.keys(state?.byIssuer ?? {})) {
17164
17668
  fields.add(issuerTokensField(issuer));
17165
- fields.add(issuerClientSecretField(issuer));
17166
- fields.add(issuerRegistrationTokenField(issuer));
17669
+ for (const field of stamped(issuerClientSecretField(issuer))) {
17670
+ fields.add(field);
17671
+ }
17672
+ for (const field of stamped(issuerRegistrationTokenField(issuer))) {
17673
+ fields.add(field);
17674
+ }
17167
17675
  }
17168
17676
  return [...fields];
17169
17677
  }
17678
+ function clientCredentialOwners(state) {
17679
+ const owners = /* @__PURE__ */ new Map();
17680
+ const add2 = (info, fields) => {
17681
+ if (typeof info?.client_id !== "string") return;
17682
+ owners.set(fields.clientSecret, info.client_id);
17683
+ owners.set(fields.registrationToken, info.client_id);
17684
+ };
17685
+ add2(state.clientInformation, LEGACY_CLIENT_INFO_FIELDS);
17686
+ for (const [issuer, slot] of Object.entries(state.byIssuer ?? {})) {
17687
+ add2(slot.clientInformation, issuerClientInfoFields(issuer));
17688
+ }
17689
+ return owners;
17690
+ }
17691
+ function restampClientCredentials(state, secrets) {
17692
+ const out = { ...secrets };
17693
+ for (const [field, clientId] of clientCredentialOwners(state)) {
17694
+ const stampField = clientCredentialStampField(field);
17695
+ const value = Object.hasOwn(out, field) ? out[field] : void 0;
17696
+ if (value === void 0) {
17697
+ delete out[stampField];
17698
+ } else if (stampedOwner(out[stampField], value) === void 0) {
17699
+ setOwnEntry(out, stampField, clientCredentialStamp(clientId, value));
17700
+ }
17701
+ }
17702
+ return out;
17703
+ }
17170
17704
  function clientInfoHasPlaintext(info) {
17171
17705
  return info !== void 0 && (info.client_secret !== void 0 || info.registration_access_token !== void 0);
17172
17706
  }
@@ -17207,6 +17741,44 @@ function coveredLocations(location, byLocation) {
17207
17741
  ([recorded]) => recorded === location || location === KEYRING_LOCATION && recorded.startsWith(FILE_LOCATION_PREFIX)
17208
17742
  ).map(([recorded, keys]) => ({ location: recorded, keys }));
17209
17743
  }
17744
+ var emptyKeys = () => ({
17745
+ servers: /* @__PURE__ */ new Set(),
17746
+ idpSessions: /* @__PURE__ */ new Set(),
17747
+ owners: /* @__PURE__ */ new Map()
17748
+ });
17749
+ function candidateFields(location, byLocation) {
17750
+ const fields = /* @__PURE__ */ new Map();
17751
+ for (const { keys } of coveredLocations(location, byLocation)) {
17752
+ for (const [url, owned] of keys.owners) {
17753
+ const kept = fields.get(url) ?? /* @__PURE__ */ new Set();
17754
+ for (const field of owned.keys()) kept.add(field);
17755
+ fields.set(url, kept);
17756
+ }
17757
+ }
17758
+ return fields;
17759
+ }
17760
+ function addMissingOwners(target, source) {
17761
+ for (const [url, fields] of source) {
17762
+ const kept = target.get(url) ?? /* @__PURE__ */ new Map();
17763
+ for (const [field, clientId] of fields) {
17764
+ if (!kept.has(field)) kept.set(field, clientId);
17765
+ }
17766
+ if (kept.size > 0) target.set(url, kept);
17767
+ }
17768
+ }
17769
+ function mergeOwners(target, source) {
17770
+ let changed = false;
17771
+ for (const [url, fields] of source) {
17772
+ const kept = target.get(url) ?? /* @__PURE__ */ new Map();
17773
+ for (const [field, clientId] of fields) {
17774
+ if (kept.get(field) === clientId) continue;
17775
+ kept.set(field, clientId);
17776
+ changed = true;
17777
+ }
17778
+ if (kept.size > 0) target.set(url, kept);
17779
+ }
17780
+ return changed;
17781
+ }
17210
17782
  var warnedLedgerFailures = /* @__PURE__ */ new Set();
17211
17783
  function warnLedgerFailure(what, error) {
17212
17784
  const reason = error instanceof Error ? error.message : String(error);
@@ -17220,6 +17792,19 @@ function warnLedgerFailure(what, error) {
17220
17792
  function isRecord5(value) {
17221
17793
  return typeof value === "object" && value !== null && !Array.isArray(value);
17222
17794
  }
17795
+ function parseOwners(value) {
17796
+ const owners = /* @__PURE__ */ new Map();
17797
+ if (!isRecord5(value)) return owners;
17798
+ for (const [url, fields] of Object.entries(value)) {
17799
+ if (!isRecord5(fields)) continue;
17800
+ const byField = /* @__PURE__ */ new Map();
17801
+ for (const [field, clientId] of Object.entries(fields)) {
17802
+ if (typeof clientId === "string") byField.set(field, clientId);
17803
+ }
17804
+ if (byField.size > 0) owners.set(url, byField);
17805
+ }
17806
+ return owners;
17807
+ }
17223
17808
  function stringsOf(value) {
17224
17809
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
17225
17810
  }
@@ -17241,7 +17826,9 @@ function parseLedger(raw) {
17241
17826
  if (!isRecord5(keys)) continue;
17242
17827
  byLocation.set(location, {
17243
17828
  servers: new Set(stringsOf(keys.servers)),
17244
- idpSessions: new Set(stringsOf(keys.idpSessions))
17829
+ idpSessions: new Set(stringsOf(keys.idpSessions)),
17830
+ owners: parseOwners(keys.owners),
17831
+ ...isValidSecretsNamespace(keys.owedTo) && { owedTo: keys.owedTo }
17245
17832
  });
17246
17833
  }
17247
17834
  if (byLocation.size > 0) ledger.set(namespace, byLocation);
@@ -17255,7 +17842,19 @@ function serializeLedger(ledger) {
17255
17842
  Object.fromEntries(
17256
17843
  [...byLocation].map(([location, keys]) => [
17257
17844
  location,
17258
- { servers: [...keys.servers], idpSessions: [...keys.idpSessions] }
17845
+ {
17846
+ servers: [...keys.servers],
17847
+ idpSessions: [...keys.idpSessions],
17848
+ ...keys.owners.size > 0 && {
17849
+ owners: Object.fromEntries(
17850
+ [...keys.owners].map(([url, fields]) => [
17851
+ url,
17852
+ Object.fromEntries(fields)
17853
+ ])
17854
+ )
17855
+ },
17856
+ ...keys.owedTo !== void 0 && { owedTo: keys.owedTo }
17857
+ }
17259
17858
  ])
17260
17859
  )
17261
17860
  ])
@@ -17267,7 +17866,7 @@ async function writeLedger(stateFilePath, ledger) {
17267
17866
  if (ledger.size === 0) await deleteStoreFile(path7);
17268
17867
  else await writeStoreFile(path7, serializeLedger(ledger));
17269
17868
  }
17270
- async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions) {
17869
+ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions, owners = /* @__PURE__ */ new Map()) {
17271
17870
  try {
17272
17871
  const ledger = parseLedger(
17273
17872
  await readStoreFile(namespaceLedgerPath(stateFilePath))
@@ -17281,7 +17880,7 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
17281
17880
  let keys = byLocation.get(location);
17282
17881
  let changed = false;
17283
17882
  if (!keys) {
17284
- keys = { servers: /* @__PURE__ */ new Set(), idpSessions: /* @__PURE__ */ new Set() };
17883
+ keys = emptyKeys();
17285
17884
  byLocation.set(location, keys);
17286
17885
  changed = true;
17287
17886
  }
@@ -17295,12 +17894,105 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
17295
17894
  keys.idpSessions.add(issuer);
17296
17895
  changed = true;
17297
17896
  }
17897
+ if (mergeOwners(keys.owners, owners)) changed = true;
17298
17898
  if (changed) await writeLedger(stateFilePath, ledger);
17299
17899
  } catch (error) {
17300
17900
  warnLedgerFailure("update", error);
17301
17901
  }
17302
17902
  }
17303
- async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
17903
+ async function markUncarriedNamespaces(stateFilePath, secretStore, into) {
17904
+ const ledger = parseLedger(
17905
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
17906
+ );
17907
+ const location = await secretStoreLocation(secretStore);
17908
+ let changed = false;
17909
+ for (const [namespace, byLocation] of ledger) {
17910
+ if (namespace === into) continue;
17911
+ for (const [recorded, keys] of byLocation) {
17912
+ if (recorded === location || keys.owners.size === 0) continue;
17913
+ if (keys.owedTo === into) continue;
17914
+ keys.owedTo = into;
17915
+ changed = true;
17916
+ }
17917
+ }
17918
+ if (changed) await writeLedger(stateFilePath, ledger);
17919
+ }
17920
+ async function pendingCarries(stateFilePath, secretStore, current) {
17921
+ const ledger = parseLedger(
17922
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
17923
+ );
17924
+ const location = await secretStoreLocation(secretStore);
17925
+ const carries = [];
17926
+ for (const [namespace, byLocation] of ledger) {
17927
+ const owed = coveredLocations(location, byLocation).some(
17928
+ ({ keys }) => keys.owedTo === current
17929
+ );
17930
+ if (owed) {
17931
+ carries.push({
17932
+ namespace,
17933
+ fields: candidateFields(location, byLocation)
17934
+ });
17935
+ }
17936
+ }
17937
+ return carries.reverse();
17938
+ }
17939
+ async function carryOwed(stateFilePath, ledger, location, secretStore, namespace, current, recorded, now) {
17940
+ let carried = true;
17941
+ for (const [url, fields] of recorded) {
17942
+ for (const field of fields) {
17943
+ const clientId = now.get(url)?.get(field);
17944
+ if (clientId === void 0) continue;
17945
+ try {
17946
+ const from = oauthSecretServerId(url, namespace);
17947
+ const to = oauthSecretServerId(url, current);
17948
+ const stampField = clientCredentialStampField(field);
17949
+ const value = await secretStoreGetStrict(secretStore, from, field);
17950
+ if (value === null) continue;
17951
+ const stamp = await secretStoreGetStrict(secretStore, from, stampField);
17952
+ if (stamp === null || stampedOwner(stamp, value) !== clientId) {
17953
+ continue;
17954
+ }
17955
+ if (await secretStoreGetStrict(secretStore, to, field) !== null) {
17956
+ continue;
17957
+ }
17958
+ let byLocation = ledger.get(current);
17959
+ if (!byLocation) {
17960
+ byLocation = /* @__PURE__ */ new Map();
17961
+ ledger.set(current, byLocation);
17962
+ }
17963
+ const target = byLocation.get(location) ?? emptyKeys();
17964
+ byLocation.set(location, target);
17965
+ const isNew = !target.servers.has(url);
17966
+ target.servers.add(url);
17967
+ const owner = /* @__PURE__ */ new Map([[url, /* @__PURE__ */ new Map([[field, clientId]])]]);
17968
+ if (mergeOwners(target.owners, owner) || isNew) {
17969
+ await writeLedger(stateFilePath, ledger);
17970
+ }
17971
+ await secretStore.set(to, stampField, stamp);
17972
+ await secretStore.set(to, field, value);
17973
+ } catch (error) {
17974
+ carried = false;
17975
+ warnLedgerFailure(
17976
+ "carry over a superseded namespace recorded in",
17977
+ error
17978
+ );
17979
+ }
17980
+ }
17981
+ }
17982
+ return carried;
17983
+ }
17984
+ async function recordedNamespaces(stateFilePath) {
17985
+ try {
17986
+ const ledger = parseLedger(
17987
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
17988
+ );
17989
+ return [...ledger.keys()].reverse();
17990
+ } catch (error) {
17991
+ warnLedgerFailure("read", error);
17992
+ return [];
17993
+ }
17994
+ }
17995
+ async function purgeSupersededNamespaces(stateFilePath, secretStore, current, owners = /* @__PURE__ */ new Map()) {
17304
17996
  let ledger;
17305
17997
  let location;
17306
17998
  try {
@@ -17313,8 +18005,22 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
17313
18005
  return;
17314
18006
  }
17315
18007
  let changed = false;
17316
- for (const [namespace, byLocation] of ledger) {
18008
+ for (const [namespace, byLocation] of [...ledger].reverse()) {
17317
18009
  if (namespace === current) continue;
18010
+ if (current !== void 0 && coveredLocations(location, byLocation).some(
18011
+ ({ keys }) => keys.owedTo === current
18012
+ ) && !await carryOwed(
18013
+ stateFilePath,
18014
+ ledger,
18015
+ location,
18016
+ secretStore,
18017
+ namespace,
18018
+ current,
18019
+ candidateFields(location, byLocation),
18020
+ owners
18021
+ )) {
18022
+ break;
18023
+ }
17318
18024
  for (const { location: recorded, keys } of coveredLocations(
17319
18025
  location,
17320
18026
  byLocation
@@ -17339,12 +18045,11 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
17339
18045
  if (!purged || recorded !== location) continue;
17340
18046
  byLocation.delete(recorded);
17341
18047
  if (recorded.startsWith(FILE_LOCATION_PREFIX)) {
17342
- const keyring = byLocation.get(KEYRING_LOCATION) ?? {
17343
- servers: /* @__PURE__ */ new Set(),
17344
- idpSessions: /* @__PURE__ */ new Set()
17345
- };
18048
+ const keyring = byLocation.get(KEYRING_LOCATION) ?? emptyKeys();
18049
+ addMissingOwners(keyring.owners, keys.owners);
17346
18050
  for (const url of keys.servers) keyring.servers.add(url);
17347
18051
  for (const issuer of keys.idpSessions) keyring.idpSessions.add(issuer);
18052
+ keyring.owedTo ??= keys.owedTo;
17348
18053
  byLocation.set(KEYRING_LOCATION, keyring);
17349
18054
  }
17350
18055
  changed = true;
@@ -17455,7 +18160,12 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
17455
18160
  const existing = parseSecretsNamespace(raw);
17456
18161
  if (existing !== void 0) {
17457
18162
  if (locked) {
17458
- await purgeSupersededNamespaces(filePath, secretStore, existing);
18163
+ await purgeSupersededNamespaces(
18164
+ filePath,
18165
+ secretStore,
18166
+ existing,
18167
+ snapshotClientOwners(parseOAuthPersistBlob(raw))
18168
+ );
17459
18169
  }
17460
18170
  return existing;
17461
18171
  }
@@ -17463,42 +18173,53 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
17463
18173
  if (raw !== null && snapshot === null) {
17464
18174
  throw new OAuthStateFileUnrecognizedError(filePath, "save");
17465
18175
  }
17466
- if (locked) {
17467
- await purgeSupersededNamespaces(filePath, secretStore, void 0);
17468
- }
17469
18176
  const namespace = newSecretsNamespace();
17470
- if (snapshot === null) return namespace;
17471
- const moves = [
17472
- ...Object.entries(snapshot.servers).map(([url, state]) => ({
17473
- legacyId: oauthSecretServerId(url),
17474
- scopedId: oauthSecretServerId(url, namespace),
17475
- fields: serverSecretFields(state)
17476
- })),
17477
- ...Object.keys(snapshot.idpSessions).map((issuer) => ({
17478
- legacyId: oauthIdpSecretServerId(issuer),
17479
- scopedId: oauthIdpSecretServerId(issuer, namespace),
17480
- fields: [IDP_SESSION_FIELD]
17481
- }))
17482
- ];
17483
- if (moves.length === 0) return namespace;
18177
+ if (snapshot === null || Object.keys(snapshot.servers).length === 0 && Object.keys(snapshot.idpSessions).length === 0) {
18178
+ if (locked) {
18179
+ await purgeSupersededNamespaces(filePath, secretStore, void 0);
18180
+ }
18181
+ return namespace;
18182
+ }
17484
18183
  if (!locked) {
17485
18184
  throw new SecretStoreUnavailableError(
17486
18185
  `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).`
17487
18186
  );
17488
18187
  }
18188
+ const values = await readEntrySecrets(
18189
+ snapshot,
18190
+ secretStore,
18191
+ void 0,
18192
+ await recordedNamespaces(filePath)
18193
+ );
18194
+ const owners = snapshotClientOwners(snapshot);
17489
18195
  await recordNamespaceKeys(
17490
18196
  filePath,
17491
18197
  secretStore,
17492
18198
  namespace,
17493
18199
  Object.keys(snapshot.servers),
17494
- Object.keys(snapshot.idpSessions)
18200
+ Object.keys(snapshot.idpSessions),
18201
+ writtenClientOwners(snapshot, (url) => values.servers.get(url) ?? {})
17495
18202
  );
18203
+ await markUncarriedNamespaces(filePath, secretStore, namespace);
17496
18204
  const touched = [];
17497
18205
  try {
17498
- for (const { legacyId, scopedId, fields } of moves) {
17499
- for (const field of fields) {
17500
- const value = await secretStoreGetStrict(secretStore, legacyId, field);
17501
- if (value === null) continue;
18206
+ const copies = [
18207
+ // Restamped: a value read from the legacy ids predates stamps, and a
18208
+ // fallback's stamp may belong to a value read from elsewhere (#2734).
18209
+ ...[...values.servers].map(([url, secrets]) => ({
18210
+ scopedId: oauthSecretServerId(url, namespace),
18211
+ secrets: restampClientCredentials(
18212
+ getOwnEntry(snapshot.servers, url) ?? {},
18213
+ secrets
18214
+ )
18215
+ })),
18216
+ ...[...values.idpSessions].map(([issuer, secrets]) => ({
18217
+ scopedId: oauthIdpSecretServerId(issuer, namespace),
18218
+ secrets
18219
+ }))
18220
+ ];
18221
+ for (const { scopedId, secrets } of copies) {
18222
+ for (const [field, value] of Object.entries(secrets)) {
17502
18223
  touched.push({ serverId: scopedId, field, value: null });
17503
18224
  await secretStore.set(scopedId, field, value);
17504
18225
  }
@@ -17508,7 +18229,14 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
17508
18229
  await restoreSecretFields(secretStore, touched, warnRestoreFailure);
17509
18230
  throw error;
17510
18231
  }
17511
- for (const { legacyId } of moves) {
18232
+ await purgeSupersededNamespaces(filePath, secretStore, namespace, owners);
18233
+ const legacyIds = [
18234
+ ...Object.keys(snapshot.servers).map((url) => oauthSecretServerId(url)),
18235
+ ...Object.keys(snapshot.idpSessions).map(
18236
+ (issuer) => oauthIdpSecretServerId(issuer)
18237
+ )
18238
+ ];
18239
+ for (const legacyId of legacyIds) {
17512
18240
  try {
17513
18241
  await secretStore.deleteAllForServer(legacyId);
17514
18242
  } catch (error) {
@@ -17561,6 +18289,14 @@ function preserveNonDurableSecrets(diskSecrets, secrets) {
17561
18289
  return keep;
17562
18290
  }
17563
18291
  async function writeOAuthSections(filePath, snapshot, sections, secretStore = defaultSecretStore()) {
18292
+ await writeOAuthSectionsFrom(
18293
+ filePath,
18294
+ async () => snapshot,
18295
+ sections,
18296
+ secretStore
18297
+ );
18298
+ }
18299
+ async function writeOAuthSectionsFrom(filePath, resolveSnapshot, sections, secretStore) {
17564
18300
  const policy = getPersistTokensPolicy();
17565
18301
  const durable = await secretStoreIsDurable(secretStore);
17566
18302
  await withOAuthStateLock(filePath, "save", async (locked) => {
@@ -17644,6 +18380,7 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
17644
18380
  unconfirmed = null;
17645
18381
  namespace = diskNamespace;
17646
18382
  }
18383
+ const snapshot = await resolveSnapshot(disk, namespace);
17647
18384
  const effective = sections ? {
17648
18385
  servers: sections.servers && [...new Set(sections.servers)],
17649
18386
  idpSessions: sections.idpSessions && [
@@ -17669,7 +18406,22 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
17669
18406
  secretStore,
17670
18407
  namespace,
17671
18408
  Object.keys(merged.servers),
17672
- Object.keys(merged.idpSessions)
18409
+ Object.keys(merged.idpSessions),
18410
+ // Only the entries this save writes: another entry's secret in
18411
+ // this store was written for whatever client it was written for,
18412
+ // and relabeling it from the file would misattribute it (#2672).
18413
+ writtenClientOwners(
18414
+ {
18415
+ servers: Object.fromEntries(
18416
+ (effective.servers ?? []).flatMap((url) => {
18417
+ const entry = getOwnEntry(merged.servers, url);
18418
+ return entry === void 0 ? [] : [[url, entry]];
18419
+ })
18420
+ ),
18421
+ idpSessions: {}
18422
+ },
18423
+ splitSecretsOf
18424
+ )
17673
18425
  );
17674
18426
  for (const url of effective.servers ?? []) {
17675
18427
  const serverId = oauthSecretServerId(url, namespace);
@@ -17677,7 +18429,8 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
17677
18429
  const candidates = [
17678
18430
  .../* @__PURE__ */ new Set([
17679
18431
  ...serverSecretFields(getOwnEntry(disk?.servers, url)),
17680
- ...serverSecretFields(next)
18432
+ ...serverSecretFields(next),
18433
+ ...[...restoreBaseline.values()].filter((prior) => prior.serverId === serverId).map(({ field }) => field)
17681
18434
  ])
17682
18435
  ];
17683
18436
  const entryPrior = await snapshotSecretFields(
@@ -17686,8 +18439,12 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
17686
18439
  candidates
17687
18440
  );
17688
18441
  priorSecrets.push(...entryPrior);
18442
+ const entryPriorValue = new Map(
18443
+ entryPrior.map(({ field, value }) => [field, value])
18444
+ );
17689
18445
  if (next === void 0) {
17690
18446
  for (const field of candidates) {
18447
+ if (entryPriorValue.get(field) === null) continue;
17691
18448
  ourWrites.set(priorKey({ serverId, field }), null);
17692
18449
  }
17693
18450
  await secretStore.deleteAllForServer(serverId);
@@ -17702,10 +18459,9 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
17702
18459
  }
17703
18460
  const { residue, secrets } = splitServerOAuthState(next, policy);
17704
18461
  for (const field of candidates) {
17705
- ourWrites.set(
17706
- priorKey({ serverId, field }),
17707
- secrets[field] ?? null
17708
- );
18462
+ const value = secrets[field] ?? null;
18463
+ if (value === entryPriorValue.get(field)) continue;
18464
+ ourWrites.set(priorKey({ serverId, field }), value);
17709
18465
  }
17710
18466
  setOwnEntry(merged.servers, url, residue);
17711
18467
  if (!durable) {
@@ -17848,26 +18604,109 @@ function secretRequestsFor(snapshot, namespace) {
17848
18604
  }
17849
18605
  return requests;
17850
18606
  }
17851
- async function joinSnapshot(snapshot, secretStore, namespace) {
17852
- const requests = secretRequestsFor(snapshot, namespace);
17853
- const values = requests.length > 0 ? await secretStoreGetManyStrict(secretStore, requests) : {};
18607
+ function fillMissingSecrets(target, source) {
18608
+ for (const [field, value] of Object.entries(source ?? {})) {
18609
+ if (!Object.hasOwn(target, field)) setOwnEntry(target, field, value);
18610
+ }
18611
+ }
18612
+ function writtenClientOwners(snapshot, secretsOf) {
18613
+ const owners = /* @__PURE__ */ new Map();
18614
+ for (const [url, state] of Object.entries(snapshot.servers)) {
18615
+ const secrets = secretsOf(url, state);
18616
+ const fields = new Map(
18617
+ [...clientCredentialOwners(state)].filter(
18618
+ ([field]) => Object.hasOwn(secrets, field)
18619
+ )
18620
+ );
18621
+ if (fields.size > 0) owners.set(url, fields);
18622
+ }
18623
+ return owners;
18624
+ }
18625
+ var splitSecretsOf = (_url, state) => splitServerOAuthState(state, "all").secrets;
18626
+ function snapshotClientOwners(snapshot) {
18627
+ const owners = /* @__PURE__ */ new Map();
18628
+ for (const [url, state] of Object.entries(snapshot?.servers ?? {})) {
18629
+ const fields = clientCredentialOwners(state);
18630
+ if (fields.size > 0) owners.set(url, fields);
18631
+ }
18632
+ return owners;
18633
+ }
18634
+ async function readEntrySecrets(snapshot, secretStore, namespace, fallbacks, carries = []) {
18635
+ const servers = /* @__PURE__ */ new Map();
18636
+ const idpSessions = /* @__PURE__ */ new Map();
18637
+ for (const url of Object.keys(snapshot.servers)) servers.set(url, {});
18638
+ for (const issuer of Object.keys(snapshot.idpSessions)) {
18639
+ idpSessions.set(issuer, {});
18640
+ }
18641
+ for (const source of [namespace, ...fallbacks]) {
18642
+ const requests = secretRequestsFor(snapshot, source);
18643
+ if (requests.length === 0) break;
18644
+ const values = await secretStoreGetManyStrict(secretStore, requests);
18645
+ for (const [url, target] of servers) {
18646
+ fillMissingSecrets(target, values[oauthSecretServerId(url, source)]);
18647
+ }
18648
+ for (const [issuer, target] of idpSessions) {
18649
+ fillMissingSecrets(
18650
+ target,
18651
+ values[oauthIdpSecretServerId(issuer, source)]
18652
+ );
18653
+ }
18654
+ }
18655
+ const owners = snapshotClientOwners(snapshot);
18656
+ for (const { namespace: from, fields: recorded } of carries) {
18657
+ const requests = [];
18658
+ for (const [url, fields] of recorded) {
18659
+ const target = servers.get(url);
18660
+ if (!target) continue;
18661
+ const wanted = [...fields].filter(
18662
+ (field) => owners.get(url)?.has(field) === true && !Object.hasOwn(target, field)
18663
+ );
18664
+ if (wanted.length > 0) {
18665
+ requests.push({
18666
+ serverId: oauthSecretServerId(url, from),
18667
+ fields: wanted.flatMap((field) => [
18668
+ field,
18669
+ clientCredentialStampField(field)
18670
+ ])
18671
+ });
18672
+ }
18673
+ }
18674
+ if (requests.length === 0) continue;
18675
+ const values = await secretStoreGetManyStrict(secretStore, requests);
18676
+ for (const [url, target] of servers) {
18677
+ const found = values[oauthSecretServerId(url, from)] ?? {};
18678
+ for (const [field, clientId] of owners.get(url) ?? []) {
18679
+ const value = Object.hasOwn(found, field) ? found[field] : void 0;
18680
+ const stampField = clientCredentialStampField(field);
18681
+ if (value === void 0 || Object.hasOwn(target, field) || stampedOwner(found[stampField], value) !== clientId) {
18682
+ continue;
18683
+ }
18684
+ setOwnEntry(target, field, value);
18685
+ setOwnEntry(target, stampField, found[stampField]);
18686
+ }
18687
+ }
18688
+ }
18689
+ return { servers, idpSessions };
18690
+ }
18691
+ async function joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries = []) {
18692
+ const values = await readEntrySecrets(
18693
+ snapshot,
18694
+ secretStore,
18695
+ namespace,
18696
+ fallbacks,
18697
+ carries
18698
+ );
17854
18699
  return {
17855
18700
  servers: Object.fromEntries(
17856
18701
  Object.entries(snapshot.servers).map(([url, state]) => [
17857
18702
  url,
17858
- joinServerOAuthState(
17859
- state,
17860
- values[oauthSecretServerId(url, namespace)] ?? {}
17861
- )
18703
+ joinServerOAuthState(state, values.servers.get(url) ?? {})
17862
18704
  ])
17863
18705
  ),
17864
18706
  idpSessions: Object.fromEntries(
17865
18707
  Object.entries(snapshot.idpSessions).map(([issuer, session]) => [
17866
18708
  issuer,
17867
- joinIdpSession(
17868
- session,
17869
- values[oauthIdpSecretServerId(issuer, namespace)] ?? {}
17870
- )
18709
+ joinIdpSession(session, values.idpSessions.get(issuer) ?? {})
17871
18710
  ])
17872
18711
  )
17873
18712
  };
@@ -17883,7 +18722,8 @@ async function migratePlaintextSecrets(filePath, secretStore) {
17883
18722
  secretStore,
17884
18723
  namespace,
17885
18724
  Object.keys(fresh.servers),
17886
- Object.keys(fresh.idpSessions)
18725
+ Object.keys(fresh.idpSessions),
18726
+ writtenClientOwners(fresh, splitSecretsOf)
17887
18727
  );
17888
18728
  }
17889
18729
  const migrateEntrySecrets = async (serverId, secrets) => {
@@ -17934,7 +18774,10 @@ async function readOAuthStore(filePath, secretStore = defaultSecretStore()) {
17934
18774
  warnMigrationFailure(error);
17935
18775
  }
17936
18776
  }
17937
- return joinSnapshot(snapshot, secretStore, parseSecretsNamespace(raw));
18777
+ const namespace = parseSecretsNamespace(raw);
18778
+ const fallbacks = namespace === void 0 ? await recordedNamespaces(filePath) : [];
18779
+ const carries = namespace === void 0 ? [] : await pendingCarries(filePath, secretStore, namespace);
18780
+ return joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries);
17938
18781
  });
17939
18782
  }
17940
18783
  async function removeOAuthStore(filePath, secretStore = defaultSecretStore()) {
@@ -18244,7 +19087,11 @@ async function runRunnerInteractiveOAuth(options) {
18244
19087
  path: options.callbackListen.pathname,
18245
19088
  onCallback: async (params) => {
18246
19089
  try {
18247
- await options.client.completeOAuthFlow(params.code, params.iss);
19090
+ await options.client.completeOAuthFlow(
19091
+ params.code,
19092
+ params.iss,
19093
+ params.state
19094
+ );
18248
19095
  flowResolve();
18249
19096
  } catch (err) {
18250
19097
  flowReject(toRunnerOAuthError(err));
@@ -18296,7 +19143,10 @@ async function runRunnerInteractiveOAuth(options) {
18296
19143
  }
18297
19144
  if (options.authChallenge) {
18298
19145
  const satisfied = await racingSignals(
18299
- options.client.checkAuthChallengeSatisfied(options.authChallenge)
19146
+ // The token exchange just completed, so only scope is in question.
19147
+ options.client.checkAuthChallengeSatisfied(options.authChallenge, {
19148
+ scopeOnly: true
19149
+ })
18300
19150
  );
18301
19151
  if (!satisfied) {
18302
19152
  return {
@@ -18349,14 +19199,25 @@ function getTuiLogger() {
18349
19199
  // ../../core/node/openUrl.ts
18350
19200
  import open from "open";
18351
19201
  var OPEN_URL_TIMEOUT_MS = 5e3;
18352
- function launched(child) {
18353
- return new Promise((resolve9, reject) => {
18354
- child.on("error", reject);
18355
- child.once("spawn", () => resolve9());
19202
+ var OPENABLE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
19203
+ function toOpenableHref(href) {
19204
+ let parsed;
19205
+ try {
19206
+ parsed = new URL(href);
19207
+ } catch {
19208
+ throw new Error("refusing to open an invalid URL");
19209
+ }
19210
+ if (!OPENABLE_PROTOCOLS.has(parsed.protocol)) {
19211
+ throw new Error(`refusing to open a non-http(s) URL (${parsed.protocol})`);
19212
+ }
19213
+ return parsed.href;
19214
+ }
19215
+ function absorbLateErrors(child) {
19216
+ child.on("error", () => {
18356
19217
  });
18357
19218
  }
18358
19219
  async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
18359
- const href = typeof url === "string" ? url : url.href;
19220
+ const href = toOpenableHref(typeof url === "string" ? url : url.href);
18360
19221
  let timer;
18361
19222
  const timeout = new Promise((_, reject) => {
18362
19223
  timer = setTimeout(
@@ -18365,8 +19226,7 @@ async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
18365
19226
  );
18366
19227
  });
18367
19228
  try {
18368
- const opened = Promise.resolve().then(() => open(href)).then(launched);
18369
- await Promise.race([opened, timeout]);
19229
+ await Promise.race([open(href).then(absorbLateErrors), timeout]);
18370
19230
  } finally {
18371
19231
  clearTimeout(timer);
18372
19232
  }
@@ -19069,8 +19929,24 @@ function copyViaOsc52(text, sink) {
19069
19929
  large: payloadBytes > OSC52_LARGE_PAYLOAD_BYTES
19070
19930
  };
19071
19931
  }
19932
+ var copyDirs = /* @__PURE__ */ new Set();
19933
+ var exitCleanupRegistered = false;
19934
+ function removeCopyFiles() {
19935
+ for (const dir of copyDirs) {
19936
+ try {
19937
+ fs5.rmSync(dir, { recursive: true, force: true });
19938
+ } catch {
19939
+ }
19940
+ copyDirs.delete(dir);
19941
+ }
19942
+ }
19072
19943
  function writeCopyFile(text, baseDir = os.tmpdir()) {
19073
19944
  const dir = fs5.mkdtempSync(path6.join(baseDir, "mcp-inspector-copy-"));
19945
+ copyDirs.add(dir);
19946
+ if (!exitCleanupRegistered) {
19947
+ process.on("exit", removeCopyFiles);
19948
+ exitCleanupRegistered = true;
19949
+ }
19074
19950
  const file = path6.join(dir, "value.txt");
19075
19951
  fs5.writeFileSync(file, text, { encoding: "utf8", mode: 384 });
19076
19952
  return file;
@@ -19107,7 +19983,10 @@ function useCopyKeys(value, label) {
19107
19983
  if (key === SAVE_KEY) {
19108
19984
  try {
19109
19985
  const file = writeCopyFile(value);
19110
- setStatus({ tone: "success", message: `Saved ${label} to ${file}` });
19986
+ setStatus({
19987
+ tone: "success",
19988
+ message: `Saved ${label} to ${file} (deleted when the TUI exits)`
19989
+ });
19111
19990
  } catch (err) {
19112
19991
  setStatus({
19113
19992
  tone: "error",
@@ -19516,7 +20395,7 @@ import { useCallback as useCallback14, useEffect as useEffect4, useMemo, useStat
19516
20395
  function printable(input) {
19517
20396
  return input.replace(/[\u0000-\u001f\u007f]/g, "");
19518
20397
  }
19519
- function useListFilter(items, fields, { enabled, onEditingChange }) {
20398
+ function useListFilter(items, fields, { enabled, onEditingChange, onEscapeClaimChange }) {
19520
20399
  const [query, setQuery] = useState7("");
19521
20400
  const [editingRequested, setEditingRequested] = useState7(false);
19522
20401
  const editing = editingRequested && enabled;
@@ -19525,6 +20404,13 @@ function useListFilter(items, fields, { enabled, onEditingChange }) {
19525
20404
  onEditingChange?.(true);
19526
20405
  return () => onEditingChange?.(false);
19527
20406
  }, [editing, onEditingChange]);
20407
+ const active = query.trim() !== "";
20408
+ const claimsEscape = enabled && active && !editingRequested;
20409
+ useEffect4(() => {
20410
+ if (!claimsEscape) return;
20411
+ onEscapeClaimChange?.(true);
20412
+ return () => onEscapeClaimChange?.(false);
20413
+ }, [claimsEscape, onEscapeClaimChange]);
19528
20414
  const { filtered, indices } = useMemo(() => {
19529
20415
  const needle = query.trim().toLowerCase();
19530
20416
  const filtered2 = [];
@@ -19545,6 +20431,10 @@ function useListFilter(items, fields, { enabled, onEditingChange }) {
19545
20431
  setEditingRequested(true);
19546
20432
  return true;
19547
20433
  }
20434
+ if (key.escape && query.trim() !== "") {
20435
+ setQuery("");
20436
+ return true;
20437
+ }
19548
20438
  return false;
19549
20439
  }
19550
20440
  if (key.escape) {
@@ -19568,12 +20458,12 @@ function useListFilter(items, fields, { enabled, onEditingChange }) {
19568
20458
  if (text) setQuery((prev) => prev + text);
19569
20459
  return true;
19570
20460
  },
19571
- [enabled, editingRequested]
20461
+ [enabled, editingRequested, query]
19572
20462
  );
19573
20463
  return {
19574
20464
  query,
19575
20465
  editing,
19576
- active: query.trim() !== "",
20466
+ active,
19577
20467
  items: filtered,
19578
20468
  indices,
19579
20469
  handleInput
@@ -19593,7 +20483,7 @@ function ListFilterBar({ query, editing, focused }) {
19593
20483
  ] }) : query.trim() !== "" ? /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate-end", children: [
19594
20484
  /* @__PURE__ */ jsx6(Text5, { color: "cyan", children: "/" }),
19595
20485
  query,
19596
- /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: " (/ to edit)" })
20486
+ /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: focused ? " (/ edit \xB7 Esc clear)" : " (/ to edit)" })
19597
20487
  ] }) : focused ? /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: "/ to filter" }) : /* @__PURE__ */ jsx6(Text5, { children: " " }) });
19598
20488
  }
19599
20489
  function filterCount(active, shown, total) {
@@ -19618,7 +20508,8 @@ function ResourcesTab({
19618
20508
  onFetchTemplate,
19619
20509
  onAuthRecoveryRequired,
19620
20510
  modalOpen = false,
19621
- onFilterEditingChange
20511
+ onFilterEditingChange,
20512
+ onFilterEscapeClaimChange
19622
20513
  }) {
19623
20514
  const [error, setError] = useState8(null);
19624
20515
  const [resourceContent, setResourceContent] = useState8(null);
@@ -19641,7 +20532,8 @@ function ResourcesTab({
19641
20532
  const visibleCount = Math.max(1, height - 7 - LIST_FILTER_ROWS);
19642
20533
  const filter = useListFilter(allItems, resourceFilterFields, {
19643
20534
  enabled: !modalOpen && focusedPane === "list",
19644
- onEditingChange: onFilterEditingChange
20535
+ onEditingChange: onFilterEditingChange,
20536
+ onEscapeClaimChange: onFilterEscapeClaimChange
19645
20537
  });
19646
20538
  const shownItems = filter.items;
19647
20539
  const { selectedIndex, firstVisible, setSelection } = useSelectableList(
@@ -19914,12 +20806,14 @@ function PromptsTab({
19914
20806
  onFetchPrompt,
19915
20807
  onAuthRecoveryRequired,
19916
20808
  modalOpen = false,
19917
- onFilterEditingChange
20809
+ onFilterEditingChange,
20810
+ onFilterEscapeClaimChange
19918
20811
  }) {
19919
20812
  const visibleCount = Math.max(1, height - 7 - LIST_FILTER_ROWS);
19920
20813
  const filter = useListFilter(prompts, promptFilterFields, {
19921
20814
  enabled: !modalOpen && focusedPane === "list",
19922
- onEditingChange: onFilterEditingChange
20815
+ onEditingChange: onFilterEditingChange,
20816
+ onEscapeClaimChange: onFilterEscapeClaimChange
19923
20817
  });
19924
20818
  const shownPrompts = filter.items;
19925
20819
  const { selectedIndex, firstVisible, setSelection } = useSelectableList(
@@ -20137,6 +21031,10 @@ import {
20137
21031
  import { Box as Box9, Text as Text8, useInput as useInput5 } from "ink";
20138
21032
 
20139
21033
  // ../../core/mcp/skillsVerification.ts
21034
+ var DEFAULT_SKILL_BUDGET_HINT = {
21035
+ raise: "with the server's skillCatalogMaxSkills / skillCatalogMaxBytes settings",
21036
+ verifyOne: "by verifying this skill alone"
21037
+ };
20140
21038
  function reasonOf(err) {
20141
21039
  return err instanceof Error ? err.message : String(err);
20142
21040
  }
@@ -20198,7 +21096,7 @@ function responseBytes(result) {
20198
21096
  }
20199
21097
  return total;
20200
21098
  }
20201
- async function verifySkills(client, entries, metadata) {
21099
+ async function verifySkills(client, entries, metadata, budgetHint = DEFAULT_SKILL_BUDGET_HINT) {
20202
21100
  const collisions = checkSkillNameCollisions(entries);
20203
21101
  const reports = [];
20204
21102
  const budget = resolveSkillCatalogBudget(client.getServerSettings?.());
@@ -20210,7 +21108,7 @@ async function verifySkills(client, entries, metadata) {
20210
21108
  const files = [];
20211
21109
  const declared = entry.resources === DYNAMIC_RESOURCES ? [] : entry.resources;
20212
21110
  const manifest = withinBudget ? boundedManifest(declared) : [];
20213
- let incomplete = !withinBudget ? `Not read: this run already reached its catalog budget of ${budget.maxSkills} skills / ${budget.maxBytes} bytes (raise it in the server's Skills settings). Nothing about this skill's files has been checked \u2014 verify it on its own with \`--method skills/get --uri <uri> --verify\`, where <uri> is this report's uri, to get a verdict.` : manifest.length < declared.length ? `Only ${manifest.length} of ${declared.length} manifest entries were read: the skill exceeds the ${SKILL_MAX_RESOURCE_ENTRIES}-entry / ${SKILL_MAX_TOTAL_BYTES}-byte interoperability limits, so the rest were not fetched and cannot be reported on.` : void 0;
21111
+ let incomplete = !withinBudget ? `Not read: this run already reached its catalog budget of ${budget.maxSkills} skills / ${budget.maxBytes} bytes (raise it ${budgetHint.raise}). Nothing about this skill's files has been checked \u2014 verify it on its own ${budgetHint.verifyOne} to get a verdict.` : manifest.length < declared.length ? `Only ${manifest.length} of ${declared.length} manifest entries were read: the skill exceeds the ${SKILL_MAX_RESOURCE_ENTRIES}-entry / ${SKILL_MAX_TOTAL_BYTES}-byte interoperability limits, so the rest were not fetched and cannot be reported on.` : void 0;
20214
21112
  let receivedBytes = 0;
20215
21113
  const entryIdentity = skillUriIdentity(entry.uri);
20216
21114
  let selfAttempted = false;
@@ -20365,6 +21263,10 @@ var FILE_COLOR = {
20365
21263
  error: "red",
20366
21264
  "read-error": "red"
20367
21265
  };
21266
+ var TUI_SKILL_BUDGET_HINT = {
21267
+ raise: "with --skill-catalog-max-skills / --skill-catalog-max-bytes at launch",
21268
+ verifyOne: "by selecting it and pressing Enter"
21269
+ };
20368
21270
  var VERIFY_STATUS = {
20369
21271
  verified: "[Verified \u2014 Enter to re-verify]",
20370
21272
  unverifiable: "[UNVERIFIABLE \u2014 Enter to re-verify]",
@@ -20425,12 +21327,14 @@ function SkillsTab({
20425
21327
  focusedPane = null,
20426
21328
  onAuthRecoveryRequired,
20427
21329
  modalOpen = false,
20428
- onFilterEditingChange
21330
+ onFilterEditingChange,
21331
+ onFilterEscapeClaimChange
20429
21332
  }) {
20430
21333
  const visibleCount = Math.max(1, height - 8 - LIST_FILTER_ROWS);
20431
21334
  const filter = useListFilter(skills, skillFilterFields, {
20432
21335
  enabled: !modalOpen && focusedPane === "list",
20433
- onEditingChange: onFilterEditingChange
21336
+ onEditingChange: onFilterEditingChange,
21337
+ onEscapeClaimChange: onFilterEscapeClaimChange
20434
21338
  });
20435
21339
  const shownSkills = filter.items;
20436
21340
  const { selectedIndex, firstVisible, setSelection } = useSelectableList(
@@ -20454,7 +21358,12 @@ function SkillsTab({
20454
21358
  setError(null);
20455
21359
  void (async () => {
20456
21360
  try {
20457
- const results = await verifySkills(inspectorClient, entries);
21361
+ const results = await verifySkills(
21362
+ inspectorClient,
21363
+ entries,
21364
+ void 0,
21365
+ TUI_SKILL_BUDGET_HINT
21366
+ );
20458
21367
  const live = new Set(skillsRef.current.map(skillEntryKey));
20459
21368
  setReports(
20460
21369
  (previous) => mergeReports(previous, entries, results, live)
@@ -21022,12 +21931,14 @@ function ToolsTab({
21022
21931
  onTestTool,
21023
21932
  onViewDetails,
21024
21933
  modalOpen = false,
21025
- onFilterEditingChange
21934
+ onFilterEditingChange,
21935
+ onFilterEscapeClaimChange
21026
21936
  }) {
21027
21937
  const visibleCount = Math.max(1, height - 7 - LIST_FILTER_ROWS);
21028
21938
  const filter = useListFilter(tools, toolFilterFields, {
21029
21939
  enabled: !modalOpen && focusedPane === "list",
21030
- onEditingChange: onFilterEditingChange
21940
+ onEditingChange: onFilterEditingChange,
21941
+ onEscapeClaimChange: onFilterEscapeClaimChange
21031
21942
  });
21032
21943
  const shownTools = filter.items;
21033
21944
  const { selectedIndex, firstVisible, setSelection } = useSelectableList(
@@ -24574,7 +25485,10 @@ function tabAcceleratorBinding(visible) {
24574
25485
  function globalBindings(visible = tabs) {
24575
25486
  return [
24576
25487
  { keys: "?", action: "Show or hide this help" },
24577
- { keys: "Esc / Ctrl+C", action: "Exit (Esc closes a dialog first)" },
25488
+ {
25489
+ keys: "Esc / Ctrl+C",
25490
+ action: "Exit (Esc closes a dialog or clears a kept filter first)"
25491
+ },
24578
25492
  {
24579
25493
  keys: "Tab / Shift+Tab",
24580
25494
  action: "Move focus: servers \u2192 tabs \u2192 list \u2192 details"
@@ -25063,7 +25977,8 @@ var STREAM_STATUS_TEXT = {
25063
25977
  "never-acknowledged": {
25064
25978
  text: "closed without acknowledging (server answered listen with a result)",
25065
25979
  color: "red"
25066
- }
25980
+ },
25981
+ refused: { text: "refused by the server", color: "red" }
25067
25982
  };
25068
25983
  function SubscriptionsTab({
25069
25984
  resources,
@@ -25152,6 +26067,7 @@ function SubscriptionsTab({
25152
26067
  scrollViewRef.current?.scrollTo(0);
25153
26068
  }, [selectedIndex]);
25154
26069
  const streamText = STREAM_STATUS_TEXT[streamState.status];
26070
+ const streamRefusal = streamState.status === "refused" ? streamState.refusal : void 0;
25155
26071
  const notHonored = !!selected && selected.subscribed && streamState.active && streamState.status === "acknowledged" && !streamState.honoredUris.includes(selected.resource.uri);
25156
26072
  return /* @__PURE__ */ jsxs21(Box33, { flexDirection: "row", width, height, children: [
25157
26073
  /* @__PURE__ */ jsxs21(
@@ -25238,11 +26154,12 @@ function SubscriptionsTab({
25238
26154
  ] }) }),
25239
26155
  notHonored && /* @__PURE__ */ jsx23(Box33, { flexShrink: 0, children: /* @__PURE__ */ jsx23(Text30, { color: "yellow", children: "Server did not honor this URI in its listen filter" }) })
25240
26156
  ] }),
25241
- streamState.active && /* @__PURE__ */ jsx23(Box33, { flexShrink: 0, children: /* @__PURE__ */ jsxs21(Text30, { children: [
26157
+ (streamState.active || streamRefusal) && /* @__PURE__ */ jsx23(Box33, { flexShrink: 0, children: /* @__PURE__ */ jsxs21(Text30, { children: [
25242
26158
  "Listen stream:",
25243
26159
  " ",
25244
26160
  /* @__PURE__ */ jsx23(Text30, { color: streamText.color, children: streamText.text })
25245
26161
  ] }) }),
26162
+ streamRefusal && /* @__PURE__ */ jsx23(Box33, { flexShrink: 0, children: /* @__PURE__ */ jsx23(Text30, { color: "red", children: subscriptionListenRefusalMessage(streamRefusal) }) }),
25246
26163
  error && /* @__PURE__ */ jsx23(Box33, { marginTop: 1, flexShrink: 0, children: /* @__PURE__ */ jsx23(Text30, { color: "red", children: error }) }),
25247
26164
  /* @__PURE__ */ jsx23(Box33, { marginTop: 1, flexShrink: 0, children: /* @__PURE__ */ jsxs21(Text30, { bold: true, children: [
25248
26165
  "Updates (",
@@ -25451,6 +26368,7 @@ var APP_NAME = "MCP Inspector TUI";
25451
26368
  var APP_DESCRIPTION = "Terminal User Interface for the Model Context Protocol Inspector";
25452
26369
  var APP_VERSION = readInspectorVersion(import.meta.url);
25453
26370
  var TUI_CLIENT_NAME = "inspector-tui";
26371
+ var TUI_CONNECTION_TIMEOUT_REMEDY = "To accommodate a slower server, set connectionTimeout on the server's entry in a catalog (--catalog) or config (--config) file.";
25454
26372
  var NO_ROOTS = [];
25455
26373
  var readRoots = (client) => client.getRoots();
25456
26374
  function App({
@@ -25474,6 +26392,7 @@ function App({
25474
26392
  const [helpOpen, setHelpOpen] = useState22(false);
25475
26393
  const focus = helpOpen ? "help" : paneFocus;
25476
26394
  const [listFilterEditing, setListFilterEditing] = useState22(false);
26395
+ const [listFilterClaimsEscape, setListFilterClaimsEscape] = useState22(false);
25477
26396
  const [tabCounts, setTabCounts] = useState22({});
25478
26397
  const [oauthStatus, setOauthStatus] = useState22("idle");
25479
26398
  const [oauthMessage, setOauthMessage] = useState22(null);
@@ -25569,6 +26488,7 @@ function App({
25569
26488
  environment,
25570
26489
  clientIdentity: { name: TUI_CLIENT_NAME, version: APP_VERSION },
25571
26490
  pipeStderr: true,
26491
+ connectionTimeoutRemedy: TUI_CONNECTION_TIMEOUT_REMEDY,
25572
26492
  ...savedSettings && savedSettings.requestTimeout > 0 && {
25573
26493
  timeout: savedSettings.requestTimeout
25574
26494
  },
@@ -26452,6 +27372,7 @@ function App({
26452
27372
  return;
26453
27373
  }
26454
27374
  if (key.escape) {
27375
+ if (listFilterClaimsEscape) return;
26455
27376
  exit();
26456
27377
  }
26457
27378
  const showAuthTab = !!selectedServer && !!selectedServerConfig && isOAuthCapableServerConfig(selectedServerConfig);
@@ -26553,7 +27474,7 @@ function App({
26553
27474
  }
26554
27475
  }
26555
27476
  });
26556
- const headerHeight = 1;
27477
+ const headerHeight = 2;
26557
27478
  const serverListWidth = Math.floor(dimensions.width * 0.3);
26558
27479
  const contentWidth = dimensions.width - serverListWidth;
26559
27480
  const shownTabs = visibleTabs({
@@ -26612,17 +27533,18 @@ function App({
26612
27533
  justifyContent: "space-between",
26613
27534
  alignItems: "center",
26614
27535
  children: [
26615
- /* @__PURE__ */ jsxs23(Box35, { children: [
26616
- /* @__PURE__ */ jsx25(Text32, { bold: true, color: "cyan", children: APP_NAME }),
26617
- /* @__PURE__ */ jsxs23(Text32, { dimColor: true, children: [
26618
- " - ",
27536
+ /* @__PURE__ */ jsxs23(Box35, { flexShrink: 1, overflow: "hidden", children: [
27537
+ /* @__PURE__ */ jsx25(Box35, { flexShrink: 0, children: /* @__PURE__ */ jsx25(Text32, { bold: true, color: "cyan", children: APP_NAME }) }),
27538
+ /* @__PURE__ */ jsxs23(Text32, { dimColor: true, wrap: "truncate-end", children: [
27539
+ " ",
27540
+ "- ",
26619
27541
  APP_DESCRIPTION
26620
27542
  ] })
26621
27543
  ] }),
26622
- /* @__PURE__ */ jsxs23(Text32, { dimColor: true, children: [
27544
+ /* @__PURE__ */ jsx25(Box35, { flexShrink: 0, marginLeft: 1, children: /* @__PURE__ */ jsxs23(Text32, { dimColor: true, children: [
26623
27545
  "v",
26624
27546
  APP_VERSION
26625
- ] })
27547
+ ] }) })
26626
27548
  ]
26627
27549
  }
26628
27550
  ),
@@ -26884,6 +27806,7 @@ function App({
26884
27806
  ResourcesTab,
26885
27807
  {
26886
27808
  onFilterEditingChange: setListFilterEditing,
27809
+ onFilterEscapeClaimChange: setListFilterClaimsEscape,
26887
27810
  resources: currentServerState.resources,
26888
27811
  resourceTemplates: currentServerState.resourceTemplates,
26889
27812
  inspectorClient: selectedInspectorClient,
@@ -26948,6 +27871,7 @@ function App({
26948
27871
  SkillsTab,
26949
27872
  {
26950
27873
  onFilterEditingChange: setListFilterEditing,
27874
+ onFilterEscapeClaimChange: setListFilterClaimsEscape,
26951
27875
  skills: managedSkills,
26952
27876
  pageCount: managedSkillsPageCount,
26953
27877
  loadError: managedSkillsError,
@@ -26963,6 +27887,7 @@ function App({
26963
27887
  PromptsTab,
26964
27888
  {
26965
27889
  onFilterEditingChange: setListFilterEditing,
27890
+ onFilterEscapeClaimChange: setListFilterClaimsEscape,
26966
27891
  prompts: currentServerState.prompts,
26967
27892
  inspectorClient: selectedInspectorClient,
26968
27893
  width: contentWidth,
@@ -26988,6 +27913,7 @@ function App({
26988
27913
  ToolsTab,
26989
27914
  {
26990
27915
  onFilterEditingChange: setListFilterEditing,
27916
+ onFilterEscapeClaimChange: setListFilterClaimsEscape,
26991
27917
  tools: currentServerState.tools,
26992
27918
  isConnected: inspectorStatus === "connected",
26993
27919
  width: contentWidth,
@@ -27145,7 +28071,11 @@ var App_default = App;
27145
28071
  import { jsx as jsx26 } from "react/jsx-runtime";
27146
28072
  async function runTui(args) {
27147
28073
  const program = new Command();
27148
- program.name("mcp-inspector-tui").description("Terminal UI for MCP Inspector").option(
28074
+ program.name("mcp-inspector-tui").description("Terminal UI for MCP Inspector").version(
28075
+ readInspectorVersion(import.meta.url),
28076
+ "-V, --version",
28077
+ "Print the Inspector version"
28078
+ ).option(
27149
28079
  "--catalog <path>",
27150
28080
  "Writable catalog file (created if missing; default: ~/.mcp-inspector/mcp.json, or MCP_CATALOG_PATH)"
27151
28081
  ).option(