summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/cli/cmd/mcp.ts289
1 files changed, 180 insertions, 109 deletions
diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts
index aaef75267..cfb54081f 100644
--- a/packages/opencode/src/cli/cmd/mcp.ts
+++ b/packages/opencode/src/cli/cmd/mcp.ts
@@ -1,7 +1,6 @@
import { cmd } from "./cmd"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
-import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
@@ -13,6 +12,7 @@ import { Instance } from "../../project/instance"
import { Installation } from "../../installation"
import path from "path"
import { Global } from "../../global"
+import { modify, applyEdits } from "jsonc-parser"
function getAuthStatusIcon(status: MCP.AuthStatus): string {
switch (status) {
@@ -366,133 +366,204 @@ export const McpLogoutCommand = cmd({
},
})
-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()
+async function resolveConfigPath(baseDir: string, global = false) {
+ // Check for existing config files (prefer .jsonc over .json, check .opencode/ subdirectory too)
+ const candidates = [path.join(baseDir, "opencode.json"), path.join(baseDir, "opencode.jsonc")]
- 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()
+ if (!global) {
+ candidates.push(path.join(baseDir, ".opencode", "opencode.json"), path.join(baseDir, ".opencode", "opencode.jsonc"))
+ }
- prompts.log.info(`Local MCP server "${name}" configured with command: ${command}`)
- prompts.outro("MCP server added successfully")
- return
+ for (const candidate of candidates) {
+ if (await Bun.file(candidate).exists()) {
+ return candidate
}
+ }
- 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()
+ // Default to opencode.json if none exist
+ return candidates[0]
+}
- const useOAuth = await prompts.confirm({
- message: "Does this server require OAuth authentication?",
- initialValue: false,
- })
- if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
+async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) {
+ const file = Bun.file(configPath)
+
+ let text = "{}"
+ if (await file.exists()) {
+ text = await file.text()
+ }
+
+ // Use jsonc-parser to modify while preserving comments
+ const edits = modify(text, ["mcp", name], mcpConfig, {
+ formattingOptions: { tabSize: 2, insertSpaces: true },
+ })
+ const result = applyEdits(text, edits)
+
+ await Bun.write(configPath, result)
+
+ return configPath
+}
- if (useOAuth) {
- const hasClientId = await prompts.confirm({
- message: "Do you have a pre-registered client ID?",
- initialValue: false,
+export const McpAddCommand = cmd({
+ command: "add",
+ describe: "add an MCP server",
+ async handler() {
+ await Instance.provide({
+ directory: process.cwd(),
+ async fn() {
+ UI.empty()
+ prompts.intro("Add MCP server")
+
+ const project = Instance.project
+
+ // Resolve config paths eagerly for hints
+ const [projectConfigPath, globalConfigPath] = await Promise.all([
+ resolveConfigPath(Instance.worktree),
+ resolveConfigPath(Global.Path.config, true),
+ ])
+
+ // Determine scope
+ let configPath = globalConfigPath
+ if (project.vcs === "git") {
+ const scopeResult = await prompts.select({
+ message: "Location",
+ options: [
+ {
+ label: "Current project",
+ value: projectConfigPath,
+ hint: projectConfigPath,
+ },
+ {
+ label: "Global",
+ value: globalConfigPath,
+ hint: globalConfigPath,
+ },
+ ],
+ })
+ if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
+ configPath = scopeResult
+ }
+
+ 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(hasClientId)) throw new UI.CancelledError()
+ if (prompts.isCancel(type)) throw new UI.CancelledError()
- if (hasClientId) {
- const clientId = await prompts.text({
- message: "Enter client ID",
+ 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(clientId)) throw new UI.CancelledError()
+ if (prompts.isCancel(command)) throw new UI.CancelledError()
- const hasSecret = await prompts.confirm({
- message: "Do you have a client secret?",
+ const mcpConfig: Config.Mcp = {
+ type: "local",
+ command: command.split(" "),
+ }
+
+ await addMcpToConfig(name, mcpConfig, configPath)
+ prompts.log.success(`MCP server "${name}" added to ${configPath}`)
+ 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(hasSecret)) throw new UI.CancelledError()
+ if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
- let clientSecret: string | undefined
- if (hasSecret) {
- const secret = await prompts.password({
- message: "Enter client secret",
+ let mcpConfig: Config.Mcp
+
+ if (useOAuth) {
+ const hasClientId = await prompts.confirm({
+ message: "Do you have a pre-registered client ID?",
+ initialValue: false,
})
- if (prompts.isCancel(secret)) throw new UI.CancelledError()
- clientSecret = secret
+ 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
+ }
+
+ mcpConfig = {
+ type: "remote",
+ url,
+ oauth: {
+ clientId,
+ ...(clientSecret && { clientSecret }),
+ },
+ }
+ } else {
+ mcpConfig = {
+ type: "remote",
+ url,
+ oauth: {},
+ }
+ }
+ } else {
+ mcpConfig = {
+ type: "remote",
+ url,
+ }
}
- 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": {}
- }
- }`)
+ await addMcpToConfig(name, mcpConfig, configPath)
+ prompts.log.success(`MCP server "${name}" added to ${configPath}`)
}
- } 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")
+ prompts.outro("MCP server added successfully")
+ },
+ })
},
})