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
This commit is contained in:
Roo Code 2025-08-18 02:42:40 +00:00
parent 185365af5d
commit fe8446f5a2
4 changed files with 348 additions and 4 deletions

View file

@ -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<string> {
// 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")
}

View file

@ -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")

View file

@ -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)
})
})
})

98
src/utils/wsl.ts Normal file
View file

@ -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
}