diff --git a/core/context/mcp/MCPConnection.ts b/core/context/mcp/MCPConnection.ts index f1199066f2a..7a1019d6937 100644 --- a/core/context/mcp/MCPConnection.ts +++ b/core/context/mcp/MCPConnection.ts @@ -1,5 +1,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import * as URI from "uri-js"; import { fileURLToPath } from "url"; import { @@ -445,6 +446,28 @@ Org-level secrets can only be used for MCP by Background Agents (https://docs.co }; } + /** + * Converts a URI (file://, vscode-remote://, etc.) to a filesystem path. + * @param uri The URI to convert + * @returns The filesystem path + */ + private uriToFsPath(uri: string): string { + // Handle file:// URIs using Node's built-in converter + if (uri.startsWith("file://")) { + return fileURLToPath(uri); + } + + // For other URI schemes (vscode-remote://, etc.), extract the path portion + const parsed = URI.parse(uri); + if (parsed.scheme && parsed.path) { + // The path from URI.parse already includes the leading / + return parsed.path; + } + + // If it's not a URI, return as-is + return uri; + } + /** * Resolves the current working directory of the current workspace. * @param cwd The cwd parameter provided by user. @@ -455,8 +478,9 @@ Org-level secrets can only be used for MCP by Background Agents (https://docs.co return this.resolveWorkspaceCwd(undefined); } - if (cwd.startsWith("file://")) { - return fileURLToPath(cwd); + // Check if it's a URI (has a scheme like file://, vscode-remote://, etc.) + if (cwd.includes("://")) { + return this.uriToFsPath(cwd); } // Return cwd if cwd is an absolute path. @@ -473,10 +497,8 @@ Org-level secrets can only be used for MCP by Background Agents (https://docs.co const target = cwd ?? "."; const resolved = await resolveRelativePathInDir(target, IDE); if (resolved) { - if (resolved.startsWith("file://")) { - return fileURLToPath(resolved); - } - return resolved; + // resolved is a URI, convert to filesystem path + return this.uriToFsPath(resolved); } return resolved; } @@ -559,24 +581,31 @@ Org-level secrets can only be used for MCP by Background Agents (https://docs.co ...(options.env ?? {}), }; + // Always try to set a proper PATH + const ideInfo = await this.extras?.ide?.getIdeInfo(); + const isWindowsHostWithWslRemote = + process.platform === "win32" && ideInfo?.remoteName === "wsl"; + + // Set initial PATH - use process.env.PATH if available if (process.env.PATH !== undefined) { - // Set the initial PATH from process.env env.PATH = process.env.PATH; + } - // For non-Windows platforms or WSL remotes, try to get the PATH from user shell - const ideInfo = await this.extras?.ide?.getIdeInfo(); - const isWindowsHostWithWslRemote = - process.platform === "win32" && ideInfo?.remoteName === "wsl"; - if (process.platform !== "win32" || isWindowsHostWithWslRemote) { - try { - const shellEnvPath = await getEnvPathFromUserShell( - ideInfo?.remoteName, + // For non-Windows platforms or remote connections, get proper shell PATH + if (process.platform !== "win32" || isWindowsHostWithWslRemote) { + try { + const shellEnvPath = await getEnvPathFromUserShell(ideInfo?.remoteName); + // Always use shell path if available (it includes defaults as fallback) + if (shellEnvPath) { + env.PATH = shellEnvPath; + } + } catch (err) { + console.error("Error getting PATH from shell:", err); + // If we still don't have a PATH, this is a critical issue + if (!env.PATH) { + console.error( + "WARNING: No PATH set for MCP server. Command execution will likely fail.", ); - if (shellEnvPath && shellEnvPath !== process.env.PATH) { - env.PATH = shellEnvPath; - } - } catch (err) { - console.error("Error getting PATH:", err); } } } diff --git a/core/context/mcp/MCPConnection.vitest.ts b/core/context/mcp/MCPConnection.vitest.ts index 7920a9a1ddc..d613ada3603 100644 --- a/core/context/mcp/MCPConnection.vitest.ts +++ b/core/context/mcp/MCPConnection.vitest.ts @@ -175,6 +175,39 @@ describe("MCPConnection", () => { ); expect(mockResolve).toHaveBeenCalledWith("src", ide); }); + + it("should convert file:// URIs to filesystem paths", async () => { + const conn = new MCPConnection(baseOptions); + + await expect( + (conn as any).resolveCwd("file:///home/user/project"), + ).resolves.toBe("/home/user/project"); + }); + + it("should convert vscode-remote:// URIs to filesystem paths", async () => { + const conn = new MCPConnection(baseOptions); + + await expect( + (conn as any).resolveCwd( + "vscode-remote://ssh-remote+myserver/home/user/project", + ), + ).resolves.toBe("/home/user/project"); + }); + + it("should handle workspace URIs from remote IDE", async () => { + const ide = {} as any; + const mockResolve = vi + .spyOn(ideUtils, "resolveRelativePathInDir") + .mockResolvedValue( + "vscode-remote://ssh-remote+myserver/home/user/workspace/src", + ); + const conn = new MCPConnection(baseOptions, { ide }); + + await expect((conn as any).resolveCwd("src")).resolves.toBe( + "/home/user/workspace/src", + ); + expect(mockResolve).toHaveBeenCalledWith("src", ide); + }); }); describe("connectClient", () => { diff --git a/core/util/shellPath.ts b/core/util/shellPath.ts index 961307c28bb..bd58c9e8368 100644 --- a/core/util/shellPath.ts +++ b/core/util/shellPath.ts @@ -2,6 +2,19 @@ import { exec } from "child_process"; import { promisify } from "util"; const execAsync = promisify(exec); + +// Common Unix binary paths that should be included in PATH +const DEFAULT_UNIX_PATHS = [ + "/usr/local/bin", + "/usr/bin", + "/bin", + "/usr/local/sbin", + "/usr/sbin", + "/sbin", + "/opt/homebrew/bin", // macOS Homebrew on Apple Silicon + "/home/linuxbrew/.linuxbrew/bin", // Linux Homebrew +]; + export async function getEnvPathFromUserShell( remoteName?: string, ): Promise { @@ -11,20 +24,34 @@ export async function getEnvPathFromUserShell( return undefined; } - if (!process.env.SHELL) { - return undefined; - } + // Try to find a shell to use + const shell = process.env.SHELL || "/bin/bash"; try { // Source common profile files - const command = `${process.env.SHELL} -l -c 'for f in ~/.zprofile ~/.zshrc ~/.bash_profile ~/.bashrc; do [ -f "$f" ] && source "$f" 2>/dev/null; done; echo $PATH'`; + const command = `${shell} -l -c 'for f in ~/.zprofile ~/.zshrc ~/.bash_profile ~/.bashrc; do [ -f "$f" ] && source "$f" 2>/dev/null; done; echo $PATH'`; const { stdout } = await execAsync(command, { encoding: "utf8", + timeout: 5000, // 5 second timeout }); - return stdout.trim(); + const pathFromShell = stdout.trim(); + if (pathFromShell) { + return pathFromShell; + } } catch (error) { - return process.env.PATH; // Fallback to current PATH + // If shell command fails, fall through to default handling + console.warn("Failed to get PATH from shell:", error); } + + // Fallback: build a reasonable default PATH + if (!process.env.PATH) { + return DEFAULT_UNIX_PATHS.join(":"); + } + + // Merge current PATH with common paths (avoiding duplicates) + const currentPaths = process.env.PATH.split(":"); + const allPaths = [...new Set([...currentPaths, ...DEFAULT_UNIX_PATHS])]; + return allPaths.join(":"); }