diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 3ce232b7..0346f15b 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -108,3 +108,30 @@ jobs: git tag -a "$TAG" -m "Version ${NEW_VERSION}" git push origin "$TAG" + + - name: Package server for release + env: + NEW_VERSION: ${{ steps.compute.outputs.new_version }} + shell: bash + run: | + set -euo pipefail + cd MCPForUnity/UnityMcpServer~ + zip -r ../../mcp-for-unity-server-v${NEW_VERSION}.zip . + cd ../.. + ls -lh mcp-for-unity-server-v${NEW_VERSION}.zip + echo "Server package created: mcp-for-unity-server-v${NEW_VERSION}.zip" + + - name: Create GitHub release with server artifact + env: + NEW_VERSION: ${{ steps.compute.outputs.new_version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + TAG="v${NEW_VERSION}" + + # Create release + gh release create "$TAG" \ + --title "v${NEW_VERSION}" \ + --notes "Release v${NEW_VERSION}" \ + "mcp-for-unity-server-v${NEW_VERSION}.zip#MCP Server v${NEW_VERSION}" diff --git a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs index f03b66c7..dac1facf 100644 --- a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs +++ b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs @@ -1,4 +1,9 @@ using System; +using System.IO; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using PackageInfo = UnityEditor.PackageManager.PackageInfo; namespace MCPForUnity.Editor.Helpers { @@ -25,5 +30,133 @@ public static string SanitizeAssetPath(string path) return path; } + + /// + /// Gets the MCP for Unity package root path. + /// Works for registry Package Manager, local Package Manager, and Asset Store installations. + /// + /// The package root path (virtual for PM, absolute for Asset Store), or null if not found + public static string GetMcpPackageRootPath() + { + try + { + // Try Package Manager first (registry and local installs) + var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly); + if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.assetPath)) + { + return packageInfo.assetPath; + } + + // Fallback to AssetDatabase for Asset Store installs (Assets/MCPForUnity) + string[] guids = AssetDatabase.FindAssets($"t:Script {nameof(AssetPathUtility)}"); + + if (guids.Length == 0) + { + McpLog.Warn("Could not find AssetPathUtility script in AssetDatabase"); + return null; + } + + string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]); + + // Script is at: {packageRoot}/Editor/Helpers/AssetPathUtility.cs + // Extract {packageRoot} + int editorIndex = scriptPath.IndexOf("/Editor/", StringComparison.Ordinal); + + if (editorIndex >= 0) + { + return scriptPath.Substring(0, editorIndex); + } + + McpLog.Warn($"Could not determine package root from script path: {scriptPath}"); + return null; + } + catch (Exception ex) + { + McpLog.Error($"Failed to get package root path: {ex.Message}"); + return null; + } + } + + /// + /// Reads and parses the package.json file for MCP for Unity. + /// Handles both Package Manager (registry/local) and Asset Store installations. + /// + /// JObject containing package.json data, or null if not found or parse failed + public static JObject GetPackageJson() + { + try + { + string packageRoot = GetMcpPackageRootPath(); + if (string.IsNullOrEmpty(packageRoot)) + { + return null; + } + + string packageJsonPath = Path.Combine(packageRoot, "package.json"); + + // Convert virtual asset path to file system path + if (packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase)) + { + // Package Manager install - must use PackageInfo.resolvedPath + // Virtual paths like "Packages/..." don't work with File.Exists() + // Registry packages live in Library/PackageCache/package@version/ + var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly); + if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.resolvedPath)) + { + packageJsonPath = Path.Combine(packageInfo.resolvedPath, "package.json"); + } + else + { + McpLog.Warn("Could not resolve Package Manager path for package.json"); + return null; + } + } + else if (packageRoot.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + { + // Asset Store install - convert to absolute file system path + // Application.dataPath is the absolute path to the Assets folder + string relativePath = packageRoot.Substring("Assets/".Length); + packageJsonPath = Path.Combine(Application.dataPath, relativePath, "package.json"); + } + + if (!File.Exists(packageJsonPath)) + { + McpLog.Warn($"package.json not found at: {packageJsonPath}"); + return null; + } + + string json = File.ReadAllText(packageJsonPath); + return JObject.Parse(json); + } + catch (Exception ex) + { + McpLog.Warn($"Failed to read or parse package.json: {ex.Message}"); + return null; + } + } + + /// + /// Gets the version string from the package.json file. + /// + /// Version string, or "unknown" if not found + public static string GetPackageVersion() + { + try + { + var packageJson = GetPackageJson(); + if (packageJson == null) + { + return "unknown"; + } + + string version = packageJson["version"]?.ToString(); + return string.IsNullOrEmpty(version) ? "unknown" : version; + } + catch (Exception ex) + { + McpLog.Warn($"Failed to get package version: {ex.Message}"); + return "unknown"; + } + } } } diff --git a/MCPForUnity/Editor/Helpers/McpPathResolver.cs b/MCPForUnity/Editor/Helpers/McpPathResolver.cs index 8e683965..be1089f7 100644 --- a/MCPForUnity/Editor/Helpers/McpPathResolver.cs +++ b/MCPForUnity/Editor/Helpers/McpPathResolver.cs @@ -7,7 +7,7 @@ namespace MCPForUnity.Editor.Helpers { /// - /// Shared helper for resolving Python server directory paths with support for + /// Shared helper for resolving MCP server directory paths with support for /// development mode, embedded servers, and installed packages /// public static class McpPathResolver @@ -15,7 +15,7 @@ public static class McpPathResolver private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer"; /// - /// Resolves the Python server directory path with comprehensive logic + /// Resolves the MCP server directory path with comprehensive logic /// including development mode support and fallback mechanisms /// public static string FindPackagePythonDirectory(bool debugLogsEnabled = false) diff --git a/MCPForUnity/Editor/Helpers/PackageDetector.cs b/MCPForUnity/Editor/Helpers/PackageDetector.cs index bb8861fe..59e22348 100644 --- a/MCPForUnity/Editor/Helpers/PackageDetector.cs +++ b/MCPForUnity/Editor/Helpers/PackageDetector.cs @@ -49,8 +49,7 @@ static PackageDetector() if (!string.IsNullOrEmpty(error)) { - Debug.LogWarning($"MCP for Unity: Auto-detect on load failed: {capturedEx}"); - // Alternatively: Debug.LogException(capturedEx); + McpLog.Info($"Server check: {error}. Download via Window > MCP For Unity if needed.", always: false); } }; } diff --git a/MCPForUnity/Editor/Helpers/PackageInstaller.cs b/MCPForUnity/Editor/Helpers/PackageInstaller.cs index 031a6aed..1d46f321 100644 --- a/MCPForUnity/Editor/Helpers/PackageInstaller.cs +++ b/MCPForUnity/Editor/Helpers/PackageInstaller.cs @@ -4,7 +4,7 @@ namespace MCPForUnity.Editor.Helpers { /// - /// Handles automatic installation of the Python server when the package is first installed. + /// Handles automatic installation of the MCP server when the package is first installed. /// [InitializeOnLoad] public static class PackageInstaller @@ -25,18 +25,21 @@ private static void InstallServerOnFirstLoad() { try { - Debug.Log("MCP-FOR-UNITY: Installing Python server..."); ServerInstaller.EnsureServerInstalled(); - // Mark as installed + // Mark as installed/checked EditorPrefs.SetBool(InstallationFlagKey, true); - Debug.Log("MCP-FOR-UNITY: Python server installation completed successfully."); + // Only log success if server was actually embedded and copied + if (ServerInstaller.HasEmbeddedServer()) + { + McpLog.Info("MCP server installation completed successfully."); + } } - catch (System.Exception ex) + catch (System.Exception) { - Debug.LogError($"MCP-FOR-UNITY: Failed to install Python server: {ex.Message}"); - Debug.LogWarning("MCP-FOR-UNITY: You may need to manually install the Python server. Check the MCP For Unity Window for instructions."); + 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/ServerInstaller.cs b/MCPForUnity/Editor/Helpers/ServerInstaller.cs index f41e03c3..5ab823eb 100644 --- a/MCPForUnity/Editor/Helpers/ServerInstaller.cs +++ b/MCPForUnity/Editor/Helpers/ServerInstaller.cs @@ -1,9 +1,11 @@ using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; using System.Linq; +using System.Net; +using System.Runtime.InteropServices; using UnityEditor; using UnityEngine; @@ -34,8 +36,19 @@ public static void EnsureServerInstalled() // Resolve embedded source and versions if (!TryGetEmbeddedServerSource(out string embeddedSrc)) { - throw new Exception("Could not find embedded UnityMcpServer/src in the package."); + // Asset Store install - no embedded server + // Check if server was already downloaded + if (File.Exists(Path.Combine(destSrc, "server.py"))) + { + McpLog.Info("Using previously downloaded MCP server.", always: false); + } + else + { + McpLog.Info("MCP server not found. Download via Window > MCP For Unity > Open MCP Window.", always: false); + } + return; // Graceful exit - no exception } + string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown"; string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName)); @@ -151,7 +164,7 @@ private static string GetSaveLocation() TryCreateMacSymlinkForAppSupport(); return Path.Combine(localAppSupport, RootFolder); } - throw new Exception("Unsupported operating system."); + throw new Exception("Unsupported operating system"); } /// @@ -177,7 +190,7 @@ private static void TryCreateMacSymlinkForAppSupport() if (!Directory.Exists(canonical)) return; // Use 'ln -s' to create a directory symlink (macOS) - var psi = new System.Diagnostics.ProcessStartInfo + var psi = new ProcessStartInfo { FileName = "/bin/ln", Arguments = $"-s \"{canonical}\" \"{symlink}\"", @@ -186,7 +199,7 @@ private static void TryCreateMacSymlinkForAppSupport() RedirectStandardError = true, CreateNoWindow = true }; - using var p = System.Diagnostics.Process.Start(psi); + using var p = Process.Start(psi); p?.WaitForExit(2000); } catch { /* best-effort */ } @@ -303,7 +316,7 @@ private static bool PathsEqualSafe(string a, string b) private static IEnumerable GetLegacyRootsForDetection() { - var roots = new System.Collections.Generic.List(); + var roots = new List(); string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; // macOS/Linux legacy roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer")); @@ -331,7 +344,7 @@ private static void TryKillUvForPath(string serverSrcPath) if (string.IsNullOrEmpty(serverSrcPath)) return; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; - var psi = new System.Diagnostics.ProcessStartInfo + var psi = new ProcessStartInfo { FileName = "/usr/bin/pgrep", Arguments = $"-f \"uv .*--directory {serverSrcPath}\"", @@ -340,7 +353,7 @@ private static void TryKillUvForPath(string serverSrcPath) RedirectStandardError = true, CreateNoWindow = true }; - using var p = System.Diagnostics.Process.Start(psi); + using var p = Process.Start(psi); if (p == null) return; string outp = p.StandardOutput.ReadToEnd(); p.WaitForExit(1500); @@ -350,7 +363,7 @@ private static void TryKillUvForPath(string serverSrcPath) { if (int.TryParse(line.Trim(), out int pid)) { - try { System.Diagnostics.Process.GetProcessById(pid).Kill(); } catch { } + try { Process.GetProcessById(pid).Kill(); } catch { } } } } @@ -430,7 +443,7 @@ public static bool RebuildMcpServer() // Find embedded source if (!TryGetEmbeddedServerSource(out string embeddedSrc)) { - Debug.LogError("RebuildMcpServer: Could not find embedded server source."); + McpLog.Error("RebuildMcpServer: Could not find embedded server source."); return false; } @@ -447,11 +460,11 @@ public static bool RebuildMcpServer() try { Directory.Delete(destRoot, recursive: true); - Debug.Log($"MCP-FOR-UNITY: Deleted existing server at {destRoot}"); + McpLog.Info($"Deleted existing server at {destRoot}"); } catch (Exception ex) { - Debug.LogError($"Failed to delete existing server: {ex.Message}"); + McpLog.Error($"Failed to delete existing server: {ex.Message}"); return false; } } @@ -469,15 +482,15 @@ public static bool RebuildMcpServer() } catch (Exception ex) { - Debug.LogWarning($"Failed to write version file: {ex.Message}"); + McpLog.Warn($"Failed to write version file: {ex.Message}"); } - Debug.Log($"MCP-FOR-UNITY: Server rebuilt successfully at {destRoot} (version {embeddedVer})"); + McpLog.Info($"Server rebuilt successfully at {destRoot} (version {embeddedVer})"); return true; } catch (Exception ex) { - Debug.LogError($"RebuildMcpServer failed: {ex.Message}"); + McpLog.Error($"RebuildMcpServer failed: {ex.Message}"); return false; } } @@ -508,7 +521,7 @@ internal static string FindUvPath() // Fast path: resolve from PATH first try { - var wherePsi = new System.Diagnostics.ProcessStartInfo + var wherePsi = new ProcessStartInfo { FileName = "where", Arguments = "uv.exe", @@ -517,7 +530,7 @@ internal static string FindUvPath() RedirectStandardError = true, CreateNoWindow = true }; - using var wp = System.Diagnostics.Process.Start(wherePsi); + using var wp = Process.Start(wherePsi); string output = wp.StandardOutput.ReadToEnd().Trim(); wp.WaitForExit(1500); if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output)) @@ -613,7 +626,7 @@ internal static string FindUvPath() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var whichPsi = new System.Diagnostics.ProcessStartInfo + var whichPsi = new ProcessStartInfo { FileName = "/usr/bin/which", Arguments = "uv", @@ -628,7 +641,7 @@ internal static string FindUvPath() string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; string prepend = string.Join(":", new[] { - System.IO.Path.Combine(homeDir, ".local", "bin"), + Path.Combine(homeDir, ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", @@ -638,7 +651,7 @@ internal static string FindUvPath() whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath); } catch { } - using var wp = System.Diagnostics.Process.Start(whichPsi); + using var wp = Process.Start(whichPsi); string output = wp.StandardOutput.ReadToEnd().Trim(); wp.WaitForExit(3000); if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) @@ -676,7 +689,7 @@ private static bool ValidateUvBinary(string uvPath) { try { - var psi = new System.Diagnostics.ProcessStartInfo + var psi = new ProcessStartInfo { FileName = uvPath, Arguments = "--version", @@ -685,7 +698,7 @@ private static bool ValidateUvBinary(string uvPath) RedirectStandardError = true, CreateNoWindow = true }; - using var p = System.Diagnostics.Process.Start(psi); + using var p = Process.Start(psi); if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; } if (p.ExitCode == 0) { @@ -696,5 +709,133 @@ private static bool ValidateUvBinary(string uvPath) catch { } return false; } + + /// + /// Download and install server from GitHub release (Asset Store workflow) + /// + public static bool DownloadAndInstallServer() + { + string packageVersion = AssetPathUtility.GetPackageVersion(); + if (packageVersion == "unknown") + { + McpLog.Error("Cannot determine package version for download."); + return false; + } + + string downloadUrl = $"https://github.com/CoplayDev/unity-mcp/releases/download/v{packageVersion}/mcp-for-unity-server-v{packageVersion}.zip"; + string tempZip = Path.Combine(Path.GetTempPath(), $"mcp-server-v{packageVersion}.zip"); + string destRoot = Path.Combine(GetSaveLocation(), ServerFolder); + + try + { + EditorUtility.DisplayProgressBar("MCP for Unity", "Downloading server...", 0.3f); + + // Download + using (var client = new WebClient()) + { + client.DownloadFile(downloadUrl, tempZip); + } + + EditorUtility.DisplayProgressBar("MCP for Unity", "Extracting server...", 0.7f); + + // Kill any running UV processes + string destSrc = Path.Combine(destRoot, "src"); + TryKillUvForPath(destSrc); + + // Delete old installation + if (Directory.Exists(destRoot)) + { + try + { + Directory.Delete(destRoot, recursive: true); + } + catch (Exception ex) + { + McpLog.Warn($"Could not fully delete old server: {ex.Message}"); + } + } + + // Extract to temp location first + string tempExtractDir = Path.Combine(Path.GetTempPath(), $"mcp-server-extract-{Guid.NewGuid()}"); + Directory.CreateDirectory(tempExtractDir); + + try + { + ZipFile.ExtractToDirectory(tempZip, tempExtractDir); + + // The ZIP contains UnityMcpServer~ folder, find it and move its contents + string extractedServerFolder = Path.Combine(tempExtractDir, "UnityMcpServer~"); + Directory.CreateDirectory(destRoot); + CopyDirectoryRecursive(extractedServerFolder, destRoot); + } + finally + { + // Cleanup temp extraction directory + try + { + if (Directory.Exists(tempExtractDir)) + { + Directory.Delete(tempExtractDir, recursive: true); + } + } + catch (Exception ex) + { + McpLog.Warn($"Could not fully delete temp extraction directory: {ex.Message}"); + } + } + + EditorUtility.ClearProgressBar(); + McpLog.Info($"Server v{packageVersion} downloaded and installed successfully!"); + return true; + } + catch (Exception ex) + { + EditorUtility.ClearProgressBar(); + McpLog.Error($"Failed to download server: {ex.Message}"); + EditorUtility.DisplayDialog( + "Download Failed", + $"Could not download server from GitHub.\n\n{ex.Message}\n\nPlease check your internet connection or try again later.", + "OK" + ); + return false; + } + finally + { + try { + if (File.Exists(tempZip)) File.Delete(tempZip); + } catch (Exception ex) { + McpLog.Warn($"Could not delete temp zip file: {ex.Message}"); + } + } + } + + /// + /// Check if the package has an embedded server (Git install vs Asset Store) + /// + public static bool HasEmbeddedServer() + { + return TryGetEmbeddedServerSource(out _); + } + + /// + /// Get the installed server version from the local installation + /// + public static string GetInstalledServerVersion() + { + try + { + string destRoot = Path.Combine(GetSaveLocation(), ServerFolder); + string versionPath = Path.Combine(destRoot, "src", VersionFileName); + if (File.Exists(versionPath)) + { + return File.ReadAllText(versionPath)?.Trim() ?? string.Empty; + } + } + catch (Exception ex) + { + McpLog.Warn($"Could not read version file: {ex.Message}"); + } + return string.Empty; + } } } diff --git a/MCPForUnity/Editor/Helpers/TelemetryHelper.cs b/MCPForUnity/Editor/Helpers/TelemetryHelper.cs index 6440a675..0f436231 100644 --- a/MCPForUnity/Editor/Helpers/TelemetryHelper.cs +++ b/MCPForUnity/Editor/Helpers/TelemetryHelper.cs @@ -81,8 +81,8 @@ public static void EnableTelemetry() } /// - /// Send telemetry data to Python server for processing - /// This is a lightweight bridge - the actual telemetry logic is in Python + /// Send telemetry data to MCP server for processing + /// This is a lightweight bridge - the actual telemetry logic is in the MCP server /// public static void RecordEvent(string eventType, Dictionary data = null) { @@ -106,16 +106,16 @@ public static void RecordEvent(string eventType, Dictionary data telemetryData["data"] = data; } - // Send to Python server via existing bridge communication - // The Python server will handle actual telemetry transmission - SendTelemetryToPythonServer(telemetryData); + // Send to MCP server via existing bridge communication + // The MCP server will handle actual telemetry transmission + SendTelemetryToMcpServer(telemetryData); } catch (Exception e) { // Never let telemetry errors interfere with functionality if (IsDebugEnabled()) { - Debug.LogWarning($"Telemetry error (non-blocking): {e.Message}"); + McpLog.Warn($"Telemetry error (non-blocking): {e.Message}"); } } } @@ -183,7 +183,7 @@ public static void RecordToolExecution(string toolName, bool success, float dura RecordEvent("tool_execution_unity", data); } - private static void SendTelemetryToPythonServer(Dictionary telemetryData) + private static void SendTelemetryToMcpServer(Dictionary telemetryData) { var sender = Volatile.Read(ref s_sender); if (sender != null) @@ -197,7 +197,7 @@ private static void SendTelemetryToPythonServer(Dictionary telem { if (IsDebugEnabled()) { - Debug.LogWarning($"Telemetry sender error (non-blocking): {e.Message}"); + McpLog.Warn($"Telemetry sender error (non-blocking): {e.Message}"); } } } @@ -205,7 +205,7 @@ private static void SendTelemetryToPythonServer(Dictionary telem // Fallback: log when debug is enabled if (IsDebugEnabled()) { - Debug.Log($"MCP-TELEMETRY: {telemetryData["event_type"]}"); + McpLog.Info($"Telemetry: {telemetryData["event_type"]}"); } } diff --git a/MCPForUnity/Editor/Services.meta b/MCPForUnity/Editor/Services.meta new file mode 100644 index 00000000..e800deae --- /dev/null +++ b/MCPForUnity/Editor/Services.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2ab6b1cc527214416b21e07b96164f24 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/BridgeControlService.cs b/MCPForUnity/Editor/Services/BridgeControlService.cs new file mode 100644 index 00000000..a462e68b --- /dev/null +++ b/MCPForUnity/Editor/Services/BridgeControlService.cs @@ -0,0 +1,174 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Implementation of bridge control service + /// + public class BridgeControlService : IBridgeControlService + { + public bool IsRunning => MCPForUnityBridge.IsRunning; + public int CurrentPort => MCPForUnityBridge.GetCurrentPort(); + public bool IsAutoConnectMode => MCPForUnityBridge.IsAutoConnectMode(); + + public void Start() + { + // If server is installed, use auto-connect mode + // Otherwise use standard mode + string serverPath = MCPServiceLocator.Paths.GetMcpServerPath(); + if (!string.IsNullOrEmpty(serverPath) && File.Exists(Path.Combine(serverPath, "server.py"))) + { + MCPForUnityBridge.StartAutoConnect(); + } + else + { + MCPForUnityBridge.Start(); + } + } + + public void Stop() + { + MCPForUnityBridge.Stop(); + } + + public BridgeVerificationResult Verify(int port) + { + var result = new BridgeVerificationResult + { + Success = false, + HandshakeValid = false, + PingSucceeded = false, + Message = "Verification not started" + }; + + const int ConnectTimeoutMs = 1000; + const int FrameTimeoutMs = 30000; // Match bridge frame I/O timeout + + try + { + using (var client = new TcpClient()) + { + // Attempt connection + var connectTask = client.ConnectAsync(IPAddress.Loopback, port); + if (!connectTask.Wait(ConnectTimeoutMs)) + { + result.Message = "Connection timeout"; + return result; + } + + using (var 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) + { + result.Message = "Bridge handshake missing FRAMING=1"; + return result; + } + + result.HandshakeValid = true; + + // 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); + if (!string.IsNullOrEmpty(response) && response.IndexOf("pong", StringComparison.OrdinalIgnoreCase) >= 0) + { + result.PingSucceeded = true; + result.Success = true; + result.Message = "Bridge verified successfully"; + } + else + { + result.Message = $"Ping failed; response='{response}'"; + } + } + } + } + catch (Exception ex) + { + result.Message = $"Verification error: {ex.Message}"; + } + + return result; + } + + // 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()); + } + } + } +} diff --git a/MCPForUnity/Editor/Services/BridgeControlService.cs.meta b/MCPForUnity/Editor/Services/BridgeControlService.cs.meta new file mode 100644 index 00000000..93966a28 --- /dev/null +++ b/MCPForUnity/Editor/Services/BridgeControlService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ed4f9f69d84a945248dafc0f0b5a62dd +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 new file mode 100644 index 00000000..39728e01 --- /dev/null +++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs @@ -0,0 +1,502 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Data; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; +using Newtonsoft.Json; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Implementation of client configuration service + /// + public class ClientConfigurationService : IClientConfigurationService + { + private readonly Data.McpClients mcpClients = new(); + + public void ConfigureClient(McpClient client) + { + try + { + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); + + string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath(); + + if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py"))) + { + throw new InvalidOperationException("Server not found. Please use manual configuration or set server path in Advanced Settings."); + } + + string result = client.mcpType == McpTypes.Codex + ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client) + : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); + + if (result == "Configured successfully") + { + client.SetStatus(McpStatus.Configured); + Debug.Log($"MCP-FOR-UNITY: {client.name} configured successfully"); + } + else + { + Debug.LogWarning($"Configuration completed with message: {result}"); + } + + CheckClientStatus(client); + } + catch (Exception ex) + { + Debug.LogError($"Failed to configure {client.name}: {ex.Message}"); + throw; + } + } + + public ClientConfigurationSummary ConfigureAllDetectedClients() + { + var summary = new ClientConfigurationSummary(); + var pathService = MCPServiceLocator.Paths; + + foreach (var client in mcpClients.clients) + { + try + { + // Skip if already configured + CheckClientStatus(client, attemptAutoRewrite: false); + if (client.status == McpStatus.Configured) + { + summary.SkippedCount++; + summary.Messages.Add($"✓ {client.name}: Already configured"); + continue; + } + + // Check if required tools are available + if (client.mcpType == McpTypes.ClaudeCode) + { + if (!pathService.IsClaudeCliDetected()) + { + summary.SkippedCount++; + summary.Messages.Add($"➜ {client.name}: Claude CLI not found"); + continue; + } + + RegisterClaudeCode(); + summary.SuccessCount++; + summary.Messages.Add($"✓ {client.name}: Registered successfully"); + } + else + { + // Other clients require UV + if (!pathService.IsUvDetected()) + { + summary.SkippedCount++; + summary.Messages.Add($"➜ {client.name}: UV not found"); + continue; + } + + ConfigureClient(client); + summary.SuccessCount++; + summary.Messages.Add($"✓ {client.name}: Configured successfully"); + } + } + catch (Exception ex) + { + summary.FailureCount++; + summary.Messages.Add($"⚠ {client.name}: {ex.Message}"); + } + } + + return summary; + } + + public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true) + { + var previousStatus = client.status; + + try + { + // Special handling for Claude Code + if (client.mcpType == McpTypes.ClaudeCode) + { + CheckClaudeCodeConfiguration(client); + return client.status != previousStatus; + } + + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + + if (!File.Exists(configPath)) + { + client.SetStatus(McpStatus.NotConfigured); + return client.status != previousStatus; + } + + string configJson = File.ReadAllText(configPath); + string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath(); + + // Check configuration based on client type + string[] args = null; + bool configExists = false; + + switch (client.mcpType) + { + case McpTypes.VSCode: + dynamic vsConfig = JsonConvert.DeserializeObject(configJson); + if (vsConfig?.servers?.unityMCP != null) + { + args = vsConfig.servers.unityMCP.args.ToObject(); + configExists = true; + } + else if (vsConfig?.mcp?.servers?.unityMCP != null) + { + args = vsConfig.mcp.servers.unityMCP.args.ToObject(); + configExists = true; + } + break; + + case McpTypes.Codex: + if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs)) + { + args = codexArgs; + configExists = true; + } + break; + + default: + McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); + if (standardConfig?.mcpServers?.unityMCP != null) + { + args = standardConfig.mcpServers.unityMCP.args; + configExists = true; + } + break; + } + + if (configExists) + { + string configuredDir = McpConfigFileHelper.ExtractDirectoryArg(args); + bool matches = !string.IsNullOrEmpty(configuredDir) && + McpConfigFileHelper.PathsEqual(configuredDir, pythonDir); + + if (matches) + { + client.SetStatus(McpStatus.Configured); + } + else if (attemptAutoRewrite) + { + // Attempt auto-rewrite if path mismatch detected + try + { + string rewriteResult = client.mcpType == McpTypes.Codex + ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client) + : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client); + + if (rewriteResult == "Configured successfully") + { + bool debugLogsEnabled = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); + if (debugLogsEnabled) + { + McpLog.Info($"Auto-updated MCP config for '{client.name}' to new path: {pythonDir}", always: false); + } + client.SetStatus(McpStatus.Configured); + } + else + { + client.SetStatus(McpStatus.IncorrectPath); + } + } + catch + { + client.SetStatus(McpStatus.IncorrectPath); + } + } + else + { + client.SetStatus(McpStatus.IncorrectPath); + } + } + else + { + client.SetStatus(McpStatus.MissingConfig); + } + } + catch (Exception ex) + { + client.SetStatus(McpStatus.Error, ex.Message); + } + + return client.status != previousStatus; + } + + public void RegisterClaudeCode() + { + var pathService = MCPServiceLocator.Paths; + string pythonDir = pathService.GetMcpServerPath(); + + if (string.IsNullOrEmpty(pythonDir)) + { + throw new InvalidOperationException("Cannot register: Python directory not found"); + } + + string claudePath = pathService.GetClaudeCliPath(); + if (string.IsNullOrEmpty(claudePath)) + { + throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); + } + + string uvPath = pathService.GetUvPath() ?? "uv"; + string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; + string projectDir = Path.GetDirectoryName(Application.dataPath); + + string pathPrepend = null; + if (Application.platform == RuntimePlatform.OSXEditor) + { + pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; + } + else if (Application.platform == RuntimePlatform.LinuxEditor) + { + pathPrepend = "/usr/local/bin:/usr/bin:/bin"; + } + + // Add the directory containing Claude CLI to PATH (for node/nvm scenarios) + try + { + string claudeDir = Path.GetDirectoryName(claudePath); + if (!string.IsNullOrEmpty(claudeDir)) + { + pathPrepend = string.IsNullOrEmpty(pathPrepend) + ? claudeDir + : $"{claudeDir}:{pathPrepend}"; + } + } + catch { } + + if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend)) + { + string combined = ($"{stdout}\n{stderr}") ?? string.Empty; + if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0) + { + Debug.Log("MCP-FOR-UNITY: MCP for Unity already registered with Claude Code."); + } + else + { + throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}"); + } + return; + } + + Debug.Log("MCP-FOR-UNITY: Successfully registered with Claude Code."); + + // Update status + var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (claudeClient != null) + { + CheckClaudeCodeConfiguration(claudeClient); + } + } + + public void UnregisterClaudeCode() + { + var pathService = MCPServiceLocator.Paths; + string claudePath = pathService.GetClaudeCliPath(); + + if (string.IsNullOrEmpty(claudePath)) + { + throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); + } + + string projectDir = Path.GetDirectoryName(Application.dataPath); + string pathPrepend = Application.platform == RuntimePlatform.OSXEditor + ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" + : null; + + // Check if UnityMCP server exists (fixed - only check for "UnityMCP") + bool serverExists = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out _, out _, 7000, pathPrepend); + + if (!serverExists) + { + // Nothing to unregister + var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (claudeClient != null) + { + claudeClient.SetStatus(McpStatus.NotConfigured); + } + Debug.Log("MCP-FOR-UNITY: No MCP for Unity server found - already unregistered."); + return; + } + + // Remove the server + if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var stdout, out var stderr, 10000, pathPrepend)) + { + Debug.Log("MCP-FOR-UNITY: MCP server successfully unregistered from Claude Code."); + } + else + { + throw new InvalidOperationException($"Failed to unregister: {stderr}"); + } + + // Update status + var client = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (client != null) + { + client.SetStatus(McpStatus.NotConfigured); + CheckClaudeCodeConfiguration(client); + } + } + + public string GetConfigPath(McpClient client) + { + // Claude Code is managed via CLI, not config files + if (client.mcpType == McpTypes.ClaudeCode) + { + return "Not applicable (managed via Claude CLI)"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return client.windowsConfigPath; + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return client.macConfigPath; + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return client.linuxConfigPath; + + return "Unknown"; + } + + public string GenerateConfigJson(McpClient client) + { + string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath(); + string uvPath = MCPServiceLocator.Paths.GetUvPath(); + + // Claude Code uses CLI commands, not JSON config + if (client.mcpType == McpTypes.ClaudeCode) + { + if (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath)) + { + return "# Error: Configuration not available - check paths in Advanced Settings"; + } + + // Show the actual command that RegisterClaudeCode() uses + string registerCommand = $"claude mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; + + return "# Register the MCP server with Claude Code:\n" + + $"{registerCommand}\n\n" + + "# Unregister the MCP server:\n" + + "claude mcp remove UnityMCP\n\n" + + "# List registered servers:\n" + + "claude mcp list # Only works when claude is run in the project's directory"; + } + + if (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath)) + return "{ \"error\": \"Configuration not available - check paths in Advanced Settings\" }"; + + try + { + if (client.mcpType == McpTypes.Codex) + { + return CodexConfigHelper.BuildCodexServerBlock(uvPath, + McpConfigFileHelper.ResolveServerDirectory(pythonDir, null)); + } + else + { + return ConfigJsonBuilder.BuildManualConfigJson(uvPath, pythonDir, client); + } + } + catch (Exception ex) + { + return $"{{ \"error\": \"{ex.Message}\" }}"; + } + } + + public string GetInstallationSteps(McpClient client) + { + string baseSteps = client.mcpType switch + { + McpTypes.ClaudeDesktop => + "1. Open Claude Desktop\n" + + "2. Go to Settings > Developer > Edit Config\n" + + " OR open the config file at the path above\n" + + "3. Paste the configuration JSON\n" + + "4. Save and restart Claude Desktop", + + McpTypes.Cursor => + "1. Open Cursor\n" + + "2. Go to File > Preferences > Cursor Settings > MCP > Add new global MCP server\n" + + " OR open the config file at the path above\n" + + "3. Paste the configuration JSON\n" + + "4. Save and restart Cursor", + + McpTypes.Windsurf => + "1. Open Windsurf\n" + + "2. Go to File > Preferences > Windsurf Settings > MCP > Manage MCPs > View raw config\n" + + " OR open the config file at the path above\n" + + "3. Paste the configuration JSON\n" + + "4. Save and restart Windsurf", + + McpTypes.VSCode => + "1. Ensure VSCode and GitHub Copilot extension are installed\n" + + "2. Open or create mcp.json at the path above\n" + + "3. Paste the configuration JSON\n" + + "4. Save and restart VSCode", + + McpTypes.Kiro => + "1. Open Kiro\n" + + "2. Go to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config\n" + + " OR open the config file at the path above\n" + + "3. Paste the configuration JSON\n" + + "4. Save and restart Kiro", + + McpTypes.Codex => + "1. Run 'codex config edit' in a terminal\n" + + " OR open the config file at the path above\n" + + "2. Paste the configuration TOML\n" + + "3. Save and restart Codex", + + McpTypes.ClaudeCode => + "1. Ensure Claude CLI is installed\n" + + "2. Use the Register button to register automatically\n" + + " OR manually run: claude mcp add UnityMCP\n" + + "3. Restart Claude Code", + + _ => "Configuration steps not available for this client." + }; + + return baseSteps; + } + + private void CheckClaudeCodeConfiguration(McpClient client) + { + try + { + string configPath = McpConfigurationHelper.GetClientConfigPath(client); + + if (!File.Exists(configPath)) + { + client.SetStatus(McpStatus.NotConfigured); + return; + } + + string configJson = File.ReadAllText(configPath); + dynamic claudeConfig = JsonConvert.DeserializeObject(configJson); + + if (claudeConfig?.mcpServers != null) + { + var servers = claudeConfig.mcpServers; + // Only check for UnityMCP (fixed - removed candidate hacks) + if (servers.UnityMCP != null) + { + client.SetStatus(McpStatus.Configured); + return; + } + } + + client.SetStatus(McpStatus.NotConfigured); + } + catch (Exception ex) + { + client.SetStatus(McpStatus.Error, ex.Message); + } + } + } +} diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs.meta b/MCPForUnity/Editor/Services/ClientConfigurationService.cs.meta new file mode 100644 index 00000000..ce6ade0b --- /dev/null +++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 76cad34d10fd24aaa95c4583c1f88fdf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/IBridgeControlService.cs b/MCPForUnity/Editor/Services/IBridgeControlService.cs new file mode 100644 index 00000000..21952c37 --- /dev/null +++ b/MCPForUnity/Editor/Services/IBridgeControlService.cs @@ -0,0 +1,66 @@ +namespace MCPForUnity.Editor.Services +{ + /// + /// Service for controlling the Unity MCP Bridge connection + /// + public interface IBridgeControlService + { + /// + /// Gets whether the bridge is currently running + /// + bool IsRunning { get; } + + /// + /// Gets the current port the bridge is listening on + /// + int CurrentPort { get; } + + /// + /// Gets whether the bridge is in auto-connect mode + /// + bool IsAutoConnectMode { get; } + + /// + /// Starts the Unity MCP Bridge + /// + void Start(); + + /// + /// Stops the Unity MCP Bridge + /// + void Stop(); + + /// + /// Verifies the bridge connection by sending a ping and waiting for a pong response + /// + /// The port to verify + /// Verification result with detailed status + BridgeVerificationResult Verify(int port); + } + + /// + /// Result of a bridge verification attempt + /// + public class BridgeVerificationResult + { + /// + /// Whether the verification was successful + /// + public bool Success { get; set; } + + /// + /// Human-readable message about the verification result + /// + public string Message { get; set; } + + /// + /// Whether the handshake was valid (FRAMING=1 protocol) + /// + public bool HandshakeValid { get; set; } + + /// + /// Whether the ping/pong exchange succeeded + /// + public bool PingSucceeded { get; set; } + } +} diff --git a/MCPForUnity/Editor/Services/IBridgeControlService.cs.meta b/MCPForUnity/Editor/Services/IBridgeControlService.cs.meta new file mode 100644 index 00000000..ec50da3d --- /dev/null +++ b/MCPForUnity/Editor/Services/IBridgeControlService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6b5d9f677f6f54fc59e6fe921b260c61 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/IClientConfigurationService.cs b/MCPForUnity/Editor/Services/IClientConfigurationService.cs new file mode 100644 index 00000000..07af087b --- /dev/null +++ b/MCPForUnity/Editor/Services/IClientConfigurationService.cs @@ -0,0 +1,95 @@ +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Service for configuring MCP clients + /// + public interface IClientConfigurationService + { + /// + /// Configures a specific MCP client + /// + /// The client to configure + void ConfigureClient(McpClient client); + + /// + /// Configures all detected/installed MCP clients (skips clients where CLI/tools not found) + /// + /// Summary of configuration results + ClientConfigurationSummary ConfigureAllDetectedClients(); + + /// + /// Checks the configuration status of a client + /// + /// The client to check + /// If true, attempts to auto-fix mismatched paths + /// True if status changed, false otherwise + bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true); + + /// + /// Registers Unity MCP with Claude Code CLI + /// + void RegisterClaudeCode(); + + /// + /// Unregisters Unity MCP from Claude Code CLI + /// + void UnregisterClaudeCode(); + + /// + /// Gets the configuration file path for a client + /// + /// The client + /// Platform-specific config path + string GetConfigPath(McpClient client); + + /// + /// Generates the configuration JSON for a client + /// + /// The client + /// JSON configuration string + string GenerateConfigJson(McpClient client); + + /// + /// Gets human-readable installation steps for a client + /// + /// The client + /// Installation instructions + string GetInstallationSteps(McpClient client); + } + + /// + /// Summary of configuration results for multiple clients + /// + public class ClientConfigurationSummary + { + /// + /// Number of clients successfully configured + /// + public int SuccessCount { get; set; } + + /// + /// Number of clients that failed to configure + /// + public int FailureCount { get; set; } + + /// + /// Number of clients skipped (already configured or tool not found) + /// + public int SkippedCount { get; set; } + + /// + /// Detailed messages for each client + /// + public System.Collections.Generic.List Messages { get; set; } = new(); + + /// + /// Gets a human-readable summary message + /// + public string GetSummaryMessage() + { + return $"✓ {SuccessCount} configured, ⚠ {FailureCount} failed, ➜ {SkippedCount} skipped"; + } + } +} diff --git a/MCPForUnity/Editor/Services/IClientConfigurationService.cs.meta b/MCPForUnity/Editor/Services/IClientConfigurationService.cs.meta new file mode 100644 index 00000000..5110b69f --- /dev/null +++ b/MCPForUnity/Editor/Services/IClientConfigurationService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aae139cfae7ac4044ac52e2658005ea1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/IPathResolverService.cs b/MCPForUnity/Editor/Services/IPathResolverService.cs new file mode 100644 index 00000000..9968af65 --- /dev/null +++ b/MCPForUnity/Editor/Services/IPathResolverService.cs @@ -0,0 +1,92 @@ +namespace MCPForUnity.Editor.Services +{ + /// + /// Service for resolving paths to required tools and supporting user overrides + /// + public interface IPathResolverService + { + /// + /// Gets the MCP server path (respects override if set) + /// + /// Path to the MCP server directory containing server.py, or null if not found + string GetMcpServerPath(); + + /// + /// Gets the UV package manager path (respects override if set) + /// + /// Path to the uv executable, or null if not found + string GetUvPath(); + + /// + /// Gets the Claude CLI path (respects override if set) + /// + /// Path to the claude executable, or null if not found + string GetClaudeCliPath(); + + /// + /// Checks if Python is detected on the system + /// + /// True if Python is found + bool IsPythonDetected(); + + /// + /// Checks if UV is detected on the system + /// + /// True if UV is found + bool IsUvDetected(); + + /// + /// Checks if Claude CLI is detected on the system + /// + /// True if Claude CLI is found + bool IsClaudeCliDetected(); + + /// + /// Sets an override for the MCP server path + /// + /// Path to override with + void SetMcpServerOverride(string path); + + /// + /// Sets an override for the UV path + /// + /// Path to override with + void SetUvPathOverride(string path); + + /// + /// Sets an override for the Claude CLI path + /// + /// Path to override with + void SetClaudeCliPathOverride(string path); + + /// + /// Clears the MCP server path override + /// + void ClearMcpServerOverride(); + + /// + /// Clears the UV path override + /// + void ClearUvPathOverride(); + + /// + /// Clears the Claude CLI path override + /// + void ClearClaudeCliPathOverride(); + + /// + /// Gets whether a MCP server path override is active + /// + bool HasMcpServerOverride { get; } + + /// + /// Gets whether a UV path override is active + /// + bool HasUvPathOverride { get; } + + /// + /// Gets whether a Claude CLI path override is active + /// + bool HasClaudeCliPathOverride { get; } + } +} diff --git a/MCPForUnity/Editor/Services/IPathResolverService.cs.meta b/MCPForUnity/Editor/Services/IPathResolverService.cs.meta new file mode 100644 index 00000000..75634531 --- /dev/null +++ b/MCPForUnity/Editor/Services/IPathResolverService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1e8d388be507345aeb0eaf27fbd3c022 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/MCPServiceLocator.cs b/MCPForUnity/Editor/Services/MCPServiceLocator.cs new file mode 100644 index 00000000..547f9c23 --- /dev/null +++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs @@ -0,0 +1,52 @@ +namespace MCPForUnity.Editor.Services +{ + /// + /// Service locator for accessing MCP services without dependency injection + /// + public static class MCPServiceLocator + { + private static IBridgeControlService _bridgeService; + private static IClientConfigurationService _clientService; + private static IPathResolverService _pathService; + + /// + /// Gets the bridge control service + /// + public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService(); + + /// + /// Gets the client configuration service + /// + public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService(); + + /// + /// Gets the path resolver service + /// + public static IPathResolverService Paths => _pathService ??= new PathResolverService(); + + /// + /// Registers a custom implementation for a service (useful for testing) + /// + /// The service interface type + /// The implementation to register + public static void Register(T implementation) where T : class + { + if (implementation is IBridgeControlService b) + _bridgeService = b; + else if (implementation is IClientConfigurationService c) + _clientService = c; + else if (implementation is IPathResolverService p) + _pathService = p; + } + + /// + /// Resets all services to their default implementations (useful for testing) + /// + public static void Reset() + { + _bridgeService = null; + _clientService = null; + _pathService = null; + } + } +} diff --git a/MCPForUnity/Editor/Services/MCPServiceLocator.cs.meta b/MCPForUnity/Editor/Services/MCPServiceLocator.cs.meta new file mode 100644 index 00000000..cd9b4021 --- /dev/null +++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 276d6a9f9a1714ead91573945de78992 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/PathResolverService.cs b/MCPForUnity/Editor/Services/PathResolverService.cs new file mode 100644 index 00000000..6d239cc0 --- /dev/null +++ b/MCPForUnity/Editor/Services/PathResolverService.cs @@ -0,0 +1,242 @@ +using System; +using System.Diagnostics; +using System.IO; +using MCPForUnity.Editor.Helpers; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Implementation of path resolver service with override support + /// + public class PathResolverService : IPathResolverService + { + private const string PythonDirOverrideKey = "MCPForUnity.PythonDirOverride"; + private const string UvPathOverrideKey = "MCPForUnity.UvPath"; + private const string ClaudeCliPathOverrideKey = "MCPForUnity.ClaudeCliPath"; + + public bool HasMcpServerOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(PythonDirOverrideKey, null)); + public bool HasUvPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(UvPathOverrideKey, null)); + public bool HasClaudeCliPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(ClaudeCliPathOverrideKey, null)); + + public string GetMcpServerPath() + { + // Check for override first + string overridePath = EditorPrefs.GetString(PythonDirOverrideKey, null); + if (!string.IsNullOrEmpty(overridePath) && File.Exists(Path.Combine(overridePath, "server.py"))) + { + return overridePath; + } + + // Fall back to automatic detection + return McpPathResolver.FindPackagePythonDirectory(false); + } + + public string GetUvPath() + { + // Check for override first + string overridePath = EditorPrefs.GetString(UvPathOverrideKey, null); + if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath)) + { + return overridePath; + } + + // Fall back to automatic detection + try + { + return ServerInstaller.FindUvPath(); + } + catch + { + return null; + } + } + + public string GetClaudeCliPath() + { + // Check for override first + string overridePath = EditorPrefs.GetString(ClaudeCliPathOverrideKey, null); + if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath)) + { + return overridePath; + } + + // Fall back to automatic detection + return ExecPath.ResolveClaude(); + } + + public bool IsPythonDetected() + { + try + { + // Windows-specific Python detection + if (Application.platform == RuntimePlatform.WindowsEditor) + { + // Common Windows Python installation paths + string[] windowsCandidates = + { + @"C:\Python313\python.exe", + @"C:\Python312\python.exe", + @"C:\Python311\python.exe", + @"C:\Python310\python.exe", + @"C:\Python39\python.exe", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python313\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python312\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python311\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python310\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python313\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python312\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python311\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python310\python.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python39\python.exe"), + }; + + foreach (string c in windowsCandidates) + { + if (File.Exists(c)) return true; + } + + // Try 'where python' command (Windows equivalent of 'which') + var psi = new ProcessStartInfo + { + FileName = "where", + Arguments = "python", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using (var p = Process.Start(psi)) + { + string outp = p.StandardOutput.ReadToEnd().Trim(); + p.WaitForExit(2000); + if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp)) + { + string[] lines = outp.Split('\n'); + foreach (string line in lines) + { + string trimmed = line.Trim(); + if (File.Exists(trimmed)) return true; + } + } + } + } + else + { + // macOS/Linux detection + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; + string[] candidates = + { + "/opt/homebrew/bin/python3", + "/usr/local/bin/python3", + "/usr/bin/python3", + "/opt/local/bin/python3", + Path.Combine(home, ".local", "bin", "python3"), + "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3", + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3", + }; + foreach (string c in candidates) + { + if (File.Exists(c)) return true; + } + + // Try 'which python3' + var psi = new ProcessStartInfo + { + FileName = "/usr/bin/which", + Arguments = "python3", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using (var p = Process.Start(psi)) + { + string outp = p.StandardOutput.ReadToEnd().Trim(); + p.WaitForExit(2000); + if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp) && File.Exists(outp)) return true; + } + } + } + catch { } + return false; + } + + public bool IsUvDetected() + { + return !string.IsNullOrEmpty(GetUvPath()); + } + + public bool IsClaudeCliDetected() + { + return !string.IsNullOrEmpty(GetClaudeCliPath()); + } + + public void SetMcpServerOverride(string path) + { + if (string.IsNullOrEmpty(path)) + { + ClearMcpServerOverride(); + return; + } + + if (!File.Exists(Path.Combine(path, "server.py"))) + { + throw new ArgumentException("The selected folder does not contain server.py"); + } + + EditorPrefs.SetString(PythonDirOverrideKey, path); + } + + public void SetUvPathOverride(string path) + { + if (string.IsNullOrEmpty(path)) + { + ClearUvPathOverride(); + return; + } + + if (!File.Exists(path)) + { + throw new ArgumentException("The selected UV executable does not exist"); + } + + EditorPrefs.SetString(UvPathOverrideKey, path); + } + + public void SetClaudeCliPathOverride(string path) + { + if (string.IsNullOrEmpty(path)) + { + ClearClaudeCliPathOverride(); + return; + } + + if (!File.Exists(path)) + { + throw new ArgumentException("The selected Claude CLI executable does not exist"); + } + + EditorPrefs.SetString(ClaudeCliPathOverrideKey, path); + // Also update the ExecPath helper for backwards compatibility + ExecPath.SetClaudeCliPath(path); + } + + public void ClearMcpServerOverride() + { + EditorPrefs.DeleteKey(PythonDirOverrideKey); + } + + public void ClearUvPathOverride() + { + EditorPrefs.DeleteKey(UvPathOverrideKey); + } + + public void ClearClaudeCliPathOverride() + { + EditorPrefs.DeleteKey(ClaudeCliPathOverrideKey); + } + } +} diff --git a/MCPForUnity/Editor/Services/PathResolverService.cs.meta b/MCPForUnity/Editor/Services/PathResolverService.cs.meta new file mode 100644 index 00000000..09dbc1ec --- /dev/null +++ b/MCPForUnity/Editor/Services/PathResolverService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 00a6188fd15a847fa8cc7cb7a4ce3dce +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Setup/SetupWizard.cs b/MCPForUnity/Editor/Setup/SetupWizard.cs index a97926ea..691e482f 100644 --- a/MCPForUnity/Editor/Setup/SetupWizard.cs +++ b/MCPForUnity/Editor/Setup/SetupWizard.cs @@ -141,8 +141,17 @@ public static void CheckDependencies() /// /// Open MCP Client Configuration window /// - [MenuItem("Window/MCP For Unity/Open MCP Window", priority = 4)] + [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/Tools/ManageScript.cs b/MCPForUnity/Editor/Tools/ManageScript.cs index 2d970486..51669c65 100644 --- a/MCPForUnity/Editor/Tools/ManageScript.cs +++ b/MCPForUnity/Editor/Tools/ManageScript.cs @@ -2551,111 +2551,111 @@ private static void ValidateSemanticRules(string contents, System.Collections.Ge // } // } } -} - -// Debounced refresh/compile scheduler to coalesce bursts of edits -static class RefreshDebounce -{ - private static int _pending; - private static readonly object _lock = new object(); - private static readonly HashSet _paths = new HashSet(StringComparer.OrdinalIgnoreCase); - - // The timestamp of the most recent schedule request. - private static DateTime _lastRequest; - - // Guard to ensure we only have a single ticking callback running. - private static bool _scheduled; - public static void Schedule(string relPath, TimeSpan window) + // Debounced refresh/compile scheduler to coalesce bursts of edits + static class RefreshDebounce { - // Record that work is pending and track the path in a threadsafe way. - Interlocked.Exchange(ref _pending, 1); - lock (_lock) - { - _paths.Add(relPath); - _lastRequest = DateTime.UtcNow; + private static int _pending; + private static readonly object _lock = new object(); + private static readonly HashSet _paths = new HashSet(StringComparer.OrdinalIgnoreCase); - // If a debounce timer is already scheduled it will pick up the new request. - if (_scheduled) - return; + // The timestamp of the most recent schedule request. + private static DateTime _lastRequest; - _scheduled = true; - } - - // Kick off a ticking callback that waits until the window has elapsed - // from the last request before performing the refresh. - EditorApplication.delayCall += () => Tick(window); - // Nudge the editor loop so ticks run even if the window is unfocused - EditorApplication.QueuePlayerLoopUpdate(); - } + // Guard to ensure we only have a single ticking callback running. + private static bool _scheduled; - private static void Tick(TimeSpan window) - { - bool ready; - lock (_lock) + public static void Schedule(string relPath, TimeSpan window) { - // Only proceed once the debounce window has fully elapsed. - ready = (DateTime.UtcNow - _lastRequest) >= window; - if (ready) + // Record that work is pending and track the path in a threadsafe way. + Interlocked.Exchange(ref _pending, 1); + lock (_lock) { - _scheduled = false; + _paths.Add(relPath); + _lastRequest = DateTime.UtcNow; + + // If a debounce timer is already scheduled it will pick up the new request. + if (_scheduled) + return; + + _scheduled = true; } - } - if (!ready) - { - // Window has not yet elapsed; check again on the next editor tick. + // Kick off a ticking callback that waits until the window has elapsed + // from the last request before performing the refresh. EditorApplication.delayCall += () => Tick(window); - return; + // Nudge the editor loop so ticks run even if the window is unfocused + EditorApplication.QueuePlayerLoopUpdate(); } - if (Interlocked.Exchange(ref _pending, 0) == 1) + private static void Tick(TimeSpan window) { - string[] toImport; - lock (_lock) { toImport = _paths.ToArray(); _paths.Clear(); } - foreach (var p in toImport) + bool ready; + lock (_lock) + { + // Only proceed once the debounce window has fully elapsed. + ready = (DateTime.UtcNow - _lastRequest) >= window; + if (ready) + { + _scheduled = false; + } + } + + if (!ready) { - var sp = ManageScriptRefreshHelpers.SanitizeAssetsPath(p); - AssetDatabase.ImportAsset(sp, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport); + // Window has not yet elapsed; check again on the next editor tick. + EditorApplication.delayCall += () => Tick(window); + return; } + + if (Interlocked.Exchange(ref _pending, 0) == 1) + { + string[] toImport; + lock (_lock) { toImport = _paths.ToArray(); _paths.Clear(); } + foreach (var p in toImport) + { + var sp = ManageScriptRefreshHelpers.SanitizeAssetsPath(p); + AssetDatabase.ImportAsset(sp, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport); + } #if UNITY_EDITOR - UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); + UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); #endif - // Fallback if needed: - // AssetDatabase.Refresh(); + // Fallback if needed: + // AssetDatabase.Refresh(); + } } } -} -static class ManageScriptRefreshHelpers -{ - public static string SanitizeAssetsPath(string p) + static class ManageScriptRefreshHelpers { - if (string.IsNullOrEmpty(p)) return p; - p = p.Replace('\\', '/').Trim(); - if (p.StartsWith("unity://path/", StringComparison.OrdinalIgnoreCase)) - p = p.Substring("unity://path/".Length); - while (p.StartsWith("Assets/Assets/", StringComparison.OrdinalIgnoreCase)) - p = p.Substring("Assets/".Length); - if (!p.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) - p = "Assets/" + p.TrimStart('/'); - return p; - } + public static string SanitizeAssetsPath(string p) + { + if (string.IsNullOrEmpty(p)) return p; + p = p.Replace('\\', '/').Trim(); + if (p.StartsWith("unity://path/", StringComparison.OrdinalIgnoreCase)) + p = p.Substring("unity://path/".Length); + while (p.StartsWith("Assets/Assets/", StringComparison.OrdinalIgnoreCase)) + p = p.Substring("Assets/".Length); + if (!p.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + p = "Assets/" + p.TrimStart('/'); + return p; + } - public static void ScheduleScriptRefresh(string relPath) - { - var sp = SanitizeAssetsPath(relPath); - RefreshDebounce.Schedule(sp, TimeSpan.FromMilliseconds(200)); - } + public static void ScheduleScriptRefresh(string relPath) + { + var sp = SanitizeAssetsPath(relPath); + RefreshDebounce.Schedule(sp, TimeSpan.FromMilliseconds(200)); + } - public static void ImportAndRequestCompile(string relPath, bool synchronous = true) - { - var sp = SanitizeAssetsPath(relPath); - var opts = ImportAssetOptions.ForceUpdate; - if (synchronous) opts |= ImportAssetOptions.ForceSynchronousImport; - AssetDatabase.ImportAsset(sp, opts); + public static void ImportAndRequestCompile(string relPath, bool synchronous = true) + { + var sp = SanitizeAssetsPath(relPath); + var opts = ImportAssetOptions.ForceUpdate; + if (synchronous) opts |= ImportAssetOptions.ForceSynchronousImport; + AssetDatabase.ImportAsset(sp, opts); #if UNITY_EDITOR - UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); + UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); #endif + } } } diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs new file mode 100644 index 00000000..3987f112 --- /dev/null +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs @@ -0,0 +1,834 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +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 MCPForUnityEditorWindowNew : EditorWindow + { + // Protocol enum for future HTTP support + private enum ConnectionProtocol + { + Stdio, + // HTTPStreaming // Future + } + + // Settings UI Elements + 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() + { + 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/MCPForUnityEditorWindowNew.uxml" + ); + + if (visualTree == null) + { + McpLog.Error($"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindowNew.uxml"); + return; + } + + visualTree.CloneTree(rootVisualElement); + + // Load USS + var styleSheet = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/MCPForUnityEditorWindowNew.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() + { + EditorApplication.update += OnEditorUpdate; + } + + private void OnDisable() + { + EditorApplication.update -= OnEditorUpdate; + } + + private void OnFocus() + { + // Only refresh data if UI is built + if (rootVisualElement == null || rootVisualElement.childCount == 0) + return; + + RefreshAllData(); + } + + private void OnEditorUpdate() + { + // Only update UI if it's built + if (rootVisualElement == null || rootVisualElement.childCount == 0) + return; + + UpdateConnectionStatus(); + } + + private void RefreshAllData() + { + // Update connection status + UpdateConnectionStatus(); + + // Auto-verify bridge health if connected + if (MCPServiceLocator.Bridge.IsRunning) + { + VerifyBridgeConnection(); + } + + // 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 CacheUIElements() + { + // Settings + debugLogsToggle = rootVisualElement.Q("debug-logs-toggle"); + validationLevelField = rootVisualElement.Q("validation-level"); + validationDescription = rootVisualElement.Q