Skip to content

Commit 6f9720e

Browse files
authored
Merge pull request #53 from AltimateAI/feat/AI-0000-mcp-add-non-interactive
feat: add non-interactive mode to mcp add command
2 parents 82b252a + b47f127 commit 6f9720e

File tree

1 file changed

+144
-1
lines changed
  • packages/opencode/src/cli/cmd

1 file changed

+144
-1
lines changed

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

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const McpCommand = cmd({
5656
builder: (yargs) =>
5757
yargs
5858
.command(McpAddCommand)
59+
.command(McpRemoveCommand)
5960
.command(McpListCommand)
6061
.command(McpAuthCommand)
6162
.command(McpLogoutCommand)
@@ -398,6 +399,25 @@ async function resolveConfigPath(baseDir: string, global = false) {
398399
return candidates[0]
399400
}
400401

402+
async function removeMcpFromConfig(name: string, configPath: string) {
403+
if (!(await Filesystem.exists(configPath))) {
404+
return false
405+
}
406+
407+
const text = await Filesystem.readText(configPath)
408+
const edits = modify(text, ["mcp", name], undefined, {
409+
formattingOptions: { tabSize: 2, insertSpaces: true },
410+
})
411+
412+
if (edits.length === 0) {
413+
return false
414+
}
415+
416+
const result = applyEdits(text, edits)
417+
await Filesystem.write(configPath, result)
418+
return true
419+
}
420+
401421
async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) {
402422
let text = "{}"
403423
if (await Filesystem.exists(configPath)) {
@@ -418,10 +438,79 @@ async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: s
418438
export const McpAddCommand = cmd({
419439
command: "add",
420440
describe: "add an MCP server",
421-
async handler() {
441+
builder: (yargs) =>
442+
yargs
443+
.option("name", { type: "string", describe: "MCP server name" })
444+
.option("type", { type: "string", describe: "Server type", choices: ["local", "remote"] })
445+
.option("url", { type: "string", describe: "Server URL (for remote type)" })
446+
.option("command", { type: "string", describe: "Command to run (for local type)" })
447+
.option("header", { type: "array", string: true, describe: "HTTP headers as key=value (repeatable)" })
448+
.option("oauth", { type: "boolean", describe: "Enable OAuth", default: true })
449+
.option("global", { type: "boolean", describe: "Add to global config", default: false }),
450+
async handler(args) {
422451
await Instance.provide({
423452
directory: process.cwd(),
424453
async fn() {
454+
// Non-interactive mode: all required args provided via flags
455+
if (args.name && args.type) {
456+
if (!args.name.trim()) {
457+
console.error("MCP server name cannot be empty")
458+
process.exit(1)
459+
}
460+
461+
const useGlobal = args.global || Instance.project.vcs !== "git"
462+
const configPath = await resolveConfigPath(
463+
useGlobal ? Global.Path.config : Instance.worktree,
464+
useGlobal,
465+
)
466+
467+
let mcpConfig: Config.Mcp
468+
469+
if (args.type === "local") {
470+
if (!args.command?.trim()) {
471+
console.error("--command is required for local type")
472+
process.exit(1)
473+
}
474+
mcpConfig = {
475+
type: "local",
476+
command: args.command.trim().split(/\s+/).filter(Boolean),
477+
}
478+
} else {
479+
if (!args.url) {
480+
console.error("--url is required for remote type")
481+
process.exit(1)
482+
}
483+
if (!URL.canParse(args.url)) {
484+
console.error(`Invalid URL: ${args.url}`)
485+
process.exit(1)
486+
}
487+
488+
const headers: Record<string, string> = {}
489+
if (args.header) {
490+
for (const h of args.header) {
491+
const eq = h.indexOf("=")
492+
if (eq === -1) {
493+
console.error(`Invalid header format: ${h} (expected key=value)`)
494+
process.exit(1)
495+
}
496+
headers[h.substring(0, eq)] = h.substring(eq + 1)
497+
}
498+
}
499+
500+
mcpConfig = {
501+
type: "remote",
502+
url: args.url,
503+
...(!args.oauth ? { oauth: false as const } : {}),
504+
...(Object.keys(headers).length > 0 ? { headers } : {}),
505+
}
506+
}
507+
508+
await addMcpToConfig(args.name, mcpConfig, configPath)
509+
console.log(`MCP server "${args.name}" added to ${configPath}`)
510+
return
511+
}
512+
513+
// Interactive mode: existing behavior
425514
UI.empty()
426515
prompts.intro("Add MCP server")
427516

@@ -579,6 +668,60 @@ export const McpAddCommand = cmd({
579668
},
580669
})
581670

671+
export const McpRemoveCommand = cmd({
672+
command: "remove <name>",
673+
aliases: ["rm"],
674+
describe: "remove an MCP server",
675+
builder: (yargs) =>
676+
yargs
677+
.positional("name", {
678+
describe: "name of the MCP server to remove",
679+
type: "string",
680+
demandOption: true,
681+
})
682+
.option("global", { type: "boolean", describe: "Remove from global config", default: false }),
683+
async handler(args) {
684+
await Instance.provide({
685+
directory: process.cwd(),
686+
async fn() {
687+
const useGlobal = args.global || Instance.project.vcs !== "git"
688+
const configPath = await resolveConfigPath(
689+
useGlobal ? Global.Path.config : Instance.worktree,
690+
useGlobal,
691+
)
692+
693+
const removed = await removeMcpFromConfig(args.name, configPath)
694+
if (removed) {
695+
console.log(`MCP server "${args.name}" removed from ${configPath}`)
696+
} else if (Instance.project.vcs === "git" && !args.global) {
697+
// Try global scope as fallback only when in a git repo
698+
const globalPath = await resolveConfigPath(Global.Path.config, true)
699+
const removedGlobal = await removeMcpFromConfig(args.name, globalPath)
700+
if (removedGlobal) {
701+
console.log(`MCP server "${args.name}" removed from ${globalPath}`)
702+
} else {
703+
console.error(`MCP server "${args.name}" not found in any config`)
704+
process.exit(1)
705+
}
706+
} else if (args.global && Instance.project.vcs === "git") {
707+
// Try local scope as fallback when --global was explicit and we're in a git repo
708+
const localPath = await resolveConfigPath(Instance.worktree, false)
709+
const removedLocal = await removeMcpFromConfig(args.name, localPath)
710+
if (removedLocal) {
711+
console.log(`MCP server "${args.name}" removed from ${localPath}`)
712+
} else {
713+
console.error(`MCP server "${args.name}" not found in any config`)
714+
process.exit(1)
715+
}
716+
} else {
717+
console.error(`MCP server "${args.name}" not found in any config`)
718+
process.exit(1)
719+
}
720+
},
721+
})
722+
},
723+
})
724+
582725
export const McpDebugCommand = cmd({
583726
command: "debug <name>",
584727
describe: "debug OAuth connection for an MCP server",

0 commit comments

Comments
 (0)