perf(mcp): cache immutable config discovery (#79882)

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Stephen Hebson
2026-07-17 09:20:48 -04:00
committed by GitHub
parent 2c43e86787
commit cc9d80c3d6
2 changed files with 333 additions and 7 deletions
@@ -0,0 +1,155 @@
/** Tests process-wide caching for immutable bundled MCP config discovery. */
import { afterEach, describe, expect, it, vi } from "vitest";
import { clearPluginMetadataLifecycleCaches } from "../plugins/plugin-metadata-lifecycle.js";
import { loadSessionMcpConfig } from "./agent-bundle-mcp-runtime-config.js";
const mocks = vi.hoisted(() => ({
loadCount: 0,
diagnostics: [] as Array<{ pluginId: string; message: string }>,
}));
vi.mock("./embedded-agent-mcp.js", () => ({
loadEmbeddedAgentMcpConfig: (params: {
cfg?: { mcp?: { servers?: Record<string, unknown> } };
}) => {
mocks.loadCount += 1;
return {
diagnostics: structuredClone(mocks.diagnostics),
mcpServers: params.cfg?.mcp?.servers ?? {},
};
},
}));
afterEach(() => {
mocks.loadCount = 0;
mocks.diagnostics = [];
clearPluginMetadataLifecycleCaches();
});
describe("session MCP config discovery cache", () => {
it("reuses immutable discovery across full and filtered catalog preparation", () => {
const cfg = {
mcp: {
servers: {
alpha: { command: "alpha" },
beta: { command: "beta" },
},
},
};
const full = loadSessionMcpConfig({ workspaceDir: "/reuse-workspace", cfg });
const filtered = loadSessionMcpConfig({
workspaceDir: "/reuse-workspace",
cfg,
includeServerNames: new Set(["alpha"]),
});
const filteredAgain = loadSessionMcpConfig({
workspaceDir: "/reuse-workspace",
cfg,
includeServerNames: new Set(["alpha"]),
});
expect(mocks.loadCount).toBe(1);
expect(filteredAgain).not.toBe(filtered);
expect(filteredAgain).toEqual(filtered);
expect(Object.keys(full.loaded.mcpServers)).toEqual(["alpha", "beta"]);
expect(Object.keys(filtered.loaded.mcpServers)).toEqual(["alpha"]);
expect(filtered.fingerprint).not.toBe(full.fingerprint);
const alpha = filtered.loaded.mcpServers.alpha;
expect(alpha).toBeDefined();
if (!alpha) {
throw new Error("expected filtered alpha server");
}
alpha.command = "mutated";
const isolated = loadSessionMcpConfig({
workspaceDir: "/reuse-workspace",
cfg,
includeServerNames: new Set(["alpha"]),
});
expect(isolated.loaded.mcpServers.alpha).toEqual({ command: "alpha" });
});
it("invalidates discovery when config, workspace, or manifest snapshot changes", () => {
const firstConfig = { mcp: { servers: { alpha: { command: "alpha" } } } };
const secondConfig = { mcp: { servers: { beta: { command: "beta" } } } };
const firstRegistry = { plugins: [] };
const secondRegistry = { plugins: [] };
const first = loadSessionMcpConfig({
workspaceDir: "/workspace",
cfg: firstConfig,
manifestRegistry: firstRegistry,
});
const second = loadSessionMcpConfig({
workspaceDir: "/workspace",
cfg: secondConfig,
manifestRegistry: firstRegistry,
});
loadSessionMcpConfig({
workspaceDir: "/other-workspace",
cfg: firstConfig,
manifestRegistry: firstRegistry,
});
loadSessionMcpConfig({
workspaceDir: "/workspace",
cfg: firstConfig,
manifestRegistry: secondRegistry,
});
expect(mocks.loadCount).toBe(4);
expect(first.fingerprint).not.toBe(second.fingerprint);
});
it("snapshots nested config values at the cache boundary", () => {
const cfg = {
mcp: {
servers: {
alpha: { command: "alpha", args: ["original"], env: { MODE: "original" } },
},
},
};
loadSessionMcpConfig({ workspaceDir: "/snapshot-workspace", cfg });
cfg.mcp.servers.alpha.args[0] = "mutated";
cfg.mcp.servers.alpha.env.MODE = "mutated";
const isolated = loadSessionMcpConfig({
workspaceDir: "/snapshot-workspace",
cfg: {
mcp: {
servers: {
alpha: { command: "alpha", args: ["original"], env: { MODE: "original" } },
},
},
},
});
expect(isolated.loaded.mcpServers.alpha).toEqual({
command: "alpha",
args: ["original"],
env: { MODE: "original" },
});
});
it("reloads discovery after plugin metadata lifecycle invalidation", () => {
const cfg = { mcp: { servers: { alpha: { command: "alpha" } } } };
loadSessionMcpConfig({ workspaceDir: "/reload-workspace", cfg });
clearPluginMetadataLifecycleCaches();
loadSessionMcpConfig({ workspaceDir: "/reload-workspace", cfg });
expect(mocks.loadCount).toBe(2);
});
it("retries discovery after a diagnostic result", () => {
const cfg = { mcp: { servers: { alpha: { command: "alpha" } } } };
mocks.diagnostics = [{ pluginId: "example", message: "temporary read failure" }];
loadSessionMcpConfig({ workspaceDir: "/retry-workspace", cfg, logDiagnostics: false });
mocks.diagnostics = [];
loadSessionMcpConfig({ workspaceDir: "/retry-workspace", cfg, logDiagnostics: false });
loadSessionMcpConfig({ workspaceDir: "/retry-workspace", cfg, logDiagnostics: false });
expect(mocks.loadCount).toBe(2);
});
});
+178 -7
View File
@@ -1,8 +1,11 @@
/** Session MCP config loading, filtering, and catalog fingerprints. */
import crypto from "node:crypto";
import { resolveRuntimeConfigCacheKey } from "../config/runtime-snapshot.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { logWarn } from "../logger.js";
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
import { registerPluginMetadataProcessMemoLifecycleClear } from "../plugins/plugin-metadata-lifecycle.js";
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import { assignSafeServerNames } from "./agent-bundle-mcp-names.js";
import { loadEmbeddedAgentMcpConfig } from "./embedded-agent-mcp.js";
import {
@@ -11,6 +14,137 @@ import {
} from "./mcp-connection-resolver.js";
type LoadedMcpConfig = ReturnType<typeof loadEmbeddedAgentMcpConfig>;
type PreparedSessionMcpConfig = {
loaded: LoadedMcpConfig;
fingerprint: string;
};
type SessionMcpConfigDiscoveryCacheEntry = {
loaded: LoadedMcpConfig;
preparedByVariant: Map<string, PreparedSessionMcpConfig>;
};
const SESSION_MCP_CONFIG_DISCOVERY_CACHE_KEY = Symbol.for(
"openclaw.sessionMcpConfigDiscoveryCache",
);
const SESSION_MCP_CONFIG_DISCOVERY_CACHE_LIMIT = 128;
const SESSION_MCP_PREPARED_CONFIG_VARIANT_LIMIT = 64;
const EMPTY_OPENCLAW_CONFIG: OpenClawConfig = {};
type SessionMcpConfigDiscoveryCacheState = {
entries: Map<string, SessionMcpConfigDiscoveryCacheEntry>;
manifestRegistryIds: WeakMap<object, number>;
nextManifestRegistryId: number;
};
function getSessionMcpConfigDiscoveryCacheState(): SessionMcpConfigDiscoveryCacheState {
return resolveGlobalSingleton(SESSION_MCP_CONFIG_DISCOVERY_CACHE_KEY, () => ({
entries: new Map(),
manifestRegistryIds: new WeakMap(),
nextManifestRegistryId: 1,
}));
}
function resolveManifestRegistryCacheId(
manifestRegistry?: Pick<PluginManifestRegistry, "plugins">,
): string {
if (!manifestRegistry) {
return "discovered";
}
const state = getSessionMcpConfigDiscoveryCacheState();
const identity = manifestRegistry.plugins;
const existing = state.manifestRegistryIds.get(identity);
if (existing !== undefined) {
return String(existing);
}
const created = state.nextManifestRegistryId;
state.nextManifestRegistryId += 1;
state.manifestRegistryIds.set(identity, created);
return String(created);
}
function buildSessionMcpConfigDiscoveryCacheKey(params: {
workspaceDir: string;
cfg?: OpenClawConfig;
manifestRegistry?: Pick<PluginManifestRegistry, "plugins">;
}): string {
return JSON.stringify({
v: 1,
workspaceDir: params.workspaceDir,
config: resolveRuntimeConfigCacheKey(params.cfg ?? EMPTY_OPENCLAW_CONFIG),
manifestRegistry: resolveManifestRegistryCacheId(params.manifestRegistry),
});
}
function trimSessionMcpConfigDiscoveryCache(state: SessionMcpConfigDiscoveryCacheState): void {
while (state.entries.size > SESSION_MCP_CONFIG_DISCOVERY_CACHE_LIMIT) {
const oldest = state.entries.keys().next().value;
if (typeof oldest !== "string") {
return;
}
state.entries.delete(oldest);
}
}
function trimPreparedConfigVariants(
preparedByVariant: Map<string, PreparedSessionMcpConfig>,
): void {
while (preparedByVariant.size > SESSION_MCP_PREPARED_CONFIG_VARIANT_LIMIT) {
const oldest = preparedByVariant.keys().next().value;
if (typeof oldest !== "string") {
return;
}
preparedByVariant.delete(oldest);
}
}
function clonePreparedSessionMcpConfig(
prepared: PreparedSessionMcpConfig,
): PreparedSessionMcpConfig {
// Session runtimes own and may normalize their launch config. Keep cached
// preparation immutable by never exposing its object graph to a caller.
return structuredClone(prepared);
}
function loadCachedEmbeddedAgentMcpConfig(params: {
workspaceDir: string;
cfg?: OpenClawConfig;
manifestRegistry?: Pick<PluginManifestRegistry, "plugins">;
}): SessionMcpConfigDiscoveryCacheEntry {
const state = getSessionMcpConfigDiscoveryCacheState();
const key = buildSessionMcpConfigDiscoveryCacheKey(params);
const cached = state.entries.get(key);
if (cached) {
// LRU order bounds long-lived processes that observe many config revisions.
state.entries.delete(key);
state.entries.set(key, cached);
return cached;
}
// Bundle manifests and their MCP JSON are process-stable metadata. Keep the
// merged discovery result warm; live clients, catalogs, and failures remain
// session-owned and are never stored here.
const discovered = structuredClone(loadEmbeddedAgentMcpConfig(params));
const loaded = {
loaded: discovered,
preparedByVariant: new Map(),
};
// Diagnostics can represent transient filesystem or manifest failures. Keep
// those results session-owned so the next run retries discovery.
if (discovered.diagnostics.length > 0) {
return loaded;
}
state.entries.set(key, loaded);
trimSessionMcpConfigDiscoveryCache(state);
return loaded;
}
function clearSessionMcpConfigDiscoveryCache(): void {
const state = getSessionMcpConfigDiscoveryCacheState();
state.entries.clear();
state.manifestRegistryIds = new WeakMap();
state.nextManifestRegistryId = 1;
}
registerPluginMetadataProcessMemoLifecycleClear(clearSessionMcpConfigDiscoveryCache);
function digestSafeServerNameAssignments(
safeServerNamesByServer?: ReadonlyMap<string, string>,
@@ -23,6 +157,26 @@ function digestSafeServerNameAssignments(
);
}
function sortedSetEntries(values?: ReadonlySet<string>): string[] | undefined {
return values ? [...values].toSorted((a, b) => a.localeCompare(b)) : undefined;
}
function buildPreparedConfigVariantKey(params: {
includeServerNames?: ReadonlySet<string>;
excludeServerNames?: ReadonlySet<string>;
redactConnectionServerNames?: ReadonlySet<string>;
safeServerNames?: Record<string, string>;
mcpAppsEnabled: boolean;
}): string {
return JSON.stringify({
include: sortedSetEntries(params.includeServerNames),
exclude: sortedSetEntries(params.excludeServerNames),
redact: sortedSetEntries(params.redactConnectionServerNames),
safeServerNames: params.safeServerNames,
mcpAppsEnabled: params.mcpAppsEnabled,
});
}
function createCatalogFingerprint(params: {
servers: Record<string, unknown>;
mcpAppsEnabled: boolean;
@@ -73,35 +227,52 @@ export function loadSessionMcpConfig(params: {
loaded: LoadedMcpConfig;
fingerprint: string;
} {
const loaded = loadEmbeddedAgentMcpConfig({
const discovery = loadCachedEmbeddedAgentMcpConfig({
workspaceDir: params.workspaceDir,
cfg: params.cfg,
manifestRegistry: params.manifestRegistry,
});
if (params.logDiagnostics !== false) {
for (const diagnostic of loaded.diagnostics) {
for (const diagnostic of discovery.loaded.diagnostics) {
logWarn(`bundle-mcp: ${diagnostic.pluginId}: ${diagnostic.message}`);
}
}
const mcpServers = filterMcpServers(loaded.mcpServers, {
const safeServerNames = digestSafeServerNameAssignments(params.safeServerNamesByServer);
const mcpAppsEnabled = params.cfg?.mcp?.apps?.enabled === true;
const variantKey = buildPreparedConfigVariantKey({
includeServerNames: params.includeServerNames,
excludeServerNames: params.excludeServerNames,
redactConnectionServerNames: params.redactConnectionServerNames,
safeServerNames,
mcpAppsEnabled,
});
const prepared = discovery.preparedByVariant.get(variantKey);
if (prepared) {
discovery.preparedByVariant.delete(variantKey);
discovery.preparedByVariant.set(variantKey, prepared);
return clonePreparedSessionMcpConfig(prepared);
}
const mcpServers = filterMcpServers(discovery.loaded.mcpServers, {
includeServerNames: params.includeServerNames,
excludeServerNames: params.excludeServerNames,
});
const fingerprintServers = params.redactConnectionServerNames?.size
? redactMcpServersForFingerprint(mcpServers, params.redactConnectionServerNames)
: mcpServers;
const safeServerNames = digestSafeServerNameAssignments(params.safeServerNamesByServer);
return {
const result = {
loaded: {
...loaded,
...discovery.loaded,
mcpServers,
},
fingerprint: createCatalogFingerprint({
servers: fingerprintServers,
mcpAppsEnabled: params.cfg?.mcp?.apps?.enabled === true,
mcpAppsEnabled,
...(safeServerNames ? { safeServerNames } : {}),
}),
};
discovery.preparedByVariant.set(variantKey, result);
trimPreparedConfigVariants(discovery.preparedByVariant);
return clonePreparedSessionMcpConfig(result);
}
/**