From 94bbc9873c04a2920fef41b7fedeeb7f056209bb Mon Sep 17 00:00:00 2001 From: dsarno Date: Tue, 29 Jul 2025 13:01:17 -0700 Subject: [PATCH] Improve Windows compatibility and code cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enhanced Windows support for UV and Claude executable detection with OS-specific path handling - Added PowerShell integration for Windows command execution with proper PATH environment setup - Implemented comprehensive UV path scanning for various installation methods (Python, Chocolatey, Scoop, Cargo, etc.) - Added executable validation using IsValidUvInstallation() method - Improved Claude path detection with fallback to PowerShell's Get-Command - Enhanced configuration path handling with Windows-specific paths and cross-platform normalization - Cleaned up exception handling by removing unused exception variables - Fixed method signature in VSCodeManualSetupWindow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../Editor/Helpers/GameObjectSerializer.cs | 8 +- .../Editor/Windows/UnityMcpEditorWindow.cs | 364 +++++++++++++++--- .../Editor/Windows/VSCodeManualSetupWindow.cs | 2 +- 3 files changed, 318 insertions(+), 56 deletions(-) diff --git a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs index 5fc3fce0..aa83c039 100644 --- a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs +++ b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs @@ -360,9 +360,9 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ Type propType = propInfo.PropertyType; AddSerializableValue(serializablePropertiesOutput, propName, propType, value); } - catch (Exception ex) + catch (Exception) { - // Debug.LogWarning($"Could not read property {propName} on {componentType.Name}: {ex.Message}"); + // Debug.LogWarning($"Could not read property {propName} on {componentType.Name}"); } } @@ -383,9 +383,9 @@ public static object GetComponentData(Component c, bool includeNonPublicSerializ Type fieldType = fieldInfo.FieldType; AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value); } - catch (Exception ex) + catch (Exception) { - // Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}: {ex.Message}"); + // Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}"); } } // --- End Use cached metadata --- diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 62c919d8..2cd61b20 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -941,7 +942,7 @@ private void RegisterWithClaudeCode(string pythonDir) } // Try to find uv.exe in common locations - string uvPath = FindWindowsUvPath(); + string uvPath = FindUvPath(); if (string.IsNullOrEmpty(uvPath)) { @@ -966,21 +967,49 @@ private void RegisterWithClaudeCode(string pythonDir) string unityProjectDir = Application.dataPath; string projectDir = Path.GetDirectoryName(unityProjectDir); - var psi = new ProcessStartInfo + var psi = new ProcessStartInfo(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - FileName = command, - Arguments = args, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - WorkingDirectory = projectDir // Set working directory to Unity project directory - }; + // On Windows, run through PowerShell with explicit PATH setting + psi.FileName = "powershell.exe"; + string nodePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "nodejs"); + psi.Arguments = $"-Command \"$env:PATH += ';{nodePath}'; & '{command}' {args}\""; + UnityEngine.Debug.Log($"Executing: powershell.exe {psi.Arguments}"); + } + else + { + psi.FileName = command; + psi.Arguments = args; + UnityEngine.Debug.Log($"Executing: {command} {args}"); + } + + psi.UseShellExecute = false; + psi.RedirectStandardOutput = true; + psi.RedirectStandardError = true; + psi.CreateNoWindow = true; + psi.WorkingDirectory = projectDir; - // Set PATH to include common binary locations + // Set PATH to include common binary locations (OS-specific) string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; - string additionalPaths = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"; - psi.EnvironmentVariables["PATH"] = $"{additionalPaths}:{currentPath}"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Windows: Add common Node.js and npm locations + string[] windowsPaths = { + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "nodejs"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "nodejs"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "npm"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "npm") + }; + string additionalPaths = string.Join(";", windowsPaths); + psi.EnvironmentVariables["PATH"] = $"{currentPath};{additionalPaths}"; + } + else + { + // macOS/Linux: Add common binary locations + string additionalPaths = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"; + psi.EnvironmentVariables["PATH"] = $"{additionalPaths}:{currentPath}"; + } using var process = Process.Start(psi); string output = process.StandardOutput.ReadToEnd(); @@ -996,7 +1025,7 @@ private void RegisterWithClaudeCode(string pythonDir) var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); if (claudeClient != null) { - CheckMcpConfiguration(claudeClient); + CheckClaudeCodeConfiguration(claudeClient); } Repaint(); UnityEngine.Debug.Log("UnityMCP server successfully registered from Claude Code."); @@ -1040,21 +1069,47 @@ private void UnregisterWithClaudeCode() string unityProjectDir = Application.dataPath; string projectDir = Path.GetDirectoryName(unityProjectDir); - var psi = new ProcessStartInfo + var psi = new ProcessStartInfo(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - FileName = command, - Arguments = "mcp remove UnityMCP", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - WorkingDirectory = projectDir // Set working directory to Unity project directory - }; + // On Windows, run through PowerShell with explicit PATH setting + psi.FileName = "powershell.exe"; + string nodePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "nodejs"); + psi.Arguments = $"-Command \"$env:PATH += ';{nodePath}'; & '{command}' mcp remove UnityMCP\""; + } + else + { + psi.FileName = command; + psi.Arguments = "mcp remove UnityMCP"; + } + + psi.UseShellExecute = false; + psi.RedirectStandardOutput = true; + psi.RedirectStandardError = true; + psi.CreateNoWindow = true; + psi.WorkingDirectory = projectDir; - // Set PATH to include common binary locations + // Set PATH to include common binary locations (OS-specific) string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; - string additionalPaths = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"; - psi.EnvironmentVariables["PATH"] = $"{additionalPaths}:{currentPath}"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Windows: Add common Node.js and npm locations + string[] windowsPaths = { + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "nodejs"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "nodejs"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "npm"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "npm") + }; + string additionalPaths = string.Join(";", windowsPaths); + psi.EnvironmentVariables["PATH"] = $"{currentPath};{additionalPaths}"; + } + else + { + // macOS/Linux: Add common binary locations + string additionalPaths = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"; + psi.EnvironmentVariables["PATH"] = $"{additionalPaths}:{currentPath}"; + } using var process = Process.Start(psi); string output = process.StandardOutput.ReadToEnd(); @@ -1068,7 +1123,7 @@ private void UnregisterWithClaudeCode() var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); if (claudeClient != null) { - CheckMcpConfiguration(claudeClient); + CheckClaudeCodeConfiguration(claudeClient); } Repaint(); @@ -1105,7 +1160,7 @@ private string FindUvPath() foreach (string path in possiblePaths) { - if (File.Exists(path)) + if (File.Exists(path) && IsValidUvInstallation(path)) { uvPath = path; break; @@ -1130,7 +1185,7 @@ private string FindUvPath() string output = process.StandardOutput.ReadToEnd().Trim(); process.WaitForExit(); - if (!string.IsNullOrEmpty(output) && File.Exists(output)) + if (!string.IsNullOrEmpty(output) && File.Exists(output) && IsValidUvInstallation(output)) { uvPath = output; } @@ -1142,6 +1197,17 @@ private string FindUvPath() } } + // If no specific path found, fall back to using 'uv' from PATH + if (uvPath == null) + { + // Test if 'uv' is available in PATH by trying to run it + string uvCommand = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "uv.exe" : "uv"; + if (IsValidUvInstallation(uvCommand)) + { + uvPath = uvCommand; + } + } + if (uvPath == null) { UnityEngine.Debug.LogError("UV package manager not found! Please install UV first:\n" + @@ -1154,42 +1220,201 @@ private string FindUvPath() return uvPath; } + private bool IsValidUvInstallation(string uvPath) + { + try + { + var psi = new ProcessStartInfo + { + FileName = uvPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + process.WaitForExit(5000); // 5 second timeout + + if (process.ExitCode == 0) + { + string output = process.StandardOutput.ReadToEnd().Trim(); + // Basic validation - just check if it responds with version info + // UV typically outputs "uv 0.x.x" format + if (output.StartsWith("uv ") && output.Contains(".")) + { + return true; + } + } + + return false; + } + catch + { + return false; + } + } + private string FindWindowsUvPath() { string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + // Dynamic Python version detection - check what's actually installed + List pythonVersions = new List(); + + // Add common versions but also scan for any Python* directories + string[] commonVersions = { "Python313", "Python312", "Python311", "Python310", "Python39", "Python38", "Python37" }; + pythonVersions.AddRange(commonVersions); + + // Scan for additional Python installations + string[] pythonBasePaths = { + Path.Combine(appData, "Python"), + Path.Combine(localAppData, "Programs", "Python"), + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\Python", + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86) + "\\Python" + }; - // Check for different Python versions - string[] pythonVersions = { "Python313", "Python312", "Python311", "Python310", "Python39", "Python38" }; + foreach (string basePath in pythonBasePaths) + { + if (Directory.Exists(basePath)) + { + try + { + foreach (string dir in Directory.GetDirectories(basePath, "Python*")) + { + string versionName = Path.GetFileName(dir); + if (!pythonVersions.Contains(versionName)) + { + pythonVersions.Add(versionName); + } + } + } + catch + { + // Ignore directory access errors + } + } + } + // Check Python installations for UV foreach (string version in pythonVersions) { - string uvPath = Path.Combine(appData, version, "Scripts", "uv.exe"); - if (File.Exists(uvPath)) + string[] pythonPaths = { + Path.Combine(appData, "Python", version, "Scripts", "uv.exe"), + Path.Combine(localAppData, "Programs", "Python", version, "Scripts", "uv.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Python", version, "Scripts", "uv.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Python", version, "Scripts", "uv.exe") + }; + + foreach (string uvPath in pythonPaths) { - return uvPath; + if (File.Exists(uvPath) && IsValidUvInstallation(uvPath)) + { + return uvPath; + } } } - // Check Program Files locations - string[] programFilesPaths = { - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Python"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Python"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "Python") + // Check package manager installations + string[] packageManagerPaths = { + // Chocolatey + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "chocolatey", "lib", "uv", "tools", "uv.exe"), + Path.Combine("C:", "ProgramData", "chocolatey", "lib", "uv", "tools", "uv.exe"), + + // Scoop + Path.Combine(userProfile, "scoop", "apps", "uv", "current", "uv.exe"), + Path.Combine(userProfile, "scoop", "shims", "uv.exe"), + + // Winget/msstore + Path.Combine(localAppData, "Microsoft", "WinGet", "Packages", "astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe", "uv.exe"), + + // Common standalone installations + Path.Combine(localAppData, "uv", "uv.exe"), + Path.Combine(appData, "uv", "uv.exe"), + Path.Combine(userProfile, ".local", "bin", "uv.exe"), + Path.Combine(userProfile, "bin", "uv.exe"), + + // Cargo/Rust installations + Path.Combine(userProfile, ".cargo", "bin", "uv.exe"), + + // Manual installations in common locations + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "uv", "uv.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "uv", "uv.exe") }; - foreach (string basePath in programFilesPaths) + foreach (string uvPath in packageManagerPaths) { - if (Directory.Exists(basePath)) + if (File.Exists(uvPath) && IsValidUvInstallation(uvPath)) + { + return uvPath; + } + } + + // Try to find uv via where command (Windows equivalent of which) + // Use where.exe explicitly to avoid PowerShell alias conflicts + try + { + var psi = new ProcessStartInfo + { + FileName = "where.exe", + Arguments = "uv", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output)) + { + string[] lines = output.Split('\n'); + foreach (string line in lines) + { + string cleanPath = line.Trim(); + if (File.Exists(cleanPath) && IsValidUvInstallation(cleanPath)) + { + return cleanPath; + } + } + } + } + catch + { + // If where.exe fails, try PowerShell's Get-Command as fallback + try { - foreach (string dir in Directory.GetDirectories(basePath, "Python*")) + var psi = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = "-Command \"(Get-Command uv -ErrorAction SilentlyContinue).Source\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) { - string uvPath = Path.Combine(dir, "Scripts", "uv.exe"); - if (File.Exists(uvPath)) + if (IsValidUvInstallation(output)) { - return uvPath; + return output; } } } + catch + { + // Ignore PowerShell errors too + } } return null; // Will fallback to using 'uv' from PATH @@ -1215,15 +1440,16 @@ private string FindClaudeCommand() } } - // Try to find via where command + // Try to find via where command (PowerShell compatible) try { var psi = new ProcessStartInfo { - FileName = "where", + FileName = "where.exe", Arguments = "claude", UseShellExecute = false, RedirectStandardOutput = true, + RedirectStandardError = true, CreateNoWindow = true }; @@ -1231,7 +1457,7 @@ private string FindClaudeCommand() string output = process.StandardOutput.ReadToEnd().Trim(); process.WaitForExit(); - if (!string.IsNullOrEmpty(output)) + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output)) { string[] lines = output.Split('\n'); foreach (string line in lines) @@ -1246,7 +1472,32 @@ private string FindClaudeCommand() } catch { - // Ignore errors and fall back + // If where.exe fails, try PowerShell's Get-Command as fallback + try + { + var psi = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = "-Command \"(Get-Command claude -ErrorAction SilentlyContinue).Source\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) + { + return output; + } + } + catch + { + // Ignore PowerShell errors too + } } return "claude"; // Final fallback to PATH @@ -1266,9 +1517,15 @@ private void CheckClaudeCodeConfiguration(McpClient mcpClient) string projectDir = Path.GetDirectoryName(unityProjectDir); // Read the global Claude config file - string configPath = mcpClient.linuxConfigPath; // ~/.claude.json + string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? mcpClient.windowsConfigPath + : mcpClient.linuxConfigPath; + + UnityEngine.Debug.Log($"Checking Claude config at: {configPath}"); + if (!File.Exists(configPath)) { + UnityEngine.Debug.LogWarning($"Claude config file not found at: {configPath}"); mcpClient.SetStatus(McpStatus.NotConfigured); return; } @@ -1295,7 +1552,12 @@ private void CheckClaudeCodeConfiguration(McpClient mcpClient) foreach (var project in claudeConfig.projects) { string projectPath = project.Name; - if (projectPath == projectDir && project.Value?.mcpServers != null) + + // Normalize paths for comparison (handle forward/back slash differences) + string normalizedProjectPath = Path.GetFullPath(projectPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + string normalizedProjectDir = Path.GetFullPath(projectDir).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + if (string.Equals(normalizedProjectPath, normalizedProjectDir, StringComparison.OrdinalIgnoreCase) && project.Value?.mcpServers != null) { // Check for UnityMCP (case variations) var servers = project.Value.mcpServers; diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs index a0b78e2d..1ee77b5c 100644 --- a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs +++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs @@ -7,7 +7,7 @@ namespace UnityMcpBridge.Editor.Windows { public class VSCodeManualSetupWindow : ManualConfigEditorWindow { - public static new void ShowWindow(string configPath, string configJson) + public static void ShowWindow(string configPath, string configJson) { var window = GetWindow("VSCode GitHub Copilot Setup"); window.configPath = configPath;