From bbf6cacfe257d9c5a447835297644baef0400eb5 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Fri, 24 Oct 2025 00:50:29 -0400 Subject: [PATCH] Remove old UI and do lots of cleanup (#340) * Remove legacy UI and correct priority ordering of menu items * Remove old UI screen Users now have the new UI alone, less confusing and more predictable * Remove unused config files * Remove test for window that doesn't exist * Remove unused code * Remove dangling .meta file * refactor: remove client configuration step from setup wizard * refactor: remove menu item attributes and manual window actions from Python tool sync * feat: update minimum Python version requirement from 3.10 to 3.11 The docs have 3.12. However, feature wise it seems that 3.11 is required * fix: replace emoji warning symbol with unicode character in setup wizard dialogs * docs: reorganize images into docs/images directory and update references * docs: add UI preview image to README * docs: add run_test function and resources section to available tools list The recent changes should close #311 * fix: add SystemRoot env var to Windows config to support Python path resolution Closes #315 * refactor: consolidate package installation and detection into unified lifecycle manager Duplicate code for pretty much no reason, as they both initialized there was a small chance of a race condition as well. Consolidating made sense here * Doc fixes from CodeRabbit * Excellent bug catch from CodeRabbit * fix: preserve existing environment variables when updating codex server config * Update docs so the paths match the original name * style: fix list indentation in README-DEV.md development docs * refactor: simplify env table handling in CodexConfigHelper by removing preservation logic * refactor: simplify configuration logic by removing redundant change detection Always overwrite configs * feat: ensure config directory exists before writing config files * feat: persist server installation errors and show retry UI instead of auto-marking as handled * refactor: consolidate configuration helpers by merging McpConfigFileHelper into McpConfigurationHelper * Small fixes from CodeRabbit * Remove test because we overwrite Codex configs * Remove unused function * feat: improve server cleanup and process handling on Windows - Added DeleteDirectoryWithRetry helper to handle Windows file locking with retries and readonly attribute clearing - Implemented KillWindowsUvProcesses to safely terminate Python processes in virtual environments using WMIC - Extended TryKillUvForPath to work on Windows, preventing file handle locks during server deletion - Improved error messages to be more descriptive about file locking issues - Replaced direct Directory.Delete calls with * fix: improve TCP socket cleanup to prevent CLOSE_WAIT states - Added proper socket shutdown sequence using Socket.Shutdown() before closing connections - Enhanced error handling with specific catches for SocketException vs general exceptions - Added debug logging for socket shutdown errors to help diagnose connection issues - Restructured HandleClientAsync to ensure socket cleanup happens in the correct order - Implemented proper socket teardown in both client handling and connection cleanup paths --- .../Editor/Data/DefaultServerConfig.cs | 17 - .../Editor/Dependencies/DependencyManager.cs | 2 +- .../LinuxPlatformDetector.cs | 6 +- .../MacOSPlatformDetector.cs | 9 +- .../WindowsPlatformDetector.cs | 6 +- .../Editor/Helpers/CodexConfigHelper.cs | 16 +- .../Editor/Helpers/McpConfigFileHelper.cs | 186 -- .../Editor/Helpers/McpConfigurationHelper.cs | 221 +- MCPForUnity/Editor/Helpers/McpPathResolver.cs | 2 +- MCPForUnity/Editor/Helpers/PackageDetector.cs | 106 - .../Editor/Helpers/PackageInstaller.cs | 46 - .../Editor/Helpers/PackageLifecycleManager.cs | 240 ++ ...s.meta => PackageLifecycleManager.cs.meta} | 2 +- .../Editor/Helpers/PythonToolSyncProcessor.cs | 10 +- MCPForUnity/Editor/Helpers/ServerInstaller.cs | 195 +- MCPForUnity/Editor/Helpers/Vector3Helper.cs | 24 - .../Editor/Helpers/Vector3Helper.cs.meta | 11 - MCPForUnity/Editor/MCPForUnityMenu.cs | 75 + ...Config.cs.meta => MCPForUnityMenu.cs.meta} | 2 +- MCPForUnity/Editor/Models/ServerConfig.cs | 36 - .../Editor/Models/ServerConfig.cs.meta | 11 - .../Services/ClientConfigurationService.cs | 6 +- .../Editor/Services/IPlatformService.cs | 20 + .../IPlatformService.cs.meta} | 2 +- .../Editor/Services/MCPServiceLocator.cs | 6 + .../Editor/Services/PlatformService.cs | 31 + .../PlatformService.cs.meta} | 2 +- MCPForUnity/Editor/Setup/SetupWizard.cs | 58 - MCPForUnity/Editor/Setup/SetupWizardWindow.cs | 388 +-- .../Editor/Windows/MCPForUnityEditorWindow.cs | 2336 ++++++----------- .../Windows/MCPForUnityEditorWindow.cs.meta | 8 +- ...dowNew.uss => MCPForUnityEditorWindow.uss} | 0 ....meta => MCPForUnityEditorWindow.uss.meta} | 0 ...wNew.uxml => MCPForUnityEditorWindow.uxml} | 0 ...meta => MCPForUnityEditorWindow.uxml.meta} | 0 .../Windows/MCPForUnityEditorWindowNew.cs | 860 ------ .../MCPForUnityEditorWindowNew.cs.meta | 11 - .../Windows/ManualConfigEditorWindow.cs | 303 --- .../Windows/ManualConfigEditorWindow.cs.meta | 11 - .../Editor/Windows/VSCodeManualSetupWindow.cs | 291 -- .../Windows/VSCodeManualSetupWindow.cs.meta | 11 - .../UnityMcpServer~/src/pyproject.toml | 2 +- MCPForUnity/UnityMcpServer~/src/uv.lock | 759 +++++- README.md | 128 +- .../Helpers/CodexConfigHelperTests.cs | 220 ++ .../Helpers/PackageLifecycleManagerTests.cs | 166 ++ .../PackageLifecycleManagerTests.cs.meta | 11 + .../Assets/Tests/EditMode/Windows.meta | 8 - .../Windows/ManualConfigJsonBuilderTests.cs | 54 - .../ManualConfigJsonBuilderTests.cs.meta | 11 - UnityMcpBridge/Editor/MCPForUnityBridge.cs | 68 +- docs/CUSTOM_TOOLS.md | 4 +- docs/README-DEV.md | 45 +- docs/images/coplay-logo.png | Bin 0 -> 40503 bytes logo.png => docs/images/logo.png | Bin docs/images/readme_ui.png | Bin 0 -> 630654 bytes .../v5_01_uninstall.png | Bin .../{screenshots => images}/v5_02_install.png | Bin .../v5_03_open_mcp_window.png | Bin .../v5_04_rebuild_mcp_server.png | Bin .../v5_05_rebuild_success.png | Bin .../v6_2_create_python_tools_asset.png | Bin .../v6_2_python_tools_asset.png | Bin .../v6_new_ui_asset_store_version.png | Bin .../v6_new_ui_dark.png | Bin .../v6_new_ui_light.png | Bin docs/v5_MIGRATION.md | 10 +- docs/v6_NEW_UI_CHANGES.md | 6 +- 68 files changed, 2772 insertions(+), 4287 deletions(-) delete mode 100644 MCPForUnity/Editor/Data/DefaultServerConfig.cs delete mode 100644 MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs delete mode 100644 MCPForUnity/Editor/Helpers/PackageDetector.cs delete mode 100644 MCPForUnity/Editor/Helpers/PackageInstaller.cs create mode 100644 MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs rename MCPForUnity/Editor/Helpers/{PackageDetector.cs.meta => PackageLifecycleManager.cs.meta} (83%) delete mode 100644 MCPForUnity/Editor/Helpers/Vector3Helper.cs delete mode 100644 MCPForUnity/Editor/Helpers/Vector3Helper.cs.meta create mode 100644 MCPForUnity/Editor/MCPForUnityMenu.cs rename MCPForUnity/Editor/{Data/DefaultServerConfig.cs.meta => MCPForUnityMenu.cs.meta} (83%) delete mode 100644 MCPForUnity/Editor/Models/ServerConfig.cs delete mode 100644 MCPForUnity/Editor/Models/ServerConfig.cs.meta create mode 100644 MCPForUnity/Editor/Services/IPlatformService.cs rename MCPForUnity/Editor/{Helpers/PackageInstaller.cs.meta => Services/IPlatformService.cs.meta} (83%) create mode 100644 MCPForUnity/Editor/Services/PlatformService.cs rename MCPForUnity/Editor/{Helpers/McpConfigFileHelper.cs.meta => Services/PlatformService.cs.meta} (83%) rename MCPForUnity/Editor/Windows/{MCPForUnityEditorWindowNew.uss => MCPForUnityEditorWindow.uss} (100%) rename MCPForUnity/Editor/Windows/{MCPForUnityEditorWindowNew.uss.meta => MCPForUnityEditorWindow.uss.meta} (100%) rename MCPForUnity/Editor/Windows/{MCPForUnityEditorWindowNew.uxml => MCPForUnityEditorWindow.uxml} (100%) rename MCPForUnity/Editor/Windows/{MCPForUnityEditorWindowNew.uxml.meta => MCPForUnityEditorWindow.uxml.meta} (100%) delete mode 100644 MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs delete mode 100644 MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs.meta delete mode 100644 MCPForUnity/Editor/Windows/ManualConfigEditorWindow.cs delete mode 100644 MCPForUnity/Editor/Windows/ManualConfigEditorWindow.cs.meta delete mode 100644 MCPForUnity/Editor/Windows/VSCodeManualSetupWindow.cs delete mode 100644 MCPForUnity/Editor/Windows/VSCodeManualSetupWindow.cs.meta create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/PackageLifecycleManagerTests.cs create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/PackageLifecycleManagerTests.cs.meta delete mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows.meta delete mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs delete mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs.meta create mode 100644 docs/images/coplay-logo.png rename logo.png => docs/images/logo.png (100%) create mode 100644 docs/images/readme_ui.png rename docs/{screenshots => images}/v5_01_uninstall.png (100%) rename docs/{screenshots => images}/v5_02_install.png (100%) rename docs/{screenshots => images}/v5_03_open_mcp_window.png (100%) rename docs/{screenshots => images}/v5_04_rebuild_mcp_server.png (100%) rename docs/{screenshots => images}/v5_05_rebuild_success.png (100%) rename docs/{screenshots => images}/v6_2_create_python_tools_asset.png (100%) rename docs/{screenshots => images}/v6_2_python_tools_asset.png (100%) rename docs/{screenshots => images}/v6_new_ui_asset_store_version.png (100%) rename docs/{screenshots => images}/v6_new_ui_dark.png (100%) rename docs/{screenshots => images}/v6_new_ui_light.png (100%) diff --git a/MCPForUnity/Editor/Data/DefaultServerConfig.cs b/MCPForUnity/Editor/Data/DefaultServerConfig.cs deleted file mode 100644 index 59cced7..0000000 --- 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 ce6efef..d5a082a 100644 --- a/MCPForUnity/Editor/Dependencies/DependencyManager.cs +++ b/MCPForUnity/Editor/Dependencies/DependencyManager.cs @@ -126,7 +126,7 @@ namespace MCPForUnity.Editor.Dependencies { 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 f654612..a128174 100644 --- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs @@ -62,7 +62,7 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors } } - 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 @@ Note: Make sure ~/.local/bin is in your PATH for user-local installations."; 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 c9d152d..64e9a50 100644 --- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs @@ -35,8 +35,7 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors "/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 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors } } - 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 @@ Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH."; 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 7891c6e..bcb2c7d 100644 --- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs @@ -68,7 +68,7 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors } } - 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 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors 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 d3d77dc..a472890 100644 --- a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs +++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using MCPForUnity.External.Tommy; +using MCPForUnity.Editor.Services; namespace MCPForUnity.Editor.Helpers { @@ -26,10 +27,10 @@ namespace MCPForUnity.Editor.Helpers 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 @@ namespace MCPForUnity.Editor.Helpers /// /// 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 @@ namespace MCPForUnity.Editor.Helpers 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 389d47d..0000000 --- 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 d88bdbc..96ad7ec 100644 --- a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs +++ b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs @@ -2,6 +2,7 @@ using System; 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 @@ namespace MCPForUnity.Editor.Helpers } 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 @@ namespace MCPForUnity.Editor.Helpers string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); - McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson); + EnsureConfigDirectoryExists(configPath); + WriteAtomicFile(configPath, mergedJson); try { @@ -190,24 +181,12 @@ namespace MCPForUnity.Editor.Helpers 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 @@ namespace MCPForUnity.Editor.Helpers 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 @@ namespace MCPForUnity.Editor.Helpers { 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 be1089f..04082a9 100644 --- a/MCPForUnity/Editor/Helpers/McpPathResolver.cs +++ b/MCPForUnity/Editor/Helpers/McpPathResolver.cs @@ -20,7 +20,7 @@ namespace MCPForUnity.Editor.Helpers /// 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 59e2234..0000000 --- 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 1d46f32..0000000 --- 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 0000000..02e482c --- /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 f1a5dbe..f1e14f7 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 fce0e78..de6167a 100644 --- a/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs +++ b/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs @@ -139,9 +139,8 @@ namespace MCPForUnity.Editor.Helpers } /// - /// 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 @@ namespace MCPForUnity.Editor.Helpers } /// - /// 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 @@ namespace MCPForUnity.Editor.Helpers } /// - /// 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 @@ namespace MCPForUnity.Editor.Helpers /// /// 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 2b0c8f4..1066634 100644 --- a/MCPForUnity/Editor/Helpers/ServerInstaller.cs +++ b/MCPForUnity/Editor/Helpers/ServerInstaller.cs @@ -84,14 +84,13 @@ namespace MCPForUnity.Editor.Helpers 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 @@ namespace MCPForUnity.Editor.Helpers 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 @@ namespace MCPForUnity.Editor.Helpers 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 @@ namespace MCPForUnity.Editor.Helpers // 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 @@ namespace MCPForUnity.Editor.Helpers } 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 @@ namespace MCPForUnity.Editor.Helpers // Delete old installation if (Directory.Exists(destRoot)) { - try + if (!DeleteDirectoryWithRetry(destRoot, maxRetries: 5, delayMs: 1000)) { - Directory.Delete(destRoot, recursive: true); - } - catch (Exception ex) - { - 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 @@ namespace MCPForUnity.Editor.Helpers } 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 4156618..0000000 --- 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 280381c..0000000 --- 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 0000000..714e485 --- /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 82e437f..af82a27 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 4b185f1..0000000 --- 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 6e675e9..0000000 --- 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 dea5358..8a9c4ca 100644 --- a/MCPForUnity/Editor/Services/ClientConfigurationService.cs +++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs @@ -176,9 +176,9 @@ namespace MCPForUnity.Editor.Services 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 @@ namespace MCPForUnity.Editor.Services 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 0000000..ec686b2 --- /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 156e75f..e501f58 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 2a7f070..a743d4c 100644 --- a/MCPForUnity/Editor/Services/MCPServiceLocator.cs +++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs @@ -14,6 +14,7 @@ namespace MCPForUnity.Editor.Services 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 @@ namespace MCPForUnity.Editor.Services 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 @@ namespace MCPForUnity.Editor.Services _toolSyncService = ts; else if (implementation is IPackageUpdateService pu) _packageUpdateService = pu; + else if (implementation is IPlatformService ps) + _platformService = ps; } /// @@ -58,6 +62,7 @@ namespace MCPForUnity.Editor.Services (_testRunnerService as IDisposable)?.Dispose(); (_toolSyncService as IDisposable)?.Dispose(); (_packageUpdateService as IDisposable)?.Dispose(); + (_platformService as IDisposable)?.Dispose(); _bridgeService = null; _clientService = null; @@ -66,6 +71,7 @@ namespace MCPForUnity.Editor.Services _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 0000000..6e66371 --- /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 8f81ae9..172daf8 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 691e482..7bb77de 100644 --- a/MCPForUnity/Editor/Setup/SetupWizard.cs +++ b/MCPForUnity/Editor/Setup/SetupWizard.cs @@ -97,63 +97,5 @@ namespace MCPForUnity.Editor.Setup 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 7229be9..40599c8 100644 --- a/MCPForUnity/Editor/Setup/SetupWizardWindow.cs +++ b/MCPForUnity/Editor/Setup/SetupWizardWindow.cs @@ -18,12 +18,9 @@ namespace MCPForUnity.Editor.Setup 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 @@ namespace MCPForUnity.Editor.Setup { _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 @@ namespace MCPForUnity.Editor.Setup switch (_currentStep) { case 0: DrawSetupStep(); break; - case 1: DrawConfigureStep(); break; - case 2: DrawCompleteStep(); break; + case 1: DrawCompleteStep(); break; } EditorGUILayout.EndScrollView(); @@ -132,7 +120,7 @@ namespace MCPForUnity.Editor.Setup { // 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 @@ namespace MCPForUnity.Editor.Setup } } - - private void DrawCompleteStep() { DrawSectionTitle("Setup Complete"); @@ -273,85 +259,6 @@ namespace MCPForUnity.Editor.Setup 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 @@ namespace MCPForUnity.Editor.Setup { 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 @@ namespace MCPForUnity.Editor.Setup 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 fdbfcbb..b8fb3c3 100644 --- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs @@ -1,1214 +1,328 @@ 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) - 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[] + // Protocol enum for future HTTP support + private enum ConnectionProtocol { - "Basic - Only syntax checks", - "Standard - Syntax + Unity practices", - "Comprehensive - All checks + semantic analysis", - "Strict - Full semantic validation (requires Roslyn)" - }; + Stdio, + // HTTPStreaming // Future + } - // UI state + // 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 int selectedClientIndex = 0; + private ValidationLevel currentValidationLevel = ValidationLevel.Standard; + + // Validation levels matching the existing enum + private enum ValidationLevel + { + Basic, + Standard, + Comprehensive, + Strict + } public static void ShowWindow() { - GetWindow("MCP For Unity"); + var window = GetWindow("MCP For Unity"); + window.minSize = new Vector2(500, 600); + } + public void CreateGUI() + { + // Determine base path (Package Manager vs Asset Store install) + string basePath = AssetPathUtility.GetMcpPackageRootPath(); + + // Load UXML + var visualTree = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml" + ); + + if (visualTree == null) + { + McpLog.Error($"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml"); + return; + } + + visualTree.CloneTree(rootVisualElement); + + // Load USS + var styleSheet = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uss" + ); + + if (styleSheet != null) + { + rootVisualElement.styleSheets.Add(styleSheet); + } + + // 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 void OnEnable() { - UpdatePythonServerInstallationStatus(); + EditorApplication.update += OnEditorUpdate; + } - // 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) - { - CheckMcpConfiguration(mcpClient); - } - - // Load validation level setting - LoadValidationLevelSetting(); - - // First-run auto-setup only if Claude CLI is available - if (autoRegisterEnabled && !string.IsNullOrEmpty(ExecPath.ResolveClaude())) - { - AutoFirstRunSetup(); - } + private void OnDisable() + { + EditorApplication.update -= OnEditorUpdate; } 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) - { - McpClient selectedClient = mcpClients.clients[selectedClientIndex]; - CheckMcpConfiguration(selectedClient); - } - Repaint(); - } - - private Color GetStatusColor(McpStatus status) - { - // 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 - }; - } - - private void UpdatePythonServerInstallationStatus() - { - 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; - } - } - - - private void DrawStatusDot(Rect statusRect, Color statusColor, float size = 12) - { - 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); - - // 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); - } - - private void OnGUI() - { - 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(); - - EditorGUILayout.Space(horizontalSpacing); - - EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight)); - DrawBridgeSection(); - EditorGUILayout.EndVertical(); - } - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(10); - - // Second row: MCP Client Configuration (left) and Script Validation (right) - EditorGUILayout.BeginHorizontal(); - { - 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(); - } - EditorGUILayout.EndHorizontal(); - - // Minimal bottom padding - EditorGUILayout.Space(2); - - EditorGUILayout.EndScrollView(); - } - - private void DrawHeader() - { - EditorGUILayout.Space(15); - Rect titleRect = EditorGUILayout.GetControlRect(false, 40); - EditorGUI.DrawRect(titleRect, new Color(0.2f, 0.2f, 0.2f, 0.1f)); - - GUIStyle titleStyle = new GUIStyle(EditorStyles.boldLabel) - { - fontSize = 16, - alignment = TextAnchor.MiddleLeft - }; - - GUI.Label( - new Rect(titleRect.x + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height), - "MCP For Unity", - titleStyle - ); - - // Place the Show Debug Logs toggle on the same header row, right-aligned - float toggleWidth = 160f; - Rect toggleRect = new Rect(titleRect.xMax - toggleWidth - 12f, titleRect.y + 10f, toggleWidth, 20f); - bool newDebug = GUI.Toggle(toggleRect, debugLogsEnabled, "Show Debug Logs"); - if (newDebug != debugLogsEnabled) - { - debugLogsEnabled = newDebug; - EditorPrefs.SetBool("MCPForUnity.DebugLogs", debugLogsEnabled); - if (debugLogsEnabled) - { - LogDebugPrefsState(); - } - } - EditorGUILayout.Space(15); - } - - private void LogDebugPrefsState() - { - try - { - string pythonDirOverridePref = SafeGetPrefString("MCPForUnity.PythonDirOverride"); - string uvPathPref = SafeGetPrefString("MCPForUnity.UvPath"); - string serverSrcPref = SafeGetPrefString("MCPForUnity.ServerSrc"); - bool useEmbedded = SafeGetPrefBool("MCPForUnity.UseEmbeddedServer"); - - // Version-scoped detection key - string embeddedVer = ReadEmbeddedVersionOrFallback(); - string detectKey = $"MCPForUnity.LegacyDetectLogged:{embeddedVer}"; - bool detectLogged = SafeGetPrefBool(detectKey); - - // Project-scoped auto-register key - string projectPath = Application.dataPath ?? string.Empty; - string autoKey = $"MCPForUnity.AutoRegistered.{ComputeSha1(projectPath)}"; - bool autoRegistered = SafeGetPrefBool(autoKey); - - MCPForUnity.Editor.Helpers.McpLog.Info( - "MCP Debug Prefs:\n" + - $" DebugLogs: {debugLogsEnabled}\n" + - $" PythonDirOverride: '{pythonDirOverridePref}'\n" + - $" UvPath: '{uvPathPref}'\n" + - $" ServerSrc: '{serverSrcPref}'\n" + - $" UseEmbeddedServer: {useEmbedded}\n" + - $" DetectOnceKey: '{detectKey}' => {detectLogged}\n" + - $" AutoRegisteredKey: '{autoKey}' => {autoRegistered}", - always: false - ); - } - catch (Exception ex) - { - UnityEngine.Debug.LogWarning($"MCP Debug Prefs logging failed: {ex.Message}"); - } - } - - private static string SafeGetPrefString(string key) - { - try { return EditorPrefs.GetString(key, string.Empty) ?? string.Empty; } catch { return string.Empty; } - } - - private static bool SafeGetPrefBool(string key) - { - try { return EditorPrefs.GetBool(key, false); } catch { return false; } - } - - private static string ReadEmbeddedVersionOrFallback() - { - try - { - if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc)) - { - var p = Path.Combine(embeddedSrc, "server_version.txt"); - if (File.Exists(p)) - { - var s = File.ReadAllText(p)?.Trim(); - if (!string.IsNullOrEmpty(s)) return s; - } - } - } - catch { } - return "unknown"; - } - - private void DrawServerStatusSection() - { - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - - GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) - { - fontSize = 14 - }; - EditorGUILayout.LabelField("Server Status", sectionTitleStyle); - EditorGUILayout.Space(8); - - EditorGUILayout.BeginHorizontal(); - Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); - DrawStatusDot(statusRect, pythonServerInstallationStatusColor, 16); - - GUIStyle statusStyle = new GUIStyle(EditorStyles.label) - { - fontSize = 12, - fontStyle = FontStyle.Bold - }; - EditorGUILayout.LabelField(pythonServerInstallationStatus, statusStyle, GUILayout.Height(28)); - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(5); - - EditorGUILayout.BeginHorizontal(); - bool isAutoMode = MCPForUnityBridge.IsAutoConnectMode(); - GUIStyle modeStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 }; - EditorGUILayout.LabelField($"Mode: {(isAutoMode ? "Auto" : "Standard")}", modeStyle); - GUILayout.FlexibleSpace(); - EditorGUILayout.EndHorizontal(); - - int currentUnityPort = MCPForUnityBridge.GetCurrentPort(); - GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel) - { - fontSize = 11 - }; - EditorGUILayout.LabelField($"Ports: Unity {currentUnityPort}, MCP {mcpPort}", portStyle); - EditorGUILayout.Space(5); - - /// Auto-Setup button below ports - string setupButtonText = (lastClientRegisteredOk && lastBridgeVerifiedOk) ? "Connected ✓" : "Auto-Setup"; - if (GUILayout.Button(setupButtonText, GUILayout.Height(24))) - { - RunSetupNow(); - } - EditorGUILayout.Space(4); - - // Rebuild MCP Server button with tooltip tag - using (new EditorGUILayout.HorizontalScope()) - { - GUILayout.FlexibleSpace(); - GUIContent repairLabel = new GUIContent( - "Rebuild MCP Server", - "Deletes the installed server and re-copies it from the package. Use this to update the server after making source code changes or if the installation is corrupted." - ); - if (GUILayout.Button(repairLabel, GUILayout.Width(160), GUILayout.Height(22))) - { - bool ok = global::MCPForUnity.Editor.Helpers.ServerInstaller.RebuildMcpServer(); - if (ok) - { - EditorUtility.DisplayDialog("MCP For Unity", "Server rebuilt successfully.", "OK"); - UpdatePythonServerInstallationStatus(); - } - else - { - EditorUtility.DisplayDialog("MCP For Unity", "Rebuild failed. Please check Console for details.", "OK"); - } - } - } - // (Removed descriptive tool tag under the Repair button) - - // (Show Debug Logs toggle moved to header) - EditorGUILayout.Space(2); - - // Python detection warning with link - if (!IsPythonDetected()) - { - GUIStyle warnStyle = new GUIStyle(EditorStyles.label) { richText = true, wordWrap = true }; - EditorGUILayout.LabelField("Warning: No Python installation found.", warnStyle); - using (new EditorGUILayout.HorizontalScope()) - { - if (GUILayout.Button("Open Install Instructions", GUILayout.Width(200))) - { - Application.OpenURL("https://www.python.org/downloads/"); - } - } - EditorGUILayout.Space(4); - } - - // Troubleshooting helpers - if (pythonServerInstallationStatusColor != Color.green) - { - using (new EditorGUILayout.HorizontalScope()) - { - if (GUILayout.Button("Select server folder…", GUILayout.Width(160))) - { - string picked = EditorUtility.OpenFolderPanel("Select UnityMcpServer/src", Application.dataPath, ""); - if (!string.IsNullOrEmpty(picked) && File.Exists(Path.Combine(picked, "server.py"))) - { - pythonDirOverride = picked; - EditorPrefs.SetString("MCPForUnity.PythonDirOverride", pythonDirOverride); - UpdatePythonServerInstallationStatus(); - } - else if (!string.IsNullOrEmpty(picked)) - { - EditorUtility.DisplayDialog("Invalid Selection", "The selected folder does not contain server.py", "OK"); - } - } - if (GUILayout.Button("Verify again", GUILayout.Width(120))) - { - UpdatePythonServerInstallationStatus(); - } - } - } - EditorGUILayout.EndVertical(); - } - - private void DrawBridgeSection() - { - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - - // Always reflect the live state each repaint to avoid stale UI after recompiles - isUnityBridgeRunning = MCPForUnityBridge.IsRunning; - - GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) - { - fontSize = 14 - }; - EditorGUILayout.LabelField("Unity Bridge", sectionTitleStyle); - EditorGUILayout.Space(8); - - EditorGUILayout.BeginHorizontal(); - Color bridgeColor = isUnityBridgeRunning ? Color.green : Color.red; - Rect bridgeStatusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); - DrawStatusDot(bridgeStatusRect, bridgeColor, 16); - - GUIStyle bridgeStatusStyle = new GUIStyle(EditorStyles.label) - { - fontSize = 12, - fontStyle = FontStyle.Bold - }; - EditorGUILayout.LabelField(isUnityBridgeRunning ? "Running" : "Stopped", bridgeStatusStyle, GUILayout.Height(28)); - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(8); - if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge", GUILayout.Height(32))) - { - ToggleUnityBridge(); - } - EditorGUILayout.Space(5); - EditorGUILayout.EndVertical(); - } - - private void DrawValidationSection() - { - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - - GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) - { - fontSize = 14 - }; - EditorGUILayout.LabelField("Script Validation", sectionTitleStyle); - EditorGUILayout.Space(8); - - EditorGUI.BeginChangeCheck(); - validationLevelIndex = EditorGUILayout.Popup("Validation Level", validationLevelIndex, validationLevelOptions, GUILayout.Height(20)); - if (EditorGUI.EndChangeCheck()) - { - SaveValidationLevelSetting(); - } - - EditorGUILayout.Space(8); - string description = GetValidationLevelDescription(validationLevelIndex); - EditorGUILayout.HelpBox(description, MessageType.Info); - EditorGUILayout.Space(4); - // (Show Debug Logs toggle moved to header) - EditorGUILayout.Space(2); - EditorGUILayout.EndVertical(); - } - - private void DrawUnifiedClientConfiguration() - { - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - - GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) - { - fontSize = 14 - }; - EditorGUILayout.LabelField("MCP Client Configuration", sectionTitleStyle); - EditorGUILayout.Space(10); - - // (Auto-connect toggle removed per design) - - // Client selector - string[] clientNames = mcpClients.clients.Select(c => c.name).ToArray(); - EditorGUI.BeginChangeCheck(); - selectedClientIndex = EditorGUILayout.Popup("Select Client", selectedClientIndex, clientNames, GUILayout.Height(20)); - if (EditorGUI.EndChangeCheck()) - { - selectedClientIndex = Mathf.Clamp(selectedClientIndex, 0, mcpClients.clients.Count - 1); - } - - EditorGUILayout.Space(10); - - if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count) - { - McpClient selectedClient = mcpClients.clients[selectedClientIndex]; - DrawClientConfigurationCompact(selectedClient); - } - - EditorGUILayout.Space(5); - EditorGUILayout.EndVertical(); - } - - private void AutoFirstRunSetup() - { - try - { - // Project-scoped one-time flag - string projectPath = Application.dataPath ?? string.Empty; - string key = $"MCPForUnity.AutoRegistered.{ComputeSha1(projectPath)}"; - if (EditorPrefs.GetBool(key, false)) - { - return; - } - - // Attempt client registration using discovered Python server dir - pythonDirOverride ??= EditorPrefs.GetString("MCPForUnity.PythonDirOverride", null); - string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory(); - if (!string.IsNullOrEmpty(pythonDir) && File.Exists(Path.Combine(pythonDir, "server.py"))) - { - bool anyRegistered = false; - foreach (McpClient client in mcpClients.clients) - { - try - { - if (client.mcpType == McpTypes.ClaudeCode) - { - // Only attempt if Claude CLI is present - if (!IsClaudeConfigured() && !string.IsNullOrEmpty(ExecPath.ResolveClaude())) - { - RegisterWithClaudeCode(pythonDir); - anyRegistered = true; - } - } - else - { - CheckMcpConfiguration(client); - bool alreadyConfigured = client.status == McpStatus.Configured; - if (!alreadyConfigured) - { - ConfigureMcpClient(client); - anyRegistered = true; - } - } - } - catch (Exception ex) - { - MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup client '{client.name}' failed: {ex.Message}"); - } - } - lastClientRegisteredOk = anyRegistered - || IsCursorConfigured(pythonDir) - || CodexConfigHelper.IsCodexConfigured(pythonDir) - || IsClaudeConfigured(); - } - - // Ensure the bridge is listening and has a fresh saved port - if (!MCPForUnityBridge.IsRunning) - { - try - { - MCPForUnityBridge.StartAutoConnect(); - isUnityBridgeRunning = MCPForUnityBridge.IsRunning; - Repaint(); - } - catch (Exception ex) - { - MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup StartAutoConnect failed: {ex.Message}"); - } - } - - // Verify bridge with a quick ping - lastBridgeVerifiedOk = VerifyBridgePing(MCPForUnityBridge.GetCurrentPort()); - - EditorPrefs.SetBool(key, true); - } - catch (Exception e) - { - MCPForUnity.Editor.Helpers.McpLog.Warn($"MCP for Unity auto-setup skipped: {e.Message}"); - } - } - - private static string ComputeSha1(string input) - { - try - { - using SHA1 sha1 = SHA1.Create(); - byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty); - byte[] hash = sha1.ComputeHash(bytes); - StringBuilder sb = new StringBuilder(hash.Length * 2); - foreach (byte b in hash) - { - sb.Append(b.ToString("x2")); - } - return sb.ToString(); - } - catch - { - return ""; - } - } - - private void RunSetupNow() - { - // Force a one-shot setup regardless of first-run flag - try - { - pythonDirOverride ??= EditorPrefs.GetString("MCPForUnity.PythonDirOverride", null); - string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory(); - if (string.IsNullOrEmpty(pythonDir) || !File.Exists(Path.Combine(pythonDir, "server.py"))) - { - EditorUtility.DisplayDialog("Setup", "Python server not found. Please select UnityMcpServer/src.", "OK"); - return; - } - - bool anyRegistered = false; - foreach (McpClient client in mcpClients.clients) - { - try - { - if (client.mcpType == McpTypes.ClaudeCode) - { - if (!IsClaudeConfigured()) - { - RegisterWithClaudeCode(pythonDir); - anyRegistered = true; - } - } - else - { - CheckMcpConfiguration(client); - bool alreadyConfigured = client.status == McpStatus.Configured; - if (!alreadyConfigured) - { - ConfigureMcpClient(client); - anyRegistered = true; - } - } - } - catch (Exception ex) - { - UnityEngine.Debug.LogWarning($"Setup client '{client.name}' failed: {ex.Message}"); - } - } - lastClientRegisteredOk = anyRegistered - || IsCursorConfigured(pythonDir) - || CodexConfigHelper.IsCodexConfigured(pythonDir) - || IsClaudeConfigured(); - - // Restart/ensure bridge - MCPForUnityBridge.StartAutoConnect(); - isUnityBridgeRunning = MCPForUnityBridge.IsRunning; - - // Verify - lastBridgeVerifiedOk = VerifyBridgePing(MCPForUnityBridge.GetCurrentPort()); - Repaint(); - } - catch (Exception e) - { - EditorUtility.DisplayDialog("Setup Failed", e.Message, "OK"); - } - } - - private static bool IsCursorConfigured(string pythonDir) - { - try - { - string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".cursor", "mcp.json") - : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".cursor", "mcp.json"); - if (!File.Exists(configPath)) return false; - string json = File.ReadAllText(configPath); - dynamic cfg = JsonConvert.DeserializeObject(json); - var servers = cfg?.mcpServers; - if (servers == null) return false; - var unity = servers.unityMCP ?? servers.UnityMCP; - if (unity == null) return false; - var args = unity.args; - if (args == null) return false; - // Prefer exact extraction of the --directory value and compare normalized paths - string[] strArgs = ((System.Collections.Generic.IEnumerable)args) - .Select(x => x?.ToString() ?? string.Empty) - .ToArray(); - string dir = McpConfigFileHelper.ExtractDirectoryArg(strArgs); - if (string.IsNullOrEmpty(dir)) return false; - return McpConfigFileHelper.PathsEqual(dir, pythonDir); - } - catch { return false; } - } - - private static bool IsClaudeConfigured() - { - try - { - string claudePath = ExecPath.ResolveClaude(); - if (string.IsNullOrEmpty(claudePath)) return false; - - // Only prepend PATH on Unix - string pathPrepend = null; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - pathPrepend = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) - ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" - : "/usr/local/bin:/usr/bin:/bin"; - } - - if (!ExecPath.TryRun(claudePath, "mcp list", workingDir: null, out var stdout, out var stderr, 5000, pathPrepend)) - { - return false; - } - return (stdout ?? string.Empty).IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0; - } - catch { return false; } - } - - private static bool VerifyBridgePing(int port) - { - // Use strict framed protocol to match bridge (FRAMING=1) - const int ConnectTimeoutMs = 1000; - const int FrameTimeoutMs = 30000; // match bridge frame I/O timeout - - try - { - using TcpClient client = new TcpClient(); - var connectTask = client.ConnectAsync(IPAddress.Loopback, port); - if (!connectTask.Wait(ConnectTimeoutMs)) return false; - - using NetworkStream stream = client.GetStream(); - try { client.NoDelay = true; } catch { } - - // 1) Read handshake line (ASCII, newline-terminated) - string handshake = ReadLineAscii(stream, 2000); - if (string.IsNullOrEmpty(handshake) || handshake.IndexOf("FRAMING=1", StringComparison.OrdinalIgnoreCase) < 0) - { - UnityEngine.Debug.LogWarning("MCP for Unity: Bridge handshake missing FRAMING=1"); - return false; - } - - // 2) Send framed "ping" - byte[] payload = Encoding.UTF8.GetBytes("ping"); - WriteFrame(stream, payload, FrameTimeoutMs); - - // 3) Read framed response and check for pong - string response = ReadFrameUtf8(stream, FrameTimeoutMs); - bool ok = !string.IsNullOrEmpty(response) && response.IndexOf("pong", StringComparison.OrdinalIgnoreCase) >= 0; - if (!ok) - { - UnityEngine.Debug.LogWarning($"MCP for Unity: Framed ping failed; response='{response}'"); - } - return ok; - } - catch (Exception ex) - { - UnityEngine.Debug.LogWarning($"MCP for Unity: VerifyBridgePing error: {ex.Message}"); - return false; - } - } - - // Minimal framing helpers (8-byte big-endian length prefix), blocking with timeouts - private static void WriteFrame(NetworkStream stream, byte[] payload, int timeoutMs) - { - if (payload == null) throw new ArgumentNullException(nameof(payload)); - if (payload.LongLength < 1) throw new IOException("Zero-length frames are not allowed"); - byte[] header = new byte[8]; - ulong len = (ulong)payload.LongLength; - header[0] = (byte)(len >> 56); - header[1] = (byte)(len >> 48); - header[2] = (byte)(len >> 40); - header[3] = (byte)(len >> 32); - header[4] = (byte)(len >> 24); - header[5] = (byte)(len >> 16); - header[6] = (byte)(len >> 8); - header[7] = (byte)(len); - - stream.WriteTimeout = timeoutMs; - stream.Write(header, 0, header.Length); - stream.Write(payload, 0, payload.Length); - } - - private static string ReadFrameUtf8(NetworkStream stream, int timeoutMs) - { - byte[] header = ReadExact(stream, 8, timeoutMs); - ulong len = ((ulong)header[0] << 56) - | ((ulong)header[1] << 48) - | ((ulong)header[2] << 40) - | ((ulong)header[3] << 32) - | ((ulong)header[4] << 24) - | ((ulong)header[5] << 16) - | ((ulong)header[6] << 8) - | header[7]; - if (len == 0UL) throw new IOException("Zero-length frames are not allowed"); - if (len > int.MaxValue) throw new IOException("Frame too large"); - byte[] payload = ReadExact(stream, (int)len, timeoutMs); - return Encoding.UTF8.GetString(payload); - } - - private static byte[] ReadExact(NetworkStream stream, int count, int timeoutMs) - { - byte[] buffer = new byte[count]; - int offset = 0; - stream.ReadTimeout = timeoutMs; - while (offset < count) - { - int read = stream.Read(buffer, offset, count - offset); - if (read <= 0) throw new IOException("Connection closed before reading expected bytes"); - offset += read; - } - return buffer; - } - - private static string ReadLineAscii(NetworkStream stream, int timeoutMs, int maxLen = 512) - { - stream.ReadTimeout = timeoutMs; - using var ms = new MemoryStream(); - byte[] one = new byte[1]; - while (ms.Length < maxLen) - { - int n = stream.Read(one, 0, 1); - if (n <= 0) break; - if (one[0] == (byte)'\n') break; - ms.WriteByte(one[0]); - } - return Encoding.ASCII.GetString(ms.ToArray()); - } - - private void DrawClientConfigurationCompact(McpClient mcpClient) - { - // Special pre-check for Claude Code: if CLI missing, reflect in status UI - if (mcpClient.mcpType == McpTypes.ClaudeCode) - { - string claudeCheck = ExecPath.ResolveClaude(); - if (string.IsNullOrEmpty(claudeCheck)) - { - mcpClient.configStatus = "Claude Not Found"; - mcpClient.status = McpStatus.NotConfigured; - } - } - - // Pre-check for clients that require uv (all except Claude Code) - bool uvRequired = mcpClient.mcpType != McpTypes.ClaudeCode; - bool uvMissingEarly = false; - if (uvRequired) - { - string uvPathEarly = FindUvPath(); - if (string.IsNullOrEmpty(uvPathEarly)) - { - uvMissingEarly = true; - mcpClient.configStatus = "uv Not Found"; - mcpClient.status = McpStatus.NotConfigured; - } - } - - // Status display - EditorGUILayout.BeginHorizontal(); - Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); - Color statusColor = GetStatusColor(mcpClient.status); - DrawStatusDot(statusRect, statusColor, 16); - - GUIStyle clientStatusStyle = new GUIStyle(EditorStyles.label) - { - fontSize = 12, - fontStyle = FontStyle.Bold - }; - EditorGUILayout.LabelField(mcpClient.configStatus, clientStatusStyle, GUILayout.Height(28)); - EditorGUILayout.EndHorizontal(); - // When Claude CLI is missing, show a clear install hint directly below status - if (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) - { - GUIStyle installHintStyle = new GUIStyle(clientStatusStyle); - installHintStyle.normal.textColor = new Color(1f, 0.5f, 0f); // orange - EditorGUILayout.BeginHorizontal(); - GUIContent installText = new GUIContent("Make sure Claude Code is installed!"); - Vector2 textSize = installHintStyle.CalcSize(installText); - EditorGUILayout.LabelField(installText, installHintStyle, GUILayout.Height(22), GUILayout.Width(textSize.x + 2), GUILayout.ExpandWidth(false)); - GUIStyle helpLinkStyle = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; - GUILayout.Space(6); - if (GUILayout.Button("[HELP]", helpLinkStyle, GUILayout.Height(22), GUILayout.ExpandWidth(false))) - { - Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Claude-Code"); - } - EditorGUILayout.EndHorizontal(); - } - - EditorGUILayout.Space(10); - - // If uv is missing for required clients, show hint and picker then exit early to avoid showing other controls - if (uvRequired && uvMissingEarly) - { - GUIStyle installHintStyle2 = new GUIStyle(EditorStyles.label) - { - fontSize = 12, - fontStyle = FontStyle.Bold, - wordWrap = false - }; - installHintStyle2.normal.textColor = new Color(1f, 0.5f, 0f); - EditorGUILayout.BeginHorizontal(); - GUIContent installText2 = new GUIContent("Make sure uv is installed!"); - Vector2 sz = installHintStyle2.CalcSize(installText2); - EditorGUILayout.LabelField(installText2, installHintStyle2, GUILayout.Height(22), GUILayout.Width(sz.x + 2), GUILayout.ExpandWidth(false)); - GUIStyle helpLinkStyle2 = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; - GUILayout.Space(6); - if (GUILayout.Button("[HELP]", helpLinkStyle2, GUILayout.Height(22), GUILayout.ExpandWidth(false))) - { - Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Cursor,-VSCode-&-Windsurf"); - } - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(8); - EditorGUILayout.BeginHorizontal(); - if (GUILayout.Button("Choose uv Install Location", GUILayout.Width(260), GUILayout.Height(22))) - { - string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - string picked = EditorUtility.OpenFilePanel("Select 'uv' binary", suggested, ""); - if (!string.IsNullOrEmpty(picked)) - { - EditorPrefs.SetString("MCPForUnity.UvPath", picked); - ConfigureMcpClient(mcpClient); - Repaint(); - } - } - EditorGUILayout.EndHorizontal(); + // Only refresh data if UI is built + if (rootVisualElement == null || rootVisualElement.childCount == 0) return; - } - // Action buttons in horizontal layout - EditorGUILayout.BeginHorizontal(); - - if (mcpClient.mcpType == McpTypes.VSCode) - { - if (GUILayout.Button("Auto Configure", GUILayout.Height(32))) - { - ConfigureMcpClient(mcpClient); - } - } - else if (mcpClient.mcpType == McpTypes.ClaudeCode) - { - bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); - if (claudeAvailable) - { - bool isConfigured = mcpClient.status == McpStatus.Configured; - string buttonText = isConfigured ? "Unregister MCP for Unity with Claude Code" : "Register with Claude Code"; - if (GUILayout.Button(buttonText, GUILayout.Height(32))) - { - if (isConfigured) - { - UnregisterWithClaudeCode(); - } - else - { - string pythonDir = FindPackagePythonDirectory(); - RegisterWithClaudeCode(pythonDir); - } - } - // Hide the picker once a valid binary is available - EditorGUILayout.EndHorizontal(); - EditorGUILayout.BeginHorizontal(); - GUIStyle pathLabelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true }; - string resolvedClaude = ExecPath.ResolveClaude(); - EditorGUILayout.LabelField($"Claude CLI: {resolvedClaude}", pathLabelStyle); - EditorGUILayout.EndHorizontal(); - EditorGUILayout.BeginHorizontal(); - } - // CLI picker row (only when not found) - EditorGUILayout.EndHorizontal(); - EditorGUILayout.BeginHorizontal(); - if (!claudeAvailable) - { - // Only show the picker button in not-found state (no redundant "not found" label) - if (GUILayout.Button("Choose Claude Install Location", GUILayout.Width(260), GUILayout.Height(22))) - { - string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - string picked = EditorUtility.OpenFilePanel("Select 'claude' CLI", suggested, ""); - if (!string.IsNullOrEmpty(picked)) - { - ExecPath.SetClaudeCliPath(picked); - // Auto-register after setting a valid path - string pythonDir = FindPackagePythonDirectory(); - RegisterWithClaudeCode(pythonDir); - Repaint(); - } - } - } - EditorGUILayout.EndHorizontal(); - EditorGUILayout.BeginHorizontal(); - } - else - { - if (GUILayout.Button($"Auto Configure", GUILayout.Height(32))) - { - ConfigureMcpClient(mcpClient); - } - } - - if (mcpClient.mcpType != McpTypes.ClaudeCode) - { - if (GUILayout.Button("Manual Setup", GUILayout.Height(32))) - { - string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? mcpClient.windowsConfigPath - : mcpClient.linuxConfigPath; - - if (mcpClient.mcpType == McpTypes.VSCode) - { - string pythonDir = FindPackagePythonDirectory(); - string uvPath = FindUvPath(); - if (uvPath == null) - { - UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode."); - return; - } - // VSCode now reads from mcp.json with a top-level "servers" block - var vscodeConfig = new - { - servers = new - { - unityMCP = new - { - command = uvPath, - args = new[] { "run", "--directory", pythonDir, "server.py" } - } - } - }; - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); - VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson); - } - else - { - ShowManualInstructionsWindow(configPath, mcpClient); - } - } - } - - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(8); - // Quick info (hide when Claude is not found to avoid confusion) - bool hideConfigInfo = - (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) - || ((mcpClient.mcpType != McpTypes.ClaudeCode) && string.IsNullOrEmpty(FindUvPath())); - if (!hideConfigInfo) - { - GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel) - { - fontSize = 10 - }; - EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle); - } + RefreshAllData(); } - private void ToggleUnityBridge() + private void OnEditorUpdate() { - if (isUnityBridgeRunning) - { - MCPForUnityBridge.Stop(); - } - else - { - MCPForUnityBridge.Start(); - } - // Reflect the actual state post-operation (avoid optimistic toggle) - isUnityBridgeRunning = MCPForUnityBridge.IsRunning; - Repaint(); - } - - // New method to show manual instructions without changing status - private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient) - { - // Get the Python directory path using Package Manager API - string pythonDir = FindPackagePythonDirectory(); - // Build manual JSON centrally using the shared builder - string uvPathForManual = FindUvPath(); - if (uvPathForManual == null) - { - UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration."); + // Only update UI if it's built + if (rootVisualElement == null || rootVisualElement.childCount == 0) return; - } - string manualConfig = mcpClient?.mcpType == McpTypes.Codex - ? CodexConfigHelper.BuildCodexServerBlock(uvPathForManual, McpConfigFileHelper.ResolveServerDirectory(pythonDir, null)).TrimEnd() + Environment.NewLine - : ConfigJsonBuilder.BuildManualConfigJson(uvPathForManual, pythonDir, mcpClient); - ManualConfigEditorWindow.ShowWindow(configPath, manualConfig, mcpClient); + UpdateConnectionStatus(); } - private string FindPackagePythonDirectory() + private void RefreshAllData() { - // Use shared helper for consistent path resolution across both windows - return McpPathResolver.FindPackagePythonDirectory(debugLogsEnabled); - } + // Update connection status + UpdateConnectionStatus(); - private string ConfigureMcpClient(McpClient mcpClient) - { - try + // Auto-verify bridge health if connected + if (MCPServiceLocator.Bridge.IsRunning) { - // Use shared helper for consistent config path resolution - string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient); - - // Create directory if it doesn't exist - McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); - - // Find the server.py file location using shared helper - string pythonDir = FindPackagePythonDirectory(); - - if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py"))) - { - ShowManualInstructionsWindow(configPath, mcpClient); - return "Manual Configuration Required"; - } - - string result = mcpClient.mcpType == McpTypes.Codex - ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient) - : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient); - - // Update the client status after successful configuration - if (result == "Configured successfully") - { - mcpClient.SetStatus(McpStatus.Configured); - } - - return result; + VerifyBridgeConnection(); } - catch (Exception e) - { - // Determine the config file path based on OS for error message - 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; - } - ShowManualInstructionsWindow(configPath, mcpClient); - UnityEngine.Debug.LogError( - $"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}" - ); - return $"Failed to configure {mcpClient.name}"; + // Update path overrides + UpdatePathOverrides(); + + // Refresh selected client (may have been configured externally) + if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count) + { + var client = mcpClients.clients[selectedClientIndex]; + MCPServiceLocator.Client.CheckClientStatus(client); + UpdateClientStatus(); + UpdateManualConfiguration(); + UpdateClaudeCliPathVisibility(); } } - private void LoadValidationLevelSetting() + private void CacheUIElements() { - string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard"); - validationLevelIndex = savedLevel.ToLower() switch - { - "basic" => 0, - "standard" => 1, - "comprehensive" => 2, - "strict" => 3, - _ => 1 // Default to Standard - }; + // Settings + versionLabel = rootVisualElement.Q