Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c1b3d11
feat: add per-project MCP config overrides
jknlsn Dec 3, 2025
4e42c69
refactor(plugin): migrate to SDK v2
jknlsn Dec 12, 2025
42dde5c
fix(mcp): add type guards for MCP config override support
jknlsn Dec 12, 2025
2229108
chore: regenerate SDK
jknlsn Dec 12, 2025
2f1a181
revert: Remove unnecessary properties and changes
jknlsn Dec 12, 2025
c93b7c3
fix: check for type prop to avoid using overrides
jknlsn Dec 12, 2025
8deb7e1
Revert "refactor(plugin): migrate to SDK v2"
jknlsn Dec 13, 2025
5841603
fix: Filter config instead of updating to SDK v2
jknlsn Dec 13, 2025
4f0838a
Merge remote-tracking branch 'upstream/dev' into dev
jknlsn Dec 13, 2025
6e2ab28
Merge remote-tracking branch 'upstream/dev' into dev
jknlsn Dec 16, 2025
b4fc714
Merge remote-tracking branch 'upstream/dev' into dev
jknlsn Dec 16, 2025
9d2e836
Merge remote-tracking branch 'upstream/dev' into dev
jknlsn Dec 17, 2025
d0fa43e
Merge remote-tracking branch 'upstream/dev' into dev
jknlsn Dec 17, 2025
6eb6825
Merge remote-tracking branch 'upstream/dev' into dev
jknlsn Dec 18, 2025
6c1c4bd
Merge remote-tracking 'upstream/dev' into dev
jknlsn Dec 23, 2025
4ff2c2d
fix: Add type in checks for local overrides
jknlsn Dec 23, 2025
bae9e52
Merge remote branch 'upstream/dev' into dev
jknlsn Dec 27, 2025
47511ff
Merge remote-tracking branch 'upstream/dev' into dev
jknlsn Dec 30, 2025
07f17e4
Merge remote-tracking branch 'upstream/dev' into dev
jknlsn Dec 31, 2025
c6dd744
Merge remote-tracking branch 'upstream/dev' into dev
jknlsn Jan 1, 2026
012ce6b
Merge branch 'dev' into dev
rekram1-node Jan 3, 2026
799c834
revert changes
rekram1-node Jan 3, 2026
49816c2
tweak
rekram1-node Jan 3, 2026
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
38 changes: 27 additions & 11 deletions packages/opencode/src/cli/cmd/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ function getAuthStatusText(status: MCP.AuthStatus): string {
}
}

type McpEntry = NonNullable<Config.Info["mcp"]>[string]

type McpConfigured = Config.Mcp
function isMcpConfigured(config: McpEntry): config is McpConfigured {
return typeof config === "object" && config !== null && "type" in config
}

type McpRemote = Extract<McpConfigured, { type: "remote" }>
function isMcpRemote(config: McpEntry): config is McpRemote {
return isMcpConfigured(config) && config.type === "remote"
}

export const McpCommand = cmd({
command: "mcp",
builder: (yargs) =>
Expand Down Expand Up @@ -64,15 +76,19 @@ export const McpListCommand = cmd({
const mcpServers = config.mcp ?? {}
const statuses = await MCP.status()

if (Object.keys(mcpServers).length === 0) {
const servers = Object.entries(mcpServers).filter((entry): entry is [string, McpConfigured] =>
isMcpConfigured(entry[1]),
)

if (servers.length === 0) {
prompts.log.warn("No MCP servers configured")
prompts.outro("Add servers with: opencode mcp add")
return
}

for (const [name, serverConfig] of Object.entries(mcpServers)) {
for (const [name, serverConfig] of servers) {
const status = statuses[name]
const hasOAuth = serverConfig.type === "remote" && !!serverConfig.oauth
const hasOAuth = isMcpRemote(serverConfig) && !!serverConfig.oauth
const hasStoredTokens = await MCP.hasStoredTokens(name)

let statusIcon: string
Expand Down Expand Up @@ -110,7 +126,7 @@ export const McpListCommand = cmd({
)
}

prompts.outro(`${Object.keys(mcpServers).length} server(s)`)
prompts.outro(`${servers.length} server(s)`)
},
})
},
Expand Down Expand Up @@ -138,7 +154,7 @@ export const McpAuthCommand = cmd({

// Get OAuth-capable servers (remote servers with oauth not explicitly disabled)
const oauthServers = Object.entries(mcpServers).filter(
([_, cfg]) => cfg.type === "remote" && cfg.oauth !== false,
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
)

if (oauthServers.length === 0) {
Expand All @@ -163,7 +179,7 @@ export const McpAuthCommand = cmd({
const authStatus = await MCP.getAuthStatus(name)
const icon = getAuthStatusIcon(authStatus)
const statusText = getAuthStatusText(authStatus)
const url = cfg.type === "remote" ? cfg.url : ""
const url = cfg.url
return {
label: `${icon} ${name} (${statusText})`,
value: name,
Expand All @@ -187,8 +203,8 @@ export const McpAuthCommand = cmd({
return
}

if (serverConfig.type !== "remote" || serverConfig.oauth === false) {
prompts.log.error(`MCP server ${serverName} does not support OAuth (oauth is disabled)`)
if (!isMcpRemote(serverConfig) || serverConfig.oauth === false) {
prompts.log.error(`MCP server ${serverName} is not an OAuth-capable remote server`)
prompts.outro("Done")
return
}
Expand Down Expand Up @@ -263,7 +279,7 @@ export const McpAuthListCommand = cmd({

// Get OAuth-capable servers
const oauthServers = Object.entries(mcpServers).filter(
([_, cfg]) => cfg.type === "remote" && cfg.oauth !== false,
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
)

if (oauthServers.length === 0) {
Expand All @@ -276,7 +292,7 @@ export const McpAuthListCommand = cmd({
const authStatus = await MCP.getAuthStatus(name)
const icon = getAuthStatusIcon(authStatus)
const statusText = getAuthStatusText(authStatus)
const url = serverConfig.type === "remote" ? serverConfig.url : ""
const url = serverConfig.url

prompts.log.info(`${icon} ${name} ${UI.Style.TEXT_DIM}${statusText}\n ${UI.Style.TEXT_DIM}${url}`)
}
Expand Down Expand Up @@ -506,7 +522,7 @@ export const McpDebugCommand = cmd({
return
}

if (serverConfig.type !== "remote") {
if (!isMcpRemote(serverConfig)) {
prompts.log.error(`MCP server ${serverName} is not a remote server`)
prompts.outro("Done")
return
Expand Down
15 changes: 14 additions & 1 deletion packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,20 @@ export namespace Config {
.record(z.string(), Provider)
.optional()
.describe("Custom provider configurations and model overrides"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
mcp: z
.record(
z.string(),
z.union([
Mcp,
z
.object({
enabled: z.boolean(),
})
.strict(),
]),
)
.optional()
.describe("MCP (Model Context Protocol) server configurations"),
formatter: z
.union([
z.literal(false),
Expand Down
33 changes: 30 additions & 3 deletions packages/opencode/src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ export namespace MCP {
// Prompt cache types
type PromptInfo = Awaited<ReturnType<MCPClient["listPrompts"]>>["prompts"][number]

type McpEntry = NonNullable<Config.Info["mcp"]>[string]
function isMcpConfigured(entry: McpEntry): entry is Config.Mcp {
return typeof entry === "object" && entry !== null && "type" in entry
}

const state = Instance.state(
async () => {
const cfg = await Config.get()
Expand All @@ -144,6 +149,11 @@ export namespace MCP {

await Promise.all(
Object.entries(config).map(async ([key, mcp]) => {
if (!isMcpConfigured(mcp)) {
log.error("Ignoring MCP config entry without type", { key })
return
}

// If disabled by config, mark as disabled without trying to connect
if (mcp.enabled === false) {
status[key] = { status: "disabled" }
Expand Down Expand Up @@ -237,6 +247,7 @@ export namespace MCP {
status: { status: "disabled" as const },
}
}

log.info("found", { key, type: mcp.type })
let mcpClient: MCPClient | undefined
let status: Status | undefined = undefined
Expand Down Expand Up @@ -434,8 +445,9 @@ export namespace MCP {
const config = cfg.mcp ?? {}
const result: Record<string, Status> = {}

// Include all MCPs from config, not just connected ones
for (const key of Object.keys(config)) {
// Include all configured MCPs from config, not just connected ones
for (const [key, mcp] of Object.entries(config)) {
if (!isMcpConfigured(mcp)) continue
result[key] = s.status[key] ?? { status: "disabled" }
}

Expand All @@ -455,6 +467,11 @@ export namespace MCP {
return
}

if (!isMcpConfigured(mcp)) {
log.error("Ignoring MCP connect request for config without type", { name })
return
}

const result = await create(name, { ...mcp, enabled: true })

if (!result) {
Expand Down Expand Up @@ -579,6 +596,10 @@ export namespace MCP {
throw new Error(`MCP server not found: ${mcpName}`)
}

if (!isMcpConfigured(mcpConfig)) {
throw new Error(`MCP server ${mcpName} is disabled or missing configuration`)
}

if (mcpConfig.type !== "remote") {
throw new Error(`MCP server ${mcpName} is not a remote server`)
}
Expand Down Expand Up @@ -705,6 +726,10 @@ export namespace MCP {
throw new Error(`MCP server not found: ${mcpName}`)
}

if (!isMcpConfigured(mcpConfig)) {
throw new Error(`MCP server ${mcpName} is disabled or missing configuration`)
}

// Re-add the MCP server to establish connection
pendingOAuthTransports.delete(mcpName)
const result = await add(mcpName, mcpConfig)
Expand Down Expand Up @@ -737,7 +762,9 @@ export namespace MCP {
export async function supportsOAuth(mcpName: string): Promise<boolean> {
const cfg = await Config.get()
const mcpConfig = cfg.mcp?.[mcpName]
return mcpConfig?.type === "remote" && mcpConfig.oauth !== false
if (!mcpConfig) return false
if (!isMcpConfigured(mcpConfig)) return false
return mcpConfig.type === "remote" && mcpConfig.oauth !== false
}

/**
Expand Down
Loading