refactor(config): centralize MCP config commits (#118462)

This commit is contained in:
Peter Steinberger
2026-08-02 21:54:10 -07:00
committed by GitHub
parent a842f482d5
commit 0226fdcbf7
+76 -129
View File
@@ -21,26 +21,18 @@ type McpArgvRestoreResult =
| { ok: true; server: Record<string, unknown> }
| { ok: false; error: string };
type ConfigMcpReadResult =
| {
ok: true;
path: string;
config: OpenClawConfig;
mcpServers: ConfigMcpServers;
baseHash?: string;
}
| { ok: false; path: string; error: string };
type ConfigMcpSuccess = {
path: string;
config: OpenClawConfig;
mcpServers: ConfigMcpServers;
};
type ConfigMcpFailure = { ok: false; path: string; error: string };
type ConfigMcpReadResult = (ConfigMcpSuccess & { ok: true; baseHash?: string }) | ConfigMcpFailure;
type ConfigMcpWriteResult =
| {
ok: true;
path: string;
config: OpenClawConfig;
mcpServers: ConfigMcpServers;
removed?: boolean;
updated?: boolean;
}
| { ok: false; path: string; error: string };
| (ConfigMcpSuccess & { ok: true; removed?: boolean; updated?: boolean })
| ConfigMcpFailure;
type LoadedConfigMcpServers = Extract<ConfigMcpReadResult, { ok: true }>;
/** Include/exclude tool selection stored for a configured MCP server. */
type McpServerToolSelection = {
@@ -81,10 +73,7 @@ function restoreMcpServerArgvSentinels(params: {
) {
return {
ok: false,
error:
'Cannot restore MCP args containing "' +
REDACTED_SENTINEL +
'" without the same original argv shape.',
error: `Cannot restore MCP args containing "${REDACTED_SENTINEL}" without the same original argv shape.`,
};
}
@@ -92,10 +81,7 @@ function restoreMcpServerArgvSentinels(params: {
if (incomingArgs.some((arg, index) => arg !== displayedArgs[index])) {
return {
ok: false,
error:
'Cannot restore MCP args containing "' +
REDACTED_SENTINEL +
'" after argv changed. Replace every redacted value explicitly before editing args.',
error: `Cannot restore MCP args containing "${REDACTED_SENTINEL}" after argv changed. Replace every redacted value explicitly before editing args.`,
};
}
return {
@@ -126,6 +112,48 @@ export async function listConfiguredMcpServers(): Promise<ConfigMcpReadResult> {
};
}
async function commitConfiguredMcpServers(params: {
loaded: LoadedConfigMcpServers;
servers: ConfigMcpServers;
errorLabel: string;
success?: { removed?: boolean; updated?: boolean };
independentlyOwnedName?: string;
}): Promise<ConfigMcpWriteResult> {
const next = structuredClone(params.loaded.config);
if (Object.keys(params.servers).length > 0) {
next.mcp = { ...next.mcp, servers: params.servers };
} else if (next.mcp) {
delete next.mcp.servers;
if (Object.keys(next.mcp).length === 0) {
delete next.mcp;
}
}
const validated = validateConfigObjectWithPlugins(next);
if (!validated.ok) {
const issue = expectDefined(validated.issues[0], "issues entry at 0");
return {
ok: false,
path: params.loaded.path,
error: `Config invalid after MCP ${params.errorLabel} (${issue.path}: ${issue.message}).`,
};
}
await replaceConfigFile({
nextConfig: validated.config,
baseHash: params.loaded.baseHash,
});
if (params.independentlyOwnedName) {
markClawMcpServerIndependentlyOwned(params.independentlyOwnedName);
}
return {
ok: true,
path: params.loaded.path,
config: validated.config,
mcpServers: params.servers,
...params.success,
};
}
async function updateConfiguredMcpServerConfig(params: {
name: string;
update: (server: Record<string, unknown>) => Record<string, unknown>;
@@ -142,46 +170,19 @@ async function updateConfiguredMcpServerConfig(params: {
return loaded;
}
if (!Object.hasOwn(loaded.mcpServers, name)) {
return {
ok: true,
path: loaded.path,
config: loaded.config,
mcpServers: loaded.mcpServers,
updated: false,
};
const { baseHash: _baseHash, ...unchanged } = loaded;
return { ...unchanged, updated: false };
}
const next = structuredClone(loaded.config);
const servers = normalizeConfiguredMcpServers(next.mcp?.servers);
const servers = normalizeConfiguredMcpServers(loaded.config.mcp?.servers);
servers[name] = params.update({ ...servers[name] });
next.mcp = {
...next.mcp,
return commitConfiguredMcpServers({
loaded,
servers,
};
const validated = validateConfigObjectWithPlugins(next);
if (!validated.ok) {
const issue = expectDefined(validated.issues[0], "issues entry at 0");
return {
ok: false,
path: loaded.path,
error: `Config invalid after MCP ${params.errorLabel} (${issue.path}: ${issue.message}).`,
};
}
await replaceConfigFile({
nextConfig: validated.config,
baseHash: loaded.baseHash,
errorLabel: params.errorLabel,
success: { updated: true },
independentlyOwnedName: params.recordIndependentOwner === false ? undefined : name,
});
if (params.recordIndependentOwner !== false) {
markClawMcpServerIndependentlyOwned(name);
}
return {
ok: true,
path: loaded.path,
config: validated.config,
mcpServers: servers,
updated: true,
};
}
export async function updateConfiguredMcpServerTools(params: {
@@ -301,36 +302,14 @@ export async function setConfiguredMcpServer(params: {
return { ok: false, path: loaded.path, error: "MCP server config must be a JSON object." };
}
const next = structuredClone(loaded.config);
const servers = normalizeConfiguredMcpServers(next.mcp?.servers);
const servers = normalizeConfiguredMcpServers(loaded.config.mcp?.servers);
servers[name] = canonicalizeConfiguredMcpServer(restoredServer);
next.mcp = {
...next.mcp,
return commitConfiguredMcpServers({
loaded,
servers,
};
const validated = validateConfigObjectWithPlugins(next);
if (!validated.ok) {
const issue = expectDefined(validated.issues[0], "issues entry at 0");
return {
ok: false,
path: loaded.path,
error: `Config invalid after MCP set (${issue.path}: ${issue.message}).`,
};
}
await replaceConfigFile({
nextConfig: validated.config,
baseHash: loaded.baseHash,
errorLabel: "set",
independentlyOwnedName: params.recordIndependentOwner === false ? undefined : name,
});
if (params.recordIndependentOwner !== false) {
markClawMcpServerIndependentlyOwned(name);
}
return {
ok: true,
path: loaded.path,
config: validated.config,
mcpServers: servers,
};
}
export async function unsetConfiguredMcpServer(params: {
@@ -347,13 +326,8 @@ export async function unsetConfiguredMcpServer(params: {
return loaded;
}
if (!Object.hasOwn(loaded.mcpServers, name)) {
return {
ok: true,
path: loaded.path,
config: loaded.config,
mcpServers: loaded.mcpServers,
removed: false,
};
const { baseHash: _baseHash, ...unchanged } = loaded;
return { ...unchanged, removed: false };
}
const loadedServer = loaded.mcpServers[name];
if (
@@ -369,39 +343,12 @@ export async function unsetConfiguredMcpServer(params: {
};
}
const next = structuredClone(loaded.config);
const servers = normalizeConfiguredMcpServers(next.mcp?.servers);
const servers = normalizeConfiguredMcpServers(loaded.config.mcp?.servers);
delete servers[name];
if (Object.keys(servers).length > 0) {
next.mcp = {
...next.mcp,
servers,
};
} else if (next.mcp) {
delete next.mcp.servers;
if (Object.keys(next.mcp).length === 0) {
delete next.mcp;
}
}
const validated = validateConfigObjectWithPlugins(next);
if (!validated.ok) {
const issue = expectDefined(validated.issues[0], "issues entry at 0");
return {
ok: false,
path: loaded.path,
error: `Config invalid after MCP unset (${issue.path}: ${issue.message}).`,
};
}
await replaceConfigFile({
nextConfig: validated.config,
baseHash: loaded.baseHash,
return commitConfiguredMcpServers({
loaded,
servers,
errorLabel: "unset",
success: { removed: true },
});
return {
ok: true,
path: loaded.path,
config: validated.config,
mcpServers: servers,
removed: true,
};
}