diff --git a/UnityMcpBridge/Editor/Dependencies.meta b/UnityMcpBridge/Editor/Dependencies.meta new file mode 100644 index 000000000..77685d170 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 221a4d6e595be6897a5b17b77aedd4d0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs b/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs new file mode 100644 index 000000000..2f7b5ca18 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Dependencies.PlatformDetectors; +using MCPForUnity.Editor.Helpers; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Dependencies +{ + /// + /// Main orchestrator for dependency validation and management + /// + public static class DependencyManager + { + private static readonly List _detectors = new List + { + new WindowsPlatformDetector(), + new MacOSPlatformDetector(), + new LinuxPlatformDetector() + }; + + private static IPlatformDetector _currentDetector; + + /// + /// Get the platform detector for the current operating system + /// + public static IPlatformDetector GetCurrentPlatformDetector() + { + if (_currentDetector == null) + { + _currentDetector = _detectors.FirstOrDefault(d => d.CanDetect); + if (_currentDetector == null) + { + throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}"); + } + } + return _currentDetector; + } + + /// + /// Perform a comprehensive dependency check + /// + public static DependencyCheckResult CheckAllDependencies() + { + var result = new DependencyCheckResult(); + + try + { + var detector = GetCurrentPlatformDetector(); + McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false); + + // Check Python + var pythonStatus = detector.DetectPython(); + result.Dependencies.Add(pythonStatus); + + // Check UV + var uvStatus = detector.DetectUV(); + result.Dependencies.Add(uvStatus); + + // Check MCP Server + var serverStatus = detector.DetectMCPServer(); + result.Dependencies.Add(serverStatus); + + // Generate summary and recommendations + result.GenerateSummary(); + GenerateRecommendations(result, detector); + + McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false); + } + catch (Exception ex) + { + McpLog.Error($"Error during dependency check: {ex.Message}"); + result.Summary = $"Dependency check failed: {ex.Message}"; + result.IsSystemReady = false; + } + + return result; + } + + /// + /// Quick check if system is ready for MCP operations + /// + public static bool IsSystemReady() + { + try + { + var result = CheckAllDependencies(); + return result.IsSystemReady; + } + catch + { + return false; + } + } + + /// + /// Get a summary of missing dependencies + /// + public static string GetMissingDependenciesSummary() + { + try + { + var result = CheckAllDependencies(); + var missing = result.GetMissingRequired(); + + if (missing.Count == 0) + { + return "All required dependencies are available."; + } + + var names = missing.Select(d => d.Name).ToArray(); + return $"Missing required dependencies: {string.Join(", ", names)}"; + } + catch (Exception ex) + { + return $"Error checking dependencies: {ex.Message}"; + } + } + + /// + /// Check if a specific dependency is available + /// + public static bool IsDependencyAvailable(string dependencyName) + { + try + { + var detector = GetCurrentPlatformDetector(); + + return dependencyName.ToLowerInvariant() switch + { + "python" => detector.DetectPython().IsAvailable, + "uv" => detector.DetectUV().IsAvailable, + "mcpserver" or "mcp-server" => detector.DetectMCPServer().IsAvailable, + _ => false + }; + } + catch + { + return false; + } + } + + /// + /// Get installation recommendations for the current platform + /// + public static string GetInstallationRecommendations() + { + try + { + var detector = GetCurrentPlatformDetector(); + return detector.GetInstallationRecommendations(); + } + catch (Exception ex) + { + return $"Error getting installation recommendations: {ex.Message}"; + } + } + + /// + /// Get platform-specific installation URLs + /// + public static (string pythonUrl, string uvUrl) GetInstallationUrls() + { + try + { + var detector = GetCurrentPlatformDetector(); + return (detector.GetPythonInstallUrl(), detector.GetUVInstallUrl()); + } + catch + { + return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/"); + } + } + + /// + /// Validate that the MCP server can be started + /// + public static bool ValidateMCPServerStartup() + { + try + { + // Check if Python and UV are available + if (!IsDependencyAvailable("python") || !IsDependencyAvailable("uv")) + { + return false; + } + + // Try to ensure server is installed + ServerInstaller.EnsureServerInstalled(); + + // Check if server files exist + var serverStatus = GetCurrentPlatformDetector().DetectMCPServer(); + return serverStatus.IsAvailable; + } + catch (Exception ex) + { + McpLog.Error($"Error validating MCP server startup: {ex.Message}"); + return false; + } + } + + /// + /// Attempt to repair the Python environment + /// + public static bool RepairPythonEnvironment() + { + try + { + McpLog.Info("Attempting to repair Python environment..."); + return ServerInstaller.RepairPythonEnvironment(); + } + catch (Exception ex) + { + McpLog.Error($"Error repairing Python environment: {ex.Message}"); + return false; + } + } + + /// + /// Get detailed dependency information for diagnostics + /// + public static string GetDependencyDiagnostics() + { + try + { + var result = CheckAllDependencies(); + var detector = GetCurrentPlatformDetector(); + + var diagnostics = new System.Text.StringBuilder(); + diagnostics.AppendLine($"Platform: {detector.PlatformName}"); + diagnostics.AppendLine($"Check Time: {result.CheckedAt:yyyy-MM-dd HH:mm:ss} UTC"); + diagnostics.AppendLine($"System Ready: {result.IsSystemReady}"); + diagnostics.AppendLine(); + + foreach (var dep in result.Dependencies) + { + diagnostics.AppendLine($"=== {dep.Name} ==="); + diagnostics.AppendLine($"Available: {dep.IsAvailable}"); + diagnostics.AppendLine($"Required: {dep.IsRequired}"); + + if (!string.IsNullOrEmpty(dep.Version)) + diagnostics.AppendLine($"Version: {dep.Version}"); + + if (!string.IsNullOrEmpty(dep.Path)) + diagnostics.AppendLine($"Path: {dep.Path}"); + + if (!string.IsNullOrEmpty(dep.Details)) + diagnostics.AppendLine($"Details: {dep.Details}"); + + if (!string.IsNullOrEmpty(dep.ErrorMessage)) + diagnostics.AppendLine($"Error: {dep.ErrorMessage}"); + + diagnostics.AppendLine(); + } + + if (result.RecommendedActions.Count > 0) + { + diagnostics.AppendLine("=== Recommended Actions ==="); + foreach (var action in result.RecommendedActions) + { + diagnostics.AppendLine($"- {action}"); + } + } + + return diagnostics.ToString(); + } + catch (Exception ex) + { + return $"Error generating diagnostics: {ex.Message}"; + } + } + + private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector) + { + var missing = result.GetMissingDependencies(); + + if (missing.Count == 0) + { + result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity."); + return; + } + + foreach (var dep in missing) + { + if (dep.Name == "Python") + { + result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}"); + } + else if (dep.Name == "UV Package Manager") + { + result.RecommendedActions.Add($"Install UV package manager from: {detector.GetUVInstallUrl()}"); + } + else if (dep.Name == "MCP Server") + { + result.RecommendedActions.Add("MCP Server will be installed automatically when needed."); + } + } + + if (result.GetMissingRequired().Count > 0) + { + result.RecommendedActions.Add("Use the Setup Wizard (Window > MCP for Unity > Setup Wizard) for guided installation."); + } + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs.meta b/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs.meta new file mode 100644 index 000000000..ae03260ae --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/DependencyManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6789012345678901234abcdef012345 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/Models.meta b/UnityMcpBridge/Editor/Dependencies/Models.meta new file mode 100644 index 000000000..2174dd520 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/Models.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b2c3d4e5f6789012345678901234abcd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs b/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs new file mode 100644 index 000000000..5dd2edafb --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MCPForUnity.Editor.Dependencies.Models +{ + /// + /// Result of a comprehensive dependency check + /// + [Serializable] + public class DependencyCheckResult + { + /// + /// List of all dependency statuses checked + /// + public List Dependencies { get; set; } + + /// + /// Overall system readiness for MCP operations + /// + public bool IsSystemReady { get; set; } + + /// + /// Whether all required dependencies are available + /// + public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false; + + /// + /// Whether any optional dependencies are missing + /// + public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false; + + /// + /// Summary message about the dependency state + /// + public string Summary { get; set; } + + /// + /// Recommended next steps for the user + /// + public List RecommendedActions { get; set; } + + /// + /// Timestamp when this check was performed + /// + public DateTime CheckedAt { get; set; } + + public DependencyCheckResult() + { + Dependencies = new List(); + RecommendedActions = new List(); + CheckedAt = DateTime.UtcNow; + } + + /// + /// Get dependencies by availability status + /// + public List GetMissingDependencies() + { + return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List(); + } + + /// + /// Get missing required dependencies + /// + public List GetMissingRequired() + { + return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List(); + } + + /// + /// Generate a user-friendly summary of the dependency state + /// + public void GenerateSummary() + { + var missing = GetMissingDependencies(); + var missingRequired = GetMissingRequired(); + + if (missing.Count == 0) + { + Summary = "All dependencies are available and ready."; + IsSystemReady = true; + } + else if (missingRequired.Count == 0) + { + Summary = $"System is ready. {missing.Count} optional dependencies are missing."; + IsSystemReady = true; + } + else + { + Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing."; + IsSystemReady = false; + } + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs.meta b/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs.meta new file mode 100644 index 000000000..a88c3bb2f --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/Models/DependencyCheckResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 789012345678901234abcdef01234567 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs b/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs new file mode 100644 index 000000000..e755ecadf --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs @@ -0,0 +1,65 @@ +using System; + +namespace MCPForUnity.Editor.Dependencies.Models +{ + /// + /// Represents the status of a dependency check + /// + [Serializable] + public class DependencyStatus + { + /// + /// Name of the dependency being checked + /// + public string Name { get; set; } + + /// + /// Whether the dependency is available and functional + /// + public bool IsAvailable { get; set; } + + /// + /// Version information if available + /// + public string Version { get; set; } + + /// + /// Path to the dependency executable/installation + /// + public string Path { get; set; } + + /// + /// Additional details about the dependency status + /// + public string Details { get; set; } + + /// + /// Error message if dependency check failed + /// + public string ErrorMessage { get; set; } + + /// + /// Whether this dependency is required for basic functionality + /// + public bool IsRequired { get; set; } + + /// + /// Suggested installation method or URL + /// + public string InstallationHint { get; set; } + + public DependencyStatus(string name, bool isRequired = true) + { + Name = name; + IsRequired = isRequired; + IsAvailable = false; + } + + public override string ToString() + { + var status = IsAvailable ? "✓" : "✗"; + var version = !string.IsNullOrEmpty(Version) ? $" ({Version})" : ""; + return $"{status} {Name}{version}"; + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs.meta b/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs.meta new file mode 100644 index 000000000..d6eb1d59c --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/Models/DependencyStatus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6789012345678901234abcdef0123456 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors.meta new file mode 100644 index 000000000..22a6b1db6 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c3d4e5f6789012345678901234abcdef +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs new file mode 100644 index 000000000..7fba58f92 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs @@ -0,0 +1,50 @@ +using MCPForUnity.Editor.Dependencies.Models; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Interface for platform-specific dependency detection + /// + public interface IPlatformDetector + { + /// + /// Platform name this detector handles + /// + string PlatformName { get; } + + /// + /// Whether this detector can run on the current platform + /// + bool CanDetect { get; } + + /// + /// Detect Python installation on this platform + /// + DependencyStatus DetectPython(); + + /// + /// Detect UV package manager on this platform + /// + DependencyStatus DetectUV(); + + /// + /// Detect MCP server installation on this platform + /// + DependencyStatus DetectMCPServer(); + + /// + /// Get platform-specific installation recommendations + /// + string GetInstallationRecommendations(); + + /// + /// Get platform-specific Python installation URL + /// + string GetPythonInstallUrl(); + + /// + /// Get platform-specific UV installation URL + /// + string GetUVInstallUrl(); + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta new file mode 100644 index 000000000..d2cd9f072 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9012345678901234abcdef0123456789 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs new file mode 100644 index 000000000..09fded140 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs @@ -0,0 +1,253 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Linux-specific dependency detection + /// + public class LinuxPlatformDetector : PlatformDetectorBase + { + public override string PlatformName => "Linux"; + + public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + public override DependencyStatus DetectPython() + { + var status = new DependencyStatus("Python", isRequired: true) + { + InstallationHint = GetPythonInstallUrl() + }; + + try + { + // Check common Python installation paths on Linux + var candidates = new[] + { + "python3", + "python", + "/usr/bin/python3", + "/usr/local/bin/python3", + "/opt/python/bin/python3", + "/snap/bin/python3" + }; + + foreach (var candidate in candidates) + { + if (TryValidatePython(candidate, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} at {fullPath}"; + return status; + } + } + + // Try PATH resolution using 'which' command + if (TryFindInPath("python3", out string pathResult) || + TryFindInPath("python", out pathResult)) + { + if (TryValidatePython(pathResult, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} in PATH at {fullPath}"; + return status; + } + } + + status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.Details = "Checked common installation paths including system, snap, and user-local locations."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting Python: {ex.Message}"; + } + + return status; + } + + public override string GetPythonInstallUrl() + { + return "https://www.python.org/downloads/source/"; + } + + public override string GetUVInstallUrl() + { + return "https://docs.astral.sh/uv/getting-started/installation/#linux"; + } + + public override string GetInstallationRecommendations() + { + return @"Linux Installation Recommendations: + +1. Python: Install via package manager or pyenv + - Ubuntu/Debian: sudo apt install python3 python3-pip + - Fedora/RHEL: sudo dnf install python3 python3-pip + - Arch: sudo pacman -S python python-pip + - Or use pyenv: https://github.com/pyenv/pyenv + +2. UV Package Manager: Install via curl + - Run: curl -LsSf https://astral.sh/uv/install.sh | sh + - Or download from: https://github.com/astral-sh/uv/releases + +3. MCP Server: Will be installed automatically by Unity MCP Bridge + +Note: Make sure ~/.local/bin is in your PATH for user-local installations."; + } + + private bool TryValidatePython(string pythonPath, out string version, out string fullPath) + { + version = null; + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = pythonPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Set PATH to include common locations + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/usr/local/bin", + "/usr/bin", + "/bin", + "/snap/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("Python ")) + { + version = output.Substring(7); // Remove "Python " prefix + fullPath = pythonPath; + + // Validate minimum version (Python 4+ or Python 3.10+) + if (TryParseVersion(version, out var major, out var minor)) + { + return major > 3 || (major >= 3 && minor >= 10); + } + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryValidateUV(string uvPath, out string version) + { + version = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = uvPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("uv ")) + { + version = output.Substring(3); // Remove "uv " prefix + return true; + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryFindInPath(string executable, out string fullPath) + { + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = "/usr/bin/which", + Arguments = executable, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Enhance PATH for Unity's GUI environment + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/usr/local/bin", + "/usr/bin", + "/bin", + "/snap/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(3000); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) + { + fullPath = output; + return true; + } + } + catch + { + // Ignore errors + } + + return false; + } + + private bool TryParseVersion(string version, out int major, out int minor) + { + return base.TryParseVersion(version, out major, out minor); + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta new file mode 100644 index 000000000..4f8267fdf --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2345678901234abcdef0123456789abc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs new file mode 100644 index 000000000..715338ce8 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs @@ -0,0 +1,253 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// macOS-specific dependency detection + /// + public class MacOSPlatformDetector : PlatformDetectorBase + { + public override string PlatformName => "macOS"; + + public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + + public override DependencyStatus DetectPython() + { + var status = new DependencyStatus("Python", isRequired: true) + { + InstallationHint = GetPythonInstallUrl() + }; + + try + { + // Check common Python installation paths on macOS + var candidates = new[] + { + "python3", + "python", + "/usr/bin/python3", + "/usr/local/bin/python3", + "/opt/homebrew/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.11/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.10/bin/python3" + }; + + foreach (var candidate in candidates) + { + if (TryValidatePython(candidate, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} at {fullPath}"; + return status; + } + } + + // Try PATH resolution using 'which' command + if (TryFindInPath("python3", out string pathResult) || + TryFindInPath("python", out pathResult)) + { + if (TryValidatePython(pathResult, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} in PATH at {fullPath}"; + return status; + } + } + + status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.Details = "Checked common installation paths including Homebrew, Framework, and system locations."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting Python: {ex.Message}"; + } + + return status; + } + + public override string GetPythonInstallUrl() + { + return "https://www.python.org/downloads/macos/"; + } + + public override string GetUVInstallUrl() + { + return "https://docs.astral.sh/uv/getting-started/installation/#macos"; + } + + public override string GetInstallationRecommendations() + { + return @"macOS Installation Recommendations: + +1. Python: Install via Homebrew (recommended) or python.org + - Homebrew: brew install python3 + - Direct download: https://python.org/downloads/macos/ + +2. UV Package Manager: Install via curl or Homebrew + - Curl: curl -LsSf https://astral.sh/uv/install.sh | sh + - Homebrew: brew install uv + +3. MCP Server: Will be installed automatically by Unity MCP Bridge + +Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH."; + } + + private bool TryValidatePython(string pythonPath, out string version, out string fullPath) + { + version = null; + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = pythonPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Set PATH to include common locations + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("Python ")) + { + version = output.Substring(7); // Remove "Python " prefix + fullPath = pythonPath; + + // Validate minimum version (Python 4+ or Python 3.10+) + if (TryParseVersion(version, out var major, out var minor)) + { + return major > 3 || (major >= 3 && minor >= 10); + } + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryValidateUV(string uvPath, out string version) + { + version = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = uvPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("uv ")) + { + version = output.Substring(3); // Remove "uv " prefix + return true; + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryFindInPath(string executable, out string fullPath) + { + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = "/usr/bin/which", + Arguments = executable, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Enhance PATH for Unity's GUI environment + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pathAdditions = new[] + { + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + Path.Combine(homeDir, ".local", "bin") + }; + + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(3000); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) + { + fullPath = output; + return true; + } + } + catch + { + // Ignore errors + } + + return false; + } + + private bool TryParseVersion(string version, out int major, out int minor) + { + return base.TryParseVersion(version, out major, out minor); + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta new file mode 100644 index 000000000..b43864a2e --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 12345678901234abcdef0123456789ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs new file mode 100644 index 000000000..98044f17e --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs @@ -0,0 +1,161 @@ +using System; +using System.Diagnostics; +using System.IO; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Base class for platform-specific dependency detection + /// + public abstract class PlatformDetectorBase : IPlatformDetector + { + public abstract string PlatformName { get; } + public abstract bool CanDetect { get; } + + public abstract DependencyStatus DetectPython(); + public abstract string GetPythonInstallUrl(); + public abstract string GetUVInstallUrl(); + public abstract string GetInstallationRecommendations(); + + public virtual DependencyStatus DetectUV() + { + var status = new DependencyStatus("UV Package Manager", isRequired: true) + { + InstallationHint = GetUVInstallUrl() + }; + + try + { + // Use existing UV detection from ServerInstaller + string uvPath = ServerInstaller.FindUvPath(); + if (!string.IsNullOrEmpty(uvPath)) + { + if (TryValidateUV(uvPath, out string version)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = uvPath; + status.Details = $"Found UV {version} at {uvPath}"; + return status; + } + } + + status.ErrorMessage = "UV package manager not found. Please install UV."; + status.Details = "UV is required for managing Python dependencies."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting UV: {ex.Message}"; + } + + return status; + } + + public virtual DependencyStatus DetectMCPServer() + { + var status = new DependencyStatus("MCP Server", isRequired: false); + + try + { + // Check if server is installed + string serverPath = ServerInstaller.GetServerPath(); + string serverPy = Path.Combine(serverPath, "server.py"); + + if (File.Exists(serverPy)) + { + status.IsAvailable = true; + status.Path = serverPath; + + // Try to get version + string versionFile = Path.Combine(serverPath, "server_version.txt"); + if (File.Exists(versionFile)) + { + status.Version = File.ReadAllText(versionFile).Trim(); + } + + status.Details = $"MCP Server found at {serverPath}"; + } + else + { + // Check for embedded server + if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath)) + { + status.IsAvailable = true; + status.Path = embeddedPath; + status.Details = "MCP Server available (embedded in package)"; + } + else + { + status.ErrorMessage = "MCP Server not found"; + status.Details = "Server will be installed automatically when needed"; + } + } + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}"; + } + + return status; + } + + protected bool TryValidateUV(string uvPath, out string version) + { + version = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = uvPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("uv ")) + { + version = output.Substring(3); // Remove "uv " prefix + return true; + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + protected bool TryParseVersion(string version, out int major, out int minor) + { + major = 0; + minor = 0; + + try + { + var parts = version.Split('.'); + if (parts.Length >= 2) + { + return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor); + } + } + catch + { + // Ignore parsing errors + } + + return false; + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta new file mode 100644 index 000000000..4821e7576 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 44d715aedea2b8b41bf914433bbb2c49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs new file mode 100644 index 000000000..ea57d5ef9 --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs @@ -0,0 +1,232 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Dependencies.PlatformDetectors +{ + /// + /// Windows-specific dependency detection + /// + public class WindowsPlatformDetector : PlatformDetectorBase + { + public override string PlatformName => "Windows"; + + public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + public override DependencyStatus DetectPython() + { + var status = new DependencyStatus("Python", isRequired: true) + { + InstallationHint = GetPythonInstallUrl() + }; + + try + { + // Check common Python installation paths + var candidates = new[] + { + "python.exe", + "python3.exe", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Programs", "Python", "Python313", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Programs", "Python", "Python312", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Programs", "Python", "Python311", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "Python313", "python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "Python312", "python.exe") + }; + + foreach (var candidate in candidates) + { + if (TryValidatePython(candidate, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} at {fullPath}"; + return status; + } + } + + // Try PATH resolution using 'where' command + if (TryFindInPath("python.exe", out string pathResult) || + TryFindInPath("python3.exe", out pathResult)) + { + if (TryValidatePython(pathResult, out string version, out string fullPath)) + { + status.IsAvailable = true; + status.Version = version; + status.Path = fullPath; + status.Details = $"Found Python {version} in PATH at {fullPath}"; + return status; + } + } + + status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.Details = "Checked common installation paths and PATH environment variable."; + } + catch (Exception ex) + { + status.ErrorMessage = $"Error detecting Python: {ex.Message}"; + } + + return status; + } + + public override string GetPythonInstallUrl() + { + return "https://apps.microsoft.com/store/detail/python-313/9NCVDN91XZQP"; + } + + public override string GetUVInstallUrl() + { + return "https://docs.astral.sh/uv/getting-started/installation/#windows"; + } + + public override string GetInstallationRecommendations() + { + return @"Windows Installation Recommendations: + +1. Python: Install from Microsoft Store or python.org + - Microsoft Store: Search for 'Python 3.12' or 'Python 3.13' + - Direct download: https://python.org/downloads/windows/ + +2. UV Package Manager: Install via PowerShell + - Run: powershell -ExecutionPolicy ByPass -c ""irm https://astral.sh/uv/install.ps1 | iex"" + - Or download from: https://github.com/astral-sh/uv/releases + +3. MCP Server: Will be installed automatically by Unity MCP Bridge"; + } + + private bool TryValidatePython(string pythonPath, out string version, out string fullPath) + { + version = null; + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = pythonPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("Python ")) + { + version = output.Substring(7); // Remove "Python " prefix + fullPath = pythonPath; + + // Validate minimum version (Python 4+ or Python 3.10+) + if (TryParseVersion(version, out var major, out var minor)) + { + return major > 3 || (major >= 3 && minor >= 10); + } + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryValidateUV(string uvPath, out string version) + { + version = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = uvPath, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (process.ExitCode == 0 && output.StartsWith("uv ")) + { + version = output.Substring(3); // Remove "uv " prefix + return true; + } + } + catch + { + // Ignore validation errors + } + + return false; + } + + private bool TryFindInPath(string executable, out string fullPath) + { + fullPath = null; + + try + { + var psi = new ProcessStartInfo + { + FileName = "where", + Arguments = executable, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) return false; + + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(3000); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output)) + { + // Take the first result + var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + if (lines.Length > 0) + { + fullPath = lines[0].Trim(); + return File.Exists(fullPath); + } + } + } + catch + { + // Ignore errors + } + + return false; + } + + private bool TryParseVersion(string version, out int major, out int minor) + { + return base.TryParseVersion(version, out major, out minor); + } + } +} diff --git a/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta new file mode 100644 index 000000000..e7e53d7de --- /dev/null +++ b/UnityMcpBridge/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 012345678901234abcdef0123456789a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs b/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs new file mode 100644 index 000000000..8e727efb1 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs @@ -0,0 +1,297 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Editor.Dependencies; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Shared helper for MCP client configuration management with sophisticated + /// logic for preserving existing configs and handling different client types + /// + public static class McpConfigurationHelper + { + private const string LOCK_CONFIG_KEY = "MCPForUnity.LockCursorConfig"; + + /// + /// Writes MCP configuration to the specified path using sophisticated logic + /// that preserves existing configuration and only writes when necessary + /// + public static string WriteMcpConfiguration(string pythonDir, string configPath, McpClient mcpClient = null) + { + // 0) Respect explicit lock (hidden pref or UI toggle) + try + { + if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false)) + return "Skipped (locked)"; + } + catch { } + + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + + // Read existing config if it exists + string existingJson = "{}"; + if (File.Exists(configPath)) + { + try + { + existingJson = File.ReadAllText(configPath); + } + catch (Exception e) + { + Debug.LogWarning($"Error reading existing config: {e.Message}."); + } + } + + // Parse the existing JSON while preserving all properties + dynamic existingConfig; + try + { + if (string.IsNullOrWhiteSpace(existingJson)) + { + existingConfig = new JObject(); + } + else + { + existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new JObject(); + } + } + catch + { + // If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object + if (!string.IsNullOrWhiteSpace(existingJson)) + { + Debug.LogWarning("UnityMCP: Configuration file could not be parsed; rewriting server block."); + } + existingConfig = new JObject(); + } + + // Determine existing entry references (command/args) + string existingCommand = null; + string[] existingArgs = null; + bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode); + try + { + if (isVSCode) + { + existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString(); + existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject(); + } + else + { + existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString(); + existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject(); + } + } + catch { } + + // 1) Start from existing, only fill gaps (prefer trusted resolver) + string uvPath = ServerInstaller.FindUvPath(); + // Optionally trust existingCommand if it looks like uv/uv.exe + try + { + var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); + if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand)) + { + uvPath = existingCommand; + } + } + catch { } + if (uvPath == null) return "UV package manager not found. Please install UV first."; + string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); + + // 2) Canonical args order + var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; + + // 3) Only write if changed + bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) + || !ArgsEqual(existingArgs, newArgs); + if (!changed) + { + return "Configured successfully"; // nothing to do + } + + // 4) Ensure containers exist and write back minimal changes + JObject existingRoot; + if (existingConfig is JObject eo) + existingRoot = eo; + else + existingRoot = JObject.FromObject(existingConfig); + + existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient); + + string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); + + McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson); + + try + { + if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); + EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); + } + catch { } + + return "Configured successfully"; + } + + /// + /// Configures a Codex client with sophisticated TOML handling + /// + public static string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient) + { + try + { + if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false)) + return "Skipped (locked)"; + } + catch { } + + string existingToml = string.Empty; + if (File.Exists(configPath)) + { + try + { + existingToml = File.ReadAllText(configPath); + } + catch (Exception e) + { + Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}"); + existingToml = string.Empty; + } + } + + string existingCommand = null; + string[] existingArgs = null; + if (!string.IsNullOrWhiteSpace(existingToml)) + { + CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs); + } + + string uvPath = ServerInstaller.FindUvPath(); + try + { + var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); + if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand)) + { + uvPath = existingCommand; + } + } + catch { } + + if (uvPath == null) + { + return "UV package manager not found. Please install UV first."; + } + + string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); + var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; + + bool changed = true; + if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null) + { + changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) + || !ArgsEqual(existingArgs, newArgs); + } + + if (!changed) + { + return "Configured successfully"; + } + + string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc); + string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock); + + McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml); + + try + { + if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); + EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); + } + catch { } + + return "Configured successfully"; + } + + /// + /// Validates UV binary by running --version command + /// + private static bool IsValidUvBinary(string path) + { + try + { + if (!File.Exists(path)) return false; + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = path, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var p = System.Diagnostics.Process.Start(psi); + if (p == null) return false; + if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; } + if (p.ExitCode != 0) return false; + string output = p.StandardOutput.ReadToEnd().Trim(); + return output.StartsWith("uv "); + } + catch { return false; } + } + + /// + /// Compares two string arrays for equality + /// + private static bool ArgsEqual(string[] a, string[] b) + { + if (a == null || b == null) return a == b; + if (a.Length != b.Length) return false; + for (int i = 0; i < a.Length; i++) + { + if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false; + } + return true; + } + + /// + /// Gets the appropriate config file path for the given MCP client based on OS + /// + public static string GetClientConfigPath(McpClient mcpClient) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return mcpClient.windowsConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return string.IsNullOrEmpty(mcpClient.macConfigPath) + ? mcpClient.linuxConfigPath + : mcpClient.macConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return mcpClient.linuxConfigPath; + } + else + { + return mcpClient.linuxConfigPath; // fallback + } + } + + /// + /// Creates the directory for the config file if it doesn't exist + /// + public static void EnsureConfigDirectoryExists(string configPath) + { + Directory.CreateDirectory(Path.GetDirectoryName(configPath)); + } + } +} diff --git a/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs.meta b/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs.meta new file mode 100644 index 000000000..17de56c8f --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e45ac2a13b4c1ba468b8e3aa67b292ca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs b/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs new file mode 100644 index 000000000..8e6839658 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using UnityEngine; +using UnityEditor; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Shared helper for resolving Python server directory paths with support for + /// development mode, embedded servers, and installed packages + /// + public static class McpPathResolver + { + private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer"; + + /// + /// Resolves the Python server directory path with comprehensive logic + /// including development mode support and fallback mechanisms + /// + public static string FindPackagePythonDirectory(bool debugLogsEnabled = false) + { + string pythonDir = McpConfigFileHelper.ResolveServerSource(); + + try + { + // Only check dev paths if we're using a file-based package (development mode) + bool isDevelopmentMode = IsDevelopmentMode(); + if (isDevelopmentMode) + { + string currentPackagePath = Path.GetDirectoryName(Application.dataPath); + string[] devPaths = { + Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"), + Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"), + }; + + foreach (string devPath in devPaths) + { + if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py"))) + { + if (debugLogsEnabled) + { + Debug.Log($"Currently in development mode. Package: {devPath}"); + } + return devPath; + } + } + } + + // Resolve via shared helper (handles local registry and older fallback) only if dev override on + if (EditorPrefs.GetBool(USE_EMBEDDED_SERVER_KEY, false)) + { + if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)) + { + return embedded; + } + } + + // Log only if the resolved path does not actually contain server.py + if (debugLogsEnabled) + { + bool hasServer = false; + try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { } + if (!hasServer) + { + Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path"); + } + } + } + catch (Exception e) + { + Debug.LogError($"Error finding package path: {e.Message}"); + } + + return pythonDir; + } + + /// + /// Checks if the current Unity project is in development mode + /// (i.e., the package is referenced as a local file path in manifest.json) + /// + private static bool IsDevelopmentMode() + { + try + { + // Only treat as development if manifest explicitly references a local file path for the package + string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json"); + if (!File.Exists(manifestPath)) return false; + + string manifestContent = File.ReadAllText(manifestPath); + // Look specifically for our package dependency set to a file: URL + // This avoids auto-enabling dev mode just because a repo exists elsewhere on disk + if (manifestContent.IndexOf("\"com.coplaydev.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0) + { + int idx = manifestContent.IndexOf("com.coplaydev.unity-mcp", StringComparison.OrdinalIgnoreCase); + // Crude but effective: check for "file:" in the same line/value + if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0 + && manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + catch + { + return false; + } + } + + /// + /// Gets the appropriate PATH prepend for the current platform when running external processes + /// + public static string GetPathPrepend() + { + if (Application.platform == RuntimePlatform.OSXEditor) + return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; + else if (Application.platform == RuntimePlatform.LinuxEditor) + return "/usr/local/bin:/usr/bin:/bin"; + return null; + } + } +} diff --git a/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs.meta b/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs.meta new file mode 100644 index 000000000..38f19973a --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/McpPathResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c76f0c7ff138ba4a952481e04bc3974 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs b/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs index 795256a78..031a6aedc 100644 --- a/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/PackageInstaller.cs @@ -36,7 +36,7 @@ private static void InstallServerOnFirstLoad() catch (System.Exception ex) { Debug.LogError($"MCP-FOR-UNITY: Failed to install Python server: {ex.Message}"); - Debug.LogWarning("MCP-FOR-UNITY: You may need to manually install the Python server. Check the MCP for Unity Editor Window for instructions."); + Debug.LogWarning("MCP-FOR-UNITY: You may need to manually install the Python server. Check the MCP For Unity Window for instructions."); } } } diff --git a/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs b/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs index 1342dc121..0e4629458 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs @@ -12,7 +12,7 @@ public static class ServerPathResolver /// or common development locations. Returns true if found and sets srcPath to the folder /// containing server.py. /// - public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLegacyPackageId = true) + public static bool TryFindEmbeddedServerSource(out string srcPath) { // 1) Repo development layouts commonly used alongside this package try @@ -43,7 +43,7 @@ public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLe var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly); if (owner != null) { - if (TryResolveWithinPackage(owner, out srcPath, warnOnLegacyPackageId)) + if (TryResolveWithinPackage(owner, out srcPath)) { return true; } @@ -52,7 +52,7 @@ public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLe // Secondary: scan all registered packages locally foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages()) { - if (TryResolveWithinPackage(p, out srcPath, warnOnLegacyPackageId)) + if (TryResolveWithinPackage(p, out srcPath)) { return true; } @@ -65,7 +65,7 @@ public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLe { foreach (var pkg in list.Result) { - if (TryResolveWithinPackage(pkg, out srcPath, warnOnLegacyPackageId)) + if (TryResolveWithinPackage(pkg, out srcPath)) { return true; } @@ -99,24 +99,16 @@ public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLe return false; } - private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath, bool warnOnLegacyPackageId) + private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath) { const string CurrentId = "com.coplaydev.unity-mcp"; - const string LegacyId = "com.justinpbarnett.unity-mcp"; srcPath = null; - if (p == null || (p.name != CurrentId && p.name != LegacyId)) + if (p == null || p.name != CurrentId) { return false; } - if (warnOnLegacyPackageId && p.name == LegacyId) - { - Debug.LogWarning( - "MCP for Unity: Detected legacy package id 'com.justinpbarnett.unity-mcp'. " + - "Please update Packages/manifest.json to 'com.coplaydev.unity-mcp' to avoid future breakage."); - } - string packagePath = p.resolvedPath; // Preferred tilde folder (embedded but excluded from import) diff --git a/UnityMcpBridge/Editor/Setup.meta b/UnityMcpBridge/Editor/Setup.meta new file mode 100644 index 000000000..1157b1e98 --- /dev/null +++ b/UnityMcpBridge/Editor/Setup.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 600c9cb20c329d761bfa799158a87bac +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Setup/SetupWizard.cs b/UnityMcpBridge/Editor/Setup/SetupWizard.cs new file mode 100644 index 000000000..a97926ea5 --- /dev/null +++ b/UnityMcpBridge/Editor/Setup/SetupWizard.cs @@ -0,0 +1,150 @@ +using System; +using MCPForUnity.Editor.Dependencies; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Windows; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Setup +{ + /// + /// Handles automatic triggering of the setup wizard + /// + [InitializeOnLoad] + public static class SetupWizard + { + private const string SETUP_COMPLETED_KEY = "MCPForUnity.SetupCompleted"; + private const string SETUP_DISMISSED_KEY = "MCPForUnity.SetupDismissed"; + private static bool _hasCheckedThisSession = false; + + static SetupWizard() + { + // Skip in batch mode + if (Application.isBatchMode) + return; + + // Show setup wizard on package import + EditorApplication.delayCall += CheckSetupNeeded; + } + + /// + /// Check if setup wizard should be shown + /// + private static void CheckSetupNeeded() + { + if (_hasCheckedThisSession) + return; + + _hasCheckedThisSession = true; + + try + { + // Check if setup was already completed or dismissed in previous sessions + bool setupCompleted = EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false); + bool setupDismissed = EditorPrefs.GetBool(SETUP_DISMISSED_KEY, false); + + // Only show setup wizard if it hasn't been completed or dismissed before + if (!(setupCompleted || setupDismissed)) + { + McpLog.Info("Package imported - showing setup wizard", always: false); + + var dependencyResult = DependencyManager.CheckAllDependencies(); + EditorApplication.delayCall += () => ShowSetupWizard(dependencyResult); + } + else + { + McpLog.Info("Setup wizard skipped - previously completed or dismissed", always: false); + } + } + catch (Exception ex) + { + McpLog.Error($"Error checking setup status: {ex.Message}"); + } + } + + /// + /// Show the setup wizard window + /// + public static void ShowSetupWizard(DependencyCheckResult dependencyResult = null) + { + try + { + dependencyResult ??= DependencyManager.CheckAllDependencies(); + SetupWizardWindow.ShowWindow(dependencyResult); + } + catch (Exception ex) + { + McpLog.Error($"Error showing setup wizard: {ex.Message}"); + } + } + + /// + /// Mark setup as completed + /// + public static void MarkSetupCompleted() + { + EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true); + McpLog.Info("Setup marked as completed"); + } + + /// + /// Mark setup as dismissed + /// + public static void MarkSetupDismissed() + { + EditorPrefs.SetBool(SETUP_DISMISSED_KEY, true); + McpLog.Info("Setup marked as dismissed"); + } + + /// + /// Force show setup wizard (for manual invocation) + /// + [MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)] + public static void ShowSetupWizardManual() + { + ShowSetupWizard(); + } + + /// + /// Check dependencies and show status + /// + [MenuItem("Window/MCP For Unity/Check Dependencies", priority = 3)] + public static void CheckDependencies() + { + var result = DependencyManager.CheckAllDependencies(); + + if (!result.IsSystemReady) + { + bool showWizard = EditorUtility.DisplayDialog( + "MCP for Unity - Dependencies", + $"System Status: {result.Summary}\n\nWould you like to open the Setup Wizard?", + "Open Setup Wizard", + "Close" + ); + + if (showWizard) + { + ShowSetupWizard(result); + } + } + else + { + EditorUtility.DisplayDialog( + "MCP for Unity - Dependencies", + "✓ All dependencies are available and ready!\n\nMCP for Unity is ready to use.", + "OK" + ); + } + } + + /// + /// Open MCP Client Configuration window + /// + [MenuItem("Window/MCP For Unity/Open MCP Window", priority = 4)] + public static void OpenClientConfiguration() + { + Windows.MCPForUnityEditorWindow.ShowWindow(); + } + } +} diff --git a/UnityMcpBridge/Editor/Setup/SetupWizard.cs.meta b/UnityMcpBridge/Editor/Setup/SetupWizard.cs.meta new file mode 100644 index 000000000..1a0e4e5fc --- /dev/null +++ b/UnityMcpBridge/Editor/Setup/SetupWizard.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 345678901234abcdef0123456789abcd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs b/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs new file mode 100644 index 000000000..7229be979 --- /dev/null +++ b/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs @@ -0,0 +1,726 @@ +using System; +using System.Linq; +using MCPForUnity.Editor.Data; +using MCPForUnity.Editor.Dependencies; +using MCPForUnity.Editor.Dependencies.Models; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Setup +{ + /// + /// Setup wizard window for guiding users through dependency installation + /// + public class SetupWizardWindow : EditorWindow + { + private DependencyCheckResult _dependencyResult; + private Vector2 _scrollPosition; + private int _currentStep = 0; + private McpClients _mcpClients; + private int _selectedClientIndex = 0; + + private readonly string[] _stepTitles = { + "Setup", + "Configure", + "Complete" + }; + + public static void ShowWindow(DependencyCheckResult dependencyResult = null) + { + var window = GetWindow("MCP for Unity Setup"); + window.minSize = new Vector2(500, 400); + window.maxSize = new Vector2(800, 600); + window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies(); + window.Show(); + } + + private void OnEnable() + { + if (_dependencyResult == null) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + + _mcpClients = new McpClients(); + + // Check client configurations on startup + foreach (var client in _mcpClients.clients) + { + CheckClientConfiguration(client); + } + } + + private void OnGUI() + { + DrawHeader(); + DrawProgressBar(); + + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + + switch (_currentStep) + { + case 0: DrawSetupStep(); break; + case 1: DrawConfigureStep(); break; + case 2: DrawCompleteStep(); break; + } + + EditorGUILayout.EndScrollView(); + + DrawFooter(); + } + + private void DrawHeader() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}"); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + // Step title + var titleStyle = new GUIStyle(EditorStyles.largeLabel) + { + fontSize = 16, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle); + EditorGUILayout.Space(); + } + + private void DrawProgressBar() + { + var rect = EditorGUILayout.GetControlRect(false, 4); + var progress = (_currentStep + 1) / (float)_stepTitles.Length; + EditorGUI.ProgressBar(rect, progress, ""); + EditorGUILayout.Space(); + } + + private void DrawSetupStep() + { + // Welcome section + DrawSectionTitle("MCP for Unity Setup"); + + EditorGUILayout.LabelField( + "This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.", + EditorStyles.wordWrappedLabel + ); + EditorGUILayout.Space(); + + // Dependency check section + EditorGUILayout.BeginHorizontal(); + DrawSectionTitle("System Check", 14); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20))) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + EditorGUILayout.EndHorizontal(); + + // Show simplified dependency status + foreach (var dep in _dependencyResult.Dependencies) + { + DrawSimpleDependencyStatus(dep); + } + + // Overall status and installation guidance + EditorGUILayout.Space(); + if (!_dependencyResult.IsSystemReady) + { + // Only show critical warnings when dependencies are actually missing + EditorGUILayout.HelpBox( + "⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.", + MessageType.Warning + ); + + EditorGUILayout.Space(); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + DrawErrorStatus("Installation Required"); + + var recommendations = DependencyManager.GetInstallationRecommendations(); + EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel); + + EditorGUILayout.Space(); + if (GUILayout.Button("Open Installation Links", GUILayout.Height(25))) + { + OpenInstallationUrls(); + } + EditorGUILayout.EndVertical(); + } + else + { + DrawSuccessStatus("System Ready"); + EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel); + } + } + + + + private void DrawCompleteStep() + { + DrawSectionTitle("Setup Complete"); + + // Refresh dependency check with caching to avoid heavy operations on every repaint + if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + + if (_dependencyResult.IsSystemReady) + { + DrawSuccessStatus("MCP for Unity Ready!"); + + EditorGUILayout.HelpBox( + "🎉 MCP for Unity is now set up and ready to use!\n\n" + + "• Dependencies verified\n" + + "• MCP server ready\n" + + "• Client configuration accessible", + MessageType.Info + ); + + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Documentation", GUILayout.Height(30))) + { + Application.OpenURL("https://github.com/CoplayDev/unity-mcp"); + } + if (GUILayout.Button("Client Settings", GUILayout.Height(30))) + { + Windows.MCPForUnityEditorWindow.ShowWindow(); + } + EditorGUILayout.EndHorizontal(); + } + else + { + DrawErrorStatus("Setup Incomplete - Package Non-Functional"); + + EditorGUILayout.HelpBox( + "🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" + + "Install ALL required dependencies before the package will function.", + MessageType.Error + ); + + var missingDeps = _dependencyResult.GetMissingRequired(); + if (missingDeps.Count > 0) + { + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel); + foreach (var dep in missingDeps) + { + EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label); + } + } + + EditorGUILayout.Space(); + if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) + { + _currentStep = 0; + } + } + } + + // Helper methods for consistent UI components + private void DrawSectionTitle(string title, int fontSize = 16) + { + var titleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = fontSize, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(title, titleStyle); + EditorGUILayout.Space(); + } + + private void DrawSuccessStatus(string message) + { + var originalColor = GUI.color; + GUI.color = Color.green; + EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel); + GUI.color = originalColor; + EditorGUILayout.Space(); + } + + private void DrawErrorStatus(string message) + { + var originalColor = GUI.color; + GUI.color = Color.red; + EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel); + GUI.color = originalColor; + EditorGUILayout.Space(); + } + + private void DrawSimpleDependencyStatus(DependencyStatus dep) + { + EditorGUILayout.BeginHorizontal(); + + var statusIcon = dep.IsAvailable ? "✓" : "✗"; + var statusColor = dep.IsAvailable ? Color.green : Color.red; + + var originalColor = GUI.color; + GUI.color = statusColor; + GUILayout.Label(statusIcon, GUILayout.Width(20)); + EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel); + GUI.color = originalColor; + + if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage)) + { + EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel); + } + + EditorGUILayout.EndHorizontal(); + } + + private void DrawConfigureStep() + { + DrawSectionTitle("AI Client Configuration"); + + // Check dependencies first (with caching to avoid heavy operations on every repaint) + if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2) + { + _dependencyResult = DependencyManager.CheckAllDependencies(); + } + if (!_dependencyResult.IsSystemReady) + { + DrawErrorStatus("Cannot Configure - System Requirements Not Met"); + + EditorGUILayout.HelpBox( + "Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.", + MessageType.Warning + ); + + if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30))) + { + _currentStep = 0; + } + return; + } + + EditorGUILayout.LabelField( + "Configure your AI assistants to work with Unity. Select a client below to set it up:", + EditorStyles.wordWrappedLabel + ); + EditorGUILayout.Space(); + + // Client selection and configuration + if (_mcpClients.clients.Count > 0) + { + // Client selector dropdown + string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray(); + EditorGUI.BeginChangeCheck(); + _selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames); + if (EditorGUI.EndChangeCheck()) + { + _selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1); + // Refresh client status when selection changes + CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]); + } + + EditorGUILayout.Space(); + + var selectedClient = _mcpClients.clients[_selectedClientIndex]; + DrawClientConfigurationInWizard(selectedClient); + + EditorGUILayout.Space(); + + // Batch configuration option + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel); + EditorGUILayout.LabelField( + "Automatically configure all detected AI clients at once:", + EditorStyles.wordWrappedLabel + ); + EditorGUILayout.Space(); + + if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30))) + { + ConfigureAllClientsInWizard(); + } + EditorGUILayout.EndVertical(); + } + else + { + EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info); + } + + EditorGUILayout.Space(); + EditorGUILayout.HelpBox( + "💡 You might need to restart your AI client after configuring.", + MessageType.Info + ); + } + + private void DrawFooter() + { + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + + // Back button + GUI.enabled = _currentStep > 0; + if (GUILayout.Button("Back", GUILayout.Width(60))) + { + _currentStep--; + } + + GUILayout.FlexibleSpace(); + + // Skip button + if (GUILayout.Button("Skip", GUILayout.Width(60))) + { + bool dismiss = EditorUtility.DisplayDialog( + "Skip Setup", + "⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" + + "You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)", + "Skip Anyway", + "Cancel" + ); + + if (dismiss) + { + SetupWizard.MarkSetupDismissed(); + Close(); + } + } + + // Next/Done button + GUI.enabled = true; + string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next"; + + if (GUILayout.Button(buttonText, GUILayout.Width(80))) + { + if (_currentStep == _stepTitles.Length - 1) + { + SetupWizard.MarkSetupCompleted(); + Close(); + } + else + { + _currentStep++; + } + } + + GUI.enabled = true; + EditorGUILayout.EndHorizontal(); + } + + private void DrawClientConfigurationInWizard(McpClient client) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + // Show current status + var statusColor = GetClientStatusColor(client); + var originalColor = GUI.color; + GUI.color = statusColor; + EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label); + GUI.color = originalColor; + + EditorGUILayout.Space(); + + // Configuration buttons + EditorGUILayout.BeginHorizontal(); + + if (client.mcpType == McpTypes.ClaudeCode) + { + // Special handling for Claude Code + bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); + if (claudeAvailable) + { + bool isConfigured = client.status == McpStatus.Configured; + string buttonText = isConfigured ? "Unregister" : "Register"; + if (GUILayout.Button($"{buttonText} with Claude Code")) + { + if (isConfigured) + { + UnregisterFromClaudeCode(client); + } + else + { + RegisterWithClaudeCode(client); + } + } + } + else + { + EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning); + if (GUILayout.Button("Open Claude Code Website")) + { + Application.OpenURL("https://claude.ai/download"); + } + } + } + else + { + // Standard client configuration + if (GUILayout.Button($"Configure {client.name}")) + { + ConfigureClientInWizard(client); + } + + if (GUILayout.Button("Manual Setup")) + { + ShowManualSetupInWizard(client); + } + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + } + + private Color GetClientStatusColor(McpClient client) + { + return client.status switch + { + McpStatus.Configured => Color.green, + McpStatus.Running => Color.green, + McpStatus.Connected => Color.green, + McpStatus.IncorrectPath => Color.yellow, + McpStatus.CommunicationError => Color.yellow, + McpStatus.NoResponse => Color.yellow, + _ => Color.red + }; + } + + private void ConfigureClientInWizard(McpClient client) + { + try + { + string result = PerformClientConfiguration(client); + + EditorUtility.DisplayDialog( + $"{client.name} Configuration", + result, + "OK" + ); + + // Refresh client status + CheckClientConfiguration(client); + Repaint(); + } + catch (System.Exception ex) + { + EditorUtility.DisplayDialog( + "Configuration Error", + $"Failed to configure {client.name}: {ex.Message}", + "OK" + ); + } + } + + private void ConfigureAllClientsInWizard() + { + int successCount = 0; + int totalCount = _mcpClients.clients.Count; + + foreach (var client in _mcpClients.clients) + { + try + { + if (client.mcpType == McpTypes.ClaudeCode) + { + if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured) + { + RegisterWithClaudeCode(client); + successCount++; + } + else if (client.status == McpStatus.Configured) + { + successCount++; // Already configured + } + } + else + { + string result = PerformClientConfiguration(client); + if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase)) + { + successCount++; + } + } + + CheckClientConfiguration(client); + } + catch (System.Exception ex) + { + McpLog.Error($"Failed to configure {client.name}: {ex.Message}"); + } + } + + EditorUtility.DisplayDialog( + "Batch Configuration Complete", + $"Successfully configured {successCount} out of {totalCount} clients.\n\n" + + "Restart your AI clients for changes to take effect.", + "OK" + ); + + Repaint(); + } + + private void RegisterWithClaudeCode(McpClient client) + { + try + { + string pythonDir = McpPathResolver.FindPackagePythonDirectory(); + string claudePath = ExecPath.ResolveClaude(); + string uvPath = ExecPath.ResolveUv() ?? "uv"; + + string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; + + if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend())) + { + if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase)) + { + CheckClientConfiguration(client); + EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK"); + } + else + { + throw new System.Exception($"Registration failed: {stderr}"); + } + } + else + { + CheckClientConfiguration(client); + EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK"); + } + } + catch (System.Exception ex) + { + EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK"); + } + } + + private void UnregisterFromClaudeCode(McpClient client) + { + try + { + string claudePath = ExecPath.ResolveClaude(); + if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend())) + { + CheckClientConfiguration(client); + EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK"); + } + else + { + throw new System.Exception($"Unregistration failed: {stderr}"); + } + } + catch (System.Exception ex) + { + EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK"); + } + } + + private string PerformClientConfiguration(McpClient client) + { + // This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + string pythonDir = McpPathResolver.FindPackagePythonDirectory(); + + if (string.IsNullOrEmpty(pythonDir)) + { + return "Manual configuration required - Python server directory not found."; + } + + McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); + return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); + } + + private void ShowManualSetupInWizard(McpClient client) + { + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + string pythonDir = McpPathResolver.FindPackagePythonDirectory(); + string uvPath = ServerInstaller.FindUvPath(); + + if (string.IsNullOrEmpty(uvPath)) + { + EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK"); + return; + } + + // Build manual configuration using the sophisticated helper logic + string result = McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); + string manualConfig; + + if (result == "Configured successfully") + { + // Read back the configuration that was written + try + { + manualConfig = System.IO.File.ReadAllText(configPath); + } + catch + { + manualConfig = "Configuration written successfully, but could not read back for display."; + } + } + else + { + manualConfig = $"Configuration failed: {result}"; + } + + EditorUtility.DisplayDialog( + $"Manual Setup - {client.name}", + $"Configuration file location:\n{configPath}\n\n" + + $"Configuration result:\n{manualConfig}", + "OK" + ); + } + + private void CheckClientConfiguration(McpClient client) + { + // Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic + try + { + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + if (System.IO.File.Exists(configPath)) + { + client.configStatus = "Configured"; + client.status = McpStatus.Configured; + } + else + { + client.configStatus = "Not Configured"; + client.status = McpStatus.NotConfigured; + } + } + catch + { + client.configStatus = "Error"; + client.status = McpStatus.Error; + } + } + + private void OpenInstallationUrls() + { + var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls(); + + bool openPython = EditorUtility.DisplayDialog( + "Open Installation URLs", + "Open Python installation page?", + "Yes", + "No" + ); + + if (openPython) + { + Application.OpenURL(pythonUrl); + } + + bool openUV = EditorUtility.DisplayDialog( + "Open Installation URLs", + "Open UV installation page?", + "Yes", + "No" + ); + + if (openUV) + { + Application.OpenURL(uvUrl); + } + } + } +} diff --git a/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs.meta b/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs.meta new file mode 100644 index 000000000..5361de3d3 --- /dev/null +++ b/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 45678901234abcdef0123456789abcde +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs index a02193e6d..ed70181bb 100644 --- a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs @@ -45,10 +45,10 @@ public class MCPForUnityEditorWindow : EditorWindow // UI state private int selectedClientIndex = 0; - [MenuItem("Window/MCP for Unity")] + [MenuItem("Window/MCP For Unity")] public static void ShowWindow() { - GetWindow("MCP for Unity"); + GetWindow("MCP For Unity"); } private void OnEnable() @@ -235,7 +235,7 @@ private void DrawHeader() GUI.Label( new Rect(titleRect.x + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height), - "MCP for Unity Editor", + "MCP For Unity", titleStyle ); @@ -381,12 +381,12 @@ private void DrawServerStatusSection() bool ok = global::MCPForUnity.Editor.Helpers.ServerInstaller.RepairPythonEnvironment(); if (ok) { - EditorUtility.DisplayDialog("MCP for Unity", "Python environment repaired.", "OK"); + EditorUtility.DisplayDialog("MCP For Unity", "Python environment repaired.", "OK"); UpdatePythonServerInstallationStatus(); } else { - EditorUtility.DisplayDialog("MCP for Unity", "Repair failed. Please check Console for details.", "OK"); + EditorUtility.DisplayDialog("MCP For Unity", "Repair failed. Please check Console for details.", "OK"); } } } @@ -1099,170 +1099,6 @@ private void ToggleUnityBridge() Repaint(); } - private static bool IsValidUv(string path) - { - return !string.IsNullOrEmpty(path) - && System.IO.Path.IsPathRooted(path) - && System.IO.File.Exists(path); - } - - private static bool ValidateUvBinarySafe(string path) - { - try - { - if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return false; - var psi = new System.Diagnostics.ProcessStartInfo - { - FileName = path, - Arguments = "--version", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using var p = System.Diagnostics.Process.Start(psi); - if (p == null) return false; - if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; } - if (p.ExitCode != 0) return false; - string output = p.StandardOutput.ReadToEnd().Trim(); - return output.StartsWith("uv "); - } - catch { return false; } - } - - private static bool ArgsEqual(string[] a, string[] b) - { - if (a == null || b == null) return a == b; - if (a.Length != b.Length) return false; - for (int i = 0; i < a.Length; i++) - { - if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false; - } - return true; - } - - private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null) - { - // 0) Respect explicit lock (hidden pref or UI toggle) - try { if (UnityEditor.EditorPrefs.GetBool("MCPForUnity.LockCursorConfig", false)) return "Skipped (locked)"; } catch { } - - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - - // Read existing config if it exists - string existingJson = "{}"; - if (File.Exists(configPath)) - { - try - { - existingJson = File.ReadAllText(configPath); - } - catch (Exception e) - { - UnityEngine.Debug.LogWarning($"Error reading existing config: {e.Message}."); - } - } - - // Parse the existing JSON while preserving all properties - dynamic existingConfig; - try - { - if (string.IsNullOrWhiteSpace(existingJson)) - { - existingConfig = new Newtonsoft.Json.Linq.JObject(); - } - else - { - existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new Newtonsoft.Json.Linq.JObject(); - } - } - catch - { - // If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object - if (!string.IsNullOrWhiteSpace(existingJson)) - { - UnityEngine.Debug.LogWarning("UnityMCP: VSCode mcp.json could not be parsed; rewriting servers block."); - } - existingConfig = new Newtonsoft.Json.Linq.JObject(); - } - - // Determine existing entry references (command/args) - string existingCommand = null; - string[] existingArgs = null; - bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode); - try - { - if (isVSCode) - { - existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString(); - existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject(); - } - else - { - existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString(); - existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject(); - } - } - catch { } - - // 1) Start from existing, only fill gaps (prefer trusted resolver) - string uvPath = ServerInstaller.FindUvPath(); - // Optionally trust existingCommand if it looks like uv/uv.exe - try - { - var name = System.IO.Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); - if ((name == "uv" || name == "uv.exe") && ValidateUvBinarySafe(existingCommand)) - { - uvPath = existingCommand; - } - } - catch { } - if (uvPath == null) return "UV package manager not found. Please install UV first."; - string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); - - // 2) Canonical args order - var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; - - // 3) Only write if changed - bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) - || !ArgsEqual(existingArgs, newArgs); - if (!changed) - { - return "Configured successfully"; // nothing to do - } - - // 4) Ensure containers exist and write back minimal changes - JObject existingRoot; - if (existingConfig is JObject eo) - existingRoot = eo; - else - existingRoot = JObject.FromObject(existingConfig); - - existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient); - - string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); - - McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson); - - try - { - if (IsValidUv(uvPath)) UnityEditor.EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); - UnityEditor.EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); - } - catch { } - - return "Configured successfully"; - } - - private void ShowManualConfigurationInstructions( - string configPath, - McpClient mcpClient - ) - { - mcpClient.SetStatus(McpStatus.Error, "Manual configuration required"); - - ShowManualInstructionsWindow(configPath, mcpClient); - } - // New method to show manual instructions without changing status private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient) { @@ -1284,124 +1120,21 @@ private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient private string FindPackagePythonDirectory() { - string pythonDir = McpConfigFileHelper.ResolveServerSource(); - - try - { - // Only check dev paths if we're using a file-based package (development mode) - bool isDevelopmentMode = IsDevelopmentMode(); - if (isDevelopmentMode) - { - string currentPackagePath = Path.GetDirectoryName(Application.dataPath); - string[] devPaths = { - Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"), - Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"), - }; - - foreach (string devPath in devPaths) - { - if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py"))) - { - if (debugLogsEnabled) - { - UnityEngine.Debug.Log($"Currently in development mode. Package: {devPath}"); - } - return devPath; - } - } - } - - // Resolve via shared helper (handles local registry and older fallback) only if dev override on - if (UnityEditor.EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false)) - { - if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)) - { - return embedded; - } - } - - // Log only if the resolved path does not actually contain server.py - if (debugLogsEnabled) - { - bool hasServer = false; - try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { } - if (!hasServer) - { - UnityEngine.Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path"); - } - } - } - catch (Exception e) - { - UnityEngine.Debug.LogError($"Error finding package path: {e.Message}"); - } - - return pythonDir; - } - - private bool IsDevelopmentMode() - { - try - { - // Only treat as development if manifest explicitly references a local file path for the package - string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json"); - if (!File.Exists(manifestPath)) return false; - - string manifestContent = File.ReadAllText(manifestPath); - // Look specifically for our package dependency set to a file: URL - // This avoids auto-enabling dev mode just because a repo exists elsewhere on disk - if (manifestContent.IndexOf("\"com.justinpbarnett.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0) - { - int idx = manifestContent.IndexOf("com.justinpbarnett.unity-mcp", StringComparison.OrdinalIgnoreCase); - // Crude but effective: check for "file:" in the same line/value - if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0 - && manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; - } - catch - { - return false; - } + // Use shared helper for consistent path resolution across both windows + return McpPathResolver.FindPackagePythonDirectory(debugLogsEnabled); } private string ConfigureMcpClient(McpClient mcpClient) { try { - // Determine the config file path based on OS - string configPath; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - configPath = mcpClient.windowsConfigPath; - } - else if ( - RuntimeInformation.IsOSPlatform(OSPlatform.OSX) - ) - { - configPath = string.IsNullOrEmpty(mcpClient.macConfigPath) - ? mcpClient.linuxConfigPath - : mcpClient.macConfigPath; - } - else if ( - RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ) - { - configPath = mcpClient.linuxConfigPath; - } - else - { - return "Unsupported OS"; - } + // Use shared helper for consistent config path resolution + string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient); // Create directory if it doesn't exist - Directory.CreateDirectory(Path.GetDirectoryName(configPath)); + McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); - // Find the server.py file location using the same logic as FindPackagePythonDirectory + // Find the server.py file location using shared helper string pythonDir = FindPackagePythonDirectory(); if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py"))) @@ -1411,8 +1144,8 @@ private string ConfigureMcpClient(McpClient mcpClient) } string result = mcpClient.mcpType == McpTypes.Codex - ? ConfigureCodexClient(pythonDir, configPath, mcpClient) - : WriteToConfig(pythonDir, configPath, mcpClient); + ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient) + : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient); // Update the client status after successful configuration if (result == "Configured successfully") @@ -1453,116 +1186,6 @@ private string ConfigureMcpClient(McpClient mcpClient) } } - private string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient) - { - try { if (EditorPrefs.GetBool("MCPForUnity.LockCursorConfig", false)) return "Skipped (locked)"; } catch { } - - string existingToml = string.Empty; - if (File.Exists(configPath)) - { - try - { - existingToml = File.ReadAllText(configPath); - } - catch (Exception e) - { - if (debugLogsEnabled) - { - UnityEngine.Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}"); - } - existingToml = string.Empty; - } - } - - string existingCommand = null; - string[] existingArgs = null; - if (!string.IsNullOrWhiteSpace(existingToml)) - { - CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs); - } - - string uvPath = ServerInstaller.FindUvPath(); - try - { - var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); - if ((name == "uv" || name == "uv.exe") && ValidateUvBinarySafe(existingCommand)) - { - uvPath = existingCommand; - } - } - catch { } - - if (uvPath == null) - { - return "UV package manager not found. Please install UV first."; - } - - string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); - var newArgs = new[] { "run", "--directory", serverSrc, "server.py" }; - - bool changed = true; - if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null) - { - changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal) - || !ArgsEqual(existingArgs, newArgs); - } - - if (!changed) - { - return "Configured successfully"; - } - - string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc); - string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock); - - McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml); - - try - { - if (IsValidUv(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath); - EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc); - } - catch { } - - return "Configured successfully"; - } - - private void ShowCursorManualConfigurationInstructions( - string configPath, - McpClient mcpClient - ) - { - mcpClient.SetStatus(McpStatus.Error, "Manual configuration required"); - - // Get the Python directory path using Package Manager API - string pythonDir = FindPackagePythonDirectory(); - - // Create the manual configuration message - string uvPath = FindUvPath(); - if (uvPath == null) - { - UnityEngine.Debug.LogError("UV package manager not found. Cannot configure manual setup."); - return; - } - - McpConfig jsonConfig = new() - { - mcpServers = new McpConfigServers - { - unityMCP = new McpConfigServer - { - command = uvPath, - args = new[] { "run", "--directory", pythonDir, "server.py" }, - }, - }, - }; - - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); - - ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); - } - private void LoadValidationLevelSetting() { string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard"); @@ -1601,12 +1224,6 @@ private string GetValidationLevelDescription(int index) }; } - public static string GetCurrentValidationLevel() - { - string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard"); - return savedLevel; - } - private void CheckMcpConfiguration(McpClient mcpClient) { try @@ -1618,30 +1235,8 @@ private void CheckMcpConfiguration(McpClient mcpClient) return; } - string configPath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - configPath = mcpClient.windowsConfigPath; - } - else if ( - RuntimeInformation.IsOSPlatform(OSPlatform.OSX) - ) - { - configPath = string.IsNullOrEmpty(mcpClient.macConfigPath) - ? mcpClient.linuxConfigPath - : mcpClient.macConfigPath; - } - else if ( - RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ) - { - configPath = mcpClient.linuxConfigPath; - } - else - { - mcpClient.SetStatus(McpStatus.UnsupportedOS); - return; - } + // Use shared helper for consistent config path resolution + string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient); if (!File.Exists(configPath)) { @@ -1711,8 +1306,8 @@ private void CheckMcpConfiguration(McpClient mcpClient) try { string rewriteResult = mcpClient.mcpType == McpTypes.Codex - ? ConfigureCodexClient(pythonDir, configPath, mcpClient) - : WriteToConfig(pythonDir, configPath, mcpClient); + ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient) + : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient); if (rewriteResult == "Configured successfully") { if (debugLogsEnabled) diff --git a/UnityMcpBridge/package.json b/UnityMcpBridge/package.json index 3ec3f9858..b239aca54 100644 --- a/UnityMcpBridge/package.json +++ b/UnityMcpBridge/package.json @@ -2,7 +2,7 @@ "name": "com.coplaydev.unity-mcp", "version": "4.1.1", "displayName": "MCP for Unity", - "description": "A bridge that connects an LLM to Unity via the MCP (Model Context Protocol). This allows MCP Clients like Claude Desktop or Cursor to directly control your Unity Editor.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4", + "description": "A bridge that connects AI assistants to Unity via the MCP (Model Context Protocol). Allows AI clients like Claude Code, Cursor, and VSCode to directly control your Unity Editor for enhanced development workflows.\n\nFeatures automated setup wizard, cross-platform support, and seamless integration with popular AI development tools.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4", "unity": "2021.3", "documentationUrl": "https://github.com/CoplayDev/unity-mcp", "licensesUrl": "https://github.com/CoplayDev/unity-mcp/blob/main/LICENSE",