mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
185365af5d
commit
fe8446f5a2
4 changed files with 348 additions and 4 deletions
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
231
src/utils/__tests__/wsl.spec.ts
Normal file
231
src/utils/__tests__/wsl.spec.ts
Normal 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
98
src/utils/wsl.ts
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue