diff --git a/MCPForUnity/Editor/Data/DefaultServerConfig.cs b/MCPForUnity/Editor/Data/DefaultServerConfig.cs deleted file mode 100644 index 59cced75..00000000 --- a/MCPForUnity/Editor/Data/DefaultServerConfig.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Data -{ - public class DefaultServerConfig : ServerConfig - { - public new string unityHost = "localhost"; - public new int unityPort = 6400; - public new int mcpPort = 6500; - public new float connectionTimeout = 15.0f; - public new int bufferSize = 32768; - public new string logLevel = "INFO"; - public new string logFormat = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"; - public new int maxRetries = 3; - public new float retryDelay = 1.0f; - } -} diff --git a/MCPForUnity/Editor/Dependencies/DependencyManager.cs b/MCPForUnity/Editor/Dependencies/DependencyManager.cs index ce6efef2..d5a082a1 100644 --- a/MCPForUnity/Editor/Dependencies/DependencyManager.cs +++ b/MCPForUnity/Editor/Dependencies/DependencyManager.cs @@ -126,7 +126,7 @@ private static void GenerateRecommendations(DependencyCheckResult result, IPlatf { if (dep.Name == "Python") { - result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}"); + result.RecommendedActions.Add($"Install Python 3.11+ from: {detector.GetPythonInstallUrl()}"); } else if (dep.Name == "UV Package Manager") { diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs index f654612c..a1281747 100644 --- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs @@ -62,7 +62,7 @@ public override DependencyStatus DetectPython() } } - status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.ErrorMessage = "Python not found. Please install Python 3.11 or later."; status.Details = "Checked common installation paths including system, snap, and user-local locations."; } catch (Exception ex) @@ -144,10 +144,10 @@ private bool TryValidatePython(string pythonPath, out string version, out string version = output.Substring(7); // Remove "Python " prefix fullPath = pythonPath; - // Validate minimum version (Python 4+ or Python 3.10+) + // Validate minimum version (Python 4+ or Python 3.11+) if (TryParseVersion(version, out var major, out var minor)) { - return major > 3 || (major >= 3 && minor >= 10); + return major > 3 || (major >= 3 && minor >= 11); } } } diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs index c9d152d8..64e9a50a 100644 --- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs @@ -35,8 +35,7 @@ public override DependencyStatus DetectPython() "/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" + "/Library/Frameworks/Python.framework/Versions/3.11/bin/python3" }; foreach (var candidate in candidates) @@ -65,7 +64,7 @@ public override DependencyStatus DetectPython() } } - status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.ErrorMessage = "Python not found. Please install Python 3.11 or later."; status.Details = "Checked common installation paths including Homebrew, Framework, and system locations."; } catch (Exception ex) @@ -144,10 +143,10 @@ private bool TryValidatePython(string pythonPath, out string version, out string version = output.Substring(7); // Remove "Python " prefix fullPath = pythonPath; - // Validate minimum version (Python 4+ or Python 3.10+) + // Validate minimum version (Python 4+ or Python 3.11+) if (TryParseVersion(version, out var major, out var minor)) { - return major > 3 || (major >= 3 && minor >= 10); + return major > 3 || (major >= 3 && minor >= 11); } } } diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs index 7891c6e3..bcb2c7d4 100644 --- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs @@ -68,7 +68,7 @@ public override DependencyStatus DetectPython() } } - status.ErrorMessage = "Python not found. Please install Python 3.10 or later."; + status.ErrorMessage = "Python not found. Please install Python 3.11 or later."; status.Details = "Checked common installation paths and PATH environment variable."; } catch (Exception ex) @@ -132,10 +132,10 @@ private bool TryValidatePython(string pythonPath, out string version, out string version = output.Substring(7); // Remove "Python " prefix fullPath = pythonPath; - // Validate minimum version (Python 4+ or Python 3.10+) + // Validate minimum version (Python 4+ or Python 3.11+) if (TryParseVersion(version, out var major, out var minor)) { - return major > 3 || (major >= 3 && minor >= 10); + return major > 3 || (major >= 3 && minor >= 11); } } } diff --git a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs index d3d77dc3..a4728901 100644 --- a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs +++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using MCPForUnity.External.Tommy; +using MCPForUnity.Editor.Services; namespace MCPForUnity.Editor.Helpers { @@ -26,10 +27,10 @@ public static bool IsCodexConfigured(string pythonDir) string toml = File.ReadAllText(configPath); if (!TryParseCodexServer(toml, out _, out var args)) return false; - string dir = McpConfigFileHelper.ExtractDirectoryArg(args); + string dir = McpConfigurationHelper.ExtractDirectoryArg(args); if (string.IsNullOrEmpty(dir)) return false; - return McpConfigFileHelper.PathsEqual(dir, pythonDir); + return McpConfigurationHelper.PathsEqual(dir, pythonDir); } catch { @@ -125,6 +126,8 @@ private static TomlTable TryParseToml(string toml) /// /// Creates a TomlTable for the unityMCP server configuration /// + /// Path to uv executable + /// Path to server source directory private static TomlTable CreateUnityMcpTable(string uvPath, string serverSrc) { var unityMCP = new TomlTable(); @@ -137,6 +140,15 @@ private static TomlTable CreateUnityMcpTable(string uvPath, string serverSrc) argsArray.Add(new TomlString { Value = "server.py" }); unityMCP["args"] = argsArray; + // Add Windows-specific environment configuration, see: https://github.com/CoplayDev/unity-mcp/issues/315 + var platformService = MCPServiceLocator.Platform; + if (platformService.IsWindows()) + { + var envTable = new TomlTable { IsInline = true }; + envTable["SystemRoot"] = new TomlString { Value = platformService.GetSystemRoot() }; + unityMCP["env"] = envTable; + } + return unityMCP; } diff --git a/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs b/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs deleted file mode 100644 index 389d47d2..00000000 --- a/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using UnityEditor; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Shared helpers for reading and writing MCP client configuration files. - /// Consolidates file atomics and server directory resolution so the editor - /// window can focus on UI concerns only. - /// - public static class McpConfigFileHelper - { - public static string ExtractDirectoryArg(string[] args) - { - if (args == null) return null; - for (int i = 0; i < args.Length - 1; i++) - { - if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase)) - { - return args[i + 1]; - } - } - return null; - } - - public static bool PathsEqual(string a, string b) - { - if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false; - try - { - string na = Path.GetFullPath(a.Trim()); - string nb = Path.GetFullPath(b.Trim()); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase); - } - return string.Equals(na, nb, StringComparison.Ordinal); - } - catch - { - return false; - } - } - - /// - /// Resolves the server directory to use for MCP tools, preferring - /// existing config values and falling back to installed/embedded copies. - /// - public static string ResolveServerDirectory(string pythonDir, string[] existingArgs) - { - string serverSrc = ExtractDirectoryArg(existingArgs); - bool serverValid = !string.IsNullOrEmpty(serverSrc) - && File.Exists(Path.Combine(serverSrc, "server.py")); - if (!serverValid) - { - if (!string.IsNullOrEmpty(pythonDir) - && File.Exists(Path.Combine(pythonDir, "server.py"))) - { - serverSrc = pythonDir; - } - else - { - serverSrc = ResolveServerSource(); - } - } - - try - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !string.IsNullOrEmpty(serverSrc)) - { - string norm = serverSrc.Replace('\\', '/'); - int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal); - if (idx >= 0) - { - string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty; - string suffix = norm.Substring(idx + "/.local/share/".Length); - serverSrc = Path.Combine(home, "Library", "Application Support", suffix); - } - } - } - catch - { - // Ignore failures and fall back to the original path. - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - && !string.IsNullOrEmpty(serverSrc) - && serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0 - && !EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false)) - { - serverSrc = ServerInstaller.GetServerPath(); - } - - return serverSrc; - } - - public static void WriteAtomicFile(string path, string contents) - { - string tmp = path + ".tmp"; - string backup = path + ".backup"; - bool writeDone = false; - try - { - File.WriteAllText(tmp, contents, new UTF8Encoding(false)); - try - { - File.Replace(tmp, path, backup); - writeDone = true; - } - catch (FileNotFoundException) - { - File.Move(tmp, path); - writeDone = true; - } - catch (PlatformNotSupportedException) - { - if (File.Exists(path)) - { - try - { - if (File.Exists(backup)) File.Delete(backup); - } - catch { } - File.Move(path, backup); - } - File.Move(tmp, path); - writeDone = true; - } - } - catch (Exception ex) - { - try - { - if (!writeDone && File.Exists(backup)) - { - try { File.Copy(backup, path, true); } catch { } - } - } - catch { } - throw new Exception($"Failed to write config file '{path}': {ex.Message}", ex); - } - finally - { - try { if (File.Exists(tmp)) File.Delete(tmp); } catch { } - try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { } - } - } - - public static string ResolveServerSource() - { - try - { - string remembered = EditorPrefs.GetString("MCPForUnity.ServerSrc", string.Empty); - if (!string.IsNullOrEmpty(remembered) - && File.Exists(Path.Combine(remembered, "server.py"))) - { - return remembered; - } - - ServerInstaller.EnsureServerInstalled(); - string installed = ServerInstaller.GetServerPath(); - if (File.Exists(Path.Combine(installed, "server.py"))) - { - return installed; - } - - bool useEmbedded = EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false); - if (useEmbedded - && ServerPathResolver.TryFindEmbeddedServerSource(out string embedded) - && File.Exists(Path.Combine(embedded, "server.py"))) - { - return embedded; - } - - return installed; - } - catch - { - return ServerInstaller.GetServerPath(); - } - } - } -} diff --git a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs index d88bdbc8..96ad7ec2 100644 --- a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs +++ b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; @@ -105,20 +106,9 @@ public static string WriteMcpConfiguration(string pythonDir, string configPath, } catch { } if (uvPath == null) return "UV package manager not found. Please install UV first."; - string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs); + string serverSrc = 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 + // Ensure containers exist and write back configuration JObject existingRoot; if (existingConfig is JObject eo) existingRoot = eo; @@ -129,7 +119,8 @@ public static string WriteMcpConfiguration(string pythonDir, string configPath, string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); - McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson); + EnsureConfigDirectoryExists(configPath); + WriteAtomicFile(configPath, mergedJson); try { @@ -190,24 +181,12 @@ public static string ConfigureCodexClient(string pythonDir, string configPath, M 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 serverSrc = ResolveServerDirectory(pythonDir, existingArgs); string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvPath, serverSrc); - McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml); + EnsureConfigDirectoryExists(configPath); + WriteAtomicFile(configPath, updatedToml); try { @@ -246,20 +225,6 @@ private static bool IsValidUvBinary(string path) 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 /// @@ -292,5 +257,175 @@ public static void EnsureConfigDirectoryExists(string configPath) { Directory.CreateDirectory(Path.GetDirectoryName(configPath)); } + + public static string ExtractDirectoryArg(string[] args) + { + if (args == null) return null; + for (int i = 0; i < args.Length - 1; i++) + { + if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase)) + { + return args[i + 1]; + } + } + return null; + } + + public static bool PathsEqual(string a, string b) + { + if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false; + try + { + string na = Path.GetFullPath(a.Trim()); + string nb = Path.GetFullPath(b.Trim()); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase); + } + return string.Equals(na, nb, StringComparison.Ordinal); + } + catch + { + return false; + } + } + + /// + /// Resolves the server directory to use for MCP tools, preferring + /// existing config values and falling back to installed/embedded copies. + /// + public static string ResolveServerDirectory(string pythonDir, string[] existingArgs) + { + string serverSrc = ExtractDirectoryArg(existingArgs); + bool serverValid = !string.IsNullOrEmpty(serverSrc) + && File.Exists(Path.Combine(serverSrc, "server.py")); + if (!serverValid) + { + if (!string.IsNullOrEmpty(pythonDir) + && File.Exists(Path.Combine(pythonDir, "server.py"))) + { + serverSrc = pythonDir; + } + else + { + serverSrc = ResolveServerSource(); + } + } + + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !string.IsNullOrEmpty(serverSrc)) + { + string norm = serverSrc.Replace('\\', '/'); + int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal); + if (idx >= 0) + { + string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty; + string suffix = norm.Substring(idx + "/.local/share/".Length); + serverSrc = Path.Combine(home, "Library", "Application Support", suffix); + } + } + } + catch + { + // Ignore failures and fall back to the original path. + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + && !string.IsNullOrEmpty(serverSrc) + && serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0 + && !EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false)) + { + serverSrc = ServerInstaller.GetServerPath(); + } + + return serverSrc; + } + + public static void WriteAtomicFile(string path, string contents) + { + string tmp = path + ".tmp"; + string backup = path + ".backup"; + bool writeDone = false; + try + { + File.WriteAllText(tmp, contents, new UTF8Encoding(false)); + try + { + File.Replace(tmp, path, backup); + writeDone = true; + } + catch (FileNotFoundException) + { + File.Move(tmp, path); + writeDone = true; + } + catch (PlatformNotSupportedException) + { + if (File.Exists(path)) + { + try + { + if (File.Exists(backup)) File.Delete(backup); + } + catch { } + File.Move(path, backup); + } + File.Move(tmp, path); + writeDone = true; + } + } + catch (Exception ex) + { + try + { + if (!writeDone && File.Exists(backup)) + { + try { File.Copy(backup, path, true); } catch { } + } + } + catch { } + throw new Exception($"Failed to write config file '{path}': {ex.Message}", ex); + } + finally + { + try { if (File.Exists(tmp)) File.Delete(tmp); } catch { } + try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { } + } + } + + public static string ResolveServerSource() + { + try + { + string remembered = EditorPrefs.GetString("MCPForUnity.ServerSrc", string.Empty); + if (!string.IsNullOrEmpty(remembered) + && File.Exists(Path.Combine(remembered, "server.py"))) + { + return remembered; + } + + ServerInstaller.EnsureServerInstalled(); + string installed = ServerInstaller.GetServerPath(); + if (File.Exists(Path.Combine(installed, "server.py"))) + { + return installed; + } + + bool useEmbedded = EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false); + if (useEmbedded + && ServerPathResolver.TryFindEmbeddedServerSource(out string embedded) + && File.Exists(Path.Combine(embedded, "server.py"))) + { + return embedded; + } + + return installed; + } + catch + { + return ServerInstaller.GetServerPath(); + } + } } } diff --git a/MCPForUnity/Editor/Helpers/McpPathResolver.cs b/MCPForUnity/Editor/Helpers/McpPathResolver.cs index be1089f7..04082a94 100644 --- a/MCPForUnity/Editor/Helpers/McpPathResolver.cs +++ b/MCPForUnity/Editor/Helpers/McpPathResolver.cs @@ -20,7 +20,7 @@ public static class McpPathResolver /// public static string FindPackagePythonDirectory(bool debugLogsEnabled = false) { - string pythonDir = McpConfigFileHelper.ResolveServerSource(); + string pythonDir = McpConfigurationHelper.ResolveServerSource(); try { diff --git a/MCPForUnity/Editor/Helpers/PackageDetector.cs b/MCPForUnity/Editor/Helpers/PackageDetector.cs deleted file mode 100644 index 59e22348..00000000 --- a/MCPForUnity/Editor/Helpers/PackageDetector.cs +++ /dev/null @@ -1,106 +0,0 @@ -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Auto-runs legacy/older install detection on package load/update (log-only). - /// Runs once per embedded server version using an EditorPrefs version-scoped key. - /// - [InitializeOnLoad] - public static class PackageDetector - { - private const string DetectOnceFlagKeyPrefix = "MCPForUnity.LegacyDetectLogged:"; - - static PackageDetector() - { - try - { - string pkgVer = ReadPackageVersionOrFallback(); - string key = DetectOnceFlagKeyPrefix + pkgVer; - - // Always force-run if legacy roots exist or canonical install is missing - bool legacyPresent = LegacyRootsExist(); - bool canonicalMissing = !System.IO.File.Exists(System.IO.Path.Combine(ServerInstaller.GetServerPath(), "server.py")); - - if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing) - { - // Marshal the entire flow to the main thread. EnsureServerInstalled may touch Unity APIs. - EditorApplication.delayCall += () => - { - string error = null; - System.Exception capturedEx = null; - try - { - // Ensure any UnityEditor API usage inside runs on the main thread - ServerInstaller.EnsureServerInstalled(); - } - catch (System.Exception ex) - { - error = ex.Message; - capturedEx = ex; - } - - // Unity APIs must stay on main thread - try { EditorPrefs.SetBool(key, true); } catch { } - // Ensure prefs cleanup happens on main thread - try { EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); } catch { } - try { EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); } catch { } - - if (!string.IsNullOrEmpty(error)) - { - McpLog.Info($"Server check: {error}. Download via Window > MCP For Unity if needed.", always: false); - } - }; - } - } - catch { /* ignore */ } - } - - private static string ReadEmbeddedVersionOrFallback() - { - try - { - if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc)) - { - var p = System.IO.Path.Combine(embeddedSrc, "server_version.txt"); - if (System.IO.File.Exists(p)) - return (System.IO.File.ReadAllText(p)?.Trim() ?? "unknown"); - } - } - catch { } - return "unknown"; - } - - private static string ReadPackageVersionOrFallback() - { - try - { - var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(PackageDetector).Assembly); - if (info != null && !string.IsNullOrEmpty(info.version)) return info.version; - } - catch { } - // Fallback to embedded server version if package info unavailable - return ReadEmbeddedVersionOrFallback(); - } - - private static bool LegacyRootsExist() - { - try - { - string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile) ?? string.Empty; - string[] roots = - { - System.IO.Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"), - System.IO.Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src") - }; - foreach (var r in roots) - { - try { if (System.IO.File.Exists(System.IO.Path.Combine(r, "server.py"))) return true; } catch { } - } - } - catch { } - return false; - } - } -} diff --git a/MCPForUnity/Editor/Helpers/PackageInstaller.cs b/MCPForUnity/Editor/Helpers/PackageInstaller.cs deleted file mode 100644 index 1d46f321..00000000 --- a/MCPForUnity/Editor/Helpers/PackageInstaller.cs +++ /dev/null @@ -1,46 +0,0 @@ -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Handles automatic installation of the MCP server when the package is first installed. - /// - [InitializeOnLoad] - public static class PackageInstaller - { - private const string InstallationFlagKey = "MCPForUnity.ServerInstalled"; - - static PackageInstaller() - { - // Check if this is the first time the package is loaded - if (!EditorPrefs.GetBool(InstallationFlagKey, false)) - { - // Schedule the installation for after Unity is fully loaded - EditorApplication.delayCall += InstallServerOnFirstLoad; - } - } - - private static void InstallServerOnFirstLoad() - { - try - { - ServerInstaller.EnsureServerInstalled(); - - // Mark as installed/checked - EditorPrefs.SetBool(InstallationFlagKey, true); - - // Only log success if server was actually embedded and copied - if (ServerInstaller.HasEmbeddedServer()) - { - McpLog.Info("MCP server installation completed successfully."); - } - } - catch (System.Exception) - { - EditorPrefs.SetBool(InstallationFlagKey, true); // Mark as handled - McpLog.Info("Server installation pending. Open Window > MCP For Unity to download the server."); - } - } - } -} diff --git a/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs b/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs new file mode 100644 index 00000000..02e482c2 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs @@ -0,0 +1,240 @@ +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Manages package lifecycle events including first-time installation, + /// version updates, and legacy installation detection. + /// Consolidates the functionality of PackageInstaller and PackageDetector. + /// + [InitializeOnLoad] + public static class PackageLifecycleManager + { + private const string VersionKeyPrefix = "MCPForUnity.InstalledVersion:"; + private const string LegacyInstallFlagKey = "MCPForUnity.ServerInstalled"; // For migration + private const string InstallErrorKeyPrefix = "MCPForUnity.InstallError:"; // Stores last installation error + + static PackageLifecycleManager() + { + // Schedule the check for after Unity is fully loaded + EditorApplication.delayCall += CheckAndInstallServer; + } + + private static void CheckAndInstallServer() + { + try + { + string currentVersion = GetPackageVersion(); + string versionKey = VersionKeyPrefix + currentVersion; + bool hasRunForThisVersion = EditorPrefs.GetBool(versionKey, false); + + // Check for conditions that require installation/verification + bool isFirstTimeInstall = !EditorPrefs.HasKey(LegacyInstallFlagKey) && !hasRunForThisVersion; + bool legacyPresent = LegacyRootsExist(); + bool canonicalMissing = !File.Exists( + Path.Combine(ServerInstaller.GetServerPath(), "server.py") + ); + + // Run if: first install, version update, legacy detected, or canonical missing + if (isFirstTimeInstall || !hasRunForThisVersion || legacyPresent || canonicalMissing) + { + PerformInstallation(currentVersion, versionKey, isFirstTimeInstall); + } + } + catch (System.Exception ex) + { + McpLog.Info($"Package lifecycle check failed: {ex.Message}. Open Window > MCP For Unity if needed.", always: false); + } + } + + private static void PerformInstallation(string version, string versionKey, bool isFirstTimeInstall) + { + string error = null; + + try + { + ServerInstaller.EnsureServerInstalled(); + + // Mark as installed for this version + EditorPrefs.SetBool(versionKey, true); + + // Migrate legacy flag if this is first time + if (isFirstTimeInstall) + { + EditorPrefs.SetBool(LegacyInstallFlagKey, true); + } + + // Clean up old version keys (keep only current version) + CleanupOldVersionKeys(version); + + // Clean up legacy preference keys + CleanupLegacyPrefs(); + + // Only log success if server was actually embedded and copied + if (ServerInstaller.HasEmbeddedServer() && isFirstTimeInstall) + { + McpLog.Info("MCP server installation completed successfully."); + } + } + catch (System.Exception ex) + { + error = ex.Message; + + // Store the error for display in the UI, but don't mark as handled + // This allows the user to manually rebuild via the "Rebuild Server" button + string errorKey = InstallErrorKeyPrefix + version; + EditorPrefs.SetString(errorKey, ex.Message ?? "Unknown error"); + + // Don't mark as installed - user needs to manually rebuild + } + + if (!string.IsNullOrEmpty(error)) + { + McpLog.Info($"Server installation failed: {error}. Use Window > MCP For Unity > Rebuild Server to retry.", always: false); + } + } + + private static string GetPackageVersion() + { + try + { + var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly( + typeof(PackageLifecycleManager).Assembly + ); + if (info != null && !string.IsNullOrEmpty(info.version)) + { + return info.version; + } + } + catch { } + + // Fallback to embedded server version + return GetEmbeddedServerVersion(); + } + + private static string GetEmbeddedServerVersion() + { + try + { + if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc)) + { + var versionPath = Path.Combine(embeddedSrc, "server_version.txt"); + if (File.Exists(versionPath)) + { + return File.ReadAllText(versionPath)?.Trim() ?? "unknown"; + } + } + } + catch { } + return "unknown"; + } + + private static bool LegacyRootsExist() + { + try + { + string home = System.Environment.GetFolderPath( + System.Environment.SpecialFolder.UserProfile + ) ?? string.Empty; + + string[] legacyRoots = + { + Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"), + Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src") + }; + + foreach (var root in legacyRoots) + { + try + { + if (File.Exists(Path.Combine(root, "server.py"))) + { + return true; + } + } + catch { } + } + } + catch { } + return false; + } + + private static void CleanupOldVersionKeys(string currentVersion) + { + try + { + // Get all EditorPrefs keys that start with our version prefix + // Note: Unity doesn't provide a way to enumerate all keys, so we can only + // clean up known legacy keys. Future versions will be cleaned up when + // a newer version runs. + // This is a best-effort cleanup. + } + catch { } + } + + private static void CleanupLegacyPrefs() + { + try + { + // Clean up old preference keys that are no longer used + string[] legacyKeys = + { + "MCPForUnity.ServerSrc", + "MCPForUnity.PythonDirOverride", + "MCPForUnity.LegacyDetectLogged" // Old prefix without version + }; + + foreach (var key in legacyKeys) + { + try + { + if (EditorPrefs.HasKey(key)) + { + EditorPrefs.DeleteKey(key); + } + } + catch { } + } + } + catch { } + } + + /// + /// Gets the last installation error for the current package version, if any. + /// Returns null if there was no error or the error has been cleared. + /// + public static string GetLastInstallError() + { + try + { + string currentVersion = GetPackageVersion(); + string errorKey = InstallErrorKeyPrefix + currentVersion; + if (EditorPrefs.HasKey(errorKey)) + { + return EditorPrefs.GetString(errorKey, null); + } + } + catch { } + return null; + } + + /// + /// Clears the last installation error. Should be called after a successful manual rebuild. + /// + public static void ClearLastInstallError() + { + try + { + string currentVersion = GetPackageVersion(); + string errorKey = InstallErrorKeyPrefix + currentVersion; + if (EditorPrefs.HasKey(errorKey)) + { + EditorPrefs.DeleteKey(errorKey); + } + } + catch { } + } + } +} diff --git a/MCPForUnity/Editor/Helpers/PackageDetector.cs.meta b/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs.meta similarity index 83% rename from MCPForUnity/Editor/Helpers/PackageDetector.cs.meta rename to MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs.meta index f1a5dbe4..f1e14f70 100644 --- a/MCPForUnity/Editor/Helpers/PackageDetector.cs.meta +++ b/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: b82eaef548d164ca095f17db64d15af8 +guid: c40bd28f2310d463c8cd00181202cbe4 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs b/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs index fce0e783..de6167a7 100644 --- a/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs +++ b/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs @@ -139,9 +139,8 @@ public static void SetAutoSyncEnabled(bool enabled) } /// - /// Menu item to reimport all Python files in the project + /// Reimport all Python files in the project /// - [MenuItem("Window/MCP For Unity/Tool Sync/Reimport Python Files", priority = 99)] public static void ReimportPythonFiles() { // Find all Python files (imported as TextAssets by PythonFileImporter) @@ -161,9 +160,8 @@ public static void ReimportPythonFiles() } /// - /// Menu item to manually trigger sync + /// Manually trigger sync /// - [MenuItem("Window/MCP For Unity/Tool Sync/Sync Python Tools", priority = 100)] public static void ManualSync() { McpLog.Info("Manually syncing Python tools..."); @@ -171,9 +169,8 @@ public static void ManualSync() } /// - /// Menu item to toggle auto-sync + /// Toggle auto-sync /// - [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", priority = 101)] public static void ToggleAutoSync() { SetAutoSyncEnabled(!IsAutoSyncEnabled()); @@ -182,7 +179,6 @@ public static void ToggleAutoSync() /// /// Validate menu item (shows checkmark when enabled) /// - [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", true, priority = 101)] public static bool ToggleAutoSyncValidate() { Menu.SetChecked("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", IsAutoSyncEnabled()); diff --git a/MCPForUnity/Editor/Helpers/ServerInstaller.cs b/MCPForUnity/Editor/Helpers/ServerInstaller.cs index 2b0c8f45..10666342 100644 --- a/MCPForUnity/Editor/Helpers/ServerInstaller.cs +++ b/MCPForUnity/Editor/Helpers/ServerInstaller.cs @@ -84,14 +84,13 @@ public static void EnsureServerInstalled() if (legacyOlder) { TryKillUvForPath(legacySrc); - try + if (DeleteDirectoryWithRetry(legacyRoot)) { - Directory.Delete(legacyRoot, recursive: true); McpLog.Info($"Removed legacy server at '{legacyRoot}'."); } - catch (Exception ex) + else { - McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}': {ex.Message}"); + McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}' (files may be in use)"); } } } @@ -338,13 +337,24 @@ private static IEnumerable GetLegacyRootsForDetection() return roots; } + /// + /// Attempts to kill UV and Python processes associated with a specific server path. + /// This is necessary on Windows because the OS blocks file deletion when processes + /// have open file handles, unlike macOS/Linux which allow unlinking open files. + /// private static void TryKillUvForPath(string serverSrcPath) { try { if (string.IsNullOrEmpty(serverSrcPath)) return; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + KillWindowsUvProcesses(serverSrcPath); + return; + } + + // Unix: use pgrep to find processes by command line var psi = new ProcessStartInfo { FileName = "/usr/bin/pgrep", @@ -372,6 +382,148 @@ private static void TryKillUvForPath(string serverSrcPath) catch { } } + /// + /// Kills Windows processes running from the virtual environment directory. + /// Uses WMIC (Windows Management Instrumentation) to safely query only processes + /// with executables in the .venv path, avoiding the need to iterate all system processes. + /// This prevents accidentally killing IDE processes or other critical system processes. + /// + /// Why this is needed on Windows: + /// - Windows blocks file/directory deletion when ANY process has an open file handle + /// - UV creates a virtual environment with python.exe and other executables + /// - These processes may hold locks on DLLs, .pyd files, or the executables themselves + /// - macOS/Linux allow deletion of open files (unlink), but Windows does not + /// + private static void KillWindowsUvProcesses(string serverSrcPath) + { + try + { + if (string.IsNullOrEmpty(serverSrcPath)) return; + + string venvPath = Path.Combine(serverSrcPath, ".venv"); + if (!Directory.Exists(venvPath)) return; + + string normalizedVenvPath = Path.GetFullPath(venvPath).ToLowerInvariant(); + + // Use WMIC to find processes with executables in the .venv directory + // This is much safer than iterating all processes + var psi = new ProcessStartInfo + { + FileName = "wmic", + Arguments = $"process where \"ExecutablePath like '%{normalizedVenvPath.Replace("\\", "\\\\")}%'\" get ProcessId", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var proc = Process.Start(psi); + if (proc == null) return; + + string output = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(5000); + + if (proc.ExitCode != 0) return; + + // Parse PIDs from WMIC output + var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + string trimmed = line.Trim(); + if (trimmed.Equals("ProcessId", StringComparison.OrdinalIgnoreCase)) continue; + if (string.IsNullOrWhiteSpace(trimmed)) continue; + + if (int.TryParse(trimmed, out int pid)) + { + try + { + using var p = Process.GetProcessById(pid); + // Double-check it's not a critical process + string name = p.ProcessName.ToLowerInvariant(); + if (name == "unity" || name == "code" || name == "devenv" || name == "rider64") + { + continue; // Skip IDE processes + } + p.Kill(); + p.WaitForExit(2000); + } + catch { } + } + } + + // Give processes time to fully exit + System.Threading.Thread.Sleep(500); + } + catch { } + } + + /// + /// Attempts to delete a directory with retry logic to handle Windows file locking issues. + /// + /// Why retries are necessary on Windows: + /// - Even after killing processes, Windows may take time to release file handles + /// - Antivirus, Windows Defender, or indexing services may temporarily lock files + /// - File Explorer previews can hold locks on certain file types + /// - Readonly attributes on files (common in .venv) block deletion + /// + /// This method handles these cases by: + /// - Retrying deletion after a delay to allow handle release + /// - Clearing readonly attributes that block deletion + /// - Distinguishing between temporary locks (retry) and permanent failures + /// + private static bool DeleteDirectoryWithRetry(string path, int maxRetries = 3, int delayMs = 500) + { + for (int i = 0; i < maxRetries; i++) + { + try + { + if (!Directory.Exists(path)) return true; + + Directory.Delete(path, recursive: true); + return true; + } + catch (UnauthorizedAccessException) + { + if (i < maxRetries - 1) + { + // Wait for file handles to be released + System.Threading.Thread.Sleep(delayMs); + + // Try to clear readonly attributes + try + { + foreach (var file in Directory.GetFiles(path, "*", SearchOption.AllDirectories)) + { + try + { + var attrs = File.GetAttributes(file); + if ((attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + File.SetAttributes(file, attrs & ~FileAttributes.ReadOnly); + } + } + catch { } + } + } + catch { } + } + } + catch (IOException) + { + if (i < maxRetries - 1) + { + // File in use, wait and retry + System.Threading.Thread.Sleep(delayMs); + } + } + catch + { + return false; + } + } + return false; + } + private static string ReadVersionFile(string path) { try @@ -459,16 +611,12 @@ public static bool RebuildMcpServer() // Delete the entire installed server directory if (Directory.Exists(destRoot)) { - try + if (!DeleteDirectoryWithRetry(destRoot, maxRetries: 5, delayMs: 1000)) { - Directory.Delete(destRoot, recursive: true); - McpLog.Info($"Deleted existing server at {destRoot}"); - } - catch (Exception ex) - { - McpLog.Error($"Failed to delete existing server: {ex.Message}"); + McpLog.Error($"Failed to delete existing server at {destRoot}. Please close any applications using the Python virtual environment and try again."); return false; } + McpLog.Info($"Deleted existing server at {destRoot}"); } // Re-copy from embedded source @@ -488,6 +636,12 @@ public static bool RebuildMcpServer() } McpLog.Info($"Server rebuilt successfully at {destRoot} (version {embeddedVer})"); + + // Clear any previous installation error + + PackageLifecycleManager.ClearLastInstallError(); + + return true; } catch (Exception ex) @@ -747,13 +901,9 @@ public static bool DownloadAndInstallServer() // Delete old installation if (Directory.Exists(destRoot)) { - try - { - Directory.Delete(destRoot, recursive: true); - } - catch (Exception ex) + if (!DeleteDirectoryWithRetry(destRoot, maxRetries: 5, delayMs: 1000)) { - McpLog.Warn($"Could not fully delete old server: {ex.Message}"); + McpLog.Warn($"Could not fully delete old server (files may be in use)"); } } @@ -803,9 +953,12 @@ public static bool DownloadAndInstallServer() } finally { - try { - if (File.Exists(tempZip)) File.Delete(tempZip); - } catch (Exception ex) { + try + { + if (File.Exists(tempZip)) File.Delete(tempZip); + } + catch (Exception ex) + { McpLog.Warn($"Could not delete temp zip file: {ex.Message}"); } } diff --git a/MCPForUnity/Editor/Helpers/Vector3Helper.cs b/MCPForUnity/Editor/Helpers/Vector3Helper.cs deleted file mode 100644 index 41566188..00000000 --- a/MCPForUnity/Editor/Helpers/Vector3Helper.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Newtonsoft.Json.Linq; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Helper class for Vector3 operations - /// - public static class Vector3Helper - { - /// - /// Parses a JArray into a Vector3 - /// - /// The array containing x, y, z coordinates - /// A Vector3 with the parsed coordinates - /// Thrown when array is invalid - public static Vector3 ParseVector3(JArray array) - { - if (array == null || array.Count != 3) - throw new System.Exception("Vector3 must be an array of 3 floats [x, y, z]."); - return new Vector3((float)array[0], (float)array[1], (float)array[2]); - } - } -} diff --git a/MCPForUnity/Editor/Helpers/Vector3Helper.cs.meta b/MCPForUnity/Editor/Helpers/Vector3Helper.cs.meta deleted file mode 100644 index 280381ca..00000000 --- a/MCPForUnity/Editor/Helpers/Vector3Helper.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: f8514fd42f23cb641a36e52550825b35 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MCPForUnity/Editor/MCPForUnityMenu.cs b/MCPForUnity/Editor/MCPForUnityMenu.cs new file mode 100644 index 00000000..714e4853 --- /dev/null +++ b/MCPForUnity/Editor/MCPForUnityMenu.cs @@ -0,0 +1,75 @@ +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Setup; +using MCPForUnity.Editor.Windows; +using UnityEditor; + +namespace MCPForUnity.Editor +{ + /// + /// Centralized menu items for MCP For Unity + /// + public static class MCPForUnityMenu + { + // ======================================== + // Main Menu Items + // ======================================== + + /// + /// Show the setup wizard + /// + [MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)] + public static void ShowSetupWizard() + { + SetupWizard.ShowSetupWizard(); + } + + /// + /// Open the main MCP For Unity window + /// + [MenuItem("Window/MCP For Unity/Open MCP Window %#m", priority = 2)] + public static void OpenMCPWindow() + { + MCPForUnityEditorWindow.ShowWindow(); + } + + // ======================================== + // Tool Sync Menu Items + // ======================================== + + /// + /// Reimport all Python files in the project + /// + [MenuItem("Window/MCP For Unity/Tool Sync/Reimport Python Files", priority = 99)] + public static void ReimportPythonFiles() + { + PythonToolSyncProcessor.ReimportPythonFiles(); + } + + /// + /// Manually sync Python tools to the MCP server + /// + [MenuItem("Window/MCP For Unity/Tool Sync/Sync Python Tools", priority = 100)] + public static void SyncPythonTools() + { + PythonToolSyncProcessor.ManualSync(); + } + + /// + /// Toggle auto-sync for Python tools + /// + [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", priority = 101)] + public static void ToggleAutoSync() + { + PythonToolSyncProcessor.ToggleAutoSync(); + } + + /// + /// Validate menu item (shows checkmark when auto-sync is enabled) + /// + [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", true, priority = 101)] + public static bool ToggleAutoSyncValidate() + { + return PythonToolSyncProcessor.ToggleAutoSyncValidate(); + } + } +} diff --git a/MCPForUnity/Editor/Data/DefaultServerConfig.cs.meta b/MCPForUnity/Editor/MCPForUnityMenu.cs.meta similarity index 83% rename from MCPForUnity/Editor/Data/DefaultServerConfig.cs.meta rename to MCPForUnity/Editor/MCPForUnityMenu.cs.meta index 82e437f2..af82a270 100644 --- a/MCPForUnity/Editor/Data/DefaultServerConfig.cs.meta +++ b/MCPForUnity/Editor/MCPForUnityMenu.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: de8f5721c34f7194392e9d8c7d0226c0 +guid: 42b27c415aa084fe6a9cc6cf03979d36 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/MCPForUnity/Editor/Models/ServerConfig.cs b/MCPForUnity/Editor/Models/ServerConfig.cs deleted file mode 100644 index 4b185f1f..00000000 --- a/MCPForUnity/Editor/Models/ServerConfig.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace MCPForUnity.Editor.Models -{ - [Serializable] - public class ServerConfig - { - [JsonProperty("unity_host")] - public string unityHost = "localhost"; - - [JsonProperty("unity_port")] - public int unityPort; - - [JsonProperty("mcp_port")] - public int mcpPort; - - [JsonProperty("connection_timeout")] - public float connectionTimeout; - - [JsonProperty("buffer_size")] - public int bufferSize; - - [JsonProperty("log_level")] - public string logLevel; - - [JsonProperty("log_format")] - public string logFormat; - - [JsonProperty("max_retries")] - public int maxRetries; - - [JsonProperty("retry_delay")] - public float retryDelay; - } -} diff --git a/MCPForUnity/Editor/Models/ServerConfig.cs.meta b/MCPForUnity/Editor/Models/ServerConfig.cs.meta deleted file mode 100644 index 6e675e9e..00000000 --- a/MCPForUnity/Editor/Models/ServerConfig.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e4e45386fcc282249907c2e3c7e5d9c6 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs b/MCPForUnity/Editor/Services/ClientConfigurationService.cs index dea5358a..8a9c4caf 100644 --- a/MCPForUnity/Editor/Services/ClientConfigurationService.cs +++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs @@ -176,9 +176,9 @@ public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true) if (configExists) { - string configuredDir = McpConfigFileHelper.ExtractDirectoryArg(args); + string configuredDir = McpConfigurationHelper.ExtractDirectoryArg(args); bool matches = !string.IsNullOrEmpty(configuredDir) && - McpConfigFileHelper.PathsEqual(configuredDir, pythonDir); + McpConfigurationHelper.PathsEqual(configuredDir, pythonDir); if (matches) { @@ -396,7 +396,7 @@ public string GenerateConfigJson(McpClient client) if (client.mcpType == McpTypes.Codex) { return CodexConfigHelper.BuildCodexServerBlock(uvPath, - McpConfigFileHelper.ResolveServerDirectory(pythonDir, null)); + McpConfigurationHelper.ResolveServerDirectory(pythonDir, null)); } else { diff --git a/MCPForUnity/Editor/Services/IPlatformService.cs b/MCPForUnity/Editor/Services/IPlatformService.cs new file mode 100644 index 00000000..ec686b24 --- /dev/null +++ b/MCPForUnity/Editor/Services/IPlatformService.cs @@ -0,0 +1,20 @@ +namespace MCPForUnity.Editor.Services +{ + /// + /// Service for platform detection and platform-specific environment access + /// + public interface IPlatformService + { + /// + /// Checks if the current platform is Windows + /// + /// True if running on Windows + bool IsWindows(); + + /// + /// Gets the SystemRoot environment variable (Windows-specific) + /// + /// SystemRoot path, or null if not available + string GetSystemRoot(); + } +} diff --git a/MCPForUnity/Editor/Helpers/PackageInstaller.cs.meta b/MCPForUnity/Editor/Services/IPlatformService.cs.meta similarity index 83% rename from MCPForUnity/Editor/Helpers/PackageInstaller.cs.meta rename to MCPForUnity/Editor/Services/IPlatformService.cs.meta index 156e75fb..e501f58a 100644 --- a/MCPForUnity/Editor/Helpers/PackageInstaller.cs.meta +++ b/MCPForUnity/Editor/Services/IPlatformService.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 19e6eaa637484e9fa19f9a0459809de2 +guid: 1d90ff7f9a1e84c9bbbbedee2f7eda2a MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/MCPForUnity/Editor/Services/MCPServiceLocator.cs b/MCPForUnity/Editor/Services/MCPServiceLocator.cs index 2a7f070c..a743d4ce 100644 --- a/MCPForUnity/Editor/Services/MCPServiceLocator.cs +++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs @@ -14,6 +14,7 @@ public static class MCPServiceLocator private static ITestRunnerService _testRunnerService; private static IToolSyncService _toolSyncService; private static IPackageUpdateService _packageUpdateService; + private static IPlatformService _platformService; public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService(); public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService(); @@ -22,6 +23,7 @@ public static class MCPServiceLocator public static ITestRunnerService Tests => _testRunnerService ??= new TestRunnerService(); public static IToolSyncService ToolSync => _toolSyncService ??= new ToolSyncService(); public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService(); + public static IPlatformService Platform => _platformService ??= new PlatformService(); /// /// Registers a custom implementation for a service (useful for testing) @@ -44,6 +46,8 @@ public static void Register(T implementation) where T : class _toolSyncService = ts; else if (implementation is IPackageUpdateService pu) _packageUpdateService = pu; + else if (implementation is IPlatformService ps) + _platformService = ps; } /// @@ -58,6 +62,7 @@ public static void Reset() (_testRunnerService as IDisposable)?.Dispose(); (_toolSyncService as IDisposable)?.Dispose(); (_packageUpdateService as IDisposable)?.Dispose(); + (_platformService as IDisposable)?.Dispose(); _bridgeService = null; _clientService = null; @@ -66,6 +71,7 @@ public static void Reset() _testRunnerService = null; _toolSyncService = null; _packageUpdateService = null; + _platformService = null; } } } diff --git a/MCPForUnity/Editor/Services/PlatformService.cs b/MCPForUnity/Editor/Services/PlatformService.cs new file mode 100644 index 00000000..6e663717 --- /dev/null +++ b/MCPForUnity/Editor/Services/PlatformService.cs @@ -0,0 +1,31 @@ +using System; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Default implementation of platform detection service + /// + public class PlatformService : IPlatformService + { + /// + /// Checks if the current platform is Windows + /// + /// True if running on Windows + public bool IsWindows() + { + return Environment.OSVersion.Platform == PlatformID.Win32NT; + } + + /// + /// Gets the SystemRoot environment variable (Windows-specific) + /// + /// SystemRoot path, or "C:\\Windows" as fallback on Windows, null on other platforms + public string GetSystemRoot() + { + if (!IsWindows()) + return null; + + return Environment.GetEnvironmentVariable("SystemRoot") ?? "C:\\Windows"; + } + } +} diff --git a/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs.meta b/MCPForUnity/Editor/Services/PlatformService.cs.meta similarity index 83% rename from MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs.meta rename to MCPForUnity/Editor/Services/PlatformService.cs.meta index 8f81ae99..172daf8f 100644 --- a/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs.meta +++ b/MCPForUnity/Editor/Services/PlatformService.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: f69ad468942b74c0ea24e3e8e5f21a4b +guid: 3b2d7f32a595c45dd8c01f141c69761c MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/MCPForUnity/Editor/Setup/SetupWizard.cs b/MCPForUnity/Editor/Setup/SetupWizard.cs index 691e482f..7bb77ded 100644 --- a/MCPForUnity/Editor/Setup/SetupWizard.cs +++ b/MCPForUnity/Editor/Setup/SetupWizard.cs @@ -97,63 +97,5 @@ public static void MarkSetupDismissed() 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 %#m", priority = 4)] - public static void OpenClientConfiguration() - { - Windows.MCPForUnityEditorWindowNew.ShowWindow(); - } - - /// - /// Open legacy MCP Client Configuration window - /// - [MenuItem("Window/MCP For Unity/Open Legacy MCP Window", priority = 5)] - public static void OpenLegacyClientConfiguration() - { - Windows.MCPForUnityEditorWindow.ShowWindow(); - } } } diff --git a/MCPForUnity/Editor/Setup/SetupWizardWindow.cs b/MCPForUnity/Editor/Setup/SetupWizardWindow.cs index 7229be97..40599c8e 100644 --- a/MCPForUnity/Editor/Setup/SetupWizardWindow.cs +++ b/MCPForUnity/Editor/Setup/SetupWizardWindow.cs @@ -18,12 +18,9 @@ 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" }; @@ -42,14 +39,6 @@ private void OnEnable() { _dependencyResult = DependencyManager.CheckAllDependencies(); } - - _mcpClients = new McpClients(); - - // Check client configurations on startup - foreach (var client in _mcpClients.clients) - { - CheckClientConfiguration(client); - } } private void OnGUI() @@ -62,8 +51,7 @@ private void OnGUI() switch (_currentStep) { case 0: DrawSetupStep(); break; - case 1: DrawConfigureStep(); break; - case 2: DrawCompleteStep(); break; + case 1: DrawCompleteStep(); break; } EditorGUILayout.EndScrollView(); @@ -132,7 +120,7 @@ private void DrawSetupStep() { // 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.", + "\u26A0 Missing Dependencies: MCP for Unity requires Python 3.11+ and UV package manager to function properly.", MessageType.Warning ); @@ -157,8 +145,6 @@ private void DrawSetupStep() } } - - private void DrawCompleteStep() { DrawSectionTitle("Setup Complete"); @@ -273,85 +259,6 @@ private void DrawSimpleDependencyStatus(DependencyStatus dep) 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(); @@ -371,7 +278,7 @@ private void DrawFooter() { bool dismiss = EditorUtility.DisplayDialog( "Skip Setup", - "⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" + + "\u26A0 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" @@ -405,295 +312,6 @@ private void DrawFooter() 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(); diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs index fdbfcbb5..b8fb3c3a 100644 --- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs @@ -1,1669 +1,867 @@ using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Security.Cryptography; -using System.Text; -using System.Net.Sockets; -using System.Net; using System.IO; using System.Linq; using System.Runtime.InteropServices; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using UnityEditor; +using UnityEditor.UIElements; // For Unity 2021 compatibility using UnityEngine; +using UnityEngine.UIElements; using MCPForUnity.Editor.Data; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; +using MCPForUnity.Editor.Services; namespace MCPForUnity.Editor.Windows { public class MCPForUnityEditorWindow : EditorWindow { - private bool isUnityBridgeRunning = false; - private Vector2 scrollPosition; - private string pythonServerInstallationStatus = "Not Installed"; - private Color pythonServerInstallationStatusColor = Color.red; - private const int mcpPort = 6500; // MCP port (still hardcoded for MCP server) + // Protocol enum for future HTTP support + private enum ConnectionProtocol + { + Stdio, + // HTTPStreaming // Future + } + + // Settings UI Elements + private Label versionLabel; + private Toggle debugLogsToggle; + private EnumField validationLevelField; + private Label validationDescription; + private Foldout advancedSettingsFoldout; + private TextField mcpServerPathOverride; + private TextField uvPathOverride; + private Button browsePythonButton; + private Button clearPythonButton; + private Button browseUvButton; + private Button clearUvButton; + private VisualElement mcpServerPathStatus; + private VisualElement uvPathStatus; + + // Connection UI Elements + private EnumField protocolDropdown; + private TextField unityPortField; + private TextField serverPortField; + private VisualElement statusIndicator; + private Label connectionStatusLabel; + private Button connectionToggleButton; + private VisualElement healthIndicator; + private Label healthStatusLabel; + private Button testConnectionButton; + private VisualElement serverStatusBanner; + private Label serverStatusMessage; + private Button downloadServerButton; + private Button rebuildServerButton; + + // Client UI Elements + private DropdownField clientDropdown; + private Button configureAllButton; + private VisualElement clientStatusIndicator; + private Label clientStatusLabel; + private Button configureButton; + private VisualElement claudeCliPathRow; + private TextField claudeCliPath; + private Button browseClaudeButton; + private Foldout manualConfigFoldout; + private TextField configPathField; + private Button copyPathButton; + private Button openFileButton; + private TextField configJsonField; + private Button copyJsonButton; + private Label installationStepsLabel; + + // Data private readonly McpClients mcpClients = new(); - private bool autoRegisterEnabled; - private bool lastClientRegisteredOk; - private bool lastBridgeVerifiedOk; - private string pythonDirOverride = null; - private bool debugLogsEnabled; - - // Script validation settings - private int validationLevelIndex = 1; // Default to Standard - private readonly string[] validationLevelOptions = new string[] - { - "Basic - Only syntax checks", - "Standard - Syntax + Unity practices", - "Comprehensive - All checks + semantic analysis", - "Strict - Full semantic validation (requires Roslyn)" - }; - - // UI state private int selectedClientIndex = 0; + private ValidationLevel currentValidationLevel = ValidationLevel.Standard; - public static void ShowWindow() + // Validation levels matching the existing enum + private enum ValidationLevel { - GetWindow("MCP For Unity"); + Basic, + Standard, + Comprehensive, + Strict } - private void OnEnable() + public static void ShowWindow() + { + var window = GetWindow("MCP For Unity"); + window.minSize = new Vector2(500, 600); + } + public void CreateGUI() { - UpdatePythonServerInstallationStatus(); + // Determine base path (Package Manager vs Asset Store install) + string basePath = AssetPathUtility.GetMcpPackageRootPath(); - // Refresh bridge status - isUnityBridgeRunning = MCPForUnityBridge.IsRunning; - autoRegisterEnabled = EditorPrefs.GetBool("MCPForUnity.AutoRegisterEnabled", true); - debugLogsEnabled = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); - if (debugLogsEnabled) - { - LogDebugPrefsState(); - } - foreach (McpClient mcpClient in mcpClients.clients) + // Load UXML + var visualTree = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml" + ); + + if (visualTree == null) { - CheckMcpConfiguration(mcpClient); + McpLog.Error($"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml"); + return; } - // Load validation level setting - LoadValidationLevelSetting(); + visualTree.CloneTree(rootVisualElement); - // First-run auto-setup only if Claude CLI is available - if (autoRegisterEnabled && !string.IsNullOrEmpty(ExecPath.ResolveClaude())) - { - AutoFirstRunSetup(); - } - } + // Load USS + var styleSheet = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uss" + ); - private void OnFocus() - { - // Refresh bridge running state on focus in case initialization completed after domain reload - isUnityBridgeRunning = MCPForUnityBridge.IsRunning; - if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count) + if (styleSheet != null) { - McpClient selectedClient = mcpClients.clients[selectedClientIndex]; - CheckMcpConfiguration(selectedClient); + rootVisualElement.styleSheets.Add(styleSheet); } - Repaint(); + + // Cache UI elements + CacheUIElements(); + + // Initialize UI + InitializeUI(); + + // Register callbacks + RegisterCallbacks(); + + // Initial update + UpdateConnectionStatus(); + UpdateServerStatusBanner(); + UpdateClientStatus(); + UpdatePathOverrides(); + // Technically not required to connect, but if we don't do this, the UI will be blank + UpdateManualConfiguration(); + UpdateClaudeCliPathVisibility(); } - private Color GetStatusColor(McpStatus status) + private void OnEnable() { - // Return appropriate color based on the status enum - return 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, // Default to red for error states or not configured - }; + EditorApplication.update += OnEditorUpdate; } - private void UpdatePythonServerInstallationStatus() + private void OnDisable() { - try - { - string installedPath = ServerInstaller.GetServerPath(); - bool installedOk = !string.IsNullOrEmpty(installedPath) && File.Exists(Path.Combine(installedPath, "server.py")); - if (installedOk) - { - pythonServerInstallationStatus = "Installed"; - pythonServerInstallationStatusColor = Color.green; - return; - } - - // Fall back to embedded/dev source via our existing resolution logic - string embeddedPath = FindPackagePythonDirectory(); - bool embeddedOk = !string.IsNullOrEmpty(embeddedPath) && File.Exists(Path.Combine(embeddedPath, "server.py")); - if (embeddedOk) - { - pythonServerInstallationStatus = "Installed (Embedded)"; - pythonServerInstallationStatusColor = Color.green; - } - else - { - pythonServerInstallationStatus = "Not Installed"; - pythonServerInstallationStatusColor = Color.red; - } - } - catch - { - pythonServerInstallationStatus = "Not Installed"; - pythonServerInstallationStatusColor = Color.red; - } + EditorApplication.update -= OnEditorUpdate; } - - private void DrawStatusDot(Rect statusRect, Color statusColor, float size = 12) + private void OnFocus() { - float offsetX = (statusRect.width - size) / 2; - float offsetY = (statusRect.height - size) / 2; - Rect dotRect = new(statusRect.x + offsetX, statusRect.y + offsetY, size, size); - Vector3 center = new( - dotRect.x + (dotRect.width / 2), - dotRect.y + (dotRect.height / 2), - 0 - ); - float radius = size / 2; - - // Draw the main dot - Handles.color = statusColor; - Handles.DrawSolidDisc(center, Vector3.forward, radius); + // Only refresh data if UI is built + if (rootVisualElement == null || rootVisualElement.childCount == 0) + return; - // Draw the border - Color borderColor = new( - statusColor.r * 0.7f, - statusColor.g * 0.7f, - statusColor.b * 0.7f - ); - Handles.color = borderColor; - Handles.DrawWireDisc(center, Vector3.forward, radius); + RefreshAllData(); } - private void OnGUI() + private void OnEditorUpdate() { - scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); - - // Header - DrawHeader(); - - // Compute equal column widths for uniform layout - float horizontalSpacing = 2f; - float outerPadding = 20f; // approximate padding - // Make columns a bit less wide for a tighter layout - float computed = (position.width - outerPadding - horizontalSpacing) / 2f; - float colWidth = Mathf.Clamp(computed, 220f, 340f); - // Use fixed heights per row so paired panels match exactly - float topPanelHeight = 190f; - float bottomPanelHeight = 230f; - - // Top row: Server Status (left) and Unity Bridge (right) - EditorGUILayout.BeginHorizontal(); - { - EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight)); - DrawServerStatusSection(); - EditorGUILayout.EndVertical(); + // Only update UI if it's built + if (rootVisualElement == null || rootVisualElement.childCount == 0) + return; - EditorGUILayout.Space(horizontalSpacing); + UpdateConnectionStatus(); + } + + private void RefreshAllData() + { + // Update connection status + UpdateConnectionStatus(); - EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight)); - DrawBridgeSection(); - EditorGUILayout.EndVertical(); + // Auto-verify bridge health if connected + if (MCPServiceLocator.Bridge.IsRunning) + { + VerifyBridgeConnection(); } - EditorGUILayout.EndHorizontal(); - EditorGUILayout.Space(10); + // Update path overrides + UpdatePathOverrides(); - // Second row: MCP Client Configuration (left) and Script Validation (right) - EditorGUILayout.BeginHorizontal(); + // Refresh selected client (may have been configured externally) + if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count) { - EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight)); - DrawUnifiedClientConfiguration(); - EditorGUILayout.EndVertical(); - - EditorGUILayout.Space(horizontalSpacing); - - EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight)); - DrawValidationSection(); - EditorGUILayout.EndVertical(); + var client = mcpClients.clients[selectedClientIndex]; + MCPServiceLocator.Client.CheckClientStatus(client); + UpdateClientStatus(); + UpdateManualConfiguration(); + UpdateClaudeCliPathVisibility(); } - EditorGUILayout.EndHorizontal(); + } - // Minimal bottom padding - EditorGUILayout.Space(2); + private void CacheUIElements() + { + // Settings + versionLabel = rootVisualElement.Q