From b86a04262d43cd10a15bd6f3a427eac872979f6a Mon Sep 17 00:00:00 2001 From: Kei Shingu Date: Sat, 25 Apr 2026 08:45:49 +0900 Subject: [PATCH] fix(cli-runtime): replace overlapping user mcp servers --- src/agents/cli-runner/bundle-mcp.test.ts | 85 ++++++++++++++++++++++++ src/agents/cli-runner/bundle-mcp.ts | 10 ++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/agents/cli-runner/bundle-mcp.test.ts b/src/agents/cli-runner/bundle-mcp.test.ts index 1396d5399a9..ba269e07c17 100644 --- a/src/agents/cli-runner/bundle-mcp.test.ts +++ b/src/agents/cli-runner/bundle-mcp.test.ts @@ -263,6 +263,7 @@ describe("prepareCliBundleMcpConfig", () => { }); const configFlagIndex = prepared.backend.args?.indexOf("--mcp-config") ?? -1; + expect(configFlagIndex).toBeGreaterThanOrEqual(0); const generatedConfigPath = prepared.backend.args?.[configFlagIndex + 1]; const raw = JSON.parse(await fs.readFile(generatedConfigPath as string, "utf-8")) as { mcpServers?: Record; @@ -272,6 +273,90 @@ describe("prepareCliBundleMcpConfig", () => { await prepared.cleanup?.(); }); + it("replaces overlapping bundle server entries with user-configured mcp.servers", async () => { + const workspaceDir = await tempHarness.createTempDir( + "openclaw-cli-bundle-mcp-user-servers-replace-", + ); + await writeClaudeBundleManifest({ + homeDir: bundleProbeHomeDir, + pluginId: "omi", + manifest: { name: "omi" }, + }); + const pluginDir = path.join(bundleProbeHomeDir, ".openclaw", "extensions", "omi"); + await fs.writeFile( + path.join(pluginDir, ".mcp.json"), + `${JSON.stringify( + { + mcpServers: { + omi: { + command: process.execPath, + args: [bundleProbeServerPath], + env: { BUNDLE_ONLY: "true" }, + }, + }, + }, + null, + 2, + )}\n`, + "utf-8", + ); + + const env = captureEnv(["HOME"]); + try { + process.env.HOME = bundleProbeHomeDir; + const prepared = await prepareCliBundleMcpConfig({ + enabled: true, + mode: "claude-config-file", + backend: { + command: "node", + args: ["./fake-claude.mjs"], + }, + workspaceDir, + config: { + plugins: { + entries: { + omi: { enabled: true, path: pluginDir }, + }, + }, + mcp: { + servers: { + omi: { + type: "sse", + url: "https://api.omi.me/v1/mcp/sse", + headers: { Authorization: "Bearer test-token" }, + }, + }, + }, + }, + }); + + const configFlagIndex = prepared.backend.args?.indexOf("--mcp-config") ?? -1; + expect(configFlagIndex).toBeGreaterThanOrEqual(0); + const generatedConfigPath = prepared.backend.args?.[configFlagIndex + 1]; + const raw = JSON.parse(await fs.readFile(generatedConfigPath as string, "utf-8")) as { + mcpServers?: Record< + string, + { + type?: string; + url?: string; + command?: string; + args?: string[]; + env?: Record; + } + >; + }; + expect(raw.mcpServers?.omi?.type).toBe("sse"); + expect(raw.mcpServers?.omi?.url).toBe("https://api.omi.me/v1/mcp/sse"); + expect(raw.mcpServers?.omi?.command).toBeUndefined(); + expect(raw.mcpServers?.omi?.args).toBeUndefined(); + expect(raw.mcpServers?.omi?.env).toBeUndefined(); + + await prepared.cleanup?.(); + } finally { + env.restore(); + } + }); + it("stabilizes the resume hash when only the OpenClaw loopback port changes", async () => { const first = await prepareBundleProbeCliConfig({ additionalConfig: { diff --git a/src/agents/cli-runner/bundle-mcp.ts b/src/agents/cli-runner/bundle-mcp.ts index 1189dba253a..d0a7f513ca0 100644 --- a/src/agents/cli-runner/bundle-mcp.ts +++ b/src/agents/cli-runner/bundle-mcp.ts @@ -2,8 +2,8 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { applyMergePatch } from "../../config/merge-patch.js"; import { normalizeConfiguredMcpServers } from "../../config/mcp-config.js"; +import { applyMergePatch } from "../../config/merge-patch.js"; import type { CliBackendConfig } from "../../config/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { @@ -418,7 +418,13 @@ export async function prepareCliBundleMcpConfig(params: { mergedConfig = applyMergePatch(mergedConfig, bundleConfig.config) as BundleMcpConfig; const configuredMcp = normalizeConfiguredMcpServers(params.config?.mcp?.servers); if (Object.keys(configuredMcp).length > 0) { - mergedConfig = applyMergePatch(mergedConfig, { mcpServers: configuredMcp }) as BundleMcpConfig; + mergedConfig = { + ...mergedConfig, + mcpServers: { + ...(mergedConfig.mcpServers ?? {}), + ...configuredMcp, + }, + } satisfies BundleMcpConfig; } if (params.additionalConfig) { mergedConfig = applyMergePatch(mergedConfig, params.additionalConfig) as BundleMcpConfig;