added remote install of python server
parent
75b62d3710
commit
61a7cb9e28
|
|
@ -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";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures that UnityMcpServer is installed and up to date by checking the typical application path via Python's package manager.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="branch">The GitHub branch to install from. Defaults to "master" if not specified.</param>
|
||||||
|
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."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a command and returns its output.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the version from 'uv pip show' output.
|
||||||
|
/// </summary>
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches the latest version from the GitHub repository's pyproject.toml.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="branch">The GitHub branch to fetch the version from.</param>
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5862c6a6d0a914f4d83224f8d039cf7b
|
||||||
|
|
@ -9,6 +9,7 @@ using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityMcpBridge.Editor.Helpers;
|
||||||
using UnityMcpBridge.Editor.Models;
|
using UnityMcpBridge.Editor.Models;
|
||||||
using UnityMcpBridge.Editor.Tools;
|
using UnityMcpBridge.Editor.Tools;
|
||||||
|
|
||||||
|
|
@ -31,14 +32,18 @@ namespace UnityMcpBridge.Editor
|
||||||
public static bool FolderExists(string path)
|
public static bool FolderExists(string path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase))
|
if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
string fullPath = Path.Combine(
|
string fullPath = Path.Combine(
|
||||||
Application.dataPath,
|
Application.dataPath,
|
||||||
path.StartsWith("Assets/") ? path.Substring(7) : path
|
path.StartsWith("Assets/") ? path[7..] : path
|
||||||
);
|
);
|
||||||
return Directory.Exists(fullPath);
|
return Directory.Exists(fullPath);
|
||||||
}
|
}
|
||||||
|
|
@ -51,12 +56,23 @@ namespace UnityMcpBridge.Editor
|
||||||
|
|
||||||
public static void Start()
|
public static void Start()
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ServerInstaller.EnsureServerInstalled();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Failed to ensure UnityMcpServer is installed: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
if (isRunning)
|
if (isRunning)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
listener = new TcpListener(IPAddress.Loopback, unityPort);
|
listener = new TcpListener(IPAddress.Loopback, unityPort);
|
||||||
listener.Start();
|
listener.Start();
|
||||||
Debug.Log($"UnityMCPBridge started on port {unityPort}.");
|
|
||||||
Task.Run(ListenerLoop);
|
Task.Run(ListenerLoop);
|
||||||
EditorApplication.update += ProcessCommands;
|
EditorApplication.update += ProcessCommands;
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +80,10 @@ namespace UnityMcpBridge.Editor
|
||||||
public static void Stop()
|
public static void Stop()
|
||||||
{
|
{
|
||||||
if (!isRunning)
|
if (!isRunning)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
listener.Stop();
|
listener.Stop();
|
||||||
EditorApplication.update -= ProcessCommands;
|
EditorApplication.update -= ProcessCommands;
|
||||||
|
|
@ -77,7 +96,7 @@ namespace UnityMcpBridge.Editor
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = await listener.AcceptTcpClientAsync();
|
TcpClient client = await listener.AcceptTcpClientAsync();
|
||||||
// Enable basic socket keepalive
|
// Enable basic socket keepalive
|
||||||
client.Client.SetSocketOption(
|
client.Client.SetSocketOption(
|
||||||
SocketOptionLevel.Socket,
|
SocketOptionLevel.Socket,
|
||||||
|
|
@ -94,7 +113,9 @@ namespace UnityMcpBridge.Editor
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (isRunning)
|
if (isRunning)
|
||||||
|
{
|
||||||
Debug.LogError($"Listener error: {ex.Message}");
|
Debug.LogError($"Listener error: {ex.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,16 +123,18 @@ namespace UnityMcpBridge.Editor
|
||||||
private static async Task HandleClientAsync(TcpClient client)
|
private static async Task HandleClientAsync(TcpClient client)
|
||||||
{
|
{
|
||||||
using (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)
|
while (isRunning)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
|
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
|
||||||
if (bytesRead == 0)
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
break; // Client disconnected
|
break; // Client disconnected
|
||||||
|
}
|
||||||
|
|
||||||
string commandText = System.Text.Encoding.UTF8.GetString(
|
string commandText = System.Text.Encoding.UTF8.GetString(
|
||||||
buffer,
|
buffer,
|
||||||
|
|
@ -119,13 +142,14 @@ namespace UnityMcpBridge.Editor
|
||||||
bytesRead
|
bytesRead
|
||||||
);
|
);
|
||||||
string commandId = Guid.NewGuid().ToString();
|
string commandId = Guid.NewGuid().ToString();
|
||||||
var tcs = new TaskCompletionSource<string>();
|
TaskCompletionSource<string> tcs = new();
|
||||||
|
|
||||||
// Special handling for ping command to avoid JSON parsing
|
// Special handling for ping command to avoid JSON parsing
|
||||||
if (commandText.Trim() == "ping")
|
if (commandText.Trim() == "ping")
|
||||||
{
|
{
|
||||||
// Direct response to ping without going through JSON parsing
|
// Direct response to ping without going through JSON parsing
|
||||||
byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes(
|
byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes(
|
||||||
|
/*lang=json,strict*/
|
||||||
"{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}"
|
"{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}"
|
||||||
);
|
);
|
||||||
await stream.WriteAsync(pingResponseBytes, 0, pingResponseBytes.Length);
|
await stream.WriteAsync(pingResponseBytes, 0, pingResponseBytes.Length);
|
||||||
|
|
@ -155,11 +179,16 @@ namespace UnityMcpBridge.Editor
|
||||||
List<string> processedIds = new();
|
List<string> processedIds = new();
|
||||||
lock (lockObj)
|
lock (lockObj)
|
||||||
{
|
{
|
||||||
foreach (var kvp in commandQueue.ToList())
|
foreach (
|
||||||
|
KeyValuePair<
|
||||||
|
string,
|
||||||
|
(string commandJson, TaskCompletionSource<string> tcs)
|
||||||
|
> kvp in commandQueue.ToList()
|
||||||
|
)
|
||||||
{
|
{
|
||||||
string id = kvp.Key;
|
string id = kvp.Key;
|
||||||
string commandText = kvp.Value.commandJson;
|
string commandText = kvp.Value.commandJson;
|
||||||
var tcs = kvp.Value.tcs;
|
TaskCompletionSource<string> tcs = kvp.Value.tcs;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -200,7 +229,7 @@ namespace UnityMcpBridge.Editor
|
||||||
status = "error",
|
status = "error",
|
||||||
error = "Invalid JSON format",
|
error = "Invalid JSON format",
|
||||||
receivedText = commandText.Length > 50
|
receivedText = commandText.Length > 50
|
||||||
? commandText.Substring(0, 50) + "..."
|
? commandText[..50] + "..."
|
||||||
: commandText,
|
: commandText,
|
||||||
};
|
};
|
||||||
tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse));
|
tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse));
|
||||||
|
|
@ -209,7 +238,7 @@ namespace UnityMcpBridge.Editor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal JSON command processing
|
// Normal JSON command processing
|
||||||
var command = JsonConvert.DeserializeObject<Command>(commandText);
|
Command command = JsonConvert.DeserializeObject<Command>(commandText);
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
var nullCommandResponse = new
|
var nullCommandResponse = new
|
||||||
|
|
@ -236,7 +265,7 @@ namespace UnityMcpBridge.Editor
|
||||||
error = ex.Message,
|
error = ex.Message,
|
||||||
commandType = "Unknown (error during processing)",
|
commandType = "Unknown (error during processing)",
|
||||||
receivedText = commandText?.Length > 50
|
receivedText = commandText?.Length > 50
|
||||||
? commandText.Substring(0, 50) + "..."
|
? commandText[..50] + "..."
|
||||||
: commandText,
|
: commandText,
|
||||||
};
|
};
|
||||||
string responseJson = JsonConvert.SerializeObject(response);
|
string responseJson = JsonConvert.SerializeObject(response);
|
||||||
|
|
@ -246,7 +275,7 @@ namespace UnityMcpBridge.Editor
|
||||||
processedIds.Add(id);
|
processedIds.Add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var id in processedIds)
|
foreach (string id in processedIds)
|
||||||
{
|
{
|
||||||
commandQueue.Remove(id);
|
commandQueue.Remove(id);
|
||||||
}
|
}
|
||||||
|
|
@ -257,7 +286,9 @@ namespace UnityMcpBridge.Editor
|
||||||
private static bool IsValidJson(string text)
|
private static bool IsValidJson(string text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
text = text.Trim();
|
text = text.Trim();
|
||||||
if (
|
if (
|
||||||
|
|
@ -357,17 +388,16 @@ namespace UnityMcpBridge.Editor
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (@params == null || !@params.HasValues)
|
return @params == null || !@params.HasValues
|
||||||
return "No parameters";
|
? "No parameters"
|
||||||
|
: string.Join(
|
||||||
return string.Join(
|
", ",
|
||||||
", ",
|
@params
|
||||||
@params
|
.Properties()
|
||||||
.Properties()
|
.Select(static p =>
|
||||||
.Select(p =>
|
$"{p.Name}: {p.Value?.ToString()?[..Math.Min(20, p.Value?.ToString()?.Length ?? 0)]}"
|
||||||
$"{p.Name}: {p.Value?.ToString()?.Substring(0, Math.Min(20, p.Value?.ToString()?.Length ?? 0))}"
|
)
|
||||||
)
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue