diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index a5751ce83667..f27b9d1e72e1 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -163,9 +163,11 @@ export const McpListCommand = cmd({ hint = "\n " + status.error } + const provenance = config.mcp_provenance?.[name] + const scopeTag = provenance ? "(" + provenance + ")" : "" const typeHint = serverConfig.type === "remote" ? serverConfig.url : serverConfig.command.join(" ") prompts.log.info( - `${statusIcon} ${name} ${UI.Style.TEXT_DIM}${statusText}${hint}\n ${UI.Style.TEXT_DIM}${typeHint}`, + `${statusIcon} ${name}${scopeTag} ${UI.Style.TEXT_DIM}${statusText}${hint}\n ${UI.Style.TEXT_DIM}${typeHint}`, ) } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f1ceb1b4ed39..3d0d093dd501 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -233,6 +233,10 @@ export const Info = Schema.Struct({ description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.", }), }), + mcp_inherit: Schema.optional(Schema.Boolean).annotate({ + description: + "When false, project-level config does NOT inherit global MCP servers. Defaults to true.", + }), ), experimental: Schema.optional( Schema.Struct({ @@ -269,6 +273,10 @@ export type Info = DeepMutable> & { // plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together // with the file and scope it came from so later runtime code can make location-sensitive decisions. plugin_origins?: ConfigPlugin.Origin[] + + // mcp_provenance is derived state (like plugin_origins) — maps each MCP server to its scope. + // Used by the CLI and UI to show where each server came from. Not persisted. + mcp_provenance?: Record } type State = { @@ -319,7 +327,7 @@ function patchJsonc(input: string, patch: unknown, path: string[] = []): string } function writable(info: Info) { - const { plugin_origins: _plugin_origins, ...next } = info + const { plugin_origins: _plugin_origins, mcp_provenance: _mcp_provenance, ...next } = info return next } @@ -476,8 +484,16 @@ export const layer = Layer.effect( result.plugin_origins = plugins }) + const localMcpKeys = new Set() + const merge = (source: string, next: Info, kind?: ConfigPlugin.Scope) => { result = mergeConfigConcatArrays(result, next) + // Track which MCP server keys come from local config sources + if (kind === "local" && next.mcp) { + for (const key of Object.keys(next.mcp)) { + localMcpKeys.add(key) + } + } return mergePluginOrigins(source, next.plugin, kind) } @@ -683,6 +699,24 @@ export const layer = Layer.effect( result.compaction = { ...result.compaction, prune: false } } + // mcp_inherit filter and provenance + if (result.mcp) { + const provenance = result.mcp_provenance ?? {} + // Build provenance for every MCP server currently in the merged config + for (const key of Object.keys(result.mcp)) { + provenance[key] = localMcpKeys.has(key) ? "local" : "global" + } + // When mcp_inherit is explicitly false, drop servers not defined locally + if (result.mcp_inherit === false) { + for (const key of Object.keys(result.mcp)) { + if (!localMcpKeys.has(key)) { + delete result.mcp[key] + } + } + } + result.mcp_provenance = provenance + } + return { config: result, directories,