fix(discovery): prioritized project codex mcp entries

Load project Codex MCP entries before user entries so a disabled project server claims its dedupe key before a same-named user server can survive.

Added regression coverage for project-over-user disable precedence.

Fixes #7538
This commit is contained in:
roboomp
2026-08-03 16:52:54 +00:00
parent 012836d99e
commit 6f7600cd89
2 changed files with 42 additions and 12 deletions
+18 -12
View File
@@ -87,19 +87,13 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
]);
const items: MCPServer[] = [];
if (userConfig) {
const servers = extractMCPServersFromToml(userConfig, path.dirname(userConfigPath));
for (const [name, config] of Object.entries(servers)) {
items.push({
name,
...config,
_source: createSourceMeta(PROVIDER_ID, userConfigPath, "user"),
});
}
}
// Capability dedupe is first-wins, including suppressed items claiming their
// key. Load project entries first so a project `enabled = false` keeps a
// same-named user server disabled.
if (projectConfig) {
const servers = extractMCPServersFromToml(projectConfig, path.dirname(projectConfigPath));
for (const [name, config] of Object.entries(servers)) {
for (const name in servers) {
const config = servers[name];
items.push({
name,
...config,
@@ -107,6 +101,17 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
});
}
}
if (userConfig) {
const servers = extractMCPServersFromToml(userConfig, path.dirname(userConfigPath));
for (const name in servers) {
const config = servers[name];
items.push({
name,
...config,
_source: createSourceMeta(PROVIDER_ID, userConfigPath, "user"),
});
}
}
return { items, warnings };
}
@@ -153,7 +158,8 @@ function extractMCPServersFromToml(
const codexServers = toml.mcp_servers as Record<string, CodexMCPConfig>;
const result: Record<string, Partial<MCPServer>> = {};
for (const [name, config] of Object.entries(codexServers)) {
for (const name in codexServers) {
const config = codexServers[name];
// Root relative cwd/command against the Codex config directory. Codex
// spawns the process with the resolved cwd, so a relative command is
// resolved by the OS from there — pass "cwd" so e.g. cwd="server",
@@ -77,6 +77,30 @@ test("disabled Codex MCP servers survive discovery tagged enabled: false (#7538)
expect(context7?.enabled).toBeUndefined();
});
test("project Codex disable suppresses a same-named user server (#7538)", async () => {
const userCodexDir = path.join(tempHome, ".codex");
const projectCodexDir = path.join(tempCwd, ".codex");
await fs.mkdir(projectCodexDir, { recursive: true });
await Promise.all([
fs.writeFile(
path.join(userCodexDir, "config.toml"),
["[mcp_servers.shared]", 'command = "user-server"', ""].join("\n"),
),
fs.writeFile(
path.join(projectCodexDir, "config.toml"),
["[mcp_servers.shared]", 'command = "project-server"', "enabled = false", ""].join("\n"),
),
]);
const result = await loadCapability<MCPServer>(mcpCapability.id, {
cwd: tempCwd,
providers: ["codex"],
suppress: server => server.enabled === false,
});
expect(result.items.find(server => server.name === "shared")).toBeUndefined();
});
test("relative path-like command and cwd resolve against the Codex config directory (#5561)", async () => {
const codexDir = path.join(tempHome, ".codex");
await fs.writeFile(