Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions core/context/mcp/MCPConnection.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { fileURLToPath } from "url";

import {
SSEClientTransport,
SseError,
} from "@modelcontextprotocol/sdk/client/sse.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { Agent as HttpsAgent } from "https";
import {
IDE,
Expand All @@ -22,6 +24,7 @@ import {
MCPServerStatus,
MCPTool,
} from "../..";
import { resolveRelativePathInDir } from "../../util/ideUtils";
import { getEnvPathFromUserShell } from "../../util/shellPath";
import { getOauthToken } from "./MCPOauth";

Expand Down Expand Up @@ -413,6 +416,44 @@ class MCPConnection {
};
}

/**
* Resolves the current working directory of the current workspace.
* @param cwd The cwd parameter provided by user.
* @returns Current working directory (user-provided cwd or workspace root).
*/
private async resolveCwd(cwd?: string) {
if (!cwd) {
return this.resolveWorkspaceCwd(undefined);
}

if (cwd.startsWith("file://")) {
return fileURLToPath(cwd);
}

// Return cwd if cwd is an absolute path.
if (cwd.charAt(0) === "/") {
return cwd;
}

return this.resolveWorkspaceCwd(cwd);
}

private async resolveWorkspaceCwd(cwd: string | undefined) {
const IDE = this.extras?.ide;
if (IDE) {
const target = cwd ?? ".";
const resolved = await resolveRelativePathInDir(target, IDE);
if (resolved) {
if (resolved.startsWith("file://")) {
return fileURLToPath(resolved);
}
return resolved;
}
return resolved;
}
return cwd;
}

private constructWebsocketTransport(
options: InternalWebsocketMcpOptions,
): WebSocketClientTransport {
Expand Down Expand Up @@ -499,11 +540,13 @@ class MCPConnection {
options.args || [],
);

const cwd = await this.resolveCwd(options.cwd);

const transport = new StdioClientTransport({
command,
args,
env,
cwd: options.cwd,
cwd,
stderr: "pipe",
});

Expand Down
32 changes: 32 additions & 0 deletions core/context/mcp/MCPConnection.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
InternalStdioMcpOptions,
InternalWebsocketMcpOptions,
} from "../..";
import * as ideUtils from "../../util/ideUtils";
import MCPConnection from "./MCPConnection";

// Mock the shell path utility
Expand Down Expand Up @@ -145,6 +146,37 @@ describe("MCPConnection", () => {
});
});

describe("resolveCwd", () => {
const baseOptions = {
name: "test-mcp",
id: "test-id",
type: "stdio" as const,
command: "test-cmd",
args: [],
};

it("should return absolute cwd unchanged", async () => {
const conn = new MCPConnection(baseOptions);

await expect((conn as any).resolveCwd("/tmp/project")).resolves.toBe(
"/tmp/project",
);
});

it("should resolve relative cwd using IDE workspace", async () => {
const ide = {} as any;
const mockResolve = vi
.spyOn(ideUtils, "resolveRelativePathInDir")
.mockResolvedValue("file:///workspace/src");
const conn = new MCPConnection(baseOptions, { ide });

await expect((conn as any).resolveCwd("src")).resolves.toBe(
"/workspace/src",
);
expect(mockResolve).toHaveBeenCalledWith("src", ide);
});
});

describe("connectClient", () => {
const options: InternalStdioMcpOptions = {
name: "test-mcp",
Expand Down
Loading