Explorar o código

feat(opencode): support cwd on local MCP servers (#30676)

Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Grant Martin hai 2 meses
pai
achega
7e7ad37736

+ 3 - 0
packages/core/src/config/mcp.ts

@@ -6,6 +6,9 @@ import { PositiveInt } from "../schema"
 export class Local extends Schema.Class<Local>("ConfigV2.MCP.Local")({
   type: Schema.Literal("local"),
   command: Schema.String.pipe(Schema.Array),
+  cwd: Schema.String.pipe(Schema.optional).annotate({
+    description: "Working directory for the MCP server process. Relative paths resolve from the workspace directory.",
+  }),
   environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
   disabled: Schema.Boolean.pipe(Schema.optional),
   timeout: PositiveInt.pipe(Schema.optional),

+ 3 - 0
packages/core/src/v1/config/mcp.ts

@@ -8,6 +8,9 @@ export const Local = Schema.Struct({
   command: Schema.mutable(Schema.Array(Schema.String)).annotate({
     description: "Command and arguments to run the MCP server",
   }),
+  cwd: Schema.optional(Schema.String).annotate({
+    description: "Working directory for the MCP server process. Relative paths resolve from the workspace directory.",
+  }),
   environment: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({
     description: "Environment variables to set when running the MCP server",
   }),

+ 8 - 1
packages/core/src/v1/config/migrate.ts

@@ -139,7 +139,14 @@ function mcp(info: typeof ConfigV1.Info.Type) {
 function migrateMcp(info: ConfigMCPV1.Info) {
   const disabled = info.enabled === undefined ? undefined : !info.enabled
   if (info.type === "local")
-    return { type: info.type, command: info.command, environment: info.environment, disabled, timeout: info.timeout }
+    return {
+      type: info.type,
+      command: info.command,
+      cwd: info.cwd,
+      environment: info.environment,
+      disabled,
+      timeout: info.timeout,
+    }
   return {
     type: info.type,
     url: info.url,

+ 3 - 1
packages/opencode/src/mcp/index.ts

@@ -1,3 +1,4 @@
+import path from "node:path"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
 import { type Tool } from "ai"
 import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
@@ -304,7 +305,8 @@ export const layer = Layer.effect(
       mcp: ConfigMCPV1.Info & { type: "local" },
     ) {
       const [cmd, ...args] = mcp.command
-      const cwd = yield* InstanceState.directory
+      const baseDir = yield* InstanceState.directory
+      const cwd = mcp.cwd ? path.resolve(baseDir, mcp.cwd) : baseDir
       const transport = new StdioClientTransport({
         stderr: "pipe",
         command: cmd,

+ 21 - 2
packages/opencode/test/mcp/lifecycle.test.ts

@@ -1,8 +1,10 @@
+import path from "node:path"
 import { expect, mock, beforeEach } from "bun:test"
 import { ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js"
 import { Cause, Effect, Exit } from "effect"
 import type { MCP as MCPNS } from "../../src/mcp/index"
 import { testEffect } from "../lib/effect"
+import { TestInstance } from "../fixture/fixture"
 
 // --- Mock infrastructure ---
 
@@ -48,6 +50,8 @@ let connectError = "Mock transport cannot connect"
 let clientCreateCount = 0
 // Tracks how many times transport.close() is called across all mock transports
 let transportCloseCount = 0
+// Captures the opts passed to each MockStdioTransport, keyed by lastCreatedClientName
+const stdioOptsByName = new Map<string, any>()
 
 function getOrCreateClientState(name?: string): MockClientState {
   const key = name ?? "default"
@@ -82,8 +86,9 @@ function getOrCreateClientState(name?: string): MockClientState {
 class MockStdioTransport {
   stderr: null = null
   pid = 12345
-  // oxlint-disable-next-line no-useless-constructor
-  constructor(_opts: any) {}
+  constructor(opts: any) {
+    if (lastCreatedClientName) stdioOptsByName.set(lastCreatedClientName, opts)
+  }
   async start() {
     if (connectShouldHang) return new Promise<void>(() => {}) // never resolves
     if (connectShouldFail) throw new Error(connectError)
@@ -246,6 +251,20 @@ function statusName(status: Record<string, MCPNS.Status> | MCPNS.Status, server:
   return status[server]?.status
 }
 
+it.instance(
+  "local mcp cwd resolves relative paths against instance directory",
+  () =>
+    MCP.Service.use((mcp: MCPNS.Interface) =>
+      Effect.gen(function* () {
+        const { directory } = yield* TestInstance
+        lastCreatedClientName = "rel-cwd"
+        yield* mcp.add("rel-cwd", { type: "local", command: ["echo", "test"], cwd: "plugins/sub" })
+        expect(stdioOptsByName.get("rel-cwd")?.cwd).toBe(path.resolve(directory, "plugins/sub"))
+      }),
+    ),
+  { config: { mcp: {} } },
+)
+
 // ========================================================================
 // Test: tools() are cached after connect
 // ========================================================================

+ 8 - 7
packages/web/src/content/docs/mcp-servers.mdx

@@ -116,13 +116,14 @@ use the mcp_everything tool to add the number 3 and 4
 
 Here are all the options for configuring a local MCP server.
 
-| Option        | Type    | Required | Description                                                                         |
-| ------------- | ------- | -------- | ----------------------------------------------------------------------------------- |
-| `type`        | String  | Y        | Type of MCP server connection, must be `"local"`.                                   |
-| `command`     | Array   | Y        | Command and arguments to run the MCP server.                                        |
-| `environment` | Object  |          | Environment variables to set when running the server.                               |
-| `enabled`     | Boolean |          | Enable or disable the MCP server on startup.                                        |
-| `timeout`     | Number  |          | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
+| Option        | Type    | Required | Description                                                                              |
+| ------------- | ------- | -------- | ---------------------------------------------------------------------------------------- |
+| `type`        | String  | Y        | Type of MCP server connection, must be `"local"`.                                        |
+| `command`     | Array   | Y        | Command and arguments to run the MCP server.                                             |
+| `cwd`         | String  |          | Working directory for the MCP server process. Relative paths resolve from the workspace. |
+| `environment` | Object  |          | Environment variables to set when running the server.                                    |
+| `enabled`     | Boolean |          | Enable or disable the MCP server on startup.                                             |
+| `timeout`     | Number  |          | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds).      |
 
 ---