| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- 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")
- },
- })
|