From 8984ab95bc8a1e3aa8cfe21c898ab85d681bd167 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Tue, 12 Aug 2025 08:32:51 -0700 Subject: [PATCH] feat: local-only package resolution + Claude CLI resolver; quieter install logs; guarded auto-registration --- UnityMcpBridge/Editor/Helpers/ExecPath.cs | 175 ++++++++++ .../Editor/Helpers/ServerInstaller.cs | 112 +------ .../Editor/Helpers/ServerPathResolver.cs | 151 +++++++++ .../Editor/Windows/UnityMcpEditorWindow.cs | 310 +++--------------- 4 files changed, 391 insertions(+), 357 deletions(-) create mode 100644 UnityMcpBridge/Editor/Helpers/ExecPath.cs create mode 100644 UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs diff --git a/UnityMcpBridge/Editor/Helpers/ExecPath.cs b/UnityMcpBridge/Editor/Helpers/ExecPath.cs new file mode 100644 index 0000000..1848dca --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/ExecPath.cs @@ -0,0 +1,175 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +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_WINDOWS + // 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.Environment["PATH"] = string.IsNullOrEmpty(currentPath) + ? extraPathPrepend + : (extraPathPrepend + System.IO.Path.PathSeparator + currentPath); + } + using var p = Process.Start(psi); + if (p == null) return false; + stdout = p.StandardOutput.ReadToEnd(); + stderr = p.StandardError.ReadToEnd(); + if (!p.WaitForExit(timeoutMs)) { try { p.Kill(); } catch { } return false; } + return p.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.Environment["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_WINDOWS + 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/ServerInstaller.cs b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs index 32a3070..0c3138b 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Runtime.InteropServices; +using System.Reflection; using UnityEditor; using UnityEngine; @@ -42,6 +43,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 +125,7 @@ namespace UnityMcpBridge.Editor.Helpers /// 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) @@ -313,7 +227,7 @@ try } } - private static string FindUvPath() + internal static string FindUvPath() { // Allow user override via EditorPrefs try diff --git a/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs b/UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs new file mode 100644 index 0000000..aa79fd0 --- /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/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 859ce15..f86acd1 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -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; @@ -987,65 +988,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 +1304,66 @@ 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 Node/npm locations so claude can spawn node internally if needed + 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, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend)) { - // 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: Failed to start Claude CLI.\n{stderr}\n{stdout}"); + return; } - 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}' {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); - } - Repaint(); - UnityEngine.Debug.Log("UnityMCP server successfully registered from Claude Code."); - - - } - else if (!string.IsNullOrEmpty(errors)) - { - if (debugLogsEnabled) - { - UnityEngine.Debug.LogWarning($"Claude MCP errors: {errors}"); - } - } - } - 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)) + 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}"); } }