@modelcontextprotocol/inspector 2.8.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/README.md +7 -5
- package/clients/cli/build/index.js +1565 -329
- package/clients/tui/build/index.js +1651 -576
- package/clients/web/build/index.js +2481 -1216
- package/clients/web/dist/assets/index-DgXruQhz.js +1598 -0
- package/clients/web/dist/assets/{prism-light-CGBvpchP.js → prism-light-leP1dqtE.js} +1 -1
- package/clients/web/dist/index.html +1 -1
- package/package.json +9 -7
- package/clients/web/dist/assets/index-9ZWu1ACP.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
|
|
@@ -146,6 +146,10 @@ var ModernListSkillsResultSchema = ListSkillsResultSchema.extend({
|
|
|
146
146
|
var GetSkillEnvelopeSchema = z.looseObject({
|
|
147
147
|
skill: SkillEntrySchema
|
|
148
148
|
});
|
|
149
|
+
var ModernGetSkillEnvelopeSchema = GetSkillEnvelopeSchema.extend({
|
|
150
|
+
ttlMs: z.int().min(0),
|
|
151
|
+
cacheScope: z.enum(["public", "private"])
|
|
152
|
+
});
|
|
149
153
|
var GetSkillResultSchema = GetSkillEnvelopeSchema.transform(
|
|
150
154
|
(result) => result.skill
|
|
151
155
|
);
|
|
@@ -1850,12 +1854,31 @@ function redactJsonValue(value) {
|
|
|
1850
1854
|
}
|
|
1851
1855
|
return value;
|
|
1852
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
|
+
}
|
|
1853
1876
|
function redactBody(body, contentType) {
|
|
1854
1877
|
if (!body) return body;
|
|
1855
1878
|
const type = (contentType ?? "").toLowerCase();
|
|
1856
1879
|
if (type.includes("application/x-www-form-urlencoded")) {
|
|
1857
1880
|
try {
|
|
1858
|
-
const params = new URLSearchParams(body);
|
|
1881
|
+
const params = new URLSearchParams(joinSensitiveContinuations(body));
|
|
1859
1882
|
let changed = false;
|
|
1860
1883
|
for (const key of new Set(params.keys())) {
|
|
1861
1884
|
if (isSensitiveField(key)) {
|
|
@@ -2544,1082 +2567,793 @@ function createTransportNode(config, options = {}) {
|
|
|
2544
2567
|
// ../../core/auth/requestTimeout.ts
|
|
2545
2568
|
var OAUTH_TIMEOUT_WIRE_CODE = "oauth_request_timeout";
|
|
2546
2569
|
|
|
2547
|
-
// ../../core/
|
|
2548
|
-
|
|
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
|
+
}
|
|
2549
2582
|
|
|
2550
|
-
// ../../core/
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
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
|
+
}
|
|
2558
2598
|
}
|
|
2559
|
-
|
|
2560
|
-
|
|
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
|
+
}
|
|
2561
2606
|
}
|
|
2562
|
-
return
|
|
2607
|
+
return merged;
|
|
2563
2608
|
}
|
|
2564
|
-
function
|
|
2565
|
-
|
|
2566
|
-
for (const [id, raw] of Object.entries(servers)) {
|
|
2567
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
2568
|
-
const normalized = normalizeServerType(
|
|
2569
|
-
raw
|
|
2570
|
-
);
|
|
2571
|
-
out[id] = normalized;
|
|
2572
|
-
}
|
|
2573
|
-
return out;
|
|
2609
|
+
function isStringArray(value) {
|
|
2610
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
2574
2611
|
}
|
|
2575
|
-
function
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
2579
|
-
throw new Error("No 'mcpServers' object found in config file");
|
|
2612
|
+
function parseOAuthPersistSections(value) {
|
|
2613
|
+
if (!isRecord2(value)) {
|
|
2614
|
+
return null;
|
|
2580
2615
|
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
}
|
|
2585
|
-
function parseVsCodeConfig(raw) {
|
|
2586
|
-
const data = parseJsonObject(raw);
|
|
2587
|
-
const servers = data.servers;
|
|
2588
|
-
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
2589
|
-
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;
|
|
2590
2619
|
}
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
|
-
// ../../core/mcp/import/strategies.ts
|
|
2597
|
-
function join4(...segments) {
|
|
2598
|
-
return segments.map(
|
|
2599
|
-
(s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")
|
|
2600
|
-
).filter((s) => s.length > 0).join("/");
|
|
2601
|
-
}
|
|
2602
|
-
function appData(homeDir) {
|
|
2603
|
-
return join4(homeDir, "AppData", "Roaming");
|
|
2604
|
-
}
|
|
2605
|
-
function appSupportDir(platform, homeDir, app) {
|
|
2606
|
-
if (platform === "darwin") {
|
|
2607
|
-
return join4(homeDir, "Library", "Application Support", app);
|
|
2620
|
+
if ("servers" in value) {
|
|
2621
|
+
if (!isStringArray(value.servers)) return null;
|
|
2622
|
+
sections.servers = value.servers;
|
|
2608
2623
|
}
|
|
2609
|
-
if (
|
|
2610
|
-
|
|
2624
|
+
if ("idpSessions" in value) {
|
|
2625
|
+
if (!isStringArray(value.idpSessions)) return null;
|
|
2626
|
+
sections.idpSessions = value.idpSessions;
|
|
2611
2627
|
}
|
|
2612
|
-
return
|
|
2628
|
+
return sections;
|
|
2613
2629
|
}
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
parse: parseMcpServersConfig
|
|
2624
|
-
};
|
|
2625
|
-
var cursor = {
|
|
2626
|
-
id: "cursor",
|
|
2627
|
-
label: "Cursor",
|
|
2628
|
-
defaultPaths: (_platform, homeDir) => [join4(homeDir, ".cursor", "mcp.json")],
|
|
2629
|
-
parse: parseMcpServersConfig
|
|
2630
|
-
};
|
|
2631
|
-
var cline = {
|
|
2632
|
-
id: "cline",
|
|
2633
|
-
label: "Cline",
|
|
2634
|
-
defaultPaths: (platform, homeDir) => [
|
|
2635
|
-
// Cline (VS Code extension) global settings live under the editor's
|
|
2636
|
-
// globalStorage; the documented cross-platform fallback is ~/Documents.
|
|
2637
|
-
join4(
|
|
2638
|
-
appSupportDir(platform, homeDir, "Code"),
|
|
2639
|
-
"User",
|
|
2640
|
-
"globalStorage",
|
|
2641
|
-
"saoudrizwan.claude-dev",
|
|
2642
|
-
"settings",
|
|
2643
|
-
"cline_mcp_settings.json"
|
|
2644
|
-
),
|
|
2645
|
-
join4(homeDir, "Documents", "Cline", "MCP", "cline_mcp_settings.json")
|
|
2646
|
-
],
|
|
2647
|
-
parse: parseMcpServersConfig
|
|
2648
|
-
};
|
|
2649
|
-
var vscode = {
|
|
2650
|
-
id: "vscode",
|
|
2651
|
-
label: "VS Code",
|
|
2652
|
-
defaultPaths: (platform, homeDir) => [
|
|
2653
|
-
join4(appSupportDir(platform, homeDir, "Code"), "User", "mcp.json")
|
|
2654
|
-
],
|
|
2655
|
-
parse: parseVsCodeConfig
|
|
2656
|
-
};
|
|
2657
|
-
var IMPORT_STRATEGY_LIST = [
|
|
2658
|
-
claudeDesktop,
|
|
2659
|
-
cursor,
|
|
2660
|
-
cline,
|
|
2661
|
-
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"
|
|
2662
2639
|
];
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
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
|
+
);
|
|
2666
2646
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
path: path6,
|
|
2683
|
-
error: `Failed to read ${path6}: ${msg}`,
|
|
2684
|
-
searched
|
|
2685
|
-
};
|
|
2686
|
-
}
|
|
2687
|
-
if (raw === null) continue;
|
|
2688
|
-
try {
|
|
2689
|
-
const config = strategy.parse(raw);
|
|
2690
|
-
return { type, found: true, path: path6, config, searched };
|
|
2691
|
-
} catch (err) {
|
|
2692
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2693
|
-
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;
|
|
2694
2662
|
}
|
|
2695
2663
|
}
|
|
2696
|
-
return
|
|
2664
|
+
return true;
|
|
2697
2665
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
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
|
+
}
|
|
2708
2680
|
}
|
|
2709
2681
|
}
|
|
2710
|
-
return
|
|
2682
|
+
return Object.values(snapshot.idpSessions).every(isValidIdpSession);
|
|
2711
2683
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
*/
|
|
2726
|
-
authHttpEchoSuppressUntilMs = 0;
|
|
2727
|
-
static AUTH_HTTP_ECHO_SUPPRESS_MS = 3e4;
|
|
2728
|
-
requestWaits = /* @__PURE__ */ new Map();
|
|
2729
|
-
authProviderHandle = null;
|
|
2730
|
-
appliedProtocolVersion;
|
|
2731
|
-
/** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
|
|
2732
|
-
static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
2733
|
-
constructor(sessionId) {
|
|
2734
|
-
this.sessionId = sessionId;
|
|
2735
|
-
}
|
|
2736
|
-
setAuthProviderHandle(handle) {
|
|
2737
|
-
this.authProviderHandle = handle;
|
|
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;
|
|
2738
2697
|
}
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
}
|
|
2743
|
-
this.authProviderHandle.setAuthState(authState);
|
|
2698
|
+
const parsed = typeof raw === "string" ? raw ? parseStore(raw) : null : raw;
|
|
2699
|
+
if (!isRecord2(parsed)) {
|
|
2700
|
+
return null;
|
|
2744
2701
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2702
|
+
if (isRecord2(parsed.state) && "version" in parsed) {
|
|
2703
|
+
return snapshotFromPayload(parsed.state);
|
|
2747
2704
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
* (#1935). The browser's SDK Client owns the `initialize` handshake, so the
|
|
2751
|
-
* `setProtocolVersion` call it makes lands on the *remote* transport; without
|
|
2752
|
-
* this the backend's real HTTP transport never learns the version and drops
|
|
2753
|
-
* `Mcp-Protocol-Version` from every subsequent request.
|
|
2754
|
-
*
|
|
2755
|
-
* Ignores anything that isn't a plausible version token so a client can't
|
|
2756
|
-
* push arbitrary bytes into an upstream header, and re-applies only on
|
|
2757
|
-
* change (the value rides every `/api/mcp/send`). The value arrives from an
|
|
2758
|
-
* unvalidated JSON body, so the type is checked rather than assumed —
|
|
2759
|
-
* `RegExp.test` would otherwise coerce a `123` or `true` into a "matching"
|
|
2760
|
-
* string and hand the wrong type to the transport.
|
|
2761
|
-
*/
|
|
2762
|
-
applyProtocolVersion(version) {
|
|
2763
|
-
if (typeof version !== "string" || version === this.appliedProtocolVersion || !_RemoteSession.PROTOCOL_VERSION_PATTERN.test(version)) {
|
|
2764
|
-
return;
|
|
2765
|
-
}
|
|
2766
|
-
this.appliedProtocolVersion = version;
|
|
2767
|
-
this.transport.setProtocolVersion?.(version);
|
|
2705
|
+
if ("servers" in parsed || "idpSessions" in parsed) {
|
|
2706
|
+
return snapshotFromPayload(parsed);
|
|
2768
2707
|
}
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
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;
|
|
2774
2718
|
}
|
|
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 };
|
|
2775
2726
|
}
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
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";
|
|
2779
2745
|
}
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
data: {
|
|
2786
|
-
error,
|
|
2787
|
-
code: -32e3
|
|
2788
|
-
// MCP error code for connection closed
|
|
2789
|
-
}
|
|
2790
|
-
});
|
|
2791
|
-
}
|
|
2792
|
-
isTransportDead() {
|
|
2793
|
-
return this.transportDead;
|
|
2794
|
-
}
|
|
2795
|
-
getTransportError() {
|
|
2796
|
-
return this.transportError;
|
|
2797
|
-
}
|
|
2798
|
-
hasEventConsumer() {
|
|
2799
|
-
return this.eventConsumer !== null;
|
|
2800
|
-
}
|
|
2801
|
-
beginSend() {
|
|
2802
|
-
this.activeSendCount++;
|
|
2803
|
-
}
|
|
2804
|
-
endSend() {
|
|
2805
|
-
if (this.activeSendCount > 0) {
|
|
2806
|
-
this.activeSendCount--;
|
|
2746
|
+
};
|
|
2747
|
+
var checkKeyringShape = (mod) => {
|
|
2748
|
+
try {
|
|
2749
|
+
if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
|
|
2750
|
+
return { ok: true, mod };
|
|
2807
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
|
+
};
|
|
2808
2766
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
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";
|
|
2812
2788
|
}
|
|
2813
|
-
|
|
2814
|
-
|
|
2789
|
+
};
|
|
2790
|
+
var SecretFileLockHeldError = class extends SecretStoreUnavailableError {
|
|
2791
|
+
constructor(message, options) {
|
|
2792
|
+
super(message, options);
|
|
2793
|
+
this.name = "SecretFileLockHeldError";
|
|
2815
2794
|
}
|
|
2816
|
-
|
|
2817
|
-
|
|
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";
|
|
2818
2803
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
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.`;
|
|
2821
2808
|
}
|
|
2822
|
-
|
|
2823
|
-
this
|
|
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).`;
|
|
2824
2811
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
return
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
clearTimeout(timer);
|
|
2847
|
-
timer = void 0;
|
|
2848
|
-
}
|
|
2849
|
-
};
|
|
2850
|
-
arm();
|
|
2851
|
-
this.requestWaits.set(requestId, {
|
|
2852
|
-
resolve: () => {
|
|
2853
|
-
clear();
|
|
2854
|
-
resolve9();
|
|
2855
|
-
},
|
|
2856
|
-
reject: (error) => {
|
|
2857
|
-
clear();
|
|
2858
|
-
reject(error);
|
|
2859
|
-
},
|
|
2860
|
-
// A progress notification for this request re-arms the full window, so
|
|
2861
|
-
// a tool that keeps reporting progress never trips the relay's own
|
|
2862
|
-
// deadline — the same guarantee the SDK client's `resetTimeoutOnProgress`
|
|
2863
|
-
// gives on a direct transport (#2028).
|
|
2864
|
-
resetTimeout: () => {
|
|
2865
|
-
if (timer === void 0) return;
|
|
2866
|
-
clear();
|
|
2867
|
-
arm();
|
|
2868
|
-
}
|
|
2869
|
-
});
|
|
2870
|
-
});
|
|
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
|
+
};
|
|
2822
|
+
}
|
|
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
|
+
};
|
|
2871
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 {
|
|
2872
2838
|
/**
|
|
2873
|
-
*
|
|
2874
|
-
*
|
|
2875
|
-
*
|
|
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.
|
|
2876
2842
|
*/
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
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);
|
|
2884
2855
|
}
|
|
2885
|
-
this.requestWaits.delete(requestId);
|
|
2886
|
-
wait.reject(new Error("MCP request wait cancelled"));
|
|
2887
2856
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
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;
|
|
2891
2869
|
}
|
|
2892
|
-
this.requestWaits.clear();
|
|
2893
2870
|
}
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
this.extendAuthHttpEchoSuppression();
|
|
2907
|
-
return true;
|
|
2908
|
-
}
|
|
2909
|
-
if (this.shouldSuppressAuthEcho()) {
|
|
2910
|
-
return true;
|
|
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);
|
|
2911
2883
|
}
|
|
2912
|
-
this.pushAuthChallenge(error.authChallenge);
|
|
2913
|
-
return true;
|
|
2914
2884
|
}
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
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);
|
|
2919
2898
|
}
|
|
2920
|
-
this.requestWaits.delete(requestId);
|
|
2921
|
-
wait.resolve();
|
|
2922
2899
|
}
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
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);
|
|
2928
2909
|
}
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
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);
|
|
2933
2916
|
}
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2917
|
+
}
|
|
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);
|
|
2937
2938
|
}
|
|
2938
|
-
this.pushEvent({ type: "message", data: message });
|
|
2939
2939
|
}
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
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);
|
|
2946
2954
|
}
|
|
2947
|
-
|
|
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;
|
|
2948
2986
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
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;
|
|
2954
3003
|
}
|
|
2955
|
-
|
|
2956
|
-
this.
|
|
2957
|
-
type: "fetch_stream_update",
|
|
2958
|
-
data: {
|
|
2959
|
-
id,
|
|
2960
|
-
eventCount: stream.eventCount,
|
|
2961
|
-
...stream.closedAt && { closedAt: stream.closedAt.toISOString() }
|
|
2962
|
-
}
|
|
2963
|
-
});
|
|
3004
|
+
async set(serverId, field, value) {
|
|
3005
|
+
this.map.set(buildAccount(serverId, field), value);
|
|
2964
3006
|
}
|
|
2965
|
-
|
|
2966
|
-
this.
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
}
|
|
3007
|
+
async delete(serverId, field) {
|
|
3008
|
+
this.map.delete(buildAccount(serverId, field));
|
|
3009
|
+
}
|
|
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
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
};
|
|
3017
|
+
var SessionSecretStore = class extends InMemorySecretStore {
|
|
3018
|
+
async isDurable() {
|
|
3019
|
+
return false;
|
|
2973
3020
|
}
|
|
2974
3021
|
};
|
|
2975
3022
|
|
|
2976
|
-
// ../../core/
|
|
2977
|
-
var
|
|
2978
|
-
|
|
2979
|
-
|
|
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
|
|
2980
3030
|
};
|
|
2981
|
-
|
|
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
|
+
};
|
|
2982
3074
|
return {
|
|
2983
|
-
...
|
|
2984
|
-
|
|
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
|
+
});
|
|
2985
3089
|
},
|
|
2986
|
-
|
|
2987
|
-
|
|
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();
|
|
2988
3106
|
}
|
|
2989
3107
|
};
|
|
2990
3108
|
}
|
|
2991
|
-
function
|
|
2992
|
-
|
|
2993
|
-
|
|
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
|
+
});
|
|
3128
|
+
}
|
|
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;
|
|
2994
3137
|
}
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
authState = {
|
|
3019
|
-
...authState,
|
|
3020
|
-
oauthTokens: {
|
|
3021
|
-
access_token: tokens.access_token,
|
|
3022
|
-
token_type: tokens.token_type,
|
|
3023
|
-
expires_in: tokens.expires_in,
|
|
3024
|
-
refresh_token: tokens.refresh_token,
|
|
3025
|
-
scope: tokens.scope,
|
|
3026
|
-
id_token: tokens.id_token
|
|
3027
|
-
}
|
|
3028
|
-
};
|
|
3029
|
-
},
|
|
3030
|
-
codeVerifier() {
|
|
3031
|
-
return void 0;
|
|
3032
|
-
},
|
|
3033
|
-
async saveCodeVerifier() {
|
|
3034
|
-
},
|
|
3035
|
-
clear() {
|
|
3036
|
-
authState = {};
|
|
3037
|
-
},
|
|
3038
|
-
async redirectToAuthorization() {
|
|
3039
|
-
throw new Error(
|
|
3040
|
-
"OAuth re-authorization must be performed in the Inspector web client (remote server cannot complete OAuth flows)"
|
|
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;
|
|
3152
|
+
try {
|
|
3153
|
+
release = await acquire(target, 0, fsShim).catch((err) => {
|
|
3154
|
+
if (!isStuckOrHeld(err, lockPathOf(target))) throw err;
|
|
3155
|
+
return acquire(target, RETRY, fsShim);
|
|
3156
|
+
});
|
|
3157
|
+
} catch (err) {
|
|
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.`
|
|
3041
3161
|
);
|
|
3042
|
-
},
|
|
3043
|
-
state() {
|
|
3044
|
-
return "";
|
|
3045
3162
|
}
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
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;
|
|
3167
|
+
}
|
|
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
|
+
);
|
|
3060
3189
|
}
|
|
3061
3190
|
};
|
|
3062
3191
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
var keyringLoad;
|
|
3067
|
-
var KeyringModuleShapeError = class extends Error {
|
|
3068
|
-
constructor(detail, options) {
|
|
3069
|
-
super(`@napi-rs/keyring loaded but ${detail}`, options);
|
|
3070
|
-
this.name = "KeyringModuleShapeError";
|
|
3071
|
-
}
|
|
3072
|
-
};
|
|
3073
|
-
var checkKeyringShape = (mod) => {
|
|
3192
|
+
async function withSecretFileLock(filePath, fn) {
|
|
3193
|
+
const release = await openSecretFileLock(filePath);
|
|
3194
|
+
if (release === null) return fn();
|
|
3074
3195
|
try {
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
return {
|
|
3079
|
-
ok: false,
|
|
3080
|
-
err: new KeyringModuleShapeError(
|
|
3081
|
-
"did not expose AsyncEntry / findCredentialsAsync"
|
|
3082
|
-
)
|
|
3083
|
-
};
|
|
3084
|
-
} catch (err) {
|
|
3085
|
-
return {
|
|
3086
|
-
ok: false,
|
|
3087
|
-
err: new KeyringModuleShapeError(
|
|
3088
|
-
`its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
|
|
3089
|
-
{ cause: err }
|
|
3090
|
-
)
|
|
3091
|
-
};
|
|
3196
|
+
return await fn();
|
|
3197
|
+
} finally {
|
|
3198
|
+
await release();
|
|
3092
3199
|
}
|
|
3093
|
-
};
|
|
3094
|
-
var loadKeyring = () => {
|
|
3095
|
-
keyringLoad ??= import("@napi-rs/keyring").then(
|
|
3096
|
-
checkKeyringShape,
|
|
3097
|
-
(err) => ({ ok: false, err })
|
|
3098
|
-
);
|
|
3099
|
-
return keyringLoad;
|
|
3100
|
-
};
|
|
3101
|
-
function parseAccount(account) {
|
|
3102
|
-
const idx = account.indexOf(":");
|
|
3103
|
-
if (idx <= 0 || idx === account.length - 1) return null;
|
|
3104
|
-
return {
|
|
3105
|
-
serverId: account.slice(0, idx),
|
|
3106
|
-
field: account.slice(idx + 1)
|
|
3107
|
-
};
|
|
3108
3200
|
}
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
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;
|
|
3123
3237
|
}
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
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
|
+
};
|
|
3128
3249
|
}
|
|
3129
|
-
if (
|
|
3130
|
-
|
|
3250
|
+
if (hasDirect) return { passphrase: direct };
|
|
3251
|
+
if (!wantsKeyFile) return {};
|
|
3252
|
+
if (!keyFile) {
|
|
3253
|
+
return { problem: `${SECRET_KEY_FILE_ENV} is set but empty` };
|
|
3131
3254
|
}
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
async function probeKeyringAvailable() {
|
|
3135
|
-
const keyring = await loadKeyring();
|
|
3136
|
-
if (!keyring.ok) {
|
|
3137
|
-
const err = keyring.err;
|
|
3255
|
+
const resolved2 = path3.resolve(keyFile);
|
|
3256
|
+
if (secretFilePath !== void 0 && isSameFile(resolved2, secretFilePath)) {
|
|
3138
3257
|
return {
|
|
3139
|
-
|
|
3140
|
-
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`
|
|
3141
3259
|
};
|
|
3142
3260
|
}
|
|
3261
|
+
let contents;
|
|
3143
3262
|
try {
|
|
3144
|
-
|
|
3145
|
-
await entry.getPassword();
|
|
3146
|
-
return { available: true };
|
|
3263
|
+
contents = readFileSync4(resolved2, "utf-8");
|
|
3147
3264
|
} catch (err) {
|
|
3148
3265
|
return {
|
|
3149
|
-
|
|
3150
|
-
|
|
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)}`
|
|
3151
3269
|
};
|
|
3152
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 };
|
|
3153
3276
|
}
|
|
3154
|
-
var
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
*/
|
|
3161
|
-
async getStrict(serverId, field) {
|
|
3162
|
-
try {
|
|
3163
|
-
const keyring = await loadKeyring();
|
|
3164
|
-
if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
|
|
3165
|
-
const entry = new keyring.mod.AsyncEntry(
|
|
3166
|
-
SERVICE_NAME,
|
|
3167
|
-
buildAccount(serverId, field)
|
|
3168
|
-
);
|
|
3169
|
-
return await entry.getPassword() ?? null;
|
|
3170
|
-
} catch (err) {
|
|
3171
|
-
if (err instanceof KeychainUnavailableError) throw err;
|
|
3172
|
-
throw new KeychainUnavailableError(err);
|
|
3173
|
-
}
|
|
3174
|
-
}
|
|
3175
|
-
async get(serverId, field) {
|
|
3176
|
-
try {
|
|
3177
|
-
const keyring = await loadKeyring();
|
|
3178
|
-
if (!keyring.ok) return null;
|
|
3179
|
-
const entry = new keyring.mod.AsyncEntry(
|
|
3180
|
-
SERVICE_NAME,
|
|
3181
|
-
buildAccount(serverId, field)
|
|
3182
|
-
);
|
|
3183
|
-
const v = await entry.getPassword();
|
|
3184
|
-
return v ?? null;
|
|
3185
|
-
} catch {
|
|
3186
|
-
return null;
|
|
3187
|
-
}
|
|
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
|
+
);
|
|
3188
3283
|
}
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
buildAccount(serverId, field)
|
|
3196
|
-
);
|
|
3197
|
-
await entry.setPassword(value);
|
|
3198
|
-
} catch (err) {
|
|
3199
|
-
if (err instanceof KeychainUnavailableError) throw err;
|
|
3200
|
-
throw new KeychainUnavailableError(err);
|
|
3201
|
-
}
|
|
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
|
+
);
|
|
3202
3290
|
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
const entry = new keyring.mod.AsyncEntry(
|
|
3208
|
-
SERVICE_NAME,
|
|
3209
|
-
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.`
|
|
3210
3295
|
);
|
|
3211
|
-
await entry.deleteCredential();
|
|
3212
|
-
} catch {
|
|
3213
3296
|
}
|
|
3214
3297
|
}
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
}
|
|
3224
|
-
const prefix = `${serverId}:`;
|
|
3225
|
-
for (const c of creds) {
|
|
3226
|
-
if (!c.account.startsWith(prefix)) continue;
|
|
3227
|
-
const parsed = parseAccount(c.account);
|
|
3228
|
-
if (!parsed || parsed.serverId !== serverId) continue;
|
|
3229
|
-
await this.delete(serverId, parsed.field);
|
|
3230
|
-
}
|
|
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";
|
|
3231
3306
|
}
|
|
3232
3307
|
};
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
async (field) => [field, await store.get(serverId, field)]
|
|
3244
|
-
)
|
|
3245
|
-
);
|
|
3246
|
-
const found = {};
|
|
3247
|
-
for (const [field, value] of entries) {
|
|
3248
|
-
if (value !== null) found[field] = value;
|
|
3249
|
-
}
|
|
3250
|
-
out[serverId] = found;
|
|
3251
|
-
})
|
|
3252
|
-
);
|
|
3253
|
-
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
|
+
};
|
|
3254
3318
|
}
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
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]
|
|
3261
3326
|
);
|
|
3262
3327
|
}
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
map = /* @__PURE__ */ new Map();
|
|
3268
|
-
async get(serverId, field) {
|
|
3269
|
-
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";
|
|
3270
3332
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
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}`;
|
|
3273
3337
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3338
|
+
if (parts.tag.length !== GCM_TAG_BYTES) {
|
|
3339
|
+
return `authentication tag is ${parts.tag.length} bytes, expected ${GCM_TAG_BYTES}`;
|
|
3276
3340
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
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";
|
|
3282
3346
|
}
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
async isDurable() {
|
|
3286
|
-
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`;
|
|
3287
3349
|
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
// ../../core/auth/node/secret-store-selection.ts
|
|
3291
|
-
import { randomUUID } from "crypto";
|
|
3292
|
-
import * as fsSync from "fs";
|
|
3293
|
-
import * as fs4 from "fs/promises";
|
|
3294
|
-
import * as path4 from "path";
|
|
3295
|
-
|
|
3296
|
-
// ../../core/auth/node/file-secret-store.ts
|
|
3297
|
-
import * as crypto2 from "crypto";
|
|
3298
|
-
import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
3299
|
-
import * as fs3 from "fs/promises";
|
|
3300
|
-
import * as path3 from "path";
|
|
3301
|
-
|
|
3302
|
-
// ../../core/auth/node/file-lock.ts
|
|
3303
|
-
import nodeFs, { statSync, rmdirSync } from "fs";
|
|
3304
|
-
import * as fs2 from "fs/promises";
|
|
3305
|
-
import * as path2 from "path";
|
|
3306
|
-
import properLockfile from "proper-lockfile";
|
|
3307
|
-
var STALE_MS = 1e4;
|
|
3308
|
-
var RETRY = {
|
|
3309
|
-
retries: 20,
|
|
3310
|
-
factor: 2,
|
|
3311
|
-
minTimeout: 20,
|
|
3312
|
-
maxTimeout: 1e3
|
|
3313
|
-
};
|
|
3314
|
-
var RETRY_BUDGET_MS = (() => {
|
|
3315
|
-
let total = 0;
|
|
3316
|
-
let delay = RETRY.minTimeout;
|
|
3317
|
-
for (let i = 0; i < RETRY.retries; i++) {
|
|
3318
|
-
total += Math.min(delay, RETRY.maxTimeout);
|
|
3319
|
-
delay *= RETRY.factor;
|
|
3350
|
+
if (!isPositiveInt(r) || !isPositiveInt(p)) {
|
|
3351
|
+
return `kdf r/p are ${String(r)}/${String(p)}, which are not positive integers`;
|
|
3320
3352
|
}
|
|
3321
|
-
|
|
3322
|
-
})()
|
|
3323
|
-
var warned = /* @__PURE__ */ new Set();
|
|
3324
|
-
function warnOnce(message) {
|
|
3325
|
-
if (warned.has(message)) return;
|
|
3326
|
-
warned.add(message);
|
|
3327
|
-
console.warn(`[mcp-inspector] ${message}`);
|
|
3328
|
-
}
|
|
3329
|
-
function isStuckOrHeld(err, lockPath) {
|
|
3330
|
-
return isHeldElsewhere(err) || identifySync(lockPath) !== null;
|
|
3331
|
-
}
|
|
3332
|
-
var isHeldElsewhere = (err) => err.code === "ELOCKED";
|
|
3333
|
-
var describeError = (err) => err instanceof Error ? err.message : String(err);
|
|
3334
|
-
var lockPathOf = (target) => `${target}.lock`;
|
|
3335
|
-
function identifySync(lockPath) {
|
|
3336
|
-
try {
|
|
3337
|
-
const stat2 = statSync(lockPath);
|
|
3338
|
-
return { ino: stat2.ino, birthtimeMs: stat2.birthtimeMs };
|
|
3339
|
-
} catch {
|
|
3340
|
-
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})`;
|
|
3341
3355
|
}
|
|
3342
|
-
|
|
3343
|
-
function guardedFs(lockPath, owned, onRefused) {
|
|
3344
|
-
const mine = () => {
|
|
3345
|
-
if (owned.id === null) return true;
|
|
3346
|
-
const now = identifySync(lockPath);
|
|
3347
|
-
if (now === null) return false;
|
|
3348
|
-
return now.ino === owned.id.ino && now.birthtimeMs === owned.id.birthtimeMs;
|
|
3349
|
-
};
|
|
3350
|
-
const removeIfMine = () => {
|
|
3351
|
-
if (!mine()) {
|
|
3352
|
-
onRefused();
|
|
3353
|
-
return;
|
|
3354
|
-
}
|
|
3355
|
-
rmdirSync(lockPath);
|
|
3356
|
-
};
|
|
3357
|
-
return {
|
|
3358
|
-
...nodeFs,
|
|
3359
|
-
// **Identity is captured here, not after `lock()` resolves.** `mkdir` is
|
|
3360
|
-
// the moment the directory becomes ours, and it is synchronous with
|
|
3361
|
-
// respect to this process: recording it in the callback, before yielding,
|
|
3362
|
-
// leaves no window. Capturing after the acquire promise settled — as this
|
|
3363
|
-
// did originally — spans the library's own `utimes`/`stat` probe, and a
|
|
3364
|
-
// waiter replacing our directory inside that span would have us record
|
|
3365
|
-
// *the winner's* identity as our own, after which the guard below would
|
|
3366
|
-
// cheerfully delete their lock.
|
|
3367
|
-
mkdir: (p, cb) => {
|
|
3368
|
-
nodeFs.mkdir(p, (err) => {
|
|
3369
|
-
if (!err) owned.id = identifySync(lockPath);
|
|
3370
|
-
cb(err);
|
|
3371
|
-
});
|
|
3372
|
-
},
|
|
3373
|
-
// Reported as success when refused: the library's bookkeeping should
|
|
3374
|
-
// forget this lock either way. Leaving it registered would hand the
|
|
3375
|
-
// `signal-exit` handler a record pointing at the winner's directory.
|
|
3376
|
-
rmdir: (_p, cb) => {
|
|
3377
|
-
try {
|
|
3378
|
-
removeIfMine();
|
|
3379
|
-
cb(null);
|
|
3380
|
-
} catch (err) {
|
|
3381
|
-
cb(err);
|
|
3382
|
-
}
|
|
3383
|
-
},
|
|
3384
|
-
/* v8 ignore next 3 -- @preserve: only reachable from proper-lockfile's
|
|
3385
|
-
signal-exit handler, i.e. at real process exit, which no in-process
|
|
3386
|
-
test can drive. Its logic is `removeIfMine`, covered via `rmdir`. */
|
|
3387
|
-
rmdirSync: () => {
|
|
3388
|
-
removeIfMine();
|
|
3389
|
-
}
|
|
3390
|
-
};
|
|
3391
|
-
}
|
|
3392
|
-
function acquire(target, retries, fsShim) {
|
|
3393
|
-
return properLockfile.lock(target, {
|
|
3394
|
-
realpath: false,
|
|
3395
|
-
stale: STALE_MS,
|
|
3396
|
-
retries,
|
|
3397
|
-
// Every directory removal the library performs — on release and from its
|
|
3398
|
-
// exit handler — routes through here. See `guardedFs`.
|
|
3399
|
-
fs: fsShim,
|
|
3400
|
-
// The library's default `onCompromised` *throws* — from a timer, with no
|
|
3401
|
-
// caller on the stack, so it lands as an uncaught exception and takes the
|
|
3402
|
-
// process down. This is also the library's *only* signal for the
|
|
3403
|
-
// stale-takeover race described at the top of this file — someone
|
|
3404
|
-
// declared our lock stale and replaced it — so it is the one place a user
|
|
3405
|
-
// learns the guarantee was lost. Worth saying; not worth killing an
|
|
3406
|
-
// Inspector session over.
|
|
3407
|
-
onCompromised: (err) => warnOnce(
|
|
3408
|
-
`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.`
|
|
3409
|
-
)
|
|
3410
|
-
});
|
|
3411
|
-
}
|
|
3412
|
-
async function isFileLockHeld(filePath) {
|
|
3413
|
-
try {
|
|
3414
|
-
return await properLockfile.check(path2.resolve(filePath), {
|
|
3415
|
-
realpath: false,
|
|
3416
|
-
stale: STALE_MS
|
|
3417
|
-
});
|
|
3418
|
-
} catch {
|
|
3419
|
-
return false;
|
|
3420
|
-
}
|
|
3421
|
-
}
|
|
3422
|
-
async function openSecretFileLock(filePath) {
|
|
3423
|
-
const target = path2.resolve(filePath);
|
|
3424
|
-
await fs2.mkdir(path2.dirname(target), { recursive: true }).catch(() => {
|
|
3425
|
-
});
|
|
3426
|
-
const owned = { id: null };
|
|
3427
|
-
const fsShim = guardedFs(
|
|
3428
|
-
lockPathOf(target),
|
|
3429
|
-
owned,
|
|
3430
|
-
() => warnOnce(
|
|
3431
|
-
`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.`
|
|
3432
|
-
)
|
|
3433
|
-
);
|
|
3434
|
-
let release;
|
|
3435
|
-
try {
|
|
3436
|
-
release = await acquire(target, 0, fsShim).catch((err) => {
|
|
3437
|
-
if (!isStuckOrHeld(err, lockPathOf(target))) throw err;
|
|
3438
|
-
return acquire(target, RETRY, fsShim);
|
|
3439
|
-
});
|
|
3440
|
-
} catch (err) {
|
|
3441
|
-
if (isStuckOrHeld(err, lockPathOf(target))) {
|
|
3442
|
-
throw new SecretStoreUnavailableError(
|
|
3443
|
-
`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.`
|
|
3444
|
-
);
|
|
3445
|
-
}
|
|
3446
|
-
warnOnce(
|
|
3447
|
-
`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.`
|
|
3448
|
-
);
|
|
3449
|
-
return null;
|
|
3450
|
-
}
|
|
3451
|
-
return async () => {
|
|
3452
|
-
try {
|
|
3453
|
-
await release();
|
|
3454
|
-
} catch (err) {
|
|
3455
|
-
if (err.code === "ERELEASED") return;
|
|
3456
|
-
warnOnce(
|
|
3457
|
-
// **Conditional, not an instruction.** Two blanket versions of this
|
|
3458
|
-
// have now been wrong in opposite directions: "it expires on its own"
|
|
3459
|
-
// (false whenever the same `rmdir` that blocked us also blocks stale
|
|
3460
|
-
// takeover — `ENOTEMPTY`, `EACCES`, `EPERM`, `EROFS`), and "remove it
|
|
3461
|
-
// by hand" (false whenever the failure was transient, since the path
|
|
3462
|
-
// may by then hold a *different, live* Inspector's lock, and deleting
|
|
3463
|
-
// that destroys the exclusion of a process that did nothing wrong).
|
|
3464
|
-
//
|
|
3465
|
-
// `proper-lockfile` forwards whatever the filesystem returned, and
|
|
3466
|
-
// nothing here can tell a permanent refusal from a passing one. So
|
|
3467
|
-
// this reports what happened and states the two conditions the
|
|
3468
|
-
// operator can check for themselves, rather than asserting an outcome
|
|
3469
|
-
// this code does not know.
|
|
3470
|
-
`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.`
|
|
3471
|
-
);
|
|
3472
|
-
}
|
|
3473
|
-
};
|
|
3474
|
-
}
|
|
3475
|
-
async function withSecretFileLock(filePath, fn) {
|
|
3476
|
-
const release = await openSecretFileLock(filePath);
|
|
3477
|
-
if (release === null) return fn();
|
|
3478
|
-
try {
|
|
3479
|
-
return await fn();
|
|
3480
|
-
} finally {
|
|
3481
|
-
await release();
|
|
3482
|
-
}
|
|
3483
|
-
}
|
|
3484
|
-
|
|
3485
|
-
// ../../core/auth/node/file-secret-store.ts
|
|
3486
|
-
var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve9, reject) => {
|
|
3487
|
-
crypto2.scrypt(passphrase, salt, keylen, options, (err, key) => {
|
|
3488
|
-
if (err) reject(err);
|
|
3489
|
-
else resolve9(key);
|
|
3490
|
-
});
|
|
3491
|
-
});
|
|
3492
|
-
var FORMAT_VERSION = 1;
|
|
3493
|
-
var CIPHER = "aes-256-gcm";
|
|
3494
|
-
var KEY_BYTES = 32;
|
|
3495
|
-
var IV_BYTES = 12;
|
|
3496
|
-
var GCM_TAG_BYTES = 16;
|
|
3497
|
-
var SCRYPT_N = 16384;
|
|
3498
|
-
var SCRYPT_R = 8;
|
|
3499
|
-
var SCRYPT_P = 1;
|
|
3500
|
-
var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
|
|
3501
|
-
var SECRET_KEY_FILE_ENV = "MCP_INSPECTOR_SECRET_KEY_FILE";
|
|
3502
|
-
function isSameFile(a, b) {
|
|
3503
|
-
if (path3.resolve(a) === path3.resolve(b)) return true;
|
|
3504
|
-
try {
|
|
3505
|
-
const sa = statSync2(a);
|
|
3506
|
-
const sb = statSync2(b);
|
|
3507
|
-
return sa.dev === sb.dev && sa.ino === sb.ino;
|
|
3508
|
-
} catch {
|
|
3509
|
-
return false;
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
function resolveSecretPassphrase(env = process.env, secretFilePath) {
|
|
3513
|
-
const direct = env[SECRET_KEY_ENV];
|
|
3514
|
-
const hasDirect = direct !== void 0 && direct.trim() !== "";
|
|
3515
|
-
const rawKeyFile = env[SECRET_KEY_FILE_ENV];
|
|
3516
|
-
const wantsKeyFile = rawKeyFile !== void 0;
|
|
3517
|
-
const keyFile = rawKeyFile?.trim();
|
|
3518
|
-
if (hasDirect && wantsKeyFile) {
|
|
3519
|
-
return {
|
|
3520
|
-
problem: `both ${SECRET_KEY_ENV} and ${SECRET_KEY_FILE_ENV} are set; set only one`
|
|
3521
|
-
};
|
|
3522
|
-
}
|
|
3523
|
-
if (hasDirect) return { passphrase: direct };
|
|
3524
|
-
if (!wantsKeyFile) return {};
|
|
3525
|
-
if (!keyFile) {
|
|
3526
|
-
return { problem: `${SECRET_KEY_FILE_ENV} is set but empty` };
|
|
3527
|
-
}
|
|
3528
|
-
const resolved2 = path3.resolve(keyFile);
|
|
3529
|
-
if (secretFilePath !== void 0 && isSameFile(resolved2, secretFilePath)) {
|
|
3530
|
-
return {
|
|
3531
|
-
problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is the secrets file itself; point it at a separate key file`
|
|
3532
|
-
};
|
|
3533
|
-
}
|
|
3534
|
-
let contents;
|
|
3535
|
-
try {
|
|
3536
|
-
contents = readFileSync4(resolved2, "utf-8");
|
|
3537
|
-
} catch (err) {
|
|
3538
|
-
return {
|
|
3539
|
-
// `String(err)` rather than `.message`: `readFileSync` only ever throws
|
|
3540
|
-
// an `Error`, so an `instanceof` guard would be a branch no test can take.
|
|
3541
|
-
problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) could not be read: ${String(err)}`
|
|
3542
|
-
};
|
|
3543
|
-
}
|
|
3544
|
-
const passphrase = contents.replace(/[\r\n]+$/, "");
|
|
3545
|
-
if (passphrase.trim() === "") {
|
|
3546
|
-
return { problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is empty` };
|
|
3547
|
-
}
|
|
3548
|
-
return { passphrase };
|
|
3549
|
-
}
|
|
3550
|
-
var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
|
|
3551
|
-
function asSecretMap(value, filePath) {
|
|
3552
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3553
|
-
throw new SecretStoreUnavailableError(
|
|
3554
|
-
`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.`
|
|
3555
|
-
);
|
|
3556
|
-
}
|
|
3557
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
3558
|
-
if (typeof entry !== "string") {
|
|
3559
|
-
throw new SecretStoreUnavailableError(
|
|
3560
|
-
`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.`
|
|
3561
|
-
);
|
|
3562
|
-
}
|
|
3563
|
-
}
|
|
3564
|
-
return { ...value };
|
|
3565
|
-
}
|
|
3566
|
-
var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
|
|
3567
|
-
constructor(filePath, hasKey) {
|
|
3568
|
-
super(
|
|
3569
|
-
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.`
|
|
3570
|
-
);
|
|
3571
|
-
this.name = "SecretFileKeyMismatchError";
|
|
3572
|
-
}
|
|
3573
|
-
};
|
|
3574
|
-
var encodeParts = (iv, tag, body) => `${iv.toString("base64")}.${tag.toString("base64")}.${body.toString("base64")}`;
|
|
3575
|
-
function decodeParts(payload) {
|
|
3576
|
-
const parts = payload.split(".");
|
|
3577
|
-
if (parts.length !== 3) return null;
|
|
3578
|
-
const [iv, tag, body] = parts;
|
|
3579
|
-
return {
|
|
3580
|
-
iv: Buffer.from(iv, "base64"),
|
|
3581
|
-
tag: Buffer.from(tag, "base64"),
|
|
3582
|
-
body: Buffer.from(body, "base64")
|
|
3583
|
-
};
|
|
3584
|
-
}
|
|
3585
|
-
var MAX_WRITE_ATTEMPTS = 5;
|
|
3586
|
-
var fileQueues = /* @__PURE__ */ new Map();
|
|
3587
|
-
function sameMap(a, b) {
|
|
3588
|
-
const ak = Object.keys(a);
|
|
3589
|
-
if (ak.length !== Object.keys(b).length) return false;
|
|
3590
|
-
return ak.every(
|
|
3591
|
-
(k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]
|
|
3592
|
-
);
|
|
3593
|
-
}
|
|
3594
|
-
var isPositiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
3595
|
-
function encryptedEnvelopeProblem(parsed) {
|
|
3596
|
-
if (!parsed.kdf || typeof parsed.data !== "string") {
|
|
3597
|
-
return "encrypted envelope is missing its kdf or data";
|
|
3598
|
-
}
|
|
3599
|
-
const parts = decodeParts(parsed.data);
|
|
3600
|
-
if (!parts) return "encrypted payload is not iv.tag.ciphertext";
|
|
3601
|
-
if (parts.iv.length !== IV_BYTES) {
|
|
3602
|
-
return `initialization vector is ${parts.iv.length} bytes, expected ${IV_BYTES}`;
|
|
3603
|
-
}
|
|
3604
|
-
if (parts.tag.length !== GCM_TAG_BYTES) {
|
|
3605
|
-
return `authentication tag is ${parts.tag.length} bytes, expected ${GCM_TAG_BYTES}`;
|
|
3606
|
-
}
|
|
3607
|
-
if (parts.body.length === 0) return "ciphertext is empty";
|
|
3608
|
-
const { algorithm, salt, N, r, p } = parsed.kdf;
|
|
3609
|
-
if (algorithm !== "scrypt") return `unsupported kdf "${String(algorithm)}"`;
|
|
3610
|
-
if (typeof salt !== "string" || Buffer.from(salt, "base64").length === 0) {
|
|
3611
|
-
return "kdf salt is missing or not base64";
|
|
3612
|
-
}
|
|
3613
|
-
if (!isPositiveInt(N) || N < 2 || (N & N - 1) !== 0) {
|
|
3614
|
-
return `kdf N is ${String(N)}, which is not a power of two above 1`;
|
|
3615
|
-
}
|
|
3616
|
-
if (!isPositiveInt(r) || !isPositiveInt(p)) {
|
|
3617
|
-
return `kdf r/p are ${String(r)}/${String(p)}, which are not positive integers`;
|
|
3618
|
-
}
|
|
3619
|
-
if (N > SCRYPT_N || r > SCRYPT_R || p > SCRYPT_P) {
|
|
3620
|
-
return `kdf cost parameters (N=${N}, r=${r}, p=${p}) exceed the supported maximum (N=${SCRYPT_N}, r=${SCRYPT_R}, p=${SCRYPT_P})`;
|
|
3621
|
-
}
|
|
3622
|
-
return null;
|
|
3356
|
+
return null;
|
|
3623
3357
|
}
|
|
3624
3358
|
var FileSecretStore = class {
|
|
3625
3359
|
filePath;
|
|
@@ -3806,7 +3540,9 @@ var FileSecretStore = class {
|
|
|
3806
3540
|
let plain;
|
|
3807
3541
|
try {
|
|
3808
3542
|
const key = await this.deriveKey(parsed.kdf);
|
|
3809
|
-
const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv
|
|
3543
|
+
const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv, {
|
|
3544
|
+
authTagLength: GCM_TAG_BYTES
|
|
3545
|
+
});
|
|
3810
3546
|
decipher.setAuthTag(parts.tag);
|
|
3811
3547
|
plain = Buffer.concat([
|
|
3812
3548
|
decipher.update(parts.body),
|
|
@@ -3840,7 +3576,9 @@ var FileSecretStore = class {
|
|
|
3840
3576
|
};
|
|
3841
3577
|
const key = await this.deriveKey(kdf);
|
|
3842
3578
|
const iv = crypto2.randomBytes(IV_BYTES);
|
|
3843
|
-
const cipher = crypto2.createCipheriv(CIPHER, key, iv
|
|
3579
|
+
const cipher = crypto2.createCipheriv(CIPHER, key, iv, {
|
|
3580
|
+
authTagLength: GCM_TAG_BYTES
|
|
3581
|
+
});
|
|
3844
3582
|
const body = Buffer.concat([
|
|
3845
3583
|
cipher.update(JSON.stringify(map), "utf-8"),
|
|
3846
3584
|
cipher.final()
|
|
@@ -4005,27 +3743,54 @@ var FileSecretStore = class {
|
|
|
4005
3743
|
const found = {};
|
|
4006
3744
|
for (const field of fields) {
|
|
4007
3745
|
const value = map?.[buildAccount2(serverId, field)];
|
|
4008
|
-
if (value !== void 0) found
|
|
3746
|
+
if (value !== void 0) setOwnEntry(found, field, value);
|
|
4009
3747
|
}
|
|
4010
|
-
out
|
|
3748
|
+
setOwnEntry(out, serverId, found);
|
|
4011
3749
|
}
|
|
4012
3750
|
return out;
|
|
4013
3751
|
}
|
|
4014
3752
|
/**
|
|
4015
|
-
*
|
|
4016
|
-
*
|
|
4017
|
-
*
|
|
4018
|
-
*
|
|
4019
|
-
* migrations *write* on `null` and then strip the disk copy, so a
|
|
4020
|
-
* transient read error that cleared before the write let an older
|
|
4021
|
-
* on-disk value replace a newer stored one — the same inversion the
|
|
4022
|
-
* strict seam was introduced to prevent, still open on the store the
|
|
4023
|
-
* seam was introduced for.
|
|
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.
|
|
4024
3757
|
*/
|
|
4025
|
-
async
|
|
3758
|
+
async getManyStrict(requests) {
|
|
3759
|
+
let map;
|
|
4026
3760
|
try {
|
|
4027
|
-
|
|
4028
|
-
|
|
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);
|
|
3774
|
+
}
|
|
3775
|
+
setOwnEntry(out, serverId, found);
|
|
3776
|
+
}
|
|
3777
|
+
return out;
|
|
3778
|
+
}
|
|
3779
|
+
/**
|
|
3780
|
+
* The intolerant read — same lookup as {@link get}, minus the catch.
|
|
3781
|
+
*
|
|
3782
|
+
* Without this, `secretStoreGetStrict` fell back to `get` for the file
|
|
3783
|
+
* store and turned every read failure into `null`. Both plaintext
|
|
3784
|
+
* migrations *write* on `null` and then strip the disk copy, so a
|
|
3785
|
+
* transient read error that cleared before the write let an older
|
|
3786
|
+
* on-disk value replace a newer stored one — the same inversion the
|
|
3787
|
+
* strict seam was introduced to prevent, still open on the store the
|
|
3788
|
+
* seam was introduced for.
|
|
3789
|
+
*/
|
|
3790
|
+
async getStrict(serverId, field) {
|
|
3791
|
+
try {
|
|
3792
|
+
const map = await this.serialize(() => this.readMap());
|
|
3793
|
+
return map?.[buildAccount2(serverId, field)] ?? null;
|
|
4029
3794
|
} catch (err) {
|
|
4030
3795
|
if (err instanceof SecretStoreUnavailableError) throw err;
|
|
4031
3796
|
throw new SecretStoreUnavailableError(
|
|
@@ -4051,6 +3816,9 @@ var FileSecretStore = class {
|
|
|
4051
3816
|
*/
|
|
4052
3817
|
async setMany(serverId, values) {
|
|
4053
3818
|
if (Object.keys(values).length === 0) return;
|
|
3819
|
+
for (const [field, value] of Object.entries(values)) {
|
|
3820
|
+
assertSecretString(field, value);
|
|
3821
|
+
}
|
|
4054
3822
|
await this.serialize(
|
|
4055
3823
|
() => this.mutate((map) => {
|
|
4056
3824
|
const next = { ...map };
|
|
@@ -4062,6 +3830,7 @@ var FileSecretStore = class {
|
|
|
4062
3830
|
);
|
|
4063
3831
|
}
|
|
4064
3832
|
async set(serverId, field, value) {
|
|
3833
|
+
assertSecretString(field, value);
|
|
4065
3834
|
await this.serialize(
|
|
4066
3835
|
() => this.mutate((map) => ({
|
|
4067
3836
|
...map,
|
|
@@ -4077,9 +3846,11 @@ var FileSecretStore = class {
|
|
|
4077
3846
|
await this.deleteWhere((key) => key.startsWith(prefix));
|
|
4078
3847
|
}
|
|
4079
3848
|
/**
|
|
4080
|
-
* Shared delete body.
|
|
4081
|
-
*
|
|
4082
|
-
*
|
|
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.
|
|
4083
3854
|
*/
|
|
4084
3855
|
async deleteWhere(match) {
|
|
4085
3856
|
try {
|
|
@@ -4092,7 +3863,12 @@ var FileSecretStore = class {
|
|
|
4092
3863
|
return next;
|
|
4093
3864
|
})
|
|
4094
3865
|
);
|
|
4095
|
-
} 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
|
+
);
|
|
4096
3872
|
}
|
|
4097
3873
|
}
|
|
4098
3874
|
};
|
|
@@ -4353,150 +4129,1432 @@ async function handOffStagedSecrets(staged, filePath, keyring) {
|
|
|
4353
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.`
|
|
4354
4130
|
);
|
|
4355
4131
|
}
|
|
4356
|
-
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
|
+
}
|
|
4357
5303
|
}
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
} catch {
|
|
4362
|
-
console.warn(
|
|
4363
|
-
`
|
|
4364
|
-
[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.`
|
|
4365
|
-
);
|
|
5304
|
+
/** Command-scoped auth was returned on HTTP; suppress async onerror echo on SSE. */
|
|
5305
|
+
noteAuthChallengeDeliveredViaHttp() {
|
|
5306
|
+
this.extendAuthHttpEchoSuppression();
|
|
4366
5307
|
}
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
async function anythingToMigrate(filePath) {
|
|
4370
|
-
const base = path4.basename(filePath);
|
|
4371
|
-
try {
|
|
4372
|
-
return (await fs4.readdir(path4.dirname(filePath))).some(
|
|
4373
|
-
(name) => name === base || isSnapshotName(name, base)
|
|
4374
|
-
);
|
|
4375
|
-
} catch (err) {
|
|
4376
|
-
const code = err.code;
|
|
4377
|
-
return code !== "ENOENT" && code !== "ENOTDIR";
|
|
5308
|
+
extendAuthHttpEchoSuppression() {
|
|
5309
|
+
this.authHttpEchoSuppressUntilMs = Date.now() + _RemoteSession.AUTH_HTTP_ECHO_SUPPRESS_MS;
|
|
4378
5310
|
}
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
const dir = path4.dirname(filePath);
|
|
4382
|
-
const base = path4.basename(filePath);
|
|
4383
|
-
let names;
|
|
4384
|
-
try {
|
|
4385
|
-
names = (await fs4.readdir(dir)).filter((n) => isSnapshotName(n, base));
|
|
4386
|
-
} catch {
|
|
4387
|
-
return;
|
|
5311
|
+
shouldSuppressAuthEcho() {
|
|
5312
|
+
return Date.now() < this.authHttpEchoSuppressUntilMs;
|
|
4388
5313
|
}
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
if (await isFileLockHeld(orphan)) continue;
|
|
4392
|
-
try {
|
|
4393
|
-
await fs4.link(orphan, filePath);
|
|
4394
|
-
await fs4.rm(orphan, { force: true });
|
|
4395
|
-
console.warn(
|
|
4396
|
-
`
|
|
4397
|
-
[mcp-inspector] Recovered secrets left behind by an interrupted migration (${orphan} \u2192 ${filePath}).`
|
|
4398
|
-
);
|
|
4399
|
-
} catch {
|
|
4400
|
-
console.warn(
|
|
4401
|
-
`
|
|
4402
|
-
[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.`
|
|
4403
|
-
);
|
|
4404
|
-
}
|
|
5314
|
+
hasActiveSend() {
|
|
5315
|
+
return this.activeSendCount > 0;
|
|
4405
5316
|
}
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
let entries;
|
|
4409
|
-
try {
|
|
4410
|
-
entries = await file.readAll();
|
|
4411
|
-
} catch (err) {
|
|
4412
|
-
console.warn(
|
|
4413
|
-
`
|
|
4414
|
-
[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)}`
|
|
4415
|
-
);
|
|
4416
|
-
return false;
|
|
5317
|
+
pushAuthChallenge(challenge) {
|
|
5318
|
+
this.pushEvent({ type: "auth_challenge", data: challenge });
|
|
4417
5319
|
}
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
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
|
+
});
|
|
4442
5366
|
}
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
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();
|
|
4449
5375
|
}
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
store.filePath,
|
|
4458
|
-
"file",
|
|
4459
|
-
info.reason,
|
|
4460
|
-
info.detail
|
|
4461
|
-
);
|
|
4462
|
-
}
|
|
4463
|
-
var DeferredSecretStore = class {
|
|
4464
|
-
async target() {
|
|
4465
|
-
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"));
|
|
4466
5383
|
}
|
|
4467
|
-
|
|
4468
|
-
|
|
5384
|
+
rejectActiveRequestWaits(error) {
|
|
5385
|
+
for (const wait of this.requestWaits.values()) {
|
|
5386
|
+
wait.reject(error);
|
|
5387
|
+
}
|
|
5388
|
+
this.requestWaits.clear();
|
|
4469
5389
|
}
|
|
4470
5390
|
/**
|
|
4471
|
-
*
|
|
4472
|
-
*
|
|
4473
|
-
*
|
|
4474
|
-
*
|
|
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).
|
|
4475
5395
|
*/
|
|
4476
|
-
|
|
4477
|
-
|
|
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;
|
|
4478
5410
|
}
|
|
4479
|
-
|
|
4480
|
-
|
|
5411
|
+
settleRequestWait(requestId) {
|
|
5412
|
+
const wait = this.requestWaits.get(requestId);
|
|
5413
|
+
if (!wait) {
|
|
5414
|
+
return;
|
|
5415
|
+
}
|
|
5416
|
+
this.requestWaits.delete(requestId);
|
|
5417
|
+
wait.resolve();
|
|
4481
5418
|
}
|
|
4482
|
-
|
|
4483
|
-
|
|
5419
|
+
pushEvent(event) {
|
|
5420
|
+
if (this.eventConsumer) {
|
|
5421
|
+
this.eventConsumer(event);
|
|
5422
|
+
} else {
|
|
5423
|
+
this.eventQueue.push(event);
|
|
5424
|
+
}
|
|
4484
5425
|
}
|
|
4485
|
-
|
|
4486
|
-
|
|
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 });
|
|
4487
5435
|
}
|
|
4488
|
-
|
|
4489
|
-
|
|
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
|
+
});
|
|
4490
5444
|
}
|
|
4491
|
-
|
|
4492
|
-
|
|
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
|
+
});
|
|
4493
5460
|
}
|
|
4494
|
-
|
|
4495
|
-
|
|
5461
|
+
onStderr(entry) {
|
|
5462
|
+
this.pushEvent({
|
|
5463
|
+
type: "stdio_log",
|
|
5464
|
+
data: {
|
|
5465
|
+
timestamp: entry.timestamp.toISOString(),
|
|
5466
|
+
message: entry.message
|
|
5467
|
+
}
|
|
5468
|
+
});
|
|
4496
5469
|
}
|
|
4497
5470
|
};
|
|
4498
|
-
|
|
4499
|
-
|
|
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
|
+
};
|
|
4500
5558
|
}
|
|
4501
5559
|
|
|
4502
5560
|
// ../../core/client/config-parse.ts
|
|
@@ -4651,6 +5709,23 @@ function mergeSecretsIntoClientConfig(config, secrets) {
|
|
|
4651
5709
|
}
|
|
4652
5710
|
|
|
4653
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
|
+
}
|
|
4654
5729
|
async function readIdpSecretFromKeychain(secretStore) {
|
|
4655
5730
|
const secret = await secretStore.get(
|
|
4656
5731
|
CLIENT_KEYCHAIN_ID,
|
|
@@ -4660,29 +5735,39 @@ async function readIdpSecretFromKeychain(secretStore) {
|
|
|
4660
5735
|
return { [SECRET_FIELD_IDP_CLIENT_SECRET]: secret };
|
|
4661
5736
|
}
|
|
4662
5737
|
async function migrateClientPlaintextSecret(filePath, config, secretStore) {
|
|
4663
|
-
const { stripped, secrets } = extractSecretsFromClientConfig(config);
|
|
4664
|
-
const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
|
|
4665
|
-
if (!value) return config;
|
|
4666
5738
|
try {
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
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
|
+
});
|
|
4682
5769
|
} catch (err) {
|
|
4683
|
-
if (err instanceof
|
|
4684
|
-
return config;
|
|
4685
|
-
}
|
|
5770
|
+
if (err instanceof SecretFileLockHeldError) return config;
|
|
4686
5771
|
throw err;
|
|
4687
5772
|
}
|
|
4688
5773
|
}
|
|
@@ -4702,30 +5787,44 @@ async function writeClientConfigStore(filePath, body, secretStore) {
|
|
|
4702
5787
|
const validated = parseClientConfig(body);
|
|
4703
5788
|
const { stripped, secrets } = extractSecretsFromClientConfig(validated);
|
|
4704
5789
|
const idpSecret = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
|
|
4705
|
-
|
|
4706
|
-
await secretStore
|
|
4707
|
-
CLIENT_KEYCHAIN_ID,
|
|
4708
|
-
SECRET_FIELD_IDP_CLIENT_SECRET,
|
|
4709
|
-
idpSecret
|
|
4710
|
-
);
|
|
4711
|
-
} else {
|
|
4712
|
-
await secretStore.delete(
|
|
4713
|
-
CLIENT_KEYCHAIN_ID,
|
|
5790
|
+
await withClientConfigLock(filePath, "save", async () => {
|
|
5791
|
+
const prior = await snapshotSecretFields(secretStore, CLIENT_KEYCHAIN_ID, [
|
|
4714
5792
|
SECRET_FIELD_IDP_CLIENT_SECRET
|
|
4715
|
-
);
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
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(
|
|
4722
5809
|
filePath,
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
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
|
+
});
|
|
4729
5828
|
}
|
|
4730
5829
|
async function preserveLegacyPlaintext(filePath, validated, stripped, idpSecret) {
|
|
4731
5830
|
if (!idpSecret) return stripped;
|
|
@@ -4740,8 +5839,25 @@ async function preserveLegacyPlaintext(filePath, validated, stripped, idpSecret)
|
|
|
4740
5839
|
}
|
|
4741
5840
|
}
|
|
4742
5841
|
async function deleteClientConfigStore(filePath, secretStore) {
|
|
4743
|
-
await
|
|
4744
|
-
|
|
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
|
+
});
|
|
4745
5861
|
}
|
|
4746
5862
|
|
|
4747
5863
|
// ../../core/mcp/remote/node/server.ts
|
|
@@ -4768,6 +5884,7 @@ async function primeAndHoldSseStream(stream, cleanup) {
|
|
|
4768
5884
|
var MAX_APP_DOCUMENT_CHARS = 8 * 1024 * 1024;
|
|
4769
5885
|
var MAX_APP_DOCUMENT_BODY_BYTES = 24 * 1024 * 1024;
|
|
4770
5886
|
var MAX_APP_CSP_CHARS = 8 * 1024;
|
|
5887
|
+
var MAX_STORAGE_BODY_BYTES = 4 * 1024 * 1024;
|
|
4771
5888
|
function isHeaderSafeValue(value) {
|
|
4772
5889
|
return !/[^\t\x20-\x7e]/.test(value);
|
|
4773
5890
|
}
|
|
@@ -5400,6 +6517,10 @@ function createRemoteApp(options) {
|
|
|
5400
6517
|
const config = await readClientConfigStore(filePath, secretStore);
|
|
5401
6518
|
return c.json(config);
|
|
5402
6519
|
}
|
|
6520
|
+
if (storeId === OAUTH_PERSIST_STORE_ID) {
|
|
6521
|
+
const snapshot = await readOAuthStore(filePath, secretStore);
|
|
6522
|
+
return c.json(snapshot ?? {});
|
|
6523
|
+
}
|
|
5403
6524
|
const raw = await readStoreFile(filePath);
|
|
5404
6525
|
if (raw === null) {
|
|
5405
6526
|
return c.json({}, 200);
|
|
@@ -5407,40 +6528,74 @@ function createRemoteApp(options) {
|
|
|
5407
6528
|
const store = parseStore(raw);
|
|
5408
6529
|
return c.json(store);
|
|
5409
6530
|
} catch (error) {
|
|
6531
|
+
const keychainResp = keychainErrorResponse(c, error);
|
|
6532
|
+
if (keychainResp) return keychainResp;
|
|
5410
6533
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5411
6534
|
return c.json({ error: `Failed to read store: ${msg}` }, 500);
|
|
5412
6535
|
}
|
|
5413
6536
|
});
|
|
5414
|
-
app.post(
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
const filePath = getStoreFilePath(storageDir, storeId);
|
|
5426
|
-
try {
|
|
5427
|
-
if (storeId === "client") {
|
|
5428
|
-
await writeClientConfigStore(filePath, body, secretStore);
|
|
5429
|
-
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);
|
|
5430
6548
|
}
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
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);
|
|
5437
6596
|
}
|
|
5438
|
-
const keychainResp = storeId === "client" ? keychainErrorResponse(c, error) : void 0;
|
|
5439
|
-
if (keychainResp) return keychainResp;
|
|
5440
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
5441
|
-
return c.json({ error: `Failed to write store: ${msg}` }, 500);
|
|
5442
6597
|
}
|
|
5443
|
-
|
|
6598
|
+
);
|
|
5444
6599
|
app.delete("/api/storage/:storeId", async (c) => {
|
|
5445
6600
|
const storeId = c.req.param("storeId");
|
|
5446
6601
|
if (!storeId || !validateStoreId(storeId)) {
|
|
@@ -5452,9 +6607,15 @@ function createRemoteApp(options) {
|
|
|
5452
6607
|
await deleteClientConfigStore(filePath, secretStore);
|
|
5453
6608
|
return c.json({ ok: true });
|
|
5454
6609
|
}
|
|
6610
|
+
if (storeId === OAUTH_PERSIST_STORE_ID) {
|
|
6611
|
+
await removeOAuthStore(filePath, secretStore);
|
|
6612
|
+
return c.json({ ok: true });
|
|
6613
|
+
}
|
|
5455
6614
|
await deleteStoreFile(filePath);
|
|
5456
6615
|
return c.json({ ok: true });
|
|
5457
6616
|
} catch (error) {
|
|
6617
|
+
const keychainResp = keychainErrorResponse(c, error);
|
|
6618
|
+
if (keychainResp) return keychainResp;
|
|
5458
6619
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5459
6620
|
return c.json({ error: `Failed to delete store: ${msg}` }, 500);
|
|
5460
6621
|
}
|
|
@@ -5525,6 +6686,13 @@ function createRemoteApp(options) {
|
|
|
5525
6686
|
const out = {};
|
|
5526
6687
|
for (const [id, val] of Object.entries(raw)) {
|
|
5527
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
|
+
}
|
|
5528
6696
|
const valObj = val;
|
|
5529
6697
|
if ("settings" in valObj) {
|
|
5530
6698
|
logWarn(
|
|
@@ -5952,7 +7120,7 @@ function createRemoteApp(options) {
|
|
|
5952
7120
|
return false;
|
|
5953
7121
|
};
|
|
5954
7122
|
let warnedSessionPreserved = false;
|
|
5955
|
-
const
|
|
7123
|
+
const migratePlaintextSecrets2 = async (config) => {
|
|
5956
7124
|
let changed = false;
|
|
5957
7125
|
const next = { mcpServers: {} };
|
|
5958
7126
|
const durable = await secretStoreIsDurable(secretStore);
|
|
@@ -6023,7 +7191,22 @@ function createRemoteApp(options) {
|
|
|
6023
7191
|
return out;
|
|
6024
7192
|
};
|
|
6025
7193
|
const deleteKeychainFields = async (id, fields) => {
|
|
6026
|
-
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
|
+
}
|
|
6027
7210
|
};
|
|
6028
7211
|
const entryForDisk = async (built, stripped, previous) => {
|
|
6029
7212
|
if (await secretStoreIsDurable(secretStore)) return stripped;
|
|
@@ -6079,7 +7262,7 @@ function createRemoteApp(options) {
|
|
|
6079
7262
|
mcpServers: normalizeMcpServers(parsedInside?.mcpServers)
|
|
6080
7263
|
};
|
|
6081
7264
|
if (!hasPlaintextSecrets(inside)) return inside;
|
|
6082
|
-
const { migrated, changed } = await
|
|
7265
|
+
const { migrated, changed } = await migratePlaintextSecrets2(inside);
|
|
6083
7266
|
if (changed) {
|
|
6084
7267
|
await writeMcpAndTrackMtime(serializeStore(migrated));
|
|
6085
7268
|
}
|
|
@@ -6121,7 +7304,7 @@ function createRemoteApp(options) {
|
|
|
6121
7304
|
if (typeof body.id !== "string" || !validateStoreId(body.id)) {
|
|
6122
7305
|
return c.json(
|
|
6123
7306
|
{
|
|
6124
|
-
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__`"
|
|
6125
7308
|
},
|
|
6126
7309
|
400
|
|
6127
7310
|
);
|
|
@@ -6139,15 +7322,29 @@ function createRemoteApp(options) {
|
|
|
6139
7322
|
try {
|
|
6140
7323
|
return await withWriteLock(async () => {
|
|
6141
7324
|
const current = await readMcpConfig();
|
|
6142
|
-
if (
|
|
7325
|
+
if (Object.hasOwn(current.mcpServers, id)) {
|
|
6143
7326
|
return c.json({ error: `Server '${id}' already exists` }, 409);
|
|
6144
7327
|
}
|
|
6145
7328
|
const built = buildStoredEntry(id, body.config, postSettings);
|
|
6146
7329
|
const { stripped, secrets } = extractSecretsFromStored(built);
|
|
6147
7330
|
await secretStore.deleteAllForServer(id);
|
|
6148
|
-
await
|
|
6149
|
-
|
|
6150
|
-
|
|
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
|
+
}
|
|
6151
7348
|
return c.json({ ok: true });
|
|
6152
7349
|
});
|
|
6153
7350
|
} catch (error) {
|
|
@@ -6237,10 +7434,10 @@ function createRemoteApp(options) {
|
|
|
6237
7434
|
try {
|
|
6238
7435
|
return await withWriteLock(async () => {
|
|
6239
7436
|
const current = await readMcpConfig();
|
|
6240
|
-
if (!(
|
|
7437
|
+
if (!Object.hasOwn(current.mcpServers, originalId)) {
|
|
6241
7438
|
return c.json({ error: `Server '${originalId}' not found` }, 404);
|
|
6242
7439
|
}
|
|
6243
|
-
if (newId !== originalId &&
|
|
7440
|
+
if (newId !== originalId && Object.hasOwn(current.mcpServers, newId)) {
|
|
6244
7441
|
return c.json({ error: `Server '${newId}' already exists` }, 409);
|
|
6245
7442
|
}
|
|
6246
7443
|
const existing = current.mcpServers[originalId];
|
|
@@ -6297,27 +7494,80 @@ function createRemoteApp(options) {
|
|
|
6297
7494
|
}
|
|
6298
7495
|
if (newId !== originalId) {
|
|
6299
7496
|
const previousFields = expectedSecretFields(existing);
|
|
6300
|
-
const
|
|
7497
|
+
const originalPrior = await snapshotSecretFields(
|
|
7498
|
+
secretStore,
|
|
6301
7499
|
originalId,
|
|
6302
7500
|
previousFields
|
|
6303
7501
|
);
|
|
7502
|
+
const keychainSecrets = {};
|
|
7503
|
+
for (const { field, value } of originalPrior) {
|
|
7504
|
+
if (value !== null) setOwnEntry(keychainSecrets, field, value);
|
|
7505
|
+
}
|
|
6304
7506
|
const secretsToWrite = mergeRenameKeychainSecrets(
|
|
6305
7507
|
stripped,
|
|
6306
7508
|
keychainSecrets,
|
|
6307
7509
|
secrets
|
|
6308
7510
|
);
|
|
6309
|
-
await
|
|
6310
|
-
|
|
6311
|
-
|
|
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
|
+
}
|
|
6312
7541
|
} else {
|
|
6313
7542
|
const previousFields = new Set(expectedSecretFields(existing));
|
|
6314
7543
|
const stillExpected = new Set(expectedSecretFields(built));
|
|
6315
7544
|
const obsolete = [...previousFields].filter(
|
|
6316
7545
|
(field) => field === SECRET_FIELD_OAUTH_CLIENT_SECRET ? settingsIntent.kind !== "preserve" && !(field in secrets) : !stillExpected.has(field)
|
|
6317
7546
|
);
|
|
6318
|
-
await
|
|
6319
|
-
|
|
6320
|
-
|
|
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
|
+
}
|
|
6321
7571
|
}
|
|
6322
7572
|
return c.json({ ok: true });
|
|
6323
7573
|
});
|
|
@@ -6338,13 +7588,28 @@ function createRemoteApp(options) {
|
|
|
6338
7588
|
try {
|
|
6339
7589
|
return await withWriteLock(async () => {
|
|
6340
7590
|
const current = await readMcpConfig();
|
|
6341
|
-
if (!(
|
|
7591
|
+
if (!Object.hasOwn(current.mcpServers, id)) {
|
|
6342
7592
|
await secretStore.deleteAllForServer(id);
|
|
6343
7593
|
return c.json({ ok: true });
|
|
6344
7594
|
}
|
|
7595
|
+
const stored = current.mcpServers[id];
|
|
6345
7596
|
delete current.mcpServers[id];
|
|
6346
|
-
await
|
|
6347
|
-
|
|
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
|
+
}
|
|
6348
7613
|
return c.json({ ok: true });
|
|
6349
7614
|
});
|
|
6350
7615
|
} catch (error) {
|