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",