From e489f39890e6478ce50e8fe0782c70e7d2287658 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Thu, 5 Mar 2026 11:50:16 -0800 Subject: [PATCH 1/6] feat: add non-interactive mode to `mcp add` command Add CLI flags (`--name`, `--type`, `--url`, `--command`, `--header`, `--no-oauth`, `--global`) to `mcp add` for scripted usage. When `--name` and `--type` are provided, the command skips all interactive prompts and writes the MCP config directly. This enables copy-paste onboarding commands from the Altimate web UI. Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/mcp.ts | 61 +++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index e6791df9d1..69adede52a 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -418,10 +418,69 @@ 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("no-oauth", { type: "boolean", describe: "Disable OAuth" }) + .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 configPath = await resolveConfigPath( + args.global ? Global.Path.config : Instance.worktree, + args.global, + ) + + 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 = {} + 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["no-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") From b89f8f6ecb92da2158964cb0c773a0fa8f653471 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Thu, 5 Mar 2026 12:02:05 -0800 Subject: [PATCH 2/6] fix: use yargs built-in `--no-` negation for `--no-oauth` flag Define `oauth` as a boolean (default `true`) so `--no-oauth` works via yargs' built-in prefix negation instead of a custom `no-oauth` option that conflicts with yargs' `--no-X` handling. Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/mcp.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 69adede52a..f2550328f4 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -425,7 +425,7 @@ export const McpAddCommand = cmd({ .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("no-oauth", { type: "boolean", describe: "Disable OAuth" }) + .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({ @@ -470,7 +470,7 @@ export const McpAddCommand = cmd({ mcpConfig = { type: "remote", url: args.url, - ...(args["no-oauth"] ? { oauth: false as const } : {}), + ...(!args.oauth ? { oauth: false as const } : {}), ...(Object.keys(headers).length > 0 ? { headers } : {}), } } From 6800458101f4a967e3490ca5f3adcb97d80113c7 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Thu, 5 Mar 2026 12:04:50 -0800 Subject: [PATCH 3/6] feat: add `mcp remove` command Add `altimate-code mcp remove ` (alias `rm`) to remove an MCP server from config. Supports `--global` flag; auto-searches the other scope if the server isn't found in the specified one. Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/mcp.ts | 63 ++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index f2550328f4..df244f1324 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -56,6 +56,7 @@ export const McpCommand = cmd({ builder: (yargs) => yargs .command(McpAddCommand) + .command(McpRemoveCommand) .command(McpListCommand) .command(McpAuthCommand) .command(McpLogoutCommand) @@ -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)) { @@ -638,6 +658,49 @@ export const McpAddCommand = cmd({ }, }) +export const McpRemoveCommand = cmd({ + command: "remove ", + 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 configPath = await resolveConfigPath( + args.global ? Global.Path.config : Instance.worktree, + args.global, + ) + + 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( + args.global ? Instance.worktree : Global.Path.config, + !args.global, + ) + 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 ", describe: "debug OAuth connection for an MCP server", From 7233f7733a935e1423cd4ec0fa855bdb385ac853 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Thu, 5 Mar 2026 12:15:17 -0800 Subject: [PATCH 4/6] fix: fall back to global config when not in a git repo `mcp add` and `mcp remove` now auto-detect when `cwd` is not inside a git project and write to the global config instead of trying to create `/altimate-code.json`. Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/mcp.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index df244f1324..494dcdc5b9 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -453,9 +453,10 @@ export const McpAddCommand = cmd({ 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( - args.global ? Global.Path.config : Instance.worktree, - args.global, + useGlobal ? Global.Path.config : Instance.worktree, + useGlobal, ) let mcpConfig: Config.Mcp @@ -674,9 +675,10 @@ export const McpRemoveCommand = cmd({ await Instance.provide({ directory: process.cwd(), async fn() { + const useGlobal = args.global || Instance.project.vcs !== "git" const configPath = await resolveConfigPath( - args.global ? Global.Path.config : Instance.worktree, - args.global, + useGlobal ? Global.Path.config : Instance.worktree, + useGlobal, ) const removed = await removeMcpFromConfig(args.name, configPath) @@ -685,8 +687,8 @@ export const McpRemoveCommand = cmd({ } else { // Try the other scope const otherPath = await resolveConfigPath( - args.global ? Instance.worktree : Global.Path.config, - !args.global, + useGlobal ? Instance.worktree : Global.Path.config, + !useGlobal, ) const removedOther = await removeMcpFromConfig(args.name, otherPath) if (removedOther) { From 8b7ab633d3e884bc48e88dd1b560c09b12c9c065 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Fri, 6 Mar 2026 14:34:46 -0800 Subject: [PATCH 5/6] fix: CI workflow path and `mcp remove` fallback in non-git dirs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update `ci.yml` TypeScript test `working-directory` from `packages/altimate-code` to `packages/opencode` (post-restructure) - Fix `mcp remove` fallback logic to avoid accessing filesystem root when not in a git repo — only try cross-scope fallback when `Instance.worktree` is valid (i.e., in a git repo) Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/mcp.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 494dcdc5b9..89b8022739 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -684,19 +684,29 @@ export const McpRemoveCommand = cmd({ 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, - ) - const removedOther = await removeMcpFromConfig(args.name, otherPath) - if (removedOther) { - console.log(`MCP server "${args.name}" removed from ${otherPath}`) + } else if (Instance.project.vcs === "git" && !args.global) { + // Try global scope as fallback only when in a git repo + const globalPath = await resolveConfigPath(Global.Path.config, true) + const removedGlobal = await removeMcpFromConfig(args.name, globalPath) + if (removedGlobal) { + console.log(`MCP server "${args.name}" removed from ${globalPath}`) } else { console.error(`MCP server "${args.name}" not found in any config`) process.exit(1) } + } else if (args.global && Instance.project.vcs === "git") { + // Try local scope as fallback when --global was explicit and we're in a git repo + const localPath = await resolveConfigPath(Instance.worktree, false) + const removedLocal = await removeMcpFromConfig(args.name, localPath) + if (removedLocal) { + console.log(`MCP server "${args.name}" removed from ${localPath}`) + } else { + console.error(`MCP server "${args.name}" not found in any config`) + process.exit(1) + } + } else { + console.error(`MCP server "${args.name}" not found in any config`) + process.exit(1) } }, }) From b47f127bb28b5a0ca41f911558a2cd5158c0fdb3 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Fri, 6 Mar 2026 15:07:35 -0800 Subject: [PATCH 6/6] fix: add input validation for non-interactive `mcp add` - Validate server name is non-empty (reject whitespace-only) - Use `trim().split(/\s+/).filter(Boolean)` for command parsing instead of naive `split(" ")` that breaks on multiple spaces - Add `URL.canParse()` validation for remote server URLs, matching interactive mode's validation Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/mcp.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 89b8022739..1c4bebcf11 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -453,6 +453,11 @@ export const McpAddCommand = cmd({ async fn() { // Non-interactive mode: all required args provided via flags if (args.name && args.type) { + if (!args.name.trim()) { + console.error("MCP server name cannot be empty") + process.exit(1) + } + const useGlobal = args.global || Instance.project.vcs !== "git" const configPath = await resolveConfigPath( useGlobal ? Global.Path.config : Instance.worktree, @@ -462,19 +467,23 @@ export const McpAddCommand = cmd({ let mcpConfig: Config.Mcp if (args.type === "local") { - if (!args.command) { + if (!args.command?.trim()) { console.error("--command is required for local type") process.exit(1) } mcpConfig = { type: "local", - command: args.command.split(" "), + command: args.command.trim().split(/\s+/).filter(Boolean), } } else { if (!args.url) { console.error("--url is required for remote type") process.exit(1) } + if (!URL.canParse(args.url)) { + console.error(`Invalid URL: ${args.url}`) + process.exit(1) + } const headers: Record = {} if (args.header) {