refactor(cli): split mcp add url and command parsing

This commit is contained in:
opencode-agent[bot]
2026-06-01 05:05:37 +00:00
parent 8e0bceaea9
commit 7d97c4c040
3 changed files with 69 additions and 40 deletions
+58 -33
View File
@@ -1,4 +1,4 @@
import { cmd, type WithDoubleDash } from "./cmd" import { cmd } from "./cmd"
import { effectCmd, fail } from "../effect-cmd" import { effectCmd, fail } from "../effect-cmd"
import { Cause } from "effect" import { Cause } from "effect"
import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { Client } from "@modelcontextprotocol/sdk/client/index.js"
@@ -64,6 +64,15 @@ type McpAddArgs = {
global?: boolean global?: boolean
} }
type InlineMcpAdd = {
name?: string
positional: string[]
command: string[]
type?: "local" | "remote"
env?: string[]
header?: string[]
}
function configuredServers(config: Config.Info) { function configuredServers(config: Config.Info) {
return Object.entries(config.mcp ?? {}).filter((entry): entry is [string, McpConfigured] => isMcpConfigured(entry[1])) 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({ export const McpAddCommand = effectCmd({
command: "add [name] [args..]", command: "add [name] [args...]",
describe: "add an MCP server", describe: "add an MCP server",
builder: (yargs) => builder: (yargs) =>
yargs yargs
@@ -454,7 +463,7 @@ export const McpAddCommand = effectCmd({
type: "string", type: "string",
}) })
.positional("args", { .positional("args", {
describe: "URL for remote servers or command and arguments for local servers", describe: "URL for remote servers",
type: "string", type: "string",
array: true, array: true,
default: [], default: [],
@@ -490,12 +499,18 @@ Examples:
opencode mcp add local-env --env FOO=bar -- node server.js 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 sg --header Authorization=token https://sg.example/mcp
opencode mcp add hugging-face https://huggingface.co/mcp`), opencode mcp add hugging-face https://huggingface.co/mcp`),
handler: Effect.fn("Cli.mcp.add")(function* (input: WithDoubleDash<McpAddArgs>) { handler: Effect.fn("Cli.mcp.add")(function* (input) {
const maybeCtx = yield* InstanceRef const maybeCtx = yield* InstanceRef
if (!maybeCtx) return yield* Effect.die("InstanceRef not provided") if (!maybeCtx) return yield* Effect.die("InstanceRef not provided")
const ctx = maybeCtx const ctx = maybeCtx
const inlineArgs = mcpAddArgs(input) const inlineConfig = parseInlineMcpAdd({
const inlineConfig = parseInlineMcpAdd(input, inlineArgs) 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) if (inlineConfig && "error" in inlineConfig) return yield* fail(inlineConfig.error)
yield* Effect.promise(async () => { yield* Effect.promise(async () => {
UI.empty() UI.empty()
@@ -532,8 +547,8 @@ Examples:
})() })()
if (inlineConfig) { if (inlineConfig) {
await addMcpToConfig(input.name!.trim(), inlineConfig.config, configPath) await addMcpToConfig(inlineConfig.name, inlineConfig.config, configPath)
prompts.log.success(`MCP server "${input.name!.trim()}" added to ${configPath}`) prompts.log.success(`MCP server "${inlineConfig.name}" added to ${configPath}`)
prompts.outro("MCP server added successfully") prompts.outro("MCP server added successfully")
return return
} }
@@ -661,51 +676,61 @@ Examples:
}), }),
}) })
function mcpAddArgs(input: WithDoubleDash<McpAddArgs>) {
return [...(input.args ?? []), ...(input["--"] ?? [])]
}
function parseInlineMcpAdd( function parseInlineMcpAdd(
input: McpAddArgs, input: InlineMcpAdd,
inlineArgs: string[], ): { name: string; config: ConfigMCP.Info } | { error: string } | undefined {
): { config: ConfigMCP.Info } | { error: string } | undefined { if (!hasInlineMcpAdd(input)) return undefined
if (!hasInlineMcpAdd(input, inlineArgs)) return undefined
const name = input.name?.trim() const name = input.name?.trim()
if (!name) return { error: "MCP server name is required" } if (!name) return { error: "MCP server name is required" }
if (inlineArgs.length === 0) return { error: "URL or command is required" } const result = input.command.length > 0 ? parseInlineLocalMcp(input) : parseInlineRemoteMcp(input)
if ("error" in result) return result
const type = input.type ?? (inlineArgs.length === 1 && URL.canParse(inlineArgs[0]) ? "remote" : "local") return { name, config: result.config }
if (type === "local") return parseInlineLocalMcp(input, inlineArgs)
return parseInlineRemoteMcp(input, inlineArgs)
} }
function hasInlineMcpAdd(input: McpAddArgs, inlineArgs: string[]) { function hasInlineMcpAdd(input: InlineMcpAdd) {
return !!(input.name || inlineArgs.length > 0 || input.type || input.env?.length || input.header?.length) 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 } { function parseInlineLocalMcp(input: InlineMcpAdd): { config: ConfigMCP.Info } | { error: string } {
if (args.header?.length) return { error: "--header can only be used with --type remote" } if (input.positional.length > 0) return { error: "Remote URL arguments cannot be combined with -- <command>" }
const environment = parseEnv(args.env) if (input.type === "remote") return { error: "-- <command> 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 if ("error" in environment) return environment
return { return {
config: { config: {
type: "local", type: "local",
command, command: input.command,
...(environment.value && { environment: environment.value }), ...(environment.value && { environment: environment.value }),
}, },
} }
} }
function parseInlineRemoteMcp(args: McpAddArgs, url: string[]): { config: ConfigMCP.Info } | { error: string } { function parseInlineRemoteMcp(input: InlineMcpAdd): { config: ConfigMCP.Info } | { error: string } {
if (url.length !== 1) return { error: "Remote MCP servers require exactly one URL" } if (input.type === "local" || input.env?.length) return { error: "Local MCP commands must be passed after --" }
if (!URL.canParse(url[0])) return { error: "Remote MCP server URL is invalid" } if (input.positional.length === 0) return { error: "URL or command is required" }
if (args.env?.length) return { error: "--env can only be used with --type local" } const wantsRemote = input.type === "remote" || !!input.header?.length
const headers = parseHeader(args.header) 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 if ("error" in headers) return headers
return { return {
config: { config: {
type: "remote", type: "remote",
url: url[0], url: input.positional[0],
...(headers.value && { headers: headers.value }), ...(headers.value && { headers: headers.value }),
}, },
} }
@@ -27,11 +27,11 @@ exports[`opencode CLI help-text snapshots every documented command emits stable
manage MCP (Model Context Protocol) servers manage MCP (Model Context Protocol) servers
Commands: Commands:
opencode mcp add [name] [args..] add 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 list list MCP servers and their status [aliases: ls]
opencode mcp auth [name] authenticate with an OAuth-enabled MCP server opencode mcp auth [name] authenticate with an OAuth-enabled MCP server
opencode mcp logout [name] remove OAuth credentials for an MCP server opencode mcp logout [name] remove OAuth credentials for an MCP server
opencode mcp debug <name> debug OAuth connection for an MCP server opencode mcp debug <name> debug OAuth connection for an MCP server
Options: Options:
-h, --help show help [boolean] -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`] = ` 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 add an MCP server
Positionals: Positionals:
name name of the MCP server [string] 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: Options:
-h, --help show help [boolean] -h, --help show help [boolean]
+4
View File
@@ -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, 120_000,
) )