Skip to content

Commit 8984ab9

Browse files
committed
feat: local-only package resolution + Claude CLI resolver; quieter install logs; guarded auto-registration
1 parent ae87e3f commit 8984ab9

File tree

4 files changed

+387
-353
lines changed

4 files changed

+387
-353
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Runtime.InteropServices;
6+
using UnityEditor;
7+
8+
namespace UnityMcpBridge.Editor.Helpers
9+
{
10+
internal static class ExecPath
11+
{
12+
private const string PrefClaude = "UnityMCP.ClaudeCliPath";
13+
14+
// Resolve Claude CLI absolute path. Pref → env → common locations → PATH.
15+
internal static string ResolveClaude()
16+
{
17+
try
18+
{
19+
string pref = EditorPrefs.GetString(PrefClaude, string.Empty);
20+
if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref;
21+
}
22+
catch { }
23+
24+
string env = Environment.GetEnvironmentVariable("CLAUDE_CLI");
25+
if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env;
26+
27+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
28+
{
29+
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
30+
string[] candidates =
31+
{
32+
"/opt/homebrew/bin/claude",
33+
"/usr/local/bin/claude",
34+
Path.Combine(home, ".local", "bin", "claude"),
35+
};
36+
foreach (string c in candidates) { if (File.Exists(c)) return c; }
37+
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
38+
return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
39+
#else
40+
return null;
41+
#endif
42+
}
43+
44+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
45+
{
46+
#if UNITY_EDITOR_WINDOWS
47+
// Common npm global locations
48+
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
49+
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
50+
string[] candidates =
51+
{
52+
Path.Combine(appData, "npm", "claude.cmd"),
53+
Path.Combine(localAppData, "npm", "claude.cmd"),
54+
};
55+
foreach (string c in candidates) { if (File.Exists(c)) return c; }
56+
string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude");
57+
if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
58+
#endif
59+
return null;
60+
}
61+
62+
// Linux
63+
{
64+
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
65+
string[] candidates =
66+
{
67+
"/usr/local/bin/claude",
68+
"/usr/bin/claude",
69+
Path.Combine(home, ".local", "bin", "claude"),
70+
};
71+
foreach (string c in candidates) { if (File.Exists(c)) return c; }
72+
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
73+
return Which("claude", "/usr/local/bin:/usr/bin:/bin");
74+
#else
75+
return null;
76+
#endif
77+
}
78+
}
79+
80+
// Use existing UV resolver; returns absolute path or null.
81+
internal static string ResolveUv()
82+
{
83+
return ServerInstaller.FindUvPath();
84+
}
85+
86+
internal static bool TryRun(
87+
string file,
88+
string args,
89+
string workingDir,
90+
out string stdout,
91+
out string stderr,
92+
int timeoutMs = 15000,
93+
string extraPathPrepend = null)
94+
{
95+
stdout = string.Empty;
96+
stderr = string.Empty;
97+
try
98+
{
99+
var psi = new ProcessStartInfo
100+
{
101+
FileName = file,
102+
Arguments = args,
103+
WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
104+
UseShellExecute = false,
105+
RedirectStandardOutput = true,
106+
RedirectStandardError = true,
107+
CreateNoWindow = true,
108+
};
109+
if (!string.IsNullOrEmpty(extraPathPrepend))
110+
{
111+
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
112+
psi.Environment["PATH"] = string.IsNullOrEmpty(currentPath)
113+
? extraPathPrepend
114+
: (extraPathPrepend + System.IO.Path.PathSeparator + currentPath);
115+
}
116+
using var p = Process.Start(psi);
117+
if (p == null) return false;
118+
stdout = p.StandardOutput.ReadToEnd();
119+
stderr = p.StandardError.ReadToEnd();
120+
if (!p.WaitForExit(timeoutMs)) { try { p.Kill(); } catch { } return false; }
121+
return p.ExitCode == 0;
122+
}
123+
catch
124+
{
125+
return false;
126+
}
127+
}
128+
129+
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
130+
private static string Which(string exe, string prependPath)
131+
{
132+
try
133+
{
134+
var psi = new ProcessStartInfo("/usr/bin/which", exe)
135+
{
136+
UseShellExecute = false,
137+
RedirectStandardOutput = true,
138+
CreateNoWindow = true,
139+
};
140+
string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
141+
psi.Environment["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path);
142+
using var p = Process.Start(psi);
143+
string output = p?.StandardOutput.ReadToEnd().Trim();
144+
p?.WaitForExit(1500);
145+
return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null;
146+
}
147+
catch { return null; }
148+
}
149+
#endif
150+
151+
#if UNITY_EDITOR_WINDOWS
152+
private static string Where(string exe)
153+
{
154+
try
155+
{
156+
var psi = new ProcessStartInfo("where", exe)
157+
{
158+
UseShellExecute = false,
159+
RedirectStandardOutput = true,
160+
CreateNoWindow = true,
161+
};
162+
using var p = Process.Start(psi);
163+
string first = p?.StandardOutput.ReadToEnd()
164+
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
165+
.FirstOrDefault();
166+
p?.WaitForExit(1500);
167+
return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null;
168+
}
169+
catch { return null; }
170+
}
171+
#endif
172+
}
173+
}
174+
175+

UnityMcpBridge/Editor/Helpers/ServerInstaller.cs

Lines changed: 13 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.IO;
33
using System.Runtime.InteropServices;
4+
using System.Reflection;
45
using UnityEditor;
56
using UnityEngine;
67

@@ -42,6 +43,16 @@ public static void EnsureServerInstalled()
4243
}
4344
catch (Exception ex)
4445
{
46+
// If a usable server is already present (installed or embedded), don't fail hard—just warn.
47+
bool hasInstalled = false;
48+
try { hasInstalled = File.Exists(Path.Combine(GetServerPath(), "server.py")); } catch { }
49+
50+
if (hasInstalled || TryGetEmbeddedServerSource(out _))
51+
{
52+
Debug.LogWarning($"UnityMCP: Using existing server; skipped install. Details: {ex.Message}");
53+
return;
54+
}
55+
4556
Debug.LogError($"Failed to ensure server installation: {ex.Message}");
4657
}
4758
}
@@ -114,104 +125,7 @@ private static bool IsServerInstalled(string location)
114125
/// </summary>
115126
private static bool TryGetEmbeddedServerSource(out string srcPath)
116127
{
117-
// 1) Development mode: common repo layouts
118-
try
119-
{
120-
string projectRoot = Path.GetDirectoryName(Application.dataPath);
121-
string[] devCandidates =
122-
{
123-
Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
124-
Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
125-
};
126-
foreach (string candidate in devCandidates)
127-
{
128-
string full = Path.GetFullPath(candidate);
129-
if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
130-
{
131-
srcPath = full;
132-
return true;
133-
}
134-
}
135-
}
136-
catch { /* ignore */ }
137-
138-
// 2) Installed package: resolve via Package Manager
139-
// 2) Installed package: resolve via Package Manager (support new + legacy IDs, warn on legacy)
140-
try
141-
{
142-
var list = UnityEditor.PackageManager.Client.List();
143-
while (!list.IsCompleted) { }
144-
if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
145-
{
146-
const string CurrentId = "com.coplaydev.unity-mcp";
147-
const string LegacyId = "com.justinpbarnett.unity-mcp";
148-
149-
foreach (var pkg in list.Result)
150-
{
151-
if (pkg.name == CurrentId || pkg.name == LegacyId)
152-
{
153-
if (pkg.name == LegacyId)
154-
{
155-
Debug.LogWarning(
156-
"UnityMCP: Detected legacy package id 'com.justinpbarnett.unity-mcp'. " +
157-
"Please update Packages/manifest.json to 'com.coplaydev.unity-mcp' to avoid future breakage."
158-
);
159-
}
160-
161-
string packagePath = pkg.resolvedPath; // e.g., Library/PackageCache/... or local path
162-
163-
// Preferred: tilde folder embedded alongside Editor/Runtime within the package
164-
string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src");
165-
if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py")))
166-
{
167-
srcPath = embeddedTilde;
168-
return true;
169-
}
170-
171-
// Fallback: legacy non-tilde folder name inside the package
172-
string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
173-
if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
174-
{
175-
srcPath = embedded;
176-
return true;
177-
}
178-
179-
// Legacy: sibling of the package folder (dev-linked). Only valid when present on disk.
180-
string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
181-
if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
182-
{
183-
srcPath = sibling;
184-
return true;
185-
}
186-
}
187-
}
188-
}
189-
}
190-
191-
catch { /* ignore */ }
192-
193-
// 3) Fallback to previous common install locations
194-
try
195-
{
196-
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
197-
string[] candidates =
198-
{
199-
Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
200-
Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
201-
};
202-
foreach (string candidate in candidates)
203-
{
204-
if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
205-
{
206-
srcPath = candidate;
207-
return true;
208-
}
209-
}
210-
}
211-
catch { /* ignore */ }
212-
213-
srcPath = null;
214-
return false;
128+
return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath);
215129
}
216130

217131
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
@@ -313,7 +227,7 @@ public static bool RepairPythonEnvironment()
313227
}
314228
}
315229

316-
private static string FindUvPath()
230+
internal static string FindUvPath()
317231
{
318232
// Allow user override via EditorPrefs
319233
try

0 commit comments

Comments
 (0)