Skip to content

Commit 498abb2

Browse files
paoloricciutisauerdaniel
authored andcommitted
fix: actually modify opencode config with mcp add (anomalyco#7339)
1 parent 2669912 commit 498abb2

File tree

1 file changed

+180
-109
lines changed
  • packages/opencode/src/cli/cmd

1 file changed

+180
-109
lines changed

packages/opencode/src/cli/cmd/mcp.ts

Lines changed: 180 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { cmd } from "./cmd"
22
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
33
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
4-
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
54
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
65
import * as prompts from "@clack/prompts"
76
import { UI } from "../ui"
@@ -13,6 +12,7 @@ import { Instance } from "../../project/instance"
1312
import { Installation } from "../../installation"
1413
import path from "path"
1514
import { Global } from "../../global"
15+
import { modify, applyEdits } from "jsonc-parser"
1616

1717
function getAuthStatusIcon(status: MCP.AuthStatus): string {
1818
switch (status) {
@@ -366,133 +366,204 @@ export const McpLogoutCommand = cmd({
366366
},
367367
})
368368

369-
export const McpAddCommand = cmd({
370-
command: "add",
371-
describe: "add an MCP server",
372-
async handler() {
373-
UI.empty()
374-
prompts.intro("Add MCP server")
375-
376-
const name = await prompts.text({
377-
message: "Enter MCP server name",
378-
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
379-
})
380-
if (prompts.isCancel(name)) throw new UI.CancelledError()
381-
382-
const type = await prompts.select({
383-
message: "Select MCP server type",
384-
options: [
385-
{
386-
label: "Local",
387-
value: "local",
388-
hint: "Run a local command",
389-
},
390-
{
391-
label: "Remote",
392-
value: "remote",
393-
hint: "Connect to a remote URL",
394-
},
395-
],
396-
})
397-
if (prompts.isCancel(type)) throw new UI.CancelledError()
369+
async function resolveConfigPath(baseDir: string, global = false) {
370+
// Check for existing config files (prefer .jsonc over .json, check .opencode/ subdirectory too)
371+
const candidates = [path.join(baseDir, "opencode.json"), path.join(baseDir, "opencode.jsonc")]
398372

399-
if (type === "local") {
400-
const command = await prompts.text({
401-
message: "Enter command to run",
402-
placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem",
403-
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
404-
})
405-
if (prompts.isCancel(command)) throw new UI.CancelledError()
373+
if (!global) {
374+
candidates.push(path.join(baseDir, ".opencode", "opencode.json"), path.join(baseDir, ".opencode", "opencode.jsonc"))
375+
}
406376

407-
prompts.log.info(`Local MCP server "${name}" configured with command: ${command}`)
408-
prompts.outro("MCP server added successfully")
409-
return
377+
for (const candidate of candidates) {
378+
if (await Bun.file(candidate).exists()) {
379+
return candidate
410380
}
381+
}
411382

412-
if (type === "remote") {
413-
const url = await prompts.text({
414-
message: "Enter MCP server URL",
415-
placeholder: "e.g., https://example.com/mcp",
416-
validate: (x) => {
417-
if (!x) return "Required"
418-
if (x.length === 0) return "Required"
419-
const isValid = URL.canParse(x)
420-
return isValid ? undefined : "Invalid URL"
421-
},
422-
})
423-
if (prompts.isCancel(url)) throw new UI.CancelledError()
383+
// Default to opencode.json if none exist
384+
return candidates[0]
385+
}
424386

425-
const useOAuth = await prompts.confirm({
426-
message: "Does this server require OAuth authentication?",
427-
initialValue: false,
428-
})
429-
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
387+
async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) {
388+
const file = Bun.file(configPath)
389+
390+
let text = "{}"
391+
if (await file.exists()) {
392+
text = await file.text()
393+
}
394+
395+
// Use jsonc-parser to modify while preserving comments
396+
const edits = modify(text, ["mcp", name], mcpConfig, {
397+
formattingOptions: { tabSize: 2, insertSpaces: true },
398+
})
399+
const result = applyEdits(text, edits)
400+
401+
await Bun.write(configPath, result)
402+
403+
return configPath
404+
}
430405

431-
if (useOAuth) {
432-
const hasClientId = await prompts.confirm({
433-
message: "Do you have a pre-registered client ID?",
434-
initialValue: false,
406+
export const McpAddCommand = cmd({
407+
command: "add",
408+
describe: "add an MCP server",
409+
async handler() {
410+
await Instance.provide({
411+
directory: process.cwd(),
412+
async fn() {
413+
UI.empty()
414+
prompts.intro("Add MCP server")
415+
416+
const project = Instance.project
417+
418+
// Resolve config paths eagerly for hints
419+
const [projectConfigPath, globalConfigPath] = await Promise.all([
420+
resolveConfigPath(Instance.worktree),
421+
resolveConfigPath(Global.Path.config, true),
422+
])
423+
424+
// Determine scope
425+
let configPath = globalConfigPath
426+
if (project.vcs === "git") {
427+
const scopeResult = await prompts.select({
428+
message: "Location",
429+
options: [
430+
{
431+
label: "Current project",
432+
value: projectConfigPath,
433+
hint: projectConfigPath,
434+
},
435+
{
436+
label: "Global",
437+
value: globalConfigPath,
438+
hint: globalConfigPath,
439+
},
440+
],
441+
})
442+
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
443+
configPath = scopeResult
444+
}
445+
446+
const name = await prompts.text({
447+
message: "Enter MCP server name",
448+
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
449+
})
450+
if (prompts.isCancel(name)) throw new UI.CancelledError()
451+
452+
const type = await prompts.select({
453+
message: "Select MCP server type",
454+
options: [
455+
{
456+
label: "Local",
457+
value: "local",
458+
hint: "Run a local command",
459+
},
460+
{
461+
label: "Remote",
462+
value: "remote",
463+
hint: "Connect to a remote URL",
464+
},
465+
],
435466
})
436-
if (prompts.isCancel(hasClientId)) throw new UI.CancelledError()
467+
if (prompts.isCancel(type)) throw new UI.CancelledError()
437468

438-
if (hasClientId) {
439-
const clientId = await prompts.text({
440-
message: "Enter client ID",
469+
if (type === "local") {
470+
const command = await prompts.text({
471+
message: "Enter command to run",
472+
placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem",
441473
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
442474
})
443-
if (prompts.isCancel(clientId)) throw new UI.CancelledError()
475+
if (prompts.isCancel(command)) throw new UI.CancelledError()
444476

445-
const hasSecret = await prompts.confirm({
446-
message: "Do you have a client secret?",
477+
const mcpConfig: Config.Mcp = {
478+
type: "local",
479+
command: command.split(" "),
480+
}
481+
482+
await addMcpToConfig(name, mcpConfig, configPath)
483+
prompts.log.success(`MCP server "${name}" added to ${configPath}`)
484+
prompts.outro("MCP server added successfully")
485+
return
486+
}
487+
488+
if (type === "remote") {
489+
const url = await prompts.text({
490+
message: "Enter MCP server URL",
491+
placeholder: "e.g., https://example.com/mcp",
492+
validate: (x) => {
493+
if (!x) return "Required"
494+
if (x.length === 0) return "Required"
495+
const isValid = URL.canParse(x)
496+
return isValid ? undefined : "Invalid URL"
497+
},
498+
})
499+
if (prompts.isCancel(url)) throw new UI.CancelledError()
500+
501+
const useOAuth = await prompts.confirm({
502+
message: "Does this server require OAuth authentication?",
447503
initialValue: false,
448504
})
449-
if (prompts.isCancel(hasSecret)) throw new UI.CancelledError()
505+
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
450506

451-
let clientSecret: string | undefined
452-
if (hasSecret) {
453-
const secret = await prompts.password({
454-
message: "Enter client secret",
507+
let mcpConfig: Config.Mcp
508+
509+
if (useOAuth) {
510+
const hasClientId = await prompts.confirm({
511+
message: "Do you have a pre-registered client ID?",
512+
initialValue: false,
455513
})
456-
if (prompts.isCancel(secret)) throw new UI.CancelledError()
457-
clientSecret = secret
514+
if (prompts.isCancel(hasClientId)) throw new UI.CancelledError()
515+
516+
if (hasClientId) {
517+
const clientId = await prompts.text({
518+
message: "Enter client ID",
519+
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
520+
})
521+
if (prompts.isCancel(clientId)) throw new UI.CancelledError()
522+
523+
const hasSecret = await prompts.confirm({
524+
message: "Do you have a client secret?",
525+
initialValue: false,
526+
})
527+
if (prompts.isCancel(hasSecret)) throw new UI.CancelledError()
528+
529+
let clientSecret: string | undefined
530+
if (hasSecret) {
531+
const secret = await prompts.password({
532+
message: "Enter client secret",
533+
})
534+
if (prompts.isCancel(secret)) throw new UI.CancelledError()
535+
clientSecret = secret
536+
}
537+
538+
mcpConfig = {
539+
type: "remote",
540+
url,
541+
oauth: {
542+
clientId,
543+
...(clientSecret && { clientSecret }),
544+
},
545+
}
546+
} else {
547+
mcpConfig = {
548+
type: "remote",
549+
url,
550+
oauth: {},
551+
}
552+
}
553+
} else {
554+
mcpConfig = {
555+
type: "remote",
556+
url,
557+
}
458558
}
459559

460-
prompts.log.info(`Remote MCP server "${name}" configured with OAuth (client ID: ${clientId})`)
461-
prompts.log.info("Add this to your opencode.json:")
462-
prompts.log.info(`
463-
"mcp": {
464-
"${name}": {
465-
"type": "remote",
466-
"url": "${url}",
467-
"oauth": {
468-
"clientId": "${clientId}"${clientSecret ? `,\n "clientSecret": "${clientSecret}"` : ""}
469-
}
470-
}
471-
}`)
472-
} else {
473-
prompts.log.info(`Remote MCP server "${name}" configured with OAuth (dynamic registration)`)
474-
prompts.log.info("Add this to your opencode.json:")
475-
prompts.log.info(`
476-
"mcp": {
477-
"${name}": {
478-
"type": "remote",
479-
"url": "${url}",
480-
"oauth": {}
481-
}
482-
}`)
560+
await addMcpToConfig(name, mcpConfig, configPath)
561+
prompts.log.success(`MCP server "${name}" added to ${configPath}`)
483562
}
484-
} else {
485-
const client = new Client({
486-
name: "opencode",
487-
version: "1.0.0",
488-
})
489-
const transport = new StreamableHTTPClientTransport(new URL(url))
490-
await client.connect(transport)
491-
prompts.log.info(`Remote MCP server "${name}" configured with URL: ${url}`)
492-
}
493-
}
494563

495-
prompts.outro("MCP server added successfully")
564+
prompts.outro("MCP server added successfully")
565+
},
566+
})
496567
},
497568
})
498569

0 commit comments

Comments
 (0)