From 963d74e0e687940f0235e3f4f94efd9dc84de0cf Mon Sep 17 00:00:00 2001 From: Alexandr Suhinin Date: Tue, 21 Jul 2026 11:49:25 +0300 Subject: [PATCH] fix: handle project MCP config conflicts --- src/CodexAcpClient.ts | 11 ++++-- .../CodexACPAgent/mcp-config-merge.test.ts | 35 +++++++++++++++++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index b7664d36..3c48232c 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -518,11 +518,16 @@ export class CodexAcpClient { private async getConfigMcpServerNames(projectPath: string): Promise> { const response = await this.codexClient.configRead({ includeLayers: true, cwd: projectPath }); - const mcpServers = response?.config?.["mcp_servers"]; - if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) { + const effectiveMcpServers = response?.config?.["mcp_servers"]; + const configLayers = response?.layers ?? []; + const layerMcpServers = configLayers.map(layer => { + return isJsonObject(layer.config) ? layer.config["mcp_servers"] : undefined; + }); + const configuredMcpServers = [effectiveMcpServers, ...layerMcpServers].filter(isJsonObject); + if (configuredMcpServers.length === 0) { return new Set(); } - return new Set(Object.keys(mcpServers)); + return new Set(configuredMcpServers.flatMap(server => Object.keys(server))); } getModelProvider(): string | null { diff --git a/src/__tests__/CodexACPAgent/mcp-config-merge.test.ts b/src/__tests__/CodexACPAgent/mcp-config-merge.test.ts index f2cb510c..ac387e5c 100644 --- a/src/__tests__/CodexACPAgent/mcp-config-merge.test.ts +++ b/src/__tests__/CodexACPAgent/mcp-config-merge.test.ts @@ -8,20 +8,30 @@ import type {McpServerStdio} from "@agentclientprotocol/sdk"; import {startCodexConnection} from "../../CodexJsonRpcConnection"; import {createBaseTestFixture, removeDirectoryWithRetry, type TestFixture} from "../acp-test-utils"; -describe('MCP config merge across global config and ACP request', { timeout: 40_000 }, () => { +describe('MCP config merge across configured MCP servers and ACP request', { timeout: 40_000 }, () => { let codexHome: string; + let projectPath: string; let fixture: TestFixture; beforeEach(() => { vi.clearAllMocks(); - const configToml = ` + const globalConfig = ` [mcp_servers.shared-mcp] url = "https://example.com/mcp" `; + + const projectConfig = ` +[mcp_servers.project-mcp] +url = "https://example.com/mcp" +`; + codexHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-acp-mcp-merge-")); - fs.writeFileSync(path.join(codexHome, "config.toml"), configToml, "utf8"); + fs.writeFileSync(path.join(codexHome, "config.toml"), globalConfig, "utf8"); + projectPath = fs.mkdtempSync(path.join(os.tmpdir(), "codex-acp-mcp-project-")); + fs.mkdirSync(path.join(projectPath, ".codex")); + fs.writeFileSync(path.join(projectPath, ".codex", "config.toml"), projectConfig, "utf8"); const codexConnection = startCodexConnection(undefined, { ...process.env, @@ -37,6 +47,7 @@ url = "https://example.com/mcp" afterEach(() => { vi.unstubAllEnvs(); removeDirectoryWithRetry(codexHome); + removeDirectoryWithRetry(projectPath); }); it('should preserve the global url-based MCP when ACP passes a command-type MCP with the same name', async () => { @@ -68,6 +79,24 @@ url = "https://example.com/mcp" expect(transportDump).contain("- shared-mcp"); }); + it('should preserve a project url-based MCP when ACP passes a command-type MCP with the same name', async () => { + const codexAcpAgent = fixture.getCodexAcpAgent(); + await codexAcpAgent.initialize({protocolVersion: 1}); + fixture.getCodexAcpClient().authRequired = vi.fn().mockResolvedValue(false); + + const conflictingMcp = { + name: "project-mcp", + command: "./node_modules/.bin/mcp-hello-world", + args: ["example"], + env: [{name: "example", value: "example"}], + }; + + await expect(codexAcpAgent.newSession({ + cwd: projectPath, + mcpServers: [conflictingMcp], + })).resolves.toBeDefined(); + }); + it('should not filter the conflicting ACP MCP when config filtering is disabled', async () => { vi.stubEnv("DISABLE_MCP_CONFIG_FILTERING", "true"); const codexAcpAgent = fixture.getCodexAcpAgent();