fix: adds handling for WSL on installing MCP on Claude on Windows (#8738)

* Added handling for wsl isntallation of claude

* Fixed installed server not appearing

* Added message to restart

* fix formatting and error handling

* fixed lint
This commit is contained in:
Lucas Oliveira 2025-06-26 11:28:17 -03:00 • committed by GitHub
commit b08d080c67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 130 additions and 11 deletions

View file

@ -9,6 +9,7 @@ from contextvars import ContextVar
from datetime import datetime, timezone from datetime import datetime, timezone
from ipaddress import ip_address from ipaddress import ip_address
from pathlib import Path from pathlib import Path
from subprocess import CalledProcessError
from urllib.parse import quote, unquote, urlparse from urllib.parse import quote, unquote, urlparse
from uuid import UUID, uuid4 from uuid import UUID, uuid4
@ -355,7 +356,9 @@ async def install_mcp_config(
args = ["mcp-proxy", sse_url] args = ["mcp-proxy", sse_url]
# Check if running on WSL (will appear as Linux but with Microsoft in release info) # Check if running on WSL (will appear as Linux but with Microsoft in release info)
if os_type == "Linux" and "microsoft" in platform.uname().release.lower(): is_wsl = os_type == "Linux" and "microsoft" in platform.uname().release.lower()
if is_wsl:
logger.debug("WSL detected, using Windows-specific configuration") logger.debug("WSL detected, using Windows-specific configuration")
# If we're in WSL and the host is localhost, we might need to adjust the URL # If we're in WSL and the host is localhost, we might need to adjust the URL
@ -399,13 +402,63 @@ async def install_mcp_config(
} }
} }
server_name = f"lf-{name.lower().replace(' ', '_')[: (MAX_MCP_SERVER_NAME_LENGTH - 4)]}"
logger.debug("Installing MCP config for project: %s (server name: %s)", project.name, server_name)
# Determine the config file path based on the client and OS # Determine the config file path based on the client and OS
if body.client.lower() == "cursor": if body.client.lower() == "cursor":
config_path = Path.home() / ".cursor" / "mcp.json" config_path = Path.home() / ".cursor" / "mcp.json"
elif body.client.lower() == "claude": elif body.client.lower() == "claude":
if os_type == "Darwin": # macOS if os_type == "Darwin": # macOS
config_path = Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json" config_path = Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
elif os_type == "Windows": elif os_type == "Windows" or is_wsl: # Windows or WSL (Claude runs on Windows host)
if is_wsl:
# In WSL, we need to access the Windows APPDATA directory
try:
# First try to get the Windows username
proc = await create_subprocess_exec(
"/mnt/c/Windows/System32/cmd.exe",
"/c",
"echo %USERNAME%",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode == 0 and stdout.strip():
windows_username = stdout.decode().strip()
config_path = Path(
f"/mnt/c/Users/{windows_username}/AppData/Roaming/Claude/claude_desktop_config.json"
)
else:
# Fallback: try to find the Windows user directory
users_dir = Path("/mnt/c/Users")
if users_dir.exists():
# Get the first non-system user directory
user_dirs = [
d
for d in users_dir.iterdir()
if d.is_dir() and not d.name.startswith(("Default", "Public", "All Users"))
]
if user_dirs:
config_path = (
user_dirs[0] / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json"
)
else:
raise HTTPException(
status_code=400, detail="Could not find Windows user directory in WSL"
)
else:
raise HTTPException(
status_code=400, detail="Windows C: drive not mounted at /mnt/c in WSL"
)
except (OSError, CalledProcessError) as e:
logger.warning("Failed to determine Windows user path in WSL: %s", str(e))
raise HTTPException(
status_code=400, detail=f"Could not determine Windows Claude config path in WSL: {e!s}"
) from e
else:
# Regular Windows
config_path = Path(os.environ["APPDATA"]) / "Claude" / "claude_desktop_config.json" config_path = Path(os.environ["APPDATA"]) / "Claude" / "claude_desktop_config.json"
else: else:
raise HTTPException(status_code=400, detail="Unsupported operating system for Claude configuration") raise HTTPException(status_code=400, detail="Unsupported operating system for Claude configuration")
@ -460,40 +513,106 @@ async def check_installed_mcp_servers(
if not project: if not project:
raise HTTPException(status_code=404, detail="Project not found") raise HTTPException(status_code=404, detail="Project not found")
# Project server name pattern # Project server name pattern (must match the logic in install function)
project_server_name = f"lf-{project.name.lower().replace(' ', '_')[: (MAX_MCP_SERVER_NAME_LENGTH - 4)]}" name = project.name
name = NEW_FOLDER_NAME if name == DEFAULT_FOLDER_NAME else name
project_server_name = f"lf-{name.lower().replace(' ', '_')[: (MAX_MCP_SERVER_NAME_LENGTH - 4)]}"
logger.debug(
"Checking for installed MCP servers for project: %s (server name: %s)", project.name, project_server_name
)
# Check configurations for different clients # Check configurations for different clients
results = [] results = []
# Check Cursor configuration # Check Cursor configuration
cursor_config_path = Path.home() / ".cursor" / "mcp.json" cursor_config_path = Path.home() / ".cursor" / "mcp.json"
logger.debug("Checking Cursor config at: %s (exists: %s)", cursor_config_path, cursor_config_path.exists())
if cursor_config_path.exists(): if cursor_config_path.exists():
try: try:
with cursor_config_path.open("r") as f: with cursor_config_path.open("r") as f:
cursor_config = json.load(f) cursor_config = json.load(f)
if "mcpServers" in cursor_config and project_server_name in cursor_config["mcpServers"]: if "mcpServers" in cursor_config and project_server_name in cursor_config["mcpServers"]:
logger.debug("Found Cursor config for project server: %s", project_server_name)
results.append("cursor") results.append("cursor")
else:
logger.debug(
"Cursor config exists but no entry for server: %s (available servers: %s)",
project_server_name,
list(cursor_config.get("mcpServers", {}).keys()),
)
except json.JSONDecodeError: except json.JSONDecodeError:
pass logger.warning("Failed to parse Cursor config JSON at: %s", cursor_config_path)
# Check Claude configuration # Check Claude configuration
claude_config_path = None claude_config_path = None
if platform.system() == "Darwin": # macOS os_type = platform.system()
is_wsl = os_type == "Linux" and "microsoft" in platform.uname().release.lower()
if os_type == "Darwin": # macOS
claude_config_path = ( claude_config_path = (
Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json" Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
) )
elif platform.system() == "Windows": elif os_type == "Windows" or is_wsl: # Windows or WSL (Claude runs on Windows host)
if is_wsl:
# In WSL, we need to access the Windows APPDATA directory
try:
# First try to get the Windows username
proc = await create_subprocess_exec(
"/mnt/c/Windows/System32/cmd.exe",
"/c",
"echo %USERNAME%",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode == 0 and stdout.strip():
windows_username = stdout.decode().strip()
claude_config_path = Path(
f"/mnt/c/Users/{windows_username}/AppData/Roaming/Claude/claude_desktop_config.json"
)
else:
# Fallback: try to find the Windows user directory
users_dir = Path("/mnt/c/Users")
if users_dir.exists():
# Get the first non-system user directory
user_dirs = [
d
for d in users_dir.iterdir()
if d.is_dir() and not d.name.startswith(("Default", "Public", "All Users"))
]
if user_dirs:
claude_config_path = (
user_dirs[0] / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json"
)
except (OSError, CalledProcessError) as e:
logger.warning(
"Failed to determine Windows user path in WSL for checking Claude config: %s", str(e)
)
# Don't set claude_config_path, so it will be skipped
else:
# Regular Windows
claude_config_path = Path(os.environ["APPDATA"]) / "Claude" / "claude_desktop_config.json" claude_config_path = Path(os.environ["APPDATA"]) / "Claude" / "claude_desktop_config.json"
if claude_config_path and claude_config_path.exists(): if claude_config_path and claude_config_path.exists():
logger.debug("Checking Claude config at: %s", claude_config_path)
try: try:
with claude_config_path.open("r") as f: with claude_config_path.open("r") as f:
claude_config = json.load(f) claude_config = json.load(f)
if "mcpServers" in claude_config and project_server_name in claude_config["mcpServers"]: if "mcpServers" in claude_config and project_server_name in claude_config["mcpServers"]:
logger.debug("Found Claude config for project server: %s", project_server_name)
results.append("claude") results.append("claude")
else:
logger.debug(
"Claude config exists but no entry for server: %s (available servers: %s)",
project_server_name,
list(claude_config.get("mcpServers", {}).keys()),
)
except json.JSONDecodeError: except json.JSONDecodeError:
pass logger.warning("Failed to parse Claude config JSON at: %s", claude_config_path)
else:
logger.debug("Claude config path not found or doesn't exist: %s", claude_config_path)
except Exception as e: except Exception as e:
msg = f"Error checking MCP configuration: {e!s}" msg = f"Error checking MCP configuration: {e!s}"

View file

@ -409,7 +409,7 @@ const McpServerTab = ({ folderName }: { folderName: string }) => {
{ {
onSuccess: () => { onSuccess: () => {
setSuccessData({ setSuccessData({
title: `MCP Server installed successfully on ${installer.title}`, title: `MCP Server installed successfully on ${installer.title}. You may need to restart your client to see the changes.`,
}); });
setLoadingMCP( setLoadingMCP(
loadingMCP.filter( loadingMCP.filter(