using System; using System.IO; using System.Runtime.InteropServices; using UnityEditor; using UnityEngine; namespace UnityMcpBridge.Editor.Helpers { public static class ServerInstaller { private const string RootFolder = "UnityMCP"; private const string ServerFolder = "UnityMcpServer"; /// /// Ensures the unity-mcp-server is installed locally by copying from the embedded package source. /// No network calls or Git operations are performed. /// public static void EnsureServerInstalled() { try { string saveLocation = GetSaveLocation(); string destRoot = Path.Combine(saveLocation, ServerFolder); string destSrc = Path.Combine(destRoot, "src"); if (File.Exists(Path.Combine(destSrc, "server.py"))) { return; // Already installed } if (!TryGetEmbeddedServerSource(out string embeddedSrc)) { throw new Exception("Could not find embedded UnityMcpServer/src in the package."); } // Ensure destination exists Directory.CreateDirectory(destRoot); // Copy the entire UnityMcpServer folder (parent of src) string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer CopyDirectoryRecursive(embeddedRoot, destRoot); } catch (Exception ex) { Debug.LogError($"Failed to ensure server installation: {ex.Message}"); } } public static string GetServerPath() { return Path.Combine(GetSaveLocation(), ServerFolder, "src"); } /// /// Gets the platform-specific save location for the server. /// private static string GetSaveLocation() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "Local", "Programs", RootFolder ); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "bin", RootFolder ); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { string path = "/usr/local/bin"; return !Directory.Exists(path) || !IsDirectoryWritable(path) ? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications", RootFolder ) : Path.Combine(path, RootFolder); } throw new Exception("Unsupported operating system."); } private static bool IsDirectoryWritable(string path) { try { File.Create(Path.Combine(path, "test.txt")).Dispose(); File.Delete(Path.Combine(path, "test.txt")); return true; } catch { return false; } } /// /// Checks if the server is installed at the specified location. /// private static bool IsServerInstalled(string location) { return Directory.Exists(location) && File.Exists(Path.Combine(location, ServerFolder, "src", "server.py")); } /// /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package /// or common development locations. /// private static bool TryGetEmbeddedServerSource(out string srcPath) { // 1) Development mode: common repo layouts try { string projectRoot = Path.GetDirectoryName(Application.dataPath); string[] devCandidates = { Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"), Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"), }; foreach (string candidate in devCandidates) { string full = Path.GetFullPath(candidate); if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py"))) { srcPath = full; return true; } } } catch { /* ignore */ } // 2) Installed package: resolve via Package Manager try { var list = UnityEditor.PackageManager.Client.List(); while (!list.IsCompleted) { } if (list.Status == UnityEditor.PackageManager.StatusCode.Success) { foreach (var pkg in list.Result) { if (pkg.name == "com.justinpbarnett.unity-mcp") { string packagePath = pkg.resolvedPath; // e.g., Library/PackageCache/... or local path // Preferred: UnityMcpServer embedded alongside Editor/Runtime within the package string embedded = Path.Combine(packagePath, "UnityMcpServer", "src"); if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py"))) { srcPath = embedded; return true; } // Legacy: sibling of the package folder (dev-linked). Only valid when present on disk. string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src"); if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py"))) { srcPath = sibling; return true; } } } } } catch { /* ignore */ } // 3) Fallback to previous common install locations try { string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); string[] candidates = { Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"), Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"), }; foreach (string candidate in candidates) { if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py"))) { srcPath = candidate; return true; } } } catch { /* ignore */ } srcPath = null; return false; } private static void CopyDirectoryRecursive(string sourceDir, string destinationDir) { Directory.CreateDirectory(destinationDir); foreach (string filePath in Directory.GetFiles(sourceDir)) { string fileName = Path.GetFileName(filePath); string destFile = Path.Combine(destinationDir, fileName); File.Copy(filePath, destFile, overwrite: true); } foreach (string dirPath in Directory.GetDirectories(sourceDir)) { string dirName = Path.GetFileName(dirPath); string destSubDir = Path.Combine(destinationDir, dirName); CopyDirectoryRecursive(dirPath, destSubDir); } } } }