feat(opencode): support non-interactive MCP add (#31054)
This commit is contained in:
parent
3f0ef9b71c
commit
ba57718b05
@ -437,13 +437,79 @@ async function addMcpToConfig(name: string, mcpConfig: ConfigMCPV1.Info, configP
|
||||
}
|
||||
|
||||
export const McpAddCommand = effectCmd({
|
||||
command: "add",
|
||||
command: "add [name]",
|
||||
describe: "add an MCP server",
|
||||
handler: Effect.fn("Cli.mcp.add")(function* () {
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional("name", {
|
||||
describe: "name of the MCP server",
|
||||
type: "string",
|
||||
})
|
||||
.option("url", {
|
||||
describe: "URL for a remote MCP server",
|
||||
type: "string",
|
||||
})
|
||||
.option("env", {
|
||||
describe: "environment variable for a local MCP server (KEY=VALUE)",
|
||||
type: "string",
|
||||
array: true,
|
||||
})
|
||||
.option("header", {
|
||||
describe: "HTTP header for a remote MCP server (KEY=VALUE)",
|
||||
type: "string",
|
||||
array: true,
|
||||
}),
|
||||
handler: Effect.fn("Cli.mcp.add")(function* (args) {
|
||||
const maybeCtx = yield* InstanceRef
|
||||
if (!maybeCtx) return yield* Effect.die("InstanceRef not provided")
|
||||
const ctx = maybeCtx
|
||||
yield* Effect.promise(async () => {
|
||||
const command = args["--"] ?? []
|
||||
if (!args.name && (args.url || args.env?.length || args.header?.length || command.length)) {
|
||||
throw new Error("A server name is required for non-interactive MCP configuration")
|
||||
}
|
||||
if (args.name) {
|
||||
if (!!args.url === !!command.length) {
|
||||
throw new Error("Provide either --url <url> or a command after --")
|
||||
}
|
||||
if (args.url && !URL.canParse(args.url)) {
|
||||
throw new Error(`Invalid URL: ${args.url}`)
|
||||
}
|
||||
if (args.url && args.env?.length) {
|
||||
throw new Error("--env is only valid for local MCP servers")
|
||||
}
|
||||
if (command.length && args.header?.length) {
|
||||
throw new Error("--header is only valid for remote MCP servers")
|
||||
}
|
||||
|
||||
const entries = (values: string[], kind: string) =>
|
||||
Object.fromEntries(
|
||||
values.map((entry) => {
|
||||
const index = entry.indexOf("=")
|
||||
if (index < 1) throw new Error(`Invalid ${kind}: ${entry}. Expected KEY=VALUE`)
|
||||
return [entry.slice(0, index), entry.slice(index + 1)]
|
||||
}),
|
||||
)
|
||||
const environment = entries(args.env ?? [], "environment variable")
|
||||
const headers = entries(args.header ?? [], "HTTP header")
|
||||
const mcpConfig: ConfigMCPV1.Info = args.url
|
||||
? {
|
||||
type: "remote",
|
||||
url: args.url,
|
||||
...(Object.keys(headers).length ? { headers } : {}),
|
||||
}
|
||||
: {
|
||||
type: "local",
|
||||
command,
|
||||
...(Object.keys(environment).length ? { environment } : {}),
|
||||
}
|
||||
|
||||
const configPath = await resolveConfigPath(Global.Path.config, true)
|
||||
await addMcpToConfig(args.name, mcpConfig, configPath)
|
||||
prompts.log.success(`MCP server "${args.name}" added to ${configPath}`)
|
||||
return
|
||||
}
|
||||
|
||||
UI.empty()
|
||||
prompts.intro("Add MCP server")
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ exports[`opencode CLI help-text snapshots every documented command emits stable
|
||||
manage MCP (Model Context Protocol) servers
|
||||
|
||||
Commands:
|
||||
opencode mcp add add an MCP server
|
||||
opencode mcp add [name] 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
|
||||
@ -425,16 +425,22 @@ Options:
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp add --help 1`] = `
|
||||
"opencode mcp add
|
||||
"opencode mcp add [name]
|
||||
|
||||
add an MCP server
|
||||
|
||||
Positionals:
|
||||
name name of the MCP server [string]
|
||||
|
||||
Options:
|
||||
-h, --help show help [boolean]
|
||||
-v, --version show version number [boolean]
|
||||
--print-logs print logs to stderr [boolean]
|
||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||
--pure run without external plugins [boolean]"
|
||||
--pure run without external plugins [boolean]
|
||||
--url URL for a remote MCP server [string]
|
||||
--env environment variable for a local MCP server (KEY=VALUE) [array]
|
||||
--header HTTP header for a remote MCP server (KEY=VALUE) [array]"
|
||||
`;
|
||||
|
||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp auth --help 1`] = `
|
||||
|
||||
74
packages/opencode/test/cli/mcp-add.test.ts
Normal file
74
packages/opencode/test/cli/mcp-add.test.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import path from "path"
|
||||
import { cliIt } from "../lib/cli-process"
|
||||
|
||||
describe("opencode mcp add (non-interactive subprocess)", () => {
|
||||
cliIt.concurrent(
|
||||
"adds a remote server with HTTP headers",
|
||||
({ home, opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* opencode.spawn([
|
||||
"mcp",
|
||||
"add",
|
||||
"github",
|
||||
"--url",
|
||||
"https://example.com/mcp",
|
||||
"--header",
|
||||
"Authorization=Bearer {env:GITHUB_TOKEN}",
|
||||
"--header",
|
||||
"X-Option=one=two",
|
||||
])
|
||||
opencode.expectExit(result, 0)
|
||||
|
||||
const config = yield* Effect.promise(() =>
|
||||
Bun.file(path.join(home, ".config", "opencode", "opencode.json")).json(),
|
||||
)
|
||||
expect(config.mcp.github).toEqual({
|
||||
type: "remote",
|
||||
url: "https://example.com/mcp",
|
||||
headers: {
|
||||
Authorization: "Bearer {env:GITHUB_TOKEN}",
|
||||
"X-Option": "one=two",
|
||||
},
|
||||
})
|
||||
}),
|
||||
60_000,
|
||||
)
|
||||
|
||||
cliIt.concurrent(
|
||||
"adds a local server while preserving argv and environment values",
|
||||
({ home, opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* opencode.spawn([
|
||||
"mcp",
|
||||
"add",
|
||||
"local",
|
||||
"--env",
|
||||
"API_KEY=secret",
|
||||
"--env",
|
||||
"VALUE=one=two",
|
||||
"--",
|
||||
"npx",
|
||||
"-y",
|
||||
"@example/server",
|
||||
"--label",
|
||||
"two words",
|
||||
])
|
||||
opencode.expectExit(result, 0)
|
||||
|
||||
const config = yield* Effect.promise(() =>
|
||||
Bun.file(path.join(home, ".config", "opencode", "opencode.json")).json(),
|
||||
)
|
||||
expect(config.mcp.local).toEqual({
|
||||
type: "local",
|
||||
command: ["npx", "-y", "@example/server", "--label", "two words"],
|
||||
environment: {
|
||||
API_KEY: "secret",
|
||||
VALUE: "one=two",
|
||||
},
|
||||
})
|
||||
}),
|
||||
60_000,
|
||||
)
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user