@modelcontextprotocol/inspector 2.3.0 → 2.4.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.
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-MCKGQKYU.js";
3
2
 
4
3
  // index.ts
5
- import { resolve as resolve3 } from "path";
4
+ import { resolve as resolve6 } from "path";
6
5
  import { fileURLToPath as fileURLToPath2 } from "url";
7
6
 
8
7
  // tui.tsx
@@ -132,6 +131,22 @@ var SECRET_FIELD_IDP_CLIENT_SECRET = "idp-client-secret";
132
131
  var envSecretField = (envKey) => `env:${envKey}`;
133
132
 
134
133
  // ../../core/json/jsonUtils.ts
134
+ function isSerializableJson(value) {
135
+ if (value === null) return true;
136
+ switch (typeof value) {
137
+ case "string":
138
+ case "boolean":
139
+ return true;
140
+ case "number":
141
+ return Number.isFinite(value);
142
+ case "object":
143
+ return Array.isArray(value) ? value.every(isSerializableJson) : Object.values(value).every(
144
+ isSerializableJson
145
+ );
146
+ default:
147
+ return false;
148
+ }
149
+ }
135
150
  function toRecord(value) {
136
151
  return value;
137
152
  }
@@ -241,6 +256,60 @@ function envPairsToRecord(pairs) {
241
256
  }
242
257
  return out;
243
258
  }
259
+ function normalizeStoredMetadata(metadata) {
260
+ if (metadata === void 0 || metadata === null) return {};
261
+ if (Array.isArray(metadata)) {
262
+ const out = {};
263
+ for (const entry of metadata) {
264
+ if (typeof entry !== "object" || entry === null || typeof entry.key !== "string") {
265
+ console.warn(
266
+ "Ignoring malformed legacy metadata entry (expected `{ key, value }`)."
267
+ );
268
+ continue;
269
+ }
270
+ const { key, value } = entry;
271
+ if (key.trim() === "") continue;
272
+ const resolved2 = value === void 0 ? "" : value;
273
+ if (!keepSerializable(key, resolved2)) continue;
274
+ defineMetadataKey(out, key, resolved2);
275
+ }
276
+ return out;
277
+ }
278
+ if (typeof metadata === "object") {
279
+ const out = {};
280
+ for (const [key, value] of Object.entries(metadata)) {
281
+ if (keepSerializable(key, value)) defineMetadataKey(out, key, value);
282
+ }
283
+ return out;
284
+ }
285
+ console.warn(
286
+ `Ignoring malformed \`metadata\` (expected a JSON object, got ${describeJsonType(metadata)}).`
287
+ );
288
+ return {};
289
+ }
290
+ function defineMetadataKey(out, key, value) {
291
+ Object.defineProperty(out, key, {
292
+ value,
293
+ writable: true,
294
+ enumerable: true,
295
+ configurable: true
296
+ });
297
+ }
298
+ function keepSerializable(key, value) {
299
+ if (isSerializableJson(value)) return true;
300
+ console.warn(
301
+ `Ignoring metadata key "${key}" \u2014 its value is not JSON that can be sent (got ${describeJsonType(value)}; note \`1e400\` parses to Infinity).`
302
+ );
303
+ return false;
304
+ }
305
+ function describeJsonType(value) {
306
+ if (value === null) return "null";
307
+ if (Array.isArray(value)) return "array";
308
+ if (typeof value === "number" && !Number.isFinite(value)) {
309
+ return Number.isNaN(value) ? "NaN" : "a non-finite number";
310
+ }
311
+ return typeof value;
312
+ }
244
313
  function cleanAuthorizationParams(params) {
245
314
  if (params === void 0) return void 0;
246
315
  if (typeof params !== "object" || params === null || Array.isArray(params)) {
@@ -294,7 +363,7 @@ function storedFieldsToInspectorSettings(stored) {
294
363
  const settings = {
295
364
  headers: headersPairs,
296
365
  env: envRecordToPairs(stored.env),
297
- metadata: stored.metadata ?? [],
366
+ metadata: normalizeStoredMetadata(stored.metadata),
298
367
  connectionTimeout: stored.connectionTimeout ?? 0,
299
368
  requestTimeout: stored.requestTimeout ?? 0,
300
369
  // Unlike the timeouts (0 = "SDK default"), task TTL has a concrete product
@@ -343,6 +412,9 @@ function storedFieldsToInspectorSettings(stored) {
343
412
  if (stored.oauth?.enterpriseManaged === true) {
344
413
  settings.enterpriseManaged = true;
345
414
  }
415
+ if (stored.oauth?.requestRefreshToken === false) {
416
+ settings.oauthRequestRefreshToken = false;
417
+ }
346
418
  if (stored.cwd) settings.cwd = stored.cwd;
347
419
  return settings;
348
420
  }
@@ -657,155 +729,1427 @@ function resolveServerConfigs(options, mode) {
657
729
  cwd: options.cwd
658
730
  })
659
731
  );
660
- return configs;
732
+ return configs;
733
+ }
734
+ return [buildConfigFromOptions(options)];
735
+ }
736
+ return [];
737
+ }
738
+
739
+ // ../../core/auth/node/secret-store-selection.ts
740
+ import { randomUUID } from "crypto";
741
+ import * as fsSync from "fs";
742
+ import * as fs4 from "fs/promises";
743
+ import * as path4 from "path";
744
+
745
+ // ../../core/auth/secret-storage-info.ts
746
+ function secretStorageLabel(info) {
747
+ switch (info.kind) {
748
+ case "keyring":
749
+ return "OS keychain";
750
+ case "file":
751
+ if (info.encryptionUnknown !== void 0) return "File (unreadable)";
752
+ return info.plaintext ? "File (unencrypted)" : "File (encrypted)";
753
+ case "memory":
754
+ return "Memory (this session only)";
755
+ }
756
+ }
757
+ function secretStorageCaveat(info) {
758
+ if (info.kind === "memory") {
759
+ return "Secrets are not written anywhere and are lost on exit.";
760
+ }
761
+ if (info.looseMode !== void 0) {
762
+ const mode = info.looseMode.toString(8).padStart(4, "0");
763
+ if (info.encryptionUnknown !== void 0) {
764
+ return `The secrets file is mode ${mode}, not 0600, and could not be tightened \u2014 others can copy it, and whether that exposes the secrets depends on encryption this build could not determine (${info.encryptionUnknown}).`;
765
+ }
766
+ return info.plaintext === false ? `The secrets file is mode ${mode}, not 0600, and could not be tightened \u2014 others can copy it, and the passphrase is then the only thing protecting its contents.` : `The secrets file is mode ${mode}, not 0600, and could not be tightened \u2014 anyone who can read it can read the secrets in it.`;
767
+ }
768
+ if (info.permissionsUnknown !== void 0) {
769
+ return `The secrets file's permissions could not be checked (${info.permissionsUnknown}), so it is not known whether others can read it.`;
770
+ }
771
+ if (info.encryptionUnknown !== void 0) {
772
+ return `The secrets file could not be read (${info.encryptionUnknown}). Saving a secret will fail rather than overwrite it.`;
773
+ }
774
+ if (info.plaintext) {
775
+ 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.";
776
+ }
777
+ return void 0;
778
+ }
779
+ function secretStorageSummary(info) {
780
+ const parts = [secretStorageLabel(info)];
781
+ if (info.path) parts.push(`at ${info.path}`);
782
+ if (info.reason === "fallback") {
783
+ parts.push("\u2014 fell back from the OS keychain");
784
+ }
785
+ return parts.join(" ");
786
+ }
787
+
788
+ // ../../core/auth/node/file-secret-store.ts
789
+ import * as crypto2 from "crypto";
790
+ import * as fs3 from "fs/promises";
791
+ import * as path3 from "path";
792
+
793
+ // ../../core/auth/node/file-lock.ts
794
+ import nodeFs, { statSync, rmdirSync } from "fs";
795
+ import * as fs2 from "fs/promises";
796
+ import * as path2 from "path";
797
+ import properLockfile from "proper-lockfile";
798
+
799
+ // ../../core/auth/node/secret-store.ts
800
+ var SERVICE_NAME = "mcp-inspector";
801
+ var keyringLoad;
802
+ var KeyringModuleShapeError = class extends Error {
803
+ constructor(detail, options) {
804
+ super(`@napi-rs/keyring loaded but ${detail}`, options);
805
+ this.name = "KeyringModuleShapeError";
806
+ }
807
+ };
808
+ var checkKeyringShape = (mod) => {
809
+ try {
810
+ if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
811
+ return { ok: true, mod };
812
+ }
813
+ return {
814
+ ok: false,
815
+ err: new KeyringModuleShapeError(
816
+ "did not expose AsyncEntry / findCredentialsAsync"
817
+ )
818
+ };
819
+ } catch (err) {
820
+ return {
821
+ ok: false,
822
+ err: new KeyringModuleShapeError(
823
+ `its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
824
+ { cause: err }
825
+ )
826
+ };
827
+ }
828
+ };
829
+ var loadKeyring = () => {
830
+ keyringLoad ??= import("@napi-rs/keyring").then(
831
+ checkKeyringShape,
832
+ (err) => ({ ok: false, err })
833
+ );
834
+ return keyringLoad;
835
+ };
836
+ function parseAccount(account) {
837
+ const idx = account.indexOf(":");
838
+ if (idx <= 0 || idx === account.length - 1) return null;
839
+ return {
840
+ serverId: account.slice(0, idx),
841
+ field: account.slice(idx + 1)
842
+ };
843
+ }
844
+ var buildAccount = (serverId, field) => `${serverId}:${field}`;
845
+ var SecretStoreUnavailableError = class extends Error {
846
+ constructor(message, options) {
847
+ super(message, options);
848
+ this.name = "SecretStoreUnavailableError";
849
+ }
850
+ };
851
+ var KeychainUnavailableError = class extends SecretStoreUnavailableError {
852
+ constructor(cause) {
853
+ const message = cause instanceof Error ? cause.message : String(cause);
854
+ super(
855
+ `OS keychain is not available. ${hintFor(cause, message)} Underlying error: ${message}`
856
+ );
857
+ this.name = "KeychainUnavailableError";
858
+ }
859
+ };
860
+ var hintFor = (cause, message) => {
861
+ if (cause instanceof KeyringModuleShapeError) {
862
+ return `The @napi-rs/keyring package loaded but does not expose the API this build expects \u2014 most likely a version or packaging mismatch; reinstall the Inspector, and report this if it persists.`;
863
+ }
864
+ if (message.includes("Cannot find native binding")) {
865
+ return `The @napi-rs/keyring platform package for this OS is missing or unavailable \u2014 reinstall the Inspector (for npx, clear the npx cache under your npm cache directory first).`;
866
+ }
867
+ return `On Linux, install libsecret / gnome-keyring.`;
868
+ };
869
+ async function probeKeyringAvailable() {
870
+ const keyring = await loadKeyring();
871
+ if (!keyring.ok) {
872
+ const err = keyring.err;
873
+ return {
874
+ available: false,
875
+ detail: err instanceof Error ? err.message : String(err)
876
+ };
877
+ }
878
+ try {
879
+ const entry = new keyring.mod.AsyncEntry(SERVICE_NAME, PROBE_ACCOUNT);
880
+ await entry.getPassword();
881
+ return { available: true };
882
+ } catch (err) {
883
+ return {
884
+ available: false,
885
+ detail: err instanceof Error ? err.message : String(err)
886
+ };
887
+ }
888
+ }
889
+ var PROBE_ACCOUNT = "__inspector:probe";
890
+ var KeyringSecretStore = class {
891
+ /**
892
+ * The intolerant read. Same lookup as {@link get}, minus the catch — a
893
+ * keychain that cannot answer must not be reported as an empty one to a
894
+ * caller that is about to write based on the answer.
895
+ */
896
+ async getStrict(serverId, field) {
897
+ try {
898
+ const keyring = await loadKeyring();
899
+ if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
900
+ const entry = new keyring.mod.AsyncEntry(
901
+ SERVICE_NAME,
902
+ buildAccount(serverId, field)
903
+ );
904
+ return await entry.getPassword() ?? null;
905
+ } catch (err) {
906
+ if (err instanceof KeychainUnavailableError) throw err;
907
+ throw new KeychainUnavailableError(err);
908
+ }
909
+ }
910
+ async get(serverId, field) {
911
+ try {
912
+ const keyring = await loadKeyring();
913
+ if (!keyring.ok) return null;
914
+ const entry = new keyring.mod.AsyncEntry(
915
+ SERVICE_NAME,
916
+ buildAccount(serverId, field)
917
+ );
918
+ const v = await entry.getPassword();
919
+ return v ?? null;
920
+ } catch {
921
+ return null;
922
+ }
923
+ }
924
+ async set(serverId, field, value) {
925
+ try {
926
+ const keyring = await loadKeyring();
927
+ if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
928
+ const entry = new keyring.mod.AsyncEntry(
929
+ SERVICE_NAME,
930
+ buildAccount(serverId, field)
931
+ );
932
+ await entry.setPassword(value);
933
+ } catch (err) {
934
+ if (err instanceof KeychainUnavailableError) throw err;
935
+ throw new KeychainUnavailableError(err);
936
+ }
937
+ }
938
+ async delete(serverId, field) {
939
+ try {
940
+ const keyring = await loadKeyring();
941
+ if (!keyring.ok) return;
942
+ const entry = new keyring.mod.AsyncEntry(
943
+ SERVICE_NAME,
944
+ buildAccount(serverId, field)
945
+ );
946
+ await entry.deleteCredential();
947
+ } catch {
948
+ }
949
+ }
950
+ async deleteAllForServer(serverId) {
951
+ let creds;
952
+ try {
953
+ const keyring = await loadKeyring();
954
+ if (!keyring.ok) return;
955
+ creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
956
+ } catch {
957
+ return;
958
+ }
959
+ const prefix = `${serverId}:`;
960
+ for (const c of creds) {
961
+ if (!c.account.startsWith(prefix)) continue;
962
+ const parsed = parseAccount(c.account);
963
+ if (!parsed || parsed.serverId !== serverId) continue;
964
+ await this.delete(serverId, parsed.field);
965
+ }
966
+ }
967
+ };
968
+ async function secretStoreGetStrict(store, serverId, field) {
969
+ return store.getStrict ? store.getStrict(serverId, field) : store.get(serverId, field);
970
+ }
971
+ async function secretStoreGetMany(store, requests) {
972
+ if (store.getMany) return store.getMany(requests);
973
+ const out = {};
974
+ await Promise.all(
975
+ requests.map(async ({ serverId, fields }) => {
976
+ const entries = await Promise.all(
977
+ fields.map(
978
+ async (field) => [field, await store.get(serverId, field)]
979
+ )
980
+ );
981
+ const found = {};
982
+ for (const [field, value] of entries) {
983
+ if (value !== null) found[field] = value;
984
+ }
985
+ out[serverId] = found;
986
+ })
987
+ );
988
+ return out;
989
+ }
990
+ async function secretStoreSetMany(store, serverId, values) {
991
+ if (store.setMany) return store.setMany(serverId, values);
992
+ await Promise.all(
993
+ Object.entries(values).map(
994
+ ([field, value]) => store.set(serverId, field, value)
995
+ )
996
+ );
997
+ }
998
+ async function secretStoreIsDurable(store) {
999
+ return store.isDurable ? store.isDurable() : true;
1000
+ }
1001
+ var InMemorySecretStore = class {
1002
+ map = /* @__PURE__ */ new Map();
1003
+ async get(serverId, field) {
1004
+ return this.map.get(buildAccount(serverId, field)) ?? null;
1005
+ }
1006
+ async set(serverId, field, value) {
1007
+ this.map.set(buildAccount(serverId, field), value);
1008
+ }
1009
+ async delete(serverId, field) {
1010
+ this.map.delete(buildAccount(serverId, field));
1011
+ }
1012
+ async deleteAllForServer(serverId) {
1013
+ const prefix = `${serverId}:`;
1014
+ for (const key of [...this.map.keys()]) {
1015
+ if (key.startsWith(prefix)) this.map.delete(key);
1016
+ }
1017
+ }
1018
+ };
1019
+ var SessionSecretStore = class extends InMemorySecretStore {
1020
+ async isDurable() {
1021
+ return false;
1022
+ }
1023
+ };
1024
+
1025
+ // ../../core/auth/node/file-lock.ts
1026
+ var STALE_MS = 1e4;
1027
+ var RETRY = {
1028
+ retries: 20,
1029
+ factor: 2,
1030
+ minTimeout: 20,
1031
+ maxTimeout: 1e3
1032
+ };
1033
+ var RETRY_BUDGET_MS = (() => {
1034
+ let total = 0;
1035
+ let delay = RETRY.minTimeout;
1036
+ for (let i = 0; i < RETRY.retries; i++) {
1037
+ total += Math.min(delay, RETRY.maxTimeout);
1038
+ delay *= RETRY.factor;
1039
+ }
1040
+ return total;
1041
+ })();
1042
+ var warned = /* @__PURE__ */ new Set();
1043
+ function warnOnce(message) {
1044
+ if (warned.has(message)) return;
1045
+ warned.add(message);
1046
+ console.warn(`[mcp-inspector] ${message}`);
1047
+ }
1048
+ function isStuckOrHeld(err, lockPath) {
1049
+ return isHeldElsewhere(err) || identifySync(lockPath) !== null;
1050
+ }
1051
+ var isHeldElsewhere = (err) => err.code === "ELOCKED";
1052
+ var describeError = (err) => err instanceof Error ? err.message : String(err);
1053
+ var lockPathOf = (target) => `${target}.lock`;
1054
+ function identifySync(lockPath) {
1055
+ try {
1056
+ const stat2 = statSync(lockPath);
1057
+ return { ino: stat2.ino, birthtimeMs: stat2.birthtimeMs };
1058
+ } catch {
1059
+ return null;
1060
+ }
1061
+ }
1062
+ function guardedFs(lockPath, owned, onRefused) {
1063
+ const mine = () => {
1064
+ if (owned.id === null) return true;
1065
+ const now = identifySync(lockPath);
1066
+ if (now === null) return false;
1067
+ return now.ino === owned.id.ino && now.birthtimeMs === owned.id.birthtimeMs;
1068
+ };
1069
+ const removeIfMine = () => {
1070
+ if (!mine()) {
1071
+ onRefused();
1072
+ return;
1073
+ }
1074
+ rmdirSync(lockPath);
1075
+ };
1076
+ return {
1077
+ ...nodeFs,
1078
+ // **Identity is captured here, not after `lock()` resolves.** `mkdir` is
1079
+ // the moment the directory becomes ours, and it is synchronous with
1080
+ // respect to this process: recording it in the callback, before yielding,
1081
+ // leaves no window. Capturing after the acquire promise settled — as this
1082
+ // did originally — spans the library's own `utimes`/`stat` probe, and a
1083
+ // waiter replacing our directory inside that span would have us record
1084
+ // *the winner's* identity as our own, after which the guard below would
1085
+ // cheerfully delete their lock.
1086
+ mkdir: (p, cb) => {
1087
+ nodeFs.mkdir(p, (err) => {
1088
+ if (!err) owned.id = identifySync(lockPath);
1089
+ cb(err);
1090
+ });
1091
+ },
1092
+ // Reported as success when refused: the library's bookkeeping should
1093
+ // forget this lock either way. Leaving it registered would hand the
1094
+ // `signal-exit` handler a record pointing at the winner's directory.
1095
+ rmdir: (_p, cb) => {
1096
+ try {
1097
+ removeIfMine();
1098
+ cb(null);
1099
+ } catch (err) {
1100
+ cb(err);
1101
+ }
1102
+ },
1103
+ /* v8 ignore next 3 -- @preserve: only reachable from proper-lockfile's
1104
+ signal-exit handler, i.e. at real process exit, which no in-process
1105
+ test can drive. Its logic is `removeIfMine`, covered via `rmdir`. */
1106
+ rmdirSync: () => {
1107
+ removeIfMine();
1108
+ }
1109
+ };
1110
+ }
1111
+ function acquire(target, retries, fsShim) {
1112
+ return properLockfile.lock(target, {
1113
+ realpath: false,
1114
+ stale: STALE_MS,
1115
+ retries,
1116
+ // Every directory removal the library performs — on release and from its
1117
+ // exit handler — routes through here. See `guardedFs`.
1118
+ fs: fsShim,
1119
+ // The library's default `onCompromised` *throws* — from a timer, with no
1120
+ // caller on the stack, so it lands as an uncaught exception and takes the
1121
+ // process down. This is also the library's *only* signal for the
1122
+ // stale-takeover race described at the top of this file — someone
1123
+ // declared our lock stale and replaced it — so it is the one place a user
1124
+ // learns the guarantee was lost. Worth saying; not worth killing an
1125
+ // Inspector session over.
1126
+ onCompromised: (err) => warnOnce(
1127
+ `The lock on the secrets file at ${target} was taken over by another process while a write was in progress (${err.message}). If a secret you just saved is missing, save it again.`
1128
+ )
1129
+ });
1130
+ }
1131
+ async function isFileLockHeld(filePath) {
1132
+ try {
1133
+ return await properLockfile.check(path2.resolve(filePath), {
1134
+ realpath: false,
1135
+ stale: STALE_MS
1136
+ });
1137
+ } catch {
1138
+ return false;
1139
+ }
1140
+ }
1141
+ async function openSecretFileLock(filePath) {
1142
+ const target = path2.resolve(filePath);
1143
+ await fs2.mkdir(path2.dirname(target), { recursive: true }).catch(() => {
1144
+ });
1145
+ const owned = { id: null };
1146
+ const fsShim = guardedFs(
1147
+ lockPathOf(target),
1148
+ owned,
1149
+ () => warnOnce(
1150
+ `The lock on the secrets file at ${target} was taken over by another process while a write was in progress, so it was left alone rather than removed. If a secret you just saved is missing, save it again.`
1151
+ )
1152
+ );
1153
+ let release;
1154
+ try {
1155
+ release = await acquire(target, 0, fsShim).catch((err) => {
1156
+ if (!isStuckOrHeld(err, lockPathOf(target))) throw err;
1157
+ return acquire(target, RETRY, fsShim);
1158
+ });
1159
+ } catch (err) {
1160
+ if (isStuckOrHeld(err, lockPathOf(target))) {
1161
+ throw new SecretStoreUnavailableError(
1162
+ `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.`
1163
+ );
1164
+ }
1165
+ warnOnce(
1166
+ `Could not take a lock on the secrets file at ${target} (${describeError(err)}), so writes to it are not protected against another process writing at the same moment.`
1167
+ );
1168
+ return null;
1169
+ }
1170
+ return async () => {
1171
+ try {
1172
+ await release();
1173
+ } catch (err) {
1174
+ if (err.code === "ERELEASED") return;
1175
+ warnOnce(
1176
+ // **Conditional, not an instruction.** Two blanket versions of this
1177
+ // have now been wrong in opposite directions: "it expires on its own"
1178
+ // (false whenever the same `rmdir` that blocked us also blocks stale
1179
+ // takeover — `ENOTEMPTY`, `EACCES`, `EPERM`, `EROFS`), and "remove it
1180
+ // by hand" (false whenever the failure was transient, since the path
1181
+ // may by then hold a *different, live* Inspector's lock, and deleting
1182
+ // that destroys the exclusion of a process that did nothing wrong).
1183
+ //
1184
+ // `proper-lockfile` forwards whatever the filesystem returned, and
1185
+ // nothing here can tell a permanent refusal from a passing one. So
1186
+ // this reports what happened and states the two conditions the
1187
+ // operator can check for themselves, rather than asserting an outcome
1188
+ // this code does not know.
1189
+ `Could not release the lock on the secrets file at ${target} (${describeError(err)}). It may clear on its own \u2014 stale takeover reclaims a lock through the same directory removal, so it will not if whatever blocked this persists. If saves keep failing against this file and no other Inspector is running, remove ${lockPathOf(target)} by hand.`
1190
+ );
1191
+ }
1192
+ };
1193
+ }
1194
+ async function withSecretFileLock(filePath, fn) {
1195
+ const release = await openSecretFileLock(filePath);
1196
+ if (release === null) return fn();
1197
+ try {
1198
+ return await fn();
1199
+ } finally {
1200
+ await release();
1201
+ }
1202
+ }
1203
+
1204
+ // ../../core/auth/node/file-secret-store.ts
1205
+ var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve7, reject) => {
1206
+ crypto2.scrypt(passphrase, salt, keylen, options, (err, key) => {
1207
+ if (err) reject(err);
1208
+ else resolve7(key);
1209
+ });
1210
+ });
1211
+ var FORMAT_VERSION = 1;
1212
+ var CIPHER = "aes-256-gcm";
1213
+ var KEY_BYTES = 32;
1214
+ var IV_BYTES = 12;
1215
+ var GCM_TAG_BYTES = 16;
1216
+ var SCRYPT_N = 16384;
1217
+ var SCRYPT_R = 8;
1218
+ var SCRYPT_P = 1;
1219
+ var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
1220
+ var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
1221
+ function asSecretMap(value, filePath) {
1222
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1223
+ throw new SecretStoreUnavailableError(
1224
+ `The secrets file at ${filePath} does not hold a secret map (found ${Array.isArray(value) ? "an array" : typeof value}). Refusing to write, which would replace it. Move the file aside to start over.`
1225
+ );
1226
+ }
1227
+ for (const [key, entry] of Object.entries(value)) {
1228
+ if (typeof entry !== "string") {
1229
+ throw new SecretStoreUnavailableError(
1230
+ `The secrets file at ${filePath} holds a non-string value for "${key}". Refusing to write, which would replace it. Move the file aside to start over.`
1231
+ );
1232
+ }
1233
+ }
1234
+ return { ...value };
1235
+ }
1236
+ var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
1237
+ constructor(filePath, hasKey) {
1238
+ super(
1239
+ 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.`
1240
+ );
1241
+ this.name = "SecretFileKeyMismatchError";
1242
+ }
1243
+ };
1244
+ var encodeParts = (iv, tag, body) => `${iv.toString("base64")}.${tag.toString("base64")}.${body.toString("base64")}`;
1245
+ function decodeParts(payload) {
1246
+ const parts = payload.split(".");
1247
+ if (parts.length !== 3) return null;
1248
+ const [iv, tag, body] = parts;
1249
+ return {
1250
+ iv: Buffer.from(iv, "base64"),
1251
+ tag: Buffer.from(tag, "base64"),
1252
+ body: Buffer.from(body, "base64")
1253
+ };
1254
+ }
1255
+ var MAX_WRITE_ATTEMPTS = 5;
1256
+ var fileQueues = /* @__PURE__ */ new Map();
1257
+ function sameMap(a, b) {
1258
+ const ak = Object.keys(a);
1259
+ if (ak.length !== Object.keys(b).length) return false;
1260
+ return ak.every(
1261
+ (k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]
1262
+ );
1263
+ }
1264
+ var isPositiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
1265
+ function encryptedEnvelopeProblem(parsed) {
1266
+ if (!parsed.kdf || typeof parsed.data !== "string") {
1267
+ return "encrypted envelope is missing its kdf or data";
1268
+ }
1269
+ const parts = decodeParts(parsed.data);
1270
+ if (!parts) return "encrypted payload is not iv.tag.ciphertext";
1271
+ if (parts.iv.length !== IV_BYTES) {
1272
+ return `initialization vector is ${parts.iv.length} bytes, expected ${IV_BYTES}`;
1273
+ }
1274
+ if (parts.tag.length !== GCM_TAG_BYTES) {
1275
+ return `authentication tag is ${parts.tag.length} bytes, expected ${GCM_TAG_BYTES}`;
1276
+ }
1277
+ if (parts.body.length === 0) return "ciphertext is empty";
1278
+ const { algorithm, salt, N, r, p } = parsed.kdf;
1279
+ if (algorithm !== "scrypt") return `unsupported kdf "${String(algorithm)}"`;
1280
+ if (typeof salt !== "string" || Buffer.from(salt, "base64").length === 0) {
1281
+ return "kdf salt is missing or not base64";
1282
+ }
1283
+ if (!isPositiveInt(N) || N < 2 || (N & N - 1) !== 0) {
1284
+ return `kdf N is ${String(N)}, which is not a power of two above 1`;
1285
+ }
1286
+ if (!isPositiveInt(r) || !isPositiveInt(p)) {
1287
+ return `kdf r/p are ${String(r)}/${String(p)}, which are not positive integers`;
1288
+ }
1289
+ if (N > SCRYPT_N || r > SCRYPT_R || p > SCRYPT_P) {
1290
+ return `kdf cost parameters (N=${N}, r=${r}, p=${p}) exceed the supported maximum (N=${SCRYPT_N}, r=${SCRYPT_R}, p=${SCRYPT_P})`;
1291
+ }
1292
+ return null;
1293
+ }
1294
+ var FileSecretStore = class {
1295
+ filePath;
1296
+ passphrase;
1297
+ constructor(options) {
1298
+ this.filePath = options.filePath;
1299
+ const raw = options.passphrase ?? process.env[SECRET_KEY_ENV];
1300
+ this.passphrase = raw && raw.trim() ? raw : void 0;
1301
+ }
1302
+ /**
1303
+ * True when the *next write* will encrypt. This is the configured policy,
1304
+ * not the state of the file — see {@link readOnDiskEncryption}, which is
1305
+ * what the user-facing descriptor is built from.
1306
+ */
1307
+ get encrypted() {
1308
+ return this.passphrase !== void 0;
1309
+ }
1310
+ /**
1311
+ * What the file on disk is *currently* written as — `null` when there is
1312
+ * no file yet, or when it cannot be read or parsed.
1313
+ *
1314
+ * Separate from {@link encrypted} because the two genuinely disagree for a
1315
+ * whole session: adding `MCP_INSPECTOR_SECRET_KEY` to an install that
1316
+ * already has a plaintext file flips `encrypted` to true immediately,
1317
+ * while the bytes stay readable until the next `set`. A descriptor built
1318
+ * from the policy would tell that user "File (encrypted)" while their
1319
+ * existing secrets sat in the clear — the precise reassurance this whole
1320
+ * subsystem exists not to give.
1321
+ *
1322
+ * Reads the envelope, and for an encrypted one **attempts to open it**.
1323
+ * Structure alone is not access: a well-formed file this passphrase cannot
1324
+ * decrypt answers `null` from every `get` and refuses every `set`, so
1325
+ * reporting it as a healthy "encrypted" store hands the quiet footer to
1326
+ * exactly the state the unreadable one exists to warn about. The
1327
+ * authentication costs one scrypt derivation per call, which is once per
1328
+ * `/api/config`, and the decrypted bytes are discarded.
1329
+ *
1330
+ * `absent` and `unreadable` are kept apart deliberately. Collapsing them
1331
+ * (as a bare `null` did) means an existing file that is corrupt, or
1332
+ * written in an envelope this build does not know, is reported as "no
1333
+ * file yet" — and the descriptor then falls back to the configured write
1334
+ * policy, so a passphrase-configured install claims "Encrypted file"
1335
+ * about a file whose encryption state was never established and which
1336
+ * `set` is in fact about to refuse.
1337
+ */
1338
+ async readOnDiskEncryption() {
1339
+ let raw;
1340
+ try {
1341
+ raw = await readStoreFile(this.filePath);
1342
+ } catch (err) {
1343
+ return {
1344
+ state: "unreadable",
1345
+ detail: err instanceof Error ? err.message : String(err)
1346
+ };
1347
+ }
1348
+ if (raw === null) return { state: "absent" };
1349
+ try {
1350
+ const parsed = JSON.parse(raw);
1351
+ if (parsed.version !== FORMAT_VERSION) {
1352
+ return {
1353
+ state: "unreadable",
1354
+ detail: `format version ${String(parsed.version)}, expected ${FORMAT_VERSION}`
1355
+ };
1356
+ }
1357
+ if (parsed.encryption === "none") {
1358
+ try {
1359
+ asSecretMap(parsed.secrets ?? {}, this.filePath);
1360
+ } catch (err) {
1361
+ return {
1362
+ state: "unreadable",
1363
+ detail: err instanceof Error ? err.message : String(err)
1364
+ };
1365
+ }
1366
+ return { state: "plaintext" };
1367
+ }
1368
+ if (parsed.encryption === CIPHER) {
1369
+ const problem = encryptedEnvelopeProblem(parsed);
1370
+ if (problem) return { state: "unreadable", detail: problem };
1371
+ try {
1372
+ await this.readMap();
1373
+ } catch (err) {
1374
+ return {
1375
+ state: "unreadable",
1376
+ detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV}` : err instanceof Error ? err.message : String(err)
1377
+ };
1378
+ }
1379
+ return { state: "encrypted" };
1380
+ }
1381
+ return {
1382
+ state: "unreadable",
1383
+ detail: `unsupported envelope (encryption="${String(parsed.encryption)}", version=${String(parsed.version)})`
1384
+ };
1385
+ } catch {
1386
+ return { state: "unreadable", detail: "not valid JSON" };
1387
+ }
1388
+ }
1389
+ async deriveKey(kdf) {
1390
+ return scrypt2(
1391
+ // Only reached when the passphrase is set — every caller checks first,
1392
+ // which is what makes the non-null assertion safe here rather than a
1393
+ // widened `string | undefined` threaded through the crypto helpers.
1394
+ this.passphrase,
1395
+ Buffer.from(kdf.salt, "base64"),
1396
+ KEY_BYTES,
1397
+ { N: kdf.N, r: kdf.r, p: kdf.p }
1398
+ );
1399
+ }
1400
+ /**
1401
+ * Read and decode the file into a plain map.
1402
+ *
1403
+ * Distinguishes three outcomes that callers treat differently: `null` for
1404
+ * "no file yet" (a first write should create one), a map for success, and a
1405
+ * throw for "there is a file and we cannot read it" — which must never be
1406
+ * flattened into an empty map, or `set` would cheerfully overwrite it.
1407
+ */
1408
+ /**
1409
+ * Every secret in the file, keyed by `${serverId}:${field}` — `null` when
1410
+ * there is no file.
1411
+ *
1412
+ * Exists for one caller: the hand-off performed when a keychain becomes
1413
+ * available on a box that had been falling back to a file (see
1414
+ * `absorbFileSecretsIntoKeyring`). Nothing in the normal request path
1415
+ * enumerates secrets, and deliberately so — this is the widest possible
1416
+ * read of the store and it should stay a one-purpose seam rather than a
1417
+ * general convenience.
1418
+ *
1419
+ * Throws for the same reasons `get` swallows: an unreadable or
1420
+ * undecryptable file must not present as "no secrets", because the caller
1421
+ * would conclude there was nothing to migrate and move on.
1422
+ */
1423
+ async readAll() {
1424
+ return this.serialize(() => this.readMap());
1425
+ }
1426
+ async readMap() {
1427
+ const raw = await readStoreFile(this.filePath);
1428
+ if (raw === null) return null;
1429
+ let parsed;
1430
+ try {
1431
+ parsed = JSON.parse(raw);
1432
+ } catch (err) {
1433
+ throw new SecretStoreUnavailableError(
1434
+ `The secrets file at ${this.filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
1435
+ );
1436
+ }
1437
+ if (parsed.version !== FORMAT_VERSION) {
1438
+ throw new SecretStoreUnavailableError(
1439
+ `The secrets file at ${this.filePath} declares format version ${String(parsed.version)}, but this Inspector only understands version ${FORMAT_VERSION}. It was probably written by a newer Inspector; upgrade, or move the file aside to start over.`
1440
+ );
1441
+ }
1442
+ if (parsed.encryption === "none") {
1443
+ return asSecretMap(parsed.secrets ?? {}, this.filePath);
1444
+ }
1445
+ if (parsed.encryption !== CIPHER || !parsed.kdf || !parsed.data) {
1446
+ throw new SecretStoreUnavailableError(
1447
+ `The secrets file at ${this.filePath} declares an unsupported format (encryption="${String(parsed.encryption)}", version=${String(parsed.version)}). It may have been written by a newer Inspector.`
1448
+ );
1449
+ }
1450
+ if (!this.passphrase)
1451
+ throw new SecretFileKeyMismatchError(this.filePath, false);
1452
+ const structural = encryptedEnvelopeProblem(parsed);
1453
+ if (structural) {
1454
+ throw new SecretStoreUnavailableError(
1455
+ `The secrets file at ${this.filePath} is not readable: ${structural}. Restoring a passphrase will not repair it; the file itself is malformed.`
1456
+ );
1457
+ }
1458
+ const parts = decodeParts(parsed.data);
1459
+ if (!parts) throw new SecretFileKeyMismatchError(this.filePath, true);
1460
+ let plain;
1461
+ try {
1462
+ const key = await this.deriveKey(parsed.kdf);
1463
+ const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv);
1464
+ decipher.setAuthTag(parts.tag);
1465
+ plain = Buffer.concat([
1466
+ decipher.update(parts.body),
1467
+ decipher.final()
1468
+ ]).toString("utf-8");
1469
+ } catch {
1470
+ throw new SecretFileKeyMismatchError(this.filePath, true);
1471
+ }
1472
+ let payload;
1473
+ try {
1474
+ payload = JSON.parse(plain);
1475
+ } catch (err) {
1476
+ throw new SecretStoreUnavailableError(
1477
+ `The secrets file at ${this.filePath} decrypted successfully but its contents are not valid JSON: ${err instanceof Error ? err.message : String(err)}. The passphrase is correct; the file itself is corrupt.`
1478
+ );
1479
+ }
1480
+ return asSecretMap(payload, this.filePath);
1481
+ }
1482
+ async writeMap(map) {
1483
+ let file;
1484
+ if (this.passphrase) {
1485
+ const kdf = {
1486
+ algorithm: "scrypt",
1487
+ // A fresh salt per write, not per file: rewriting with the same salt
1488
+ // and a fresh IV would still be safe for GCM, but re-salting costs one
1489
+ // scrypt derivation we were doing anyway and removes the question.
1490
+ salt: crypto2.randomBytes(16).toString("base64"),
1491
+ N: SCRYPT_N,
1492
+ r: SCRYPT_R,
1493
+ p: SCRYPT_P
1494
+ };
1495
+ const key = await this.deriveKey(kdf);
1496
+ const iv = crypto2.randomBytes(IV_BYTES);
1497
+ const cipher = crypto2.createCipheriv(CIPHER, key, iv);
1498
+ const body = Buffer.concat([
1499
+ cipher.update(JSON.stringify(map), "utf-8"),
1500
+ cipher.final()
1501
+ ]);
1502
+ file = {
1503
+ version: FORMAT_VERSION,
1504
+ encryption: CIPHER,
1505
+ kdf,
1506
+ data: encodeParts(iv, cipher.getAuthTag(), body)
1507
+ };
1508
+ } else {
1509
+ file = { version: FORMAT_VERSION, encryption: "none", secrets: map };
1510
+ }
1511
+ await writeStoreFile(this.filePath, `${JSON.stringify(file, null, 2)}
1512
+ `);
1513
+ }
1514
+ /**
1515
+ * Apply a mutation to the file, under a cross-process lock, and confirm it
1516
+ * survived.
1517
+ *
1518
+ * **Two mechanisms, and they answer different questions** — this is not
1519
+ * belt-and-braces.
1520
+ *
1521
+ * The lock ({@link withSecretFileLock}, #2082) is the one that provides
1522
+ * mutual exclusion: a durable secrets file is shared state, and a second
1523
+ * Inspector on the same box — a CLI run beside a web session is the
1524
+ * ordinary case, not a contrived one — otherwise reads the same "before"
1525
+ * map and then atomically replaces the file, dropping whatever the first
1526
+ * process had just added. Both writes report success; one secret is simply
1527
+ * gone. Held across the read *and* the write, so no other holder can
1528
+ * observe or replace the map in between.
1529
+ *
1530
+ * The verify covers what a lock structurally cannot. A lock is an advisory
1531
+ * convention between the processes that take it, so it says nothing about
1532
+ * a writer that does not: an editor, a restored backup, a `jq` one-liner,
1533
+ * or an Inspector predating #2082. It is also the fallback when the lock
1534
+ * is unavailable at all — `withSecretFileLock` runs the body anyway on a
1535
+ * read-only or lock-hostile directory rather than failing a `set` on
1536
+ * exactly the deployments this store exists for (#1848, #1905).
1537
+ *
1538
+ * So: read `M0`, apply the mutation to get `M1`, write it, then read back
1539
+ * `M2`. If `M2` equals `M1` nothing interleaved. If it does not, someone
1540
+ * wrote between our write and our read — so re-apply onto what they left
1541
+ * and try again.
1542
+ *
1543
+ * The comparison is over the **whole map**, not just the entry we touched.
1544
+ * Checking only our own key would pass in precisely the case that loses
1545
+ * data: our entry is present, and it is the *other* writer's that is gone.
1546
+ * When the verify does fire, the writer that was clobbered is the one that
1547
+ * repairs it — A writes `MA`, B writes `MB` over it, B verifies `MB`
1548
+ * correctly, A verifies, sees `M2 !== MA`, re-applies onto `MB`.
1549
+ *
1550
+ * **What is left, stated without overselling.** Against an unlocked writer
1551
+ * the verify is not mutual exclusion: it only catches a clobber that has
1552
+ * already landed, so ordering the two as write-A, verify-A, write-B,
1553
+ * verify-B loses A's entry with both reporting success. That residual is
1554
+ * now confined to a writer outside this codebase, or to a directory where
1555
+ * no lock could be taken — and the latter announces itself, once, on the
1556
+ * console.
1557
+ *
1558
+ * Reads deliberately do not participate: `writeStoreFile` is atomic
1559
+ * (write-temp-then-rename), so a reader sees either the old file or the
1560
+ * new one, never a torn one. Taking the lock for them would serialize
1561
+ * every `GET /api/servers` behind whatever else holds it, to prevent
1562
+ * nothing.
1563
+ */
1564
+ async mutate(apply) {
1565
+ await withSecretFileLock(this.filePath, () => this.mutateLocked(apply));
1566
+ }
1567
+ /** {@link mutate}'s body, with the lock already held. */
1568
+ async mutateLocked(apply) {
1569
+ for (let attempt = 0; attempt < MAX_WRITE_ATTEMPTS; attempt++) {
1570
+ let current;
1571
+ try {
1572
+ current = await this.readMap() ?? {};
1573
+ } catch (err) {
1574
+ if (err instanceof SecretStoreUnavailableError) throw err;
1575
+ throw new SecretStoreUnavailableError(
1576
+ `Could not read the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
1577
+ );
1578
+ }
1579
+ const next = apply(current);
1580
+ if (next === null) return;
1581
+ try {
1582
+ await this.writeMap(next);
1583
+ } catch (err) {
1584
+ throw new SecretStoreUnavailableError(
1585
+ `Could not write the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
1586
+ );
1587
+ }
1588
+ let observed;
1589
+ try {
1590
+ observed = await this.readMap() ?? {};
1591
+ } catch {
1592
+ continue;
1593
+ }
1594
+ if (sameMap(next, observed)) return;
1595
+ }
1596
+ throw new SecretStoreUnavailableError(
1597
+ `Could not write the secrets file at ${this.filePath}: another process kept overwriting it (gave up after ${MAX_WRITE_ATTEMPTS} attempts). Its secrets are intact; the value you just entered was not saved.`
1598
+ );
1599
+ }
1600
+ /**
1601
+ * Run `fn` with the file to itself, so concurrent callers can't lose
1602
+ * writes.
1603
+ *
1604
+ * Keyed on the **resolved path, process-wide** rather than on this
1605
+ * instance. Two `FileSecretStore`s pointing at one file are ordinary —
1606
+ * the resolved store holds one and `absorbFileSecretsIntoKeyring`
1607
+ * constructs another — and a per-instance queue does not order them
1608
+ * against each other at all. They then race in-process, which the
1609
+ * optimistic verify is only *supposed* to catch for writers in different
1610
+ * processes: both read the same map, both write, and whichever verifies
1611
+ * before the other's write lands sees its own value and returns happy.
1612
+ * Measured, not theorised — two instances racing `set` lost an entry on
1613
+ * the first run of the convergence test below.
1614
+ *
1615
+ * So the in-process case is made correct by construction here, and
1616
+ * {@link mutate}'s cross-process lock covers what this cannot see: a
1617
+ * second Inspector process.
1618
+ *
1619
+ * The queue is *also* what keeps that lock usable. `proper-lockfile` is
1620
+ * not reentrant — a second `lock()` on a path this process already holds
1621
+ * fails `ELOCKED`, which is indistinguishable from a genuine remote
1622
+ * holder. Serializing here means the lock is only ever contended between
1623
+ * processes, which is the only contention it is asked to arbitrate.
1624
+ */
1625
+ serialize(fn) {
1626
+ const key = path3.resolve(this.filePath);
1627
+ const run = (fileQueues.get(key) ?? Promise.resolve()).then(fn);
1628
+ const settled = run.then(
1629
+ () => {
1630
+ },
1631
+ () => {
1632
+ }
1633
+ );
1634
+ fileQueues.set(key, settled);
1635
+ void settled.then(() => {
1636
+ if (fileQueues.get(key) === settled) fileQueues.delete(key);
1637
+ });
1638
+ return run;
1639
+ }
1640
+ /**
1641
+ * One read and one derivation for an entire rehydration.
1642
+ *
1643
+ * `get` reads and decrypts the whole file, so the cost is per *call*, not
1644
+ * per field — which is why the batch has to span servers. A per-server
1645
+ * version of this shipped first and left a 20-server catalog paying 20
1646
+ * serialized derivations, the same stall it was written to remove.
1647
+ * Tolerant like `get`: an unreadable store yields no fields for anyone
1648
+ * rather than throwing.
1649
+ */
1650
+ async getMany(requests) {
1651
+ let map;
1652
+ try {
1653
+ map = await this.serialize(() => this.readMap());
1654
+ } catch {
1655
+ map = null;
1656
+ }
1657
+ const out = {};
1658
+ for (const { serverId, fields } of requests) {
1659
+ const found = {};
1660
+ for (const field of fields) {
1661
+ const value = map?.[buildAccount2(serverId, field)];
1662
+ if (value !== void 0) found[field] = value;
1663
+ }
1664
+ out[serverId] = found;
1665
+ }
1666
+ return out;
1667
+ }
1668
+ /**
1669
+ * The intolerant read — same lookup as {@link get}, minus the catch.
1670
+ *
1671
+ * Without this, `secretStoreGetStrict` fell back to `get` for the file
1672
+ * store and turned every read failure into `null`. Both plaintext
1673
+ * migrations *write* on `null` and then strip the disk copy, so a
1674
+ * transient read error that cleared before the write let an older
1675
+ * on-disk value replace a newer stored one — the same inversion the
1676
+ * strict seam was introduced to prevent, still open on the store the
1677
+ * seam was introduced for.
1678
+ */
1679
+ async getStrict(serverId, field) {
1680
+ try {
1681
+ const map = await this.serialize(() => this.readMap());
1682
+ return map?.[buildAccount2(serverId, field)] ?? null;
1683
+ } catch (err) {
1684
+ if (err instanceof SecretStoreUnavailableError) throw err;
1685
+ throw new SecretStoreUnavailableError(
1686
+ `Could not read the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
1687
+ );
1688
+ }
1689
+ }
1690
+ async get(serverId, field) {
1691
+ try {
1692
+ const map = await this.serialize(() => this.readMap());
1693
+ return map?.[buildAccount2(serverId, field)] ?? null;
1694
+ } catch {
1695
+ return null;
1696
+ }
1697
+ }
1698
+ /**
1699
+ * One read-modify-write-verify cycle for a whole server's fields.
1700
+ *
1701
+ * `set` pays that cycle — and, encrypted, three scrypt derivations — per
1702
+ * call, serialized behind this store's queue. The settings form resends a
1703
+ * server's full `env` map on any edit, so a 10-variable stdio server spent
1704
+ * ~30 derivations saving an unrelated timeout change.
1705
+ */
1706
+ async setMany(serverId, values) {
1707
+ if (Object.keys(values).length === 0) return;
1708
+ await this.serialize(
1709
+ () => this.mutate((map) => {
1710
+ const next = { ...map };
1711
+ for (const [field, value] of Object.entries(values)) {
1712
+ next[buildAccount2(serverId, field)] = value;
1713
+ }
1714
+ return next;
1715
+ })
1716
+ );
1717
+ }
1718
+ async set(serverId, field, value) {
1719
+ await this.serialize(
1720
+ () => this.mutate((map) => ({
1721
+ ...map,
1722
+ [buildAccount2(serverId, field)]: value
1723
+ }))
1724
+ );
1725
+ }
1726
+ async delete(serverId, field) {
1727
+ await this.deleteWhere((key) => key === buildAccount2(serverId, field));
1728
+ }
1729
+ async deleteAllForServer(serverId) {
1730
+ const prefix = `${serverId}:`;
1731
+ await this.deleteWhere((key) => key.startsWith(prefix));
1732
+ }
1733
+ /**
1734
+ * Shared delete body. Silent on every failure, matching the keyring store:
1735
+ * every reason a delete can fail collapses to "the entry isn't there
1736
+ * anymore", and `set` is the operation that reports a broken store.
1737
+ */
1738
+ async deleteWhere(match) {
1739
+ try {
1740
+ await this.serialize(
1741
+ () => this.mutate((map) => {
1742
+ const keys = Object.keys(map).filter(match);
1743
+ if (keys.length === 0) return null;
1744
+ const next = { ...map };
1745
+ for (const key of keys) delete next[key];
1746
+ return next;
1747
+ })
1748
+ );
1749
+ } catch {
1750
+ }
1751
+ }
1752
+ };
1753
+ async function tightenSecretFilePermissions(filePath) {
1754
+ try {
1755
+ await fs3.chmod(filePath, 384);
1756
+ } catch {
1757
+ }
1758
+ return readSecretFilePermissions(filePath);
1759
+ }
1760
+ async function readSecretFilePermissions(filePath) {
1761
+ try {
1762
+ if (process.platform === "win32") {
1763
+ await fs3.stat(filePath);
1764
+ return { state: "unknown", detail: "not checked on Windows" };
1765
+ }
1766
+ const mode = (await fs3.stat(filePath)).mode & 511;
1767
+ if (mode === 384) return { state: "ok" };
1768
+ return { state: "loose", mode };
1769
+ } catch (err) {
1770
+ const code = err.code;
1771
+ if (code === "ENOENT") {
1772
+ return { state: "absent" };
1773
+ }
1774
+ return { state: "unknown", detail: code ?? "unknown error" };
1775
+ }
1776
+ }
1777
+
1778
+ // ../../core/auth/node/secret-store-selection.ts
1779
+ var SECRET_STORE_ENV = "MCP_INSPECTOR_SECRET_STORE";
1780
+ var SECRET_FILE_ENV = "MCP_INSPECTOR_SECRET_FILE";
1781
+ var STORAGE_DIR_ENV = "MCP_STORAGE_DIR";
1782
+ var KINDS = ["keyring", "file", "memory"];
1783
+ function parseSecretStoreEnv(raw) {
1784
+ const value = raw?.trim().toLowerCase();
1785
+ if (!value) return void 0;
1786
+ if (KINDS.includes(value)) return value;
1787
+ console.warn(
1788
+ `[mcp-inspector] Ignoring ${SECRET_STORE_ENV}="${raw}": expected one of ${KINDS.join(", ")}.`
1789
+ );
1790
+ return void 0;
1791
+ }
1792
+ function defaultSecretFilePath() {
1793
+ const override = process.env[SECRET_FILE_ENV]?.trim();
1794
+ if (override) return path4.resolve(override);
1795
+ const storageDir = process.env[STORAGE_DIR_ENV]?.trim();
1796
+ if (storageDir) return path4.resolve(storageDir, "secrets.json");
1797
+ const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
1798
+ return path4.resolve(homeDir, ".mcp-inspector", "secrets.json");
1799
+ }
1800
+ function isContainer() {
1801
+ if (process.env.KUBERNETES_SERVICE_HOST) return true;
1802
+ try {
1803
+ if (fsSync.existsSync("/.dockerenv") || fsSync.existsSync("/run/.containerenv")) {
1804
+ return true;
1805
+ }
1806
+ } catch {
1807
+ }
1808
+ try {
1809
+ const cgroup = fsSync.readFileSync("/proc/1/cgroup", "utf-8");
1810
+ return /\b(docker|containerd|kubepods|podman|libpod|lxc)\b/.test(cgroup);
1811
+ } catch {
1812
+ return false;
1813
+ }
1814
+ }
1815
+ function nearestExisting(dir) {
1816
+ let current = path4.resolve(dir);
1817
+ while (!fsSync.existsSync(current)) {
1818
+ const parent = path4.dirname(current);
1819
+ if (parent === current) return null;
1820
+ current = parent;
1821
+ }
1822
+ return current;
1823
+ }
1824
+ function unescapeMountPoint(point) {
1825
+ return point.replace(
1826
+ /\\(\d{3})/g,
1827
+ (_match, octal) => String.fromCharCode(parseInt(octal, 8))
1828
+ );
1829
+ }
1830
+ var EPHEMERAL_FSTYPES = /* @__PURE__ */ new Set(["tmpfs", "ramfs"]);
1831
+ function readMountPoints() {
1832
+ try {
1833
+ return fsSync.readFileSync("/proc/self/mountinfo", "utf-8").split("\n").flatMap((line) => {
1834
+ const point = line.split(" ")[4];
1835
+ if (!point) return [];
1836
+ const after = line.split(" - ")[1];
1837
+ const fstype = after?.trim().split(/\s+/)[0];
1838
+ if (fstype && EPHEMERAL_FSTYPES.has(fstype)) return [];
1839
+ return [unescapeMountPoint(point)];
1840
+ }).filter((point) => point !== "/");
1841
+ } catch {
1842
+ return null;
1843
+ }
1844
+ }
1845
+ function isOnMountPoint(dir) {
1846
+ try {
1847
+ const nearest = nearestExisting(dir);
1848
+ if (nearest === null) return false;
1849
+ let current = nearest;
1850
+ try {
1851
+ current = fsSync.realpathSync(nearest);
1852
+ } catch {
1853
+ }
1854
+ const mounts = readMountPoints();
1855
+ if (mounts !== null) {
1856
+ return mounts.some(
1857
+ (point) => current === point || current.startsWith(`${point}/`)
1858
+ );
1859
+ }
1860
+ const parent = path4.dirname(current);
1861
+ if (parent === current) return false;
1862
+ return fsSync.statSync(current).dev !== fsSync.statSync("/").dev;
1863
+ } catch {
1864
+ return false;
1865
+ }
1866
+ }
1867
+ async function buildStore(kind, reason, detail) {
1868
+ if (kind === "keyring") {
1869
+ return {
1870
+ store: new KeyringSecretStore(),
1871
+ info: { kind, reason, durable: true }
1872
+ };
1873
+ }
1874
+ if (kind === "memory") {
1875
+ return {
1876
+ store: new SessionSecretStore(),
1877
+ info: { kind, reason, durable: false, detail }
1878
+ };
1879
+ }
1880
+ const filePath = defaultSecretFilePath();
1881
+ const store = new FileSecretStore({ filePath });
1882
+ await tightenSecretFilePermissions(filePath);
1883
+ return {
1884
+ store,
1885
+ info: await describeFileStore(store, filePath, kind, reason, detail)
1886
+ };
1887
+ }
1888
+ async function describeFileStore(store, filePath, kind, reason, detail) {
1889
+ const onDisk = await store.readOnDiskEncryption();
1890
+ const unreadable = onDisk.state === "unreadable" ? onDisk.detail : void 0;
1891
+ const plaintext = onDisk.state === "absent" ? !store.encrypted : onDisk.state === "plaintext";
1892
+ const perms = await readSecretFilePermissions(filePath);
1893
+ return {
1894
+ kind,
1895
+ reason,
1896
+ path: filePath,
1897
+ // Omitted entirely when the envelope could not be read, so no consumer
1898
+ // can read a default out of it.
1899
+ ...unreadable === void 0 ? { plaintext } : {},
1900
+ ...unreadable !== void 0 ? { encryptionUnknown: unreadable } : {},
1901
+ ...perms.state === "loose" ? { looseMode: perms.mode } : {},
1902
+ ...perms.state === "unknown" ? { permissionsUnknown: perms.detail } : {},
1903
+ // Only meaningful while the two disagree; omitted otherwise so the
1904
+ // payload does not carry a flag that says nothing.
1905
+ ...unreadable === void 0 && plaintext && store.encrypted ? { pendingEncryption: true } : {},
1906
+ durable: true,
1907
+ detail
1908
+ };
1909
+ }
1910
+ function chooseFallbackKind(opts) {
1911
+ return opts.container && !opts.mounted ? "memory" : "file";
1912
+ }
1913
+ function warnAboutSecretStorage(info) {
1914
+ if (info.reason === "fallback") {
1915
+ console.warn(
1916
+ `
1917
+ [mcp-inspector] The OS keychain is not available, so secrets will be kept in: ${secretStorageSummary(info)}.` + (info.detail ? `
1918
+ [mcp-inspector] Keychain error: ${info.detail}` : "") + `
1919
+ [mcp-inspector] Set ${SECRET_STORE_ENV}=keyring|file|memory to choose explicitly.`
1920
+ );
1921
+ }
1922
+ const caveat = secretStorageCaveat(info);
1923
+ if (caveat) console.warn(`[mcp-inspector] ${caveat}`);
1924
+ }
1925
+ var resolved;
1926
+ function resolveSecretStore() {
1927
+ resolved ??= (async () => {
1928
+ const configured = parseSecretStoreEnv(process.env[SECRET_STORE_ENV]);
1929
+ if (configured) {
1930
+ const result2 = await buildStore(configured, "configured");
1931
+ if (configured === "keyring") {
1932
+ await absorbFileSecretsIntoKeyring(result2.store);
1933
+ }
1934
+ warnAboutSecretStorage(result2.info);
1935
+ return result2;
1936
+ }
1937
+ const probe = await probeKeyringAvailable();
1938
+ if (probe.available) {
1939
+ const result2 = await buildStore("keyring", "default");
1940
+ await absorbFileSecretsIntoKeyring(result2.store);
1941
+ return result2;
1942
+ }
1943
+ const kind = chooseFallbackKind({
1944
+ container: isContainer(),
1945
+ mounted: isOnMountPoint(path4.dirname(defaultSecretFilePath()))
1946
+ });
1947
+ const result = await buildStore(kind, "fallback", probe.detail);
1948
+ warnAboutSecretStorage(result.info);
1949
+ return result;
1950
+ })();
1951
+ return resolved;
1952
+ }
1953
+ async function absorbFileSecretsIntoKeyring(keyring) {
1954
+ const filePath = defaultSecretFilePath();
1955
+ if (!await anythingToMigrate(filePath)) return;
1956
+ const claimed = await withSecretFileLock(filePath, async () => {
1957
+ await recoverOrphanedSnapshots(filePath);
1958
+ if (!fsSync.existsSync(filePath)) return null;
1959
+ const staged = `${filePath}.migrating-${process.pid}-${randomUUID()}`;
1960
+ try {
1961
+ await fs4.rename(filePath, staged);
1962
+ } catch (err) {
1963
+ const code = err.code;
1964
+ if (code !== "ENOENT") {
1965
+ console.warn(
1966
+ `
1967
+ [mcp-inspector] Could not claim the secrets file at ${filePath} for migration into the OS keychain (${code ?? "unknown error"}), so it has been left in place. Its secrets are not visible to this session.`
1968
+ );
1969
+ }
1970
+ return null;
1971
+ }
1972
+ const releaseSnapshot = await openSecretFileLock(staged).catch(() => null);
1973
+ return { staged, releaseSnapshot };
1974
+ }).catch((err) => {
1975
+ console.warn(
1976
+ `
1977
+ [mcp-inspector] Could not lock the secrets file at ${filePath} to migrate it into the OS keychain (${err.message}), so it has been left in place. Its secrets are not visible to this session; the next run will try again.`
1978
+ );
1979
+ return null;
1980
+ });
1981
+ if (claimed === null) return;
1982
+ try {
1983
+ await handOffStagedSecrets(claimed.staged, filePath, keyring);
1984
+ } finally {
1985
+ await claimed.releaseSnapshot?.();
1986
+ }
1987
+ }
1988
+ async function handOffStagedSecrets(staged, filePath, keyring) {
1989
+ const file = new FileSecretStore({ filePath: staged });
1990
+ const absorbed = await handOff(file, staged, keyring);
1991
+ if (absorbed) {
1992
+ try {
1993
+ await fs4.rm(staged, { force: true });
1994
+ console.warn(
1995
+ `
1996
+ [mcp-inspector] The OS keychain is available again. Secrets kept in ${filePath} while it was not have been moved into the keychain, and the file has been removed.`
1997
+ );
1998
+ } catch (err) {
1999
+ console.warn(
2000
+ `
2001
+ [mcp-inspector] Secrets were moved into the OS keychain, but the temporary copy at ${staged} could not be removed (${err instanceof Error ? err.message : String(err)}). Delete it once you have confirmed the keychain has your secrets.`
2002
+ );
2003
+ }
2004
+ return;
2005
+ }
2006
+ try {
2007
+ await fs4.link(staged, filePath);
2008
+ await fs4.rm(staged, { force: true });
2009
+ } catch {
2010
+ console.warn(
2011
+ `
2012
+ [mcp-inspector] Could not restore ${staged} to ${filePath}; both files exist and the snapshot holds the secrets that were not migrated. Merge it by hand, or delete it once you have confirmed nothing is missing.`
2013
+ );
2014
+ }
2015
+ }
2016
+ var isSnapshotName = (name, base) => name.startsWith(`${base}.migrating-`) && !name.endsWith(".lock");
2017
+ async function anythingToMigrate(filePath) {
2018
+ const base = path4.basename(filePath);
2019
+ try {
2020
+ return (await fs4.readdir(path4.dirname(filePath))).some(
2021
+ (name) => name === base || isSnapshotName(name, base)
2022
+ );
2023
+ } catch (err) {
2024
+ const code = err.code;
2025
+ return code !== "ENOENT" && code !== "ENOTDIR";
2026
+ }
2027
+ }
2028
+ async function recoverOrphanedSnapshots(filePath) {
2029
+ const dir = path4.dirname(filePath);
2030
+ const base = path4.basename(filePath);
2031
+ let names;
2032
+ try {
2033
+ names = (await fs4.readdir(dir)).filter((n) => isSnapshotName(n, base));
2034
+ } catch {
2035
+ return;
2036
+ }
2037
+ for (const name of names) {
2038
+ const orphan = path4.join(dir, name);
2039
+ if (await isFileLockHeld(orphan)) continue;
2040
+ try {
2041
+ await fs4.link(orphan, filePath);
2042
+ await fs4.rm(orphan, { force: true });
2043
+ console.warn(
2044
+ `
2045
+ [mcp-inspector] Recovered secrets left behind by an interrupted migration (${orphan} \u2192 ${filePath}).`
2046
+ );
2047
+ } catch {
2048
+ console.warn(
2049
+ `
2050
+ [mcp-inspector] Found secrets from an interrupted migration at ${orphan}, and a current ${filePath} already exists, so it was left alone. It may hold values the current file does not.`
2051
+ );
661
2052
  }
662
- return [buildConfigFromOptions(options)];
663
2053
  }
664
- return [];
665
2054
  }
666
-
667
- // ../../core/auth/node/secret-store.ts
668
- var SERVICE_NAME = "mcp-inspector";
669
- var keyringLoad;
670
- var KeyringModuleShapeError = class extends Error {
671
- constructor(detail, options) {
672
- super(`@napi-rs/keyring loaded but ${detail}`, options);
673
- this.name = "KeyringModuleShapeError";
2055
+ async function handOff(file, filePath, keyring) {
2056
+ let entries;
2057
+ try {
2058
+ entries = await file.readAll();
2059
+ } catch (err) {
2060
+ console.warn(
2061
+ `
2062
+ [mcp-inspector] The OS keychain is available again, but the secrets file at ${filePath} could not be read, so its contents were left there: ${err instanceof Error ? err.message : String(err)}`
2063
+ );
2064
+ return false;
674
2065
  }
675
- };
676
- var checkKeyringShape = (mod) => {
2066
+ if (!entries || Object.keys(entries).length === 0) return false;
2067
+ let complete = true;
677
2068
  try {
678
- if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
679
- return { ok: true, mod };
2069
+ for (const [account, value] of Object.entries(entries)) {
2070
+ const parsed = parseAccount(account);
2071
+ if (!parsed) {
2072
+ complete = false;
2073
+ continue;
2074
+ }
2075
+ const existing = await secretStoreGetStrict(
2076
+ keyring,
2077
+ parsed.serverId,
2078
+ parsed.field
2079
+ );
2080
+ if (existing === null) {
2081
+ await keyring.set(parsed.serverId, parsed.field, value);
2082
+ }
680
2083
  }
681
- return {
682
- ok: false,
683
- err: new KeyringModuleShapeError(
684
- "did not expose AsyncEntry / findCredentialsAsync"
685
- )
686
- };
687
2084
  } catch (err) {
688
- return {
689
- ok: false,
690
- err: new KeyringModuleShapeError(
691
- `its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
692
- { cause: err }
693
- )
694
- };
2085
+ console.warn(
2086
+ `
2087
+ [mcp-inspector] Could not move the secrets file at ${filePath} into the OS keychain, so it has been left in place: ${err instanceof Error ? err.message : String(err)}`
2088
+ );
2089
+ return false;
695
2090
  }
696
- };
697
- var loadKeyring = () => {
698
- keyringLoad ??= import("@napi-rs/keyring").then(
699
- checkKeyringShape,
700
- (err) => ({ ok: false, err })
701
- );
702
- return keyringLoad;
703
- };
704
- function parseAccount(account) {
705
- const idx = account.indexOf(":");
706
- if (idx <= 0 || idx === account.length - 1) return null;
707
- return {
708
- serverId: account.slice(0, idx),
709
- field: account.slice(idx + 1)
710
- };
711
- }
712
- var buildAccount = (serverId, field) => `${serverId}:${field}`;
713
- var KeychainUnavailableError = class extends Error {
714
- constructor(cause) {
715
- const message = cause instanceof Error ? cause.message : String(cause);
716
- super(
717
- `OS keychain is not available. ${hintFor(cause, message)} Underlying error: ${message}`
2091
+ if (!complete) {
2092
+ console.warn(
2093
+ `
2094
+ [mcp-inspector] Secrets from ${filePath} have been copied into the OS keychain, but at least one entry could not be read (its key is not in \`serverId:field\` form). The file has been left in place so nothing is lost.`
718
2095
  );
719
- this.name = "KeychainUnavailableError";
2096
+ return false;
720
2097
  }
721
- };
722
- var hintFor = (cause, message) => {
723
- if (cause instanceof KeyringModuleShapeError) {
724
- return `The @napi-rs/keyring package loaded but does not expose the API this build expects \u2014 most likely a version or packaging mismatch; reinstall the Inspector, and report this if it persists.`;
2098
+ return true;
2099
+ }
2100
+ var DeferredSecretStore = class {
2101
+ async target() {
2102
+ return (await resolveSecretStore()).store;
725
2103
  }
726
- if (message.includes("Cannot find native binding")) {
727
- return `The @napi-rs/keyring platform package for this OS is missing or unavailable \u2014 reinstall the Inspector (for npx, clear the npx cache under your npm cache directory first).`;
2104
+ async isDurable() {
2105
+ return secretStoreIsDurable(await this.target());
2106
+ }
2107
+ /**
2108
+ * Forwarded, and its absence was a real defect rather than an omission:
2109
+ * this is the store production actually uses, so a `secretStoreGetStrict`
2110
+ * call against it fell through to the tolerant `get` and the strictness
2111
+ * existed only in tests that injected a concrete store.
2112
+ */
2113
+ async getStrict(serverId, field) {
2114
+ return secretStoreGetStrict(await this.target(), serverId, field);
2115
+ }
2116
+ async getMany(requests) {
2117
+ return secretStoreGetMany(await this.target(), requests);
728
2118
  }
729
- return `On Linux, install libsecret / gnome-keyring.`;
730
- };
731
- var KeyringSecretStore = class {
732
2119
  async get(serverId, field) {
733
- try {
734
- const keyring = await loadKeyring();
735
- if (!keyring.ok) return null;
736
- const entry = new keyring.mod.AsyncEntry(
737
- SERVICE_NAME,
738
- buildAccount(serverId, field)
739
- );
740
- const v = await entry.getPassword();
741
- return v ?? null;
742
- } catch {
743
- return null;
744
- }
2120
+ return (await this.target()).get(serverId, field);
745
2121
  }
746
2122
  async set(serverId, field, value) {
747
- try {
748
- const keyring = await loadKeyring();
749
- if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
750
- const entry = new keyring.mod.AsyncEntry(
751
- SERVICE_NAME,
752
- buildAccount(serverId, field)
753
- );
754
- await entry.setPassword(value);
755
- } catch (err) {
756
- if (err instanceof KeychainUnavailableError) throw err;
757
- throw new KeychainUnavailableError(err);
758
- }
2123
+ await (await this.target()).set(serverId, field, value);
2124
+ }
2125
+ async setMany(serverId, values) {
2126
+ await secretStoreSetMany(await this.target(), serverId, values);
759
2127
  }
760
2128
  async delete(serverId, field) {
761
- try {
762
- const keyring = await loadKeyring();
763
- if (!keyring.ok) return;
764
- const entry = new keyring.mod.AsyncEntry(
765
- SERVICE_NAME,
766
- buildAccount(serverId, field)
767
- );
768
- await entry.deleteCredential();
769
- } catch {
770
- }
2129
+ await (await this.target()).delete(serverId, field);
771
2130
  }
772
2131
  async deleteAllForServer(serverId) {
773
- let creds;
774
- try {
775
- const keyring = await loadKeyring();
776
- if (!keyring.ok) return;
777
- creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
778
- } catch {
779
- return;
780
- }
781
- const prefix = `${serverId}:`;
782
- for (const c of creds) {
783
- if (!c.account.startsWith(prefix)) continue;
784
- const parsed = parseAccount(c.account);
785
- if (!parsed || parsed.serverId !== serverId) continue;
786
- await this.delete(serverId, parsed.field);
787
- }
2132
+ await (await this.target()).deleteAllForServer(serverId);
788
2133
  }
789
2134
  };
2135
+ function defaultSecretStore() {
2136
+ return new DeferredSecretStore();
2137
+ }
790
2138
 
791
2139
  // ../../core/mcp/node/server-secrets.ts
792
2140
  async function rehydrateMcpConfigFromKeychain(config, secretStore) {
793
- const out = { mcpServers: {} };
794
- await Promise.all(
795
- Object.entries(config.mcpServers).map(async ([id, stored]) => {
796
- const fields = expectedSecretFields(stored);
797
- const secrets = {};
798
- await Promise.all(
799
- fields.map(async (field) => {
800
- const value = await secretStore.get(id, field);
801
- if (value !== null) {
802
- secrets[field] = value;
803
- }
804
- })
805
- );
806
- out.mcpServers[id] = mergeSecretsIntoStored(stored, secrets);
807
- })
2141
+ const entries = Object.entries(config.mcpServers);
2142
+ const secrets = await secretStoreGetMany(
2143
+ secretStore,
2144
+ entries.map(([serverId, stored]) => ({
2145
+ serverId,
2146
+ fields: expectedSecretFields(stored)
2147
+ }))
808
2148
  );
2149
+ const out = { mcpServers: {} };
2150
+ for (const [id, stored] of entries) {
2151
+ out.mcpServers[id] = mergeSecretsIntoStored(stored, secrets[id] ?? {});
2152
+ }
809
2153
  return out;
810
2154
  }
811
2155
 
@@ -817,7 +2161,7 @@ function headersToServerSettings(headers) {
817
2161
  return {
818
2162
  headers: Object.entries(headers).map(([key, value]) => ({ key, value })),
819
2163
  env: [],
820
- metadata: [],
2164
+ metadata: {},
821
2165
  connectionTimeout: 0,
822
2166
  requestTimeout: 0,
823
2167
  taskTtl: DEFAULT_TASK_TTL_MS,
@@ -846,7 +2190,7 @@ async function loadServerEntries(serverOptions) {
846
2190
  const source = resolveServerSource(serverOptions);
847
2191
  if (source) {
848
2192
  let config = readServerListFile(source.path, source.writable);
849
- const secretStore = serverOptions.secretStore ?? new KeyringSecretStore();
2193
+ const secretStore = serverOptions.secretStore ?? defaultSecretStore();
850
2194
  config = await rehydrateMcpConfigFromKeychain(config, secretStore);
851
2195
  const entries = mcpConfigToServerEntries(config);
852
2196
  const result = {};
@@ -1282,6 +2626,17 @@ function parseWwwAuthenticateBearer(header) {
1282
2626
  errorDescription: params.error_description
1283
2627
  };
1284
2628
  }
2629
+ function challengeResourceMetadataUrl(challenge) {
2630
+ const value = challenge.resourceMetadataUrl?.trim();
2631
+ if (!value) {
2632
+ return void 0;
2633
+ }
2634
+ try {
2635
+ return new URL(value);
2636
+ } catch {
2637
+ return void 0;
2638
+ }
2639
+ }
1285
2640
  function parseScopeString(scope) {
1286
2641
  if (!scope?.trim()) {
1287
2642
  return [];
@@ -1331,6 +2686,7 @@ function parseAuthChallengeFromResponse(response, context) {
1331
2686
  return {
1332
2687
  reason: reasonFromHttpResponse(status, bearer),
1333
2688
  ...requiredScopes.length > 0 ? { requiredScopes } : {},
2689
+ ...bearer.resourceMetadata ? { resourceMetadataUrl: bearer.resourceMetadata } : {},
1334
2690
  ...bearer.errorDescription ? { message: bearer.errorDescription } : {},
1335
2691
  ...context ? { context } : {},
1336
2692
  raw: {
@@ -1371,6 +2727,7 @@ function parseAuthChallengeFromError(err, context) {
1371
2727
  return {
1372
2728
  reason: reasonFromHttpResponse(status, bearer),
1373
2729
  ...requiredScopes.length > 0 ? { requiredScopes } : {},
2730
+ ...bearer.resourceMetadata ? { resourceMetadataUrl: bearer.resourceMetadata } : {},
1374
2731
  ...context ? { context } : {},
1375
2732
  raw: {
1376
2733
  httpStatus: status,
@@ -1417,8 +2774,26 @@ function createAuthChallengeInterceptFetch(baseFetch) {
1417
2774
  );
1418
2775
  };
1419
2776
  }
2777
+ function createAuthChallengeObserverFetch(baseFetch, onChallenge) {
2778
+ return async (input, init) => {
2779
+ const response = await baseFetch(input, init);
2780
+ if (response.status === 401 || response.status === 403) {
2781
+ const challenge = parseAuthChallengeFromResponse(response);
2782
+ if (challenge) {
2783
+ onChallenge(challenge);
2784
+ }
2785
+ }
2786
+ return response;
2787
+ };
2788
+ }
1420
2789
 
1421
- // ../../core/mcp/node/transport.ts
2790
+ // ../../core/mcp/node/proxyFetch.ts
2791
+ function asUndiciInit(init) {
2792
+ return init;
2793
+ }
2794
+ function asBodyInit(body) {
2795
+ return body;
2796
+ }
1422
2797
  var PROXY_ENV_VARS = [
1423
2798
  "HTTPS_PROXY",
1424
2799
  "https_proxy",
@@ -1432,27 +2807,95 @@ function readProxyEnv() {
1432
2807
  }
1433
2808
  return void 0;
1434
2809
  }
1435
- function withProxyDispatcher(baseFetch) {
1436
- if (readProxyEnv() === void 0) return baseFetch;
1437
- let dispatcherPromise;
1438
- const getDispatcher = () => {
1439
- dispatcherPromise ??= import("./undici-KNXAVD2F.js").then(
1440
- ({ EnvHttpProxyAgent }) => new EnvHttpProxyAgent(),
1441
- (cause) => {
1442
- throw new Error(
1443
- "HTTPS_PROXY / HTTP_PROXY is set but the `undici` package is not available. Install it (it ships with the CLI client) or unset the proxy env var.",
1444
- { cause }
1445
- );
1446
- }
2810
+ var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
2811
+ 101,
2812
+ 103,
2813
+ 204,
2814
+ 205,
2815
+ 304
2816
+ ]);
2817
+ function toGlobalResponse(res) {
2818
+ const headers = new Headers();
2819
+ for (const [key, value] of res.headers) headers.append(key, value);
2820
+ const out = new Response(
2821
+ NULL_BODY_STATUSES.has(res.status) ? null : asBodyInit(res.body),
2822
+ {
2823
+ status: res.status,
2824
+ statusText: res.statusText,
2825
+ headers
2826
+ }
2827
+ );
2828
+ for (const [key, value] of [
2829
+ ["url", res.url],
2830
+ ["redirected", res.redirected],
2831
+ ["type", res.type]
2832
+ ]) {
2833
+ Object.defineProperty(out, key, {
2834
+ value,
2835
+ configurable: true,
2836
+ enumerable: false
2837
+ });
2838
+ }
2839
+ return out;
2840
+ }
2841
+ var loadedProxiedFetch;
2842
+ async function loadProxiedFetch() {
2843
+ let undici;
2844
+ try {
2845
+ undici = await import("undici");
2846
+ } catch (cause) {
2847
+ throw new Error(
2848
+ "HTTPS_PROXY / HTTP_PROXY is set but the `undici` package could not be loaded, so requests cannot be routed through the proxy. This is a packaging fault rather than something to fix by installing undici; please report it, or unset the proxy env var to fall back to a direct connection.",
2849
+ { cause }
2850
+ );
2851
+ }
2852
+ const dispatcher = new undici.EnvHttpProxyAgent();
2853
+ return async (input, init) => {
2854
+ const [target, requestInit] = toUndiciRequest(input, init);
2855
+ return toGlobalResponse(
2856
+ await undici.fetch(target, { ...asUndiciInit(requestInit), dispatcher })
1447
2857
  );
1448
- return dispatcherPromise;
1449
2858
  };
1450
- const proxiedFetch = async (input, init) => {
1451
- const dispatcher = await getDispatcher();
1452
- return baseFetch(input, { ...init, dispatcher });
2859
+ }
2860
+ function toUndiciRequest(input, init) {
2861
+ if (input instanceof Request) {
2862
+ return [
2863
+ input.url,
2864
+ {
2865
+ method: input.method,
2866
+ headers: input.headers,
2867
+ // A stream body needs `duplex: "half"`; the field is undici's, not in
2868
+ // the DOM `RequestInit`, so it rides along as an extra property.
2869
+ ...input.body ? { body: input.body, duplex: "half" } : {},
2870
+ signal: input.signal,
2871
+ // Everything else the Request carries. Dropping these would silently
2872
+ // change fetch semantics under a proxy — most visibly `redirect`, where
2873
+ // a `new Request(url, { redirect: "manual" })` would start following
2874
+ // redirects instead of surfacing them.
2875
+ redirect: input.redirect,
2876
+ cache: input.cache,
2877
+ credentials: input.credentials,
2878
+ integrity: input.integrity,
2879
+ keepalive: input.keepalive,
2880
+ mode: input.mode,
2881
+ referrer: input.referrer,
2882
+ referrerPolicy: input.referrerPolicy,
2883
+ // An explicit `init` still wins, per the fetch contract.
2884
+ ...init
2885
+ }
2886
+ ];
2887
+ }
2888
+ return [input instanceof URL ? input.href : input, init ?? {}];
2889
+ }
2890
+ function createProxyFetch() {
2891
+ if (readProxyEnv() === void 0) return void 0;
2892
+ return async (input, init) => {
2893
+ loadedProxiedFetch ??= loadProxiedFetch();
2894
+ return (await loadedProxiedFetch)(input, init);
1453
2895
  };
1454
- return proxiedFetch;
1455
2896
  }
2897
+
2898
+ // ../../core/mcp/node/transport.ts
1456
2899
  function headersFromSettings(settings) {
1457
2900
  if (!settings || settings.headers.length === 0) return void 0;
1458
2901
  const out = {};
@@ -1472,10 +2915,12 @@ function createTransportNode(config, options = {}) {
1472
2915
  onFetchResponseBody,
1473
2916
  authProvider,
1474
2917
  settings,
1475
- interceptAuthChallenges = false
2918
+ interceptAuthChallenges = false,
2919
+ onAuthChallengeObserved
1476
2920
  } = options;
1477
- const baseFetch = withProxyDispatcher(optionsFetchFn ?? globalThis.fetch);
1478
- const fetchWithOptionalAuthIntercept = interceptAuthChallenges ? createAuthChallengeInterceptFetch(baseFetch) : baseFetch;
2921
+ const baseFetch = optionsFetchFn ?? createProxyFetch() ?? globalThis.fetch;
2922
+ const withChallengeObserver = (inner) => onAuthChallengeObserved ? createAuthChallengeObserverFetch(inner, onAuthChallengeObserved) : inner;
2923
+ const fetchWithOptionalAuthIntercept = interceptAuthChallenges ? createAuthChallengeInterceptFetch(withChallengeObserver(baseFetch)) : withChallengeObserver(baseFetch);
1479
2924
  if (serverType === "stdio") {
1480
2925
  const stdioConfig = config;
1481
2926
  const transport = new StdioClientTransport({
@@ -1500,7 +2945,8 @@ function createTransportNode(config, options = {}) {
1500
2945
  } else if (serverType === "sse") {
1501
2946
  const sseConfig = config;
1502
2947
  const url = new URL(sseConfig.url);
1503
- const sseFetch = sseConfig.eventSourceInit?.fetch || fetchWithOptionalAuthIntercept;
2948
+ const configuredSseFetch = sseConfig.eventSourceInit?.fetch;
2949
+ const sseFetch = configuredSseFetch ? withChallengeObserver(configuredSseFetch) : fetchWithOptionalAuthIntercept;
1504
2950
  const trackedFetch = onFetchRequest ? createFetchTracker(sseFetch, {
1505
2951
  trackRequest: onFetchRequest,
1506
2952
  updateResponseBody: onFetchResponseBody
@@ -1699,7 +3145,8 @@ async function migrateClientPlaintextSecret(filePath, config, secretStore) {
1699
3145
  const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
1700
3146
  if (!value) return config;
1701
3147
  try {
1702
- const existing = await secretStore.get(
3148
+ const existing = await secretStoreGetStrict(
3149
+ secretStore,
1703
3150
  CLIENT_KEYCHAIN_ID,
1704
3151
  SECRET_FIELD_IDP_CLIENT_SECRET
1705
3152
  );
@@ -1710,10 +3157,11 @@ async function migrateClientPlaintextSecret(filePath, config, secretStore) {
1710
3157
  value
1711
3158
  );
1712
3159
  }
3160
+ if (!await secretStoreIsDurable(secretStore)) return config;
1713
3161
  await writeStoreFile(filePath, serializeStore(stripped));
1714
3162
  return stripped;
1715
3163
  } catch (err) {
1716
- if (err instanceof KeychainUnavailableError) {
3164
+ if (err instanceof SecretStoreUnavailableError) {
1717
3165
  return config;
1718
3166
  }
1719
3167
  throw err;
@@ -1770,7 +3218,7 @@ function getActiveCimdClientMetadataUrl(config) {
1770
3218
  // ../../core/client/runner.ts
1771
3219
  async function loadRunnerClientConfig(options) {
1772
3220
  const customPath = options?.clientConfigPath?.trim() || process.env.MCP_CLIENT_CONFIG_PATH?.trim() || void 0;
1773
- const secretStore = options?.secretStore ?? new KeyringSecretStore();
3221
+ const secretStore = options?.secretStore ?? defaultSecretStore();
1774
3222
  return loadClientConfig({ filePath: customPath, secretStore });
1775
3223
  }
1776
3224
  function isOAuthCapableServerConfig(config) {
@@ -1782,7 +3230,7 @@ function buildRunnerClientAuthOptions(clientConfig, savedSettings, cliOverrides)
1782
3230
  const activeCimdUrl = getActiveCimdClientMetadataUrl(clientConfig);
1783
3231
  const serverAuthorizationParams = savedSettings ? oauthAuthorizationParamsFromSettings(savedSettings) : void 0;
1784
3232
  const serverEndpointOverrides = savedSettings ? oauthEndpointOverridesFromSettings(savedSettings) : void 0;
1785
- const oauthFromServer = savedSettings && (savedSettings.oauthClientId || savedSettings.oauthClientSecret || savedSettings.oauthScopes || serverAuthorizationParams || serverEndpointOverrides || savedSettings.enterpriseManaged) ? {
3233
+ const oauthFromServer = savedSettings && (savedSettings.oauthClientId || savedSettings.oauthClientSecret || savedSettings.oauthScopes || serverAuthorizationParams || serverEndpointOverrides || savedSettings.enterpriseManaged || savedSettings.oauthRequestRefreshToken === false) ? {
1786
3234
  ...savedSettings.oauthClientId && {
1787
3235
  clientId: savedSettings.oauthClientId
1788
3236
  },
@@ -1798,6 +3246,11 @@ function buildRunnerClientAuthOptions(clientConfig, savedSettings, cliOverrides)
1798
3246
  ...serverEndpointOverrides,
1799
3247
  ...savedSettings.enterpriseManaged && {
1800
3248
  enterpriseManaged: true
3249
+ },
3250
+ // #2068: only the explicit opt-out is forwarded; omitting the key
3251
+ // leaves the provider's default (declare `refresh_token`) in place.
3252
+ ...savedSettings.oauthRequestRefreshToken === false && {
3253
+ requestRefreshToken: false
1801
3254
  }
1802
3255
  } : void 0;
1803
3256
  const clientMetadataUrl = cliOverrides?.clientMetadataUrl?.trim() || activeCimdUrl;
@@ -1908,7 +3361,7 @@ function formatRunnerOAuthRedirectUrl(config) {
1908
3361
  // src/App.tsx
1909
3362
  import {
1910
3363
  useState as useState23,
1911
- useMemo as useMemo3,
3364
+ useMemo as useMemo4,
1912
3365
  useEffect as useEffect20,
1913
3366
  useCallback as useCallback11,
1914
3367
  useRef as useRef13
@@ -1917,6 +3370,10 @@ import { Box as Box27, Text as Text24, useInput as useInput21, useApp } from "in
1917
3370
 
1918
3371
  // ../../core/mcp/inspectorClient.ts
1919
3372
  import { Client } from "@modelcontextprotocol/client";
3373
+ import {
3374
+ ProgressTokenSchema,
3375
+ RequestMetaSchema
3376
+ } from "@modelcontextprotocol/core";
1920
3377
 
1921
3378
  // ../../core/json/xMcpHeader.ts
1922
3379
  var X_MCP_HEADER_KEY = "x-mcp-header";
@@ -1954,8 +3411,8 @@ var OBJECT_VALUED_SUBSCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
1954
3411
  "$defs",
1955
3412
  "definitions"
1956
3413
  ]);
1957
- function pathName(path3) {
1958
- return path3.length === 0 ? "<root>" : path3.join(".");
3414
+ function pathName(path6) {
3415
+ return path6.length === 0 ? "<root>" : path6.join(".");
1959
3416
  }
1960
3417
  function isRecord(node) {
1961
3418
  return node !== null && typeof node === "object";
@@ -1963,23 +3420,23 @@ function isRecord(node) {
1963
3420
  function scanXMcpHeaderDeclarations(inputSchema) {
1964
3421
  const declarations = [];
1965
3422
  const seenLower = /* @__PURE__ */ new Map();
1966
- const visit = (node, path3, reachable) => {
3423
+ const visit = (node, path6, reachable) => {
1967
3424
  if (!isRecord(node)) return void 0;
1968
3425
  const schema = node;
1969
3426
  if (X_MCP_HEADER_KEY in schema) {
1970
- if (!reachable || path3.length === 0) {
1971
- return `${pathName(path3)}: x-mcp-header is only permitted on properties statically reachable via a chain of 'properties' keys (not under items, additionalProperties, oneOf/anyOf/allOf/not, if/then/else, or $ref)`;
3427
+ if (!reachable || path6.length === 0) {
3428
+ return `${pathName(path6)}: x-mcp-header is only permitted on properties statically reachable via a chain of 'properties' keys (not under items, additionalProperties, oneOf/anyOf/allOf/not, if/then/else, or $ref)`;
1972
3429
  }
1973
3430
  const raw = schema[X_MCP_HEADER_KEY];
1974
3431
  if (typeof raw !== "string" || raw.length === 0) {
1975
- return `${pathName(path3)}: x-mcp-header MUST be a non-empty string`;
3432
+ return `${pathName(path6)}: x-mcp-header MUST be a non-empty string`;
1976
3433
  }
1977
3434
  if (!RFC9110_TOKEN.test(raw)) {
1978
- return `${pathName(path3)}: x-mcp-header '${raw}' is not a valid RFC 9110 token (no spaces, control characters or HTTP delimiters)`;
3435
+ return `${pathName(path6)}: x-mcp-header '${raw}' is not a valid RFC 9110 token (no spaces, control characters or HTTP delimiters)`;
1979
3436
  }
1980
3437
  const type = typeof schema.type === "string" ? schema.type : void 0;
1981
3438
  if (type === void 0 || !PERMITTED_X_MCP_HEADER_TYPES.has(type)) {
1982
- return `${pathName(path3)}: x-mcp-header is only permitted on primitive-typed properties (string, integer, boolean); got ${type ?? "<none>"}`;
3439
+ return `${pathName(path6)}: x-mcp-header is only permitted on primitive-typed properties (string, integer, boolean); got ${type ?? "<none>"}`;
1983
3440
  }
1984
3441
  const lower = raw.toLowerCase();
1985
3442
  const prior = seenLower.get(lower);
@@ -1987,12 +3444,12 @@ function scanXMcpHeaderDeclarations(inputSchema) {
1987
3444
  return `x-mcp-header '${raw}' is not case-insensitively unique (also declared as '${prior}')`;
1988
3445
  }
1989
3446
  seenLower.set(lower, raw);
1990
- declarations.push({ path: path3, headerName: raw, type });
3447
+ declarations.push({ path: path6, headerName: raw, type });
1991
3448
  }
1992
3449
  const properties = schema.properties;
1993
3450
  if (isRecord(properties)) {
1994
3451
  for (const [key, child] of Object.entries(properties)) {
1995
- const fault2 = visit(child, [...path3, key], reachable);
3452
+ const fault2 = visit(child, [...path6, key], reachable);
1996
3453
  if (fault2 !== void 0) return fault2;
1997
3454
  }
1998
3455
  }
@@ -2001,7 +3458,7 @@ function scanXMcpHeaderDeclarations(inputSchema) {
2001
3458
  if (sub === void 0) continue;
2002
3459
  const branches = Array.isArray(sub) ? sub : isRecord(sub) && OBJECT_VALUED_SUBSCHEMA_KEYWORDS.has(k) ? Object.values(sub) : [sub];
2003
3460
  for (const branch of branches) {
2004
- const fault2 = visit(branch, [...path3, `<${k}>`], false);
3461
+ const fault2 = visit(branch, [...path6, `<${k}>`], false);
2005
3462
  if (fault2 !== void 0) return fault2;
2006
3463
  }
2007
3464
  }
@@ -2046,9 +3503,9 @@ function utf8ToBase64(s) {
2046
3503
  function encodeMcpParamValue(value) {
2047
3504
  return needsBase64(value) ? `${BASE64_SENTINEL_PREFIX}${utf8ToBase64(value)}${BASE64_SENTINEL_SUFFIX}` : value;
2048
3505
  }
2049
- function valueAtPath(root, path3) {
3506
+ function valueAtPath(root, path6) {
2050
3507
  let node = root;
2051
- for (const key of path3) {
3508
+ for (const key of path6) {
2052
3509
  if (node === null || typeof node !== "object") return void 0;
2053
3510
  node = node[key];
2054
3511
  }
@@ -2269,12 +3726,84 @@ function buildClientExtensions(input) {
2269
3726
  map[ext.key] = ext.advertisement ? structuredClone(ext.advertisement) : {};
2270
3727
  }
2271
3728
  }
3729
+ const uiAdvertisement = map[UI_EXTENSION_KEY];
3730
+ if (input.appElicitation && uiAdvertisement) {
3731
+ map[UI_EXTENSION_KEY] = { ...uiAdvertisement, elicitation: {} };
3732
+ }
2272
3733
  if (input.enterpriseManaged) {
2273
3734
  map[EMA_EXTENSION_KEY] = {};
2274
3735
  }
2275
3736
  return map;
2276
3737
  }
2277
3738
 
3739
+ // ../../core/mcp/appElicitation.ts
3740
+ import { ElicitResultSchema } from "@modelcontextprotocol/core";
3741
+ function uiExtension(capabilities) {
3742
+ const ext = capabilities?.extensions?.[UI_EXTENSION_KEY];
3743
+ return typeof ext === "object" && ext !== null ? ext : void 0;
3744
+ }
3745
+ function getUiClientCapability(capabilities) {
3746
+ return uiExtension(capabilities);
3747
+ }
3748
+ function getUiServerCapability(capabilities) {
3749
+ return uiExtension(capabilities);
3750
+ }
3751
+ function supportsAppElicitation(clientCapabilities, serverCapabilities) {
3752
+ const clientUi = getUiClientCapability(clientCapabilities);
3753
+ const serverUi = getUiServerCapability(serverCapabilities);
3754
+ return Boolean(
3755
+ clientCapabilities?.elicitation?.form && clientUi?.mimeTypes?.includes(MCP_APP_MIME_TYPE) && clientUi.elicitation && serverUi?.elicitation
3756
+ );
3757
+ }
3758
+ var ELICITATION_UI_URI_ERROR = "Elicitation UI resourceUri must be an absolute ui:// URI";
3759
+ function validateElicitationUiResourceUri(resourceUri) {
3760
+ let parsed;
3761
+ try {
3762
+ parsed = new URL(resourceUri);
3763
+ } catch {
3764
+ throw new Error(ELICITATION_UI_URI_ERROR);
3765
+ }
3766
+ if (parsed.protocol !== "ui:" || !/^ui:\/\//i.test(resourceUri) || parsed.host.length === 0) {
3767
+ throw new Error(ELICITATION_UI_URI_ERROR);
3768
+ }
3769
+ }
3770
+ function getElicitationUiResourceUri(params) {
3771
+ const ui = params._meta?.ui;
3772
+ if (typeof ui !== "object" || ui === null) return void 0;
3773
+ const resourceUri = ui.resourceUri;
3774
+ if (resourceUri === void 0) return void 0;
3775
+ if (typeof resourceUri !== "string") {
3776
+ throw new Error("Elicitation UI resourceUri must be a string");
3777
+ }
3778
+ validateElicitationUiResourceUri(resourceUri);
3779
+ return resourceUri;
3780
+ }
3781
+ function isFormElicitation(params) {
3782
+ const mode = params.mode;
3783
+ return mode === void 0 || mode === "form";
3784
+ }
3785
+ var ELICIT_RESULT_ERROR = "App returned an invalid elicitation result";
3786
+ function validateAppElicitResult(provider, params, result) {
3787
+ const parsed = ElicitResultSchema.safeParse(result);
3788
+ if (!parsed.success) {
3789
+ return `${ELICIT_RESULT_ERROR}: ${parsed.error.issues[0]?.message ?? "does not match ElicitResult"}`;
3790
+ }
3791
+ if (parsed.data.action !== "accept") return void 0;
3792
+ const content = parsed.data.content;
3793
+ if (typeof content !== "object" || content === null || Array.isArray(content)) {
3794
+ return "App accepted the elicitation without a content object";
3795
+ }
3796
+ const schema = params.requestedSchema;
3797
+ if (typeof schema !== "object" || schema === null) return void 0;
3798
+ try {
3799
+ const validate = provider.getValidator(schema);
3800
+ const validation = validate(content);
3801
+ return validation.valid ? void 0 : `App content does not match the requested schema: ${validation.errorMessage}`;
3802
+ } catch {
3803
+ return void 0;
3804
+ }
3805
+ }
3806
+
2278
3807
  // ../../core/mcp/inspectorClient.ts
2279
3808
  import {
2280
3809
  EmptyResultSchema,
@@ -2365,10 +3894,10 @@ function labelForRawItem(value) {
2365
3894
  function describeIssues(error) {
2366
3895
  const [issue] = error.issues;
2367
3896
  if (!issue) return "did not match the expected shape";
2368
- const path3 = issue.path.join(".");
3897
+ const path6 = issue.path.join(".");
2369
3898
  const detail = expectedDetail(issue);
2370
3899
  const message = detail ? `${issue.message} (${detail})` : issue.message;
2371
- return path3.length > 0 ? `${path3}: ${message}` : message;
3900
+ return path6.length > 0 ? `${path6}: ${message}` : message;
2372
3901
  }
2373
3902
  function expectedDetail(issue) {
2374
3903
  if (!("expected" in issue) || typeof issue.expected !== "string") {
@@ -2726,7 +4255,7 @@ var SamplingCreateMessage = class {
2726
4255
  resolvePromise;
2727
4256
  rejectPromise;
2728
4257
  onRemove;
2729
- constructor(request, resolve4, reject, onRemove, origin = "server-request") {
4258
+ constructor(request, resolve7, reject, onRemove, origin = "server-request") {
2730
4259
  this.onRemove = onRemove;
2731
4260
  this.id = `sampling-${crypto.randomUUID()}`;
2732
4261
  this.timestamp = /* @__PURE__ */ new Date();
@@ -2734,7 +4263,7 @@ var SamplingCreateMessage = class {
2734
4263
  const relatedTask = request.params?._meta?.[RELATED_TASK_META_KEY];
2735
4264
  this.taskId = relatedTask?.taskId;
2736
4265
  this.origin = origin;
2737
- this.resolvePromise = resolve4;
4266
+ this.resolvePromise = resolve7;
2738
4267
  this.rejectPromise = reject;
2739
4268
  }
2740
4269
  /**
@@ -2826,7 +4355,7 @@ var ElicitationCreateMessage = class {
2826
4355
  */
2827
4356
  rejectCallback;
2828
4357
  onRemove;
2829
- constructor(request, resolve4, onRemove, reject, origin = "server-request") {
4358
+ constructor(request, resolve7, onRemove, reject, origin = "server-request") {
2830
4359
  this.onRemove = onRemove;
2831
4360
  this.id = `elicitation-${crypto.randomUUID()}`;
2832
4361
  this.timestamp = /* @__PURE__ */ new Date();
@@ -2834,7 +4363,7 @@ var ElicitationCreateMessage = class {
2834
4363
  const relatedTask = request.params?._meta?.[RELATED_TASK_META_KEY2];
2835
4364
  this.taskId = relatedTask?.taskId;
2836
4365
  this.origin = origin;
2837
- this.resolvePromise = resolve4;
4366
+ this.resolvePromise = resolve7;
2838
4367
  this.rejectCallback = reject;
2839
4368
  }
2840
4369
  /**
@@ -3022,7 +4551,7 @@ function isAuthorizationServerMetadataRequest(input) {
3022
4551
  } catch {
3023
4552
  pathname = raw;
3024
4553
  }
3025
- return AS_METADATA_WELL_KNOWN_PATHS.some((path3) => pathname.includes(path3));
4554
+ return AS_METADATA_WELL_KNOWN_PATHS.some((path6) => pathname.includes(path6));
3026
4555
  }
3027
4556
  function isJsonDocumentResponse(contentType) {
3028
4557
  if (!contentType) return false;
@@ -3127,6 +4656,47 @@ function applyAuthorizationParams(authorizationUrl, params) {
3127
4656
  return merged;
3128
4657
  }
3129
4658
 
4659
+ // ../../core/auth/scopes.ts
4660
+ import {
4661
+ computeScopeUnion as sdkComputeScopeUnion,
4662
+ isStrictScopeSuperset as sdkIsStrictScopeSuperset
4663
+ } from "@modelcontextprotocol/client";
4664
+ var computeScopeUnion = sdkComputeScopeUnion;
4665
+ var isStrictScopeSuperset = sdkIsStrictScopeSuperset;
4666
+ var OFFLINE_ACCESS_SCOPE = "offline_access";
4667
+ function scopeForDeclinedRefreshGrant(effectiveScope, configuredScope, requiredScopes) {
4668
+ if (!effectiveScope) return effectiveScope;
4669
+ const configured = configuredScope?.trim().split(/\s+/) ?? [];
4670
+ const required = requiredScopes ?? [];
4671
+ if (configured.includes(OFFLINE_ACCESS_SCOPE) || required.includes(OFFLINE_ACCESS_SCOPE)) {
4672
+ const tokens = effectiveScope.trim().split(/\s+/).filter((token) => token !== "");
4673
+ if (!tokens.includes(OFFLINE_ACCESS_SCOPE)) {
4674
+ tokens.push(OFFLINE_ACCESS_SCOPE);
4675
+ }
4676
+ return tokens.join(" ");
4677
+ }
4678
+ const kept = effectiveScope.trim().split(/\s+/).filter((token) => token !== "" && token !== OFFLINE_ACCESS_SCOPE);
4679
+ if (kept.length > 0) return kept.join(" ");
4680
+ const fallback = configuredScope?.trim();
4681
+ if (fallback) return fallback;
4682
+ return void 0;
4683
+ }
4684
+ function resolvePersistedScopeAfterGrant(grantedScope, requestedScope) {
4685
+ const granted = grantedScope?.trim();
4686
+ if (granted) {
4687
+ return granted;
4688
+ }
4689
+ const requested = requestedScope?.trim();
4690
+ return requested || void 0;
4691
+ }
4692
+ function resolveEffectiveGrantedScope(storedScope, tokenScope) {
4693
+ const granted = tokenScope?.trim();
4694
+ if (granted) {
4695
+ return granted;
4696
+ }
4697
+ return computeScopeUnion(storedScope, tokenScope);
4698
+ }
4699
+
3130
4700
  // ../../core/auth/providers.ts
3131
4701
  var MutableRedirectUrlProvider = class {
3132
4702
  redirectUrl = "";
@@ -3164,6 +4734,10 @@ var BaseOAuthClientProvider = class {
3164
4734
  clientMetadataUrl;
3165
4735
  /** Custom authorization-request parameters (#2018). Authorize URL only. */
3166
4736
  authorizationParams;
4737
+ /** Declare the `refresh_token` grant in {@link clientMetadata} (#2068). */
4738
+ requestRefreshToken;
4739
+ /** Server-configured scope, for the #2068 `offline_access` distinction. */
4740
+ configuredScope;
3167
4741
  constructor(serverUrl, oauthConfig) {
3168
4742
  this.serverUrl = serverUrl;
3169
4743
  this.storage = oauthConfig.storage;
@@ -3171,6 +4745,8 @@ var BaseOAuthClientProvider = class {
3171
4745
  this.navigation = oauthConfig.navigation;
3172
4746
  this.clientMetadataUrl = oauthConfig.clientMetadataUrl;
3173
4747
  this.authorizationParams = oauthConfig.authorizationParams;
4748
+ this.requestRefreshToken = oauthConfig.requestRefreshToken ?? true;
4749
+ this.configuredScope = oauthConfig.configuredScope;
3174
4750
  }
3175
4751
  /**
3176
4752
  * Load persisted scope into {@link cachedScope} before SDK `auth()` (which
@@ -3202,6 +4778,12 @@ var BaseOAuthClientProvider = class {
3202
4778
  this.suppressAuthorizationNavigation = suppress;
3203
4779
  }
3204
4780
  get scope() {
4781
+ if (!this.requestRefreshToken) {
4782
+ return scopeForDeclinedRefreshGrant(
4783
+ this.cachedScope,
4784
+ this.configuredScope
4785
+ );
4786
+ }
3205
4787
  return this.cachedScope;
3206
4788
  }
3207
4789
  get redirectUrl() {
@@ -3214,7 +4796,12 @@ var BaseOAuthClientProvider = class {
3214
4796
  const metadata = {
3215
4797
  redirect_uris: this.redirect_uris,
3216
4798
  token_endpoint_auth_method: "none",
3217
- grant_types: ["authorization_code", "refresh_token"],
4799
+ // #2068: `refresh_token` is declared by default, and dropped when the
4800
+ // server's "Request refresh token" setting is off — which stops the SDK
4801
+ // *adding* `offline_access` (and so the `prompt=consent` it forces). It
4802
+ // does not remove an `offline_access` that reaches the scope another way.
4803
+ // See `OAuthProviderConfig.requestRefreshToken`.
4804
+ grant_types: this.requestRefreshToken ? ["authorization_code", "refresh_token"] : ["authorization_code"],
3218
4805
  response_types: ["code"],
3219
4806
  client_name: "MCP Inspector",
3220
4807
  client_uri: "https://github.com/modelcontextprotocol/inspector",
@@ -3471,6 +5058,10 @@ function isJwtExpired(token, skewMs = 6e4, nowMs = Date.now()) {
3471
5058
  if (exp === void 0) return false;
3472
5059
  return nowMs >= exp - skewMs;
3473
5060
  }
5061
+ function isJwtFormat(token) {
5062
+ const parts = token.split(".");
5063
+ return parts.length === 3 && parts[0].length > 0 && parts[1].length > 0 && parts[2] !== void 0;
5064
+ }
3474
5065
 
3475
5066
  // ../../core/auth/ema/storage.ts
3476
5067
  function normalizeIdpIssuer(issuer) {
@@ -3703,13 +5294,12 @@ import {
3703
5294
 
3704
5295
  // ../../core/auth/discovery.ts
3705
5296
  import { discoverAuthorizationServerMetadata as discoverAuthorizationServerMetadata2 } from "@modelcontextprotocol/client";
3706
- function getAuthorizationServerUrl(serverUrl, resourceMetadata) {
3707
- const first = resourceMetadata?.authorization_servers?.[0];
3708
- if (first) {
3709
- return parseHttpUrl(first, "protected resource authorization_servers[0]");
3710
- }
5297
+ function serverUrlAsAuthorizationServer(serverUrl) {
3711
5298
  try {
3712
- return new URL("/", serverUrl);
5299
+ const url = new URL(serverUrl);
5300
+ url.search = "";
5301
+ url.hash = "";
5302
+ return url;
3713
5303
  } catch (err) {
3714
5304
  const detail = err instanceof Error ? err.message : String(err);
3715
5305
  throw new Error(`Invalid MCP server URL: "${serverUrl}" (${detail})`, {
@@ -3717,6 +5307,43 @@ function getAuthorizationServerUrl(serverUrl, resourceMetadata) {
3717
5307
  });
3718
5308
  }
3719
5309
  }
5310
+ function getAuthorizationServerUrl(serverUrl, resourceMetadata) {
5311
+ const first = resourceMetadata?.authorization_servers?.[0];
5312
+ if (first) {
5313
+ return parseHttpUrl(first, "protected resource authorization_servers[0]");
5314
+ }
5315
+ return serverUrlAsAuthorizationServer(serverUrl);
5316
+ }
5317
+ function getAuthorizationServerUrlCandidates(serverUrl, resourceMetadata) {
5318
+ const primary = getAuthorizationServerUrl(serverUrl, resourceMetadata);
5319
+ const namedByMetadata = Boolean(resourceMetadata?.authorization_servers?.[0]);
5320
+ if (namedByMetadata || primary.pathname === "/") return [primary];
5321
+ return [primary, new URL("/", primary)];
5322
+ }
5323
+ async function discoverAuthorizationServerMetadataFromCandidates(candidates, discover) {
5324
+ let firstError;
5325
+ let sawError = false;
5326
+ for (const candidate of candidates) {
5327
+ try {
5328
+ const metadata = await discover(candidate);
5329
+ if (metadata) return { authorizationServerUrl: candidate, metadata };
5330
+ } catch (err) {
5331
+ if (!sawError) {
5332
+ firstError = err;
5333
+ sawError = true;
5334
+ }
5335
+ }
5336
+ }
5337
+ if (sawError) throw firstError;
5338
+ return void 0;
5339
+ }
5340
+ async function discoverAuthorizationServerMetadataForServer(serverUrl, resourceMetadata, fetchFn) {
5341
+ const found = await discoverAuthorizationServerMetadataFromCandidates(
5342
+ getAuthorizationServerUrlCandidates(serverUrl, resourceMetadata),
5343
+ (candidate) => discoverAuthorizationServerMetadata2(candidate, { fetchFn })
5344
+ );
5345
+ return found?.metadata;
5346
+ }
3720
5347
 
3721
5348
  // ../../core/auth/ema/resourceContext.ts
3722
5349
  function resolveEmaScopes(resourceMetadata, configuredScope) {
@@ -3899,7 +5526,7 @@ async function mintEmaResourceTokens(config, resourceContext) {
3899
5526
  scope: ctx.scope,
3900
5527
  fetchFn: config.fetchFn
3901
5528
  });
3902
- return redeemIdJagForAccessToken({
5529
+ const tokens = await redeemIdJagForAccessToken({
3903
5530
  resourceAsUrl: ctx.resourceAsUrl,
3904
5531
  idJag,
3905
5532
  resourceClientId: config.resourceClientId,
@@ -3908,6 +5535,19 @@ async function mintEmaResourceTokens(config, resourceContext) {
3908
5535
  scope: ctx.scope,
3909
5536
  fetchFn: config.fetchFn
3910
5537
  });
5538
+ return { tokens, requestedScope: ctx.scope };
5539
+ }
5540
+ async function saveMintedTokens(config, { tokens, requestedScope }) {
5541
+ await config.storage.saveTokens(config.serverUrl, tokens, {
5542
+ enterpriseManaged: true
5543
+ });
5544
+ const scopeToPersist = resolvePersistedScopeAfterGrant(
5545
+ tokens.scope,
5546
+ requestedScope
5547
+ );
5548
+ if (scopeToPersist) {
5549
+ await config.storage.saveScope(config.serverUrl, scopeToPersist);
5550
+ }
3911
5551
  }
3912
5552
  async function trySilentEmaAuth(config) {
3913
5553
  const idToken = await getValidIdToken({
@@ -3919,10 +5559,7 @@ async function trySilentEmaAuth(config) {
3919
5559
  return { status: "no_idp_session" };
3920
5560
  }
3921
5561
  try {
3922
- const tokens = await mintEmaResourceTokens(config);
3923
- await config.storage.saveTokens(config.serverUrl, tokens, {
3924
- enterpriseManaged: true
3925
- });
5562
+ await saveMintedTokens(config, await mintEmaResourceTokens(config));
3926
5563
  return { status: "success" };
3927
5564
  } catch (err) {
3928
5565
  return { status: "mint_failed", error: wrapEmaMintError(err) };
@@ -3953,16 +5590,14 @@ async function completeEmaIdpAuthorizationAndMint(config, authorizationCode, iss
3953
5590
  cause: err
3954
5591
  });
3955
5592
  }
3956
- let tokens;
5593
+ let minted;
3957
5594
  try {
3958
- tokens = await mintEmaResourceTokens(config);
5595
+ minted = await mintEmaResourceTokens(config);
3959
5596
  } catch (err) {
3960
5597
  throw wrapEmaMintError(err);
3961
5598
  }
3962
- await config.storage.saveTokens(config.serverUrl, tokens, {
3963
- enterpriseManaged: true
3964
- });
3965
- return tokens;
5599
+ await saveMintedTokens(config, minted);
5600
+ return minted;
3966
5601
  }
3967
5602
  async function refreshEmaResourceTokens(config) {
3968
5603
  const idToken = await getValidIdToken({
@@ -3973,11 +5608,9 @@ async function refreshEmaResourceTokens(config) {
3973
5608
  if (!idToken) {
3974
5609
  return void 0;
3975
5610
  }
3976
- const tokens = await mintEmaResourceTokens(config);
3977
- await config.storage.saveTokens(config.serverUrl, tokens, {
3978
- enterpriseManaged: true
3979
- });
3980
- return tokens;
5611
+ const minted = await mintEmaResourceTokens(config);
5612
+ await saveMintedTokens(config, minted);
5613
+ return minted.tokens;
3981
5614
  }
3982
5615
 
3983
5616
  // ../../core/auth/ema/idpSession.ts
@@ -3996,6 +5629,12 @@ function isAccessTokenUsable(tokens) {
3996
5629
  if (!tokens?.access_token) return false;
3997
5630
  return !isJwtExpired(tokens.access_token);
3998
5631
  }
5632
+ function isAccessTokenProvablyUnexpired(tokens) {
5633
+ if (!tokens?.access_token) return false;
5634
+ if (!isJwtFormat(tokens.access_token)) return false;
5635
+ if (jwtExpiresAtMs(tokens.access_token) === void 0) return false;
5636
+ return !isJwtExpired(tokens.access_token);
5637
+ }
3999
5638
  function resolveClient(preregistered, dynamic, dynamicRegistrationKind) {
4000
5639
  if (preregistered?.client_id) {
4001
5640
  return {
@@ -4083,10 +5722,7 @@ function protocolFromOAuthConfig(config) {
4083
5722
  }
4084
5723
 
4085
5724
  // ../../core/auth/cimd.ts
4086
- import {
4087
- discoverAuthorizationServerMetadata as discoverAuthorizationServerMetadata4,
4088
- discoverOAuthProtectedResourceMetadata as discoverOAuthProtectedResourceMetadata2
4089
- } from "@modelcontextprotocol/client";
5725
+ import { discoverOAuthProtectedResourceMetadata as discoverOAuthProtectedResourceMetadata2 } from "@modelcontextprotocol/client";
4090
5726
  async function ensureCimdClientRegistration(params) {
4091
5727
  const clientMetadataUrl = params.provider.clientMetadataUrl?.trim();
4092
5728
  if (!clientMetadataUrl) return;
@@ -4095,18 +5731,23 @@ async function ensureCimdClientRegistration(params) {
4095
5731
  let resourceMetadata;
4096
5732
  try {
4097
5733
  resourceMetadata = await discoverOAuthProtectedResourceMetadata2(
4098
- params.serverUrl
5734
+ params.serverUrl,
5735
+ { resourceMetadataUrl: params.resourceMetadataUrl },
5736
+ // The same fetch the AS-metadata leg below uses. On web that is
5737
+ // `createRemoteFetch`, which proxies through the backend to sidestep
5738
+ // CORS — on the global `fetch` this leg would fail in the browser, be
5739
+ // swallowed by the catch, and leave CIMD probing the wrong
5740
+ // authorization server (Copilot).
5741
+ params.fetchFn
4099
5742
  );
4100
5743
  } catch {
4101
5744
  resourceMetadata = void 0;
4102
5745
  }
4103
- const authServerUrl = getAuthorizationServerUrl(
5746
+ const metadata = await discoverAuthorizationServerMetadataForServer(
4104
5747
  params.serverUrl,
4105
- resourceMetadata
5748
+ resourceMetadata,
5749
+ params.fetchFn
4106
5750
  );
4107
- const metadata = await discoverAuthorizationServerMetadata4(authServerUrl, {
4108
- ...params.fetchFn && { fetchFn: params.fetchFn }
4109
- });
4110
5751
  if (!metadata?.client_id_metadata_document_supported) return;
4111
5752
  const clientInformation = {
4112
5753
  client_id: clientMetadataUrl
@@ -4175,29 +5816,6 @@ var EmaTransportOAuthProvider = class {
4175
5816
  }
4176
5817
  };
4177
5818
 
4178
- // ../../core/auth/scopes.ts
4179
- import {
4180
- computeScopeUnion as sdkComputeScopeUnion,
4181
- isStrictScopeSuperset as sdkIsStrictScopeSuperset
4182
- } from "@modelcontextprotocol/client";
4183
- var computeScopeUnion = sdkComputeScopeUnion;
4184
- var isStrictScopeSuperset = sdkIsStrictScopeSuperset;
4185
- function resolvePersistedScopeAfterGrant(grantedScope, requestedScope) {
4186
- const granted = grantedScope?.trim();
4187
- if (granted) {
4188
- return granted;
4189
- }
4190
- const requested = requestedScope?.trim();
4191
- return requested || void 0;
4192
- }
4193
- function resolveEffectiveGrantedScope(storedScope, tokenScope) {
4194
- const granted = tokenScope?.trim();
4195
- if (granted) {
4196
- return granted;
4197
- }
4198
- return computeScopeUnion(storedScope, tokenScope);
4199
- }
4200
-
4201
5819
  // ../../core/auth/oauthUx.ts
4202
5820
  function isStandardOAuthStepUp(challenge, options) {
4203
5821
  return challenge.reason === "insufficient_scope" && !options?.enterpriseManaged;
@@ -4285,6 +5903,11 @@ var OAuthManager = class {
4285
5903
  oauthFlowState = null;
4286
5904
  /** SEP-2350 union scope pending until interactive step-up completes. */
4287
5905
  pendingAuthorizationScope;
5906
+ /**
5907
+ * The RFC 9728 metadata URL from the last 401/403 the transport observed.
5908
+ * See {@link noteObservedAuthChallenge}.
5909
+ */
5910
+ observedResourceMetadataUrl;
4288
5911
  authChallengeMutex = Promise.resolve();
4289
5912
  constructor(params) {
4290
5913
  this.params = params;
@@ -4352,7 +5975,14 @@ var OAuthManager = class {
4352
5975
  // the provider (not in OAuth storage like `scope`) — they are pure config
4353
5976
  // read from mcp.json, and nothing in the flow needs to persist or union
4354
5977
  // them.
4355
- authorizationParams: this.oauthConfig.authorizationParams
5978
+ authorizationParams: this.oauthConfig.authorizationParams,
5979
+ // #2068: per-server opt-out of the `refresh_token` grant. Like
5980
+ // `authorizationParams` this is pure config off mcp.json, so it lives on
5981
+ // the provider rather than in OAuth storage.
5982
+ requestRefreshToken: this.oauthConfig.requestRefreshToken,
5983
+ // #2068: lets the provider tell an `offline_access` the user asked for
5984
+ // from one inherited from a previous grant's persisted scope.
5985
+ configuredScope: this.oauthConfig.scope
4356
5986
  });
4357
5987
  provider.setEventTarget(this.params.getEventTarget());
4358
5988
  await provider.prepareForAuth();
@@ -4431,6 +6061,19 @@ var OAuthManager = class {
4431
6061
  await this.recordAuthorizationCodeFlowState(authorizationUrl);
4432
6062
  return authorizationUrl;
4433
6063
  }
6064
+ /**
6065
+ * Record a challenge the transport saw but did not raise as one.
6066
+ *
6067
+ * The legacy first-authorization path connects with no `authProvider` and no
6068
+ * challenge interception, so the client calls {@link authenticate} off a
6069
+ * headerless SDK `UnauthorizedError` — this is what carries the advertised
6070
+ * RFC 9728 metadata URL across that gap (#2071). Latest-wins: every
6071
+ * challenge comes from the same server, so the most recent one is what that
6072
+ * server currently says, including when it says nothing.
6073
+ */
6074
+ noteObservedAuthChallenge(challenge) {
6075
+ this.observedResourceMetadataUrl = challengeResourceMetadataUrl(challenge);
6076
+ }
4434
6077
  async authenticate() {
4435
6078
  if (this.isEnterpriseManaged()) {
4436
6079
  return this.authenticateEnterpriseManaged();
@@ -4438,15 +6081,19 @@ var OAuthManager = class {
4438
6081
  const provider = await this.createOAuthProvider();
4439
6082
  const serverUrl = this.getServerUrl();
4440
6083
  provider.clearCapturedAuthUrl();
6084
+ const resourceMetadataUrl = this.observedResourceMetadataUrl;
4441
6085
  await ensureCimdClientRegistration({
4442
6086
  serverUrl,
4443
6087
  provider,
4444
- fetchFn: this.params.effectiveAuthFetch
6088
+ fetchFn: this.params.effectiveAuthFetch,
6089
+ resourceMetadataUrl
4445
6090
  });
6091
+ const requestedScope = provider.scope;
4446
6092
  const result = await mcpAuth(provider, {
4447
6093
  serverUrl,
4448
- scope: provider.scope,
4449
- fetchFn: this.params.effectiveAuthFetch
6094
+ scope: requestedScope,
6095
+ fetchFn: this.params.effectiveAuthFetch,
6096
+ resourceMetadataUrl
4450
6097
  });
4451
6098
  if (result === "AUTHORIZED") {
4452
6099
  throw new Error(
@@ -4457,6 +6104,7 @@ var OAuthManager = class {
4457
6104
  if (!capturedUrl) {
4458
6105
  throw new Error("Failed to capture authorization URL");
4459
6106
  }
6107
+ this.recordAuthorizationRequestScope(capturedUrl, requestedScope);
4460
6108
  const stateParam = capturedUrl.searchParams.get("state");
4461
6109
  if (stateParam && this.params.onBeforeOAuthRedirect) {
4462
6110
  const parsedState = parseOAuthState(stateParam);
@@ -4473,28 +6121,42 @@ var OAuthManager = class {
4473
6121
  };
4474
6122
  return capturedUrl;
4475
6123
  }
6124
+ /**
6125
+ * Remember what an interactive authorization asked for, so the callback can
6126
+ * apply RFC 6749 §5.1 — a token response that omits `scope` granted exactly
6127
+ * the requested scope (#2117). Without it, an ordinary grant whose AS echoes
6128
+ * no scope leaves the previous stored scope standing as though it were what
6129
+ * the current token carries.
6130
+ *
6131
+ * The authorize URL is the authority, not the scope handed to `auth()`: the
6132
+ * SDK augments the request with `offline_access` when the AS advertises it
6133
+ * and the client asks for a refresh token, so the input can be a strict
6134
+ * subset of what was actually requested. The fallback covers an authorize
6135
+ * URL that carries no `scope` parameter at all.
6136
+ *
6137
+ * This composes with the #2068 refresh-token opt-out rather than defeating
6138
+ * it. The SDK's augmentation is gated on `clientMetadata.grant_types`
6139
+ * including `refresh_token`, which the provider drops when the opt-out is
6140
+ * on — so with the grant declined the URL carries the *filtered* scope, and
6141
+ * that is what gets persisted. #2068 recorded the same value here behind a
6142
+ * `requestRefreshToken === false` gate; reading the URL makes the gate
6143
+ * unnecessary, so one wire case now has one persistence behavior instead of
6144
+ * two selected by a checkbox (#2117).
6145
+ */
6146
+ recordAuthorizationRequestScope(authorizationUrl, requestedScope) {
6147
+ this.pendingAuthorizationScope = authorizationUrl.searchParams.get("scope")?.trim() || requestedScope;
6148
+ }
4476
6149
  async completeOAuthFlow(authorizationCode, iss) {
4477
6150
  try {
4478
6151
  if (this.isEnterpriseManaged()) {
4479
6152
  const emaConfig = await this.getEmaFlowConfig();
4480
6153
  const scopeForMint = this.pendingAuthorizationScope ?? emaConfig.scope;
4481
6154
  const config = scopeForMint ? { ...emaConfig, scope: scopeForMint } : emaConfig;
4482
- const tokens2 = await completeEmaIdpAuthorizationAndMint(
6155
+ const { tokens: tokens2 } = await completeEmaIdpAuthorizationAndMint(
4483
6156
  config,
4484
6157
  authorizationCode,
4485
6158
  iss
4486
6159
  );
4487
- const requestedScope2 = this.pendingAuthorizationScope;
4488
- const scopeToPersist2 = resolvePersistedScopeAfterGrant(
4489
- tokens2.scope,
4490
- requestedScope2
4491
- );
4492
- if (scopeToPersist2) {
4493
- await this.requireStorage().saveScope(
4494
- this.getServerUrl(),
4495
- scopeToPersist2
4496
- );
4497
- }
4498
6160
  this.pendingAuthorizationScope = void 0;
4499
6161
  const completedAt2 = Date.now();
4500
6162
  this.oauthFlowState = {
@@ -4624,9 +6286,12 @@ var OAuthManager = class {
4624
6286
  * satisfied without an authorization-server round-trip.
4625
6287
  *
4626
6288
  * Returns `true` for `insufficient_scope` when stored + token scope cover the
4627
- * SEP-2350 union. For `token_expired`, returns `true` when a usable access
4628
- * token is already in storage. `invalid_token` and `unauthorized` always
4629
- * return `false` — the resource server explicitly rejected the credential.
6289
+ * SEP-2350 union. For `token_expired`, returns `true` only when storage holds
6290
+ * a *provably* unexpired token — a JWT whose `exp` is still in the future. An
6291
+ * opaque token carries no local expiry evidence, so the resource server's
6292
+ * verdict stands and re-authorization proceeds. `invalid_token` and
6293
+ * `unauthorized` always return `false` — the resource server explicitly
6294
+ * rejected the credential.
4630
6295
  */
4631
6296
  async checkAuthChallengeSatisfied(challenge) {
4632
6297
  const storage = this.oauthConfig.storage;
@@ -4639,7 +6304,7 @@ var OAuthManager = class {
4639
6304
  return false;
4640
6305
  }
4641
6306
  if (challenge.reason !== "insufficient_scope") {
4642
- return challenge.reason === "token_expired" && isAccessTokenUsable(tokens);
6307
+ return challenge.reason === "token_expired" && isAccessTokenProvablyUnexpired(tokens);
4643
6308
  }
4644
6309
  const enriched = await this.enrichChallengeWithAuthorizationScopes(
4645
6310
  challenge,
@@ -4674,8 +6339,8 @@ var OAuthManager = class {
4674
6339
  }
4675
6340
  const prior = this.authChallengeMutex;
4676
6341
  let release;
4677
- const gate = new Promise((resolve4) => {
4678
- release = resolve4;
6342
+ const gate = new Promise((resolve7) => {
6343
+ release = resolve7;
4679
6344
  });
4680
6345
  this.authChallengeMutex = gate;
4681
6346
  await prior;
@@ -4799,12 +6464,27 @@ var OAuthManager = class {
4799
6464
  tokens?.scope
4800
6465
  );
4801
6466
  provider.clearCapturedAuthUrl();
6467
+ const resourceMetadataUrl = challengeResourceMetadataUrl(enriched);
4802
6468
  await ensureCimdClientRegistration({
4803
6469
  serverUrl,
4804
6470
  provider,
4805
- fetchFn: this.params.effectiveAuthFetch
6471
+ fetchFn: this.params.effectiveAuthFetch,
6472
+ resourceMetadataUrl
4806
6473
  });
4807
- const scopeForAuth = enriched.reason === "insufficient_scope" ? enriched.authorizationScopes?.join(" ") : this.oauthConfig.scope?.trim() || (enriched.requiredScopes?.length ? enriched.requiredScopes.join(" ") : void 0);
6474
+ const requestedScopeForAuth = enriched.reason === "insufficient_scope" ? enriched.authorizationScopes?.join(" ") : this.oauthConfig.scope?.trim() || (enriched.requiredScopes?.length ? enriched.requiredScopes.join(" ") : void 0);
6475
+ const scopeForAuth = this.oauthConfig.requestRefreshToken === false ? scopeForDeclinedRefreshGrant(
6476
+ requestedScopeForAuth,
6477
+ this.oauthConfig.scope,
6478
+ // The RAW challenge scopes, not `enriched.requiredScopes`:
6479
+ // `enrichChallengeWithScopes` narrows that to the *missing* subset,
6480
+ // so a challenge requiring `offline_access tools:write` against a
6481
+ // stored scope that already has `offline_access` arrives here as
6482
+ // just `["tools:write"]`. Passing the narrowed list would read the
6483
+ // server's explicit requirement as inherited and strip it — then
6484
+ // re-authorization earns the same challenge forever, which is the
6485
+ // loop this argument exists to prevent.
6486
+ challenge.requiredScopes
6487
+ ) : requestedScopeForAuth;
4808
6488
  provider.setSuppressAuthorizationNavigation(true);
4809
6489
  let result;
4810
6490
  try {
@@ -4812,6 +6492,7 @@ var OAuthManager = class {
4812
6492
  serverUrl,
4813
6493
  scope: scopeForAuth,
4814
6494
  fetchFn: this.params.effectiveAuthFetch,
6495
+ resourceMetadataUrl,
4815
6496
  ...enriched.reason === "insufficient_scope" && {
4816
6497
  forceReauthorization: isStrictScopeSuperset(
4817
6498
  scopeForAuth,
@@ -4859,9 +6540,7 @@ var OAuthManager = class {
4859
6540
  error: new Error("Failed to capture authorization URL")
4860
6541
  };
4861
6542
  }
4862
- if (enriched.reason === "insufficient_scope" && scopeForAuth) {
4863
- this.pendingAuthorizationScope = scopeForAuth;
4864
- }
6543
+ this.recordAuthorizationRequestScope(capturedUrl, scopeForAuth);
4865
6544
  const clientInfo = await provider.clientInformation();
4866
6545
  await this.recordAuthorizationCodeFlowState(
4867
6546
  capturedUrl,
@@ -4918,6 +6597,7 @@ var OAuthManager = class {
4918
6597
  serverUrl,
4919
6598
  scope: scopeForAuth,
4920
6599
  fetchFn: this.params.effectiveAuthFetch,
6600
+ resourceMetadataUrl: challengeResourceMetadataUrl(enriched),
4921
6601
  forceReauthorization: true
4922
6602
  });
4923
6603
  } finally {
@@ -4998,40 +6678,38 @@ function legacySessionId(json) {
4998
6678
  async function* parseSSE(reader) {
4999
6679
  const decoder = new TextDecoder();
5000
6680
  let buffer = "";
6681
+ let currentEvent = "message";
6682
+ let currentData = [];
6683
+ const consume = (line) => {
6684
+ if (line.startsWith("event:")) {
6685
+ currentEvent = line.slice(6).trim();
6686
+ } else if (line.startsWith("data:")) {
6687
+ currentData.push(line.slice(5).trimStart());
6688
+ } else if (line === "") {
6689
+ const complete = currentData.length > 0 ? { event: currentEvent, data: currentData.join("\n") } : null;
6690
+ currentEvent = "message";
6691
+ currentData = [];
6692
+ return complete;
6693
+ }
6694
+ return null;
6695
+ };
5001
6696
  while (true) {
5002
6697
  const { done, value } = await reader.read();
5003
6698
  if (done) break;
5004
6699
  buffer += decoder.decode(value, { stream: true });
5005
6700
  const lines = buffer.split("\n");
5006
6701
  buffer = lines.pop() ?? "";
5007
- let currentEvent = "message";
5008
- let currentData = [];
5009
6702
  for (const line of lines) {
5010
- if (line.startsWith("event:")) {
5011
- currentEvent = line.slice(6).trim();
5012
- } else if (line.startsWith("data:")) {
5013
- currentData.push(line.slice(5).trimStart());
5014
- } else if (line === "") {
5015
- if (currentData.length > 0) {
5016
- yield { event: currentEvent, data: currentData.join("\n") };
5017
- }
5018
- currentEvent = "message";
5019
- currentData = [];
5020
- }
6703
+ const complete = consume(line);
6704
+ if (complete) yield complete;
5021
6705
  }
5022
6706
  }
5023
- if (buffer.trim()) {
5024
- const lines = buffer.split("\n");
5025
- let currentEvent = "message";
5026
- const currentData = [];
5027
- for (const line of lines) {
5028
- if (line.startsWith("event:")) currentEvent = line.slice(6).trim();
5029
- else if (line.startsWith("data:"))
5030
- currentData.push(line.slice(5).trimStart());
5031
- }
5032
- if (currentData.length > 0) {
5033
- yield { event: currentEvent, data: currentData.join("\n") };
5034
- }
6707
+ if (buffer) {
6708
+ const complete = consume(buffer);
6709
+ if (complete) yield complete;
6710
+ }
6711
+ if (currentData.length > 0) {
6712
+ yield { event: currentEvent, data: currentData.join("\n") };
5035
6713
  }
5036
6714
  }
5037
6715
  var RemoteClientTransport = class {
@@ -5366,7 +7044,7 @@ var RemoteClientTransport = class {
5366
7044
  this.sseResponseWaits.clear();
5367
7045
  }
5368
7046
  waitForSseResponse(requestId) {
5369
- return new Promise((resolve4, reject) => {
7047
+ return new Promise((resolve7, reject) => {
5370
7048
  const timer = setTimeout(() => {
5371
7049
  this.sseResponseWaits.delete(requestId);
5372
7050
  reject(
@@ -5378,7 +7056,7 @@ var RemoteClientTransport = class {
5378
7056
  this.sseResponseWaits.set(requestId, {
5379
7057
  resolve: () => {
5380
7058
  clearTimeout(timer);
5381
- resolve4();
7059
+ resolve7();
5382
7060
  },
5383
7061
  reject: (error) => {
5384
7062
  clearTimeout(timer);
@@ -5510,6 +7188,29 @@ function notificationMethodFromSchema(schema) {
5510
7188
  }
5511
7189
  return void 0;
5512
7190
  }
7191
+ function describeJsonType2(value) {
7192
+ if (value === null) return "null";
7193
+ if (Array.isArray(value)) return "array";
7194
+ return typeof value;
7195
+ }
7196
+ function isProgressToken(value) {
7197
+ return ProgressTokenSchema.safeParse(value).success;
7198
+ }
7199
+ function dropInvalidReservedMeta(meta, logger) {
7200
+ const result = RequestMetaSchema.safeParse(meta);
7201
+ if (result.success) return meta;
7202
+ const cleaned = { ...meta };
7203
+ for (const issue of result.error.issues) {
7204
+ const key = issue.path[0];
7205
+ if (typeof key !== "string" || !(key in cleaned)) continue;
7206
+ logger.warn(
7207
+ { key, received: describeJsonType2(cleaned[key]) },
7208
+ "Dropping reserved `_meta` member \u2014 it does not match the protocol schema."
7209
+ );
7210
+ delete cleaned[key];
7211
+ }
7212
+ return cleaned;
7213
+ }
5513
7214
  var MODERN_RECONNECT_BASE_MS = 500;
5514
7215
  var MODERN_RECONNECT_MAX_MS = 15e3;
5515
7216
  var MODERN_RECONNECT_MAX_ATTEMPTS = 8;
@@ -5699,6 +7400,37 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
5699
7400
  // Per-extension advertise overrides (#1738); undefined key falls back to the
5700
7401
  // registry default in ADVERTISABLE_EXTENSIONS.
5701
7402
  advertisedExtensions;
7403
+ /**
7404
+ * Host-supplied renderer for app-rendered form elicitations (#1854), or
7405
+ * undefined on a client that cannot host MCP Apps. Its presence is what
7406
+ * advertises the nested MCP Apps `elicitation` capability, so this is the one
7407
+ * fact both the advertisement and the routing gate read.
7408
+ */
7409
+ appElicitationRenderer;
7410
+ /**
7411
+ * Monotonic counter behind the request-scoped id handed to the renderer. The
7412
+ * association MUST be per request (a map keyed by this id owns the resource
7413
+ * URI, renderer instance and promise on the host side) so two concurrent
7414
+ * elicitations cannot resolve through each other's bridges.
7415
+ */
7416
+ appElicitationSeq = 0;
7417
+ /**
7418
+ * Per-instance prefix for {@link appElicitationSeq}.
7419
+ *
7420
+ * A counter alone is not enough: a host may hold ONE renderer across
7421
+ * replacement clients (the web client rebuilds its `InspectorClient` on a
7422
+ * settings change), and every fresh instance would otherwise mint
7423
+ * `app-elicitation-1` for its first request. Settling that id would then
7424
+ * resolve — or discard — whichever connection's request the host happened to
7425
+ * be keying.
7426
+ */
7427
+ appElicitationPrefix = `app-elicitation-${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
7428
+ /**
7429
+ * Abort controllers for app-rendered elicitations still awaiting an answer.
7430
+ * Aborted alongside the native pending queue on disconnect, so a rendered app
7431
+ * cannot outlive the connection that asked for it.
7432
+ */
7433
+ activeAppElicitations = /* @__PURE__ */ new Set();
5702
7434
  receiverTaskTtlMs;
5703
7435
  receiverTaskRecords = /* @__PURE__ */ new Map();
5704
7436
  // OAuth support (config owned by oauthManager; client delegates and uses !!oauthManager for "is OAuth configured")
@@ -5746,6 +7478,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
5746
7478
  this.elicit = options.elicit ?? true;
5747
7479
  this.receiverTasks = options.receiverTasks ?? false;
5748
7480
  this.advertisedExtensions = options.advertisedExtensions;
7481
+ this.appElicitationRenderer = options.appElicitation;
5749
7482
  this.receiverTaskTtlMs = options.receiverTaskTtlMs ?? 6e4;
5750
7483
  this.progress = options.progress ?? true;
5751
7484
  this.resetTimeoutOnProgress = options.resetTimeoutOnProgress ?? true;
@@ -5850,7 +7583,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
5850
7583
  }
5851
7584
  const advertisedExtensions = buildClientExtensions({
5852
7585
  enterpriseManaged: options.oauth?.enterpriseManaged ?? false,
5853
- advertised: this.advertisedExtensions
7586
+ advertised: this.advertisedExtensions,
7587
+ // Read off the built `capabilities.elicitation.form` rather than
7588
+ // re-deriving from `options.elicit`: the nested MCP Apps `elicitation`
7589
+ // setting must never be advertised without the core form capability it
7590
+ // extends, and two derivations of the same fact can drift. Disabling form
7591
+ // elicitation therefore drops both, as the contract requires. (#1854)
7592
+ appElicitation: this.appElicitationRenderer !== void 0 && capabilities.elicitation?.form !== void 0
5854
7593
  });
5855
7594
  if (Object.keys(advertisedExtensions).length > 0) {
5856
7595
  capabilities.extensions = {
@@ -5974,7 +7713,23 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
5974
7713
  ...logMeta ?? {},
5975
7714
  ...callMetadata ?? {}
5976
7715
  };
5977
- return Object.keys(merged).length > 0 ? merged : void 0;
7716
+ const sanitized = dropInvalidReservedMeta(merged, this.logger);
7717
+ return Object.keys(sanitized).length > 0 ? sanitized : void 0;
7718
+ }
7719
+ /**
7720
+ * The `progressToken` a caller stamped on its request metadata, when it is
7721
+ * one the SDK will accept.
7722
+ *
7723
+ * This governs only **callback correlation** — whether the client wires up an
7724
+ * `onprogress` for the request. Keeping an invalid value off the **wire** is
7725
+ * a separate concern handled in {@link mergeMeta}, which deletes the member
7726
+ * from the outgoing `_meta` so a conforming server is never sent something
7727
+ * `ProgressTokenSchema` rejects. The two use the same {@link isProgressToken}
7728
+ * predicate, so they cannot disagree about what counts as valid.
7729
+ */
7730
+ progressTokenOf(metadata) {
7731
+ const token = metadata?.progressToken;
7732
+ return isProgressToken(token) ? token : void 0;
5978
7733
  }
5979
7734
  getRequestOptions(progressToken, signal) {
5980
7735
  const opts = {
@@ -6085,8 +7840,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6085
7840
  };
6086
7841
  let resolvePayload;
6087
7842
  let rejectPayload;
6088
- const payloadPromise = new Promise((resolve4, reject) => {
6089
- resolvePayload = resolve4;
7843
+ const payloadPromise = new Promise((resolve7, reject) => {
7844
+ resolvePayload = resolve7;
6090
7845
  rejectPayload = reject;
6091
7846
  });
6092
7847
  void payloadPromise.catch(() => {
@@ -6095,7 +7850,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6095
7850
  task,
6096
7851
  payloadPromise,
6097
7852
  resolvePayload,
6098
- rejectPayload
7853
+ rejectPayload,
7854
+ abort: new AbortController()
6099
7855
  };
6100
7856
  record.cleanupTimeoutId = setTimeout(() => {
6101
7857
  record.cleanupTimeoutId = void 0;
@@ -6166,6 +7922,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6166
7922
  };
6167
7923
  record.task = updatedTask;
6168
7924
  record.rejectPayload(new Error("Task cancelled"));
7925
+ record.abort.abort();
6169
7926
  if (record.cleanupTimeoutId != null) {
6170
7927
  clearTimeout(record.cleanupTimeoutId);
6171
7928
  record.cleanupTimeoutId = void 0;
@@ -6265,7 +8022,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6265
8022
  }
6266
8023
  }
6267
8024
  if (this.elicitationCapabilityAdvertised && this.client) {
6268
- const elicitHandler = (request) => {
8025
+ const elicitHandler = (request, ctx) => {
6269
8026
  const paramsTask = request.params?.task;
6270
8027
  if (this.tasksCapabilityAdvertised && paramsTask != null) {
6271
8028
  const record = this.createReceiverTask({
@@ -6273,40 +8030,68 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6273
8030
  initialStatus: "input_required",
6274
8031
  statusMessage: "Awaiting user input"
6275
8032
  });
8033
+ const completeTask = (result) => {
8034
+ if (_InspectorClient.isTerminalTaskStatus(record.task.status))
8035
+ return;
8036
+ record.resolvePayload(result);
8037
+ const updated = {
8038
+ ...record.task,
8039
+ status: "completed",
8040
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
8041
+ };
8042
+ record.task = updated;
8043
+ this.upsertReceiverTask(updated);
8044
+ };
8045
+ const failTask = (error) => {
8046
+ if (_InspectorClient.isTerminalTaskStatus(record.task.status))
8047
+ return;
8048
+ record.rejectPayload(error);
8049
+ const updated = {
8050
+ ...record.task,
8051
+ status: "failed",
8052
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8053
+ statusMessage: error.message
8054
+ };
8055
+ record.task = updated;
8056
+ this.upsertReceiverTask(updated);
8057
+ };
6276
8058
  void (async () => {
8059
+ let appResult;
8060
+ try {
8061
+ appResult = await this.tryAppElicitation(
8062
+ request,
8063
+ record.abort.signal
8064
+ );
8065
+ } catch (error) {
8066
+ failTask(
8067
+ error instanceof Error ? error : new Error(String(error))
8068
+ );
8069
+ return;
8070
+ }
8071
+ if (appResult) {
8072
+ completeTask(appResult);
8073
+ return;
8074
+ }
6277
8075
  const elicitationRequest = new ElicitationCreateMessage(
6278
8076
  request,
6279
- (result) => {
6280
- record.resolvePayload(result);
6281
- const now = (/* @__PURE__ */ new Date()).toISOString();
6282
- const updated = {
6283
- ...record.task,
6284
- status: "completed",
6285
- lastUpdatedAt: now
6286
- };
6287
- record.task = updated;
6288
- this.upsertReceiverTask(updated);
6289
- },
8077
+ completeTask,
6290
8078
  (id) => this.removePendingElicitation(id),
6291
- (error) => {
6292
- record.rejectPayload(error);
6293
- const now = (/* @__PURE__ */ new Date()).toISOString();
6294
- const updated = {
6295
- ...record.task,
6296
- status: "failed",
6297
- lastUpdatedAt: now,
6298
- statusMessage: error.message
6299
- };
6300
- record.task = updated;
6301
- this.upsertReceiverTask(updated);
6302
- }
8079
+ failTask
6303
8080
  );
6304
8081
  this.addPendingElicitation(elicitationRequest);
8082
+ this.wirePendingAbort(
8083
+ record.abort.signal,
8084
+ () => this.removePendingElicitation(elicitationRequest.id)
8085
+ );
6305
8086
  })();
6306
8087
  const taskResult = { task: record.task };
6307
8088
  return Promise.resolve(taskResult);
6308
8089
  }
6309
- return this.enqueuePendingElicitation(request, "server-request");
8090
+ return this.enqueuePendingElicitation(
8091
+ request,
8092
+ "server-request",
8093
+ ctx?.mcpReq?.signal
8094
+ );
6310
8095
  };
6311
8096
  this.client.setRequestHandler("elicitation/create", elicitHandler);
6312
8097
  if (this.tasksCapabilityAdvertised) {
@@ -6396,6 +8181,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6396
8181
  if (record.cleanupTimeoutId != null) {
6397
8182
  clearTimeout(record.cleanupTimeoutId);
6398
8183
  }
8184
+ record.abort.abort();
6399
8185
  }
6400
8186
  this.receiverTaskRecords.clear();
6401
8187
  }
@@ -6502,6 +8288,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6502
8288
  elicitation.cancel();
6503
8289
  }
6504
8290
  this.pendingElicitations = [];
8291
+ for (const controller of this.activeAppElicitations) {
8292
+ controller.abort();
8293
+ }
8294
+ this.activeAppElicitations.clear();
6505
8295
  }
6506
8296
  /**
6507
8297
  * {@link clearPendingPeerRequests} plus the change events, for every route
@@ -6522,7 +8312,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6522
8312
  * `dropCachedTransport` also fires `onclose`) announce it once.
6523
8313
  */
6524
8314
  clearAndAnnouncePendingPeerRequests() {
6525
- if (this.pendingSamples.length === 0 && this.pendingElicitations.length === 0) {
8315
+ if (this.pendingSamples.length === 0 && this.pendingElicitations.length === 0 && // App-rendered elicitations (#1854) are not in either array — they live in
8316
+ // the host's renderer — so an emptiness check that ignores them lets a
8317
+ // mid-session close (the `onclose` route, which reaches teardown only
8318
+ // through here) leave a modal open for a connection that is gone.
8319
+ this.activeAppElicitations.size === 0) {
6526
8320
  return;
6527
8321
  }
6528
8322
  this.clearPendingPeerRequests();
@@ -6565,6 +8359,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6565
8359
  ...this.serverSettings && { settings: this.serverSettings }
6566
8360
  };
6567
8361
  if (this.isHttpOAuthConfig() && oauthManager) {
8362
+ const manager = oauthManager;
8363
+ transportOptions.onAuthChallengeObserved = (challenge) => {
8364
+ manager.noteObservedAuthChallenge(challenge);
8365
+ };
6568
8366
  if (oauthManager.isEnterpriseManaged()) {
6569
8367
  await oauthManager.trySilentEnterpriseManagedAuth();
6570
8368
  const provider = await oauthManager.createOAuthProviderForTransport();
@@ -7041,14 +8839,14 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7041
8839
  params
7042
8840
  };
7043
8841
  const timeoutMs = this.requestTimeout ?? 3e4;
7044
- const raw = await new Promise((resolve4, reject) => {
8842
+ const raw = await new Promise((resolve7, reject) => {
7045
8843
  const timer = setTimeout(() => {
7046
8844
  this.pendingRawWireRequests.delete(id);
7047
8845
  reject(
7048
8846
  new Error(`Raw request "${method}" timed out after ${timeoutMs} ms`)
7049
8847
  );
7050
8848
  }, timeoutMs);
7051
- this.pendingRawWireRequests.set(id, { resolve: resolve4, reject, timer });
8849
+ this.pendingRawWireRequests.set(id, { resolve: resolve7, reject, timer });
7052
8850
  transport.send(message).catch((err) => {
7053
8851
  const pending = this.pendingRawWireRequests.get(id);
7054
8852
  if (pending) {
@@ -7269,7 +9067,9 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7269
9067
  * corresponding `ElicitResult` (echoed to the server on retry); only a
7270
9068
  * genuine failure or a `signal` abort rejects.
7271
9069
  */
7272
- enqueuePendingElicitation(request, origin, signal) {
9070
+ async enqueuePendingElicitation(request, origin, signal) {
9071
+ const appResult = await this.tryAppElicitation(request, signal);
9072
+ if (appResult) return appResult;
7273
9073
  return new Promise((resolvePromise, rejectPromise) => {
7274
9074
  const elicitation = new ElicitationCreateMessage(
7275
9075
  request,
@@ -7285,6 +9085,79 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7285
9085
  });
7286
9086
  });
7287
9087
  }
9088
+ /**
9089
+ * Attempt to resolve an `elicitation/create` request by rendering the MCP App
9090
+ * the server attached to it (#1854), returning the app's standard
9091
+ * `ElicitResult`.
9092
+ *
9093
+ * Returns `null` for "not app-rendered — use the native UI", which covers
9094
+ * every negotiation gate and every failure mode the contract lists: either
9095
+ * peer did not negotiate the capability, the metadata is absent or unusable,
9096
+ * the mode is not `form`, the renderer failed (resource read, sandbox/bridge
9097
+ * init, missing app capability, timeout), or the app returned something that
9098
+ * is not a valid result for this request. An explicit `decline` or `cancel`
9099
+ * is a *completed* elicitation and is returned, not fallen back on.
9100
+ *
9101
+ * The one case that does not fall back is an abort of the originating
9102
+ * request: the caller is cancelling the whole elicitation, so re-opening it
9103
+ * in the native queue would resurrect work the user just abandoned.
9104
+ */
9105
+ async tryAppElicitation(request, signal) {
9106
+ const renderer = this.appElicitationRenderer;
9107
+ if (!renderer) return null;
9108
+ if (!supportsAppElicitation(this.clientCapabilities, this.capabilities)) {
9109
+ return null;
9110
+ }
9111
+ if (!isFormElicitation(request.params)) return null;
9112
+ let resourceUri;
9113
+ try {
9114
+ resourceUri = getElicitationUiResourceUri(request.params);
9115
+ } catch (error) {
9116
+ this.logger.warn(
9117
+ { error },
9118
+ "Elicitation carried unusable _meta.ui.resourceUri; using the native elicitation UI"
9119
+ );
9120
+ return null;
9121
+ }
9122
+ if (!resourceUri) return null;
9123
+ if (signal?.aborted) throw createPendingAbortError();
9124
+ const controller = new AbortController();
9125
+ const forwardAbort = () => controller.abort(signal?.reason);
9126
+ signal?.addEventListener("abort", forwardAbort, { once: true });
9127
+ this.activeAppElicitations.add(controller);
9128
+ try {
9129
+ const result = await renderer({
9130
+ requestId: `${this.appElicitationPrefix}-${++this.appElicitationSeq}`,
9131
+ resourceUri,
9132
+ params: request.params,
9133
+ signal: controller.signal
9134
+ });
9135
+ this.outputValidator ??= new AjvJsonSchemaValidator();
9136
+ const invalid = validateAppElicitResult(
9137
+ this.outputValidator,
9138
+ request.params,
9139
+ result
9140
+ );
9141
+ if (invalid) {
9142
+ this.logger.warn(
9143
+ { resourceUri, reason: invalid },
9144
+ "App-rendered elicitation returned an invalid result; using the native elicitation UI"
9145
+ );
9146
+ return null;
9147
+ }
9148
+ return result;
9149
+ } catch (error) {
9150
+ if (controller.signal.aborted) throw createPendingAbortError();
9151
+ this.logger.warn(
9152
+ { error, resourceUri },
9153
+ "App-rendered elicitation failed; using the native elicitation UI"
9154
+ );
9155
+ return null;
9156
+ } finally {
9157
+ this.activeAppElicitations.delete(controller);
9158
+ signal?.removeEventListener("abort", forwardAbort);
9159
+ }
9160
+ }
7288
9161
  /**
7289
9162
  * Reject a still-pending request when `signal` aborts (e.g. the user cancels
7290
9163
  * the tool call while its MRTR round is awaiting an answer). No-op when
@@ -7587,7 +9460,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7587
9460
  () => this.client.request(
7588
9461
  { method: "tools/list", params },
7589
9462
  ListToolsResultSchema,
7590
- this.getRequestOptions(metadata?.progressToken)
9463
+ this.getRequestOptions(this.progressTokenOf(metadata))
7591
9464
  )
7592
9465
  );
7593
9466
  const tools = [...response.tools || []];
@@ -7808,7 +9681,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7808
9681
  ) : this.client.request(
7809
9682
  { method, params },
7810
9683
  pageSchema,
7811
- this.getRequestOptions(metadata?.progressToken)
9684
+ this.getRequestOptions(this.progressTokenOf(metadata))
7812
9685
  ),
7813
9686
  { method }
7814
9687
  );
@@ -7937,7 +9810,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7937
9810
  ) : this.client.request(
7938
9811
  { method: "tools/list", params },
7939
9812
  pageSchema,
7940
- this.getRequestOptions(metadata?.progressToken)
9813
+ this.getRequestOptions(this.progressTokenOf(metadata))
7941
9814
  ),
7942
9815
  { method: "tools/list" }
7943
9816
  );
@@ -8157,7 +10030,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8157
10030
  callParams.task = { ttl: taskOptions.ttl };
8158
10031
  }
8159
10032
  const requestOptions = this.getRequestOptions(
8160
- metadata?.progressToken,
10033
+ this.progressTokenOf(metadata),
8161
10034
  signal
8162
10035
  );
8163
10036
  this.applyMirroredParamHeaders(tool, convertedArgs, requestOptions);
@@ -8243,14 +10116,14 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8243
10116
  * `completeIfPending`).
8244
10117
  */
8245
10118
  awaitUrlElicitation(params) {
8246
- return new Promise((resolve4) => {
10119
+ return new Promise((resolve7) => {
8247
10120
  const request = {
8248
10121
  method: "elicitation/create",
8249
10122
  params
8250
10123
  };
8251
10124
  const message = new ElicitationCreateMessage(
8252
10125
  request,
8253
- (result) => resolve4(result.action),
10126
+ (result) => resolve7(result.action),
8254
10127
  (id) => this.removePendingElicitation(id)
8255
10128
  );
8256
10129
  this.addPendingElicitation(message);
@@ -8409,7 +10282,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8409
10282
  inputSignal
8410
10283
  );
8411
10284
  await new Promise(
8412
- (resolve4) => setTimeout(resolve4, this.taskPollInterval(task))
10285
+ (resolve7) => setTimeout(resolve7, this.taskPollInterval(task))
8413
10286
  );
8414
10287
  detailed = await this.rawWireRequest(
8415
10288
  "tasks/get",
@@ -8487,7 +10360,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8487
10360
  inputSignal
8488
10361
  );
8489
10362
  await new Promise(
8490
- (resolve4) => setTimeout(resolve4, this.taskPollInterval(task2))
10363
+ (resolve7) => setTimeout(resolve7, this.taskPollInterval(task2))
8491
10364
  );
8492
10365
  detailed = await this.rawWireRequest(
8493
10366
  "tasks/get",
@@ -8518,7 +10391,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8518
10391
  try {
8519
10392
  while (!_InspectorClient.isTerminalTaskStatus(task.status)) {
8520
10393
  await new Promise(
8521
- (resolve4) => setTimeout(resolve4, this.taskPollInterval(task))
10394
+ (resolve7) => setTimeout(resolve7, this.taskPollInterval(task))
8522
10395
  );
8523
10396
  task = await client.request(
8524
10397
  { method: "tasks/get", params: { taskId: task.taskId } },
@@ -8581,7 +10454,9 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8581
10454
  let finalResult;
8582
10455
  let taskId;
8583
10456
  let error;
8584
- const requestOptions = this.getRequestOptions(metadata?.progressToken);
10457
+ const requestOptions = this.getRequestOptions(
10458
+ this.progressTokenOf(metadata)
10459
+ );
8585
10460
  this.applyMirroredParamHeaders(tool, convertedArgs, requestOptions);
8586
10461
  if (this.progress) {
8587
10462
  const innerOnProgress = requestOptions.onprogress;
@@ -8748,7 +10623,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8748
10623
  () => this.client.request(
8749
10624
  { method: "resources/list", params },
8750
10625
  ListResourcesResultSchema,
8751
- this.getRequestOptions(metadata?.progressToken)
10626
+ this.getRequestOptions(this.progressTokenOf(metadata))
8752
10627
  )
8753
10628
  );
8754
10629
  return {
@@ -8801,7 +10676,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8801
10676
  "resources/read",
8802
10677
  params,
8803
10678
  ReadResourceResultSchema,
8804
- this.getRequestOptions(metadata?.progressToken)
10679
+ this.getRequestOptions(this.progressTokenOf(metadata))
8805
10680
  ),
8806
10681
  { method: "resources/read" }
8807
10682
  );
@@ -8878,7 +10753,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8878
10753
  () => this.client.request(
8879
10754
  { method: "resources/templates/list", params },
8880
10755
  ListResourceTemplatesResultSchema,
8881
- this.getRequestOptions(metadata?.progressToken)
10756
+ this.getRequestOptions(this.progressTokenOf(metadata))
8882
10757
  ),
8883
10758
  { method: "resources/templates/list" }
8884
10759
  );
@@ -8931,7 +10806,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8931
10806
  () => this.client.request(
8932
10807
  { method: "prompts/list", params },
8933
10808
  ListPromptsResultSchema,
8934
- this.getRequestOptions(metadata?.progressToken)
10809
+ this.getRequestOptions(this.progressTokenOf(metadata))
8935
10810
  )
8936
10811
  );
8937
10812
  return {
@@ -8987,7 +10862,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8987
10862
  "prompts/get",
8988
10863
  params,
8989
10864
  GetPromptResultSchema,
8990
- this.getRequestOptions(metadata?.progressToken)
10865
+ this.getRequestOptions(this.progressTokenOf(metadata))
8991
10866
  ),
8992
10867
  { method: "prompts/get", toolName: name }
8993
10868
  );
@@ -9033,7 +10908,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
9033
10908
  const response = await this.invokeMcpClient(
9034
10909
  () => this.client.complete(
9035
10910
  params,
9036
- this.getRequestOptions(metadata?.progressToken)
10911
+ this.getRequestOptions(this.progressTokenOf(metadata))
9037
10912
  ),
9038
10913
  {
9039
10914
  method: "completion/complete",
@@ -11022,19 +12897,19 @@ function classifyAuthorizationServerMismatch(err) {
11022
12897
  }
11023
12898
 
11024
12899
  // ../../core/node/version.ts
11025
- import { readFileSync as readFileSync2 } from "fs";
11026
- import { dirname as dirname3, join as join2, parse } from "path";
12900
+ import { readFileSync as readFileSync3 } from "fs";
12901
+ import { dirname as dirname5, join as join3, parse } from "path";
11027
12902
  import { fileURLToPath } from "url";
11028
12903
  var ROOT_PACKAGE_NAME = "@modelcontextprotocol/inspector";
11029
12904
  function readInspectorVersion(callerUrl) {
11030
- const startDir = dirname3(fileURLToPath(callerUrl));
12905
+ const startDir = dirname5(fileURLToPath(callerUrl));
11031
12906
  const { root } = parse(startDir);
11032
12907
  let dir = startDir;
11033
12908
  for (; ; ) {
11034
12909
  let manifest;
11035
12910
  try {
11036
12911
  manifest = JSON.parse(
11037
- readFileSync2(join2(dir, "package.json"), "utf-8")
12912
+ readFileSync3(join3(dir, "package.json"), "utf-8")
11038
12913
  );
11039
12914
  } catch {
11040
12915
  }
@@ -11042,7 +12917,7 @@ function readInspectorVersion(callerUrl) {
11042
12917
  return manifest.version;
11043
12918
  }
11044
12919
  if (dir === root) break;
11045
- dir = dirname3(dir);
12920
+ dir = dirname5(dir);
11046
12921
  }
11047
12922
  throw new Error(
11048
12923
  `Could not locate the ${ROOT_PACKAGE_NAME} root package.json above ${startDir}`
@@ -11550,11 +13425,11 @@ var OAuthCallbackServer = class {
11550
13425
  const {
11551
13426
  port = 0,
11552
13427
  hostname = DEFAULT_HOSTNAME,
11553
- path: path3 = DEFAULT_CALLBACK_PATH,
13428
+ path: path6 = DEFAULT_CALLBACK_PATH,
11554
13429
  onCallback,
11555
13430
  onError
11556
13431
  } = options;
11557
- if (!path3.startsWith("/")) {
13432
+ if (!path6.startsWith("/")) {
11558
13433
  return Promise.reject(
11559
13434
  new Error("Callback path must start with '/' (absolute path)")
11560
13435
  );
@@ -11562,8 +13437,8 @@ var OAuthCallbackServer = class {
11562
13437
  this.onCallback = onCallback;
11563
13438
  this.onError = onError;
11564
13439
  this.handled = false;
11565
- this.callbackPath = path3;
11566
- return new Promise((resolve4, reject) => {
13440
+ this.callbackPath = path6;
13441
+ return new Promise((resolve7, reject) => {
11567
13442
  this.server = createServer((req, res) => this.handleRequest(req, res));
11568
13443
  this.server.on("error", reject);
11569
13444
  this.server.listen(port, hostname, () => {
@@ -11573,9 +13448,9 @@ var OAuthCallbackServer = class {
11573
13448
  return;
11574
13449
  }
11575
13450
  this.port = a.port;
11576
- resolve4({
13451
+ resolve7({
11577
13452
  port: this.port,
11578
- redirectUrl: buildRedirectUrl(hostname, this.port, path3)
13453
+ redirectUrl: buildRedirectUrl(hostname, this.port, path6)
11579
13454
  });
11580
13455
  });
11581
13456
  });
@@ -11585,8 +13460,8 @@ var OAuthCallbackServer = class {
11585
13460
  */
11586
13461
  async stop() {
11587
13462
  if (!this.server) return;
11588
- await new Promise((resolve4) => {
11589
- this.server.close(() => resolve4());
13463
+ await new Promise((resolve7) => {
13464
+ this.server.close(() => resolve7());
11590
13465
  });
11591
13466
  this.server = null;
11592
13467
  }
@@ -11660,8 +13535,8 @@ var OAuthCallbackServer = class {
11660
13535
  function createOAuthCallbackServer() {
11661
13536
  return new OAuthCallbackServer();
11662
13537
  }
11663
- function buildRedirectUrl(host, port, path3) {
11664
- return `http://${formatHostForUrl(host)}:${port}${path3}`;
13538
+ function buildRedirectUrl(host, port, path6) {
13539
+ return `http://${formatHostForUrl(host)}:${port}${path6}`;
11665
13540
  }
11666
13541
 
11667
13542
  // ../../core/auth/node/runner-interactive-oauth.ts
@@ -11679,8 +13554,8 @@ async function runRunnerInteractiveOAuth(options) {
11679
13554
  const server = createServer2();
11680
13555
  let flowResolve;
11681
13556
  let flowReject;
11682
- const flowDone = new Promise((resolve4, reject) => {
11683
- flowResolve = resolve4;
13557
+ const flowDone = new Promise((resolve7, reject) => {
13558
+ flowResolve = resolve7;
11684
13559
  flowReject = reject;
11685
13560
  });
11686
13561
  let timeoutId;
@@ -11760,16 +13635,16 @@ async function runRunnerInteractiveOAuth(options) {
11760
13635
  }
11761
13636
 
11762
13637
  // src/logger.ts
11763
- import path2 from "path";
13638
+ import path5 from "path";
11764
13639
  import pino from "pino";
11765
13640
  var tuiLoggerInstance;
11766
13641
  function getTuiLogger() {
11767
13642
  if (!tuiLoggerInstance) {
11768
- const logDir = process.env.MCP_INSPECTOR_LOG_DIR ?? path2.join(
13643
+ const logDir = process.env.MCP_INSPECTOR_LOG_DIR ?? path5.join(
11769
13644
  process.env.HOME || process.env.USERPROFILE || ".",
11770
13645
  ".mcp-inspector"
11771
13646
  );
11772
- const logPath = path2.join(logDir, "auth.log");
13647
+ const logPath = path5.join(logDir, "auth.log");
11773
13648
  tuiLoggerInstance = pino(
11774
13649
  {
11775
13650
  name: "mcp-inspector-tui",
@@ -13168,9 +15043,283 @@ function PromptsTab({
13168
15043
  }
13169
15044
 
13170
15045
  // src/components/ToolsTab.tsx
13171
- import { useState as useState14, useEffect as useEffect13, useRef as useRef7 } from "react";
15046
+ import { useState as useState14, useEffect as useEffect13, useMemo as useMemo2, useRef as useRef7 } from "react";
13172
15047
  import { Box as Box8, Text as Text7, useInput as useInput5 } from "ink";
15048
+
15049
+ // ../../core/json/schemaLint.ts
15050
+ var BOOLEAN_VALUED_KEYWORDS = /* @__PURE__ */ new Set([
15051
+ "additionalProperties",
15052
+ "unevaluatedProperties",
15053
+ "additionalItems",
15054
+ "unevaluatedItems"
15055
+ ]);
15056
+ var SUBSCHEMA_KEYWORDS = [
15057
+ "items",
15058
+ "contains",
15059
+ "not",
15060
+ "propertyNames",
15061
+ "if",
15062
+ "then",
15063
+ "else",
15064
+ "additionalProperties",
15065
+ "unevaluatedProperties",
15066
+ "additionalItems",
15067
+ "unevaluatedItems",
15068
+ // 2019-09/2020-12: the schema the *decoded* string content must satisfy.
15069
+ // A real schema position, so a bare `true` or a remote `$ref` under it is a
15070
+ // real finding.
15071
+ "contentSchema"
15072
+ ];
15073
+ var SUBSCHEMA_ARRAY_KEYWORDS = [
15074
+ "allOf",
15075
+ "anyOf",
15076
+ "oneOf",
15077
+ "prefixItems"
15078
+ ];
15079
+ var SUBSCHEMA_MAP_KEYWORDS = [
15080
+ "properties",
15081
+ "patternProperties",
15082
+ "dependentSchemas",
15083
+ "dependencies",
15084
+ "$defs",
15085
+ "definitions"
15086
+ ];
15087
+ var CONSTRAINING_KEYWORDS = /* @__PURE__ */ new Set([
15088
+ // Assertions
15089
+ "type",
15090
+ "enum",
15091
+ "const",
15092
+ "multipleOf",
15093
+ "maximum",
15094
+ "exclusiveMaximum",
15095
+ "minimum",
15096
+ "exclusiveMinimum",
15097
+ "maxLength",
15098
+ "minLength",
15099
+ "pattern",
15100
+ "format",
15101
+ "contentMediaType",
15102
+ "contentEncoding",
15103
+ "contentSchema",
15104
+ "maxItems",
15105
+ "minItems",
15106
+ "uniqueItems",
15107
+ "maxContains",
15108
+ "minContains",
15109
+ "maxProperties",
15110
+ "minProperties",
15111
+ "required",
15112
+ "dependentRequired",
15113
+ // Applicators
15114
+ "allOf",
15115
+ "anyOf",
15116
+ "oneOf",
15117
+ "not",
15118
+ // `if` / `then` / `else` are deliberately absent — they only constrain as a
15119
+ // PAIR. `if` alone has no assertion effect, and `then` / `else` are ignored
15120
+ // without it, so `{"if": {"const": 1}}` accepts every value. The pairing is
15121
+ // checked separately in `constrainsInstance`.
15122
+ "items",
15123
+ "prefixItems",
15124
+ "contains",
15125
+ "additionalItems",
15126
+ "unevaluatedItems",
15127
+ "properties",
15128
+ "patternProperties",
15129
+ "additionalProperties",
15130
+ "unevaluatedProperties",
15131
+ "propertyNames",
15132
+ "dependentSchemas",
15133
+ "dependencies",
15134
+ // References
15135
+ "$ref",
15136
+ "$dynamicRef",
15137
+ "$recursiveRef"
15138
+ ]);
15139
+ var JSON_SCHEMA_TYPES = /* @__PURE__ */ new Set([
15140
+ "null",
15141
+ "boolean",
15142
+ "object",
15143
+ "array",
15144
+ "number",
15145
+ "string",
15146
+ "integer"
15147
+ ]);
15148
+ function constrainsInstance(node) {
15149
+ if (Object.keys(node).some((key) => CONSTRAINING_KEYWORDS.has(key))) {
15150
+ return true;
15151
+ }
15152
+ return "if" in node && ("then" in node || "else" in node);
15153
+ }
15154
+ function isPortableTypeUnion(type) {
15155
+ if (type.length === 0) return false;
15156
+ if (!type.every((t) => typeof t === "string" && JSON_SCHEMA_TYPES.has(t))) {
15157
+ return false;
15158
+ }
15159
+ return new Set(type).size === type.length;
15160
+ }
15161
+ var MAX_DEPTH = 64;
15162
+ function isRecord3(value) {
15163
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15164
+ }
15165
+ function childPath(parent, key) {
15166
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) {
15167
+ return parent === "" ? key : `${parent}.${key}`;
15168
+ }
15169
+ return `${parent}[${JSON.stringify(key)}]`;
15170
+ }
15171
+ function indexPath(parent, index) {
15172
+ return `${parent}[${index}]`;
15173
+ }
15174
+ function describeSchemaPath(schema, path6) {
15175
+ return path6 === "" ? schema : `${schema}.${path6}`;
15176
+ }
15177
+ function declaresAnyId(node, depth = 0) {
15178
+ if (depth > MAX_DEPTH || !isRecord3(node)) return false;
15179
+ if (typeof node.$id === "string" && node.$id !== "") return true;
15180
+ let found = false;
15181
+ forEachSubschema(node, "", (child) => {
15182
+ if (!found) found = declaresAnyId(child, depth + 1);
15183
+ });
15184
+ return found;
15185
+ }
15186
+ function add(ctx, rule, severity, path6, issue, suggestion) {
15187
+ ctx.findings.push({
15188
+ rule,
15189
+ severity,
15190
+ schema: ctx.schema,
15191
+ path: path6,
15192
+ issue,
15193
+ suggestion
15194
+ });
15195
+ }
15196
+ function forEachSubschema(node, path6, visit) {
15197
+ for (const key of SUBSCHEMA_MAP_KEYWORDS) {
15198
+ const map = node[key];
15199
+ if (!isRecord3(map)) continue;
15200
+ for (const [name, sub] of Object.entries(map)) {
15201
+ visit(sub, childPath(childPath(path6, key), name), key);
15202
+ }
15203
+ }
15204
+ for (const key of SUBSCHEMA_ARRAY_KEYWORDS) {
15205
+ const arr = node[key];
15206
+ if (!Array.isArray(arr)) continue;
15207
+ arr.forEach((sub, i) => {
15208
+ visit(sub, indexPath(childPath(path6, key), i), key);
15209
+ });
15210
+ }
15211
+ for (const key of SUBSCHEMA_KEYWORDS) {
15212
+ if (!(key in node)) continue;
15213
+ const value = node[key];
15214
+ if (key === "items" && Array.isArray(value)) {
15215
+ value.forEach((sub, i) => {
15216
+ visit(sub, indexPath(childPath(path6, key), i), key);
15217
+ });
15218
+ continue;
15219
+ }
15220
+ visit(value, childPath(path6, key), key);
15221
+ }
15222
+ }
15223
+ function booleanTrueSuggestion(keyword) {
15224
+ if (keyword === "not") {
15225
+ return "Use `{}`, the object form of the always-true schema \u2014 under `not` that is an exact replacement. (Declaring a concrete type here would instead let every value of another type through.)";
15226
+ }
15227
+ 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.';
15228
+ }
15229
+ function walk(node, path6, keyword, depth, ctx) {
15230
+ if (depth > MAX_DEPTH) return;
15231
+ if (typeof node === "boolean") {
15232
+ if (keyword !== void 0 && BOOLEAN_VALUED_KEYWORDS.has(keyword)) return;
15233
+ add(
15234
+ ctx,
15235
+ "boolean-schema",
15236
+ "error",
15237
+ path6,
15238
+ `Bare \`${String(node)}\` used where a schema object is expected.`,
15239
+ node ? booleanTrueSuggestion(keyword) : (
15240
+ // `false` in a property position means the property is FORBIDDEN.
15241
+ // Deleting the entry is not the same thing: with `additionalProperties`
15242
+ // absent (default `true`) the property becomes allowed with any value,
15243
+ // so the suggestion has to preserve the always-false constraint.
15244
+ 'Use the object form of the always-false schema \u2014 `{"not": {}}`. (Deleting the entry is a different contract: under the default `additionalProperties` the property would then be allowed with any value, not forbidden.)'
15245
+ )
15246
+ );
15247
+ return;
15248
+ }
15249
+ if (!isRecord3(node)) return;
15250
+ lintNode(node, path6, keyword, ctx);
15251
+ forEachSubschema(node, path6, (child, childPathValue, childKeyword) => {
15252
+ walk(child, childPathValue, childKeyword, depth + 1, ctx);
15253
+ });
15254
+ }
15255
+ function lintNode(node, path6, keyword, ctx) {
15256
+ const type = node.type;
15257
+ if (Array.isArray(type) && isPortableTypeUnion(type)) {
15258
+ add(
15259
+ ctx,
15260
+ "type-union",
15261
+ "warning",
15262
+ path6,
15263
+ `\`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.`,
15264
+ `Split it into \`anyOf\` branches, each with a single \`type\` \u2014 \`{"anyOf": [${type.map((t) => `{"type": "${t}"}`).join(
15265
+ ", "
15266
+ )}]}\`. (Making the property optional instead is a different contract: absent is not the same as \`null\`.)`
15267
+ );
15268
+ }
15269
+ const ref = node.$ref;
15270
+ if (typeof ref === "string" && ref !== "" && !ref.startsWith("#") && !ctx.hasEmbeddedIds) {
15271
+ add(
15272
+ ctx,
15273
+ "remote-ref",
15274
+ "warning",
15275
+ path6,
15276
+ `\`$ref\` points outside this document (\`${ref}\`). Clients do not fetch remote schemas, so the constraint is dropped or the tool is rejected.`,
15277
+ "Inline the referenced schema, or move it into `$defs` and reference it as `#/$defs/<name>`."
15278
+ );
15279
+ }
15280
+ const constrains = constrainsInstance(node);
15281
+ if (!constrains && keyword !== "not") {
15282
+ add(
15283
+ ctx,
15284
+ "untyped-schema",
15285
+ "warning",
15286
+ path6,
15287
+ "Schema carries no validation keyword at all, so it accepts any value \u2014 the object-literal spelling of a bare `true`.",
15288
+ // "changes", not "narrows": in an ordinary property position it does
15289
+ // narrow, but under a conditional (`if`) a typed replacement redirects
15290
+ // which branch applies rather than tightening anything.
15291
+ '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, which is the point: as written it constrains nothing.'
15292
+ );
15293
+ }
15294
+ }
15295
+ function lintSchema(schema, kind, findings) {
15296
+ walk(schema, "", void 0, 0, {
15297
+ schema: kind,
15298
+ findings,
15299
+ hasEmbeddedIds: declaresAnyId(schema)
15300
+ });
15301
+ }
15302
+ function lintToolSchemas(tool) {
15303
+ const findings = [];
15304
+ if (tool.inputSchema !== void 0) {
15305
+ lintSchema(tool.inputSchema, "inputSchema", findings);
15306
+ }
15307
+ if (tool.outputSchema !== void 0) {
15308
+ lintSchema(tool.outputSchema, "outputSchema", findings);
15309
+ }
15310
+ return findings;
15311
+ }
15312
+
15313
+ // src/components/ToolsTab.tsx
13173
15314
  import { Fragment as Fragment5, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
15315
+ var SEVERITY_MARKER = {
15316
+ error: { glyph: "!", color: "red" },
15317
+ warning: { glyph: "?", color: "yellow" }
15318
+ };
15319
+ function schemaMarker(findings) {
15320
+ if (!findings || findings.length === 0) return void 0;
15321
+ return findings.some((f) => f.severity === "error") ? SEVERITY_MARKER.error : SEVERITY_MARKER.warning;
15322
+ }
13174
15323
  function ToolsTab({
13175
15324
  tools,
13176
15325
  isConnected,
@@ -13188,6 +15337,10 @@ function ToolsTab({
13188
15337
  { resetWhen: tools }
13189
15338
  );
13190
15339
  const [error] = useState14(null);
15340
+ const findingsByTool = useMemo2(
15341
+ () => tools.map((tool) => lintToolSchemas(tool)),
15342
+ [tools]
15343
+ );
13191
15344
  const scrollViewRef = useRef7(null);
13192
15345
  const listWidth = Math.floor(width * 0.4);
13193
15346
  const detailWidth = width - listWidth;
@@ -13231,6 +15384,7 @@ function ToolsTab({
13231
15384
  scrollViewRef.current?.scrollTo(0);
13232
15385
  }, [selectedIndex]);
13233
15386
  const selectedTool = tools[selectedIndex] || null;
15387
+ const selectedFindings = findingsByTool[selectedIndex] ?? [];
13234
15388
  return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", width, height, children: [
13235
15389
  /* @__PURE__ */ jsxs7(
13236
15390
  Box8,
@@ -13267,9 +15421,14 @@ function ToolsTab({
13267
15421
  children: tools.slice(firstVisible, firstVisible + visibleCount).map((tool, i) => {
13268
15422
  const index = firstVisible + i;
13269
15423
  const isSelected = index === selectedIndex;
15424
+ const marker = schemaMarker(findingsByTool[index]);
13270
15425
  return /* @__PURE__ */ jsx8(Box8, { paddingY: 0, flexShrink: 0, children: /* @__PURE__ */ jsxs7(Text7, { children: [
13271
15426
  isSelected ? "\u25B6 " : " ",
13272
- tool.name || `Tool ${index + 1}`
15427
+ tool.name || `Tool ${index + 1}`,
15428
+ marker && /* @__PURE__ */ jsxs7(Text7, { color: marker.color, children: [
15429
+ " ",
15430
+ marker.glyph
15431
+ ] })
13273
15432
  ] }) }, tool.name || index);
13274
15433
  })
13275
15434
  }
@@ -13317,6 +15476,38 @@ function ToolsTab({
13317
15476
  },
13318
15477
  `desc-${idx}`
13319
15478
  )) }),
15479
+ selectedFindings.length > 0 && /* @__PURE__ */ jsxs7(Fragment5, { children: [
15480
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexShrink: 0, children: /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "yellow", children: [
15481
+ "Schema Portability (",
15482
+ selectedFindings.length,
15483
+ "):"
15484
+ ] }) }),
15485
+ selectedFindings.map((finding, idx) => {
15486
+ const marker = SEVERITY_MARKER[finding.severity];
15487
+ return /* @__PURE__ */ jsxs7(
15488
+ Box8,
15489
+ {
15490
+ flexDirection: "column",
15491
+ paddingLeft: 2,
15492
+ marginTop: 1,
15493
+ flexShrink: 0,
15494
+ children: [
15495
+ /* @__PURE__ */ jsxs7(Text7, { color: marker.color, children: [
15496
+ marker.glyph,
15497
+ " ",
15498
+ describeSchemaPath(finding.schema, finding.path)
15499
+ ] }),
15500
+ /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: finding.issue }),
15501
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
15502
+ "Fix: ",
15503
+ finding.suggestion
15504
+ ] })
15505
+ ]
15506
+ },
15507
+ `finding-${finding.schema}-${finding.path}-${finding.rule}-${idx}`
15508
+ );
15509
+ })
15510
+ ] }),
13320
15511
  selectedTool.inputSchema && /* @__PURE__ */ jsxs7(Fragment5, { children: [
13321
15512
  /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexShrink: 0, children: /* @__PURE__ */ jsx8(Text7, { bold: true, children: "Input Schema:" }) }),
13322
15513
  JSON.stringify(selectedTool.inputSchema, null, 2).split("\n").map((line, idx) => /* @__PURE__ */ jsx8(
@@ -13907,7 +16098,7 @@ import { Box as Box23, Text as Text20, useInput as useInput17 } from "ink";
13907
16098
 
13908
16099
  // node_modules/ink-form/lib/Form.js
13909
16100
  import { Box as Box22, useFocusManager, useInput as useInput16 } from "ink";
13910
- import { useEffect as useEffect19, useMemo as useMemo2, useState as useState18 } from "react";
16101
+ import { useEffect as useEffect19, useMemo as useMemo3, useState as useState18 } from "react";
13911
16102
  import * as React23 from "react";
13912
16103
 
13913
16104
  // node_modules/ink-form/lib/FormHeader.js
@@ -15325,7 +17516,7 @@ var Form = (props) => {
15325
17516
  const [currentTab, setCurrentTab] = useState18(0);
15326
17517
  const [value, setValue] = useState18(props.value ?? {});
15327
17518
  const [editingField, setEditingField] = useState18();
15328
- const canSubmitForm = useMemo2(() => canSubmit(props.form, value), [value, props.form]);
17519
+ const canSubmitForm = useMemo3(() => canSubmit(props.form, value), [value, props.form]);
15329
17520
  const focusManager = useFocusManager();
15330
17521
  useEffect19(() => {
15331
17522
  focusManager.enableFocus();
@@ -16461,11 +18652,13 @@ function App({
16461
18652
  const { config: serverConfig, settings: savedSettings } = mcpServers[serverName];
16462
18653
  const environment = {
16463
18654
  transport: createTransportNode,
16464
- logger: getTuiLogger()
18655
+ logger: getTuiLogger(),
18656
+ // Bottom of the fetch stack, so InspectorClient's wrappers compose
18657
+ // over it and OAuth requests are proxied too (#2067). Undefined when
18658
+ // no proxy env var is set.
18659
+ fetch: createProxyFetch()
16465
18660
  };
16466
- const defaultMetadata = savedSettings?.metadata ? Object.fromEntries(
16467
- savedSettings.metadata.filter((m) => m.key.trim() !== "").map((m) => [m.key, m.value])
16468
- ) : void 0;
18661
+ const defaultMetadata = savedSettings?.metadata;
16469
18662
  const clientAuthOptions = buildRunnerClientAuthOptions(
16470
18663
  clientConfig,
16471
18664
  savedSettings,
@@ -16614,7 +18807,7 @@ function App({
16614
18807
  setActiveTab("info");
16615
18808
  }
16616
18809
  }, [activeTab, selectedServerConfig]);
16617
- const selectedInspectorClient = useMemo3(
18810
+ const selectedInspectorClient = useMemo4(
16618
18811
  () => selectedServer ? inspectorClients[selectedServer] : null,
16619
18812
  [selectedServer, inspectorClients]
16620
18813
  );
@@ -16627,15 +18820,15 @@ function App({
16627
18820
  disconnect: disconnectInspector,
16628
18821
  lastError: inspectorLastError
16629
18822
  } = useInspectorClient(selectedInspectorClient);
16630
- const selectedMessageLogState = useMemo3(
18823
+ const selectedMessageLogState = useMemo4(
16631
18824
  () => selectedServer && messageLogStates[selectedServer] ? messageLogStates[selectedServer] : null,
16632
18825
  [selectedServer, messageLogStates]
16633
18826
  );
16634
- const selectedFetchRequestLogState = useMemo3(
18827
+ const selectedFetchRequestLogState = useMemo4(
16635
18828
  () => selectedServer && fetchRequestLogStates[selectedServer] ? fetchRequestLogStates[selectedServer] : null,
16636
18829
  [selectedServer, fetchRequestLogStates]
16637
18830
  );
16638
- const selectedStderrLogState = useMemo3(
18831
+ const selectedStderrLogState = useMemo4(
16639
18832
  () => selectedServer && stderrLogStates[selectedServer] ? stderrLogStates[selectedServer] : null,
16640
18833
  [selectedServer, stderrLogStates]
16641
18834
  );
@@ -16648,7 +18841,7 @@ function App({
16648
18841
  const { stderrLogs: inspectorStderrLogs } = useStderrLog(
16649
18842
  selectedStderrLogState
16650
18843
  );
16651
- const selectedManagedToolsState = useMemo3(
18844
+ const selectedManagedToolsState = useMemo4(
16652
18845
  () => selectedServer && managedToolsStates[selectedServer] ? managedToolsStates[selectedServer] : null,
16653
18846
  [selectedServer, managedToolsStates]
16654
18847
  );
@@ -16656,15 +18849,15 @@ function App({
16656
18849
  selectedInspectorClient,
16657
18850
  selectedManagedToolsState
16658
18851
  );
16659
- const selectedManagedResourcesState = useMemo3(
18852
+ const selectedManagedResourcesState = useMemo4(
16660
18853
  () => selectedServer && managedResourcesStates[selectedServer] ? managedResourcesStates[selectedServer] : null,
16661
18854
  [selectedServer, managedResourcesStates]
16662
18855
  );
16663
- const selectedManagedResourceTemplatesState = useMemo3(
18856
+ const selectedManagedResourceTemplatesState = useMemo4(
16664
18857
  () => selectedServer && managedResourceTemplatesStates[selectedServer] ? managedResourceTemplatesStates[selectedServer] : null,
16665
18858
  [selectedServer, managedResourceTemplatesStates]
16666
18859
  );
16667
- const selectedManagedPromptsState = useMemo3(
18860
+ const selectedManagedPromptsState = useMemo4(
16668
18861
  () => selectedServer && managedPromptsStates[selectedServer] ? managedPromptsStates[selectedServer] : null,
16669
18862
  [selectedServer, managedPromptsStates]
16670
18863
  );
@@ -16975,7 +19168,7 @@ function App({
16975
19168
  }
16976
19169
  setOauthRevision((n) => n + 1);
16977
19170
  }, [selectedInspectorClient, inspectorStatus, disconnectInspector]);
16978
- const currentServerState = useMemo3(() => {
19171
+ const currentServerState = useMemo4(() => {
16979
19172
  if (!selectedServer) return null;
16980
19173
  return {
16981
19174
  status: inspectorStatus,
@@ -17937,7 +20130,7 @@ async function runTui(args) {
17937
20130
 
17938
20131
  // index.ts
17939
20132
  var __filename = fileURLToPath2(import.meta.url);
17940
- var isMain = process.argv[1] !== void 0 && resolve3(process.argv[1]) === resolve3(__filename);
20133
+ var isMain = process.argv[1] !== void 0 && resolve6(process.argv[1]) === resolve6(__filename);
17941
20134
  if (isMain) {
17942
20135
  runTui(process.argv).catch((err) => {
17943
20136
  console.error("Error:", err instanceof Error ? err.message : err);
@@ -17954,3 +20147,16 @@ if (isMain) {
17954
20147
  export {
17955
20148
  runTui
17956
20149
  };
20150
+ /* v8 ignore next 2 -- @preserve: the non-Error arm is unreachable via
20151
+ proper-lockfile, which rejects only with Errors. */
20152
+ /* v8 ignore next -- @preserve: node validates the cost parameters
20153
+ synchronously (an out-of-grammar `N` throws from the call itself,
20154
+ which the corrupt-KDF test exercises), so this asynchronous channel
20155
+ is left for allocation failure — not provokable deterministically. */
20156
+ /* v8 ignore next -- @preserve: unreachable, `encryptedEnvelopeProblem`
20157
+ above returns a reason for anything `decodeParts` would reject. Kept as
20158
+ the type narrowing `parts` needs. */
20159
+ /* v8 ignore next -- @preserve: the walk can only run out of ancestors if
20160
+ the filesystem root itself does not exist, which would mean the process
20161
+ has no cwd to have resolved against. Kept as the loop's termination
20162
+ guard rather than trusting that invariant. */