using System; using System.IO; using System.Linq; using System.Net; using System.Runtime.InteropServices; using UnityEngine; namespace UnityMcpBridge.Editor.Helpers { public static class ServerInstaller { private const string RootFolder = "UnityMCP"; private const string ServerFolder = "UnityMcpServer"; private const string BranchName = "master"; private const string GitUrl = "https://github.com/justinpbarnett/unity-mcp.git"; private const string PyprojectUrl = "https://raw.githubusercontent.com/justinpbarnett/unity-mcp/refs/heads/" + BranchName + "/UnityMcpServer/src/pyproject.toml"; /// /// Ensures the unity-mcp-server is installed and up to date. /// public static void EnsureServerInstalled() { try { string saveLocation = GetSaveLocation(); if (!IsServerInstalled(saveLocation)) { InstallServer(saveLocation); } else { string installedVersion = GetInstalledVersion(); string latestVersion = GetLatestVersion(); if (IsNewerVersion(latestVersion, installedVersion)) { UpdateServer(saveLocation); } else { } } } 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", "pyproject.toml")); } /// /// Installs the server by cloning only the UnityMcpServer folder from the repository and setting up dependencies. /// private static void InstallServer(string location) { // Create the src directory where the server code will reside Directory.CreateDirectory(location); // Initialize git repo in the src directory RunCommand("git", $"init", workingDirectory: location); // Add remote RunCommand("git", $"remote add origin {GitUrl}", workingDirectory: location); // Configure sparse checkout RunCommand("git", "config core.sparseCheckout true", workingDirectory: location); // Set sparse checkout path to only include UnityMcpServer folder string sparseCheckoutPath = Path.Combine(location, ".git", "info", "sparse-checkout"); File.WriteAllText(sparseCheckoutPath, $"{ServerFolder}/"); // Fetch and checkout the branch RunCommand("git", $"fetch --depth=1 origin {BranchName}", workingDirectory: location); RunCommand("git", $"checkout {BranchName}", workingDirectory: location); } /// /// Fetches the currently installed version from the local pyproject.toml file. /// public static string GetInstalledVersion() { string pyprojectPath = Path.Combine( GetSaveLocation(), ServerFolder, "src", "pyproject.toml" ); return ParseVersionFromPyproject(File.ReadAllText(pyprojectPath)); } /// /// Fetches the latest version from the GitHub pyproject.toml file. /// public static string GetLatestVersion() { using WebClient webClient = new(); string pyprojectContent = webClient.DownloadString(PyprojectUrl); return ParseVersionFromPyproject(pyprojectContent); } /// /// Updates the server by pulling the latest changes for the UnityMcpServer folder only. /// private static void UpdateServer(string location) { RunCommand("git", $"pull origin {BranchName}", workingDirectory: location); } /// /// Parses the version number from pyproject.toml content. /// private static string ParseVersionFromPyproject(string content) { foreach (string line in content.Split('\n')) { if (line.Trim().StartsWith("version =")) { string[] parts = line.Split('='); if (parts.Length == 2) { return parts[1].Trim().Trim('"'); } } } throw new Exception("Version not found in pyproject.toml"); } /// /// Compares two version strings to determine if the latest is newer. /// public static bool IsNewerVersion(string latest, string installed) { int[] latestParts = latest.Split('.').Select(int.Parse).ToArray(); int[] installedParts = installed.Split('.').Select(int.Parse).ToArray(); for (int i = 0; i < Math.Min(latestParts.Length, installedParts.Length); i++) { if (latestParts[i] > installedParts[i]) { return true; } if (latestParts[i] < installedParts[i]) { return false; } } return latestParts.Length > installedParts.Length; } /// /// Runs a command-line process and handles output/errors. /// private static void RunCommand( string command, string arguments, string workingDirectory = null ) { System.Diagnostics.Process process = new() { StartInfo = new System.Diagnostics.ProcessStartInfo { FileName = command, Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = workingDirectory ?? string.Empty, }, }; process.Start(); string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); process.WaitForExit(); if (process.ExitCode != 0) { throw new Exception( $"Command failed: {command} {arguments}\nOutput: {output}\nError: {error}" ); } } } }