@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.
@@ -2707,6 +2707,84 @@ function isJsonDocumentResponse(contentType) {
2707
2707
  const mediaType = contentType.split(";")[0].trim().toLowerCase();
2708
2708
  return mediaType === "application/json" || mediaType.endsWith("+json");
2709
2709
  }
2710
+ var SYNTHESIZED_METADATA_HEADER = "x-inspector-oauth-metadata-synthesized";
2711
+ var synthesizedResponses = /* @__PURE__ */ new WeakSet();
2712
+ function isSynthesizedMetadataResponse(response) {
2713
+ return synthesizedResponses.has(response);
2714
+ }
2715
+ var OIDC_WELL_KNOWN = "/.well-known/openid-configuration";
2716
+ var RFC8414_WELL_KNOWN = "/.well-known/oauth-authorization-server";
2717
+ function continuesDiscovery(status) {
2718
+ return status < 500 || status === 502;
2719
+ }
2720
+ function authorizationServerForFinalCandidate(url) {
2721
+ let parsed;
2722
+ try {
2723
+ parsed = new URL(url);
2724
+ } catch {
2725
+ return void 0;
2726
+ }
2727
+ const { pathname } = parsed;
2728
+ if (pathname === OIDC_WELL_KNOWN) return parsed.origin;
2729
+ if (!pathname.endsWith(OIDC_WELL_KNOWN)) return void 0;
2730
+ if (pathname.startsWith(`${RFC8414_WELL_KNOWN}/`) || pathname.startsWith(`${OIDC_WELL_KNOWN}/`)) {
2731
+ return void 0;
2732
+ }
2733
+ return `${parsed.origin}${pathname.slice(0, -OIDC_WELL_KNOWN.length)}`;
2734
+ }
2735
+ function synthesizeAuthorizationServerMetadata(issuer, overrides) {
2736
+ return {
2737
+ issuer,
2738
+ authorization_endpoint: overrides.authorizationUrl ?? new URL("/authorize", issuer).href,
2739
+ token_endpoint: overrides.tokenUrl ?? new URL("/token", issuer).href,
2740
+ registration_endpoint: new URL("/register", issuer).href,
2741
+ response_types_supported: ["code"],
2742
+ code_challenge_methods_supported: ["S256"]
2743
+ };
2744
+ }
2745
+ function requestMethodOf(input, init) {
2746
+ if (init?.method) return init.method.toUpperCase();
2747
+ if (typeof input !== "string" && !(input instanceof URL)) {
2748
+ return input.method.toUpperCase();
2749
+ }
2750
+ return "GET";
2751
+ }
2752
+ function acceptsJson(input, init) {
2753
+ let accept = null;
2754
+ if (init?.headers) {
2755
+ accept = new Headers(init.headers).get("accept");
2756
+ } else if (typeof input !== "string" && !(input instanceof URL)) {
2757
+ accept = input.headers.get("accept");
2758
+ }
2759
+ if (!accept) return false;
2760
+ const mediaTypes = accept.split(",").map((part) => part.split(";")[0].trim().toLowerCase());
2761
+ return mediaTypes.includes("application/json") && !mediaTypes.includes("text/event-stream");
2762
+ }
2763
+ function synthesizedMetadataResponse(input, init, response, overrides) {
2764
+ if (!continuesDiscovery(response.status)) return void 0;
2765
+ if (requestMethodOf(input, init) !== "GET") return void 0;
2766
+ if (!acceptsJson(input, init)) return void 0;
2767
+ const issuer = authorizationServerForFinalCandidate(requestUrlOf(input));
2768
+ if (!issuer) return void 0;
2769
+ void response.body?.cancel().catch(() => {
2770
+ });
2771
+ console.warn(
2772
+ `[oauth] ${issuer} publishes no authorization server metadata; using the configured endpoint override(s) in place of the SDK's /authorize and /token fallbacks.`
2773
+ );
2774
+ const synthesized = new Response(
2775
+ JSON.stringify(synthesizeAuthorizationServerMetadata(issuer, overrides)),
2776
+ {
2777
+ status: 200,
2778
+ statusText: "OK",
2779
+ headers: {
2780
+ "content-type": "application/json",
2781
+ [SYNTHESIZED_METADATA_HEADER]: "endpoint-overrides"
2782
+ }
2783
+ }
2784
+ );
2785
+ synthesizedResponses.add(synthesized);
2786
+ return synthesized;
2787
+ }
2710
2788
  function responseWithBody(response, body) {
2711
2789
  const headers = new Headers(response.headers);
2712
2790
  headers.delete("content-length");
@@ -2734,7 +2812,9 @@ function withOAuthEndpointOverrides(fetchFn, getOverrides) {
2734
2812
  const overrides = resolveOverrides();
2735
2813
  if (!overrides) return response;
2736
2814
  if (!isAuthorizationServerMetadataRequest(input)) return response;
2737
- if (!response.ok) return response;
2815
+ if (!response.ok) {
2816
+ return synthesizedMetadataResponse(input, init, response, overrides) ?? response;
2817
+ }
2738
2818
  if (!isJsonDocumentResponse(response.headers.get("content-type")))
2739
2819
  return response;
2740
2820
  let parsed;
@@ -2910,8 +2990,8 @@ import {
2910
2990
  OAuthMetadataSchema,
2911
2991
  OpenIdProviderDiscoveryMetadataSchema
2912
2992
  } from "@modelcontextprotocol/core";
2913
- var RFC8414_WELL_KNOWN = "/.well-known/oauth-authorization-server";
2914
- var OIDC_WELL_KNOWN = "/.well-known/openid-configuration";
2993
+ var RFC8414_WELL_KNOWN2 = "/.well-known/oauth-authorization-server";
2994
+ var OIDC_WELL_KNOWN2 = "/.well-known/openid-configuration";
2915
2995
  var COMPAT_SOURCE_HEADER = "x-inspector-oauth-metadata-source";
2916
2996
  function oidcDiscoveryCandidates(rfc8414Url) {
2917
2997
  let url;
@@ -2920,16 +3000,16 @@ function oidcDiscoveryCandidates(rfc8414Url) {
2920
3000
  } catch {
2921
3001
  return [];
2922
3002
  }
2923
- if (!url.pathname.startsWith(RFC8414_WELL_KNOWN)) return [];
2924
- const path7 = url.pathname.slice(RFC8414_WELL_KNOWN.length);
3003
+ if (!url.pathname.startsWith(RFC8414_WELL_KNOWN2)) return [];
3004
+ const path7 = url.pathname.slice(RFC8414_WELL_KNOWN2.length);
2925
3005
  if (path7 !== "" && !path7.startsWith("/")) return [];
2926
- if (path7 === "") return [new URL(OIDC_WELL_KNOWN, url.origin).href];
3006
+ if (path7 === "") return [new URL(OIDC_WELL_KNOWN2, url.origin).href];
2927
3007
  return [
2928
- new URL(`${OIDC_WELL_KNOWN}${path7}`, url.origin).href,
2929
- new URL(`${path7}${OIDC_WELL_KNOWN}`, url.origin).href
3008
+ new URL(`${OIDC_WELL_KNOWN2}${path7}`, url.origin).href,
3009
+ new URL(`${path7}${OIDC_WELL_KNOWN2}`, url.origin).href
2930
3010
  ];
2931
3011
  }
2932
- function continuesDiscovery(status) {
3012
+ function continuesDiscovery2(status) {
2933
3013
  return status >= 400 && status < 500 || status === 502;
2934
3014
  }
2935
3015
  async function releaseBody(response) {
@@ -2955,7 +3035,7 @@ function discoveryHeaders(input, init) {
2955
3035
  }
2956
3036
  return new Headers();
2957
3037
  }
2958
- function requestMethodOf(input, init) {
3038
+ function requestMethodOf2(input, init) {
2959
3039
  if (init?.method) return init.method.toUpperCase();
2960
3040
  if (typeof input !== "string" && !(input instanceof URL)) {
2961
3041
  return input.method.toUpperCase();
@@ -2966,8 +3046,8 @@ function withRfc8414OidcCompat(fetchFn) {
2966
3046
  return async (input, init) => {
2967
3047
  const response = await fetchFn(input, init);
2968
3048
  if (response.ok) return response;
2969
- if (!continuesDiscovery(response.status)) return response;
2970
- if (requestMethodOf(input, init) !== "GET") return response;
3049
+ if (!continuesDiscovery2(response.status)) return response;
3050
+ if (requestMethodOf2(input, init) !== "GET") return response;
2971
3051
  const candidates = oidcDiscoveryCandidates(requestUrlOf3(input));
2972
3052
  if (candidates.length === 0) return response;
2973
3053
  const headers = discoveryHeaders(input, init);
@@ -2997,7 +3077,7 @@ function withRfc8414OidcCompat(fetchFn) {
2997
3077
  releaseBodyDetached(response);
2998
3078
  throw callerSignal.reason;
2999
3079
  }
3000
- if (continuesDiscovery(probe.status)) continue;
3080
+ if (continuesDiscovery2(probe.status)) continue;
3001
3081
  return response;
3002
3082
  }
3003
3083
  let parsed;
@@ -3017,6 +3097,16 @@ function withRfc8414OidcCompat(fetchFn) {
3017
3097
  }
3018
3098
  const source = probe.url || candidate;
3019
3099
  releaseBodyDetached(response);
3100
+ if (isSynthesizedMetadataResponse(probe)) {
3101
+ return new Response(body, {
3102
+ status: 200,
3103
+ statusText: "OK",
3104
+ headers: {
3105
+ "content-type": "application/json",
3106
+ [SYNTHESIZED_METADATA_HEADER]: "endpoint-overrides"
3107
+ }
3108
+ });
3109
+ }
3020
3110
  console.warn(
3021
3111
  `[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.`
3022
3112
  );
@@ -3087,6 +3177,13 @@ var BaseOAuthClientProvider = class {
3087
3177
  requestRefreshToken;
3088
3178
  /** Server-configured scope, for the #2068 `offline_access` distinction. */
3089
3179
  configuredScope;
3180
+ /** Per-flow verifier store (#2726); absent → the shared storage slot. */
3181
+ flowStore;
3182
+ /**
3183
+ * The authId of the flow this provider is driving: set by {@link state} on the
3184
+ * redirect leg and by {@link resumeAuthorizationFlow} on the callback leg.
3185
+ */
3186
+ flowId;
3090
3187
  constructor(serverUrl, oauthConfig) {
3091
3188
  this.serverUrl = serverUrl;
3092
3189
  this.storage = oauthConfig.storage;
@@ -3096,6 +3193,7 @@ var BaseOAuthClientProvider = class {
3096
3193
  this.authorizationParams = oauthConfig.authorizationParams;
3097
3194
  this.requestRefreshToken = oauthConfig.requestRefreshToken ?? true;
3098
3195
  this.configuredScope = oauthConfig.configuredScope;
3196
+ this.flowStore = oauthConfig.flowStore;
3099
3197
  }
3100
3198
  /**
3101
3199
  * Load persisted scope into {@link cachedScope} before SDK `auth()` (which
@@ -3166,7 +3264,24 @@ var BaseOAuthClientProvider = class {
3166
3264
  return metadata;
3167
3265
  }
3168
3266
  state() {
3169
- return generateOAuthState();
3267
+ const state = generateOAuthState();
3268
+ this.flowId = state;
3269
+ return state;
3270
+ }
3271
+ /**
3272
+ * Bind this provider to the flow a callback's `state` names, so
3273
+ * {@link codeVerifier} reads that flow's verifier and no other (#2726). A
3274
+ * `state` that is not one of ours binds nothing — and, with a flow store, a
3275
+ * flow-less read then fails rather than reaching for the shared slot.
3276
+ */
3277
+ resumeAuthorizationFlow(state) {
3278
+ this.flowId = parseOAuthState(state)?.authId;
3279
+ }
3280
+ /** Drop the bound flow's verifier once its exchange has completed. */
3281
+ async clearAuthorizationFlow() {
3282
+ if (this.flowStore && this.flowId) {
3283
+ await this.flowStore.clearCodeVerifier(this.flowId);
3284
+ }
3170
3285
  }
3171
3286
  async clientInformation(ctx) {
3172
3287
  const preregistered = await this.storage.getClientInformation(
@@ -3311,9 +3426,26 @@ var BaseOAuthClientProvider = class {
3311
3426
  }
3312
3427
  }
3313
3428
  async saveCodeVerifier(codeVerifier) {
3429
+ if (this.flowStore && this.flowId) {
3430
+ await this.flowStore.saveCodeVerifier(
3431
+ this.flowId,
3432
+ this.serverUrl,
3433
+ codeVerifier
3434
+ );
3435
+ return;
3436
+ }
3314
3437
  await this.storage.saveCodeVerifier(this.serverUrl, codeVerifier);
3315
3438
  }
3316
3439
  async codeVerifier() {
3440
+ if (this.flowStore) {
3441
+ const verifier2 = this.flowId ? await this.flowStore.getCodeVerifier(this.flowId, this.serverUrl) : void 0;
3442
+ if (!verifier2) {
3443
+ throw new Error(
3444
+ "No code verifier saved for this authorization flow. It may have been started in another browser tab or already completed; please re-authenticate."
3445
+ );
3446
+ }
3447
+ return verifier2;
3448
+ }
3317
3449
  const verifier = await this.storage.getCodeVerifier(this.serverUrl);
3318
3450
  if (!verifier) {
3319
3451
  throw new Error("No code verifier saved for session");
@@ -3360,6 +3492,10 @@ var BaseOAuthClientProvider = class {
3360
3492
  await this.storage.clearTokens(this.serverUrl);
3361
3493
  return;
3362
3494
  case "verifier":
3495
+ if (this.flowStore) {
3496
+ await this.clearAuthorizationFlow();
3497
+ return;
3498
+ }
3363
3499
  await this.storage.clearCodeVerifier(this.serverUrl);
3364
3500
  return;
3365
3501
  case "discovery":
@@ -3497,7 +3633,10 @@ function parseAuthChallengeFromResponse(response, context) {
3497
3633
  if (status !== 401 && status !== 403) {
3498
3634
  return void 0;
3499
3635
  }
3500
- const wwwAuthenticate = response.headers.get("WWW-Authenticate") ?? void 0;
3636
+ const wwwAuthenticate = response.headers.get("WWW-Authenticate") || void 0;
3637
+ if (status === 403 && !wwwAuthenticate) {
3638
+ return void 0;
3639
+ }
3501
3640
  const bearer = wwwAuthenticate ? parseWwwAuthenticateBearer(wwwAuthenticate) : {};
3502
3641
  const requiredScopes = parseScopeString(bearer.scope);
3503
3642
  return {
@@ -4097,7 +4236,7 @@ function serializeOAuthPersistBlob(snapshot) {
4097
4236
  import { resolve as resolve5 } from "path";
4098
4237
 
4099
4238
  // ../../core/auth/node/oauth-secrets.ts
4100
- import { randomUUID as randomUUID2 } from "crypto";
4239
+ import { createHmac, randomUUID as randomUUID2 } from "crypto";
4101
4240
  import { OAuthTokensSchema as OAuthTokensSchema3 } from "@modelcontextprotocol/core";
4102
4241
  var PERSIST_TOKENS_ENV = "MCP_INSPECTOR_PERSIST_TOKENS";
4103
4242
  var warnedPolicyValues = /* @__PURE__ */ new Set();
@@ -4130,6 +4269,29 @@ var LEGACY_REGISTRATION_TOKEN_FIELD = "registration-token";
4130
4269
  var PREREG_CLIENT_SECRET_FIELD = "prereg-client-secret";
4131
4270
  var PREREG_REGISTRATION_TOKEN_FIELD = "prereg-registration-token";
4132
4271
  var IDP_SESSION_FIELD = "idp-session";
4272
+ var clientCredentialStampField = (field) => `${field}#owner`;
4273
+ var credentialMac = (clientId, value) => createHmac("sha256", value).update(clientId).digest("hex");
4274
+ function clientCredentialStamp(clientId, value) {
4275
+ return JSON.stringify({
4276
+ client_id: clientId,
4277
+ mac: credentialMac(clientId, value)
4278
+ });
4279
+ }
4280
+ function stampedOwner(stamp, value) {
4281
+ if (stamp === void 0) return void 0;
4282
+ let parsed;
4283
+ try {
4284
+ parsed = JSON.parse(stamp);
4285
+ } catch {
4286
+ return void 0;
4287
+ }
4288
+ if (typeof parsed !== "object" || parsed === null) return void 0;
4289
+ const { client_id: clientId, mac } = parsed;
4290
+ if (typeof clientId !== "string" || mac !== credentialMac(clientId, value)) {
4291
+ return void 0;
4292
+ }
4293
+ return clientId;
4294
+ }
4133
4295
  function applyTokensPolicy(tokens, policy) {
4134
4296
  if (policy === "none") return void 0;
4135
4297
  if (policy === "access" && tokens.refresh_token !== void 0) {
@@ -4158,12 +4320,18 @@ function splitClientInformation(info, fields, secrets) {
4158
4320
  if (client_secret === void 0 && registration_access_token === void 0) {
4159
4321
  return info;
4160
4322
  }
4161
- if (client_secret !== void 0) {
4162
- secrets[fields.clientSecret] = client_secret;
4163
- }
4164
- if (registration_access_token !== void 0) {
4165
- secrets[fields.registrationToken] = registration_access_token;
4166
- }
4323
+ const put = (field, value) => {
4324
+ if (value === void 0) return;
4325
+ secrets[field] = value;
4326
+ if (fields.stamped && typeof info.client_id === "string") {
4327
+ secrets[clientCredentialStampField(field)] = clientCredentialStamp(
4328
+ info.client_id,
4329
+ value
4330
+ );
4331
+ }
4332
+ };
4333
+ put(fields.clientSecret, client_secret);
4334
+ put(fields.registrationToken, registration_access_token);
4167
4335
  return publicInfo;
4168
4336
  }
4169
4337
  function joinClientInformation(info, fields, secrets) {
@@ -4182,15 +4350,18 @@ function joinClientInformation(info, fields, secrets) {
4182
4350
  }
4183
4351
  var issuerClientInfoFields = (issuer) => ({
4184
4352
  clientSecret: issuerClientSecretField(issuer),
4185
- registrationToken: issuerRegistrationTokenField(issuer)
4353
+ registrationToken: issuerRegistrationTokenField(issuer),
4354
+ stamped: true
4186
4355
  });
4187
4356
  var LEGACY_CLIENT_INFO_FIELDS = {
4188
4357
  clientSecret: LEGACY_CLIENT_SECRET_FIELD,
4189
- registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD
4358
+ registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD,
4359
+ stamped: true
4190
4360
  };
4191
4361
  var PREREG_CLIENT_INFO_FIELDS = {
4192
4362
  clientSecret: PREREG_CLIENT_SECRET_FIELD,
4193
- registrationToken: PREREG_REGISTRATION_TOKEN_FIELD
4363
+ registrationToken: PREREG_REGISTRATION_TOKEN_FIELD,
4364
+ stamped: false
4194
4365
  };
4195
4366
  function splitServerOAuthState(state, policy) {
4196
4367
  const secrets = {};
@@ -4340,20 +4511,54 @@ function joinIdpSession(residue, secrets) {
4340
4511
  return stored ? { ...residue, ...stored } : { ...residue };
4341
4512
  }
4342
4513
  function serverSecretFields(state) {
4514
+ const stamped = (field) => [
4515
+ field,
4516
+ clientCredentialStampField(field)
4517
+ ];
4343
4518
  const fields = /* @__PURE__ */ new Set([
4344
4519
  LEGACY_TOKENS_FIELD,
4345
- LEGACY_CLIENT_SECRET_FIELD,
4346
- LEGACY_REGISTRATION_TOKEN_FIELD,
4520
+ ...stamped(LEGACY_CLIENT_SECRET_FIELD),
4521
+ ...stamped(LEGACY_REGISTRATION_TOKEN_FIELD),
4347
4522
  PREREG_CLIENT_SECRET_FIELD,
4348
4523
  PREREG_REGISTRATION_TOKEN_FIELD
4349
4524
  ]);
4350
4525
  for (const issuer of Object.keys(state?.byIssuer ?? {})) {
4351
4526
  fields.add(issuerTokensField(issuer));
4352
- fields.add(issuerClientSecretField(issuer));
4353
- fields.add(issuerRegistrationTokenField(issuer));
4527
+ for (const field of stamped(issuerClientSecretField(issuer))) {
4528
+ fields.add(field);
4529
+ }
4530
+ for (const field of stamped(issuerRegistrationTokenField(issuer))) {
4531
+ fields.add(field);
4532
+ }
4354
4533
  }
4355
4534
  return [...fields];
4356
4535
  }
4536
+ function clientCredentialOwners(state) {
4537
+ const owners = /* @__PURE__ */ new Map();
4538
+ const add = (info, fields) => {
4539
+ if (typeof info?.client_id !== "string") return;
4540
+ owners.set(fields.clientSecret, info.client_id);
4541
+ owners.set(fields.registrationToken, info.client_id);
4542
+ };
4543
+ add(state.clientInformation, LEGACY_CLIENT_INFO_FIELDS);
4544
+ for (const [issuer, slot] of Object.entries(state.byIssuer ?? {})) {
4545
+ add(slot.clientInformation, issuerClientInfoFields(issuer));
4546
+ }
4547
+ return owners;
4548
+ }
4549
+ function restampClientCredentials(state, secrets) {
4550
+ const out = { ...secrets };
4551
+ for (const [field, clientId] of clientCredentialOwners(state)) {
4552
+ const stampField = clientCredentialStampField(field);
4553
+ const value = Object.hasOwn(out, field) ? out[field] : void 0;
4554
+ if (value === void 0) {
4555
+ delete out[stampField];
4556
+ } else if (stampedOwner(out[stampField], value) === void 0) {
4557
+ setOwnEntry(out, stampField, clientCredentialStamp(clientId, value));
4558
+ }
4559
+ }
4560
+ return out;
4561
+ }
4357
4562
  function clientInfoHasPlaintext(info) {
4358
4563
  return info !== void 0 && (info.client_secret !== void 0 || info.registration_access_token !== void 0);
4359
4564
  }
@@ -4394,6 +4599,44 @@ function coveredLocations(location, byLocation) {
4394
4599
  ([recorded]) => recorded === location || location === KEYRING_LOCATION && recorded.startsWith(FILE_LOCATION_PREFIX)
4395
4600
  ).map(([recorded, keys]) => ({ location: recorded, keys }));
4396
4601
  }
4602
+ var emptyKeys = () => ({
4603
+ servers: /* @__PURE__ */ new Set(),
4604
+ idpSessions: /* @__PURE__ */ new Set(),
4605
+ owners: /* @__PURE__ */ new Map()
4606
+ });
4607
+ function candidateFields(location, byLocation) {
4608
+ const fields = /* @__PURE__ */ new Map();
4609
+ for (const { keys } of coveredLocations(location, byLocation)) {
4610
+ for (const [url, owned] of keys.owners) {
4611
+ const kept = fields.get(url) ?? /* @__PURE__ */ new Set();
4612
+ for (const field of owned.keys()) kept.add(field);
4613
+ fields.set(url, kept);
4614
+ }
4615
+ }
4616
+ return fields;
4617
+ }
4618
+ function addMissingOwners(target, source) {
4619
+ for (const [url, fields] of source) {
4620
+ const kept = target.get(url) ?? /* @__PURE__ */ new Map();
4621
+ for (const [field, clientId] of fields) {
4622
+ if (!kept.has(field)) kept.set(field, clientId);
4623
+ }
4624
+ if (kept.size > 0) target.set(url, kept);
4625
+ }
4626
+ }
4627
+ function mergeOwners(target, source) {
4628
+ let changed = false;
4629
+ for (const [url, fields] of source) {
4630
+ const kept = target.get(url) ?? /* @__PURE__ */ new Map();
4631
+ for (const [field, clientId] of fields) {
4632
+ if (kept.get(field) === clientId) continue;
4633
+ kept.set(field, clientId);
4634
+ changed = true;
4635
+ }
4636
+ if (kept.size > 0) target.set(url, kept);
4637
+ }
4638
+ return changed;
4639
+ }
4397
4640
  var warnedLedgerFailures = /* @__PURE__ */ new Set();
4398
4641
  function warnLedgerFailure(what, error) {
4399
4642
  const reason = error instanceof Error ? error.message : String(error);
@@ -4407,6 +4650,19 @@ function warnLedgerFailure(what, error) {
4407
4650
  function isRecord2(value) {
4408
4651
  return typeof value === "object" && value !== null && !Array.isArray(value);
4409
4652
  }
4653
+ function parseOwners(value) {
4654
+ const owners = /* @__PURE__ */ new Map();
4655
+ if (!isRecord2(value)) return owners;
4656
+ for (const [url, fields] of Object.entries(value)) {
4657
+ if (!isRecord2(fields)) continue;
4658
+ const byField = /* @__PURE__ */ new Map();
4659
+ for (const [field, clientId] of Object.entries(fields)) {
4660
+ if (typeof clientId === "string") byField.set(field, clientId);
4661
+ }
4662
+ if (byField.size > 0) owners.set(url, byField);
4663
+ }
4664
+ return owners;
4665
+ }
4410
4666
  function stringsOf(value) {
4411
4667
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
4412
4668
  }
@@ -4428,7 +4684,9 @@ function parseLedger(raw) {
4428
4684
  if (!isRecord2(keys)) continue;
4429
4685
  byLocation.set(location, {
4430
4686
  servers: new Set(stringsOf(keys.servers)),
4431
- idpSessions: new Set(stringsOf(keys.idpSessions))
4687
+ idpSessions: new Set(stringsOf(keys.idpSessions)),
4688
+ owners: parseOwners(keys.owners),
4689
+ ...isValidSecretsNamespace(keys.owedTo) && { owedTo: keys.owedTo }
4432
4690
  });
4433
4691
  }
4434
4692
  if (byLocation.size > 0) ledger.set(namespace, byLocation);
@@ -4442,7 +4700,19 @@ function serializeLedger(ledger) {
4442
4700
  Object.fromEntries(
4443
4701
  [...byLocation].map(([location, keys]) => [
4444
4702
  location,
4445
- { servers: [...keys.servers], idpSessions: [...keys.idpSessions] }
4703
+ {
4704
+ servers: [...keys.servers],
4705
+ idpSessions: [...keys.idpSessions],
4706
+ ...keys.owners.size > 0 && {
4707
+ owners: Object.fromEntries(
4708
+ [...keys.owners].map(([url, fields]) => [
4709
+ url,
4710
+ Object.fromEntries(fields)
4711
+ ])
4712
+ )
4713
+ },
4714
+ ...keys.owedTo !== void 0 && { owedTo: keys.owedTo }
4715
+ }
4446
4716
  ])
4447
4717
  )
4448
4718
  ])
@@ -4454,7 +4724,7 @@ async function writeLedger(stateFilePath, ledger) {
4454
4724
  if (ledger.size === 0) await deleteStoreFile(path7);
4455
4725
  else await writeStoreFile(path7, serializeLedger(ledger));
4456
4726
  }
4457
- async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions) {
4727
+ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions, owners = /* @__PURE__ */ new Map()) {
4458
4728
  try {
4459
4729
  const ledger = parseLedger(
4460
4730
  await readStoreFile(namespaceLedgerPath(stateFilePath))
@@ -4468,7 +4738,7 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
4468
4738
  let keys = byLocation.get(location);
4469
4739
  let changed = false;
4470
4740
  if (!keys) {
4471
- keys = { servers: /* @__PURE__ */ new Set(), idpSessions: /* @__PURE__ */ new Set() };
4741
+ keys = emptyKeys();
4472
4742
  byLocation.set(location, keys);
4473
4743
  changed = true;
4474
4744
  }
@@ -4482,12 +4752,105 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
4482
4752
  keys.idpSessions.add(issuer);
4483
4753
  changed = true;
4484
4754
  }
4755
+ if (mergeOwners(keys.owners, owners)) changed = true;
4485
4756
  if (changed) await writeLedger(stateFilePath, ledger);
4486
4757
  } catch (error) {
4487
4758
  warnLedgerFailure("update", error);
4488
4759
  }
4489
4760
  }
4490
- async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
4761
+ async function markUncarriedNamespaces(stateFilePath, secretStore, into) {
4762
+ const ledger = parseLedger(
4763
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
4764
+ );
4765
+ const location = await secretStoreLocation(secretStore);
4766
+ let changed = false;
4767
+ for (const [namespace, byLocation] of ledger) {
4768
+ if (namespace === into) continue;
4769
+ for (const [recorded, keys] of byLocation) {
4770
+ if (recorded === location || keys.owners.size === 0) continue;
4771
+ if (keys.owedTo === into) continue;
4772
+ keys.owedTo = into;
4773
+ changed = true;
4774
+ }
4775
+ }
4776
+ if (changed) await writeLedger(stateFilePath, ledger);
4777
+ }
4778
+ async function pendingCarries(stateFilePath, secretStore, current) {
4779
+ const ledger = parseLedger(
4780
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
4781
+ );
4782
+ const location = await secretStoreLocation(secretStore);
4783
+ const carries = [];
4784
+ for (const [namespace, byLocation] of ledger) {
4785
+ const owed = coveredLocations(location, byLocation).some(
4786
+ ({ keys }) => keys.owedTo === current
4787
+ );
4788
+ if (owed) {
4789
+ carries.push({
4790
+ namespace,
4791
+ fields: candidateFields(location, byLocation)
4792
+ });
4793
+ }
4794
+ }
4795
+ return carries.reverse();
4796
+ }
4797
+ async function carryOwed(stateFilePath, ledger, location, secretStore, namespace, current, recorded, now) {
4798
+ let carried = true;
4799
+ for (const [url, fields] of recorded) {
4800
+ for (const field of fields) {
4801
+ const clientId = now.get(url)?.get(field);
4802
+ if (clientId === void 0) continue;
4803
+ try {
4804
+ const from = oauthSecretServerId(url, namespace);
4805
+ const to = oauthSecretServerId(url, current);
4806
+ const stampField = clientCredentialStampField(field);
4807
+ const value = await secretStoreGetStrict(secretStore, from, field);
4808
+ if (value === null) continue;
4809
+ const stamp = await secretStoreGetStrict(secretStore, from, stampField);
4810
+ if (stamp === null || stampedOwner(stamp, value) !== clientId) {
4811
+ continue;
4812
+ }
4813
+ if (await secretStoreGetStrict(secretStore, to, field) !== null) {
4814
+ continue;
4815
+ }
4816
+ let byLocation = ledger.get(current);
4817
+ if (!byLocation) {
4818
+ byLocation = /* @__PURE__ */ new Map();
4819
+ ledger.set(current, byLocation);
4820
+ }
4821
+ const target = byLocation.get(location) ?? emptyKeys();
4822
+ byLocation.set(location, target);
4823
+ const isNew = !target.servers.has(url);
4824
+ target.servers.add(url);
4825
+ const owner = /* @__PURE__ */ new Map([[url, /* @__PURE__ */ new Map([[field, clientId]])]]);
4826
+ if (mergeOwners(target.owners, owner) || isNew) {
4827
+ await writeLedger(stateFilePath, ledger);
4828
+ }
4829
+ await secretStore.set(to, stampField, stamp);
4830
+ await secretStore.set(to, field, value);
4831
+ } catch (error) {
4832
+ carried = false;
4833
+ warnLedgerFailure(
4834
+ "carry over a superseded namespace recorded in",
4835
+ error
4836
+ );
4837
+ }
4838
+ }
4839
+ }
4840
+ return carried;
4841
+ }
4842
+ async function recordedNamespaces(stateFilePath) {
4843
+ try {
4844
+ const ledger = parseLedger(
4845
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
4846
+ );
4847
+ return [...ledger.keys()].reverse();
4848
+ } catch (error) {
4849
+ warnLedgerFailure("read", error);
4850
+ return [];
4851
+ }
4852
+ }
4853
+ async function purgeSupersededNamespaces(stateFilePath, secretStore, current, owners = /* @__PURE__ */ new Map()) {
4491
4854
  let ledger;
4492
4855
  let location;
4493
4856
  try {
@@ -4500,8 +4863,22 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
4500
4863
  return;
4501
4864
  }
4502
4865
  let changed = false;
4503
- for (const [namespace, byLocation] of ledger) {
4866
+ for (const [namespace, byLocation] of [...ledger].reverse()) {
4504
4867
  if (namespace === current) continue;
4868
+ if (current !== void 0 && coveredLocations(location, byLocation).some(
4869
+ ({ keys }) => keys.owedTo === current
4870
+ ) && !await carryOwed(
4871
+ stateFilePath,
4872
+ ledger,
4873
+ location,
4874
+ secretStore,
4875
+ namespace,
4876
+ current,
4877
+ candidateFields(location, byLocation),
4878
+ owners
4879
+ )) {
4880
+ break;
4881
+ }
4505
4882
  for (const { location: recorded, keys } of coveredLocations(
4506
4883
  location,
4507
4884
  byLocation
@@ -4526,12 +4903,11 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
4526
4903
  if (!purged || recorded !== location) continue;
4527
4904
  byLocation.delete(recorded);
4528
4905
  if (recorded.startsWith(FILE_LOCATION_PREFIX)) {
4529
- const keyring = byLocation.get(KEYRING_LOCATION) ?? {
4530
- servers: /* @__PURE__ */ new Set(),
4531
- idpSessions: /* @__PURE__ */ new Set()
4532
- };
4906
+ const keyring = byLocation.get(KEYRING_LOCATION) ?? emptyKeys();
4907
+ addMissingOwners(keyring.owners, keys.owners);
4533
4908
  for (const url of keys.servers) keyring.servers.add(url);
4534
4909
  for (const issuer of keys.idpSessions) keyring.idpSessions.add(issuer);
4910
+ keyring.owedTo ??= keys.owedTo;
4535
4911
  byLocation.set(KEYRING_LOCATION, keyring);
4536
4912
  }
4537
4913
  changed = true;
@@ -4642,7 +5018,12 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4642
5018
  const existing = parseSecretsNamespace(raw);
4643
5019
  if (existing !== void 0) {
4644
5020
  if (locked) {
4645
- await purgeSupersededNamespaces(filePath, secretStore, existing);
5021
+ await purgeSupersededNamespaces(
5022
+ filePath,
5023
+ secretStore,
5024
+ existing,
5025
+ snapshotClientOwners(parseOAuthPersistBlob(raw))
5026
+ );
4646
5027
  }
4647
5028
  return existing;
4648
5029
  }
@@ -4650,42 +5031,53 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4650
5031
  if (raw !== null && snapshot === null) {
4651
5032
  throw new OAuthStateFileUnrecognizedError(filePath, "save");
4652
5033
  }
4653
- if (locked) {
4654
- await purgeSupersededNamespaces(filePath, secretStore, void 0);
4655
- }
4656
5034
  const namespace = newSecretsNamespace();
4657
- if (snapshot === null) return namespace;
4658
- const moves = [
4659
- ...Object.entries(snapshot.servers).map(([url, state]) => ({
4660
- legacyId: oauthSecretServerId(url),
4661
- scopedId: oauthSecretServerId(url, namespace),
4662
- fields: serverSecretFields(state)
4663
- })),
4664
- ...Object.keys(snapshot.idpSessions).map((issuer) => ({
4665
- legacyId: oauthIdpSecretServerId(issuer),
4666
- scopedId: oauthIdpSecretServerId(issuer, namespace),
4667
- fields: [IDP_SESSION_FIELD]
4668
- }))
4669
- ];
4670
- if (moves.length === 0) return namespace;
5035
+ if (snapshot === null || Object.keys(snapshot.servers).length === 0 && Object.keys(snapshot.idpSessions).length === 0) {
5036
+ if (locked) {
5037
+ await purgeSupersededNamespaces(filePath, secretStore, void 0);
5038
+ }
5039
+ return namespace;
5040
+ }
4671
5041
  if (!locked) {
4672
5042
  throw new SecretStoreUnavailableError(
4673
5043
  `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).`
4674
5044
  );
4675
5045
  }
5046
+ const values = await readEntrySecrets(
5047
+ snapshot,
5048
+ secretStore,
5049
+ void 0,
5050
+ await recordedNamespaces(filePath)
5051
+ );
5052
+ const owners = snapshotClientOwners(snapshot);
4676
5053
  await recordNamespaceKeys(
4677
5054
  filePath,
4678
5055
  secretStore,
4679
5056
  namespace,
4680
5057
  Object.keys(snapshot.servers),
4681
- Object.keys(snapshot.idpSessions)
5058
+ Object.keys(snapshot.idpSessions),
5059
+ writtenClientOwners(snapshot, (url) => values.servers.get(url) ?? {})
4682
5060
  );
5061
+ await markUncarriedNamespaces(filePath, secretStore, namespace);
4683
5062
  const touched = [];
4684
5063
  try {
4685
- for (const { legacyId, scopedId, fields } of moves) {
4686
- for (const field of fields) {
4687
- const value = await secretStoreGetStrict(secretStore, legacyId, field);
4688
- if (value === null) continue;
5064
+ const copies = [
5065
+ // Restamped: a value read from the legacy ids predates stamps, and a
5066
+ // fallback's stamp may belong to a value read from elsewhere (#2734).
5067
+ ...[...values.servers].map(([url, secrets]) => ({
5068
+ scopedId: oauthSecretServerId(url, namespace),
5069
+ secrets: restampClientCredentials(
5070
+ getOwnEntry(snapshot.servers, url) ?? {},
5071
+ secrets
5072
+ )
5073
+ })),
5074
+ ...[...values.idpSessions].map(([issuer, secrets]) => ({
5075
+ scopedId: oauthIdpSecretServerId(issuer, namespace),
5076
+ secrets
5077
+ }))
5078
+ ];
5079
+ for (const { scopedId, secrets } of copies) {
5080
+ for (const [field, value] of Object.entries(secrets)) {
4689
5081
  touched.push({ serverId: scopedId, field, value: null });
4690
5082
  await secretStore.set(scopedId, field, value);
4691
5083
  }
@@ -4695,7 +5087,14 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4695
5087
  await restoreSecretFields(secretStore, touched, warnRestoreFailure);
4696
5088
  throw error;
4697
5089
  }
4698
- for (const { legacyId } of moves) {
5090
+ await purgeSupersededNamespaces(filePath, secretStore, namespace, owners);
5091
+ const legacyIds = [
5092
+ ...Object.keys(snapshot.servers).map((url) => oauthSecretServerId(url)),
5093
+ ...Object.keys(snapshot.idpSessions).map(
5094
+ (issuer) => oauthIdpSecretServerId(issuer)
5095
+ )
5096
+ ];
5097
+ for (const legacyId of legacyIds) {
4699
5098
  try {
4700
5099
  await secretStore.deleteAllForServer(legacyId);
4701
5100
  } catch (error) {
@@ -4748,6 +5147,14 @@ function preserveNonDurableSecrets(diskSecrets, secrets) {
4748
5147
  return keep;
4749
5148
  }
4750
5149
  async function writeOAuthSections(filePath, snapshot, sections, secretStore = defaultSecretStore()) {
5150
+ await writeOAuthSectionsFrom(
5151
+ filePath,
5152
+ async () => snapshot,
5153
+ sections,
5154
+ secretStore
5155
+ );
5156
+ }
5157
+ async function writeOAuthSectionsFrom(filePath, resolveSnapshot, sections, secretStore) {
4751
5158
  const policy = getPersistTokensPolicy();
4752
5159
  const durable = await secretStoreIsDurable(secretStore);
4753
5160
  await withOAuthStateLock(filePath, "save", async (locked) => {
@@ -4831,6 +5238,7 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
4831
5238
  unconfirmed = null;
4832
5239
  namespace = diskNamespace;
4833
5240
  }
5241
+ const snapshot = await resolveSnapshot(disk, namespace);
4834
5242
  const effective = sections ? {
4835
5243
  servers: sections.servers && [...new Set(sections.servers)],
4836
5244
  idpSessions: sections.idpSessions && [
@@ -4856,7 +5264,22 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
4856
5264
  secretStore,
4857
5265
  namespace,
4858
5266
  Object.keys(merged.servers),
4859
- Object.keys(merged.idpSessions)
5267
+ Object.keys(merged.idpSessions),
5268
+ // Only the entries this save writes: another entry's secret in
5269
+ // this store was written for whatever client it was written for,
5270
+ // and relabeling it from the file would misattribute it (#2672).
5271
+ writtenClientOwners(
5272
+ {
5273
+ servers: Object.fromEntries(
5274
+ (effective.servers ?? []).flatMap((url) => {
5275
+ const entry = getOwnEntry(merged.servers, url);
5276
+ return entry === void 0 ? [] : [[url, entry]];
5277
+ })
5278
+ ),
5279
+ idpSessions: {}
5280
+ },
5281
+ splitSecretsOf
5282
+ )
4860
5283
  );
4861
5284
  for (const url of effective.servers ?? []) {
4862
5285
  const serverId = oauthSecretServerId(url, namespace);
@@ -4864,7 +5287,8 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
4864
5287
  const candidates = [
4865
5288
  .../* @__PURE__ */ new Set([
4866
5289
  ...serverSecretFields(getOwnEntry(disk?.servers, url)),
4867
- ...serverSecretFields(next)
5290
+ ...serverSecretFields(next),
5291
+ ...[...restoreBaseline.values()].filter((prior) => prior.serverId === serverId).map(({ field }) => field)
4868
5292
  ])
4869
5293
  ];
4870
5294
  const entryPrior = await snapshotSecretFields(
@@ -4873,8 +5297,12 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
4873
5297
  candidates
4874
5298
  );
4875
5299
  priorSecrets.push(...entryPrior);
5300
+ const entryPriorValue = new Map(
5301
+ entryPrior.map(({ field, value }) => [field, value])
5302
+ );
4876
5303
  if (next === void 0) {
4877
5304
  for (const field of candidates) {
5305
+ if (entryPriorValue.get(field) === null) continue;
4878
5306
  ourWrites.set(priorKey({ serverId, field }), null);
4879
5307
  }
4880
5308
  await secretStore.deleteAllForServer(serverId);
@@ -4889,10 +5317,9 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
4889
5317
  }
4890
5318
  const { residue, secrets } = splitServerOAuthState(next, policy);
4891
5319
  for (const field of candidates) {
4892
- ourWrites.set(
4893
- priorKey({ serverId, field }),
4894
- secrets[field] ?? null
4895
- );
5320
+ const value = secrets[field] ?? null;
5321
+ if (value === entryPriorValue.get(field)) continue;
5322
+ ourWrites.set(priorKey({ serverId, field }), value);
4896
5323
  }
4897
5324
  setOwnEntry(merged.servers, url, residue);
4898
5325
  if (!durable) {
@@ -5035,26 +5462,109 @@ function secretRequestsFor(snapshot, namespace) {
5035
5462
  }
5036
5463
  return requests;
5037
5464
  }
5038
- async function joinSnapshot(snapshot, secretStore, namespace) {
5039
- const requests = secretRequestsFor(snapshot, namespace);
5040
- const values = requests.length > 0 ? await secretStoreGetManyStrict(secretStore, requests) : {};
5465
+ function fillMissingSecrets(target, source) {
5466
+ for (const [field, value] of Object.entries(source ?? {})) {
5467
+ if (!Object.hasOwn(target, field)) setOwnEntry(target, field, value);
5468
+ }
5469
+ }
5470
+ function writtenClientOwners(snapshot, secretsOf) {
5471
+ const owners = /* @__PURE__ */ new Map();
5472
+ for (const [url, state] of Object.entries(snapshot.servers)) {
5473
+ const secrets = secretsOf(url, state);
5474
+ const fields = new Map(
5475
+ [...clientCredentialOwners(state)].filter(
5476
+ ([field]) => Object.hasOwn(secrets, field)
5477
+ )
5478
+ );
5479
+ if (fields.size > 0) owners.set(url, fields);
5480
+ }
5481
+ return owners;
5482
+ }
5483
+ var splitSecretsOf = (_url, state) => splitServerOAuthState(state, "all").secrets;
5484
+ function snapshotClientOwners(snapshot) {
5485
+ const owners = /* @__PURE__ */ new Map();
5486
+ for (const [url, state] of Object.entries(snapshot?.servers ?? {})) {
5487
+ const fields = clientCredentialOwners(state);
5488
+ if (fields.size > 0) owners.set(url, fields);
5489
+ }
5490
+ return owners;
5491
+ }
5492
+ async function readEntrySecrets(snapshot, secretStore, namespace, fallbacks, carries = []) {
5493
+ const servers = /* @__PURE__ */ new Map();
5494
+ const idpSessions = /* @__PURE__ */ new Map();
5495
+ for (const url of Object.keys(snapshot.servers)) servers.set(url, {});
5496
+ for (const issuer of Object.keys(snapshot.idpSessions)) {
5497
+ idpSessions.set(issuer, {});
5498
+ }
5499
+ for (const source of [namespace, ...fallbacks]) {
5500
+ const requests = secretRequestsFor(snapshot, source);
5501
+ if (requests.length === 0) break;
5502
+ const values = await secretStoreGetManyStrict(secretStore, requests);
5503
+ for (const [url, target] of servers) {
5504
+ fillMissingSecrets(target, values[oauthSecretServerId(url, source)]);
5505
+ }
5506
+ for (const [issuer, target] of idpSessions) {
5507
+ fillMissingSecrets(
5508
+ target,
5509
+ values[oauthIdpSecretServerId(issuer, source)]
5510
+ );
5511
+ }
5512
+ }
5513
+ const owners = snapshotClientOwners(snapshot);
5514
+ for (const { namespace: from, fields: recorded } of carries) {
5515
+ const requests = [];
5516
+ for (const [url, fields] of recorded) {
5517
+ const target = servers.get(url);
5518
+ if (!target) continue;
5519
+ const wanted = [...fields].filter(
5520
+ (field) => owners.get(url)?.has(field) === true && !Object.hasOwn(target, field)
5521
+ );
5522
+ if (wanted.length > 0) {
5523
+ requests.push({
5524
+ serverId: oauthSecretServerId(url, from),
5525
+ fields: wanted.flatMap((field) => [
5526
+ field,
5527
+ clientCredentialStampField(field)
5528
+ ])
5529
+ });
5530
+ }
5531
+ }
5532
+ if (requests.length === 0) continue;
5533
+ const values = await secretStoreGetManyStrict(secretStore, requests);
5534
+ for (const [url, target] of servers) {
5535
+ const found = values[oauthSecretServerId(url, from)] ?? {};
5536
+ for (const [field, clientId] of owners.get(url) ?? []) {
5537
+ const value = Object.hasOwn(found, field) ? found[field] : void 0;
5538
+ const stampField = clientCredentialStampField(field);
5539
+ if (value === void 0 || Object.hasOwn(target, field) || stampedOwner(found[stampField], value) !== clientId) {
5540
+ continue;
5541
+ }
5542
+ setOwnEntry(target, field, value);
5543
+ setOwnEntry(target, stampField, found[stampField]);
5544
+ }
5545
+ }
5546
+ }
5547
+ return { servers, idpSessions };
5548
+ }
5549
+ async function joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries = []) {
5550
+ const values = await readEntrySecrets(
5551
+ snapshot,
5552
+ secretStore,
5553
+ namespace,
5554
+ fallbacks,
5555
+ carries
5556
+ );
5041
5557
  return {
5042
5558
  servers: Object.fromEntries(
5043
5559
  Object.entries(snapshot.servers).map(([url, state]) => [
5044
5560
  url,
5045
- joinServerOAuthState(
5046
- state,
5047
- values[oauthSecretServerId(url, namespace)] ?? {}
5048
- )
5561
+ joinServerOAuthState(state, values.servers.get(url) ?? {})
5049
5562
  ])
5050
5563
  ),
5051
5564
  idpSessions: Object.fromEntries(
5052
5565
  Object.entries(snapshot.idpSessions).map(([issuer, session]) => [
5053
5566
  issuer,
5054
- joinIdpSession(
5055
- session,
5056
- values[oauthIdpSecretServerId(issuer, namespace)] ?? {}
5057
- )
5567
+ joinIdpSession(session, values.idpSessions.get(issuer) ?? {})
5058
5568
  ])
5059
5569
  )
5060
5570
  };
@@ -5070,7 +5580,8 @@ async function migratePlaintextSecrets(filePath, secretStore) {
5070
5580
  secretStore,
5071
5581
  namespace,
5072
5582
  Object.keys(fresh.servers),
5073
- Object.keys(fresh.idpSessions)
5583
+ Object.keys(fresh.idpSessions),
5584
+ writtenClientOwners(fresh, splitSecretsOf)
5074
5585
  );
5075
5586
  }
5076
5587
  const migrateEntrySecrets = async (serverId, secrets) => {
@@ -5121,7 +5632,10 @@ async function readOAuthStore(filePath, secretStore = defaultSecretStore()) {
5121
5632
  warnMigrationFailure(error);
5122
5633
  }
5123
5634
  }
5124
- return joinSnapshot(snapshot, secretStore, parseSecretsNamespace(raw));
5635
+ const namespace = parseSecretsNamespace(raw);
5636
+ const fallbacks = namespace === void 0 ? await recordedNamespaces(filePath) : [];
5637
+ const carries = namespace === void 0 ? [] : await pendingCarries(filePath, secretStore, namespace);
5638
+ return joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries);
5125
5639
  });
5126
5640
  }
5127
5641
  async function removeOAuthStore(filePath, secretStore = defaultSecretStore()) {
@@ -5748,6 +6262,11 @@ var ManagedListState = class extends TypedEventTarget {
5748
6262
  // rendered by the list panel instead of vanishing into an unhandled
5749
6263
  // rejection — which read as "this server has no tools" (#1953).
5750
6264
  error = null;
6265
+ // Whether a fetch has committed a list this session. Until it has, the empty
6266
+ // `items` mean "not loaded yet", not "the server has none" — a distinction a
6267
+ // count of the list cannot make from `items` alone (#2607). Set and cleared
6268
+ // just before the `*Change` dispatch, so a reader keyed on that event sees it.
6269
+ loaded = false;
5751
6270
  config;
5752
6271
  // Debounce a burst of `list_changed` notifications into a single
5753
6272
  // refresh (or one indicator light) once it settles.
@@ -5783,6 +6302,7 @@ var ManagedListState = class extends TypedEventTarget {
5783
6302
  this.listChangedTimer = null;
5784
6303
  }
5785
6304
  this.items = [];
6305
+ this.loaded = false;
5786
6306
  this.dispatchChange();
5787
6307
  this.setListChanged(false);
5788
6308
  this.setError(null);
@@ -5841,6 +6361,14 @@ var ManagedListState = class extends TypedEventTarget {
5841
6361
  getItems() {
5842
6362
  return [...this.items];
5843
6363
  }
6364
+ /**
6365
+ * Whether a fetch has committed a list since the last connect. False while
6366
+ * the connect-time load is in flight, when it was deferred (paginated mode),
6367
+ * and after a disconnect. Changes only alongside the `*Change` event (#2607).
6368
+ */
6369
+ isLoaded() {
6370
+ return this.loaded;
6371
+ }
5844
6372
  /** Whether a `list_changed` arrived since the last refresh (indicator on). */
5845
6373
  getListChanged() {
5846
6374
  return this.listChanged;
@@ -5934,6 +6462,7 @@ var ManagedListState = class extends TypedEventTarget {
5934
6462
  /** Commit a fetched list as the current one and notify subscribers. */
5935
6463
  applyItems(items) {
5936
6464
  this.items = items;
6465
+ this.loaded = true;
5937
6466
  this.dispatchChange();
5938
6467
  }
5939
6468
  dispatchChange() {
@@ -5944,6 +6473,7 @@ var ManagedListState = class extends TypedEventTarget {
5944
6473
  this.unsubscribe?.();
5945
6474
  this.unsubscribe = null;
5946
6475
  this.items = [];
6476
+ this.loaded = false;
5947
6477
  }
5948
6478
  };
5949
6479
 
@@ -6027,6 +6557,16 @@ var MessageLogState = class extends TypedEventTarget {
6027
6557
  return;
6028
6558
  }
6029
6559
  };
6560
+ const onRequestFailed = (event) => {
6561
+ const { id, reason } = event.detail;
6562
+ const requestEntry = this.pendingRequestEntries.get(id);
6563
+ if (!requestEntry) return;
6564
+ this.pendingRequestEntries.delete(id);
6565
+ requestEntry.clientError = reason;
6566
+ requestEntry.duration = Date.now() - requestEntry.timestamp.getTime();
6567
+ this.dispatchTypedEvent("message", requestEntry);
6568
+ this.dispatchTypedEvent("messagesChange", this.getMessages());
6569
+ };
6030
6570
  const onStatusChange = () => {
6031
6571
  if (isTerminalStatus(this.client?.getStatus())) {
6032
6572
  this.messages = [];
@@ -6036,11 +6576,13 @@ var MessageLogState = class extends TypedEventTarget {
6036
6576
  };
6037
6577
  this.client.addEventListener("message", onMessage);
6038
6578
  this.client.addEventListener("responseRejected", onResponseRejected);
6579
+ this.client.addEventListener("requestFailed", onRequestFailed);
6039
6580
  this.client.addEventListener("statusChange", onStatusChange);
6040
6581
  this.unsubscribe = () => {
6041
6582
  if (this.client) {
6042
6583
  this.client.removeEventListener("message", onMessage);
6043
6584
  this.client.removeEventListener("responseRejected", onResponseRejected);
6585
+ this.client.removeEventListener("requestFailed", onRequestFailed);
6044
6586
  this.client.removeEventListener("statusChange", onStatusChange);
6045
6587
  }
6046
6588
  this.client = null;
@@ -6400,6 +6942,12 @@ var DirectoryReadResultSchema = z2.looseObject({
6400
6942
  resources: z2.array(DirectoryChildSchema),
6401
6943
  nextCursor: z2.string().optional()
6402
6944
  });
6945
+ var ModernDirectoryReadResultSchema = DirectoryReadResultSchema.extend(
6946
+ {
6947
+ ttlMs: z2.int().min(0),
6948
+ cacheScope: z2.enum(["public", "private"])
6949
+ }
6950
+ );
6403
6951
 
6404
6952
  // ../../core/mcp/state/managedSkillsState.ts
6405
6953
  var REPEATED_CURSOR_MESSAGE = "Server repeated a pagination cursor in skills/list; stopped to avoid an infinite walk.";
@@ -7212,6 +7760,10 @@ function checkSkillNameCollisions(entries) {
7212
7760
  }
7213
7761
 
7214
7762
  // ../../core/mcp/skillsVerification.ts
7763
+ var DEFAULT_SKILL_BUDGET_HINT = {
7764
+ raise: "with the server's skillCatalogMaxSkills / skillCatalogMaxBytes settings",
7765
+ verifyOne: "by verifying this skill alone"
7766
+ };
7215
7767
  function reasonOf(err) {
7216
7768
  return err instanceof Error ? err.message : String(err);
7217
7769
  }
@@ -7273,7 +7825,7 @@ function responseBytes(result) {
7273
7825
  }
7274
7826
  return total;
7275
7827
  }
7276
- async function verifySkills(client, entries, metadata) {
7828
+ async function verifySkills(client, entries, metadata, budgetHint = DEFAULT_SKILL_BUDGET_HINT) {
7277
7829
  const collisions = checkSkillNameCollisions(entries);
7278
7830
  const reports = [];
7279
7831
  const budget = resolveSkillCatalogBudget(client.getServerSettings?.());
@@ -7285,7 +7837,7 @@ async function verifySkills(client, entries, metadata) {
7285
7837
  const files = [];
7286
7838
  const declared = entry.resources === DYNAMIC_RESOURCES ? [] : entry.resources;
7287
7839
  const manifest = withinBudget ? boundedManifest(declared) : [];
7288
- 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;
7840
+ 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;
7289
7841
  let receivedBytes = 0;
7290
7842
  const entryIdentity = skillUriIdentity(entry.uri);
7291
7843
  let selfAttempted = false;
@@ -7423,6 +7975,14 @@ function anySkillUnverifiable(reports) {
7423
7975
  }
7424
7976
 
7425
7977
  // ../../core/cli/handlers/skills-verify.ts
7978
+ var CLI_SKILL_BUDGET_HINT = {
7979
+ raise: "with --skill-catalog-max-skills / --skill-catalog-max-bytes, or the server's skillCatalogMaxSkills / skillCatalogMaxBytes",
7980
+ verifyOne: "with `--method skills/get --uri <uri> --verify`, where <uri> is this report's uri,"
7981
+ };
7982
+ var MCPDO_SKILL_BUDGET_HINT = {
7983
+ raise: "with skillCatalogMaxSkills / skillCatalogMaxBytes in the connection's catalog entry",
7984
+ verifyOne: "with `mcpdo @<conn> skills/get <uri> --verify`, where <uri> is this report's uri,"
7985
+ };
7426
7986
  function skillVerificationExitCode(reports, requireDigests) {
7427
7987
  if (allSkillsVerified(reports)) return void 0;
7428
7988
  if (anySkillFailed(reports)) return EXIT_CODES.SKILL_NONCONFORMANT;
@@ -7474,7 +8034,8 @@ function assertSkillsSupported(inspectorClient, method) {
7474
8034
  }
7475
8035
  }
7476
8036
  var resourceStreamRefs = /* @__PURE__ */ new WeakMap();
7477
- async function runMethod(inspectorClient, args) {
8037
+ async function runMethod(inspectorClient, args, options = {}) {
8038
+ const skillBudgetHint = options.skillBudgetHint ?? CLI_SKILL_BUDGET_HINT;
7478
8039
  let managedToolsState = null;
7479
8040
  let managedResourcesState = null;
7480
8041
  let managedResourceTemplatesState = null;
@@ -7539,7 +8100,8 @@ async function runMethod(inspectorClient, args) {
7539
8100
  if (args.appInfo) {
7540
8101
  result = { ...appInfo };
7541
8102
  } else {
7542
- const invocation = args.task ? await inspectorClient.callToolStream(
8103
+ const useTask = args.task === true || tool.execution?.taskSupport === "required";
8104
+ const invocation = useTask ? await inspectorClient.callToolStream(
7543
8105
  tool,
7544
8106
  args.toolArg || {},
7545
8107
  args.metadata,
@@ -7770,7 +8332,8 @@ async function runMethod(inspectorClient, args) {
7770
8332
  const reports = await verifySkills(
7771
8333
  inspectorClient,
7772
8334
  skills,
7773
- args.metadata
8335
+ args.metadata,
8336
+ skillBudgetHint
7774
8337
  );
7775
8338
  return {
7776
8339
  kind: "ndjson",
@@ -7799,7 +8362,8 @@ async function runMethod(inspectorClient, args) {
7799
8362
  const reports = await verifySkills(
7800
8363
  inspectorClient,
7801
8364
  [skill],
7802
- args.metadata
8365
+ args.metadata,
8366
+ skillBudgetHint
7803
8367
  );
7804
8368
  return {
7805
8369
  kind: "ndjson",
@@ -9636,6 +10200,23 @@ function subscriptionFailureMessage(error) {
9636
10200
  return error instanceof Error ? error.message : String(error);
9637
10201
  }
9638
10202
 
10203
+ // ../../core/mcp/subscriptionRefusal.ts
10204
+ import { SdkErrorCode as SdkErrorCode3, SdkHttpError } from "@modelcontextprotocol/client";
10205
+ var REFUSAL_STATUSES = /* @__PURE__ */ new Set([400, 403, 404, 405]);
10206
+ function subscriptionListenRefusal(error) {
10207
+ if (!SdkHttpError.isInstance(error) || error.code !== SdkErrorCode3.ClientHttpNotImplemented || !REFUSAL_STATUSES.has(error.status)) {
10208
+ return void 0;
10209
+ }
10210
+ return {
10211
+ status: error.status,
10212
+ ...error.statusText ? { statusText: error.statusText } : {}
10213
+ };
10214
+ }
10215
+ function subscriptionListenRefusalMessage(refusal) {
10216
+ const status = refusal.statusText ? `${refusal.status} ${refusal.statusText}` : String(refusal.status);
10217
+ return `The server refused subscriptions/listen (HTTP ${status}). Not retrying \u2014 the server would answer the same way again.`;
10218
+ }
10219
+
9639
10220
  // ../../core/mcp/messageTrackingTransport.ts
9640
10221
  var MessageTrackingTransport = class {
9641
10222
  baseTransport;
@@ -9865,7 +10446,7 @@ var TaskProgressRouter = class {
9865
10446
  };
9866
10447
 
9867
10448
  // ../../core/extension/tasks/rawWireChannel.ts
9868
- import { SdkError as SdkError3, SdkErrorCode as SdkErrorCode3 } from "@modelcontextprotocol/client";
10449
+ import { SdkError as SdkError3, SdkErrorCode as SdkErrorCode4 } from "@modelcontextprotocol/client";
9869
10450
  import { DispatchError as DispatchError2 } from "@modelcontextprotocol/ext-tasks/client";
9870
10451
  var RAW_WIRE_ID_PREFIX = "inspector-ext-";
9871
10452
  var RawWireChannel = class {
@@ -9938,8 +10519,8 @@ var RawWireChannel = class {
9938
10519
  `Raw MCP request "${message.method}" timed out after ${timeoutMs} ms`,
9939
10520
  false,
9940
10521
  {
9941
- cause: this.host.annotateTimeout(
9942
- new SdkError3(SdkErrorCode3.RequestTimeout, "Request timed out", {
10522
+ cause: this.host.annotateError(
10523
+ new SdkError3(SdkErrorCode4.RequestTimeout, "Request timed out", {
9943
10524
  timeout: timeoutMs
9944
10525
  }),
9945
10526
  message.method
@@ -9979,7 +10560,7 @@ var RawWireChannel = class {
9979
10560
  if (!pending) return;
9980
10561
  pending.cleanup();
9981
10562
  reject(
9982
- this.host.annotateTimeout(
10563
+ this.host.annotateError(
9983
10564
  error instanceof Error ? error : new Error(String(error)),
9984
10565
  message.method
9985
10566
  )
@@ -10712,12 +11293,33 @@ var ToolCallCancelledError = class extends Error {
10712
11293
  };
10713
11294
 
10714
11295
  // ../../core/mcp/connectionDiagnostics.ts
10715
- import { SdkError as SdkError4, SdkErrorCode as SdkErrorCode4 } from "@modelcontextprotocol/client";
11296
+ import { SdkError as SdkError4, SdkErrorCode as SdkErrorCode5 } from "@modelcontextprotocol/client";
10716
11297
  function isRequestTimeoutData(value) {
10717
11298
  return typeof value === "object" && value !== null && typeof value.timeout === "number";
10718
11299
  }
10719
11300
  function isRequestTimeoutError(err) {
10720
- return SdkError4.isInstance(err) && err.code === SdkErrorCode4.RequestTimeout && isRequestTimeoutData(err.data);
11301
+ return SdkError4.isInstance(err) && err.code === SdkErrorCode5.RequestTimeout && isRequestTimeoutData(err.data);
11302
+ }
11303
+ function describeSendFailure(err) {
11304
+ const message = err instanceof Error ? err.message : String(err);
11305
+ const status = httpStatusOf(err);
11306
+ if (status === void 0 || message.includes(`HTTP ${status}`)) {
11307
+ return message;
11308
+ }
11309
+ const statusText = httpStatusTextOf(err);
11310
+ return `HTTP ${status}${statusText ? ` ${statusText}` : ""}: ${message}`;
11311
+ }
11312
+ function httpStatusOf(err) {
11313
+ if (typeof err !== "object" || err === null || !("status" in err)) {
11314
+ return void 0;
11315
+ }
11316
+ return typeof err.status === "number" ? err.status : void 0;
11317
+ }
11318
+ function httpStatusTextOf(err) {
11319
+ if (typeof err !== "object" || err === null || !("statusText" in err)) {
11320
+ return void 0;
11321
+ }
11322
+ return typeof err.statusText === "string" ? err.statusText : void 0;
10721
11323
  }
10722
11324
  function formatDuration(ms) {
10723
11325
  const clamped = Math.max(0, ms);
@@ -10779,11 +11381,108 @@ function annotateRequestTimeout(err, method, diagnostics) {
10779
11381
  diagnostics
10780
11382
  };
10781
11383
  const message = `Request timed out after ${formatDuration(err.data.timeout)} (${method}). ${describeConnectionDiagnostics(diagnostics)}`;
10782
- const annotated = new SdkError4(SdkErrorCode4.RequestTimeout, message, data);
11384
+ const annotated = new SdkError4(SdkErrorCode5.RequestTimeout, message, data);
10783
11385
  annotated.cause = err;
10784
11386
  return annotated;
10785
11387
  }
10786
11388
 
11389
+ // ../../core/mcp/rateLimit.ts
11390
+ import {
11391
+ SdkHttpError as SdkHttpError2
11392
+ } from "@modelcontextprotocol/client";
11393
+ var HTTP_TOO_MANY_REQUESTS = 429;
11394
+ function requestIdsOf(body) {
11395
+ if (body === void 0) return [];
11396
+ let parsed;
11397
+ try {
11398
+ parsed = JSON.parse(body);
11399
+ } catch {
11400
+ return [];
11401
+ }
11402
+ const messages = Array.isArray(parsed) ? parsed : [parsed];
11403
+ const ids = [];
11404
+ for (const message of messages) {
11405
+ if (typeof message !== "object" || message === null) continue;
11406
+ if (!("method" in message) || !("id" in message)) continue;
11407
+ const { id } = message;
11408
+ if (typeof id === "string" || typeof id === "number") ids.push(id);
11409
+ }
11410
+ return ids;
11411
+ }
11412
+ var MAX_UNCLAIMED_RATE_LIMITS = 100;
11413
+ var RateLimitLedger = class {
11414
+ // `undefined` is a real value here: a 429 that carried no header.
11415
+ byRequestId = /* @__PURE__ */ new Map();
11416
+ /** File a tracked fetch, if it was a 429. Anything else is ignored. */
11417
+ record(entry) {
11418
+ if (entry.responseStatus !== HTTP_TOO_MANY_REQUESTS) return;
11419
+ const retryAfter = findHeader(entry.responseHeaders, "retry-after");
11420
+ for (const id of requestIdsOf(entry.requestBody)) {
11421
+ this.byRequestId.delete(id);
11422
+ this.byRequestId.set(id, retryAfter);
11423
+ if (this.byRequestId.size > MAX_UNCLAIMED_RATE_LIMITS) {
11424
+ const [oldest] = this.byRequestId.keys();
11425
+ if (oldest !== void 0) this.byRequestId.delete(oldest);
11426
+ }
11427
+ }
11428
+ }
11429
+ /** The `Retry-After` filed for `id`, removed as it is read. */
11430
+ take(id) {
11431
+ const retryAfter = this.byRequestId.get(id);
11432
+ this.byRequestId.delete(id);
11433
+ return retryAfter;
11434
+ }
11435
+ clear() {
11436
+ this.byRequestId.clear();
11437
+ }
11438
+ };
11439
+ var IMF_FIXDATE = /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/;
11440
+ function parseRetryAfter(value, now) {
11441
+ if (value === void 0) return void 0;
11442
+ const trimmed = value.trim();
11443
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
11444
+ if (!IMF_FIXDATE.test(trimmed)) return void 0;
11445
+ const at = Date.parse(trimmed);
11446
+ if (Number.isNaN(at) || new Date(at).toUTCString() !== trimmed) {
11447
+ return void 0;
11448
+ }
11449
+ return Math.max(0, at - now);
11450
+ }
11451
+ function isRateLimitError(err) {
11452
+ return SdkHttpError2.isInstance(err) && err.status === HTTP_TOO_MANY_REQUESTS;
11453
+ }
11454
+ function retryAfterOn(err) {
11455
+ const value = err.data.retryAfter;
11456
+ return typeof value === "string" ? value : void 0;
11457
+ }
11458
+ function attachRetryAfter(err, retryAfter) {
11459
+ if (!isRateLimitError(err) || retryAfter === void 0) return;
11460
+ if (retryAfterOn(err) !== void 0) return;
11461
+ err.data.retryAfter = retryAfter;
11462
+ }
11463
+ function describeRetry(retryAfter, retryAfterMs) {
11464
+ if (retryAfterMs !== void 0) {
11465
+ return `The server asked to retry after ${formatDuration(retryAfterMs)}.`;
11466
+ }
11467
+ if (retryAfter !== void 0) {
11468
+ return `The server sent an unparseable Retry-After (${retryAfter}).`;
11469
+ }
11470
+ return "The server sent no Retry-After header.";
11471
+ }
11472
+ function annotateRateLimit(err, method, now = Date.now()) {
11473
+ if (!isRateLimitError(err)) return err;
11474
+ const header = retryAfterOn(err);
11475
+ const retryAfterMs = parseRetryAfter(header, now);
11476
+ const data = {
11477
+ ...err.data,
11478
+ method,
11479
+ ...header !== void 0 && { retryAfter: header },
11480
+ ...retryAfterMs !== void 0 && { retryAfterMs }
11481
+ };
11482
+ const message = `Rate limited by the server (HTTP 429) on ${method}. ${describeRetry(header, retryAfterMs)} ${err.message}`;
11483
+ return new SdkHttpError2(err.code, message, data, { cause: err });
11484
+ }
11485
+
10787
11486
  // ../../core/auth/ema/clientConfigError.ts
10788
11487
  function emaClientNotConfiguredMessage(reason) {
10789
11488
  if (reason === "disabled") {
@@ -10881,10 +11580,10 @@ async function resolveIdpMetadata(issuer, storage, fetchFn) {
10881
11580
  }
10882
11581
  return discoverIdpMetadata(issuer, fetchFn);
10883
11582
  }
10884
- var OIDC_WELL_KNOWN2 = "/.well-known/openid-configuration";
11583
+ var OIDC_WELL_KNOWN3 = "/.well-known/openid-configuration";
10885
11584
  async function fetchOpenIdConfiguration(issuer, issuerUrl, fetchFn) {
10886
11585
  const url = new URL(
10887
- `${issuerUrl.pathname.replace(/\/$/, "")}${OIDC_WELL_KNOWN2}`,
11586
+ `${issuerUrl.pathname.replace(/\/$/, "")}${OIDC_WELL_KNOWN3}`,
10888
11587
  issuerUrl.origin
10889
11588
  );
10890
11589
  try {
@@ -11219,11 +11918,6 @@ async function mintEmaResourceTokens(config, resourceContext) {
11219
11918
  "EMA requires resource authorization server clientId (per-server oauth.clientId)"
11220
11919
  );
11221
11920
  }
11222
- if (!config.resourceClientSecret?.trim()) {
11223
- throw new Error(
11224
- "EMA requires resource authorization server client secret in server OAuth settings (Test Client secret from xaa.dev resource registration)"
11225
- );
11226
- }
11227
11921
  const idToken = await getValidIdToken({
11228
11922
  idp: config.idp,
11229
11923
  storage: config.storage,
@@ -11489,7 +12183,9 @@ var OAuthManager = class {
11489
12183
  requestRefreshToken: this.oauthConfig.requestRefreshToken,
11490
12184
  // #2068: lets the provider tell an `offline_access` the user asked for
11491
12185
  // from one inherited from a previous grant's persisted scope.
11492
- configuredScope: this.oauthConfig.scope
12186
+ configuredScope: this.oauthConfig.scope,
12187
+ // #2726: in-flight PKCE verifiers keyed by flow, not by server URL.
12188
+ flowStore: this.oauthConfig.flowStore
11493
12189
  });
11494
12190
  provider.setEventTarget(this.params.getEventTarget());
11495
12191
  await provider.prepareForAuth();
@@ -11653,7 +12349,12 @@ var OAuthManager = class {
11653
12349
  recordAuthorizationRequestScope(authorizationUrl, requestedScope) {
11654
12350
  this.pendingAuthorizationScope = authorizationUrl.searchParams.get("scope")?.trim() || requestedScope;
11655
12351
  }
11656
- async completeOAuthFlow(authorizationCode, iss) {
12352
+ /**
12353
+ * `state` is the OAuth `state` the callback was redirected back with. It
12354
+ * selects the flow whose PKCE verifier the exchange uses (#2726); without it
12355
+ * a provider with a flow store has no verifier to send.
12356
+ */
12357
+ async completeOAuthFlow(authorizationCode, iss, state) {
11657
12358
  try {
11658
12359
  if (this.isEnterpriseManaged()) {
11659
12360
  const emaConfig = await this.getEmaFlowConfig();
@@ -11677,6 +12378,9 @@ var OAuthManager = class {
11677
12378
  }
11678
12379
  const provider = await this.createOAuthProvider();
11679
12380
  const serverUrl = this.getServerUrl();
12381
+ if (state !== void 0) {
12382
+ provider.resumeAuthorizationFlow(state);
12383
+ }
11680
12384
  const result = await mcpAuth(provider, {
11681
12385
  serverUrl,
11682
12386
  authorizationCode,
@@ -11688,6 +12392,7 @@ var OAuthManager = class {
11688
12392
  `Expected AUTHORIZED after providing authorization code, got: ${result}`
11689
12393
  );
11690
12394
  }
12395
+ await provider.clearAuthorizationFlow();
11691
12396
  const tokens = await provider.tokens();
11692
12397
  if (!tokens) {
11693
12398
  throw new Error("Failed to retrieve tokens after authorization");
@@ -11819,15 +12524,21 @@ var OAuthManager = class {
11819
12524
  * Re-read persisted OAuth state and determine whether `challenge` is already
11820
12525
  * satisfied without an authorization-server round-trip.
11821
12526
  *
11822
- * Returns `true` for `insufficient_scope` when stored + token scope cover the
11823
- * SEP-2350 union. For `token_expired`, returns `true` only when storage holds
11824
- * a *provably* unexpired token — a JWT whose `exp` is still in the future. An
11825
- * opaque token carries no local expiry evidence, so the resource server's
11826
- * verdict stands and re-authorization proceeds. `invalid_token` and
11827
- * `unauthorized` always return `false` — the resource server explicitly
11828
- * rejected the credential.
12527
+ * For `token_expired` and `insufficient_scope`, returns `true` only when
12528
+ * storage holds a *provably* unexpired token — a JWT whose `exp` is still in
12529
+ * the future. An opaque token carries no local expiry evidence, so the
12530
+ * resource server's verdict stands and re-authorization proceeds.
12531
+ * `insufficient_scope` additionally requires stored + token scope to cover
12532
+ * the SEP-2350 union: some resource servers answer an *expired* token with
12533
+ * `insufficient_scope`, and a scope-only check would replay that dead token
12534
+ * instead of refreshing it (#2724, the same gap #2051 closed for
12535
+ * `token_expired`). `invalid_token` and `unauthorized` always return
12536
+ * `false` — the resource server explicitly rejected the credential.
12537
+ *
12538
+ * A caller that has just completed a grant passes `scopeOnly` — see
12539
+ * {@link CheckAuthChallengeSatisfiedOptions}.
11829
12540
  */
11830
- async checkAuthChallengeSatisfied(challenge) {
12541
+ async checkAuthChallengeSatisfied(challenge, options) {
11831
12542
  const storage = this.oauthConfig.storage;
11832
12543
  if (!storage) {
11833
12544
  return false;
@@ -11840,6 +12551,14 @@ var OAuthManager = class {
11840
12551
  if (challenge.reason !== "insufficient_scope") {
11841
12552
  return challenge.reason === "token_expired" && isAccessTokenProvablyUnexpired(tokens);
11842
12553
  }
12554
+ if (!options?.scopeOnly && !isAccessTokenProvablyUnexpired(tokens)) {
12555
+ return false;
12556
+ }
12557
+ return this.isStepUpScopeCovered(challenge, storage, tokens);
12558
+ }
12559
+ /** Stored + token scope cover the challenge's SEP-2350 union. */
12560
+ async isStepUpScopeCovered(challenge, storage, tokens) {
12561
+ const serverUrl = this.getServerUrl();
11843
12562
  const enriched = await this.enrichChallengeWithAuthorizationScopes(
11844
12563
  challenge,
11845
12564
  tokens.scope
@@ -11943,14 +12662,14 @@ var OAuthManager = class {
11943
12662
  }
11944
12663
  async handleEnterpriseManagedAuthChallenge(challenge, options) {
11945
12664
  const enriched = await this.enrichChallengeWithAuthorizationScopes(challenge);
11946
- if (enriched.reason === "insufficient_scope" && !options?.confirmedStepUp) {
12665
+ if (enriched.reason === "insufficient_scope" && !options?.confirmedStepUp && !await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
11947
12666
  return { kind: "step_up_confirm", challenge: enriched };
11948
12667
  }
11949
12668
  const config = await this.emaFlowConfigForChallenge(enriched);
11950
12669
  if (enriched.reason === "insufficient_scope") {
11951
12670
  const silent = await trySilentEmaAuth(config);
11952
12671
  if (silent.status === "success") {
11953
- if (await this.checkAuthChallengeSatisfied(enriched)) {
12672
+ if (await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
11954
12673
  const minted = await this.requireStorage().getTokens(
11955
12674
  this.getServerUrl()
11956
12675
  );
@@ -12028,9 +12747,16 @@ var OAuthManager = class {
12028
12747
  fetchFn: this.params.effectiveAuthFetch,
12029
12748
  resourceMetadataUrl,
12030
12749
  ...enriched.reason === "insufficient_scope" && {
12750
+ // Judge coverage the way checkAuthChallengeSatisfied does: a token
12751
+ // response that omitted `scope` granted what was stored, so a
12752
+ // covered challenge on an expired token refreshes rather than
12753
+ // forcing interactive authorization (#2724).
12031
12754
  forceReauthorization: isStrictScopeSuperset(
12032
12755
  scopeForAuth,
12033
- tokens?.scope
12756
+ resolveEffectiveGrantedScope(
12757
+ await this.oauthConfig.storage?.getScope(serverUrl),
12758
+ tokens?.scope
12759
+ )
12034
12760
  )
12035
12761
  }
12036
12762
  });
@@ -12039,7 +12765,7 @@ var OAuthManager = class {
12039
12765
  }
12040
12766
  if (result === "AUTHORIZED") {
12041
12767
  if (enriched.reason === "insufficient_scope") {
12042
- if (await this.checkAuthChallengeSatisfied(enriched)) {
12768
+ if (await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
12043
12769
  const freshTokens = await provider.tokens();
12044
12770
  const scopeToPersist = resolvePersistedScopeAfterGrant(
12045
12771
  freshTokens?.scope,
@@ -12141,7 +12867,7 @@ var OAuthManager = class {
12141
12867
  if (result !== "AUTHORIZED") {
12142
12868
  return null;
12143
12869
  }
12144
- if (await this.checkAuthChallengeSatisfied(enriched)) {
12870
+ if (await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
12145
12871
  const freshTokens = await provider.tokens();
12146
12872
  const scopeToPersist = resolvePersistedScopeAfterGrant(
12147
12873
  freshTokens?.scope,
@@ -12171,7 +12897,11 @@ var OAuthManager = class {
12171
12897
  };
12172
12898
 
12173
12899
  // ../../core/mcp/remote/remoteClientTransport.ts
12174
- import { SdkError as SdkError5, SdkErrorCode as SdkErrorCode5 } from "@modelcontextprotocol/client";
12900
+ import {
12901
+ SdkError as SdkError5,
12902
+ SdkErrorCode as SdkErrorCode6,
12903
+ SdkHttpError as SdkHttpError3
12904
+ } from "@modelcontextprotocol/client";
12175
12905
 
12176
12906
  // ../../core/mcp/remote/types.ts
12177
12907
  function oauthTokensToRemoteAuthState(tokens) {
@@ -12226,6 +12956,18 @@ function isConnectAuthChallenge(json) {
12226
12956
  function isConnectTransportError(json) {
12227
12957
  return typeof json === "object" && json !== null && "ok" in json && json.ok === false && "kind" in json && json.kind === "transport_error";
12228
12958
  }
12959
+ var SDK_ERROR_CODES = new Set(
12960
+ Object.values(SdkErrorCode6)
12961
+ );
12962
+ function toSdkHttpError(message, http) {
12963
+ const code = SDK_ERROR_CODES.has(http.code) ? http.code : SdkErrorCode6.ClientHttpNotImplemented;
12964
+ return new SdkHttpError3(code, message, {
12965
+ status: http.status,
12966
+ ...http.statusText !== void 0 && { statusText: http.statusText },
12967
+ ...http.text !== void 0 && { text: http.text },
12968
+ ...http.retryAfter !== void 0 && { retryAfter: http.retryAfter }
12969
+ });
12970
+ }
12229
12971
  function legacySessionId(json) {
12230
12972
  if (typeof json === "object" && json !== null && "sessionId" in json && typeof json.sessionId === "string" && !("ok" in json)) {
12231
12973
  return json.sessionId;
@@ -12657,7 +13399,7 @@ var RemoteClientTransport = class {
12657
13399
  timer = setTimeout(() => {
12658
13400
  this.sseResponseWaits.delete(requestId);
12659
13401
  reject(
12660
- new SdkError5(SdkErrorCode5.RequestTimeout, "Request timed out", {
13402
+ new SdkError5(SdkErrorCode6.RequestTimeout, "Request timed out", {
12661
13403
  timeout: this.sseResponseTimeoutMs
12662
13404
  })
12663
13405
  );
@@ -12726,6 +13468,9 @@ var RemoteClientTransport = class {
12726
13468
  });
12727
13469
  return;
12728
13470
  }
13471
+ if (json.http) {
13472
+ throw toSdkHttpError(json.error, json.http);
13473
+ }
12729
13474
  throw new Error(json.error);
12730
13475
  }
12731
13476
  async requestSend(message, options) {
@@ -12798,8 +13543,9 @@ var corePackageJson = {
12798
13543
  version: "0.0.0"
12799
13544
  };
12800
13545
  var MAX_URL_ELICITATION_RETRIES = 5;
12801
- function connectionTimeoutMessage(timeoutMs) {
12802
- return `Connection timed out after ${timeoutMs} ms. To accommodate a slower server, you may increase the timeout value in Server Settings.`;
13546
+ var WEB_CONNECTION_TIMEOUT_REMEDY = "To accommodate a slower server, you may increase the timeout value in Server Settings.";
13547
+ function connectionTimeoutMessage(timeoutMs, remedy = WEB_CONNECTION_TIMEOUT_REMEDY) {
13548
+ return `Connection timed out after ${timeoutMs} ms. ${remedy}`;
12803
13549
  }
12804
13550
  function createPendingAbortError() {
12805
13551
  return new Error("Pending request aborted");
@@ -12865,7 +13611,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12865
13611
  transport: () => this.transport,
12866
13612
  defaultTimeoutMs: () => this.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC,
12867
13613
  resetTimeoutOnProgress: () => this.resetTimeoutOnProgress,
12868
- annotateTimeout: (error, method) => annotateRequestTimeout(error, method, this.getConnectionDiagnostics())
13614
+ annotateError: (error, method) => this.annotateRequestError(error, method)
12869
13615
  });
12870
13616
  dispatchTaskRequest = (request, options) => this.rawWire.dispatch(request, options);
12871
13617
  // Every outbound request still awaiting a response, keyed by JSON-RPC id.
@@ -12885,11 +13631,16 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12885
13631
  // `GET` on Streamable HTTP). Both per-session, reset with the maps above.
12886
13632
  lastResponse;
12887
13633
  notificationStream;
13634
+ // Each tracked `429`'s `Retry-After`, by the JSON-RPC id that drew it, until
13635
+ // that id's failed send claims it (#2442). Per-session, reset with the
13636
+ // diagnostics above.
13637
+ rateLimits = new RateLimitLedger();
12888
13638
  /** True when the cached transport was built with an OAuth authProvider attached. */
12889
13639
  transportHasAuthProvider = false;
12890
13640
  /** Dedupes concurrent ambient auth challenges (reason + scopes). */
12891
13641
  ambientAuthChallengeInFlight = /* @__PURE__ */ new Map();
12892
13642
  pipeStderr;
13643
+ connectionTimeoutRemedy;
12893
13644
  initialLoggingLevel;
12894
13645
  // Modern-era per-request log level (#1629). On 2026-07-28 servers
12895
13646
  // `logging/setLevel` is gone; the client opts into logs per request via the
@@ -13028,6 +13779,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13028
13779
  // site to the two failure handlers. Cleared by any user-initiated refresh (a
13029
13780
  // subscribe/unsubscribe is a fresh attempt, and the server may have changed).
13030
13781
  modernNeverAcknowledged = false;
13782
+ // Set when the last `listen()` was refused at the HTTP layer with a status a
13783
+ // retry cannot change (#2722) — the sibling of `modernNeverAcknowledged`,
13784
+ // carried and cleared the same way, holding the HTTP answer the UI reports.
13785
+ modernListenRefusal = null;
13031
13786
  /** Which in-flight task calls own each progress token (#2316). */
13032
13787
  taskProgress = new TaskProgressRouter();
13033
13788
  // Abort controller for the in-flight ordinary (non-task) tool call. Aborting
@@ -13127,6 +13882,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13127
13882
  this.fetchFn = options.environment.fetch;
13128
13883
  this.logger = options.environment.logger ?? silentLogger;
13129
13884
  this.pipeStderr = options.pipeStderr ?? false;
13885
+ this.connectionTimeoutRemedy = options.connectionTimeoutRemedy ?? WEB_CONNECTION_TIMEOUT_REMEDY;
13130
13886
  this.initialLoggingLevel = options.initialLoggingLevel;
13131
13887
  this.sample = options.sample ?? true;
13132
13888
  this.elicit = options.elicit ?? true;
@@ -13284,7 +14040,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13284
14040
  this.clientInfo,
13285
14041
  Object.keys(clientOptions).length > 0 ? clientOptions : void 0
13286
14042
  );
13287
- this.annotateSdkRequestTimeouts(this.client);
14043
+ this.annotateSdkRequestErrors(this.client);
13288
14044
  if (this.tasksCapabilityAdvertised) {
13289
14045
  this.bindReceiverTasks();
13290
14046
  }
@@ -13292,7 +14048,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13292
14048
  /**
13293
14049
  * Decorate the SDK Client's `request` so a per-request timeout rejects with
13294
14050
  * the connection's state at that moment instead of a bare `Request timed
13295
- * out` (#2318).
14051
+ * out` (#2318), and a `429` rejects as rate limiting with the server's
14052
+ * `Retry-After` instead of a bare `Error POSTing to endpoint` (#2442).
13296
14053
  *
13297
14054
  * `Protocol.request` is the one funnel every SDK verb goes through —
13298
14055
  * `listTools`, `callTool`, `readResource`, the handshake's `initialize`,
@@ -13310,21 +14067,30 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13310
14067
  * implementation signature serves both at runtime; the single cast on the
13311
14068
  * way back is what the overloads cost.
13312
14069
  */
13313
- annotateSdkRequestTimeouts(client) {
14070
+ annotateSdkRequestErrors(client) {
13314
14071
  const sdkRequest = client.request.bind(client);
13315
14072
  const decorated = async (...args) => {
13316
14073
  try {
13317
14074
  return await sdkRequest(...args);
13318
14075
  } catch (err) {
13319
- throw annotateRequestTimeout(
13320
- err,
13321
- args[0].method,
13322
- this.getConnectionDiagnostics()
13323
- );
14076
+ throw this.annotateRequestError(err, args[0].method);
13324
14077
  }
13325
14078
  };
13326
14079
  client.request = decorated;
13327
14080
  }
14081
+ /**
14082
+ * The one place a failed request's error is decorated, shared by the SDK
14083
+ * funnel above and the raw-wire channel that bypasses it (modern-era tool
14084
+ * calls and task operations), so a caller sees the same error whichever
14085
+ * path its request took: a timeout with the connection's state (#2318), a
14086
+ * `429` named as rate limiting (#2442), anything else untouched.
14087
+ */
14088
+ annotateRequestError(err, method) {
14089
+ return annotateRateLimit(
14090
+ annotateRequestTimeout(err, method, this.getConnectionDiagnostics()),
14091
+ method
14092
+ );
14093
+ }
13328
14094
  /**
13329
14095
  * @param base - the composed OAuth-path fetch, deadline innermost (#2319).
13330
14096
  *
@@ -13368,9 +14134,14 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13368
14134
  },
13369
14135
  trackSendFailure: (message, error) => {
13370
14136
  if (isRequestTimeoutError(error)) return;
14137
+ attachRetryAfter(error, this.rateLimits.take(message.id));
13371
14138
  if (this.outstandingRequests.delete(message.id)) {
13372
14139
  this.dispatchConnectionDiagnosticsChange();
13373
14140
  }
14141
+ this.dispatchTypedEvent("requestFailed", {
14142
+ id: message.id,
14143
+ reason: describeSendFailure(error)
14144
+ });
13374
14145
  },
13375
14146
  trackResponse: (message, origin) => {
13376
14147
  const responseId = message.id;
@@ -13436,6 +14207,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13436
14207
  };
13437
14208
  baseTransport.onerror = (error) => {
13438
14209
  if (this.status === "connecting") return;
14210
+ if (subscriptionListenRefusal(error) !== void 0) return;
13439
14211
  this.status = "error";
13440
14212
  this.dispatchTypedEvent("statusChange", this.status);
13441
14213
  this.dispatchTypedEvent("error", error);
@@ -13681,6 +14453,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13681
14453
  this.clearModernReconnectTimer();
13682
14454
  this.modernReconnectAttempts = 0;
13683
14455
  this.modernNeverAcknowledged = false;
14456
+ this.modernListenRefusal = null;
13684
14457
  this.modernSubscription = null;
13685
14458
  this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
13686
14459
  this.dispatchSubscriptionsChange();
@@ -13719,6 +14492,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13719
14492
  this.taskProgress.clear();
13720
14493
  this.lastResponse = void 0;
13721
14494
  this.notificationStream = void 0;
14495
+ this.rateLimits.clear();
13722
14496
  this.dispatchConnectionDiagnosticsChange();
13723
14497
  if (this.malformedListItems.size > 0) {
13724
14498
  this.malformedListItems.clear();
@@ -13821,6 +14595,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13821
14595
  onFetchRequest: (entry) => {
13822
14596
  this.dispatchFetchRequest({ ...entry, category: "transport" });
13823
14597
  this.noteTransportStream(entry);
14598
+ this.rateLimits.record(entry);
13824
14599
  },
13825
14600
  onFetchResponseBody: (id, body) => {
13826
14601
  this.dispatchFetchRequestBodyUpdate(id, body);
@@ -13868,9 +14643,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13868
14643
  // for that upstream gap and should be deleted with it.
13869
14644
  //
13870
14645
  // Known, accepted side effect of turning intercept on with no stored
13871
- // tokens: `parseAuthChallengeFromResponse` treats 403 as a challenge
13872
- // too, so a probe answered 403 for a *non-auth* reason (a gateway
13873
- // rejecting the unknown `server/discover` method, say) now starts OAuth
14646
+ // tokens: `parseAuthChallengeFromResponse` treats a 403 carrying
14647
+ // `WWW-Authenticate` as a challenge too, so a probe answered that way
14648
+ // for a *non-auth* reason (a gateway rejecting the unknown
14649
+ // `server/discover` method, say) now starts OAuth
13874
14650
  // discovery instead of letting "auto" fall back to the legacy
13875
14651
  // `initialize`. The outcome is a surfaced `oauthError`, not a hang, and
13876
14652
  // it goes away with this clause.
@@ -13937,7 +14713,14 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13937
14713
  const timeoutPromise = new Promise((_, reject) => {
13938
14714
  timer = setTimeout(() => {
13939
14715
  connectTimedOut = true;
13940
- reject(new Error(connectionTimeoutMessage(connectTimeoutMs)));
14716
+ reject(
14717
+ new Error(
14718
+ connectionTimeoutMessage(
14719
+ connectTimeoutMs,
14720
+ this.connectionTimeoutRemedy
14721
+ )
14722
+ )
14723
+ );
13941
14724
  }, connectTimeoutMs);
13942
14725
  });
13943
14726
  try {
@@ -16152,11 +16935,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16152
16935
  // for page one.
16153
16936
  ...cursor !== void 0 ? { cursor } : {}
16154
16937
  };
16938
+ const resultSchema = this.isModernEra() ? ModernDirectoryReadResultSchema : DirectoryReadResultSchema;
16155
16939
  try {
16156
16940
  return await this.invokeMcpClient(
16157
16941
  () => this.client.request(
16158
16942
  { method: RESOURCES_DIRECTORY_READ_METHOD, params },
16159
- DirectoryReadResultSchema,
16943
+ resultSchema,
16160
16944
  this.getRequestOptions(this.progressTokenOf(metadata))
16161
16945
  ),
16162
16946
  { method: RESOURCES_DIRECTORY_READ_METHOD }
@@ -16611,6 +17395,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16611
17395
  this.clearModernReconnectTimer();
16612
17396
  this.modernReconnectAttempts = 0;
16613
17397
  this.modernNeverAcknowledged = false;
17398
+ this.modernListenRefusal = null;
16614
17399
  }
16615
17400
  const generation = ++this.modernListenGeneration;
16616
17401
  const previous = this.modernSubscription;
@@ -16629,8 +17414,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16629
17414
  this.getRequestOptions()
16630
17415
  );
16631
17416
  } catch (error) {
16632
- if (generation === this.modernListenGeneration && isNeverAcknowledgedSubscriptionClose(error)) {
16633
- this.modernNeverAcknowledged = true;
17417
+ if (generation === this.modernListenGeneration) {
17418
+ if (isNeverAcknowledgedSubscriptionClose(error)) {
17419
+ this.modernNeverAcknowledged = true;
17420
+ }
17421
+ this.modernListenRefusal = subscriptionListenRefusal(error) ?? null;
16634
17422
  }
16635
17423
  throw error;
16636
17424
  }
@@ -16711,6 +17499,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16711
17499
  * its error either way — the retry is about the subscriptions, not the call.
16712
17500
  */
16713
17501
  reconcileModernStreamStateAfterFailedRefresh() {
17502
+ if (this.modernListenRefusal) {
17503
+ this.endModernStreamRefused(this.modernListenRefusal);
17504
+ return;
17505
+ }
16714
17506
  if (!this.wantsModernStream()) {
16715
17507
  this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
16716
17508
  return;
@@ -16743,6 +17535,23 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16743
17535
  honoredUris: []
16744
17536
  });
16745
17537
  }
17538
+ /**
17539
+ * Settle the stream on an HTTP refusal of the listen (#2722): the
17540
+ * `"refused"` status carrying the HTTP answer, no reconnect, and a log line
17541
+ * with the sentence the UI shows. Reported whatever `active` says — see
17542
+ * `ResourceSubscriptionStreamState`.
17543
+ */
17544
+ endModernStreamRefused(refusal) {
17545
+ this.clearModernReconnectTimer();
17546
+ const message = subscriptionListenRefusalMessage(refusal);
17547
+ this.logger.warn({ message }, "subscriptions/listen refused over HTTP");
17548
+ this.setModernStreamState({
17549
+ active: this.modernStreamActive(),
17550
+ status: "refused",
17551
+ honoredUris: [],
17552
+ refusal
17553
+ });
17554
+ }
16746
17555
  /**
16747
17556
  * Schedule a reconnect re-listen after the current backoff delay (#1630).
16748
17557
  * `modernReconnectAttempts` reflects the number of *consecutive failed*
@@ -16780,6 +17589,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16780
17589
  this.endModernStreamNeverAcknowledged();
16781
17590
  return;
16782
17591
  }
17592
+ if (this.modernListenRefusal && !isTerminalStatus(this.status)) {
17593
+ this.endModernStreamRefused(this.modernListenRefusal);
17594
+ return;
17595
+ }
16783
17596
  this.modernReconnectAttempts += 1;
16784
17597
  if (this.modernReconnectAttempts > MODERN_RECONNECT_MAX_ATTEMPTS || isTerminalStatus(this.status) || !this.wantsModernStream()) {
16785
17598
  this.setModernStreamState({
@@ -16934,8 +17747,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16934
17747
  * Re-read OAuth storage and test whether a challenge is already satisfied.
16935
17748
  * See {@link OAuthManager.checkAuthChallengeSatisfied}.
16936
17749
  */
16937
- async checkAuthChallengeSatisfied(challenge) {
16938
- return this.ensureOAuthManager().checkAuthChallengeSatisfied(challenge);
17750
+ async checkAuthChallengeSatisfied(challenge, options) {
17751
+ return this.ensureOAuthManager().checkAuthChallengeSatisfied(
17752
+ challenge,
17753
+ options
17754
+ );
16939
17755
  }
16940
17756
  /**
16941
17757
  * Push recovered OAuth auth state to the remote backend (same MCP session).
@@ -17099,8 +17915,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
17099
17915
  * Direct transports reconnect after token exchange so the live MCP session
17100
17916
  * picks up the new Bearer token (mirrors silent recovery reconnect).
17101
17917
  */
17102
- async completeOAuthFlow(authorizationCode, iss) {
17103
- await this.ensureOAuthManager().completeOAuthFlow(authorizationCode, iss);
17918
+ async completeOAuthFlow(authorizationCode, iss, state) {
17919
+ await this.ensureOAuthManager().completeOAuthFlow(
17920
+ authorizationCode,
17921
+ iss,
17922
+ state
17923
+ );
17104
17924
  if (this.usesDirectAuthRecovery()) {
17105
17925
  await this.reconnectAfterAuthRecovery();
17106
17926
  }
@@ -17124,7 +17944,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
17124
17944
  * Finish OAuth after a full-page redirect and reconnect (or reattach) the MCP session.
17125
17945
  */
17126
17946
  async resumeAfterOAuth(authorizationCode, options) {
17127
- await this.completeOAuthFlow(authorizationCode, options?.iss);
17947
+ await this.completeOAuthFlow(
17948
+ authorizationCode,
17949
+ options?.iss,
17950
+ options?.state
17951
+ );
17128
17952
  const remoteSessionId = options?.remoteSessionId;
17129
17953
  const transport = this.baseTransport;
17130
17954
  if (remoteSessionId && transport instanceof RemoteClientTransport) {
@@ -17287,16 +18111,30 @@ function buildConfigFromOptions(options) {
17287
18111
  const target = options.target ?? [];
17288
18112
  const first = target[0];
17289
18113
  const rest = target.slice(1);
18114
+ if (first && options.serverUrl) {
18115
+ throw new Error(
18116
+ `Cannot combine a positional target (${first}) with --server-url (${options.serverUrl}). Pass one or the other.`
18117
+ );
18118
+ }
17290
18119
  const urlFromTarget = first && (first.startsWith("http://") || first.startsWith("https://")) ? first : null;
17291
18120
  const url = urlFromTarget ?? options.serverUrl ?? null;
17292
18121
  if (url) {
17293
- if (rest.length > 0 && urlFromTarget) {
18122
+ if (rest.length > 0) {
17294
18123
  throw new Error("Arguments cannot be passed to a URL-based MCP server.");
17295
18124
  }
18125
+ if (options.transport === "stdio") {
18126
+ throw new Error(
18127
+ `stdio transport cannot be used with a URL (${url}). Use --transport http or --transport sse, or pass a local command instead.`
18128
+ );
18129
+ }
17296
18130
  let transportType;
17297
18131
  const t = options.transport === "http" ? "streamable-http" : options.transport;
17298
18132
  if (t === "sse" || t === "streamable-http") {
17299
18133
  transportType = t;
18134
+ } else if (t) {
18135
+ throw new Error(
18136
+ `Invalid transport type: ${t}. Valid types are: sse, http, stdio`
18137
+ );
17300
18138
  } else {
17301
18139
  const u = new URL(url);
17302
18140
  if (u.pathname.endsWith("/mcp")) {
@@ -18665,7 +19503,11 @@ async function runRunnerInteractiveOAuth(options) {
18665
19503
  path: options.callbackListen.pathname,
18666
19504
  onCallback: async (params) => {
18667
19505
  try {
18668
- await options.client.completeOAuthFlow(params.code, params.iss);
19506
+ await options.client.completeOAuthFlow(
19507
+ params.code,
19508
+ params.iss,
19509
+ params.state
19510
+ );
18669
19511
  flowResolve();
18670
19512
  } catch (err) {
18671
19513
  flowReject(toRunnerOAuthError(err));
@@ -18717,7 +19559,10 @@ async function runRunnerInteractiveOAuth(options) {
18717
19559
  }
18718
19560
  if (options.authChallenge) {
18719
19561
  const satisfied = await racingSignals(
18720
- options.client.checkAuthChallengeSatisfied(options.authChallenge)
19562
+ // The token exchange just completed, so only scope is in question.
19563
+ options.client.checkAuthChallengeSatisfied(options.authChallenge, {
19564
+ scopeOnly: true
19565
+ })
18721
19566
  );
18722
19567
  if (!satisfied) {
18723
19568
  return {
@@ -19078,6 +19923,26 @@ function readInspectorVersion(callerUrl) {
19078
19923
  );
19079
19924
  }
19080
19925
 
19926
+ // ../../core/cli/handlers/connect-timeout.ts
19927
+ var DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
19928
+ var CLI_CONNECTION_TIMEOUT_REMEDY = "To accommodate a slower server, pass a larger --connect-timeout <ms>, or set connectionTimeout on the server's catalog or config entry.";
19929
+ function withConnectTimeout(settings, connectionTimeout) {
19930
+ if (connectionTimeout === void 0) return settings;
19931
+ if (settings) return { ...settings, connectionTimeout };
19932
+ return {
19933
+ headers: [],
19934
+ metadata: {},
19935
+ env: [],
19936
+ connectionTimeout,
19937
+ requestTimeout: 0,
19938
+ taskTtl: DEFAULT_TASK_TTL_MS,
19939
+ maxFetchRequests: DEFAULT_MAX_FETCH_REQUESTS,
19940
+ autoRefreshOnListChanged: false,
19941
+ paginatedLists: false,
19942
+ roots: []
19943
+ };
19944
+ }
19945
+
19081
19946
  // src/connection/auth-helper.ts
19082
19947
  import { spawn } from "child_process";
19083
19948
  import { createHash as createHash2 } from "crypto";
@@ -19087,14 +19952,25 @@ import * as path6 from "path";
19087
19952
  // ../../core/node/openUrl.ts
19088
19953
  import open from "open";
19089
19954
  var OPEN_URL_TIMEOUT_MS = 5e3;
19090
- function launched(child) {
19091
- return new Promise((resolve8, reject) => {
19092
- child.on("error", reject);
19093
- child.once("spawn", () => resolve8());
19955
+ var OPENABLE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
19956
+ function toOpenableHref(href) {
19957
+ let parsed;
19958
+ try {
19959
+ parsed = new URL(href);
19960
+ } catch {
19961
+ throw new Error("refusing to open an invalid URL");
19962
+ }
19963
+ if (!OPENABLE_PROTOCOLS.has(parsed.protocol)) {
19964
+ throw new Error(`refusing to open a non-http(s) URL (${parsed.protocol})`);
19965
+ }
19966
+ return parsed.href;
19967
+ }
19968
+ function absorbLateErrors(child) {
19969
+ child.on("error", () => {
19094
19970
  });
19095
19971
  }
19096
19972
  async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
19097
- const href = typeof url === "string" ? url : url.href;
19973
+ const href = toOpenableHref(typeof url === "string" ? url : url.href);
19098
19974
  let timer;
19099
19975
  const timeout = new Promise((_, reject) => {
19100
19976
  timer = setTimeout(
@@ -19103,8 +19979,7 @@ async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
19103
19979
  );
19104
19980
  });
19105
19981
  try {
19106
- const opened = Promise.resolve().then(() => open(href)).then(launched);
19107
- await Promise.race([opened, timeout]);
19982
+ await Promise.race([open(href).then(absorbLateErrors), timeout]);
19108
19983
  } finally {
19109
19984
  clearTimeout(timer);
19110
19985
  }
@@ -19471,13 +20346,15 @@ async function runEmaIdpInteractiveFlow(active, storage, navigation) {
19471
20346
  );
19472
20347
  const redirectUrlProvider = new MutableRedirectUrlProvider();
19473
20348
  redirectUrlProvider.redirectUrl = formatRunnerOAuthRedirectUrl(callbackUrlConfig);
20349
+ const fetchFn = createProxyFetch();
19474
20350
  await runRunnerInteractiveOAuth({
19475
20351
  client: {
19476
20352
  authenticate: async () => {
19477
20353
  const { authorizationUrl } = await startIdpOidcAuthorization({
19478
20354
  idp: active,
19479
20355
  redirectUrl: redirectUrlProvider.redirectUrl,
19480
- storage
20356
+ storage,
20357
+ fetchFn
19481
20358
  });
19482
20359
  navigation.navigateToAuthorization(authorizationUrl);
19483
20360
  return authorizationUrl;
@@ -19491,7 +20368,8 @@ async function runEmaIdpInteractiveFlow(active, storage, navigation) {
19491
20368
  authorizationCode,
19492
20369
  iss,
19493
20370
  redirectUrl: redirectUrlProvider.redirectUrl,
19494
- storage
20371
+ storage,
20372
+ fetchFn
19495
20373
  });
19496
20374
  },
19497
20375
  /* v8 ignore next 2 -- only reached when options.authChallenge is set, which this flow never does */
@@ -19511,7 +20389,13 @@ async function authorizeInFrontend(serverConfig, serverSettings, options) {
19511
20389
  return;
19512
20390
  }
19513
20391
  const environment = {
19514
- transport: createTransportNode
20392
+ transport: createTransportNode,
20393
+ // HTTPS_PROXY / HTTP_PROXY / NO_PROXY, as in the one-shot CLI (#2642):
20394
+ // `InspectorClient` always hands the transport a defined fetch, so the
20395
+ // transport's own proxy fallback never fires — the proxy has to sit here,
20396
+ // at the bottom of the stack, for OAuth discovery and token requests to
20397
+ // reach it. Undefined when no proxy variable is set.
20398
+ fetch: createProxyFetch()
19515
20399
  };
19516
20400
  const redirectUrlProvider = new MutableRedirectUrlProvider();
19517
20401
  const callbackUrlConfig = parseRunnerOAuthCallbackUrl(
@@ -19545,6 +20429,8 @@ async function authorizeInFrontend(serverConfig, serverSettings, options) {
19545
20429
  name: "inspector-cli",
19546
20430
  version: readInspectorVersion(import.meta.url)
19547
20431
  },
20432
+ // Same knobs as the daemon's connect: --connect-timeout or mcp.json (#2615).
20433
+ connectionTimeoutRemedy: CLI_CONNECTION_TIMEOUT_REMEDY,
19548
20434
  initialLoggingLevel: "debug",
19549
20435
  progress: false,
19550
20436
  sample: false,
@@ -19754,9 +20640,15 @@ function tryReserveAuthFlow(lockPath) {
19754
20640
  if (!isStale(fs8.statSync(lockPath).mtimeMs)) return false;
19755
20641
  const claimPath = `${lockPath}.claim-${process.pid}`;
19756
20642
  fs8.renameSync(lockPath, claimPath);
19757
- const claimedFresh = !isStale(fs8.statSync(claimPath).mtimeMs);
20643
+ if (!isStale(fs8.statSync(claimPath).mtimeMs)) {
20644
+ try {
20645
+ fs8.linkSync(claimPath, lockPath);
20646
+ } finally {
20647
+ fs8.rmSync(claimPath, { force: true });
20648
+ }
20649
+ return false;
20650
+ }
19758
20651
  fs8.rmSync(claimPath, { force: true });
19759
- if (claimedFresh) return false;
19760
20652
  create();
19761
20653
  return true;
19762
20654
  } catch {
@@ -20411,7 +21303,13 @@ async function getLiveConnectionAuthInfo(connection) {
20411
21303
  }
20412
21304
  async function createConnectionClient(serverConfig, serverSettings) {
20413
21305
  const environment = {
20414
- transport: createTransportNode
21306
+ transport: createTransportNode,
21307
+ // HTTPS_PROXY / HTTP_PROXY / NO_PROXY, as in the one-shot CLI (#2642).
21308
+ // `InspectorClient` builds its fetch chain on `environment.fetch ?? fetch`
21309
+ // and always hands the transport the result, so the transport's own
21310
+ // `createProxyFetch()` fallback is never reached — leave this out and the
21311
+ // daemon connects direct. Undefined when no proxy variable is set.
21312
+ fetch: createProxyFetch()
20415
21313
  };
20416
21314
  const redirectUrlProvider = new MutableRedirectUrlProvider();
20417
21315
  if (isOAuthCapableServerConfig(serverConfig)) {
@@ -20437,6 +21335,9 @@ async function createConnectionClient(serverConfig, serverSettings) {
20437
21335
  name: CONNECTION_CLIENT_NAME,
20438
21336
  version: readInspectorVersion(import.meta.url)
20439
21337
  },
21338
+ // `mcpdo connect` takes the same --connect-timeout as the one-shot CLI, so
21339
+ // a timeout names that rather than the web's Server Settings (#2615).
21340
+ connectionTimeoutRemedy: CLI_CONNECTION_TIMEOUT_REMEDY,
20440
21341
  initialLoggingLevel: "debug",
20441
21342
  progress: false,
20442
21343
  sample: false,
@@ -20493,6 +21394,9 @@ function connectCancelledError() {
20493
21394
  }
20494
21395
 
20495
21396
  // src/daemon/server.ts
21397
+ var MCPDO_RUN_OPTIONS = {
21398
+ skillBudgetHint: MCPDO_SKILL_BUDGET_HINT
21399
+ };
20496
21400
  var autoCancelElicitationChannel = {
20497
21401
  request(frame) {
20498
21402
  return Promise.resolve({
@@ -20924,7 +21828,7 @@ var DaemonServer = class _DaemonServer {
20924
21828
  const clearAmbientSignal = client.setAmbientRequestSignal(signal);
20925
21829
  let outcome;
20926
21830
  try {
20927
- outcome = await runMethod(client, methodArgs);
21831
+ outcome = await runMethod(client, methodArgs, MCPDO_RUN_OPTIONS);
20928
21832
  } finally {
20929
21833
  clearAmbientSignal();
20930
21834
  signal?.removeEventListener("abort", onAbort);
@@ -20968,7 +21872,10 @@ var DaemonServer = class _DaemonServer {
20968
21872
  const unwire = wireElicitationBridge(client, channel, requestId);
20969
21873
  const outcome = (async () => {
20970
21874
  try {
20971
- return toRpcResult(await runMethod(client, methodArgs), params.method);
21875
+ return toRpcResult(
21876
+ await runMethod(client, methodArgs, MCPDO_RUN_OPTIONS),
21877
+ params.method
21878
+ );
20972
21879
  } finally {
20973
21880
  unwire();
20974
21881
  }
@@ -21087,7 +21994,7 @@ var DaemonServer = class _DaemonServer {
21087
21994
  params.interactive
21088
21995
  );
21089
21996
  const methodArgs = stripConnectionFields(params);
21090
- const outcome = await runMethod(client, methodArgs);
21997
+ const outcome = await runMethod(client, methodArgs, MCPDO_RUN_OPTIONS);
21091
21998
  if (outcome.kind !== "stream") {
21092
21999
  throw new CliExitCodeError(
21093
22000
  EXIT_CODES.USAGE,
@@ -21336,6 +22243,8 @@ export {
21336
22243
  resetNodeOAuthStorageCache,
21337
22244
  clearAllOAuthClientState,
21338
22245
  NodeOAuthStorage,
22246
+ DEFAULT_CONNECT_TIMEOUT_MS,
22247
+ withConnectTimeout,
21339
22248
  PLAIN,
21340
22249
  styleFromOpts,
21341
22250
  isCliAutoOpenForced,