Skip to content

Commit f24e124

Browse files
committed
MCP: Embedded server reliability and UX\n\n- Embed-first installer: copies embedded server, adds RepairPythonEnvironment() (deletes .venv, runs 'uv sync'); robust uv path discovery; macOS install path -> Application Support\n- UI: Server Status shows Installed(Embedded); Python missing warning with install link; Repair button tooltip; header Show Debug Logs; cleaned layout\n- Python: unpin .python-version; set requires-python >=3.10 in both pyprojects\n- Dev: improved package/dev path resolution
1 parent 06f2719 commit f24e124

File tree

7 files changed

+356
-39
lines changed

7 files changed

+356
-39
lines changed

UnityMcpBridge/Editor/Helpers/ServerInstaller.cs

Lines changed: 189 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,11 @@ private static string GetSaveLocation()
7575
}
7676
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
7777
{
78-
string path = "/usr/local/bin";
79-
return !Directory.Exists(path) || !IsDirectoryWritable(path)
80-
? Path.Combine(
81-
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
82-
"Applications",
83-
RootFolder
84-
)
85-
: Path.Combine(path, RootFolder);
78+
// Use Application Support for a stable, user-writable location
79+
return Path.Combine(
80+
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
81+
"UnityMCP"
82+
);
8683
}
8784
throw new Exception("Unsupported operating system.");
8885
}
@@ -213,5 +210,189 @@ private static void CopyDirectoryRecursive(string sourceDir, string destinationD
213210
CopyDirectoryRecursive(dirPath, destSubDir);
214211
}
215212
}
213+
214+
public static bool RepairPythonEnvironment()
215+
{
216+
try
217+
{
218+
string serverSrc = GetServerPath();
219+
bool hasServer = File.Exists(Path.Combine(serverSrc, "server.py"));
220+
if (!hasServer)
221+
{
222+
// In dev mode or if not installed yet, try the embedded/dev source
223+
if (TryGetEmbeddedServerSource(out string embeddedSrc) && File.Exists(Path.Combine(embeddedSrc, "server.py")))
224+
{
225+
serverSrc = embeddedSrc;
226+
hasServer = true;
227+
}
228+
else
229+
{
230+
// Attempt to install then retry
231+
EnsureServerInstalled();
232+
serverSrc = GetServerPath();
233+
hasServer = File.Exists(Path.Combine(serverSrc, "server.py"));
234+
}
235+
}
236+
237+
if (!hasServer)
238+
{
239+
Debug.LogWarning("RepairPythonEnvironment: server.py not found; ensure server is installed first.");
240+
return false;
241+
}
242+
243+
// Remove stale venv and pinned version file if present
244+
string venvPath = Path.Combine(serverSrc, ".venv");
245+
if (Directory.Exists(venvPath))
246+
{
247+
try { Directory.Delete(venvPath, recursive: true); } catch (Exception ex) { Debug.LogWarning($"Failed to delete .venv: {ex.Message}"); }
248+
}
249+
string pyPin = Path.Combine(serverSrc, ".python-version");
250+
if (File.Exists(pyPin))
251+
{
252+
try { File.Delete(pyPin); } catch (Exception ex) { Debug.LogWarning($"Failed to delete .python-version: {ex.Message}"); }
253+
}
254+
255+
string uvPath = FindUvPath();
256+
if (uvPath == null)
257+
{
258+
Debug.LogError("UV not found. Please install uv (https://docs.astral.sh/uv/)." );
259+
return false;
260+
}
261+
262+
var psi = new System.Diagnostics.ProcessStartInfo
263+
{
264+
FileName = uvPath,
265+
Arguments = "sync",
266+
WorkingDirectory = serverSrc,
267+
UseShellExecute = false,
268+
RedirectStandardOutput = true,
269+
RedirectStandardError = true,
270+
CreateNoWindow = true
271+
};
272+
273+
using var p = System.Diagnostics.Process.Start(psi);
274+
string stdout = p.StandardOutput.ReadToEnd();
275+
string stderr = p.StandardError.ReadToEnd();
276+
p.WaitForExit(60000);
277+
278+
if (p.ExitCode != 0)
279+
{
280+
Debug.LogError($"uv sync failed: {stderr}\n{stdout}");
281+
return false;
282+
}
283+
284+
Debug.Log("Unity MCP: Python environment repaired successfully.");
285+
return true;
286+
}
287+
catch (Exception ex)
288+
{
289+
Debug.LogError($"RepairPythonEnvironment failed: {ex.Message}");
290+
return false;
291+
}
292+
}
293+
294+
private static string FindUvPath()
295+
{
296+
// Allow user override via EditorPrefs
297+
try
298+
{
299+
string overridePath = EditorPrefs.GetString("UnityMCP.UvPath", string.Empty);
300+
if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
301+
{
302+
if (ValidateUvBinary(overridePath)) return overridePath;
303+
}
304+
}
305+
catch { }
306+
307+
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
308+
string[] candidates =
309+
{
310+
"/opt/homebrew/bin/uv",
311+
"/usr/local/bin/uv",
312+
"/usr/bin/uv",
313+
"/opt/local/bin/uv",
314+
Path.Combine(home, ".local", "bin", "uv"),
315+
"/opt/homebrew/opt/uv/bin/uv",
316+
// Framework Python installs
317+
"/Library/Frameworks/Python.framework/Versions/3.13/bin/uv",
318+
"/Library/Frameworks/Python.framework/Versions/3.12/bin/uv",
319+
// Fallback to PATH resolution by name
320+
"uv"
321+
};
322+
foreach (string c in candidates)
323+
{
324+
try
325+
{
326+
if (ValidateUvBinary(c)) return c;
327+
}
328+
catch { /* ignore */ }
329+
}
330+
331+
// Try which uv (explicit path)
332+
try
333+
{
334+
var whichPsi = new System.Diagnostics.ProcessStartInfo
335+
{
336+
FileName = "/usr/bin/which",
337+
Arguments = "uv",
338+
UseShellExecute = false,
339+
RedirectStandardOutput = true,
340+
RedirectStandardError = true,
341+
CreateNoWindow = true
342+
};
343+
using var wp = System.Diagnostics.Process.Start(whichPsi);
344+
string output = wp.StandardOutput.ReadToEnd().Trim();
345+
wp.WaitForExit(3000);
346+
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
347+
{
348+
if (ValidateUvBinary(output)) return output;
349+
}
350+
}
351+
catch { }
352+
353+
// Manual PATH scan
354+
try
355+
{
356+
string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
357+
string[] parts = pathEnv.Split(Path.PathSeparator);
358+
foreach (string part in parts)
359+
{
360+
try
361+
{
362+
string candidate = Path.Combine(part, "uv");
363+
if (File.Exists(candidate) && ValidateUvBinary(candidate)) return candidate;
364+
}
365+
catch { }
366+
}
367+
}
368+
catch { }
369+
370+
return null;
371+
}
372+
373+
private static bool ValidateUvBinary(string uvPath)
374+
{
375+
try
376+
{
377+
var psi = new System.Diagnostics.ProcessStartInfo
378+
{
379+
FileName = uvPath,
380+
Arguments = "--version",
381+
UseShellExecute = false,
382+
RedirectStandardOutput = true,
383+
RedirectStandardError = true,
384+
CreateNoWindow = true
385+
};
386+
using var p = System.Diagnostics.Process.Start(psi);
387+
if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; }
388+
if (p.ExitCode == 0)
389+
{
390+
string output = p.StandardOutput.ReadToEnd().Trim();
391+
return output.StartsWith("uv ");
392+
}
393+
}
394+
catch { }
395+
return false;
396+
}
216397
}
217398
}

0 commit comments

Comments
 (0)