@modelcontextprotocol/inspector 2.11.0 → 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.
@@ -86,6 +86,12 @@ var DirectoryReadResultSchema = z.looseObject({
86
86
  resources: z.array(DirectoryChildSchema),
87
87
  nextCursor: z.string().optional()
88
88
  });
89
+ var ModernDirectoryReadResultSchema = DirectoryReadResultSchema.extend(
90
+ {
91
+ ttlMs: z.int().min(0),
92
+ cacheScope: z.enum(["public", "private"])
93
+ }
94
+ );
89
95
 
90
96
  // ../../core/mcp/sha256.ts
91
97
  var K = new Uint32Array([
@@ -2129,6 +2135,13 @@ var BaseOAuthClientProvider = class {
2129
2135
  requestRefreshToken;
2130
2136
  /** Server-configured scope, for the #2068 `offline_access` distinction. */
2131
2137
  configuredScope;
2138
+ /** Per-flow verifier store (#2726); absent → the shared storage slot. */
2139
+ flowStore;
2140
+ /**
2141
+ * The authId of the flow this provider is driving: set by {@link state} on the
2142
+ * redirect leg and by {@link resumeAuthorizationFlow} on the callback leg.
2143
+ */
2144
+ flowId;
2132
2145
  constructor(serverUrl, oauthConfig) {
2133
2146
  this.serverUrl = serverUrl;
2134
2147
  this.storage = oauthConfig.storage;
@@ -2138,6 +2151,7 @@ var BaseOAuthClientProvider = class {
2138
2151
  this.authorizationParams = oauthConfig.authorizationParams;
2139
2152
  this.requestRefreshToken = oauthConfig.requestRefreshToken ?? true;
2140
2153
  this.configuredScope = oauthConfig.configuredScope;
2154
+ this.flowStore = oauthConfig.flowStore;
2141
2155
  }
2142
2156
  /**
2143
2157
  * Load persisted scope into {@link cachedScope} before SDK `auth()` (which
@@ -2208,7 +2222,24 @@ var BaseOAuthClientProvider = class {
2208
2222
  return metadata;
2209
2223
  }
2210
2224
  state() {
2211
- return generateOAuthState();
2225
+ const state = generateOAuthState();
2226
+ this.flowId = state;
2227
+ return state;
2228
+ }
2229
+ /**
2230
+ * Bind this provider to the flow a callback's `state` names, so
2231
+ * {@link codeVerifier} reads that flow's verifier and no other (#2726). A
2232
+ * `state` that is not one of ours binds nothing — and, with a flow store, a
2233
+ * flow-less read then fails rather than reaching for the shared slot.
2234
+ */
2235
+ resumeAuthorizationFlow(state) {
2236
+ this.flowId = parseOAuthState(state)?.authId;
2237
+ }
2238
+ /** Drop the bound flow's verifier once its exchange has completed. */
2239
+ async clearAuthorizationFlow() {
2240
+ if (this.flowStore && this.flowId) {
2241
+ await this.flowStore.clearCodeVerifier(this.flowId);
2242
+ }
2212
2243
  }
2213
2244
  async clientInformation(ctx) {
2214
2245
  const preregistered = await this.storage.getClientInformation(
@@ -2353,9 +2384,26 @@ var BaseOAuthClientProvider = class {
2353
2384
  }
2354
2385
  }
2355
2386
  async saveCodeVerifier(codeVerifier) {
2387
+ if (this.flowStore && this.flowId) {
2388
+ await this.flowStore.saveCodeVerifier(
2389
+ this.flowId,
2390
+ this.serverUrl,
2391
+ codeVerifier
2392
+ );
2393
+ return;
2394
+ }
2356
2395
  await this.storage.saveCodeVerifier(this.serverUrl, codeVerifier);
2357
2396
  }
2358
2397
  async codeVerifier() {
2398
+ if (this.flowStore) {
2399
+ const verifier2 = this.flowId ? await this.flowStore.getCodeVerifier(this.flowId, this.serverUrl) : void 0;
2400
+ if (!verifier2) {
2401
+ throw new Error(
2402
+ "No code verifier saved for this authorization flow. It may have been started in another browser tab or already completed; please re-authenticate."
2403
+ );
2404
+ }
2405
+ return verifier2;
2406
+ }
2359
2407
  const verifier = await this.storage.getCodeVerifier(this.serverUrl);
2360
2408
  if (!verifier) {
2361
2409
  throw new Error("No code verifier saved for session");
@@ -2402,6 +2450,10 @@ var BaseOAuthClientProvider = class {
2402
2450
  await this.storage.clearTokens(this.serverUrl);
2403
2451
  return;
2404
2452
  case "verifier":
2453
+ if (this.flowStore) {
2454
+ await this.clearAuthorizationFlow();
2455
+ return;
2456
+ }
2405
2457
  await this.storage.clearCodeVerifier(this.serverUrl);
2406
2458
  return;
2407
2459
  case "discovery":
@@ -4885,7 +4937,7 @@ async function resolveConcreteSecretStore(store) {
4885
4937
  import { resolve as resolve5 } from "path";
4886
4938
 
4887
4939
  // ../../core/auth/node/oauth-secrets.ts
4888
- import { randomUUID as randomUUID2 } from "crypto";
4940
+ import { createHmac, randomUUID as randomUUID2 } from "crypto";
4889
4941
  import { OAuthTokensSchema as OAuthTokensSchema3 } from "@modelcontextprotocol/core";
4890
4942
  var PERSIST_TOKENS_ENV = "MCP_INSPECTOR_PERSIST_TOKENS";
4891
4943
  var warnedPolicyValues = /* @__PURE__ */ new Set();
@@ -4918,6 +4970,29 @@ var LEGACY_REGISTRATION_TOKEN_FIELD = "registration-token";
4918
4970
  var PREREG_CLIENT_SECRET_FIELD = "prereg-client-secret";
4919
4971
  var PREREG_REGISTRATION_TOKEN_FIELD = "prereg-registration-token";
4920
4972
  var IDP_SESSION_FIELD = "idp-session";
4973
+ var clientCredentialStampField = (field) => `${field}#owner`;
4974
+ var credentialMac = (clientId, value) => createHmac("sha256", value).update(clientId).digest("hex");
4975
+ function clientCredentialStamp(clientId, value) {
4976
+ return JSON.stringify({
4977
+ client_id: clientId,
4978
+ mac: credentialMac(clientId, value)
4979
+ });
4980
+ }
4981
+ function stampedOwner(stamp, value) {
4982
+ if (stamp === void 0) return void 0;
4983
+ let parsed;
4984
+ try {
4985
+ parsed = JSON.parse(stamp);
4986
+ } catch {
4987
+ return void 0;
4988
+ }
4989
+ if (typeof parsed !== "object" || parsed === null) return void 0;
4990
+ const { client_id: clientId, mac } = parsed;
4991
+ if (typeof clientId !== "string" || mac !== credentialMac(clientId, value)) {
4992
+ return void 0;
4993
+ }
4994
+ return clientId;
4995
+ }
4921
4996
  function applyTokensPolicy(tokens, policy) {
4922
4997
  if (policy === "none") return void 0;
4923
4998
  if (policy === "access" && tokens.refresh_token !== void 0) {
@@ -4946,12 +5021,18 @@ function splitClientInformation(info, fields, secrets) {
4946
5021
  if (client_secret === void 0 && registration_access_token === void 0) {
4947
5022
  return info;
4948
5023
  }
4949
- if (client_secret !== void 0) {
4950
- secrets[fields.clientSecret] = client_secret;
4951
- }
4952
- if (registration_access_token !== void 0) {
4953
- secrets[fields.registrationToken] = registration_access_token;
4954
- }
5024
+ const put = (field, value) => {
5025
+ if (value === void 0) return;
5026
+ secrets[field] = value;
5027
+ if (fields.stamped && typeof info.client_id === "string") {
5028
+ secrets[clientCredentialStampField(field)] = clientCredentialStamp(
5029
+ info.client_id,
5030
+ value
5031
+ );
5032
+ }
5033
+ };
5034
+ put(fields.clientSecret, client_secret);
5035
+ put(fields.registrationToken, registration_access_token);
4955
5036
  return publicInfo;
4956
5037
  }
4957
5038
  function joinClientInformation(info, fields, secrets) {
@@ -4970,15 +5051,18 @@ function joinClientInformation(info, fields, secrets) {
4970
5051
  }
4971
5052
  var issuerClientInfoFields = (issuer) => ({
4972
5053
  clientSecret: issuerClientSecretField(issuer),
4973
- registrationToken: issuerRegistrationTokenField(issuer)
5054
+ registrationToken: issuerRegistrationTokenField(issuer),
5055
+ stamped: true
4974
5056
  });
4975
5057
  var LEGACY_CLIENT_INFO_FIELDS = {
4976
5058
  clientSecret: LEGACY_CLIENT_SECRET_FIELD,
4977
- registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD
5059
+ registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD,
5060
+ stamped: true
4978
5061
  };
4979
5062
  var PREREG_CLIENT_INFO_FIELDS = {
4980
5063
  clientSecret: PREREG_CLIENT_SECRET_FIELD,
4981
- registrationToken: PREREG_REGISTRATION_TOKEN_FIELD
5064
+ registrationToken: PREREG_REGISTRATION_TOKEN_FIELD,
5065
+ stamped: false
4982
5066
  };
4983
5067
  function splitServerOAuthState(state, policy) {
4984
5068
  const secrets = {};
@@ -5128,20 +5212,54 @@ function joinIdpSession(residue, secrets) {
5128
5212
  return stored ? { ...residue, ...stored } : { ...residue };
5129
5213
  }
5130
5214
  function serverSecretFields(state) {
5215
+ const stamped = (field) => [
5216
+ field,
5217
+ clientCredentialStampField(field)
5218
+ ];
5131
5219
  const fields = /* @__PURE__ */ new Set([
5132
5220
  LEGACY_TOKENS_FIELD,
5133
- LEGACY_CLIENT_SECRET_FIELD,
5134
- LEGACY_REGISTRATION_TOKEN_FIELD,
5221
+ ...stamped(LEGACY_CLIENT_SECRET_FIELD),
5222
+ ...stamped(LEGACY_REGISTRATION_TOKEN_FIELD),
5135
5223
  PREREG_CLIENT_SECRET_FIELD,
5136
5224
  PREREG_REGISTRATION_TOKEN_FIELD
5137
5225
  ]);
5138
5226
  for (const issuer of Object.keys(state?.byIssuer ?? {})) {
5139
5227
  fields.add(issuerTokensField(issuer));
5140
- fields.add(issuerClientSecretField(issuer));
5141
- fields.add(issuerRegistrationTokenField(issuer));
5228
+ for (const field of stamped(issuerClientSecretField(issuer))) {
5229
+ fields.add(field);
5230
+ }
5231
+ for (const field of stamped(issuerRegistrationTokenField(issuer))) {
5232
+ fields.add(field);
5233
+ }
5142
5234
  }
5143
5235
  return [...fields];
5144
5236
  }
5237
+ function clientCredentialOwners(state) {
5238
+ const owners = /* @__PURE__ */ new Map();
5239
+ const add2 = (info, fields) => {
5240
+ if (typeof info?.client_id !== "string") return;
5241
+ owners.set(fields.clientSecret, info.client_id);
5242
+ owners.set(fields.registrationToken, info.client_id);
5243
+ };
5244
+ add2(state.clientInformation, LEGACY_CLIENT_INFO_FIELDS);
5245
+ for (const [issuer, slot] of Object.entries(state.byIssuer ?? {})) {
5246
+ add2(slot.clientInformation, issuerClientInfoFields(issuer));
5247
+ }
5248
+ return owners;
5249
+ }
5250
+ function restampClientCredentials(state, secrets) {
5251
+ const out = { ...secrets };
5252
+ for (const [field, clientId] of clientCredentialOwners(state)) {
5253
+ const stampField = clientCredentialStampField(field);
5254
+ const value = Object.hasOwn(out, field) ? out[field] : void 0;
5255
+ if (value === void 0) {
5256
+ delete out[stampField];
5257
+ } else if (stampedOwner(out[stampField], value) === void 0) {
5258
+ setOwnEntry(out, stampField, clientCredentialStamp(clientId, value));
5259
+ }
5260
+ }
5261
+ return out;
5262
+ }
5145
5263
  function clientInfoHasPlaintext(info) {
5146
5264
  return info !== void 0 && (info.client_secret !== void 0 || info.registration_access_token !== void 0);
5147
5265
  }
@@ -5182,6 +5300,44 @@ function coveredLocations(location, byLocation) {
5182
5300
  ([recorded]) => recorded === location || location === KEYRING_LOCATION && recorded.startsWith(FILE_LOCATION_PREFIX)
5183
5301
  ).map(([recorded, keys]) => ({ location: recorded, keys }));
5184
5302
  }
5303
+ var emptyKeys = () => ({
5304
+ servers: /* @__PURE__ */ new Set(),
5305
+ idpSessions: /* @__PURE__ */ new Set(),
5306
+ owners: /* @__PURE__ */ new Map()
5307
+ });
5308
+ function candidateFields(location, byLocation) {
5309
+ const fields = /* @__PURE__ */ new Map();
5310
+ for (const { keys } of coveredLocations(location, byLocation)) {
5311
+ for (const [url, owned] of keys.owners) {
5312
+ const kept = fields.get(url) ?? /* @__PURE__ */ new Set();
5313
+ for (const field of owned.keys()) kept.add(field);
5314
+ fields.set(url, kept);
5315
+ }
5316
+ }
5317
+ return fields;
5318
+ }
5319
+ function addMissingOwners(target, source) {
5320
+ for (const [url, fields] of source) {
5321
+ const kept = target.get(url) ?? /* @__PURE__ */ new Map();
5322
+ for (const [field, clientId] of fields) {
5323
+ if (!kept.has(field)) kept.set(field, clientId);
5324
+ }
5325
+ if (kept.size > 0) target.set(url, kept);
5326
+ }
5327
+ }
5328
+ function mergeOwners(target, source) {
5329
+ let changed = false;
5330
+ for (const [url, fields] of source) {
5331
+ const kept = target.get(url) ?? /* @__PURE__ */ new Map();
5332
+ for (const [field, clientId] of fields) {
5333
+ if (kept.get(field) === clientId) continue;
5334
+ kept.set(field, clientId);
5335
+ changed = true;
5336
+ }
5337
+ if (kept.size > 0) target.set(url, kept);
5338
+ }
5339
+ return changed;
5340
+ }
5185
5341
  var warnedLedgerFailures = /* @__PURE__ */ new Set();
5186
5342
  function warnLedgerFailure(what, error) {
5187
5343
  const reason = error instanceof Error ? error.message : String(error);
@@ -5195,6 +5351,19 @@ function warnLedgerFailure(what, error) {
5195
5351
  function isRecord2(value) {
5196
5352
  return typeof value === "object" && value !== null && !Array.isArray(value);
5197
5353
  }
5354
+ function parseOwners(value) {
5355
+ const owners = /* @__PURE__ */ new Map();
5356
+ if (!isRecord2(value)) return owners;
5357
+ for (const [url, fields] of Object.entries(value)) {
5358
+ if (!isRecord2(fields)) continue;
5359
+ const byField = /* @__PURE__ */ new Map();
5360
+ for (const [field, clientId] of Object.entries(fields)) {
5361
+ if (typeof clientId === "string") byField.set(field, clientId);
5362
+ }
5363
+ if (byField.size > 0) owners.set(url, byField);
5364
+ }
5365
+ return owners;
5366
+ }
5198
5367
  function stringsOf(value) {
5199
5368
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
5200
5369
  }
@@ -5216,7 +5385,9 @@ function parseLedger(raw) {
5216
5385
  if (!isRecord2(keys)) continue;
5217
5386
  byLocation.set(location, {
5218
5387
  servers: new Set(stringsOf(keys.servers)),
5219
- idpSessions: new Set(stringsOf(keys.idpSessions))
5388
+ idpSessions: new Set(stringsOf(keys.idpSessions)),
5389
+ owners: parseOwners(keys.owners),
5390
+ ...isValidSecretsNamespace(keys.owedTo) && { owedTo: keys.owedTo }
5220
5391
  });
5221
5392
  }
5222
5393
  if (byLocation.size > 0) ledger.set(namespace, byLocation);
@@ -5230,7 +5401,19 @@ function serializeLedger(ledger) {
5230
5401
  Object.fromEntries(
5231
5402
  [...byLocation].map(([location, keys]) => [
5232
5403
  location,
5233
- { servers: [...keys.servers], idpSessions: [...keys.idpSessions] }
5404
+ {
5405
+ servers: [...keys.servers],
5406
+ idpSessions: [...keys.idpSessions],
5407
+ ...keys.owners.size > 0 && {
5408
+ owners: Object.fromEntries(
5409
+ [...keys.owners].map(([url, fields]) => [
5410
+ url,
5411
+ Object.fromEntries(fields)
5412
+ ])
5413
+ )
5414
+ },
5415
+ ...keys.owedTo !== void 0 && { owedTo: keys.owedTo }
5416
+ }
5234
5417
  ])
5235
5418
  )
5236
5419
  ])
@@ -5242,7 +5425,7 @@ async function writeLedger(stateFilePath, ledger) {
5242
5425
  if (ledger.size === 0) await deleteStoreFile(path5);
5243
5426
  else await writeStoreFile(path5, serializeLedger(ledger));
5244
5427
  }
5245
- async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions) {
5428
+ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, servers, idpSessions, owners = /* @__PURE__ */ new Map()) {
5246
5429
  try {
5247
5430
  const ledger = parseLedger(
5248
5431
  await readStoreFile(namespaceLedgerPath(stateFilePath))
@@ -5256,7 +5439,7 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
5256
5439
  let keys = byLocation.get(location);
5257
5440
  let changed = false;
5258
5441
  if (!keys) {
5259
- keys = { servers: /* @__PURE__ */ new Set(), idpSessions: /* @__PURE__ */ new Set() };
5442
+ keys = emptyKeys();
5260
5443
  byLocation.set(location, keys);
5261
5444
  changed = true;
5262
5445
  }
@@ -5270,11 +5453,93 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
5270
5453
  keys.idpSessions.add(issuer);
5271
5454
  changed = true;
5272
5455
  }
5456
+ if (mergeOwners(keys.owners, owners)) changed = true;
5273
5457
  if (changed) await writeLedger(stateFilePath, ledger);
5274
5458
  } catch (error) {
5275
5459
  warnLedgerFailure("update", error);
5276
5460
  }
5277
5461
  }
5462
+ async function markUncarriedNamespaces(stateFilePath, secretStore, into) {
5463
+ const ledger = parseLedger(
5464
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
5465
+ );
5466
+ const location = await secretStoreLocation(secretStore);
5467
+ let changed = false;
5468
+ for (const [namespace, byLocation] of ledger) {
5469
+ if (namespace === into) continue;
5470
+ for (const [recorded, keys] of byLocation) {
5471
+ if (recorded === location || keys.owners.size === 0) continue;
5472
+ if (keys.owedTo === into) continue;
5473
+ keys.owedTo = into;
5474
+ changed = true;
5475
+ }
5476
+ }
5477
+ if (changed) await writeLedger(stateFilePath, ledger);
5478
+ }
5479
+ async function pendingCarries(stateFilePath, secretStore, current) {
5480
+ const ledger = parseLedger(
5481
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
5482
+ );
5483
+ const location = await secretStoreLocation(secretStore);
5484
+ const carries = [];
5485
+ for (const [namespace, byLocation] of ledger) {
5486
+ const owed = coveredLocations(location, byLocation).some(
5487
+ ({ keys }) => keys.owedTo === current
5488
+ );
5489
+ if (owed) {
5490
+ carries.push({
5491
+ namespace,
5492
+ fields: candidateFields(location, byLocation)
5493
+ });
5494
+ }
5495
+ }
5496
+ return carries.reverse();
5497
+ }
5498
+ async function carryOwed(stateFilePath, ledger, location, secretStore, namespace, current, recorded, now) {
5499
+ let carried = true;
5500
+ for (const [url, fields] of recorded) {
5501
+ for (const field of fields) {
5502
+ const clientId = now.get(url)?.get(field);
5503
+ if (clientId === void 0) continue;
5504
+ try {
5505
+ const from = oauthSecretServerId(url, namespace);
5506
+ const to = oauthSecretServerId(url, current);
5507
+ const stampField = clientCredentialStampField(field);
5508
+ const value = await secretStoreGetStrict(secretStore, from, field);
5509
+ if (value === null) continue;
5510
+ const stamp = await secretStoreGetStrict(secretStore, from, stampField);
5511
+ if (stamp === null || stampedOwner(stamp, value) !== clientId) {
5512
+ continue;
5513
+ }
5514
+ if (await secretStoreGetStrict(secretStore, to, field) !== null) {
5515
+ continue;
5516
+ }
5517
+ let byLocation = ledger.get(current);
5518
+ if (!byLocation) {
5519
+ byLocation = /* @__PURE__ */ new Map();
5520
+ ledger.set(current, byLocation);
5521
+ }
5522
+ const target = byLocation.get(location) ?? emptyKeys();
5523
+ byLocation.set(location, target);
5524
+ const isNew = !target.servers.has(url);
5525
+ target.servers.add(url);
5526
+ const owner = /* @__PURE__ */ new Map([[url, /* @__PURE__ */ new Map([[field, clientId]])]]);
5527
+ if (mergeOwners(target.owners, owner) || isNew) {
5528
+ await writeLedger(stateFilePath, ledger);
5529
+ }
5530
+ await secretStore.set(to, stampField, stamp);
5531
+ await secretStore.set(to, field, value);
5532
+ } catch (error) {
5533
+ carried = false;
5534
+ warnLedgerFailure(
5535
+ "carry over a superseded namespace recorded in",
5536
+ error
5537
+ );
5538
+ }
5539
+ }
5540
+ }
5541
+ return carried;
5542
+ }
5278
5543
  async function recordedNamespaces(stateFilePath) {
5279
5544
  try {
5280
5545
  const ledger = parseLedger(
@@ -5286,7 +5551,7 @@ async function recordedNamespaces(stateFilePath) {
5286
5551
  return [];
5287
5552
  }
5288
5553
  }
5289
- async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
5554
+ async function purgeSupersededNamespaces(stateFilePath, secretStore, current, owners = /* @__PURE__ */ new Map()) {
5290
5555
  let ledger;
5291
5556
  let location;
5292
5557
  try {
@@ -5299,8 +5564,22 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
5299
5564
  return;
5300
5565
  }
5301
5566
  let changed = false;
5302
- for (const [namespace, byLocation] of ledger) {
5567
+ for (const [namespace, byLocation] of [...ledger].reverse()) {
5303
5568
  if (namespace === current) continue;
5569
+ if (current !== void 0 && coveredLocations(location, byLocation).some(
5570
+ ({ keys }) => keys.owedTo === current
5571
+ ) && !await carryOwed(
5572
+ stateFilePath,
5573
+ ledger,
5574
+ location,
5575
+ secretStore,
5576
+ namespace,
5577
+ current,
5578
+ candidateFields(location, byLocation),
5579
+ owners
5580
+ )) {
5581
+ break;
5582
+ }
5304
5583
  for (const { location: recorded, keys } of coveredLocations(
5305
5584
  location,
5306
5585
  byLocation
@@ -5325,12 +5604,11 @@ async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
5325
5604
  if (!purged || recorded !== location) continue;
5326
5605
  byLocation.delete(recorded);
5327
5606
  if (recorded.startsWith(FILE_LOCATION_PREFIX)) {
5328
- const keyring = byLocation.get(KEYRING_LOCATION) ?? {
5329
- servers: /* @__PURE__ */ new Set(),
5330
- idpSessions: /* @__PURE__ */ new Set()
5331
- };
5607
+ const keyring = byLocation.get(KEYRING_LOCATION) ?? emptyKeys();
5608
+ addMissingOwners(keyring.owners, keys.owners);
5332
5609
  for (const url of keys.servers) keyring.servers.add(url);
5333
5610
  for (const issuer of keys.idpSessions) keyring.idpSessions.add(issuer);
5611
+ keyring.owedTo ??= keys.owedTo;
5334
5612
  byLocation.set(KEYRING_LOCATION, keyring);
5335
5613
  }
5336
5614
  changed = true;
@@ -5441,7 +5719,12 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
5441
5719
  const existing = parseSecretsNamespace(raw);
5442
5720
  if (existing !== void 0) {
5443
5721
  if (locked) {
5444
- await purgeSupersededNamespaces(filePath, secretStore, existing);
5722
+ await purgeSupersededNamespaces(
5723
+ filePath,
5724
+ secretStore,
5725
+ existing,
5726
+ snapshotClientOwners(parseOAuthPersistBlob(raw))
5727
+ );
5445
5728
  }
5446
5729
  return existing;
5447
5730
  }
@@ -5461,25 +5744,33 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
5461
5744
  `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).`
5462
5745
  );
5463
5746
  }
5747
+ const values = await readEntrySecrets(
5748
+ snapshot,
5749
+ secretStore,
5750
+ void 0,
5751
+ await recordedNamespaces(filePath)
5752
+ );
5753
+ const owners = snapshotClientOwners(snapshot);
5464
5754
  await recordNamespaceKeys(
5465
5755
  filePath,
5466
5756
  secretStore,
5467
5757
  namespace,
5468
5758
  Object.keys(snapshot.servers),
5469
- Object.keys(snapshot.idpSessions)
5759
+ Object.keys(snapshot.idpSessions),
5760
+ writtenClientOwners(snapshot, (url) => values.servers.get(url) ?? {})
5470
5761
  );
5762
+ await markUncarriedNamespaces(filePath, secretStore, namespace);
5471
5763
  const touched = [];
5472
5764
  try {
5473
- const values = await readEntrySecrets(
5474
- snapshot,
5475
- secretStore,
5476
- void 0,
5477
- await recordedNamespaces(filePath)
5478
- );
5479
5765
  const copies = [
5766
+ // Restamped: a value read from the legacy ids predates stamps, and a
5767
+ // fallback's stamp may belong to a value read from elsewhere (#2734).
5480
5768
  ...[...values.servers].map(([url, secrets]) => ({
5481
5769
  scopedId: oauthSecretServerId(url, namespace),
5482
- secrets
5770
+ secrets: restampClientCredentials(
5771
+ getOwnEntry(snapshot.servers, url) ?? {},
5772
+ secrets
5773
+ )
5483
5774
  })),
5484
5775
  ...[...values.idpSessions].map(([issuer, secrets]) => ({
5485
5776
  scopedId: oauthIdpSecretServerId(issuer, namespace),
@@ -5497,7 +5788,7 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
5497
5788
  await restoreSecretFields(secretStore, touched, warnRestoreFailure);
5498
5789
  throw error;
5499
5790
  }
5500
- await purgeSupersededNamespaces(filePath, secretStore, namespace);
5791
+ await purgeSupersededNamespaces(filePath, secretStore, namespace, owners);
5501
5792
  const legacyIds = [
5502
5793
  ...Object.keys(snapshot.servers).map((url) => oauthSecretServerId(url)),
5503
5794
  ...Object.keys(snapshot.idpSessions).map(
@@ -5704,7 +5995,22 @@ async function writeOAuthSectionsFrom(filePath, resolveSnapshot, sections, secre
5704
5995
  secretStore,
5705
5996
  namespace,
5706
5997
  Object.keys(merged.servers),
5707
- Object.keys(merged.idpSessions)
5998
+ Object.keys(merged.idpSessions),
5999
+ // Only the entries this save writes: another entry's secret in
6000
+ // this store was written for whatever client it was written for,
6001
+ // and relabeling it from the file would misattribute it (#2672).
6002
+ writtenClientOwners(
6003
+ {
6004
+ servers: Object.fromEntries(
6005
+ (effective.servers ?? []).flatMap((url) => {
6006
+ const entry = getOwnEntry(merged.servers, url);
6007
+ return entry === void 0 ? [] : [[url, entry]];
6008
+ })
6009
+ ),
6010
+ idpSessions: {}
6011
+ },
6012
+ splitSecretsOf
6013
+ )
5708
6014
  );
5709
6015
  for (const url of effective.servers ?? []) {
5710
6016
  const serverId = oauthSecretServerId(url, namespace);
@@ -5892,7 +6198,29 @@ function fillMissingSecrets(target, source) {
5892
6198
  if (!Object.hasOwn(target, field)) setOwnEntry(target, field, value);
5893
6199
  }
5894
6200
  }
5895
- async function readEntrySecrets(snapshot, secretStore, namespace, fallbacks) {
6201
+ function writtenClientOwners(snapshot, secretsOf) {
6202
+ const owners = /* @__PURE__ */ new Map();
6203
+ for (const [url, state] of Object.entries(snapshot.servers)) {
6204
+ const secrets = secretsOf(url, state);
6205
+ const fields = new Map(
6206
+ [...clientCredentialOwners(state)].filter(
6207
+ ([field]) => Object.hasOwn(secrets, field)
6208
+ )
6209
+ );
6210
+ if (fields.size > 0) owners.set(url, fields);
6211
+ }
6212
+ return owners;
6213
+ }
6214
+ var splitSecretsOf = (_url, state) => splitServerOAuthState(state, "all").secrets;
6215
+ function snapshotClientOwners(snapshot) {
6216
+ const owners = /* @__PURE__ */ new Map();
6217
+ for (const [url, state] of Object.entries(snapshot?.servers ?? {})) {
6218
+ const fields = clientCredentialOwners(state);
6219
+ if (fields.size > 0) owners.set(url, fields);
6220
+ }
6221
+ return owners;
6222
+ }
6223
+ async function readEntrySecrets(snapshot, secretStore, namespace, fallbacks, carries = []) {
5896
6224
  const servers = /* @__PURE__ */ new Map();
5897
6225
  const idpSessions = /* @__PURE__ */ new Map();
5898
6226
  for (const url of Object.keys(snapshot.servers)) servers.set(url, {});
@@ -5913,14 +6241,49 @@ async function readEntrySecrets(snapshot, secretStore, namespace, fallbacks) {
5913
6241
  );
5914
6242
  }
5915
6243
  }
6244
+ const owners = snapshotClientOwners(snapshot);
6245
+ for (const { namespace: from, fields: recorded } of carries) {
6246
+ const requests = [];
6247
+ for (const [url, fields] of recorded) {
6248
+ const target = servers.get(url);
6249
+ if (!target) continue;
6250
+ const wanted = [...fields].filter(
6251
+ (field) => owners.get(url)?.has(field) === true && !Object.hasOwn(target, field)
6252
+ );
6253
+ if (wanted.length > 0) {
6254
+ requests.push({
6255
+ serverId: oauthSecretServerId(url, from),
6256
+ fields: wanted.flatMap((field) => [
6257
+ field,
6258
+ clientCredentialStampField(field)
6259
+ ])
6260
+ });
6261
+ }
6262
+ }
6263
+ if (requests.length === 0) continue;
6264
+ const values = await secretStoreGetManyStrict(secretStore, requests);
6265
+ for (const [url, target] of servers) {
6266
+ const found = values[oauthSecretServerId(url, from)] ?? {};
6267
+ for (const [field, clientId] of owners.get(url) ?? []) {
6268
+ const value = Object.hasOwn(found, field) ? found[field] : void 0;
6269
+ const stampField = clientCredentialStampField(field);
6270
+ if (value === void 0 || Object.hasOwn(target, field) || stampedOwner(found[stampField], value) !== clientId) {
6271
+ continue;
6272
+ }
6273
+ setOwnEntry(target, field, value);
6274
+ setOwnEntry(target, stampField, found[stampField]);
6275
+ }
6276
+ }
6277
+ }
5916
6278
  return { servers, idpSessions };
5917
6279
  }
5918
- async function joinSnapshot(snapshot, secretStore, namespace, fallbacks) {
6280
+ async function joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries = []) {
5919
6281
  const values = await readEntrySecrets(
5920
6282
  snapshot,
5921
6283
  secretStore,
5922
6284
  namespace,
5923
- fallbacks
6285
+ fallbacks,
6286
+ carries
5924
6287
  );
5925
6288
  return {
5926
6289
  servers: Object.fromEntries(
@@ -5948,7 +6311,8 @@ async function migratePlaintextSecrets(filePath, secretStore) {
5948
6311
  secretStore,
5949
6312
  namespace,
5950
6313
  Object.keys(fresh.servers),
5951
- Object.keys(fresh.idpSessions)
6314
+ Object.keys(fresh.idpSessions),
6315
+ writtenClientOwners(fresh, splitSecretsOf)
5952
6316
  );
5953
6317
  }
5954
6318
  const migrateEntrySecrets = async (serverId, secrets) => {
@@ -6001,7 +6365,8 @@ async function readOAuthStore(filePath, secretStore = defaultSecretStore()) {
6001
6365
  }
6002
6366
  const namespace = parseSecretsNamespace(raw);
6003
6367
  const fallbacks = namespace === void 0 ? await recordedNamespaces(filePath) : [];
6004
- return joinSnapshot(snapshot, secretStore, namespace, fallbacks);
6368
+ const carries = namespace === void 0 ? [] : await pendingCarries(filePath, secretStore, namespace);
6369
+ return joinSnapshot(snapshot, secretStore, namespace, fallbacks, carries);
6005
6370
  });
6006
6371
  }
6007
6372
  async function removeOAuthStore(filePath, secretStore = defaultSecretStore()) {
@@ -6761,8 +7126,226 @@ function createTransportNode(config, options = {}) {
6761
7126
  }
6762
7127
  }
6763
7128
 
7129
+ // ../../core/mcp/remote/node/cappedBody.ts
7130
+ async function readCappedText(body, maxBytes) {
7131
+ const reader = body.getReader();
7132
+ const decoder = new TextDecoder();
7133
+ let text = "";
7134
+ let bytes = 0;
7135
+ for (; ; ) {
7136
+ const next = await reader.read();
7137
+ if (next.done) return { kind: "complete", text: text + decoder.decode() };
7138
+ bytes += next.value.byteLength;
7139
+ if (bytes > maxBytes) {
7140
+ void reader.cancel().catch(() => {
7141
+ });
7142
+ return { kind: "overflow" };
7143
+ }
7144
+ text += decoder.decode(next.value, { stream: true });
7145
+ }
7146
+ }
7147
+ function declaresOversizeBody(contentLength, maxBytes) {
7148
+ if (contentLength === null || !/^\s*\d+\s*$/.test(contentLength)) {
7149
+ return false;
7150
+ }
7151
+ return Number(contentLength) > maxBytes;
7152
+ }
7153
+
7154
+ // ../../core/mcp/rateLimit.ts
7155
+ import {
7156
+ SdkHttpError
7157
+ } from "@modelcontextprotocol/client";
7158
+
7159
+ // ../../core/mcp/connectionDiagnostics.ts
7160
+ import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
7161
+ function isRequestTimeoutData(value) {
7162
+ return typeof value === "object" && value !== null && typeof value.timeout === "number";
7163
+ }
7164
+ function isRequestTimeoutError(err) {
7165
+ return SdkError.isInstance(err) && err.code === SdkErrorCode.RequestTimeout && isRequestTimeoutData(err.data);
7166
+ }
7167
+ function describeSendFailure(err) {
7168
+ const message = err instanceof Error ? err.message : String(err);
7169
+ const status = httpStatusOf(err);
7170
+ if (status === void 0 || message.includes(`HTTP ${status}`)) {
7171
+ return message;
7172
+ }
7173
+ const statusText = httpStatusTextOf(err);
7174
+ return `HTTP ${status}${statusText ? ` ${statusText}` : ""}: ${message}`;
7175
+ }
7176
+ function httpStatusOf(err) {
7177
+ if (typeof err !== "object" || err === null || !("status" in err)) {
7178
+ return void 0;
7179
+ }
7180
+ return typeof err.status === "number" ? err.status : void 0;
7181
+ }
7182
+ function httpStatusTextOf(err) {
7183
+ if (typeof err !== "object" || err === null || !("statusText" in err)) {
7184
+ return void 0;
7185
+ }
7186
+ return typeof err.statusText === "string" ? err.statusText : void 0;
7187
+ }
7188
+ function formatDuration(ms) {
7189
+ const clamped = Math.max(0, ms);
7190
+ if (clamped < 1e3) return `${Math.round(clamped)}ms`;
7191
+ const totalSeconds = Math.round(clamped / 1e3);
7192
+ const hours = Math.floor(totalSeconds / 3600);
7193
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
7194
+ const seconds = totalSeconds % 60;
7195
+ if (hours > 0) return `${hours}h${String(minutes).padStart(2, "0")}m`;
7196
+ if (minutes > 0) return `${minutes}m${String(seconds).padStart(2, "0")}s`;
7197
+ return `${seconds}s`;
7198
+ }
7199
+ var MAX_LISTED_OUTSTANDING_REQUESTS = 5;
7200
+ function describeStreamEndpoint(url) {
7201
+ try {
7202
+ return `GET ${new URL(url).pathname}`;
7203
+ } catch {
7204
+ return `GET ${url}`;
7205
+ }
7206
+ }
7207
+ function plural(count, noun) {
7208
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
7209
+ }
7210
+ function describeOutstanding(requests, now) {
7211
+ if (requests.length === 0) return "No requests are unanswered.";
7212
+ const listed = requests.slice(0, MAX_LISTED_OUTSTANDING_REQUESTS).map((r) => `${r.method} (sent ${formatDuration(now - r.sentAt)} ago)`);
7213
+ const rest = requests.length - listed.length;
7214
+ const tail = rest > 0 ? `, and ${rest} more` : "";
7215
+ const verb = requests.length === 1 ? "is" : "are";
7216
+ return `${plural(requests.length, "request")} ${verb} unanswered: ${listed.join(", ")}${tail}.`;
7217
+ }
7218
+ function describeLastResponse(lastResponse, now) {
7219
+ if (!lastResponse) return "No response has been received this session.";
7220
+ return `Last response received ${formatDuration(now - lastResponse.receivedAt)} ago (${lastResponse.method}).`;
7221
+ }
7222
+ function describeStream(stream, now) {
7223
+ if (!stream) return void 0;
7224
+ const endpoint = describeStreamEndpoint(stream.url);
7225
+ const events = plural(stream.eventCount, "event");
7226
+ if (stream.closedAt !== void 0) {
7227
+ return `Notification stream (${endpoint}) closed ${formatDuration(now - stream.closedAt)} ago after ${formatDuration(stream.closedAt - stream.openedAt)}, ${events} delivered.`;
7228
+ }
7229
+ return `Notification stream (${endpoint}) open for ${formatDuration(now - stream.openedAt)}, ${events} delivered.`;
7230
+ }
7231
+ function describeConnectionDiagnostics(diagnostics) {
7232
+ const now = diagnostics.capturedAt;
7233
+ const parts = [
7234
+ describeOutstanding(diagnostics.outstandingRequests, now),
7235
+ describeLastResponse(diagnostics.lastResponse, now),
7236
+ describeStream(diagnostics.notificationStream, now)
7237
+ ];
7238
+ return parts.filter((p) => p !== void 0).join(" ");
7239
+ }
7240
+ function annotateRequestTimeout(err, method, diagnostics) {
7241
+ if (!isRequestTimeoutError(err)) return err;
7242
+ const data = {
7243
+ ...err.data,
7244
+ method,
7245
+ diagnostics
7246
+ };
7247
+ const message = `Request timed out after ${formatDuration(err.data.timeout)} (${method}). ${describeConnectionDiagnostics(diagnostics)}`;
7248
+ const annotated = new SdkError(SdkErrorCode.RequestTimeout, message, data);
7249
+ annotated.cause = err;
7250
+ return annotated;
7251
+ }
7252
+
7253
+ // ../../core/mcp/rateLimit.ts
7254
+ var HTTP_TOO_MANY_REQUESTS = 429;
7255
+ function requestIdsOf(body) {
7256
+ if (body === void 0) return [];
7257
+ let parsed;
7258
+ try {
7259
+ parsed = JSON.parse(body);
7260
+ } catch {
7261
+ return [];
7262
+ }
7263
+ const messages = Array.isArray(parsed) ? parsed : [parsed];
7264
+ const ids = [];
7265
+ for (const message of messages) {
7266
+ if (typeof message !== "object" || message === null) continue;
7267
+ if (!("method" in message) || !("id" in message)) continue;
7268
+ const { id } = message;
7269
+ if (typeof id === "string" || typeof id === "number") ids.push(id);
7270
+ }
7271
+ return ids;
7272
+ }
7273
+ var MAX_UNCLAIMED_RATE_LIMITS = 100;
7274
+ var RateLimitLedger = class {
7275
+ // `undefined` is a real value here: a 429 that carried no header.
7276
+ byRequestId = /* @__PURE__ */ new Map();
7277
+ /** File a tracked fetch, if it was a 429. Anything else is ignored. */
7278
+ record(entry) {
7279
+ if (entry.responseStatus !== HTTP_TOO_MANY_REQUESTS) return;
7280
+ const retryAfter = findHeader(entry.responseHeaders, "retry-after");
7281
+ for (const id of requestIdsOf(entry.requestBody)) {
7282
+ this.byRequestId.delete(id);
7283
+ this.byRequestId.set(id, retryAfter);
7284
+ if (this.byRequestId.size > MAX_UNCLAIMED_RATE_LIMITS) {
7285
+ const [oldest] = this.byRequestId.keys();
7286
+ if (oldest !== void 0) this.byRequestId.delete(oldest);
7287
+ }
7288
+ }
7289
+ }
7290
+ /** The `Retry-After` filed for `id`, removed as it is read. */
7291
+ take(id) {
7292
+ const retryAfter = this.byRequestId.get(id);
7293
+ this.byRequestId.delete(id);
7294
+ return retryAfter;
7295
+ }
7296
+ clear() {
7297
+ this.byRequestId.clear();
7298
+ }
7299
+ };
7300
+ var IMF_FIXDATE = /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/;
7301
+ function parseRetryAfter(value, now) {
7302
+ if (value === void 0) return void 0;
7303
+ const trimmed = value.trim();
7304
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
7305
+ if (!IMF_FIXDATE.test(trimmed)) return void 0;
7306
+ const at = Date.parse(trimmed);
7307
+ if (Number.isNaN(at) || new Date(at).toUTCString() !== trimmed) {
7308
+ return void 0;
7309
+ }
7310
+ return Math.max(0, at - now);
7311
+ }
7312
+ function isRateLimitError(err) {
7313
+ return SdkHttpError.isInstance(err) && err.status === HTTP_TOO_MANY_REQUESTS;
7314
+ }
7315
+ function retryAfterOn(err) {
7316
+ const value = err.data.retryAfter;
7317
+ return typeof value === "string" ? value : void 0;
7318
+ }
7319
+ function attachRetryAfter(err, retryAfter) {
7320
+ if (!isRateLimitError(err) || retryAfter === void 0) return;
7321
+ if (retryAfterOn(err) !== void 0) return;
7322
+ err.data.retryAfter = retryAfter;
7323
+ }
7324
+ function describeRetry(retryAfter, retryAfterMs) {
7325
+ if (retryAfterMs !== void 0) {
7326
+ return `The server asked to retry after ${formatDuration(retryAfterMs)}.`;
7327
+ }
7328
+ if (retryAfter !== void 0) {
7329
+ return `The server sent an unparseable Retry-After (${retryAfter}).`;
7330
+ }
7331
+ return "The server sent no Retry-After header.";
7332
+ }
7333
+ function annotateRateLimit(err, method, now = Date.now()) {
7334
+ if (!isRateLimitError(err)) return err;
7335
+ const header2 = retryAfterOn(err);
7336
+ const retryAfterMs = parseRetryAfter(header2, now);
7337
+ const data = {
7338
+ ...err.data,
7339
+ method,
7340
+ ...header2 !== void 0 && { retryAfter: header2 },
7341
+ ...retryAfterMs !== void 0 && { retryAfterMs }
7342
+ };
7343
+ const message = `Rate limited by the server (HTTP 429) on ${method}. ${describeRetry(header2, retryAfterMs)} ${err.message}`;
7344
+ return new SdkHttpError(err.code, message, data, { cause: err });
7345
+ }
7346
+
6764
7347
  // ../../core/mcp/remote/node/server.ts
6765
- import { SdkHttpError } from "@modelcontextprotocol/client";
7348
+ import { SdkHttpError as SdkHttpError2 } from "@modelcontextprotocol/client";
6766
7349
 
6767
7350
  // ../../core/json/xMcpHeader.ts
6768
7351
  var X_MCP_HEADER_KEY = "x-mcp-header";
@@ -8393,6 +8976,8 @@ var RemoteSession = class _RemoteSession {
8393
8976
  requestWaits = /* @__PURE__ */ new Map();
8394
8977
  authProviderHandle = null;
8395
8978
  appliedProtocolVersion;
8979
+ /** Each upstream `429`'s `Retry-After`, by the JSON-RPC id that drew it (#2442). */
8980
+ rateLimits = new RateLimitLedger();
8396
8981
  /** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
8397
8982
  static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
8398
8983
  constructor(sessionId) {
@@ -8604,6 +9189,7 @@ var RemoteSession = class _RemoteSession {
8604
9189
  this.pushEvent({ type: "message", data: message });
8605
9190
  }
8606
9191
  onFetchRequest(entry) {
9192
+ this.rateLimits.record(entry);
8607
9193
  this.pushEvent({
8608
9194
  type: "fetch_request",
8609
9195
  data: {
@@ -8612,6 +9198,14 @@ var RemoteSession = class _RemoteSession {
8612
9198
  }
8613
9199
  });
8614
9200
  }
9201
+ /**
9202
+ * The `Retry-After` of the upstream `429` that answered request `id` — read
9203
+ * by the send route, so the header rides back on the send's own error rather
9204
+ * than on a fetch event racing it (#2442).
9205
+ */
9206
+ takeRetryAfter(id) {
9207
+ return this.rateLimits.take(id);
9208
+ }
8615
9209
  onFetchResponseBody(id, responseBody) {
8616
9210
  this.pushEvent({
8617
9211
  type: "fetch_request_body_update",
@@ -9064,6 +9658,17 @@ var MAX_APP_DOCUMENT_CHARS = 8 * 1024 * 1024;
9064
9658
  var MAX_APP_DOCUMENT_BODY_BYTES = 24 * 1024 * 1024;
9065
9659
  var MAX_APP_CSP_CHARS = 8 * 1024;
9066
9660
  var MAX_STORAGE_BODY_BYTES = 4 * 1024 * 1024;
9661
+ var MAX_PROXIED_RESPONSE_BYTES = 4 * 1024 * 1024;
9662
+ var BODY_FRAMING_HEADERS = /* @__PURE__ */ new Set([
9663
+ "content-length",
9664
+ "transfer-encoding",
9665
+ "content-encoding"
9666
+ ]);
9667
+ function withoutBodyFraming(headers) {
9668
+ return Object.fromEntries(
9669
+ Object.entries(headers).filter(([k]) => !BODY_FRAMING_HEADERS.has(k))
9670
+ );
9671
+ }
9067
9672
  function isHeaderSafeValue(value) {
9068
9673
  return !/[^\t\x20-\x7e]/.test(value);
9069
9674
  }
@@ -9519,8 +10124,10 @@ function createRemoteApp(options) {
9519
10124
  });
9520
10125
  }
9521
10126
  const msg = err instanceof Error ? err.message : String(err);
9522
- if (err instanceof SdkHttpError) {
10127
+ if (err instanceof SdkHttpError2) {
9523
10128
  const text = err.data.text;
10129
+ const sentId = "id" in message ? message.id : void 0;
10130
+ const retryAfter = isRateLimitError(err) && (typeof sentId === "string" || typeof sentId === "number") ? session.takeRetryAfter(sentId) : void 0;
9524
10131
  return c.json({
9525
10132
  ok: false,
9526
10133
  kind: "transport_error",
@@ -9531,7 +10138,8 @@ function createRemoteApp(options) {
9531
10138
  ...err.statusText !== void 0 && {
9532
10139
  statusText: err.statusText
9533
10140
  },
9534
- ...typeof text === "string" && { text }
10141
+ ...typeof text === "string" && { text },
10142
+ ...retryAfter !== void 0 && { retryAfter }
9535
10143
  }
9536
10144
  });
9537
10145
  }
@@ -9664,7 +10272,28 @@ function createRemoteApp(options) {
9664
10272
  const isStream = contentType?.includes("text/event-stream") || contentType?.includes("application/x-ndjson");
9665
10273
  let resBody;
9666
10274
  if (!isStream && res.body) {
9667
- resBody = await res.text();
10275
+ const refuseOversize = () => res.ok ? c.json(
10276
+ {
10277
+ error: `response from ${safeUrl} exceeds the ${MAX_PROXIED_RESPONSE_BYTES}-byte limit for a proxied response`
10278
+ },
10279
+ 502
10280
+ ) : c.json({
10281
+ ok: res.ok,
10282
+ status: res.status,
10283
+ statusText: res.statusText,
10284
+ headers: withoutBodyFraming(resHeaders)
10285
+ });
10286
+ if (declaresOversizeBody(
10287
+ res.headers.get("content-length"),
10288
+ MAX_PROXIED_RESPONSE_BYTES
10289
+ )) {
10290
+ void res.body.cancel().catch(() => {
10291
+ });
10292
+ return refuseOversize();
10293
+ }
10294
+ const read = await readCappedText(res.body, MAX_PROXIED_RESPONSE_BYTES);
10295
+ if (read.kind === "overflow") return refuseOversize();
10296
+ resBody = read.text;
9668
10297
  } else if (isStream) {
9669
10298
  void res.body?.cancel().catch(() => {
9670
10299
  });
@@ -11792,12 +12421,12 @@ function countFindings(results) {
11792
12421
  }
11793
12422
  return { errors, warnings };
11794
12423
  }
11795
- function plural(count, noun) {
12424
+ function plural2(count, noun) {
11796
12425
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
11797
12426
  }
11798
12427
  function summarizeFindings(results) {
11799
12428
  const { errors, warnings } = countFindings(results);
11800
- return `${plural(errors, "error")}, ${plural(warnings, "warning")} across ${plural(results.length, "tool")}`;
12429
+ return `${plural2(errors, "error")}, ${plural2(warnings, "warning")} across ${plural2(results.length, "tool")}`;
11801
12430
  }
11802
12431
  function formatSchemaLintReport(results) {
11803
12432
  const lines = [];
@@ -12935,11 +13564,11 @@ import {
12935
13564
  } from "@modelcontextprotocol/core";
12936
13565
 
12937
13566
  // ../../core/mcp/subscriptionAck.ts
12938
- import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
13567
+ import { SdkError as SdkError2, SdkErrorCode as SdkErrorCode2 } from "@modelcontextprotocol/client";
12939
13568
  var NEVER_ACKNOWLEDGED_SDK_MESSAGE = "closed the subscription gracefully before acknowledging";
12940
13569
  var NEVER_ACKNOWLEDGED_SUBSCRIPTION_MESSAGE = "The server closed the subscription without acknowledging it. A subscriptions/listen request must first be answered with a notifications/subscriptions/acknowledged notification; a JSON-RPC result for the listen id means graceful closure. Not retrying \u2014 the server would answer the same way again.";
12941
13570
  function isNeverAcknowledgedSubscriptionClose(error) {
12942
- return SdkError.isInstance(error) && error.code === SdkErrorCode.ConnectionClosed && error.message.includes(NEVER_ACKNOWLEDGED_SDK_MESSAGE);
13571
+ return SdkError2.isInstance(error) && error.code === SdkErrorCode2.ConnectionClosed && error.message.includes(NEVER_ACKNOWLEDGED_SDK_MESSAGE);
12943
13572
  }
12944
13573
  function subscriptionFailureMessage(error) {
12945
13574
  if (isNeverAcknowledgedSubscriptionClose(error)) {
@@ -12948,6 +13577,23 @@ function subscriptionFailureMessage(error) {
12948
13577
  return error instanceof Error ? error.message : String(error);
12949
13578
  }
12950
13579
 
13580
+ // ../../core/mcp/subscriptionRefusal.ts
13581
+ import { SdkErrorCode as SdkErrorCode3, SdkHttpError as SdkHttpError3 } from "@modelcontextprotocol/client";
13582
+ var REFUSAL_STATUSES = /* @__PURE__ */ new Set([400, 403, 404, 405]);
13583
+ function subscriptionListenRefusal(error) {
13584
+ if (!SdkHttpError3.isInstance(error) || error.code !== SdkErrorCode3.ClientHttpNotImplemented || !REFUSAL_STATUSES.has(error.status)) {
13585
+ return void 0;
13586
+ }
13587
+ return {
13588
+ status: error.status,
13589
+ ...error.statusText ? { statusText: error.statusText } : {}
13590
+ };
13591
+ }
13592
+ function subscriptionListenRefusalMessage(refusal) {
13593
+ const status = refusal.statusText ? `${refusal.status} ${refusal.statusText}` : String(refusal.status);
13594
+ return `The server refused subscriptions/listen (HTTP ${status}). Not retrying \u2014 the server would answer the same way again.`;
13595
+ }
13596
+
12951
13597
  // ../../core/mcp/messageTrackingTransport.ts
12952
13598
  var MessageTrackingTransport = class {
12953
13599
  baseTransport;
@@ -13177,7 +13823,7 @@ var TaskProgressRouter = class {
13177
13823
  };
13178
13824
 
13179
13825
  // ../../core/extension/tasks/rawWireChannel.ts
13180
- import { SdkError as SdkError2, SdkErrorCode as SdkErrorCode2 } from "@modelcontextprotocol/client";
13826
+ import { SdkError as SdkError3, SdkErrorCode as SdkErrorCode4 } from "@modelcontextprotocol/client";
13181
13827
  import { DispatchError as DispatchError2 } from "@modelcontextprotocol/ext-tasks/client";
13182
13828
  var RAW_WIRE_ID_PREFIX = "inspector-ext-";
13183
13829
  var RawWireChannel = class {
@@ -13250,8 +13896,8 @@ var RawWireChannel = class {
13250
13896
  `Raw MCP request "${message.method}" timed out after ${timeoutMs} ms`,
13251
13897
  false,
13252
13898
  {
13253
- cause: this.host.annotateTimeout(
13254
- new SdkError2(SdkErrorCode2.RequestTimeout, "Request timed out", {
13899
+ cause: this.host.annotateError(
13900
+ new SdkError3(SdkErrorCode4.RequestTimeout, "Request timed out", {
13255
13901
  timeout: timeoutMs
13256
13902
  }),
13257
13903
  message.method
@@ -13291,7 +13937,7 @@ var RawWireChannel = class {
13291
13937
  if (!pending) return;
13292
13938
  pending.cleanup();
13293
13939
  reject(
13294
- this.host.annotateTimeout(
13940
+ this.host.annotateError(
13295
13941
  error instanceof Error ? error : new Error(String(error)),
13296
13942
  message.method
13297
13943
  )
@@ -13591,7 +14237,7 @@ function validateToolOutput(provider, tool, result) {
13591
14237
 
13592
14238
  // ../../core/mcp/listSalvage.ts
13593
14239
  import { z as z4 } from "zod/v4";
13594
- import { SdkError as SdkError3, SdkErrorCode as SdkErrorCode3 } from "@modelcontextprotocol/client";
14240
+ import { SdkError as SdkError4, SdkErrorCode as SdkErrorCode5 } from "@modelcontextprotocol/client";
13595
14241
  import { ToolSchema } from "@modelcontextprotocol/core";
13596
14242
  var LIST_MAX_PAGES = 64;
13597
14243
  function listPaginationExceeded(method) {
@@ -13600,10 +14246,10 @@ function listPaginationExceeded(method) {
13600
14246
  );
13601
14247
  }
13602
14248
  function isClientDecodeRejection(err) {
13603
- return SdkError3.isInstance(err) && (err.code === SdkErrorCode3.InvalidResult || err.code === SdkErrorCode3.UnsupportedResultType);
14249
+ return SdkError4.isInstance(err) && (err.code === SdkErrorCode5.InvalidResult || err.code === SdkErrorCode5.UnsupportedResultType);
13604
14250
  }
13605
14251
  function isSalvageableRejection(err) {
13606
- return SdkError3.isInstance(err) && err.code === SdkErrorCode3.InvalidResult;
14252
+ return SdkError4.isInstance(err) && err.code === SdkErrorCode5.InvalidResult;
13607
14253
  }
13608
14254
  function lenientListPageSchema(resultSchema, itemsKey) {
13609
14255
  return resultSchema.extend({ [itemsKey]: z4.array(z4.unknown()) });
@@ -14209,79 +14855,6 @@ var ToolCallCancelledError = class extends Error {
14209
14855
  }
14210
14856
  };
14211
14857
 
14212
- // ../../core/mcp/connectionDiagnostics.ts
14213
- import { SdkError as SdkError4, SdkErrorCode as SdkErrorCode4 } from "@modelcontextprotocol/client";
14214
- function isRequestTimeoutData(value) {
14215
- return typeof value === "object" && value !== null && typeof value.timeout === "number";
14216
- }
14217
- function isRequestTimeoutError(err) {
14218
- return SdkError4.isInstance(err) && err.code === SdkErrorCode4.RequestTimeout && isRequestTimeoutData(err.data);
14219
- }
14220
- function formatDuration(ms) {
14221
- const clamped = Math.max(0, ms);
14222
- if (clamped < 1e3) return `${Math.round(clamped)}ms`;
14223
- const totalSeconds = Math.round(clamped / 1e3);
14224
- const hours = Math.floor(totalSeconds / 3600);
14225
- const minutes = Math.floor(totalSeconds % 3600 / 60);
14226
- const seconds = totalSeconds % 60;
14227
- if (hours > 0) return `${hours}h${String(minutes).padStart(2, "0")}m`;
14228
- if (minutes > 0) return `${minutes}m${String(seconds).padStart(2, "0")}s`;
14229
- return `${seconds}s`;
14230
- }
14231
- var MAX_LISTED_OUTSTANDING_REQUESTS = 5;
14232
- function describeStreamEndpoint(url) {
14233
- try {
14234
- return `GET ${new URL(url).pathname}`;
14235
- } catch {
14236
- return `GET ${url}`;
14237
- }
14238
- }
14239
- function plural2(count, noun) {
14240
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
14241
- }
14242
- function describeOutstanding(requests, now) {
14243
- if (requests.length === 0) return "No requests are unanswered.";
14244
- const listed = requests.slice(0, MAX_LISTED_OUTSTANDING_REQUESTS).map((r) => `${r.method} (sent ${formatDuration(now - r.sentAt)} ago)`);
14245
- const rest = requests.length - listed.length;
14246
- const tail = rest > 0 ? `, and ${rest} more` : "";
14247
- const verb = requests.length === 1 ? "is" : "are";
14248
- return `${plural2(requests.length, "request")} ${verb} unanswered: ${listed.join(", ")}${tail}.`;
14249
- }
14250
- function describeLastResponse(lastResponse, now) {
14251
- if (!lastResponse) return "No response has been received this session.";
14252
- return `Last response received ${formatDuration(now - lastResponse.receivedAt)} ago (${lastResponse.method}).`;
14253
- }
14254
- function describeStream(stream, now) {
14255
- if (!stream) return void 0;
14256
- const endpoint = describeStreamEndpoint(stream.url);
14257
- const events = plural2(stream.eventCount, "event");
14258
- if (stream.closedAt !== void 0) {
14259
- return `Notification stream (${endpoint}) closed ${formatDuration(now - stream.closedAt)} ago after ${formatDuration(stream.closedAt - stream.openedAt)}, ${events} delivered.`;
14260
- }
14261
- return `Notification stream (${endpoint}) open for ${formatDuration(now - stream.openedAt)}, ${events} delivered.`;
14262
- }
14263
- function describeConnectionDiagnostics(diagnostics) {
14264
- const now = diagnostics.capturedAt;
14265
- const parts = [
14266
- describeOutstanding(diagnostics.outstandingRequests, now),
14267
- describeLastResponse(diagnostics.lastResponse, now),
14268
- describeStream(diagnostics.notificationStream, now)
14269
- ];
14270
- return parts.filter((p) => p !== void 0).join(" ");
14271
- }
14272
- function annotateRequestTimeout(err, method, diagnostics) {
14273
- if (!isRequestTimeoutError(err)) return err;
14274
- const data = {
14275
- ...err.data,
14276
- method,
14277
- diagnostics
14278
- };
14279
- const message = `Request timed out after ${formatDuration(err.data.timeout)} (${method}). ${describeConnectionDiagnostics(diagnostics)}`;
14280
- const annotated = new SdkError4(SdkErrorCode4.RequestTimeout, message, data);
14281
- annotated.cause = err;
14282
- return annotated;
14283
- }
14284
-
14285
14858
  // ../../core/auth/ema/clientConfigError.ts
14286
14859
  function emaClientNotConfiguredMessage(reason) {
14287
14860
  if (reason === "disabled") {
@@ -14714,11 +15287,6 @@ async function mintEmaResourceTokens(config, resourceContext) {
14714
15287
  "EMA requires resource authorization server clientId (per-server oauth.clientId)"
14715
15288
  );
14716
15289
  }
14717
- if (!config.resourceClientSecret?.trim()) {
14718
- throw new Error(
14719
- "EMA requires resource authorization server client secret in server OAuth settings (Test Client secret from xaa.dev resource registration)"
14720
- );
14721
- }
14722
15290
  const idToken = await getValidIdToken({
14723
15291
  idp: config.idp,
14724
15292
  storage: config.storage,
@@ -14984,7 +15552,9 @@ var OAuthManager = class {
14984
15552
  requestRefreshToken: this.oauthConfig.requestRefreshToken,
14985
15553
  // #2068: lets the provider tell an `offline_access` the user asked for
14986
15554
  // from one inherited from a previous grant's persisted scope.
14987
- configuredScope: this.oauthConfig.scope
15555
+ configuredScope: this.oauthConfig.scope,
15556
+ // #2726: in-flight PKCE verifiers keyed by flow, not by server URL.
15557
+ flowStore: this.oauthConfig.flowStore
14988
15558
  });
14989
15559
  provider.setEventTarget(this.params.getEventTarget());
14990
15560
  await provider.prepareForAuth();
@@ -15148,7 +15718,12 @@ var OAuthManager = class {
15148
15718
  recordAuthorizationRequestScope(authorizationUrl, requestedScope) {
15149
15719
  this.pendingAuthorizationScope = authorizationUrl.searchParams.get("scope")?.trim() || requestedScope;
15150
15720
  }
15151
- async completeOAuthFlow(authorizationCode, iss) {
15721
+ /**
15722
+ * `state` is the OAuth `state` the callback was redirected back with. It
15723
+ * selects the flow whose PKCE verifier the exchange uses (#2726); without it
15724
+ * a provider with a flow store has no verifier to send.
15725
+ */
15726
+ async completeOAuthFlow(authorizationCode, iss, state) {
15152
15727
  try {
15153
15728
  if (this.isEnterpriseManaged()) {
15154
15729
  const emaConfig = await this.getEmaFlowConfig();
@@ -15172,6 +15747,9 @@ var OAuthManager = class {
15172
15747
  }
15173
15748
  const provider = await this.createOAuthProvider();
15174
15749
  const serverUrl = this.getServerUrl();
15750
+ if (state !== void 0) {
15751
+ provider.resumeAuthorizationFlow(state);
15752
+ }
15175
15753
  const result = await mcpAuth(provider, {
15176
15754
  serverUrl,
15177
15755
  authorizationCode,
@@ -15183,6 +15761,7 @@ var OAuthManager = class {
15183
15761
  `Expected AUTHORIZED after providing authorization code, got: ${result}`
15184
15762
  );
15185
15763
  }
15764
+ await provider.clearAuthorizationFlow();
15186
15765
  const tokens = await provider.tokens();
15187
15766
  if (!tokens) {
15188
15767
  throw new Error("Failed to retrieve tokens after authorization");
@@ -15314,15 +15893,21 @@ var OAuthManager = class {
15314
15893
  * Re-read persisted OAuth state and determine whether `challenge` is already
15315
15894
  * satisfied without an authorization-server round-trip.
15316
15895
  *
15317
- * Returns `true` for `insufficient_scope` when stored + token scope cover the
15318
- * SEP-2350 union. For `token_expired`, returns `true` only when storage holds
15319
- * a *provably* unexpired token — a JWT whose `exp` is still in the future. An
15320
- * opaque token carries no local expiry evidence, so the resource server's
15321
- * verdict stands and re-authorization proceeds. `invalid_token` and
15322
- * `unauthorized` always return `false` — the resource server explicitly
15323
- * rejected the credential.
15896
+ * For `token_expired` and `insufficient_scope`, returns `true` only when
15897
+ * storage holds a *provably* unexpired token — a JWT whose `exp` is still in
15898
+ * the future. An opaque token carries no local expiry evidence, so the
15899
+ * resource server's verdict stands and re-authorization proceeds.
15900
+ * `insufficient_scope` additionally requires stored + token scope to cover
15901
+ * the SEP-2350 union: some resource servers answer an *expired* token with
15902
+ * `insufficient_scope`, and a scope-only check would replay that dead token
15903
+ * instead of refreshing it (#2724, the same gap #2051 closed for
15904
+ * `token_expired`). `invalid_token` and `unauthorized` always return
15905
+ * `false` — the resource server explicitly rejected the credential.
15906
+ *
15907
+ * A caller that has just completed a grant passes `scopeOnly` — see
15908
+ * {@link CheckAuthChallengeSatisfiedOptions}.
15324
15909
  */
15325
- async checkAuthChallengeSatisfied(challenge) {
15910
+ async checkAuthChallengeSatisfied(challenge, options) {
15326
15911
  const storage = this.oauthConfig.storage;
15327
15912
  if (!storage) {
15328
15913
  return false;
@@ -15335,6 +15920,14 @@ var OAuthManager = class {
15335
15920
  if (challenge.reason !== "insufficient_scope") {
15336
15921
  return challenge.reason === "token_expired" && isAccessTokenProvablyUnexpired(tokens);
15337
15922
  }
15923
+ if (!options?.scopeOnly && !isAccessTokenProvablyUnexpired(tokens)) {
15924
+ return false;
15925
+ }
15926
+ return this.isStepUpScopeCovered(challenge, storage, tokens);
15927
+ }
15928
+ /** Stored + token scope cover the challenge's SEP-2350 union. */
15929
+ async isStepUpScopeCovered(challenge, storage, tokens) {
15930
+ const serverUrl = this.getServerUrl();
15338
15931
  const enriched = await this.enrichChallengeWithAuthorizationScopes(
15339
15932
  challenge,
15340
15933
  tokens.scope
@@ -15438,14 +16031,14 @@ var OAuthManager = class {
15438
16031
  }
15439
16032
  async handleEnterpriseManagedAuthChallenge(challenge, options) {
15440
16033
  const enriched = await this.enrichChallengeWithAuthorizationScopes(challenge);
15441
- if (enriched.reason === "insufficient_scope" && !options?.confirmedStepUp) {
16034
+ if (enriched.reason === "insufficient_scope" && !options?.confirmedStepUp && !await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
15442
16035
  return { kind: "step_up_confirm", challenge: enriched };
15443
16036
  }
15444
16037
  const config = await this.emaFlowConfigForChallenge(enriched);
15445
16038
  if (enriched.reason === "insufficient_scope") {
15446
16039
  const silent = await trySilentEmaAuth(config);
15447
16040
  if (silent.status === "success") {
15448
- if (await this.checkAuthChallengeSatisfied(enriched)) {
16041
+ if (await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
15449
16042
  const minted = await this.requireStorage().getTokens(
15450
16043
  this.getServerUrl()
15451
16044
  );
@@ -15523,9 +16116,16 @@ var OAuthManager = class {
15523
16116
  fetchFn: this.params.effectiveAuthFetch,
15524
16117
  resourceMetadataUrl,
15525
16118
  ...enriched.reason === "insufficient_scope" && {
16119
+ // Judge coverage the way checkAuthChallengeSatisfied does: a token
16120
+ // response that omitted `scope` granted what was stored, so a
16121
+ // covered challenge on an expired token refreshes rather than
16122
+ // forcing interactive authorization (#2724).
15526
16123
  forceReauthorization: isStrictScopeSuperset(
15527
16124
  scopeForAuth,
15528
- tokens?.scope
16125
+ resolveEffectiveGrantedScope(
16126
+ await this.oauthConfig.storage?.getScope(serverUrl),
16127
+ tokens?.scope
16128
+ )
15529
16129
  )
15530
16130
  }
15531
16131
  });
@@ -15534,7 +16134,7 @@ var OAuthManager = class {
15534
16134
  }
15535
16135
  if (result === "AUTHORIZED") {
15536
16136
  if (enriched.reason === "insufficient_scope") {
15537
- if (await this.checkAuthChallengeSatisfied(enriched)) {
16137
+ if (await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
15538
16138
  const freshTokens = await provider.tokens();
15539
16139
  const scopeToPersist = resolvePersistedScopeAfterGrant(
15540
16140
  freshTokens?.scope,
@@ -15636,7 +16236,7 @@ var OAuthManager = class {
15636
16236
  if (result !== "AUTHORIZED") {
15637
16237
  return null;
15638
16238
  }
15639
- if (await this.checkAuthChallengeSatisfied(enriched)) {
16239
+ if (await this.checkAuthChallengeSatisfied(enriched, { scopeOnly: true })) {
15640
16240
  const freshTokens = await provider.tokens();
15641
16241
  const scopeToPersist = resolvePersistedScopeAfterGrant(
15642
16242
  freshTokens?.scope,
@@ -15668,8 +16268,8 @@ var OAuthManager = class {
15668
16268
  // ../../core/mcp/remote/remoteClientTransport.ts
15669
16269
  import {
15670
16270
  SdkError as SdkError5,
15671
- SdkErrorCode as SdkErrorCode5,
15672
- SdkHttpError as SdkHttpError2
16271
+ SdkErrorCode as SdkErrorCode6,
16272
+ SdkHttpError as SdkHttpError4
15673
16273
  } from "@modelcontextprotocol/client";
15674
16274
 
15675
16275
  // ../../core/mcp/remote/types.ts
@@ -15704,14 +16304,15 @@ function isConnectTransportError(json) {
15704
16304
  return typeof json === "object" && json !== null && "ok" in json && json.ok === false && "kind" in json && json.kind === "transport_error";
15705
16305
  }
15706
16306
  var SDK_ERROR_CODES = new Set(
15707
- Object.values(SdkErrorCode5)
16307
+ Object.values(SdkErrorCode6)
15708
16308
  );
15709
16309
  function toSdkHttpError(message, http) {
15710
- const code = SDK_ERROR_CODES.has(http.code) ? http.code : SdkErrorCode5.ClientHttpNotImplemented;
15711
- return new SdkHttpError2(code, message, {
16310
+ const code = SDK_ERROR_CODES.has(http.code) ? http.code : SdkErrorCode6.ClientHttpNotImplemented;
16311
+ return new SdkHttpError4(code, message, {
15712
16312
  status: http.status,
15713
16313
  ...http.statusText !== void 0 && { statusText: http.statusText },
15714
- ...http.text !== void 0 && { text: http.text }
16314
+ ...http.text !== void 0 && { text: http.text },
16315
+ ...http.retryAfter !== void 0 && { retryAfter: http.retryAfter }
15715
16316
  });
15716
16317
  }
15717
16318
  function legacySessionId(json) {
@@ -16145,7 +16746,7 @@ var RemoteClientTransport = class {
16145
16746
  timer = setTimeout(() => {
16146
16747
  this.sseResponseWaits.delete(requestId);
16147
16748
  reject(
16148
- new SdkError5(SdkErrorCode5.RequestTimeout, "Request timed out", {
16749
+ new SdkError5(SdkErrorCode6.RequestTimeout, "Request timed out", {
16149
16750
  timeout: this.sseResponseTimeoutMs
16150
16751
  })
16151
16752
  );
@@ -16357,7 +16958,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16357
16958
  transport: () => this.transport,
16358
16959
  defaultTimeoutMs: () => this.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC,
16359
16960
  resetTimeoutOnProgress: () => this.resetTimeoutOnProgress,
16360
- annotateTimeout: (error, method) => annotateRequestTimeout(error, method, this.getConnectionDiagnostics())
16961
+ annotateError: (error, method) => this.annotateRequestError(error, method)
16361
16962
  });
16362
16963
  dispatchTaskRequest = (request, options) => this.rawWire.dispatch(request, options);
16363
16964
  // Every outbound request still awaiting a response, keyed by JSON-RPC id.
@@ -16377,6 +16978,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16377
16978
  // `GET` on Streamable HTTP). Both per-session, reset with the maps above.
16378
16979
  lastResponse;
16379
16980
  notificationStream;
16981
+ // Each tracked `429`'s `Retry-After`, by the JSON-RPC id that drew it, until
16982
+ // that id's failed send claims it (#2442). Per-session, reset with the
16983
+ // diagnostics above.
16984
+ rateLimits = new RateLimitLedger();
16380
16985
  /** True when the cached transport was built with an OAuth authProvider attached. */
16381
16986
  transportHasAuthProvider = false;
16382
16987
  /** Dedupes concurrent ambient auth challenges (reason + scopes). */
@@ -16521,6 +17126,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16521
17126
  // site to the two failure handlers. Cleared by any user-initiated refresh (a
16522
17127
  // subscribe/unsubscribe is a fresh attempt, and the server may have changed).
16523
17128
  modernNeverAcknowledged = false;
17129
+ // Set when the last `listen()` was refused at the HTTP layer with a status a
17130
+ // retry cannot change (#2722) — the sibling of `modernNeverAcknowledged`,
17131
+ // carried and cleared the same way, holding the HTTP answer the UI reports.
17132
+ modernListenRefusal = null;
16524
17133
  /** Which in-flight task calls own each progress token (#2316). */
16525
17134
  taskProgress = new TaskProgressRouter();
16526
17135
  // Abort controller for the in-flight ordinary (non-task) tool call. Aborting
@@ -16778,7 +17387,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16778
17387
  this.clientInfo,
16779
17388
  Object.keys(clientOptions).length > 0 ? clientOptions : void 0
16780
17389
  );
16781
- this.annotateSdkRequestTimeouts(this.client);
17390
+ this.annotateSdkRequestErrors(this.client);
16782
17391
  if (this.tasksCapabilityAdvertised) {
16783
17392
  this.bindReceiverTasks();
16784
17393
  }
@@ -16786,7 +17395,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16786
17395
  /**
16787
17396
  * Decorate the SDK Client's `request` so a per-request timeout rejects with
16788
17397
  * the connection's state at that moment instead of a bare `Request timed
16789
- * out` (#2318).
17398
+ * out` (#2318), and a `429` rejects as rate limiting with the server's
17399
+ * `Retry-After` instead of a bare `Error POSTing to endpoint` (#2442).
16790
17400
  *
16791
17401
  * `Protocol.request` is the one funnel every SDK verb goes through —
16792
17402
  * `listTools`, `callTool`, `readResource`, the handshake's `initialize`,
@@ -16804,21 +17414,30 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16804
17414
  * implementation signature serves both at runtime; the single cast on the
16805
17415
  * way back is what the overloads cost.
16806
17416
  */
16807
- annotateSdkRequestTimeouts(client) {
17417
+ annotateSdkRequestErrors(client) {
16808
17418
  const sdkRequest = client.request.bind(client);
16809
17419
  const decorated = async (...args) => {
16810
17420
  try {
16811
17421
  return await sdkRequest(...args);
16812
17422
  } catch (err) {
16813
- throw annotateRequestTimeout(
16814
- err,
16815
- args[0].method,
16816
- this.getConnectionDiagnostics()
16817
- );
17423
+ throw this.annotateRequestError(err, args[0].method);
16818
17424
  }
16819
17425
  };
16820
17426
  client.request = decorated;
16821
17427
  }
17428
+ /**
17429
+ * The one place a failed request's error is decorated, shared by the SDK
17430
+ * funnel above and the raw-wire channel that bypasses it (modern-era tool
17431
+ * calls and task operations), so a caller sees the same error whichever
17432
+ * path its request took: a timeout with the connection's state (#2318), a
17433
+ * `429` named as rate limiting (#2442), anything else untouched.
17434
+ */
17435
+ annotateRequestError(err, method) {
17436
+ return annotateRateLimit(
17437
+ annotateRequestTimeout(err, method, this.getConnectionDiagnostics()),
17438
+ method
17439
+ );
17440
+ }
16822
17441
  /**
16823
17442
  * @param base - the composed OAuth-path fetch, deadline innermost (#2319).
16824
17443
  *
@@ -16862,9 +17481,14 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16862
17481
  },
16863
17482
  trackSendFailure: (message, error) => {
16864
17483
  if (isRequestTimeoutError(error)) return;
17484
+ attachRetryAfter(error, this.rateLimits.take(message.id));
16865
17485
  if (this.outstandingRequests.delete(message.id)) {
16866
17486
  this.dispatchConnectionDiagnosticsChange();
16867
17487
  }
17488
+ this.dispatchTypedEvent("requestFailed", {
17489
+ id: message.id,
17490
+ reason: describeSendFailure(error)
17491
+ });
16868
17492
  },
16869
17493
  trackResponse: (message, origin) => {
16870
17494
  const responseId = message.id;
@@ -16930,6 +17554,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
16930
17554
  };
16931
17555
  baseTransport.onerror = (error) => {
16932
17556
  if (this.status === "connecting") return;
17557
+ if (subscriptionListenRefusal(error) !== void 0) return;
16933
17558
  this.status = "error";
16934
17559
  this.dispatchTypedEvent("statusChange", this.status);
16935
17560
  this.dispatchTypedEvent("error", error);
@@ -17175,6 +17800,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
17175
17800
  this.clearModernReconnectTimer();
17176
17801
  this.modernReconnectAttempts = 0;
17177
17802
  this.modernNeverAcknowledged = false;
17803
+ this.modernListenRefusal = null;
17178
17804
  this.modernSubscription = null;
17179
17805
  this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
17180
17806
  this.dispatchSubscriptionsChange();
@@ -17213,6 +17839,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
17213
17839
  this.taskProgress.clear();
17214
17840
  this.lastResponse = void 0;
17215
17841
  this.notificationStream = void 0;
17842
+ this.rateLimits.clear();
17216
17843
  this.dispatchConnectionDiagnosticsChange();
17217
17844
  if (this.malformedListItems.size > 0) {
17218
17845
  this.malformedListItems.clear();
@@ -17315,6 +17942,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
17315
17942
  onFetchRequest: (entry) => {
17316
17943
  this.dispatchFetchRequest({ ...entry, category: "transport" });
17317
17944
  this.noteTransportStream(entry);
17945
+ this.rateLimits.record(entry);
17318
17946
  },
17319
17947
  onFetchResponseBody: (id, body) => {
17320
17948
  this.dispatchFetchRequestBodyUpdate(id, body);
@@ -18031,7 +18659,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
18031
18659
  }
18032
18660
  const wrapped = withInputRequired(resultSchema);
18033
18661
  const signal = requestOptions.signal;
18034
- let round = 0;
18662
+ let round2 = 0;
18035
18663
  let nextParams = params;
18036
18664
  while (true) {
18037
18665
  const outcome = await client.request(
@@ -18045,8 +18673,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
18045
18673
  if (!isInputRequiredResult(outcome)) {
18046
18674
  return outcome;
18047
18675
  }
18048
- round += 1;
18049
- if (round > _InspectorClient.MRTR_MAX_ROUNDS) {
18676
+ round2 += 1;
18677
+ if (round2 > _InspectorClient.MRTR_MAX_ROUNDS) {
18050
18678
  throw new Error(
18051
18679
  `Multi-round-trip "${method}" exceeded ${_InspectorClient.MRTR_MAX_ROUNDS} input_required rounds without completing.`
18052
18680
  );
@@ -19654,11 +20282,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
19654
20282
  // for page one.
19655
20283
  ...cursor2 !== void 0 ? { cursor: cursor2 } : {}
19656
20284
  };
20285
+ const resultSchema = this.isModernEra() ? ModernDirectoryReadResultSchema : DirectoryReadResultSchema;
19657
20286
  try {
19658
20287
  return await this.invokeMcpClient(
19659
20288
  () => this.client.request(
19660
20289
  { method: RESOURCES_DIRECTORY_READ_METHOD, params },
19661
- DirectoryReadResultSchema,
20290
+ resultSchema,
19662
20291
  this.getRequestOptions(this.progressTokenOf(metadata))
19663
20292
  ),
19664
20293
  { method: RESOURCES_DIRECTORY_READ_METHOD }
@@ -20113,6 +20742,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
20113
20742
  this.clearModernReconnectTimer();
20114
20743
  this.modernReconnectAttempts = 0;
20115
20744
  this.modernNeverAcknowledged = false;
20745
+ this.modernListenRefusal = null;
20116
20746
  }
20117
20747
  const generation = ++this.modernListenGeneration;
20118
20748
  const previous = this.modernSubscription;
@@ -20131,8 +20761,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
20131
20761
  this.getRequestOptions()
20132
20762
  );
20133
20763
  } catch (error) {
20134
- if (generation === this.modernListenGeneration && isNeverAcknowledgedSubscriptionClose(error)) {
20135
- this.modernNeverAcknowledged = true;
20764
+ if (generation === this.modernListenGeneration) {
20765
+ if (isNeverAcknowledgedSubscriptionClose(error)) {
20766
+ this.modernNeverAcknowledged = true;
20767
+ }
20768
+ this.modernListenRefusal = subscriptionListenRefusal(error) ?? null;
20136
20769
  }
20137
20770
  throw error;
20138
20771
  }
@@ -20213,6 +20846,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
20213
20846
  * its error either way — the retry is about the subscriptions, not the call.
20214
20847
  */
20215
20848
  reconcileModernStreamStateAfterFailedRefresh() {
20849
+ if (this.modernListenRefusal) {
20850
+ this.endModernStreamRefused(this.modernListenRefusal);
20851
+ return;
20852
+ }
20216
20853
  if (!this.wantsModernStream()) {
20217
20854
  this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
20218
20855
  return;
@@ -20245,6 +20882,23 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
20245
20882
  honoredUris: []
20246
20883
  });
20247
20884
  }
20885
+ /**
20886
+ * Settle the stream on an HTTP refusal of the listen (#2722): the
20887
+ * `"refused"` status carrying the HTTP answer, no reconnect, and a log line
20888
+ * with the sentence the UI shows. Reported whatever `active` says — see
20889
+ * `ResourceSubscriptionStreamState`.
20890
+ */
20891
+ endModernStreamRefused(refusal) {
20892
+ this.clearModernReconnectTimer();
20893
+ const message = subscriptionListenRefusalMessage(refusal);
20894
+ this.logger.warn({ message }, "subscriptions/listen refused over HTTP");
20895
+ this.setModernStreamState({
20896
+ active: this.modernStreamActive(),
20897
+ status: "refused",
20898
+ honoredUris: [],
20899
+ refusal
20900
+ });
20901
+ }
20248
20902
  /**
20249
20903
  * Schedule a reconnect re-listen after the current backoff delay (#1630).
20250
20904
  * `modernReconnectAttempts` reflects the number of *consecutive failed*
@@ -20282,6 +20936,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
20282
20936
  this.endModernStreamNeverAcknowledged();
20283
20937
  return;
20284
20938
  }
20939
+ if (this.modernListenRefusal && !isTerminalStatus(this.status)) {
20940
+ this.endModernStreamRefused(this.modernListenRefusal);
20941
+ return;
20942
+ }
20285
20943
  this.modernReconnectAttempts += 1;
20286
20944
  if (this.modernReconnectAttempts > MODERN_RECONNECT_MAX_ATTEMPTS || isTerminalStatus(this.status) || !this.wantsModernStream()) {
20287
20945
  this.setModernStreamState({
@@ -20436,8 +21094,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
20436
21094
  * Re-read OAuth storage and test whether a challenge is already satisfied.
20437
21095
  * See {@link OAuthManager.checkAuthChallengeSatisfied}.
20438
21096
  */
20439
- async checkAuthChallengeSatisfied(challenge) {
20440
- return this.ensureOAuthManager().checkAuthChallengeSatisfied(challenge);
21097
+ async checkAuthChallengeSatisfied(challenge, options) {
21098
+ return this.ensureOAuthManager().checkAuthChallengeSatisfied(
21099
+ challenge,
21100
+ options
21101
+ );
20441
21102
  }
20442
21103
  /**
20443
21104
  * Push recovered OAuth auth state to the remote backend (same MCP session).
@@ -20601,8 +21262,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
20601
21262
  * Direct transports reconnect after token exchange so the live MCP session
20602
21263
  * picks up the new Bearer token (mirrors silent recovery reconnect).
20603
21264
  */
20604
- async completeOAuthFlow(authorizationCode, iss) {
20605
- await this.ensureOAuthManager().completeOAuthFlow(authorizationCode, iss);
21265
+ async completeOAuthFlow(authorizationCode, iss, state) {
21266
+ await this.ensureOAuthManager().completeOAuthFlow(
21267
+ authorizationCode,
21268
+ iss,
21269
+ state
21270
+ );
20606
21271
  if (this.usesDirectAuthRecovery()) {
20607
21272
  await this.reconnectAfterAuthRecovery();
20608
21273
  }
@@ -20626,7 +21291,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
20626
21291
  * Finish OAuth after a full-page redirect and reconnect (or reattach) the MCP session.
20627
21292
  */
20628
21293
  async resumeAfterOAuth(authorizationCode, options) {
20629
- await this.completeOAuthFlow(authorizationCode, options?.iss);
21294
+ await this.completeOAuthFlow(
21295
+ authorizationCode,
21296
+ options?.iss,
21297
+ options?.state
21298
+ );
20630
21299
  const remoteSessionId = options?.remoteSessionId;
20631
21300
  const transport = this.baseTransport;
20632
21301
  if (remoteSessionId && transport instanceof RemoteClientTransport) {
@@ -21064,6 +21733,16 @@ var MessageLogState = class extends TypedEventTarget {
21064
21733
  return;
21065
21734
  }
21066
21735
  };
21736
+ const onRequestFailed = (event) => {
21737
+ const { id, reason } = event.detail;
21738
+ const requestEntry = this.pendingRequestEntries.get(id);
21739
+ if (!requestEntry) return;
21740
+ this.pendingRequestEntries.delete(id);
21741
+ requestEntry.clientError = reason;
21742
+ requestEntry.duration = Date.now() - requestEntry.timestamp.getTime();
21743
+ this.dispatchTypedEvent("message", requestEntry);
21744
+ this.dispatchTypedEvent("messagesChange", this.getMessages());
21745
+ };
21067
21746
  const onStatusChange = () => {
21068
21747
  if (isTerminalStatus(this.client?.getStatus())) {
21069
21748
  this.messages = [];
@@ -21073,11 +21752,13 @@ var MessageLogState = class extends TypedEventTarget {
21073
21752
  };
21074
21753
  this.client.addEventListener("message", onMessage);
21075
21754
  this.client.addEventListener("responseRejected", onResponseRejected);
21755
+ this.client.addEventListener("requestFailed", onRequestFailed);
21076
21756
  this.client.addEventListener("statusChange", onStatusChange);
21077
21757
  this.unsubscribe = () => {
21078
21758
  if (this.client) {
21079
21759
  this.client.removeEventListener("message", onMessage);
21080
21760
  this.client.removeEventListener("responseRejected", onResponseRejected);
21761
+ this.client.removeEventListener("requestFailed", onRequestFailed);
21081
21762
  this.client.removeEventListener("statusChange", onStatusChange);
21082
21763
  }
21083
21764
  this.client = null;
@@ -21526,6 +22207,192 @@ var ManagedSkillsState = class extends TypedEventTarget {
21526
22207
  }
21527
22208
  };
21528
22209
 
22210
+ // ../../core/cli/handlers/load-test.ts
22211
+ var DEFAULT_LOAD_CONCURRENCY = 10;
22212
+ var DEFAULT_LOAD_DURATION_MS = 1e4;
22213
+ function positiveIntegerParser(flag) {
22214
+ return (value) => {
22215
+ const trimmed = value.trim();
22216
+ const n = /^\d+$/.test(trimmed) ? Number(trimmed) : NaN;
22217
+ if (!Number.isSafeInteger(n) || n <= 0) {
22218
+ throw new Error(`${flag} must be a positive integer.`);
22219
+ }
22220
+ return n;
22221
+ };
22222
+ }
22223
+ function positiveNumberParser(flag) {
22224
+ return (value) => {
22225
+ const n = value.trim() === "" ? NaN : Number(value);
22226
+ if (!Number.isFinite(n) || n <= 0) {
22227
+ throw new Error(`${flag} must be a positive number.`);
22228
+ }
22229
+ return n;
22230
+ };
22231
+ }
22232
+ function resolveLoadOptions(flags) {
22233
+ const durationMs = flags.durationSec !== void 0 ? flags.durationSec * 1e3 : flags.requests === void 0 ? DEFAULT_LOAD_DURATION_MS : void 0;
22234
+ return {
22235
+ concurrency: flags.concurrency ?? DEFAULT_LOAD_CONCURRENCY,
22236
+ ...durationMs !== void 0 && { durationMs },
22237
+ ...flags.requests !== void 0 && { requests: flags.requests },
22238
+ ...flags.rate !== void 0 && { rate: flags.rate }
22239
+ };
22240
+ }
22241
+ var MAX_ERROR_KINDS = 10;
22242
+ var OTHER_ERRORS = "(other)";
22243
+ var realClock = {
22244
+ now: () => performance.now(),
22245
+ sleep: (ms) => new Promise((resolve9) => setTimeout(resolve9, ms))
22246
+ };
22247
+ function percentile(sorted, p) {
22248
+ const rank = Math.ceil(p / 100 * sorted.length);
22249
+ return sorted[Math.min(Math.max(rank, 1), sorted.length) - 1];
22250
+ }
22251
+ function round(n, places = 2) {
22252
+ const scale = 10 ** places;
22253
+ return Math.round(n * scale) / scale;
22254
+ }
22255
+ function summarizeLatency(latencies) {
22256
+ if (latencies.length === 0) return null;
22257
+ const sorted = [...latencies].sort((a, b) => a - b);
22258
+ const total = sorted.reduce((sum, n) => sum + n, 0);
22259
+ return {
22260
+ min: round(sorted[0]),
22261
+ mean: round(total / sorted.length),
22262
+ p50: round(percentile(sorted, 50)),
22263
+ p90: round(percentile(sorted, 90)),
22264
+ p99: round(percentile(sorted, 99)),
22265
+ max: round(sorted[sorted.length - 1])
22266
+ };
22267
+ }
22268
+ var ErrorTally = class {
22269
+ counts = /* @__PURE__ */ new Map();
22270
+ other = 0;
22271
+ add(message) {
22272
+ const count = this.counts.get(message);
22273
+ if (count !== void 0) this.counts.set(message, count + 1);
22274
+ else if (this.counts.size < MAX_ERROR_KINDS) this.counts.set(message, 1);
22275
+ else this.other++;
22276
+ }
22277
+ entries() {
22278
+ const list = [...this.counts].map(([message, count]) => ({
22279
+ message,
22280
+ count
22281
+ }));
22282
+ list.sort((a, b) => b.count - a.count);
22283
+ if (this.other > 0) list.push({ message: OTHER_ERRORS, count: this.other });
22284
+ return list;
22285
+ }
22286
+ };
22287
+ async function runLoad(call, options, clock = realClock) {
22288
+ const { concurrency, durationMs, requests, rate } = options;
22289
+ const latencies = [];
22290
+ const tally = new ErrorTally();
22291
+ let ok = 0;
22292
+ let toolErrors = 0;
22293
+ let errors = 0;
22294
+ let started = 0;
22295
+ const start = clock.now();
22296
+ const deadline = durationMs === void 0 ? Infinity : start + durationMs;
22297
+ const interval = rate === void 0 ? 0 : 1e3 / rate;
22298
+ let nextStart = start;
22299
+ let turnstile = Promise.resolve();
22300
+ const claimSlot = async () => {
22301
+ const ahead = turnstile;
22302
+ let release;
22303
+ turnstile = new Promise((resolve9) => release = resolve9);
22304
+ await ahead;
22305
+ try {
22306
+ for (; ; ) {
22307
+ if (requests !== void 0 && started >= requests) return false;
22308
+ const now = clock.now();
22309
+ if (now >= deadline) return false;
22310
+ const scheduled = Math.max(nextStart, now);
22311
+ if (scheduled >= deadline) return false;
22312
+ if (scheduled <= now) {
22313
+ started++;
22314
+ nextStart = now - nextStart < interval ? nextStart + interval : now + interval;
22315
+ return true;
22316
+ }
22317
+ await clock.sleep(scheduled - now);
22318
+ }
22319
+ } finally {
22320
+ release();
22321
+ }
22322
+ };
22323
+ const worker = async () => {
22324
+ while (await claimSlot()) {
22325
+ const t0 = clock.now();
22326
+ try {
22327
+ const outcome = await call();
22328
+ if (outcome === "ok") ok++;
22329
+ else toolErrors++;
22330
+ } catch (err) {
22331
+ errors++;
22332
+ tally.add(err instanceof Error ? err.message : String(err));
22333
+ }
22334
+ latencies.push(clock.now() - t0);
22335
+ }
22336
+ };
22337
+ await Promise.all(Array.from({ length: concurrency }, worker));
22338
+ const elapsedMs = clock.now() - start;
22339
+ const completed = ok + toolErrors + errors;
22340
+ return {
22341
+ concurrency,
22342
+ ...durationMs !== void 0 && { durationMs },
22343
+ ...requests !== void 0 && { requestLimit: requests },
22344
+ ...rate !== void 0 && { rate },
22345
+ requests: completed,
22346
+ ok,
22347
+ toolErrors,
22348
+ errors,
22349
+ // Four places, so a rare failure under heavy load (23 in 41k) still shows.
22350
+ errorRate: completed === 0 ? 0 : round((toolErrors + errors) / completed, 4),
22351
+ elapsedMs: round(elapsedMs),
22352
+ throughput: elapsedMs > 0 ? round(completed * 1e3 / elapsedMs) : 0,
22353
+ latencyMs: summarizeLatency(latencies),
22354
+ errorMessages: tally.entries()
22355
+ };
22356
+ }
22357
+ async function runToolLoad(inspectorClient, args, options, clock) {
22358
+ if (!args.toolName) {
22359
+ throw new Error("--load requires --tool-name.");
22360
+ }
22361
+ const toolsState = new ManagedToolsState(inspectorClient);
22362
+ let found;
22363
+ try {
22364
+ toolsState.setMetadata(args.metadata);
22365
+ await toolsState.refresh();
22366
+ found = toolsState.getTools().find((t) => t.name === args.toolName);
22367
+ } finally {
22368
+ toolsState.destroy();
22369
+ }
22370
+ if (!found) {
22371
+ throw new CliExitCodeError(
22372
+ EXIT_CODES.TOOL_ERROR,
22373
+ `Tool '${args.toolName}' not found on server.`,
22374
+ { code: "tool_not_found" }
22375
+ );
22376
+ }
22377
+ const tool = found;
22378
+ const useTask = tool.execution?.taskSupport === "required";
22379
+ const call = async () => {
22380
+ const params = [
22381
+ tool,
22382
+ args.toolArg ?? {},
22383
+ args.metadata,
22384
+ args.toolMeta
22385
+ ];
22386
+ const invocation = useTask ? await inspectorClient.callToolStream(...params) : await inspectorClient.callTool(...params);
22387
+ if (invocation.result === null) {
22388
+ throw new Error(invocation.error ?? "Tool call failed");
22389
+ }
22390
+ return invocation.result.isError === true ? "tool_error" : "ok";
22391
+ };
22392
+ const summary = await runLoad(call, options, clock);
22393
+ return { tool: tool.name, ...summary };
22394
+ }
22395
+
21529
22396
  // ../../core/mcp/apps.ts
21530
22397
  import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
21531
22398
  function getAppResourceUri(tool) {
@@ -22512,7 +23379,11 @@ async function runRunnerInteractiveOAuth(options) {
22512
23379
  path: options.callbackListen.pathname,
22513
23380
  onCallback: async (params) => {
22514
23381
  try {
22515
- await options.client.completeOAuthFlow(params.code, params.iss);
23382
+ await options.client.completeOAuthFlow(
23383
+ params.code,
23384
+ params.iss,
23385
+ params.state
23386
+ );
22516
23387
  flowResolve();
22517
23388
  } catch (err) {
22518
23389
  flowReject(toRunnerOAuthError(err));
@@ -22564,7 +23435,10 @@ async function runRunnerInteractiveOAuth(options) {
22564
23435
  }
22565
23436
  if (options.authChallenge) {
22566
23437
  const satisfied = await racingSignals(
22567
- options.client.checkAuthChallengeSatisfied(options.authChallenge)
23438
+ // The token exchange just completed, so only scope is in question.
23439
+ options.client.checkAuthChallengeSatisfied(options.authChallenge, {
23440
+ scopeOnly: true
23441
+ })
22568
23442
  );
22569
23443
  if (!satisfied) {
22570
23444
  return {
@@ -22590,14 +23464,25 @@ async function runRunnerInteractiveOAuth(options) {
22590
23464
  // ../../core/node/openUrl.ts
22591
23465
  import open from "open";
22592
23466
  var OPEN_URL_TIMEOUT_MS = 5e3;
22593
- function launched(child) {
22594
- return new Promise((resolve9, reject) => {
22595
- child.on("error", reject);
22596
- child.once("spawn", () => resolve9());
23467
+ var OPENABLE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
23468
+ function toOpenableHref(href) {
23469
+ let parsed;
23470
+ try {
23471
+ parsed = new URL(href);
23472
+ } catch {
23473
+ throw new Error("refusing to open an invalid URL");
23474
+ }
23475
+ if (!OPENABLE_PROTOCOLS.has(parsed.protocol)) {
23476
+ throw new Error(`refusing to open a non-http(s) URL (${parsed.protocol})`);
23477
+ }
23478
+ return parsed.href;
23479
+ }
23480
+ function absorbLateErrors(child) {
23481
+ child.on("error", () => {
22597
23482
  });
22598
23483
  }
22599
23484
  async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
22600
- const href = typeof url === "string" ? url : url.href;
23485
+ const href = toOpenableHref(typeof url === "string" ? url : url.href);
22601
23486
  let timer;
22602
23487
  const timeout = new Promise((_, reject) => {
22603
23488
  timer = setTimeout(
@@ -22606,8 +23491,7 @@ async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
22606
23491
  );
22607
23492
  });
22608
23493
  try {
22609
- const opened = Promise.resolve().then(() => open(href)).then(launched);
22610
- await Promise.race([opened, timeout]);
23494
+ await Promise.race([open(href).then(absorbLateErrors), timeout]);
22611
23495
  } finally {
22612
23496
  clearTimeout(timer);
22613
23497
  }
@@ -23061,7 +23945,7 @@ var validLogLevels = Object.values(
23061
23945
  LoggingLevelSchema2.enum
23062
23946
  );
23063
23947
  var CLI_CLIENT_NAME = "inspector-cli";
23064
- async function callMethod(serverConfig, serverSettings, args, clientConfig, cliAuthOverrides, callbackUrlConfig, storedAuthOnly, relogin, revoke) {
23948
+ async function callMethod(serverConfig, serverSettings, args, clientConfig, cliAuthOverrides, callbackUrlConfig, storedAuthOnly, relogin, revoke, load) {
23065
23949
  if (relogin) {
23066
23950
  if (!("url" in serverConfig && serverConfig.url)) {
23067
23951
  throw new Error(
@@ -23167,6 +24051,22 @@ async function callMethod(serverConfig, serverSettings, args, clientConfig, cliA
23167
24051
  serverSettings,
23168
24052
  { storedAuthOnly, autoOpenControl, quiet: args.quiet }
23169
24053
  );
24054
+ if (load) {
24055
+ const summary = await withCliAuthRecoveryRetry(
24056
+ inspectorClient,
24057
+ serverConfig,
24058
+ redirectUrlProvider,
24059
+ callbackUrlConfig,
24060
+ serverSettings,
24061
+ () => runToolLoad(inspectorClient, args, load),
24062
+ { storedAuthOnly, autoOpenControl, quiet: args.quiet }
24063
+ );
24064
+ await writeFormattedResult(
24065
+ summary,
24066
+ args.format === "json" ? "json" : "text"
24067
+ );
24068
+ return;
24069
+ }
23170
24070
  const outcome = await withCliAuthRecoveryRetry(
23171
24071
  inspectorClient,
23172
24072
  serverConfig,
@@ -23357,6 +24257,7 @@ function buildHandoff(serverUrl, statePath, transport) {
23357
24257
  const host = process.env.HOST || "127.0.0.1";
23358
24258
  const linkHost = isAllInterfacesHost(host) ? "localhost" : canonicalUrlHost(host);
23359
24259
  const clientPort = process.env.CLIENT_PORT || "6274";
24260
+ const scheme = process.env.INSPECTOR_TLS_KEY_PATH?.trim() && process.env.INSPECTOR_TLS_CERT_PATH?.trim() ? "https" : "http";
23360
24261
  const sandboxPort = process.env.MCP_SANDBOX_PORT || "6275";
23361
24262
  const appOriginPort = process.env.MCP_APP_ORIGIN_PORT || "6278";
23362
24263
  const apiToken = process.env.MCP_INSPECTOR_API_TOKEN || void 0;
@@ -23368,7 +24269,7 @@ function buildHandoff(serverUrl, statePath, transport) {
23368
24269
  if (apiToken) params.set("autoConnect", apiToken);
23369
24270
  return {
23370
24271
  serverUrl: normalizedUrl,
23371
- deepLink: `http://${linkHost}:${clientPort}/?${params.toString()}`,
24272
+ deepLink: `${scheme}://${linkHost}:${clientPort}/?${params.toString()}`,
23372
24273
  portForwardCmd: `coder port-forward <workspace> --tcp ${clientPort}:${clientPort} --tcp ${sandboxPort}:${sandboxPort} --tcp ${appOriginPort}:${appOriginPort}`,
23373
24274
  oauthStatePath: statePath,
23374
24275
  apiToken: apiToken ?? null,
@@ -23528,6 +24429,25 @@ async function parseArgs(argv) {
23528
24429
  ).option(
23529
24430
  "--require-digests",
23530
24431
  'With --verify: exit 9 when a skill advertises no digests (resources: "dynamic"), instead of reporting it as unverifiable and exiting 0.'
24432
+ ).option(
24433
+ "--load",
24434
+ "Load-test one tool: call it repeatedly and concurrently, then print a latency and error-rate summary instead of a result. Use with --method tools/call --tool-name <name>. Bounded by --load-duration (default 10s) and/or --load-requests."
24435
+ ).option(
24436
+ "--load-concurrency <n>",
24437
+ "With --load: most calls in flight at once (positive integer; default 10).",
24438
+ positiveIntegerParser("--load-concurrency")
24439
+ ).option(
24440
+ "--load-duration <sec>",
24441
+ "With --load: stop starting calls after this many seconds; in-flight calls still finish (default 10 when --load-requests is not set).",
24442
+ positiveNumberParser("--load-duration")
24443
+ ).option(
24444
+ "--load-requests <n>",
24445
+ "With --load: stop after starting this many calls (positive integer).",
24446
+ positiveIntegerParser("--load-requests")
24447
+ ).option(
24448
+ "--load-rate <n>",
24449
+ "With --load: start at most this many calls per second, spread evenly (positive number; default unlimited).",
24450
+ positiveNumberParser("--load-rate")
23531
24451
  ).option(
23532
24452
  "--connect-timeout <ms>",
23533
24453
  `Connection timeout in ms (default ${DEFAULT_CONNECT_TIMEOUT_MS} for ad-hoc --server-url / target invocations; 0 = no timeout).`,
@@ -23680,6 +24600,35 @@ async function parseArgs(argv) {
23680
24600
  if (options.skillCatalogMaxBytes !== void 0 && !options.verify) {
23681
24601
  throw new Error("--skill-catalog-max-bytes requires --verify.");
23682
24602
  }
24603
+ const loadBounds = [
24604
+ ["--load-concurrency", options.loadConcurrency],
24605
+ ["--load-duration", options.loadDuration],
24606
+ ["--load-requests", options.loadRequests],
24607
+ ["--load-rate", options.loadRate]
24608
+ ];
24609
+ for (const [flag, value] of loadBounds) {
24610
+ if (value !== void 0 && !options.load) {
24611
+ throw new Error(`${flag} requires --load.`);
24612
+ }
24613
+ }
24614
+ if (options.load) {
24615
+ if (options.method !== "tools/call") {
24616
+ throw new Error("--load requires --method tools/call.");
24617
+ }
24618
+ if (options.appInfo) {
24619
+ throw new Error("--load cannot be combined with --app-info.");
24620
+ }
24621
+ if (options.listStoredAuth || options.printHandoff) {
24622
+ throw new Error(
24623
+ "--load cannot be combined with --list-stored-auth or --print-handoff."
24624
+ );
24625
+ }
24626
+ if (options.output !== void 0 || options.outputFormat !== void 0) {
24627
+ throw new Error(
24628
+ "--load cannot be combined with --output or --output-format; redirect stdout to keep the summary."
24629
+ );
24630
+ }
24631
+ }
23683
24632
  if (options.advertiseApps && (options.listStoredAuth || options.printHandoff || options.method === "servers/list" || options.method === "servers/show" || isCatalogWriteMethod(options.method))) {
23684
24633
  throw new Error(
23685
24634
  "--advertise-apps requires a command that connects to a server; it has no effect with --list-stored-auth, --print-handoff, or a --method servers/* catalog command."
@@ -23902,7 +24851,15 @@ async function parseArgs(argv) {
23902
24851
  relogin: options.relogin === true,
23903
24852
  // Commander's `--no-revoke` defaults this to true; only an explicit
23904
24853
  // `--no-revoke` makes it false.
23905
- revoke: options.revoke !== false
24854
+ revoke: options.revoke !== false,
24855
+ ...options.load && {
24856
+ load: resolveLoadOptions({
24857
+ concurrency: options.loadConcurrency,
24858
+ durationSec: options.loadDuration,
24859
+ requests: options.loadRequests,
24860
+ rate: options.loadRate
24861
+ })
24862
+ }
23906
24863
  };
23907
24864
  }
23908
24865
  function discardWarning() {
@@ -23929,7 +24886,8 @@ async function runParsedCli(argv) {
23929
24886
  callbackUrl,
23930
24887
  storedAuthOnly,
23931
24888
  relogin,
23932
- revoke
24889
+ revoke,
24890
+ load
23933
24891
  } = parsed;
23934
24892
  const clientConfig = await loadRunnerClientConfig({ clientConfigPath });
23935
24893
  let callbackUrlConfig;
@@ -23954,7 +24912,8 @@ async function runParsedCli(argv) {
23954
24912
  callbackUrlConfig,
23955
24913
  storedAuthOnly === true,
23956
24914
  relogin === true,
23957
- revoke !== false
24915
+ revoke !== false,
24916
+ load
23958
24917
  );
23959
24918
  }
23960
24919