@modelcontextprotocol/inspector 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +216 -0
- package/README.md +13 -6
- package/clients/cli/build/index.js +1834 -280
- package/clients/tui/build/index.js +1987 -593
- package/clients/web/build/index.js +2596 -1168
- package/clients/web/dist/assets/index-DgXruQhz.js +1598 -0
- package/clients/web/dist/assets/{prism-light-CoQfbOXL.js → prism-light-leP1dqtE.js} +1 -1
- package/clients/web/dist/index.html +1 -1
- package/package.json +10 -8
- package/clients/web/dist/assets/index-DZkZ6KYt.js +0 -1598
|
@@ -90,6 +90,19 @@ var SECRET_FIELD_OAUTH_CLIENT_SECRET = "oauth-client-secret";
|
|
|
90
90
|
var SECRET_FIELD_IDP_CLIENT_SECRET = "idp-client-secret";
|
|
91
91
|
var envSecretField = (envKey) => `env:${envKey}`;
|
|
92
92
|
|
|
93
|
+
// ../../core/storage/own-entry.ts
|
|
94
|
+
function setOwnEntry(map, key, value) {
|
|
95
|
+
Object.defineProperty(map, key, {
|
|
96
|
+
value,
|
|
97
|
+
enumerable: true,
|
|
98
|
+
writable: true,
|
|
99
|
+
configurable: true
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function getOwnEntry(map, key) {
|
|
103
|
+
return map !== void 0 && Object.hasOwn(map, key) ? map[key] : void 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
// ../../core/auth/node/secret-store.ts
|
|
94
107
|
var SERVICE_NAME = "mcp-inspector";
|
|
95
108
|
var keyringLoad;
|
|
@@ -142,6 +155,12 @@ var SecretStoreUnavailableError = class extends Error {
|
|
|
142
155
|
this.name = "SecretStoreUnavailableError";
|
|
143
156
|
}
|
|
144
157
|
};
|
|
158
|
+
var SecretFileLockHeldError = class extends SecretStoreUnavailableError {
|
|
159
|
+
constructor(message, options) {
|
|
160
|
+
super(message, options);
|
|
161
|
+
this.name = "SecretFileLockHeldError";
|
|
162
|
+
}
|
|
163
|
+
};
|
|
145
164
|
var KeychainUnavailableError = class extends SecretStoreUnavailableError {
|
|
146
165
|
constructor(cause) {
|
|
147
166
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
@@ -172,6 +191,7 @@ async function probeKeyringAvailable() {
|
|
|
172
191
|
try {
|
|
173
192
|
const entry = new keyring.mod.AsyncEntry(SERVICE_NAME, PROBE_ACCOUNT);
|
|
174
193
|
await entry.getPassword();
|
|
194
|
+
await keyring.mod.findCredentialsAsync(SERVICE_NAME);
|
|
175
195
|
return { available: true };
|
|
176
196
|
} catch (err) {
|
|
177
197
|
return {
|
|
@@ -181,6 +201,7 @@ async function probeKeyringAvailable() {
|
|
|
181
201
|
}
|
|
182
202
|
}
|
|
183
203
|
var PROBE_ACCOUNT = "__inspector:probe";
|
|
204
|
+
var isNoEntryError = (err) => err instanceof Error && /no (matching )?entry/i.test(err.message);
|
|
184
205
|
var KeyringSecretStore = class {
|
|
185
206
|
/**
|
|
186
207
|
* The intolerant read. Same lookup as {@link get}, minus the catch — a
|
|
@@ -232,23 +253,27 @@ var KeyringSecretStore = class {
|
|
|
232
253
|
async delete(serverId, field) {
|
|
233
254
|
try {
|
|
234
255
|
const keyring = await loadKeyring();
|
|
235
|
-
if (!keyring.ok)
|
|
256
|
+
if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
|
|
236
257
|
const entry = new keyring.mod.AsyncEntry(
|
|
237
258
|
SERVICE_NAME,
|
|
238
259
|
buildAccount(serverId, field)
|
|
239
260
|
);
|
|
240
261
|
await entry.deleteCredential();
|
|
241
|
-
} catch {
|
|
262
|
+
} catch (err) {
|
|
263
|
+
if (isNoEntryError(err)) return;
|
|
264
|
+
if (err instanceof KeychainUnavailableError) throw err;
|
|
265
|
+
throw new KeychainUnavailableError(err);
|
|
242
266
|
}
|
|
243
267
|
}
|
|
244
268
|
async deleteAllForServer(serverId) {
|
|
245
269
|
let creds;
|
|
246
270
|
try {
|
|
247
271
|
const keyring = await loadKeyring();
|
|
248
|
-
if (!keyring.ok)
|
|
272
|
+
if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
|
|
249
273
|
creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
|
|
250
|
-
} catch {
|
|
251
|
-
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (err instanceof KeychainUnavailableError) throw err;
|
|
276
|
+
throw new KeychainUnavailableError(err);
|
|
252
277
|
}
|
|
253
278
|
const prefix = `${serverId}:`;
|
|
254
279
|
for (const c of creds) {
|
|
@@ -262,6 +287,25 @@ var KeyringSecretStore = class {
|
|
|
262
287
|
async function secretStoreGetStrict(store, serverId, field) {
|
|
263
288
|
return store.getStrict ? store.getStrict(serverId, field) : store.get(serverId, field);
|
|
264
289
|
}
|
|
290
|
+
async function snapshotSecretFields(store, serverId, fields) {
|
|
291
|
+
return Promise.all(
|
|
292
|
+
fields.map(async (field) => ({
|
|
293
|
+
serverId,
|
|
294
|
+
field,
|
|
295
|
+
value: await secretStoreGetStrict(store, serverId, field)
|
|
296
|
+
}))
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
async function restoreSecretFields(store, snapshot, onRestoreFailure) {
|
|
300
|
+
for (const { serverId, field, value } of snapshot) {
|
|
301
|
+
try {
|
|
302
|
+
if (value === null) await store.delete(serverId, field);
|
|
303
|
+
else await store.set(serverId, field, value);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
onRestoreFailure(error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
265
309
|
async function secretStoreGetMany(store, requests) {
|
|
266
310
|
if (store.getMany) return store.getMany(requests);
|
|
267
311
|
const out = {};
|
|
@@ -274,16 +318,44 @@ async function secretStoreGetMany(store, requests) {
|
|
|
274
318
|
);
|
|
275
319
|
const found = {};
|
|
276
320
|
for (const [field, value] of entries) {
|
|
277
|
-
if (value !== null) found
|
|
321
|
+
if (value !== null) setOwnEntry(found, field, value);
|
|
278
322
|
}
|
|
279
|
-
out
|
|
323
|
+
setOwnEntry(out, serverId, found);
|
|
280
324
|
})
|
|
281
325
|
);
|
|
282
326
|
return out;
|
|
283
327
|
}
|
|
328
|
+
async function secretStoreGetManyStrict(store, requests) {
|
|
329
|
+
if (store.getManyStrict) return store.getManyStrict(requests);
|
|
330
|
+
const out = {};
|
|
331
|
+
await Promise.all(
|
|
332
|
+
requests.map(async ({ serverId, fields }) => {
|
|
333
|
+
const entries = await Promise.all(
|
|
334
|
+
fields.map(
|
|
335
|
+
async (field) => [
|
|
336
|
+
field,
|
|
337
|
+
await secretStoreGetStrict(store, serverId, field)
|
|
338
|
+
]
|
|
339
|
+
)
|
|
340
|
+
);
|
|
341
|
+
const found = {};
|
|
342
|
+
for (const [field, value] of entries) {
|
|
343
|
+
if (value !== null) setOwnEntry(found, field, value);
|
|
344
|
+
}
|
|
345
|
+
setOwnEntry(out, serverId, found);
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
async function settleStoreMutations(mutations) {
|
|
351
|
+
const results = await Promise.allSettled(mutations);
|
|
352
|
+
for (const result of results) {
|
|
353
|
+
if (result.status === "rejected") throw result.reason;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
284
356
|
async function secretStoreSetMany(store, serverId, values) {
|
|
285
357
|
if (store.setMany) return store.setMany(serverId, values);
|
|
286
|
-
await
|
|
358
|
+
await settleStoreMutations(
|
|
287
359
|
Object.entries(values).map(
|
|
288
360
|
([field, value]) => store.set(serverId, field, value)
|
|
289
361
|
)
|
|
@@ -430,6 +502,10 @@ var ModernListSkillsResultSchema = ListSkillsResultSchema.extend({
|
|
|
430
502
|
var GetSkillEnvelopeSchema = z.looseObject({
|
|
431
503
|
skill: SkillEntrySchema
|
|
432
504
|
});
|
|
505
|
+
var ModernGetSkillEnvelopeSchema = GetSkillEnvelopeSchema.extend({
|
|
506
|
+
ttlMs: z.int().min(0),
|
|
507
|
+
cacheScope: z.enum(["public", "private"])
|
|
508
|
+
});
|
|
433
509
|
var GetSkillResultSchema = GetSkillEnvelopeSchema.transform(
|
|
434
510
|
(result) => result.skill
|
|
435
511
|
);
|
|
@@ -1041,6 +1117,176 @@ function checkSkillNameCollisions(entries) {
|
|
|
1041
1117
|
return issues;
|
|
1042
1118
|
}
|
|
1043
1119
|
|
|
1120
|
+
// ../../core/json/localRefs.ts
|
|
1121
|
+
var SUBSCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1122
|
+
"items",
|
|
1123
|
+
"contains",
|
|
1124
|
+
"not",
|
|
1125
|
+
"propertyNames",
|
|
1126
|
+
"if",
|
|
1127
|
+
"then",
|
|
1128
|
+
"else",
|
|
1129
|
+
"additionalProperties",
|
|
1130
|
+
"unevaluatedProperties",
|
|
1131
|
+
"additionalItems",
|
|
1132
|
+
"unevaluatedItems",
|
|
1133
|
+
"contentSchema"
|
|
1134
|
+
]);
|
|
1135
|
+
var SUBSCHEMA_ARRAY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1136
|
+
"allOf",
|
|
1137
|
+
"anyOf",
|
|
1138
|
+
"oneOf",
|
|
1139
|
+
"prefixItems"
|
|
1140
|
+
]);
|
|
1141
|
+
var SUBSCHEMA_MAP_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1142
|
+
"properties",
|
|
1143
|
+
"patternProperties",
|
|
1144
|
+
"dependentSchemas",
|
|
1145
|
+
// Pre-2019 spelling of `dependentSchemas`; its array values are property
|
|
1146
|
+
// name lists, which are not schemas and pass through untouched.
|
|
1147
|
+
"dependencies",
|
|
1148
|
+
"$defs",
|
|
1149
|
+
"definitions"
|
|
1150
|
+
]);
|
|
1151
|
+
var NON_CONSTRAINT_SIBLINGS = /* @__PURE__ */ new Set([
|
|
1152
|
+
"title",
|
|
1153
|
+
"description",
|
|
1154
|
+
"default",
|
|
1155
|
+
"examples",
|
|
1156
|
+
"deprecated",
|
|
1157
|
+
"readOnly",
|
|
1158
|
+
"writeOnly",
|
|
1159
|
+
// Non-standard labels for `enum` values, read by both form builders.
|
|
1160
|
+
"enumNames",
|
|
1161
|
+
"$comment",
|
|
1162
|
+
"$schema",
|
|
1163
|
+
"$id",
|
|
1164
|
+
"$defs",
|
|
1165
|
+
"definitions"
|
|
1166
|
+
]);
|
|
1167
|
+
var EXPANSION_BUDGET = 1e4;
|
|
1168
|
+
var MAX_DEPTH = 64;
|
|
1169
|
+
var Bail = class extends Error {
|
|
1170
|
+
};
|
|
1171
|
+
function spend(traversal) {
|
|
1172
|
+
traversal.remaining -= 1;
|
|
1173
|
+
if (traversal.remaining < 0) throw new Bail();
|
|
1174
|
+
}
|
|
1175
|
+
function mapSubschemas(node, visit) {
|
|
1176
|
+
const result = {};
|
|
1177
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1178
|
+
let next = value;
|
|
1179
|
+
if (SUBSCHEMA_KEYWORDS.has(key)) {
|
|
1180
|
+
next = Array.isArray(value) ? value.map(visit) : visit(value);
|
|
1181
|
+
} else if (SUBSCHEMA_ARRAY_KEYWORDS.has(key) && Array.isArray(value)) {
|
|
1182
|
+
next = value.map(visit);
|
|
1183
|
+
} else if (SUBSCHEMA_MAP_KEYWORDS.has(key) && isRecord(value)) {
|
|
1184
|
+
next = mapNames(value, visit);
|
|
1185
|
+
}
|
|
1186
|
+
define(result, key, next);
|
|
1187
|
+
}
|
|
1188
|
+
return result;
|
|
1189
|
+
}
|
|
1190
|
+
function mapNames(map, visit) {
|
|
1191
|
+
const result = {};
|
|
1192
|
+
for (const [name, value] of Object.entries(map)) {
|
|
1193
|
+
define(result, name, Array.isArray(value) ? value : visit(value));
|
|
1194
|
+
}
|
|
1195
|
+
return result;
|
|
1196
|
+
}
|
|
1197
|
+
function define(target, key, value) {
|
|
1198
|
+
Object.defineProperty(target, key, {
|
|
1199
|
+
value,
|
|
1200
|
+
writable: true,
|
|
1201
|
+
enumerable: true,
|
|
1202
|
+
configurable: true
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
function isRecord(value) {
|
|
1206
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1207
|
+
}
|
|
1208
|
+
function resolvePointer(root, ref) {
|
|
1209
|
+
if (!ref.startsWith("#")) return void 0;
|
|
1210
|
+
let pointer;
|
|
1211
|
+
try {
|
|
1212
|
+
pointer = decodeURIComponent(ref.slice(1));
|
|
1213
|
+
} catch {
|
|
1214
|
+
return void 0;
|
|
1215
|
+
}
|
|
1216
|
+
if (pointer === "") return root;
|
|
1217
|
+
if (!pointer.startsWith("/")) return void 0;
|
|
1218
|
+
let current = root;
|
|
1219
|
+
for (const token of pointer.slice(1).split("/")) {
|
|
1220
|
+
if (/~(?![01])/.test(token)) return void 0;
|
|
1221
|
+
const segment = token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
1222
|
+
if (Array.isArray(current)) {
|
|
1223
|
+
const index = Number(segment);
|
|
1224
|
+
if (!/^(0|[1-9]\d*)$/.test(segment) || index >= current.length) {
|
|
1225
|
+
return void 0;
|
|
1226
|
+
}
|
|
1227
|
+
current = current[index];
|
|
1228
|
+
} else if (isRecord(current) && Object.hasOwn(current, segment)) {
|
|
1229
|
+
current = current[segment];
|
|
1230
|
+
} else {
|
|
1231
|
+
return void 0;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return current;
|
|
1235
|
+
}
|
|
1236
|
+
function containsRef(node, depth) {
|
|
1237
|
+
if (depth > MAX_DEPTH) throw new Bail();
|
|
1238
|
+
if (!isRecord(node)) return false;
|
|
1239
|
+
if (typeof node.$ref === "string") return true;
|
|
1240
|
+
let found = false;
|
|
1241
|
+
mapSubschemas(node, (child) => {
|
|
1242
|
+
found ||= containsRef(child, depth + 1);
|
|
1243
|
+
return child;
|
|
1244
|
+
});
|
|
1245
|
+
return found;
|
|
1246
|
+
}
|
|
1247
|
+
function inline(node, traversal, depth) {
|
|
1248
|
+
if (depth > MAX_DEPTH) throw new Bail();
|
|
1249
|
+
if (!isRecord(node)) return node;
|
|
1250
|
+
if (node !== traversal.root && typeof node.$id === "string") return node;
|
|
1251
|
+
spend(traversal);
|
|
1252
|
+
const visit = (child) => inline(child, traversal, depth + 1);
|
|
1253
|
+
const ref = node.$ref;
|
|
1254
|
+
const siblingsConstrain = Object.keys(node).some(
|
|
1255
|
+
(key) => key !== "$ref" && !NON_CONSTRAINT_SIBLINGS.has(key)
|
|
1256
|
+
);
|
|
1257
|
+
if (typeof ref === "string" && !siblingsConstrain && !traversal.active.has(ref)) {
|
|
1258
|
+
const target = resolvePointer(traversal.root, ref);
|
|
1259
|
+
if (isRecord(target)) {
|
|
1260
|
+
traversal.active.add(ref);
|
|
1261
|
+
const resolved2 = visit(target);
|
|
1262
|
+
traversal.active.delete(ref);
|
|
1263
|
+
const siblings = { ...node };
|
|
1264
|
+
delete siblings.$ref;
|
|
1265
|
+
return { ...resolved2, ...mapSubschemas(siblings, visit) };
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return mapSubschemas(node, visit);
|
|
1269
|
+
}
|
|
1270
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
1271
|
+
function inlineLocalRefs(schema) {
|
|
1272
|
+
if (!isRecord(schema)) return schema;
|
|
1273
|
+
const cached = cache.get(schema);
|
|
1274
|
+
if (cached !== void 0) return cached;
|
|
1275
|
+
let resolved2;
|
|
1276
|
+
try {
|
|
1277
|
+
resolved2 = containsRef(schema, 0) ? inline(
|
|
1278
|
+
schema,
|
|
1279
|
+
{ root: schema, active: /* @__PURE__ */ new Set(), remaining: EXPANSION_BUDGET },
|
|
1280
|
+
0
|
|
1281
|
+
) : schema;
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
if (!(error instanceof Bail)) throw error;
|
|
1284
|
+
resolved2 = schema;
|
|
1285
|
+
}
|
|
1286
|
+
cache.set(schema, resolved2);
|
|
1287
|
+
return resolved2;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1044
1290
|
// ../../core/json/nullableUnion.ts
|
|
1045
1291
|
var RENDERABLE_TYPES = [
|
|
1046
1292
|
"string",
|
|
@@ -1639,7 +1885,7 @@ function convertToolParameters(tool, params) {
|
|
|
1639
1885
|
function convertParametersForSchema(inputSchema, params) {
|
|
1640
1886
|
const result = {};
|
|
1641
1887
|
const { base, branches } = resolveRootUnion(
|
|
1642
|
-
inputSchema ?? {}
|
|
1888
|
+
inlineLocalRefs(inputSchema ?? {})
|
|
1643
1889
|
);
|
|
1644
1890
|
const properties = coercionProperties(base, branches, params);
|
|
1645
1891
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -1831,7 +2077,7 @@ function oauthEndpointOverridesFromSettings(settings) {
|
|
|
1831
2077
|
};
|
|
1832
2078
|
}
|
|
1833
2079
|
function storedFieldsToInspectorSettings(stored) {
|
|
1834
|
-
const hasAny = stored.headers !== void 0 || stored.metadata !== void 0 || stored.connectionTimeout !== void 0 || stored.requestTimeout !== void 0 || stored.taskTtl !== void 0 || stored.autoRefreshOnListChanged !== void 0 || stored.paginatedLists !== void 0 || stored.advertisedExtensions !== void 0 || stored.maxFetchRequests !== void 0 || stored.skillCatalogMaxSkills !== void 0 || stored.skillCatalogMaxBytes !== void 0 || stored.oauth !== void 0 || stored.roots !== void 0 || stored.protocolEra !== void 0 || stored.modernLogLevel !== void 0 || stored.env !== void 0 || stored.cwd !== void 0;
|
|
2080
|
+
const hasAny = stored.headers !== void 0 || stored.metadata !== void 0 || stored.connectionTimeout !== void 0 || stored.requestTimeout !== void 0 || stored.taskTtl !== void 0 || stored.autoRefreshOnListChanged !== void 0 || stored.paginatedLists !== void 0 || stored.suppressNotificationStream !== void 0 || stored.advertisedExtensions !== void 0 || stored.maxFetchRequests !== void 0 || stored.skillCatalogMaxSkills !== void 0 || stored.skillCatalogMaxBytes !== void 0 || stored.oauth !== void 0 || stored.roots !== void 0 || stored.protocolEra !== void 0 || stored.modernLogLevel !== void 0 || stored.env !== void 0 || stored.cwd !== void 0;
|
|
1835
2081
|
if (!hasAny) return void 0;
|
|
1836
2082
|
const headersPairs = stored.headers ? Object.entries(stored.headers).map(([key, value]) => ({ key, value })) : [];
|
|
1837
2083
|
const settings = {
|
|
@@ -1863,6 +2109,9 @@ function storedFieldsToInspectorSettings(stored) {
|
|
|
1863
2109
|
if (isSkillCatalogLimit(stored.skillCatalogMaxBytes)) {
|
|
1864
2110
|
settings.skillCatalogMaxBytes = stored.skillCatalogMaxBytes;
|
|
1865
2111
|
}
|
|
2112
|
+
if (stored.suppressNotificationStream === true) {
|
|
2113
|
+
settings.suppressNotificationStream = true;
|
|
2114
|
+
}
|
|
1866
2115
|
if (isProtocolEra(stored.protocolEra)) {
|
|
1867
2116
|
settings.protocolEra = stored.protocolEra;
|
|
1868
2117
|
}
|
|
@@ -1914,6 +2163,7 @@ var INSPECTOR_FIELD_KEY_MAP = {
|
|
|
1914
2163
|
taskTtl: true,
|
|
1915
2164
|
autoRefreshOnListChanged: true,
|
|
1916
2165
|
paginatedLists: true,
|
|
2166
|
+
suppressNotificationStream: true,
|
|
1917
2167
|
advertisedExtensions: true,
|
|
1918
2168
|
maxFetchRequests: true,
|
|
1919
2169
|
skillCatalogMaxSkills: true,
|
|
@@ -2268,7 +2518,7 @@ function secretStorageCaveat(info) {
|
|
|
2268
2518
|
return `The secrets file could not be read (${info.encryptionUnknown}). Saving a secret will fail rather than overwrite it.`;
|
|
2269
2519
|
}
|
|
2270
2520
|
if (info.plaintext) {
|
|
2271
|
-
return info.pendingEncryption ? "Existing secrets in this file are still unencrypted (file mode 0600). They are re-encrypted the next time a secret is saved." : "Secrets are stored unencrypted (file mode 0600). Set MCP_INSPECTOR_SECRET_KEY to encrypt them.";
|
|
2521
|
+
return info.pendingEncryption ? "Existing secrets in this file are still unencrypted (file mode 0600). They are re-encrypted the next time a secret is saved." : "Secrets are stored unencrypted (file mode 0600). Set MCP_INSPECTOR_SECRET_KEY or MCP_INSPECTOR_SECRET_KEY_FILE to encrypt them.";
|
|
2272
2522
|
}
|
|
2273
2523
|
return void 0;
|
|
2274
2524
|
}
|
|
@@ -2283,6 +2533,7 @@ function secretStorageSummary(info) {
|
|
|
2283
2533
|
|
|
2284
2534
|
// ../../core/auth/node/file-secret-store.ts
|
|
2285
2535
|
import * as crypto2 from "crypto";
|
|
2536
|
+
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
2286
2537
|
import * as fs3 from "fs/promises";
|
|
2287
2538
|
import * as path3 from "path";
|
|
2288
2539
|
|
|
@@ -2426,7 +2677,7 @@ async function openSecretFileLock(filePath) {
|
|
|
2426
2677
|
});
|
|
2427
2678
|
} catch (err) {
|
|
2428
2679
|
if (isStuckOrHeld(err, lockPathOf(target))) {
|
|
2429
|
-
throw new
|
|
2680
|
+
throw new SecretFileLockHeldError(
|
|
2430
2681
|
`Could not save to the secrets file at ${target}: its lock (${lockPathOf(target)}) was still held after the ${Math.round(RETRY_BUDGET_MS / 1e3)} seconds this save waited. Its secrets are intact; the value you just entered was not saved. If no other Inspector is running, remove that lock and try again.`
|
|
2431
2682
|
);
|
|
2432
2683
|
}
|
|
@@ -2485,7 +2736,63 @@ var SCRYPT_N = 16384;
|
|
|
2485
2736
|
var SCRYPT_R = 8;
|
|
2486
2737
|
var SCRYPT_P = 1;
|
|
2487
2738
|
var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
|
|
2739
|
+
var SECRET_KEY_FILE_ENV = "MCP_INSPECTOR_SECRET_KEY_FILE";
|
|
2740
|
+
function isSameFile(a, b) {
|
|
2741
|
+
if (path3.resolve(a) === path3.resolve(b)) return true;
|
|
2742
|
+
try {
|
|
2743
|
+
const sa = statSync2(a);
|
|
2744
|
+
const sb = statSync2(b);
|
|
2745
|
+
return sa.dev === sb.dev && sa.ino === sb.ino;
|
|
2746
|
+
} catch {
|
|
2747
|
+
return false;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
function resolveSecretPassphrase(env = process.env, secretFilePath) {
|
|
2751
|
+
const direct = env[SECRET_KEY_ENV];
|
|
2752
|
+
const hasDirect = direct !== void 0 && direct.trim() !== "";
|
|
2753
|
+
const rawKeyFile = env[SECRET_KEY_FILE_ENV];
|
|
2754
|
+
const wantsKeyFile = rawKeyFile !== void 0;
|
|
2755
|
+
const keyFile = rawKeyFile?.trim();
|
|
2756
|
+
if (hasDirect && wantsKeyFile) {
|
|
2757
|
+
return {
|
|
2758
|
+
problem: `both ${SECRET_KEY_ENV} and ${SECRET_KEY_FILE_ENV} are set; set only one`
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
if (hasDirect) return { passphrase: direct };
|
|
2762
|
+
if (!wantsKeyFile) return {};
|
|
2763
|
+
if (!keyFile) {
|
|
2764
|
+
return { problem: `${SECRET_KEY_FILE_ENV} is set but empty` };
|
|
2765
|
+
}
|
|
2766
|
+
const resolved2 = path3.resolve(keyFile);
|
|
2767
|
+
if (secretFilePath !== void 0 && isSameFile(resolved2, secretFilePath)) {
|
|
2768
|
+
return {
|
|
2769
|
+
problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is the secrets file itself; point it at a separate key file`
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
let contents;
|
|
2773
|
+
try {
|
|
2774
|
+
contents = readFileSync2(resolved2, "utf-8");
|
|
2775
|
+
} catch (err) {
|
|
2776
|
+
return {
|
|
2777
|
+
// `String(err)` rather than `.message`: `readFileSync` only ever throws
|
|
2778
|
+
// an `Error`, so an `instanceof` guard would be a branch no test can take.
|
|
2779
|
+
problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) could not be read: ${String(err)}`
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
const passphrase = contents.replace(/[\r\n]+$/, "");
|
|
2783
|
+
if (passphrase.trim() === "") {
|
|
2784
|
+
return { problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is empty` };
|
|
2785
|
+
}
|
|
2786
|
+
return { passphrase };
|
|
2787
|
+
}
|
|
2488
2788
|
var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
|
|
2789
|
+
function assertSecretString(field, value) {
|
|
2790
|
+
if (typeof value !== "string") {
|
|
2791
|
+
throw new TypeError(
|
|
2792
|
+
`Refusing to store a non-string secret value (${typeof value}) for "${field}": the secrets file holds only strings, and writing this would make the whole file unreadable.`
|
|
2793
|
+
);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2489
2796
|
function asSecretMap(value, filePath) {
|
|
2490
2797
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2491
2798
|
throw new SecretStoreUnavailableError(
|
|
@@ -2504,7 +2811,7 @@ function asSecretMap(value, filePath) {
|
|
|
2504
2811
|
var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
|
|
2505
2812
|
constructor(filePath, hasKey) {
|
|
2506
2813
|
super(
|
|
2507
|
-
hasKey ? `The secrets file at ${filePath} could not be decrypted with the current ${SECRET_KEY_ENV}. Refusing to write, which would overwrite the existing secrets. Restore the original passphrase, or delete the file to start over.` : `The secrets file at ${filePath} is encrypted but ${SECRET_KEY_ENV} is not set. Refusing to write, which would overwrite the existing secrets. Set the passphrase this file was written with, or delete the file to start over.`
|
|
2814
|
+
hasKey ? `The secrets file at ${filePath} could not be decrypted with the current ${SECRET_KEY_ENV} (or ${SECRET_KEY_FILE_ENV}). Refusing to write, which would overwrite the existing secrets. Restore the original passphrase, or delete the file to start over.` : `The secrets file at ${filePath} is encrypted but ${SECRET_KEY_ENV} is not set (nor ${SECRET_KEY_FILE_ENV}). Refusing to write, which would overwrite the existing secrets. Set the passphrase this file was written with, or delete the file to start over.`
|
|
2508
2815
|
);
|
|
2509
2816
|
this.name = "SecretFileKeyMismatchError";
|
|
2510
2817
|
}
|
|
@@ -2562,10 +2869,17 @@ function encryptedEnvelopeProblem(parsed) {
|
|
|
2562
2869
|
var FileSecretStore = class {
|
|
2563
2870
|
filePath;
|
|
2564
2871
|
passphrase;
|
|
2872
|
+
/**
|
|
2873
|
+
* Why a configured key could not be obtained. While set, every read throws
|
|
2874
|
+
* and every write refuses — see {@link SecretPassphrase}.
|
|
2875
|
+
*/
|
|
2876
|
+
keyProblem;
|
|
2565
2877
|
constructor(options) {
|
|
2566
2878
|
this.filePath = options.filePath;
|
|
2567
|
-
const
|
|
2879
|
+
const resolved2 = options.passphrase !== void 0 ? { passphrase: options.passphrase } : resolveSecretPassphrase(process.env, this.filePath);
|
|
2880
|
+
const raw = resolved2.passphrase;
|
|
2568
2881
|
this.passphrase = raw && raw.trim() ? raw : void 0;
|
|
2882
|
+
this.keyProblem = resolved2.problem;
|
|
2569
2883
|
}
|
|
2570
2884
|
/**
|
|
2571
2885
|
* True when the *next write* will encrypt. This is the configured policy,
|
|
@@ -2580,7 +2894,8 @@ var FileSecretStore = class {
|
|
|
2580
2894
|
* no file yet, or when it cannot be read or parsed.
|
|
2581
2895
|
*
|
|
2582
2896
|
* Separate from {@link encrypted} because the two genuinely disagree for a
|
|
2583
|
-
* whole session: adding `MCP_INSPECTOR_SECRET_KEY`
|
|
2897
|
+
* whole session: adding a passphrase (`MCP_INSPECTOR_SECRET_KEY` or
|
|
2898
|
+
* `MCP_INSPECTOR_SECRET_KEY_FILE`) to an install that
|
|
2584
2899
|
* already has a plaintext file flips `encrypted` to true immediately,
|
|
2585
2900
|
* while the bytes stay readable until the next `set`. A descriptor built
|
|
2586
2901
|
* from the policy would tell that user "File (encrypted)" while their
|
|
@@ -2604,6 +2919,9 @@ var FileSecretStore = class {
|
|
|
2604
2919
|
* `set` is in fact about to refuse.
|
|
2605
2920
|
*/
|
|
2606
2921
|
async readOnDiskEncryption() {
|
|
2922
|
+
if (this.keyProblem !== void 0) {
|
|
2923
|
+
return { state: "unreadable", detail: this.keyProblem };
|
|
2924
|
+
}
|
|
2607
2925
|
let raw;
|
|
2608
2926
|
try {
|
|
2609
2927
|
raw = await readStoreFile(this.filePath);
|
|
@@ -2641,7 +2959,7 @@ var FileSecretStore = class {
|
|
|
2641
2959
|
} catch (err) {
|
|
2642
2960
|
return {
|
|
2643
2961
|
state: "unreadable",
|
|
2644
|
-
detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV}` : err instanceof Error ? err.message : String(err)
|
|
2962
|
+
detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV} (or ${SECRET_KEY_FILE_ENV})` : err instanceof Error ? err.message : String(err)
|
|
2645
2963
|
};
|
|
2646
2964
|
}
|
|
2647
2965
|
return { state: "encrypted" };
|
|
@@ -2692,6 +3010,11 @@ var FileSecretStore = class {
|
|
|
2692
3010
|
return this.serialize(() => this.readMap());
|
|
2693
3011
|
}
|
|
2694
3012
|
async readMap() {
|
|
3013
|
+
if (this.keyProblem !== void 0) {
|
|
3014
|
+
throw new SecretStoreUnavailableError(
|
|
3015
|
+
`The secrets file at ${this.filePath} cannot be used: ${this.keyProblem}. Refusing to read or write it rather than store secrets unencrypted.`
|
|
3016
|
+
);
|
|
3017
|
+
}
|
|
2695
3018
|
const raw = await readStoreFile(this.filePath);
|
|
2696
3019
|
if (raw === null) return null;
|
|
2697
3020
|
let parsed;
|
|
@@ -2728,7 +3051,9 @@ var FileSecretStore = class {
|
|
|
2728
3051
|
let plain;
|
|
2729
3052
|
try {
|
|
2730
3053
|
const key = await this.deriveKey(parsed.kdf);
|
|
2731
|
-
const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv
|
|
3054
|
+
const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv, {
|
|
3055
|
+
authTagLength: GCM_TAG_BYTES
|
|
3056
|
+
});
|
|
2732
3057
|
decipher.setAuthTag(parts.tag);
|
|
2733
3058
|
plain = Buffer.concat([
|
|
2734
3059
|
decipher.update(parts.body),
|
|
@@ -2762,7 +3087,9 @@ var FileSecretStore = class {
|
|
|
2762
3087
|
};
|
|
2763
3088
|
const key = await this.deriveKey(kdf);
|
|
2764
3089
|
const iv = crypto2.randomBytes(IV_BYTES);
|
|
2765
|
-
const cipher = crypto2.createCipheriv(CIPHER, key, iv
|
|
3090
|
+
const cipher = crypto2.createCipheriv(CIPHER, key, iv, {
|
|
3091
|
+
authTagLength: GCM_TAG_BYTES
|
|
3092
|
+
});
|
|
2766
3093
|
const body = Buffer.concat([
|
|
2767
3094
|
cipher.update(JSON.stringify(map), "utf-8"),
|
|
2768
3095
|
cipher.final()
|
|
@@ -2927,9 +3254,36 @@ var FileSecretStore = class {
|
|
|
2927
3254
|
const found = {};
|
|
2928
3255
|
for (const field of fields) {
|
|
2929
3256
|
const value = map?.[buildAccount2(serverId, field)];
|
|
2930
|
-
if (value !== void 0) found
|
|
3257
|
+
if (value !== void 0) setOwnEntry(found, field, value);
|
|
3258
|
+
}
|
|
3259
|
+
setOwnEntry(out, serverId, found);
|
|
3260
|
+
}
|
|
3261
|
+
return out;
|
|
3262
|
+
}
|
|
3263
|
+
/**
|
|
3264
|
+
* Bulk twin of {@link getStrict}: the same single-pass read as
|
|
3265
|
+
* {@link getMany}, but an unreadable store throws (with the typed advice
|
|
3266
|
+
* {@link getStrict} attaches) instead of yielding no fields — required by
|
|
3267
|
+
* OAuth read hydration, whose result later drives store deletions.
|
|
3268
|
+
*/
|
|
3269
|
+
async getManyStrict(requests) {
|
|
3270
|
+
let map;
|
|
3271
|
+
try {
|
|
3272
|
+
map = await this.serialize(() => this.readMap());
|
|
3273
|
+
} catch (err) {
|
|
3274
|
+
if (err instanceof SecretStoreUnavailableError) throw err;
|
|
3275
|
+
throw new SecretStoreUnavailableError(
|
|
3276
|
+
`Could not read the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3277
|
+
);
|
|
3278
|
+
}
|
|
3279
|
+
const out = {};
|
|
3280
|
+
for (const { serverId, fields } of requests) {
|
|
3281
|
+
const found = {};
|
|
3282
|
+
for (const field of fields) {
|
|
3283
|
+
const value = map?.[buildAccount2(serverId, field)];
|
|
3284
|
+
if (value !== void 0) setOwnEntry(found, field, value);
|
|
2931
3285
|
}
|
|
2932
|
-
out
|
|
3286
|
+
setOwnEntry(out, serverId, found);
|
|
2933
3287
|
}
|
|
2934
3288
|
return out;
|
|
2935
3289
|
}
|
|
@@ -2973,6 +3327,9 @@ var FileSecretStore = class {
|
|
|
2973
3327
|
*/
|
|
2974
3328
|
async setMany(serverId, values) {
|
|
2975
3329
|
if (Object.keys(values).length === 0) return;
|
|
3330
|
+
for (const [field, value] of Object.entries(values)) {
|
|
3331
|
+
assertSecretString(field, value);
|
|
3332
|
+
}
|
|
2976
3333
|
await this.serialize(
|
|
2977
3334
|
() => this.mutate((map) => {
|
|
2978
3335
|
const next = { ...map };
|
|
@@ -2984,6 +3341,7 @@ var FileSecretStore = class {
|
|
|
2984
3341
|
);
|
|
2985
3342
|
}
|
|
2986
3343
|
async set(serverId, field, value) {
|
|
3344
|
+
assertSecretString(field, value);
|
|
2987
3345
|
await this.serialize(
|
|
2988
3346
|
() => this.mutate((map) => ({
|
|
2989
3347
|
...map,
|
|
@@ -2999,9 +3357,11 @@ var FileSecretStore = class {
|
|
|
2999
3357
|
await this.deleteWhere((key) => key.startsWith(prefix));
|
|
3000
3358
|
}
|
|
3001
3359
|
/**
|
|
3002
|
-
* Shared delete body.
|
|
3003
|
-
*
|
|
3004
|
-
*
|
|
3360
|
+
* Shared delete body. Same confirmed-delete contract as the keyring
|
|
3361
|
+
* store: a deletion that cannot be confirmed (unreadable file, failed
|
|
3362
|
+
* decrypt, failed write) must escape, because reporting success would
|
|
3363
|
+
* let a caller commit state that assumes the entry is gone — once the
|
|
3364
|
+
* file becomes readable again, a later read would resurrect it.
|
|
3005
3365
|
*/
|
|
3006
3366
|
async deleteWhere(match) {
|
|
3007
3367
|
try {
|
|
@@ -3014,7 +3374,12 @@ var FileSecretStore = class {
|
|
|
3014
3374
|
return next;
|
|
3015
3375
|
})
|
|
3016
3376
|
);
|
|
3017
|
-
} catch {
|
|
3377
|
+
} catch (err) {
|
|
3378
|
+
if (err instanceof SecretStoreUnavailableError) throw err;
|
|
3379
|
+
throw new SecretStoreUnavailableError(
|
|
3380
|
+
`Could not delete from the secrets file: ${err instanceof Error ? err.message : String(err)}`,
|
|
3381
|
+
{ cause: err }
|
|
3382
|
+
);
|
|
3018
3383
|
}
|
|
3019
3384
|
}
|
|
3020
3385
|
};
|
|
@@ -3047,6 +3412,7 @@ async function readSecretFilePermissions(filePath) {
|
|
|
3047
3412
|
var SECRET_STORE_ENV = "MCP_INSPECTOR_SECRET_STORE";
|
|
3048
3413
|
var SECRET_FILE_ENV = "MCP_INSPECTOR_SECRET_FILE";
|
|
3049
3414
|
var STORAGE_DIR_ENV = "MCP_STORAGE_DIR";
|
|
3415
|
+
var SECRET_STORAGE_DOCS_URL = "https://github.com/modelcontextprotocol/inspector/blob/main/docs/secret-storage.md";
|
|
3050
3416
|
var KINDS = ["keyring", "file", "memory"];
|
|
3051
3417
|
function parseSecretStoreEnv(raw) {
|
|
3052
3418
|
const value = raw?.trim().toLowerCase();
|
|
@@ -3189,6 +3555,11 @@ function warnAboutSecretStorage(info) {
|
|
|
3189
3555
|
}
|
|
3190
3556
|
const caveat = secretStorageCaveat(info);
|
|
3191
3557
|
if (caveat) console.warn(`[mcp-inspector] ${caveat}`);
|
|
3558
|
+
if (info.reason === "fallback" || caveat) {
|
|
3559
|
+
console.warn(
|
|
3560
|
+
`[mcp-inspector] How the secret store is chosen, and how to secure it: ${SECRET_STORAGE_DOCS_URL}`
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3192
3563
|
}
|
|
3193
3564
|
var resolved;
|
|
3194
3565
|
function resolveSecretStore() {
|
|
@@ -3384,6 +3755,9 @@ var DeferredSecretStore = class {
|
|
|
3384
3755
|
async getMany(requests) {
|
|
3385
3756
|
return secretStoreGetMany(await this.target(), requests);
|
|
3386
3757
|
}
|
|
3758
|
+
async getManyStrict(requests) {
|
|
3759
|
+
return secretStoreGetManyStrict(await this.target(), requests);
|
|
3760
|
+
}
|
|
3387
3761
|
async get(serverId, field) {
|
|
3388
3762
|
return (await this.target()).get(serverId, field);
|
|
3389
3763
|
}
|
|
@@ -3622,12 +3996,31 @@ function redactJsonValue(value) {
|
|
|
3622
3996
|
}
|
|
3623
3997
|
return value;
|
|
3624
3998
|
}
|
|
3999
|
+
function joinSensitiveContinuations(body) {
|
|
4000
|
+
const out = [];
|
|
4001
|
+
let sensitiveAt = -1;
|
|
4002
|
+
for (const pair of body.split("&")) {
|
|
4003
|
+
const eq = pair.indexOf("=");
|
|
4004
|
+
if (eq === -1) {
|
|
4005
|
+
if (sensitiveAt !== -1 && pair.length > 0) {
|
|
4006
|
+
out[sensitiveAt] += `%26${pair}`;
|
|
4007
|
+
} else {
|
|
4008
|
+
out.push(pair);
|
|
4009
|
+
}
|
|
4010
|
+
continue;
|
|
4011
|
+
}
|
|
4012
|
+
const [key] = new URLSearchParams(pair).keys();
|
|
4013
|
+
sensitiveAt = isSensitiveField(key) && eq < pair.length - 1 ? out.length : -1;
|
|
4014
|
+
out.push(pair);
|
|
4015
|
+
}
|
|
4016
|
+
return out.join("&");
|
|
4017
|
+
}
|
|
3625
4018
|
function redactBody(body, contentType) {
|
|
3626
4019
|
if (!body) return body;
|
|
3627
4020
|
const type = (contentType ?? "").toLowerCase();
|
|
3628
4021
|
if (type.includes("application/x-www-form-urlencoded")) {
|
|
3629
4022
|
try {
|
|
3630
|
-
const params = new URLSearchParams(body);
|
|
4023
|
+
const params = new URLSearchParams(joinSensitiveContinuations(body));
|
|
3631
4024
|
let changed = false;
|
|
3632
4025
|
for (const key of new Set(params.keys())) {
|
|
3633
4026
|
if (isSensitiveField(key)) {
|
|
@@ -4351,6 +4744,73 @@ function createProxyFetch() {
|
|
|
4351
4744
|
};
|
|
4352
4745
|
}
|
|
4353
4746
|
|
|
4747
|
+
// ../../core/mcp/node/notificationHeadersFetch.ts
|
|
4748
|
+
import { PROTOCOL_VERSION_META_KEY } from "@modelcontextprotocol/client";
|
|
4749
|
+
function createNotificationHeadersFetch(baseFetch) {
|
|
4750
|
+
return (input, init) => {
|
|
4751
|
+
const method = modernNotificationMethod(init);
|
|
4752
|
+
if (method === void 0) return baseFetch(input, init);
|
|
4753
|
+
const headers = new Headers(init?.headers);
|
|
4754
|
+
headers.set("mcp-protocol-version", method.version);
|
|
4755
|
+
headers.set("mcp-method", method.method);
|
|
4756
|
+
return baseFetch(input, { ...init, headers });
|
|
4757
|
+
};
|
|
4758
|
+
}
|
|
4759
|
+
function modernNotificationMethod(init) {
|
|
4760
|
+
if (init?.method?.toUpperCase() !== "POST") return void 0;
|
|
4761
|
+
if (typeof init.body !== "string") return void 0;
|
|
4762
|
+
let message;
|
|
4763
|
+
try {
|
|
4764
|
+
message = JSON.parse(init.body);
|
|
4765
|
+
} catch {
|
|
4766
|
+
return void 0;
|
|
4767
|
+
}
|
|
4768
|
+
if (!isRecord2(message) || "id" in message) return void 0;
|
|
4769
|
+
if (typeof message.method !== "string") return void 0;
|
|
4770
|
+
const meta = isRecord2(message.params) ? message.params._meta : void 0;
|
|
4771
|
+
const version = isRecord2(meta) ? meta[PROTOCOL_VERSION_META_KEY] : void 0;
|
|
4772
|
+
if (typeof version !== "string" || version < MODERN_PROTOCOL_VERSION) {
|
|
4773
|
+
return void 0;
|
|
4774
|
+
}
|
|
4775
|
+
return { method: message.method, version };
|
|
4776
|
+
}
|
|
4777
|
+
function isRecord2(value) {
|
|
4778
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
// ../../core/mcp/node/suppressNotificationStreamFetch.ts
|
|
4782
|
+
function createSuppressNotificationStreamFetch(baseFetch, endpoint) {
|
|
4783
|
+
return async (input, init) => {
|
|
4784
|
+
if (isStandaloneStreamRequest(input, init, endpoint)) {
|
|
4785
|
+
return new Response(null, {
|
|
4786
|
+
status: 405,
|
|
4787
|
+
statusText: "Method Not Allowed"
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
return baseFetch(input, init);
|
|
4791
|
+
};
|
|
4792
|
+
}
|
|
4793
|
+
function requestUrl(input) {
|
|
4794
|
+
const raw = input instanceof Request ? input.url : input instanceof URL ? input.href : input;
|
|
4795
|
+
try {
|
|
4796
|
+
return new URL(raw);
|
|
4797
|
+
} catch {
|
|
4798
|
+
return void 0;
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
function isStandaloneStreamRequest(input, init, endpoint) {
|
|
4802
|
+
const request = input instanceof Request ? input : void 0;
|
|
4803
|
+
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
4804
|
+
if (method !== "GET") return false;
|
|
4805
|
+
const url = requestUrl(input);
|
|
4806
|
+
if (!url || url.origin !== endpoint.origin || url.pathname !== endpoint.pathname || url.search !== endpoint.search) {
|
|
4807
|
+
return false;
|
|
4808
|
+
}
|
|
4809
|
+
const headers = new Headers(init?.headers ?? request?.headers);
|
|
4810
|
+
const accept = headers.get("accept")?.toLowerCase() ?? "";
|
|
4811
|
+
return accept.includes("text/event-stream") && !headers.has("last-event-id");
|
|
4812
|
+
}
|
|
4813
|
+
|
|
4354
4814
|
// ../../core/mcp/node/transport.ts
|
|
4355
4815
|
function headersFromSettings(settings) {
|
|
4356
4816
|
if (!settings || settings.headers.length === 0) return void 0;
|
|
@@ -4438,10 +4898,14 @@ function createTransportNode(config, options = {}) {
|
|
|
4438
4898
|
...httpConfig.requestInit,
|
|
4439
4899
|
...headers && { headers }
|
|
4440
4900
|
};
|
|
4901
|
+
const stampedFetch = createNotificationHeadersFetch(
|
|
4902
|
+
fetchWithOptionalAuthIntercept
|
|
4903
|
+
);
|
|
4904
|
+
const httpFetch = settings?.suppressNotificationStream === true ? createSuppressNotificationStreamFetch(stampedFetch, url) : stampedFetch;
|
|
4441
4905
|
const transport = new StreamableHTTPClientTransport(url, {
|
|
4442
4906
|
authProvider,
|
|
4443
4907
|
requestInit,
|
|
4444
|
-
fetch:
|
|
4908
|
+
fetch: httpFetch,
|
|
4445
4909
|
// SEP-2350: how the transport reacts to a `403 insufficient_scope`
|
|
4446
4910
|
// challenge. Defaults to the SDK's `reauthorize` when unset.
|
|
4447
4911
|
...settings?.oauthOnInsufficientScope && {
|
|
@@ -4587,42 +5051,57 @@ import {
|
|
|
4587
5051
|
function withIssuer(value, issuer) {
|
|
4588
5052
|
return issuer === void 0 ? value : { ...value, issuer };
|
|
4589
5053
|
}
|
|
4590
|
-
var
|
|
5054
|
+
var OAuthStorageCoordination = class {
|
|
4591
5055
|
loaded = false;
|
|
4592
5056
|
loadPromise;
|
|
4593
5057
|
/** Serializes persist writes so concurrent mutators cannot reorder POSTs. */
|
|
4594
5058
|
persistQueue = Promise.resolve();
|
|
5059
|
+
};
|
|
5060
|
+
var OAuthStorageBase = class {
|
|
5061
|
+
coordination;
|
|
4595
5062
|
memory;
|
|
4596
5063
|
backend;
|
|
4597
|
-
constructor(memory, backend) {
|
|
5064
|
+
constructor(memory, backend, coordination = new OAuthStorageCoordination()) {
|
|
4598
5065
|
this.memory = memory;
|
|
4599
5066
|
this.backend = backend;
|
|
5067
|
+
this.coordination = coordination;
|
|
4600
5068
|
}
|
|
4601
5069
|
load() {
|
|
4602
|
-
|
|
4603
|
-
|
|
5070
|
+
const coordination = this.coordination;
|
|
5071
|
+
if (!coordination.loadPromise) {
|
|
5072
|
+
coordination.loadPromise = this.doLoad().catch((error) => {
|
|
5073
|
+
coordination.loadPromise = void 0;
|
|
5074
|
+
throw error;
|
|
5075
|
+
});
|
|
4604
5076
|
}
|
|
4605
|
-
return
|
|
5077
|
+
return coordination.loadPromise;
|
|
4606
5078
|
}
|
|
4607
5079
|
async doLoad() {
|
|
4608
|
-
if (this.loaded) {
|
|
5080
|
+
if (this.coordination.loaded) {
|
|
4609
5081
|
return;
|
|
4610
5082
|
}
|
|
4611
5083
|
const snapshot = await this.backend.read();
|
|
4612
5084
|
if (snapshot) {
|
|
4613
5085
|
this.memory.replace(snapshot);
|
|
4614
5086
|
}
|
|
4615
|
-
this.loaded = true;
|
|
5087
|
+
this.coordination.loaded = true;
|
|
4616
5088
|
}
|
|
4617
5089
|
async ensureLoaded() {
|
|
4618
5090
|
await this.load();
|
|
4619
5091
|
}
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
5092
|
+
/**
|
|
5093
|
+
* Flush memory to the backend, naming the sections the calling mutation
|
|
5094
|
+
* touched so shared-store backends can merge just those entries over a
|
|
5095
|
+
* fresh read (the clobber fix — see {@link OAuthPersistSections}). The
|
|
5096
|
+
* snapshot is taken *inside* the queued closure, so a write that waited in
|
|
5097
|
+
* the queue carries the state as of when it actually runs, coalescing with
|
|
5098
|
+
* any mutations that landed in memory while it waited.
|
|
5099
|
+
*/
|
|
5100
|
+
async persist(sections) {
|
|
5101
|
+
const prior = this.coordination.persistQueue;
|
|
4623
5102
|
const tracked = prior.catch(() => {
|
|
4624
|
-
}).then(() => this.backend.write(snapshot));
|
|
4625
|
-
this.persistQueue = tracked;
|
|
5103
|
+
}).then(() => this.backend.write(this.memory.snapshot(), sections));
|
|
5104
|
+
this.coordination.persistQueue = tracked;
|
|
4626
5105
|
await tracked;
|
|
4627
5106
|
}
|
|
4628
5107
|
/**
|
|
@@ -4636,7 +5115,7 @@ var OAuthStorageBase = class {
|
|
|
4636
5115
|
/** Read the issuer-bound slot for a read, if one exists. */
|
|
4637
5116
|
issuerSlot(state, issuer) {
|
|
4638
5117
|
const key = this.resolveReadIssuer(state, issuer);
|
|
4639
|
-
return key ? state.byIssuer
|
|
5118
|
+
return key ? getOwnEntry(state.byIssuer, key) : void 0;
|
|
4640
5119
|
}
|
|
4641
5120
|
/**
|
|
4642
5121
|
* Read-modify-write the `byIssuer[issuer]` slot, set `activeIssuer`, and clear
|
|
@@ -4647,7 +5126,9 @@ var OAuthStorageBase = class {
|
|
|
4647
5126
|
const state = this.memory.getState().getServerState(serverUrl);
|
|
4648
5127
|
const byIssuer = {
|
|
4649
5128
|
...state.byIssuer,
|
|
4650
|
-
|
|
5129
|
+
// The computed key defines an own property; the merge-base read needs
|
|
5130
|
+
// the own guard so an absent `__proto__` slot reads as undefined.
|
|
5131
|
+
[issuer]: { ...getOwnEntry(state.byIssuer, issuer), ...updates }
|
|
4651
5132
|
};
|
|
4652
5133
|
this.memory.getState().setServerState(serverUrl, {
|
|
4653
5134
|
byIssuer,
|
|
@@ -4659,7 +5140,7 @@ var OAuthStorageBase = class {
|
|
|
4659
5140
|
mapIssuerSlots(state, fn) {
|
|
4660
5141
|
const byIssuer = {};
|
|
4661
5142
|
for (const [key, slot] of Object.entries(state.byIssuer ?? {})) {
|
|
4662
|
-
byIssuer
|
|
5143
|
+
setOwnEntry(byIssuer, key, { ...slot, ...fn(slot) });
|
|
4663
5144
|
}
|
|
4664
5145
|
return byIssuer;
|
|
4665
5146
|
}
|
|
@@ -4705,7 +5186,7 @@ var OAuthStorageBase = class {
|
|
|
4705
5186
|
clientRegistrationKind: options.registrationKind
|
|
4706
5187
|
});
|
|
4707
5188
|
}
|
|
4708
|
-
await this.persist();
|
|
5189
|
+
await this.persist({ servers: [serverUrl] });
|
|
4709
5190
|
}
|
|
4710
5191
|
async savePreregisteredClientInformation(serverUrl, clientInformation) {
|
|
4711
5192
|
await this.ensureLoaded();
|
|
@@ -4713,7 +5194,7 @@ var OAuthStorageBase = class {
|
|
|
4713
5194
|
preregisteredClientInformation: clientInformation,
|
|
4714
5195
|
clientRegistrationKind: "static"
|
|
4715
5196
|
});
|
|
4716
|
-
await this.persist();
|
|
5197
|
+
await this.persist({ servers: [serverUrl] });
|
|
4717
5198
|
}
|
|
4718
5199
|
async clearClientInformation(serverUrl, isPreregistered, issuer) {
|
|
4719
5200
|
await this.ensureLoaded();
|
|
@@ -4721,7 +5202,7 @@ var OAuthStorageBase = class {
|
|
|
4721
5202
|
this.memory.getState().setServerState(serverUrl, {
|
|
4722
5203
|
preregisteredClientInformation: void 0
|
|
4723
5204
|
});
|
|
4724
|
-
await this.persist();
|
|
5205
|
+
await this.persist({ servers: [serverUrl] });
|
|
4725
5206
|
return;
|
|
4726
5207
|
}
|
|
4727
5208
|
if (issuer !== void 0) {
|
|
@@ -4744,7 +5225,7 @@ var OAuthStorageBase = class {
|
|
|
4744
5225
|
clientRegistrationKind: void 0
|
|
4745
5226
|
});
|
|
4746
5227
|
}
|
|
4747
|
-
await this.persist();
|
|
5228
|
+
await this.persist({ servers: [serverUrl] });
|
|
4748
5229
|
}
|
|
4749
5230
|
async getTokens(serverUrl, issuer) {
|
|
4750
5231
|
await this.ensureLoaded();
|
|
@@ -4755,9 +5236,12 @@ var OAuthStorageBase = class {
|
|
|
4755
5236
|
if (!tokens) {
|
|
4756
5237
|
return void 0;
|
|
4757
5238
|
}
|
|
4758
|
-
const
|
|
5239
|
+
const result = await OAuthTokensSchema.safeParseAsync(tokens);
|
|
5240
|
+
if (!result.success) {
|
|
5241
|
+
return void 0;
|
|
5242
|
+
}
|
|
4759
5243
|
return withIssuer(
|
|
4760
|
-
|
|
5244
|
+
result.data,
|
|
4761
5245
|
fromSlot ? this.resolveReadIssuer(state, issuer) : void 0
|
|
4762
5246
|
);
|
|
4763
5247
|
}
|
|
@@ -4781,7 +5265,7 @@ var OAuthStorageBase = class {
|
|
|
4781
5265
|
...options?.enterpriseManaged === true && { enterpriseManaged: true }
|
|
4782
5266
|
});
|
|
4783
5267
|
}
|
|
4784
|
-
await this.persist();
|
|
5268
|
+
await this.persist({ servers: [serverUrl] });
|
|
4785
5269
|
}
|
|
4786
5270
|
async clearTokens(serverUrl, issuer) {
|
|
4787
5271
|
await this.ensureLoaded();
|
|
@@ -4800,7 +5284,7 @@ var OAuthStorageBase = class {
|
|
|
4800
5284
|
}));
|
|
4801
5285
|
this.memory.getState().setServerState(serverUrl, { byIssuer, tokens: void 0 });
|
|
4802
5286
|
}
|
|
4803
|
-
await this.persist();
|
|
5287
|
+
await this.persist({ servers: [serverUrl] });
|
|
4804
5288
|
}
|
|
4805
5289
|
async getCodeVerifier(serverUrl) {
|
|
4806
5290
|
await this.ensureLoaded();
|
|
@@ -4810,12 +5294,12 @@ var OAuthStorageBase = class {
|
|
|
4810
5294
|
async saveCodeVerifier(serverUrl, codeVerifier) {
|
|
4811
5295
|
await this.ensureLoaded();
|
|
4812
5296
|
this.memory.getState().setServerState(serverUrl, { codeVerifier });
|
|
4813
|
-
await this.persist();
|
|
5297
|
+
await this.persist({ servers: [serverUrl] });
|
|
4814
5298
|
}
|
|
4815
5299
|
async clearCodeVerifier(serverUrl) {
|
|
4816
5300
|
await this.ensureLoaded();
|
|
4817
5301
|
this.memory.getState().setServerState(serverUrl, { codeVerifier: void 0 });
|
|
4818
|
-
await this.persist();
|
|
5302
|
+
await this.persist({ servers: [serverUrl] });
|
|
4819
5303
|
}
|
|
4820
5304
|
async getScope(serverUrl) {
|
|
4821
5305
|
await this.ensureLoaded();
|
|
@@ -4825,12 +5309,12 @@ var OAuthStorageBase = class {
|
|
|
4825
5309
|
async saveScope(serverUrl, scope) {
|
|
4826
5310
|
await this.ensureLoaded();
|
|
4827
5311
|
this.memory.getState().setServerState(serverUrl, { scope });
|
|
4828
|
-
await this.persist();
|
|
5312
|
+
await this.persist({ servers: [serverUrl] });
|
|
4829
5313
|
}
|
|
4830
5314
|
async clearScope(serverUrl) {
|
|
4831
5315
|
await this.ensureLoaded();
|
|
4832
5316
|
this.memory.getState().setServerState(serverUrl, { scope: void 0 });
|
|
4833
|
-
await this.persist();
|
|
5317
|
+
await this.persist({ servers: [serverUrl] });
|
|
4834
5318
|
}
|
|
4835
5319
|
async getServerMetadata(serverUrl) {
|
|
4836
5320
|
await this.ensureLoaded();
|
|
@@ -4840,12 +5324,12 @@ var OAuthStorageBase = class {
|
|
|
4840
5324
|
async saveServerMetadata(serverUrl, metadata) {
|
|
4841
5325
|
await this.ensureLoaded();
|
|
4842
5326
|
this.memory.getState().setServerState(serverUrl, { serverMetadata: metadata });
|
|
4843
|
-
await this.persist();
|
|
5327
|
+
await this.persist({ servers: [serverUrl] });
|
|
4844
5328
|
}
|
|
4845
5329
|
async clearServerMetadata(serverUrl) {
|
|
4846
5330
|
await this.ensureLoaded();
|
|
4847
5331
|
this.memory.getState().setServerState(serverUrl, { serverMetadata: void 0 });
|
|
4848
|
-
await this.persist();
|
|
5332
|
+
await this.persist({ servers: [serverUrl] });
|
|
4849
5333
|
}
|
|
4850
5334
|
async getDiscoveryState(serverUrl) {
|
|
4851
5335
|
await this.ensureLoaded();
|
|
@@ -4854,12 +5338,12 @@ var OAuthStorageBase = class {
|
|
|
4854
5338
|
async saveDiscoveryState(serverUrl, state) {
|
|
4855
5339
|
await this.ensureLoaded();
|
|
4856
5340
|
this.memory.getState().setServerState(serverUrl, { discoveryState: state });
|
|
4857
|
-
await this.persist();
|
|
5341
|
+
await this.persist({ servers: [serverUrl] });
|
|
4858
5342
|
}
|
|
4859
5343
|
async clearDiscoveryState(serverUrl) {
|
|
4860
5344
|
await this.ensureLoaded();
|
|
4861
5345
|
this.memory.getState().setServerState(serverUrl, { discoveryState: void 0 });
|
|
4862
|
-
await this.persist();
|
|
5346
|
+
await this.persist({ servers: [serverUrl] });
|
|
4863
5347
|
}
|
|
4864
5348
|
async takeRevocationSnapshot(serverUrl) {
|
|
4865
5349
|
await this.ensureLoaded();
|
|
@@ -4877,13 +5361,13 @@ var OAuthStorageBase = class {
|
|
|
4877
5361
|
serverMetadata: state.serverMetadata
|
|
4878
5362
|
};
|
|
4879
5363
|
this.memory.getState().clearServerState(serverUrl);
|
|
4880
|
-
await this.persist();
|
|
5364
|
+
await this.persist({ servers: [serverUrl] });
|
|
4881
5365
|
return snapshot;
|
|
4882
5366
|
}
|
|
4883
5367
|
async clear(serverUrl) {
|
|
4884
5368
|
await this.ensureLoaded();
|
|
4885
5369
|
this.memory.getState().clearServerState(serverUrl);
|
|
4886
|
-
await this.persist();
|
|
5370
|
+
await this.persist({ servers: [serverUrl] });
|
|
4887
5371
|
}
|
|
4888
5372
|
async getIdpSession(issuer) {
|
|
4889
5373
|
await this.ensureLoaded();
|
|
@@ -4896,17 +5380,18 @@ var OAuthStorageBase = class {
|
|
|
4896
5380
|
async saveIdpSession(issuer, session) {
|
|
4897
5381
|
await this.ensureLoaded();
|
|
4898
5382
|
this.memory.getState().setIdpSession(issuer, session);
|
|
4899
|
-
await this.persist();
|
|
5383
|
+
await this.persist({ idpSessions: [issuer] });
|
|
4900
5384
|
}
|
|
4901
5385
|
async clearIdpSession(issuer) {
|
|
4902
5386
|
await this.ensureLoaded();
|
|
4903
5387
|
this.memory.getState().clearIdpSession(issuer);
|
|
4904
|
-
await this.persist();
|
|
5388
|
+
await this.persist({ idpSessions: [issuer] });
|
|
4905
5389
|
}
|
|
4906
5390
|
async clearEnterpriseManagedResourceServers() {
|
|
4907
5391
|
await this.ensureLoaded();
|
|
5392
|
+
const servers = Object.entries(this.memory.getState().servers).filter(([, state]) => state.enterpriseManaged === true).map(([url]) => url);
|
|
4908
5393
|
this.memory.getState().clearEnterpriseManagedResourceServers();
|
|
4909
|
-
await this.persist();
|
|
5394
|
+
await this.persist({ servers });
|
|
4910
5395
|
}
|
|
4911
5396
|
};
|
|
4912
5397
|
|
|
@@ -4924,13 +5409,15 @@ var OAuthMemoryStore = class {
|
|
|
4924
5409
|
servers: this.servers,
|
|
4925
5410
|
idpSessions: this.idpSessions,
|
|
4926
5411
|
getServerState: (serverUrl) => {
|
|
4927
|
-
return this.servers
|
|
5412
|
+
return getOwnEntry(this.servers, serverUrl) || {};
|
|
4928
5413
|
},
|
|
4929
5414
|
setServerState: (serverUrl, updates) => {
|
|
4930
5415
|
this.servers = {
|
|
4931
5416
|
...this.servers,
|
|
5417
|
+
// Computed keys define own properties, so this write is safe for
|
|
5418
|
+
// `__proto__`; only the merge-base read needs the own guard.
|
|
4932
5419
|
[serverUrl]: {
|
|
4933
|
-
...this.servers
|
|
5420
|
+
...getOwnEntry(this.servers, serverUrl),
|
|
4934
5421
|
...updates
|
|
4935
5422
|
}
|
|
4936
5423
|
};
|
|
@@ -4941,13 +5428,13 @@ var OAuthMemoryStore = class {
|
|
|
4941
5428
|
this.servers = rest;
|
|
4942
5429
|
},
|
|
4943
5430
|
getIdpSession: (issuer) => {
|
|
4944
|
-
return this.idpSessions
|
|
5431
|
+
return getOwnEntry(this.idpSessions, issuer) || {};
|
|
4945
5432
|
},
|
|
4946
5433
|
setIdpSession: (issuer, updates) => {
|
|
4947
5434
|
this.idpSessions = {
|
|
4948
5435
|
...this.idpSessions,
|
|
4949
5436
|
[issuer]: {
|
|
4950
|
-
...this.idpSessions
|
|
5437
|
+
...getOwnEntry(this.idpSessions, issuer),
|
|
4951
5438
|
...updates
|
|
4952
5439
|
}
|
|
4953
5440
|
};
|
|
@@ -4981,24 +5468,81 @@ var OAuthMemoryStore = class {
|
|
|
4981
5468
|
};
|
|
4982
5469
|
|
|
4983
5470
|
// ../../core/auth/oauth-persist.ts
|
|
4984
|
-
|
|
5471
|
+
import { OAuthTokensSchema as OAuthTokensSchema2 } from "@modelcontextprotocol/core";
|
|
5472
|
+
function mergeOAuthSections(disk, snapshot, sections) {
|
|
5473
|
+
const merged = {
|
|
5474
|
+
servers: { ...disk?.servers },
|
|
5475
|
+
idpSessions: { ...disk?.idpSessions }
|
|
5476
|
+
};
|
|
5477
|
+
for (const url of sections.servers ?? []) {
|
|
5478
|
+
const value = getOwnEntry(snapshot.servers, url);
|
|
5479
|
+
if (value === void 0) {
|
|
5480
|
+
delete merged.servers[url];
|
|
5481
|
+
} else {
|
|
5482
|
+
setOwnEntry(merged.servers, url, value);
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5485
|
+
for (const issuer of sections.idpSessions ?? []) {
|
|
5486
|
+
const value = getOwnEntry(snapshot.idpSessions, issuer);
|
|
5487
|
+
if (value === void 0) {
|
|
5488
|
+
delete merged.idpSessions[issuer];
|
|
5489
|
+
} else {
|
|
5490
|
+
setOwnEntry(merged.idpSessions, issuer, value);
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
return merged;
|
|
5494
|
+
}
|
|
5495
|
+
function isRecord3(value) {
|
|
4985
5496
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4986
5497
|
}
|
|
4987
|
-
function
|
|
4988
|
-
return
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
5498
|
+
function isEntryMap(value) {
|
|
5499
|
+
return isRecord3(value) && Object.values(value).every(isRecord3);
|
|
5500
|
+
}
|
|
5501
|
+
var VERBATIM_SECRET_KEYS = [
|
|
5502
|
+
"client_secret",
|
|
5503
|
+
"registration_access_token"
|
|
5504
|
+
];
|
|
5505
|
+
function isValidClientInformation(value) {
|
|
5506
|
+
if (value === void 0) return true;
|
|
5507
|
+
if (!isRecord3(value)) return false;
|
|
5508
|
+
return VERBATIM_SECRET_KEYS.every(
|
|
5509
|
+
(key) => !Object.hasOwn(value, key) || typeof value[key] === "string"
|
|
5510
|
+
);
|
|
5511
|
+
}
|
|
5512
|
+
var PartialTokensSchema = OAuthTokensSchema2.partial();
|
|
5513
|
+
function isValidServerEntry(entry) {
|
|
5514
|
+
if (!isRecord3(entry)) return false;
|
|
5515
|
+
if (!isValidClientInformation(entry.clientInformation)) return false;
|
|
5516
|
+
if (!isValidClientInformation(entry.preregisteredClientInformation))
|
|
5517
|
+
return false;
|
|
5518
|
+
if (entry.byIssuer !== void 0) {
|
|
5519
|
+
if (!isRecord3(entry.byIssuer)) return false;
|
|
5520
|
+
for (const slot of Object.values(entry.byIssuer)) {
|
|
5521
|
+
if (!isRecord3(slot)) return false;
|
|
5522
|
+
if (!isValidClientInformation(slot.clientInformation)) return false;
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
return true;
|
|
5526
|
+
}
|
|
5527
|
+
function snapshotFromPayload(payload) {
|
|
5528
|
+
const servers = payload.servers ?? {};
|
|
5529
|
+
const idpSessions = payload.idpSessions ?? {};
|
|
5530
|
+
if (!isEntryMap(servers) || !isEntryMap(idpSessions)) return null;
|
|
5531
|
+
if (!Object.values(servers).every(isValidServerEntry)) return null;
|
|
5532
|
+
return {
|
|
5533
|
+
servers,
|
|
5534
|
+
idpSessions
|
|
5535
|
+
};
|
|
4992
5536
|
}
|
|
4993
5537
|
function parseOAuthPersistBlob(raw) {
|
|
4994
5538
|
if (raw === null || raw === void 0) {
|
|
4995
5539
|
return null;
|
|
4996
5540
|
}
|
|
4997
5541
|
const parsed = typeof raw === "string" ? raw ? parseStore(raw) : null : raw;
|
|
4998
|
-
if (!
|
|
5542
|
+
if (!isRecord3(parsed)) {
|
|
4999
5543
|
return null;
|
|
5000
5544
|
}
|
|
5001
|
-
if (
|
|
5545
|
+
if (isRecord3(parsed.state) && "version" in parsed) {
|
|
5002
5546
|
return snapshotFromPayload(parsed.state);
|
|
5003
5547
|
}
|
|
5004
5548
|
if ("servers" in parsed || "idpSessions" in parsed) {
|
|
@@ -5010,21 +5554,774 @@ function serializeOAuthPersistBlob(snapshot) {
|
|
|
5010
5554
|
return serializeStore(snapshot);
|
|
5011
5555
|
}
|
|
5012
5556
|
|
|
5557
|
+
// ../../core/auth/node/oauth-secrets.ts
|
|
5558
|
+
import { OAuthTokensSchema as OAuthTokensSchema3 } from "@modelcontextprotocol/core";
|
|
5559
|
+
var PERSIST_TOKENS_ENV = "MCP_INSPECTOR_PERSIST_TOKENS";
|
|
5560
|
+
var warnedPolicyValues = /* @__PURE__ */ new Set();
|
|
5561
|
+
function getPersistTokensPolicy(env = process.env) {
|
|
5562
|
+
const raw = env[PERSIST_TOKENS_ENV];
|
|
5563
|
+
if (raw === void 0 || raw === "") return "all";
|
|
5564
|
+
if (raw === "all" || raw === "access" || raw === "none") return raw;
|
|
5565
|
+
if (!warnedPolicyValues.has(raw)) {
|
|
5566
|
+
warnedPolicyValues.add(raw);
|
|
5567
|
+
console.warn(
|
|
5568
|
+
`[mcp-inspector] Ignoring invalid ${PERSIST_TOKENS_ENV}=${JSON.stringify(raw)} (expected "all", "access", or "none"); persisting all tokens.`
|
|
5569
|
+
);
|
|
5570
|
+
}
|
|
5571
|
+
return "all";
|
|
5572
|
+
}
|
|
5573
|
+
var oauthSecretServerId = (serverUrl) => `oauth+${encodeURIComponent(serverUrl)}`;
|
|
5574
|
+
var oauthIdpSecretServerId = (issuer) => `oauth-idp+${encodeURIComponent(issuer)}`;
|
|
5575
|
+
var issuerTokensField = (issuer) => `tokens:${issuer}`;
|
|
5576
|
+
var issuerClientSecretField = (issuer) => `client-secret:${issuer}`;
|
|
5577
|
+
var issuerRegistrationTokenField = (issuer) => `registration-token:${issuer}`;
|
|
5578
|
+
var LEGACY_TOKENS_FIELD = "tokens";
|
|
5579
|
+
var LEGACY_CLIENT_SECRET_FIELD = "client-secret";
|
|
5580
|
+
var LEGACY_REGISTRATION_TOKEN_FIELD = "registration-token";
|
|
5581
|
+
var PREREG_CLIENT_SECRET_FIELD = "prereg-client-secret";
|
|
5582
|
+
var PREREG_REGISTRATION_TOKEN_FIELD = "prereg-registration-token";
|
|
5583
|
+
var IDP_SESSION_FIELD = "idp-session";
|
|
5584
|
+
function applyTokensPolicy(tokens, policy) {
|
|
5585
|
+
if (policy === "none") return void 0;
|
|
5586
|
+
if (policy === "access" && tokens.refresh_token !== void 0) {
|
|
5587
|
+
const { refresh_token: _stripped, ...rest } = tokens;
|
|
5588
|
+
return rest;
|
|
5589
|
+
}
|
|
5590
|
+
return tokens;
|
|
5591
|
+
}
|
|
5592
|
+
var StorableTokensSchema = OAuthTokensSchema3.partial();
|
|
5593
|
+
function hasSecretBearingTokenField(tokens) {
|
|
5594
|
+
return tokens.access_token !== void 0 || tokens.refresh_token !== void 0 || tokens.id_token !== void 0;
|
|
5595
|
+
}
|
|
5596
|
+
function splitTokens(tokens, policy) {
|
|
5597
|
+
const kept = applyTokensPolicy(tokens, policy);
|
|
5598
|
+
if (!kept) return {};
|
|
5599
|
+
if (!StorableTokensSchema.safeParse(kept).success) {
|
|
5600
|
+
return { plaintext: kept };
|
|
5601
|
+
}
|
|
5602
|
+
if (!hasSecretBearingTokenField(kept)) {
|
|
5603
|
+
return {};
|
|
5604
|
+
}
|
|
5605
|
+
return { secret: JSON.stringify(kept) };
|
|
5606
|
+
}
|
|
5607
|
+
function splitClientInformation(info, fields, secrets) {
|
|
5608
|
+
const { client_secret, registration_access_token, ...publicInfo } = info;
|
|
5609
|
+
if (client_secret === void 0 && registration_access_token === void 0) {
|
|
5610
|
+
return info;
|
|
5611
|
+
}
|
|
5612
|
+
if (client_secret !== void 0) {
|
|
5613
|
+
secrets[fields.clientSecret] = client_secret;
|
|
5614
|
+
}
|
|
5615
|
+
if (registration_access_token !== void 0) {
|
|
5616
|
+
secrets[fields.registrationToken] = registration_access_token;
|
|
5617
|
+
}
|
|
5618
|
+
return publicInfo;
|
|
5619
|
+
}
|
|
5620
|
+
function joinClientInformation(info, fields, secrets) {
|
|
5621
|
+
const clientSecret = secrets[fields.clientSecret];
|
|
5622
|
+
const registrationToken = secrets[fields.registrationToken];
|
|
5623
|
+
if (clientSecret === void 0 && registrationToken === void 0) {
|
|
5624
|
+
return info;
|
|
5625
|
+
}
|
|
5626
|
+
return {
|
|
5627
|
+
...info,
|
|
5628
|
+
...clientSecret !== void 0 && { client_secret: clientSecret },
|
|
5629
|
+
...registrationToken !== void 0 && {
|
|
5630
|
+
registration_access_token: registrationToken
|
|
5631
|
+
}
|
|
5632
|
+
};
|
|
5633
|
+
}
|
|
5634
|
+
var issuerClientInfoFields = (issuer) => ({
|
|
5635
|
+
clientSecret: issuerClientSecretField(issuer),
|
|
5636
|
+
registrationToken: issuerRegistrationTokenField(issuer)
|
|
5637
|
+
});
|
|
5638
|
+
var LEGACY_CLIENT_INFO_FIELDS = {
|
|
5639
|
+
clientSecret: LEGACY_CLIENT_SECRET_FIELD,
|
|
5640
|
+
registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD
|
|
5641
|
+
};
|
|
5642
|
+
var PREREG_CLIENT_INFO_FIELDS = {
|
|
5643
|
+
clientSecret: PREREG_CLIENT_SECRET_FIELD,
|
|
5644
|
+
registrationToken: PREREG_REGISTRATION_TOKEN_FIELD
|
|
5645
|
+
};
|
|
5646
|
+
function splitServerOAuthState(state, policy) {
|
|
5647
|
+
const secrets = {};
|
|
5648
|
+
const residue = { ...state };
|
|
5649
|
+
if (state.byIssuer) {
|
|
5650
|
+
const byIssuer = {};
|
|
5651
|
+
for (const [issuer, slot] of Object.entries(state.byIssuer)) {
|
|
5652
|
+
const slotResidue = { ...slot };
|
|
5653
|
+
if (slot.tokens) {
|
|
5654
|
+
delete slotResidue.tokens;
|
|
5655
|
+
const tokenSplit = splitTokens(slot.tokens, policy);
|
|
5656
|
+
if (tokenSplit.secret !== void 0) {
|
|
5657
|
+
secrets[issuerTokensField(issuer)] = tokenSplit.secret;
|
|
5658
|
+
}
|
|
5659
|
+
if (tokenSplit.plaintext) slotResidue.tokens = tokenSplit.plaintext;
|
|
5660
|
+
}
|
|
5661
|
+
if (slot.clientInformation) {
|
|
5662
|
+
slotResidue.clientInformation = splitClientInformation(
|
|
5663
|
+
slot.clientInformation,
|
|
5664
|
+
issuerClientInfoFields(issuer),
|
|
5665
|
+
secrets
|
|
5666
|
+
);
|
|
5667
|
+
}
|
|
5668
|
+
setOwnEntry(byIssuer, issuer, slotResidue);
|
|
5669
|
+
}
|
|
5670
|
+
residue.byIssuer = byIssuer;
|
|
5671
|
+
}
|
|
5672
|
+
if (state.tokens) {
|
|
5673
|
+
delete residue.tokens;
|
|
5674
|
+
const tokenSplit = splitTokens(state.tokens, policy);
|
|
5675
|
+
if (tokenSplit.secret !== void 0) {
|
|
5676
|
+
secrets[LEGACY_TOKENS_FIELD] = tokenSplit.secret;
|
|
5677
|
+
}
|
|
5678
|
+
if (tokenSplit.plaintext) residue.tokens = tokenSplit.plaintext;
|
|
5679
|
+
}
|
|
5680
|
+
if (state.clientInformation) {
|
|
5681
|
+
residue.clientInformation = splitClientInformation(
|
|
5682
|
+
state.clientInformation,
|
|
5683
|
+
LEGACY_CLIENT_INFO_FIELDS,
|
|
5684
|
+
secrets
|
|
5685
|
+
);
|
|
5686
|
+
}
|
|
5687
|
+
if (state.preregisteredClientInformation) {
|
|
5688
|
+
residue.preregisteredClientInformation = splitClientInformation(
|
|
5689
|
+
state.preregisteredClientInformation,
|
|
5690
|
+
PREREG_CLIENT_INFO_FIELDS,
|
|
5691
|
+
secrets
|
|
5692
|
+
);
|
|
5693
|
+
}
|
|
5694
|
+
return { residue, secrets };
|
|
5695
|
+
}
|
|
5696
|
+
function parseStoredTokens(raw) {
|
|
5697
|
+
try {
|
|
5698
|
+
const parsed = JSON.parse(raw);
|
|
5699
|
+
if (StorableTokensSchema.safeParse(parsed).success && hasSecretBearingTokenField(parsed)) {
|
|
5700
|
+
return parsed;
|
|
5701
|
+
}
|
|
5702
|
+
} catch {
|
|
5703
|
+
}
|
|
5704
|
+
return void 0;
|
|
5705
|
+
}
|
|
5706
|
+
function parseStoredIdpSession(raw) {
|
|
5707
|
+
try {
|
|
5708
|
+
const parsed = JSON.parse(raw);
|
|
5709
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
5710
|
+
const { idToken, refreshToken } = parsed;
|
|
5711
|
+
const usable = {
|
|
5712
|
+
...typeof idToken === "string" && { idToken },
|
|
5713
|
+
...typeof refreshToken === "string" && { refreshToken }
|
|
5714
|
+
};
|
|
5715
|
+
if (Object.keys(usable).length > 0) return usable;
|
|
5716
|
+
}
|
|
5717
|
+
} catch {
|
|
5718
|
+
}
|
|
5719
|
+
return void 0;
|
|
5720
|
+
}
|
|
5721
|
+
function isUsableStoredSecret(field, raw) {
|
|
5722
|
+
if (field === LEGACY_TOKENS_FIELD || field.startsWith("tokens:")) {
|
|
5723
|
+
return parseStoredTokens(raw) !== void 0;
|
|
5724
|
+
}
|
|
5725
|
+
if (field === IDP_SESSION_FIELD) {
|
|
5726
|
+
return parseStoredIdpSession(raw) !== void 0;
|
|
5727
|
+
}
|
|
5728
|
+
return true;
|
|
5729
|
+
}
|
|
5730
|
+
function joinServerOAuthState(residue, secrets) {
|
|
5731
|
+
const joined = { ...residue };
|
|
5732
|
+
if (residue.byIssuer) {
|
|
5733
|
+
const byIssuer = {};
|
|
5734
|
+
for (const [issuer, slot] of Object.entries(residue.byIssuer)) {
|
|
5735
|
+
const joinedSlot = { ...slot };
|
|
5736
|
+
const tokensRaw = secrets[issuerTokensField(issuer)];
|
|
5737
|
+
if (tokensRaw !== void 0) {
|
|
5738
|
+
const tokens = parseStoredTokens(tokensRaw);
|
|
5739
|
+
if (tokens) joinedSlot.tokens = tokens;
|
|
5740
|
+
}
|
|
5741
|
+
if (joinedSlot.clientInformation) {
|
|
5742
|
+
joinedSlot.clientInformation = joinClientInformation(
|
|
5743
|
+
joinedSlot.clientInformation,
|
|
5744
|
+
issuerClientInfoFields(issuer),
|
|
5745
|
+
secrets
|
|
5746
|
+
);
|
|
5747
|
+
}
|
|
5748
|
+
setOwnEntry(byIssuer, issuer, joinedSlot);
|
|
5749
|
+
}
|
|
5750
|
+
joined.byIssuer = byIssuer;
|
|
5751
|
+
}
|
|
5752
|
+
const legacyTokensRaw = secrets[LEGACY_TOKENS_FIELD];
|
|
5753
|
+
if (legacyTokensRaw !== void 0) {
|
|
5754
|
+
const tokens = parseStoredTokens(legacyTokensRaw);
|
|
5755
|
+
if (tokens) joined.tokens = tokens;
|
|
5756
|
+
}
|
|
5757
|
+
if (joined.clientInformation) {
|
|
5758
|
+
joined.clientInformation = joinClientInformation(
|
|
5759
|
+
joined.clientInformation,
|
|
5760
|
+
LEGACY_CLIENT_INFO_FIELDS,
|
|
5761
|
+
secrets
|
|
5762
|
+
);
|
|
5763
|
+
}
|
|
5764
|
+
if (joined.preregisteredClientInformation) {
|
|
5765
|
+
joined.preregisteredClientInformation = joinClientInformation(
|
|
5766
|
+
joined.preregisteredClientInformation,
|
|
5767
|
+
PREREG_CLIENT_INFO_FIELDS,
|
|
5768
|
+
secrets
|
|
5769
|
+
);
|
|
5770
|
+
}
|
|
5771
|
+
return joined;
|
|
5772
|
+
}
|
|
5773
|
+
function splitIdpSession(session, policy) {
|
|
5774
|
+
const { idToken, refreshToken, ...residue } = session;
|
|
5775
|
+
const secrets = {};
|
|
5776
|
+
if (policy !== "none" && (idToken !== void 0 || refreshToken !== void 0)) {
|
|
5777
|
+
const kept = {
|
|
5778
|
+
...typeof idToken === "string" && { idToken },
|
|
5779
|
+
...policy === "all" && typeof refreshToken === "string" && { refreshToken }
|
|
5780
|
+
};
|
|
5781
|
+
if (Object.keys(kept).length > 0) {
|
|
5782
|
+
secrets[IDP_SESSION_FIELD] = JSON.stringify(kept);
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5785
|
+
return { residue, secrets };
|
|
5786
|
+
}
|
|
5787
|
+
function joinIdpSession(residue, secrets) {
|
|
5788
|
+
const raw = secrets[IDP_SESSION_FIELD];
|
|
5789
|
+
if (raw === void 0) return { ...residue };
|
|
5790
|
+
const stored = parseStoredIdpSession(raw);
|
|
5791
|
+
return stored ? { ...residue, ...stored } : { ...residue };
|
|
5792
|
+
}
|
|
5793
|
+
function serverSecretFields(state) {
|
|
5794
|
+
const fields = /* @__PURE__ */ new Set([
|
|
5795
|
+
LEGACY_TOKENS_FIELD,
|
|
5796
|
+
LEGACY_CLIENT_SECRET_FIELD,
|
|
5797
|
+
LEGACY_REGISTRATION_TOKEN_FIELD,
|
|
5798
|
+
PREREG_CLIENT_SECRET_FIELD,
|
|
5799
|
+
PREREG_REGISTRATION_TOKEN_FIELD
|
|
5800
|
+
]);
|
|
5801
|
+
for (const issuer of Object.keys(state?.byIssuer ?? {})) {
|
|
5802
|
+
fields.add(issuerTokensField(issuer));
|
|
5803
|
+
fields.add(issuerClientSecretField(issuer));
|
|
5804
|
+
fields.add(issuerRegistrationTokenField(issuer));
|
|
5805
|
+
}
|
|
5806
|
+
return [...fields];
|
|
5807
|
+
}
|
|
5808
|
+
function clientInfoHasPlaintext(info) {
|
|
5809
|
+
return info !== void 0 && (info.client_secret !== void 0 || info.registration_access_token !== void 0);
|
|
5810
|
+
}
|
|
5811
|
+
function snapshotHasPlaintextSecrets(snapshot) {
|
|
5812
|
+
for (const state of Object.values(snapshot.servers)) {
|
|
5813
|
+
if (state.tokens) return true;
|
|
5814
|
+
if (clientInfoHasPlaintext(state.clientInformation)) return true;
|
|
5815
|
+
if (clientInfoHasPlaintext(state.preregisteredClientInformation)) {
|
|
5816
|
+
return true;
|
|
5817
|
+
}
|
|
5818
|
+
for (const slot of Object.values(state.byIssuer ?? {})) {
|
|
5819
|
+
if (slot.tokens) return true;
|
|
5820
|
+
if (clientInfoHasPlaintext(slot.clientInformation)) return true;
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5823
|
+
for (const session of Object.values(snapshot.idpSessions)) {
|
|
5824
|
+
if (session.idToken !== void 0 || session.refreshToken !== void 0) {
|
|
5825
|
+
return true;
|
|
5826
|
+
}
|
|
5827
|
+
}
|
|
5828
|
+
return false;
|
|
5829
|
+
}
|
|
5830
|
+
|
|
5013
5831
|
// ../../core/auth/node/oauth-persist-file.ts
|
|
5832
|
+
var warnedStoreFailures = /* @__PURE__ */ new Set();
|
|
5833
|
+
function warnStoreWriteFailure(error) {
|
|
5834
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
5835
|
+
if (warnedStoreFailures.has(reason)) return;
|
|
5836
|
+
warnedStoreFailures.add(reason);
|
|
5837
|
+
console.warn(
|
|
5838
|
+
`[mcp-inspector] Could not write OAuth tokens to the secret store (${reason}). Tokens will be kept in memory for this session only and will NOT be persisted; expect to re-authorize after a restart.`
|
|
5839
|
+
);
|
|
5840
|
+
}
|
|
5841
|
+
function warnRestoreFailure(error) {
|
|
5842
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
5843
|
+
const key = `restore:${reason}`;
|
|
5844
|
+
if (warnedStoreFailures.has(key)) return;
|
|
5845
|
+
warnedStoreFailures.add(key);
|
|
5846
|
+
console.warn(
|
|
5847
|
+
`[mcp-inspector] Could not restore secret-store entries after a failed OAuth state write (${reason}). The secret store may be inconsistent with oauth.json; if a server's stored credentials stop working, re-authorize it.`
|
|
5848
|
+
);
|
|
5849
|
+
}
|
|
5850
|
+
function warnUnconfirmedFile(error) {
|
|
5851
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
5852
|
+
const key = `unconfirmed:${reason}`;
|
|
5853
|
+
if (warnedStoreFailures.has(key)) return;
|
|
5854
|
+
warnedStoreFailures.add(key);
|
|
5855
|
+
console.warn(
|
|
5856
|
+
`[mcp-inspector] Could not re-read oauth.json after a failed OAuth state save (${reason}). The file may still contain the interrupted save while the secret store was restored to the previous values; if a server's stored credentials stop working, re-authorize it.`
|
|
5857
|
+
);
|
|
5858
|
+
}
|
|
5859
|
+
function warnMigrationFailure(error) {
|
|
5860
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
5861
|
+
const key = `migration:${reason}`;
|
|
5862
|
+
if (warnedStoreFailures.has(key)) return;
|
|
5863
|
+
warnedStoreFailures.add(key);
|
|
5864
|
+
console.warn(
|
|
5865
|
+
`[mcp-inspector] Could not migrate plaintext OAuth secrets into the secret store (${reason}). The plaintext copy in oauth.json was kept and keeps working; migration will be retried on the next read.`
|
|
5866
|
+
);
|
|
5867
|
+
}
|
|
5868
|
+
function rethrowLockError(filePath, error, action = "save") {
|
|
5869
|
+
if (error instanceof SecretFileLockHeldError) {
|
|
5870
|
+
throw new SecretFileLockHeldError(
|
|
5871
|
+
`Could not ${action} OAuth state: the state file at ${filePath} is locked by another Inspector process and did not become available.`,
|
|
5872
|
+
{ cause: error }
|
|
5873
|
+
);
|
|
5874
|
+
}
|
|
5875
|
+
throw error;
|
|
5876
|
+
}
|
|
5877
|
+
async function withOAuthStateLock(filePath, action, body) {
|
|
5878
|
+
let entered = false;
|
|
5879
|
+
try {
|
|
5880
|
+
return await withSecretFileLock(filePath, async () => {
|
|
5881
|
+
entered = true;
|
|
5882
|
+
return body();
|
|
5883
|
+
});
|
|
5884
|
+
} catch (error) {
|
|
5885
|
+
if (!entered) rethrowLockError(filePath, error, action);
|
|
5886
|
+
throw error;
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
var OAuthStateFileUnrecognizedError = class extends Error {
|
|
5890
|
+
constructor(filePath, action) {
|
|
5891
|
+
super(
|
|
5892
|
+
`Refusing to ${action} OAuth state: ${filePath} exists but is not a recognized OAuth state file (it may be corrupt, truncated, or written by something else). Its entries are the only index of credentials in the OS keychain / secret store, so overwriting it would strand them. Restore the file from a backup or fix its JSON; deleting it starts fresh but abandons any credentials it indexed.`
|
|
5893
|
+
);
|
|
5894
|
+
this.name = "OAuthStateFileUnrecognizedError";
|
|
5895
|
+
}
|
|
5896
|
+
};
|
|
5897
|
+
async function readDiskForMutation(filePath, action) {
|
|
5898
|
+
const raw = await readStoreFile(filePath);
|
|
5899
|
+
const parsed = parseOAuthPersistBlob(raw);
|
|
5900
|
+
if (raw !== null && parsed === null) {
|
|
5901
|
+
throw new OAuthStateFileUnrecognizedError(filePath, action);
|
|
5902
|
+
}
|
|
5903
|
+
return parsed;
|
|
5904
|
+
}
|
|
5905
|
+
async function persistEntrySecrets(store, serverId, candidates, secrets, prior, allowDegrade) {
|
|
5906
|
+
const priorValue = new Map(prior.map(({ field, value }) => [field, value]));
|
|
5907
|
+
const changed = {};
|
|
5908
|
+
for (const [field, value] of Object.entries(secrets)) {
|
|
5909
|
+
if (priorValue.get(field) !== value) changed[field] = value;
|
|
5910
|
+
}
|
|
5911
|
+
try {
|
|
5912
|
+
if (Object.keys(changed).length > 0) {
|
|
5913
|
+
await secretStoreSetMany(store, serverId, changed);
|
|
5914
|
+
}
|
|
5915
|
+
} catch (error) {
|
|
5916
|
+
if (!allowDegrade) throw error;
|
|
5917
|
+
warnStoreWriteFailure(error);
|
|
5918
|
+
const touched = prior.filter(({ field }) => changed[field] !== void 0);
|
|
5919
|
+
let restoreFailure;
|
|
5920
|
+
await restoreSecretFields(store, touched, (err) => {
|
|
5921
|
+
restoreFailure = err;
|
|
5922
|
+
});
|
|
5923
|
+
if (restoreFailure !== void 0) throw restoreFailure;
|
|
5924
|
+
return false;
|
|
5925
|
+
}
|
|
5926
|
+
await settleStoreMutations(
|
|
5927
|
+
candidates.filter(
|
|
5928
|
+
(field) => secrets[field] === void 0 && priorValue.get(field) !== null
|
|
5929
|
+
).map((field) => store.delete(serverId, field))
|
|
5930
|
+
);
|
|
5931
|
+
return true;
|
|
5932
|
+
}
|
|
5933
|
+
function revertEntryToDisk(merged, disk, key) {
|
|
5934
|
+
const diskEntry = getOwnEntry(disk, key);
|
|
5935
|
+
if (diskEntry === void 0) {
|
|
5936
|
+
if (Object.hasOwn(merged, key)) delete merged[key];
|
|
5937
|
+
} else {
|
|
5938
|
+
setOwnEntry(merged, key, diskEntry);
|
|
5939
|
+
}
|
|
5940
|
+
}
|
|
5941
|
+
function preserveNonDurableSecrets(diskSecrets, secrets) {
|
|
5942
|
+
const keep = {};
|
|
5943
|
+
for (const [field, value] of Object.entries(secrets)) {
|
|
5944
|
+
if (diskSecrets[field] === value) keep[field] = value;
|
|
5945
|
+
}
|
|
5946
|
+
return keep;
|
|
5947
|
+
}
|
|
5948
|
+
async function writeOAuthSections(filePath, snapshot, sections, secretStore = defaultSecretStore()) {
|
|
5949
|
+
const policy = getPersistTokensPolicy();
|
|
5950
|
+
const durable = await secretStoreIsDurable(secretStore);
|
|
5951
|
+
await withOAuthStateLock(filePath, "save", async () => {
|
|
5952
|
+
const restoreBaseline = /* @__PURE__ */ new Map();
|
|
5953
|
+
const ourWrites = /* @__PURE__ */ new Map();
|
|
5954
|
+
const priorKey = (p) => `${p.serverId}\0${p.field}`;
|
|
5955
|
+
const foldPriors = (priors) => {
|
|
5956
|
+
for (const prior of priors) {
|
|
5957
|
+
const key = priorKey(prior);
|
|
5958
|
+
if (restoreBaseline.has(key) && ourWrites.get(key) !== void 0 && prior.value === ourWrites.get(key)) {
|
|
5959
|
+
continue;
|
|
5960
|
+
}
|
|
5961
|
+
restoreBaseline.set(key, prior);
|
|
5962
|
+
}
|
|
5963
|
+
};
|
|
5964
|
+
const restoreToBaseline = () => restoreSecretFields(
|
|
5965
|
+
secretStore,
|
|
5966
|
+
[...restoreBaseline.values()],
|
|
5967
|
+
warnRestoreFailure
|
|
5968
|
+
);
|
|
5969
|
+
let unconfirmed = null;
|
|
5970
|
+
const reconcileFailure = async () => {
|
|
5971
|
+
if (unconfirmed !== null) {
|
|
5972
|
+
const committed = unconfirmed;
|
|
5973
|
+
let observed = null;
|
|
5974
|
+
let readable = true;
|
|
5975
|
+
try {
|
|
5976
|
+
observed = await readStoreFile(filePath);
|
|
5977
|
+
} catch (error) {
|
|
5978
|
+
readable = false;
|
|
5979
|
+
warnUnconfirmedFile(error);
|
|
5980
|
+
}
|
|
5981
|
+
if (readable && observed === committed.written) {
|
|
5982
|
+
const target = new Map(
|
|
5983
|
+
[...restoreBaseline].map(([key, { serverId, field, value }]) => [
|
|
5984
|
+
key,
|
|
5985
|
+
{ serverId, field, value }
|
|
5986
|
+
])
|
|
5987
|
+
);
|
|
5988
|
+
for (const [key, write] of committed.writes) target.set(key, write);
|
|
5989
|
+
try {
|
|
5990
|
+
await settleStoreMutations(
|
|
5991
|
+
[...target.values()].map(
|
|
5992
|
+
({ serverId, field, value }) => value === null ? secretStore.delete(serverId, field) : secretStore.set(serverId, field, value)
|
|
5993
|
+
)
|
|
5994
|
+
);
|
|
5995
|
+
return true;
|
|
5996
|
+
} catch (error) {
|
|
5997
|
+
warnRestoreFailure(error);
|
|
5998
|
+
return false;
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
await restoreToBaseline();
|
|
6003
|
+
return false;
|
|
6004
|
+
};
|
|
6005
|
+
for (let attempt = 0; attempt < MAX_WRITE_ATTEMPTS; attempt++) {
|
|
6006
|
+
const priorSecrets = [];
|
|
6007
|
+
const attemptWrites = /* @__PURE__ */ new Map();
|
|
6008
|
+
let written;
|
|
6009
|
+
try {
|
|
6010
|
+
const disk = await readDiskForMutation(filePath, "save");
|
|
6011
|
+
const effective = sections ? {
|
|
6012
|
+
servers: sections.servers && [...new Set(sections.servers)],
|
|
6013
|
+
idpSessions: sections.idpSessions && [
|
|
6014
|
+
...new Set(sections.idpSessions)
|
|
6015
|
+
]
|
|
6016
|
+
} : {
|
|
6017
|
+
servers: [
|
|
6018
|
+
.../* @__PURE__ */ new Set([
|
|
6019
|
+
...Object.keys(disk?.servers ?? {}),
|
|
6020
|
+
...Object.keys(snapshot.servers)
|
|
6021
|
+
])
|
|
6022
|
+
],
|
|
6023
|
+
idpSessions: [
|
|
6024
|
+
.../* @__PURE__ */ new Set([
|
|
6025
|
+
...Object.keys(disk?.idpSessions ?? {}),
|
|
6026
|
+
...Object.keys(snapshot.idpSessions)
|
|
6027
|
+
])
|
|
6028
|
+
]
|
|
6029
|
+
};
|
|
6030
|
+
const merged = mergeOAuthSections(disk, snapshot, effective);
|
|
6031
|
+
for (const url of effective.servers ?? []) {
|
|
6032
|
+
const serverId = oauthSecretServerId(url);
|
|
6033
|
+
const next = getOwnEntry(snapshot.servers, url);
|
|
6034
|
+
const candidates = [
|
|
6035
|
+
.../* @__PURE__ */ new Set([
|
|
6036
|
+
...serverSecretFields(getOwnEntry(disk?.servers, url)),
|
|
6037
|
+
...serverSecretFields(next)
|
|
6038
|
+
])
|
|
6039
|
+
];
|
|
6040
|
+
const entryPrior = await snapshotSecretFields(
|
|
6041
|
+
secretStore,
|
|
6042
|
+
serverId,
|
|
6043
|
+
candidates
|
|
6044
|
+
);
|
|
6045
|
+
priorSecrets.push(...entryPrior);
|
|
6046
|
+
if (next === void 0) {
|
|
6047
|
+
for (const field of candidates) {
|
|
6048
|
+
ourWrites.set(priorKey({ serverId, field }), null);
|
|
6049
|
+
}
|
|
6050
|
+
await secretStore.deleteAllForServer(serverId);
|
|
6051
|
+
for (const field of candidates) {
|
|
6052
|
+
attemptWrites.set(priorKey({ serverId, field }), {
|
|
6053
|
+
serverId,
|
|
6054
|
+
field,
|
|
6055
|
+
value: null
|
|
6056
|
+
});
|
|
6057
|
+
}
|
|
6058
|
+
continue;
|
|
6059
|
+
}
|
|
6060
|
+
const { residue, secrets } = splitServerOAuthState(next, policy);
|
|
6061
|
+
for (const field of candidates) {
|
|
6062
|
+
ourWrites.set(
|
|
6063
|
+
priorKey({ serverId, field }),
|
|
6064
|
+
secrets[field] ?? null
|
|
6065
|
+
);
|
|
6066
|
+
}
|
|
6067
|
+
setOwnEntry(merged.servers, url, residue);
|
|
6068
|
+
if (!durable) {
|
|
6069
|
+
const diskEntry = getOwnEntry(disk?.servers, url);
|
|
6070
|
+
const keep = preserveNonDurableSecrets(
|
|
6071
|
+
diskEntry ? splitServerOAuthState(diskEntry, policy).secrets : {},
|
|
6072
|
+
secrets
|
|
6073
|
+
);
|
|
6074
|
+
if (Object.keys(keep).length > 0) {
|
|
6075
|
+
setOwnEntry(
|
|
6076
|
+
merged.servers,
|
|
6077
|
+
url,
|
|
6078
|
+
joinServerOAuthState(residue, keep)
|
|
6079
|
+
);
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
6082
|
+
const persisted = await persistEntrySecrets(
|
|
6083
|
+
secretStore,
|
|
6084
|
+
serverId,
|
|
6085
|
+
candidates,
|
|
6086
|
+
secrets,
|
|
6087
|
+
entryPrior,
|
|
6088
|
+
// Degrading is only sound when the disk entry is the pre-call
|
|
6089
|
+
// state this attempt's priors pair with; on a retry it may be an
|
|
6090
|
+
// earlier attempt's committed residue, so escalate into the
|
|
6091
|
+
// reconciling exit instead.
|
|
6092
|
+
attempt === 0
|
|
6093
|
+
);
|
|
6094
|
+
if (!persisted) {
|
|
6095
|
+
revertEntryToDisk(merged.servers, disk?.servers, url);
|
|
6096
|
+
continue;
|
|
6097
|
+
}
|
|
6098
|
+
for (const field of candidates) {
|
|
6099
|
+
attemptWrites.set(priorKey({ serverId, field }), {
|
|
6100
|
+
serverId,
|
|
6101
|
+
field,
|
|
6102
|
+
value: secrets[field] ?? null
|
|
6103
|
+
});
|
|
6104
|
+
}
|
|
6105
|
+
}
|
|
6106
|
+
for (const issuer of effective.idpSessions ?? []) {
|
|
6107
|
+
const serverId = oauthIdpSecretServerId(issuer);
|
|
6108
|
+
const next = getOwnEntry(snapshot.idpSessions, issuer);
|
|
6109
|
+
const entryPrior = await snapshotSecretFields(secretStore, serverId, [
|
|
6110
|
+
IDP_SESSION_FIELD
|
|
6111
|
+
]);
|
|
6112
|
+
priorSecrets.push(...entryPrior);
|
|
6113
|
+
if (next === void 0) {
|
|
6114
|
+
ourWrites.set(
|
|
6115
|
+
priorKey({ serverId, field: IDP_SESSION_FIELD }),
|
|
6116
|
+
null
|
|
6117
|
+
);
|
|
6118
|
+
await secretStore.deleteAllForServer(serverId);
|
|
6119
|
+
attemptWrites.set(
|
|
6120
|
+
priorKey({ serverId, field: IDP_SESSION_FIELD }),
|
|
6121
|
+
{
|
|
6122
|
+
serverId,
|
|
6123
|
+
field: IDP_SESSION_FIELD,
|
|
6124
|
+
value: null
|
|
6125
|
+
}
|
|
6126
|
+
);
|
|
6127
|
+
continue;
|
|
6128
|
+
}
|
|
6129
|
+
const { residue, secrets } = splitIdpSession(next, policy);
|
|
6130
|
+
ourWrites.set(
|
|
6131
|
+
priorKey({ serverId, field: IDP_SESSION_FIELD }),
|
|
6132
|
+
secrets[IDP_SESSION_FIELD] ?? null
|
|
6133
|
+
);
|
|
6134
|
+
setOwnEntry(merged.idpSessions, issuer, residue);
|
|
6135
|
+
if (!durable) {
|
|
6136
|
+
const diskSession = getOwnEntry(disk?.idpSessions, issuer);
|
|
6137
|
+
const keep = preserveNonDurableSecrets(
|
|
6138
|
+
diskSession ? splitIdpSession(diskSession, policy).secrets : {},
|
|
6139
|
+
secrets
|
|
6140
|
+
);
|
|
6141
|
+
if (Object.keys(keep).length > 0) {
|
|
6142
|
+
setOwnEntry(
|
|
6143
|
+
merged.idpSessions,
|
|
6144
|
+
issuer,
|
|
6145
|
+
joinIdpSession(residue, keep)
|
|
6146
|
+
);
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
const persisted = await persistEntrySecrets(
|
|
6150
|
+
secretStore,
|
|
6151
|
+
serverId,
|
|
6152
|
+
[IDP_SESSION_FIELD],
|
|
6153
|
+
secrets,
|
|
6154
|
+
entryPrior,
|
|
6155
|
+
// Same as the server loop: degrade on the first attempt only.
|
|
6156
|
+
attempt === 0
|
|
6157
|
+
);
|
|
6158
|
+
if (!persisted) {
|
|
6159
|
+
revertEntryToDisk(merged.idpSessions, disk?.idpSessions, issuer);
|
|
6160
|
+
continue;
|
|
6161
|
+
}
|
|
6162
|
+
attemptWrites.set(priorKey({ serverId, field: IDP_SESSION_FIELD }), {
|
|
6163
|
+
serverId,
|
|
6164
|
+
field: IDP_SESSION_FIELD,
|
|
6165
|
+
value: secrets[IDP_SESSION_FIELD] ?? null
|
|
6166
|
+
});
|
|
6167
|
+
}
|
|
6168
|
+
written = serializeOAuthPersistBlob(merged);
|
|
6169
|
+
await writeStoreFile(filePath, written);
|
|
6170
|
+
unconfirmed = { written, writes: attemptWrites };
|
|
6171
|
+
} catch (error) {
|
|
6172
|
+
foldPriors(priorSecrets);
|
|
6173
|
+
if (await reconcileFailure()) return;
|
|
6174
|
+
throw error;
|
|
6175
|
+
}
|
|
6176
|
+
foldPriors(priorSecrets);
|
|
6177
|
+
let observed;
|
|
6178
|
+
try {
|
|
6179
|
+
observed = await readStoreFile(filePath);
|
|
6180
|
+
} catch {
|
|
6181
|
+
continue;
|
|
6182
|
+
}
|
|
6183
|
+
if (observed === written) return;
|
|
6184
|
+
unconfirmed = null;
|
|
6185
|
+
}
|
|
6186
|
+
if (await reconcileFailure()) return;
|
|
6187
|
+
throw new SecretStoreUnavailableError(
|
|
6188
|
+
`Could not save OAuth state: another process kept overwriting ${filePath} (gave up after ${MAX_WRITE_ATTEMPTS} attempts). Re-run the action to retry.`
|
|
6189
|
+
);
|
|
6190
|
+
});
|
|
6191
|
+
}
|
|
6192
|
+
function secretRequestsFor(snapshot) {
|
|
6193
|
+
const requests = [];
|
|
6194
|
+
for (const [url, state] of Object.entries(snapshot.servers)) {
|
|
6195
|
+
requests.push({
|
|
6196
|
+
serverId: oauthSecretServerId(url),
|
|
6197
|
+
fields: serverSecretFields(state)
|
|
6198
|
+
});
|
|
6199
|
+
}
|
|
6200
|
+
for (const issuer of Object.keys(snapshot.idpSessions)) {
|
|
6201
|
+
requests.push({
|
|
6202
|
+
serverId: oauthIdpSecretServerId(issuer),
|
|
6203
|
+
fields: [IDP_SESSION_FIELD]
|
|
6204
|
+
});
|
|
6205
|
+
}
|
|
6206
|
+
return requests;
|
|
6207
|
+
}
|
|
6208
|
+
async function joinSnapshot(snapshot, secretStore) {
|
|
6209
|
+
const requests = secretRequestsFor(snapshot);
|
|
6210
|
+
const values = requests.length > 0 ? await secretStoreGetManyStrict(secretStore, requests) : {};
|
|
6211
|
+
return {
|
|
6212
|
+
servers: Object.fromEntries(
|
|
6213
|
+
Object.entries(snapshot.servers).map(([url, state]) => [
|
|
6214
|
+
url,
|
|
6215
|
+
joinServerOAuthState(state, values[oauthSecretServerId(url)] ?? {})
|
|
6216
|
+
])
|
|
6217
|
+
),
|
|
6218
|
+
idpSessions: Object.fromEntries(
|
|
6219
|
+
Object.entries(snapshot.idpSessions).map(([issuer, session]) => [
|
|
6220
|
+
issuer,
|
|
6221
|
+
joinIdpSession(session, values[oauthIdpSecretServerId(issuer)] ?? {})
|
|
6222
|
+
])
|
|
6223
|
+
)
|
|
6224
|
+
};
|
|
6225
|
+
}
|
|
6226
|
+
async function migratePlaintextSecrets(filePath, secretStore) {
|
|
6227
|
+
const raw = await readStoreFile(filePath);
|
|
6228
|
+
const fresh = parseOAuthPersistBlob(raw);
|
|
6229
|
+
if (!fresh || !snapshotHasPlaintextSecrets(fresh)) return;
|
|
6230
|
+
const migrateEntrySecrets = async (serverId, secrets) => {
|
|
6231
|
+
const absent = {};
|
|
6232
|
+
for (const [field, value] of Object.entries(secrets)) {
|
|
6233
|
+
const existing = await secretStoreGetStrict(secretStore, serverId, field);
|
|
6234
|
+
if (existing === null || !isUsableStoredSecret(field, existing)) {
|
|
6235
|
+
absent[field] = value;
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
if (Object.keys(absent).length > 0) {
|
|
6239
|
+
await secretStoreSetMany(secretStore, serverId, absent);
|
|
6240
|
+
}
|
|
6241
|
+
};
|
|
6242
|
+
const residue = { servers: {}, idpSessions: {} };
|
|
6243
|
+
for (const [url, state] of Object.entries(fresh.servers)) {
|
|
6244
|
+
const split = splitServerOAuthState(state, "all");
|
|
6245
|
+
setOwnEntry(residue.servers, url, split.residue);
|
|
6246
|
+
await migrateEntrySecrets(oauthSecretServerId(url), split.secrets);
|
|
6247
|
+
}
|
|
6248
|
+
for (const [issuer, session] of Object.entries(fresh.idpSessions)) {
|
|
6249
|
+
const split = splitIdpSession(session, "all");
|
|
6250
|
+
setOwnEntry(residue.idpSessions, issuer, split.residue);
|
|
6251
|
+
await migrateEntrySecrets(oauthIdpSecretServerId(issuer), split.secrets);
|
|
6252
|
+
}
|
|
6253
|
+
const stripped = serializeOAuthPersistBlob(residue);
|
|
6254
|
+
if (stripped !== raw) {
|
|
6255
|
+
await writeStoreFile(filePath, stripped);
|
|
6256
|
+
}
|
|
6257
|
+
}
|
|
6258
|
+
async function readOAuthStore(filePath, secretStore = defaultSecretStore()) {
|
|
6259
|
+
return withOAuthStateLock(filePath, "read", async () => {
|
|
6260
|
+
let snapshot = parseOAuthPersistBlob(await readStoreFile(filePath));
|
|
6261
|
+
if (snapshot === null) return null;
|
|
6262
|
+
if (snapshotHasPlaintextSecrets(snapshot) && await secretStoreIsDurable(secretStore)) {
|
|
6263
|
+
try {
|
|
6264
|
+
await migratePlaintextSecrets(filePath, secretStore);
|
|
6265
|
+
snapshot = parseOAuthPersistBlob(await readStoreFile(filePath)) ?? snapshot;
|
|
6266
|
+
} catch (error) {
|
|
6267
|
+
warnMigrationFailure(error);
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
return joinSnapshot(snapshot, secretStore);
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
async function removeOAuthStore(filePath, secretStore = defaultSecretStore()) {
|
|
6274
|
+
await withOAuthStateLock(filePath, "remove", async () => {
|
|
6275
|
+
const snapshot = await readDiskForMutation(filePath, "remove");
|
|
6276
|
+
if (snapshot) {
|
|
6277
|
+
const targets = [
|
|
6278
|
+
...Object.entries(snapshot.servers).map(([url, state]) => ({
|
|
6279
|
+
id: oauthSecretServerId(url),
|
|
6280
|
+
fields: serverSecretFields(state)
|
|
6281
|
+
})),
|
|
6282
|
+
...Object.keys(snapshot.idpSessions).map((issuer) => ({
|
|
6283
|
+
id: oauthIdpSecretServerId(issuer),
|
|
6284
|
+
fields: [IDP_SESSION_FIELD]
|
|
6285
|
+
}))
|
|
6286
|
+
];
|
|
6287
|
+
const priorSecrets = [];
|
|
6288
|
+
try {
|
|
6289
|
+
for (const { id, fields } of targets) {
|
|
6290
|
+
priorSecrets.push(
|
|
6291
|
+
...await snapshotSecretFields(secretStore, id, fields)
|
|
6292
|
+
);
|
|
6293
|
+
await secretStore.deleteAllForServer(id);
|
|
6294
|
+
}
|
|
6295
|
+
await deleteStoreFile(filePath);
|
|
6296
|
+
} catch (error) {
|
|
6297
|
+
await restoreSecretFields(
|
|
6298
|
+
secretStore,
|
|
6299
|
+
priorSecrets,
|
|
6300
|
+
warnRestoreFailure
|
|
6301
|
+
);
|
|
6302
|
+
throw error;
|
|
6303
|
+
}
|
|
6304
|
+
} else {
|
|
6305
|
+
await deleteStoreFile(filePath);
|
|
6306
|
+
}
|
|
6307
|
+
});
|
|
6308
|
+
}
|
|
5014
6309
|
function createFileOAuthPersistBackend(options) {
|
|
6310
|
+
const secretStore = options.secretStore ?? defaultSecretStore();
|
|
5015
6311
|
return {
|
|
5016
6312
|
async read() {
|
|
5017
|
-
|
|
5018
|
-
return parseOAuthPersistBlob(raw);
|
|
6313
|
+
return readOAuthStore(options.filePath, secretStore);
|
|
5019
6314
|
},
|
|
5020
|
-
async write(snapshot) {
|
|
5021
|
-
await
|
|
6315
|
+
async write(snapshot, sections) {
|
|
6316
|
+
await writeOAuthSections(
|
|
5022
6317
|
options.filePath,
|
|
5023
|
-
|
|
6318
|
+
snapshot,
|
|
6319
|
+
sections,
|
|
6320
|
+
secretStore
|
|
5024
6321
|
);
|
|
5025
6322
|
},
|
|
5026
6323
|
async remove() {
|
|
5027
|
-
await
|
|
6324
|
+
await removeOAuthStore(options.filePath, secretStore);
|
|
5028
6325
|
}
|
|
5029
6326
|
};
|
|
5030
6327
|
}
|
|
@@ -5041,31 +6338,39 @@ function getStateFilePath(customPath) {
|
|
|
5041
6338
|
}
|
|
5042
6339
|
return DEFAULT_STATE_PATH;
|
|
5043
6340
|
}
|
|
5044
|
-
var
|
|
6341
|
+
var sharedStateCache = /* @__PURE__ */ new Map();
|
|
5045
6342
|
var storageCache = /* @__PURE__ */ new Map();
|
|
5046
|
-
function
|
|
6343
|
+
function getSharedState(stateFilePath) {
|
|
5047
6344
|
const key = getStateFilePath(stateFilePath);
|
|
5048
|
-
let
|
|
5049
|
-
if (!
|
|
5050
|
-
|
|
5051
|
-
|
|
6345
|
+
let shared = sharedStateCache.get(key);
|
|
6346
|
+
if (!shared) {
|
|
6347
|
+
shared = {
|
|
6348
|
+
memory: new OAuthMemoryStore(),
|
|
6349
|
+
coordination: new OAuthStorageCoordination()
|
|
6350
|
+
};
|
|
6351
|
+
sharedStateCache.set(key, shared);
|
|
5052
6352
|
}
|
|
5053
|
-
return
|
|
6353
|
+
return shared;
|
|
5054
6354
|
}
|
|
5055
6355
|
function resetNodeOAuthStorageCache(stateFilePath) {
|
|
5056
6356
|
const key = getStateFilePath(stateFilePath);
|
|
5057
|
-
|
|
6357
|
+
sharedStateCache.delete(key);
|
|
5058
6358
|
storageCache.delete(key);
|
|
5059
6359
|
}
|
|
5060
6360
|
var NodeOAuthStorage = class extends OAuthStorageBase {
|
|
5061
6361
|
/**
|
|
5062
6362
|
* @param storagePath - Optional path to state file. Default: ~/.mcp-inspector/storage/oauth.json
|
|
6363
|
+
* @param secretStore - Optional secret store for tokens/client secrets.
|
|
6364
|
+
* Default: the process-wide selected store. Tests inject an in-memory
|
|
6365
|
+
* double here so they never touch the OS keychain.
|
|
5063
6366
|
*/
|
|
5064
|
-
constructor(storagePath) {
|
|
6367
|
+
constructor(storagePath, secretStore) {
|
|
5065
6368
|
const filePath = getStateFilePath(storagePath);
|
|
6369
|
+
const shared = getSharedState(storagePath);
|
|
5066
6370
|
super(
|
|
5067
|
-
|
|
5068
|
-
createFileOAuthPersistBackend({ filePath })
|
|
6371
|
+
shared.memory,
|
|
6372
|
+
createFileOAuthPersistBackend({ filePath, secretStore }),
|
|
6373
|
+
shared.coordination
|
|
5069
6374
|
);
|
|
5070
6375
|
}
|
|
5071
6376
|
};
|
|
@@ -5074,7 +6379,7 @@ var NodeOAuthStorage = class extends OAuthStorageBase {
|
|
|
5074
6379
|
import { selectClientAuthMethod } from "@modelcontextprotocol/client";
|
|
5075
6380
|
import {
|
|
5076
6381
|
OAuthClientInformationSchema as OAuthClientInformationSchema2,
|
|
5077
|
-
OAuthTokensSchema as
|
|
6382
|
+
OAuthTokensSchema as OAuthTokensSchema4
|
|
5078
6383
|
} from "@modelcontextprotocol/core";
|
|
5079
6384
|
var DEFAULT_REVOCATION_TIMEOUT_MS = 5e3;
|
|
5080
6385
|
var MIN_REVOCATION_REQUEST_BUDGET_MS = 5;
|
|
@@ -5302,7 +6607,7 @@ async function collectGrants(snapshot) {
|
|
|
5302
6607
|
return { grants, failures };
|
|
5303
6608
|
}
|
|
5304
6609
|
async function parseTokens(raw) {
|
|
5305
|
-
return raw === void 0 || raw === null ? void 0 : await
|
|
6610
|
+
return raw === void 0 || raw === null ? void 0 : await OAuthTokensSchema4.partial().parseAsync(raw);
|
|
5306
6611
|
}
|
|
5307
6612
|
async function parseClient(raw) {
|
|
5308
6613
|
return raw === void 0 || raw === null ? void 0 : await OAuthClientInformationSchema2.parseAsync(raw);
|
|
@@ -5462,14 +6767,14 @@ var OBJECT_VALUED_SUBSCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
5462
6767
|
function pathName(path5) {
|
|
5463
6768
|
return path5.length === 0 ? "<root>" : path5.join(".");
|
|
5464
6769
|
}
|
|
5465
|
-
function
|
|
6770
|
+
function isRecord4(node) {
|
|
5466
6771
|
return node !== null && typeof node === "object";
|
|
5467
6772
|
}
|
|
5468
6773
|
function scanXMcpHeaderDeclarations(inputSchema) {
|
|
5469
6774
|
const declarations = [];
|
|
5470
6775
|
const seenLower = /* @__PURE__ */ new Map();
|
|
5471
6776
|
const visit = (node, path5, reachable) => {
|
|
5472
|
-
if (!
|
|
6777
|
+
if (!isRecord4(node)) return void 0;
|
|
5473
6778
|
const schema = node;
|
|
5474
6779
|
if (X_MCP_HEADER_KEY in schema) {
|
|
5475
6780
|
if (!reachable || path5.length === 0) {
|
|
@@ -5495,7 +6800,7 @@ function scanXMcpHeaderDeclarations(inputSchema) {
|
|
|
5495
6800
|
declarations.push({ path: path5, headerName: raw, type });
|
|
5496
6801
|
}
|
|
5497
6802
|
const properties = schema.properties;
|
|
5498
|
-
if (
|
|
6803
|
+
if (isRecord4(properties)) {
|
|
5499
6804
|
for (const [key, child] of Object.entries(properties)) {
|
|
5500
6805
|
const fault2 = visit(child, [...path5, key], reachable);
|
|
5501
6806
|
if (fault2 !== void 0) return fault2;
|
|
@@ -5504,7 +6809,7 @@ function scanXMcpHeaderDeclarations(inputSchema) {
|
|
|
5504
6809
|
for (const k of NON_REACHABLE_SUBSCHEMA_KEYWORDS) {
|
|
5505
6810
|
const sub = schema[k];
|
|
5506
6811
|
if (sub === void 0) continue;
|
|
5507
|
-
const branches = Array.isArray(sub) ? sub :
|
|
6812
|
+
const branches = Array.isArray(sub) ? sub : isRecord4(sub) && OBJECT_VALUED_SUBSCHEMA_KEYWORDS.has(k) ? Object.values(sub) : [sub];
|
|
5508
6813
|
for (const branch of branches) {
|
|
5509
6814
|
const fault2 = visit(branch, [...path5, `<${k}>`], false);
|
|
5510
6815
|
if (fault2 !== void 0) return fault2;
|
|
@@ -5736,7 +7041,7 @@ import {
|
|
|
5736
7041
|
LOG_LEVEL_META_KEY,
|
|
5737
7042
|
CLIENT_CAPABILITIES_META_KEY,
|
|
5738
7043
|
CLIENT_INFO_META_KEY,
|
|
5739
|
-
PROTOCOL_VERSION_META_KEY,
|
|
7044
|
+
PROTOCOL_VERSION_META_KEY as PROTOCOL_VERSION_META_KEY2,
|
|
5740
7045
|
RELATED_TASK_META_KEY as RELATED_TASK_META_KEY3
|
|
5741
7046
|
} from "@modelcontextprotocol/client";
|
|
5742
7047
|
|
|
@@ -5805,10 +7110,13 @@ var ADVERTISABLE_EXTENSIONS = [
|
|
|
5805
7110
|
{
|
|
5806
7111
|
key: UI_EXTENSION_KEY,
|
|
5807
7112
|
label: "MCP Apps UI (io.modelcontextprotocol/ui)",
|
|
5808
|
-
// The MCP Apps UI extension
|
|
5809
|
-
//
|
|
5810
|
-
// a
|
|
7113
|
+
// The MCP Apps UI extension, advertised with the App resource MIME type the
|
|
7114
|
+
// Inspector renders — a conforming server checks the `mimeTypes` before
|
|
7115
|
+
// serving a UI resource. Default-on only where Apps can actually be
|
|
7116
|
+
// rendered (the web client); the CLI and TUI cannot, so they leave it off
|
|
7117
|
+
// unless explicitly overridden (#2403).
|
|
5811
7118
|
defaultAdvertised: true,
|
|
7119
|
+
requiresAppRenderer: true,
|
|
5812
7120
|
advertisement: { mimeTypes: [MCP_APP_MIME_TYPE] }
|
|
5813
7121
|
},
|
|
5814
7122
|
{
|
|
@@ -5824,10 +7132,13 @@ var ADVERTISABLE_EXTENSIONS = [
|
|
|
5824
7132
|
defaultAdvertised: true
|
|
5825
7133
|
}
|
|
5826
7134
|
];
|
|
7135
|
+
function isAdvertisedByDefault(ext, rendersApps) {
|
|
7136
|
+
return ext.defaultAdvertised && (!ext.requiresAppRenderer || rendersApps);
|
|
7137
|
+
}
|
|
5827
7138
|
function buildClientExtensions(input) {
|
|
5828
7139
|
const map = {};
|
|
5829
7140
|
for (const ext of ADVERTISABLE_EXTENSIONS) {
|
|
5830
|
-
const advertised = input.advertised?.[ext.key] ?? ext.
|
|
7141
|
+
const advertised = input.advertised?.[ext.key] ?? isAdvertisedByDefault(ext, input.rendersApps === true);
|
|
5831
7142
|
if (advertised) {
|
|
5832
7143
|
map[ext.key] = ext.advertisement ? structuredClone(ext.advertisement) : {};
|
|
5833
7144
|
}
|
|
@@ -6772,6 +8083,14 @@ function exemptMcpEndpoint(getServerUrl, negotiatesEndpoint) {
|
|
|
6772
8083
|
};
|
|
6773
8084
|
}
|
|
6774
8085
|
var REQUEST_DEADLINES = /* @__PURE__ */ new WeakMap();
|
|
8086
|
+
function stampRequestDeadline(init, budget) {
|
|
8087
|
+
if (REQUEST_DEADLINES.has(init)) {
|
|
8088
|
+
throw new Error(
|
|
8089
|
+
"stampRequestDeadline: this RequestInit already carries a deadline \u2014 withOAuthRequestTimeout must build a fresh init per call (#2418)"
|
|
8090
|
+
);
|
|
8091
|
+
}
|
|
8092
|
+
REQUEST_DEADLINES.set(init, budget);
|
|
8093
|
+
}
|
|
6775
8094
|
function requestHeadersOf(input, init) {
|
|
6776
8095
|
if (init?.headers) return new Headers(init.headers);
|
|
6777
8096
|
if (typeof input !== "string" && !(input instanceof URL)) {
|
|
@@ -6829,7 +8148,7 @@ function withOAuthRequestTimeout(fetchFn, timeoutMs = DEFAULT_OAUTH_REQUEST_TIME
|
|
|
6829
8148
|
}
|
|
6830
8149
|
});
|
|
6831
8150
|
const nextInit = { ...init, signal };
|
|
6832
|
-
|
|
8151
|
+
stampRequestDeadline(nextInit, budget);
|
|
6833
8152
|
let settled;
|
|
6834
8153
|
let drainReader;
|
|
6835
8154
|
try {
|
|
@@ -6942,7 +8261,7 @@ function withRfc8414OidcCompat(fetchFn) {
|
|
|
6942
8261
|
return response;
|
|
6943
8262
|
}
|
|
6944
8263
|
if (!probe.ok) {
|
|
6945
|
-
|
|
8264
|
+
releaseBodyDetached(probe);
|
|
6946
8265
|
if (callerSignal?.aborted) {
|
|
6947
8266
|
releaseBodyDetached(response);
|
|
6948
8267
|
throw callerSignal.reason;
|
|
@@ -6966,7 +8285,7 @@ function withRfc8414OidcCompat(fetchFn) {
|
|
|
6966
8285
|
return response;
|
|
6967
8286
|
}
|
|
6968
8287
|
const source = probe.url || candidate;
|
|
6969
|
-
|
|
8288
|
+
releaseBodyDetached(response);
|
|
6970
8289
|
console.warn(
|
|
6971
8290
|
`[oauth] ${source} returned RFC 8414 OAuth 2.0 authorization server metadata, not an OpenID provider document. The MCP TypeScript SDK validates that path as OpenID Connect Discovery and would reject it (modelcontextprotocol/typescript-sdk#2733), so the Inspector is handing it to discovery as the RFC 8414 document it is.`
|
|
6972
8291
|
);
|
|
@@ -7926,7 +9245,7 @@ async function discoverResourceAsMetadata(resourceAsUrl, fetchFn) {
|
|
|
7926
9245
|
|
|
7927
9246
|
// ../../core/auth/ema/wire.ts
|
|
7928
9247
|
import { discoverAndRequestJwtAuthGrant } from "@modelcontextprotocol/client";
|
|
7929
|
-
import { OAuthTokensSchema as
|
|
9248
|
+
import { OAuthTokensSchema as OAuthTokensSchema5 } from "@modelcontextprotocol/core";
|
|
7930
9249
|
async function exchangeIdJag(params) {
|
|
7931
9250
|
const issuer = normalizeIdpIssuer(params.idp.issuer);
|
|
7932
9251
|
const resource = params.resource.trim();
|
|
@@ -8005,7 +9324,7 @@ async function redeemIdJagForAccessToken(params) {
|
|
|
8005
9324
|
"EMA leg 3 (resource AS JWT bearer grant)"
|
|
8006
9325
|
);
|
|
8007
9326
|
}
|
|
8008
|
-
return
|
|
9327
|
+
return OAuthTokensSchema5.parse(await response.json());
|
|
8009
9328
|
}
|
|
8010
9329
|
|
|
8011
9330
|
// ../../core/auth/ema/emaFlow.ts
|
|
@@ -9230,6 +10549,14 @@ function progressTokenOf(message) {
|
|
|
9230
10549
|
}
|
|
9231
10550
|
return void 0;
|
|
9232
10551
|
}
|
|
10552
|
+
function waitForProgressToken(waits, token) {
|
|
10553
|
+
const exact = waits.get(token);
|
|
10554
|
+
if (exact !== void 0 || typeof token === "number") {
|
|
10555
|
+
return exact;
|
|
10556
|
+
}
|
|
10557
|
+
const numeric = Number(token);
|
|
10558
|
+
return Number.isSafeInteger(numeric) ? waits.get(numeric) : void 0;
|
|
10559
|
+
}
|
|
9233
10560
|
|
|
9234
10561
|
// ../../core/mcp/remote/remoteClientTransport.ts
|
|
9235
10562
|
var DEFAULT_SSE_RESPONSE_TIMEOUT_MS = 6e4;
|
|
@@ -9706,8 +11033,8 @@ var RemoteClientTransport = class {
|
|
|
9706
11033
|
});
|
|
9707
11034
|
}
|
|
9708
11035
|
/** Re-arm a pending request's SSE wait timeout on a matching progress note. */
|
|
9709
|
-
resetSseResponseWait(
|
|
9710
|
-
this.sseResponseWaits
|
|
11036
|
+
resetSseResponseWait(progressToken) {
|
|
11037
|
+
waitForProgressToken(this.sseResponseWaits, progressToken)?.resetTimeout();
|
|
9711
11038
|
}
|
|
9712
11039
|
async postSend(message, options, retryCount) {
|
|
9713
11040
|
const requestId = requestIdForMessage(message);
|
|
@@ -9912,6 +11239,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
9912
11239
|
requestTimeout;
|
|
9913
11240
|
defaultMetadata;
|
|
9914
11241
|
serverSettings;
|
|
11242
|
+
// The settings the current transport was built from. `serverSettings` is
|
|
11243
|
+
// replaced live on every settings save (#1444), but transport-level inputs
|
|
11244
|
+
// such as custom headers are fixed when the transport is created, so this
|
|
11245
|
+
// is what the open connection actually sends (#2460).
|
|
11246
|
+
transportSettings;
|
|
9915
11247
|
versionNegotiation;
|
|
9916
11248
|
status = "disconnected";
|
|
9917
11249
|
// True only while an explicit disconnect() owns the teardown. close() can
|
|
@@ -10266,6 +11598,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
10266
11598
|
const advertisedExtensions = buildClientExtensions({
|
|
10267
11599
|
enterpriseManaged: options.oauth?.enterpriseManaged ?? false,
|
|
10268
11600
|
advertised: this.advertisedExtensions,
|
|
11601
|
+
// Only a client that can render Apps claims the UI extension by default
|
|
11602
|
+
// (#2403). An app-elicitation renderer is itself a claim to host an App,
|
|
11603
|
+
// so supplying one implies it.
|
|
11604
|
+
rendersApps: options.rendersApps === true || this.appElicitationRenderer !== void 0,
|
|
10269
11605
|
// Read off the built `capabilities.elicitation.form` rather than
|
|
10270
11606
|
// re-deriving from `options.elicit`: the nested MCP Apps `elicitation`
|
|
10271
11607
|
// setting must never be advertised without the core form capability it
|
|
@@ -10691,6 +12027,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
10691
12027
|
}
|
|
10692
12028
|
this.baseTransport = null;
|
|
10693
12029
|
this.transport = null;
|
|
12030
|
+
this.transportSettings = void 0;
|
|
10694
12031
|
this.transportHasAuthProvider = false;
|
|
10695
12032
|
}
|
|
10696
12033
|
/**
|
|
@@ -11164,6 +12501,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
11164
12501
|
transportOptions
|
|
11165
12502
|
);
|
|
11166
12503
|
this.baseTransport = baseTransport;
|
|
12504
|
+
this.transportSettings = transportOptions.settings;
|
|
11167
12505
|
if (this.directAuthRecovery) {
|
|
11168
12506
|
this.directAuthRecoveryActive = !(baseTransport instanceof RemoteClientTransport);
|
|
11169
12507
|
}
|
|
@@ -11365,6 +12703,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
11365
12703
|
}
|
|
11366
12704
|
this.baseTransport = null;
|
|
11367
12705
|
this.transport = null;
|
|
12706
|
+
this.transportSettings = void 0;
|
|
11368
12707
|
this.transportHasAuthProvider = false;
|
|
11369
12708
|
this.clearPendingPeerRequests();
|
|
11370
12709
|
if (this.status !== "disconnected") {
|
|
@@ -11521,7 +12860,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
11521
12860
|
...params,
|
|
11522
12861
|
_meta: {
|
|
11523
12862
|
...existingMeta,
|
|
11524
|
-
[
|
|
12863
|
+
[PROTOCOL_VERSION_META_KEY2]: protocolVersion,
|
|
11525
12864
|
[CLIENT_INFO_META_KEY]: this.clientInfo,
|
|
11526
12865
|
[CLIENT_CAPABILITIES_META_KEY]: clientCapabilities
|
|
11527
12866
|
}
|
|
@@ -12097,6 +13436,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
12097
13436
|
getClientCapabilities() {
|
|
12098
13437
|
return this.clientCapabilities;
|
|
12099
13438
|
}
|
|
13439
|
+
/**
|
|
13440
|
+
* Get the client identity (name, version) this client reports to servers —
|
|
13441
|
+
* the caller's `clientIdentity`, or core's neutral fallback without one.
|
|
13442
|
+
*/
|
|
13443
|
+
getClientInfo() {
|
|
13444
|
+
return this.clientInfo;
|
|
13445
|
+
}
|
|
12100
13446
|
/**
|
|
12101
13447
|
* Get server info (name, version)
|
|
12102
13448
|
*/
|
|
@@ -12148,6 +13494,16 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
12148
13494
|
getServerSettings() {
|
|
12149
13495
|
return this.serverSettings;
|
|
12150
13496
|
}
|
|
13497
|
+
/**
|
|
13498
|
+
* The settings the current transport was built from, or `undefined` when no
|
|
13499
|
+
* transport exists. Unlike {@link getServerSettings}, a live settings edit
|
|
13500
|
+
* does not change this: transport-level inputs (custom headers) apply only
|
|
13501
|
+
* when the next transport is created, so comparing the two tells a caller
|
|
13502
|
+
* whether an edit is still waiting on a reconnect (#2460).
|
|
13503
|
+
*/
|
|
13504
|
+
getTransportSettings() {
|
|
13505
|
+
return this.transportSettings;
|
|
13506
|
+
}
|
|
12151
13507
|
/**
|
|
12152
13508
|
* Replace the in-memory per-server settings on a live client. Lets a settings
|
|
12153
13509
|
* edit (e.g. toggling auto-refresh-on-list-changed) take effect on the
|
|
@@ -13669,10 +15025,9 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
13669
15025
|
* removes it before the result gets here (#2373).
|
|
13670
15026
|
*
|
|
13671
15027
|
* Separate from {@link getSkill} because the callers differ: the UIs want the
|
|
13672
|
-
* entry, while the CLI prints the result and must not reshape it.
|
|
13673
|
-
*
|
|
13674
|
-
*
|
|
13675
|
-
* those (Copilot).
|
|
15028
|
+
* entry, while the CLI prints the result and must not reshape it. A modern
|
|
15029
|
+
* result carries `ttlMs` / `cacheScope` (#2404), and unwrapping to the entry
|
|
15030
|
+
* discards exactly those (Copilot).
|
|
13676
15031
|
*/
|
|
13677
15032
|
async getSkillResult(uri, metadata) {
|
|
13678
15033
|
if (!this.client) {
|
|
@@ -13683,11 +15038,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
13683
15038
|
uri,
|
|
13684
15039
|
...effectiveMeta ? { _meta: effectiveMeta } : {}
|
|
13685
15040
|
};
|
|
15041
|
+
const resultSchema = this.isModernEra() ? ModernGetSkillEnvelopeSchema : GetSkillEnvelopeSchema;
|
|
13686
15042
|
try {
|
|
13687
15043
|
return await this.invokeMcpClient(
|
|
13688
15044
|
() => this.client.request(
|
|
13689
15045
|
{ method: SKILLS_GET_METHOD, params },
|
|
13690
|
-
|
|
15046
|
+
resultSchema,
|
|
13691
15047
|
this.getRequestOptions(this.progressTokenOf(metadata))
|
|
13692
15048
|
),
|
|
13693
15049
|
{ method: SKILLS_GET_METHOD }
|
|
@@ -14838,9 +16194,74 @@ function isLoopbackHost(host) {
|
|
|
14838
16194
|
return h === "localhost" || h === "[::1]" || /^127(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/.test(h);
|
|
14839
16195
|
}
|
|
14840
16196
|
|
|
14841
|
-
//
|
|
14842
|
-
|
|
14843
|
-
|
|
16197
|
+
// ../../core/auth/issuerBinding.ts
|
|
16198
|
+
import { AuthorizationServerMismatchError } from "@modelcontextprotocol/client";
|
|
16199
|
+
var MISSING_DISCOVERY_STATE_PHRASE = "discoveryState was not available";
|
|
16200
|
+
function isAuthorizationServerMismatchShape(err) {
|
|
16201
|
+
if (err === null || typeof err !== "object") {
|
|
16202
|
+
return false;
|
|
16203
|
+
}
|
|
16204
|
+
const candidate = err;
|
|
16205
|
+
if (typeof candidate.recordedIssuer !== "string" || typeof candidate.currentIssuer !== "string") {
|
|
16206
|
+
return false;
|
|
16207
|
+
}
|
|
16208
|
+
return AuthorizationServerMismatchError.isInstance(err) || candidate.name === "AuthorizationServerMismatchError";
|
|
16209
|
+
}
|
|
16210
|
+
function isMissingDiscoveryStatePlaceholder(recordedIssuer) {
|
|
16211
|
+
if (recordedIssuer.includes(MISSING_DISCOVERY_STATE_PHRASE)) {
|
|
16212
|
+
return true;
|
|
16213
|
+
}
|
|
16214
|
+
try {
|
|
16215
|
+
const url = new URL(recordedIssuer);
|
|
16216
|
+
return url.protocol !== "http:" && url.protocol !== "https:";
|
|
16217
|
+
} catch {
|
|
16218
|
+
return true;
|
|
16219
|
+
}
|
|
16220
|
+
}
|
|
16221
|
+
function findIssuerBindingFailure(err) {
|
|
16222
|
+
return findIssuerBindingFailureDeep(err, /* @__PURE__ */ new Set());
|
|
16223
|
+
}
|
|
16224
|
+
function findIssuerBindingFailureDeep(err, seen) {
|
|
16225
|
+
if (err === null || typeof err !== "object" || seen.has(err)) {
|
|
16226
|
+
return void 0;
|
|
16227
|
+
}
|
|
16228
|
+
seen.add(err);
|
|
16229
|
+
if (isAuthorizationServerMismatchShape(err)) {
|
|
16230
|
+
return classifyAuthorizationServerMismatch(err);
|
|
16231
|
+
}
|
|
16232
|
+
const nested = findIssuerBindingFailureDeep(
|
|
16233
|
+
err.cause,
|
|
16234
|
+
seen
|
|
16235
|
+
);
|
|
16236
|
+
if (nested) {
|
|
16237
|
+
return nested;
|
|
16238
|
+
}
|
|
16239
|
+
const data = err.data;
|
|
16240
|
+
if (data !== null && typeof data === "object") {
|
|
16241
|
+
return findIssuerBindingFailureDeep(
|
|
16242
|
+
data.cause,
|
|
16243
|
+
seen
|
|
16244
|
+
);
|
|
16245
|
+
}
|
|
16246
|
+
return void 0;
|
|
16247
|
+
}
|
|
16248
|
+
function classifyAuthorizationServerMismatch(err) {
|
|
16249
|
+
if (isMissingDiscoveryStatePlaceholder(err.recordedIssuer)) {
|
|
16250
|
+
return {
|
|
16251
|
+
kind: "lost_authorization_state",
|
|
16252
|
+
currentIssuer: err.currentIssuer
|
|
16253
|
+
};
|
|
16254
|
+
}
|
|
16255
|
+
return {
|
|
16256
|
+
kind: "issuer_mismatch",
|
|
16257
|
+
recordedIssuer: err.recordedIssuer,
|
|
16258
|
+
currentIssuer: err.currentIssuer
|
|
16259
|
+
};
|
|
16260
|
+
}
|
|
16261
|
+
|
|
16262
|
+
// src/error-handler.ts
|
|
16263
|
+
var EXIT_CODES = {
|
|
16264
|
+
OK: 0,
|
|
14844
16265
|
USAGE: 1,
|
|
14845
16266
|
NO_APP: 2,
|
|
14846
16267
|
AUTH_REQUIRED: 3,
|
|
@@ -14869,7 +16290,20 @@ var EXIT_CODES = {
|
|
|
14869
16290
|
* job that wants to tolerate oversized catalogs can allow 8 and still fail
|
|
14870
16291
|
* on 7.
|
|
14871
16292
|
*/
|
|
14872
|
-
SKILL_INCOMPLETE: 8
|
|
16293
|
+
SKILL_INCOMPLETE: 8,
|
|
16294
|
+
/**
|
|
16295
|
+
* `--verify --require-digests` found a skill whose `resources` is
|
|
16296
|
+
* `"dynamic"`, so no digest was advertised and nothing was hashed (#2405).
|
|
16297
|
+
*
|
|
16298
|
+
* Only ever produced under `--require-digests`: `"dynamic"` is a conforming
|
|
16299
|
+
* wire form, and SEP-2640 leaves declining such skills to the host ("Hosts
|
|
16300
|
+
* MAY decline to load such skills"). The flag is how a CI job standing in for
|
|
16301
|
+
* a host that declines them says so; without it the run exits 0 and reports
|
|
16302
|
+
* `outcome: "unverifiable"`. Its own code, like 8, so a job can tell "no
|
|
16303
|
+
* digests to check" apart from "a digest was wrong" and "the walk was cut
|
|
16304
|
+
* short".
|
|
16305
|
+
*/
|
|
16306
|
+
SKILL_UNVERIFIABLE: 9
|
|
14873
16307
|
};
|
|
14874
16308
|
var CliExitCodeError = class extends Error {
|
|
14875
16309
|
constructor(exitCode, message, envelope) {
|
|
@@ -14904,7 +16338,44 @@ function statusOf(error) {
|
|
|
14904
16338
|
return void 0;
|
|
14905
16339
|
}
|
|
14906
16340
|
var UNREACHABLE_PATTERN = /ENOTFOUND|ECONNREFUSED|ECONNRESET|EAI_AGAIN|ETIMEDOUT|fetch failed|getaddrinfo|connect(?:ion)? timed out|aborted/i;
|
|
16341
|
+
var EMBEDDED_URL_PATTERN = /\bhttps?:\/\/(?:(?!https?:\/\/)[^\s"<>])+/gi;
|
|
16342
|
+
var TRAILING_PUNCTUATION = /* @__PURE__ */ new Set([
|
|
16343
|
+
".",
|
|
16344
|
+
",",
|
|
16345
|
+
";",
|
|
16346
|
+
":",
|
|
16347
|
+
"!",
|
|
16348
|
+
"?",
|
|
16349
|
+
")",
|
|
16350
|
+
"]",
|
|
16351
|
+
"'"
|
|
16352
|
+
]);
|
|
16353
|
+
function trailingPunctuationStart(match) {
|
|
16354
|
+
let end = match.length;
|
|
16355
|
+
while (end > 0 && TRAILING_PUNCTUATION.has(match.charAt(end - 1))) end--;
|
|
16356
|
+
return end;
|
|
16357
|
+
}
|
|
16358
|
+
function redactUrlsInText(text) {
|
|
16359
|
+
return text.replace(EMBEDDED_URL_PATTERN, (match) => {
|
|
16360
|
+
const end = trailingPunctuationStart(match);
|
|
16361
|
+
return redactUrlQuery(match.slice(0, end)) + match.slice(end);
|
|
16362
|
+
});
|
|
16363
|
+
}
|
|
16364
|
+
function redactEnvelope(envelope) {
|
|
16365
|
+
return {
|
|
16366
|
+
...envelope,
|
|
16367
|
+
message: redactUrlsInText(envelope.message),
|
|
16368
|
+
...envelope.cause !== void 0 && {
|
|
16369
|
+
cause: redactUrlsInText(envelope.cause)
|
|
16370
|
+
},
|
|
16371
|
+
...envelope.url !== void 0 && { url: redactUrlQuery(envelope.url) }
|
|
16372
|
+
};
|
|
16373
|
+
}
|
|
14907
16374
|
function classifyError(error, context) {
|
|
16375
|
+
const { exitCode, envelope } = classifyUnredacted(error, context);
|
|
16376
|
+
return { exitCode, envelope: redactEnvelope(envelope) };
|
|
16377
|
+
}
|
|
16378
|
+
function classifyUnredacted(error, context) {
|
|
14908
16379
|
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
14909
16380
|
const cause = causeOf(error);
|
|
14910
16381
|
const status = statusOf(error);
|
|
@@ -14925,9 +16396,29 @@ function classifyError(error, context) {
|
|
|
14925
16396
|
}
|
|
14926
16397
|
};
|
|
14927
16398
|
}
|
|
14928
|
-
if (
|
|
14929
|
-
|
|
14930
|
-
|
|
16399
|
+
if (error instanceof SecretStoreUnavailableError) {
|
|
16400
|
+
return {
|
|
16401
|
+
exitCode: EXIT_CODES.USAGE,
|
|
16402
|
+
envelope: {
|
|
16403
|
+
code: "store_unavailable",
|
|
16404
|
+
message,
|
|
16405
|
+
...cause !== void 0 && { cause },
|
|
16406
|
+
...url !== void 0 && { url }
|
|
16407
|
+
}
|
|
16408
|
+
};
|
|
16409
|
+
}
|
|
16410
|
+
if (error instanceof OAuthStateFileUnrecognizedError) {
|
|
16411
|
+
return {
|
|
16412
|
+
exitCode: EXIT_CODES.USAGE,
|
|
16413
|
+
envelope: {
|
|
16414
|
+
code: "oauth_state_unrecognized",
|
|
16415
|
+
message,
|
|
16416
|
+
...cause !== void 0 && { cause },
|
|
16417
|
+
...url !== void 0 && { url }
|
|
16418
|
+
}
|
|
16419
|
+
};
|
|
16420
|
+
}
|
|
16421
|
+
if (status === 401 || status === 403 || isUnauthorizedError(error)) {
|
|
14931
16422
|
return {
|
|
14932
16423
|
exitCode: EXIT_CODES.AUTH_REQUIRED,
|
|
14933
16424
|
envelope: {
|
|
@@ -14998,7 +16489,7 @@ var BOOLEAN_VALUED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
14998
16489
|
"additionalItems",
|
|
14999
16490
|
"unevaluatedItems"
|
|
15000
16491
|
]);
|
|
15001
|
-
var
|
|
16492
|
+
var SUBSCHEMA_KEYWORDS2 = [
|
|
15002
16493
|
"items",
|
|
15003
16494
|
"contains",
|
|
15004
16495
|
"not",
|
|
@@ -15015,13 +16506,13 @@ var SUBSCHEMA_KEYWORDS = [
|
|
|
15015
16506
|
// real finding.
|
|
15016
16507
|
"contentSchema"
|
|
15017
16508
|
];
|
|
15018
|
-
var
|
|
16509
|
+
var SUBSCHEMA_ARRAY_KEYWORDS2 = [
|
|
15019
16510
|
"allOf",
|
|
15020
16511
|
"anyOf",
|
|
15021
16512
|
"oneOf",
|
|
15022
16513
|
"prefixItems"
|
|
15023
16514
|
];
|
|
15024
|
-
var
|
|
16515
|
+
var SUBSCHEMA_MAP_KEYWORDS2 = [
|
|
15025
16516
|
"properties",
|
|
15026
16517
|
"patternProperties",
|
|
15027
16518
|
"dependentSchemas",
|
|
@@ -15103,8 +16594,8 @@ function isPortableTypeUnion(type) {
|
|
|
15103
16594
|
}
|
|
15104
16595
|
return new Set(type).size === type.length;
|
|
15105
16596
|
}
|
|
15106
|
-
var
|
|
15107
|
-
function
|
|
16597
|
+
var MAX_DEPTH2 = 64;
|
|
16598
|
+
function isRecord5(value) {
|
|
15108
16599
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15109
16600
|
}
|
|
15110
16601
|
function childPath(parent, key) {
|
|
@@ -15120,7 +16611,7 @@ function describeSchemaPath(schema, path5) {
|
|
|
15120
16611
|
return path5 === "" ? schema : `${schema}.${path5}`;
|
|
15121
16612
|
}
|
|
15122
16613
|
function declaresAnyId(node, depth = 0) {
|
|
15123
|
-
if (depth >
|
|
16614
|
+
if (depth > MAX_DEPTH2 || !isRecord5(node)) return false;
|
|
15124
16615
|
if (typeof node.$id === "string" && node.$id !== "") return true;
|
|
15125
16616
|
let found = false;
|
|
15126
16617
|
forEachSubschema(node, "", (child) => {
|
|
@@ -15139,21 +16630,21 @@ function add(ctx, rule, severity, path5, issue, suggestion) {
|
|
|
15139
16630
|
});
|
|
15140
16631
|
}
|
|
15141
16632
|
function forEachSubschema(node, path5, visit) {
|
|
15142
|
-
for (const key of
|
|
16633
|
+
for (const key of SUBSCHEMA_MAP_KEYWORDS2) {
|
|
15143
16634
|
const map = node[key];
|
|
15144
|
-
if (!
|
|
16635
|
+
if (!isRecord5(map)) continue;
|
|
15145
16636
|
for (const [name, sub] of Object.entries(map)) {
|
|
15146
16637
|
visit(sub, childPath(childPath(path5, key), name), key);
|
|
15147
16638
|
}
|
|
15148
16639
|
}
|
|
15149
|
-
for (const key of
|
|
16640
|
+
for (const key of SUBSCHEMA_ARRAY_KEYWORDS2) {
|
|
15150
16641
|
const arr = node[key];
|
|
15151
16642
|
if (!Array.isArray(arr)) continue;
|
|
15152
16643
|
arr.forEach((sub, i) => {
|
|
15153
16644
|
visit(sub, indexPath(childPath(path5, key), i), key);
|
|
15154
16645
|
});
|
|
15155
16646
|
}
|
|
15156
|
-
for (const key of
|
|
16647
|
+
for (const key of SUBSCHEMA_KEYWORDS2) {
|
|
15157
16648
|
if (!(key in node)) continue;
|
|
15158
16649
|
const value = node[key];
|
|
15159
16650
|
if (key === "items" && Array.isArray(value)) {
|
|
@@ -15172,7 +16663,7 @@ function booleanTrueSuggestion(keyword) {
|
|
|
15172
16663
|
return 'Declare what the value actually is \u2014 e.g. `{"type": "object", "additionalProperties": true}` for a free-form object. That is a deliberate change of contract, not an equivalent rewrite: `true` accepts any JSON value at all.';
|
|
15173
16664
|
}
|
|
15174
16665
|
function walk(node, path5, keyword, depth, ctx) {
|
|
15175
|
-
if (depth >
|
|
16666
|
+
if (depth > MAX_DEPTH2) return;
|
|
15176
16667
|
if (typeof node === "boolean") {
|
|
15177
16668
|
if (keyword !== void 0 && BOOLEAN_VALUED_KEYWORDS.has(keyword)) return;
|
|
15178
16669
|
add(
|
|
@@ -15191,12 +16682,13 @@ function walk(node, path5, keyword, depth, ctx) {
|
|
|
15191
16682
|
);
|
|
15192
16683
|
return;
|
|
15193
16684
|
}
|
|
15194
|
-
if (!
|
|
16685
|
+
if (!isRecord5(node)) return;
|
|
15195
16686
|
lintNode(node, path5, keyword, ctx);
|
|
15196
16687
|
forEachSubschema(node, path5, (child, childPathValue, childKeyword) => {
|
|
15197
16688
|
walk(child, childPathValue, childKeyword, depth + 1, ctx);
|
|
15198
16689
|
});
|
|
15199
16690
|
}
|
|
16691
|
+
var NULL_BRANCH_CAVEAT = " A dialect with no `null` type, such as OpenAPI 3.0, still cannot express the `null` branch directly; there nullability is written `nullable: true`, which is not JSON Schema, so no single spelling is portable to both.";
|
|
15200
16692
|
function lintNode(node, path5, keyword, ctx) {
|
|
15201
16693
|
const type = node.type;
|
|
15202
16694
|
if (Array.isArray(type) && isPortableTypeUnion(type)) {
|
|
@@ -15205,10 +16697,10 @@ function lintNode(node, path5, keyword, ctx) {
|
|
|
15205
16697
|
"type-union",
|
|
15206
16698
|
"warning",
|
|
15207
16699
|
path5,
|
|
15208
|
-
`\`type\` is an array (${JSON.stringify(type)}).
|
|
16700
|
+
`\`type\` is an array (${JSON.stringify(type)}). This is legal JSON Schema, and some model providers recommend it for nullable fields, but it is less portable: a client that maps tool schemas onto a single-\`type\` dialect (such as the OpenAPI subset used for Gemini function declarations) may reject the tool or drop the constraint.`,
|
|
15209
16701
|
`Split it into \`anyOf\` branches, each with a single \`type\` \u2014 \`{"anyOf": [${type.map((t) => `{"type": "${t}"}`).join(
|
|
15210
16702
|
", "
|
|
15211
|
-
)}]}\`. (Making the property optional instead is a different contract: absent is not the same as \`null\`.)`
|
|
16703
|
+
)}]}\`. (Making the property optional instead is a different contract: absent is not the same as \`null\`.)${type.includes("null") ? NULL_BRANCH_CAVEAT : ""}`
|
|
15212
16704
|
);
|
|
15213
16705
|
}
|
|
15214
16706
|
const ref = node.$ref;
|
|
@@ -15368,6 +16860,9 @@ async function emitResult(result, appInfo, args) {
|
|
|
15368
16860
|
}
|
|
15369
16861
|
|
|
15370
16862
|
// src/handlers/consume-outcome.ts
|
|
16863
|
+
function isBrokenPipe(err) {
|
|
16864
|
+
return err instanceof Error && err.code === "EPIPE";
|
|
16865
|
+
}
|
|
15371
16866
|
async function consumeMethodOutcome(outcome, args) {
|
|
15372
16867
|
if (outcome.kind === "result") {
|
|
15373
16868
|
await emitResult(outcome.result, outcome.appInfo, args);
|
|
@@ -15383,23 +16878,37 @@ async function consumeMethodOutcome(outcome, args) {
|
|
|
15383
16878
|
throw new CliExitCodeError(outcome.exitCode, outcome.summary ?? "", {
|
|
15384
16879
|
// The envelope's `code` follows the exit code, so a caller reading one
|
|
15385
16880
|
// never has to reconcile it against the other.
|
|
15386
|
-
code: outcome.exitCode === EXIT_CODES.SKILL_INCOMPLETE ? "skills_incomplete" : "skills_nonconformant"
|
|
16881
|
+
code: outcome.exitCode === EXIT_CODES.SKILL_INCOMPLETE ? "skills_incomplete" : outcome.exitCode === EXIT_CODES.SKILL_UNVERIFIABLE ? "skills_unverifiable" : "skills_nonconformant"
|
|
15387
16882
|
});
|
|
15388
16883
|
}
|
|
15389
16884
|
return;
|
|
15390
16885
|
}
|
|
15391
|
-
await new Promise((resolve7) => {
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
});
|
|
15395
|
-
const onSignal = () => {
|
|
15396
|
-
stop();
|
|
16886
|
+
await new Promise((resolve7, reject) => {
|
|
16887
|
+
let stop;
|
|
16888
|
+
const detach = () => {
|
|
15397
16889
|
process.off("SIGINT", onSignal);
|
|
15398
16890
|
process.off("SIGTERM", onSignal);
|
|
15399
|
-
|
|
16891
|
+
process.stdout.off("error", onStdoutError);
|
|
16892
|
+
};
|
|
16893
|
+
const finish = (err) => {
|
|
16894
|
+
detach();
|
|
16895
|
+
stop?.();
|
|
16896
|
+
if (err === void 0) resolve7();
|
|
16897
|
+
else reject(err);
|
|
15400
16898
|
};
|
|
16899
|
+
const onSignal = () => finish();
|
|
16900
|
+
const onStdoutError = (err) => finish(isBrokenPipe(err) ? void 0 : err);
|
|
16901
|
+
process.stdout.on("error", onStdoutError);
|
|
15401
16902
|
process.on("SIGINT", onSignal);
|
|
15402
16903
|
process.on("SIGTERM", onSignal);
|
|
16904
|
+
try {
|
|
16905
|
+
stop = outcome.start((obj) => {
|
|
16906
|
+
void awaitableLog(JSON.stringify(obj) + "\n");
|
|
16907
|
+
});
|
|
16908
|
+
} catch (err) {
|
|
16909
|
+
detach();
|
|
16910
|
+
reject(err);
|
|
16911
|
+
}
|
|
15403
16912
|
});
|
|
15404
16913
|
}
|
|
15405
16914
|
|
|
@@ -16220,24 +17729,6 @@ async function collectAppInfo(client, tool, metadata) {
|
|
|
16220
17729
|
}
|
|
16221
17730
|
}
|
|
16222
17731
|
|
|
16223
|
-
// src/handlers/skills-verify.ts
|
|
16224
|
-
function summarizeSkillVerification(reports) {
|
|
16225
|
-
const failed = reports.filter((report) => report.outcome === "failed").length;
|
|
16226
|
-
const incomplete = reports.filter(
|
|
16227
|
-
(report) => report.outcome === "incomplete"
|
|
16228
|
-
).length;
|
|
16229
|
-
const files = reports.reduce((sum, report) => sum + report.files.length, 0);
|
|
16230
|
-
const mismatched = reports.reduce(
|
|
16231
|
-
(sum, report) => sum + report.files.filter((file) => file.status === "mismatch").length,
|
|
16232
|
-
0
|
|
16233
|
-
);
|
|
16234
|
-
const skillWord = reports.length === 1 ? "skill" : "skills";
|
|
16235
|
-
const fileWord = files === 1 ? "file" : "files";
|
|
16236
|
-
const incompleteClause = incomplete === 0 ? "" : ` ${incomplete} of ${reports.length} ${skillWord} could not be fully checked: the read bounds stopped the walk.`;
|
|
16237
|
-
const headline = failed === 0 ? incomplete === 0 ? `Verified ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors.` : `Checked ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors in what was read.` : `${failed} of ${reports.length} ${skillWord} failed verification (${mismatched} digest/size mismatch across ${files} ${fileWord}).`;
|
|
16238
|
-
return `${headline}${incompleteClause}`;
|
|
16239
|
-
}
|
|
16240
|
-
|
|
16241
17732
|
// ../../core/mcp/skillsVerification.ts
|
|
16242
17733
|
function reasonOf(err) {
|
|
16243
17734
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -16430,7 +17921,11 @@ async function verifySkills(client, entries, metadata) {
|
|
|
16430
17921
|
files,
|
|
16431
17922
|
...incomplete ? { incomplete } : {},
|
|
16432
17923
|
ok: !hasError && !fileFailed,
|
|
16433
|
-
|
|
17924
|
+
// Precedence runs from the loudest verdict down: a broken MUST outranks
|
|
17925
|
+
// an unfinished walk, and an unfinished walk outranks a skill that had
|
|
17926
|
+
// nothing to hash — a `"dynamic"` skill past the run's catalog budget
|
|
17927
|
+
// was not read at all, which `incomplete` already says more precisely.
|
|
17928
|
+
outcome: hasError || fileFailed ? "failed" : incomplete !== void 0 ? "incomplete" : entry.resources === DYNAMIC_RESOURCES ? "unverifiable" : "verified"
|
|
16434
17929
|
});
|
|
16435
17930
|
}
|
|
16436
17931
|
return reports;
|
|
@@ -16441,6 +17936,38 @@ function allSkillsVerified(reports) {
|
|
|
16441
17936
|
function anySkillFailed(reports) {
|
|
16442
17937
|
return reports.some((report) => report.outcome === "failed");
|
|
16443
17938
|
}
|
|
17939
|
+
function anySkillUnverifiable(reports) {
|
|
17940
|
+
return reports.some((report) => report.outcome === "unverifiable");
|
|
17941
|
+
}
|
|
17942
|
+
|
|
17943
|
+
// src/handlers/skills-verify.ts
|
|
17944
|
+
function skillVerificationExitCode(reports, requireDigests) {
|
|
17945
|
+
if (allSkillsVerified(reports)) return void 0;
|
|
17946
|
+
if (anySkillFailed(reports)) return EXIT_CODES.SKILL_NONCONFORMANT;
|
|
17947
|
+
if (reports.some((report) => report.outcome === "incomplete"))
|
|
17948
|
+
return EXIT_CODES.SKILL_INCOMPLETE;
|
|
17949
|
+
return requireDigests && anySkillUnverifiable(reports) ? EXIT_CODES.SKILL_UNVERIFIABLE : void 0;
|
|
17950
|
+
}
|
|
17951
|
+
function summarizeSkillVerification(reports) {
|
|
17952
|
+
const failed = reports.filter((report) => report.outcome === "failed").length;
|
|
17953
|
+
const incomplete = reports.filter(
|
|
17954
|
+
(report) => report.outcome === "incomplete"
|
|
17955
|
+
).length;
|
|
17956
|
+
const unverifiable = reports.filter(
|
|
17957
|
+
(report) => report.outcome === "unverifiable"
|
|
17958
|
+
).length;
|
|
17959
|
+
const files = reports.reduce((sum, report) => sum + report.files.length, 0);
|
|
17960
|
+
const mismatched = reports.reduce(
|
|
17961
|
+
(sum, report) => sum + report.files.filter((file) => file.status === "mismatch").length,
|
|
17962
|
+
0
|
|
17963
|
+
);
|
|
17964
|
+
const skillWord = reports.length === 1 ? "skill" : "skills";
|
|
17965
|
+
const fileWord = files === 1 ? "file" : "files";
|
|
17966
|
+
const incompleteClause = incomplete === 0 ? "" : ` ${incomplete} of ${reports.length} ${skillWord} could not be fully checked: the read bounds stopped the walk.`;
|
|
17967
|
+
const unverifiableClause = unverifiable === 0 ? "" : ` ${unverifiable} of ${reports.length} ${skillWord} advertised no digests (resources: "dynamic"), so ${unverifiable === 1 ? "its" : "their"} integrity was not checked.`;
|
|
17968
|
+
const headline = failed !== 0 ? `${failed} of ${reports.length} ${skillWord} failed verification (${mismatched} digest/size mismatch across ${files} ${fileWord}).` : incomplete !== 0 ? `Checked ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors in what was read.` : unverifiable !== 0 ? `Checked ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors.` : `Verified ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors.`;
|
|
17969
|
+
return `${headline}${incompleteClause}${unverifiableClause}`;
|
|
17970
|
+
}
|
|
16444
17971
|
|
|
16445
17972
|
// src/handlers/run-method.ts
|
|
16446
17973
|
function assertSkillsSupported(inspectorClient, method) {
|
|
@@ -16691,13 +18218,10 @@ async function runMethod(inspectorClient, args) {
|
|
|
16691
18218
|
kind: "ndjson",
|
|
16692
18219
|
lines: reports,
|
|
16693
18220
|
summary: summarizeSkillVerification(reports),
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
...allSkillsVerified(reports) ? {} : {
|
|
16699
|
-
exitCode: anySkillFailed(reports) ? EXIT_CODES.SKILL_NONCONFORMANT : EXIT_CODES.SKILL_INCOMPLETE
|
|
16700
|
-
}
|
|
18221
|
+
exitCode: skillVerificationExitCode(
|
|
18222
|
+
reports,
|
|
18223
|
+
args.requireDigests === true
|
|
18224
|
+
)
|
|
16701
18225
|
};
|
|
16702
18226
|
}
|
|
16703
18227
|
result = { skills };
|
|
@@ -16723,13 +18247,10 @@ async function runMethod(inspectorClient, args) {
|
|
|
16723
18247
|
kind: "ndjson",
|
|
16724
18248
|
lines: reports,
|
|
16725
18249
|
summary: summarizeSkillVerification(reports),
|
|
16726
|
-
|
|
16727
|
-
|
|
16728
|
-
|
|
16729
|
-
|
|
16730
|
-
...allSkillsVerified(reports) ? {} : {
|
|
16731
|
-
exitCode: anySkillFailed(reports) ? EXIT_CODES.SKILL_NONCONFORMANT : EXIT_CODES.SKILL_INCOMPLETE
|
|
16732
|
-
}
|
|
18250
|
+
exitCode: skillVerificationExitCode(
|
|
18251
|
+
reports,
|
|
18252
|
+
args.requireDigests === true
|
|
18253
|
+
)
|
|
16733
18254
|
};
|
|
16734
18255
|
}
|
|
16735
18256
|
result = envelope;
|
|
@@ -16811,71 +18332,6 @@ import {
|
|
|
16811
18332
|
discoverAuthorizationServerMetadata as discoverAuthorizationServerMetadata4
|
|
16812
18333
|
} from "@modelcontextprotocol/client";
|
|
16813
18334
|
|
|
16814
|
-
// ../../core/auth/issuerBinding.ts
|
|
16815
|
-
import { AuthorizationServerMismatchError } from "@modelcontextprotocol/client";
|
|
16816
|
-
var MISSING_DISCOVERY_STATE_PHRASE = "discoveryState was not available";
|
|
16817
|
-
function isAuthorizationServerMismatchShape(err) {
|
|
16818
|
-
if (err === null || typeof err !== "object") {
|
|
16819
|
-
return false;
|
|
16820
|
-
}
|
|
16821
|
-
const candidate = err;
|
|
16822
|
-
if (typeof candidate.recordedIssuer !== "string" || typeof candidate.currentIssuer !== "string") {
|
|
16823
|
-
return false;
|
|
16824
|
-
}
|
|
16825
|
-
return AuthorizationServerMismatchError.isInstance(err) || candidate.name === "AuthorizationServerMismatchError";
|
|
16826
|
-
}
|
|
16827
|
-
function isMissingDiscoveryStatePlaceholder(recordedIssuer) {
|
|
16828
|
-
if (recordedIssuer.includes(MISSING_DISCOVERY_STATE_PHRASE)) {
|
|
16829
|
-
return true;
|
|
16830
|
-
}
|
|
16831
|
-
try {
|
|
16832
|
-
const url = new URL(recordedIssuer);
|
|
16833
|
-
return url.protocol !== "http:" && url.protocol !== "https:";
|
|
16834
|
-
} catch {
|
|
16835
|
-
return true;
|
|
16836
|
-
}
|
|
16837
|
-
}
|
|
16838
|
-
function findIssuerBindingFailure(err) {
|
|
16839
|
-
return findIssuerBindingFailureDeep(err, /* @__PURE__ */ new Set());
|
|
16840
|
-
}
|
|
16841
|
-
function findIssuerBindingFailureDeep(err, seen) {
|
|
16842
|
-
if (err === null || typeof err !== "object" || seen.has(err)) {
|
|
16843
|
-
return void 0;
|
|
16844
|
-
}
|
|
16845
|
-
seen.add(err);
|
|
16846
|
-
if (isAuthorizationServerMismatchShape(err)) {
|
|
16847
|
-
return classifyAuthorizationServerMismatch(err);
|
|
16848
|
-
}
|
|
16849
|
-
const nested = findIssuerBindingFailureDeep(
|
|
16850
|
-
err.cause,
|
|
16851
|
-
seen
|
|
16852
|
-
);
|
|
16853
|
-
if (nested) {
|
|
16854
|
-
return nested;
|
|
16855
|
-
}
|
|
16856
|
-
const data = err.data;
|
|
16857
|
-
if (data !== null && typeof data === "object") {
|
|
16858
|
-
return findIssuerBindingFailureDeep(
|
|
16859
|
-
data.cause,
|
|
16860
|
-
seen
|
|
16861
|
-
);
|
|
16862
|
-
}
|
|
16863
|
-
return void 0;
|
|
16864
|
-
}
|
|
16865
|
-
function classifyAuthorizationServerMismatch(err) {
|
|
16866
|
-
if (isMissingDiscoveryStatePlaceholder(err.recordedIssuer)) {
|
|
16867
|
-
return {
|
|
16868
|
-
kind: "lost_authorization_state",
|
|
16869
|
-
currentIssuer: err.currentIssuer
|
|
16870
|
-
};
|
|
16871
|
-
}
|
|
16872
|
-
return {
|
|
16873
|
-
kind: "issuer_mismatch",
|
|
16874
|
-
recordedIssuer: err.recordedIssuer,
|
|
16875
|
-
currentIssuer: err.currentIssuer
|
|
16876
|
-
};
|
|
16877
|
-
}
|
|
16878
|
-
|
|
16879
18335
|
// ../../core/auth/node/oauth-callback-server.ts
|
|
16880
18336
|
import { createServer } from "http";
|
|
16881
18337
|
var DEFAULT_HOSTNAME = "127.0.0.1";
|
|
@@ -17171,8 +18627,28 @@ async function runRunnerInteractiveOAuth(options) {
|
|
|
17171
18627
|
|
|
17172
18628
|
// src/open-url.ts
|
|
17173
18629
|
import open from "open";
|
|
17174
|
-
|
|
17175
|
-
|
|
18630
|
+
var OPEN_URL_TIMEOUT_MS = 5e3;
|
|
18631
|
+
function launched(child) {
|
|
18632
|
+
return new Promise((resolve7, reject) => {
|
|
18633
|
+
child.on("error", reject);
|
|
18634
|
+
child.once("spawn", () => resolve7());
|
|
18635
|
+
});
|
|
18636
|
+
}
|
|
18637
|
+
async function openUrl(url, timeoutMs = OPEN_URL_TIMEOUT_MS) {
|
|
18638
|
+
const href = typeof url === "string" ? url : url.href;
|
|
18639
|
+
let timer;
|
|
18640
|
+
const timeout = new Promise((_, reject) => {
|
|
18641
|
+
timer = setTimeout(
|
|
18642
|
+
() => reject(new Error(`browser did not open within ${timeoutMs / 1e3}s`)),
|
|
18643
|
+
timeoutMs
|
|
18644
|
+
);
|
|
18645
|
+
});
|
|
18646
|
+
try {
|
|
18647
|
+
const opened = Promise.resolve().then(() => open(href)).then(launched);
|
|
18648
|
+
await Promise.race([opened, timeout]);
|
|
18649
|
+
} finally {
|
|
18650
|
+
clearTimeout(timer);
|
|
18651
|
+
}
|
|
17176
18652
|
}
|
|
17177
18653
|
|
|
17178
18654
|
// src/style.ts
|
|
@@ -17238,7 +18714,12 @@ function createCliOAuthNavigation(options = {}) {
|
|
|
17238
18714
|
if (!forced && !tty) return;
|
|
17239
18715
|
try {
|
|
17240
18716
|
await (options.openBrowser ?? openUrl)(href);
|
|
17241
|
-
} catch {
|
|
18717
|
+
} catch (error) {
|
|
18718
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
18719
|
+
write(
|
|
18720
|
+
`Could not open a browser automatically (${reason}). Open the URL above manually.
|
|
18721
|
+
`
|
|
18722
|
+
);
|
|
17242
18723
|
}
|
|
17243
18724
|
});
|
|
17244
18725
|
}
|
|
@@ -17381,6 +18862,23 @@ function mergeSecretsIntoClientConfig(config, secrets) {
|
|
|
17381
18862
|
}
|
|
17382
18863
|
|
|
17383
18864
|
// ../../core/client/node-persistence.ts
|
|
18865
|
+
async function withClientConfigLock(filePath, action, body) {
|
|
18866
|
+
let entered = false;
|
|
18867
|
+
try {
|
|
18868
|
+
return await withSecretFileLock(filePath, async () => {
|
|
18869
|
+
entered = true;
|
|
18870
|
+
return body();
|
|
18871
|
+
});
|
|
18872
|
+
} catch (error) {
|
|
18873
|
+
if (!entered && error instanceof SecretFileLockHeldError) {
|
|
18874
|
+
throw new SecretFileLockHeldError(
|
|
18875
|
+
`Could not ${action} the client configuration: the file at ${filePath} is locked by another Inspector process and did not become available.`,
|
|
18876
|
+
{ cause: error }
|
|
18877
|
+
);
|
|
18878
|
+
}
|
|
18879
|
+
throw error;
|
|
18880
|
+
}
|
|
18881
|
+
}
|
|
17384
18882
|
async function readIdpSecretFromKeychain(secretStore) {
|
|
17385
18883
|
const secret = await secretStore.get(
|
|
17386
18884
|
CLIENT_KEYCHAIN_ID,
|
|
@@ -17390,29 +18888,39 @@ async function readIdpSecretFromKeychain(secretStore) {
|
|
|
17390
18888
|
return { [SECRET_FIELD_IDP_CLIENT_SECRET]: secret };
|
|
17391
18889
|
}
|
|
17392
18890
|
async function migrateClientPlaintextSecret(filePath, config, secretStore) {
|
|
17393
|
-
const { stripped, secrets } = extractSecretsFromClientConfig(config);
|
|
17394
|
-
const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
|
|
17395
|
-
if (!value) return config;
|
|
17396
18891
|
try {
|
|
17397
|
-
|
|
17398
|
-
|
|
17399
|
-
|
|
17400
|
-
|
|
17401
|
-
|
|
17402
|
-
|
|
17403
|
-
|
|
17404
|
-
|
|
17405
|
-
|
|
17406
|
-
|
|
17407
|
-
|
|
17408
|
-
|
|
17409
|
-
|
|
17410
|
-
|
|
17411
|
-
|
|
18892
|
+
return await withClientConfigLock(filePath, "migrate", async () => {
|
|
18893
|
+
const raw = await readStoreFile(filePath);
|
|
18894
|
+
if (raw === null) return {};
|
|
18895
|
+
const fresh = parseClientConfig(parseStore(raw));
|
|
18896
|
+
const { stripped, secrets } = extractSecretsFromClientConfig(fresh);
|
|
18897
|
+
const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
|
|
18898
|
+
if (!value) return fresh;
|
|
18899
|
+
try {
|
|
18900
|
+
const existing = await secretStoreGetStrict(
|
|
18901
|
+
secretStore,
|
|
18902
|
+
CLIENT_KEYCHAIN_ID,
|
|
18903
|
+
SECRET_FIELD_IDP_CLIENT_SECRET
|
|
18904
|
+
);
|
|
18905
|
+
if (existing === null) {
|
|
18906
|
+
await secretStore.set(
|
|
18907
|
+
CLIENT_KEYCHAIN_ID,
|
|
18908
|
+
SECRET_FIELD_IDP_CLIENT_SECRET,
|
|
18909
|
+
value
|
|
18910
|
+
);
|
|
18911
|
+
}
|
|
18912
|
+
if (!await secretStoreIsDurable(secretStore)) return fresh;
|
|
18913
|
+
await writeStoreFile(filePath, serializeStore(stripped));
|
|
18914
|
+
return stripped;
|
|
18915
|
+
} catch (err) {
|
|
18916
|
+
if (err instanceof SecretStoreUnavailableError) {
|
|
18917
|
+
return fresh;
|
|
18918
|
+
}
|
|
18919
|
+
throw err;
|
|
18920
|
+
}
|
|
18921
|
+
});
|
|
17412
18922
|
} catch (err) {
|
|
17413
|
-
if (err instanceof
|
|
17414
|
-
return config;
|
|
17415
|
-
}
|
|
18923
|
+
if (err instanceof SecretFileLockHeldError) return config;
|
|
17416
18924
|
throw err;
|
|
17417
18925
|
}
|
|
17418
18926
|
}
|
|
@@ -17764,7 +19272,7 @@ async function withCliAuthRecoveryRetry(inspectorClient, serverConfig, redirectU
|
|
|
17764
19272
|
import { LoggingLevelSchema } from "@modelcontextprotocol/core";
|
|
17765
19273
|
|
|
17766
19274
|
// ../../core/node/version.ts
|
|
17767
|
-
import { readFileSync as
|
|
19275
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
17768
19276
|
import { dirname as dirname5, join as join3, parse } from "path";
|
|
17769
19277
|
import { fileURLToPath } from "url";
|
|
17770
19278
|
var ROOT_PACKAGE_NAME = "@modelcontextprotocol/inspector";
|
|
@@ -17776,7 +19284,7 @@ function readInspectorVersion(callerUrl) {
|
|
|
17776
19284
|
let manifest;
|
|
17777
19285
|
try {
|
|
17778
19286
|
manifest = JSON.parse(
|
|
17779
|
-
|
|
19287
|
+
readFileSync4(join3(dir, "package.json"), "utf-8")
|
|
17780
19288
|
);
|
|
17781
19289
|
} catch {
|
|
17782
19290
|
}
|
|
@@ -17875,6 +19383,13 @@ async function callMethod(serverConfig, serverSettings, args, clientConfig, cliA
|
|
|
17875
19383
|
...serverSettings?.protocolEra && {
|
|
17876
19384
|
versionNegotiation: eraToVersionNegotiation(serverSettings.protocolEra)
|
|
17877
19385
|
},
|
|
19386
|
+
// The CLI cannot render an MCP App, so it does not advertise the UI
|
|
19387
|
+
// extension by default (#2403). `--advertise-apps` claims it explicitly,
|
|
19388
|
+
// for a server that only exposes its App tools to a client that does —
|
|
19389
|
+
// which is what an `--app-info` probe against such a server needs.
|
|
19390
|
+
...args.advertiseApps && {
|
|
19391
|
+
advertisedExtensions: { [UI_EXTENSION_KEY]: true }
|
|
19392
|
+
},
|
|
17878
19393
|
...clientAuthOptions
|
|
17879
19394
|
});
|
|
17880
19395
|
try {
|
|
@@ -17908,14 +19423,8 @@ function normalizeServerUrl2(serverUrl) {
|
|
|
17908
19423
|
}
|
|
17909
19424
|
}
|
|
17910
19425
|
async function readOAuthSnapshot(statePath) {
|
|
17911
|
-
const
|
|
17912
|
-
|
|
17913
|
-
const text = await readFile2(statePath, "utf8");
|
|
17914
|
-
const snapshot = parseOAuthPersistBlob(text);
|
|
17915
|
-
if (snapshot) return snapshot;
|
|
17916
|
-
} catch {
|
|
17917
|
-
}
|
|
17918
|
-
return { servers: {}, idpSessions: {} };
|
|
19426
|
+
const snapshot = await readOAuthStore(statePath);
|
|
19427
|
+
return snapshot ?? { servers: {}, idpSessions: {} };
|
|
17919
19428
|
}
|
|
17920
19429
|
async function readOAuthServers(statePath) {
|
|
17921
19430
|
return (await readOAuthSnapshot(statePath)).servers;
|
|
@@ -17990,17 +19499,44 @@ async function refreshStoredAuthToken(serverUrl, statePath, deps = {}) {
|
|
|
17990
19499
|
);
|
|
17991
19500
|
}
|
|
17992
19501
|
servers[found.key] = { ...found.state, tokens };
|
|
17993
|
-
await
|
|
19502
|
+
await writeOAuthSections(statePath, snapshot, { servers: [found.key] });
|
|
17994
19503
|
return tokens.access_token;
|
|
17995
19504
|
}
|
|
17996
|
-
|
|
19505
|
+
var DEADLINE_ELAPSED = /* @__PURE__ */ Symbol("deadline-elapsed");
|
|
19506
|
+
async function raceDeadline(promise, deadline) {
|
|
19507
|
+
promise.catch(() => {
|
|
19508
|
+
});
|
|
19509
|
+
const remaining = deadline - Date.now();
|
|
19510
|
+
if (remaining <= 0) return DEADLINE_ELAPSED;
|
|
19511
|
+
let timer;
|
|
19512
|
+
const elapsed = new Promise((resolve7) => {
|
|
19513
|
+
timer = setTimeout(() => resolve7(DEADLINE_ELAPSED), remaining);
|
|
19514
|
+
});
|
|
19515
|
+
try {
|
|
19516
|
+
return await Promise.race([promise, elapsed]);
|
|
19517
|
+
} finally {
|
|
19518
|
+
clearTimeout(timer);
|
|
19519
|
+
}
|
|
19520
|
+
}
|
|
19521
|
+
async function waitForStoredToken(serverUrl, statePath, timeoutSec, readServers = readOAuthServers) {
|
|
17997
19522
|
const key = normalizeServerUrl2(serverUrl);
|
|
17998
19523
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
19524
|
+
let servers = {};
|
|
19525
|
+
let lastError;
|
|
17999
19526
|
for (; ; ) {
|
|
18000
|
-
|
|
19527
|
+
try {
|
|
19528
|
+
const read = await raceDeadline(readServers(statePath), deadline);
|
|
19529
|
+
if (read !== DEADLINE_ELAPSED) {
|
|
19530
|
+
servers = read;
|
|
19531
|
+
lastError = void 0;
|
|
19532
|
+
}
|
|
19533
|
+
} catch (error) {
|
|
19534
|
+
if (!(error instanceof SecretFileLockHeldError)) lastError = error;
|
|
19535
|
+
}
|
|
18001
19536
|
const token = findStoredToken(servers, serverUrl);
|
|
18002
19537
|
if (token) return token;
|
|
18003
19538
|
if (Date.now() >= deadline) {
|
|
19539
|
+
if (lastError !== void 0) throw lastError;
|
|
18004
19540
|
const stored = Object.keys(servers);
|
|
18005
19541
|
throw new CliExitCodeError(
|
|
18006
19542
|
EXIT_CODES.AUTH_REQUIRED,
|
|
@@ -18008,7 +19544,9 @@ async function waitForStoredToken(serverUrl, statePath, timeoutSec) {
|
|
|
18008
19544
|
{ code: "auth_wait_timeout", url: serverUrl }
|
|
18009
19545
|
);
|
|
18010
19546
|
}
|
|
18011
|
-
await new Promise(
|
|
19547
|
+
await new Promise(
|
|
19548
|
+
(r) => setTimeout(r, Math.min(500, deadline - Date.now()))
|
|
19549
|
+
);
|
|
18012
19550
|
}
|
|
18013
19551
|
}
|
|
18014
19552
|
function deepLinkTransport(serverUrl, transport) {
|
|
@@ -18164,12 +19702,18 @@ async function parseArgs(argv) {
|
|
|
18164
19702
|
).option(
|
|
18165
19703
|
"--app-info",
|
|
18166
19704
|
"Probe the tool's MCP App UI metadata (resourceUri, csp, permissions, domain) and emit it as one JSON line; exit 2 when the tool has no app. Use with --method tools/call --tool-name <name> (the tool itself is not invoked) or --method tools/list (one NDJSON line per tool)."
|
|
19705
|
+
).option(
|
|
19706
|
+
"--advertise-apps",
|
|
19707
|
+
"Advertise the MCP Apps UI extension (io.modelcontextprotocol/ui) at initialize. Off by default because the CLI cannot render an App; set it when a server only exposes its App tools to a client that claims App support, e.g. for an --app-info probe."
|
|
18167
19708
|
).option(
|
|
18168
19709
|
"--strict",
|
|
18169
19710
|
"Report tool-schema portability problems in full (path, issue, suggested fix) on stderr, and exit 6 if any is error-severity. Use with --method tools/list. Without it, a one-line count is printed instead."
|
|
18170
19711
|
).option(
|
|
18171
19712
|
"--verify",
|
|
18172
19713
|
"Run the SEP-2640 conformance and digest checks over the skills returned, emit one JSON report per skill on stdout, and exit 7 if any fails or 8 if any could not be fully checked within the read bounds. Use with --method skills/list or --method skills/get."
|
|
19714
|
+
).option(
|
|
19715
|
+
"--require-digests",
|
|
19716
|
+
'With --verify: exit 9 when a skill advertises no digests (resources: "dynamic"), instead of reporting it as unverifiable and exiting 0.'
|
|
18173
19717
|
).option(
|
|
18174
19718
|
"--connect-timeout <ms>",
|
|
18175
19719
|
`Connection timeout in ms (default ${DEFAULT_CONNECT_TIMEOUT_MS} for ad-hoc --server-url / target invocations; 0 = no timeout).`,
|
|
@@ -18284,6 +19828,14 @@ async function parseArgs(argv) {
|
|
|
18284
19828
|
);
|
|
18285
19829
|
}
|
|
18286
19830
|
}
|
|
19831
|
+
if (options.requireDigests && !options.verify) {
|
|
19832
|
+
throw new Error("--require-digests requires --verify.");
|
|
19833
|
+
}
|
|
19834
|
+
if (options.advertiseApps && (options.listStoredAuth || options.printHandoff || options.method === "servers/list" || options.method === "servers/show")) {
|
|
19835
|
+
throw new Error(
|
|
19836
|
+
"--advertise-apps requires a command that connects to a server; it has no effect with --list-stored-auth, --print-handoff, or --method servers/list / servers/show."
|
|
19837
|
+
);
|
|
19838
|
+
}
|
|
18287
19839
|
const oauthStatePath = getStateFilePath();
|
|
18288
19840
|
if (options.listStoredAuth) {
|
|
18289
19841
|
const servers = await readOAuthServers(oauthStatePath);
|
|
@@ -18449,8 +20001,10 @@ async function parseArgs(argv) {
|
|
|
18449
20001
|
metadata: options.metadata,
|
|
18450
20002
|
toolMeta: options.toolMetadata,
|
|
18451
20003
|
appInfo: options.appInfo === true,
|
|
20004
|
+
advertiseApps: options.advertiseApps === true,
|
|
18452
20005
|
strict: options.strict === true,
|
|
18453
20006
|
verify: options.verify === true,
|
|
20007
|
+
requireDigests: options.requireDigests === true,
|
|
18454
20008
|
cursor: options.cursor,
|
|
18455
20009
|
format: options.format
|
|
18456
20010
|
};
|