diff --git a/core/config/types.ts b/core/config/types.ts index df29d6071c..406f40dddb 100644 --- a/core/config/types.ts +++ b/core/config/types.ts @@ -1004,6 +1004,8 @@ declare global { type: "stdio"; command: string; args: string[]; + env?: Record; + cwd?: string; } interface WebSocketOptions { diff --git a/core/config/yaml/loadYaml.ts b/core/config/yaml/loadYaml.ts index f1546e255f..935e8b9849 100644 --- a/core/config/yaml/loadYaml.ts +++ b/core/config/yaml/loadYaml.ts @@ -69,6 +69,7 @@ function convertYamlMcpToContinueMcp( command: server.command, args: server.args ?? [], env: server.env, + cwd: server.cwd, } as any, // TODO: Fix the mcpServers types in config-yaml (discriminated union) timeout: server.connectionTimeout, }; diff --git a/core/config/yaml/loadYaml.vitest.ts b/core/config/yaml/loadYaml.vitest.ts new file mode 100644 index 0000000000..fd6769f709 --- /dev/null +++ b/core/config/yaml/loadYaml.vitest.ts @@ -0,0 +1,128 @@ +import { describe, it, expect } from "vitest"; +import { + validateConfigYaml, + AssistantUnrolledNonNullable, +} from "@continuedev/config-yaml"; + +describe("MCP Server cwd configuration", () => { + describe("YAML schema validation", () => { + it("should accept valid MCP server with cwd", () => { + const config: AssistantUnrolledNonNullable = { + name: "test-assistant", + version: "1.0.0", + mcpServers: [ + { + name: "test-server", + command: "node", + args: ["server.js"], + env: { NODE_ENV: "production" }, + cwd: "/path/to/project", + connectionTimeout: 5000, + }, + ], + }; + + const errors = validateConfigYaml(config); + expect(errors).toHaveLength(0); + }); + + it("should accept MCP server without cwd", () => { + const config: AssistantUnrolledNonNullable = { + name: "test-assistant", + version: "1.0.0", + mcpServers: [ + { + name: "test-server", + command: "python", + args: ["-m", "server"], + }, + ], + }; + + const errors = validateConfigYaml(config); + expect(errors).toHaveLength(0); + }); + + it("should accept relative paths in cwd", () => { + const config: AssistantUnrolledNonNullable = { + name: "test-assistant", + version: "1.0.0", + mcpServers: [ + { + name: "test-server", + command: "cargo", + args: ["run"], + cwd: "./rust-project", + }, + ], + }; + + const errors = validateConfigYaml(config); + expect(errors).toHaveLength(0); + }); + + it("should accept empty string cwd", () => { + const config: AssistantUnrolledNonNullable = { + name: "test-assistant", + version: "1.0.0", + mcpServers: [ + { + name: "test-server", + command: "deno", + args: ["run", "server.ts"], + cwd: "", + }, + ], + }; + + const errors = validateConfigYaml(config); + expect(errors).toHaveLength(0); + }); + }); + + describe("MCP server configuration examples", () => { + it("should support common MCP server patterns with cwd", () => { + const configs = [ + { + name: "Local project MCP server", + server: { + name: "project-mcp", + command: "npm", + args: ["run", "mcp-server"], + cwd: "/Users/developer/my-project", + }, + }, + { + name: "Python MCP with virtual environment", + server: { + name: "python-mcp", + command: "python", + args: ["-m", "my_mcp_server"], + env: { PYTHONPATH: "./src" }, + cwd: "/home/user/python-project", + }, + }, + { + name: "Relative path MCP server", + server: { + name: "relative-mcp", + command: "node", + args: ["index.js"], + cwd: "../mcp-servers/filesystem", + }, + }, + ]; + + configs.forEach(({ name, server }) => { + const config: AssistantUnrolledNonNullable = { + name: "test-assistant", + version: "1.0.0", + mcpServers: [server], + }; + + const errors = validateConfigYaml(config); + expect(errors).toHaveLength(0); + }); + }); + }); +}); diff --git a/core/context/mcp/MCPConnection.ts b/core/context/mcp/MCPConnection.ts index c66a93ed33..af971d8137 100644 --- a/core/context/mcp/MCPConnection.ts +++ b/core/context/mcp/MCPConnection.ts @@ -329,6 +329,7 @@ class MCPConnection { command, args, env, + cwd: options.transport.cwd, stderr: "pipe", }); diff --git a/core/context/mcp/MCPConnection.vitest.ts b/core/context/mcp/MCPConnection.vitest.ts index 7736ebec95..0933ec0205 100644 --- a/core/context/mcp/MCPConnection.vitest.ts +++ b/core/context/mcp/MCPConnection.vitest.ts @@ -1,6 +1,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { beforeEach, describe, expect, it, vi } from "vitest"; import MCPConnection from "./MCPConnection"; +import { StdioOptions } from "../../index.d.js"; describe("MCPConnection", () => { beforeEach(() => { @@ -25,6 +26,27 @@ describe("MCPConnection", () => { expect(conn.status).toBe("not-connected"); }); + it("should create instance with stdio transport including cwd", () => { + const options = { + name: "test-mcp", + id: "test-id", + transport: { + type: "stdio" as const, + command: "test-cmd", + args: ["--test"], + env: { TEST: "true" }, + cwd: "/path/to/working/directory", + }, + }; + + const conn = new MCPConnection(options); + expect(conn).toBeInstanceOf(MCPConnection); + expect(conn.status).toBe("not-connected"); + if (conn.options.transport.type === "stdio") { + expect(conn.options.transport.cwd).toBe("/path/to/working/directory"); + } + }); + it("should create instance with websocket transport", () => { const options = { name: "test-mcp", diff --git a/core/index.d.ts b/core/index.d.ts index 3d6f1602c1..997ffd85e8 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -1162,6 +1162,7 @@ export interface StdioOptions { command: string; args: string[]; env?: Record; + cwd?: string; requestOptions?: RequestOptions; } diff --git a/docs/docs/customize/deep-dives/mcp.mdx b/docs/docs/customize/deep-dives/mcp.mdx index 9899518e78..589b04798c 100644 --- a/docs/docs/customize/deep-dives/mcp.mdx +++ b/docs/docs/customize/deep-dives/mcp.mdx @@ -110,6 +110,7 @@ MCP blocks follow the established syntax for blocks, with a few additional prope - `type` (optional): The type of the MCP server: `sse`, `stdio`, `streamable-http` - `args` (optional): Arguments to pass to the command. - `env` (optional): Secrets to be injected into the command as environment variables. +- `cwd` (optional): Working directory to run the command in. Can be absolute or relative path. ### Transport Types @@ -132,12 +133,22 @@ For local MCP servers that communicate via standard input and output: ```yaml mcpServers: - - name: Name + - name: SQLite Server type: stdio command: npx args: - "@modelcontextprotocol/server-sqlite" - - "/path/to/your/database.db" + - "./database.db" + cwd: "/path/to/project" + + - name: Local Project Server + command: npm + args: + - "run" + - "mcp-server" + cwd: "./my-project" + env: + NODE_ENV: "production" ``` #### Streamable HTTP Transport diff --git a/docs/docs/customize/yaml-migration.mdx b/docs/docs/customize/yaml-migration.mdx index 5ffd50e7cb..444d54b628 100644 --- a/docs/docs/customize/yaml-migration.mdx +++ b/docs/docs/customize/yaml-migration.mdx @@ -288,6 +288,7 @@ docs: - `command` (**required**): The command used to start the server. - `args`: An optional array of arguments for the command. - `env`: An optional map of environment variables for the server process. +- `cwd`: An optional working directory to run the command in. Can be absolute or relative path. **Before** diff --git a/docs/docs/reference.mdx b/docs/docs/reference.mdx index 8f9db13478..f685dac4bf 100644 --- a/docs/docs/reference.mdx +++ b/docs/docs/reference.mdx @@ -384,6 +384,7 @@ prompts, context, and tool use. Continue supports any MCP server with the MCP co - `command` (**required**): The command used to start the server. - `args`: An optional array of arguments for the command. - `env`: An optional map of environment variables for the server process. +- `cwd`: An optional working directory to run the command in. Can be absolute or relative path. - `connectionTimeout`: An optional connection timeout number to the server in milliseconds. **Example:** @@ -395,7 +396,10 @@ mcpServers: args: - mcp-server-sqlite - --db-path - - /Users/NAME/test.db + - ./test.db + cwd: /Users/NAME/project + env: + NODE_ENV: production ``` ### `data` diff --git a/packages/config-yaml/src/schemas/index.ts b/packages/config-yaml/src/schemas/index.ts index 917cb9866e..b3d7782f4a 100644 --- a/packages/config-yaml/src/schemas/index.ts +++ b/packages/config-yaml/src/schemas/index.ts @@ -22,6 +22,7 @@ const mcpServerSchema = z.object({ faviconUrl: z.string().optional(), args: z.array(z.string()).optional(), env: z.record(z.string()).optional(), + cwd: z.string().optional(), connectionTimeout: z.number().gt(0).optional(), requestOptions: requestOptionsSchema.optional(), });