Skip to content

Commit df18fea

Browse files
fix: handle project MCP config conflicts (#322)
1 parent 301e0f3 commit df18fea

2 files changed

Lines changed: 40 additions & 6 deletions

File tree

src/CodexAcpClient.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -518,11 +518,16 @@ export class CodexAcpClient {
518518

519519
private async getConfigMcpServerNames(projectPath: string): Promise<Set<string>> {
520520
const response = await this.codexClient.configRead({ includeLayers: true, cwd: projectPath });
521-
const mcpServers = response?.config?.["mcp_servers"];
522-
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
521+
const effectiveMcpServers = response?.config?.["mcp_servers"];
522+
const configLayers = response?.layers ?? [];
523+
const layerMcpServers = configLayers.map(layer => {
524+
return isJsonObject(layer.config) ? layer.config["mcp_servers"] : undefined;
525+
});
526+
const configuredMcpServers = [effectiveMcpServers, ...layerMcpServers].filter(isJsonObject);
527+
if (configuredMcpServers.length === 0) {
523528
return new Set();
524529
}
525-
return new Set(Object.keys(mcpServers));
530+
return new Set(configuredMcpServers.flatMap(server => Object.keys(server)));
526531
}
527532

528533
getModelProvider(): string | null {

src/__tests__/CodexACPAgent/mcp-config-merge.test.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,30 @@ import type {McpServerStdio} from "@agentclientprotocol/sdk";
88
import {startCodexConnection} from "../../CodexJsonRpcConnection";
99
import {createBaseTestFixture, removeDirectoryWithRetry, type TestFixture} from "../acp-test-utils";
1010

11-
describe('MCP config merge across global config and ACP request', { timeout: 40_000 }, () => {
11+
describe('MCP config merge across configured MCP servers and ACP request', { timeout: 40_000 }, () => {
1212

1313
let codexHome: string;
14+
let projectPath: string;
1415
let fixture: TestFixture;
1516

1617
beforeEach(() => {
1718
vi.clearAllMocks();
1819

19-
const configToml = `
20+
const globalConfig = `
2021
[mcp_servers.shared-mcp]
2122
url = "https://example.com/mcp"
2223
`;
24+
25+
const projectConfig = `
26+
[mcp_servers.project-mcp]
27+
url = "https://example.com/mcp"
28+
`;
29+
2330
codexHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-acp-mcp-merge-"));
24-
fs.writeFileSync(path.join(codexHome, "config.toml"), configToml, "utf8");
31+
fs.writeFileSync(path.join(codexHome, "config.toml"), globalConfig, "utf8");
32+
projectPath = fs.mkdtempSync(path.join(os.tmpdir(), "codex-acp-mcp-project-"));
33+
fs.mkdirSync(path.join(projectPath, ".codex"));
34+
fs.writeFileSync(path.join(projectPath, ".codex", "config.toml"), projectConfig, "utf8");
2535

2636
const codexConnection = startCodexConnection(undefined, {
2737
...process.env,
@@ -37,6 +47,7 @@ url = "https://example.com/mcp"
3747
afterEach(() => {
3848
vi.unstubAllEnvs();
3949
removeDirectoryWithRetry(codexHome);
50+
removeDirectoryWithRetry(projectPath);
4051
});
4152

4253
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"
6879
expect(transportDump).contain("- shared-mcp");
6980
});
7081

82+
it('should preserve a project url-based MCP when ACP passes a command-type MCP with the same name', async () => {
83+
const codexAcpAgent = fixture.getCodexAcpAgent();
84+
await codexAcpAgent.initialize({protocolVersion: 1});
85+
fixture.getCodexAcpClient().authRequired = vi.fn().mockResolvedValue(false);
86+
87+
const conflictingMcp = {
88+
name: "project-mcp",
89+
command: "./node_modules/.bin/mcp-hello-world",
90+
args: ["example"],
91+
env: [{name: "example", value: "example"}],
92+
};
93+
94+
await expect(codexAcpAgent.newSession({
95+
cwd: projectPath,
96+
mcpServers: [conflictingMcp],
97+
})).resolves.toBeDefined();
98+
});
99+
71100
it('should not filter the conflicting ACP MCP when config filtering is disabled', async () => {
72101
vi.stubEnv("DISABLE_MCP_CONFIG_FILTERING", "true");
73102
const codexAcpAgent = fixture.getCodexAcpAgent();

0 commit comments

Comments
 (0)