diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index b4ae8a37f7b..63069d74e4b 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -36,6 +36,18 @@ function getAuthStatusText(status: MCP.AuthStatus): string { } } +type McpEntry = NonNullable[string] + +type McpConfigured = Config.Mcp +function isMcpConfigured(config: McpEntry): config is McpConfigured { + return typeof config === "object" && config !== null && "type" in config +} + +type McpRemote = Extract +function isMcpRemote(config: McpEntry): config is McpRemote { + return isMcpConfigured(config) && config.type === "remote" +} + export const McpCommand = cmd({ command: "mcp", builder: (yargs) => @@ -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 @@ -110,7 +126,7 @@ export const McpListCommand = cmd({ ) } - prompts.outro(`${Object.keys(mcpServers).length} server(s)`) + prompts.outro(`${servers.length} server(s)`) }, }) }, @@ -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) { @@ -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, @@ -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 } @@ -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) { @@ -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}`) } @@ -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 diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 077a9dd1135..f62581db369 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -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), diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 10a0636675f..322761c551e 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -135,6 +135,11 @@ export namespace MCP { // Prompt cache types type PromptInfo = Awaited>["prompts"][number] + type McpEntry = NonNullable[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() @@ -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" } @@ -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 @@ -434,8 +445,9 @@ export namespace MCP { const config = cfg.mcp ?? {} const result: Record = {} - // 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" } } @@ -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) { @@ -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`) } @@ -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) @@ -737,7 +762,9 @@ export namespace MCP { export async function supportsOAuth(mcpName: string): Promise { 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 } /**