@modelcontextprotocol/inspector 2.7.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
  );
@@ -1041,6 +1117,176 @@ function checkSkillNameCollisions(entries) {
1041
1117
  return issues;
1042
1118
  }
1043
1119
 
1120
+ // ../../core/json/localRefs.ts
1121
+ var SUBSCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
1122
+ "items",
1123
+ "contains",
1124
+ "not",
1125
+ "propertyNames",
1126
+ "if",
1127
+ "then",
1128
+ "else",
1129
+ "additionalProperties",
1130
+ "unevaluatedProperties",
1131
+ "additionalItems",
1132
+ "unevaluatedItems",
1133
+ "contentSchema"
1134
+ ]);
1135
+ var SUBSCHEMA_ARRAY_KEYWORDS = /* @__PURE__ */ new Set([
1136
+ "allOf",
1137
+ "anyOf",
1138
+ "oneOf",
1139
+ "prefixItems"
1140
+ ]);
1141
+ var SUBSCHEMA_MAP_KEYWORDS = /* @__PURE__ */ new Set([
1142
+ "properties",
1143
+ "patternProperties",
1144
+ "dependentSchemas",
1145
+ // Pre-2019 spelling of `dependentSchemas`; its array values are property
1146
+ // name lists, which are not schemas and pass through untouched.
1147
+ "dependencies",
1148
+ "$defs",
1149
+ "definitions"
1150
+ ]);
1151
+ var NON_CONSTRAINT_SIBLINGS = /* @__PURE__ */ new Set([
1152
+ "title",
1153
+ "description",
1154
+ "default",
1155
+ "examples",
1156
+ "deprecated",
1157
+ "readOnly",
1158
+ "writeOnly",
1159
+ // Non-standard labels for `enum` values, read by both form builders.
1160
+ "enumNames",
1161
+ "$comment",
1162
+ "$schema",
1163
+ "$id",
1164
+ "$defs",
1165
+ "definitions"
1166
+ ]);
1167
+ var EXPANSION_BUDGET = 1e4;
1168
+ var MAX_DEPTH = 64;
1169
+ var Bail = class extends Error {
1170
+ };
1171
+ function spend(traversal) {
1172
+ traversal.remaining -= 1;
1173
+ if (traversal.remaining < 0) throw new Bail();
1174
+ }
1175
+ function mapSubschemas(node, visit) {
1176
+ const result = {};
1177
+ for (const [key, value] of Object.entries(node)) {
1178
+ let next = value;
1179
+ if (SUBSCHEMA_KEYWORDS.has(key)) {
1180
+ next = Array.isArray(value) ? value.map(visit) : visit(value);
1181
+ } else if (SUBSCHEMA_ARRAY_KEYWORDS.has(key) && Array.isArray(value)) {
1182
+ next = value.map(visit);
1183
+ } else if (SUBSCHEMA_MAP_KEYWORDS.has(key) && isRecord(value)) {
1184
+ next = mapNames(value, visit);
1185
+ }
1186
+ define(result, key, next);
1187
+ }
1188
+ return result;
1189
+ }
1190
+ function mapNames(map, visit) {
1191
+ const result = {};
1192
+ for (const [name, value] of Object.entries(map)) {
1193
+ define(result, name, Array.isArray(value) ? value : visit(value));
1194
+ }
1195
+ return result;
1196
+ }
1197
+ function define(target, key, value) {
1198
+ Object.defineProperty(target, key, {
1199
+ value,
1200
+ writable: true,
1201
+ enumerable: true,
1202
+ configurable: true
1203
+ });
1204
+ }
1205
+ function isRecord(value) {
1206
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1207
+ }
1208
+ function resolvePointer(root, ref) {
1209
+ if (!ref.startsWith("#")) return void 0;
1210
+ let pointer;
1211
+ try {
1212
+ pointer = decodeURIComponent(ref.slice(1));
1213
+ } catch {
1214
+ return void 0;
1215
+ }
1216
+ if (pointer === "") return root;
1217
+ if (!pointer.startsWith("/")) return void 0;
1218
+ let current = root;
1219
+ for (const token of pointer.slice(1).split("/")) {
1220
+ if (/~(?![01])/.test(token)) return void 0;
1221
+ const segment = token.replace(/~1/g, "/").replace(/~0/g, "~");
1222
+ if (Array.isArray(current)) {
1223
+ const index = Number(segment);
1224
+ if (!/^(0|[1-9]\d*)$/.test(segment) || index >= current.length) {
1225
+ return void 0;
1226
+ }
1227
+ current = current[index];
1228
+ } else if (isRecord(current) && Object.hasOwn(current, segment)) {
1229
+ current = current[segment];
1230
+ } else {
1231
+ return void 0;
1232
+ }
1233
+ }
1234
+ return current;
1235
+ }
1236
+ function containsRef(node, depth) {
1237
+ if (depth > MAX_DEPTH) throw new Bail();
1238
+ if (!isRecord(node)) return false;
1239
+ if (typeof node.$ref === "string") return true;
1240
+ let found = false;
1241
+ mapSubschemas(node, (child) => {
1242
+ found ||= containsRef(child, depth + 1);
1243
+ return child;
1244
+ });
1245
+ return found;
1246
+ }
1247
+ function inline(node, traversal, depth) {
1248
+ if (depth > MAX_DEPTH) throw new Bail();
1249
+ if (!isRecord(node)) return node;
1250
+ if (node !== traversal.root && typeof node.$id === "string") return node;
1251
+ spend(traversal);
1252
+ const visit = (child) => inline(child, traversal, depth + 1);
1253
+ const ref = node.$ref;
1254
+ const siblingsConstrain = Object.keys(node).some(
1255
+ (key) => key !== "$ref" && !NON_CONSTRAINT_SIBLINGS.has(key)
1256
+ );
1257
+ if (typeof ref === "string" && !siblingsConstrain && !traversal.active.has(ref)) {
1258
+ const target = resolvePointer(traversal.root, ref);
1259
+ if (isRecord(target)) {
1260
+ traversal.active.add(ref);
1261
+ const resolved2 = visit(target);
1262
+ traversal.active.delete(ref);
1263
+ const siblings = { ...node };
1264
+ delete siblings.$ref;
1265
+ return { ...resolved2, ...mapSubschemas(siblings, visit) };
1266
+ }
1267
+ }
1268
+ return mapSubschemas(node, visit);
1269
+ }
1270
+ var cache = /* @__PURE__ */ new WeakMap();
1271
+ function inlineLocalRefs(schema) {
1272
+ if (!isRecord(schema)) return schema;
1273
+ const cached = cache.get(schema);
1274
+ if (cached !== void 0) return cached;
1275
+ let resolved2;
1276
+ try {
1277
+ resolved2 = containsRef(schema, 0) ? inline(
1278
+ schema,
1279
+ { root: schema, active: /* @__PURE__ */ new Set(), remaining: EXPANSION_BUDGET },
1280
+ 0
1281
+ ) : schema;
1282
+ } catch (error) {
1283
+ if (!(error instanceof Bail)) throw error;
1284
+ resolved2 = schema;
1285
+ }
1286
+ cache.set(schema, resolved2);
1287
+ return resolved2;
1288
+ }
1289
+
1044
1290
  // ../../core/json/nullableUnion.ts
1045
1291
  var RENDERABLE_TYPES = [
1046
1292
  "string",
@@ -1639,7 +1885,7 @@ function convertToolParameters(tool, params) {
1639
1885
  function convertParametersForSchema(inputSchema, params) {
1640
1886
  const result = {};
1641
1887
  const { base, branches } = resolveRootUnion(
1642
- inputSchema ?? {}
1888
+ inlineLocalRefs(inputSchema ?? {})
1643
1889
  );
1644
1890
  const properties = coercionProperties(base, branches, params);
1645
1891
  for (const [key, value] of Object.entries(params)) {
@@ -1831,7 +2077,7 @@ function oauthEndpointOverridesFromSettings(settings) {
1831
2077
  };
1832
2078
  }
1833
2079
  function storedFieldsToInspectorSettings(stored) {
1834
- const hasAny = stored.headers !== void 0 || stored.metadata !== void 0 || stored.connectionTimeout !== void 0 || stored.requestTimeout !== void 0 || stored.taskTtl !== void 0 || stored.autoRefreshOnListChanged !== void 0 || stored.paginatedLists !== void 0 || stored.advertisedExtensions !== void 0 || stored.maxFetchRequests !== void 0 || stored.skillCatalogMaxSkills !== void 0 || stored.skillCatalogMaxBytes !== void 0 || stored.oauth !== void 0 || stored.roots !== void 0 || stored.protocolEra !== void 0 || stored.modernLogLevel !== void 0 || stored.env !== void 0 || stored.cwd !== void 0;
2080
+ const hasAny = stored.headers !== void 0 || stored.metadata !== void 0 || stored.connectionTimeout !== void 0 || stored.requestTimeout !== void 0 || stored.taskTtl !== void 0 || stored.autoRefreshOnListChanged !== void 0 || stored.paginatedLists !== void 0 || stored.suppressNotificationStream !== void 0 || stored.advertisedExtensions !== void 0 || stored.maxFetchRequests !== void 0 || stored.skillCatalogMaxSkills !== void 0 || stored.skillCatalogMaxBytes !== void 0 || stored.oauth !== void 0 || stored.roots !== void 0 || stored.protocolEra !== void 0 || stored.modernLogLevel !== void 0 || stored.env !== void 0 || stored.cwd !== void 0;
1835
2081
  if (!hasAny) return void 0;
1836
2082
  const headersPairs = stored.headers ? Object.entries(stored.headers).map(([key, value]) => ({ key, value })) : [];
1837
2083
  const settings = {
@@ -1863,6 +2109,9 @@ function storedFieldsToInspectorSettings(stored) {
1863
2109
  if (isSkillCatalogLimit(stored.skillCatalogMaxBytes)) {
1864
2110
  settings.skillCatalogMaxBytes = stored.skillCatalogMaxBytes;
1865
2111
  }
2112
+ if (stored.suppressNotificationStream === true) {
2113
+ settings.suppressNotificationStream = true;
2114
+ }
1866
2115
  if (isProtocolEra(stored.protocolEra)) {
1867
2116
  settings.protocolEra = stored.protocolEra;
1868
2117
  }
@@ -1914,6 +2163,7 @@ var INSPECTOR_FIELD_KEY_MAP = {
1914
2163
  taskTtl: true,
1915
2164
  autoRefreshOnListChanged: true,
1916
2165
  paginatedLists: true,
2166
+ suppressNotificationStream: true,
1917
2167
  advertisedExtensions: true,
1918
2168
  maxFetchRequests: true,
1919
2169
  skillCatalogMaxSkills: true,
@@ -2268,7 +2518,7 @@ function secretStorageCaveat(info) {
2268
2518
  return `The secrets file could not be read (${info.encryptionUnknown}). Saving a secret will fail rather than overwrite it.`;
2269
2519
  }
2270
2520
  if (info.plaintext) {
2271
- return info.pendingEncryption ? "Existing secrets in this file are still unencrypted (file mode 0600). They are re-encrypted the next time a secret is saved." : "Secrets are stored unencrypted (file mode 0600). Set MCP_INSPECTOR_SECRET_KEY to encrypt them.";
2521
+ return info.pendingEncryption ? "Existing secrets in this file are still unencrypted (file mode 0600). They are re-encrypted the next time a secret is saved." : "Secrets are stored unencrypted (file mode 0600). Set MCP_INSPECTOR_SECRET_KEY or MCP_INSPECTOR_SECRET_KEY_FILE to encrypt them.";
2272
2522
  }
2273
2523
  return void 0;
2274
2524
  }
@@ -2283,6 +2533,7 @@ function secretStorageSummary(info) {
2283
2533
 
2284
2534
  // ../../core/auth/node/file-secret-store.ts
2285
2535
  import * as crypto2 from "crypto";
2536
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
2286
2537
  import * as fs3 from "fs/promises";
2287
2538
  import * as path3 from "path";
2288
2539
 
@@ -2426,7 +2677,7 @@ async function openSecretFileLock(filePath) {
2426
2677
  });
2427
2678
  } catch (err) {
2428
2679
  if (isStuckOrHeld(err, lockPathOf(target))) {
2429
- throw new SecretStoreUnavailableError(
2680
+ throw new SecretFileLockHeldError(
2430
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.`
2431
2682
  );
2432
2683
  }
@@ -2485,7 +2736,63 @@ var SCRYPT_N = 16384;
2485
2736
  var SCRYPT_R = 8;
2486
2737
  var SCRYPT_P = 1;
2487
2738
  var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
2739
+ var SECRET_KEY_FILE_ENV = "MCP_INSPECTOR_SECRET_KEY_FILE";
2740
+ function isSameFile(a, b) {
2741
+ if (path3.resolve(a) === path3.resolve(b)) return true;
2742
+ try {
2743
+ const sa = statSync2(a);
2744
+ const sb = statSync2(b);
2745
+ return sa.dev === sb.dev && sa.ino === sb.ino;
2746
+ } catch {
2747
+ return false;
2748
+ }
2749
+ }
2750
+ function resolveSecretPassphrase(env = process.env, secretFilePath) {
2751
+ const direct = env[SECRET_KEY_ENV];
2752
+ const hasDirect = direct !== void 0 && direct.trim() !== "";
2753
+ const rawKeyFile = env[SECRET_KEY_FILE_ENV];
2754
+ const wantsKeyFile = rawKeyFile !== void 0;
2755
+ const keyFile = rawKeyFile?.trim();
2756
+ if (hasDirect && wantsKeyFile) {
2757
+ return {
2758
+ problem: `both ${SECRET_KEY_ENV} and ${SECRET_KEY_FILE_ENV} are set; set only one`
2759
+ };
2760
+ }
2761
+ if (hasDirect) return { passphrase: direct };
2762
+ if (!wantsKeyFile) return {};
2763
+ if (!keyFile) {
2764
+ return { problem: `${SECRET_KEY_FILE_ENV} is set but empty` };
2765
+ }
2766
+ const resolved2 = path3.resolve(keyFile);
2767
+ if (secretFilePath !== void 0 && isSameFile(resolved2, secretFilePath)) {
2768
+ return {
2769
+ problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is the secrets file itself; point it at a separate key file`
2770
+ };
2771
+ }
2772
+ let contents;
2773
+ try {
2774
+ contents = readFileSync2(resolved2, "utf-8");
2775
+ } catch (err) {
2776
+ return {
2777
+ // `String(err)` rather than `.message`: `readFileSync` only ever throws
2778
+ // an `Error`, so an `instanceof` guard would be a branch no test can take.
2779
+ problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) could not be read: ${String(err)}`
2780
+ };
2781
+ }
2782
+ const passphrase = contents.replace(/[\r\n]+$/, "");
2783
+ if (passphrase.trim() === "") {
2784
+ return { problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is empty` };
2785
+ }
2786
+ return { passphrase };
2787
+ }
2488
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
+ }
2489
2796
  function asSecretMap(value, filePath) {
2490
2797
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2491
2798
  throw new SecretStoreUnavailableError(
@@ -2504,7 +2811,7 @@ function asSecretMap(value, filePath) {
2504
2811
  var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
2505
2812
  constructor(filePath, hasKey) {
2506
2813
  super(
2507
- hasKey ? `The secrets file at ${filePath} could not be decrypted with the current ${SECRET_KEY_ENV}. Refusing to write, which would overwrite the existing secrets. Restore the original passphrase, or delete the file to start over.` : `The secrets file at ${filePath} is encrypted but ${SECRET_KEY_ENV} is not set. Refusing to write, which would overwrite the existing secrets. Set the passphrase this file was written with, or delete the file to start over.`
2814
+ hasKey ? `The secrets file at ${filePath} could not be decrypted with the current ${SECRET_KEY_ENV} (or ${SECRET_KEY_FILE_ENV}). Refusing to write, which would overwrite the existing secrets. Restore the original passphrase, or delete the file to start over.` : `The secrets file at ${filePath} is encrypted but ${SECRET_KEY_ENV} is not set (nor ${SECRET_KEY_FILE_ENV}). Refusing to write, which would overwrite the existing secrets. Set the passphrase this file was written with, or delete the file to start over.`
2508
2815
  );
2509
2816
  this.name = "SecretFileKeyMismatchError";
2510
2817
  }
@@ -2562,10 +2869,17 @@ function encryptedEnvelopeProblem(parsed) {
2562
2869
  var FileSecretStore = class {
2563
2870
  filePath;
2564
2871
  passphrase;
2872
+ /**
2873
+ * Why a configured key could not be obtained. While set, every read throws
2874
+ * and every write refuses — see {@link SecretPassphrase}.
2875
+ */
2876
+ keyProblem;
2565
2877
  constructor(options) {
2566
2878
  this.filePath = options.filePath;
2567
- const raw = options.passphrase ?? process.env[SECRET_KEY_ENV];
2879
+ const resolved2 = options.passphrase !== void 0 ? { passphrase: options.passphrase } : resolveSecretPassphrase(process.env, this.filePath);
2880
+ const raw = resolved2.passphrase;
2568
2881
  this.passphrase = raw && raw.trim() ? raw : void 0;
2882
+ this.keyProblem = resolved2.problem;
2569
2883
  }
2570
2884
  /**
2571
2885
  * True when the *next write* will encrypt. This is the configured policy,
@@ -2580,7 +2894,8 @@ var FileSecretStore = class {
2580
2894
  * no file yet, or when it cannot be read or parsed.
2581
2895
  *
2582
2896
  * Separate from {@link encrypted} because the two genuinely disagree for a
2583
- * whole session: adding `MCP_INSPECTOR_SECRET_KEY` to an install that
2897
+ * whole session: adding a passphrase (`MCP_INSPECTOR_SECRET_KEY` or
2898
+ * `MCP_INSPECTOR_SECRET_KEY_FILE`) to an install that
2584
2899
  * already has a plaintext file flips `encrypted` to true immediately,
2585
2900
  * while the bytes stay readable until the next `set`. A descriptor built
2586
2901
  * from the policy would tell that user "File (encrypted)" while their
@@ -2604,6 +2919,9 @@ var FileSecretStore = class {
2604
2919
  * `set` is in fact about to refuse.
2605
2920
  */
2606
2921
  async readOnDiskEncryption() {
2922
+ if (this.keyProblem !== void 0) {
2923
+ return { state: "unreadable", detail: this.keyProblem };
2924
+ }
2607
2925
  let raw;
2608
2926
  try {
2609
2927
  raw = await readStoreFile(this.filePath);
@@ -2641,7 +2959,7 @@ var FileSecretStore = class {
2641
2959
  } catch (err) {
2642
2960
  return {
2643
2961
  state: "unreadable",
2644
- detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV}` : err instanceof Error ? err.message : String(err)
2962
+ detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV} (or ${SECRET_KEY_FILE_ENV})` : err instanceof Error ? err.message : String(err)
2645
2963
  };
2646
2964
  }
2647
2965
  return { state: "encrypted" };
@@ -2692,6 +3010,11 @@ var FileSecretStore = class {
2692
3010
  return this.serialize(() => this.readMap());
2693
3011
  }
2694
3012
  async readMap() {
3013
+ if (this.keyProblem !== void 0) {
3014
+ throw new SecretStoreUnavailableError(
3015
+ `The secrets file at ${this.filePath} cannot be used: ${this.keyProblem}. Refusing to read or write it rather than store secrets unencrypted.`
3016
+ );
3017
+ }
2695
3018
  const raw = await readStoreFile(this.filePath);
2696
3019
  if (raw === null) return null;
2697
3020
  let parsed;
@@ -2728,7 +3051,9 @@ var FileSecretStore = class {
2728
3051
  let plain;
2729
3052
  try {
2730
3053
  const key = await this.deriveKey(parsed.kdf);
2731
- const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv);
3054
+ const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv, {
3055
+ authTagLength: GCM_TAG_BYTES
3056
+ });
2732
3057
  decipher.setAuthTag(parts.tag);
2733
3058
  plain = Buffer.concat([
2734
3059
  decipher.update(parts.body),
@@ -2762,7 +3087,9 @@ var FileSecretStore = class {
2762
3087
  };
2763
3088
  const key = await this.deriveKey(kdf);
2764
3089
  const iv = crypto2.randomBytes(IV_BYTES);
2765
- const cipher = crypto2.createCipheriv(CIPHER, key, iv);
3090
+ const cipher = crypto2.createCipheriv(CIPHER, key, iv, {
3091
+ authTagLength: GCM_TAG_BYTES
3092
+ });
2766
3093
  const body = Buffer.concat([
2767
3094
  cipher.update(JSON.stringify(map), "utf-8"),
2768
3095
  cipher.final()
@@ -2927,9 +3254,36 @@ var FileSecretStore = class {
2927
3254
  const found = {};
2928
3255
  for (const field of fields) {
2929
3256
  const value = map?.[buildAccount2(serverId, field)];
2930
- 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);
2931
3285
  }
2932
- out[serverId] = found;
3286
+ setOwnEntry(out, serverId, found);
2933
3287
  }
2934
3288
  return out;
2935
3289
  }
@@ -2973,6 +3327,9 @@ var FileSecretStore = class {
2973
3327
  */
2974
3328
  async setMany(serverId, values) {
2975
3329
  if (Object.keys(values).length === 0) return;
3330
+ for (const [field, value] of Object.entries(values)) {
3331
+ assertSecretString(field, value);
3332
+ }
2976
3333
  await this.serialize(
2977
3334
  () => this.mutate((map) => {
2978
3335
  const next = { ...map };
@@ -2984,6 +3341,7 @@ var FileSecretStore = class {
2984
3341
  );
2985
3342
  }
2986
3343
  async set(serverId, field, value) {
3344
+ assertSecretString(field, value);
2987
3345
  await this.serialize(
2988
3346
  () => this.mutate((map) => ({
2989
3347
  ...map,
@@ -2999,9 +3357,11 @@ var FileSecretStore = class {
2999
3357
  await this.deleteWhere((key) => key.startsWith(prefix));
3000
3358
  }
3001
3359
  /**
3002
- * Shared delete body. Silent on every failure, matching the keyring store:
3003
- * every reason a delete can fail collapses to "the entry isn't there
3004
- * 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.
3005
3365
  */
3006
3366
  async deleteWhere(match) {
3007
3367
  try {
@@ -3014,7 +3374,12 @@ var FileSecretStore = class {
3014
3374
  return next;
3015
3375
  })
3016
3376
  );
3017
- } 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
+ );
3018
3383
  }
3019
3384
  }
3020
3385
  };
@@ -3047,6 +3412,7 @@ async function readSecretFilePermissions(filePath) {
3047
3412
  var SECRET_STORE_ENV = "MCP_INSPECTOR_SECRET_STORE";
3048
3413
  var SECRET_FILE_ENV = "MCP_INSPECTOR_SECRET_FILE";
3049
3414
  var STORAGE_DIR_ENV = "MCP_STORAGE_DIR";
3415
+ var SECRET_STORAGE_DOCS_URL = "https://github.com/modelcontextprotocol/inspector/blob/main/docs/secret-storage.md";
3050
3416
  var KINDS = ["keyring", "file", "memory"];
3051
3417
  function parseSecretStoreEnv(raw) {
3052
3418
  const value = raw?.trim().toLowerCase();
@@ -3189,6 +3555,11 @@ function warnAboutSecretStorage(info) {
3189
3555
  }
3190
3556
  const caveat = secretStorageCaveat(info);
3191
3557
  if (caveat) console.warn(`[mcp-inspector] ${caveat}`);
3558
+ if (info.reason === "fallback" || caveat) {
3559
+ console.warn(
3560
+ `[mcp-inspector] How the secret store is chosen, and how to secure it: ${SECRET_STORAGE_DOCS_URL}`
3561
+ );
3562
+ }
3192
3563
  }
3193
3564
  var resolved;
3194
3565
  function resolveSecretStore() {
@@ -3384,6 +3755,9 @@ var DeferredSecretStore = class {
3384
3755
  async getMany(requests) {
3385
3756
  return secretStoreGetMany(await this.target(), requests);
3386
3757
  }
3758
+ async getManyStrict(requests) {
3759
+ return secretStoreGetManyStrict(await this.target(), requests);
3760
+ }
3387
3761
  async get(serverId, field) {
3388
3762
  return (await this.target()).get(serverId, field);
3389
3763
  }
@@ -3622,12 +3996,31 @@ function redactJsonValue(value) {
3622
3996
  }
3623
3997
  return value;
3624
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
+ }
3625
4018
  function redactBody(body, contentType) {
3626
4019
  if (!body) return body;
3627
4020
  const type = (contentType ?? "").toLowerCase();
3628
4021
  if (type.includes("application/x-www-form-urlencoded")) {
3629
4022
  try {
3630
- const params = new URLSearchParams(body);
4023
+ const params = new URLSearchParams(joinSensitiveContinuations(body));
3631
4024
  let changed = false;
3632
4025
  for (const key of new Set(params.keys())) {
3633
4026
  if (isSensitiveField(key)) {
@@ -4351,6 +4744,73 @@ function createProxyFetch() {
4351
4744
  };
4352
4745
  }
4353
4746
 
4747
+ // ../../core/mcp/node/notificationHeadersFetch.ts
4748
+ import { PROTOCOL_VERSION_META_KEY } from "@modelcontextprotocol/client";
4749
+ function createNotificationHeadersFetch(baseFetch) {
4750
+ return (input, init) => {
4751
+ const method = modernNotificationMethod(init);
4752
+ if (method === void 0) return baseFetch(input, init);
4753
+ const headers = new Headers(init?.headers);
4754
+ headers.set("mcp-protocol-version", method.version);
4755
+ headers.set("mcp-method", method.method);
4756
+ return baseFetch(input, { ...init, headers });
4757
+ };
4758
+ }
4759
+ function modernNotificationMethod(init) {
4760
+ if (init?.method?.toUpperCase() !== "POST") return void 0;
4761
+ if (typeof init.body !== "string") return void 0;
4762
+ let message;
4763
+ try {
4764
+ message = JSON.parse(init.body);
4765
+ } catch {
4766
+ return void 0;
4767
+ }
4768
+ if (!isRecord2(message) || "id" in message) return void 0;
4769
+ if (typeof message.method !== "string") return void 0;
4770
+ const meta = isRecord2(message.params) ? message.params._meta : void 0;
4771
+ const version = isRecord2(meta) ? meta[PROTOCOL_VERSION_META_KEY] : void 0;
4772
+ if (typeof version !== "string" || version < MODERN_PROTOCOL_VERSION) {
4773
+ return void 0;
4774
+ }
4775
+ return { method: message.method, version };
4776
+ }
4777
+ function isRecord2(value) {
4778
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4779
+ }
4780
+
4781
+ // ../../core/mcp/node/suppressNotificationStreamFetch.ts
4782
+ function createSuppressNotificationStreamFetch(baseFetch, endpoint) {
4783
+ return async (input, init) => {
4784
+ if (isStandaloneStreamRequest(input, init, endpoint)) {
4785
+ return new Response(null, {
4786
+ status: 405,
4787
+ statusText: "Method Not Allowed"
4788
+ });
4789
+ }
4790
+ return baseFetch(input, init);
4791
+ };
4792
+ }
4793
+ function requestUrl(input) {
4794
+ const raw = input instanceof Request ? input.url : input instanceof URL ? input.href : input;
4795
+ try {
4796
+ return new URL(raw);
4797
+ } catch {
4798
+ return void 0;
4799
+ }
4800
+ }
4801
+ function isStandaloneStreamRequest(input, init, endpoint) {
4802
+ const request = input instanceof Request ? input : void 0;
4803
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
4804
+ if (method !== "GET") return false;
4805
+ const url = requestUrl(input);
4806
+ if (!url || url.origin !== endpoint.origin || url.pathname !== endpoint.pathname || url.search !== endpoint.search) {
4807
+ return false;
4808
+ }
4809
+ const headers = new Headers(init?.headers ?? request?.headers);
4810
+ const accept = headers.get("accept")?.toLowerCase() ?? "";
4811
+ return accept.includes("text/event-stream") && !headers.has("last-event-id");
4812
+ }
4813
+
4354
4814
  // ../../core/mcp/node/transport.ts
4355
4815
  function headersFromSettings(settings) {
4356
4816
  if (!settings || settings.headers.length === 0) return void 0;
@@ -4438,10 +4898,14 @@ function createTransportNode(config, options = {}) {
4438
4898
  ...httpConfig.requestInit,
4439
4899
  ...headers && { headers }
4440
4900
  };
4901
+ const stampedFetch = createNotificationHeadersFetch(
4902
+ fetchWithOptionalAuthIntercept
4903
+ );
4904
+ const httpFetch = settings?.suppressNotificationStream === true ? createSuppressNotificationStreamFetch(stampedFetch, url) : stampedFetch;
4441
4905
  const transport = new StreamableHTTPClientTransport(url, {
4442
4906
  authProvider,
4443
4907
  requestInit,
4444
- fetch: fetchWithOptionalAuthIntercept,
4908
+ fetch: httpFetch,
4445
4909
  // SEP-2350: how the transport reacts to a `403 insufficient_scope`
4446
4910
  // challenge. Defaults to the SDK's `reauthorize` when unset.
4447
4911
  ...settings?.oauthOnInsufficientScope && {
@@ -4587,42 +5051,57 @@ import {
4587
5051
  function withIssuer(value, issuer) {
4588
5052
  return issuer === void 0 ? value : { ...value, issuer };
4589
5053
  }
4590
- var OAuthStorageBase = class {
5054
+ var OAuthStorageCoordination = class {
4591
5055
  loaded = false;
4592
5056
  loadPromise;
4593
5057
  /** Serializes persist writes so concurrent mutators cannot reorder POSTs. */
4594
5058
  persistQueue = Promise.resolve();
5059
+ };
5060
+ var OAuthStorageBase = class {
5061
+ coordination;
4595
5062
  memory;
4596
5063
  backend;
4597
- constructor(memory, backend) {
5064
+ constructor(memory, backend, coordination = new OAuthStorageCoordination()) {
4598
5065
  this.memory = memory;
4599
5066
  this.backend = backend;
5067
+ this.coordination = coordination;
4600
5068
  }
4601
5069
  load() {
4602
- if (!this.loadPromise) {
4603
- 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
+ });
4604
5076
  }
4605
- return this.loadPromise;
5077
+ return coordination.loadPromise;
4606
5078
  }
4607
5079
  async doLoad() {
4608
- if (this.loaded) {
5080
+ if (this.coordination.loaded) {
4609
5081
  return;
4610
5082
  }
4611
5083
  const snapshot = await this.backend.read();
4612
5084
  if (snapshot) {
4613
5085
  this.memory.replace(snapshot);
4614
5086
  }
4615
- this.loaded = true;
5087
+ this.coordination.loaded = true;
4616
5088
  }
4617
5089
  async ensureLoaded() {
4618
5090
  await this.load();
4619
5091
  }
4620
- async persist() {
4621
- const snapshot = this.memory.snapshot();
4622
- 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;
4623
5102
  const tracked = prior.catch(() => {
4624
- }).then(() => this.backend.write(snapshot));
4625
- this.persistQueue = tracked;
5103
+ }).then(() => this.backend.write(this.memory.snapshot(), sections));
5104
+ this.coordination.persistQueue = tracked;
4626
5105
  await tracked;
4627
5106
  }
4628
5107
  /**
@@ -4636,7 +5115,7 @@ var OAuthStorageBase = class {
4636
5115
  /** Read the issuer-bound slot for a read, if one exists. */
4637
5116
  issuerSlot(state, issuer) {
4638
5117
  const key = this.resolveReadIssuer(state, issuer);
4639
- return key ? state.byIssuer?.[key] : void 0;
5118
+ return key ? getOwnEntry(state.byIssuer, key) : void 0;
4640
5119
  }
4641
5120
  /**
4642
5121
  * Read-modify-write the `byIssuer[issuer]` slot, set `activeIssuer`, and clear
@@ -4647,7 +5126,9 @@ var OAuthStorageBase = class {
4647
5126
  const state = this.memory.getState().getServerState(serverUrl);
4648
5127
  const byIssuer = {
4649
5128
  ...state.byIssuer,
4650
- [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 }
4651
5132
  };
4652
5133
  this.memory.getState().setServerState(serverUrl, {
4653
5134
  byIssuer,
@@ -4659,7 +5140,7 @@ var OAuthStorageBase = class {
4659
5140
  mapIssuerSlots(state, fn) {
4660
5141
  const byIssuer = {};
4661
5142
  for (const [key, slot] of Object.entries(state.byIssuer ?? {})) {
4662
- byIssuer[key] = { ...slot, ...fn(slot) };
5143
+ setOwnEntry(byIssuer, key, { ...slot, ...fn(slot) });
4663
5144
  }
4664
5145
  return byIssuer;
4665
5146
  }
@@ -4705,7 +5186,7 @@ var OAuthStorageBase = class {
4705
5186
  clientRegistrationKind: options.registrationKind
4706
5187
  });
4707
5188
  }
4708
- await this.persist();
5189
+ await this.persist({ servers: [serverUrl] });
4709
5190
  }
4710
5191
  async savePreregisteredClientInformation(serverUrl, clientInformation) {
4711
5192
  await this.ensureLoaded();
@@ -4713,7 +5194,7 @@ var OAuthStorageBase = class {
4713
5194
  preregisteredClientInformation: clientInformation,
4714
5195
  clientRegistrationKind: "static"
4715
5196
  });
4716
- await this.persist();
5197
+ await this.persist({ servers: [serverUrl] });
4717
5198
  }
4718
5199
  async clearClientInformation(serverUrl, isPreregistered, issuer) {
4719
5200
  await this.ensureLoaded();
@@ -4721,7 +5202,7 @@ var OAuthStorageBase = class {
4721
5202
  this.memory.getState().setServerState(serverUrl, {
4722
5203
  preregisteredClientInformation: void 0
4723
5204
  });
4724
- await this.persist();
5205
+ await this.persist({ servers: [serverUrl] });
4725
5206
  return;
4726
5207
  }
4727
5208
  if (issuer !== void 0) {
@@ -4744,7 +5225,7 @@ var OAuthStorageBase = class {
4744
5225
  clientRegistrationKind: void 0
4745
5226
  });
4746
5227
  }
4747
- await this.persist();
5228
+ await this.persist({ servers: [serverUrl] });
4748
5229
  }
4749
5230
  async getTokens(serverUrl, issuer) {
4750
5231
  await this.ensureLoaded();
@@ -4755,9 +5236,12 @@ var OAuthStorageBase = class {
4755
5236
  if (!tokens) {
4756
5237
  return void 0;
4757
5238
  }
4758
- const parsed = await OAuthTokensSchema.parseAsync(tokens);
5239
+ const result = await OAuthTokensSchema.safeParseAsync(tokens);
5240
+ if (!result.success) {
5241
+ return void 0;
5242
+ }
4759
5243
  return withIssuer(
4760
- parsed,
5244
+ result.data,
4761
5245
  fromSlot ? this.resolveReadIssuer(state, issuer) : void 0
4762
5246
  );
4763
5247
  }
@@ -4781,7 +5265,7 @@ var OAuthStorageBase = class {
4781
5265
  ...options?.enterpriseManaged === true && { enterpriseManaged: true }
4782
5266
  });
4783
5267
  }
4784
- await this.persist();
5268
+ await this.persist({ servers: [serverUrl] });
4785
5269
  }
4786
5270
  async clearTokens(serverUrl, issuer) {
4787
5271
  await this.ensureLoaded();
@@ -4800,7 +5284,7 @@ var OAuthStorageBase = class {
4800
5284
  }));
4801
5285
  this.memory.getState().setServerState(serverUrl, { byIssuer, tokens: void 0 });
4802
5286
  }
4803
- await this.persist();
5287
+ await this.persist({ servers: [serverUrl] });
4804
5288
  }
4805
5289
  async getCodeVerifier(serverUrl) {
4806
5290
  await this.ensureLoaded();
@@ -4810,12 +5294,12 @@ var OAuthStorageBase = class {
4810
5294
  async saveCodeVerifier(serverUrl, codeVerifier) {
4811
5295
  await this.ensureLoaded();
4812
5296
  this.memory.getState().setServerState(serverUrl, { codeVerifier });
4813
- await this.persist();
5297
+ await this.persist({ servers: [serverUrl] });
4814
5298
  }
4815
5299
  async clearCodeVerifier(serverUrl) {
4816
5300
  await this.ensureLoaded();
4817
5301
  this.memory.getState().setServerState(serverUrl, { codeVerifier: void 0 });
4818
- await this.persist();
5302
+ await this.persist({ servers: [serverUrl] });
4819
5303
  }
4820
5304
  async getScope(serverUrl) {
4821
5305
  await this.ensureLoaded();
@@ -4825,12 +5309,12 @@ var OAuthStorageBase = class {
4825
5309
  async saveScope(serverUrl, scope) {
4826
5310
  await this.ensureLoaded();
4827
5311
  this.memory.getState().setServerState(serverUrl, { scope });
4828
- await this.persist();
5312
+ await this.persist({ servers: [serverUrl] });
4829
5313
  }
4830
5314
  async clearScope(serverUrl) {
4831
5315
  await this.ensureLoaded();
4832
5316
  this.memory.getState().setServerState(serverUrl, { scope: void 0 });
4833
- await this.persist();
5317
+ await this.persist({ servers: [serverUrl] });
4834
5318
  }
4835
5319
  async getServerMetadata(serverUrl) {
4836
5320
  await this.ensureLoaded();
@@ -4840,12 +5324,12 @@ var OAuthStorageBase = class {
4840
5324
  async saveServerMetadata(serverUrl, metadata) {
4841
5325
  await this.ensureLoaded();
4842
5326
  this.memory.getState().setServerState(serverUrl, { serverMetadata: metadata });
4843
- await this.persist();
5327
+ await this.persist({ servers: [serverUrl] });
4844
5328
  }
4845
5329
  async clearServerMetadata(serverUrl) {
4846
5330
  await this.ensureLoaded();
4847
5331
  this.memory.getState().setServerState(serverUrl, { serverMetadata: void 0 });
4848
- await this.persist();
5332
+ await this.persist({ servers: [serverUrl] });
4849
5333
  }
4850
5334
  async getDiscoveryState(serverUrl) {
4851
5335
  await this.ensureLoaded();
@@ -4854,12 +5338,12 @@ var OAuthStorageBase = class {
4854
5338
  async saveDiscoveryState(serverUrl, state) {
4855
5339
  await this.ensureLoaded();
4856
5340
  this.memory.getState().setServerState(serverUrl, { discoveryState: state });
4857
- await this.persist();
5341
+ await this.persist({ servers: [serverUrl] });
4858
5342
  }
4859
5343
  async clearDiscoveryState(serverUrl) {
4860
5344
  await this.ensureLoaded();
4861
5345
  this.memory.getState().setServerState(serverUrl, { discoveryState: void 0 });
4862
- await this.persist();
5346
+ await this.persist({ servers: [serverUrl] });
4863
5347
  }
4864
5348
  async takeRevocationSnapshot(serverUrl) {
4865
5349
  await this.ensureLoaded();
@@ -4877,13 +5361,13 @@ var OAuthStorageBase = class {
4877
5361
  serverMetadata: state.serverMetadata
4878
5362
  };
4879
5363
  this.memory.getState().clearServerState(serverUrl);
4880
- await this.persist();
5364
+ await this.persist({ servers: [serverUrl] });
4881
5365
  return snapshot;
4882
5366
  }
4883
5367
  async clear(serverUrl) {
4884
5368
  await this.ensureLoaded();
4885
5369
  this.memory.getState().clearServerState(serverUrl);
4886
- await this.persist();
5370
+ await this.persist({ servers: [serverUrl] });
4887
5371
  }
4888
5372
  async getIdpSession(issuer) {
4889
5373
  await this.ensureLoaded();
@@ -4896,17 +5380,18 @@ var OAuthStorageBase = class {
4896
5380
  async saveIdpSession(issuer, session) {
4897
5381
  await this.ensureLoaded();
4898
5382
  this.memory.getState().setIdpSession(issuer, session);
4899
- await this.persist();
5383
+ await this.persist({ idpSessions: [issuer] });
4900
5384
  }
4901
5385
  async clearIdpSession(issuer) {
4902
5386
  await this.ensureLoaded();
4903
5387
  this.memory.getState().clearIdpSession(issuer);
4904
- await this.persist();
5388
+ await this.persist({ idpSessions: [issuer] });
4905
5389
  }
4906
5390
  async clearEnterpriseManagedResourceServers() {
4907
5391
  await this.ensureLoaded();
5392
+ const servers = Object.entries(this.memory.getState().servers).filter(([, state]) => state.enterpriseManaged === true).map(([url]) => url);
4908
5393
  this.memory.getState().clearEnterpriseManagedResourceServers();
4909
- await this.persist();
5394
+ await this.persist({ servers });
4910
5395
  }
4911
5396
  };
4912
5397
 
@@ -4924,13 +5409,15 @@ var OAuthMemoryStore = class {
4924
5409
  servers: this.servers,
4925
5410
  idpSessions: this.idpSessions,
4926
5411
  getServerState: (serverUrl) => {
4927
- return this.servers[serverUrl] || {};
5412
+ return getOwnEntry(this.servers, serverUrl) || {};
4928
5413
  },
4929
5414
  setServerState: (serverUrl, updates) => {
4930
5415
  this.servers = {
4931
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.
4932
5419
  [serverUrl]: {
4933
- ...this.servers[serverUrl],
5420
+ ...getOwnEntry(this.servers, serverUrl),
4934
5421
  ...updates
4935
5422
  }
4936
5423
  };
@@ -4941,13 +5428,13 @@ var OAuthMemoryStore = class {
4941
5428
  this.servers = rest;
4942
5429
  },
4943
5430
  getIdpSession: (issuer) => {
4944
- return this.idpSessions[issuer] || {};
5431
+ return getOwnEntry(this.idpSessions, issuer) || {};
4945
5432
  },
4946
5433
  setIdpSession: (issuer, updates) => {
4947
5434
  this.idpSessions = {
4948
5435
  ...this.idpSessions,
4949
5436
  [issuer]: {
4950
- ...this.idpSessions[issuer],
5437
+ ...getOwnEntry(this.idpSessions, issuer),
4951
5438
  ...updates
4952
5439
  }
4953
5440
  };
@@ -4981,24 +5468,81 @@ var OAuthMemoryStore = class {
4981
5468
  };
4982
5469
 
4983
5470
  // ../../core/auth/oauth-persist.ts
4984
- function isRecord(value) {
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
+ }
5495
+ function isRecord3(value) {
4985
5496
  return typeof value === "object" && value !== null && !Array.isArray(value);
4986
5497
  }
4987
- function snapshotFromPayload(payload) {
4988
- return {
4989
- servers: payload.servers ?? {},
4990
- idpSessions: payload.idpSessions ?? {}
4991
- };
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
+ }
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;
5532
+ return {
5533
+ servers,
5534
+ idpSessions
5535
+ };
4992
5536
  }
4993
5537
  function parseOAuthPersistBlob(raw) {
4994
5538
  if (raw === null || raw === void 0) {
4995
5539
  return null;
4996
5540
  }
4997
5541
  const parsed = typeof raw === "string" ? raw ? parseStore(raw) : null : raw;
4998
- if (!isRecord(parsed)) {
5542
+ if (!isRecord3(parsed)) {
4999
5543
  return null;
5000
5544
  }
5001
- if (isRecord(parsed.state) && "version" in parsed) {
5545
+ if (isRecord3(parsed.state) && "version" in parsed) {
5002
5546
  return snapshotFromPayload(parsed.state);
5003
5547
  }
5004
5548
  if ("servers" in parsed || "idpSessions" in parsed) {
@@ -5010,21 +5554,774 @@ function serializeOAuthPersistBlob(snapshot) {
5010
5554
  return serializeStore(snapshot);
5011
5555
  }
5012
5556
 
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
+ }
5626
+ return {
5627
+ ...info,
5628
+ ...clientSecret !== void 0 && { client_secret: clientSecret },
5629
+ ...registrationToken !== void 0 && {
5630
+ registration_access_token: registrationToken
5631
+ }
5632
+ };
5633
+ }
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;
5671
+ }
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;
5679
+ }
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 };
5695
+ }
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 {
5703
+ }
5704
+ return void 0;
5705
+ }
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;
5720
+ }
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
5762
+ );
5763
+ }
5764
+ if (joined.preregisteredClientInformation) {
5765
+ joined.preregisteredClientInformation = joinClientInformation(
5766
+ joined.preregisteredClientInformation,
5767
+ PREREG_CLIENT_INFO_FIELDS,
5768
+ secrets
5769
+ );
5770
+ }
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
+ }
5784
+ }
5785
+ return { residue, secrets };
5786
+ }
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 };
5792
+ }
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));
5805
+ }
5806
+ return [...fields];
5807
+ }
5808
+ function clientInfoHasPlaintext(info) {
5809
+ return info !== void 0 && (info.client_secret !== void 0 || info.registration_access_token !== void 0);
5810
+ }
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
+
5013
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
+ }
5014
6309
  function createFileOAuthPersistBackend(options) {
6310
+ const secretStore = options.secretStore ?? defaultSecretStore();
5015
6311
  return {
5016
6312
  async read() {
5017
- const raw = await readStoreFile(options.filePath);
5018
- return parseOAuthPersistBlob(raw);
6313
+ return readOAuthStore(options.filePath, secretStore);
5019
6314
  },
5020
- async write(snapshot) {
5021
- await writeStoreFile(
6315
+ async write(snapshot, sections) {
6316
+ await writeOAuthSections(
5022
6317
  options.filePath,
5023
- serializeOAuthPersistBlob(snapshot)
6318
+ snapshot,
6319
+ sections,
6320
+ secretStore
5024
6321
  );
5025
6322
  },
5026
6323
  async remove() {
5027
- await deleteStoreFile(options.filePath);
6324
+ await removeOAuthStore(options.filePath, secretStore);
5028
6325
  }
5029
6326
  };
5030
6327
  }
@@ -5041,31 +6338,39 @@ function getStateFilePath(customPath) {
5041
6338
  }
5042
6339
  return DEFAULT_STATE_PATH;
5043
6340
  }
5044
- var memoryCache = /* @__PURE__ */ new Map();
6341
+ var sharedStateCache = /* @__PURE__ */ new Map();
5045
6342
  var storageCache = /* @__PURE__ */ new Map();
5046
- function getSharedMemory(stateFilePath) {
6343
+ function getSharedState(stateFilePath) {
5047
6344
  const key = getStateFilePath(stateFilePath);
5048
- let memory = memoryCache.get(key);
5049
- if (!memory) {
5050
- memory = new OAuthMemoryStore();
5051
- memoryCache.set(key, memory);
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);
5052
6352
  }
5053
- return memory;
6353
+ return shared;
5054
6354
  }
5055
6355
  function resetNodeOAuthStorageCache(stateFilePath) {
5056
6356
  const key = getStateFilePath(stateFilePath);
5057
- memoryCache.delete(key);
6357
+ sharedStateCache.delete(key);
5058
6358
  storageCache.delete(key);
5059
6359
  }
5060
6360
  var NodeOAuthStorage = class extends OAuthStorageBase {
5061
6361
  /**
5062
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.
5063
6366
  */
5064
- constructor(storagePath) {
6367
+ constructor(storagePath, secretStore) {
5065
6368
  const filePath = getStateFilePath(storagePath);
6369
+ const shared = getSharedState(storagePath);
5066
6370
  super(
5067
- getSharedMemory(storagePath),
5068
- createFileOAuthPersistBackend({ filePath })
6371
+ shared.memory,
6372
+ createFileOAuthPersistBackend({ filePath, secretStore }),
6373
+ shared.coordination
5069
6374
  );
5070
6375
  }
5071
6376
  };
@@ -5074,7 +6379,7 @@ var NodeOAuthStorage = class extends OAuthStorageBase {
5074
6379
  import { selectClientAuthMethod } from "@modelcontextprotocol/client";
5075
6380
  import {
5076
6381
  OAuthClientInformationSchema as OAuthClientInformationSchema2,
5077
- OAuthTokensSchema as OAuthTokensSchema2
6382
+ OAuthTokensSchema as OAuthTokensSchema4
5078
6383
  } from "@modelcontextprotocol/core";
5079
6384
  var DEFAULT_REVOCATION_TIMEOUT_MS = 5e3;
5080
6385
  var MIN_REVOCATION_REQUEST_BUDGET_MS = 5;
@@ -5302,7 +6607,7 @@ async function collectGrants(snapshot) {
5302
6607
  return { grants, failures };
5303
6608
  }
5304
6609
  async function parseTokens(raw) {
5305
- 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);
5306
6611
  }
5307
6612
  async function parseClient(raw) {
5308
6613
  return raw === void 0 || raw === null ? void 0 : await OAuthClientInformationSchema2.parseAsync(raw);
@@ -5462,14 +6767,14 @@ var OBJECT_VALUED_SUBSCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
5462
6767
  function pathName(path5) {
5463
6768
  return path5.length === 0 ? "<root>" : path5.join(".");
5464
6769
  }
5465
- function isRecord2(node) {
6770
+ function isRecord4(node) {
5466
6771
  return node !== null && typeof node === "object";
5467
6772
  }
5468
6773
  function scanXMcpHeaderDeclarations(inputSchema) {
5469
6774
  const declarations = [];
5470
6775
  const seenLower = /* @__PURE__ */ new Map();
5471
6776
  const visit = (node, path5, reachable) => {
5472
- if (!isRecord2(node)) return void 0;
6777
+ if (!isRecord4(node)) return void 0;
5473
6778
  const schema = node;
5474
6779
  if (X_MCP_HEADER_KEY in schema) {
5475
6780
  if (!reachable || path5.length === 0) {
@@ -5495,7 +6800,7 @@ function scanXMcpHeaderDeclarations(inputSchema) {
5495
6800
  declarations.push({ path: path5, headerName: raw, type });
5496
6801
  }
5497
6802
  const properties = schema.properties;
5498
- if (isRecord2(properties)) {
6803
+ if (isRecord4(properties)) {
5499
6804
  for (const [key, child] of Object.entries(properties)) {
5500
6805
  const fault2 = visit(child, [...path5, key], reachable);
5501
6806
  if (fault2 !== void 0) return fault2;
@@ -5504,7 +6809,7 @@ function scanXMcpHeaderDeclarations(inputSchema) {
5504
6809
  for (const k of NON_REACHABLE_SUBSCHEMA_KEYWORDS) {
5505
6810
  const sub = schema[k];
5506
6811
  if (sub === void 0) continue;
5507
- const branches = Array.isArray(sub) ? sub : isRecord2(sub) && OBJECT_VALUED_SUBSCHEMA_KEYWORDS.has(k) ? Object.values(sub) : [sub];
6812
+ const branches = Array.isArray(sub) ? sub : isRecord4(sub) && OBJECT_VALUED_SUBSCHEMA_KEYWORDS.has(k) ? Object.values(sub) : [sub];
5508
6813
  for (const branch of branches) {
5509
6814
  const fault2 = visit(branch, [...path5, `<${k}>`], false);
5510
6815
  if (fault2 !== void 0) return fault2;
@@ -5736,7 +7041,7 @@ import {
5736
7041
  LOG_LEVEL_META_KEY,
5737
7042
  CLIENT_CAPABILITIES_META_KEY,
5738
7043
  CLIENT_INFO_META_KEY,
5739
- PROTOCOL_VERSION_META_KEY,
7044
+ PROTOCOL_VERSION_META_KEY as PROTOCOL_VERSION_META_KEY2,
5740
7045
  RELATED_TASK_META_KEY as RELATED_TASK_META_KEY3
5741
7046
  } from "@modelcontextprotocol/client";
5742
7047
 
@@ -5805,10 +7110,13 @@ var ADVERTISABLE_EXTENSIONS = [
5805
7110
  {
5806
7111
  key: UI_EXTENSION_KEY,
5807
7112
  label: "MCP Apps UI (io.modelcontextprotocol/ui)",
5808
- // The MCP Apps UI extension. The Inspector always renders App tools, so it
5809
- // advertises this by default with the App resource MIME type it supports —
5810
- // 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).
5811
7118
  defaultAdvertised: true,
7119
+ requiresAppRenderer: true,
5812
7120
  advertisement: { mimeTypes: [MCP_APP_MIME_TYPE] }
5813
7121
  },
5814
7122
  {
@@ -5824,10 +7132,13 @@ var ADVERTISABLE_EXTENSIONS = [
5824
7132
  defaultAdvertised: true
5825
7133
  }
5826
7134
  ];
7135
+ function isAdvertisedByDefault(ext, rendersApps) {
7136
+ return ext.defaultAdvertised && (!ext.requiresAppRenderer || rendersApps);
7137
+ }
5827
7138
  function buildClientExtensions(input) {
5828
7139
  const map = {};
5829
7140
  for (const ext of ADVERTISABLE_EXTENSIONS) {
5830
- const advertised = input.advertised?.[ext.key] ?? ext.defaultAdvertised;
7141
+ const advertised = input.advertised?.[ext.key] ?? isAdvertisedByDefault(ext, input.rendersApps === true);
5831
7142
  if (advertised) {
5832
7143
  map[ext.key] = ext.advertisement ? structuredClone(ext.advertisement) : {};
5833
7144
  }
@@ -6772,6 +8083,14 @@ function exemptMcpEndpoint(getServerUrl, negotiatesEndpoint) {
6772
8083
  };
6773
8084
  }
6774
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
+ }
6775
8094
  function requestHeadersOf(input, init) {
6776
8095
  if (init?.headers) return new Headers(init.headers);
6777
8096
  if (typeof input !== "string" && !(input instanceof URL)) {
@@ -6829,7 +8148,7 @@ function withOAuthRequestTimeout(fetchFn, timeoutMs = DEFAULT_OAUTH_REQUEST_TIME
6829
8148
  }
6830
8149
  });
6831
8150
  const nextInit = { ...init, signal };
6832
- REQUEST_DEADLINES.set(nextInit, budget);
8151
+ stampRequestDeadline(nextInit, budget);
6833
8152
  let settled;
6834
8153
  let drainReader;
6835
8154
  try {
@@ -6942,7 +8261,7 @@ function withRfc8414OidcCompat(fetchFn) {
6942
8261
  return response;
6943
8262
  }
6944
8263
  if (!probe.ok) {
6945
- await releaseBody(probe);
8264
+ releaseBodyDetached(probe);
6946
8265
  if (callerSignal?.aborted) {
6947
8266
  releaseBodyDetached(response);
6948
8267
  throw callerSignal.reason;
@@ -6966,7 +8285,7 @@ function withRfc8414OidcCompat(fetchFn) {
6966
8285
  return response;
6967
8286
  }
6968
8287
  const source = probe.url || candidate;
6969
- await releaseBody(response);
8288
+ releaseBodyDetached(response);
6970
8289
  console.warn(
6971
8290
  `[oauth] ${source} returned RFC 8414 OAuth 2.0 authorization server metadata, not an OpenID provider document. The MCP TypeScript SDK validates that path as OpenID Connect Discovery and would reject it (modelcontextprotocol/typescript-sdk#2733), so the Inspector is handing it to discovery as the RFC 8414 document it is.`
6972
8291
  );
@@ -7926,7 +9245,7 @@ async function discoverResourceAsMetadata(resourceAsUrl, fetchFn) {
7926
9245
 
7927
9246
  // ../../core/auth/ema/wire.ts
7928
9247
  import { discoverAndRequestJwtAuthGrant } from "@modelcontextprotocol/client";
7929
- import { OAuthTokensSchema as OAuthTokensSchema3 } from "@modelcontextprotocol/core";
9248
+ import { OAuthTokensSchema as OAuthTokensSchema5 } from "@modelcontextprotocol/core";
7930
9249
  async function exchangeIdJag(params) {
7931
9250
  const issuer = normalizeIdpIssuer(params.idp.issuer);
7932
9251
  const resource = params.resource.trim();
@@ -8005,7 +9324,7 @@ async function redeemIdJagForAccessToken(params) {
8005
9324
  "EMA leg 3 (resource AS JWT bearer grant)"
8006
9325
  );
8007
9326
  }
8008
- return OAuthTokensSchema3.parse(await response.json());
9327
+ return OAuthTokensSchema5.parse(await response.json());
8009
9328
  }
8010
9329
 
8011
9330
  // ../../core/auth/ema/emaFlow.ts
@@ -9230,6 +10549,14 @@ function progressTokenOf(message) {
9230
10549
  }
9231
10550
  return void 0;
9232
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
+ }
9233
10560
 
9234
10561
  // ../../core/mcp/remote/remoteClientTransport.ts
9235
10562
  var DEFAULT_SSE_RESPONSE_TIMEOUT_MS = 6e4;
@@ -9706,8 +11033,8 @@ var RemoteClientTransport = class {
9706
11033
  });
9707
11034
  }
9708
11035
  /** Re-arm a pending request's SSE wait timeout on a matching progress note. */
9709
- resetSseResponseWait(requestId) {
9710
- this.sseResponseWaits.get(requestId)?.resetTimeout();
11036
+ resetSseResponseWait(progressToken) {
11037
+ waitForProgressToken(this.sseResponseWaits, progressToken)?.resetTimeout();
9711
11038
  }
9712
11039
  async postSend(message, options, retryCount) {
9713
11040
  const requestId = requestIdForMessage(message);
@@ -9912,6 +11239,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
9912
11239
  requestTimeout;
9913
11240
  defaultMetadata;
9914
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;
9915
11247
  versionNegotiation;
9916
11248
  status = "disconnected";
9917
11249
  // True only while an explicit disconnect() owns the teardown. close() can
@@ -10266,6 +11598,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10266
11598
  const advertisedExtensions = buildClientExtensions({
10267
11599
  enterpriseManaged: options.oauth?.enterpriseManaged ?? false,
10268
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,
10269
11605
  // Read off the built `capabilities.elicitation.form` rather than
10270
11606
  // re-deriving from `options.elicit`: the nested MCP Apps `elicitation`
10271
11607
  // setting must never be advertised without the core form capability it
@@ -10691,6 +12027,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10691
12027
  }
10692
12028
  this.baseTransport = null;
10693
12029
  this.transport = null;
12030
+ this.transportSettings = void 0;
10694
12031
  this.transportHasAuthProvider = false;
10695
12032
  }
10696
12033
  /**
@@ -11164,6 +12501,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11164
12501
  transportOptions
11165
12502
  );
11166
12503
  this.baseTransport = baseTransport;
12504
+ this.transportSettings = transportOptions.settings;
11167
12505
  if (this.directAuthRecovery) {
11168
12506
  this.directAuthRecoveryActive = !(baseTransport instanceof RemoteClientTransport);
11169
12507
  }
@@ -11365,6 +12703,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11365
12703
  }
11366
12704
  this.baseTransport = null;
11367
12705
  this.transport = null;
12706
+ this.transportSettings = void 0;
11368
12707
  this.transportHasAuthProvider = false;
11369
12708
  this.clearPendingPeerRequests();
11370
12709
  if (this.status !== "disconnected") {
@@ -11521,7 +12860,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11521
12860
  ...params,
11522
12861
  _meta: {
11523
12862
  ...existingMeta,
11524
- [PROTOCOL_VERSION_META_KEY]: protocolVersion,
12863
+ [PROTOCOL_VERSION_META_KEY2]: protocolVersion,
11525
12864
  [CLIENT_INFO_META_KEY]: this.clientInfo,
11526
12865
  [CLIENT_CAPABILITIES_META_KEY]: clientCapabilities
11527
12866
  }
@@ -12097,6 +13436,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12097
13436
  getClientCapabilities() {
12098
13437
  return this.clientCapabilities;
12099
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
+ }
12100
13446
  /**
12101
13447
  * Get server info (name, version)
12102
13448
  */
@@ -12148,6 +13494,16 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12148
13494
  getServerSettings() {
12149
13495
  return this.serverSettings;
12150
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
+ }
12151
13507
  /**
12152
13508
  * Replace the in-memory per-server settings on a live client. Lets a settings
12153
13509
  * edit (e.g. toggling auto-refresh-on-list-changed) take effect on the
@@ -13669,10 +15025,9 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13669
15025
  * removes it before the result gets here (#2373).
13670
15026
  *
13671
15027
  * Separate from {@link getSkill} because the callers differ: the UIs want the
13672
- * entry, while the CLI prints the result and must not reshape it. SEP-2640
13673
- * explicitly leaves open whether this result carries `ttlMs` / `cacheScope`,
13674
- * so a server may send them — and unwrapping to the entry discards exactly
13675
- * 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).
13676
15031
  */
13677
15032
  async getSkillResult(uri, metadata) {
13678
15033
  if (!this.client) {
@@ -13683,11 +15038,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
13683
15038
  uri,
13684
15039
  ...effectiveMeta ? { _meta: effectiveMeta } : {}
13685
15040
  };
15041
+ const resultSchema = this.isModernEra() ? ModernGetSkillEnvelopeSchema : GetSkillEnvelopeSchema;
13686
15042
  try {
13687
15043
  return await this.invokeMcpClient(
13688
15044
  () => this.client.request(
13689
15045
  { method: SKILLS_GET_METHOD, params },
13690
- GetSkillEnvelopeSchema,
15046
+ resultSchema,
13691
15047
  this.getRequestOptions(this.progressTokenOf(metadata))
13692
15048
  ),
13693
15049
  { method: SKILLS_GET_METHOD }
@@ -14838,9 +16194,74 @@ function isLoopbackHost(host) {
14838
16194
  return h === "localhost" || h === "[::1]" || /^127(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/.test(h);
14839
16195
  }
14840
16196
 
14841
- // src/error-handler.ts
14842
- var EXIT_CODES = {
14843
- OK: 0,
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());
16223
+ }
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;
16247
+ }
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
+ };
16260
+ }
16261
+
16262
+ // src/error-handler.ts
16263
+ var EXIT_CODES = {
16264
+ OK: 0,
14844
16265
  USAGE: 1,
14845
16266
  NO_APP: 2,
14846
16267
  AUTH_REQUIRED: 3,
@@ -14869,7 +16290,20 @@ var EXIT_CODES = {
14869
16290
  * job that wants to tolerate oversized catalogs can allow 8 and still fail
14870
16291
  * on 7.
14871
16292
  */
14872
- 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
14873
16307
  };
14874
16308
  var CliExitCodeError = class extends Error {
14875
16309
  constructor(exitCode, message, envelope) {
@@ -14904,7 +16338,44 @@ function statusOf(error) {
14904
16338
  return void 0;
14905
16339
  }
14906
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
+ }
14907
16374
  function classifyError(error, context) {
16375
+ const { exitCode, envelope } = classifyUnredacted(error, context);
16376
+ return { exitCode, envelope: redactEnvelope(envelope) };
16377
+ }
16378
+ function classifyUnredacted(error, context) {
14908
16379
  const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
14909
16380
  const cause = causeOf(error);
14910
16381
  const status = statusOf(error);
@@ -14925,9 +16396,29 @@ function classifyError(error, context) {
14925
16396
  }
14926
16397
  };
14927
16398
  }
14928
- if (status === 401 || status === 403 || /WWW-Authenticate|Unauthorized|invalid_token|OAuth/i.test(
14929
- message + " " + (cause ?? "")
14930
- )) {
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)) {
14931
16422
  return {
14932
16423
  exitCode: EXIT_CODES.AUTH_REQUIRED,
14933
16424
  envelope: {
@@ -14998,7 +16489,7 @@ var BOOLEAN_VALUED_KEYWORDS = /* @__PURE__ */ new Set([
14998
16489
  "additionalItems",
14999
16490
  "unevaluatedItems"
15000
16491
  ]);
15001
- var SUBSCHEMA_KEYWORDS = [
16492
+ var SUBSCHEMA_KEYWORDS2 = [
15002
16493
  "items",
15003
16494
  "contains",
15004
16495
  "not",
@@ -15015,13 +16506,13 @@ var SUBSCHEMA_KEYWORDS = [
15015
16506
  // real finding.
15016
16507
  "contentSchema"
15017
16508
  ];
15018
- var SUBSCHEMA_ARRAY_KEYWORDS = [
16509
+ var SUBSCHEMA_ARRAY_KEYWORDS2 = [
15019
16510
  "allOf",
15020
16511
  "anyOf",
15021
16512
  "oneOf",
15022
16513
  "prefixItems"
15023
16514
  ];
15024
- var SUBSCHEMA_MAP_KEYWORDS = [
16515
+ var SUBSCHEMA_MAP_KEYWORDS2 = [
15025
16516
  "properties",
15026
16517
  "patternProperties",
15027
16518
  "dependentSchemas",
@@ -15103,8 +16594,8 @@ function isPortableTypeUnion(type) {
15103
16594
  }
15104
16595
  return new Set(type).size === type.length;
15105
16596
  }
15106
- var MAX_DEPTH = 64;
15107
- function isRecord3(value) {
16597
+ var MAX_DEPTH2 = 64;
16598
+ function isRecord5(value) {
15108
16599
  return typeof value === "object" && value !== null && !Array.isArray(value);
15109
16600
  }
15110
16601
  function childPath(parent, key) {
@@ -15120,7 +16611,7 @@ function describeSchemaPath(schema, path5) {
15120
16611
  return path5 === "" ? schema : `${schema}.${path5}`;
15121
16612
  }
15122
16613
  function declaresAnyId(node, depth = 0) {
15123
- if (depth > MAX_DEPTH || !isRecord3(node)) return false;
16614
+ if (depth > MAX_DEPTH2 || !isRecord5(node)) return false;
15124
16615
  if (typeof node.$id === "string" && node.$id !== "") return true;
15125
16616
  let found = false;
15126
16617
  forEachSubschema(node, "", (child) => {
@@ -15139,21 +16630,21 @@ function add(ctx, rule, severity, path5, issue, suggestion) {
15139
16630
  });
15140
16631
  }
15141
16632
  function forEachSubschema(node, path5, visit) {
15142
- for (const key of SUBSCHEMA_MAP_KEYWORDS) {
16633
+ for (const key of SUBSCHEMA_MAP_KEYWORDS2) {
15143
16634
  const map = node[key];
15144
- if (!isRecord3(map)) continue;
16635
+ if (!isRecord5(map)) continue;
15145
16636
  for (const [name, sub] of Object.entries(map)) {
15146
16637
  visit(sub, childPath(childPath(path5, key), name), key);
15147
16638
  }
15148
16639
  }
15149
- for (const key of SUBSCHEMA_ARRAY_KEYWORDS) {
16640
+ for (const key of SUBSCHEMA_ARRAY_KEYWORDS2) {
15150
16641
  const arr = node[key];
15151
16642
  if (!Array.isArray(arr)) continue;
15152
16643
  arr.forEach((sub, i) => {
15153
16644
  visit(sub, indexPath(childPath(path5, key), i), key);
15154
16645
  });
15155
16646
  }
15156
- for (const key of SUBSCHEMA_KEYWORDS) {
16647
+ for (const key of SUBSCHEMA_KEYWORDS2) {
15157
16648
  if (!(key in node)) continue;
15158
16649
  const value = node[key];
15159
16650
  if (key === "items" && Array.isArray(value)) {
@@ -15172,7 +16663,7 @@ function booleanTrueSuggestion(keyword) {
15172
16663
  return 'Declare what the value actually is \u2014 e.g. `{"type": "object", "additionalProperties": true}` for a free-form object. That is a deliberate change of contract, not an equivalent rewrite: `true` accepts any JSON value at all.';
15173
16664
  }
15174
16665
  function walk(node, path5, keyword, depth, ctx) {
15175
- if (depth > MAX_DEPTH) return;
16666
+ if (depth > MAX_DEPTH2) return;
15176
16667
  if (typeof node === "boolean") {
15177
16668
  if (keyword !== void 0 && BOOLEAN_VALUED_KEYWORDS.has(keyword)) return;
15178
16669
  add(
@@ -15191,12 +16682,13 @@ function walk(node, path5, keyword, depth, ctx) {
15191
16682
  );
15192
16683
  return;
15193
16684
  }
15194
- if (!isRecord3(node)) return;
16685
+ if (!isRecord5(node)) return;
15195
16686
  lintNode(node, path5, keyword, ctx);
15196
16687
  forEachSubschema(node, path5, (child, childPathValue, childKeyword) => {
15197
16688
  walk(child, childPathValue, childKeyword, depth + 1, ctx);
15198
16689
  });
15199
16690
  }
16691
+ var NULL_BRANCH_CAVEAT = " A dialect with no `null` type, such as OpenAPI 3.0, still cannot express the `null` branch directly; there nullability is written `nullable: true`, which is not JSON Schema, so no single spelling is portable to both.";
15200
16692
  function lintNode(node, path5, keyword, ctx) {
15201
16693
  const type = node.type;
15202
16694
  if (Array.isArray(type) && isPortableTypeUnion(type)) {
@@ -15205,10 +16697,10 @@ function lintNode(node, path5, keyword, ctx) {
15205
16697
  "type-union",
15206
16698
  "warning",
15207
16699
  path5,
15208
- `\`type\` is an array (${JSON.stringify(type)}). The array form is legal JSON Schema, but several MCP clients read \`type\` as a single string and either reject the tool or drop the constraint.`,
16700
+ `\`type\` is an array (${JSON.stringify(type)}). This is legal JSON Schema, and some model providers recommend it for nullable fields, but it is less portable: a client that maps tool schemas onto a single-\`type\` dialect (such as the OpenAPI subset used for Gemini function declarations) may reject the tool or drop the constraint.`,
15209
16701
  `Split it into \`anyOf\` branches, each with a single \`type\` \u2014 \`{"anyOf": [${type.map((t) => `{"type": "${t}"}`).join(
15210
16702
  ", "
15211
- )}]}\`. (Making the property optional instead is a different contract: absent is not the same as \`null\`.)`
16703
+ )}]}\`. (Making the property optional instead is a different contract: absent is not the same as \`null\`.)${type.includes("null") ? NULL_BRANCH_CAVEAT : ""}`
15212
16704
  );
15213
16705
  }
15214
16706
  const ref = node.$ref;
@@ -15368,6 +16860,9 @@ async function emitResult(result, appInfo, args) {
15368
16860
  }
15369
16861
 
15370
16862
  // src/handlers/consume-outcome.ts
16863
+ function isBrokenPipe(err) {
16864
+ return err instanceof Error && err.code === "EPIPE";
16865
+ }
15371
16866
  async function consumeMethodOutcome(outcome, args) {
15372
16867
  if (outcome.kind === "result") {
15373
16868
  await emitResult(outcome.result, outcome.appInfo, args);
@@ -15383,23 +16878,37 @@ async function consumeMethodOutcome(outcome, args) {
15383
16878
  throw new CliExitCodeError(outcome.exitCode, outcome.summary ?? "", {
15384
16879
  // The envelope's `code` follows the exit code, so a caller reading one
15385
16880
  // never has to reconcile it against the other.
15386
- 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"
15387
16882
  });
15388
16883
  }
15389
16884
  return;
15390
16885
  }
15391
- await new Promise((resolve7) => {
15392
- const stop = outcome.start((obj) => {
15393
- void awaitableLog(JSON.stringify(obj) + "\n");
15394
- });
15395
- const onSignal = () => {
15396
- stop();
16886
+ await new Promise((resolve7, reject) => {
16887
+ let stop;
16888
+ const detach = () => {
15397
16889
  process.off("SIGINT", onSignal);
15398
16890
  process.off("SIGTERM", onSignal);
15399
- resolve7();
16891
+ process.stdout.off("error", onStdoutError);
16892
+ };
16893
+ const finish = (err) => {
16894
+ detach();
16895
+ stop?.();
16896
+ if (err === void 0) resolve7();
16897
+ else reject(err);
15400
16898
  };
16899
+ const onSignal = () => finish();
16900
+ const onStdoutError = (err) => finish(isBrokenPipe(err) ? void 0 : err);
16901
+ process.stdout.on("error", onStdoutError);
15401
16902
  process.on("SIGINT", onSignal);
15402
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
+ }
15403
16912
  });
15404
16913
  }
15405
16914
 
@@ -16220,24 +17729,6 @@ async function collectAppInfo(client, tool, metadata) {
16220
17729
  }
16221
17730
  }
16222
17731
 
16223
- // src/handlers/skills-verify.ts
16224
- function summarizeSkillVerification(reports) {
16225
- const failed = reports.filter((report) => report.outcome === "failed").length;
16226
- const incomplete = reports.filter(
16227
- (report) => report.outcome === "incomplete"
16228
- ).length;
16229
- const files = reports.reduce((sum, report) => sum + report.files.length, 0);
16230
- const mismatched = reports.reduce(
16231
- (sum, report) => sum + report.files.filter((file) => file.status === "mismatch").length,
16232
- 0
16233
- );
16234
- const skillWord = reports.length === 1 ? "skill" : "skills";
16235
- const fileWord = files === 1 ? "file" : "files";
16236
- const incompleteClause = incomplete === 0 ? "" : ` ${incomplete} of ${reports.length} ${skillWord} could not be fully checked: the read bounds stopped the walk.`;
16237
- 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}).`;
16238
- return `${headline}${incompleteClause}`;
16239
- }
16240
-
16241
17732
  // ../../core/mcp/skillsVerification.ts
16242
17733
  function reasonOf(err) {
16243
17734
  return err instanceof Error ? err.message : String(err);
@@ -16430,7 +17921,11 @@ async function verifySkills(client, entries, metadata) {
16430
17921
  files,
16431
17922
  ...incomplete ? { incomplete } : {},
16432
17923
  ok: !hasError && !fileFailed,
16433
- 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"
16434
17929
  });
16435
17930
  }
16436
17931
  return reports;
@@ -16441,6 +17936,38 @@ function allSkillsVerified(reports) {
16441
17936
  function anySkillFailed(reports) {
16442
17937
  return reports.some((report) => report.outcome === "failed");
16443
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
+ }
16444
17971
 
16445
17972
  // src/handlers/run-method.ts
16446
17973
  function assertSkillsSupported(inspectorClient, method) {
@@ -16691,13 +18218,10 @@ async function runMethod(inspectorClient, args) {
16691
18218
  kind: "ndjson",
16692
18219
  lines: reports,
16693
18220
  summary: summarizeSkillVerification(reports),
16694
- // Three outcomes, three exit codes: a broken MUST is 7, a walk the
16695
- // read bounds cut short is 8, and everything checked and passing is
16696
- // 0. Collapsing the middle case into either of the others reports
16697
- // something untrue about the server (Copilot).
16698
- ...allSkillsVerified(reports) ? {} : {
16699
- exitCode: anySkillFailed(reports) ? EXIT_CODES.SKILL_NONCONFORMANT : EXIT_CODES.SKILL_INCOMPLETE
16700
- }
18221
+ exitCode: skillVerificationExitCode(
18222
+ reports,
18223
+ args.requireDigests === true
18224
+ )
16701
18225
  };
16702
18226
  }
16703
18227
  result = { skills };
@@ -16723,13 +18247,10 @@ async function runMethod(inspectorClient, args) {
16723
18247
  kind: "ndjson",
16724
18248
  lines: reports,
16725
18249
  summary: summarizeSkillVerification(reports),
16726
- // Three outcomes, three exit codes: a broken MUST is 7, a walk the
16727
- // read bounds cut short is 8, and everything checked and passing is
16728
- // 0. Collapsing the middle case into either of the others reports
16729
- // something untrue about the server (Copilot).
16730
- ...allSkillsVerified(reports) ? {} : {
16731
- exitCode: anySkillFailed(reports) ? EXIT_CODES.SKILL_NONCONFORMANT : EXIT_CODES.SKILL_INCOMPLETE
16732
- }
18250
+ exitCode: skillVerificationExitCode(
18251
+ reports,
18252
+ args.requireDigests === true
18253
+ )
16733
18254
  };
16734
18255
  }
16735
18256
  result = envelope;
@@ -16811,71 +18332,6 @@ import {
16811
18332
  discoverAuthorizationServerMetadata as discoverAuthorizationServerMetadata4
16812
18333
  } from "@modelcontextprotocol/client";
16813
18334
 
16814
- // ../../core/auth/issuerBinding.ts
16815
- import { AuthorizationServerMismatchError } from "@modelcontextprotocol/client";
16816
- var MISSING_DISCOVERY_STATE_PHRASE = "discoveryState was not available";
16817
- function isAuthorizationServerMismatchShape(err) {
16818
- if (err === null || typeof err !== "object") {
16819
- return false;
16820
- }
16821
- const candidate = err;
16822
- if (typeof candidate.recordedIssuer !== "string" || typeof candidate.currentIssuer !== "string") {
16823
- return false;
16824
- }
16825
- return AuthorizationServerMismatchError.isInstance(err) || candidate.name === "AuthorizationServerMismatchError";
16826
- }
16827
- function isMissingDiscoveryStatePlaceholder(recordedIssuer) {
16828
- if (recordedIssuer.includes(MISSING_DISCOVERY_STATE_PHRASE)) {
16829
- return true;
16830
- }
16831
- try {
16832
- const url = new URL(recordedIssuer);
16833
- return url.protocol !== "http:" && url.protocol !== "https:";
16834
- } catch {
16835
- return true;
16836
- }
16837
- }
16838
- function findIssuerBindingFailure(err) {
16839
- return findIssuerBindingFailureDeep(err, /* @__PURE__ */ new Set());
16840
- }
16841
- function findIssuerBindingFailureDeep(err, seen) {
16842
- if (err === null || typeof err !== "object" || seen.has(err)) {
16843
- return void 0;
16844
- }
16845
- seen.add(err);
16846
- if (isAuthorizationServerMismatchShape(err)) {
16847
- return classifyAuthorizationServerMismatch(err);
16848
- }
16849
- const nested = findIssuerBindingFailureDeep(
16850
- err.cause,
16851
- seen
16852
- );
16853
- if (nested) {
16854
- return nested;
16855
- }
16856
- const data = err.data;
16857
- if (data !== null && typeof data === "object") {
16858
- return findIssuerBindingFailureDeep(
16859
- data.cause,
16860
- seen
16861
- );
16862
- }
16863
- return void 0;
16864
- }
16865
- function classifyAuthorizationServerMismatch(err) {
16866
- if (isMissingDiscoveryStatePlaceholder(err.recordedIssuer)) {
16867
- return {
16868
- kind: "lost_authorization_state",
16869
- currentIssuer: err.currentIssuer
16870
- };
16871
- }
16872
- return {
16873
- kind: "issuer_mismatch",
16874
- recordedIssuer: err.recordedIssuer,
16875
- currentIssuer: err.currentIssuer
16876
- };
16877
- }
16878
-
16879
18335
  // ../../core/auth/node/oauth-callback-server.ts
16880
18336
  import { createServer } from "http";
16881
18337
  var DEFAULT_HOSTNAME = "127.0.0.1";
@@ -17171,8 +18627,28 @@ async function runRunnerInteractiveOAuth(options) {
17171
18627
 
17172
18628
  // src/open-url.ts
17173
18629
  import open from "open";
17174
- async function openUrl(url) {
17175
- 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
+ }
17176
18652
  }
17177
18653
 
17178
18654
  // src/style.ts
@@ -17238,7 +18714,12 @@ function createCliOAuthNavigation(options = {}) {
17238
18714
  if (!forced && !tty) return;
17239
18715
  try {
17240
18716
  await (options.openBrowser ?? openUrl)(href);
17241
- } 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
+ );
17242
18723
  }
17243
18724
  });
17244
18725
  }
@@ -17381,6 +18862,23 @@ function mergeSecretsIntoClientConfig(config, secrets) {
17381
18862
  }
17382
18863
 
17383
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
+ }
17384
18882
  async function readIdpSecretFromKeychain(secretStore) {
17385
18883
  const secret = await secretStore.get(
17386
18884
  CLIENT_KEYCHAIN_ID,
@@ -17390,29 +18888,39 @@ async function readIdpSecretFromKeychain(secretStore) {
17390
18888
  return { [SECRET_FIELD_IDP_CLIENT_SECRET]: secret };
17391
18889
  }
17392
18890
  async function migrateClientPlaintextSecret(filePath, config, secretStore) {
17393
- const { stripped, secrets } = extractSecretsFromClientConfig(config);
17394
- const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
17395
- if (!value) return config;
17396
18891
  try {
17397
- const existing = await secretStoreGetStrict(
17398
- secretStore,
17399
- CLIENT_KEYCHAIN_ID,
17400
- SECRET_FIELD_IDP_CLIENT_SECRET
17401
- );
17402
- if (existing === null) {
17403
- await secretStore.set(
17404
- CLIENT_KEYCHAIN_ID,
17405
- SECRET_FIELD_IDP_CLIENT_SECRET,
17406
- value
17407
- );
17408
- }
17409
- if (!await secretStoreIsDurable(secretStore)) return config;
17410
- await writeStoreFile(filePath, serializeStore(stripped));
17411
- 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
+ });
17412
18922
  } catch (err) {
17413
- if (err instanceof SecretStoreUnavailableError) {
17414
- return config;
17415
- }
18923
+ if (err instanceof SecretFileLockHeldError) return config;
17416
18924
  throw err;
17417
18925
  }
17418
18926
  }
@@ -17764,7 +19272,7 @@ async function withCliAuthRecoveryRetry(inspectorClient, serverConfig, redirectU
17764
19272
  import { LoggingLevelSchema } from "@modelcontextprotocol/core";
17765
19273
 
17766
19274
  // ../../core/node/version.ts
17767
- import { readFileSync as readFileSync3 } from "fs";
19275
+ import { readFileSync as readFileSync4 } from "fs";
17768
19276
  import { dirname as dirname5, join as join3, parse } from "path";
17769
19277
  import { fileURLToPath } from "url";
17770
19278
  var ROOT_PACKAGE_NAME = "@modelcontextprotocol/inspector";
@@ -17776,7 +19284,7 @@ function readInspectorVersion(callerUrl) {
17776
19284
  let manifest;
17777
19285
  try {
17778
19286
  manifest = JSON.parse(
17779
- readFileSync3(join3(dir, "package.json"), "utf-8")
19287
+ readFileSync4(join3(dir, "package.json"), "utf-8")
17780
19288
  );
17781
19289
  } catch {
17782
19290
  }
@@ -17875,6 +19383,13 @@ async function callMethod(serverConfig, serverSettings, args, clientConfig, cliA
17875
19383
  ...serverSettings?.protocolEra && {
17876
19384
  versionNegotiation: eraToVersionNegotiation(serverSettings.protocolEra)
17877
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
+ },
17878
19393
  ...clientAuthOptions
17879
19394
  });
17880
19395
  try {
@@ -17908,14 +19423,8 @@ function normalizeServerUrl2(serverUrl) {
17908
19423
  }
17909
19424
  }
17910
19425
  async function readOAuthSnapshot(statePath) {
17911
- const { readFile: readFile2 } = await import("fs/promises");
17912
- try {
17913
- const text = await readFile2(statePath, "utf8");
17914
- const snapshot = parseOAuthPersistBlob(text);
17915
- if (snapshot) return snapshot;
17916
- } catch {
17917
- }
17918
- return { servers: {}, idpSessions: {} };
19426
+ const snapshot = await readOAuthStore(statePath);
19427
+ return snapshot ?? { servers: {}, idpSessions: {} };
17919
19428
  }
17920
19429
  async function readOAuthServers(statePath) {
17921
19430
  return (await readOAuthSnapshot(statePath)).servers;
@@ -17990,17 +19499,44 @@ async function refreshStoredAuthToken(serverUrl, statePath, deps = {}) {
17990
19499
  );
17991
19500
  }
17992
19501
  servers[found.key] = { ...found.state, tokens };
17993
- await writeStoreFile(statePath, serializeOAuthPersistBlob(snapshot));
19502
+ await writeOAuthSections(statePath, snapshot, { servers: [found.key] });
17994
19503
  return tokens.access_token;
17995
19504
  }
17996
- 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) {
17997
19522
  const key = normalizeServerUrl2(serverUrl);
17998
19523
  const deadline = Date.now() + timeoutSec * 1e3;
19524
+ let servers = {};
19525
+ let lastError;
17999
19526
  for (; ; ) {
18000
- 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
+ }
18001
19536
  const token = findStoredToken(servers, serverUrl);
18002
19537
  if (token) return token;
18003
19538
  if (Date.now() >= deadline) {
19539
+ if (lastError !== void 0) throw lastError;
18004
19540
  const stored = Object.keys(servers);
18005
19541
  throw new CliExitCodeError(
18006
19542
  EXIT_CODES.AUTH_REQUIRED,
@@ -18008,7 +19544,9 @@ async function waitForStoredToken(serverUrl, statePath, timeoutSec) {
18008
19544
  { code: "auth_wait_timeout", url: serverUrl }
18009
19545
  );
18010
19546
  }
18011
- await new Promise((r) => setTimeout(r, 500));
19547
+ await new Promise(
19548
+ (r) => setTimeout(r, Math.min(500, deadline - Date.now()))
19549
+ );
18012
19550
  }
18013
19551
  }
18014
19552
  function deepLinkTransport(serverUrl, transport) {
@@ -18164,12 +19702,18 @@ async function parseArgs(argv) {
18164
19702
  ).option(
18165
19703
  "--app-info",
18166
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."
18167
19708
  ).option(
18168
19709
  "--strict",
18169
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."
18170
19711
  ).option(
18171
19712
  "--verify",
18172
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.'
18173
19717
  ).option(
18174
19718
  "--connect-timeout <ms>",
18175
19719
  `Connection timeout in ms (default ${DEFAULT_CONNECT_TIMEOUT_MS} for ad-hoc --server-url / target invocations; 0 = no timeout).`,
@@ -18284,6 +19828,14 @@ async function parseArgs(argv) {
18284
19828
  );
18285
19829
  }
18286
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
+ }
18287
19839
  const oauthStatePath = getStateFilePath();
18288
19840
  if (options.listStoredAuth) {
18289
19841
  const servers = await readOAuthServers(oauthStatePath);
@@ -18449,8 +20001,10 @@ async function parseArgs(argv) {
18449
20001
  metadata: options.metadata,
18450
20002
  toolMeta: options.toolMetadata,
18451
20003
  appInfo: options.appInfo === true,
20004
+ advertiseApps: options.advertiseApps === true,
18452
20005
  strict: options.strict === true,
18453
20006
  verify: options.verify === true,
20007
+ requireDigests: options.requireDigests === true,
18454
20008
  cursor: options.cursor,
18455
20009
  format: options.format
18456
20010
  };