@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
|
@@ -20,7 +20,7 @@ import { readFile, writeFile } from "atomically";
|
|
|
20
20
|
|
|
21
21
|
// ../../core/storage/store-id.ts
|
|
22
22
|
function validateStoreId(storeId) {
|
|
23
|
-
return /^[a-zA-Z0-9_-]+$/.test(storeId) && storeId
|
|
23
|
+
return /^[a-zA-Z0-9_-]+$/.test(storeId) && storeId !== "__proto__";
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// ../../core/storage/store-serialize.ts
|
|
@@ -109,6 +109,7 @@ var MODERN_LOG_LEVELS = [
|
|
|
109
109
|
function isModernLogLevel(value) {
|
|
110
110
|
return typeof value === "string" && MODERN_LOG_LEVELS.includes(value);
|
|
111
111
|
}
|
|
112
|
+
var MODERN_PROTOCOL_VERSION = "2026-07-28";
|
|
112
113
|
|
|
113
114
|
// ../../core/mcp/skillsSchemas.ts
|
|
114
115
|
import { z } from "zod/v4";
|
|
@@ -145,6 +146,10 @@ var ModernListSkillsResultSchema = ListSkillsResultSchema.extend({
|
|
|
145
146
|
var GetSkillEnvelopeSchema = z.looseObject({
|
|
146
147
|
skill: SkillEntrySchema
|
|
147
148
|
});
|
|
149
|
+
var ModernGetSkillEnvelopeSchema = GetSkillEnvelopeSchema.extend({
|
|
150
|
+
ttlMs: z.int().min(0),
|
|
151
|
+
cacheScope: z.enum(["public", "private"])
|
|
152
|
+
});
|
|
148
153
|
var GetSkillResultSchema = GetSkillEnvelopeSchema.transform(
|
|
149
154
|
(result) => result.skill
|
|
150
155
|
);
|
|
@@ -424,7 +429,7 @@ function cleanEndpointOverride(value, field) {
|
|
|
424
429
|
return value.trim() || void 0;
|
|
425
430
|
}
|
|
426
431
|
function storedFieldsToInspectorSettings(stored) {
|
|
427
|
-
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;
|
|
432
|
+
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;
|
|
428
433
|
if (!hasAny) return void 0;
|
|
429
434
|
const headersPairs = stored.headers ? Object.entries(stored.headers).map(([key, value]) => ({ key, value })) : [];
|
|
430
435
|
const settings = {
|
|
@@ -456,6 +461,9 @@ function storedFieldsToInspectorSettings(stored) {
|
|
|
456
461
|
if (isSkillCatalogLimit(stored.skillCatalogMaxBytes)) {
|
|
457
462
|
settings.skillCatalogMaxBytes = stored.skillCatalogMaxBytes;
|
|
458
463
|
}
|
|
464
|
+
if (stored.suppressNotificationStream === true) {
|
|
465
|
+
settings.suppressNotificationStream = true;
|
|
466
|
+
}
|
|
459
467
|
if (isProtocolEra(stored.protocolEra)) {
|
|
460
468
|
settings.protocolEra = stored.protocolEra;
|
|
461
469
|
}
|
|
@@ -525,6 +533,9 @@ function inspectorSettingsToStoredFields(settings) {
|
|
|
525
533
|
if (settings.paginatedLists) {
|
|
526
534
|
out.paginatedLists = true;
|
|
527
535
|
}
|
|
536
|
+
if (settings.suppressNotificationStream) {
|
|
537
|
+
out.suppressNotificationStream = true;
|
|
538
|
+
}
|
|
528
539
|
if (settings.advertisedExtensions && Object.keys(settings.advertisedExtensions).length > 0) {
|
|
529
540
|
out.advertisedExtensions = { ...settings.advertisedExtensions };
|
|
530
541
|
}
|
|
@@ -591,6 +602,7 @@ var INSPECTOR_FIELD_KEY_MAP = {
|
|
|
591
602
|
taskTtl: true,
|
|
592
603
|
autoRefreshOnListChanged: true,
|
|
593
604
|
paginatedLists: true,
|
|
605
|
+
suppressNotificationStream: true,
|
|
594
606
|
advertisedExtensions: true,
|
|
595
607
|
maxFetchRequests: true,
|
|
596
608
|
skillCatalogMaxSkills: true,
|
|
@@ -954,7 +966,7 @@ function secretStorageCaveat(info) {
|
|
|
954
966
|
return `The secrets file could not be read (${info.encryptionUnknown}). Saving a secret will fail rather than overwrite it.`;
|
|
955
967
|
}
|
|
956
968
|
if (info.plaintext) {
|
|
957
|
-
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.";
|
|
969
|
+
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.";
|
|
958
970
|
}
|
|
959
971
|
return void 0;
|
|
960
972
|
}
|
|
@@ -1058,14 +1070,14 @@ function isAllInterfacesHost(host) {
|
|
|
1058
1070
|
|
|
1059
1071
|
// server/resolve-bind-host.ts
|
|
1060
1072
|
var BIND_ALL_INTERFACES_ENV = "DANGEROUSLY_BIND_ALL_INTERFACES";
|
|
1061
|
-
function
|
|
1073
|
+
function isEnvFlagEnabled(value) {
|
|
1062
1074
|
const v = value?.trim().toLowerCase();
|
|
1063
1075
|
return v === "true" || v === "1";
|
|
1064
1076
|
}
|
|
1065
1077
|
var DEFAULT_BIND_HOST = "127.0.0.1";
|
|
1066
1078
|
function resolveBindHostname(env = process.env) {
|
|
1067
1079
|
const host = (env.HOST ?? DEFAULT_BIND_HOST).trim();
|
|
1068
|
-
if (isAllInterfacesHost(host) && !
|
|
1080
|
+
if (isAllInterfacesHost(host) && !isEnvFlagEnabled(env[BIND_ALL_INTERFACES_ENV])) {
|
|
1069
1081
|
const resolved2 = canonicalUrlHost(host);
|
|
1070
1082
|
const shown = stripBrackets(resolved2) === stripBrackets(host.toLowerCase()) ? `HOST="${host}"` : `HOST="${host}" (resolves to ${resolved2})`;
|
|
1071
1083
|
throw new Error(
|
|
@@ -1658,7 +1670,9 @@ function buildWebServerConfig(options = {}) {
|
|
|
1658
1670
|
);
|
|
1659
1671
|
}
|
|
1660
1672
|
const hostname = resolveBindHostname();
|
|
1661
|
-
const dangerouslyOmitAuth =
|
|
1673
|
+
const dangerouslyOmitAuth = isEnvFlagEnabled(
|
|
1674
|
+
process.env.DANGEROUSLY_OMIT_AUTH
|
|
1675
|
+
);
|
|
1662
1676
|
const authToken = dangerouslyOmitAuth ? "" : process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] ?? process.env[LEGACY_AUTH_TOKEN_ENV] ?? "";
|
|
1663
1677
|
const sandboxPort = resolveSandboxPort();
|
|
1664
1678
|
const requestedAppOriginPort = resolveAppOriginPort();
|
|
@@ -1736,7 +1750,7 @@ import react from "@vitejs/plugin-react";
|
|
|
1736
1750
|
|
|
1737
1751
|
// ../../core/mcp/remote/node/server.ts
|
|
1738
1752
|
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
1739
|
-
import { existsSync as existsSync3, readFileSync as
|
|
1753
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
|
|
1740
1754
|
import { stat as fsStat } from "fs/promises";
|
|
1741
1755
|
import { homedir } from "os";
|
|
1742
1756
|
import { Hono } from "hono";
|
|
@@ -1840,12 +1854,31 @@ function redactJsonValue(value) {
|
|
|
1840
1854
|
}
|
|
1841
1855
|
return value;
|
|
1842
1856
|
}
|
|
1857
|
+
function joinSensitiveContinuations(body) {
|
|
1858
|
+
const out = [];
|
|
1859
|
+
let sensitiveAt = -1;
|
|
1860
|
+
for (const pair of body.split("&")) {
|
|
1861
|
+
const eq = pair.indexOf("=");
|
|
1862
|
+
if (eq === -1) {
|
|
1863
|
+
if (sensitiveAt !== -1 && pair.length > 0) {
|
|
1864
|
+
out[sensitiveAt] += `%26${pair}`;
|
|
1865
|
+
} else {
|
|
1866
|
+
out.push(pair);
|
|
1867
|
+
}
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
const [key] = new URLSearchParams(pair).keys();
|
|
1871
|
+
sensitiveAt = isSensitiveField(key) && eq < pair.length - 1 ? out.length : -1;
|
|
1872
|
+
out.push(pair);
|
|
1873
|
+
}
|
|
1874
|
+
return out.join("&");
|
|
1875
|
+
}
|
|
1843
1876
|
function redactBody(body, contentType) {
|
|
1844
1877
|
if (!body) return body;
|
|
1845
1878
|
const type = (contentType ?? "").toLowerCase();
|
|
1846
1879
|
if (type.includes("application/x-www-form-urlencoded")) {
|
|
1847
1880
|
try {
|
|
1848
|
-
const params = new URLSearchParams(body);
|
|
1881
|
+
const params = new URLSearchParams(joinSensitiveContinuations(body));
|
|
1849
1882
|
let changed = false;
|
|
1850
1883
|
for (const key of new Set(params.keys())) {
|
|
1851
1884
|
if (isSensitiveField(key)) {
|
|
@@ -2359,6 +2392,73 @@ function createProxyFetch() {
|
|
|
2359
2392
|
};
|
|
2360
2393
|
}
|
|
2361
2394
|
|
|
2395
|
+
// ../../core/mcp/node/notificationHeadersFetch.ts
|
|
2396
|
+
import { PROTOCOL_VERSION_META_KEY } from "@modelcontextprotocol/client";
|
|
2397
|
+
function createNotificationHeadersFetch(baseFetch) {
|
|
2398
|
+
return (input, init) => {
|
|
2399
|
+
const method = modernNotificationMethod(init);
|
|
2400
|
+
if (method === void 0) return baseFetch(input, init);
|
|
2401
|
+
const headers = new Headers(init?.headers);
|
|
2402
|
+
headers.set("mcp-protocol-version", method.version);
|
|
2403
|
+
headers.set("mcp-method", method.method);
|
|
2404
|
+
return baseFetch(input, { ...init, headers });
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
function modernNotificationMethod(init) {
|
|
2408
|
+
if (init?.method?.toUpperCase() !== "POST") return void 0;
|
|
2409
|
+
if (typeof init.body !== "string") return void 0;
|
|
2410
|
+
let message;
|
|
2411
|
+
try {
|
|
2412
|
+
message = JSON.parse(init.body);
|
|
2413
|
+
} catch {
|
|
2414
|
+
return void 0;
|
|
2415
|
+
}
|
|
2416
|
+
if (!isRecord(message) || "id" in message) return void 0;
|
|
2417
|
+
if (typeof message.method !== "string") return void 0;
|
|
2418
|
+
const meta = isRecord(message.params) ? message.params._meta : void 0;
|
|
2419
|
+
const version = isRecord(meta) ? meta[PROTOCOL_VERSION_META_KEY] : void 0;
|
|
2420
|
+
if (typeof version !== "string" || version < MODERN_PROTOCOL_VERSION) {
|
|
2421
|
+
return void 0;
|
|
2422
|
+
}
|
|
2423
|
+
return { method: message.method, version };
|
|
2424
|
+
}
|
|
2425
|
+
function isRecord(value) {
|
|
2426
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// ../../core/mcp/node/suppressNotificationStreamFetch.ts
|
|
2430
|
+
function createSuppressNotificationStreamFetch(baseFetch, endpoint) {
|
|
2431
|
+
return async (input, init) => {
|
|
2432
|
+
if (isStandaloneStreamRequest(input, init, endpoint)) {
|
|
2433
|
+
return new Response(null, {
|
|
2434
|
+
status: 405,
|
|
2435
|
+
statusText: "Method Not Allowed"
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
return baseFetch(input, init);
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
function requestUrl(input) {
|
|
2442
|
+
const raw = input instanceof Request ? input.url : input instanceof URL ? input.href : input;
|
|
2443
|
+
try {
|
|
2444
|
+
return new URL(raw);
|
|
2445
|
+
} catch {
|
|
2446
|
+
return void 0;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
function isStandaloneStreamRequest(input, init, endpoint) {
|
|
2450
|
+
const request = input instanceof Request ? input : void 0;
|
|
2451
|
+
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
2452
|
+
if (method !== "GET") return false;
|
|
2453
|
+
const url = requestUrl(input);
|
|
2454
|
+
if (!url || url.origin !== endpoint.origin || url.pathname !== endpoint.pathname || url.search !== endpoint.search) {
|
|
2455
|
+
return false;
|
|
2456
|
+
}
|
|
2457
|
+
const headers = new Headers(init?.headers ?? request?.headers);
|
|
2458
|
+
const accept = headers.get("accept")?.toLowerCase() ?? "";
|
|
2459
|
+
return accept.includes("text/event-stream") && !headers.has("last-event-id");
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2362
2462
|
// ../../core/mcp/node/transport.ts
|
|
2363
2463
|
function headersFromSettings(settings) {
|
|
2364
2464
|
if (!settings || settings.headers.length === 0) return void 0;
|
|
@@ -2446,10 +2546,14 @@ function createTransportNode(config, options = {}) {
|
|
|
2446
2546
|
...httpConfig.requestInit,
|
|
2447
2547
|
...headers && { headers }
|
|
2448
2548
|
};
|
|
2549
|
+
const stampedFetch = createNotificationHeadersFetch(
|
|
2550
|
+
fetchWithOptionalAuthIntercept
|
|
2551
|
+
);
|
|
2552
|
+
const httpFetch = settings?.suppressNotificationStream === true ? createSuppressNotificationStreamFetch(stampedFetch, url) : stampedFetch;
|
|
2449
2553
|
const transport = new StreamableHTTPClientTransport(url, {
|
|
2450
2554
|
authProvider,
|
|
2451
2555
|
requestInit,
|
|
2452
|
-
fetch:
|
|
2556
|
+
fetch: httpFetch,
|
|
2453
2557
|
// SEP-2350: how the transport reacts to a `403 insufficient_scope`
|
|
2454
2558
|
// challenge. Defaults to the SDK's `reauthorize` when unset.
|
|
2455
2559
|
...settings?.oauthOnInsufficientScope && {
|
|
@@ -2463,1040 +2567,808 @@ function createTransportNode(config, options = {}) {
|
|
|
2463
2567
|
// ../../core/auth/requestTimeout.ts
|
|
2464
2568
|
var OAUTH_TIMEOUT_WIRE_CODE = "oauth_request_timeout";
|
|
2465
2569
|
|
|
2466
|
-
// ../../core/
|
|
2467
|
-
|
|
2570
|
+
// ../../core/storage/own-entry.ts
|
|
2571
|
+
function setOwnEntry(map, key, value) {
|
|
2572
|
+
Object.defineProperty(map, key, {
|
|
2573
|
+
value,
|
|
2574
|
+
enumerable: true,
|
|
2575
|
+
writable: true,
|
|
2576
|
+
configurable: true
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
function getOwnEntry(map, key) {
|
|
2580
|
+
return map !== void 0 && Object.hasOwn(map, key) ? map[key] : void 0;
|
|
2581
|
+
}
|
|
2468
2582
|
|
|
2469
|
-
// ../../core/
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2583
|
+
// ../../core/auth/oauth-persist.ts
|
|
2584
|
+
import { OAuthTokensSchema } from "@modelcontextprotocol/core";
|
|
2585
|
+
var OAUTH_PERSIST_STORE_ID = "oauth";
|
|
2586
|
+
function mergeOAuthSections(disk, snapshot, sections) {
|
|
2587
|
+
const merged = {
|
|
2588
|
+
servers: { ...disk?.servers },
|
|
2589
|
+
idpSessions: { ...disk?.idpSessions }
|
|
2590
|
+
};
|
|
2591
|
+
for (const url of sections.servers ?? []) {
|
|
2592
|
+
const value = getOwnEntry(snapshot.servers, url);
|
|
2593
|
+
if (value === void 0) {
|
|
2594
|
+
delete merged.servers[url];
|
|
2595
|
+
} else {
|
|
2596
|
+
setOwnEntry(merged.servers, url, value);
|
|
2597
|
+
}
|
|
2477
2598
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2599
|
+
for (const issuer of sections.idpSessions ?? []) {
|
|
2600
|
+
const value = getOwnEntry(snapshot.idpSessions, issuer);
|
|
2601
|
+
if (value === void 0) {
|
|
2602
|
+
delete merged.idpSessions[issuer];
|
|
2603
|
+
} else {
|
|
2604
|
+
setOwnEntry(merged.idpSessions, issuer, value);
|
|
2605
|
+
}
|
|
2480
2606
|
}
|
|
2481
|
-
return
|
|
2607
|
+
return merged;
|
|
2482
2608
|
}
|
|
2483
|
-
function
|
|
2484
|
-
|
|
2485
|
-
for (const [id, raw] of Object.entries(servers)) {
|
|
2486
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
2487
|
-
const normalized = normalizeServerType(
|
|
2488
|
-
raw
|
|
2489
|
-
);
|
|
2490
|
-
out[id] = normalized;
|
|
2491
|
-
}
|
|
2492
|
-
return out;
|
|
2609
|
+
function isStringArray(value) {
|
|
2610
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
2493
2611
|
}
|
|
2494
|
-
function
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
2498
|
-
throw new Error("No 'mcpServers' object found in config file");
|
|
2612
|
+
function parseOAuthPersistSections(value) {
|
|
2613
|
+
if (!isRecord2(value)) {
|
|
2614
|
+
return null;
|
|
2499
2615
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
}
|
|
2504
|
-
function parseVsCodeConfig(raw) {
|
|
2505
|
-
const data = parseJsonObject(raw);
|
|
2506
|
-
const servers = data.servers;
|
|
2507
|
-
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
2508
|
-
throw new Error("No 'servers' object found in VS Code config file");
|
|
2616
|
+
const sections = {};
|
|
2617
|
+
for (const key of Object.keys(value)) {
|
|
2618
|
+
if (key !== "servers" && key !== "idpSessions") return null;
|
|
2509
2619
|
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
}
|
|
2514
|
-
|
|
2515
|
-
// ../../core/mcp/import/strategies.ts
|
|
2516
|
-
function join4(...segments) {
|
|
2517
|
-
return segments.map(
|
|
2518
|
-
(s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")
|
|
2519
|
-
).filter((s) => s.length > 0).join("/");
|
|
2520
|
-
}
|
|
2521
|
-
function appData(homeDir) {
|
|
2522
|
-
return join4(homeDir, "AppData", "Roaming");
|
|
2523
|
-
}
|
|
2524
|
-
function appSupportDir(platform, homeDir, app) {
|
|
2525
|
-
if (platform === "darwin") {
|
|
2526
|
-
return join4(homeDir, "Library", "Application Support", app);
|
|
2620
|
+
if ("servers" in value) {
|
|
2621
|
+
if (!isStringArray(value.servers)) return null;
|
|
2622
|
+
sections.servers = value.servers;
|
|
2527
2623
|
}
|
|
2528
|
-
if (
|
|
2529
|
-
|
|
2624
|
+
if ("idpSessions" in value) {
|
|
2625
|
+
if (!isStringArray(value.idpSessions)) return null;
|
|
2626
|
+
sections.idpSessions = value.idpSessions;
|
|
2530
2627
|
}
|
|
2531
|
-
return
|
|
2628
|
+
return sections;
|
|
2532
2629
|
}
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
parse: parseMcpServersConfig
|
|
2543
|
-
};
|
|
2544
|
-
var cursor = {
|
|
2545
|
-
id: "cursor",
|
|
2546
|
-
label: "Cursor",
|
|
2547
|
-
defaultPaths: (_platform, homeDir) => [join4(homeDir, ".cursor", "mcp.json")],
|
|
2548
|
-
parse: parseMcpServersConfig
|
|
2549
|
-
};
|
|
2550
|
-
var cline = {
|
|
2551
|
-
id: "cline",
|
|
2552
|
-
label: "Cline",
|
|
2553
|
-
defaultPaths: (platform, homeDir) => [
|
|
2554
|
-
// Cline (VS Code extension) global settings live under the editor's
|
|
2555
|
-
// globalStorage; the documented cross-platform fallback is ~/Documents.
|
|
2556
|
-
join4(
|
|
2557
|
-
appSupportDir(platform, homeDir, "Code"),
|
|
2558
|
-
"User",
|
|
2559
|
-
"globalStorage",
|
|
2560
|
-
"saoudrizwan.claude-dev",
|
|
2561
|
-
"settings",
|
|
2562
|
-
"cline_mcp_settings.json"
|
|
2563
|
-
),
|
|
2564
|
-
join4(homeDir, "Documents", "Cline", "MCP", "cline_mcp_settings.json")
|
|
2565
|
-
],
|
|
2566
|
-
parse: parseMcpServersConfig
|
|
2567
|
-
};
|
|
2568
|
-
var vscode = {
|
|
2569
|
-
id: "vscode",
|
|
2570
|
-
label: "VS Code",
|
|
2571
|
-
defaultPaths: (platform, homeDir) => [
|
|
2572
|
-
join4(appSupportDir(platform, homeDir, "Code"), "User", "mcp.json")
|
|
2573
|
-
],
|
|
2574
|
-
parse: parseVsCodeConfig
|
|
2575
|
-
};
|
|
2576
|
-
var IMPORT_STRATEGY_LIST = [
|
|
2577
|
-
claudeDesktop,
|
|
2578
|
-
cursor,
|
|
2579
|
-
cline,
|
|
2580
|
-
vscode
|
|
2630
|
+
function isRecord2(value) {
|
|
2631
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2632
|
+
}
|
|
2633
|
+
function isEntryMap(value) {
|
|
2634
|
+
return isRecord2(value) && Object.values(value).every(isRecord2);
|
|
2635
|
+
}
|
|
2636
|
+
var VERBATIM_SECRET_KEYS = [
|
|
2637
|
+
"client_secret",
|
|
2638
|
+
"registration_access_token"
|
|
2581
2639
|
];
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
return
|
|
2640
|
+
function isValidClientInformation(value) {
|
|
2641
|
+
if (value === void 0) return true;
|
|
2642
|
+
if (!isRecord2(value)) return false;
|
|
2643
|
+
return VERBATIM_SECRET_KEYS.every(
|
|
2644
|
+
(key) => !Object.hasOwn(value, key) || typeof value[key] === "string"
|
|
2645
|
+
);
|
|
2585
2646
|
}
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
path: path6,
|
|
2602
|
-
error: `Failed to read ${path6}: ${msg}`,
|
|
2603
|
-
searched
|
|
2604
|
-
};
|
|
2605
|
-
}
|
|
2606
|
-
if (raw === null) continue;
|
|
2607
|
-
try {
|
|
2608
|
-
const config = strategy.parse(raw);
|
|
2609
|
-
return { type, found: true, path: path6, config, searched };
|
|
2610
|
-
} catch (err) {
|
|
2611
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2612
|
-
return { type, found: true, path: path6, error: msg, searched };
|
|
2647
|
+
var PartialTokensSchema = OAuthTokensSchema.partial();
|
|
2648
|
+
function isValidTokens(value) {
|
|
2649
|
+
if (value === void 0) return true;
|
|
2650
|
+
return PartialTokensSchema.safeParse(value).success;
|
|
2651
|
+
}
|
|
2652
|
+
function isValidServerEntry(entry) {
|
|
2653
|
+
if (!isRecord2(entry)) return false;
|
|
2654
|
+
if (!isValidClientInformation(entry.clientInformation)) return false;
|
|
2655
|
+
if (!isValidClientInformation(entry.preregisteredClientInformation))
|
|
2656
|
+
return false;
|
|
2657
|
+
if (entry.byIssuer !== void 0) {
|
|
2658
|
+
if (!isRecord2(entry.byIssuer)) return false;
|
|
2659
|
+
for (const slot of Object.values(entry.byIssuer)) {
|
|
2660
|
+
if (!isRecord2(slot)) return false;
|
|
2661
|
+
if (!isValidClientInformation(slot.clientInformation)) return false;
|
|
2613
2662
|
}
|
|
2614
2663
|
}
|
|
2615
|
-
return
|
|
2664
|
+
return true;
|
|
2616
2665
|
}
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2666
|
+
function isValidIdpSession(session) {
|
|
2667
|
+
if (!isRecord2(session)) return false;
|
|
2668
|
+
return ["idToken", "refreshToken"].every(
|
|
2669
|
+
(key) => !Object.hasOwn(session, key) || typeof session[key] === "string"
|
|
2670
|
+
);
|
|
2671
|
+
}
|
|
2672
|
+
function snapshotHasValidSecretPayloads(snapshot) {
|
|
2673
|
+
for (const entry of Object.values(snapshot.servers)) {
|
|
2674
|
+
const record = entry;
|
|
2675
|
+
if (!isValidTokens(record.tokens)) return false;
|
|
2676
|
+
if (isRecord2(record.byIssuer)) {
|
|
2677
|
+
for (const slot of Object.values(record.byIssuer)) {
|
|
2678
|
+
if (isRecord2(slot) && !isValidTokens(slot.tokens)) return false;
|
|
2679
|
+
}
|
|
2627
2680
|
}
|
|
2628
2681
|
}
|
|
2629
|
-
return
|
|
2682
|
+
return Object.values(snapshot.idpSessions).every(isValidIdpSession);
|
|
2630
2683
|
}
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
*/
|
|
2645
|
-
authHttpEchoSuppressUntilMs = 0;
|
|
2646
|
-
static AUTH_HTTP_ECHO_SUPPRESS_MS = 3e4;
|
|
2647
|
-
requestWaits = /* @__PURE__ */ new Map();
|
|
2648
|
-
authProviderHandle = null;
|
|
2649
|
-
appliedProtocolVersion;
|
|
2650
|
-
/** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
|
|
2651
|
-
static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
2652
|
-
constructor(sessionId) {
|
|
2653
|
-
this.sessionId = sessionId;
|
|
2684
|
+
function snapshotFromPayload(payload) {
|
|
2685
|
+
const servers = payload.servers ?? {};
|
|
2686
|
+
const idpSessions = payload.idpSessions ?? {};
|
|
2687
|
+
if (!isEntryMap(servers) || !isEntryMap(idpSessions)) return null;
|
|
2688
|
+
if (!Object.values(servers).every(isValidServerEntry)) return null;
|
|
2689
|
+
return {
|
|
2690
|
+
servers,
|
|
2691
|
+
idpSessions
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
function parseOAuthPersistBlob(raw) {
|
|
2695
|
+
if (raw === null || raw === void 0) {
|
|
2696
|
+
return null;
|
|
2654
2697
|
}
|
|
2655
|
-
|
|
2656
|
-
|
|
2698
|
+
const parsed = typeof raw === "string" ? raw ? parseStore(raw) : null : raw;
|
|
2699
|
+
if (!isRecord2(parsed)) {
|
|
2700
|
+
return null;
|
|
2657
2701
|
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
throw new Error("Session has no OAuth auth provider");
|
|
2661
|
-
}
|
|
2662
|
-
this.authProviderHandle.setAuthState(authState);
|
|
2702
|
+
if (isRecord2(parsed.state) && "version" in parsed) {
|
|
2703
|
+
return snapshotFromPayload(parsed.state);
|
|
2663
2704
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2705
|
+
if ("servers" in parsed || "idpSessions" in parsed) {
|
|
2706
|
+
return snapshotFromPayload(parsed);
|
|
2666
2707
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
* unvalidated JSON body, so the type is checked rather than assumed —
|
|
2678
|
-
* `RegExp.test` would otherwise coerce a `123` or `true` into a "matching"
|
|
2679
|
-
* string and hand the wrong type to the transport.
|
|
2680
|
-
*/
|
|
2681
|
-
applyProtocolVersion(version) {
|
|
2682
|
-
if (typeof version !== "string" || version === this.appliedProtocolVersion || !_RemoteSession.PROTOCOL_VERSION_PATTERN.test(version)) {
|
|
2683
|
-
return;
|
|
2708
|
+
return null;
|
|
2709
|
+
}
|
|
2710
|
+
function serializeOAuthPersistBlob(snapshot) {
|
|
2711
|
+
return serializeStore(snapshot);
|
|
2712
|
+
}
|
|
2713
|
+
function parseOAuthStoreWriteBody(body) {
|
|
2714
|
+
if (!isRecord2(body)) return null;
|
|
2715
|
+
if ("sections" in body) {
|
|
2716
|
+
for (const key of Object.keys(body)) {
|
|
2717
|
+
if (key !== "sections" && key !== "snapshot") return null;
|
|
2684
2718
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
consumer(ev);
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
clearEventConsumer() {
|
|
2696
|
-
this.eventConsumer = null;
|
|
2697
|
-
return this.transportDead;
|
|
2698
|
-
}
|
|
2699
|
-
markTransportDead(error) {
|
|
2700
|
-
this.transportDead = true;
|
|
2701
|
-
this.transportError = error;
|
|
2702
|
-
this.pushEvent({
|
|
2703
|
-
type: "transport_error",
|
|
2704
|
-
data: {
|
|
2705
|
-
error,
|
|
2706
|
-
code: -32e3
|
|
2707
|
-
// MCP error code for connection closed
|
|
2708
|
-
}
|
|
2709
|
-
});
|
|
2710
|
-
}
|
|
2711
|
-
isTransportDead() {
|
|
2712
|
-
return this.transportDead;
|
|
2719
|
+
const sections = parseOAuthPersistSections(body.sections);
|
|
2720
|
+
if (!sections) return null;
|
|
2721
|
+
const snapshot2 = parseOAuthPersistBlob(
|
|
2722
|
+
"snapshot" in body ? body.snapshot : null
|
|
2723
|
+
);
|
|
2724
|
+
if (!snapshot2 || !snapshotHasValidSecretPayloads(snapshot2)) return null;
|
|
2725
|
+
return { snapshot: snapshot2, sections };
|
|
2713
2726
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
2727
|
+
const snapshot = parseOAuthPersistBlob(body);
|
|
2728
|
+
if (!snapshot || !snapshotHasValidSecretPayloads(snapshot)) return null;
|
|
2729
|
+
return { snapshot };
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
// ../../core/auth/node/file-lock.ts
|
|
2733
|
+
import nodeFs, { statSync, rmdirSync } from "fs";
|
|
2734
|
+
import * as fs2 from "fs/promises";
|
|
2735
|
+
import * as path2 from "path";
|
|
2736
|
+
import properLockfile from "proper-lockfile";
|
|
2737
|
+
|
|
2738
|
+
// ../../core/auth/node/secret-store.ts
|
|
2739
|
+
var SERVICE_NAME = "mcp-inspector";
|
|
2740
|
+
var keyringLoad;
|
|
2741
|
+
var KeyringModuleShapeError = class extends Error {
|
|
2742
|
+
constructor(detail, options) {
|
|
2743
|
+
super(`@napi-rs/keyring loaded but ${detail}`, options);
|
|
2744
|
+
this.name = "KeyringModuleShapeError";
|
|
2716
2745
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2746
|
+
};
|
|
2747
|
+
var checkKeyringShape = (mod) => {
|
|
2748
|
+
try {
|
|
2749
|
+
if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
|
|
2750
|
+
return { ok: true, mod };
|
|
2751
|
+
}
|
|
2752
|
+
return {
|
|
2753
|
+
ok: false,
|
|
2754
|
+
err: new KeyringModuleShapeError(
|
|
2755
|
+
"did not expose AsyncEntry / findCredentialsAsync"
|
|
2756
|
+
)
|
|
2757
|
+
};
|
|
2758
|
+
} catch (err) {
|
|
2759
|
+
return {
|
|
2760
|
+
ok: false,
|
|
2761
|
+
err: new KeyringModuleShapeError(
|
|
2762
|
+
`its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
|
|
2763
|
+
{ cause: err }
|
|
2764
|
+
)
|
|
2765
|
+
};
|
|
2719
2766
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
2767
|
+
};
|
|
2768
|
+
var loadKeyring = () => {
|
|
2769
|
+
keyringLoad ??= import("@napi-rs/keyring").then(
|
|
2770
|
+
checkKeyringShape,
|
|
2771
|
+
(err) => ({ ok: false, err })
|
|
2772
|
+
);
|
|
2773
|
+
return keyringLoad;
|
|
2774
|
+
};
|
|
2775
|
+
function parseAccount(account) {
|
|
2776
|
+
const idx = account.indexOf(":");
|
|
2777
|
+
if (idx <= 0 || idx === account.length - 1) return null;
|
|
2778
|
+
return {
|
|
2779
|
+
serverId: account.slice(0, idx),
|
|
2780
|
+
field: account.slice(idx + 1)
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
var buildAccount = (serverId, field) => `${serverId}:${field}`;
|
|
2784
|
+
var SecretStoreUnavailableError = class extends Error {
|
|
2785
|
+
constructor(message, options) {
|
|
2786
|
+
super(message, options);
|
|
2787
|
+
this.name = "SecretStoreUnavailableError";
|
|
2722
2788
|
}
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2789
|
+
};
|
|
2790
|
+
var SecretFileLockHeldError = class extends SecretStoreUnavailableError {
|
|
2791
|
+
constructor(message, options) {
|
|
2792
|
+
super(message, options);
|
|
2793
|
+
this.name = "SecretFileLockHeldError";
|
|
2727
2794
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2795
|
+
};
|
|
2796
|
+
var KeychainUnavailableError = class extends SecretStoreUnavailableError {
|
|
2797
|
+
constructor(cause) {
|
|
2798
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
2799
|
+
super(
|
|
2800
|
+
`OS keychain is not available. ${hintFor(cause, message)} Underlying error: ${message}`
|
|
2801
|
+
);
|
|
2802
|
+
this.name = "KeychainUnavailableError";
|
|
2731
2803
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2804
|
+
};
|
|
2805
|
+
var hintFor = (cause, message) => {
|
|
2806
|
+
if (cause instanceof KeyringModuleShapeError) {
|
|
2807
|
+
return `The @napi-rs/keyring package loaded but does not expose the API this build expects \u2014 most likely a version or packaging mismatch; reinstall the Inspector, and report this if it persists.`;
|
|
2734
2808
|
}
|
|
2735
|
-
|
|
2736
|
-
return
|
|
2809
|
+
if (message.includes("Cannot find native binding")) {
|
|
2810
|
+
return `The @napi-rs/keyring platform package for this OS is missing or unavailable \u2014 reinstall the Inspector (for npx, clear the npx cache under your npm cache directory first).`;
|
|
2737
2811
|
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2812
|
+
return `On Linux, install libsecret / gnome-keyring.`;
|
|
2813
|
+
};
|
|
2814
|
+
async function probeKeyringAvailable() {
|
|
2815
|
+
const keyring = await loadKeyring();
|
|
2816
|
+
if (!keyring.ok) {
|
|
2817
|
+
const err = keyring.err;
|
|
2818
|
+
return {
|
|
2819
|
+
available: false,
|
|
2820
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
2821
|
+
};
|
|
2740
2822
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2823
|
+
try {
|
|
2824
|
+
const entry = new keyring.mod.AsyncEntry(SERVICE_NAME, PROBE_ACCOUNT);
|
|
2825
|
+
await entry.getPassword();
|
|
2826
|
+
await keyring.mod.findCredentialsAsync(SERVICE_NAME);
|
|
2827
|
+
return { available: true };
|
|
2828
|
+
} catch (err) {
|
|
2829
|
+
return {
|
|
2830
|
+
available: false,
|
|
2831
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
2832
|
+
};
|
|
2743
2833
|
}
|
|
2834
|
+
}
|
|
2835
|
+
var PROBE_ACCOUNT = "__inspector:probe";
|
|
2836
|
+
var isNoEntryError = (err) => err instanceof Error && /no (matching )?entry/i.test(err.message);
|
|
2837
|
+
var KeyringSecretStore = class {
|
|
2744
2838
|
/**
|
|
2745
|
-
*
|
|
2746
|
-
*
|
|
2747
|
-
*
|
|
2839
|
+
* The intolerant read. Same lookup as {@link get}, minus the catch — a
|
|
2840
|
+
* keychain that cannot answer must not be reported as an empty one to a
|
|
2841
|
+
* caller that is about to write based on the answer.
|
|
2748
2842
|
*/
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
};
|
|
2763
|
-
const clear = () => {
|
|
2764
|
-
if (timer !== void 0) {
|
|
2765
|
-
clearTimeout(timer);
|
|
2766
|
-
timer = void 0;
|
|
2767
|
-
}
|
|
2768
|
-
};
|
|
2769
|
-
arm();
|
|
2770
|
-
this.requestWaits.set(requestId, {
|
|
2771
|
-
resolve: () => {
|
|
2772
|
-
clear();
|
|
2773
|
-
resolve9();
|
|
2774
|
-
},
|
|
2775
|
-
reject: (error) => {
|
|
2776
|
-
clear();
|
|
2777
|
-
reject(error);
|
|
2778
|
-
},
|
|
2779
|
-
// A progress notification for this request re-arms the full window, so
|
|
2780
|
-
// a tool that keeps reporting progress never trips the relay's own
|
|
2781
|
-
// deadline — the same guarantee the SDK client's `resetTimeoutOnProgress`
|
|
2782
|
-
// gives on a direct transport (#2028).
|
|
2783
|
-
resetTimeout: () => {
|
|
2784
|
-
if (timer === void 0) return;
|
|
2785
|
-
clear();
|
|
2786
|
-
arm();
|
|
2787
|
-
}
|
|
2788
|
-
});
|
|
2789
|
-
});
|
|
2843
|
+
async getStrict(serverId, field) {
|
|
2844
|
+
try {
|
|
2845
|
+
const keyring = await loadKeyring();
|
|
2846
|
+
if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
|
|
2847
|
+
const entry = new keyring.mod.AsyncEntry(
|
|
2848
|
+
SERVICE_NAME,
|
|
2849
|
+
buildAccount(serverId, field)
|
|
2850
|
+
);
|
|
2851
|
+
return await entry.getPassword() ?? null;
|
|
2852
|
+
} catch (err) {
|
|
2853
|
+
if (err instanceof KeychainUnavailableError) throw err;
|
|
2854
|
+
throw new KeychainUnavailableError(err);
|
|
2855
|
+
}
|
|
2790
2856
|
}
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2857
|
+
async get(serverId, field) {
|
|
2858
|
+
try {
|
|
2859
|
+
const keyring = await loadKeyring();
|
|
2860
|
+
if (!keyring.ok) return null;
|
|
2861
|
+
const entry = new keyring.mod.AsyncEntry(
|
|
2862
|
+
SERVICE_NAME,
|
|
2863
|
+
buildAccount(serverId, field)
|
|
2864
|
+
);
|
|
2865
|
+
const v = await entry.getPassword();
|
|
2866
|
+
return v ?? null;
|
|
2867
|
+
} catch {
|
|
2868
|
+
return null;
|
|
2869
|
+
}
|
|
2798
2870
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2871
|
+
async set(serverId, field, value) {
|
|
2872
|
+
try {
|
|
2873
|
+
const keyring = await loadKeyring();
|
|
2874
|
+
if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
|
|
2875
|
+
const entry = new keyring.mod.AsyncEntry(
|
|
2876
|
+
SERVICE_NAME,
|
|
2877
|
+
buildAccount(serverId, field)
|
|
2878
|
+
);
|
|
2879
|
+
await entry.setPassword(value);
|
|
2880
|
+
} catch (err) {
|
|
2881
|
+
if (err instanceof KeychainUnavailableError) throw err;
|
|
2882
|
+
throw new KeychainUnavailableError(err);
|
|
2803
2883
|
}
|
|
2804
|
-
this.requestWaits.delete(requestId);
|
|
2805
|
-
wait.reject(new Error("MCP request wait cancelled"));
|
|
2806
2884
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2885
|
+
async delete(serverId, field) {
|
|
2886
|
+
try {
|
|
2887
|
+
const keyring = await loadKeyring();
|
|
2888
|
+
if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
|
|
2889
|
+
const entry = new keyring.mod.AsyncEntry(
|
|
2890
|
+
SERVICE_NAME,
|
|
2891
|
+
buildAccount(serverId, field)
|
|
2892
|
+
);
|
|
2893
|
+
await entry.deleteCredential();
|
|
2894
|
+
} catch (err) {
|
|
2895
|
+
if (isNoEntryError(err)) return;
|
|
2896
|
+
if (err instanceof KeychainUnavailableError) throw err;
|
|
2897
|
+
throw new KeychainUnavailableError(err);
|
|
2810
2898
|
}
|
|
2811
|
-
this.requestWaits.clear();
|
|
2812
2899
|
}
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
}
|
|
2823
|
-
this.rejectActiveRequestWaits(error);
|
|
2824
|
-
if (this.hasActiveSend()) {
|
|
2825
|
-
this.extendAuthHttpEchoSuppression();
|
|
2826
|
-
return true;
|
|
2900
|
+
async deleteAllForServer(serverId) {
|
|
2901
|
+
let creds;
|
|
2902
|
+
try {
|
|
2903
|
+
const keyring = await loadKeyring();
|
|
2904
|
+
if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
|
|
2905
|
+
creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
|
|
2906
|
+
} catch (err) {
|
|
2907
|
+
if (err instanceof KeychainUnavailableError) throw err;
|
|
2908
|
+
throw new KeychainUnavailableError(err);
|
|
2827
2909
|
}
|
|
2828
|
-
|
|
2829
|
-
|
|
2910
|
+
const prefix = `${serverId}:`;
|
|
2911
|
+
for (const c of creds) {
|
|
2912
|
+
if (!c.account.startsWith(prefix)) continue;
|
|
2913
|
+
const parsed = parseAccount(c.account);
|
|
2914
|
+
if (!parsed || parsed.serverId !== serverId) continue;
|
|
2915
|
+
await this.delete(serverId, parsed.field);
|
|
2830
2916
|
}
|
|
2831
|
-
this.pushAuthChallenge(error.authChallenge);
|
|
2832
|
-
return true;
|
|
2833
2917
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2918
|
+
};
|
|
2919
|
+
async function secretStoreGetStrict(store, serverId, field) {
|
|
2920
|
+
return store.getStrict ? store.getStrict(serverId, field) : store.get(serverId, field);
|
|
2921
|
+
}
|
|
2922
|
+
async function snapshotSecretFields(store, serverId, fields) {
|
|
2923
|
+
return Promise.all(
|
|
2924
|
+
fields.map(async (field) => ({
|
|
2925
|
+
serverId,
|
|
2926
|
+
field,
|
|
2927
|
+
value: await secretStoreGetStrict(store, serverId, field)
|
|
2928
|
+
}))
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
async function restoreSecretFields(store, snapshot, onRestoreFailure) {
|
|
2932
|
+
for (const { serverId, field, value } of snapshot) {
|
|
2933
|
+
try {
|
|
2934
|
+
if (value === null) await store.delete(serverId, field);
|
|
2935
|
+
else await store.set(serverId, field, value);
|
|
2936
|
+
} catch (error) {
|
|
2937
|
+
onRestoreFailure(error);
|
|
2838
2938
|
}
|
|
2839
|
-
this.requestWaits.delete(requestId);
|
|
2840
|
-
wait.resolve();
|
|
2841
2939
|
}
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
}
|
|
2940
|
+
}
|
|
2941
|
+
async function secretStoreGetMany(store, requests) {
|
|
2942
|
+
if (store.getMany) return store.getMany(requests);
|
|
2943
|
+
const out = {};
|
|
2944
|
+
await Promise.all(
|
|
2945
|
+
requests.map(async ({ serverId, fields }) => {
|
|
2946
|
+
const entries = await Promise.all(
|
|
2947
|
+
fields.map(
|
|
2948
|
+
async (field) => [field, await store.get(serverId, field)]
|
|
2949
|
+
)
|
|
2950
|
+
);
|
|
2951
|
+
const found = {};
|
|
2952
|
+
for (const [field, value] of entries) {
|
|
2953
|
+
if (value !== null) setOwnEntry(found, field, value);
|
|
2954
|
+
}
|
|
2955
|
+
setOwnEntry(out, serverId, found);
|
|
2956
|
+
})
|
|
2957
|
+
);
|
|
2958
|
+
return out;
|
|
2959
|
+
}
|
|
2960
|
+
async function secretStoreGetManyStrict(store, requests) {
|
|
2961
|
+
if (store.getManyStrict) return store.getManyStrict(requests);
|
|
2962
|
+
const out = {};
|
|
2963
|
+
await Promise.all(
|
|
2964
|
+
requests.map(async ({ serverId, fields }) => {
|
|
2965
|
+
const entries = await Promise.all(
|
|
2966
|
+
fields.map(
|
|
2967
|
+
async (field) => [
|
|
2968
|
+
field,
|
|
2969
|
+
await secretStoreGetStrict(store, serverId, field)
|
|
2970
|
+
]
|
|
2971
|
+
)
|
|
2972
|
+
);
|
|
2973
|
+
const found = {};
|
|
2974
|
+
for (const [field, value] of entries) {
|
|
2975
|
+
if (value !== null) setOwnEntry(found, field, value);
|
|
2976
|
+
}
|
|
2977
|
+
setOwnEntry(out, serverId, found);
|
|
2978
|
+
})
|
|
2979
|
+
);
|
|
2980
|
+
return out;
|
|
2981
|
+
}
|
|
2982
|
+
async function settleStoreMutations(mutations) {
|
|
2983
|
+
const results = await Promise.allSettled(mutations);
|
|
2984
|
+
for (const result of results) {
|
|
2985
|
+
if (result.status === "rejected") throw result.reason;
|
|
2848
2986
|
}
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2987
|
+
}
|
|
2988
|
+
async function secretStoreSetMany(store, serverId, values) {
|
|
2989
|
+
if (store.setMany) return store.setMany(serverId, values);
|
|
2990
|
+
await settleStoreMutations(
|
|
2991
|
+
Object.entries(values).map(
|
|
2992
|
+
([field, value]) => store.set(serverId, field, value)
|
|
2993
|
+
)
|
|
2994
|
+
);
|
|
2995
|
+
}
|
|
2996
|
+
async function secretStoreIsDurable(store) {
|
|
2997
|
+
return store.isDurable ? store.isDurable() : true;
|
|
2998
|
+
}
|
|
2999
|
+
var InMemorySecretStore = class {
|
|
3000
|
+
map = /* @__PURE__ */ new Map();
|
|
3001
|
+
async get(serverId, field) {
|
|
3002
|
+
return this.map.get(buildAccount(serverId, field)) ?? null;
|
|
2858
3003
|
}
|
|
2859
|
-
|
|
2860
|
-
this.
|
|
2861
|
-
type: "fetch_request",
|
|
2862
|
-
data: {
|
|
2863
|
-
...entry,
|
|
2864
|
-
timestamp: entry.timestamp instanceof Date ? entry.timestamp.toISOString() : entry.timestamp
|
|
2865
|
-
}
|
|
2866
|
-
});
|
|
3004
|
+
async set(serverId, field, value) {
|
|
3005
|
+
this.map.set(buildAccount(serverId, field), value);
|
|
2867
3006
|
}
|
|
2868
|
-
|
|
2869
|
-
this.
|
|
2870
|
-
type: "fetch_request_body_update",
|
|
2871
|
-
data: { id, responseBody }
|
|
2872
|
-
});
|
|
3007
|
+
async delete(serverId, field) {
|
|
3008
|
+
this.map.delete(buildAccount(serverId, field));
|
|
2873
3009
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
eventCount: stream.eventCount,
|
|
2880
|
-
...stream.closedAt && { closedAt: stream.closedAt.toISOString() }
|
|
2881
|
-
}
|
|
2882
|
-
});
|
|
3010
|
+
async deleteAllForServer(serverId) {
|
|
3011
|
+
const prefix = `${serverId}:`;
|
|
3012
|
+
for (const key of [...this.map.keys()]) {
|
|
3013
|
+
if (key.startsWith(prefix)) this.map.delete(key);
|
|
3014
|
+
}
|
|
2883
3015
|
}
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
timestamp: entry.timestamp.toISOString(),
|
|
2889
|
-
message: entry.message
|
|
2890
|
-
}
|
|
2891
|
-
});
|
|
3016
|
+
};
|
|
3017
|
+
var SessionSecretStore = class extends InMemorySecretStore {
|
|
3018
|
+
async isDurable() {
|
|
3019
|
+
return false;
|
|
2892
3020
|
}
|
|
2893
3021
|
};
|
|
2894
3022
|
|
|
2895
|
-
// ../../core/
|
|
2896
|
-
var
|
|
2897
|
-
|
|
2898
|
-
|
|
3023
|
+
// ../../core/auth/node/file-lock.ts
|
|
3024
|
+
var STALE_MS = 1e4;
|
|
3025
|
+
var RETRY = {
|
|
3026
|
+
retries: 20,
|
|
3027
|
+
factor: 2,
|
|
3028
|
+
minTimeout: 20,
|
|
3029
|
+
maxTimeout: 1e3
|
|
2899
3030
|
};
|
|
2900
|
-
|
|
3031
|
+
var RETRY_BUDGET_MS = (() => {
|
|
3032
|
+
let total = 0;
|
|
3033
|
+
let delay = RETRY.minTimeout;
|
|
3034
|
+
for (let i = 0; i < RETRY.retries; i++) {
|
|
3035
|
+
total += Math.min(delay, RETRY.maxTimeout);
|
|
3036
|
+
delay *= RETRY.factor;
|
|
3037
|
+
}
|
|
3038
|
+
return total;
|
|
3039
|
+
})();
|
|
3040
|
+
var warned = /* @__PURE__ */ new Set();
|
|
3041
|
+
function warnOnce(message) {
|
|
3042
|
+
if (warned.has(message)) return;
|
|
3043
|
+
warned.add(message);
|
|
3044
|
+
console.warn(`[mcp-inspector] ${message}`);
|
|
3045
|
+
}
|
|
3046
|
+
function isStuckOrHeld(err, lockPath) {
|
|
3047
|
+
return isHeldElsewhere(err) || identifySync(lockPath) !== null;
|
|
3048
|
+
}
|
|
3049
|
+
var isHeldElsewhere = (err) => err.code === "ELOCKED";
|
|
3050
|
+
var describeError = (err) => err instanceof Error ? err.message : String(err);
|
|
3051
|
+
var lockPathOf = (target) => `${target}.lock`;
|
|
3052
|
+
function identifySync(lockPath) {
|
|
3053
|
+
try {
|
|
3054
|
+
const stat2 = statSync(lockPath);
|
|
3055
|
+
return { ino: stat2.ino, birthtimeMs: stat2.birthtimeMs };
|
|
3056
|
+
} catch {
|
|
3057
|
+
return null;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
function guardedFs(lockPath, owned, onRefused) {
|
|
3061
|
+
const mine = () => {
|
|
3062
|
+
if (owned.id === null) return true;
|
|
3063
|
+
const now = identifySync(lockPath);
|
|
3064
|
+
if (now === null) return false;
|
|
3065
|
+
return now.ino === owned.id.ino && now.birthtimeMs === owned.id.birthtimeMs;
|
|
3066
|
+
};
|
|
3067
|
+
const removeIfMine = () => {
|
|
3068
|
+
if (!mine()) {
|
|
3069
|
+
onRefused();
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
3072
|
+
rmdirSync(lockPath);
|
|
3073
|
+
};
|
|
2901
3074
|
return {
|
|
2902
|
-
...
|
|
2903
|
-
|
|
3075
|
+
...nodeFs,
|
|
3076
|
+
// **Identity is captured here, not after `lock()` resolves.** `mkdir` is
|
|
3077
|
+
// the moment the directory becomes ours, and it is synchronous with
|
|
3078
|
+
// respect to this process: recording it in the callback, before yielding,
|
|
3079
|
+
// leaves no window. Capturing after the acquire promise settled — as this
|
|
3080
|
+
// did originally — spans the library's own `utimes`/`stat` probe, and a
|
|
3081
|
+
// waiter replacing our directory inside that span would have us record
|
|
3082
|
+
// *the winner's* identity as our own, after which the guard below would
|
|
3083
|
+
// cheerfully delete their lock.
|
|
3084
|
+
mkdir: (p, cb) => {
|
|
3085
|
+
nodeFs.mkdir(p, (err) => {
|
|
3086
|
+
if (!err) owned.id = identifySync(lockPath);
|
|
3087
|
+
cb(err);
|
|
3088
|
+
});
|
|
2904
3089
|
},
|
|
2905
|
-
|
|
2906
|
-
|
|
3090
|
+
// Reported as success when refused: the library's bookkeeping should
|
|
3091
|
+
// forget this lock either way. Leaving it registered would hand the
|
|
3092
|
+
// `signal-exit` handler a record pointing at the winner's directory.
|
|
3093
|
+
rmdir: (_p, cb) => {
|
|
3094
|
+
try {
|
|
3095
|
+
removeIfMine();
|
|
3096
|
+
cb(null);
|
|
3097
|
+
} catch (err) {
|
|
3098
|
+
cb(err);
|
|
3099
|
+
}
|
|
3100
|
+
},
|
|
3101
|
+
/* v8 ignore next 3 -- @preserve: only reachable from proper-lockfile's
|
|
3102
|
+
signal-exit handler, i.e. at real process exit, which no in-process
|
|
3103
|
+
test can drive. Its logic is `removeIfMine`, covered via `rmdir`. */
|
|
3104
|
+
rmdirSync: () => {
|
|
3105
|
+
removeIfMine();
|
|
2907
3106
|
}
|
|
2908
3107
|
};
|
|
2909
3108
|
}
|
|
2910
|
-
function
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
return {
|
|
2930
|
-
client_id: authState.oauthClient.client_id,
|
|
2931
|
-
...authState.oauthClient.client_secret && {
|
|
2932
|
-
client_secret: authState.oauthClient.client_secret
|
|
2933
|
-
}
|
|
2934
|
-
};
|
|
2935
|
-
},
|
|
2936
|
-
async saveTokens(tokens) {
|
|
2937
|
-
authState = {
|
|
2938
|
-
...authState,
|
|
2939
|
-
oauthTokens: {
|
|
2940
|
-
access_token: tokens.access_token,
|
|
2941
|
-
token_type: tokens.token_type,
|
|
2942
|
-
expires_in: tokens.expires_in,
|
|
2943
|
-
refresh_token: tokens.refresh_token,
|
|
2944
|
-
scope: tokens.scope,
|
|
2945
|
-
id_token: tokens.id_token
|
|
2946
|
-
}
|
|
2947
|
-
};
|
|
2948
|
-
},
|
|
2949
|
-
codeVerifier() {
|
|
2950
|
-
return void 0;
|
|
2951
|
-
},
|
|
2952
|
-
async saveCodeVerifier() {
|
|
2953
|
-
},
|
|
2954
|
-
clear() {
|
|
2955
|
-
authState = {};
|
|
2956
|
-
},
|
|
2957
|
-
async redirectToAuthorization() {
|
|
2958
|
-
throw new Error(
|
|
2959
|
-
"OAuth re-authorization must be performed in the Inspector web client (remote server cannot complete OAuth flows)"
|
|
2960
|
-
);
|
|
2961
|
-
},
|
|
2962
|
-
state() {
|
|
2963
|
-
return "";
|
|
2964
|
-
}
|
|
2965
|
-
// This provider implements only the members the remote backend transport
|
|
2966
|
-
// exercises (token get/save, client info, non-interactive redirect); the
|
|
2967
|
-
// interactive-only members the SDK never calls on the server side are stubbed
|
|
2968
|
-
// with narrower shapes (e.g. `codeVerifier()` returns `undefined`, not the
|
|
2969
|
-
// interface's `string | Promise<string>`), so it can't be assigned to the
|
|
2970
|
-
// full `OAuthClientProvider` without the double cast.
|
|
2971
|
-
};
|
|
2972
|
-
return {
|
|
2973
|
-
provider,
|
|
2974
|
-
setAuthState(next) {
|
|
2975
|
-
authState = cloneAuthState(next);
|
|
2976
|
-
},
|
|
2977
|
-
getAuthState() {
|
|
2978
|
-
return cloneAuthState(authState);
|
|
2979
|
-
}
|
|
2980
|
-
};
|
|
3109
|
+
function acquire(target, retries, fsShim) {
|
|
3110
|
+
return properLockfile.lock(target, {
|
|
3111
|
+
realpath: false,
|
|
3112
|
+
stale: STALE_MS,
|
|
3113
|
+
retries,
|
|
3114
|
+
// Every directory removal the library performs — on release and from its
|
|
3115
|
+
// exit handler — routes through here. See `guardedFs`.
|
|
3116
|
+
fs: fsShim,
|
|
3117
|
+
// The library's default `onCompromised` *throws* — from a timer, with no
|
|
3118
|
+
// caller on the stack, so it lands as an uncaught exception and takes the
|
|
3119
|
+
// process down. This is also the library's *only* signal for the
|
|
3120
|
+
// stale-takeover race described at the top of this file — someone
|
|
3121
|
+
// declared our lock stale and replaced it — so it is the one place a user
|
|
3122
|
+
// learns the guarantee was lost. Worth saying; not worth killing an
|
|
3123
|
+
// Inspector session over.
|
|
3124
|
+
onCompromised: (err) => warnOnce(
|
|
3125
|
+
`The lock on the secrets file at ${target} was taken over by another process while a write was in progress (${err.message}). If a secret you just saved is missing, save it again.`
|
|
3126
|
+
)
|
|
3127
|
+
});
|
|
2981
3128
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
3129
|
+
async function isFileLockHeld(filePath) {
|
|
3130
|
+
try {
|
|
3131
|
+
return await properLockfile.check(path2.resolve(filePath), {
|
|
3132
|
+
realpath: false,
|
|
3133
|
+
stale: STALE_MS
|
|
3134
|
+
});
|
|
3135
|
+
} catch {
|
|
3136
|
+
return false;
|
|
2990
3137
|
}
|
|
2991
|
-
}
|
|
2992
|
-
|
|
3138
|
+
}
|
|
3139
|
+
async function openSecretFileLock(filePath) {
|
|
3140
|
+
const target = path2.resolve(filePath);
|
|
3141
|
+
await fs2.mkdir(path2.dirname(target), { recursive: true }).catch(() => {
|
|
3142
|
+
});
|
|
3143
|
+
const owned = { id: null };
|
|
3144
|
+
const fsShim = guardedFs(
|
|
3145
|
+
lockPathOf(target),
|
|
3146
|
+
owned,
|
|
3147
|
+
() => warnOnce(
|
|
3148
|
+
`The lock on the secrets file at ${target} was taken over by another process while a write was in progress, so it was left alone rather than removed. If a secret you just saved is missing, save it again.`
|
|
3149
|
+
)
|
|
3150
|
+
);
|
|
3151
|
+
let release;
|
|
2993
3152
|
try {
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
ok: false,
|
|
2999
|
-
err: new KeyringModuleShapeError(
|
|
3000
|
-
"did not expose AsyncEntry / findCredentialsAsync"
|
|
3001
|
-
)
|
|
3002
|
-
};
|
|
3153
|
+
release = await acquire(target, 0, fsShim).catch((err) => {
|
|
3154
|
+
if (!isStuckOrHeld(err, lockPathOf(target))) throw err;
|
|
3155
|
+
return acquire(target, RETRY, fsShim);
|
|
3156
|
+
});
|
|
3003
3157
|
} catch (err) {
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3158
|
+
if (isStuckOrHeld(err, lockPathOf(target))) {
|
|
3159
|
+
throw new SecretFileLockHeldError(
|
|
3160
|
+
`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.`
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
3163
|
+
warnOnce(
|
|
3164
|
+
`Could not take a lock on the secrets file at ${target} (${describeError(err)}), so writes to it are not protected against another process writing at the same moment.`
|
|
3165
|
+
);
|
|
3166
|
+
return null;
|
|
3011
3167
|
}
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3168
|
+
return async () => {
|
|
3169
|
+
try {
|
|
3170
|
+
await release();
|
|
3171
|
+
} catch (err) {
|
|
3172
|
+
if (err.code === "ERELEASED") return;
|
|
3173
|
+
warnOnce(
|
|
3174
|
+
// **Conditional, not an instruction.** Two blanket versions of this
|
|
3175
|
+
// have now been wrong in opposite directions: "it expires on its own"
|
|
3176
|
+
// (false whenever the same `rmdir` that blocked us also blocks stale
|
|
3177
|
+
// takeover — `ENOTEMPTY`, `EACCES`, `EPERM`, `EROFS`), and "remove it
|
|
3178
|
+
// by hand" (false whenever the failure was transient, since the path
|
|
3179
|
+
// may by then hold a *different, live* Inspector's lock, and deleting
|
|
3180
|
+
// that destroys the exclusion of a process that did nothing wrong).
|
|
3181
|
+
//
|
|
3182
|
+
// `proper-lockfile` forwards whatever the filesystem returned, and
|
|
3183
|
+
// nothing here can tell a permanent refusal from a passing one. So
|
|
3184
|
+
// this reports what happened and states the two conditions the
|
|
3185
|
+
// operator can check for themselves, rather than asserting an outcome
|
|
3186
|
+
// this code does not know.
|
|
3187
|
+
`Could not release the lock on the secrets file at ${target} (${describeError(err)}). It may clear on its own \u2014 stale takeover reclaims a lock through the same directory removal, so it will not if whatever blocked this persists. If saves keep failing against this file and no other Inspector is running, remove ${lockPathOf(target)} by hand.`
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3026
3190
|
};
|
|
3027
3191
|
}
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3192
|
+
async function withSecretFileLock(filePath, fn) {
|
|
3193
|
+
const release = await openSecretFileLock(filePath);
|
|
3194
|
+
if (release === null) return fn();
|
|
3195
|
+
try {
|
|
3196
|
+
return await fn();
|
|
3197
|
+
} finally {
|
|
3198
|
+
await release();
|
|
3033
3199
|
}
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
// ../../core/auth/node/secret-store-selection.ts
|
|
3203
|
+
import { randomUUID } from "crypto";
|
|
3204
|
+
import * as fsSync from "fs";
|
|
3205
|
+
import * as fs4 from "fs/promises";
|
|
3206
|
+
import * as path4 from "path";
|
|
3207
|
+
|
|
3208
|
+
// ../../core/auth/node/file-secret-store.ts
|
|
3209
|
+
import * as crypto2 from "crypto";
|
|
3210
|
+
import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
3211
|
+
import * as fs3 from "fs/promises";
|
|
3212
|
+
import * as path3 from "path";
|
|
3213
|
+
var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve9, reject) => {
|
|
3214
|
+
crypto2.scrypt(passphrase, salt, keylen, options, (err, key) => {
|
|
3215
|
+
if (err) reject(err);
|
|
3216
|
+
else resolve9(key);
|
|
3217
|
+
});
|
|
3218
|
+
});
|
|
3219
|
+
var FORMAT_VERSION = 1;
|
|
3220
|
+
var CIPHER = "aes-256-gcm";
|
|
3221
|
+
var KEY_BYTES = 32;
|
|
3222
|
+
var IV_BYTES = 12;
|
|
3223
|
+
var GCM_TAG_BYTES = 16;
|
|
3224
|
+
var SCRYPT_N = 16384;
|
|
3225
|
+
var SCRYPT_R = 8;
|
|
3226
|
+
var SCRYPT_P = 1;
|
|
3227
|
+
var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
|
|
3228
|
+
var SECRET_KEY_FILE_ENV = "MCP_INSPECTOR_SECRET_KEY_FILE";
|
|
3229
|
+
function isSameFile(a, b) {
|
|
3230
|
+
if (path3.resolve(a) === path3.resolve(b)) return true;
|
|
3231
|
+
try {
|
|
3232
|
+
const sa = statSync2(a);
|
|
3233
|
+
const sb = statSync2(b);
|
|
3234
|
+
return sa.dev === sb.dev && sa.ino === sb.ino;
|
|
3235
|
+
} catch {
|
|
3236
|
+
return false;
|
|
3042
3237
|
}
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3238
|
+
}
|
|
3239
|
+
function resolveSecretPassphrase(env = process.env, secretFilePath) {
|
|
3240
|
+
const direct = env[SECRET_KEY_ENV];
|
|
3241
|
+
const hasDirect = direct !== void 0 && direct.trim() !== "";
|
|
3242
|
+
const rawKeyFile = env[SECRET_KEY_FILE_ENV];
|
|
3243
|
+
const wantsKeyFile = rawKeyFile !== void 0;
|
|
3244
|
+
const keyFile = rawKeyFile?.trim();
|
|
3245
|
+
if (hasDirect && wantsKeyFile) {
|
|
3246
|
+
return {
|
|
3247
|
+
problem: `both ${SECRET_KEY_ENV} and ${SECRET_KEY_FILE_ENV} are set; set only one`
|
|
3248
|
+
};
|
|
3047
3249
|
}
|
|
3048
|
-
if (
|
|
3049
|
-
|
|
3250
|
+
if (hasDirect) return { passphrase: direct };
|
|
3251
|
+
if (!wantsKeyFile) return {};
|
|
3252
|
+
if (!keyFile) {
|
|
3253
|
+
return { problem: `${SECRET_KEY_FILE_ENV} is set but empty` };
|
|
3050
3254
|
}
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
async function probeKeyringAvailable() {
|
|
3054
|
-
const keyring = await loadKeyring();
|
|
3055
|
-
if (!keyring.ok) {
|
|
3056
|
-
const err = keyring.err;
|
|
3255
|
+
const resolved2 = path3.resolve(keyFile);
|
|
3256
|
+
if (secretFilePath !== void 0 && isSameFile(resolved2, secretFilePath)) {
|
|
3057
3257
|
return {
|
|
3058
|
-
|
|
3059
|
-
detail: err instanceof Error ? err.message : String(err)
|
|
3258
|
+
problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is the secrets file itself; point it at a separate key file`
|
|
3060
3259
|
};
|
|
3061
3260
|
}
|
|
3261
|
+
let contents;
|
|
3062
3262
|
try {
|
|
3063
|
-
|
|
3064
|
-
await entry.getPassword();
|
|
3065
|
-
return { available: true };
|
|
3263
|
+
contents = readFileSync4(resolved2, "utf-8");
|
|
3066
3264
|
} catch (err) {
|
|
3067
3265
|
return {
|
|
3068
|
-
|
|
3069
|
-
|
|
3266
|
+
// `String(err)` rather than `.message`: `readFileSync` only ever throws
|
|
3267
|
+
// an `Error`, so an `instanceof` guard would be a branch no test can take.
|
|
3268
|
+
problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) could not be read: ${String(err)}`
|
|
3070
3269
|
};
|
|
3071
3270
|
}
|
|
3271
|
+
const passphrase = contents.replace(/[\r\n]+$/, "");
|
|
3272
|
+
if (passphrase.trim() === "") {
|
|
3273
|
+
return { problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is empty` };
|
|
3274
|
+
}
|
|
3275
|
+
return { passphrase };
|
|
3072
3276
|
}
|
|
3073
|
-
var
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
*/
|
|
3080
|
-
async getStrict(serverId, field) {
|
|
3081
|
-
try {
|
|
3082
|
-
const keyring = await loadKeyring();
|
|
3083
|
-
if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
|
|
3084
|
-
const entry = new keyring.mod.AsyncEntry(
|
|
3085
|
-
SERVICE_NAME,
|
|
3086
|
-
buildAccount(serverId, field)
|
|
3087
|
-
);
|
|
3088
|
-
return await entry.getPassword() ?? null;
|
|
3089
|
-
} catch (err) {
|
|
3090
|
-
if (err instanceof KeychainUnavailableError) throw err;
|
|
3091
|
-
throw new KeychainUnavailableError(err);
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
async get(serverId, field) {
|
|
3095
|
-
try {
|
|
3096
|
-
const keyring = await loadKeyring();
|
|
3097
|
-
if (!keyring.ok) return null;
|
|
3098
|
-
const entry = new keyring.mod.AsyncEntry(
|
|
3099
|
-
SERVICE_NAME,
|
|
3100
|
-
buildAccount(serverId, field)
|
|
3101
|
-
);
|
|
3102
|
-
const v = await entry.getPassword();
|
|
3103
|
-
return v ?? null;
|
|
3104
|
-
} catch {
|
|
3105
|
-
return null;
|
|
3106
|
-
}
|
|
3277
|
+
var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
|
|
3278
|
+
function assertSecretString(field, value) {
|
|
3279
|
+
if (typeof value !== "string") {
|
|
3280
|
+
throw new TypeError(
|
|
3281
|
+
`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.`
|
|
3282
|
+
);
|
|
3107
3283
|
}
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
buildAccount(serverId, field)
|
|
3115
|
-
);
|
|
3116
|
-
await entry.setPassword(value);
|
|
3117
|
-
} catch (err) {
|
|
3118
|
-
if (err instanceof KeychainUnavailableError) throw err;
|
|
3119
|
-
throw new KeychainUnavailableError(err);
|
|
3120
|
-
}
|
|
3284
|
+
}
|
|
3285
|
+
function asSecretMap(value, filePath) {
|
|
3286
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3287
|
+
throw new SecretStoreUnavailableError(
|
|
3288
|
+
`The secrets file at ${filePath} does not hold a secret map (found ${Array.isArray(value) ? "an array" : typeof value}). Refusing to write, which would replace it. Move the file aside to start over.`
|
|
3289
|
+
);
|
|
3121
3290
|
}
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
const entry = new keyring.mod.AsyncEntry(
|
|
3127
|
-
SERVICE_NAME,
|
|
3128
|
-
buildAccount(serverId, field)
|
|
3291
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3292
|
+
if (typeof entry !== "string") {
|
|
3293
|
+
throw new SecretStoreUnavailableError(
|
|
3294
|
+
`The secrets file at ${filePath} holds a non-string value for "${key}". Refusing to write, which would replace it. Move the file aside to start over.`
|
|
3129
3295
|
);
|
|
3130
|
-
await entry.deleteCredential();
|
|
3131
|
-
} catch {
|
|
3132
3296
|
}
|
|
3133
3297
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
}
|
|
3143
|
-
const prefix = `${serverId}:`;
|
|
3144
|
-
for (const c of creds) {
|
|
3145
|
-
if (!c.account.startsWith(prefix)) continue;
|
|
3146
|
-
const parsed = parseAccount(c.account);
|
|
3147
|
-
if (!parsed || parsed.serverId !== serverId) continue;
|
|
3148
|
-
await this.delete(serverId, parsed.field);
|
|
3149
|
-
}
|
|
3298
|
+
return { ...value };
|
|
3299
|
+
}
|
|
3300
|
+
var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
|
|
3301
|
+
constructor(filePath, hasKey) {
|
|
3302
|
+
super(
|
|
3303
|
+
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.`
|
|
3304
|
+
);
|
|
3305
|
+
this.name = "SecretFileKeyMismatchError";
|
|
3150
3306
|
}
|
|
3151
3307
|
};
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
async (field) => [field, await store.get(serverId, field)]
|
|
3163
|
-
)
|
|
3164
|
-
);
|
|
3165
|
-
const found = {};
|
|
3166
|
-
for (const [field, value] of entries) {
|
|
3167
|
-
if (value !== null) found[field] = value;
|
|
3168
|
-
}
|
|
3169
|
-
out[serverId] = found;
|
|
3170
|
-
})
|
|
3171
|
-
);
|
|
3172
|
-
return out;
|
|
3308
|
+
var encodeParts = (iv, tag, body) => `${iv.toString("base64")}.${tag.toString("base64")}.${body.toString("base64")}`;
|
|
3309
|
+
function decodeParts(payload) {
|
|
3310
|
+
const parts = payload.split(".");
|
|
3311
|
+
if (parts.length !== 3) return null;
|
|
3312
|
+
const [iv, tag, body] = parts;
|
|
3313
|
+
return {
|
|
3314
|
+
iv: Buffer.from(iv, "base64"),
|
|
3315
|
+
tag: Buffer.from(tag, "base64"),
|
|
3316
|
+
body: Buffer.from(body, "base64")
|
|
3317
|
+
};
|
|
3173
3318
|
}
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3319
|
+
var MAX_WRITE_ATTEMPTS = 5;
|
|
3320
|
+
var fileQueues = /* @__PURE__ */ new Map();
|
|
3321
|
+
function sameMap(a, b) {
|
|
3322
|
+
const ak = Object.keys(a);
|
|
3323
|
+
if (ak.length !== Object.keys(b).length) return false;
|
|
3324
|
+
return ak.every(
|
|
3325
|
+
(k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]
|
|
3180
3326
|
);
|
|
3181
3327
|
}
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
map = /* @__PURE__ */ new Map();
|
|
3187
|
-
async get(serverId, field) {
|
|
3188
|
-
return this.map.get(buildAccount(serverId, field)) ?? null;
|
|
3328
|
+
var isPositiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
3329
|
+
function encryptedEnvelopeProblem(parsed) {
|
|
3330
|
+
if (!parsed.kdf || typeof parsed.data !== "string") {
|
|
3331
|
+
return "encrypted envelope is missing its kdf or data";
|
|
3189
3332
|
}
|
|
3190
|
-
|
|
3191
|
-
|
|
3333
|
+
const parts = decodeParts(parsed.data);
|
|
3334
|
+
if (!parts) return "encrypted payload is not iv.tag.ciphertext";
|
|
3335
|
+
if (parts.iv.length !== IV_BYTES) {
|
|
3336
|
+
return `initialization vector is ${parts.iv.length} bytes, expected ${IV_BYTES}`;
|
|
3192
3337
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3338
|
+
if (parts.tag.length !== GCM_TAG_BYTES) {
|
|
3339
|
+
return `authentication tag is ${parts.tag.length} bytes, expected ${GCM_TAG_BYTES}`;
|
|
3195
3340
|
}
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3341
|
+
if (parts.body.length === 0) return "ciphertext is empty";
|
|
3342
|
+
const { algorithm, salt, N, r, p } = parsed.kdf;
|
|
3343
|
+
if (algorithm !== "scrypt") return `unsupported kdf "${String(algorithm)}"`;
|
|
3344
|
+
if (typeof salt !== "string" || Buffer.from(salt, "base64").length === 0) {
|
|
3345
|
+
return "kdf salt is missing or not base64";
|
|
3201
3346
|
}
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
async isDurable() {
|
|
3205
|
-
return false;
|
|
3347
|
+
if (!isPositiveInt(N) || N < 2 || (N & N - 1) !== 0) {
|
|
3348
|
+
return `kdf N is ${String(N)}, which is not a power of two above 1`;
|
|
3206
3349
|
}
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
// ../../core/auth/node/secret-store-selection.ts
|
|
3210
|
-
import { randomUUID } from "crypto";
|
|
3211
|
-
import * as fsSync from "fs";
|
|
3212
|
-
import * as fs4 from "fs/promises";
|
|
3213
|
-
import * as path4 from "path";
|
|
3214
|
-
|
|
3215
|
-
// ../../core/auth/node/file-secret-store.ts
|
|
3216
|
-
import * as crypto2 from "crypto";
|
|
3217
|
-
import * as fs3 from "fs/promises";
|
|
3218
|
-
import * as path3 from "path";
|
|
3219
|
-
|
|
3220
|
-
// ../../core/auth/node/file-lock.ts
|
|
3221
|
-
import nodeFs, { statSync, rmdirSync } from "fs";
|
|
3222
|
-
import * as fs2 from "fs/promises";
|
|
3223
|
-
import * as path2 from "path";
|
|
3224
|
-
import properLockfile from "proper-lockfile";
|
|
3225
|
-
var STALE_MS = 1e4;
|
|
3226
|
-
var RETRY = {
|
|
3227
|
-
retries: 20,
|
|
3228
|
-
factor: 2,
|
|
3229
|
-
minTimeout: 20,
|
|
3230
|
-
maxTimeout: 1e3
|
|
3231
|
-
};
|
|
3232
|
-
var RETRY_BUDGET_MS = (() => {
|
|
3233
|
-
let total = 0;
|
|
3234
|
-
let delay = RETRY.minTimeout;
|
|
3235
|
-
for (let i = 0; i < RETRY.retries; i++) {
|
|
3236
|
-
total += Math.min(delay, RETRY.maxTimeout);
|
|
3237
|
-
delay *= RETRY.factor;
|
|
3350
|
+
if (!isPositiveInt(r) || !isPositiveInt(p)) {
|
|
3351
|
+
return `kdf r/p are ${String(r)}/${String(p)}, which are not positive integers`;
|
|
3238
3352
|
}
|
|
3239
|
-
|
|
3240
|
-
})()
|
|
3241
|
-
var warned = /* @__PURE__ */ new Set();
|
|
3242
|
-
function warnOnce(message) {
|
|
3243
|
-
if (warned.has(message)) return;
|
|
3244
|
-
warned.add(message);
|
|
3245
|
-
console.warn(`[mcp-inspector] ${message}`);
|
|
3246
|
-
}
|
|
3247
|
-
function isStuckOrHeld(err, lockPath) {
|
|
3248
|
-
return isHeldElsewhere(err) || identifySync(lockPath) !== null;
|
|
3249
|
-
}
|
|
3250
|
-
var isHeldElsewhere = (err) => err.code === "ELOCKED";
|
|
3251
|
-
var describeError = (err) => err instanceof Error ? err.message : String(err);
|
|
3252
|
-
var lockPathOf = (target) => `${target}.lock`;
|
|
3253
|
-
function identifySync(lockPath) {
|
|
3254
|
-
try {
|
|
3255
|
-
const stat2 = statSync(lockPath);
|
|
3256
|
-
return { ino: stat2.ino, birthtimeMs: stat2.birthtimeMs };
|
|
3257
|
-
} catch {
|
|
3258
|
-
return null;
|
|
3353
|
+
if (N > SCRYPT_N || r > SCRYPT_R || p > SCRYPT_P) {
|
|
3354
|
+
return `kdf cost parameters (N=${N}, r=${r}, p=${p}) exceed the supported maximum (N=${SCRYPT_N}, r=${SCRYPT_R}, p=${SCRYPT_P})`;
|
|
3259
3355
|
}
|
|
3260
|
-
|
|
3261
|
-
function guardedFs(lockPath, owned, onRefused) {
|
|
3262
|
-
const mine = () => {
|
|
3263
|
-
if (owned.id === null) return true;
|
|
3264
|
-
const now = identifySync(lockPath);
|
|
3265
|
-
if (now === null) return false;
|
|
3266
|
-
return now.ino === owned.id.ino && now.birthtimeMs === owned.id.birthtimeMs;
|
|
3267
|
-
};
|
|
3268
|
-
const removeIfMine = () => {
|
|
3269
|
-
if (!mine()) {
|
|
3270
|
-
onRefused();
|
|
3271
|
-
return;
|
|
3272
|
-
}
|
|
3273
|
-
rmdirSync(lockPath);
|
|
3274
|
-
};
|
|
3275
|
-
return {
|
|
3276
|
-
...nodeFs,
|
|
3277
|
-
// **Identity is captured here, not after `lock()` resolves.** `mkdir` is
|
|
3278
|
-
// the moment the directory becomes ours, and it is synchronous with
|
|
3279
|
-
// respect to this process: recording it in the callback, before yielding,
|
|
3280
|
-
// leaves no window. Capturing after the acquire promise settled — as this
|
|
3281
|
-
// did originally — spans the library's own `utimes`/`stat` probe, and a
|
|
3282
|
-
// waiter replacing our directory inside that span would have us record
|
|
3283
|
-
// *the winner's* identity as our own, after which the guard below would
|
|
3284
|
-
// cheerfully delete their lock.
|
|
3285
|
-
mkdir: (p, cb) => {
|
|
3286
|
-
nodeFs.mkdir(p, (err) => {
|
|
3287
|
-
if (!err) owned.id = identifySync(lockPath);
|
|
3288
|
-
cb(err);
|
|
3289
|
-
});
|
|
3290
|
-
},
|
|
3291
|
-
// Reported as success when refused: the library's bookkeeping should
|
|
3292
|
-
// forget this lock either way. Leaving it registered would hand the
|
|
3293
|
-
// `signal-exit` handler a record pointing at the winner's directory.
|
|
3294
|
-
rmdir: (_p, cb) => {
|
|
3295
|
-
try {
|
|
3296
|
-
removeIfMine();
|
|
3297
|
-
cb(null);
|
|
3298
|
-
} catch (err) {
|
|
3299
|
-
cb(err);
|
|
3300
|
-
}
|
|
3301
|
-
},
|
|
3302
|
-
/* v8 ignore next 3 -- @preserve: only reachable from proper-lockfile's
|
|
3303
|
-
signal-exit handler, i.e. at real process exit, which no in-process
|
|
3304
|
-
test can drive. Its logic is `removeIfMine`, covered via `rmdir`. */
|
|
3305
|
-
rmdirSync: () => {
|
|
3306
|
-
removeIfMine();
|
|
3307
|
-
}
|
|
3308
|
-
};
|
|
3309
|
-
}
|
|
3310
|
-
function acquire(target, retries, fsShim) {
|
|
3311
|
-
return properLockfile.lock(target, {
|
|
3312
|
-
realpath: false,
|
|
3313
|
-
stale: STALE_MS,
|
|
3314
|
-
retries,
|
|
3315
|
-
// Every directory removal the library performs — on release and from its
|
|
3316
|
-
// exit handler — routes through here. See `guardedFs`.
|
|
3317
|
-
fs: fsShim,
|
|
3318
|
-
// The library's default `onCompromised` *throws* — from a timer, with no
|
|
3319
|
-
// caller on the stack, so it lands as an uncaught exception and takes the
|
|
3320
|
-
// process down. This is also the library's *only* signal for the
|
|
3321
|
-
// stale-takeover race described at the top of this file — someone
|
|
3322
|
-
// declared our lock stale and replaced it — so it is the one place a user
|
|
3323
|
-
// learns the guarantee was lost. Worth saying; not worth killing an
|
|
3324
|
-
// Inspector session over.
|
|
3325
|
-
onCompromised: (err) => warnOnce(
|
|
3326
|
-
`The lock on the secrets file at ${target} was taken over by another process while a write was in progress (${err.message}). If a secret you just saved is missing, save it again.`
|
|
3327
|
-
)
|
|
3328
|
-
});
|
|
3329
|
-
}
|
|
3330
|
-
async function isFileLockHeld(filePath) {
|
|
3331
|
-
try {
|
|
3332
|
-
return await properLockfile.check(path2.resolve(filePath), {
|
|
3333
|
-
realpath: false,
|
|
3334
|
-
stale: STALE_MS
|
|
3335
|
-
});
|
|
3336
|
-
} catch {
|
|
3337
|
-
return false;
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
async function openSecretFileLock(filePath) {
|
|
3341
|
-
const target = path2.resolve(filePath);
|
|
3342
|
-
await fs2.mkdir(path2.dirname(target), { recursive: true }).catch(() => {
|
|
3343
|
-
});
|
|
3344
|
-
const owned = { id: null };
|
|
3345
|
-
const fsShim = guardedFs(
|
|
3346
|
-
lockPathOf(target),
|
|
3347
|
-
owned,
|
|
3348
|
-
() => warnOnce(
|
|
3349
|
-
`The lock on the secrets file at ${target} was taken over by another process while a write was in progress, so it was left alone rather than removed. If a secret you just saved is missing, save it again.`
|
|
3350
|
-
)
|
|
3351
|
-
);
|
|
3352
|
-
let release;
|
|
3353
|
-
try {
|
|
3354
|
-
release = await acquire(target, 0, fsShim).catch((err) => {
|
|
3355
|
-
if (!isStuckOrHeld(err, lockPathOf(target))) throw err;
|
|
3356
|
-
return acquire(target, RETRY, fsShim);
|
|
3357
|
-
});
|
|
3358
|
-
} catch (err) {
|
|
3359
|
-
if (isStuckOrHeld(err, lockPathOf(target))) {
|
|
3360
|
-
throw new SecretStoreUnavailableError(
|
|
3361
|
-
`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.`
|
|
3362
|
-
);
|
|
3363
|
-
}
|
|
3364
|
-
warnOnce(
|
|
3365
|
-
`Could not take a lock on the secrets file at ${target} (${describeError(err)}), so writes to it are not protected against another process writing at the same moment.`
|
|
3366
|
-
);
|
|
3367
|
-
return null;
|
|
3368
|
-
}
|
|
3369
|
-
return async () => {
|
|
3370
|
-
try {
|
|
3371
|
-
await release();
|
|
3372
|
-
} catch (err) {
|
|
3373
|
-
if (err.code === "ERELEASED") return;
|
|
3374
|
-
warnOnce(
|
|
3375
|
-
// **Conditional, not an instruction.** Two blanket versions of this
|
|
3376
|
-
// have now been wrong in opposite directions: "it expires on its own"
|
|
3377
|
-
// (false whenever the same `rmdir` that blocked us also blocks stale
|
|
3378
|
-
// takeover — `ENOTEMPTY`, `EACCES`, `EPERM`, `EROFS`), and "remove it
|
|
3379
|
-
// by hand" (false whenever the failure was transient, since the path
|
|
3380
|
-
// may by then hold a *different, live* Inspector's lock, and deleting
|
|
3381
|
-
// that destroys the exclusion of a process that did nothing wrong).
|
|
3382
|
-
//
|
|
3383
|
-
// `proper-lockfile` forwards whatever the filesystem returned, and
|
|
3384
|
-
// nothing here can tell a permanent refusal from a passing one. So
|
|
3385
|
-
// this reports what happened and states the two conditions the
|
|
3386
|
-
// operator can check for themselves, rather than asserting an outcome
|
|
3387
|
-
// this code does not know.
|
|
3388
|
-
`Could not release the lock on the secrets file at ${target} (${describeError(err)}). It may clear on its own \u2014 stale takeover reclaims a lock through the same directory removal, so it will not if whatever blocked this persists. If saves keep failing against this file and no other Inspector is running, remove ${lockPathOf(target)} by hand.`
|
|
3389
|
-
);
|
|
3390
|
-
}
|
|
3391
|
-
};
|
|
3392
|
-
}
|
|
3393
|
-
async function withSecretFileLock(filePath, fn) {
|
|
3394
|
-
const release = await openSecretFileLock(filePath);
|
|
3395
|
-
if (release === null) return fn();
|
|
3396
|
-
try {
|
|
3397
|
-
return await fn();
|
|
3398
|
-
} finally {
|
|
3399
|
-
await release();
|
|
3400
|
-
}
|
|
3401
|
-
}
|
|
3402
|
-
|
|
3403
|
-
// ../../core/auth/node/file-secret-store.ts
|
|
3404
|
-
var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve9, reject) => {
|
|
3405
|
-
crypto2.scrypt(passphrase, salt, keylen, options, (err, key) => {
|
|
3406
|
-
if (err) reject(err);
|
|
3407
|
-
else resolve9(key);
|
|
3408
|
-
});
|
|
3409
|
-
});
|
|
3410
|
-
var FORMAT_VERSION = 1;
|
|
3411
|
-
var CIPHER = "aes-256-gcm";
|
|
3412
|
-
var KEY_BYTES = 32;
|
|
3413
|
-
var IV_BYTES = 12;
|
|
3414
|
-
var GCM_TAG_BYTES = 16;
|
|
3415
|
-
var SCRYPT_N = 16384;
|
|
3416
|
-
var SCRYPT_R = 8;
|
|
3417
|
-
var SCRYPT_P = 1;
|
|
3418
|
-
var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
|
|
3419
|
-
var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
|
|
3420
|
-
function asSecretMap(value, filePath) {
|
|
3421
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3422
|
-
throw new SecretStoreUnavailableError(
|
|
3423
|
-
`The secrets file at ${filePath} does not hold a secret map (found ${Array.isArray(value) ? "an array" : typeof value}). Refusing to write, which would replace it. Move the file aside to start over.`
|
|
3424
|
-
);
|
|
3425
|
-
}
|
|
3426
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
3427
|
-
if (typeof entry !== "string") {
|
|
3428
|
-
throw new SecretStoreUnavailableError(
|
|
3429
|
-
`The secrets file at ${filePath} holds a non-string value for "${key}". Refusing to write, which would replace it. Move the file aside to start over.`
|
|
3430
|
-
);
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
return { ...value };
|
|
3434
|
-
}
|
|
3435
|
-
var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
|
|
3436
|
-
constructor(filePath, hasKey) {
|
|
3437
|
-
super(
|
|
3438
|
-
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.`
|
|
3439
|
-
);
|
|
3440
|
-
this.name = "SecretFileKeyMismatchError";
|
|
3441
|
-
}
|
|
3442
|
-
};
|
|
3443
|
-
var encodeParts = (iv, tag, body) => `${iv.toString("base64")}.${tag.toString("base64")}.${body.toString("base64")}`;
|
|
3444
|
-
function decodeParts(payload) {
|
|
3445
|
-
const parts = payload.split(".");
|
|
3446
|
-
if (parts.length !== 3) return null;
|
|
3447
|
-
const [iv, tag, body] = parts;
|
|
3448
|
-
return {
|
|
3449
|
-
iv: Buffer.from(iv, "base64"),
|
|
3450
|
-
tag: Buffer.from(tag, "base64"),
|
|
3451
|
-
body: Buffer.from(body, "base64")
|
|
3452
|
-
};
|
|
3453
|
-
}
|
|
3454
|
-
var MAX_WRITE_ATTEMPTS = 5;
|
|
3455
|
-
var fileQueues = /* @__PURE__ */ new Map();
|
|
3456
|
-
function sameMap(a, b) {
|
|
3457
|
-
const ak = Object.keys(a);
|
|
3458
|
-
if (ak.length !== Object.keys(b).length) return false;
|
|
3459
|
-
return ak.every(
|
|
3460
|
-
(k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]
|
|
3461
|
-
);
|
|
3462
|
-
}
|
|
3463
|
-
var isPositiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
3464
|
-
function encryptedEnvelopeProblem(parsed) {
|
|
3465
|
-
if (!parsed.kdf || typeof parsed.data !== "string") {
|
|
3466
|
-
return "encrypted envelope is missing its kdf or data";
|
|
3467
|
-
}
|
|
3468
|
-
const parts = decodeParts(parsed.data);
|
|
3469
|
-
if (!parts) return "encrypted payload is not iv.tag.ciphertext";
|
|
3470
|
-
if (parts.iv.length !== IV_BYTES) {
|
|
3471
|
-
return `initialization vector is ${parts.iv.length} bytes, expected ${IV_BYTES}`;
|
|
3472
|
-
}
|
|
3473
|
-
if (parts.tag.length !== GCM_TAG_BYTES) {
|
|
3474
|
-
return `authentication tag is ${parts.tag.length} bytes, expected ${GCM_TAG_BYTES}`;
|
|
3475
|
-
}
|
|
3476
|
-
if (parts.body.length === 0) return "ciphertext is empty";
|
|
3477
|
-
const { algorithm, salt, N, r, p } = parsed.kdf;
|
|
3478
|
-
if (algorithm !== "scrypt") return `unsupported kdf "${String(algorithm)}"`;
|
|
3479
|
-
if (typeof salt !== "string" || Buffer.from(salt, "base64").length === 0) {
|
|
3480
|
-
return "kdf salt is missing or not base64";
|
|
3481
|
-
}
|
|
3482
|
-
if (!isPositiveInt(N) || N < 2 || (N & N - 1) !== 0) {
|
|
3483
|
-
return `kdf N is ${String(N)}, which is not a power of two above 1`;
|
|
3484
|
-
}
|
|
3485
|
-
if (!isPositiveInt(r) || !isPositiveInt(p)) {
|
|
3486
|
-
return `kdf r/p are ${String(r)}/${String(p)}, which are not positive integers`;
|
|
3487
|
-
}
|
|
3488
|
-
if (N > SCRYPT_N || r > SCRYPT_R || p > SCRYPT_P) {
|
|
3489
|
-
return `kdf cost parameters (N=${N}, r=${r}, p=${p}) exceed the supported maximum (N=${SCRYPT_N}, r=${SCRYPT_R}, p=${SCRYPT_P})`;
|
|
3490
|
-
}
|
|
3491
|
-
return null;
|
|
3356
|
+
return null;
|
|
3492
3357
|
}
|
|
3493
3358
|
var FileSecretStore = class {
|
|
3494
3359
|
filePath;
|
|
3495
3360
|
passphrase;
|
|
3361
|
+
/**
|
|
3362
|
+
* Why a configured key could not be obtained. While set, every read throws
|
|
3363
|
+
* and every write refuses — see {@link SecretPassphrase}.
|
|
3364
|
+
*/
|
|
3365
|
+
keyProblem;
|
|
3496
3366
|
constructor(options) {
|
|
3497
3367
|
this.filePath = options.filePath;
|
|
3498
|
-
const
|
|
3368
|
+
const resolved2 = options.passphrase !== void 0 ? { passphrase: options.passphrase } : resolveSecretPassphrase(process.env, this.filePath);
|
|
3369
|
+
const raw = resolved2.passphrase;
|
|
3499
3370
|
this.passphrase = raw && raw.trim() ? raw : void 0;
|
|
3371
|
+
this.keyProblem = resolved2.problem;
|
|
3500
3372
|
}
|
|
3501
3373
|
/**
|
|
3502
3374
|
* True when the *next write* will encrypt. This is the configured policy,
|
|
@@ -3511,7 +3383,8 @@ var FileSecretStore = class {
|
|
|
3511
3383
|
* no file yet, or when it cannot be read or parsed.
|
|
3512
3384
|
*
|
|
3513
3385
|
* Separate from {@link encrypted} because the two genuinely disagree for a
|
|
3514
|
-
* whole session: adding `MCP_INSPECTOR_SECRET_KEY`
|
|
3386
|
+
* whole session: adding a passphrase (`MCP_INSPECTOR_SECRET_KEY` or
|
|
3387
|
+
* `MCP_INSPECTOR_SECRET_KEY_FILE`) to an install that
|
|
3515
3388
|
* already has a plaintext file flips `encrypted` to true immediately,
|
|
3516
3389
|
* while the bytes stay readable until the next `set`. A descriptor built
|
|
3517
3390
|
* from the policy would tell that user "File (encrypted)" while their
|
|
@@ -3535,6 +3408,9 @@ var FileSecretStore = class {
|
|
|
3535
3408
|
* `set` is in fact about to refuse.
|
|
3536
3409
|
*/
|
|
3537
3410
|
async readOnDiskEncryption() {
|
|
3411
|
+
if (this.keyProblem !== void 0) {
|
|
3412
|
+
return { state: "unreadable", detail: this.keyProblem };
|
|
3413
|
+
}
|
|
3538
3414
|
let raw;
|
|
3539
3415
|
try {
|
|
3540
3416
|
raw = await readStoreFile(this.filePath);
|
|
@@ -3572,7 +3448,7 @@ var FileSecretStore = class {
|
|
|
3572
3448
|
} catch (err) {
|
|
3573
3449
|
return {
|
|
3574
3450
|
state: "unreadable",
|
|
3575
|
-
detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV}` : err instanceof Error ? err.message : String(err)
|
|
3451
|
+
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)
|
|
3576
3452
|
};
|
|
3577
3453
|
}
|
|
3578
3454
|
return { state: "encrypted" };
|
|
@@ -3623,6 +3499,11 @@ var FileSecretStore = class {
|
|
|
3623
3499
|
return this.serialize(() => this.readMap());
|
|
3624
3500
|
}
|
|
3625
3501
|
async readMap() {
|
|
3502
|
+
if (this.keyProblem !== void 0) {
|
|
3503
|
+
throw new SecretStoreUnavailableError(
|
|
3504
|
+
`The secrets file at ${this.filePath} cannot be used: ${this.keyProblem}. Refusing to read or write it rather than store secrets unencrypted.`
|
|
3505
|
+
);
|
|
3506
|
+
}
|
|
3626
3507
|
const raw = await readStoreFile(this.filePath);
|
|
3627
3508
|
if (raw === null) return null;
|
|
3628
3509
|
let parsed;
|
|
@@ -3659,7 +3540,9 @@ var FileSecretStore = class {
|
|
|
3659
3540
|
let plain;
|
|
3660
3541
|
try {
|
|
3661
3542
|
const key = await this.deriveKey(parsed.kdf);
|
|
3662
|
-
const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv
|
|
3543
|
+
const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv, {
|
|
3544
|
+
authTagLength: GCM_TAG_BYTES
|
|
3545
|
+
});
|
|
3663
3546
|
decipher.setAuthTag(parts.tag);
|
|
3664
3547
|
plain = Buffer.concat([
|
|
3665
3548
|
decipher.update(parts.body),
|
|
@@ -3693,7 +3576,9 @@ var FileSecretStore = class {
|
|
|
3693
3576
|
};
|
|
3694
3577
|
const key = await this.deriveKey(kdf);
|
|
3695
3578
|
const iv = crypto2.randomBytes(IV_BYTES);
|
|
3696
|
-
const cipher = crypto2.createCipheriv(CIPHER, key, iv
|
|
3579
|
+
const cipher = crypto2.createCipheriv(CIPHER, key, iv, {
|
|
3580
|
+
authTagLength: GCM_TAG_BYTES
|
|
3581
|
+
});
|
|
3697
3582
|
const body = Buffer.concat([
|
|
3698
3583
|
cipher.update(JSON.stringify(map), "utf-8"),
|
|
3699
3584
|
cipher.final()
|
|
@@ -3858,9 +3743,36 @@ var FileSecretStore = class {
|
|
|
3858
3743
|
const found = {};
|
|
3859
3744
|
for (const field of fields) {
|
|
3860
3745
|
const value = map?.[buildAccount2(serverId, field)];
|
|
3861
|
-
if (value !== void 0) found
|
|
3746
|
+
if (value !== void 0) setOwnEntry(found, field, value);
|
|
3747
|
+
}
|
|
3748
|
+
setOwnEntry(out, serverId, found);
|
|
3749
|
+
}
|
|
3750
|
+
return out;
|
|
3751
|
+
}
|
|
3752
|
+
/**
|
|
3753
|
+
* Bulk twin of {@link getStrict}: the same single-pass read as
|
|
3754
|
+
* {@link getMany}, but an unreadable store throws (with the typed advice
|
|
3755
|
+
* {@link getStrict} attaches) instead of yielding no fields — required by
|
|
3756
|
+
* OAuth read hydration, whose result later drives store deletions.
|
|
3757
|
+
*/
|
|
3758
|
+
async getManyStrict(requests) {
|
|
3759
|
+
let map;
|
|
3760
|
+
try {
|
|
3761
|
+
map = await this.serialize(() => this.readMap());
|
|
3762
|
+
} catch (err) {
|
|
3763
|
+
if (err instanceof SecretStoreUnavailableError) throw err;
|
|
3764
|
+
throw new SecretStoreUnavailableError(
|
|
3765
|
+
`Could not read the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
const out = {};
|
|
3769
|
+
for (const { serverId, fields } of requests) {
|
|
3770
|
+
const found = {};
|
|
3771
|
+
for (const field of fields) {
|
|
3772
|
+
const value = map?.[buildAccount2(serverId, field)];
|
|
3773
|
+
if (value !== void 0) setOwnEntry(found, field, value);
|
|
3862
3774
|
}
|
|
3863
|
-
out
|
|
3775
|
+
setOwnEntry(out, serverId, found);
|
|
3864
3776
|
}
|
|
3865
3777
|
return out;
|
|
3866
3778
|
}
|
|
@@ -3904,6 +3816,9 @@ var FileSecretStore = class {
|
|
|
3904
3816
|
*/
|
|
3905
3817
|
async setMany(serverId, values) {
|
|
3906
3818
|
if (Object.keys(values).length === 0) return;
|
|
3819
|
+
for (const [field, value] of Object.entries(values)) {
|
|
3820
|
+
assertSecretString(field, value);
|
|
3821
|
+
}
|
|
3907
3822
|
await this.serialize(
|
|
3908
3823
|
() => this.mutate((map) => {
|
|
3909
3824
|
const next = { ...map };
|
|
@@ -3915,6 +3830,7 @@ var FileSecretStore = class {
|
|
|
3915
3830
|
);
|
|
3916
3831
|
}
|
|
3917
3832
|
async set(serverId, field, value) {
|
|
3833
|
+
assertSecretString(field, value);
|
|
3918
3834
|
await this.serialize(
|
|
3919
3835
|
() => this.mutate((map) => ({
|
|
3920
3836
|
...map,
|
|
@@ -3930,9 +3846,11 @@ var FileSecretStore = class {
|
|
|
3930
3846
|
await this.deleteWhere((key) => key.startsWith(prefix));
|
|
3931
3847
|
}
|
|
3932
3848
|
/**
|
|
3933
|
-
* Shared delete body.
|
|
3934
|
-
*
|
|
3935
|
-
*
|
|
3849
|
+
* Shared delete body. Same confirmed-delete contract as the keyring
|
|
3850
|
+
* store: a deletion that cannot be confirmed (unreadable file, failed
|
|
3851
|
+
* decrypt, failed write) must escape, because reporting success would
|
|
3852
|
+
* let a caller commit state that assumes the entry is gone — once the
|
|
3853
|
+
* file becomes readable again, a later read would resurrect it.
|
|
3936
3854
|
*/
|
|
3937
3855
|
async deleteWhere(match) {
|
|
3938
3856
|
try {
|
|
@@ -3945,7 +3863,12 @@ var FileSecretStore = class {
|
|
|
3945
3863
|
return next;
|
|
3946
3864
|
})
|
|
3947
3865
|
);
|
|
3948
|
-
} catch {
|
|
3866
|
+
} catch (err) {
|
|
3867
|
+
if (err instanceof SecretStoreUnavailableError) throw err;
|
|
3868
|
+
throw new SecretStoreUnavailableError(
|
|
3869
|
+
`Could not delete from the secrets file: ${err instanceof Error ? err.message : String(err)}`,
|
|
3870
|
+
{ cause: err }
|
|
3871
|
+
);
|
|
3949
3872
|
}
|
|
3950
3873
|
}
|
|
3951
3874
|
};
|
|
@@ -3978,6 +3901,7 @@ async function readSecretFilePermissions(filePath) {
|
|
|
3978
3901
|
var SECRET_STORE_ENV = "MCP_INSPECTOR_SECRET_STORE";
|
|
3979
3902
|
var SECRET_FILE_ENV = "MCP_INSPECTOR_SECRET_FILE";
|
|
3980
3903
|
var STORAGE_DIR_ENV = "MCP_STORAGE_DIR";
|
|
3904
|
+
var SECRET_STORAGE_DOCS_URL = "https://github.com/modelcontextprotocol/inspector/blob/main/docs/secret-storage.md";
|
|
3981
3905
|
var KINDS = ["keyring", "file", "memory"];
|
|
3982
3906
|
function parseSecretStoreEnv(raw) {
|
|
3983
3907
|
const value = raw?.trim().toLowerCase();
|
|
@@ -4120,6 +4044,11 @@ function warnAboutSecretStorage(info) {
|
|
|
4120
4044
|
}
|
|
4121
4045
|
const caveat = secretStorageCaveat(info);
|
|
4122
4046
|
if (caveat) console.warn(`[mcp-inspector] ${caveat}`);
|
|
4047
|
+
if (info.reason === "fallback" || caveat) {
|
|
4048
|
+
console.warn(
|
|
4049
|
+
`[mcp-inspector] How the secret store is chosen, and how to secure it: ${SECRET_STORAGE_DOCS_URL}`
|
|
4050
|
+
);
|
|
4051
|
+
}
|
|
4123
4052
|
}
|
|
4124
4053
|
var resolved;
|
|
4125
4054
|
function resolveSecretStore() {
|
|
@@ -4200,150 +4129,1432 @@ async function handOffStagedSecrets(staged, filePath, keyring) {
|
|
|
4200
4129
|
[mcp-inspector] Secrets were moved into the OS keychain, but the temporary copy at ${staged} could not be removed (${err instanceof Error ? err.message : String(err)}). Delete it once you have confirmed the keychain has your secrets.`
|
|
4201
4130
|
);
|
|
4202
4131
|
}
|
|
4203
|
-
return;
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
try {
|
|
4135
|
+
await fs4.link(staged, filePath);
|
|
4136
|
+
await fs4.rm(staged, { force: true });
|
|
4137
|
+
} catch {
|
|
4138
|
+
console.warn(
|
|
4139
|
+
`
|
|
4140
|
+
[mcp-inspector] Could not restore ${staged} to ${filePath}; both files exist and the snapshot holds the secrets that were not migrated. Merge it by hand, or delete it once you have confirmed nothing is missing.`
|
|
4141
|
+
);
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
var isSnapshotName = (name, base) => name.startsWith(`${base}.migrating-`) && !name.endsWith(".lock");
|
|
4145
|
+
async function anythingToMigrate(filePath) {
|
|
4146
|
+
const base = path4.basename(filePath);
|
|
4147
|
+
try {
|
|
4148
|
+
return (await fs4.readdir(path4.dirname(filePath))).some(
|
|
4149
|
+
(name) => name === base || isSnapshotName(name, base)
|
|
4150
|
+
);
|
|
4151
|
+
} catch (err) {
|
|
4152
|
+
const code = err.code;
|
|
4153
|
+
return code !== "ENOENT" && code !== "ENOTDIR";
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
async function recoverOrphanedSnapshots(filePath) {
|
|
4157
|
+
const dir = path4.dirname(filePath);
|
|
4158
|
+
const base = path4.basename(filePath);
|
|
4159
|
+
let names;
|
|
4160
|
+
try {
|
|
4161
|
+
names = (await fs4.readdir(dir)).filter((n) => isSnapshotName(n, base));
|
|
4162
|
+
} catch {
|
|
4163
|
+
return;
|
|
4164
|
+
}
|
|
4165
|
+
for (const name of names) {
|
|
4166
|
+
const orphan = path4.join(dir, name);
|
|
4167
|
+
if (await isFileLockHeld(orphan)) continue;
|
|
4168
|
+
try {
|
|
4169
|
+
await fs4.link(orphan, filePath);
|
|
4170
|
+
await fs4.rm(orphan, { force: true });
|
|
4171
|
+
console.warn(
|
|
4172
|
+
`
|
|
4173
|
+
[mcp-inspector] Recovered secrets left behind by an interrupted migration (${orphan} \u2192 ${filePath}).`
|
|
4174
|
+
);
|
|
4175
|
+
} catch {
|
|
4176
|
+
console.warn(
|
|
4177
|
+
`
|
|
4178
|
+
[mcp-inspector] Found secrets from an interrupted migration at ${orphan}, and a current ${filePath} already exists, so it was left alone. It may hold values the current file does not.`
|
|
4179
|
+
);
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
async function handOff(file, filePath, keyring) {
|
|
4184
|
+
let entries;
|
|
4185
|
+
try {
|
|
4186
|
+
entries = await file.readAll();
|
|
4187
|
+
} catch (err) {
|
|
4188
|
+
console.warn(
|
|
4189
|
+
`
|
|
4190
|
+
[mcp-inspector] The OS keychain is available again, but the secrets file at ${filePath} could not be read, so its contents were left there: ${err instanceof Error ? err.message : String(err)}`
|
|
4191
|
+
);
|
|
4192
|
+
return false;
|
|
4193
|
+
}
|
|
4194
|
+
if (!entries || Object.keys(entries).length === 0) return false;
|
|
4195
|
+
let complete = true;
|
|
4196
|
+
try {
|
|
4197
|
+
for (const [account, value] of Object.entries(entries)) {
|
|
4198
|
+
const parsed = parseAccount(account);
|
|
4199
|
+
if (!parsed) {
|
|
4200
|
+
complete = false;
|
|
4201
|
+
continue;
|
|
4202
|
+
}
|
|
4203
|
+
const existing = await secretStoreGetStrict(
|
|
4204
|
+
keyring,
|
|
4205
|
+
parsed.serverId,
|
|
4206
|
+
parsed.field
|
|
4207
|
+
);
|
|
4208
|
+
if (existing === null) {
|
|
4209
|
+
await keyring.set(parsed.serverId, parsed.field, value);
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
} catch (err) {
|
|
4213
|
+
console.warn(
|
|
4214
|
+
`
|
|
4215
|
+
[mcp-inspector] Could not move the secrets file at ${filePath} into the OS keychain, so it has been left in place: ${err instanceof Error ? err.message : String(err)}`
|
|
4216
|
+
);
|
|
4217
|
+
return false;
|
|
4218
|
+
}
|
|
4219
|
+
if (!complete) {
|
|
4220
|
+
console.warn(
|
|
4221
|
+
`
|
|
4222
|
+
[mcp-inspector] Secrets from ${filePath} have been copied into the OS keychain, but at least one entry could not be read (its key is not in \`serverId:field\` form). The file has been left in place so nothing is lost.`
|
|
4223
|
+
);
|
|
4224
|
+
return false;
|
|
4225
|
+
}
|
|
4226
|
+
return true;
|
|
4227
|
+
}
|
|
4228
|
+
async function getSecretStorageInfo() {
|
|
4229
|
+
const { store, info } = await resolveSecretStore();
|
|
4230
|
+
if (info.kind !== "file" || !(store instanceof FileSecretStore)) return info;
|
|
4231
|
+
return describeFileStore(
|
|
4232
|
+
store,
|
|
4233
|
+
store.filePath,
|
|
4234
|
+
"file",
|
|
4235
|
+
info.reason,
|
|
4236
|
+
info.detail
|
|
4237
|
+
);
|
|
4238
|
+
}
|
|
4239
|
+
var DeferredSecretStore = class {
|
|
4240
|
+
async target() {
|
|
4241
|
+
return (await resolveSecretStore()).store;
|
|
4242
|
+
}
|
|
4243
|
+
async isDurable() {
|
|
4244
|
+
return secretStoreIsDurable(await this.target());
|
|
4245
|
+
}
|
|
4246
|
+
/**
|
|
4247
|
+
* Forwarded, and its absence was a real defect rather than an omission:
|
|
4248
|
+
* this is the store production actually uses, so a `secretStoreGetStrict`
|
|
4249
|
+
* call against it fell through to the tolerant `get` and the strictness
|
|
4250
|
+
* existed only in tests that injected a concrete store.
|
|
4251
|
+
*/
|
|
4252
|
+
async getStrict(serverId, field) {
|
|
4253
|
+
return secretStoreGetStrict(await this.target(), serverId, field);
|
|
4254
|
+
}
|
|
4255
|
+
async getMany(requests) {
|
|
4256
|
+
return secretStoreGetMany(await this.target(), requests);
|
|
4257
|
+
}
|
|
4258
|
+
async getManyStrict(requests) {
|
|
4259
|
+
return secretStoreGetManyStrict(await this.target(), requests);
|
|
4260
|
+
}
|
|
4261
|
+
async get(serverId, field) {
|
|
4262
|
+
return (await this.target()).get(serverId, field);
|
|
4263
|
+
}
|
|
4264
|
+
async set(serverId, field, value) {
|
|
4265
|
+
await (await this.target()).set(serverId, field, value);
|
|
4266
|
+
}
|
|
4267
|
+
async setMany(serverId, values) {
|
|
4268
|
+
await secretStoreSetMany(await this.target(), serverId, values);
|
|
4269
|
+
}
|
|
4270
|
+
async delete(serverId, field) {
|
|
4271
|
+
await (await this.target()).delete(serverId, field);
|
|
4272
|
+
}
|
|
4273
|
+
async deleteAllForServer(serverId) {
|
|
4274
|
+
await (await this.target()).deleteAllForServer(serverId);
|
|
4275
|
+
}
|
|
4276
|
+
};
|
|
4277
|
+
function defaultSecretStore() {
|
|
4278
|
+
return new DeferredSecretStore();
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
// ../../core/auth/node/oauth-secrets.ts
|
|
4282
|
+
import { OAuthTokensSchema as OAuthTokensSchema2 } from "@modelcontextprotocol/core";
|
|
4283
|
+
var PERSIST_TOKENS_ENV = "MCP_INSPECTOR_PERSIST_TOKENS";
|
|
4284
|
+
var warnedPolicyValues = /* @__PURE__ */ new Set();
|
|
4285
|
+
function getPersistTokensPolicy(env = process.env) {
|
|
4286
|
+
const raw = env[PERSIST_TOKENS_ENV];
|
|
4287
|
+
if (raw === void 0 || raw === "") return "all";
|
|
4288
|
+
if (raw === "all" || raw === "access" || raw === "none") return raw;
|
|
4289
|
+
if (!warnedPolicyValues.has(raw)) {
|
|
4290
|
+
warnedPolicyValues.add(raw);
|
|
4291
|
+
console.warn(
|
|
4292
|
+
`[mcp-inspector] Ignoring invalid ${PERSIST_TOKENS_ENV}=${JSON.stringify(raw)} (expected "all", "access", or "none"); persisting all tokens.`
|
|
4293
|
+
);
|
|
4294
|
+
}
|
|
4295
|
+
return "all";
|
|
4296
|
+
}
|
|
4297
|
+
var oauthSecretServerId = (serverUrl) => `oauth+${encodeURIComponent(serverUrl)}`;
|
|
4298
|
+
var oauthIdpSecretServerId = (issuer) => `oauth-idp+${encodeURIComponent(issuer)}`;
|
|
4299
|
+
var issuerTokensField = (issuer) => `tokens:${issuer}`;
|
|
4300
|
+
var issuerClientSecretField = (issuer) => `client-secret:${issuer}`;
|
|
4301
|
+
var issuerRegistrationTokenField = (issuer) => `registration-token:${issuer}`;
|
|
4302
|
+
var LEGACY_TOKENS_FIELD = "tokens";
|
|
4303
|
+
var LEGACY_CLIENT_SECRET_FIELD = "client-secret";
|
|
4304
|
+
var LEGACY_REGISTRATION_TOKEN_FIELD = "registration-token";
|
|
4305
|
+
var PREREG_CLIENT_SECRET_FIELD = "prereg-client-secret";
|
|
4306
|
+
var PREREG_REGISTRATION_TOKEN_FIELD = "prereg-registration-token";
|
|
4307
|
+
var IDP_SESSION_FIELD = "idp-session";
|
|
4308
|
+
function applyTokensPolicy(tokens, policy) {
|
|
4309
|
+
if (policy === "none") return void 0;
|
|
4310
|
+
if (policy === "access" && tokens.refresh_token !== void 0) {
|
|
4311
|
+
const { refresh_token: _stripped, ...rest } = tokens;
|
|
4312
|
+
return rest;
|
|
4313
|
+
}
|
|
4314
|
+
return tokens;
|
|
4315
|
+
}
|
|
4316
|
+
var StorableTokensSchema = OAuthTokensSchema2.partial();
|
|
4317
|
+
function hasSecretBearingTokenField(tokens) {
|
|
4318
|
+
return tokens.access_token !== void 0 || tokens.refresh_token !== void 0 || tokens.id_token !== void 0;
|
|
4319
|
+
}
|
|
4320
|
+
function splitTokens(tokens, policy) {
|
|
4321
|
+
const kept = applyTokensPolicy(tokens, policy);
|
|
4322
|
+
if (!kept) return {};
|
|
4323
|
+
if (!StorableTokensSchema.safeParse(kept).success) {
|
|
4324
|
+
return { plaintext: kept };
|
|
4325
|
+
}
|
|
4326
|
+
if (!hasSecretBearingTokenField(kept)) {
|
|
4327
|
+
return {};
|
|
4328
|
+
}
|
|
4329
|
+
return { secret: JSON.stringify(kept) };
|
|
4330
|
+
}
|
|
4331
|
+
function splitClientInformation(info, fields, secrets) {
|
|
4332
|
+
const { client_secret, registration_access_token, ...publicInfo } = info;
|
|
4333
|
+
if (client_secret === void 0 && registration_access_token === void 0) {
|
|
4334
|
+
return info;
|
|
4335
|
+
}
|
|
4336
|
+
if (client_secret !== void 0) {
|
|
4337
|
+
secrets[fields.clientSecret] = client_secret;
|
|
4338
|
+
}
|
|
4339
|
+
if (registration_access_token !== void 0) {
|
|
4340
|
+
secrets[fields.registrationToken] = registration_access_token;
|
|
4341
|
+
}
|
|
4342
|
+
return publicInfo;
|
|
4343
|
+
}
|
|
4344
|
+
function joinClientInformation(info, fields, secrets) {
|
|
4345
|
+
const clientSecret = secrets[fields.clientSecret];
|
|
4346
|
+
const registrationToken = secrets[fields.registrationToken];
|
|
4347
|
+
if (clientSecret === void 0 && registrationToken === void 0) {
|
|
4348
|
+
return info;
|
|
4349
|
+
}
|
|
4350
|
+
return {
|
|
4351
|
+
...info,
|
|
4352
|
+
...clientSecret !== void 0 && { client_secret: clientSecret },
|
|
4353
|
+
...registrationToken !== void 0 && {
|
|
4354
|
+
registration_access_token: registrationToken
|
|
4355
|
+
}
|
|
4356
|
+
};
|
|
4357
|
+
}
|
|
4358
|
+
var issuerClientInfoFields = (issuer) => ({
|
|
4359
|
+
clientSecret: issuerClientSecretField(issuer),
|
|
4360
|
+
registrationToken: issuerRegistrationTokenField(issuer)
|
|
4361
|
+
});
|
|
4362
|
+
var LEGACY_CLIENT_INFO_FIELDS = {
|
|
4363
|
+
clientSecret: LEGACY_CLIENT_SECRET_FIELD,
|
|
4364
|
+
registrationToken: LEGACY_REGISTRATION_TOKEN_FIELD
|
|
4365
|
+
};
|
|
4366
|
+
var PREREG_CLIENT_INFO_FIELDS = {
|
|
4367
|
+
clientSecret: PREREG_CLIENT_SECRET_FIELD,
|
|
4368
|
+
registrationToken: PREREG_REGISTRATION_TOKEN_FIELD
|
|
4369
|
+
};
|
|
4370
|
+
function splitServerOAuthState(state, policy) {
|
|
4371
|
+
const secrets = {};
|
|
4372
|
+
const residue = { ...state };
|
|
4373
|
+
if (state.byIssuer) {
|
|
4374
|
+
const byIssuer = {};
|
|
4375
|
+
for (const [issuer, slot] of Object.entries(state.byIssuer)) {
|
|
4376
|
+
const slotResidue = { ...slot };
|
|
4377
|
+
if (slot.tokens) {
|
|
4378
|
+
delete slotResidue.tokens;
|
|
4379
|
+
const tokenSplit = splitTokens(slot.tokens, policy);
|
|
4380
|
+
if (tokenSplit.secret !== void 0) {
|
|
4381
|
+
secrets[issuerTokensField(issuer)] = tokenSplit.secret;
|
|
4382
|
+
}
|
|
4383
|
+
if (tokenSplit.plaintext) slotResidue.tokens = tokenSplit.plaintext;
|
|
4384
|
+
}
|
|
4385
|
+
if (slot.clientInformation) {
|
|
4386
|
+
slotResidue.clientInformation = splitClientInformation(
|
|
4387
|
+
slot.clientInformation,
|
|
4388
|
+
issuerClientInfoFields(issuer),
|
|
4389
|
+
secrets
|
|
4390
|
+
);
|
|
4391
|
+
}
|
|
4392
|
+
setOwnEntry(byIssuer, issuer, slotResidue);
|
|
4393
|
+
}
|
|
4394
|
+
residue.byIssuer = byIssuer;
|
|
4395
|
+
}
|
|
4396
|
+
if (state.tokens) {
|
|
4397
|
+
delete residue.tokens;
|
|
4398
|
+
const tokenSplit = splitTokens(state.tokens, policy);
|
|
4399
|
+
if (tokenSplit.secret !== void 0) {
|
|
4400
|
+
secrets[LEGACY_TOKENS_FIELD] = tokenSplit.secret;
|
|
4401
|
+
}
|
|
4402
|
+
if (tokenSplit.plaintext) residue.tokens = tokenSplit.plaintext;
|
|
4403
|
+
}
|
|
4404
|
+
if (state.clientInformation) {
|
|
4405
|
+
residue.clientInformation = splitClientInformation(
|
|
4406
|
+
state.clientInformation,
|
|
4407
|
+
LEGACY_CLIENT_INFO_FIELDS,
|
|
4408
|
+
secrets
|
|
4409
|
+
);
|
|
4410
|
+
}
|
|
4411
|
+
if (state.preregisteredClientInformation) {
|
|
4412
|
+
residue.preregisteredClientInformation = splitClientInformation(
|
|
4413
|
+
state.preregisteredClientInformation,
|
|
4414
|
+
PREREG_CLIENT_INFO_FIELDS,
|
|
4415
|
+
secrets
|
|
4416
|
+
);
|
|
4417
|
+
}
|
|
4418
|
+
return { residue, secrets };
|
|
4419
|
+
}
|
|
4420
|
+
function parseStoredTokens(raw) {
|
|
4421
|
+
try {
|
|
4422
|
+
const parsed = JSON.parse(raw);
|
|
4423
|
+
if (StorableTokensSchema.safeParse(parsed).success && hasSecretBearingTokenField(parsed)) {
|
|
4424
|
+
return parsed;
|
|
4425
|
+
}
|
|
4426
|
+
} catch {
|
|
4427
|
+
}
|
|
4428
|
+
return void 0;
|
|
4429
|
+
}
|
|
4430
|
+
function parseStoredIdpSession(raw) {
|
|
4431
|
+
try {
|
|
4432
|
+
const parsed = JSON.parse(raw);
|
|
4433
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
4434
|
+
const { idToken, refreshToken } = parsed;
|
|
4435
|
+
const usable = {
|
|
4436
|
+
...typeof idToken === "string" && { idToken },
|
|
4437
|
+
...typeof refreshToken === "string" && { refreshToken }
|
|
4438
|
+
};
|
|
4439
|
+
if (Object.keys(usable).length > 0) return usable;
|
|
4440
|
+
}
|
|
4441
|
+
} catch {
|
|
4442
|
+
}
|
|
4443
|
+
return void 0;
|
|
4444
|
+
}
|
|
4445
|
+
function isUsableStoredSecret(field, raw) {
|
|
4446
|
+
if (field === LEGACY_TOKENS_FIELD || field.startsWith("tokens:")) {
|
|
4447
|
+
return parseStoredTokens(raw) !== void 0;
|
|
4448
|
+
}
|
|
4449
|
+
if (field === IDP_SESSION_FIELD) {
|
|
4450
|
+
return parseStoredIdpSession(raw) !== void 0;
|
|
4451
|
+
}
|
|
4452
|
+
return true;
|
|
4453
|
+
}
|
|
4454
|
+
function joinServerOAuthState(residue, secrets) {
|
|
4455
|
+
const joined = { ...residue };
|
|
4456
|
+
if (residue.byIssuer) {
|
|
4457
|
+
const byIssuer = {};
|
|
4458
|
+
for (const [issuer, slot] of Object.entries(residue.byIssuer)) {
|
|
4459
|
+
const joinedSlot = { ...slot };
|
|
4460
|
+
const tokensRaw = secrets[issuerTokensField(issuer)];
|
|
4461
|
+
if (tokensRaw !== void 0) {
|
|
4462
|
+
const tokens = parseStoredTokens(tokensRaw);
|
|
4463
|
+
if (tokens) joinedSlot.tokens = tokens;
|
|
4464
|
+
}
|
|
4465
|
+
if (joinedSlot.clientInformation) {
|
|
4466
|
+
joinedSlot.clientInformation = joinClientInformation(
|
|
4467
|
+
joinedSlot.clientInformation,
|
|
4468
|
+
issuerClientInfoFields(issuer),
|
|
4469
|
+
secrets
|
|
4470
|
+
);
|
|
4471
|
+
}
|
|
4472
|
+
setOwnEntry(byIssuer, issuer, joinedSlot);
|
|
4473
|
+
}
|
|
4474
|
+
joined.byIssuer = byIssuer;
|
|
4475
|
+
}
|
|
4476
|
+
const legacyTokensRaw = secrets[LEGACY_TOKENS_FIELD];
|
|
4477
|
+
if (legacyTokensRaw !== void 0) {
|
|
4478
|
+
const tokens = parseStoredTokens(legacyTokensRaw);
|
|
4479
|
+
if (tokens) joined.tokens = tokens;
|
|
4480
|
+
}
|
|
4481
|
+
if (joined.clientInformation) {
|
|
4482
|
+
joined.clientInformation = joinClientInformation(
|
|
4483
|
+
joined.clientInformation,
|
|
4484
|
+
LEGACY_CLIENT_INFO_FIELDS,
|
|
4485
|
+
secrets
|
|
4486
|
+
);
|
|
4487
|
+
}
|
|
4488
|
+
if (joined.preregisteredClientInformation) {
|
|
4489
|
+
joined.preregisteredClientInformation = joinClientInformation(
|
|
4490
|
+
joined.preregisteredClientInformation,
|
|
4491
|
+
PREREG_CLIENT_INFO_FIELDS,
|
|
4492
|
+
secrets
|
|
4493
|
+
);
|
|
4494
|
+
}
|
|
4495
|
+
return joined;
|
|
4496
|
+
}
|
|
4497
|
+
function splitIdpSession(session, policy) {
|
|
4498
|
+
const { idToken, refreshToken, ...residue } = session;
|
|
4499
|
+
const secrets = {};
|
|
4500
|
+
if (policy !== "none" && (idToken !== void 0 || refreshToken !== void 0)) {
|
|
4501
|
+
const kept = {
|
|
4502
|
+
...typeof idToken === "string" && { idToken },
|
|
4503
|
+
...policy === "all" && typeof refreshToken === "string" && { refreshToken }
|
|
4504
|
+
};
|
|
4505
|
+
if (Object.keys(kept).length > 0) {
|
|
4506
|
+
secrets[IDP_SESSION_FIELD] = JSON.stringify(kept);
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
return { residue, secrets };
|
|
4510
|
+
}
|
|
4511
|
+
function joinIdpSession(residue, secrets) {
|
|
4512
|
+
const raw = secrets[IDP_SESSION_FIELD];
|
|
4513
|
+
if (raw === void 0) return { ...residue };
|
|
4514
|
+
const stored = parseStoredIdpSession(raw);
|
|
4515
|
+
return stored ? { ...residue, ...stored } : { ...residue };
|
|
4516
|
+
}
|
|
4517
|
+
function serverSecretFields(state) {
|
|
4518
|
+
const fields = /* @__PURE__ */ new Set([
|
|
4519
|
+
LEGACY_TOKENS_FIELD,
|
|
4520
|
+
LEGACY_CLIENT_SECRET_FIELD,
|
|
4521
|
+
LEGACY_REGISTRATION_TOKEN_FIELD,
|
|
4522
|
+
PREREG_CLIENT_SECRET_FIELD,
|
|
4523
|
+
PREREG_REGISTRATION_TOKEN_FIELD
|
|
4524
|
+
]);
|
|
4525
|
+
for (const issuer of Object.keys(state?.byIssuer ?? {})) {
|
|
4526
|
+
fields.add(issuerTokensField(issuer));
|
|
4527
|
+
fields.add(issuerClientSecretField(issuer));
|
|
4528
|
+
fields.add(issuerRegistrationTokenField(issuer));
|
|
4529
|
+
}
|
|
4530
|
+
return [...fields];
|
|
4531
|
+
}
|
|
4532
|
+
function clientInfoHasPlaintext(info) {
|
|
4533
|
+
return info !== void 0 && (info.client_secret !== void 0 || info.registration_access_token !== void 0);
|
|
4534
|
+
}
|
|
4535
|
+
function snapshotHasPlaintextSecrets(snapshot) {
|
|
4536
|
+
for (const state of Object.values(snapshot.servers)) {
|
|
4537
|
+
if (state.tokens) return true;
|
|
4538
|
+
if (clientInfoHasPlaintext(state.clientInformation)) return true;
|
|
4539
|
+
if (clientInfoHasPlaintext(state.preregisteredClientInformation)) {
|
|
4540
|
+
return true;
|
|
4541
|
+
}
|
|
4542
|
+
for (const slot of Object.values(state.byIssuer ?? {})) {
|
|
4543
|
+
if (slot.tokens) return true;
|
|
4544
|
+
if (clientInfoHasPlaintext(slot.clientInformation)) return true;
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
for (const session of Object.values(snapshot.idpSessions)) {
|
|
4548
|
+
if (session.idToken !== void 0 || session.refreshToken !== void 0) {
|
|
4549
|
+
return true;
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
return false;
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
// ../../core/auth/node/oauth-persist-file.ts
|
|
4556
|
+
var warnedStoreFailures = /* @__PURE__ */ new Set();
|
|
4557
|
+
function warnStoreWriteFailure(error) {
|
|
4558
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
4559
|
+
if (warnedStoreFailures.has(reason)) return;
|
|
4560
|
+
warnedStoreFailures.add(reason);
|
|
4561
|
+
console.warn(
|
|
4562
|
+
`[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.`
|
|
4563
|
+
);
|
|
4564
|
+
}
|
|
4565
|
+
function warnRestoreFailure(error) {
|
|
4566
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
4567
|
+
const key = `restore:${reason}`;
|
|
4568
|
+
if (warnedStoreFailures.has(key)) return;
|
|
4569
|
+
warnedStoreFailures.add(key);
|
|
4570
|
+
console.warn(
|
|
4571
|
+
`[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.`
|
|
4572
|
+
);
|
|
4573
|
+
}
|
|
4574
|
+
function warnUnconfirmedFile(error) {
|
|
4575
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
4576
|
+
const key = `unconfirmed:${reason}`;
|
|
4577
|
+
if (warnedStoreFailures.has(key)) return;
|
|
4578
|
+
warnedStoreFailures.add(key);
|
|
4579
|
+
console.warn(
|
|
4580
|
+
`[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.`
|
|
4581
|
+
);
|
|
4582
|
+
}
|
|
4583
|
+
function warnMigrationFailure(error) {
|
|
4584
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
4585
|
+
const key = `migration:${reason}`;
|
|
4586
|
+
if (warnedStoreFailures.has(key)) return;
|
|
4587
|
+
warnedStoreFailures.add(key);
|
|
4588
|
+
console.warn(
|
|
4589
|
+
`[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.`
|
|
4590
|
+
);
|
|
4591
|
+
}
|
|
4592
|
+
function rethrowLockError(filePath, error, action = "save") {
|
|
4593
|
+
if (error instanceof SecretFileLockHeldError) {
|
|
4594
|
+
throw new SecretFileLockHeldError(
|
|
4595
|
+
`Could not ${action} OAuth state: the state file at ${filePath} is locked by another Inspector process and did not become available.`,
|
|
4596
|
+
{ cause: error }
|
|
4597
|
+
);
|
|
4598
|
+
}
|
|
4599
|
+
throw error;
|
|
4600
|
+
}
|
|
4601
|
+
async function withOAuthStateLock(filePath, action, body) {
|
|
4602
|
+
let entered = false;
|
|
4603
|
+
try {
|
|
4604
|
+
return await withSecretFileLock(filePath, async () => {
|
|
4605
|
+
entered = true;
|
|
4606
|
+
return body();
|
|
4607
|
+
});
|
|
4608
|
+
} catch (error) {
|
|
4609
|
+
if (!entered) rethrowLockError(filePath, error, action);
|
|
4610
|
+
throw error;
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
var OAuthStateFileUnrecognizedError = class extends Error {
|
|
4614
|
+
constructor(filePath, action) {
|
|
4615
|
+
super(
|
|
4616
|
+
`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.`
|
|
4617
|
+
);
|
|
4618
|
+
this.name = "OAuthStateFileUnrecognizedError";
|
|
4619
|
+
}
|
|
4620
|
+
};
|
|
4621
|
+
async function readDiskForMutation(filePath, action) {
|
|
4622
|
+
const raw = await readStoreFile(filePath);
|
|
4623
|
+
const parsed = parseOAuthPersistBlob(raw);
|
|
4624
|
+
if (raw !== null && parsed === null) {
|
|
4625
|
+
throw new OAuthStateFileUnrecognizedError(filePath, action);
|
|
4626
|
+
}
|
|
4627
|
+
return parsed;
|
|
4628
|
+
}
|
|
4629
|
+
async function persistEntrySecrets(store, serverId, candidates, secrets, prior, allowDegrade) {
|
|
4630
|
+
const priorValue = new Map(prior.map(({ field, value }) => [field, value]));
|
|
4631
|
+
const changed = {};
|
|
4632
|
+
for (const [field, value] of Object.entries(secrets)) {
|
|
4633
|
+
if (priorValue.get(field) !== value) changed[field] = value;
|
|
4634
|
+
}
|
|
4635
|
+
try {
|
|
4636
|
+
if (Object.keys(changed).length > 0) {
|
|
4637
|
+
await secretStoreSetMany(store, serverId, changed);
|
|
4638
|
+
}
|
|
4639
|
+
} catch (error) {
|
|
4640
|
+
if (!allowDegrade) throw error;
|
|
4641
|
+
warnStoreWriteFailure(error);
|
|
4642
|
+
const touched = prior.filter(({ field }) => changed[field] !== void 0);
|
|
4643
|
+
let restoreFailure;
|
|
4644
|
+
await restoreSecretFields(store, touched, (err) => {
|
|
4645
|
+
restoreFailure = err;
|
|
4646
|
+
});
|
|
4647
|
+
if (restoreFailure !== void 0) throw restoreFailure;
|
|
4648
|
+
return false;
|
|
4649
|
+
}
|
|
4650
|
+
await settleStoreMutations(
|
|
4651
|
+
candidates.filter(
|
|
4652
|
+
(field) => secrets[field] === void 0 && priorValue.get(field) !== null
|
|
4653
|
+
).map((field) => store.delete(serverId, field))
|
|
4654
|
+
);
|
|
4655
|
+
return true;
|
|
4656
|
+
}
|
|
4657
|
+
function revertEntryToDisk(merged, disk, key) {
|
|
4658
|
+
const diskEntry = getOwnEntry(disk, key);
|
|
4659
|
+
if (diskEntry === void 0) {
|
|
4660
|
+
if (Object.hasOwn(merged, key)) delete merged[key];
|
|
4661
|
+
} else {
|
|
4662
|
+
setOwnEntry(merged, key, diskEntry);
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
function preserveNonDurableSecrets(diskSecrets, secrets) {
|
|
4666
|
+
const keep = {};
|
|
4667
|
+
for (const [field, value] of Object.entries(secrets)) {
|
|
4668
|
+
if (diskSecrets[field] === value) keep[field] = value;
|
|
4669
|
+
}
|
|
4670
|
+
return keep;
|
|
4671
|
+
}
|
|
4672
|
+
async function writeOAuthSections(filePath, snapshot, sections, secretStore = defaultSecretStore()) {
|
|
4673
|
+
const policy = getPersistTokensPolicy();
|
|
4674
|
+
const durable = await secretStoreIsDurable(secretStore);
|
|
4675
|
+
await withOAuthStateLock(filePath, "save", async () => {
|
|
4676
|
+
const restoreBaseline = /* @__PURE__ */ new Map();
|
|
4677
|
+
const ourWrites = /* @__PURE__ */ new Map();
|
|
4678
|
+
const priorKey = (p) => `${p.serverId}\0${p.field}`;
|
|
4679
|
+
const foldPriors = (priors) => {
|
|
4680
|
+
for (const prior of priors) {
|
|
4681
|
+
const key = priorKey(prior);
|
|
4682
|
+
if (restoreBaseline.has(key) && ourWrites.get(key) !== void 0 && prior.value === ourWrites.get(key)) {
|
|
4683
|
+
continue;
|
|
4684
|
+
}
|
|
4685
|
+
restoreBaseline.set(key, prior);
|
|
4686
|
+
}
|
|
4687
|
+
};
|
|
4688
|
+
const restoreToBaseline = () => restoreSecretFields(
|
|
4689
|
+
secretStore,
|
|
4690
|
+
[...restoreBaseline.values()],
|
|
4691
|
+
warnRestoreFailure
|
|
4692
|
+
);
|
|
4693
|
+
let unconfirmed = null;
|
|
4694
|
+
const reconcileFailure = async () => {
|
|
4695
|
+
if (unconfirmed !== null) {
|
|
4696
|
+
const committed = unconfirmed;
|
|
4697
|
+
let observed = null;
|
|
4698
|
+
let readable = true;
|
|
4699
|
+
try {
|
|
4700
|
+
observed = await readStoreFile(filePath);
|
|
4701
|
+
} catch (error) {
|
|
4702
|
+
readable = false;
|
|
4703
|
+
warnUnconfirmedFile(error);
|
|
4704
|
+
}
|
|
4705
|
+
if (readable && observed === committed.written) {
|
|
4706
|
+
const target = new Map(
|
|
4707
|
+
[...restoreBaseline].map(([key, { serverId, field, value }]) => [
|
|
4708
|
+
key,
|
|
4709
|
+
{ serverId, field, value }
|
|
4710
|
+
])
|
|
4711
|
+
);
|
|
4712
|
+
for (const [key, write] of committed.writes) target.set(key, write);
|
|
4713
|
+
try {
|
|
4714
|
+
await settleStoreMutations(
|
|
4715
|
+
[...target.values()].map(
|
|
4716
|
+
({ serverId, field, value }) => value === null ? secretStore.delete(serverId, field) : secretStore.set(serverId, field, value)
|
|
4717
|
+
)
|
|
4718
|
+
);
|
|
4719
|
+
return true;
|
|
4720
|
+
} catch (error) {
|
|
4721
|
+
warnRestoreFailure(error);
|
|
4722
|
+
return false;
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
await restoreToBaseline();
|
|
4727
|
+
return false;
|
|
4728
|
+
};
|
|
4729
|
+
for (let attempt = 0; attempt < MAX_WRITE_ATTEMPTS; attempt++) {
|
|
4730
|
+
const priorSecrets = [];
|
|
4731
|
+
const attemptWrites = /* @__PURE__ */ new Map();
|
|
4732
|
+
let written;
|
|
4733
|
+
try {
|
|
4734
|
+
const disk = await readDiskForMutation(filePath, "save");
|
|
4735
|
+
const effective = sections ? {
|
|
4736
|
+
servers: sections.servers && [...new Set(sections.servers)],
|
|
4737
|
+
idpSessions: sections.idpSessions && [
|
|
4738
|
+
...new Set(sections.idpSessions)
|
|
4739
|
+
]
|
|
4740
|
+
} : {
|
|
4741
|
+
servers: [
|
|
4742
|
+
.../* @__PURE__ */ new Set([
|
|
4743
|
+
...Object.keys(disk?.servers ?? {}),
|
|
4744
|
+
...Object.keys(snapshot.servers)
|
|
4745
|
+
])
|
|
4746
|
+
],
|
|
4747
|
+
idpSessions: [
|
|
4748
|
+
.../* @__PURE__ */ new Set([
|
|
4749
|
+
...Object.keys(disk?.idpSessions ?? {}),
|
|
4750
|
+
...Object.keys(snapshot.idpSessions)
|
|
4751
|
+
])
|
|
4752
|
+
]
|
|
4753
|
+
};
|
|
4754
|
+
const merged = mergeOAuthSections(disk, snapshot, effective);
|
|
4755
|
+
for (const url of effective.servers ?? []) {
|
|
4756
|
+
const serverId = oauthSecretServerId(url);
|
|
4757
|
+
const next = getOwnEntry(snapshot.servers, url);
|
|
4758
|
+
const candidates = [
|
|
4759
|
+
.../* @__PURE__ */ new Set([
|
|
4760
|
+
...serverSecretFields(getOwnEntry(disk?.servers, url)),
|
|
4761
|
+
...serverSecretFields(next)
|
|
4762
|
+
])
|
|
4763
|
+
];
|
|
4764
|
+
const entryPrior = await snapshotSecretFields(
|
|
4765
|
+
secretStore,
|
|
4766
|
+
serverId,
|
|
4767
|
+
candidates
|
|
4768
|
+
);
|
|
4769
|
+
priorSecrets.push(...entryPrior);
|
|
4770
|
+
if (next === void 0) {
|
|
4771
|
+
for (const field of candidates) {
|
|
4772
|
+
ourWrites.set(priorKey({ serverId, field }), null);
|
|
4773
|
+
}
|
|
4774
|
+
await secretStore.deleteAllForServer(serverId);
|
|
4775
|
+
for (const field of candidates) {
|
|
4776
|
+
attemptWrites.set(priorKey({ serverId, field }), {
|
|
4777
|
+
serverId,
|
|
4778
|
+
field,
|
|
4779
|
+
value: null
|
|
4780
|
+
});
|
|
4781
|
+
}
|
|
4782
|
+
continue;
|
|
4783
|
+
}
|
|
4784
|
+
const { residue, secrets } = splitServerOAuthState(next, policy);
|
|
4785
|
+
for (const field of candidates) {
|
|
4786
|
+
ourWrites.set(
|
|
4787
|
+
priorKey({ serverId, field }),
|
|
4788
|
+
secrets[field] ?? null
|
|
4789
|
+
);
|
|
4790
|
+
}
|
|
4791
|
+
setOwnEntry(merged.servers, url, residue);
|
|
4792
|
+
if (!durable) {
|
|
4793
|
+
const diskEntry = getOwnEntry(disk?.servers, url);
|
|
4794
|
+
const keep = preserveNonDurableSecrets(
|
|
4795
|
+
diskEntry ? splitServerOAuthState(diskEntry, policy).secrets : {},
|
|
4796
|
+
secrets
|
|
4797
|
+
);
|
|
4798
|
+
if (Object.keys(keep).length > 0) {
|
|
4799
|
+
setOwnEntry(
|
|
4800
|
+
merged.servers,
|
|
4801
|
+
url,
|
|
4802
|
+
joinServerOAuthState(residue, keep)
|
|
4803
|
+
);
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
const persisted = await persistEntrySecrets(
|
|
4807
|
+
secretStore,
|
|
4808
|
+
serverId,
|
|
4809
|
+
candidates,
|
|
4810
|
+
secrets,
|
|
4811
|
+
entryPrior,
|
|
4812
|
+
// Degrading is only sound when the disk entry is the pre-call
|
|
4813
|
+
// state this attempt's priors pair with; on a retry it may be an
|
|
4814
|
+
// earlier attempt's committed residue, so escalate into the
|
|
4815
|
+
// reconciling exit instead.
|
|
4816
|
+
attempt === 0
|
|
4817
|
+
);
|
|
4818
|
+
if (!persisted) {
|
|
4819
|
+
revertEntryToDisk(merged.servers, disk?.servers, url);
|
|
4820
|
+
continue;
|
|
4821
|
+
}
|
|
4822
|
+
for (const field of candidates) {
|
|
4823
|
+
attemptWrites.set(priorKey({ serverId, field }), {
|
|
4824
|
+
serverId,
|
|
4825
|
+
field,
|
|
4826
|
+
value: secrets[field] ?? null
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
for (const issuer of effective.idpSessions ?? []) {
|
|
4831
|
+
const serverId = oauthIdpSecretServerId(issuer);
|
|
4832
|
+
const next = getOwnEntry(snapshot.idpSessions, issuer);
|
|
4833
|
+
const entryPrior = await snapshotSecretFields(secretStore, serverId, [
|
|
4834
|
+
IDP_SESSION_FIELD
|
|
4835
|
+
]);
|
|
4836
|
+
priorSecrets.push(...entryPrior);
|
|
4837
|
+
if (next === void 0) {
|
|
4838
|
+
ourWrites.set(
|
|
4839
|
+
priorKey({ serverId, field: IDP_SESSION_FIELD }),
|
|
4840
|
+
null
|
|
4841
|
+
);
|
|
4842
|
+
await secretStore.deleteAllForServer(serverId);
|
|
4843
|
+
attemptWrites.set(
|
|
4844
|
+
priorKey({ serverId, field: IDP_SESSION_FIELD }),
|
|
4845
|
+
{
|
|
4846
|
+
serverId,
|
|
4847
|
+
field: IDP_SESSION_FIELD,
|
|
4848
|
+
value: null
|
|
4849
|
+
}
|
|
4850
|
+
);
|
|
4851
|
+
continue;
|
|
4852
|
+
}
|
|
4853
|
+
const { residue, secrets } = splitIdpSession(next, policy);
|
|
4854
|
+
ourWrites.set(
|
|
4855
|
+
priorKey({ serverId, field: IDP_SESSION_FIELD }),
|
|
4856
|
+
secrets[IDP_SESSION_FIELD] ?? null
|
|
4857
|
+
);
|
|
4858
|
+
setOwnEntry(merged.idpSessions, issuer, residue);
|
|
4859
|
+
if (!durable) {
|
|
4860
|
+
const diskSession = getOwnEntry(disk?.idpSessions, issuer);
|
|
4861
|
+
const keep = preserveNonDurableSecrets(
|
|
4862
|
+
diskSession ? splitIdpSession(diskSession, policy).secrets : {},
|
|
4863
|
+
secrets
|
|
4864
|
+
);
|
|
4865
|
+
if (Object.keys(keep).length > 0) {
|
|
4866
|
+
setOwnEntry(
|
|
4867
|
+
merged.idpSessions,
|
|
4868
|
+
issuer,
|
|
4869
|
+
joinIdpSession(residue, keep)
|
|
4870
|
+
);
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
const persisted = await persistEntrySecrets(
|
|
4874
|
+
secretStore,
|
|
4875
|
+
serverId,
|
|
4876
|
+
[IDP_SESSION_FIELD],
|
|
4877
|
+
secrets,
|
|
4878
|
+
entryPrior,
|
|
4879
|
+
// Same as the server loop: degrade on the first attempt only.
|
|
4880
|
+
attempt === 0
|
|
4881
|
+
);
|
|
4882
|
+
if (!persisted) {
|
|
4883
|
+
revertEntryToDisk(merged.idpSessions, disk?.idpSessions, issuer);
|
|
4884
|
+
continue;
|
|
4885
|
+
}
|
|
4886
|
+
attemptWrites.set(priorKey({ serverId, field: IDP_SESSION_FIELD }), {
|
|
4887
|
+
serverId,
|
|
4888
|
+
field: IDP_SESSION_FIELD,
|
|
4889
|
+
value: secrets[IDP_SESSION_FIELD] ?? null
|
|
4890
|
+
});
|
|
4891
|
+
}
|
|
4892
|
+
written = serializeOAuthPersistBlob(merged);
|
|
4893
|
+
await writeStoreFile(filePath, written);
|
|
4894
|
+
unconfirmed = { written, writes: attemptWrites };
|
|
4895
|
+
} catch (error) {
|
|
4896
|
+
foldPriors(priorSecrets);
|
|
4897
|
+
if (await reconcileFailure()) return;
|
|
4898
|
+
throw error;
|
|
4899
|
+
}
|
|
4900
|
+
foldPriors(priorSecrets);
|
|
4901
|
+
let observed;
|
|
4902
|
+
try {
|
|
4903
|
+
observed = await readStoreFile(filePath);
|
|
4904
|
+
} catch {
|
|
4905
|
+
continue;
|
|
4906
|
+
}
|
|
4907
|
+
if (observed === written) return;
|
|
4908
|
+
unconfirmed = null;
|
|
4909
|
+
}
|
|
4910
|
+
if (await reconcileFailure()) return;
|
|
4911
|
+
throw new SecretStoreUnavailableError(
|
|
4912
|
+
`Could not save OAuth state: another process kept overwriting ${filePath} (gave up after ${MAX_WRITE_ATTEMPTS} attempts). Re-run the action to retry.`
|
|
4913
|
+
);
|
|
4914
|
+
});
|
|
4915
|
+
}
|
|
4916
|
+
function secretRequestsFor(snapshot) {
|
|
4917
|
+
const requests = [];
|
|
4918
|
+
for (const [url, state] of Object.entries(snapshot.servers)) {
|
|
4919
|
+
requests.push({
|
|
4920
|
+
serverId: oauthSecretServerId(url),
|
|
4921
|
+
fields: serverSecretFields(state)
|
|
4922
|
+
});
|
|
4923
|
+
}
|
|
4924
|
+
for (const issuer of Object.keys(snapshot.idpSessions)) {
|
|
4925
|
+
requests.push({
|
|
4926
|
+
serverId: oauthIdpSecretServerId(issuer),
|
|
4927
|
+
fields: [IDP_SESSION_FIELD]
|
|
4928
|
+
});
|
|
4929
|
+
}
|
|
4930
|
+
return requests;
|
|
4931
|
+
}
|
|
4932
|
+
async function joinSnapshot(snapshot, secretStore) {
|
|
4933
|
+
const requests = secretRequestsFor(snapshot);
|
|
4934
|
+
const values = requests.length > 0 ? await secretStoreGetManyStrict(secretStore, requests) : {};
|
|
4935
|
+
return {
|
|
4936
|
+
servers: Object.fromEntries(
|
|
4937
|
+
Object.entries(snapshot.servers).map(([url, state]) => [
|
|
4938
|
+
url,
|
|
4939
|
+
joinServerOAuthState(state, values[oauthSecretServerId(url)] ?? {})
|
|
4940
|
+
])
|
|
4941
|
+
),
|
|
4942
|
+
idpSessions: Object.fromEntries(
|
|
4943
|
+
Object.entries(snapshot.idpSessions).map(([issuer, session]) => [
|
|
4944
|
+
issuer,
|
|
4945
|
+
joinIdpSession(session, values[oauthIdpSecretServerId(issuer)] ?? {})
|
|
4946
|
+
])
|
|
4947
|
+
)
|
|
4948
|
+
};
|
|
4949
|
+
}
|
|
4950
|
+
async function migratePlaintextSecrets(filePath, secretStore) {
|
|
4951
|
+
const raw = await readStoreFile(filePath);
|
|
4952
|
+
const fresh = parseOAuthPersistBlob(raw);
|
|
4953
|
+
if (!fresh || !snapshotHasPlaintextSecrets(fresh)) return;
|
|
4954
|
+
const migrateEntrySecrets = async (serverId, secrets) => {
|
|
4955
|
+
const absent = {};
|
|
4956
|
+
for (const [field, value] of Object.entries(secrets)) {
|
|
4957
|
+
const existing = await secretStoreGetStrict(secretStore, serverId, field);
|
|
4958
|
+
if (existing === null || !isUsableStoredSecret(field, existing)) {
|
|
4959
|
+
absent[field] = value;
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
if (Object.keys(absent).length > 0) {
|
|
4963
|
+
await secretStoreSetMany(secretStore, serverId, absent);
|
|
4964
|
+
}
|
|
4965
|
+
};
|
|
4966
|
+
const residue = { servers: {}, idpSessions: {} };
|
|
4967
|
+
for (const [url, state] of Object.entries(fresh.servers)) {
|
|
4968
|
+
const split = splitServerOAuthState(state, "all");
|
|
4969
|
+
setOwnEntry(residue.servers, url, split.residue);
|
|
4970
|
+
await migrateEntrySecrets(oauthSecretServerId(url), split.secrets);
|
|
4971
|
+
}
|
|
4972
|
+
for (const [issuer, session] of Object.entries(fresh.idpSessions)) {
|
|
4973
|
+
const split = splitIdpSession(session, "all");
|
|
4974
|
+
setOwnEntry(residue.idpSessions, issuer, split.residue);
|
|
4975
|
+
await migrateEntrySecrets(oauthIdpSecretServerId(issuer), split.secrets);
|
|
4976
|
+
}
|
|
4977
|
+
const stripped = serializeOAuthPersistBlob(residue);
|
|
4978
|
+
if (stripped !== raw) {
|
|
4979
|
+
await writeStoreFile(filePath, stripped);
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
async function readOAuthStore(filePath, secretStore = defaultSecretStore()) {
|
|
4983
|
+
return withOAuthStateLock(filePath, "read", async () => {
|
|
4984
|
+
let snapshot = parseOAuthPersistBlob(await readStoreFile(filePath));
|
|
4985
|
+
if (snapshot === null) return null;
|
|
4986
|
+
if (snapshotHasPlaintextSecrets(snapshot) && await secretStoreIsDurable(secretStore)) {
|
|
4987
|
+
try {
|
|
4988
|
+
await migratePlaintextSecrets(filePath, secretStore);
|
|
4989
|
+
snapshot = parseOAuthPersistBlob(await readStoreFile(filePath)) ?? snapshot;
|
|
4990
|
+
} catch (error) {
|
|
4991
|
+
warnMigrationFailure(error);
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
return joinSnapshot(snapshot, secretStore);
|
|
4995
|
+
});
|
|
4996
|
+
}
|
|
4997
|
+
async function removeOAuthStore(filePath, secretStore = defaultSecretStore()) {
|
|
4998
|
+
await withOAuthStateLock(filePath, "remove", async () => {
|
|
4999
|
+
const snapshot = await readDiskForMutation(filePath, "remove");
|
|
5000
|
+
if (snapshot) {
|
|
5001
|
+
const targets = [
|
|
5002
|
+
...Object.entries(snapshot.servers).map(([url, state]) => ({
|
|
5003
|
+
id: oauthSecretServerId(url),
|
|
5004
|
+
fields: serverSecretFields(state)
|
|
5005
|
+
})),
|
|
5006
|
+
...Object.keys(snapshot.idpSessions).map((issuer) => ({
|
|
5007
|
+
id: oauthIdpSecretServerId(issuer),
|
|
5008
|
+
fields: [IDP_SESSION_FIELD]
|
|
5009
|
+
}))
|
|
5010
|
+
];
|
|
5011
|
+
const priorSecrets = [];
|
|
5012
|
+
try {
|
|
5013
|
+
for (const { id, fields } of targets) {
|
|
5014
|
+
priorSecrets.push(
|
|
5015
|
+
...await snapshotSecretFields(secretStore, id, fields)
|
|
5016
|
+
);
|
|
5017
|
+
await secretStore.deleteAllForServer(id);
|
|
5018
|
+
}
|
|
5019
|
+
await deleteStoreFile(filePath);
|
|
5020
|
+
} catch (error) {
|
|
5021
|
+
await restoreSecretFields(
|
|
5022
|
+
secretStore,
|
|
5023
|
+
priorSecrets,
|
|
5024
|
+
warnRestoreFailure
|
|
5025
|
+
);
|
|
5026
|
+
throw error;
|
|
5027
|
+
}
|
|
5028
|
+
} else {
|
|
5029
|
+
await deleteStoreFile(filePath);
|
|
5030
|
+
}
|
|
5031
|
+
});
|
|
5032
|
+
}
|
|
5033
|
+
|
|
5034
|
+
// ../../core/json/xMcpHeader.ts
|
|
5035
|
+
var MCP_PARAM_HEADER_PREFIX = "Mcp-Param-";
|
|
5036
|
+
|
|
5037
|
+
// ../../core/mcp/import/clientConfig.ts
|
|
5038
|
+
function parseJsonObject(raw) {
|
|
5039
|
+
let data;
|
|
5040
|
+
try {
|
|
5041
|
+
data = JSON.parse(raw);
|
|
5042
|
+
} catch (err) {
|
|
5043
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
5044
|
+
throw new Error(`Invalid JSON: ${detail}`, { cause: err });
|
|
5045
|
+
}
|
|
5046
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
5047
|
+
throw new Error("Expected a JSON object at the top level");
|
|
5048
|
+
}
|
|
5049
|
+
return data;
|
|
5050
|
+
}
|
|
5051
|
+
function normalizeServerMap(servers) {
|
|
5052
|
+
const out = {};
|
|
5053
|
+
for (const [id, raw] of Object.entries(servers)) {
|
|
5054
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
5055
|
+
const normalized = normalizeServerType(
|
|
5056
|
+
raw
|
|
5057
|
+
);
|
|
5058
|
+
out[id] = normalized;
|
|
5059
|
+
}
|
|
5060
|
+
return out;
|
|
5061
|
+
}
|
|
5062
|
+
function parseMcpServersConfig(raw) {
|
|
5063
|
+
const data = parseJsonObject(raw);
|
|
5064
|
+
const servers = data.mcpServers;
|
|
5065
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
5066
|
+
throw new Error("No 'mcpServers' object found in config file");
|
|
5067
|
+
}
|
|
5068
|
+
return {
|
|
5069
|
+
mcpServers: normalizeServerMap(servers)
|
|
5070
|
+
};
|
|
5071
|
+
}
|
|
5072
|
+
function parseVsCodeConfig(raw) {
|
|
5073
|
+
const data = parseJsonObject(raw);
|
|
5074
|
+
const servers = data.servers;
|
|
5075
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
5076
|
+
throw new Error("No 'servers' object found in VS Code config file");
|
|
5077
|
+
}
|
|
5078
|
+
return {
|
|
5079
|
+
mcpServers: normalizeServerMap(servers)
|
|
5080
|
+
};
|
|
5081
|
+
}
|
|
5082
|
+
|
|
5083
|
+
// ../../core/mcp/import/strategies.ts
|
|
5084
|
+
function join5(...segments) {
|
|
5085
|
+
return segments.map(
|
|
5086
|
+
(s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")
|
|
5087
|
+
).filter((s) => s.length > 0).join("/");
|
|
5088
|
+
}
|
|
5089
|
+
function appData(homeDir) {
|
|
5090
|
+
return join5(homeDir, "AppData", "Roaming");
|
|
5091
|
+
}
|
|
5092
|
+
function appSupportDir(platform, homeDir, app) {
|
|
5093
|
+
if (platform === "darwin") {
|
|
5094
|
+
return join5(homeDir, "Library", "Application Support", app);
|
|
5095
|
+
}
|
|
5096
|
+
if (platform === "win32") {
|
|
5097
|
+
return join5(appData(homeDir), app);
|
|
5098
|
+
}
|
|
5099
|
+
return join5(homeDir, ".config", app);
|
|
5100
|
+
}
|
|
5101
|
+
var claudeDesktop = {
|
|
5102
|
+
id: "claude-desktop",
|
|
5103
|
+
label: "Claude Desktop",
|
|
5104
|
+
defaultPaths: (platform, homeDir) => [
|
|
5105
|
+
join5(
|
|
5106
|
+
appSupportDir(platform, homeDir, "Claude"),
|
|
5107
|
+
"claude_desktop_config.json"
|
|
5108
|
+
)
|
|
5109
|
+
],
|
|
5110
|
+
parse: parseMcpServersConfig
|
|
5111
|
+
};
|
|
5112
|
+
var cursor = {
|
|
5113
|
+
id: "cursor",
|
|
5114
|
+
label: "Cursor",
|
|
5115
|
+
defaultPaths: (_platform, homeDir) => [join5(homeDir, ".cursor", "mcp.json")],
|
|
5116
|
+
parse: parseMcpServersConfig
|
|
5117
|
+
};
|
|
5118
|
+
var cline = {
|
|
5119
|
+
id: "cline",
|
|
5120
|
+
label: "Cline",
|
|
5121
|
+
defaultPaths: (platform, homeDir) => [
|
|
5122
|
+
// Cline (VS Code extension) global settings live under the editor's
|
|
5123
|
+
// globalStorage; the documented cross-platform fallback is ~/Documents.
|
|
5124
|
+
join5(
|
|
5125
|
+
appSupportDir(platform, homeDir, "Code"),
|
|
5126
|
+
"User",
|
|
5127
|
+
"globalStorage",
|
|
5128
|
+
"saoudrizwan.claude-dev",
|
|
5129
|
+
"settings",
|
|
5130
|
+
"cline_mcp_settings.json"
|
|
5131
|
+
),
|
|
5132
|
+
join5(homeDir, "Documents", "Cline", "MCP", "cline_mcp_settings.json")
|
|
5133
|
+
],
|
|
5134
|
+
parse: parseMcpServersConfig
|
|
5135
|
+
};
|
|
5136
|
+
var vscode = {
|
|
5137
|
+
id: "vscode",
|
|
5138
|
+
label: "VS Code",
|
|
5139
|
+
defaultPaths: (platform, homeDir) => [
|
|
5140
|
+
join5(appSupportDir(platform, homeDir, "Code"), "User", "mcp.json")
|
|
5141
|
+
],
|
|
5142
|
+
parse: parseVsCodeConfig
|
|
5143
|
+
};
|
|
5144
|
+
var IMPORT_STRATEGY_LIST = [
|
|
5145
|
+
claudeDesktop,
|
|
5146
|
+
cursor,
|
|
5147
|
+
cline,
|
|
5148
|
+
vscode
|
|
5149
|
+
];
|
|
5150
|
+
var IMPORT_STRATEGIES = Object.fromEntries(IMPORT_STRATEGY_LIST.map((s) => [s.id, s]));
|
|
5151
|
+
function getImportStrategy(id) {
|
|
5152
|
+
return IMPORT_STRATEGIES[id];
|
|
5153
|
+
}
|
|
5154
|
+
|
|
5155
|
+
// ../../core/mcp/import/resolveSource.ts
|
|
5156
|
+
function resolveImportSource(type, platform, homeDir, readFile2) {
|
|
5157
|
+
const strategy = getImportStrategy(type);
|
|
5158
|
+
if (!strategy) return null;
|
|
5159
|
+
const searched = strategy.defaultPaths(platform, homeDir);
|
|
5160
|
+
for (const path6 of searched) {
|
|
5161
|
+
let raw;
|
|
5162
|
+
try {
|
|
5163
|
+
raw = readFile2(path6);
|
|
5164
|
+
} catch (err) {
|
|
5165
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5166
|
+
return {
|
|
5167
|
+
type,
|
|
5168
|
+
found: true,
|
|
5169
|
+
path: path6,
|
|
5170
|
+
error: `Failed to read ${path6}: ${msg}`,
|
|
5171
|
+
searched
|
|
5172
|
+
};
|
|
5173
|
+
}
|
|
5174
|
+
if (raw === null) continue;
|
|
5175
|
+
try {
|
|
5176
|
+
const config = strategy.parse(raw);
|
|
5177
|
+
return { type, found: true, path: path6, config, searched };
|
|
5178
|
+
} catch (err) {
|
|
5179
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5180
|
+
return { type, found: true, path: path6, error: msg, searched };
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
return { type, found: false, searched };
|
|
5184
|
+
}
|
|
5185
|
+
|
|
5186
|
+
// ../../core/mcp/remote/progressToken.ts
|
|
5187
|
+
import { ProgressTokenSchema } from "@modelcontextprotocol/core";
|
|
5188
|
+
function progressTokenOf(message) {
|
|
5189
|
+
if ("method" in message && message.method === "notifications/progress" && "params" in message && typeof message.params === "object" && message.params !== null) {
|
|
5190
|
+
const parsed = ProgressTokenSchema.safeParse(
|
|
5191
|
+
message.params.progressToken
|
|
5192
|
+
);
|
|
5193
|
+
if (parsed.success) {
|
|
5194
|
+
return parsed.data;
|
|
5195
|
+
}
|
|
5196
|
+
}
|
|
5197
|
+
return void 0;
|
|
5198
|
+
}
|
|
5199
|
+
function waitForProgressToken(waits, token) {
|
|
5200
|
+
const exact = waits.get(token);
|
|
5201
|
+
if (exact !== void 0 || typeof token === "number") {
|
|
5202
|
+
return exact;
|
|
5203
|
+
}
|
|
5204
|
+
const numeric = Number(token);
|
|
5205
|
+
return Number.isSafeInteger(numeric) ? waits.get(numeric) : void 0;
|
|
5206
|
+
}
|
|
5207
|
+
|
|
5208
|
+
// ../../core/mcp/remote/node/remote-session.ts
|
|
5209
|
+
var RemoteSession = class _RemoteSession {
|
|
5210
|
+
sessionId;
|
|
5211
|
+
transport;
|
|
5212
|
+
eventQueue = [];
|
|
5213
|
+
eventConsumer = null;
|
|
5214
|
+
transportDead = false;
|
|
5215
|
+
transportError = null;
|
|
5216
|
+
activeSendCount = 0;
|
|
5217
|
+
/**
|
|
5218
|
+
* Suppress duplicate ambient SSE auth echoes after HTTP command-scoped delivery.
|
|
5219
|
+
* Stale transport `onerror` may arrive after the send completes.
|
|
5220
|
+
*/
|
|
5221
|
+
authHttpEchoSuppressUntilMs = 0;
|
|
5222
|
+
static AUTH_HTTP_ECHO_SUPPRESS_MS = 3e4;
|
|
5223
|
+
requestWaits = /* @__PURE__ */ new Map();
|
|
5224
|
+
authProviderHandle = null;
|
|
5225
|
+
appliedProtocolVersion;
|
|
5226
|
+
/** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
|
|
5227
|
+
static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
5228
|
+
constructor(sessionId) {
|
|
5229
|
+
this.sessionId = sessionId;
|
|
5230
|
+
}
|
|
5231
|
+
setAuthProviderHandle(handle) {
|
|
5232
|
+
this.authProviderHandle = handle;
|
|
5233
|
+
}
|
|
5234
|
+
setAuthState(authState) {
|
|
5235
|
+
if (!this.authProviderHandle) {
|
|
5236
|
+
throw new Error("Session has no OAuth auth provider");
|
|
5237
|
+
}
|
|
5238
|
+
this.authProviderHandle.setAuthState(authState);
|
|
5239
|
+
}
|
|
5240
|
+
setTransport(transport) {
|
|
5241
|
+
this.transport = transport;
|
|
5242
|
+
}
|
|
5243
|
+
/**
|
|
5244
|
+
* Apply the client's negotiated protocol version to the upstream transport
|
|
5245
|
+
* (#1935). The browser's SDK Client owns the `initialize` handshake, so the
|
|
5246
|
+
* `setProtocolVersion` call it makes lands on the *remote* transport; without
|
|
5247
|
+
* this the backend's real HTTP transport never learns the version and drops
|
|
5248
|
+
* `Mcp-Protocol-Version` from every subsequent request.
|
|
5249
|
+
*
|
|
5250
|
+
* Ignores anything that isn't a plausible version token so a client can't
|
|
5251
|
+
* push arbitrary bytes into an upstream header, and re-applies only on
|
|
5252
|
+
* change (the value rides every `/api/mcp/send`). The value arrives from an
|
|
5253
|
+
* unvalidated JSON body, so the type is checked rather than assumed —
|
|
5254
|
+
* `RegExp.test` would otherwise coerce a `123` or `true` into a "matching"
|
|
5255
|
+
* string and hand the wrong type to the transport.
|
|
5256
|
+
*/
|
|
5257
|
+
applyProtocolVersion(version) {
|
|
5258
|
+
if (typeof version !== "string" || version === this.appliedProtocolVersion || !_RemoteSession.PROTOCOL_VERSION_PATTERN.test(version)) {
|
|
5259
|
+
return;
|
|
5260
|
+
}
|
|
5261
|
+
this.appliedProtocolVersion = version;
|
|
5262
|
+
this.transport.setProtocolVersion?.(version);
|
|
5263
|
+
}
|
|
5264
|
+
setEventConsumer(consumer) {
|
|
5265
|
+
this.eventConsumer = consumer;
|
|
5266
|
+
while (this.eventQueue.length > 0) {
|
|
5267
|
+
const ev = this.eventQueue.shift();
|
|
5268
|
+
consumer(ev);
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
clearEventConsumer() {
|
|
5272
|
+
this.eventConsumer = null;
|
|
5273
|
+
return this.transportDead;
|
|
5274
|
+
}
|
|
5275
|
+
markTransportDead(error) {
|
|
5276
|
+
this.transportDead = true;
|
|
5277
|
+
this.transportError = error;
|
|
5278
|
+
this.pushEvent({
|
|
5279
|
+
type: "transport_error",
|
|
5280
|
+
data: {
|
|
5281
|
+
error,
|
|
5282
|
+
code: -32e3
|
|
5283
|
+
// MCP error code for connection closed
|
|
5284
|
+
}
|
|
5285
|
+
});
|
|
5286
|
+
}
|
|
5287
|
+
isTransportDead() {
|
|
5288
|
+
return this.transportDead;
|
|
5289
|
+
}
|
|
5290
|
+
getTransportError() {
|
|
5291
|
+
return this.transportError;
|
|
5292
|
+
}
|
|
5293
|
+
hasEventConsumer() {
|
|
5294
|
+
return this.eventConsumer !== null;
|
|
5295
|
+
}
|
|
5296
|
+
beginSend() {
|
|
5297
|
+
this.activeSendCount++;
|
|
5298
|
+
}
|
|
5299
|
+
endSend() {
|
|
5300
|
+
if (this.activeSendCount > 0) {
|
|
5301
|
+
this.activeSendCount--;
|
|
5302
|
+
}
|
|
4204
5303
|
}
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
} catch {
|
|
4209
|
-
console.warn(
|
|
4210
|
-
`
|
|
4211
|
-
[mcp-inspector] Could not restore ${staged} to ${filePath}; both files exist and the snapshot holds the secrets that were not migrated. Merge it by hand, or delete it once you have confirmed nothing is missing.`
|
|
4212
|
-
);
|
|
5304
|
+
/** Command-scoped auth was returned on HTTP; suppress async onerror echo on SSE. */
|
|
5305
|
+
noteAuthChallengeDeliveredViaHttp() {
|
|
5306
|
+
this.extendAuthHttpEchoSuppression();
|
|
4213
5307
|
}
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
async function anythingToMigrate(filePath) {
|
|
4217
|
-
const base = path4.basename(filePath);
|
|
4218
|
-
try {
|
|
4219
|
-
return (await fs4.readdir(path4.dirname(filePath))).some(
|
|
4220
|
-
(name) => name === base || isSnapshotName(name, base)
|
|
4221
|
-
);
|
|
4222
|
-
} catch (err) {
|
|
4223
|
-
const code = err.code;
|
|
4224
|
-
return code !== "ENOENT" && code !== "ENOTDIR";
|
|
5308
|
+
extendAuthHttpEchoSuppression() {
|
|
5309
|
+
this.authHttpEchoSuppressUntilMs = Date.now() + _RemoteSession.AUTH_HTTP_ECHO_SUPPRESS_MS;
|
|
4225
5310
|
}
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
const dir = path4.dirname(filePath);
|
|
4229
|
-
const base = path4.basename(filePath);
|
|
4230
|
-
let names;
|
|
4231
|
-
try {
|
|
4232
|
-
names = (await fs4.readdir(dir)).filter((n) => isSnapshotName(n, base));
|
|
4233
|
-
} catch {
|
|
4234
|
-
return;
|
|
5311
|
+
shouldSuppressAuthEcho() {
|
|
5312
|
+
return Date.now() < this.authHttpEchoSuppressUntilMs;
|
|
4235
5313
|
}
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
if (await isFileLockHeld(orphan)) continue;
|
|
4239
|
-
try {
|
|
4240
|
-
await fs4.link(orphan, filePath);
|
|
4241
|
-
await fs4.rm(orphan, { force: true });
|
|
4242
|
-
console.warn(
|
|
4243
|
-
`
|
|
4244
|
-
[mcp-inspector] Recovered secrets left behind by an interrupted migration (${orphan} \u2192 ${filePath}).`
|
|
4245
|
-
);
|
|
4246
|
-
} catch {
|
|
4247
|
-
console.warn(
|
|
4248
|
-
`
|
|
4249
|
-
[mcp-inspector] Found secrets from an interrupted migration at ${orphan}, and a current ${filePath} already exists, so it was left alone. It may hold values the current file does not.`
|
|
4250
|
-
);
|
|
4251
|
-
}
|
|
5314
|
+
hasActiveSend() {
|
|
5315
|
+
return this.activeSendCount > 0;
|
|
4252
5316
|
}
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
let entries;
|
|
4256
|
-
try {
|
|
4257
|
-
entries = await file.readAll();
|
|
4258
|
-
} catch (err) {
|
|
4259
|
-
console.warn(
|
|
4260
|
-
`
|
|
4261
|
-
[mcp-inspector] The OS keychain is available again, but the secrets file at ${filePath} could not be read, so its contents were left there: ${err instanceof Error ? err.message : String(err)}`
|
|
4262
|
-
);
|
|
4263
|
-
return false;
|
|
5317
|
+
pushAuthChallenge(challenge) {
|
|
5318
|
+
this.pushEvent({ type: "auth_challenge", data: challenge });
|
|
4264
5319
|
}
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
5320
|
+
/**
|
|
5321
|
+
* Streamable HTTP `transport.send()` can return before the JSON-RPC response
|
|
5322
|
+
* arrives. The remote send handler awaits this so `/api/mcp/send` returns
|
|
5323
|
+
* `auth_challenge` or `ok: true` only after the MCP round-trip (or auth error).
|
|
5324
|
+
*/
|
|
5325
|
+
waitForRequestResponse(requestId, timeoutMs = 6e4) {
|
|
5326
|
+
return new Promise((resolve9, reject) => {
|
|
5327
|
+
let timer;
|
|
5328
|
+
const arm = () => {
|
|
5329
|
+
if (timeoutMs <= 0) return;
|
|
5330
|
+
timer = setTimeout(() => {
|
|
5331
|
+
this.requestWaits.delete(requestId);
|
|
5332
|
+
reject(
|
|
5333
|
+
new Error(
|
|
5334
|
+
`MCP request ${String(requestId)} timed out after ${timeoutMs}ms`
|
|
5335
|
+
)
|
|
5336
|
+
);
|
|
5337
|
+
}, timeoutMs);
|
|
5338
|
+
};
|
|
5339
|
+
const clear = () => {
|
|
5340
|
+
if (timer !== void 0) {
|
|
5341
|
+
clearTimeout(timer);
|
|
5342
|
+
timer = void 0;
|
|
5343
|
+
}
|
|
5344
|
+
};
|
|
5345
|
+
arm();
|
|
5346
|
+
this.requestWaits.set(requestId, {
|
|
5347
|
+
resolve: () => {
|
|
5348
|
+
clear();
|
|
5349
|
+
resolve9();
|
|
5350
|
+
},
|
|
5351
|
+
reject: (error) => {
|
|
5352
|
+
clear();
|
|
5353
|
+
reject(error);
|
|
5354
|
+
},
|
|
5355
|
+
// A progress notification for this request re-arms the full window, so
|
|
5356
|
+
// a tool that keeps reporting progress never trips the relay's own
|
|
5357
|
+
// deadline — the same guarantee the SDK client's `resetTimeoutOnProgress`
|
|
5358
|
+
// gives on a direct transport (#2028).
|
|
5359
|
+
resetTimeout: () => {
|
|
5360
|
+
if (timer === void 0) return;
|
|
5361
|
+
clear();
|
|
5362
|
+
arm();
|
|
5363
|
+
}
|
|
5364
|
+
});
|
|
5365
|
+
});
|
|
4289
5366
|
}
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
5367
|
+
/**
|
|
5368
|
+
* Re-arm a pending request's wait timeout. Invoked from {@link onMessage} when
|
|
5369
|
+
* a `notifications/progress` carrying this request's `progressToken` arrives.
|
|
5370
|
+
* A no-op when no wait is pending for the id. A numeric string token (`"4"`)
|
|
5371
|
+
* matches the numeric request id, as it does in the SDK (#2458).
|
|
5372
|
+
*/
|
|
5373
|
+
noteRequestProgress(progressToken) {
|
|
5374
|
+
waitForProgressToken(this.requestWaits, progressToken)?.resetTimeout();
|
|
4296
5375
|
}
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
store.filePath,
|
|
4305
|
-
"file",
|
|
4306
|
-
info.reason,
|
|
4307
|
-
info.detail
|
|
4308
|
-
);
|
|
4309
|
-
}
|
|
4310
|
-
var DeferredSecretStore = class {
|
|
4311
|
-
async target() {
|
|
4312
|
-
return (await resolveSecretStore()).store;
|
|
5376
|
+
cancelRequestWait(requestId) {
|
|
5377
|
+
const wait = this.requestWaits.get(requestId);
|
|
5378
|
+
if (!wait) {
|
|
5379
|
+
return;
|
|
5380
|
+
}
|
|
5381
|
+
this.requestWaits.delete(requestId);
|
|
5382
|
+
wait.reject(new Error("MCP request wait cancelled"));
|
|
4313
5383
|
}
|
|
4314
|
-
|
|
4315
|
-
|
|
5384
|
+
rejectActiveRequestWaits(error) {
|
|
5385
|
+
for (const wait of this.requestWaits.values()) {
|
|
5386
|
+
wait.reject(error);
|
|
5387
|
+
}
|
|
5388
|
+
this.requestWaits.clear();
|
|
4316
5389
|
}
|
|
4317
5390
|
/**
|
|
4318
|
-
*
|
|
4319
|
-
*
|
|
4320
|
-
*
|
|
4321
|
-
*
|
|
5391
|
+
* Auth errors from the MCP transport.
|
|
5392
|
+
* - During an active send: command path owns delivery (HTTP); never SSE.
|
|
5393
|
+
* - After HTTP already returned auth_challenge: swallow async onerror echo.
|
|
5394
|
+
* - Otherwise: ambient SSE (idle session).
|
|
4322
5395
|
*/
|
|
4323
|
-
|
|
4324
|
-
|
|
5396
|
+
handleTransportAuthError(error) {
|
|
5397
|
+
if (!(error instanceof AuthChallengeError)) {
|
|
5398
|
+
return false;
|
|
5399
|
+
}
|
|
5400
|
+
this.rejectActiveRequestWaits(error);
|
|
5401
|
+
if (this.hasActiveSend()) {
|
|
5402
|
+
this.extendAuthHttpEchoSuppression();
|
|
5403
|
+
return true;
|
|
5404
|
+
}
|
|
5405
|
+
if (this.shouldSuppressAuthEcho()) {
|
|
5406
|
+
return true;
|
|
5407
|
+
}
|
|
5408
|
+
this.pushAuthChallenge(error.authChallenge);
|
|
5409
|
+
return true;
|
|
4325
5410
|
}
|
|
4326
|
-
|
|
4327
|
-
|
|
5411
|
+
settleRequestWait(requestId) {
|
|
5412
|
+
const wait = this.requestWaits.get(requestId);
|
|
5413
|
+
if (!wait) {
|
|
5414
|
+
return;
|
|
5415
|
+
}
|
|
5416
|
+
this.requestWaits.delete(requestId);
|
|
5417
|
+
wait.resolve();
|
|
4328
5418
|
}
|
|
4329
|
-
|
|
4330
|
-
|
|
5419
|
+
pushEvent(event) {
|
|
5420
|
+
if (this.eventConsumer) {
|
|
5421
|
+
this.eventConsumer(event);
|
|
5422
|
+
} else {
|
|
5423
|
+
this.eventQueue.push(event);
|
|
5424
|
+
}
|
|
4331
5425
|
}
|
|
4332
|
-
|
|
4333
|
-
|
|
5426
|
+
onMessage(message) {
|
|
5427
|
+
if ("id" in message && message.id !== null && message.id !== void 0 && ("result" in message || "error" in message)) {
|
|
5428
|
+
this.settleRequestWait(message.id);
|
|
5429
|
+
}
|
|
5430
|
+
const progressToken = progressTokenOf(message);
|
|
5431
|
+
if (progressToken !== void 0) {
|
|
5432
|
+
this.noteRequestProgress(progressToken);
|
|
5433
|
+
}
|
|
5434
|
+
this.pushEvent({ type: "message", data: message });
|
|
4334
5435
|
}
|
|
4335
|
-
|
|
4336
|
-
|
|
5436
|
+
onFetchRequest(entry) {
|
|
5437
|
+
this.pushEvent({
|
|
5438
|
+
type: "fetch_request",
|
|
5439
|
+
data: {
|
|
5440
|
+
...entry,
|
|
5441
|
+
timestamp: entry.timestamp instanceof Date ? entry.timestamp.toISOString() : entry.timestamp
|
|
5442
|
+
}
|
|
5443
|
+
});
|
|
4337
5444
|
}
|
|
4338
|
-
|
|
4339
|
-
|
|
5445
|
+
onFetchResponseBody(id, responseBody) {
|
|
5446
|
+
this.pushEvent({
|
|
5447
|
+
type: "fetch_request_body_update",
|
|
5448
|
+
data: { id, responseBody }
|
|
5449
|
+
});
|
|
5450
|
+
}
|
|
5451
|
+
onFetchStreamUpdate(id, stream) {
|
|
5452
|
+
this.pushEvent({
|
|
5453
|
+
type: "fetch_stream_update",
|
|
5454
|
+
data: {
|
|
5455
|
+
id,
|
|
5456
|
+
eventCount: stream.eventCount,
|
|
5457
|
+
...stream.closedAt && { closedAt: stream.closedAt.toISOString() }
|
|
5458
|
+
}
|
|
5459
|
+
});
|
|
4340
5460
|
}
|
|
4341
|
-
|
|
4342
|
-
|
|
5461
|
+
onStderr(entry) {
|
|
5462
|
+
this.pushEvent({
|
|
5463
|
+
type: "stdio_log",
|
|
5464
|
+
data: {
|
|
5465
|
+
timestamp: entry.timestamp.toISOString(),
|
|
5466
|
+
message: entry.message
|
|
5467
|
+
}
|
|
5468
|
+
});
|
|
4343
5469
|
}
|
|
4344
5470
|
};
|
|
4345
|
-
|
|
4346
|
-
|
|
5471
|
+
|
|
5472
|
+
// ../../core/mcp/remote/node/tokenAuthProvider.ts
|
|
5473
|
+
var REMOTE_OAUTH_STUB_METADATA = {
|
|
5474
|
+
redirect_uris: [],
|
|
5475
|
+
scope: ""
|
|
5476
|
+
};
|
|
5477
|
+
function cloneAuthState(state) {
|
|
5478
|
+
return {
|
|
5479
|
+
...state.oauthTokens && {
|
|
5480
|
+
oauthTokens: { ...state.oauthTokens }
|
|
5481
|
+
},
|
|
5482
|
+
...state.oauthClient && {
|
|
5483
|
+
oauthClient: { ...state.oauthClient }
|
|
5484
|
+
}
|
|
5485
|
+
};
|
|
5486
|
+
}
|
|
5487
|
+
function createRemoteAuthProvider(initialAuthState) {
|
|
5488
|
+
if (!initialAuthState?.oauthTokens && !initialAuthState?.oauthClient) {
|
|
5489
|
+
return void 0;
|
|
5490
|
+
}
|
|
5491
|
+
let authState = cloneAuthState(initialAuthState);
|
|
5492
|
+
const provider = {
|
|
5493
|
+
get clientMetadata() {
|
|
5494
|
+
return REMOTE_OAUTH_STUB_METADATA;
|
|
5495
|
+
},
|
|
5496
|
+
get redirectUrl() {
|
|
5497
|
+
return "";
|
|
5498
|
+
},
|
|
5499
|
+
async tokens() {
|
|
5500
|
+
return authState.oauthTokens;
|
|
5501
|
+
},
|
|
5502
|
+
async clientInformation() {
|
|
5503
|
+
if (!authState.oauthClient?.client_id) {
|
|
5504
|
+
return void 0;
|
|
5505
|
+
}
|
|
5506
|
+
return {
|
|
5507
|
+
client_id: authState.oauthClient.client_id,
|
|
5508
|
+
...authState.oauthClient.client_secret && {
|
|
5509
|
+
client_secret: authState.oauthClient.client_secret
|
|
5510
|
+
}
|
|
5511
|
+
};
|
|
5512
|
+
},
|
|
5513
|
+
async saveTokens(tokens) {
|
|
5514
|
+
authState = {
|
|
5515
|
+
...authState,
|
|
5516
|
+
oauthTokens: {
|
|
5517
|
+
access_token: tokens.access_token,
|
|
5518
|
+
token_type: tokens.token_type,
|
|
5519
|
+
expires_in: tokens.expires_in,
|
|
5520
|
+
refresh_token: tokens.refresh_token,
|
|
5521
|
+
scope: tokens.scope,
|
|
5522
|
+
id_token: tokens.id_token
|
|
5523
|
+
}
|
|
5524
|
+
};
|
|
5525
|
+
},
|
|
5526
|
+
codeVerifier() {
|
|
5527
|
+
return void 0;
|
|
5528
|
+
},
|
|
5529
|
+
async saveCodeVerifier() {
|
|
5530
|
+
},
|
|
5531
|
+
clear() {
|
|
5532
|
+
authState = {};
|
|
5533
|
+
},
|
|
5534
|
+
async redirectToAuthorization() {
|
|
5535
|
+
throw new Error(
|
|
5536
|
+
"OAuth re-authorization must be performed in the Inspector web client (remote server cannot complete OAuth flows)"
|
|
5537
|
+
);
|
|
5538
|
+
},
|
|
5539
|
+
state() {
|
|
5540
|
+
return "";
|
|
5541
|
+
}
|
|
5542
|
+
// This provider implements only the members the remote backend transport
|
|
5543
|
+
// exercises (token get/save, client info, non-interactive redirect); the
|
|
5544
|
+
// interactive-only members the SDK never calls on the server side are stubbed
|
|
5545
|
+
// with narrower shapes (e.g. `codeVerifier()` returns `undefined`, not the
|
|
5546
|
+
// interface's `string | Promise<string>`), so it can't be assigned to the
|
|
5547
|
+
// full `OAuthClientProvider` without the double cast.
|
|
5548
|
+
};
|
|
5549
|
+
return {
|
|
5550
|
+
provider,
|
|
5551
|
+
setAuthState(next) {
|
|
5552
|
+
authState = cloneAuthState(next);
|
|
5553
|
+
},
|
|
5554
|
+
getAuthState() {
|
|
5555
|
+
return cloneAuthState(authState);
|
|
5556
|
+
}
|
|
5557
|
+
};
|
|
4347
5558
|
}
|
|
4348
5559
|
|
|
4349
5560
|
// ../../core/client/config-parse.ts
|
|
@@ -4498,6 +5709,23 @@ function mergeSecretsIntoClientConfig(config, secrets) {
|
|
|
4498
5709
|
}
|
|
4499
5710
|
|
|
4500
5711
|
// ../../core/client/node-persistence.ts
|
|
5712
|
+
async function withClientConfigLock(filePath, action, body) {
|
|
5713
|
+
let entered = false;
|
|
5714
|
+
try {
|
|
5715
|
+
return await withSecretFileLock(filePath, async () => {
|
|
5716
|
+
entered = true;
|
|
5717
|
+
return body();
|
|
5718
|
+
});
|
|
5719
|
+
} catch (error) {
|
|
5720
|
+
if (!entered && error instanceof SecretFileLockHeldError) {
|
|
5721
|
+
throw new SecretFileLockHeldError(
|
|
5722
|
+
`Could not ${action} the client configuration: the file at ${filePath} is locked by another Inspector process and did not become available.`,
|
|
5723
|
+
{ cause: error }
|
|
5724
|
+
);
|
|
5725
|
+
}
|
|
5726
|
+
throw error;
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
4501
5729
|
async function readIdpSecretFromKeychain(secretStore) {
|
|
4502
5730
|
const secret = await secretStore.get(
|
|
4503
5731
|
CLIENT_KEYCHAIN_ID,
|
|
@@ -4507,29 +5735,39 @@ async function readIdpSecretFromKeychain(secretStore) {
|
|
|
4507
5735
|
return { [SECRET_FIELD_IDP_CLIENT_SECRET]: secret };
|
|
4508
5736
|
}
|
|
4509
5737
|
async function migrateClientPlaintextSecret(filePath, config, secretStore) {
|
|
4510
|
-
const { stripped, secrets } = extractSecretsFromClientConfig(config);
|
|
4511
|
-
const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
|
|
4512
|
-
if (!value) return config;
|
|
4513
5738
|
try {
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
5739
|
+
return await withClientConfigLock(filePath, "migrate", async () => {
|
|
5740
|
+
const raw = await readStoreFile(filePath);
|
|
5741
|
+
if (raw === null) return {};
|
|
5742
|
+
const fresh = parseClientConfig(parseStore(raw));
|
|
5743
|
+
const { stripped, secrets } = extractSecretsFromClientConfig(fresh);
|
|
5744
|
+
const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
|
|
5745
|
+
if (!value) return fresh;
|
|
5746
|
+
try {
|
|
5747
|
+
const existing = await secretStoreGetStrict(
|
|
5748
|
+
secretStore,
|
|
5749
|
+
CLIENT_KEYCHAIN_ID,
|
|
5750
|
+
SECRET_FIELD_IDP_CLIENT_SECRET
|
|
5751
|
+
);
|
|
5752
|
+
if (existing === null) {
|
|
5753
|
+
await secretStore.set(
|
|
5754
|
+
CLIENT_KEYCHAIN_ID,
|
|
5755
|
+
SECRET_FIELD_IDP_CLIENT_SECRET,
|
|
5756
|
+
value
|
|
5757
|
+
);
|
|
5758
|
+
}
|
|
5759
|
+
if (!await secretStoreIsDurable(secretStore)) return fresh;
|
|
5760
|
+
await writeStoreFile(filePath, serializeStore(stripped));
|
|
5761
|
+
return stripped;
|
|
5762
|
+
} catch (err) {
|
|
5763
|
+
if (err instanceof SecretStoreUnavailableError) {
|
|
5764
|
+
return fresh;
|
|
5765
|
+
}
|
|
5766
|
+
throw err;
|
|
5767
|
+
}
|
|
5768
|
+
});
|
|
4529
5769
|
} catch (err) {
|
|
4530
|
-
if (err instanceof
|
|
4531
|
-
return config;
|
|
4532
|
-
}
|
|
5770
|
+
if (err instanceof SecretFileLockHeldError) return config;
|
|
4533
5771
|
throw err;
|
|
4534
5772
|
}
|
|
4535
5773
|
}
|
|
@@ -4549,30 +5787,44 @@ async function writeClientConfigStore(filePath, body, secretStore) {
|
|
|
4549
5787
|
const validated = parseClientConfig(body);
|
|
4550
5788
|
const { stripped, secrets } = extractSecretsFromClientConfig(validated);
|
|
4551
5789
|
const idpSecret = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
|
|
4552
|
-
|
|
4553
|
-
await secretStore
|
|
4554
|
-
CLIENT_KEYCHAIN_ID,
|
|
4555
|
-
SECRET_FIELD_IDP_CLIENT_SECRET,
|
|
4556
|
-
idpSecret
|
|
4557
|
-
);
|
|
4558
|
-
} else {
|
|
4559
|
-
await secretStore.delete(
|
|
4560
|
-
CLIENT_KEYCHAIN_ID,
|
|
5790
|
+
await withClientConfigLock(filePath, "save", async () => {
|
|
5791
|
+
const prior = await snapshotSecretFields(secretStore, CLIENT_KEYCHAIN_ID, [
|
|
4561
5792
|
SECRET_FIELD_IDP_CLIENT_SECRET
|
|
4562
|
-
);
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
5793
|
+
]);
|
|
5794
|
+
try {
|
|
5795
|
+
if (idpSecret) {
|
|
5796
|
+
await secretStore.set(
|
|
5797
|
+
CLIENT_KEYCHAIN_ID,
|
|
5798
|
+
SECRET_FIELD_IDP_CLIENT_SECRET,
|
|
5799
|
+
idpSecret
|
|
5800
|
+
);
|
|
5801
|
+
} else {
|
|
5802
|
+
await secretStore.delete(
|
|
5803
|
+
CLIENT_KEYCHAIN_ID,
|
|
5804
|
+
SECRET_FIELD_IDP_CLIENT_SECRET
|
|
5805
|
+
);
|
|
5806
|
+
}
|
|
5807
|
+
const durable = await secretStoreIsDurable(secretStore);
|
|
5808
|
+
await writeStoreFile(
|
|
4569
5809
|
filePath,
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
5810
|
+
serializeStore(
|
|
5811
|
+
durable ? stripped : await preserveLegacyPlaintext(
|
|
5812
|
+
filePath,
|
|
5813
|
+
validated,
|
|
5814
|
+
stripped,
|
|
5815
|
+
idpSecret
|
|
5816
|
+
)
|
|
5817
|
+
)
|
|
5818
|
+
);
|
|
5819
|
+
} catch (error) {
|
|
5820
|
+
await restoreSecretFields(secretStore, prior, (restoreError) => {
|
|
5821
|
+
console.warn(
|
|
5822
|
+
`[mcp-inspector] Could not restore the IdP client secret after a failed client.json write: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`
|
|
5823
|
+
);
|
|
5824
|
+
});
|
|
5825
|
+
throw error;
|
|
5826
|
+
}
|
|
5827
|
+
});
|
|
4576
5828
|
}
|
|
4577
5829
|
async function preserveLegacyPlaintext(filePath, validated, stripped, idpSecret) {
|
|
4578
5830
|
if (!idpSecret) return stripped;
|
|
@@ -4587,8 +5839,25 @@ async function preserveLegacyPlaintext(filePath, validated, stripped, idpSecret)
|
|
|
4587
5839
|
}
|
|
4588
5840
|
}
|
|
4589
5841
|
async function deleteClientConfigStore(filePath, secretStore) {
|
|
4590
|
-
await
|
|
4591
|
-
|
|
5842
|
+
await withClientConfigLock(filePath, "remove", async () => {
|
|
5843
|
+
const prior = await snapshotSecretFields(secretStore, CLIENT_KEYCHAIN_ID, [
|
|
5844
|
+
SECRET_FIELD_IDP_CLIENT_SECRET
|
|
5845
|
+
]);
|
|
5846
|
+
try {
|
|
5847
|
+
await secretStore.delete(
|
|
5848
|
+
CLIENT_KEYCHAIN_ID,
|
|
5849
|
+
SECRET_FIELD_IDP_CLIENT_SECRET
|
|
5850
|
+
);
|
|
5851
|
+
await deleteStoreFile(filePath);
|
|
5852
|
+
} catch (error) {
|
|
5853
|
+
await restoreSecretFields(secretStore, prior, (restoreError) => {
|
|
5854
|
+
console.warn(
|
|
5855
|
+
`[mcp-inspector] Could not restore the IdP client secret after a failed client.json delete: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`
|
|
5856
|
+
);
|
|
5857
|
+
});
|
|
5858
|
+
throw error;
|
|
5859
|
+
}
|
|
5860
|
+
});
|
|
4592
5861
|
}
|
|
4593
5862
|
|
|
4594
5863
|
// ../../core/mcp/remote/node/server.ts
|
|
@@ -4615,6 +5884,7 @@ async function primeAndHoldSseStream(stream, cleanup) {
|
|
|
4615
5884
|
var MAX_APP_DOCUMENT_CHARS = 8 * 1024 * 1024;
|
|
4616
5885
|
var MAX_APP_DOCUMENT_BODY_BYTES = 24 * 1024 * 1024;
|
|
4617
5886
|
var MAX_APP_CSP_CHARS = 8 * 1024;
|
|
5887
|
+
var MAX_STORAGE_BODY_BYTES = 4 * 1024 * 1024;
|
|
4618
5888
|
function isHeaderSafeValue(value) {
|
|
4619
5889
|
return !/[^\t\x20-\x7e]/.test(value);
|
|
4620
5890
|
}
|
|
@@ -5247,6 +6517,10 @@ function createRemoteApp(options) {
|
|
|
5247
6517
|
const config = await readClientConfigStore(filePath, secretStore);
|
|
5248
6518
|
return c.json(config);
|
|
5249
6519
|
}
|
|
6520
|
+
if (storeId === OAUTH_PERSIST_STORE_ID) {
|
|
6521
|
+
const snapshot = await readOAuthStore(filePath, secretStore);
|
|
6522
|
+
return c.json(snapshot ?? {});
|
|
6523
|
+
}
|
|
5250
6524
|
const raw = await readStoreFile(filePath);
|
|
5251
6525
|
if (raw === null) {
|
|
5252
6526
|
return c.json({}, 200);
|
|
@@ -5254,40 +6528,74 @@ function createRemoteApp(options) {
|
|
|
5254
6528
|
const store = parseStore(raw);
|
|
5255
6529
|
return c.json(store);
|
|
5256
6530
|
} catch (error) {
|
|
6531
|
+
const keychainResp = keychainErrorResponse(c, error);
|
|
6532
|
+
if (keychainResp) return keychainResp;
|
|
5257
6533
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5258
6534
|
return c.json({ error: `Failed to read store: ${msg}` }, 500);
|
|
5259
6535
|
}
|
|
5260
6536
|
});
|
|
5261
|
-
app.post(
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
const filePath = getStoreFilePath(storageDir, storeId);
|
|
5273
|
-
try {
|
|
5274
|
-
if (storeId === "client") {
|
|
5275
|
-
await writeClientConfigStore(filePath, body, secretStore);
|
|
5276
|
-
return c.json({ ok: true });
|
|
6537
|
+
app.post(
|
|
6538
|
+
"/api/storage/:storeId",
|
|
6539
|
+
// Before the parse, not after: see MAX_STORAGE_BODY_BYTES.
|
|
6540
|
+
bodyLimit({
|
|
6541
|
+
maxSize: MAX_STORAGE_BODY_BYTES,
|
|
6542
|
+
onError: (c) => c.json({ error: "Storage payload too large" }, 413)
|
|
6543
|
+
}),
|
|
6544
|
+
async (c) => {
|
|
6545
|
+
const storeId = c.req.param("storeId");
|
|
6546
|
+
if (!storeId || !validateStoreId(storeId)) {
|
|
6547
|
+
return c.json({ error: "Invalid storeId" }, 400);
|
|
5277
6548
|
}
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
6549
|
+
let body;
|
|
6550
|
+
try {
|
|
6551
|
+
body = await c.req.json();
|
|
6552
|
+
} catch {
|
|
6553
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
6554
|
+
}
|
|
6555
|
+
const filePath = getStoreFilePath(storageDir, storeId);
|
|
6556
|
+
try {
|
|
6557
|
+
if (storeId === "client") {
|
|
6558
|
+
await writeClientConfigStore(filePath, body, secretStore);
|
|
6559
|
+
return c.json({ ok: true });
|
|
6560
|
+
}
|
|
6561
|
+
if (c.req.query("sections") !== void 0) {
|
|
6562
|
+
return c.json(
|
|
6563
|
+
{
|
|
6564
|
+
error: "The sections descriptor moved from the ?sections query parameter to the request body"
|
|
6565
|
+
},
|
|
6566
|
+
400
|
|
6567
|
+
);
|
|
6568
|
+
}
|
|
6569
|
+
if (storeId === OAUTH_PERSIST_STORE_ID) {
|
|
6570
|
+
const write = parseOAuthStoreWriteBody(body);
|
|
6571
|
+
if (!write) {
|
|
6572
|
+
return c.json(
|
|
6573
|
+
{ error: "OAuth store writes require an OAuth state body" },
|
|
6574
|
+
400
|
|
6575
|
+
);
|
|
6576
|
+
}
|
|
6577
|
+
await writeOAuthSections(
|
|
6578
|
+
filePath,
|
|
6579
|
+
write.snapshot,
|
|
6580
|
+
write.sections,
|
|
6581
|
+
secretStore
|
|
6582
|
+
);
|
|
6583
|
+
return c.json({ ok: true });
|
|
6584
|
+
}
|
|
6585
|
+
const jsonData = serializeStore(body);
|
|
6586
|
+
await writeStoreFile(filePath, jsonData);
|
|
6587
|
+
return c.json({ ok: true });
|
|
6588
|
+
} catch (error) {
|
|
6589
|
+
if (error instanceof ZodError2) {
|
|
6590
|
+
return c.json({ error: formatClientConfigLoadError(error) }, 400);
|
|
6591
|
+
}
|
|
6592
|
+
const keychainResp = keychainErrorResponse(c, error);
|
|
6593
|
+
if (keychainResp) return keychainResp;
|
|
6594
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
6595
|
+
return c.json({ error: `Failed to write store: ${msg}` }, 500);
|
|
5284
6596
|
}
|
|
5285
|
-
const keychainResp = storeId === "client" ? keychainErrorResponse(c, error) : void 0;
|
|
5286
|
-
if (keychainResp) return keychainResp;
|
|
5287
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
5288
|
-
return c.json({ error: `Failed to write store: ${msg}` }, 500);
|
|
5289
6597
|
}
|
|
5290
|
-
|
|
6598
|
+
);
|
|
5291
6599
|
app.delete("/api/storage/:storeId", async (c) => {
|
|
5292
6600
|
const storeId = c.req.param("storeId");
|
|
5293
6601
|
if (!storeId || !validateStoreId(storeId)) {
|
|
@@ -5299,9 +6607,15 @@ function createRemoteApp(options) {
|
|
|
5299
6607
|
await deleteClientConfigStore(filePath, secretStore);
|
|
5300
6608
|
return c.json({ ok: true });
|
|
5301
6609
|
}
|
|
6610
|
+
if (storeId === OAUTH_PERSIST_STORE_ID) {
|
|
6611
|
+
await removeOAuthStore(filePath, secretStore);
|
|
6612
|
+
return c.json({ ok: true });
|
|
6613
|
+
}
|
|
5302
6614
|
await deleteStoreFile(filePath);
|
|
5303
6615
|
return c.json({ ok: true });
|
|
5304
6616
|
} catch (error) {
|
|
6617
|
+
const keychainResp = keychainErrorResponse(c, error);
|
|
6618
|
+
if (keychainResp) return keychainResp;
|
|
5305
6619
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5306
6620
|
return c.json({ error: `Failed to delete store: ${msg}` }, 500);
|
|
5307
6621
|
}
|
|
@@ -5372,6 +6686,13 @@ function createRemoteApp(options) {
|
|
|
5372
6686
|
const out = {};
|
|
5373
6687
|
for (const [id, val] of Object.entries(raw)) {
|
|
5374
6688
|
if (!val || typeof val !== "object") continue;
|
|
6689
|
+
if (id === "__proto__") {
|
|
6690
|
+
logWarn(
|
|
6691
|
+
{ route: "/api/servers", id },
|
|
6692
|
+
"Dropping mcp.json entry with reserved id `__proto__` \u2014 rename the server to use it."
|
|
6693
|
+
);
|
|
6694
|
+
continue;
|
|
6695
|
+
}
|
|
5375
6696
|
const valObj = val;
|
|
5376
6697
|
if ("settings" in valObj) {
|
|
5377
6698
|
logWarn(
|
|
@@ -5584,6 +6905,12 @@ function createRemoteApp(options) {
|
|
|
5584
6905
|
error: "settings.paginatedLists must be a boolean"
|
|
5585
6906
|
};
|
|
5586
6907
|
}
|
|
6908
|
+
if (obj.suppressNotificationStream !== void 0 && typeof obj.suppressNotificationStream !== "boolean") {
|
|
6909
|
+
return {
|
|
6910
|
+
ok: false,
|
|
6911
|
+
error: "settings.suppressNotificationStream must be a boolean"
|
|
6912
|
+
};
|
|
6913
|
+
}
|
|
5587
6914
|
if (obj.maxFetchRequests !== void 0 && (typeof obj.maxFetchRequests !== "number" || obj.maxFetchRequests < 0)) {
|
|
5588
6915
|
return {
|
|
5589
6916
|
ok: false,
|
|
@@ -5686,6 +7013,10 @@ function createRemoteApp(options) {
|
|
|
5686
7013
|
autoRefreshOnListChanged: obj.autoRefreshOnListChanged === true,
|
|
5687
7014
|
// Absent → false, matching the read side (omit-on-false on the write side).
|
|
5688
7015
|
paginatedLists: obj.paginatedLists === true,
|
|
7016
|
+
// Absent → unset (off); only an explicit true is carried (#2317).
|
|
7017
|
+
...obj.suppressNotificationStream === true && {
|
|
7018
|
+
suppressNotificationStream: true
|
|
7019
|
+
},
|
|
5689
7020
|
// Absent → product default, matching the read side. The default is the
|
|
5690
7021
|
// omit-sentinel in inspectorSettingsToStoredFields, so a client that
|
|
5691
7022
|
// didn't send one writes no spurious maxFetchRequests to disk.
|
|
@@ -5789,7 +7120,7 @@ function createRemoteApp(options) {
|
|
|
5789
7120
|
return false;
|
|
5790
7121
|
};
|
|
5791
7122
|
let warnedSessionPreserved = false;
|
|
5792
|
-
const
|
|
7123
|
+
const migratePlaintextSecrets2 = async (config) => {
|
|
5793
7124
|
let changed = false;
|
|
5794
7125
|
const next = { mcpServers: {} };
|
|
5795
7126
|
const durable = await secretStoreIsDurable(secretStore);
|
|
@@ -5860,7 +7191,22 @@ function createRemoteApp(options) {
|
|
|
5860
7191
|
return out;
|
|
5861
7192
|
};
|
|
5862
7193
|
const deleteKeychainFields = async (id, fields) => {
|
|
5863
|
-
await
|
|
7194
|
+
await settleStoreMutations(
|
|
7195
|
+
fields.map((field) => secretStore.delete(id, field))
|
|
7196
|
+
);
|
|
7197
|
+
};
|
|
7198
|
+
const warnSecretRestoreFailure = (error) => {
|
|
7199
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
7200
|
+
if (fileLogger) {
|
|
7201
|
+
fileLogger.warn(
|
|
7202
|
+
{ err: msg },
|
|
7203
|
+
"Could not restore keychain entries after a failed catalog mutation; keychain and mcp.json may disagree until the next successful save."
|
|
7204
|
+
);
|
|
7205
|
+
} else {
|
|
7206
|
+
console.warn(
|
|
7207
|
+
`[mcp-inspector] Could not restore keychain entries after a failed catalog mutation: ${msg}`
|
|
7208
|
+
);
|
|
7209
|
+
}
|
|
5864
7210
|
};
|
|
5865
7211
|
const entryForDisk = async (built, stripped, previous) => {
|
|
5866
7212
|
if (await secretStoreIsDurable(secretStore)) return stripped;
|
|
@@ -5916,7 +7262,7 @@ function createRemoteApp(options) {
|
|
|
5916
7262
|
mcpServers: normalizeMcpServers(parsedInside?.mcpServers)
|
|
5917
7263
|
};
|
|
5918
7264
|
if (!hasPlaintextSecrets(inside)) return inside;
|
|
5919
|
-
const { migrated, changed } = await
|
|
7265
|
+
const { migrated, changed } = await migratePlaintextSecrets2(inside);
|
|
5920
7266
|
if (changed) {
|
|
5921
7267
|
await writeMcpAndTrackMtime(serializeStore(migrated));
|
|
5922
7268
|
}
|
|
@@ -5936,7 +7282,7 @@ function createRemoteApp(options) {
|
|
|
5936
7282
|
type,
|
|
5937
7283
|
process.platform,
|
|
5938
7284
|
homedir(),
|
|
5939
|
-
(path6) => existsSync3(path6) ?
|
|
7285
|
+
(path6) => existsSync3(path6) ? readFileSync6(path6, "utf-8") : null
|
|
5940
7286
|
);
|
|
5941
7287
|
if (!result) {
|
|
5942
7288
|
return c.json(
|
|
@@ -5958,7 +7304,7 @@ function createRemoteApp(options) {
|
|
|
5958
7304
|
if (typeof body.id !== "string" || !validateStoreId(body.id)) {
|
|
5959
7305
|
return c.json(
|
|
5960
7306
|
{
|
|
5961
|
-
error: "Invalid id: must be non-empty
|
|
7307
|
+
error: "Invalid id: must be non-empty, contain only alphanumeric, hyphen, or underscore, and not be `__proto__`"
|
|
5962
7308
|
},
|
|
5963
7309
|
400
|
|
5964
7310
|
);
|
|
@@ -5976,15 +7322,29 @@ function createRemoteApp(options) {
|
|
|
5976
7322
|
try {
|
|
5977
7323
|
return await withWriteLock(async () => {
|
|
5978
7324
|
const current = await readMcpConfig();
|
|
5979
|
-
if (
|
|
7325
|
+
if (Object.hasOwn(current.mcpServers, id)) {
|
|
5980
7326
|
return c.json({ error: `Server '${id}' already exists` }, 409);
|
|
5981
7327
|
}
|
|
5982
7328
|
const built = buildStoredEntry(id, body.config, postSettings);
|
|
5983
7329
|
const { stripped, secrets } = extractSecretsFromStored(built);
|
|
5984
7330
|
await secretStore.deleteAllForServer(id);
|
|
5985
|
-
await
|
|
5986
|
-
|
|
5987
|
-
|
|
7331
|
+
const prior = await snapshotSecretFields(
|
|
7332
|
+
secretStore,
|
|
7333
|
+
id,
|
|
7334
|
+
Object.keys(secrets)
|
|
7335
|
+
);
|
|
7336
|
+
try {
|
|
7337
|
+
await writeKeychainEntriesFor(id, secrets);
|
|
7338
|
+
current.mcpServers[id] = await entryForDisk(built, stripped);
|
|
7339
|
+
await writeMcpAndTrackMtime(serializeStore(current));
|
|
7340
|
+
} catch (error) {
|
|
7341
|
+
await restoreSecretFields(
|
|
7342
|
+
secretStore,
|
|
7343
|
+
prior,
|
|
7344
|
+
warnSecretRestoreFailure
|
|
7345
|
+
);
|
|
7346
|
+
throw error;
|
|
7347
|
+
}
|
|
5988
7348
|
return c.json({ ok: true });
|
|
5989
7349
|
});
|
|
5990
7350
|
} catch (error) {
|
|
@@ -6074,10 +7434,10 @@ function createRemoteApp(options) {
|
|
|
6074
7434
|
try {
|
|
6075
7435
|
return await withWriteLock(async () => {
|
|
6076
7436
|
const current = await readMcpConfig();
|
|
6077
|
-
if (!(
|
|
7437
|
+
if (!Object.hasOwn(current.mcpServers, originalId)) {
|
|
6078
7438
|
return c.json({ error: `Server '${originalId}' not found` }, 404);
|
|
6079
7439
|
}
|
|
6080
|
-
if (newId !== originalId &&
|
|
7440
|
+
if (newId !== originalId && Object.hasOwn(current.mcpServers, newId)) {
|
|
6081
7441
|
return c.json({ error: `Server '${newId}' already exists` }, 409);
|
|
6082
7442
|
}
|
|
6083
7443
|
const existing = current.mcpServers[originalId];
|
|
@@ -6134,27 +7494,80 @@ function createRemoteApp(options) {
|
|
|
6134
7494
|
}
|
|
6135
7495
|
if (newId !== originalId) {
|
|
6136
7496
|
const previousFields = expectedSecretFields(existing);
|
|
6137
|
-
const
|
|
7497
|
+
const originalPrior = await snapshotSecretFields(
|
|
7498
|
+
secretStore,
|
|
6138
7499
|
originalId,
|
|
6139
7500
|
previousFields
|
|
6140
7501
|
);
|
|
7502
|
+
const keychainSecrets = {};
|
|
7503
|
+
for (const { field, value } of originalPrior) {
|
|
7504
|
+
if (value !== null) setOwnEntry(keychainSecrets, field, value);
|
|
7505
|
+
}
|
|
6141
7506
|
const secretsToWrite = mergeRenameKeychainSecrets(
|
|
6142
7507
|
stripped,
|
|
6143
7508
|
keychainSecrets,
|
|
6144
7509
|
secrets
|
|
6145
7510
|
);
|
|
6146
|
-
await
|
|
6147
|
-
|
|
6148
|
-
|
|
7511
|
+
await secretStore.deleteAllForServer(newId);
|
|
7512
|
+
const prior = [
|
|
7513
|
+
...await snapshotSecretFields(
|
|
7514
|
+
secretStore,
|
|
7515
|
+
newId,
|
|
7516
|
+
Object.keys(secretsToWrite)
|
|
7517
|
+
),
|
|
7518
|
+
...originalPrior
|
|
7519
|
+
];
|
|
7520
|
+
let diskCommitted = false;
|
|
7521
|
+
try {
|
|
7522
|
+
await writeKeychainEntriesFor(newId, secretsToWrite);
|
|
7523
|
+
await writeMcpAndTrackMtime(serializeStore(next));
|
|
7524
|
+
diskCommitted = true;
|
|
7525
|
+
await secretStore.deleteAllForServer(originalId);
|
|
7526
|
+
} catch (error) {
|
|
7527
|
+
if (diskCommitted) {
|
|
7528
|
+
try {
|
|
7529
|
+
await writeMcpAndTrackMtime(serializeStore(current));
|
|
7530
|
+
} catch (diskError) {
|
|
7531
|
+
warnSecretRestoreFailure(diskError);
|
|
7532
|
+
}
|
|
7533
|
+
}
|
|
7534
|
+
await restoreSecretFields(
|
|
7535
|
+
secretStore,
|
|
7536
|
+
prior,
|
|
7537
|
+
warnSecretRestoreFailure
|
|
7538
|
+
);
|
|
7539
|
+
throw error;
|
|
7540
|
+
}
|
|
6149
7541
|
} else {
|
|
6150
7542
|
const previousFields = new Set(expectedSecretFields(existing));
|
|
6151
7543
|
const stillExpected = new Set(expectedSecretFields(built));
|
|
6152
7544
|
const obsolete = [...previousFields].filter(
|
|
6153
7545
|
(field) => field === SECRET_FIELD_OAUTH_CLIENT_SECRET ? settingsIntent.kind !== "preserve" && !(field in secrets) : !stillExpected.has(field)
|
|
6154
7546
|
);
|
|
6155
|
-
await
|
|
6156
|
-
|
|
6157
|
-
|
|
7547
|
+
const prior = await snapshotSecretFields(secretStore, newId, [
|
|
7548
|
+
.../* @__PURE__ */ new Set([...Object.keys(secrets), ...obsolete])
|
|
7549
|
+
]);
|
|
7550
|
+
let diskCommitted = false;
|
|
7551
|
+
try {
|
|
7552
|
+
await writeKeychainEntriesFor(newId, secrets);
|
|
7553
|
+
await writeMcpAndTrackMtime(serializeStore(next));
|
|
7554
|
+
diskCommitted = true;
|
|
7555
|
+
await deleteKeychainFields(newId, obsolete);
|
|
7556
|
+
} catch (error) {
|
|
7557
|
+
if (diskCommitted) {
|
|
7558
|
+
try {
|
|
7559
|
+
await writeMcpAndTrackMtime(serializeStore(current));
|
|
7560
|
+
} catch (diskError) {
|
|
7561
|
+
warnSecretRestoreFailure(diskError);
|
|
7562
|
+
}
|
|
7563
|
+
}
|
|
7564
|
+
await restoreSecretFields(
|
|
7565
|
+
secretStore,
|
|
7566
|
+
prior,
|
|
7567
|
+
warnSecretRestoreFailure
|
|
7568
|
+
);
|
|
7569
|
+
throw error;
|
|
7570
|
+
}
|
|
6158
7571
|
}
|
|
6159
7572
|
return c.json({ ok: true });
|
|
6160
7573
|
});
|
|
@@ -6175,13 +7588,28 @@ function createRemoteApp(options) {
|
|
|
6175
7588
|
try {
|
|
6176
7589
|
return await withWriteLock(async () => {
|
|
6177
7590
|
const current = await readMcpConfig();
|
|
6178
|
-
if (!(
|
|
7591
|
+
if (!Object.hasOwn(current.mcpServers, id)) {
|
|
6179
7592
|
await secretStore.deleteAllForServer(id);
|
|
6180
7593
|
return c.json({ ok: true });
|
|
6181
7594
|
}
|
|
7595
|
+
const stored = current.mcpServers[id];
|
|
6182
7596
|
delete current.mcpServers[id];
|
|
6183
|
-
await
|
|
6184
|
-
|
|
7597
|
+
const prior = await snapshotSecretFields(
|
|
7598
|
+
secretStore,
|
|
7599
|
+
id,
|
|
7600
|
+
expectedSecretFields(stored)
|
|
7601
|
+
);
|
|
7602
|
+
try {
|
|
7603
|
+
await secretStore.deleteAllForServer(id);
|
|
7604
|
+
await writeMcpAndTrackMtime(serializeStore(current));
|
|
7605
|
+
} catch (error) {
|
|
7606
|
+
await restoreSecretFields(
|
|
7607
|
+
secretStore,
|
|
7608
|
+
prior,
|
|
7609
|
+
warnSecretRestoreFailure
|
|
7610
|
+
);
|
|
7611
|
+
throw error;
|
|
7612
|
+
}
|
|
6185
7613
|
return c.json({ ok: true });
|
|
6186
7614
|
});
|
|
6187
7615
|
} catch (error) {
|
|
@@ -6745,7 +8173,7 @@ async function startViteDevServer(config) {
|
|
|
6745
8173
|
}
|
|
6746
8174
|
|
|
6747
8175
|
// server/server.ts
|
|
6748
|
-
import { readFileSync as
|
|
8176
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
6749
8177
|
import { join as join8, dirname as dirname8 } from "path";
|
|
6750
8178
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
6751
8179
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
@@ -6799,7 +8227,7 @@ async function startHonoServer(config) {
|
|
|
6799
8227
|
});
|
|
6800
8228
|
const serveIndexHtml = (c) => {
|
|
6801
8229
|
const indexPath = join8(rootPath, "index.html");
|
|
6802
|
-
const html =
|
|
8230
|
+
const html = readFileSync7(indexPath, "utf-8");
|
|
6803
8231
|
c.header("Cache-Control", "no-store");
|
|
6804
8232
|
return c.html(injectAuthToken(html, resolvedAuthToken));
|
|
6805
8233
|
};
|