Skip to content
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 125 additions & 1 deletion packages/opencode/src/cli/cmd/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const McpCommand = cmd({
builder: (yargs) =>
yargs
.command(McpAddCommand)
.command(McpRemoveCommand)
.command(McpListCommand)
.command(McpAuthCommand)
.command(McpLogoutCommand)
Expand Down Expand Up @@ -398,6 +399,25 @@ async function resolveConfigPath(baseDir: string, global = false) {
return candidates[0]
}

async function removeMcpFromConfig(name: string, configPath: string) {
if (!(await Filesystem.exists(configPath))) {
return false
}

const text = await Filesystem.readText(configPath)
const edits = modify(text, ["mcp", name], undefined, {
formattingOptions: { tabSize: 2, insertSpaces: true },
})

if (edits.length === 0) {
return false
}

const result = applyEdits(text, edits)
await Filesystem.write(configPath, result)
return true
}

async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) {
let text = "{}"
if (await Filesystem.exists(configPath)) {
Expand All @@ -418,10 +438,70 @@ async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: s
export const McpAddCommand = cmd({
command: "add",
describe: "add an MCP server",
async handler() {
builder: (yargs) =>
yargs
.option("name", { type: "string", describe: "MCP server name" })
.option("type", { type: "string", describe: "Server type", choices: ["local", "remote"] })
.option("url", { type: "string", describe: "Server URL (for remote type)" })
.option("command", { type: "string", describe: "Command to run (for local type)" })
.option("header", { type: "array", string: true, describe: "HTTP headers as key=value (repeatable)" })
.option("oauth", { type: "boolean", describe: "Enable OAuth", default: true })
.option("global", { type: "boolean", describe: "Add to global config", default: false }),
async handler(args) {
await Instance.provide({
directory: process.cwd(),
async fn() {
// Non-interactive mode: all required args provided via flags
if (args.name && args.type) {
const useGlobal = args.global || Instance.project.vcs !== "git"
const configPath = await resolveConfigPath(
useGlobal ? Global.Path.config : Instance.worktree,
useGlobal,
)
Comment on lines +462 to +465

This comment was marked as outdated.


let mcpConfig: Config.Mcp

if (args.type === "local") {
if (!args.command) {
console.error("--command is required for local type")
process.exit(1)
}
mcpConfig = {
type: "local",
command: args.command.split(" "),
}
} else {
if (!args.url) {
console.error("--url is required for remote type")
process.exit(1)
}

const headers: Record<string, string> = {}
if (args.header) {
for (const h of args.header) {
const eq = h.indexOf("=")
if (eq === -1) {
console.error(`Invalid header format: ${h} (expected key=value)`)
process.exit(1)
}
headers[h.substring(0, eq)] = h.substring(eq + 1)
}
}

mcpConfig = {
type: "remote",
url: args.url,
...(!args.oauth ? { oauth: false as const } : {}),
...(Object.keys(headers).length > 0 ? { headers } : {}),
}
}

await addMcpToConfig(args.name, mcpConfig, configPath)
console.log(`MCP server "${args.name}" added to ${configPath}`)
return
}

// Interactive mode: existing behavior
UI.empty()
prompts.intro("Add MCP server")

Expand Down Expand Up @@ -579,6 +659,50 @@ export const McpAddCommand = cmd({
},
})

export const McpRemoveCommand = cmd({
command: "remove <name>",
aliases: ["rm"],
describe: "remove an MCP server",
builder: (yargs) =>
yargs
.positional("name", {
describe: "name of the MCP server to remove",
type: "string",
demandOption: true,
})
.option("global", { type: "boolean", describe: "Remove from global config", default: false }),
async handler(args) {
await Instance.provide({
directory: process.cwd(),
async fn() {
const useGlobal = args.global || Instance.project.vcs !== "git"
const configPath = await resolveConfigPath(
useGlobal ? Global.Path.config : Instance.worktree,
useGlobal,
)

const removed = await removeMcpFromConfig(args.name, configPath)
if (removed) {
console.log(`MCP server "${args.name}" removed from ${configPath}`)
} else {
// Try the other scope
const otherPath = await resolveConfigPath(
useGlobal ? Instance.worktree : Global.Path.config,
!useGlobal,
)

This comment was marked as outdated.

This comment was marked as outdated.

const removedOther = await removeMcpFromConfig(args.name, otherPath)
if (removedOther) {
console.log(`MCP server "${args.name}" removed from ${otherPath}`)
} else {
console.error(`MCP server "${args.name}" not found in any config`)
process.exit(1)
}
}
},
})
},
})

export const McpDebugCommand = cmd({
command: "debug <name>",
describe: "debug OAuth connection for an MCP server",
Expand Down
Loading