import { cmd } from "./cmd" import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import * as prompts from "@clack/prompts" import { UI } from "../ui" import { MCP } from "../../mcp" import { McpAuth } from "../../mcp/auth" import { Config } from "../../config/config" import { Instance } from "../../project/instance" import path from "path" import os from "os" import { Global } from "../../global" export const McpCommand = cmd({ command: "mcp", builder: (yargs) => yargs .command(McpAddCommand) .command(McpListCommand) .command(McpAuthCommand) .command(McpLogoutCommand) .demandCommand(), async handler() {}, }) export const McpListCommand = cmd({ command: "list", aliases: ["ls"], describe: "list MCP servers and their status", async handler() { await Instance.provide({ directory: process.cwd(), async fn() { UI.empty() prompts.intro("MCP Servers") const config = await Config.get() const mcpServers = config.mcp ?? {} const statuses = await MCP.status() if (Object.keys(mcpServers).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)) { const status = statuses[name] const hasOAuth = serverConfig.type === "remote" && !!serverConfig.oauth const hasStoredTokens = await MCP.hasStoredTokens(name) let statusIcon: string let statusText: string let hint = "" if (!status) { statusIcon = "○" statusText = "not initialized" } else if (status.status === "connected") { statusIcon = "✓" statusText = "connected" if (hasOAuth && hasStoredTokens) { hint = " (OAuth)" } } else if (status.status === "disabled") { statusIcon = "○" statusText = "disabled" } else if (status.status === "needs_auth") { statusIcon = "⚠" statusText = "needs authentication" } else if (status.status === "needs_client_registration") { statusIcon = "✗" statusText = "needs client registration" hint = "\n " + status.error } else { statusIcon = "✗" statusText = "failed" hint = "\n " + status.error } 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}`, ) } prompts.outro(`${Object.keys(mcpServers).length} server(s)`) }, }) }, }) export const McpAuthCommand = cmd({ command: "auth [name]", describe: "authenticate with an OAuth-enabled MCP server", builder: (yargs) => yargs.positional("name", { describe: "name of the MCP server", type: "string", }), async handler(args) { await Instance.provide({ directory: process.cwd(), async fn() { UI.empty() prompts.intro("MCP OAuth Authentication") const config = await Config.get() const mcpServers = config.mcp ?? {} // Get OAuth-enabled servers const oauthServers = Object.entries(mcpServers).filter(([_, cfg]) => cfg.type === "remote" && !!cfg.oauth) if (oauthServers.length === 0) { prompts.log.warn("No OAuth-enabled MCP servers configured") prompts.log.info("Add OAuth config to a remote MCP server in opencode.json:") prompts.log.info(` "mcp": { "my-server": { "type": "remote", "url": "https://example.com/mcp", "oauth": { "scope": "tools:read" } } }`) prompts.outro("Done") return } let serverName = args.name if (!serverName) { const selected = await prompts.select({ message: "Select MCP server to authenticate", options: oauthServers.map(([name, cfg]) => ({ label: name, value: name, hint: cfg.type === "remote" ? cfg.url : undefined, })), }) if (prompts.isCancel(selected)) throw new UI.CancelledError() serverName = selected } const serverConfig = mcpServers[serverName] if (!serverConfig) { prompts.log.error(`MCP server not found: ${serverName}`) prompts.outro("Done") return } if (serverConfig.type !== "remote" || !serverConfig.oauth) { prompts.log.error(`MCP server ${serverName} does not have OAuth configured`) prompts.outro("Done") return } // Check if already authenticated const hasTokens = await MCP.hasStoredTokens(serverName) if (hasTokens) { const confirm = await prompts.confirm({ message: `${serverName} already has stored credentials. Re-authenticate?`, }) if (prompts.isCancel(confirm) || !confirm) { prompts.outro("Cancelled") return } } const spinner = prompts.spinner() spinner.start("Starting OAuth flow...") try { const status = await MCP.authenticate(serverName) if (status.status === "connected") { spinner.stop("Authentication successful!") } else if (status.status === "needs_client_registration") { spinner.stop("Authentication failed", 1) prompts.log.error(status.error) prompts.log.info("Add clientId to your MCP server config:") prompts.log.info(` "mcp": { "${serverName}": { "type": "remote", "url": "${serverConfig.url}", "oauth": { "clientId": "your-client-id", "clientSecret": "your-client-secret" } } }`) } else if (status.status === "failed") { spinner.stop("Authentication failed", 1) prompts.log.error(status.error) } else { spinner.stop("Unexpected status: " + status.status, 1) } } catch (error) { spinner.stop("Authentication failed", 1) prompts.log.error(error instanceof Error ? error.message : String(error)) } prompts.outro("Done") }, }) }, }) export const McpLogoutCommand = cmd({ command: "logout [name]", describe: "remove OAuth credentials for an MCP server", builder: (yargs) => yargs.positional("name", { describe: "name of the MCP server", type: "string", }), async handler(args) { await Instance.provide({ directory: process.cwd(), async fn() { UI.empty() prompts.intro("MCP OAuth Logout") const authPath = path.join(Global.Path.data, "mcp-auth.json") const credentials = await McpAuth.all() const serverNames = Object.keys(credentials) if (serverNames.length === 0) { prompts.log.warn("No MCP OAuth credentials stored") prompts.outro("Done") return } let serverName = args.name if (!serverName) { const selected = await prompts.select({ message: "Select MCP server to logout", options: serverNames.map((name) => { const entry = credentials[name] const hasTokens = !!entry.tokens const hasClient = !!entry.clientInfo let hint = "" if (hasTokens && hasClient) hint = "tokens + client" else if (hasTokens) hint = "tokens" else if (hasClient) hint = "client registration" return { label: name, value: name, hint, } }), }) if (prompts.isCancel(selected)) throw new UI.CancelledError() serverName = selected } if (!credentials[serverName]) { prompts.log.error(`No credentials found for: ${serverName}`) prompts.outro("Done") return } await MCP.removeAuth(serverName) prompts.log.success(`Removed OAuth credentials for ${serverName}`) prompts.outro("Done") }, }) }, }) export const McpAddCommand = cmd({ command: "add", describe: "add an MCP server", async handler() { UI.empty() prompts.intro("Add MCP server") const name = await prompts.text({ message: "Enter MCP server name", validate: (x) => (x && x.length > 0 ? undefined : "Required"), }) if (prompts.isCancel(name)) throw new UI.CancelledError() const type = await prompts.select({ message: "Select MCP server type", options: [ { label: "Local", value: "local", hint: "Run a local command", }, { label: "Remote", value: "remote", hint: "Connect to a remote URL", }, ], }) if (prompts.isCancel(type)) throw new UI.CancelledError() if (type === "local") { const command = await prompts.text({ message: "Enter command to run", placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem", validate: (x) => (x && x.length > 0 ? undefined : "Required"), }) if (prompts.isCancel(command)) throw new UI.CancelledError() prompts.log.info(`Local MCP server "${name}" configured with command: ${command}`) prompts.outro("MCP server added successfully") return } if (type === "remote") { const url = await prompts.text({ message: "Enter MCP server URL", placeholder: "e.g., https://example.com/mcp", validate: (x) => { if (!x) return "Required" if (x.length === 0) return "Required" const isValid = URL.canParse(x) return isValid ? undefined : "Invalid URL" }, }) if (prompts.isCancel(url)) throw new UI.CancelledError() const useOAuth = await prompts.confirm({ message: "Does this server require OAuth authentication?", initialValue: false, }) if (prompts.isCancel(useOAuth)) throw new UI.CancelledError() if (useOAuth) { const hasClientId = await prompts.confirm({ message: "Do you have a pre-registered client ID?", initialValue: false, }) if (prompts.isCancel(hasClientId)) throw new UI.CancelledError() if (hasClientId) { const clientId = await prompts.text({ message: "Enter client ID", validate: (x) => (x && x.length > 0 ? undefined : "Required"), }) if (prompts.isCancel(clientId)) throw new UI.CancelledError() const hasSecret = await prompts.confirm({ message: "Do you have a client secret?", initialValue: false, }) if (prompts.isCancel(hasSecret)) throw new UI.CancelledError() let clientSecret: string | undefined if (hasSecret) { const secret = await prompts.password({ message: "Enter client secret", }) if (prompts.isCancel(secret)) throw new UI.CancelledError() clientSecret = secret } prompts.log.info(`Remote MCP server "${name}" configured with OAuth (client ID: ${clientId})`) prompts.log.info("Add this to your opencode.json:") prompts.log.info(` "mcp": { "${name}": { "type": "remote", "url": "${url}", "oauth": { "clientId": "${clientId}"${clientSecret ? `,\n "clientSecret": "${clientSecret}"` : ""} } } }`) } else { prompts.log.info(`Remote MCP server "${name}" configured with OAuth (dynamic registration)`) prompts.log.info("Add this to your opencode.json:") prompts.log.info(` "mcp": { "${name}": { "type": "remote", "url": "${url}", "oauth": {} } }`) } } else { const client = new Client({ name: "opencode", version: "1.0.0", }) const transport = new StreamableHTTPClientTransport(new URL(url)) await client.connect(transport) prompts.log.info(`Remote MCP server "${name}" configured with URL: ${url}`) } } prompts.outro("MCP server added successfully") }, })