From fe8446f5a2c832ae858b2717eafbb66c15df2044 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 18 Aug 2025 02:42:40 +0000 Subject: [PATCH] fix: resolve MCP marketplace visibility issue in WSL environments - Add WSL detection utility to identify when running in WSL - Update MCP servers directory path logic to use Windows-compatible paths in WSL - Fix path normalization in McpHub for WSL environments - Add comprehensive tests for WSL detection and path handling This fix ensures that MCP servers appear correctly in the marketplace when running VS Code in WSL, matching the behavior in VirtualBox and native Windows environments. Fixes #7167 --- src/core/webview/ClineProvider.ts | 19 ++- src/services/mcp/McpHub.ts | 4 +- src/utils/__tests__/wsl.spec.ts | 231 ++++++++++++++++++++++++++++++ src/utils/wsl.ts | 98 +++++++++++++ 4 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 src/utils/__tests__/wsl.spec.ts create mode 100644 src/utils/wsl.ts diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 04d336d957..34ae2e5c72 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2,6 +2,7 @@ import os from "os" import * as path from "path" import fs from "fs/promises" import EventEmitter from "events" +import { isWSL, getWindowsHomeFromWSL } from "../../utils/wsl" import { Anthropic } from "@anthropic-ai/sdk" import delay from "delay" @@ -1318,14 +1319,26 @@ export class ClineProvider async ensureMcpServersDirectoryExists(): Promise { // Get platform-specific application data directory let mcpServersDir: string - if (process.platform === "win32") { - // Windows: %APPDATA%\Roo-Code\MCP + + // Check if we're running in WSL + if (isWSL()) { + // In WSL, use Windows paths for better compatibility + const windowsHome = getWindowsHomeFromWSL() + if (windowsHome) { + // Use Windows AppData directory accessible from WSL + mcpServersDir = path.join(windowsHome, "AppData", "Roaming", "Roo-Code", "MCP") + } else { + // Fallback to a WSL-friendly location that's easily accessible from Windows + mcpServersDir = path.join("/mnt/c", "ProgramData", "Roo-Code", "MCP") + } + } else if (process.platform === "win32") { + // Native Windows: %APPDATA%\Roo-Code\MCP mcpServersDir = path.join(os.homedir(), "AppData", "Roaming", "Roo-Code", "MCP") } else if (process.platform === "darwin") { // macOS: ~/Documents/Cline/MCP mcpServersDir = path.join(os.homedir(), "Documents", "Cline", "MCP") } else { - // Linux: ~/.local/share/Cline/MCP + // Native Linux: ~/.local/share/Roo-Code/MCP mcpServersDir = path.join(os.homedir(), ".local", "share", "Roo-Code", "MCP") } diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 271c6e1fb3..1a8e47840b 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -18,6 +18,7 @@ import * as path from "path" import * as vscode from "vscode" import { z } from "zod" import { t } from "../../i18n" +import { isWSL } from "../../utils/wsl" import { ClineProvider } from "../../core/webview/ClineProvider" import { GlobalFileNames } from "../../shared/globalFileNames" @@ -1657,7 +1658,8 @@ export class McpHub { // Normalize path for cross-platform compatibility // Use a consistent path format for both reading and writing - const normalizedPath = process.platform === "win32" ? configPath.replace(/\\/g, "/") : configPath + // In WSL, we need to handle paths differently + const normalizedPath = process.platform === "win32" || isWSL() ? configPath.replace(/\\/g, "/") : configPath // Read the appropriate config file const content = await fs.readFile(normalizedPath, "utf-8") diff --git a/src/utils/__tests__/wsl.spec.ts b/src/utils/__tests__/wsl.spec.ts new file mode 100644 index 0000000000..c29f6dcc6e --- /dev/null +++ b/src/utils/__tests__/wsl.spec.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" +import * as fs from "fs" +import * as os from "os" +import { isWSL, getWindowsHomeFromWSL } from "../wsl" + +// Mock fs module +vi.mock("fs", () => ({ + readFileSync: vi.fn(), + accessSync: vi.fn(), + constants: { + F_OK: 0, + }, +})) + +// Mock os module +vi.mock("os", () => ({ + userInfo: vi.fn(() => ({ username: "testuser" })), +})) + +describe("WSL Detection", () => { + const originalEnv = process.env + const originalPlatform = process.platform + + beforeEach(() => { + // Reset environment variables + process.env = { ...originalEnv } + // Reset platform + Object.defineProperty(process, "platform", { + value: originalPlatform, + writable: true, + }) + // Clear all mocks + vi.clearAllMocks() + }) + + afterEach(() => { + // Restore original environment + process.env = originalEnv + Object.defineProperty(process, "platform", { + value: originalPlatform, + writable: false, + }) + }) + + describe("isWSL", () => { + it("should detect WSL when WSL_DISTRO_NAME is set", () => { + process.env.WSL_DISTRO_NAME = "Ubuntu" + expect(isWSL()).toBe(true) + }) + + it("should detect WSL when WSL_INTEROP is set", () => { + process.env.WSL_INTEROP = "/run/WSL/123_interop" + expect(isWSL()).toBe(true) + }) + + it("should detect WSL when /proc/version contains Microsoft", () => { + Object.defineProperty(process, "platform", { value: "linux", writable: true }) + vi.mocked(fs.readFileSync).mockReturnValue( + "Linux version 5.10.16.3-microsoft-standard-WSL2 (gcc version 9.3.0)", + ) + expect(isWSL()).toBe(true) + }) + + it("should detect WSL when /proc/version contains WSL", () => { + Object.defineProperty(process, "platform", { value: "linux", writable: true }) + vi.mocked(fs.readFileSync).mockReturnValue("Linux version 5.10.16.3-WSL2") + expect(isWSL()).toBe(true) + }) + + it("should detect WSL when /proc/sys/fs/binfmt_misc/WSLInterop exists", () => { + Object.defineProperty(process, "platform", { value: "linux", writable: true }) + vi.mocked(fs.readFileSync).mockImplementation(() => { + throw new Error("File not found") + }) + // accessSync should not throw for WSLInterop file + vi.mocked(fs.accessSync).mockImplementation((path) => { + if (path === "/proc/sys/fs/binfmt_misc/WSLInterop") { + return undefined + } + throw new Error("File not found") + }) + expect(isWSL()).toBe(true) + }) + + it("should detect WSL when PATH contains /mnt/c/", () => { + Object.defineProperty(process, "platform", { value: "linux", writable: true }) + process.env.PATH = "/usr/bin:/mnt/c/Windows/System32:/mnt/c/Windows" + vi.mocked(fs.readFileSync).mockImplementation(() => { + throw new Error("File not found") + }) + vi.mocked(fs.accessSync).mockImplementation(() => { + throw new Error("File not found") + }) + expect(isWSL()).toBe(true) + }) + + it("should detect WSL when WSLENV is set", () => { + Object.defineProperty(process, "platform", { value: "linux", writable: true }) + process.env.WSLENV = "WT_SESSION:WT_PROFILE_ID" + vi.mocked(fs.readFileSync).mockImplementation(() => { + throw new Error("File not found") + }) + vi.mocked(fs.accessSync).mockImplementation(() => { + throw new Error("File not found") + }) + expect(isWSL()).toBe(true) + }) + + it("should return false on native Windows", () => { + Object.defineProperty(process, "platform", { value: "win32", writable: true }) + vi.mocked(fs.readFileSync).mockImplementation(() => { + throw new Error("File not found") + }) + vi.mocked(fs.accessSync).mockImplementation(() => { + throw new Error("File not found") + }) + expect(isWSL()).toBe(false) + }) + + it("should return false on native Linux without WSL indicators", () => { + Object.defineProperty(process, "platform", { value: "linux", writable: true }) + process.env.PATH = "/usr/bin:/usr/local/bin" + vi.mocked(fs.readFileSync).mockReturnValue("Linux version 5.10.0-generic") + vi.mocked(fs.accessSync).mockImplementation(() => { + throw new Error("File not found") + }) + expect(isWSL()).toBe(false) + }) + + it("should return false on macOS", () => { + Object.defineProperty(process, "platform", { value: "darwin", writable: true }) + vi.mocked(fs.readFileSync).mockImplementation(() => { + throw new Error("File not found") + }) + vi.mocked(fs.accessSync).mockImplementation(() => { + throw new Error("File not found") + }) + expect(isWSL()).toBe(false) + }) + }) + + describe("getWindowsHomeFromWSL", () => { + beforeEach(() => { + // Set up WSL environment + Object.defineProperty(process, "platform", { value: "linux", writable: true }) + process.env.WSL_DISTRO_NAME = "Ubuntu" + }) + + it("should return null when not in WSL", () => { + delete process.env.WSL_DISTRO_NAME + delete process.env.WSL_INTEROP + Object.defineProperty(process, "platform", { value: "win32", writable: true }) + expect(getWindowsHomeFromWSL()).toBe(null) + }) + + it("should find Windows home directory at /mnt/c/Users/username", () => { + process.env.USER = "testuser" + vi.mocked(fs.accessSync).mockImplementation((path) => { + if (path === "/mnt/c/Users/testuser") { + return undefined + } + throw new Error("File not found") + }) + expect(getWindowsHomeFromWSL()).toBe("/mnt/c/Users/testuser") + }) + + it("should find Windows home directory at /mnt/c/users/username (lowercase)", () => { + process.env.USER = "testuser" + vi.mocked(fs.accessSync).mockImplementation((path) => { + if (path === "/mnt/c/users/testuser") { + return undefined + } + throw new Error("File not found") + }) + expect(getWindowsHomeFromWSL()).toBe("/mnt/c/users/testuser") + }) + + it("should check D: drive if C: drive not found", () => { + process.env.USER = "testuser" + vi.mocked(fs.accessSync).mockImplementation((path) => { + if (path === "/mnt/d/Users/testuser") { + return undefined + } + throw new Error("File not found") + }) + expect(getWindowsHomeFromWSL()).toBe("/mnt/d/Users/testuser") + }) + + it("should use WSL_USER_NAME if available", () => { + process.env.WSL_USER_NAME = "wsluser" + vi.mocked(fs.accessSync).mockImplementation((path) => { + if (path === "/mnt/c/Users/wsluser") { + return undefined + } + throw new Error("File not found") + }) + expect(getWindowsHomeFromWSL()).toBe("/mnt/c/Users/wsluser") + }) + + it("should convert USERPROFILE Windows path to WSL path", () => { + process.env.USERPROFILE = "C:\\Users\\winuser" + vi.mocked(fs.accessSync).mockImplementation((path) => { + if (path === "/mnt/c/Users/winuser") { + return undefined + } + throw new Error("File not found") + }) + expect(getWindowsHomeFromWSL()).toBe("/mnt/c/Users/winuser") + }) + + it("should handle USERPROFILE with different drive letter", () => { + process.env.USERPROFILE = "D:\\Users\\winuser" + vi.mocked(fs.accessSync).mockImplementation((path) => { + if (path === "/mnt/d/Users/winuser") { + return undefined + } + throw new Error("File not found") + }) + expect(getWindowsHomeFromWSL()).toBe("/mnt/d/Users/winuser") + }) + + it("should return null when no Windows home directory is found", () => { + process.env.USER = "testuser" + delete process.env.USERPROFILE + vi.mocked(fs.accessSync).mockImplementation(() => { + throw new Error("File not found") + }) + expect(getWindowsHomeFromWSL()).toBe(null) + }) + }) +}) diff --git a/src/utils/wsl.ts b/src/utils/wsl.ts new file mode 100644 index 0000000000..222bccc1c2 --- /dev/null +++ b/src/utils/wsl.ts @@ -0,0 +1,98 @@ +import * as fs from "fs" +import * as os from "os" + +/** + * Detects if the current environment is running inside WSL (Windows Subsystem for Linux) + * @returns true if running in WSL, false otherwise + */ +export function isWSL(): boolean { + // WSL detection based on multiple indicators + + // 1. Check for WSL environment variable (WSL 2) + if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) { + return true + } + + // 2. Check for /proc/version containing Microsoft or WSL + try { + const procVersion = fs.readFileSync("/proc/version", "utf8") + if (procVersion.toLowerCase().includes("microsoft") || procVersion.toLowerCase().includes("wsl")) { + return true + } + } catch { + // File doesn't exist or can't be read, not WSL + } + + // 3. Check for /proc/sys/fs/binfmt_misc/WSLInterop (WSL 2) + try { + fs.accessSync("/proc/sys/fs/binfmt_misc/WSLInterop", fs.constants.F_OK) + return true + } catch { + // File doesn't exist, might not be WSL 2 + } + + // 4. Check if running on Linux but with Windows-style paths in environment + if (process.platform === "linux") { + // Check for Windows paths in PATH environment variable + const pathEnv = process.env.PATH || "" + if (pathEnv.includes("/mnt/c/") || pathEnv.includes("\\")) { + return true + } + + // Check for WSLENV variable (used for sharing environment variables between Windows and WSL) + if (process.env.WSLENV) { + return true + } + } + + return false +} + +/** + * Gets the Windows user home directory from within WSL + * @returns The Windows home directory path or null if not in WSL or cannot determine + */ +export function getWindowsHomeFromWSL(): string | null { + if (!isWSL()) { + return null + } + + // Try to get Windows username from environment + const windowsUsername = process.env.WSL_USER_NAME || process.env.USER || os.userInfo().username + + // Common Windows home directory patterns in WSL + const possiblePaths = [ + `/mnt/c/Users/${windowsUsername}`, + `/mnt/c/users/${windowsUsername}`, + `/mnt/d/Users/${windowsUsername}`, + `/mnt/d/users/${windowsUsername}`, + ] + + // Check which path exists + for (const path of possiblePaths) { + try { + fs.accessSync(path, fs.constants.F_OK) + return path + } catch { + // Path doesn't exist, try next + } + } + + // Fallback: try to read from USERPROFILE if it's set (might be shared from Windows) + if (process.env.USERPROFILE) { + // Convert Windows path to WSL path (C:\Users\username -> /mnt/c/Users/username) + const windowsPath = process.env.USERPROFILE + const wslPath = windowsPath + .replace(/^([A-Z]):/i, (_, drive) => `/mnt/${drive.toLowerCase()}`) + .replace(/\\/g, "/") + + try { + fs.accessSync(wslPath, fs.constants.F_OK) + return wslPath + } catch { + // Path doesn't exist + } + } + + return null +}