mirror of
https://github.com/anomalyco/opencode.git
synced 2026-06-02 06:16:48 +02:00
refactor(cli): split mcp add url and command parsing
This commit is contained in:
@@ -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,7 +27,7 @@ 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
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user