Merge pull request #206 from dsarno/feat/local-resolution-and-claude-cli
Feat: Local-only package resolution + Claude CLI resolver; quieter installer logsmain
commit
86198a0484
|
|
@ -0,0 +1,195 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityMcpBridge.Editor.Helpers
|
||||
{
|
||||
internal static class ExecPath
|
||||
{
|
||||
private const string PrefClaude = "UnityMCP.ClaudeCliPath";
|
||||
|
||||
// Resolve Claude CLI absolute path. Pref → env → common locations → PATH.
|
||||
internal static string ResolveClaude()
|
||||
{
|
||||
try
|
||||
{
|
||||
string pref = EditorPrefs.GetString(PrefClaude, string.Empty);
|
||||
if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref;
|
||||
}
|
||||
catch { }
|
||||
|
||||
string env = Environment.GetEnvironmentVariable("CLAUDE_CLI");
|
||||
if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||
string[] candidates =
|
||||
{
|
||||
"/opt/homebrew/bin/claude",
|
||||
"/usr/local/bin/claude",
|
||||
Path.Combine(home, ".local", "bin", "claude"),
|
||||
};
|
||||
foreach (string c in candidates) { if (File.Exists(c)) return c; }
|
||||
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
|
||||
#else
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
#if UNITY_EDITOR_WIN
|
||||
// Common npm global locations
|
||||
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
|
||||
string[] candidates =
|
||||
{
|
||||
Path.Combine(appData, "npm", "claude.cmd"),
|
||||
Path.Combine(localAppData, "npm", "claude.cmd"),
|
||||
};
|
||||
foreach (string c in candidates) { if (File.Exists(c)) return c; }
|
||||
string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude");
|
||||
if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
// Linux
|
||||
{
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||
string[] candidates =
|
||||
{
|
||||
"/usr/local/bin/claude",
|
||||
"/usr/bin/claude",
|
||||
Path.Combine(home, ".local", "bin", "claude"),
|
||||
};
|
||||
foreach (string c in candidates) { if (File.Exists(c)) return c; }
|
||||
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return Which("claude", "/usr/local/bin:/usr/bin:/bin");
|
||||
#else
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Use existing UV resolver; returns absolute path or null.
|
||||
internal static string ResolveUv()
|
||||
{
|
||||
return ServerInstaller.FindUvPath();
|
||||
}
|
||||
|
||||
internal static bool TryRun(
|
||||
string file,
|
||||
string args,
|
||||
string workingDir,
|
||||
out string stdout,
|
||||
out string stderr,
|
||||
int timeoutMs = 15000,
|
||||
string extraPathPrepend = null)
|
||||
{
|
||||
stdout = string.Empty;
|
||||
stderr = string.Empty;
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = file,
|
||||
Arguments = args,
|
||||
WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
if (!string.IsNullOrEmpty(extraPathPrepend))
|
||||
{
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath)
|
||||
? extraPathPrepend
|
||||
: (extraPathPrepend + System.IO.Path.PathSeparator + currentPath);
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = psi, EnableRaisingEvents = false };
|
||||
|
||||
var so = new StringBuilder();
|
||||
var se = new StringBuilder();
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); };
|
||||
|
||||
if (!process.Start()) return false;
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
if (!process.WaitForExit(timeoutMs))
|
||||
{
|
||||
try { process.Kill(); } catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure async buffers are flushed
|
||||
process.WaitForExit();
|
||||
|
||||
stdout = so.ToString();
|
||||
stderr = se.ToString();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
private static string Which(string exe, string prependPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("/usr/bin/which", exe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path);
|
||||
using var p = Process.Start(psi);
|
||||
string output = p?.StandardOutput.ReadToEnd().Trim();
|
||||
p?.WaitForExit(1500);
|
||||
return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
#endif
|
||||
|
||||
#if UNITY_EDITOR_WIN
|
||||
private static string Where(string exe)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("where", exe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
using var p = Process.Start(psi);
|
||||
string first = p?.StandardOutput.ReadToEnd()
|
||||
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault();
|
||||
p?.WaitForExit(1500);
|
||||
return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8f2b7b3e9c3e4a0f9b2a1d4c7e6f5a12
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
|
|
@ -42,6 +44,16 @@ namespace UnityMcpBridge.Editor.Helpers
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If a usable server is already present (installed or embedded), don't fail hard—just warn.
|
||||
bool hasInstalled = false;
|
||||
try { hasInstalled = File.Exists(Path.Combine(GetServerPath(), "server.py")); } catch { }
|
||||
|
||||
if (hasInstalled || TryGetEmbeddedServerSource(out _))
|
||||
{
|
||||
Debug.LogWarning($"UnityMCP: Using existing server; skipped install. Details: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogError($"Failed to ensure server installation: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
|
@ -114,104 +126,7 @@ namespace UnityMcpBridge.Editor.Helpers
|
|||
/// </summary>
|
||||
private static bool TryGetEmbeddedServerSource(out string srcPath)
|
||||
{
|
||||
// 1) Development mode: common repo layouts
|
||||
try
|
||||
{
|
||||
string projectRoot = Path.GetDirectoryName(Application.dataPath);
|
||||
string[] devCandidates =
|
||||
{
|
||||
Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
|
||||
Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
|
||||
};
|
||||
foreach (string candidate in devCandidates)
|
||||
{
|
||||
string full = Path.GetFullPath(candidate);
|
||||
if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
|
||||
{
|
||||
srcPath = full;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
|
||||
// 2) Installed package: resolve via Package Manager
|
||||
// 2) Installed package: resolve via Package Manager (support new + legacy IDs, warn on legacy)
|
||||
try
|
||||
{
|
||||
var list = UnityEditor.PackageManager.Client.List();
|
||||
while (!list.IsCompleted) { }
|
||||
if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
|
||||
{
|
||||
const string CurrentId = "com.coplaydev.unity-mcp";
|
||||
const string LegacyId = "com.justinpbarnett.unity-mcp";
|
||||
|
||||
foreach (var pkg in list.Result)
|
||||
{
|
||||
if (pkg.name == CurrentId || pkg.name == LegacyId)
|
||||
{
|
||||
if (pkg.name == LegacyId)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
"UnityMCP: Detected legacy package id 'com.justinpbarnett.unity-mcp'. " +
|
||||
"Please update Packages/manifest.json to 'com.coplaydev.unity-mcp' to avoid future breakage."
|
||||
);
|
||||
}
|
||||
|
||||
string packagePath = pkg.resolvedPath; // e.g., Library/PackageCache/... or local path
|
||||
|
||||
// Preferred: tilde folder embedded alongside Editor/Runtime within the package
|
||||
string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src");
|
||||
if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py")))
|
||||
{
|
||||
srcPath = embeddedTilde;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: legacy non-tilde folder name inside the package
|
||||
string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
|
||||
if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
|
||||
{
|
||||
srcPath = embedded;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy: sibling of the package folder (dev-linked). Only valid when present on disk.
|
||||
string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
|
||||
if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
|
||||
{
|
||||
srcPath = sibling;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
catch { /* ignore */ }
|
||||
|
||||
// 3) Fallback to previous common install locations
|
||||
try
|
||||
{
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
string[] candidates =
|
||||
{
|
||||
Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
|
||||
Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
|
||||
};
|
||||
foreach (string candidate in candidates)
|
||||
{
|
||||
if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
|
||||
{
|
||||
srcPath = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
|
||||
srcPath = null;
|
||||
return false;
|
||||
return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath);
|
||||
}
|
||||
|
||||
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
|
||||
|
|
@ -292,12 +207,35 @@ try
|
|||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var p = System.Diagnostics.Process.Start(psi);
|
||||
string stdout = p.StandardOutput.ReadToEnd();
|
||||
string stderr = p.StandardError.ReadToEnd();
|
||||
p.WaitForExit(60000);
|
||||
using var proc = new System.Diagnostics.Process { StartInfo = psi };
|
||||
var sbOut = new StringBuilder();
|
||||
var sbErr = new StringBuilder();
|
||||
proc.OutputDataReceived += (_, e) => { if (e.Data != null) sbOut.AppendLine(e.Data); };
|
||||
proc.ErrorDataReceived += (_, e) => { if (e.Data != null) sbErr.AppendLine(e.Data); };
|
||||
|
||||
if (p.ExitCode != 0)
|
||||
if (!proc.Start())
|
||||
{
|
||||
Debug.LogError("Failed to start uv process.");
|
||||
return false;
|
||||
}
|
||||
|
||||
proc.BeginOutputReadLine();
|
||||
proc.BeginErrorReadLine();
|
||||
|
||||
if (!proc.WaitForExit(60000))
|
||||
{
|
||||
try { proc.Kill(); } catch { }
|
||||
Debug.LogError("uv sync timed out.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure async buffers flushed
|
||||
proc.WaitForExit();
|
||||
|
||||
string stdout = sbOut.ToString();
|
||||
string stderr = sbErr.ToString();
|
||||
|
||||
if (proc.ExitCode != 0)
|
||||
{
|
||||
Debug.LogError($"uv sync failed: {stderr}\n{stdout}");
|
||||
return false;
|
||||
|
|
@ -313,7 +251,7 @@ try
|
|||
}
|
||||
}
|
||||
|
||||
private static string FindUvPath()
|
||||
internal static string FindUvPath()
|
||||
{
|
||||
// Allow user override via EditorPrefs
|
||||
try
|
||||
|
|
@ -414,6 +352,22 @@ try
|
|||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
try
|
||||
{
|
||||
// Prepend common user-local and package manager locations so 'which' can see them in Unity's GUI env
|
||||
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||
string prepend = string.Join(":", new[]
|
||||
{
|
||||
System.IO.Path.Combine(homeDir, ".local", "bin"),
|
||||
"/opt/homebrew/bin",
|
||||
"/usr/local/bin",
|
||||
"/usr/bin",
|
||||
"/bin"
|
||||
});
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath);
|
||||
}
|
||||
catch { }
|
||||
using var wp = System.Diagnostics.Process.Start(whichPsi);
|
||||
string output = wp.StandardOutput.ReadToEnd().Trim();
|
||||
wp.WaitForExit(3000);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityMcpBridge.Editor.Helpers
|
||||
{
|
||||
public static class ServerPathResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
|
||||
/// or common development locations. Returns true if found and sets srcPath to the folder
|
||||
/// containing server.py.
|
||||
/// </summary>
|
||||
public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLegacyPackageId = true)
|
||||
{
|
||||
// 1) Repo development layouts commonly used alongside this package
|
||||
try
|
||||
{
|
||||
string projectRoot = Path.GetDirectoryName(Application.dataPath);
|
||||
string[] devCandidates =
|
||||
{
|
||||
Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
|
||||
Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
|
||||
};
|
||||
foreach (string candidate in devCandidates)
|
||||
{
|
||||
string full = Path.GetFullPath(candidate);
|
||||
if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
|
||||
{
|
||||
srcPath = full;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
|
||||
// 2) Resolve via local package info (no network). Fall back to Client.List on older editors.
|
||||
try
|
||||
{
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
// Primary: the package that owns this assembly
|
||||
var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly);
|
||||
if (owner != null)
|
||||
{
|
||||
if (TryResolveWithinPackage(owner, out srcPath, warnOnLegacyPackageId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary: scan all registered packages locally
|
||||
foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
|
||||
{
|
||||
if (TryResolveWithinPackage(p, out srcPath, warnOnLegacyPackageId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#else
|
||||
// Older Unity versions: use Package Manager Client.List as a fallback
|
||||
var list = UnityEditor.PackageManager.Client.List();
|
||||
while (!list.IsCompleted) { }
|
||||
if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
|
||||
{
|
||||
foreach (var pkg in list.Result)
|
||||
{
|
||||
if (TryResolveWithinPackage(pkg, out srcPath, warnOnLegacyPackageId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
|
||||
// 3) Fallback to previous common install locations
|
||||
try
|
||||
{
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||
string[] candidates =
|
||||
{
|
||||
Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
|
||||
Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
|
||||
};
|
||||
foreach (string candidate in candidates)
|
||||
{
|
||||
if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
|
||||
{
|
||||
srcPath = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
|
||||
srcPath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath, bool warnOnLegacyPackageId)
|
||||
{
|
||||
const string CurrentId = "com.coplaydev.unity-mcp";
|
||||
const string LegacyId = "com.justinpbarnett.unity-mcp";
|
||||
|
||||
srcPath = null;
|
||||
if (p == null || (p.name != CurrentId && p.name != LegacyId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (warnOnLegacyPackageId && p.name == LegacyId)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
"UnityMCP: Detected legacy package id 'com.justinpbarnett.unity-mcp'. " +
|
||||
"Please update Packages/manifest.json to 'com.coplaydev.unity-mcp' to avoid future breakage.");
|
||||
}
|
||||
|
||||
string packagePath = p.resolvedPath;
|
||||
|
||||
// Preferred tilde folder (embedded but excluded from import)
|
||||
string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src");
|
||||
if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py")))
|
||||
{
|
||||
srcPath = embeddedTilde;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy non-tilde folder
|
||||
string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
|
||||
if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
|
||||
{
|
||||
srcPath = embedded;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dev-linked sibling of the package folder
|
||||
string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
|
||||
if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
|
||||
{
|
||||
srcPath = sibling;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -66,8 +66,8 @@ namespace UnityMcpBridge.Editor.Windows
|
|||
// Load validation level setting
|
||||
LoadValidationLevelSetting();
|
||||
|
||||
// First-run auto-setup (register client(s) and ensure bridge is listening)
|
||||
if (autoRegisterEnabled)
|
||||
// First-run auto-setup only if Claude CLI is available
|
||||
if (autoRegisterEnabled && !string.IsNullOrEmpty(ExecPath.ResolveClaude()))
|
||||
{
|
||||
AutoFirstRunSetup();
|
||||
}
|
||||
|
|
@ -492,7 +492,8 @@ namespace UnityMcpBridge.Editor.Windows
|
|||
{
|
||||
if (client.mcpType == McpTypes.ClaudeCode)
|
||||
{
|
||||
if (!IsClaudeConfigured())
|
||||
// Only attempt if Claude CLI is present
|
||||
if (!IsClaudeConfigured() && !string.IsNullOrEmpty(ExecPath.ResolveClaude()))
|
||||
{
|
||||
RegisterWithClaudeCode(pythonDir);
|
||||
anyRegistered = true;
|
||||
|
|
@ -653,13 +654,23 @@ namespace UnityMcpBridge.Editor.Windows
|
|||
{
|
||||
try
|
||||
{
|
||||
string command = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "claude" : "/usr/local/bin/claude";
|
||||
var psi = new ProcessStartInfo { FileName = command, Arguments = "mcp list", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true };
|
||||
using var p = Process.Start(psi);
|
||||
string output = p.StandardOutput.ReadToEnd();
|
||||
p.WaitForExit(3000);
|
||||
if (p.ExitCode != 0) return false;
|
||||
return output.IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
string claudePath = ExecPath.ResolveClaude();
|
||||
if (string.IsNullOrEmpty(claudePath)) return false;
|
||||
|
||||
// Only prepend PATH on Unix
|
||||
string pathPrepend = null;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
pathPrepend = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||
? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
: "/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
|
||||
if (!ExecPath.TryRun(claudePath, "mcp list", workingDir: null, out var stdout, out var stderr, 5000, pathPrepend))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return (stdout ?? string.Empty).IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
|
@ -987,65 +998,10 @@ namespace UnityMcpBridge.Editor.Windows
|
|||
}
|
||||
}
|
||||
|
||||
// Try to find the package using Package Manager API
|
||||
UnityEditor.PackageManager.Requests.ListRequest request =
|
||||
UnityEditor.PackageManager.Client.List();
|
||||
while (!request.IsCompleted) { } // Wait for the request to complete
|
||||
|
||||
if (request.Status == UnityEditor.PackageManager.StatusCode.Success)
|
||||
// Resolve via shared helper (handles local registry and older fallback)
|
||||
if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
|
||||
{
|
||||
foreach (UnityEditor.PackageManager.PackageInfo package in request.Result)
|
||||
{
|
||||
if (package.name == "com.coplaydev.unity-mcp")
|
||||
{
|
||||
string packagePath = package.resolvedPath;
|
||||
|
||||
// Preferred: check for tilde folder inside package
|
||||
string packagedTildeDir = Path.Combine(packagePath, "UnityMcpServer~", "src");
|
||||
if (Directory.Exists(packagedTildeDir) && File.Exists(Path.Combine(packagedTildeDir, "server.py")))
|
||||
{
|
||||
return packagedTildeDir;
|
||||
}
|
||||
|
||||
// Fallback: legacy local package structure (UnityMcpServer/src)
|
||||
string localPythonDir = Path.Combine(Path.GetDirectoryName(packagePath), "UnityMcpServer", "src");
|
||||
if (Directory.Exists(localPythonDir) && File.Exists(Path.Combine(localPythonDir, "server.py")))
|
||||
{
|
||||
return localPythonDir;
|
||||
}
|
||||
|
||||
// Check for old structure (Python subdirectory)
|
||||
string potentialPythonDir = Path.Combine(packagePath, "Python");
|
||||
if (Directory.Exists(potentialPythonDir) && File.Exists(Path.Combine(potentialPythonDir, "server.py")))
|
||||
{
|
||||
return potentialPythonDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (request.Error != null)
|
||||
{
|
||||
UnityEngine.Debug.LogError("Failed to list packages: " + request.Error.message);
|
||||
}
|
||||
|
||||
// If not found via Package Manager, try manual approaches
|
||||
// Check for local development structure
|
||||
string[] possibleDirs =
|
||||
{
|
||||
// Check in user's home directory (common installation location)
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "unity-mcp", "UnityMcpServer", "src"),
|
||||
// Check in Applications folder (macOS/Linux common location)
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications", "UnityMCP", "UnityMcpServer", "src"),
|
||||
// Legacy Python folder structure
|
||||
Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python")),
|
||||
};
|
||||
|
||||
foreach (string dir in possibleDirs)
|
||||
{
|
||||
if (Directory.Exists(dir) && File.Exists(Path.Combine(dir, "server.py")))
|
||||
{
|
||||
return dir;
|
||||
}
|
||||
return embedded;
|
||||
}
|
||||
|
||||
// If still not found, return the placeholder path
|
||||
|
|
@ -1358,218 +1314,82 @@ namespace UnityMcpBridge.Editor.Windows
|
|||
|
||||
private void RegisterWithClaudeCode(string pythonDir)
|
||||
{
|
||||
string command;
|
||||
string args;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
// Resolve claude and uv; then run register command
|
||||
string claudePath = ExecPath.ResolveClaude();
|
||||
if (string.IsNullOrEmpty(claudePath))
|
||||
{
|
||||
command = FindClaudeCommand();
|
||||
|
||||
if (string.IsNullOrEmpty(command))
|
||||
{
|
||||
UnityEngine.Debug.LogError("Claude CLI not found. Please ensure Claude Code is installed and accessible.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find uv.exe in common locations
|
||||
string uvPath = FindUvPath();
|
||||
|
||||
if (string.IsNullOrEmpty(uvPath))
|
||||
{
|
||||
// Fallback to expecting uv in PATH
|
||||
args = $"mcp add UnityMCP -- uv --directory \"{pythonDir}\" run server.py";
|
||||
}
|
||||
else
|
||||
{
|
||||
args = $"mcp add UnityMCP -- \"{uvPath}\" --directory \"{pythonDir}\" run server.py";
|
||||
}
|
||||
UnityEngine.Debug.LogError("UnityMCP: Claude CLI not found. Set a path in this window or install the CLI, then try again.");
|
||||
return;
|
||||
}
|
||||
else
|
||||
string uvPath = ExecPath.ResolveUv() ?? "uv";
|
||||
|
||||
// Prefer embedded/dev path when available
|
||||
string srcDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory();
|
||||
if (string.IsNullOrEmpty(srcDir)) srcDir = pythonDir;
|
||||
|
||||
string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{srcDir}\" server.py";
|
||||
|
||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
// Ensure PATH includes common locations on Unix; on Windows leave PATH as-is
|
||||
string pathPrepend = null;
|
||||
if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.LinuxEditor)
|
||||
{
|
||||
// Use full path to claude command
|
||||
command = "/usr/local/bin/claude";
|
||||
args = $"mcp add UnityMCP -- uv --directory \"{pythonDir}\" run server.py";
|
||||
pathPrepend = Application.platform == RuntimePlatform.OSXEditor
|
||||
? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
: "/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
|
||||
try
|
||||
if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
|
||||
{
|
||||
// Get the Unity project directory (where the Assets folder is)
|
||||
string unityProjectDir = Application.dataPath;
|
||||
string projectDir = Path.GetDirectoryName(unityProjectDir);
|
||||
|
||||
var psi = new ProcessStartInfo();
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
string combined = ($"{stdout}\n{stderr}") ?? string.Empty;
|
||||
if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
// On Windows, run through PowerShell with explicit PATH setting
|
||||
psi.FileName = "powershell.exe";
|
||||
string nodePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "nodejs");
|
||||
psi.Arguments = $"-Command \"$env:PATH += ';{nodePath}'; & '{command}' {args}\"";
|
||||
UnityEngine.Debug.Log($"Executing: powershell.exe {psi.Arguments}");
|
||||
}
|
||||
else
|
||||
{
|
||||
psi.FileName = command;
|
||||
psi.Arguments = args;
|
||||
UnityEngine.Debug.Log($"Executing: {command} {args}");
|
||||
}
|
||||
|
||||
psi.UseShellExecute = false;
|
||||
psi.RedirectStandardOutput = true;
|
||||
psi.RedirectStandardError = true;
|
||||
psi.CreateNoWindow = true;
|
||||
psi.WorkingDirectory = projectDir;
|
||||
|
||||
// Set PATH to include common binary locations (OS-specific)
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Windows: Add common Node.js and npm locations
|
||||
string[] windowsPaths = {
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "nodejs"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "nodejs"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "npm"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "npm")
|
||||
};
|
||||
string additionalPaths = string.Join(";", windowsPaths);
|
||||
psi.EnvironmentVariables["PATH"] = $"{currentPath};{additionalPaths}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// macOS/Linux: Add common binary locations
|
||||
string additionalPaths = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin";
|
||||
psi.EnvironmentVariables["PATH"] = $"{additionalPaths}:{currentPath}";
|
||||
}
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
string errors = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
|
||||
|
||||
// Check for success or already exists
|
||||
if (output.Contains("Added stdio MCP server") || errors.Contains("already exists"))
|
||||
{
|
||||
// Force refresh the configuration status
|
||||
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||
if (claudeClient != null)
|
||||
{
|
||||
CheckClaudeCodeConfiguration(claudeClient);
|
||||
}
|
||||
// Treat as success if Claude reports existing registration
|
||||
var existingClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||
if (existingClient != null) CheckClaudeCodeConfiguration(existingClient);
|
||||
Repaint();
|
||||
UnityEngine.Debug.Log("UnityMCP server successfully registered from Claude Code.");
|
||||
|
||||
|
||||
UnityEngine.Debug.Log("<b><color=#2EA3FF>UNITY-MCP</color></b>: UnityMCP already registered with Claude Code.");
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(errors))
|
||||
else
|
||||
{
|
||||
if (debugLogsEnabled)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"Claude MCP errors: {errors}");
|
||||
}
|
||||
UnityEngine.Debug.LogError($"UnityMCP: Failed to start Claude CLI.\n{stderr}\n{stdout}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"Claude CLI registration failed: {e.Message}");
|
||||
}
|
||||
|
||||
// Update status
|
||||
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||
if (claudeClient != null) CheckClaudeCodeConfiguration(claudeClient);
|
||||
Repaint();
|
||||
UnityEngine.Debug.Log("<b><color=#2EA3FF>UNITY-MCP</color></b>: Registered with Claude Code.");
|
||||
}
|
||||
|
||||
private void UnregisterWithClaudeCode()
|
||||
{
|
||||
string command;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
string claudePath = ExecPath.ResolveClaude();
|
||||
if (string.IsNullOrEmpty(claudePath))
|
||||
{
|
||||
command = FindClaudeCommand();
|
||||
|
||||
if (string.IsNullOrEmpty(command))
|
||||
UnityEngine.Debug.LogError("UnityMCP: Claude CLI not found. Set a path in this window or install the CLI, then try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
string pathPrepend = Application.platform == RuntimePlatform.OSXEditor
|
||||
? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
: "/usr/local/bin:/usr/bin:/bin";
|
||||
|
||||
if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
|
||||
{
|
||||
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||
if (claudeClient != null)
|
||||
{
|
||||
UnityEngine.Debug.LogError("Claude CLI not found. Please ensure Claude Code is installed and accessible.");
|
||||
return;
|
||||
CheckClaudeCodeConfiguration(claudeClient);
|
||||
}
|
||||
Repaint();
|
||||
UnityEngine.Debug.Log("UnityMCP server successfully unregistered from Claude Code.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use full path to claude command
|
||||
command = "/usr/local/bin/claude";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get the Unity project directory (where the Assets folder is)
|
||||
string unityProjectDir = Application.dataPath;
|
||||
string projectDir = Path.GetDirectoryName(unityProjectDir);
|
||||
|
||||
var psi = new ProcessStartInfo();
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// On Windows, run through PowerShell with explicit PATH setting
|
||||
psi.FileName = "powershell.exe";
|
||||
string nodePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "nodejs");
|
||||
psi.Arguments = $"-Command \"$env:PATH += ';{nodePath}'; & '{command}' mcp remove UnityMCP\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
psi.FileName = command;
|
||||
psi.Arguments = "mcp remove UnityMCP";
|
||||
}
|
||||
|
||||
psi.UseShellExecute = false;
|
||||
psi.RedirectStandardOutput = true;
|
||||
psi.RedirectStandardError = true;
|
||||
psi.CreateNoWindow = true;
|
||||
psi.WorkingDirectory = projectDir;
|
||||
|
||||
// Set PATH to include common binary locations (OS-specific)
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Windows: Add common Node.js and npm locations
|
||||
string[] windowsPaths = {
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "nodejs"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "nodejs"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "npm"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "npm")
|
||||
};
|
||||
string additionalPaths = string.Join(";", windowsPaths);
|
||||
psi.EnvironmentVariables["PATH"] = $"{currentPath};{additionalPaths}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// macOS/Linux: Add common binary locations
|
||||
string additionalPaths = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin";
|
||||
psi.EnvironmentVariables["PATH"] = $"{additionalPaths}:{currentPath}";
|
||||
}
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
string errors = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
// Check for success
|
||||
if (output.Contains("Removed MCP server") || process.ExitCode == 0)
|
||||
{
|
||||
// Force refresh the configuration status
|
||||
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||
if (claudeClient != null)
|
||||
{
|
||||
CheckClaudeCodeConfiguration(claudeClient);
|
||||
}
|
||||
Repaint();
|
||||
|
||||
UnityEngine.Debug.Log("UnityMCP server successfully unregistered from Claude Code.");
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(errors))
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"Claude MCP removal errors: {errors}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"Claude CLI unregistration failed: {e.Message}");
|
||||
UnityEngine.Debug.LogWarning($"Claude MCP removal failed: {stderr}\n{stdout}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "com.coplaydev.unity-mcp",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"displayName": "Unity MCP Bridge",
|
||||
"description": "A bridge that manages and communicates with the sister application, Unity MCP Server, which allows for communications with MCP Clients like Claude Desktop or Cursor.",
|
||||
"unity": "2020.3",
|
||||
|
|
|
|||
Loading…
Reference in New Issue