From 7d97c4c0405c2955ba90e2b966073bfaafcb0917 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 1 Jun 2026 05:05:37 +0000 Subject: [PATCH] refactor(cli): split mcp add url and command parsing --- packages/opencode/src/cli/cmd/mcp.ts | 91 ++++++++++++------- .../__snapshots__/help-snapshots.test.ts.snap | 14 +-- packages/opencode/test/cli/mcp.test.ts | 4 + 3 files changed, 69 insertions(+), 40 deletions(-) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 73baf8e6f9..3610d94c88 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -1,4 +1,4 @@ -import { cmd, type WithDoubleDash } from "./cmd" +import { cmd } from "./cmd" import { effectCmd, fail } from "../effect-cmd" import { Cause } from "effect" import { Client } from "@modelcontextprotocol/sdk/client/index.js" @@ -64,6 +64,15 @@ type McpAddArgs = { global?: boolean } +type InlineMcpAdd = { + name?: string + positional: string[] + command: string[] + type?: "local" | "remote" + env?: string[] + header?: string[] +} + function configuredServers(config: Config.Info) { return Object.entries(config.mcp ?? {}).filter((entry): entry is [string, McpConfigured] => isMcpConfigured(entry[1])) } @@ -445,7 +454,7 @@ async function addMcpToConfig(name: string, mcpConfig: ConfigMCP.Info, configPat } export const McpAddCommand = effectCmd({ - command: "add [name] [args..]", + command: "add [name] [args...]", describe: "add an MCP server", builder: (yargs) => yargs @@ -454,7 +463,7 @@ export const McpAddCommand = effectCmd({ type: "string", }) .positional("args", { - describe: "URL for remote servers or command and arguments for local servers", + describe: "URL for remote servers", type: "string", array: true, default: [], @@ -490,12 +499,18 @@ Examples: opencode mcp add local-env --env FOO=bar -- node server.js opencode mcp add sg --header Authorization=token https://sg.example/mcp opencode mcp add hugging-face https://huggingface.co/mcp`), - handler: Effect.fn("Cli.mcp.add")(function* (input: WithDoubleDash) { + handler: Effect.fn("Cli.mcp.add")(function* (input) { const maybeCtx = yield* InstanceRef if (!maybeCtx) return yield* Effect.die("InstanceRef not provided") const ctx = maybeCtx - const inlineArgs = mcpAddArgs(input) - const inlineConfig = parseInlineMcpAdd(input, inlineArgs) + const inlineConfig = parseInlineMcpAdd({ + name: input.name, + positional: input.args ?? [], + command: input["--"] ?? [], + type: input.type, + env: input.env, + header: input.header, + }) if (inlineConfig && "error" in inlineConfig) return yield* fail(inlineConfig.error) yield* Effect.promise(async () => { UI.empty() @@ -532,8 +547,8 @@ Examples: })() if (inlineConfig) { - await addMcpToConfig(input.name!.trim(), inlineConfig.config, configPath) - prompts.log.success(`MCP server "${input.name!.trim()}" added to ${configPath}`) + await addMcpToConfig(inlineConfig.name, inlineConfig.config, configPath) + prompts.log.success(`MCP server "${inlineConfig.name}" added to ${configPath}`) prompts.outro("MCP server added successfully") return } @@ -661,51 +676,61 @@ Examples: }), }) -function mcpAddArgs(input: WithDoubleDash) { - return [...(input.args ?? []), ...(input["--"] ?? [])] -} - function parseInlineMcpAdd( - input: McpAddArgs, - inlineArgs: string[], -): { config: ConfigMCP.Info } | { error: string } | undefined { - if (!hasInlineMcpAdd(input, inlineArgs)) return undefined + input: InlineMcpAdd, +): { name: string; config: ConfigMCP.Info } | { error: string } | undefined { + if (!hasInlineMcpAdd(input)) return undefined const name = input.name?.trim() if (!name) return { error: "MCP server name is required" } - if (inlineArgs.length === 0) return { error: "URL or command is required" } - - const type = input.type ?? (inlineArgs.length === 1 && URL.canParse(inlineArgs[0]) ? "remote" : "local") - if (type === "local") return parseInlineLocalMcp(input, inlineArgs) - return parseInlineRemoteMcp(input, inlineArgs) + const result = input.command.length > 0 ? parseInlineLocalMcp(input) : parseInlineRemoteMcp(input) + if ("error" in result) return result + return { name, config: result.config } } -function hasInlineMcpAdd(input: McpAddArgs, inlineArgs: string[]) { - return !!(input.name || inlineArgs.length > 0 || input.type || input.env?.length || input.header?.length) +function hasInlineMcpAdd(input: InlineMcpAdd) { + return !!( + input.name || + input.positional.length > 0 || + input.command.length > 0 || + input.type || + input.env?.length || + input.header?.length + ) } -function parseInlineLocalMcp(args: McpAddArgs, command: string[]): { config: ConfigMCP.Info } | { error: string } { - if (args.header?.length) return { error: "--header can only be used with --type remote" } - const environment = parseEnv(args.env) +function parseInlineLocalMcp(input: InlineMcpAdd): { config: ConfigMCP.Info } | { error: string } { + if (input.positional.length > 0) return { error: "Remote URL arguments cannot be combined with -- " } + if (input.type === "remote") return { error: "-- can only be used with --type local" } + if (input.header?.length) return { error: "--header can only be used with remote MCP servers" } + const environment = parseEnv(input.env) if ("error" in environment) return environment return { config: { type: "local", - command, + command: input.command, ...(environment.value && { environment: environment.value }), }, } } -function parseInlineRemoteMcp(args: McpAddArgs, url: string[]): { config: ConfigMCP.Info } | { error: string } { - if (url.length !== 1) return { error: "Remote MCP servers require exactly one URL" } - if (!URL.canParse(url[0])) return { error: "Remote MCP server URL is invalid" } - if (args.env?.length) return { error: "--env can only be used with --type local" } - const headers = parseHeader(args.header) +function parseInlineRemoteMcp(input: InlineMcpAdd): { config: ConfigMCP.Info } | { error: string } { + if (input.type === "local" || input.env?.length) return { error: "Local MCP commands must be passed after --" } + if (input.positional.length === 0) return { error: "URL or command is required" } + const wantsRemote = input.type === "remote" || !!input.header?.length + if (input.positional.length !== 1) { + return { + error: wantsRemote ? "Remote MCP servers require exactly one URL" : "Local MCP commands must be passed after --", + } + } + if (!URL.canParse(input.positional[0])) { + return { error: wantsRemote ? "Remote MCP server URL is invalid" : "Local MCP commands must be passed after --" } + } + const headers = parseHeader(input.header) if ("error" in headers) return headers return { config: { type: "remote", - url: url[0], + url: input.positional[0], ...(headers.value && { headers: headers.value }), }, } diff --git a/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap b/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap index 425b5d176d..4ee748e4b0 100644 --- a/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap +++ b/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap @@ -27,11 +27,11 @@ exports[`opencode CLI help-text snapshots every documented command emits stable manage MCP (Model Context Protocol) servers Commands: - opencode mcp add [name] [args..] add an MCP server - opencode mcp list list MCP servers and their status [aliases: ls] - opencode mcp auth [name] authenticate with an OAuth-enabled MCP server - opencode mcp logout [name] remove OAuth credentials for an MCP server - opencode mcp debug debug OAuth connection for an MCP server + opencode mcp add [name] [args...] add an MCP server + opencode mcp list list MCP servers and their status [aliases: ls] + opencode mcp auth [name] authenticate with an OAuth-enabled MCP server + opencode mcp logout [name] remove OAuth credentials for an MCP server + opencode mcp debug debug OAuth connection for an MCP server Options: -h, --help show help [boolean] @@ -425,13 +425,13 @@ Options: `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp add --help 1`] = ` -"opencode mcp add [name] [args..] +"opencode mcp add [name] [args...] add an MCP server Positionals: name name of the MCP server [string] - args URL for remote servers or command and arguments for local servers [array] [default: []] + args URL for remote servers [array] [default: []] Options: -h, --help show help [boolean] diff --git a/packages/opencode/test/cli/mcp.test.ts b/packages/opencode/test/cli/mcp.test.ts index ac54ad55d3..69cd931c34 100644 --- a/packages/opencode/test/cli/mcp.test.ts +++ b/packages/opencode/test/cli/mcp.test.ts @@ -62,6 +62,10 @@ describe("opencode mcp", () => { }, }, }) + + const missingSeparator = yield* opencode.spawn(["mcp", "add", "bad-local", "node", "server.js", "--global"]) + expect(missingSeparator.exitCode).not.toBe(0) + expect(missingSeparator.stderr).toContain("Local MCP commands must be passed after --") }), 120_000, )