From 61a7cb9e289336267d424ef14467286a87eb27b1 Mon Sep 17 00:00:00 2001 From: Justin Barnett Date: Tue, 8 Apr 2025 07:22:24 -0400 Subject: [PATCH] added remote install of python server --- .../Editor/Helpers/ServerInstaller.cs | 124 ++++++++++++++++++ .../Editor/Helpers/ServerInstaller.cs.meta | 2 + UnityMcpBridge/Editor/UnityMcpBridge.cs | 76 +++++++---- 3 files changed, 179 insertions(+), 23 deletions(-) create mode 100644 UnityMcpBridge/Editor/Helpers/ServerInstaller.cs create mode 100644 UnityMcpBridge/Editor/Helpers/ServerInstaller.cs.meta diff --git a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs new file mode 100644 index 0000000..4cd5975 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs @@ -0,0 +1,124 @@ +using System; +using System.Net.Http; +using System.Text.RegularExpressions; +using UnityEngine; + +namespace UnityMcpBridge.Editor.Helpers +{ + public static class ServerInstaller + { + private const string PackageName = "unity-mcp-server"; + private const string GitUrlTemplate = + "git+https://github.com/justinpbarnett/unity-mcp.git@{0}#subdirectory=UnityMcpServer"; + private const string PyprojectUrlTemplate = + "https://raw.githubusercontent.com/justinpbarnett/unity-mcp/{0}/UnityMcpServer/pyproject.toml"; + private const string DefaultBranch = "master"; + + /// + /// Ensures that UnityMcpServer is installed and up to date by checking the typical application path via Python's package manager. + /// + /// The GitHub branch to install from. Defaults to "master" if not specified. + public static void EnsureServerInstalled(string branch = DefaultBranch) + { + try + { + // Format the URLs with the specified branch + string gitUrl = string.Format(GitUrlTemplate, branch); + + // Check if unity-mcp-server is installed using uv + string output = RunCommand("uv", $"pip show {PackageName}"); + if (output.Contains("WARNING: Package(s) not found")) + { + Debug.Log($"Installing {PackageName} from branch '{branch}'..."); + RunCommand("uv", $"pip install {gitUrl}"); + Debug.Log($"{PackageName} installed successfully."); + } + else + { + // Extract the installed version + string installedVersion = GetVersionFromPipShow(output); + // Get the latest version from GitHub + string latestVersion = GetLatestVersionFromGitHub(branch); + // Compare versions + if (new Version(installedVersion) < new Version(latestVersion)) + { + Debug.Log( + $"Updating {PackageName} from {installedVersion} to {latestVersion} (branch '{branch}')..." + ); + RunCommand("uv", $"pip install --upgrade {gitUrl}"); + Debug.Log($"{PackageName} updated successfully."); + } + else + { + Debug.Log($"{PackageName} is up to date (version {installedVersion})."); + } + } + } + catch (Exception ex) + { + Debug.LogError($"Failed to ensure {PackageName} is installed: {ex.Message}"); + Debug.LogWarning( + "Please ensure 'uv' is installed and accessible. See the Unity MCP README for installation instructions." + ); + } + } + + /// + /// Executes a command and returns its output. + /// + private static string RunCommand(string fileName, string arguments) + { + System.Diagnostics.Process process = new(); + process.StartInfo.FileName = fileName; + process.StartInfo.Arguments = arguments; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + if (process.ExitCode != 0) + { + throw new Exception( + $"Command '{fileName} {arguments}' failed with exit code {process.ExitCode}: {error}" + ); + } + return output; + } + + /// + /// Extracts the version from 'uv pip show' output. + /// + private static string GetVersionFromPipShow(string output) + { + string[] lines = output.Split('\n'); + foreach (string line in lines) + { + if (line.StartsWith("Version:")) + { + return line["Version:".Length..].Trim(); + } + } + throw new Exception("Version not found in pip show output"); + } + + /// + /// Fetches the latest version from the GitHub repository's pyproject.toml. + /// + /// The GitHub branch to fetch the version from. + private static string GetLatestVersionFromGitHub(string branch) + { + string pyprojectUrl = string.Format(PyprojectUrlTemplate, branch); + using HttpClient client = new(); + // Add GitHub headers to avoid rate limiting + client.DefaultRequestHeaders.Add("User-Agent", "UnityMcpBridge"); + string content = client.GetStringAsync(pyprojectUrl).Result; + string pattern = @"version\s*=\s*""(.*?)"""; + Match match = Regex.Match(content, pattern); + return match.Success + ? match.Groups[1].Value + : throw new Exception("Could not find version in pyproject.toml"); + } + } +} diff --git a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs.meta b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs.meta new file mode 100644 index 0000000..67bd7f4 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5862c6a6d0a914f4d83224f8d039cf7b \ No newline at end of file diff --git a/UnityMcpBridge/Editor/UnityMcpBridge.cs b/UnityMcpBridge/Editor/UnityMcpBridge.cs index 705fa33..0b37f9e 100644 --- a/UnityMcpBridge/Editor/UnityMcpBridge.cs +++ b/UnityMcpBridge/Editor/UnityMcpBridge.cs @@ -9,6 +9,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; +using UnityMcpBridge.Editor.Helpers; using UnityMcpBridge.Editor.Models; using UnityMcpBridge.Editor.Tools; @@ -31,14 +32,18 @@ namespace UnityMcpBridge.Editor public static bool FolderExists(string path) { if (string.IsNullOrEmpty(path)) + { return false; + } if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase)) + { return true; + } string fullPath = Path.Combine( Application.dataPath, - path.StartsWith("Assets/") ? path.Substring(7) : path + path.StartsWith("Assets/") ? path[7..] : path ); return Directory.Exists(fullPath); } @@ -51,12 +56,23 @@ namespace UnityMcpBridge.Editor public static void Start() { + try + { + ServerInstaller.EnsureServerInstalled(); + } + catch (Exception ex) + { + Debug.LogError($"Failed to ensure UnityMcpServer is installed: {ex.Message}"); + } + if (isRunning) + { return; + } + isRunning = true; listener = new TcpListener(IPAddress.Loopback, unityPort); listener.Start(); - Debug.Log($"UnityMCPBridge started on port {unityPort}."); Task.Run(ListenerLoop); EditorApplication.update += ProcessCommands; } @@ -64,7 +80,10 @@ namespace UnityMcpBridge.Editor public static void Stop() { if (!isRunning) + { return; + } + isRunning = false; listener.Stop(); EditorApplication.update -= ProcessCommands; @@ -77,7 +96,7 @@ namespace UnityMcpBridge.Editor { try { - var client = await listener.AcceptTcpClientAsync(); + TcpClient client = await listener.AcceptTcpClientAsync(); // Enable basic socket keepalive client.Client.SetSocketOption( SocketOptionLevel.Socket, @@ -94,7 +113,9 @@ namespace UnityMcpBridge.Editor catch (Exception ex) { if (isRunning) + { Debug.LogError($"Listener error: {ex.Message}"); + } } } } @@ -102,16 +123,18 @@ namespace UnityMcpBridge.Editor private static async Task HandleClientAsync(TcpClient client) { using (client) - using (var stream = client.GetStream()) + using (NetworkStream stream = client.GetStream()) { - var buffer = new byte[8192]; + byte[] buffer = new byte[8192]; while (isRunning) { try { int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); if (bytesRead == 0) + { break; // Client disconnected + } string commandText = System.Text.Encoding.UTF8.GetString( buffer, @@ -119,13 +142,14 @@ namespace UnityMcpBridge.Editor bytesRead ); string commandId = Guid.NewGuid().ToString(); - var tcs = new TaskCompletionSource(); + TaskCompletionSource tcs = new(); // Special handling for ping command to avoid JSON parsing if (commandText.Trim() == "ping") { // Direct response to ping without going through JSON parsing byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes( + /*lang=json,strict*/ "{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}" ); await stream.WriteAsync(pingResponseBytes, 0, pingResponseBytes.Length); @@ -155,11 +179,16 @@ namespace UnityMcpBridge.Editor List processedIds = new(); lock (lockObj) { - foreach (var kvp in commandQueue.ToList()) + foreach ( + KeyValuePair< + string, + (string commandJson, TaskCompletionSource tcs) + > kvp in commandQueue.ToList() + ) { string id = kvp.Key; string commandText = kvp.Value.commandJson; - var tcs = kvp.Value.tcs; + TaskCompletionSource tcs = kvp.Value.tcs; try { @@ -200,7 +229,7 @@ namespace UnityMcpBridge.Editor status = "error", error = "Invalid JSON format", receivedText = commandText.Length > 50 - ? commandText.Substring(0, 50) + "..." + ? commandText[..50] + "..." : commandText, }; tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse)); @@ -209,7 +238,7 @@ namespace UnityMcpBridge.Editor } // Normal JSON command processing - var command = JsonConvert.DeserializeObject(commandText); + Command command = JsonConvert.DeserializeObject(commandText); if (command == null) { var nullCommandResponse = new @@ -236,7 +265,7 @@ namespace UnityMcpBridge.Editor error = ex.Message, commandType = "Unknown (error during processing)", receivedText = commandText?.Length > 50 - ? commandText.Substring(0, 50) + "..." + ? commandText[..50] + "..." : commandText, }; string responseJson = JsonConvert.SerializeObject(response); @@ -246,7 +275,7 @@ namespace UnityMcpBridge.Editor processedIds.Add(id); } - foreach (var id in processedIds) + foreach (string id in processedIds) { commandQueue.Remove(id); } @@ -257,7 +286,9 @@ namespace UnityMcpBridge.Editor private static bool IsValidJson(string text) { if (string.IsNullOrWhiteSpace(text)) + { return false; + } text = text.Trim(); if ( @@ -357,17 +388,16 @@ namespace UnityMcpBridge.Editor { try { - if (@params == null || !@params.HasValues) - return "No parameters"; - - return string.Join( - ", ", - @params - .Properties() - .Select(p => - $"{p.Name}: {p.Value?.ToString()?.Substring(0, Math.Min(20, p.Value?.ToString()?.Length ?? 0))}" - ) - ); + return @params == null || !@params.HasValues + ? "No parameters" + : string.Join( + ", ", + @params + .Properties() + .Select(static p => + $"{p.Name}: {p.Value?.ToString()?[..Math.Min(20, p.Value?.ToString()?.Length ?? 0)]}" + ) + ); } catch {