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
{