fix(discovery): load project .opencode config files

This commit is contained in:
can1357
2026-08-13 01:03:23 +02:00
parent 5ef7a2b0ac
commit bfd9b6efec
2 changed files with 47 additions and 5 deletions
@@ -77,9 +77,11 @@ async function loadJsonConfig(
/**
* OpenCode config sources in ascending effective precedence (lowest first):
* user `opencode.json` → user `opencode.jsonc` → project `opencode.json` →
* project `opencode.jsonc`. This matches how OpenCode merges configs — project
* overrides user, and within a scope `opencode.jsonc` overrides `opencode.json`.
* user `opencode.json` → user `opencode.jsonc` → project-root
* `opencode.json` → project-root `opencode.jsonc` → project `.opencode/opencode.json`
* → project `.opencode/opencode.jsonc`. This matches how OpenCode merges configs:
* project overrides user, `.opencode` overrides project-root config, and within
* a directory `opencode.jsonc` overrides `opencode.json`.
*
* Both consumers apply this order low-to-high: settings deep-merge in item
* order (last wins) and `loadMCPServers` deep-merges each server across layers
@@ -94,6 +96,10 @@ function getConfigSources(ctx: LoadContext): OpenCodeConfigSource[] {
for (const filename of CONFIG_FILENAMES) {
sources.push({ path: path.join(ctx.cwd, filename), level: "project" });
}
for (const filename of CONFIG_FILENAMES) {
const configPath = getProjectPath(ctx, "opencode", filename);
if (configPath) sources.push({ path: configPath, level: "project" });
}
return sources;
}
@@ -419,7 +425,7 @@ registerProvider(contextFileCapability.id, {
registerProvider(mcpCapability.id, {
id: PROVIDER_ID,
displayName: DISPLAY_NAME,
description: "Load MCP servers from opencode.json mcp key",
description: "Load MCP servers from OpenCode config files",
priority: PRIORITY,
load: loadMCPServers,
});
@@ -451,7 +457,7 @@ registerProvider(slashCommandCapability.id, {
registerProvider(settingsCapability.id, {
id: PROVIDER_ID,
displayName: DISPLAY_NAME,
description: "Load settings from opencode.json",
description: "Load settings from OpenCode config files",
priority: PRIORITY,
load: loadSettings,
});
@@ -88,6 +88,42 @@ describe("OpenCode MCP discovery", () => {
);
});
test("loads project .opencode config after project-root config", async () => {
const projectDir = path.join(tempDir, "project");
const projectConfigDir = path.join(projectDir, ".opencode");
await fs.mkdir(projectConfigDir, { recursive: true });
await fs.writeFile(
path.join(projectDir, "opencode.json"),
JSON.stringify({
model: "root-model",
mcp: { shared: { type: "local", command: ["root-server"] } },
}),
);
await fs.writeFile(
path.join(projectConfigDir, "opencode.jsonc"),
`{
// Project .opencode config has higher precedence.
"model": "dotdir-model",
"mcp": { "shared": { "command": ["dotdir-server"] } }
}`,
);
const [servers, discoveredSettings] = await Promise.all([
loadOpenCodeMcpConfig(projectDir),
loadOpenCodeSettings(projectDir),
]);
expect(servers.filter(server => server.name === "shared")).toEqual([
expect.objectContaining({ command: "dotdir-server", transport: "stdio" }),
]);
expect(discoveredSettings.at(-1)).toMatchObject({
path: path.join(projectConfigDir, "opencode.jsonc"),
level: "project",
data: expect.objectContaining({ model: "dotdir-model" }),
});
});
test("resolves same-named MCP servers by OpenCode precedence", async () => {
const projectDir = path.join(tempDir, "project");
const userConfigDir = path.join(tempDir, ".config", "opencode");