diff --git a/UnityMcpBridge/Editor/Helpers/ExecPath.cs b/UnityMcpBridge/Editor/Helpers/ExecPath.cs new file mode 100644 index 000000000..537962e63 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/ExecPath.cs @@ -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 + } +} + + diff --git a/UnityMcpBridge/Editor/Helpers/ExecPath.cs.meta b/UnityMcpBridge/Editor/Helpers/ExecPath.cs.meta new file mode 100644 index 000000000..aba921ed9 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/ExecPath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f2b7b3e9c3e4a0f9b2a1d4c7e6f5a12 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs index 32a307017..dbdfb7435 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs @@ -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 @@ public static void EnsureServerInstalled() } 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 @@ private static bool IsServerInstalled(string location) /// 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 @@ public static bool RepairPythonEnvironment() 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 @@ public static bool RepairPythonEnvironment() } } - private static string FindUvPath() + internal static string FindUvPath() { // Allow user override via EditorPrefs try @@ -414,6 +352,22 @@ private static string FindUvPath() 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); diff --git a/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs b/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs new file mode 100644 index 000000000..aa79fd0e3 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs @@ -0,0 +1,151 @@ +using System; +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace UnityMcpBridge.Editor.Helpers +{ + public static class ServerPathResolver + { + /// + /// 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. + /// + 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; + } + } +} + + diff --git a/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs.meta b/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs.meta new file mode 100644 index 000000000..d02df608b --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 859ce15cf..b212719db 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -66,8 +66,8 @@ private void OnEnable() // 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 @@ private void AutoFirstRunSetup() { 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 @@ private static bool IsClaudeConfigured() { 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 @@ private string FindPackagePythonDirectory() } } - // 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) - { - 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) + // Resolve via shared helper (handles local registry and older fallback) + if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)) { - 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 @@ private void CheckMcpConfiguration(McpClient mcpClient) private void RegisterWithClaudeCode(string pythonDir) { - string command; - string args; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - 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"; - } - } - else + // Resolve claude and uv; then run register command + string claudePath = ExecPath.ResolveClaude(); + if (string.IsNullOrEmpty(claudePath)) { - // Use full path to claude command - command = "/usr/local/bin/claude"; - args = $"mcp add UnityMCP -- uv --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; } + string uvPath = ExecPath.ResolveUv() ?? "uv"; - try - { - // Get the Unity project directory (where the Assets folder is) - string unityProjectDir = Application.dataPath; - string projectDir = Path.GetDirectoryName(unityProjectDir); + // Prefer embedded/dev path when available + string srcDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory(); + if (string.IsNullOrEmpty(srcDir)) srcDir = pythonDir; - 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}' {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}"; - } + string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{srcDir}\" server.py"; - 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")) + 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) + { + pathPrepend = Application.platform == RuntimePlatform.OSXEditor + ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" + : "/usr/local/bin:/usr/bin:/bin"; + } + if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend)) + { + string combined = ($"{stdout}\n{stderr}") ?? string.Empty; + if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0) { - // 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("UNITY-MCP: 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("UNITY-MCP: Registered with Claude Code."); } private void UnregisterWithClaudeCode() { - string command; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - command = FindClaudeCommand(); - - if (string.IsNullOrEmpty(command)) - { - UnityEngine.Debug.LogError("Claude CLI not found. Please ensure Claude Code is installed and accessible."); - return; - } - } - else + string claudePath = ExecPath.ResolveClaude(); + if (string.IsNullOrEmpty(claudePath)) { - // Use full path to claude command - command = "/usr/local/bin/claude"; + UnityEngine.Debug.LogError("UnityMCP: Claude CLI not found. Set a path in this window or install the CLI, then try again."); + return; } - try - { - // Get the Unity project directory (where the Assets folder is) - string unityProjectDir = Application.dataPath; - string projectDir = Path.GetDirectoryName(unityProjectDir); + 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"; - 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)) + 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.LogWarning($"Claude MCP removal errors: {errors}"); + CheckClaudeCodeConfiguration(claudeClient); } + Repaint(); + UnityEngine.Debug.Log("UnityMCP server successfully unregistered from Claude Code."); } - catch (Exception e) + else { - UnityEngine.Debug.LogError($"Claude CLI unregistration failed: {e.Message}"); + UnityEngine.Debug.LogWarning($"Claude MCP removal failed: {stderr}\n{stdout}"); } } diff --git a/UnityMcpBridge/package.json b/UnityMcpBridge/package.json index 0e3ccdfc0..1091f69fb 100644 --- a/UnityMcpBridge/package.json +++ b/UnityMcpBridge/package.json @@ -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",