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:
parent
39af1ded5f
commit
b08d080c67
2 changed files with 130 additions and 11 deletions
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue