fix(cli): isolate claude MCP config

This commit is contained in:
Marko Jak
2026-03-26 01:25:23 -05:00
committed by Peter Steinberger
parent 85b169c453
commit b8ff152a98
4 changed files with 80 additions and 4 deletions

View File

@@ -272,6 +272,56 @@ describe("runCliAgent with process supervisor", () => {
expect(allArgs).toContain("You are a helpful assistant.");
});
it("injects a strict empty MCP config for bundle-MCP-enabled Claude CLI runs", async () => {
supervisorSpawnMock.mockResolvedValueOnce(
createManagedRun({
reason: "exit",
exitCode: 0,
exitSignal: null,
durationMs: 50,
stdout: JSON.stringify({
session_id: "session-123",
message: "ok",
}),
stderr: "",
timedOut: false,
noOutputTimedOut: false,
}),
);
await runCliAgent({
sessionId: "s1",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
config: {
agents: {
defaults: {
cliBackends: {
"claude-cli": {
command: "node",
args: ["/tmp/fake-claude.mjs"],
clearEnv: [],
},
},
},
},
} satisfies OpenClawConfig,
prompt: "hi",
provider: "claude-cli",
model: "claude-sonnet-4-6",
timeoutMs: 1_000,
runId: "run-bundle-mcp-empty",
});
const input = supervisorSpawnMock.mock.calls[0]?.[0] as { argv?: string[] };
expect(input.argv?.[0]).toBe("node");
expect(input.argv).toContain("/tmp/fake-claude.mjs");
expect(input.argv).toContain("--strict-mcp-config");
const configFlagIndex = input.argv?.indexOf("--mcp-config") ?? -1;
expect(configFlagIndex).toBeGreaterThanOrEqual(0);
expect(input.argv?.[configFlagIndex + 1]).toMatch(/^\/.+\/mcp\.json$/);
});
it("runs CLI through supervisor and returns payload", async () => {
supervisorSpawnMock.mockResolvedValueOnce(
createManagedRun({

View File

@@ -15,6 +15,32 @@ afterEach(async () => {
});
describe("prepareCliBundleMcpConfig", () => {
it("injects a strict empty --mcp-config overlay for bundle-MCP-enabled backends without servers", async () => {
const workspaceDir = await tempHarness.createTempDir("openclaw-cli-bundle-mcp-empty-");
const prepared = await prepareCliBundleMcpConfig({
enabled: true,
backend: {
command: "node",
args: ["./fake-claude.mjs"],
},
workspaceDir,
config: {},
});
const configFlagIndex = prepared.backend.args?.indexOf("--mcp-config") ?? -1;
expect(configFlagIndex).toBeGreaterThanOrEqual(0);
expect(prepared.backend.args).toContain("--strict-mcp-config");
const generatedConfigPath = prepared.backend.args?.[configFlagIndex + 1];
expect(typeof generatedConfigPath).toBe("string");
const raw = JSON.parse(await fs.readFile(generatedConfigPath as string, "utf-8")) as {
mcpServers?: Record<string, unknown>;
};
expect(raw.mcpServers).toEqual({});
await prepared.cleanup?.();
});
it("injects a merged --mcp-config overlay for bundle-MCP-enabled backends", async () => {
const env = captureEnv(["HOME"]);
try {

View File

@@ -98,10 +98,9 @@ export async function prepareCliBundleMcpConfig(params: {
}
mergedConfig = applyMergePatch(mergedConfig, bundleConfig.config) as BundleMcpConfig;
if (Object.keys(mergedConfig.mcpServers).length === 0) {
return { backend: params.backend };
}
// Always pass an explicit strict MCP config for background claude-cli runs.
// Otherwise Claude may inherit ambient user/global MCP servers (for example
// Playwright) and spawn unexpected background processes.
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-mcp-"));
const mcpConfigPath = path.join(tempDir, "mcp.json");
const serializedConfig = `${JSON.stringify(mergedConfig, null, 2)}\n`;