@modelcontextprotocol/inspector 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -90,6 +90,19 @@ var SECRET_FIELD_OAUTH_CLIENT_SECRET = "oauth-client-secret";
90
90
  var SECRET_FIELD_IDP_CLIENT_SECRET = "idp-client-secret";
91
91
  var envSecretField = (envKey) => `env:${envKey}`;
92
92
 
93
+ // ../../core/storage/own-entry.ts
94
+ function setOwnEntry(map, key, value) {
95
+ Object.defineProperty(map, key, {
96
+ value,
97
+ enumerable: true,
98
+ writable: true,
99
+ configurable: true
100
+ });
101
+ }
102
+ function getOwnEntry(map, key) {
103
+ return map !== void 0 && Object.hasOwn(map, key) ? map[key] : void 0;
104
+ }
105
+
93
106
  // ../../core/auth/node/secret-store.ts
94
107
  var SERVICE_NAME = "mcp-inspector";
95
108
  var keyringLoad;
@@ -142,6 +155,12 @@ var SecretStoreUnavailableError = class extends Error {
142
155
  this.name = "SecretStoreUnavailableError";
143
156
  }
144
157
  };
158
+ var SecretFileLockHeldError = class extends SecretStoreUnavailableError {
159
+ constructor(message, options) {
160
+ super(message, options);
161
+ this.name = "SecretFileLockHeldError";
162
+ }
163
+ };
145
164
  var KeychainUnavailableError = class extends SecretStoreUnavailableError {
146
165
  constructor(cause) {
147
166
  const message = cause instanceof Error ? cause.message : String(cause);
@@ -172,6 +191,7 @@ async function probeKeyringAvailable() {
172
191
  try {
173
192
  const entry = new keyring.mod.AsyncEntry(SERVICE_NAME, PROBE_ACCOUNT);
174
193
  await entry.getPassword();
194
+ await keyring.mod.findCredentialsAsync(SERVICE_NAME);
175
195
  return { available: true };
176
196
  } catch (err) {
177
197
  return {
@@ -181,6 +201,7 @@ async function probeKeyringAvailable() {
181
201
  }
182
202
  }
183
203
  var PROBE_ACCOUNT = "__inspector:probe";
204
+ var isNoEntryError = (err) => err instanceof Error && /no (matching )?entry/i.test(err.message);
184
205
  var KeyringSecretStore = class {
185
206
  /**
186
207
  * The intolerant read. Same lookup as {@link get}, minus the catch — a
@@ -232,23 +253,27 @@ var KeyringSecretStore = class {
232
253
  async delete(serverId, field) {
233
254
  try {
234
255
  const keyring = await loadKeyring();
235
- if (!keyring.ok) return;
256
+ if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
236
257
  const entry = new keyring.mod.AsyncEntry(
237
258
  SERVICE_NAME,
238
259
  buildAccount(serverId, field)
239
260
  );
240
261
  await entry.deleteCredential();
241
- } catch {
262
+ } catch (err) {
263
+ if (isNoEntryError(err)) return;
264
+ if (err instanceof KeychainUnavailableError) throw err;
265
+ throw new KeychainUnavailableError(err);
242
266
  }
243
267
  }
244
268
  async deleteAllForServer(serverId) {
245
269
  let creds;
246
270
  try {
247
271
  const keyring = await loadKeyring();
248
- if (!keyring.ok) return;
272
+ if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
249
273
  creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
250
- } catch {
251
- return;
274
+ } catch (err) {
275
+ if (err instanceof KeychainUnavailableError) throw err;
276
+ throw new KeychainUnavailableError(err);
252
277
  }
253
278
  const prefix = `${serverId}:`;
254
279
  for (const c of creds) {
@@ -262,6 +287,25 @@ var KeyringSecretStore = class {
262
287
  async function secretStoreGetStrict(store, serverId, field) {
263
288
  return store.getStrict ? store.getStrict(serverId, field) : store.get(serverId, field);
264
289
  }
290
+ async function snapshotSecretFields(store, serverId, fields) {
291
+ return Promise.all(
292
+ fields.map(async (field) => ({
293
+ serverId,
294
+ field,
295
+ value: await secretStoreGetStrict(store, serverId, field)
296
+ }))
297
+ );
298
+ }
299
+ async function restoreSecretFields(store, snapshot, onRestoreFailure) {
300
+ for (const { serverId, field, value } of snapshot) {
301
+ try {
302
+ if (value === null) await store.delete(serverId, field);
303
+ else await store.set(serverId, field, value);
304
+ } catch (error) {
305
+ onRestoreFailure(error);
306
+ }
307
+ }
308
+ }
265
309
  async function secretStoreGetMany(store, requests) {
266
310
  if (store.getMany) return store.getMany(requests);
267
311
  const out = {};
@@ -274,16 +318,44 @@ async function secretStoreGetMany(store, requests) {
274
318
  );
275
319
  const found = {};
276
320
  for (const [field, value] of entries) {
277
- if (value !== null) found[field] = value;
321
+ if (value !== null) setOwnEntry(found, field, value);
278
322
  }
279
- out[serverId] = found;
323
+ setOwnEntry(out, serverId, found);
280
324
  })
281
325
  );
282
326
  return out;
283
327
  }
328
+ async function secretStoreGetManyStrict(store, requests) {
329
+ if (store.getManyStrict) return store.getManyStrict(requests);
330
+ const out = {};
331
+ await Promise.all(
332
+ requests.map(async ({ serverId, fields }) => {
333
+ const entries = await Promise.all(
334
+ fields.map(
335
+ async (field) => [
336
+ field,
337
+ await secretStoreGetStrict(store, serverId, field)
338
+ ]
339
+ )
340
+ );
341
+ const found = {};
342
+ for (const [field, value] of entries) {
343
+ if (value !== null) setOwnEntry(found, field, value);
344
+ }
345
+ setOwnEntry(out, serverId, found);
346
+ })
347
+ );
348
+ return out;
349
+ }
350
+ async function settleStoreMutations(mutations) {
351
+ const results = await Promise.allSettled(mutations);
352
+ for (const result of results) {
353
+ if (result.status === "rejected") throw result.reason;
354
+ }
355
+ }
284
356
  async function secretStoreSetMany(store, serverId, values) {
285
357
  if (store.setMany) return store.setMany(serverId, values);
286
- await Promise.all(
358
+ await settleStoreMutations(
287
359
  Object.entries(values).map(
288
360
  ([field, value]) => store.set(serverId, field, value)
289
361
  )
@@ -430,6 +502,10 @@ var ModernListSkillsResultSchema = ListSkillsResultSchema.extend({
430
502
  var GetSkillEnvelopeSchema = z.looseObject({
431
503
  skill: SkillEntrySchema
432
504
  });
505
+ var ModernGetSkillEnvelopeSchema = GetSkillEnvelopeSchema.extend({
506
+ ttlMs: z.int().min(0),
507
+ cacheScope: z.enum(["public", "private"])
508
+ });
433
509
  var GetSkillResultSchema = GetSkillEnvelopeSchema.transform(
434
510
  (result) => result.skill
435
511
  );
@@ -2601,7 +2677,7 @@ async function openSecretFileLock(filePath) {
2601
2677
  });
2602
2678
  } catch (err) {
2603
2679
  if (isStuckOrHeld(err, lockPathOf(target))) {
2604
- throw new SecretStoreUnavailableError(
2680
+ throw new SecretFileLockHeldError(
2605
2681
  `Could not save to the secrets file at ${target}: its lock (${lockPathOf(target)}) was still held after the ${Math.round(RETRY_BUDGET_MS / 1e3)} seconds this save waited. Its secrets are intact; the value you just entered was not saved. If no other Inspector is running, remove that lock and try again.`
2606
2682
  );
2607
2683
  }
@@ -2710,6 +2786,13 @@ function resolveSecretPassphrase(env = process.env, secretFilePath) {
2710
2786
  return { passphrase };
2711
2787
  }
2712
2788
  var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
2789
+ function assertSecretString(field, value) {
2790
+ if (typeof value !== "string") {
2791
+ throw new TypeError(
2792
+ `Refusing to store a non-string secret value (${typeof value}) for "${field}": the secrets file holds only strings, and writing this would make the whole file unreadable.`
2793
+ );
2794
+ }
2795
+ }
2713
2796
  function asSecretMap(value, filePath) {
2714
2797
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2715
2798
  throw new SecretStoreUnavailableError(
@@ -2968,7 +3051,9 @@ var FileSecretStore = class {
2968
3051
  let plain;
2969
3052
  try {
2970
3053
  const key = await this.deriveKey(parsed.kdf);
2971
- const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv);
3054
+ const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv, {
3055
+ authTagLength: GCM_TAG_BYTES
3056
+ });
2972
3057
  decipher.setAuthTag(parts.tag);
2973
3058
  plain = Buffer.concat([
2974
3059
  decipher.update(parts.body),
@@ -3002,7 +3087,9 @@ var FileSecretStore = class {
3002
3087
  };
3003
3088
  const key = await this.deriveKey(kdf);
3004
3089
  const iv = crypto2.randomBytes(IV_BYTES);
3005
- const cipher = crypto2.createCipheriv(CIPHER, key, iv);
3090
+ const cipher = crypto2.createCipheriv(CIPHER, key, iv, {
3091
+ authTagLength: GCM_TAG_BYTES
3092
+ });
3006
3093
  const body = Buffer.concat([
3007
3094
  cipher.update(JSON.stringify(map), "utf-8"),
3008
3095
  cipher.final()
@@ -3167,9 +3254,36 @@ var FileSecretStore = class {
3167
3254
  const found = {};
3168
3255
  for (const field of fields) {
3169
3256
  const value = map?.[buildAccount2(serverId, field)];
3170
- if (value !== void 0) found[field] = value;
3257
+ if (value !== void 0) setOwnEntry(found, field, value);
3258
+ }
3259
+ setOwnEntry(out, serverId, found);
3260
+ }
3261
+ return out;
3262
+ }
3263
+ /**
3264
+ * Bulk twin of {@link getStrict}: the same single-pass read as
3265
+ * {@link getMany}, but an unreadable store throws (with the typed advice
3266
+ * {@link getStrict} attaches) instead of yielding no fields — required by
3267
+ * OAuth read hydration, whose result later drives store deletions.
3268
+ */
3269
+ async getManyStrict(requests) {
3270
+ let map;
3271
+ try {
3272
+ map = await this.serialize(() => this.readMap());
3273
+ } catch (err) {
3274
+ if (err instanceof SecretStoreUnavailableError) throw err;
3275
+ throw new SecretStoreUnavailableError(
3276
+ `Could not read the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
3277
+ );
3278
+ }
3279
+ const out = {};
3280
+ for (const { serverId, fields } of requests) {
3281
+ const found = {};
3282
+ for (const field of fields) {
3283
+ const value = map?.[buildAccount2(serverId, field)];
3284
+ if (value !== void 0) setOwnEntry(found, field, value);
3171
3285
  }
3172
- out[serverId] = found;
3286
+ setOwnEntry(out, serverId, found);
3173
3287
  }
3174
3288
  return out;
3175
3289
  }
@@ -3213,6 +3327,9 @@ var FileSecretStore = class {
3213
3327
  */
3214
3328
  async setMany(serverId, values) {
3215
3329
  if (Object.keys(values).length === 0) return;
3330
+ for (const [field, value] of Object.entries(values)) {
3331
+ assertSecretString(field, value);
3332
+ }
3216
3333
  await this.serialize(
3217
3334
  () => this.mutate((map) => {
3218
3335
  const next = { ...map };
@@ -3224,6 +3341,7 @@ var FileSecretStore = class {
3224
3341
  );
3225
3342
  }
3226
3343
  async set(serverId, field, value) {
3344
+ assertSecretString(field, value);
3227
3345
  await this.serialize(
3228
3346
  () => this.mutate((map) => ({
3229
3347
  ...map,
@@ -3239,9 +3357,11 @@ var FileSecretStore = class {
3239
3357
  await this.deleteWhere((key) => key.startsWith(prefix));
3240
3358
  }
3241
3359
  /**
3242
- * Shared delete body. Silent on every failure, matching the keyring store:
3243
- * every reason a delete can fail collapses to "the entry isn't there
3244
- * anymore", and `set` is the operation that reports a broken store.
3360
+ * Shared delete body. Same confirmed-delete contract as the keyring
3361
+ * store: a deletion that cannot be confirmed (unreadable file, failed
3362
+ * decrypt, failed write) must escape, because reporting success would
3363
+ * let a caller commit state that assumes the entry is gone — once the
3364
+ * file becomes readable again, a later read would resurrect it.
3245
3365
  */
3246
3366
  async deleteWhere(match) {
3247
3367
  try {
@@ -3254,7 +3374,12 @@ var FileSecretStore = class {
3254
3374
  return next;
3255
3375
  })
3256
3376
  );
3257
- } catch {
3377
+ } catch (err) {
3378
+ if (err instanceof SecretStoreUnavailableError) throw err;
3379
+ throw new SecretStoreUnavailableError(
3380
+ `Could not delete from the secrets file: ${err instanceof Error ? err.message : String(err)}`,
3381
+ { cause: err }
3382
+ );
3258
3383
  }
3259
3384
  }
3260
3385
  };
@@ -3630,6 +3755,9 @@ var DeferredSecretStore = class {
3630
3755
  async getMany(requests) {
3631
3756
  return secretStoreGetMany(await this.target(), requests);
3632
3757
  }
3758
+ async getManyStrict(requests) {
3759
+ return secretStoreGetManyStrict(await this.target(), requests);
3760
+ }
3633
3761
  async get(serverId, field) {
3634
3762
  return (await this.target()).get(serverId, field);
3635
3763
  }
@@ -3868,12 +3996,31 @@ function redactJsonValue(value) {
3868
3996
  }
3869
3997
  return value;
3870
3998
  }
3999
+ function joinSensitiveContinuations(body) {
4000
+ const out = [];
4001
+ let sensitiveAt = -1;
4002
+ for (const pair of body.split("&")) {
4003
+ const eq = pair.indexOf("=");
4004
+ if (eq === -1) {
4005
+ if (sensitiveAt !== -1 && pair.length > 0) {
4006
+ out[sensitiveAt] += `%26${pair}`;
4007
+ } else {
4008
+ out.push(pair);
4009
+ }
4010
+ continue;
4011
+ }
4012
+ const [key] = new URLSearchParams(pair).keys();
4013
+ sensitiveAt = isSensitiveField(key) && eq < pair.length - 1 ? out.length : -1;
4014
+ out.push(pair);
4015
+ }
4016
+ return out.join("&");
4017
+ }
3871
4018
  function redactBody(body, contentType) {
3872
4019
  if (!body) return body;
3873
4020
  const type = (contentType ?? "").toLowerCase();
3874
4021
  if (type.includes("application/x-www-form-urlencoded")) {
3875
4022
  try {
3876
- const params = new URLSearchParams(body);
4023
+ const params = new URLSearchParams(joinSensitiveContinuations(body));
3877
4024
  let changed = false;
3878
4025
  for (const key of new Set(params.keys())) {
3879
4026
  if (isSensitiveField(key)) {
@@ -4904,42 +5051,57 @@ import {
4904
5051
  function withIssuer(value, issuer) {
4905
5052
  return issuer === void 0 ? value : { ...value, issuer };
4906
5053
  }
4907
- var OAuthStorageBase = class {
5054
+ var OAuthStorageCoordination = class {
4908
5055
  loaded = false;
4909
5056
  loadPromise;
4910
5057
  /** Serializes persist writes so concurrent mutators cannot reorder POSTs. */
4911
5058
  persistQueue = Promise.resolve();
5059
+ };
5060
+ var OAuthStorageBase = class {
5061
+ coordination;
4912
5062
  memory;
4913
5063
  backend;
4914
- constructor(memory, backend) {
5064
+ constructor(memory, backend, coordination = new OAuthStorageCoordination()) {
4915
5065
  this.memory = memory;
4916
5066
  this.backend = backend;
5067
+ this.coordination = coordination;
4917
5068
  }
4918
5069
  load() {
4919
- if (!this.loadPromise) {
4920
- this.loadPromise = this.doLoad();
5070
+ const coordination = this.coordination;
5071
+ if (!coordination.loadPromise) {
5072
+ coordination.loadPromise = this.doLoad().catch((error) => {
5073
+ coordination.loadPromise = void 0;
5074
+ throw error;
5075
+ });
4921
5076
  }
4922
- return this.loadPromise;
5077
+ return coordination.loadPromise;
4923
5078
  }
4924
5079
  async doLoad() {
4925
- if (this.loaded) {
5080
+ if (this.coordination.loaded) {
4926
5081
  return;
4927
5082
  }
4928
5083
  const snapshot = await this.backend.read();
4929
5084
  if (snapshot) {
4930
5085
  this.memory.replace(snapshot);
4931
5086
  }
4932
- this.loaded = true;
5087
+ this.coordination.loaded = true;
4933
5088
  }
4934
5089
  async ensureLoaded() {
4935
5090
  await this.load();
4936
5091
  }
4937
- async persist() {
4938
- const snapshot = this.memory.snapshot();
4939
- const prior = this.persistQueue;
5092
+ /**
5093
+ * Flush memory to the backend, naming the sections the calling mutation
5094
+ * touched so shared-store backends can merge just those entries over a
5095
+ * fresh read (the clobber fix — see {@link OAuthPersistSections}). The
5096
+ * snapshot is taken *inside* the queued closure, so a write that waited in
5097
+ * the queue carries the state as of when it actually runs, coalescing with
5098
+ * any mutations that landed in memory while it waited.
5099
+ */
5100
+ async persist(sections) {
5101
+ const prior = this.coordination.persistQueue;
4940
5102
  const tracked = prior.catch(() => {
4941
- }).then(() => this.backend.write(snapshot));
4942
- this.persistQueue = tracked;
5103
+ }).then(() => this.backend.write(this.memory.snapshot(), sections));
5104
+ this.coordination.persistQueue = tracked;
4943
5105
  await tracked;
4944
5106
  }
4945
5107
  /**
@@ -4953,7 +5115,7 @@ var OAuthStorageBase = class {
4953
5115
  /** Read the issuer-bound slot for a read, if one exists. */
4954
5116
  issuerSlot(state, issuer) {
4955
5117
  const key = this.resolveReadIssuer(state, issuer);
4956
- return key ? state.byIssuer?.[key] : void 0;
5118
+ return key ? getOwnEntry(state.byIssuer, key) : void 0;
4957
5119
  }
4958
5120
  /**
4959
5121
  * Read-modify-write the `byIssuer[issuer]` slot, set `activeIssuer`, and clear
@@ -4964,7 +5126,9 @@ var OAuthStorageBase = class {
4964
5126
  const state = this.memory.getState().getServerState(serverUrl);
4965
5127
  const byIssuer = {
4966
5128
  ...state.byIssuer,
4967
- [issuer]: { ...state.byIssuer?.[issuer], ...updates }
5129
+ // The computed key defines an own property; the merge-base read needs
5130
+ // the own guard so an absent `__proto__` slot reads as undefined.
5131
+ [issuer]: { ...getOwnEntry(state.byIssuer, issuer), ...updates }
4968
5132
  };
4969
5133
  this.memory.getState().setServerState(serverUrl, {
4970
5134
  byIssuer,
@@ -4976,7 +5140,7 @@ var OAuthStorageBase = class {
4976
5140
  mapIssuerSlots(state, fn) {
4977
5141
  const byIssuer = {};
4978
5142
  for (const [key, slot] of Object.entries(state.byIssuer ?? {})) {
4979
- byIssuer[key] = { ...slot, ...fn(slot) };
5143
+ setOwnEntry(byIssuer, key, { ...slot, ...fn(slot) });
4980
5144
  }
4981
5145
  return byIssuer;
4982
5146
  }
@@ -5022,7 +5186,7 @@ var OAuthStorageBase = class {
5022
5186
  clientRegistrationKind: options.registrationKind
5023
5187
  });
5024
5188
  }
5025
- await this.persist();
5189
+ await this.persist({ servers: [serverUrl] });
5026
5190
  }
5027
5191
  async savePreregisteredClientInformation(serverUrl, clientInformation) {
5028
5192
  await this.ensureLoaded();
@@ -5030,7 +5194,7 @@ var OAuthStorageBase = class {
5030
5194
  preregisteredClientInformation: clientInformation,
5031
5195
  clientRegistrationKind: "static"
5032
5196
  });
5033
- await this.persist();
5197
+ await this.persist({ servers: [serverUrl] });
5034
5198
  }
5035
5199
  async clearClientInformation(serverUrl, isPreregistered, issuer) {
5036
5200
  await this.ensureLoaded();
@@ -5038,7 +5202,7 @@ var OAuthStorageBase = class {
5038
5202
  this.memory.getState().setServerState(serverUrl, {
5039
5203
  preregisteredClientInformation: void 0
5040
5204
  });
5041
- await this.persist();
5205
+ await this.persist({ servers: [serverUrl] });
5042
5206
  return;
5043
5207
  }
5044
5208
  if (issuer !== void 0) {
@@ -5061,7 +5225,7 @@ var OAuthStorageBase = class {
5061
5225
  clientRegistrationKind: void 0
5062
5226
  });
5063
5227
  }
5064
- await this.persist();
5228
+ await this.persist({ servers: [serverUrl] });
5065
5229
  }
5066
5230
  async getTokens(serverUrl, issuer) {
5067
5231
  await this.ensureLoaded();
@@ -5072,9 +5236,12 @@ var OAuthStorageBase = class {
5072
5236
  if (!tokens) {
5073
5237
  return void 0;
5074
5238
  }
5075
- const parsed = await OAuthTokensSchema.parseAsync(tokens);
5239
+ const result = await OAuthTokensSchema.safeParseAsync(tokens);
5240
+ if (!result.success) {
5241
+ return void 0;
5242
+ }
5076
5243
  return withIssuer(
5077
- parsed,
5244
+ result.data,
5078
5245
  fromSlot ? this.resolveReadIssuer(state, issuer) : void 0
5079
5246
  );
5080
5247
  }
@@ -5098,7 +5265,7 @@ var OAuthStorageBase = class {
5098
5265
  ...options?.enterpriseManaged === true && { enterpriseManaged: true }
5099
5266
  });
5100
5267
  }
5101
- await this.persist();
5268
+ await this.persist({ servers: [serverUrl] });
5102
5269
  }
5103
5270
  async clearTokens(serverUrl, issuer) {
5104
5271
  await this.ensureLoaded();
@@ -5117,7 +5284,7 @@ var OAuthStorageBase = class {
5117
5284
  }));
5118
5285
  this.memory.getState().setServerState(serverUrl, { byIssuer, tokens: void 0 });
5119
5286
  }
5120
- await this.persist();
5287
+ await this.persist({ servers: [serverUrl] });
5121
5288
  }
5122
5289
  async getCodeVerifier(serverUrl) {
5123
5290
  await this.ensureLoaded();
@@ -5127,12 +5294,12 @@ var OAuthStorageBase = class {
5127
5294
  async saveCodeVerifier(serverUrl, codeVerifier) {
5128
5295
  await this.ensureLoaded();
5129
5296
  this.memory.getState().setServerState(serverUrl, { codeVerifier });
5130
- await this.persist();
5297
+ await this.persist({ servers: [serverUrl] });
5131
5298
  }
5132
5299
  async clearCodeVerifier(serverUrl) {
5133
5300
  await this.ensureLoaded();
5134
5301
  this.memory.getState().setServerState(serverUrl, { codeVerifier: void 0 });
5135
- await this.persist();
5302
+ await this.persist({ servers: [serverUrl] });
5136
5303
  }
5137
5304
  async getScope(serverUrl) {
5138
5305
  await this.ensureLoaded();
@@ -5142,12 +5309,12 @@ var OAuthStorageBase = class {
5142
5309
  async saveScope(serverUrl, scope) {
5143
5310
  await this.ensureLoaded();
5144
5311
  this.memory.getState().setServerState(serverUrl, { scope });
5145
- await this.persist();
5312
+ await this.persist({ servers: [serverUrl] });
5146
5313
  }
5147
5314
  async clearScope(serverUrl) {
5148
5315
  await this.ensureLoaded();
5149
5316
  this.memory.getState().setServerState(serverUrl, { scope: void 0 });
5150
- await this.persist();
5317
+ await this.persist({ servers: [serverUrl] });
5151
5318
  }
5152
5319
  async getServerMetadata(serverUrl) {
5153
5320
  await this.ensureLoaded();
@@ -5157,12 +5324,12 @@ var OAuthStorageBase = class {
5157
5324
  async saveServerMetadata(serverUrl, metadata) {
5158
5325
  await this.ensureLoaded();
5159
5326
  this.memory.getState().setServerState(serverUrl, { serverMetadata: metadata });
5160
- await this.persist();
5327
+ await this.persist({ servers: [serverUrl] });
5161
5328
  }
5162
5329
  async clearServerMetadata(serverUrl) {
5163
5330
  await this.ensureLoaded();
5164
5331
  this.memory.getState().setServerState(serverUrl, { serverMetadata: void 0 });
5165
- await this.persist();
5332
+ await this.persist({ servers: [serverUrl] });
5166
5333
  }
5167
5334
  async getDiscoveryState(serverUrl) {
5168
5335
  await this.ensureLoaded();
@@ -5171,12 +5338,12 @@ var OAuthStorageBase = class {
5171
5338
  async saveDiscoveryState(serverUrl, state) {
5172
5339
  await this.ensureLoaded();
5173
5340
  this.memory.getState().setServerState(serverUrl, { discoveryState: state });
5174
- await this.persist();
5341
+ await this.persist({ servers: [serverUrl] });
5175
5342
  }
5176
5343
  async clearDiscoveryState(serverUrl) {
5177
5344
  await this.ensureLoaded();
5178
5345
  this.memory.getState().setServerState(serverUrl, { discoveryState: void 0 });
5179
- await this.persist();
5346
+ await this.persist({ servers: [serverUrl] });
5180
5347
  }
5181
5348
  async takeRevocationSnapshot(serverUrl) {
5182
5349
  await this.ensureLoaded();
@@ -5194,13 +5361,13 @@ var OAuthStorageBase = class {
5194
5361
  serverMetadata: state.serverMetadata
5195
5362
  };
5196
5363
  this.memory.getState().clearServerState(serverUrl);
5197
- await this.persist();
5364
+ await this.persist({ servers: [serverUrl] });
5198
5365
  return snapshot;
5199
5366
  }
5200
5367
  async clear(serverUrl) {
5201
5368
  await this.ensureLoaded();
5202
5369
  this.memory.getState().clearServerState(serverUrl);
5203
- await this.persist();
5370
+ await this.persist({ servers: [serverUrl] });
5204
5371
  }
5205
5372
  async getIdpSession(issuer) {
5206
5373
  await this.ensureLoaded();
@@ -5213,17 +5380,18 @@ var OAuthStorageBase = class {
5213
5380
  async saveIdpSession(issuer, session) {
5214
5381
  await this.ensureLoaded();
5215
5382
  this.memory.getState().setIdpSession(issuer, session);
5216
- await this.persist();
5383
+ await this.persist({ idpSessions: [issuer] });
5217
5384
  }
5218
5385
  async clearIdpSession(issuer) {
5219
5386
  await this.ensureLoaded();
5220
5387
  this.memory.getState().clearIdpSession(issuer);
5221
- await this.persist();
5388
+ await this.persist({ idpSessions: [issuer] });
5222
5389
  }
5223
5390
  async clearEnterpriseManagedResourceServers() {
5224
5391
  await this.ensureLoaded();
5392
+ const servers = Object.entries(this.memory.getState().servers).filter(([, state]) => state.enterpriseManaged === true).map(([url]) => url);
5225
5393
  this.memory.getState().clearEnterpriseManagedResourceServers();
5226
- await this.persist();
5394
+ await this.persist({ servers });
5227
5395
  }
5228
5396
  };
5229
5397
 
@@ -5241,13 +5409,15 @@ var OAuthMemoryStore = class {
5241
5409
  servers: this.servers,
5242
5410
  idpSessions: this.idpSessions,
5243
5411
  getServerState: (serverUrl) => {
5244
- return this.servers[serverUrl] || {};
5412
+ return getOwnEntry(this.servers, serverUrl) || {};
5245
5413
  },
5246
5414
  setServerState: (serverUrl, updates) => {
5247
5415
  this.servers = {
5248
5416
  ...this.servers,
5417
+ // Computed keys define own properties, so this write is safe for
5418
+ // `__proto__`; only the merge-base read needs the own guard.
5249
5419
  [serverUrl]: {
5250
- ...this.servers[serverUrl],
5420
+ ...getOwnEntry(this.servers, serverUrl),
5251
5421
  ...updates
5252
5422
  }
5253
5423
  };
@@ -5258,13 +5428,13 @@ var OAuthMemoryStore = class {
5258
5428
  this.servers = rest;
5259
5429
  },
5260
5430
  getIdpSession: (issuer) => {
5261
- return this.idpSessions[issuer] || {};
5431
+ return getOwnEntry(this.idpSessions, issuer) || {};
5262
5432
  },
5263
5433
  setIdpSession: (issuer, updates) => {
5264
5434
  this.idpSessions = {
5265
5435
  ...this.idpSessions,
5266
5436
  [issuer]: {
5267
- ...this.idpSessions[issuer],
5437
+ ...getOwnEntry(this.idpSessions, issuer),
5268
5438
  ...updates
5269
5439
  }
5270
5440
  };
@@ -5298,13 +5468,70 @@ var OAuthMemoryStore = class {
5298
5468
  };
5299
5469
 
5300
5470
  // ../../core/auth/oauth-persist.ts
5471
+ import { OAuthTokensSchema as OAuthTokensSchema2 } from "@modelcontextprotocol/core";
5472
+ function mergeOAuthSections(disk, snapshot, sections) {
5473
+ const merged = {
5474
+ servers: { ...disk?.servers },
5475
+ idpSessions: { ...disk?.idpSessions }
5476
+ };
5477
+ for (const url of sections.servers ?? []) {
5478
+ const value = getOwnEntry(snapshot.servers, url);
5479
+ if (value === void 0) {
5480
+ delete merged.servers[url];
5481
+ } else {
5482
+ setOwnEntry(merged.servers, url, value);
5483
+ }
5484
+ }
5485
+ for (const issuer of sections.idpSessions ?? []) {
5486
+ const value = getOwnEntry(snapshot.idpSessions, issuer);
5487
+ if (value === void 0) {
5488
+ delete merged.idpSessions[issuer];
5489
+ } else {
5490
+ setOwnEntry(merged.idpSessions, issuer, value);
5491
+ }
5492
+ }
5493
+ return merged;
5494
+ }
5301
5495
  function isRecord3(value) {
5302
5496
  return typeof value === "object" && value !== null && !Array.isArray(value);
5303
5497
  }
5498
+ function isEntryMap(value) {
5499
+ return isRecord3(value) && Object.values(value).every(isRecord3);
5500
+ }
5501
+ var VERBATIM_SECRET_KEYS = [
5502
+ "client_secret",
5503
+ "registration_access_token"
5504
+ ];
5505
+ function isValidClientInformation(value) {
5506
+ if (value === void 0) return true;
5507
+ if (!isRecord3(value)) return false;
5508
+ return VERBATIM_SECRET_KEYS.every(
5509
+ (key) => !Object.hasOwn(value, key) || typeof value[key] === "string"
5510
+ );
5511
+ }
5512
+ var PartialTokensSchema = OAuthTokensSchema2.partial();
5513
+ function isValidServerEntry(entry) {
5514
+ if (!isRecord3(entry)) return false;
5515
+ if (!isValidClientInformation(entry.clientInformation)) return false;
5516
+ if (!isValidClientInformation(entry.preregisteredClientInformation))
5517
+ return false;
5518
+ if (entry.byIssuer !== void 0) {
5519
+ if (!isRecord3(entry.byIssuer)) return false;
5520
+ for (const slot of Object.values(entry.byIssuer)) {
5521
+ if (!isRecord3(slot)) return false;
5522
+ if (!isValidClientInformation(slot.clientInformation)) return false;
5523
+ }
5524
+ }
5525
+ return true;
5526
+ }
5304
5527
  function snapshotFromPayload(payload) {
5528
+ const servers = payload.servers ?? {};
5529
+ const idpSessions = payload.idpSessions ?? {};
5530
+ if (!isEntryMap(servers) || !isEntryMap(idpSessions)) return null;
5531
+ if (!Object.values(servers).every(isValidServerEntry)) return null;
5305
5532
  return {
5306
- servers: payload.servers ?? {},
5307
- idpSessions: payload.idpSessions ?? {}
5533
+ servers,
5534
+ idpSessions
5308
5535
  };
5309
5536
  }
5310
5537
  function parseOAuthPersistBlob(raw) {
@@ -5327,123 +5554,884 @@ function serializeOAuthPersistBlob(snapshot) {
5327
5554
  return serializeStore(snapshot);
5328
5555
  }
5329
5556
 
5330
- // ../../core/auth/node/oauth-persist-file.ts
5331
- function createFileOAuthPersistBackend(options) {
5557
+ // ../../core/auth/node/oauth-secrets.ts
5558
+ import { OAuthTokensSchema as OAuthTokensSchema3 } from "@modelcontextprotocol/core";
5559
+ var PERSIST_TOKENS_ENV = "MCP_INSPECTOR_PERSIST_TOKENS";
5560
+ var warnedPolicyValues = /* @__PURE__ */ new Set();
5561
+ function getPersistTokensPolicy(env = process.env) {
5562
+ const raw = env[PERSIST_TOKENS_ENV];
5563
+ if (raw === void 0 || raw === "") return "all";
5564
+ if (raw === "all" || raw === "access" || raw === "none") return raw;
5565
+ if (!warnedPolicyValues.has(raw)) {
5566
+ warnedPolicyValues.add(raw);
5567
+ console.warn(
5568
+ `[mcp-inspector] Ignoring invalid ${PERSIST_TOKENS_ENV}=${JSON.stringify(raw)} (expected "all", "access", or "none"); persisting all tokens.`
5569
+ );
5570
+ }
5571
+ return "all";
5572
+ }
5573
+ var oauthSecretServerId = (serverUrl) => `oauth+${encodeURIComponent(serverUrl)}`;
5574
+ var oauthIdpSecretServerId = (issuer) => `oauth-idp+${encodeURIComponent(issuer)}`;
5575
+ var issuerTokensField = (issuer) => `tokens:${issuer}`;
5576
+ var issuerClientSecretField = (issuer) => `client-secret:${issuer}`;
5577
+ var issuerRegistrationTokenField = (issuer) => `registration-token:${issuer}`;
5578
+ var LEGACY_TOKENS_FIELD = "tokens";
5579
+ var LEGACY_CLIENT_SECRET_FIELD = "client-secret";
5580
+ var LEGACY_REGISTRATION_TOKEN_FIELD = "registration-token";
5581
+ var PREREG_CLIENT_SECRET_FIELD = "prereg-client-secret";
5582
+ var PREREG_REGISTRATION_TOKEN_FIELD = "prereg-registration-token";
5583
+ var IDP_SESSION_FIELD = "idp-session";
5584
+ function applyTokensPolicy(tokens, policy) {
5585
+ if (policy === "none") return void 0;
5586
+ if (policy === "access" && tokens.refresh_token !== void 0) {
5587
+ const { refresh_token: _stripped, ...rest } = tokens;
5588
+ return rest;
5589
+ }
5590
+ return tokens;
5591
+ }
5592
+ var StorableTokensSchema = OAuthTokensSchema3.partial();
5593
+ function hasSecretBearingTokenField(tokens) {
5594
+ return tokens.access_token !== void 0 || tokens.refresh_token !== void 0 || tokens.id_token !== void 0;
5595
+ }
5596
+ function splitTokens(tokens, policy) {
5597
+ const kept = applyTokensPolicy(tokens, policy);
5598
+ if (!kept) return {};
5599
+ if (!StorableTokensSchema.safeParse(kept).success) {
5600
+ return { plaintext: kept };
5601
+ }
5602
+ if (!hasSecretBearingTokenField(kept)) {
5603
+ return {};
5604
+ }
5605
+ return { secret: JSON.stringify(kept) };
5606
+ }
5607
+ function splitClientInformation(info, fields, secrets) {
5608
+ const { client_secret, registration_access_token, ...publicInfo } = info;
5609
+ if (client_secret === void 0 && registration_access_token === void 0) {
5610
+ return info;
5611
+ }
5612
+ if (client_secret !== void 0) {
5613
+ secrets[fields.clientSecret] = client_secret;
5614
+ }
5615
+ if (registration_access_token !== void 0) {
5616
+ secrets[fields.registrationToken] = registration_access_token;
5617
+ }
5618
+ return publicInfo;
5619
+ }
5620
+ function joinClientInformation(info, fields, secrets) {
5621
+ const clientSecret = secrets[fields.clientSecret];
5622
+ const registrationToken = secrets[fields.registrationToken];
5623
+ if (clientSecret === void 0 && registrationToken === void 0) {
5624
+ return info;
5625
+ }
5332
5626
  return {
5333
- async read() {
5334
- const raw = await readStoreFile(options.filePath);
5335
- return parseOAuthPersistBlob(raw);
5336
- },
5337
- async write(snapshot) {
5338
- await writeStoreFile(
5339
- options.filePath,
5340
- serializeOAuthPersistBlob(snapshot)
5341
- );
5342
- },
5343
- async remove() {
5344
- await deleteStoreFile(options.filePath);
5627
+ ...info,
5628
+ ...clientSecret !== void 0 && { client_secret: clientSecret },
5629
+ ...registrationToken !== void 0 && {
5630
+ registration_access_token: registrationToken
5345
5631
  }
5346
5632
  };
5347
5633
  }
5348
-
5349
- // ../../core/auth/node/storage-node.ts
5350
- var DEFAULT_STATE_PATH = getStoreFilePath(getDefaultStorageDir(), "oauth");
5351
- function getStateFilePath(customPath) {
5352
- if (customPath) return customPath;
5353
- if (process.env.MCP_INSPECTOR_OAUTH_STATE_PATH) {
5354
- return process.env.MCP_INSPECTOR_OAUTH_STATE_PATH;
5634
+ var issuerClientInfoFields = (issuer) => ({
5635
+ clientSecret: issuerClientSecretField(issuer),
5636
+ registrationToken: issuerRegistrationTokenField(issuer)
5637
+ });
5638
+ var LEGACY_CLIENT_INFO_FIELDS = {
5639
+ clientSecret: LEGACY_CLIENT_SECRET_FIELD,
5640
+ registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD
5641
+ };
5642
+ var PREREG_CLIENT_INFO_FIELDS = {
5643
+ clientSecret: PREREG_CLIENT_SECRET_FIELD,
5644
+ registrationToken: PREREG_REGISTRATION_TOKEN_FIELD
5645
+ };
5646
+ function splitServerOAuthState(state, policy) {
5647
+ const secrets = {};
5648
+ const residue = { ...state };
5649
+ if (state.byIssuer) {
5650
+ const byIssuer = {};
5651
+ for (const [issuer, slot] of Object.entries(state.byIssuer)) {
5652
+ const slotResidue = { ...slot };
5653
+ if (slot.tokens) {
5654
+ delete slotResidue.tokens;
5655
+ const tokenSplit = splitTokens(slot.tokens, policy);
5656
+ if (tokenSplit.secret !== void 0) {
5657
+ secrets[issuerTokensField(issuer)] = tokenSplit.secret;
5658
+ }
5659
+ if (tokenSplit.plaintext) slotResidue.tokens = tokenSplit.plaintext;
5660
+ }
5661
+ if (slot.clientInformation) {
5662
+ slotResidue.clientInformation = splitClientInformation(
5663
+ slot.clientInformation,
5664
+ issuerClientInfoFields(issuer),
5665
+ secrets
5666
+ );
5667
+ }
5668
+ setOwnEntry(byIssuer, issuer, slotResidue);
5669
+ }
5670
+ residue.byIssuer = byIssuer;
5355
5671
  }
5356
- if (process.env.MCP_STORAGE_DIR) {
5357
- return getStoreFilePath(process.env.MCP_STORAGE_DIR, "oauth");
5672
+ if (state.tokens) {
5673
+ delete residue.tokens;
5674
+ const tokenSplit = splitTokens(state.tokens, policy);
5675
+ if (tokenSplit.secret !== void 0) {
5676
+ secrets[LEGACY_TOKENS_FIELD] = tokenSplit.secret;
5677
+ }
5678
+ if (tokenSplit.plaintext) residue.tokens = tokenSplit.plaintext;
5358
5679
  }
5359
- return DEFAULT_STATE_PATH;
5680
+ if (state.clientInformation) {
5681
+ residue.clientInformation = splitClientInformation(
5682
+ state.clientInformation,
5683
+ LEGACY_CLIENT_INFO_FIELDS,
5684
+ secrets
5685
+ );
5686
+ }
5687
+ if (state.preregisteredClientInformation) {
5688
+ residue.preregisteredClientInformation = splitClientInformation(
5689
+ state.preregisteredClientInformation,
5690
+ PREREG_CLIENT_INFO_FIELDS,
5691
+ secrets
5692
+ );
5693
+ }
5694
+ return { residue, secrets };
5360
5695
  }
5361
- var memoryCache = /* @__PURE__ */ new Map();
5362
- var storageCache = /* @__PURE__ */ new Map();
5363
- function getSharedMemory(stateFilePath) {
5364
- const key = getStateFilePath(stateFilePath);
5365
- let memory = memoryCache.get(key);
5366
- if (!memory) {
5367
- memory = new OAuthMemoryStore();
5368
- memoryCache.set(key, memory);
5696
+ function parseStoredTokens(raw) {
5697
+ try {
5698
+ const parsed = JSON.parse(raw);
5699
+ if (StorableTokensSchema.safeParse(parsed).success && hasSecretBearingTokenField(parsed)) {
5700
+ return parsed;
5701
+ }
5702
+ } catch {
5369
5703
  }
5370
- return memory;
5704
+ return void 0;
5371
5705
  }
5372
- function resetNodeOAuthStorageCache(stateFilePath) {
5373
- const key = getStateFilePath(stateFilePath);
5374
- memoryCache.delete(key);
5375
- storageCache.delete(key);
5706
+ function parseStoredIdpSession(raw) {
5707
+ try {
5708
+ const parsed = JSON.parse(raw);
5709
+ if (typeof parsed === "object" && parsed !== null) {
5710
+ const { idToken, refreshToken } = parsed;
5711
+ const usable = {
5712
+ ...typeof idToken === "string" && { idToken },
5713
+ ...typeof refreshToken === "string" && { refreshToken }
5714
+ };
5715
+ if (Object.keys(usable).length > 0) return usable;
5716
+ }
5717
+ } catch {
5718
+ }
5719
+ return void 0;
5376
5720
  }
5377
- var NodeOAuthStorage = class extends OAuthStorageBase {
5378
- /**
5379
- * @param storagePath - Optional path to state file. Default: ~/.mcp-inspector/storage/oauth.json
5380
- */
5381
- constructor(storagePath) {
5382
- const filePath = getStateFilePath(storagePath);
5383
- super(
5384
- getSharedMemory(storagePath),
5385
- createFileOAuthPersistBackend({ filePath })
5721
+ function isUsableStoredSecret(field, raw) {
5722
+ if (field === LEGACY_TOKENS_FIELD || field.startsWith("tokens:")) {
5723
+ return parseStoredTokens(raw) !== void 0;
5724
+ }
5725
+ if (field === IDP_SESSION_FIELD) {
5726
+ return parseStoredIdpSession(raw) !== void 0;
5727
+ }
5728
+ return true;
5729
+ }
5730
+ function joinServerOAuthState(residue, secrets) {
5731
+ const joined = { ...residue };
5732
+ if (residue.byIssuer) {
5733
+ const byIssuer = {};
5734
+ for (const [issuer, slot] of Object.entries(residue.byIssuer)) {
5735
+ const joinedSlot = { ...slot };
5736
+ const tokensRaw = secrets[issuerTokensField(issuer)];
5737
+ if (tokensRaw !== void 0) {
5738
+ const tokens = parseStoredTokens(tokensRaw);
5739
+ if (tokens) joinedSlot.tokens = tokens;
5740
+ }
5741
+ if (joinedSlot.clientInformation) {
5742
+ joinedSlot.clientInformation = joinClientInformation(
5743
+ joinedSlot.clientInformation,
5744
+ issuerClientInfoFields(issuer),
5745
+ secrets
5746
+ );
5747
+ }
5748
+ setOwnEntry(byIssuer, issuer, joinedSlot);
5749
+ }
5750
+ joined.byIssuer = byIssuer;
5751
+ }
5752
+ const legacyTokensRaw = secrets[LEGACY_TOKENS_FIELD];
5753
+ if (legacyTokensRaw !== void 0) {
5754
+ const tokens = parseStoredTokens(legacyTokensRaw);
5755
+ if (tokens) joined.tokens = tokens;
5756
+ }
5757
+ if (joined.clientInformation) {
5758
+ joined.clientInformation = joinClientInformation(
5759
+ joined.clientInformation,
5760
+ LEGACY_CLIENT_INFO_FIELDS,
5761
+ secrets
5386
5762
  );
5387
5763
  }
5388
- };
5389
-
5390
- // ../../core/auth/revocation.ts
5391
- import { selectClientAuthMethod } from "@modelcontextprotocol/client";
5392
- import {
5393
- OAuthClientInformationSchema as OAuthClientInformationSchema2,
5394
- OAuthTokensSchema as OAuthTokensSchema2
5395
- } from "@modelcontextprotocol/core";
5396
- var DEFAULT_REVOCATION_TIMEOUT_MS = 5e3;
5397
- var MIN_REVOCATION_REQUEST_BUDGET_MS = 5;
5398
- function selectRevocableToken(tokens) {
5399
- if (!tokens) return null;
5400
- if (tokens.refresh_token) {
5401
- return { token: tokens.refresh_token, tokenTypeHint: "refresh_token" };
5764
+ if (joined.preregisteredClientInformation) {
5765
+ joined.preregisteredClientInformation = joinClientInformation(
5766
+ joined.preregisteredClientInformation,
5767
+ PREREG_CLIENT_INFO_FIELDS,
5768
+ secrets
5769
+ );
5402
5770
  }
5403
- if (tokens.access_token) {
5404
- return { token: tokens.access_token, tokenTypeHint: "access_token" };
5771
+ return joined;
5772
+ }
5773
+ function splitIdpSession(session, policy) {
5774
+ const { idToken, refreshToken, ...residue } = session;
5775
+ const secrets = {};
5776
+ if (policy !== "none" && (idToken !== void 0 || refreshToken !== void 0)) {
5777
+ const kept = {
5778
+ ...typeof idToken === "string" && { idToken },
5779
+ ...policy === "all" && typeof refreshToken === "string" && { refreshToken }
5780
+ };
5781
+ if (Object.keys(kept).length > 0) {
5782
+ secrets[IDP_SESSION_FIELD] = JSON.stringify(kept);
5783
+ }
5405
5784
  }
5406
- return null;
5785
+ return { residue, secrets };
5407
5786
  }
5408
- function revocationAuthMethods(metadata) {
5409
- return metadata.revocation_endpoint_auth_methods_supported ?? [
5410
- "client_secret_basic"
5411
- ];
5787
+ function joinIdpSession(residue, secrets) {
5788
+ const raw = secrets[IDP_SESSION_FIELD];
5789
+ if (raw === void 0) return { ...residue };
5790
+ const stored = parseStoredIdpSession(raw);
5791
+ return stored ? { ...residue, ...stored } : { ...residue };
5412
5792
  }
5413
- function buildRevocationRequest(params) {
5414
- const body = new URLSearchParams({
5415
- token: params.token,
5416
- token_type_hint: params.tokenTypeHint
5417
- });
5418
- const headers = {
5419
- "Content-Type": "application/x-www-form-urlencoded",
5420
- Accept: "application/json"
5421
- };
5422
- const client = params.clientInformation;
5423
- if (client) {
5424
- const method = selectClientAuthMethod(client, params.supportedAuthMethods);
5425
- if (method === "client_secret_basic" && client.client_secret) {
5426
- const credentials = `${encodeURIComponent(client.client_id)}:${encodeURIComponent(client.client_secret)}`;
5427
- headers.Authorization = `Basic ${base64Encode(credentials)}`;
5428
- } else if (method === "client_secret_post" && client.client_secret) {
5429
- body.set("client_id", client.client_id);
5430
- body.set("client_secret", client.client_secret);
5431
- } else {
5432
- body.set("client_id", client.client_id);
5433
- }
5793
+ function serverSecretFields(state) {
5794
+ const fields = /* @__PURE__ */ new Set([
5795
+ LEGACY_TOKENS_FIELD,
5796
+ LEGACY_CLIENT_SECRET_FIELD,
5797
+ LEGACY_REGISTRATION_TOKEN_FIELD,
5798
+ PREREG_CLIENT_SECRET_FIELD,
5799
+ PREREG_REGISTRATION_TOKEN_FIELD
5800
+ ]);
5801
+ for (const issuer of Object.keys(state?.byIssuer ?? {})) {
5802
+ fields.add(issuerTokensField(issuer));
5803
+ fields.add(issuerClientSecretField(issuer));
5804
+ fields.add(issuerRegistrationTokenField(issuer));
5434
5805
  }
5435
- return {
5436
- url: params.endpoint,
5437
- init: { method: "POST", headers, body: body.toString() }
5438
- };
5806
+ return [...fields];
5439
5807
  }
5440
- function base64Encode(value) {
5441
- if (typeof btoa === "function") return btoa(value);
5442
- return Buffer.from(value, "utf8").toString("base64");
5808
+ function clientInfoHasPlaintext(info) {
5809
+ return info !== void 0 && (info.client_secret !== void 0 || info.registration_access_token !== void 0);
5443
5810
  }
5444
- async function revokeToken(params) {
5445
- const timeoutMs = Math.max(
5446
- 0,
5811
+ function snapshotHasPlaintextSecrets(snapshot) {
5812
+ for (const state of Object.values(snapshot.servers)) {
5813
+ if (state.tokens) return true;
5814
+ if (clientInfoHasPlaintext(state.clientInformation)) return true;
5815
+ if (clientInfoHasPlaintext(state.preregisteredClientInformation)) {
5816
+ return true;
5817
+ }
5818
+ for (const slot of Object.values(state.byIssuer ?? {})) {
5819
+ if (slot.tokens) return true;
5820
+ if (clientInfoHasPlaintext(slot.clientInformation)) return true;
5821
+ }
5822
+ }
5823
+ for (const session of Object.values(snapshot.idpSessions)) {
5824
+ if (session.idToken !== void 0 || session.refreshToken !== void 0) {
5825
+ return true;
5826
+ }
5827
+ }
5828
+ return false;
5829
+ }
5830
+
5831
+ // ../../core/auth/node/oauth-persist-file.ts
5832
+ var warnedStoreFailures = /* @__PURE__ */ new Set();
5833
+ function warnStoreWriteFailure(error) {
5834
+ const reason = error instanceof Error ? error.message : String(error);
5835
+ if (warnedStoreFailures.has(reason)) return;
5836
+ warnedStoreFailures.add(reason);
5837
+ console.warn(
5838
+ `[mcp-inspector] Could not write OAuth tokens to the secret store (${reason}). Tokens will be kept in memory for this session only and will NOT be persisted; expect to re-authorize after a restart.`
5839
+ );
5840
+ }
5841
+ function warnRestoreFailure(error) {
5842
+ const reason = error instanceof Error ? error.message : String(error);
5843
+ const key = `restore:${reason}`;
5844
+ if (warnedStoreFailures.has(key)) return;
5845
+ warnedStoreFailures.add(key);
5846
+ console.warn(
5847
+ `[mcp-inspector] Could not restore secret-store entries after a failed OAuth state write (${reason}). The secret store may be inconsistent with oauth.json; if a server's stored credentials stop working, re-authorize it.`
5848
+ );
5849
+ }
5850
+ function warnUnconfirmedFile(error) {
5851
+ const reason = error instanceof Error ? error.message : String(error);
5852
+ const key = `unconfirmed:${reason}`;
5853
+ if (warnedStoreFailures.has(key)) return;
5854
+ warnedStoreFailures.add(key);
5855
+ console.warn(
5856
+ `[mcp-inspector] Could not re-read oauth.json after a failed OAuth state save (${reason}). The file may still contain the interrupted save while the secret store was restored to the previous values; if a server's stored credentials stop working, re-authorize it.`
5857
+ );
5858
+ }
5859
+ function warnMigrationFailure(error) {
5860
+ const reason = error instanceof Error ? error.message : String(error);
5861
+ const key = `migration:${reason}`;
5862
+ if (warnedStoreFailures.has(key)) return;
5863
+ warnedStoreFailures.add(key);
5864
+ console.warn(
5865
+ `[mcp-inspector] Could not migrate plaintext OAuth secrets into the secret store (${reason}). The plaintext copy in oauth.json was kept and keeps working; migration will be retried on the next read.`
5866
+ );
5867
+ }
5868
+ function rethrowLockError(filePath, error, action = "save") {
5869
+ if (error instanceof SecretFileLockHeldError) {
5870
+ throw new SecretFileLockHeldError(
5871
+ `Could not ${action} OAuth state: the state file at ${filePath} is locked by another Inspector process and did not become available.`,
5872
+ { cause: error }
5873
+ );
5874
+ }
5875
+ throw error;
5876
+ }
5877
+ async function withOAuthStateLock(filePath, action, body) {
5878
+ let entered = false;
5879
+ try {
5880
+ return await withSecretFileLock(filePath, async () => {
5881
+ entered = true;
5882
+ return body();
5883
+ });
5884
+ } catch (error) {
5885
+ if (!entered) rethrowLockError(filePath, error, action);
5886
+ throw error;
5887
+ }
5888
+ }
5889
+ var OAuthStateFileUnrecognizedError = class extends Error {
5890
+ constructor(filePath, action) {
5891
+ super(
5892
+ `Refusing to ${action} OAuth state: ${filePath} exists but is not a recognized OAuth state file (it may be corrupt, truncated, or written by something else). Its entries are the only index of credentials in the OS keychain / secret store, so overwriting it would strand them. Restore the file from a backup or fix its JSON; deleting it starts fresh but abandons any credentials it indexed.`
5893
+ );
5894
+ this.name = "OAuthStateFileUnrecognizedError";
5895
+ }
5896
+ };
5897
+ async function readDiskForMutation(filePath, action) {
5898
+ const raw = await readStoreFile(filePath);
5899
+ const parsed = parseOAuthPersistBlob(raw);
5900
+ if (raw !== null && parsed === null) {
5901
+ throw new OAuthStateFileUnrecognizedError(filePath, action);
5902
+ }
5903
+ return parsed;
5904
+ }
5905
+ async function persistEntrySecrets(store, serverId, candidates, secrets, prior, allowDegrade) {
5906
+ const priorValue = new Map(prior.map(({ field, value }) => [field, value]));
5907
+ const changed = {};
5908
+ for (const [field, value] of Object.entries(secrets)) {
5909
+ if (priorValue.get(field) !== value) changed[field] = value;
5910
+ }
5911
+ try {
5912
+ if (Object.keys(changed).length > 0) {
5913
+ await secretStoreSetMany(store, serverId, changed);
5914
+ }
5915
+ } catch (error) {
5916
+ if (!allowDegrade) throw error;
5917
+ warnStoreWriteFailure(error);
5918
+ const touched = prior.filter(({ field }) => changed[field] !== void 0);
5919
+ let restoreFailure;
5920
+ await restoreSecretFields(store, touched, (err) => {
5921
+ restoreFailure = err;
5922
+ });
5923
+ if (restoreFailure !== void 0) throw restoreFailure;
5924
+ return false;
5925
+ }
5926
+ await settleStoreMutations(
5927
+ candidates.filter(
5928
+ (field) => secrets[field] === void 0 && priorValue.get(field) !== null
5929
+ ).map((field) => store.delete(serverId, field))
5930
+ );
5931
+ return true;
5932
+ }
5933
+ function revertEntryToDisk(merged, disk, key) {
5934
+ const diskEntry = getOwnEntry(disk, key);
5935
+ if (diskEntry === void 0) {
5936
+ if (Object.hasOwn(merged, key)) delete merged[key];
5937
+ } else {
5938
+ setOwnEntry(merged, key, diskEntry);
5939
+ }
5940
+ }
5941
+ function preserveNonDurableSecrets(diskSecrets, secrets) {
5942
+ const keep = {};
5943
+ for (const [field, value] of Object.entries(secrets)) {
5944
+ if (diskSecrets[field] === value) keep[field] = value;
5945
+ }
5946
+ return keep;
5947
+ }
5948
+ async function writeOAuthSections(filePath, snapshot, sections, secretStore = defaultSecretStore()) {
5949
+ const policy = getPersistTokensPolicy();
5950
+ const durable = await secretStoreIsDurable(secretStore);
5951
+ await withOAuthStateLock(filePath, "save", async () => {
5952
+ const restoreBaseline = /* @__PURE__ */ new Map();
5953
+ const ourWrites = /* @__PURE__ */ new Map();
5954
+ const priorKey = (p) => `${p.serverId}\0${p.field}`;
5955
+ const foldPriors = (priors) => {
5956
+ for (const prior of priors) {
5957
+ const key = priorKey(prior);
5958
+ if (restoreBaseline.has(key) && ourWrites.get(key) !== void 0 && prior.value === ourWrites.get(key)) {
5959
+ continue;
5960
+ }
5961
+ restoreBaseline.set(key, prior);
5962
+ }
5963
+ };
5964
+ const restoreToBaseline = () => restoreSecretFields(
5965
+ secretStore,
5966
+ [...restoreBaseline.values()],
5967
+ warnRestoreFailure
5968
+ );
5969
+ let unconfirmed = null;
5970
+ const reconcileFailure = async () => {
5971
+ if (unconfirmed !== null) {
5972
+ const committed = unconfirmed;
5973
+ let observed = null;
5974
+ let readable = true;
5975
+ try {
5976
+ observed = await readStoreFile(filePath);
5977
+ } catch (error) {
5978
+ readable = false;
5979
+ warnUnconfirmedFile(error);
5980
+ }
5981
+ if (readable && observed === committed.written) {
5982
+ const target = new Map(
5983
+ [...restoreBaseline].map(([key, { serverId, field, value }]) => [
5984
+ key,
5985
+ { serverId, field, value }
5986
+ ])
5987
+ );
5988
+ for (const [key, write] of committed.writes) target.set(key, write);
5989
+ try {
5990
+ await settleStoreMutations(
5991
+ [...target.values()].map(
5992
+ ({ serverId, field, value }) => value === null ? secretStore.delete(serverId, field) : secretStore.set(serverId, field, value)
5993
+ )
5994
+ );
5995
+ return true;
5996
+ } catch (error) {
5997
+ warnRestoreFailure(error);
5998
+ return false;
5999
+ }
6000
+ }
6001
+ }
6002
+ await restoreToBaseline();
6003
+ return false;
6004
+ };
6005
+ for (let attempt = 0; attempt < MAX_WRITE_ATTEMPTS; attempt++) {
6006
+ const priorSecrets = [];
6007
+ const attemptWrites = /* @__PURE__ */ new Map();
6008
+ let written;
6009
+ try {
6010
+ const disk = await readDiskForMutation(filePath, "save");
6011
+ const effective = sections ? {
6012
+ servers: sections.servers && [...new Set(sections.servers)],
6013
+ idpSessions: sections.idpSessions && [
6014
+ ...new Set(sections.idpSessions)
6015
+ ]
6016
+ } : {
6017
+ servers: [
6018
+ .../* @__PURE__ */ new Set([
6019
+ ...Object.keys(disk?.servers ?? {}),
6020
+ ...Object.keys(snapshot.servers)
6021
+ ])
6022
+ ],
6023
+ idpSessions: [
6024
+ .../* @__PURE__ */ new Set([
6025
+ ...Object.keys(disk?.idpSessions ?? {}),
6026
+ ...Object.keys(snapshot.idpSessions)
6027
+ ])
6028
+ ]
6029
+ };
6030
+ const merged = mergeOAuthSections(disk, snapshot, effective);
6031
+ for (const url of effective.servers ?? []) {
6032
+ const serverId = oauthSecretServerId(url);
6033
+ const next = getOwnEntry(snapshot.servers, url);
6034
+ const candidates = [
6035
+ .../* @__PURE__ */ new Set([
6036
+ ...serverSecretFields(getOwnEntry(disk?.servers, url)),
6037
+ ...serverSecretFields(next)
6038
+ ])
6039
+ ];
6040
+ const entryPrior = await snapshotSecretFields(
6041
+ secretStore,
6042
+ serverId,
6043
+ candidates
6044
+ );
6045
+ priorSecrets.push(...entryPrior);
6046
+ if (next === void 0) {
6047
+ for (const field of candidates) {
6048
+ ourWrites.set(priorKey({ serverId, field }), null);
6049
+ }
6050
+ await secretStore.deleteAllForServer(serverId);
6051
+ for (const field of candidates) {
6052
+ attemptWrites.set(priorKey({ serverId, field }), {
6053
+ serverId,
6054
+ field,
6055
+ value: null
6056
+ });
6057
+ }
6058
+ continue;
6059
+ }
6060
+ const { residue, secrets } = splitServerOAuthState(next, policy);
6061
+ for (const field of candidates) {
6062
+ ourWrites.set(
6063
+ priorKey({ serverId, field }),
6064
+ secrets[field] ?? null
6065
+ );
6066
+ }
6067
+ setOwnEntry(merged.servers, url, residue);
6068
+ if (!durable) {
6069
+ const diskEntry = getOwnEntry(disk?.servers, url);
6070
+ const keep = preserveNonDurableSecrets(
6071
+ diskEntry ? splitServerOAuthState(diskEntry, policy).secrets : {},
6072
+ secrets
6073
+ );
6074
+ if (Object.keys(keep).length > 0) {
6075
+ setOwnEntry(
6076
+ merged.servers,
6077
+ url,
6078
+ joinServerOAuthState(residue, keep)
6079
+ );
6080
+ }
6081
+ }
6082
+ const persisted = await persistEntrySecrets(
6083
+ secretStore,
6084
+ serverId,
6085
+ candidates,
6086
+ secrets,
6087
+ entryPrior,
6088
+ // Degrading is only sound when the disk entry is the pre-call
6089
+ // state this attempt's priors pair with; on a retry it may be an
6090
+ // earlier attempt's committed residue, so escalate into the
6091
+ // reconciling exit instead.
6092
+ attempt === 0
6093
+ );
6094
+ if (!persisted) {
6095
+ revertEntryToDisk(merged.servers, disk?.servers, url);
6096
+ continue;
6097
+ }
6098
+ for (const field of candidates) {
6099
+ attemptWrites.set(priorKey({ serverId, field }), {
6100
+ serverId,
6101
+ field,
6102
+ value: secrets[field] ?? null
6103
+ });
6104
+ }
6105
+ }
6106
+ for (const issuer of effective.idpSessions ?? []) {
6107
+ const serverId = oauthIdpSecretServerId(issuer);
6108
+ const next = getOwnEntry(snapshot.idpSessions, issuer);
6109
+ const entryPrior = await snapshotSecretFields(secretStore, serverId, [
6110
+ IDP_SESSION_FIELD
6111
+ ]);
6112
+ priorSecrets.push(...entryPrior);
6113
+ if (next === void 0) {
6114
+ ourWrites.set(
6115
+ priorKey({ serverId, field: IDP_SESSION_FIELD }),
6116
+ null
6117
+ );
6118
+ await secretStore.deleteAllForServer(serverId);
6119
+ attemptWrites.set(
6120
+ priorKey({ serverId, field: IDP_SESSION_FIELD }),
6121
+ {
6122
+ serverId,
6123
+ field: IDP_SESSION_FIELD,
6124
+ value: null
6125
+ }
6126
+ );
6127
+ continue;
6128
+ }
6129
+ const { residue, secrets } = splitIdpSession(next, policy);
6130
+ ourWrites.set(
6131
+ priorKey({ serverId, field: IDP_SESSION_FIELD }),
6132
+ secrets[IDP_SESSION_FIELD] ?? null
6133
+ );
6134
+ setOwnEntry(merged.idpSessions, issuer, residue);
6135
+ if (!durable) {
6136
+ const diskSession = getOwnEntry(disk?.idpSessions, issuer);
6137
+ const keep = preserveNonDurableSecrets(
6138
+ diskSession ? splitIdpSession(diskSession, policy).secrets : {},
6139
+ secrets
6140
+ );
6141
+ if (Object.keys(keep).length > 0) {
6142
+ setOwnEntry(
6143
+ merged.idpSessions,
6144
+ issuer,
6145
+ joinIdpSession(residue, keep)
6146
+ );
6147
+ }
6148
+ }
6149
+ const persisted = await persistEntrySecrets(
6150
+ secretStore,
6151
+ serverId,
6152
+ [IDP_SESSION_FIELD],
6153
+ secrets,
6154
+ entryPrior,
6155
+ // Same as the server loop: degrade on the first attempt only.
6156
+ attempt === 0
6157
+ );
6158
+ if (!persisted) {
6159
+ revertEntryToDisk(merged.idpSessions, disk?.idpSessions, issuer);
6160
+ continue;
6161
+ }
6162
+ attemptWrites.set(priorKey({ serverId, field: IDP_SESSION_FIELD }), {
6163
+ serverId,
6164
+ field: IDP_SESSION_FIELD,
6165
+ value: secrets[IDP_SESSION_FIELD] ?? null
6166
+ });
6167
+ }
6168
+ written = serializeOAuthPersistBlob(merged);
6169
+ await writeStoreFile(filePath, written);
6170
+ unconfirmed = { written, writes: attemptWrites };
6171
+ } catch (error) {
6172
+ foldPriors(priorSecrets);
6173
+ if (await reconcileFailure()) return;
6174
+ throw error;
6175
+ }
6176
+ foldPriors(priorSecrets);
6177
+ let observed;
6178
+ try {
6179
+ observed = await readStoreFile(filePath);
6180
+ } catch {
6181
+ continue;
6182
+ }
6183
+ if (observed === written) return;
6184
+ unconfirmed = null;
6185
+ }
6186
+ if (await reconcileFailure()) return;
6187
+ throw new SecretStoreUnavailableError(
6188
+ `Could not save OAuth state: another process kept overwriting ${filePath} (gave up after ${MAX_WRITE_ATTEMPTS} attempts). Re-run the action to retry.`
6189
+ );
6190
+ });
6191
+ }
6192
+ function secretRequestsFor(snapshot) {
6193
+ const requests = [];
6194
+ for (const [url, state] of Object.entries(snapshot.servers)) {
6195
+ requests.push({
6196
+ serverId: oauthSecretServerId(url),
6197
+ fields: serverSecretFields(state)
6198
+ });
6199
+ }
6200
+ for (const issuer of Object.keys(snapshot.idpSessions)) {
6201
+ requests.push({
6202
+ serverId: oauthIdpSecretServerId(issuer),
6203
+ fields: [IDP_SESSION_FIELD]
6204
+ });
6205
+ }
6206
+ return requests;
6207
+ }
6208
+ async function joinSnapshot(snapshot, secretStore) {
6209
+ const requests = secretRequestsFor(snapshot);
6210
+ const values = requests.length > 0 ? await secretStoreGetManyStrict(secretStore, requests) : {};
6211
+ return {
6212
+ servers: Object.fromEntries(
6213
+ Object.entries(snapshot.servers).map(([url, state]) => [
6214
+ url,
6215
+ joinServerOAuthState(state, values[oauthSecretServerId(url)] ?? {})
6216
+ ])
6217
+ ),
6218
+ idpSessions: Object.fromEntries(
6219
+ Object.entries(snapshot.idpSessions).map(([issuer, session]) => [
6220
+ issuer,
6221
+ joinIdpSession(session, values[oauthIdpSecretServerId(issuer)] ?? {})
6222
+ ])
6223
+ )
6224
+ };
6225
+ }
6226
+ async function migratePlaintextSecrets(filePath, secretStore) {
6227
+ const raw = await readStoreFile(filePath);
6228
+ const fresh = parseOAuthPersistBlob(raw);
6229
+ if (!fresh || !snapshotHasPlaintextSecrets(fresh)) return;
6230
+ const migrateEntrySecrets = async (serverId, secrets) => {
6231
+ const absent = {};
6232
+ for (const [field, value] of Object.entries(secrets)) {
6233
+ const existing = await secretStoreGetStrict(secretStore, serverId, field);
6234
+ if (existing === null || !isUsableStoredSecret(field, existing)) {
6235
+ absent[field] = value;
6236
+ }
6237
+ }
6238
+ if (Object.keys(absent).length > 0) {
6239
+ await secretStoreSetMany(secretStore, serverId, absent);
6240
+ }
6241
+ };
6242
+ const residue = { servers: {}, idpSessions: {} };
6243
+ for (const [url, state] of Object.entries(fresh.servers)) {
6244
+ const split = splitServerOAuthState(state, "all");
6245
+ setOwnEntry(residue.servers, url, split.residue);
6246
+ await migrateEntrySecrets(oauthSecretServerId(url), split.secrets);
6247
+ }
6248
+ for (const [issuer, session] of Object.entries(fresh.idpSessions)) {
6249
+ const split = splitIdpSession(session, "all");
6250
+ setOwnEntry(residue.idpSessions, issuer, split.residue);
6251
+ await migrateEntrySecrets(oauthIdpSecretServerId(issuer), split.secrets);
6252
+ }
6253
+ const stripped = serializeOAuthPersistBlob(residue);
6254
+ if (stripped !== raw) {
6255
+ await writeStoreFile(filePath, stripped);
6256
+ }
6257
+ }
6258
+ async function readOAuthStore(filePath, secretStore = defaultSecretStore()) {
6259
+ return withOAuthStateLock(filePath, "read", async () => {
6260
+ let snapshot = parseOAuthPersistBlob(await readStoreFile(filePath));
6261
+ if (snapshot === null) return null;
6262
+ if (snapshotHasPlaintextSecrets(snapshot) && await secretStoreIsDurable(secretStore)) {
6263
+ try {
6264
+ await migratePlaintextSecrets(filePath, secretStore);
6265
+ snapshot = parseOAuthPersistBlob(await readStoreFile(filePath)) ?? snapshot;
6266
+ } catch (error) {
6267
+ warnMigrationFailure(error);
6268
+ }
6269
+ }
6270
+ return joinSnapshot(snapshot, secretStore);
6271
+ });
6272
+ }
6273
+ async function removeOAuthStore(filePath, secretStore = defaultSecretStore()) {
6274
+ await withOAuthStateLock(filePath, "remove", async () => {
6275
+ const snapshot = await readDiskForMutation(filePath, "remove");
6276
+ if (snapshot) {
6277
+ const targets = [
6278
+ ...Object.entries(snapshot.servers).map(([url, state]) => ({
6279
+ id: oauthSecretServerId(url),
6280
+ fields: serverSecretFields(state)
6281
+ })),
6282
+ ...Object.keys(snapshot.idpSessions).map((issuer) => ({
6283
+ id: oauthIdpSecretServerId(issuer),
6284
+ fields: [IDP_SESSION_FIELD]
6285
+ }))
6286
+ ];
6287
+ const priorSecrets = [];
6288
+ try {
6289
+ for (const { id, fields } of targets) {
6290
+ priorSecrets.push(
6291
+ ...await snapshotSecretFields(secretStore, id, fields)
6292
+ );
6293
+ await secretStore.deleteAllForServer(id);
6294
+ }
6295
+ await deleteStoreFile(filePath);
6296
+ } catch (error) {
6297
+ await restoreSecretFields(
6298
+ secretStore,
6299
+ priorSecrets,
6300
+ warnRestoreFailure
6301
+ );
6302
+ throw error;
6303
+ }
6304
+ } else {
6305
+ await deleteStoreFile(filePath);
6306
+ }
6307
+ });
6308
+ }
6309
+ function createFileOAuthPersistBackend(options) {
6310
+ const secretStore = options.secretStore ?? defaultSecretStore();
6311
+ return {
6312
+ async read() {
6313
+ return readOAuthStore(options.filePath, secretStore);
6314
+ },
6315
+ async write(snapshot, sections) {
6316
+ await writeOAuthSections(
6317
+ options.filePath,
6318
+ snapshot,
6319
+ sections,
6320
+ secretStore
6321
+ );
6322
+ },
6323
+ async remove() {
6324
+ await removeOAuthStore(options.filePath, secretStore);
6325
+ }
6326
+ };
6327
+ }
6328
+
6329
+ // ../../core/auth/node/storage-node.ts
6330
+ var DEFAULT_STATE_PATH = getStoreFilePath(getDefaultStorageDir(), "oauth");
6331
+ function getStateFilePath(customPath) {
6332
+ if (customPath) return customPath;
6333
+ if (process.env.MCP_INSPECTOR_OAUTH_STATE_PATH) {
6334
+ return process.env.MCP_INSPECTOR_OAUTH_STATE_PATH;
6335
+ }
6336
+ if (process.env.MCP_STORAGE_DIR) {
6337
+ return getStoreFilePath(process.env.MCP_STORAGE_DIR, "oauth");
6338
+ }
6339
+ return DEFAULT_STATE_PATH;
6340
+ }
6341
+ var sharedStateCache = /* @__PURE__ */ new Map();
6342
+ var storageCache = /* @__PURE__ */ new Map();
6343
+ function getSharedState(stateFilePath) {
6344
+ const key = getStateFilePath(stateFilePath);
6345
+ let shared = sharedStateCache.get(key);
6346
+ if (!shared) {
6347
+ shared = {
6348
+ memory: new OAuthMemoryStore(),
6349
+ coordination: new OAuthStorageCoordination()
6350
+ };
6351
+ sharedStateCache.set(key, shared);
6352
+ }
6353
+ return shared;
6354
+ }
6355
+ function resetNodeOAuthStorageCache(stateFilePath) {
6356
+ const key = getStateFilePath(stateFilePath);
6357
+ sharedStateCache.delete(key);
6358
+ storageCache.delete(key);
6359
+ }
6360
+ var NodeOAuthStorage = class extends OAuthStorageBase {
6361
+ /**
6362
+ * @param storagePath - Optional path to state file. Default: ~/.mcp-inspector/storage/oauth.json
6363
+ * @param secretStore - Optional secret store for tokens/client secrets.
6364
+ * Default: the process-wide selected store. Tests inject an in-memory
6365
+ * double here so they never touch the OS keychain.
6366
+ */
6367
+ constructor(storagePath, secretStore) {
6368
+ const filePath = getStateFilePath(storagePath);
6369
+ const shared = getSharedState(storagePath);
6370
+ super(
6371
+ shared.memory,
6372
+ createFileOAuthPersistBackend({ filePath, secretStore }),
6373
+ shared.coordination
6374
+ );
6375
+ }
6376
+ };
6377
+
6378
+ // ../../core/auth/revocation.ts
6379
+ import { selectClientAuthMethod } from "@modelcontextprotocol/client";
6380
+ import {
6381
+ OAuthClientInformationSchema as OAuthClientInformationSchema2,
6382
+ OAuthTokensSchema as OAuthTokensSchema4
6383
+ } from "@modelcontextprotocol/core";
6384
+ var DEFAULT_REVOCATION_TIMEOUT_MS = 5e3;
6385
+ var MIN_REVOCATION_REQUEST_BUDGET_MS = 5;
6386
+ function selectRevocableToken(tokens) {
6387
+ if (!tokens) return null;
6388
+ if (tokens.refresh_token) {
6389
+ return { token: tokens.refresh_token, tokenTypeHint: "refresh_token" };
6390
+ }
6391
+ if (tokens.access_token) {
6392
+ return { token: tokens.access_token, tokenTypeHint: "access_token" };
6393
+ }
6394
+ return null;
6395
+ }
6396
+ function revocationAuthMethods(metadata) {
6397
+ return metadata.revocation_endpoint_auth_methods_supported ?? [
6398
+ "client_secret_basic"
6399
+ ];
6400
+ }
6401
+ function buildRevocationRequest(params) {
6402
+ const body = new URLSearchParams({
6403
+ token: params.token,
6404
+ token_type_hint: params.tokenTypeHint
6405
+ });
6406
+ const headers = {
6407
+ "Content-Type": "application/x-www-form-urlencoded",
6408
+ Accept: "application/json"
6409
+ };
6410
+ const client = params.clientInformation;
6411
+ if (client) {
6412
+ const method = selectClientAuthMethod(client, params.supportedAuthMethods);
6413
+ if (method === "client_secret_basic" && client.client_secret) {
6414
+ const credentials = `${encodeURIComponent(client.client_id)}:${encodeURIComponent(client.client_secret)}`;
6415
+ headers.Authorization = `Basic ${base64Encode(credentials)}`;
6416
+ } else if (method === "client_secret_post" && client.client_secret) {
6417
+ body.set("client_id", client.client_id);
6418
+ body.set("client_secret", client.client_secret);
6419
+ } else {
6420
+ body.set("client_id", client.client_id);
6421
+ }
6422
+ }
6423
+ return {
6424
+ url: params.endpoint,
6425
+ init: { method: "POST", headers, body: body.toString() }
6426
+ };
6427
+ }
6428
+ function base64Encode(value) {
6429
+ if (typeof btoa === "function") return btoa(value);
6430
+ return Buffer.from(value, "utf8").toString("base64");
6431
+ }
6432
+ async function revokeToken(params) {
6433
+ const timeoutMs = Math.max(
6434
+ 0,
5447
6435
  Math.round(params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS)
5448
6436
  );
5449
6437
  try {
@@ -5619,7 +6607,7 @@ async function collectGrants(snapshot) {
5619
6607
  return { grants, failures };
5620
6608
  }
5621
6609
  async function parseTokens(raw) {
5622
- return raw === void 0 || raw === null ? void 0 : await OAuthTokensSchema2.parseAsync(raw);
6610
+ return raw === void 0 || raw === null ? void 0 : await OAuthTokensSchema4.partial().parseAsync(raw);
5623
6611
  }
5624
6612
  async function parseClient(raw) {
5625
6613
  return raw === void 0 || raw === null ? void 0 : await OAuthClientInformationSchema2.parseAsync(raw);
@@ -6122,10 +7110,13 @@ var ADVERTISABLE_EXTENSIONS = [
6122
7110
  {
6123
7111
  key: UI_EXTENSION_KEY,
6124
7112
  label: "MCP Apps UI (io.modelcontextprotocol/ui)",
6125
- // The MCP Apps UI extension. The Inspector always renders App tools, so it
6126
- // advertises this by default with the App resource MIME type it supports —
6127
- // a conforming server checks the `mimeTypes` before serving a UI resource.
7113
+ // The MCP Apps UI extension, advertised with the App resource MIME type the
7114
+ // Inspector renders — a conforming server checks the `mimeTypes` before
7115
+ // serving a UI resource. Default-on only where Apps can actually be
7116
+ // rendered (the web client); the CLI and TUI cannot, so they leave it off
7117
+ // unless explicitly overridden (#2403).
6128
7118
  defaultAdvertised: true,
7119
+ requiresAppRenderer: true,
6129
7120
  advertisement: { mimeTypes: [MCP_APP_MIME_TYPE] }
6130
7121
  },
6131
7122
  {
@@ -6141,10 +7132,13 @@ var ADVERTISABLE_EXTENSIONS = [
6141
7132
  defaultAdvertised: true
6142
7133
  }
6143
7134
  ];
7135
+ function isAdvertisedByDefault(ext, rendersApps) {
7136
+ return ext.defaultAdvertised && (!ext.requiresAppRenderer || rendersApps);
7137
+ }
6144
7138
  function buildClientExtensions(input) {
6145
7139
  const map = {};
6146
7140
  for (const ext of ADVERTISABLE_EXTENSIONS) {
6147
- const advertised = input.advertised?.[ext.key] ?? ext.defaultAdvertised;
7141
+ const advertised = input.advertised?.[ext.key] ?? isAdvertisedByDefault(ext, input.rendersApps === true);
6148
7142
  if (advertised) {
6149
7143
  map[ext.key] = ext.advertisement ? structuredClone(ext.advertisement) : {};
6150
7144
  }
@@ -7089,6 +8083,14 @@ function exemptMcpEndpoint(getServerUrl, negotiatesEndpoint) {
7089
8083
  };
7090
8084
  }
7091
8085
  var REQUEST_DEADLINES = /* @__PURE__ */ new WeakMap();
8086
+ function stampRequestDeadline(init, budget) {
8087
+ if (REQUEST_DEADLINES.has(init)) {
8088
+ throw new Error(
8089
+ "stampRequestDeadline: this RequestInit already carries a deadline \u2014 withOAuthRequestTimeout must build a fresh init per call (#2418)"
8090
+ );
8091
+ }
8092
+ REQUEST_DEADLINES.set(init, budget);
8093
+ }
7092
8094
  function requestHeadersOf(input, init) {
7093
8095
  if (init?.headers) return new Headers(init.headers);
7094
8096
  if (typeof input !== "string" && !(input instanceof URL)) {
@@ -7146,7 +8148,7 @@ function withOAuthRequestTimeout(fetchFn, timeoutMs = DEFAULT_OAUTH_REQUEST_TIME
7146
8148
  }
7147
8149
  });
7148
8150
  const nextInit = { ...init, signal };
7149
- REQUEST_DEADLINES.set(nextInit, budget);
8151
+ stampRequestDeadline(nextInit, budget);
7150
8152
  let settled;
7151
8153
  let drainReader;
7152
8154
  try {
@@ -8243,7 +9245,7 @@ async function discoverResourceAsMetadata(resourceAsUrl, fetchFn) {
8243
9245
 
8244
9246
  // ../../core/auth/ema/wire.ts
8245
9247
  import { discoverAndRequestJwtAuthGrant } from "@modelcontextprotocol/client";
8246
- import { OAuthTokensSchema as OAuthTokensSchema3 } from "@modelcontextprotocol/core";
9248
+ import { OAuthTokensSchema as OAuthTokensSchema5 } from "@modelcontextprotocol/core";
8247
9249
  async function exchangeIdJag(params) {
8248
9250
  const issuer = normalizeIdpIssuer(params.idp.issuer);
8249
9251
  const resource = params.resource.trim();
@@ -8322,7 +9324,7 @@ async function redeemIdJagForAccessToken(params) {
8322
9324
  "EMA leg 3 (resource AS JWT bearer grant)"
8323
9325
  );
8324
9326
  }
8325
- return OAuthTokensSchema3.parse(await response.json());
9327
+ return OAuthTokensSchema5.parse(await response.json());
8326
9328
  }
8327
9329
 
8328
9330
  // ../../core/auth/ema/emaFlow.ts
@@ -9547,6 +10549,14 @@ function progressTokenOf(message) {
9547
10549
  }
9548
10550
  return void 0;
9549
10551
  }
10552
+ function waitForProgressToken(waits, token) {
10553
+ const exact = waits.get(token);
10554
+ if (exact !== void 0 || typeof token === "number") {
10555
+ return exact;
10556
+ }
10557
+ const numeric = Number(token);
10558
+ return Number.isSafeInteger(numeric) ? waits.get(numeric) : void 0;
10559
+ }
9550
10560
 
9551
10561
  // ../../core/mcp/remote/remoteClientTransport.ts
9552
10562
  var DEFAULT_SSE_RESPONSE_TIMEOUT_MS = 6e4;
@@ -10023,8 +11033,8 @@ var RemoteClientTransport = class {
10023
11033
  });
10024
11034
  }
10025
11035
  /** Re-arm a pending request's SSE wait timeout on a matching progress note. */
10026
- resetSseResponseWait(requestId) {
10027
- this.sseResponseWaits.get(requestId)?.resetTimeout();
11036
+ resetSseResponseWait(progressToken) {
11037
+ waitForProgressToken(this.sseResponseWaits, progressToken)?.resetTimeout();
10028
11038
  }
10029
11039
  async postSend(message, options, retryCount) {
10030
11040
  const requestId = requestIdForMessage(message);
@@ -10229,6 +11239,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10229
11239
  requestTimeout;
10230
11240
  defaultMetadata;
10231
11241
  serverSettings;
11242
+ // The settings the current transport was built from. `serverSettings` is
11243
+ // replaced live on every settings save (#1444), but transport-level inputs
11244
+ // such as custom headers are fixed when the transport is created, so this
11245
+ // is what the open connection actually sends (#2460).
11246
+ transportSettings;
10232
11247
  versionNegotiation;
10233
11248
  status = "disconnected";
10234
11249
  // True only while an explicit disconnect() owns the teardown. close() can
@@ -10583,6 +11598,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10583
11598
  const advertisedExtensions = buildClientExtensions({
10584
11599
  enterpriseManaged: options.oauth?.enterpriseManaged ?? false,
10585
11600
  advertised: this.advertisedExtensions,
11601
+ // Only a client that can render Apps claims the UI extension by default
11602
+ // (#2403). An app-elicitation renderer is itself a claim to host an App,
11603
+ // so supplying one implies it.
11604
+ rendersApps: options.rendersApps === true || this.appElicitationRenderer !== void 0,
10586
11605
  // Read off the built `capabilities.elicitation.form` rather than
10587
11606
  // re-deriving from `options.elicit`: the nested MCP Apps `elicitation`
10588
11607
  // setting must never be advertised without the core form capability it
@@ -11008,6 +12027,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11008
12027
  }
11009
12028
  this.baseTransport = null;
11010
12029
  this.transport = null;
12030
+ this.transportSettings = void 0;
11011
12031
  this.transportHasAuthProvider = false;
11012
12032
  }
11013
12033
  /**
@@ -11481,6 +12501,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11481
12501
  transportOptions
11482
12502
  );
11483
12503
  this.baseTransport = baseTransport;
12504
+ this.transportSettings = transportOptions.settings;
11484
12505
  if (this.directAuthRecovery) {
11485
12506
  this.directAuthRecoveryActive = !(baseTransport instanceof RemoteClientTransport);
11486
12507
  }
@@ -11682,6 +12703,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11682
12703
  }
11683
12704
  this.baseTransport = null;
11684
12705
  this.transport = null;
12706
+ this.transportSettings = void 0;
11685
12707
  this.transportHasAuthProvider = false;
11686
12708
  this.clearPendingPeerRequests();
11687
12709
  if (this.status !== "disconnected") {
@@ -12414,6 +13436,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12414
13436
  getClientCapabilities() {
12415
13437
  return this.clientCapabilities;
12416
13438
  }
13439
+ /**
13440
+ * Get the client identity (name, version) this client reports to servers —
13441
+ * the caller's `clientIdentity`, or core's neutral fallback without one.
13442
+ */
13443
+ getClientInfo() {
13444
+ return this.clientInfo;
13445
+ }
12417
13446
  /**
12418
13447
  * Get server info (name, version)
12419
13448
  */
@@ -12465,6 +13494,16 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12465
13494
  getServerSettings() {
12466
13495
  return this.serverSettings;
12467
13496
  }
13497
+ /**
13498
+ * The settings the current transport was built from, or `undefined` when no
13499
+ * transport exists. Unlike {@link getServerSettings}, a live settings edit
13500
+ * does not change this: transport-level inputs (custom headers) apply only
13501
+ * when the next transport is created, so comparing the two tells a caller
13502
+ * whether an edit is still waiting on a reconnect (#2460).
13503
+ */
13504
+ getTransportSettings() {
13505
+ return this.transportSettings;
13506
+ }
12468
13507
  /**
12469
13508
  * Replace the in-memory per-server settings on a live client. Lets a settings
12470
13509
  * edit (e.g. toggling auto-refresh-on-list-changed) take effect on the
@@ -13986,10 +15025,9 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13986
15025
  * removes it before the result gets here (#2373).
13987
15026
  *
13988
15027
  * Separate from {@link getSkill} because the callers differ: the UIs want the
13989
- * entry, while the CLI prints the result and must not reshape it. SEP-2640
13990
- * explicitly leaves open whether this result carries `ttlMs` / `cacheScope`,
13991
- * so a server may send them — and unwrapping to the entry discards exactly
13992
- * those (Copilot).
15028
+ * entry, while the CLI prints the result and must not reshape it. A modern
15029
+ * result carries `ttlMs` / `cacheScope` (#2404), and unwrapping to the entry
15030
+ * discards exactly those (Copilot).
13993
15031
  */
13994
15032
  async getSkillResult(uri, metadata) {
13995
15033
  if (!this.client) {
@@ -14000,11 +15038,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
14000
15038
  uri,
14001
15039
  ...effectiveMeta ? { _meta: effectiveMeta } : {}
14002
15040
  };
15041
+ const resultSchema = this.isModernEra() ? ModernGetSkillEnvelopeSchema : GetSkillEnvelopeSchema;
14003
15042
  try {
14004
15043
  return await this.invokeMcpClient(
14005
15044
  () => this.client.request(
14006
15045
  { method: SKILLS_GET_METHOD, params },
14007
- GetSkillEnvelopeSchema,
15046
+ resultSchema,
14008
15047
  this.getRequestOptions(this.progressTokenOf(metadata))
14009
15048
  ),
14010
15049
  { method: SKILLS_GET_METHOD }
@@ -15141,18 +16180,83 @@ function parseAddressPart(part) {
15141
16180
  if (/^[0-9]+$/.test(part)) return parseInt(part, 10);
15142
16181
  return NaN;
15143
16182
  }
15144
- function isAllZeroIpv4(value) {
15145
- const parts = value.split(".");
15146
- if (parts.length > 4) return false;
15147
- return parts.every((part) => parseAddressPart(part) === 0);
16183
+ function isAllZeroIpv4(value) {
16184
+ const parts = value.split(".");
16185
+ if (parts.length > 4) return false;
16186
+ return parts.every((part) => parseAddressPart(part) === 0);
16187
+ }
16188
+ function isAllInterfacesHost(host) {
16189
+ const normalized = canonicalizeIpv6(stripBrackets(canonicalUrlHost(host)));
16190
+ return ALL_INTERFACES_LITERALS.has(normalized) || isAllZeroIpv4(normalized);
16191
+ }
16192
+ function isLoopbackHost(host) {
16193
+ const h = canonicalUrlHost(host).replace(/\.$/, "");
16194
+ return h === "localhost" || h === "[::1]" || /^127(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/.test(h);
16195
+ }
16196
+
16197
+ // ../../core/auth/issuerBinding.ts
16198
+ import { AuthorizationServerMismatchError } from "@modelcontextprotocol/client";
16199
+ var MISSING_DISCOVERY_STATE_PHRASE = "discoveryState was not available";
16200
+ function isAuthorizationServerMismatchShape(err) {
16201
+ if (err === null || typeof err !== "object") {
16202
+ return false;
16203
+ }
16204
+ const candidate = err;
16205
+ if (typeof candidate.recordedIssuer !== "string" || typeof candidate.currentIssuer !== "string") {
16206
+ return false;
16207
+ }
16208
+ return AuthorizationServerMismatchError.isInstance(err) || candidate.name === "AuthorizationServerMismatchError";
16209
+ }
16210
+ function isMissingDiscoveryStatePlaceholder(recordedIssuer) {
16211
+ if (recordedIssuer.includes(MISSING_DISCOVERY_STATE_PHRASE)) {
16212
+ return true;
16213
+ }
16214
+ try {
16215
+ const url = new URL(recordedIssuer);
16216
+ return url.protocol !== "http:" && url.protocol !== "https:";
16217
+ } catch {
16218
+ return true;
16219
+ }
16220
+ }
16221
+ function findIssuerBindingFailure(err) {
16222
+ return findIssuerBindingFailureDeep(err, /* @__PURE__ */ new Set());
15148
16223
  }
15149
- function isAllInterfacesHost(host) {
15150
- const normalized = canonicalizeIpv6(stripBrackets(canonicalUrlHost(host)));
15151
- return ALL_INTERFACES_LITERALS.has(normalized) || isAllZeroIpv4(normalized);
16224
+ function findIssuerBindingFailureDeep(err, seen) {
16225
+ if (err === null || typeof err !== "object" || seen.has(err)) {
16226
+ return void 0;
16227
+ }
16228
+ seen.add(err);
16229
+ if (isAuthorizationServerMismatchShape(err)) {
16230
+ return classifyAuthorizationServerMismatch(err);
16231
+ }
16232
+ const nested = findIssuerBindingFailureDeep(
16233
+ err.cause,
16234
+ seen
16235
+ );
16236
+ if (nested) {
16237
+ return nested;
16238
+ }
16239
+ const data = err.data;
16240
+ if (data !== null && typeof data === "object") {
16241
+ return findIssuerBindingFailureDeep(
16242
+ data.cause,
16243
+ seen
16244
+ );
16245
+ }
16246
+ return void 0;
15152
16247
  }
15153
- function isLoopbackHost(host) {
15154
- const h = canonicalUrlHost(host).replace(/\.$/, "");
15155
- return h === "localhost" || h === "[::1]" || /^127(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/.test(h);
16248
+ function classifyAuthorizationServerMismatch(err) {
16249
+ if (isMissingDiscoveryStatePlaceholder(err.recordedIssuer)) {
16250
+ return {
16251
+ kind: "lost_authorization_state",
16252
+ currentIssuer: err.currentIssuer
16253
+ };
16254
+ }
16255
+ return {
16256
+ kind: "issuer_mismatch",
16257
+ recordedIssuer: err.recordedIssuer,
16258
+ currentIssuer: err.currentIssuer
16259
+ };
15156
16260
  }
15157
16261
 
15158
16262
  // src/error-handler.ts
@@ -15186,7 +16290,20 @@ var EXIT_CODES = {
15186
16290
  * job that wants to tolerate oversized catalogs can allow 8 and still fail
15187
16291
  * on 7.
15188
16292
  */
15189
- SKILL_INCOMPLETE: 8
16293
+ SKILL_INCOMPLETE: 8,
16294
+ /**
16295
+ * `--verify --require-digests` found a skill whose `resources` is
16296
+ * `"dynamic"`, so no digest was advertised and nothing was hashed (#2405).
16297
+ *
16298
+ * Only ever produced under `--require-digests`: `"dynamic"` is a conforming
16299
+ * wire form, and SEP-2640 leaves declining such skills to the host ("Hosts
16300
+ * MAY decline to load such skills"). The flag is how a CI job standing in for
16301
+ * a host that declines them says so; without it the run exits 0 and reports
16302
+ * `outcome: "unverifiable"`. Its own code, like 8, so a job can tell "no
16303
+ * digests to check" apart from "a digest was wrong" and "the walk was cut
16304
+ * short".
16305
+ */
16306
+ SKILL_UNVERIFIABLE: 9
15190
16307
  };
15191
16308
  var CliExitCodeError = class extends Error {
15192
16309
  constructor(exitCode, message, envelope) {
@@ -15221,7 +16338,44 @@ function statusOf(error) {
15221
16338
  return void 0;
15222
16339
  }
15223
16340
  var UNREACHABLE_PATTERN = /ENOTFOUND|ECONNREFUSED|ECONNRESET|EAI_AGAIN|ETIMEDOUT|fetch failed|getaddrinfo|connect(?:ion)? timed out|aborted/i;
16341
+ var EMBEDDED_URL_PATTERN = /\bhttps?:\/\/(?:(?!https?:\/\/)[^\s"<>])+/gi;
16342
+ var TRAILING_PUNCTUATION = /* @__PURE__ */ new Set([
16343
+ ".",
16344
+ ",",
16345
+ ";",
16346
+ ":",
16347
+ "!",
16348
+ "?",
16349
+ ")",
16350
+ "]",
16351
+ "'"
16352
+ ]);
16353
+ function trailingPunctuationStart(match) {
16354
+ let end = match.length;
16355
+ while (end > 0 && TRAILING_PUNCTUATION.has(match.charAt(end - 1))) end--;
16356
+ return end;
16357
+ }
16358
+ function redactUrlsInText(text) {
16359
+ return text.replace(EMBEDDED_URL_PATTERN, (match) => {
16360
+ const end = trailingPunctuationStart(match);
16361
+ return redactUrlQuery(match.slice(0, end)) + match.slice(end);
16362
+ });
16363
+ }
16364
+ function redactEnvelope(envelope) {
16365
+ return {
16366
+ ...envelope,
16367
+ message: redactUrlsInText(envelope.message),
16368
+ ...envelope.cause !== void 0 && {
16369
+ cause: redactUrlsInText(envelope.cause)
16370
+ },
16371
+ ...envelope.url !== void 0 && { url: redactUrlQuery(envelope.url) }
16372
+ };
16373
+ }
15224
16374
  function classifyError(error, context) {
16375
+ const { exitCode, envelope } = classifyUnredacted(error, context);
16376
+ return { exitCode, envelope: redactEnvelope(envelope) };
16377
+ }
16378
+ function classifyUnredacted(error, context) {
15225
16379
  const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
15226
16380
  const cause = causeOf(error);
15227
16381
  const status = statusOf(error);
@@ -15242,9 +16396,29 @@ function classifyError(error, context) {
15242
16396
  }
15243
16397
  };
15244
16398
  }
15245
- if (status === 401 || status === 403 || /WWW-Authenticate|Unauthorized|invalid_token|OAuth/i.test(
15246
- message + " " + (cause ?? "")
15247
- )) {
16399
+ if (error instanceof SecretStoreUnavailableError) {
16400
+ return {
16401
+ exitCode: EXIT_CODES.USAGE,
16402
+ envelope: {
16403
+ code: "store_unavailable",
16404
+ message,
16405
+ ...cause !== void 0 && { cause },
16406
+ ...url !== void 0 && { url }
16407
+ }
16408
+ };
16409
+ }
16410
+ if (error instanceof OAuthStateFileUnrecognizedError) {
16411
+ return {
16412
+ exitCode: EXIT_CODES.USAGE,
16413
+ envelope: {
16414
+ code: "oauth_state_unrecognized",
16415
+ message,
16416
+ ...cause !== void 0 && { cause },
16417
+ ...url !== void 0 && { url }
16418
+ }
16419
+ };
16420
+ }
16421
+ if (status === 401 || status === 403 || isUnauthorizedError(error)) {
15248
16422
  return {
15249
16423
  exitCode: EXIT_CODES.AUTH_REQUIRED,
15250
16424
  envelope: {
@@ -15686,6 +16860,9 @@ async function emitResult(result, appInfo, args) {
15686
16860
  }
15687
16861
 
15688
16862
  // src/handlers/consume-outcome.ts
16863
+ function isBrokenPipe(err) {
16864
+ return err instanceof Error && err.code === "EPIPE";
16865
+ }
15689
16866
  async function consumeMethodOutcome(outcome, args) {
15690
16867
  if (outcome.kind === "result") {
15691
16868
  await emitResult(outcome.result, outcome.appInfo, args);
@@ -15701,23 +16878,37 @@ async function consumeMethodOutcome(outcome, args) {
15701
16878
  throw new CliExitCodeError(outcome.exitCode, outcome.summary ?? "", {
15702
16879
  // The envelope's `code` follows the exit code, so a caller reading one
15703
16880
  // never has to reconcile it against the other.
15704
- code: outcome.exitCode === EXIT_CODES.SKILL_INCOMPLETE ? "skills_incomplete" : "skills_nonconformant"
16881
+ code: outcome.exitCode === EXIT_CODES.SKILL_INCOMPLETE ? "skills_incomplete" : outcome.exitCode === EXIT_CODES.SKILL_UNVERIFIABLE ? "skills_unverifiable" : "skills_nonconformant"
15705
16882
  });
15706
16883
  }
15707
16884
  return;
15708
16885
  }
15709
- await new Promise((resolve7) => {
15710
- const stop = outcome.start((obj) => {
15711
- void awaitableLog(JSON.stringify(obj) + "\n");
15712
- });
15713
- const onSignal = () => {
15714
- stop();
16886
+ await new Promise((resolve7, reject) => {
16887
+ let stop;
16888
+ const detach = () => {
15715
16889
  process.off("SIGINT", onSignal);
15716
16890
  process.off("SIGTERM", onSignal);
15717
- resolve7();
16891
+ process.stdout.off("error", onStdoutError);
15718
16892
  };
16893
+ const finish = (err) => {
16894
+ detach();
16895
+ stop?.();
16896
+ if (err === void 0) resolve7();
16897
+ else reject(err);
16898
+ };
16899
+ const onSignal = () => finish();
16900
+ const onStdoutError = (err) => finish(isBrokenPipe(err) ? void 0 : err);
16901
+ process.stdout.on("error", onStdoutError);
15719
16902
  process.on("SIGINT", onSignal);
15720
16903
  process.on("SIGTERM", onSignal);
16904
+ try {
16905
+ stop = outcome.start((obj) => {
16906
+ void awaitableLog(JSON.stringify(obj) + "\n");
16907
+ });
16908
+ } catch (err) {
16909
+ detach();
16910
+ reject(err);
16911
+ }
15721
16912
  });
15722
16913
  }
15723
16914
 
@@ -16538,24 +17729,6 @@ async function collectAppInfo(client, tool, metadata) {
16538
17729
  }
16539
17730
  }
16540
17731
 
16541
- // src/handlers/skills-verify.ts
16542
- function summarizeSkillVerification(reports) {
16543
- const failed = reports.filter((report) => report.outcome === "failed").length;
16544
- const incomplete = reports.filter(
16545
- (report) => report.outcome === "incomplete"
16546
- ).length;
16547
- const files = reports.reduce((sum, report) => sum + report.files.length, 0);
16548
- const mismatched = reports.reduce(
16549
- (sum, report) => sum + report.files.filter((file) => file.status === "mismatch").length,
16550
- 0
16551
- );
16552
- const skillWord = reports.length === 1 ? "skill" : "skills";
16553
- const fileWord = files === 1 ? "file" : "files";
16554
- const incompleteClause = incomplete === 0 ? "" : ` ${incomplete} of ${reports.length} ${skillWord} could not be fully checked: the read bounds stopped the walk.`;
16555
- const headline = failed === 0 ? incomplete === 0 ? `Verified ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors.` : `Checked ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors in what was read.` : `${failed} of ${reports.length} ${skillWord} failed verification (${mismatched} digest/size mismatch across ${files} ${fileWord}).`;
16556
- return `${headline}${incompleteClause}`;
16557
- }
16558
-
16559
17732
  // ../../core/mcp/skillsVerification.ts
16560
17733
  function reasonOf(err) {
16561
17734
  return err instanceof Error ? err.message : String(err);
@@ -16748,7 +17921,11 @@ async function verifySkills(client, entries, metadata) {
16748
17921
  files,
16749
17922
  ...incomplete ? { incomplete } : {},
16750
17923
  ok: !hasError && !fileFailed,
16751
- outcome: hasError || fileFailed ? "failed" : incomplete !== void 0 ? "incomplete" : "verified"
17924
+ // Precedence runs from the loudest verdict down: a broken MUST outranks
17925
+ // an unfinished walk, and an unfinished walk outranks a skill that had
17926
+ // nothing to hash — a `"dynamic"` skill past the run's catalog budget
17927
+ // was not read at all, which `incomplete` already says more precisely.
17928
+ outcome: hasError || fileFailed ? "failed" : incomplete !== void 0 ? "incomplete" : entry.resources === DYNAMIC_RESOURCES ? "unverifiable" : "verified"
16752
17929
  });
16753
17930
  }
16754
17931
  return reports;
@@ -16759,6 +17936,38 @@ function allSkillsVerified(reports) {
16759
17936
  function anySkillFailed(reports) {
16760
17937
  return reports.some((report) => report.outcome === "failed");
16761
17938
  }
17939
+ function anySkillUnverifiable(reports) {
17940
+ return reports.some((report) => report.outcome === "unverifiable");
17941
+ }
17942
+
17943
+ // src/handlers/skills-verify.ts
17944
+ function skillVerificationExitCode(reports, requireDigests) {
17945
+ if (allSkillsVerified(reports)) return void 0;
17946
+ if (anySkillFailed(reports)) return EXIT_CODES.SKILL_NONCONFORMANT;
17947
+ if (reports.some((report) => report.outcome === "incomplete"))
17948
+ return EXIT_CODES.SKILL_INCOMPLETE;
17949
+ return requireDigests && anySkillUnverifiable(reports) ? EXIT_CODES.SKILL_UNVERIFIABLE : void 0;
17950
+ }
17951
+ function summarizeSkillVerification(reports) {
17952
+ const failed = reports.filter((report) => report.outcome === "failed").length;
17953
+ const incomplete = reports.filter(
17954
+ (report) => report.outcome === "incomplete"
17955
+ ).length;
17956
+ const unverifiable = reports.filter(
17957
+ (report) => report.outcome === "unverifiable"
17958
+ ).length;
17959
+ const files = reports.reduce((sum, report) => sum + report.files.length, 0);
17960
+ const mismatched = reports.reduce(
17961
+ (sum, report) => sum + report.files.filter((file) => file.status === "mismatch").length,
17962
+ 0
17963
+ );
17964
+ const skillWord = reports.length === 1 ? "skill" : "skills";
17965
+ const fileWord = files === 1 ? "file" : "files";
17966
+ const incompleteClause = incomplete === 0 ? "" : ` ${incomplete} of ${reports.length} ${skillWord} could not be fully checked: the read bounds stopped the walk.`;
17967
+ const unverifiableClause = unverifiable === 0 ? "" : ` ${unverifiable} of ${reports.length} ${skillWord} advertised no digests (resources: "dynamic"), so ${unverifiable === 1 ? "its" : "their"} integrity was not checked.`;
17968
+ const headline = failed !== 0 ? `${failed} of ${reports.length} ${skillWord} failed verification (${mismatched} digest/size mismatch across ${files} ${fileWord}).` : incomplete !== 0 ? `Checked ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors in what was read.` : unverifiable !== 0 ? `Checked ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors.` : `Verified ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors.`;
17969
+ return `${headline}${incompleteClause}${unverifiableClause}`;
17970
+ }
16762
17971
 
16763
17972
  // src/handlers/run-method.ts
16764
17973
  function assertSkillsSupported(inspectorClient, method) {
@@ -17009,13 +18218,10 @@ async function runMethod(inspectorClient, args) {
17009
18218
  kind: "ndjson",
17010
18219
  lines: reports,
17011
18220
  summary: summarizeSkillVerification(reports),
17012
- // Three outcomes, three exit codes: a broken MUST is 7, a walk the
17013
- // read bounds cut short is 8, and everything checked and passing is
17014
- // 0. Collapsing the middle case into either of the others reports
17015
- // something untrue about the server (Copilot).
17016
- ...allSkillsVerified(reports) ? {} : {
17017
- exitCode: anySkillFailed(reports) ? EXIT_CODES.SKILL_NONCONFORMANT : EXIT_CODES.SKILL_INCOMPLETE
17018
- }
18221
+ exitCode: skillVerificationExitCode(
18222
+ reports,
18223
+ args.requireDigests === true
18224
+ )
17019
18225
  };
17020
18226
  }
17021
18227
  result = { skills };
@@ -17041,13 +18247,10 @@ async function runMethod(inspectorClient, args) {
17041
18247
  kind: "ndjson",
17042
18248
  lines: reports,
17043
18249
  summary: summarizeSkillVerification(reports),
17044
- // Three outcomes, three exit codes: a broken MUST is 7, a walk the
17045
- // read bounds cut short is 8, and everything checked and passing is
17046
- // 0. Collapsing the middle case into either of the others reports
17047
- // something untrue about the server (Copilot).
17048
- ...allSkillsVerified(reports) ? {} : {
17049
- exitCode: anySkillFailed(reports) ? EXIT_CODES.SKILL_NONCONFORMANT : EXIT_CODES.SKILL_INCOMPLETE
17050
- }
18250
+ exitCode: skillVerificationExitCode(
18251
+ reports,
18252
+ args.requireDigests === true
18253
+ )
17051
18254
  };
17052
18255
  }
17053
18256
  result = envelope;
@@ -17129,71 +18332,6 @@ import {
17129
18332
  discoverAuthorizationServerMetadata as discoverAuthorizationServerMetadata4
17130
18333
  } from "@modelcontextprotocol/client";
17131
18334
 
17132
- // ../../core/auth/issuerBinding.ts
17133
- import { AuthorizationServerMismatchError } from "@modelcontextprotocol/client";
17134
- var MISSING_DISCOVERY_STATE_PHRASE = "discoveryState was not available";
17135
- function isAuthorizationServerMismatchShape(err) {
17136
- if (err === null || typeof err !== "object") {
17137
- return false;
17138
- }
17139
- const candidate = err;
17140
- if (typeof candidate.recordedIssuer !== "string" || typeof candidate.currentIssuer !== "string") {
17141
- return false;
17142
- }
17143
- return AuthorizationServerMismatchError.isInstance(err) || candidate.name === "AuthorizationServerMismatchError";
17144
- }
17145
- function isMissingDiscoveryStatePlaceholder(recordedIssuer) {
17146
- if (recordedIssuer.includes(MISSING_DISCOVERY_STATE_PHRASE)) {
17147
- return true;
17148
- }
17149
- try {
17150
- const url = new URL(recordedIssuer);
17151
- return url.protocol !== "http:" && url.protocol !== "https:";
17152
- } catch {
17153
- return true;
17154
- }
17155
- }
17156
- function findIssuerBindingFailure(err) {
17157
- return findIssuerBindingFailureDeep(err, /* @__PURE__ */ new Set());
17158
- }
17159
- function findIssuerBindingFailureDeep(err, seen) {
17160
- if (err === null || typeof err !== "object" || seen.has(err)) {
17161
- return void 0;
17162
- }
17163
- seen.add(err);
17164
- if (isAuthorizationServerMismatchShape(err)) {
17165
- return classifyAuthorizationServerMismatch(err);
17166
- }
17167
- const nested = findIssuerBindingFailureDeep(
17168
- err.cause,
17169
- seen
17170
- );
17171
- if (nested) {
17172
- return nested;
17173
- }
17174
- const data = err.data;
17175
- if (data !== null && typeof data === "object") {
17176
- return findIssuerBindingFailureDeep(
17177
- data.cause,
17178
- seen
17179
- );
17180
- }
17181
- return void 0;
17182
- }
17183
- function classifyAuthorizationServerMismatch(err) {
17184
- if (isMissingDiscoveryStatePlaceholder(err.recordedIssuer)) {
17185
- return {
17186
- kind: "lost_authorization_state",
17187
- currentIssuer: err.currentIssuer
17188
- };
17189
- }
17190
- return {
17191
- kind: "issuer_mismatch",
17192
- recordedIssuer: err.recordedIssuer,
17193
- currentIssuer: err.currentIssuer
17194
- };
17195
- }
17196
-
17197
18335
  // ../../core/auth/node/oauth-callback-server.ts
17198
18336
  import { createServer } from "http";
17199
18337
  var DEFAULT_HOSTNAME = "127.0.0.1";
@@ -17489,8 +18627,28 @@ async function runRunnerInteractiveOAuth(options) {
17489
18627
 
17490
18628
  // src/open-url.ts
17491
18629
  import open from "open";
17492
- async function openUrl(url) {
17493
- await open(typeof url === "string" ? url : url.href);
18630
+ var OPEN_URL_TIMEOUT_MS = 5e3;
18631
+ function launched(child) {
18632
+ return new Promise((resolve7, reject) => {
18633
+ child.on("error", reject);
18634
+ child.once("spawn", () => resolve7());
18635
+ });
18636
+ }
18637
+ async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
18638
+ const href = typeof url === "string" ? url : url.href;
18639
+ let timer;
18640
+ const timeout = new Promise((_, reject) => {
18641
+ timer = setTimeout(
18642
+ () => reject(new Error(`browser did not open within ${timeoutMs / 1e3}s`)),
18643
+ timeoutMs
18644
+ );
18645
+ });
18646
+ try {
18647
+ const opened = Promise.resolve().then(() => open(href)).then(launched);
18648
+ await Promise.race([opened, timeout]);
18649
+ } finally {
18650
+ clearTimeout(timer);
18651
+ }
17494
18652
  }
17495
18653
 
17496
18654
  // src/style.ts
@@ -17556,7 +18714,12 @@ function createCliOAuthNavigation(options = {}) {
17556
18714
  if (!forced && !tty) return;
17557
18715
  try {
17558
18716
  await (options.openBrowser ?? openUrl)(href);
17559
- } catch {
18717
+ } catch (error) {
18718
+ const reason = error instanceof Error ? error.message : String(error);
18719
+ write(
18720
+ `Could not open a browser automatically (${reason}). Open the URL above manually.
18721
+ `
18722
+ );
17560
18723
  }
17561
18724
  });
17562
18725
  }
@@ -17699,6 +18862,23 @@ function mergeSecretsIntoClientConfig(config, secrets) {
17699
18862
  }
17700
18863
 
17701
18864
  // ../../core/client/node-persistence.ts
18865
+ async function withClientConfigLock(filePath, action, body) {
18866
+ let entered = false;
18867
+ try {
18868
+ return await withSecretFileLock(filePath, async () => {
18869
+ entered = true;
18870
+ return body();
18871
+ });
18872
+ } catch (error) {
18873
+ if (!entered && error instanceof SecretFileLockHeldError) {
18874
+ throw new SecretFileLockHeldError(
18875
+ `Could not ${action} the client configuration: the file at ${filePath} is locked by another Inspector process and did not become available.`,
18876
+ { cause: error }
18877
+ );
18878
+ }
18879
+ throw error;
18880
+ }
18881
+ }
17702
18882
  async function readIdpSecretFromKeychain(secretStore) {
17703
18883
  const secret = await secretStore.get(
17704
18884
  CLIENT_KEYCHAIN_ID,
@@ -17708,29 +18888,39 @@ async function readIdpSecretFromKeychain(secretStore) {
17708
18888
  return { [SECRET_FIELD_IDP_CLIENT_SECRET]: secret };
17709
18889
  }
17710
18890
  async function migrateClientPlaintextSecret(filePath, config, secretStore) {
17711
- const { stripped, secrets } = extractSecretsFromClientConfig(config);
17712
- const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
17713
- if (!value) return config;
17714
18891
  try {
17715
- const existing = await secretStoreGetStrict(
17716
- secretStore,
17717
- CLIENT_KEYCHAIN_ID,
17718
- SECRET_FIELD_IDP_CLIENT_SECRET
17719
- );
17720
- if (existing === null) {
17721
- await secretStore.set(
17722
- CLIENT_KEYCHAIN_ID,
17723
- SECRET_FIELD_IDP_CLIENT_SECRET,
17724
- value
17725
- );
17726
- }
17727
- if (!await secretStoreIsDurable(secretStore)) return config;
17728
- await writeStoreFile(filePath, serializeStore(stripped));
17729
- return stripped;
18892
+ return await withClientConfigLock(filePath, "migrate", async () => {
18893
+ const raw = await readStoreFile(filePath);
18894
+ if (raw === null) return {};
18895
+ const fresh = parseClientConfig(parseStore(raw));
18896
+ const { stripped, secrets } = extractSecretsFromClientConfig(fresh);
18897
+ const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
18898
+ if (!value) return fresh;
18899
+ try {
18900
+ const existing = await secretStoreGetStrict(
18901
+ secretStore,
18902
+ CLIENT_KEYCHAIN_ID,
18903
+ SECRET_FIELD_IDP_CLIENT_SECRET
18904
+ );
18905
+ if (existing === null) {
18906
+ await secretStore.set(
18907
+ CLIENT_KEYCHAIN_ID,
18908
+ SECRET_FIELD_IDP_CLIENT_SECRET,
18909
+ value
18910
+ );
18911
+ }
18912
+ if (!await secretStoreIsDurable(secretStore)) return fresh;
18913
+ await writeStoreFile(filePath, serializeStore(stripped));
18914
+ return stripped;
18915
+ } catch (err) {
18916
+ if (err instanceof SecretStoreUnavailableError) {
18917
+ return fresh;
18918
+ }
18919
+ throw err;
18920
+ }
18921
+ });
17730
18922
  } catch (err) {
17731
- if (err instanceof SecretStoreUnavailableError) {
17732
- return config;
17733
- }
18923
+ if (err instanceof SecretFileLockHeldError) return config;
17734
18924
  throw err;
17735
18925
  }
17736
18926
  }
@@ -18193,6 +19383,13 @@ async function callMethod(serverConfig, serverSettings, args, clientConfig, cliA
18193
19383
  ...serverSettings?.protocolEra && {
18194
19384
  versionNegotiation: eraToVersionNegotiation(serverSettings.protocolEra)
18195
19385
  },
19386
+ // The CLI cannot render an MCP App, so it does not advertise the UI
19387
+ // extension by default (#2403). `--advertise-apps` claims it explicitly,
19388
+ // for a server that only exposes its App tools to a client that does —
19389
+ // which is what an `--app-info` probe against such a server needs.
19390
+ ...args.advertiseApps && {
19391
+ advertisedExtensions: { [UI_EXTENSION_KEY]: true }
19392
+ },
18196
19393
  ...clientAuthOptions
18197
19394
  });
18198
19395
  try {
@@ -18226,14 +19423,8 @@ function normalizeServerUrl2(serverUrl) {
18226
19423
  }
18227
19424
  }
18228
19425
  async function readOAuthSnapshot(statePath) {
18229
- const { readFile: readFile2 } = await import("fs/promises");
18230
- try {
18231
- const text = await readFile2(statePath, "utf8");
18232
- const snapshot = parseOAuthPersistBlob(text);
18233
- if (snapshot) return snapshot;
18234
- } catch {
18235
- }
18236
- return { servers: {}, idpSessions: {} };
19426
+ const snapshot = await readOAuthStore(statePath);
19427
+ return snapshot ?? { servers: {}, idpSessions: {} };
18237
19428
  }
18238
19429
  async function readOAuthServers(statePath) {
18239
19430
  return (await readOAuthSnapshot(statePath)).servers;
@@ -18308,17 +19499,44 @@ async function refreshStoredAuthToken(serverUrl, statePath, deps = {}) {
18308
19499
  );
18309
19500
  }
18310
19501
  servers[found.key] = { ...found.state, tokens };
18311
- await writeStoreFile(statePath, serializeOAuthPersistBlob(snapshot));
19502
+ await writeOAuthSections(statePath, snapshot, { servers: [found.key] });
18312
19503
  return tokens.access_token;
18313
19504
  }
18314
- async function waitForStoredToken(serverUrl, statePath, timeoutSec) {
19505
+ var DEADLINE_ELAPSED = /* @__PURE__ */ Symbol("deadline-elapsed");
19506
+ async function raceDeadline(promise, deadline) {
19507
+ promise.catch(() => {
19508
+ });
19509
+ const remaining = deadline - Date.now();
19510
+ if (remaining <= 0) return DEADLINE_ELAPSED;
19511
+ let timer;
19512
+ const elapsed = new Promise((resolve7) => {
19513
+ timer = setTimeout(() => resolve7(DEADLINE_ELAPSED), remaining);
19514
+ });
19515
+ try {
19516
+ return await Promise.race([promise, elapsed]);
19517
+ } finally {
19518
+ clearTimeout(timer);
19519
+ }
19520
+ }
19521
+ async function waitForStoredToken(serverUrl, statePath, timeoutSec, readServers = readOAuthServers) {
18315
19522
  const key = normalizeServerUrl2(serverUrl);
18316
19523
  const deadline = Date.now() + timeoutSec * 1e3;
19524
+ let servers = {};
19525
+ let lastError;
18317
19526
  for (; ; ) {
18318
- const servers = await readOAuthServers(statePath);
19527
+ try {
19528
+ const read = await raceDeadline(readServers(statePath), deadline);
19529
+ if (read !== DEADLINE_ELAPSED) {
19530
+ servers = read;
19531
+ lastError = void 0;
19532
+ }
19533
+ } catch (error) {
19534
+ if (!(error instanceof SecretFileLockHeldError)) lastError = error;
19535
+ }
18319
19536
  const token = findStoredToken(servers, serverUrl);
18320
19537
  if (token) return token;
18321
19538
  if (Date.now() >= deadline) {
19539
+ if (lastError !== void 0) throw lastError;
18322
19540
  const stored = Object.keys(servers);
18323
19541
  throw new CliExitCodeError(
18324
19542
  EXIT_CODES.AUTH_REQUIRED,
@@ -18326,7 +19544,9 @@ async function waitForStoredToken(serverUrl, statePath, timeoutSec) {
18326
19544
  { code: "auth_wait_timeout", url: serverUrl }
18327
19545
  );
18328
19546
  }
18329
- await new Promise((r) => setTimeout(r, 500));
19547
+ await new Promise(
19548
+ (r) => setTimeout(r, Math.min(500, deadline - Date.now()))
19549
+ );
18330
19550
  }
18331
19551
  }
18332
19552
  function deepLinkTransport(serverUrl, transport) {
@@ -18482,12 +19702,18 @@ async function parseArgs(argv) {
18482
19702
  ).option(
18483
19703
  "--app-info",
18484
19704
  "Probe the tool's MCP App UI metadata (resourceUri, csp, permissions, domain) and emit it as one JSON line; exit 2 when the tool has no app. Use with --method tools/call --tool-name <name> (the tool itself is not invoked) or --method tools/list (one NDJSON line per tool)."
19705
+ ).option(
19706
+ "--advertise-apps",
19707
+ "Advertise the MCP Apps UI extension (io.modelcontextprotocol/ui) at initialize. Off by default because the CLI cannot render an App; set it when a server only exposes its App tools to a client that claims App support, e.g. for an --app-info probe."
18485
19708
  ).option(
18486
19709
  "--strict",
18487
19710
  "Report tool-schema portability problems in full (path, issue, suggested fix) on stderr, and exit 6 if any is error-severity. Use with --method tools/list. Without it, a one-line count is printed instead."
18488
19711
  ).option(
18489
19712
  "--verify",
18490
19713
  "Run the SEP-2640 conformance and digest checks over the skills returned, emit one JSON report per skill on stdout, and exit 7 if any fails or 8 if any could not be fully checked within the read bounds. Use with --method skills/list or --method skills/get."
19714
+ ).option(
19715
+ "--require-digests",
19716
+ 'With --verify: exit 9 when a skill advertises no digests (resources: "dynamic"), instead of reporting it as unverifiable and exiting 0.'
18491
19717
  ).option(
18492
19718
  "--connect-timeout <ms>",
18493
19719
  `Connection timeout in ms (default ${DEFAULT_CONNECT_TIMEOUT_MS} for ad-hoc --server-url / target invocations; 0 = no timeout).`,
@@ -18602,6 +19828,14 @@ async function parseArgs(argv) {
18602
19828
  );
18603
19829
  }
18604
19830
  }
19831
+ if (options.requireDigests && !options.verify) {
19832
+ throw new Error("--require-digests requires --verify.");
19833
+ }
19834
+ if (options.advertiseApps && (options.listStoredAuth || options.printHandoff || options.method === "servers/list" || options.method === "servers/show")) {
19835
+ throw new Error(
19836
+ "--advertise-apps requires a command that connects to a server; it has no effect with --list-stored-auth, --print-handoff, or --method servers/list / servers/show."
19837
+ );
19838
+ }
18605
19839
  const oauthStatePath = getStateFilePath();
18606
19840
  if (options.listStoredAuth) {
18607
19841
  const servers = await readOAuthServers(oauthStatePath);
@@ -18767,8 +20001,10 @@ async function parseArgs(argv) {
18767
20001
  metadata: options.metadata,
18768
20002
  toolMeta: options.toolMetadata,
18769
20003
  appInfo: options.appInfo === true,
20004
+ advertiseApps: options.advertiseApps === true,
18770
20005
  strict: options.strict === true,
18771
20006
  verify: options.verify === true,
20007
+ requireDigests: options.requireDigests === true,
18772
20008
  cursor: options.cursor,
18773
20009
  format: options.format
18774
20010
  };