Skip to content

Commit af318a5

Browse files
Grantmartin2002rekram1-node
authored andcommitted
feat(opencode): support cwd on local MCP servers (anomalyco#30676)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
1 parent 248ce46 commit af318a5

6 files changed

Lines changed: 46 additions & 11 deletions

File tree

packages/core/src/config/mcp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { PositiveInt } from "../schema"
66
export class Local extends Schema.Class<Local>("ConfigV2.MCP.Local")({
77
type: Schema.Literal("local"),
88
command: Schema.String.pipe(Schema.Array),
9+
cwd: Schema.String.pipe(Schema.optional).annotate({
10+
description: "Working directory for the MCP server process. Relative paths resolve from the workspace directory.",
11+
}),
912
environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
1013
disabled: Schema.Boolean.pipe(Schema.optional),
1114
timeout: PositiveInt.pipe(Schema.optional),

packages/core/src/v1/config/mcp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export const Local = Schema.Struct({
88
command: Schema.mutable(Schema.Array(Schema.String)).annotate({
99
description: "Command and arguments to run the MCP server",
1010
}),
11+
cwd: Schema.optional(Schema.String).annotate({
12+
description: "Working directory for the MCP server process. Relative paths resolve from the workspace directory.",
13+
}),
1114
environment: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({
1215
description: "Environment variables to set when running the MCP server",
1316
}),

packages/core/src/v1/config/migrate.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,14 @@ function mcp(info: typeof ConfigV1.Info.Type) {
139139
function migrateMcp(info: ConfigMCPV1.Info) {
140140
const disabled = info.enabled === undefined ? undefined : !info.enabled
141141
if (info.type === "local")
142-
return { type: info.type, command: info.command, environment: info.environment, disabled, timeout: info.timeout }
142+
return {
143+
type: info.type,
144+
command: info.command,
145+
cwd: info.cwd,
146+
environment: info.environment,
147+
disabled,
148+
timeout: info.timeout,
149+
}
143150
return {
144151
type: info.type,
145152
url: info.url,

packages/opencode/src/mcp/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import path from "node:path"
12
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
23
import { type Tool } from "ai"
34
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
@@ -304,7 +305,8 @@ export const layer = Layer.effect(
304305
mcp: ConfigMCPV1.Info & { type: "local" },
305306
) {
306307
const [cmd, ...args] = mcp.command
307-
const cwd = yield* InstanceState.directory
308+
const baseDir = yield* InstanceState.directory
309+
const cwd = mcp.cwd ? path.resolve(baseDir, mcp.cwd) : baseDir
308310
const transport = new StdioClientTransport({
309311
stderr: "pipe",
310312
command: cmd,

packages/opencode/test/mcp/lifecycle.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import path from "node:path"
12
import { expect, mock, beforeEach } from "bun:test"
23
import { ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js"
34
import { Cause, Effect, Exit } from "effect"
45
import type { MCP as MCPNS } from "../../src/mcp/index"
56
import { testEffect } from "../lib/effect"
7+
import { TestInstance } from "../fixture/fixture"
68

79
// --- Mock infrastructure ---
810

@@ -48,6 +50,8 @@ let connectError = "Mock transport cannot connect"
4850
let clientCreateCount = 0
4951
// Tracks how many times transport.close() is called across all mock transports
5052
let transportCloseCount = 0
53+
// Captures the opts passed to each MockStdioTransport, keyed by lastCreatedClientName
54+
const stdioOptsByName = new Map<string, any>()
5155

5256
function getOrCreateClientState(name?: string): MockClientState {
5357
const key = name ?? "default"
@@ -82,8 +86,9 @@ function getOrCreateClientState(name?: string): MockClientState {
8286
class MockStdioTransport {
8387
stderr: null = null
8488
pid = 12345
85-
// oxlint-disable-next-line no-useless-constructor
86-
constructor(_opts: any) {}
89+
constructor(opts: any) {
90+
if (lastCreatedClientName) stdioOptsByName.set(lastCreatedClientName, opts)
91+
}
8792
async start() {
8893
if (connectShouldHang) return new Promise<void>(() => {}) // never resolves
8994
if (connectShouldFail) throw new Error(connectError)
@@ -246,6 +251,20 @@ function statusName(status: Record<string, MCPNS.Status> | MCPNS.Status, server:
246251
return status[server]?.status
247252
}
248253

254+
it.instance(
255+
"local mcp cwd resolves relative paths against instance directory",
256+
() =>
257+
MCP.Service.use((mcp: MCPNS.Interface) =>
258+
Effect.gen(function* () {
259+
const { directory } = yield* TestInstance
260+
lastCreatedClientName = "rel-cwd"
261+
yield* mcp.add("rel-cwd", { type: "local", command: ["echo", "test"], cwd: "plugins/sub" })
262+
expect(stdioOptsByName.get("rel-cwd")?.cwd).toBe(path.resolve(directory, "plugins/sub"))
263+
}),
264+
),
265+
{ config: { mcp: {} } },
266+
)
267+
249268
// ========================================================================
250269
// Test: tools() are cached after connect
251270
// ========================================================================

packages/web/src/content/docs/mcp-servers.mdx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,14 @@ use the mcp_everything tool to add the number 3 and 4
116116

117117
Here are all the options for configuring a local MCP server.
118118

119-
| Option | Type | Required | Description |
120-
| ------------- | ------- | -------- | ----------------------------------------------------------------------------------- |
121-
| `type` | String | Y | Type of MCP server connection, must be `"local"`. |
122-
| `command` | Array | Y | Command and arguments to run the MCP server. |
123-
| `environment` | Object | | Environment variables to set when running the server. |
124-
| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
125-
| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
119+
| Option | Type | Required | Description |
120+
| ------------- | ------- | -------- | ---------------------------------------------------------------------------------------- |
121+
| `type` | String | Y | Type of MCP server connection, must be `"local"`. |
122+
| `command` | Array | Y | Command and arguments to run the MCP server. |
123+
| `cwd` | String | | Working directory for the MCP server process. Relative paths resolve from the workspace. |
124+
| `environment` | Object | | Environment variables to set when running the server. |
125+
| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
126+
| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
126127

127128
---
128129

0 commit comments

Comments
 (0)