-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Fix stdio MCP stdio connections failing with ENOENT on remote SSH VSCode #10464
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+455
to
+468
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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; | |
| try { | |
| // 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; | |
| } catch { | |
| // If parsing or conversion fails, return the input as-is | |
| return uri; | |
| } |
Copilot
AI
Feb 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using includes("://") to detect URIs is overly broad and could produce false positives. For example, a Windows path like "C://path" would incorrectly be treated as a URI. Consider using a more robust check such as checking for a valid URI scheme pattern (e.g., /^[a-z][a-z0-9+.-]*:///i.test(cwd)) or checking if URI.parse(cwd).scheme is defined.
| if (cwd.includes("://")) { | |
| const parsedCwd = URI.parse(cwd); | |
| if (parsedCwd.scheme) { |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"); | ||
| }); | ||
|
Comment on lines
+187
to
+195
|
||
|
|
||
| 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", () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<string | undefined> { | ||
|
|
@@ -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(":"); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The path extracted from URI.parse() needs to be decoded using decodeURIComponent() to handle URL-encoded characters (spaces, special characters, etc.). Without decoding, paths like "vscode-remote://ssh-remote+myserver/home/user/my%20project" will become "/home/user/my%20project" instead of "/home/user/my project".
This is consistent with how URIs are handled elsewhere in the codebase (see core/util/paths.ts lines 495 and 504, and core/tools/implementations/runTerminalCommand.ts line 71 which uses decodeURIComponent(url.pathname) for the same purpose).