Skip to content

Commit a65f103

Browse files
committed
feat(bridge): embed Python server into package and remove Git-based installer
- Switch ServerInstaller to embedded copy-only (no network) - Simplify Editor UI server status to 'Installed (Embedded)' - Vendor UnityMcpServer/src into UnityMcpBridge/UnityMcpServer/src for UPM distribution - Keep bridge recompile robustness (heartbeat + sticky port)
1 parent a0fd919 commit a65f103

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1983
-258
lines changed

UnityMcpBridge/Editor/Helpers/ServerInstaller.cs

Lines changed: 97 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
using System;
22
using System.IO;
3-
using System.Linq;
4-
using System.Net;
53
using System.Runtime.InteropServices;
4+
using UnityEditor;
65
using UnityEngine;
76

87
namespace UnityMcpBridge.Editor.Helpers
@@ -11,37 +10,34 @@ public static class ServerInstaller
1110
{
1211
private const string RootFolder = "UnityMCP";
1312
private const string ServerFolder = "UnityMcpServer";
14-
private const string BranchName = "master";
15-
private const string GitUrl = "https://github.com/justinpbarnett/unity-mcp.git";
16-
private const string PyprojectUrl =
17-
"https://raw.githubusercontent.com/justinpbarnett/unity-mcp/refs/heads/"
18-
+ BranchName
19-
+ "/UnityMcpServer/src/pyproject.toml";
20-
2113
/// <summary>
22-
/// Ensures the unity-mcp-server is installed and up to date.
14+
/// Ensures the unity-mcp-server is installed locally by copying from the embedded package source.
15+
/// No network calls or Git operations are performed.
2316
/// </summary>
2417
public static void EnsureServerInstalled()
2518
{
2619
try
2720
{
2821
string saveLocation = GetSaveLocation();
22+
string destRoot = Path.Combine(saveLocation, ServerFolder);
23+
string destSrc = Path.Combine(destRoot, "src");
2924

30-
if (!IsServerInstalled(saveLocation))
25+
if (File.Exists(Path.Combine(destSrc, "server.py")))
3126
{
32-
InstallServer(saveLocation);
27+
return; // Already installed
3328
}
34-
else
35-
{
36-
string installedVersion = GetInstalledVersion();
37-
string latestVersion = GetLatestVersion();
3829

39-
if (IsNewerVersion(latestVersion, installedVersion))
40-
{
41-
UpdateServer(saveLocation);
42-
}
43-
else { }
30+
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
31+
{
32+
throw new Exception("Could not find embedded UnityMcpServer/src in the package.");
4433
}
34+
35+
// Ensure destination exists
36+
Directory.CreateDirectory(destRoot);
37+
38+
// Copy the entire UnityMcpServer folder (parent of src)
39+
string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
40+
CopyDirectoryRecursive(embeddedRoot, destRoot);
4541
}
4642
catch (Exception ex)
4743
{
@@ -111,139 +107,110 @@ private static bool IsDirectoryWritable(string path)
111107
private static bool IsServerInstalled(string location)
112108
{
113109
return Directory.Exists(location)
114-
&& File.Exists(Path.Combine(location, ServerFolder, "src", "pyproject.toml"));
115-
}
116-
117-
/// <summary>
118-
/// Installs the server by cloning only the UnityMcpServer folder from the repository and setting up dependencies.
119-
/// </summary>
120-
private static void InstallServer(string location)
121-
{
122-
// Create the src directory where the server code will reside
123-
Directory.CreateDirectory(location);
124-
125-
// Initialize git repo in the src directory
126-
RunCommand("git", $"init", workingDirectory: location);
127-
128-
// Add remote
129-
RunCommand("git", $"remote add origin {GitUrl}", workingDirectory: location);
130-
131-
// Configure sparse checkout
132-
RunCommand("git", "config core.sparseCheckout true", workingDirectory: location);
133-
134-
// Set sparse checkout path to only include UnityMcpServer folder
135-
string sparseCheckoutPath = Path.Combine(location, ".git", "info", "sparse-checkout");
136-
File.WriteAllText(sparseCheckoutPath, $"{ServerFolder}/");
137-
138-
// Fetch and checkout the branch
139-
RunCommand("git", $"fetch --depth=1 origin {BranchName}", workingDirectory: location);
140-
RunCommand("git", $"checkout {BranchName}", workingDirectory: location);
141-
}
142-
143-
/// <summary>
144-
/// Fetches the currently installed version from the local pyproject.toml file.
145-
/// </summary>
146-
public static string GetInstalledVersion()
147-
{
148-
string pyprojectPath = Path.Combine(
149-
GetSaveLocation(),
150-
ServerFolder,
151-
"src",
152-
"pyproject.toml"
153-
);
154-
return ParseVersionFromPyproject(File.ReadAllText(pyprojectPath));
110+
&& File.Exists(Path.Combine(location, ServerFolder, "src", "server.py"));
155111
}
156112

157113
/// <summary>
158-
/// Fetches the latest version from the GitHub pyproject.toml file.
114+
/// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
115+
/// or common development locations.
159116
/// </summary>
160-
public static string GetLatestVersion()
117+
private static bool TryGetEmbeddedServerSource(out string srcPath)
161118
{
162-
using WebClient webClient = new();
163-
string pyprojectContent = webClient.DownloadString(PyprojectUrl);
164-
return ParseVersionFromPyproject(pyprojectContent);
165-
}
166-
167-
/// <summary>
168-
/// Updates the server by pulling the latest changes for the UnityMcpServer folder only.
169-
/// </summary>
170-
private static void UpdateServer(string location)
171-
{
172-
RunCommand("git", $"pull origin {BranchName}", workingDirectory: location);
173-
}
174-
175-
/// <summary>
176-
/// Parses the version number from pyproject.toml content.
177-
/// </summary>
178-
private static string ParseVersionFromPyproject(string content)
179-
{
180-
foreach (string line in content.Split('\n'))
119+
// 1) Development mode: common repo layouts
120+
try
181121
{
182-
if (line.Trim().StartsWith("version ="))
122+
string projectRoot = Path.GetDirectoryName(Application.dataPath);
123+
string[] devCandidates =
124+
{
125+
Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
126+
Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
127+
};
128+
foreach (string candidate in devCandidates)
183129
{
184-
string[] parts = line.Split('=');
185-
if (parts.Length == 2)
130+
string full = Path.GetFullPath(candidate);
131+
if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
186132
{
187-
return parts[1].Trim().Trim('"');
133+
srcPath = full;
134+
return true;
188135
}
189136
}
190137
}
191-
throw new Exception("Version not found in pyproject.toml");
192-
}
138+
catch { /* ignore */ }
193139

194-
/// <summary>
195-
/// Compares two version strings to determine if the latest is newer.
196-
/// </summary>
197-
public static bool IsNewerVersion(string latest, string installed)
198-
{
199-
int[] latestParts = latest.Split('.').Select(int.Parse).ToArray();
200-
int[] installedParts = installed.Split('.').Select(int.Parse).ToArray();
201-
for (int i = 0; i < Math.Min(latestParts.Length, installedParts.Length); i++)
140+
// 2) Installed package: resolve via Package Manager
141+
try
202142
{
203-
if (latestParts[i] > installedParts[i])
143+
var list = UnityEditor.PackageManager.Client.List();
144+
while (!list.IsCompleted) { }
145+
if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
204146
{
205-
return true;
147+
foreach (var pkg in list.Result)
148+
{
149+
if (pkg.name == "com.justinpbarnett.unity-mcp")
150+
{
151+
string packagePath = pkg.resolvedPath; // e.g., Library/PackageCache/... or local path
152+
153+
// Preferred: UnityMcpServer embedded alongside Editor/Runtime within the package
154+
string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
155+
if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
156+
{
157+
srcPath = embedded;
158+
return true;
159+
}
160+
161+
// Legacy: sibling of the package folder (dev-linked). Only valid when present on disk.
162+
string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
163+
if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
164+
{
165+
srcPath = sibling;
166+
return true;
167+
}
168+
}
169+
}
206170
}
171+
}
172+
catch { /* ignore */ }
207173

208-
if (latestParts[i] < installedParts[i])
174+
// 3) Fallback to previous common install locations
175+
try
176+
{
177+
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
178+
string[] candidates =
209179
{
210-
return false;
180+
Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
181+
Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
182+
};
183+
foreach (string candidate in candidates)
184+
{
185+
if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
186+
{
187+
srcPath = candidate;
188+
return true;
189+
}
211190
}
212191
}
213-
return latestParts.Length > installedParts.Length;
192+
catch { /* ignore */ }
193+
194+
srcPath = null;
195+
return false;
214196
}
215197

216-
/// <summary>
217-
/// Runs a command-line process and handles output/errors.
218-
/// </summary>
219-
private static void RunCommand(
220-
string command,
221-
string arguments,
222-
string workingDirectory = null
223-
)
198+
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
224199
{
225-
System.Diagnostics.Process process = new()
200+
Directory.CreateDirectory(destinationDir);
201+
202+
foreach (string filePath in Directory.GetFiles(sourceDir))
226203
{
227-
StartInfo = new System.Diagnostics.ProcessStartInfo
228-
{
229-
FileName = command,
230-
Arguments = arguments,
231-
RedirectStandardOutput = true,
232-
RedirectStandardError = true,
233-
UseShellExecute = false,
234-
CreateNoWindow = true,
235-
WorkingDirectory = workingDirectory ?? string.Empty,
236-
},
237-
};
238-
process.Start();
239-
string output = process.StandardOutput.ReadToEnd();
240-
string error = process.StandardError.ReadToEnd();
241-
process.WaitForExit();
242-
if (process.ExitCode != 0)
204+
string fileName = Path.GetFileName(filePath);
205+
string destFile = Path.Combine(destinationDir, fileName);
206+
File.Copy(filePath, destFile, overwrite: true);
207+
}
208+
209+
foreach (string dirPath in Directory.GetDirectories(sourceDir))
243210
{
244-
throw new Exception(
245-
$"Command failed: {command} {arguments}\nOutput: {output}\nError: {error}"
246-
);
211+
string dirName = Path.GetFileName(dirPath);
212+
string destSubDir = Path.Combine(destinationDir, dirName);
213+
CopyDirectoryRecursive(dirPath, destSubDir);
247214
}
248215
}
249216
}

0 commit comments

Comments
 (0)