@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.
- package/README.md +185 -27
- package/clients/cli/build/index.js +11489 -10818
- package/clients/launcher/build/index.js +6 -4
- package/clients/tui/build/index.js +2580 -374
- package/clients/web/build/index.js +2083 -259
- package/clients/web/dist/assets/{index-CiUpWldH.css → index-DQVN73FG.css} +1 -1
- package/clients/web/dist/assets/index-JCsLrWVa.js +1484 -0
- package/clients/web/dist/assets/{prism-light-DBxbqjnM.js → prism-light-HLcnHJHw.js} +1 -1
- package/clients/web/dist/assets/worker-json-8CDXFfik.js +1631 -0
- package/clients/web/dist/index.html +2 -2
- package/clients/web/static/sandbox_proxy.html +122 -10
- package/package.json +10 -6
- package/clients/tui/build/chunk-MCKGQKYU.js +0 -15
- package/clients/tui/build/undici-KNXAVD2F.js +0 -26644
- package/clients/web/build/chunk-MCKGQKYU.js +0 -15
- package/clients/web/build/undici-KNXAVD2F.js +0 -26644
- package/clients/web/dist/assets/index-CwhDusR3.js +0 -237
|
@@ -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
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
2066
|
+
if (!entries || Object.keys(entries).length === 0) return false;
|
|
2067
|
+
let complete = true;
|
|
677
2068
|
try {
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
2096
|
+
return false;
|
|
720
2097
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
2098
|
+
return true;
|
|
2099
|
+
}
|
|
2100
|
+
var DeferredSecretStore = class {
|
|
2101
|
+
async target() {
|
|
2102
|
+
return (await resolveSecretStore()).store;
|
|
725
2103
|
}
|
|
726
|
-
|
|
727
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
794
|
-
await
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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 ??
|
|
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/
|
|
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
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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 =
|
|
1478
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
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
|
|
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(
|
|
1958
|
-
return
|
|
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,
|
|
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 ||
|
|
1971
|
-
return `${pathName(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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, [...
|
|
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, [...
|
|
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,
|
|
3506
|
+
function valueAtPath(root, path6) {
|
|
2050
3507
|
let node = root;
|
|
2051
|
-
for (const key of
|
|
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
|
|
3897
|
+
const path6 = issue.path.join(".");
|
|
2369
3898
|
const detail = expectedDetail(issue);
|
|
2370
3899
|
const message = detail ? `${issue.message} (${detail})` : issue.message;
|
|
2371
|
-
return
|
|
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,
|
|
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 =
|
|
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,
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5593
|
+
let minted;
|
|
3957
5594
|
try {
|
|
3958
|
-
|
|
5595
|
+
minted = await mintEmaResourceTokens(config);
|
|
3959
5596
|
} catch (err) {
|
|
3960
5597
|
throw wrapEmaMintError(err);
|
|
3961
5598
|
}
|
|
3962
|
-
await
|
|
3963
|
-
|
|
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
|
|
3977
|
-
await
|
|
3978
|
-
|
|
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
|
|
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:
|
|
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
|
|
4628
|
-
* token
|
|
4629
|
-
*
|
|
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" &&
|
|
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((
|
|
4678
|
-
release =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5011
|
-
|
|
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
|
|
5024
|
-
const
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
6089
|
-
resolvePayload =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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((
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
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) =>
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
11026
|
-
import { dirname as
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
13428
|
+
path: path6 = DEFAULT_CALLBACK_PATH,
|
|
11554
13429
|
onCallback,
|
|
11555
13430
|
onError
|
|
11556
13431
|
} = options;
|
|
11557
|
-
if (!
|
|
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 =
|
|
11566
|
-
return new Promise((
|
|
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
|
-
|
|
13451
|
+
resolve7({
|
|
11577
13452
|
port: this.port,
|
|
11578
|
-
redirectUrl: buildRedirectUrl(hostname, this.port,
|
|
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((
|
|
11589
|
-
this.server.close(() =>
|
|
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,
|
|
11664
|
-
return `http://${formatHostForUrl(host)}:${port}${
|
|
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((
|
|
11683
|
-
flowResolve =
|
|
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
|
|
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 ??
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
18823
|
+
const selectedMessageLogState = useMemo4(
|
|
16631
18824
|
() => selectedServer && messageLogStates[selectedServer] ? messageLogStates[selectedServer] : null,
|
|
16632
18825
|
[selectedServer, messageLogStates]
|
|
16633
18826
|
);
|
|
16634
|
-
const selectedFetchRequestLogState =
|
|
18827
|
+
const selectedFetchRequestLogState = useMemo4(
|
|
16635
18828
|
() => selectedServer && fetchRequestLogStates[selectedServer] ? fetchRequestLogStates[selectedServer] : null,
|
|
16636
18829
|
[selectedServer, fetchRequestLogStates]
|
|
16637
18830
|
);
|
|
16638
|
-
const selectedStderrLogState =
|
|
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 =
|
|
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 =
|
|
18852
|
+
const selectedManagedResourcesState = useMemo4(
|
|
16660
18853
|
() => selectedServer && managedResourcesStates[selectedServer] ? managedResourcesStates[selectedServer] : null,
|
|
16661
18854
|
[selectedServer, managedResourcesStates]
|
|
16662
18855
|
);
|
|
16663
|
-
const selectedManagedResourceTemplatesState =
|
|
18856
|
+
const selectedManagedResourceTemplatesState = useMemo4(
|
|
16664
18857
|
() => selectedServer && managedResourceTemplatesStates[selectedServer] ? managedResourceTemplatesStates[selectedServer] : null,
|
|
16665
18858
|
[selectedServer, managedResourceTemplatesStates]
|
|
16666
18859
|
);
|
|
16667
|
-
const selectedManagedPromptsState =
|
|
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 =
|
|
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 &&
|
|
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. */
|