feat(opencode): support non-interactive MCP add (#31054)
This commit is contained in:
@@ -437,13 +437,79 @@ async function addMcpToConfig(name: string, mcpConfig: ConfigMCPV1.Info, configP
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const McpAddCommand = effectCmd({
|
export const McpAddCommand = effectCmd({
|
||||||
command: "add",
|
command: "add [name]",
|
||||||
describe: "add an MCP server",
|
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
|
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
|
||||||
yield* Effect.promise(async () => {
|
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()
|
UI.empty()
|
||||||
prompts.intro("Add MCP server")
|
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
|
manage MCP (Model Context Protocol) servers
|
||||||
|
|
||||||
Commands:
|
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 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,16 +425,22 @@ 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
|
"opencode mcp add [name]
|
||||||
|
|
||||||
add an MCP server
|
add an MCP server
|
||||||
|
|
||||||
|
Positionals:
|
||||||
|
name name of the MCP server [string]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help show help [boolean]
|
-h, --help show help [boolean]
|
||||||
-v, --version show version number [boolean]
|
-v, --version show version number [boolean]
|
||||||
--print-logs print logs to stderr [boolean]
|
--print-logs print logs to stderr [boolean]
|
||||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
--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`] = `
|
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp auth --help 1`] = `
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user