Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/opencode/src/cli/cmd/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
)
}

Expand Down
36 changes: 35 additions & 1 deletion packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -269,6 +273,10 @@ export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
// 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<string, "global" | "local">
}

type State = {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -476,8 +484,16 @@ export const layer = Layer.effect(
result.plugin_origins = plugins
})

const localMcpKeys = new Set<string>()

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)
}

Expand Down Expand Up @@ -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,
Expand Down
Loading