1
0
Эх сурвалжийг харах

feat(opencode): support non-interactive MCP add (#31054)

Aiden Cline 2 сар өмнө
parent
commit
ba57718b05

+ 68 - 2
packages/opencode/src/cli/cmd/mcp.ts

@@ -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")
 

+ 9 - 3
packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap

@@ -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 - 0
packages/opencode/test/cli/mcp-add.test.ts

@@ -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,
+  )
+})