Merge pull request #206 from dsarno/feat/local-resolution-and-claude-cli

Feat: Local-only package resolution + Claude CLI resolver; quieter installer logs
main
dsarno 2025-08-12 12:19:02 -07:00 committed by GitHub
commit 86198a0484
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 506 additions and 364 deletions

View File

@ -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
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f2b7b3e9c3e4a0f9b2a1d4c7e6f5a12
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,6 +1,8 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Reflection;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
@ -42,6 +44,16 @@ namespace UnityMcpBridge.Editor.Helpers
} }
catch (Exception ex) 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}"); Debug.LogError($"Failed to ensure server installation: {ex.Message}");
} }
} }
@ -114,104 +126,7 @@ namespace UnityMcpBridge.Editor.Helpers
/// </summary> /// </summary>
private static bool TryGetEmbeddedServerSource(out string srcPath) private static bool TryGetEmbeddedServerSource(out string srcPath)
{ {
// 1) Development mode: common repo layouts return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath);
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;
} }
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir) private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
@ -292,12 +207,35 @@ try
CreateNoWindow = true CreateNoWindow = true
}; };
using var p = System.Diagnostics.Process.Start(psi); using var proc = new System.Diagnostics.Process { StartInfo = psi };
string stdout = p.StandardOutput.ReadToEnd(); var sbOut = new StringBuilder();
string stderr = p.StandardError.ReadToEnd(); var sbErr = new StringBuilder();
p.WaitForExit(60000); 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}"); Debug.LogError($"uv sync failed: {stderr}\n{stdout}");
return false; return false;
@ -313,7 +251,7 @@ try
} }
} }
private static string FindUvPath() internal static string FindUvPath()
{ {
// Allow user override via EditorPrefs // Allow user override via EditorPrefs
try try
@ -414,6 +352,22 @@ try
RedirectStandardError = true, RedirectStandardError = true,
CreateNoWindow = 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); using var wp = System.Diagnostics.Process.Start(whichPsi);
string output = wp.StandardOutput.ReadToEnd().Trim(); string output = wp.StandardOutput.ReadToEnd().Trim();
wp.WaitForExit(3000); wp.WaitForExit(3000);

View File

@ -0,0 +1,151 @@
using System;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace UnityMcpBridge.Editor.Helpers
{
public static class ServerPathResolver
{
/// <summary>
/// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
/// or common development locations. Returns true if found and sets srcPath to the folder
/// containing server.py.
/// </summary>
public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLegacyPackageId = true)
{
// 1) Repo development layouts commonly used alongside this package
try
{
string projectRoot = Path.GetDirectoryName(Application.dataPath);
string[] devCandidates =
{
Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
};
foreach (string candidate in devCandidates)
{
string full = Path.GetFullPath(candidate);
if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
{
srcPath = full;
return true;
}
}
}
catch { /* ignore */ }
// 2) Resolve via local package info (no network). Fall back to Client.List on older editors.
try
{
#if UNITY_2021_2_OR_NEWER
// Primary: the package that owns this assembly
var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly);
if (owner != null)
{
if (TryResolveWithinPackage(owner, out srcPath, warnOnLegacyPackageId))
{
return true;
}
}
// Secondary: scan all registered packages locally
foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
{
if (TryResolveWithinPackage(p, out srcPath, warnOnLegacyPackageId))
{
return true;
}
}
#else
// Older Unity versions: use Package Manager Client.List as a fallback
var list = UnityEditor.PackageManager.Client.List();
while (!list.IsCompleted) { }
if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
{
foreach (var pkg in list.Result)
{
if (TryResolveWithinPackage(pkg, out srcPath, warnOnLegacyPackageId))
{
return true;
}
}
}
#endif
}
catch { /* ignore */ }
// 3) Fallback to previous common install locations
try
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string[] candidates =
{
Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
};
foreach (string candidate in candidates)
{
if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
{
srcPath = candidate;
return true;
}
}
}
catch { /* ignore */ }
srcPath = null;
return false;
}
private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath, bool warnOnLegacyPackageId)
{
const string CurrentId = "com.coplaydev.unity-mcp";
const string LegacyId = "com.justinpbarnett.unity-mcp";
srcPath = null;
if (p == null || (p.name != CurrentId && p.name != LegacyId))
{
return false;
}
if (warnOnLegacyPackageId && p.name == LegacyId)
{
Debug.LogWarning(
"UnityMCP: Detected legacy package id 'com.justinpbarnett.unity-mcp'. " +
"Please update Packages/manifest.json to 'com.coplaydev.unity-mcp' to avoid future breakage.");
}
string packagePath = p.resolvedPath;
// Preferred tilde folder (embedded but excluded from import)
string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src");
if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py")))
{
srcPath = embeddedTilde;
return true;
}
// Legacy non-tilde folder
string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
{
srcPath = embedded;
return true;
}
// Dev-linked sibling of the package folder
string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
{
srcPath = sibling;
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -66,8 +66,8 @@ namespace UnityMcpBridge.Editor.Windows
// Load validation level setting // Load validation level setting
LoadValidationLevelSetting(); LoadValidationLevelSetting();
// First-run auto-setup (register client(s) and ensure bridge is listening) // First-run auto-setup only if Claude CLI is available
if (autoRegisterEnabled) if (autoRegisterEnabled && !string.IsNullOrEmpty(ExecPath.ResolveClaude()))
{ {
AutoFirstRunSetup(); AutoFirstRunSetup();
} }
@ -492,7 +492,8 @@ namespace UnityMcpBridge.Editor.Windows
{ {
if (client.mcpType == McpTypes.ClaudeCode) if (client.mcpType == McpTypes.ClaudeCode)
{ {
if (!IsClaudeConfigured()) // Only attempt if Claude CLI is present
if (!IsClaudeConfigured() && !string.IsNullOrEmpty(ExecPath.ResolveClaude()))
{ {
RegisterWithClaudeCode(pythonDir); RegisterWithClaudeCode(pythonDir);
anyRegistered = true; anyRegistered = true;
@ -653,13 +654,23 @@ namespace UnityMcpBridge.Editor.Windows
{ {
try try
{ {
string command = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "claude" : "/usr/local/bin/claude"; string claudePath = ExecPath.ResolveClaude();
var psi = new ProcessStartInfo { FileName = command, Arguments = "mcp list", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; if (string.IsNullOrEmpty(claudePath)) return false;
using var p = Process.Start(psi);
string output = p.StandardOutput.ReadToEnd(); // Only prepend PATH on Unix
p.WaitForExit(3000); string pathPrepend = null;
if (p.ExitCode != 0) return false; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return output.IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0; {
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; } catch { return false; }
} }
@ -987,65 +998,10 @@ namespace UnityMcpBridge.Editor.Windows
} }
} }
// Try to find the package using Package Manager API // Resolve via shared helper (handles local registry and older fallback)
UnityEditor.PackageManager.Requests.ListRequest request = if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
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) return embedded;
{
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;
}
} }
// If still not found, return the placeholder path // If still not found, return the placeholder path
@ -1358,218 +1314,82 @@ namespace UnityMcpBridge.Editor.Windows
private void RegisterWithClaudeCode(string pythonDir) private void RegisterWithClaudeCode(string pythonDir)
{ {
string command; // Resolve claude and uv; then run register command
string args; string claudePath = ExecPath.ResolveClaude();
if (string.IsNullOrEmpty(claudePath))
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
command = FindClaudeCommand(); 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";
if (string.IsNullOrEmpty(command)) // Prefer embedded/dev path when available
string srcDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory();
if (string.IsNullOrEmpty(srcDir)) srcDir = pythonDir;
string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{srcDir}\" server.py";
string projectDir = Path.GetDirectoryName(Application.dataPath);
// Ensure PATH includes common locations on Unix; on Windows leave PATH as-is
string pathPrepend = null;
if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.LinuxEditor)
{ {
UnityEngine.Debug.LogError("Claude CLI not found. Please ensure Claude Code is installed and accessible."); 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)
{
// 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("<b><color=#2EA3FF>UNITY-MCP</color></b>: UnityMCP already registered with Claude Code.");
}
else
{
UnityEngine.Debug.LogError($"UnityMCP: Failed to start Claude CLI.\n{stderr}\n{stdout}");
}
return; return;
} }
// Try to find uv.exe in common locations // Update status
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
{
// Use full path to claude command
command = "/usr/local/bin/claude";
args = $"mcp add UnityMCP -- uv --directory \"{pythonDir}\" run server.py";
}
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); var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
if (claudeClient != null) if (claudeClient != null) CheckClaudeCodeConfiguration(claudeClient);
{
CheckClaudeCodeConfiguration(claudeClient);
}
Repaint(); Repaint();
UnityEngine.Debug.Log("UnityMCP server successfully registered from Claude Code."); UnityEngine.Debug.Log("<b><color=#2EA3FF>UNITY-MCP</color></b>: Registered with 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}");
}
} }
private void UnregisterWithClaudeCode() private void UnregisterWithClaudeCode()
{ {
string command; string claudePath = ExecPath.ResolveClaude();
if (string.IsNullOrEmpty(claudePath))
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
command = FindClaudeCommand(); UnityEngine.Debug.LogError("UnityMCP: Claude CLI not found. Set a path in this window or install the CLI, then try again.");
if (string.IsNullOrEmpty(command))
{
UnityEngine.Debug.LogError("Claude CLI not found. Please ensure Claude Code is installed and accessible.");
return; return;
} }
}
else
{
// Use full path to claude command
command = "/usr/local/bin/claude";
}
try string projectDir = Path.GetDirectoryName(Application.dataPath);
{ string pathPrepend = Application.platform == RuntimePlatform.OSXEditor
// Get the Unity project directory (where the Assets folder is) ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
string unityProjectDir = Application.dataPath; : "/usr/local/bin:/usr/bin:/bin";
string projectDir = Path.GetDirectoryName(unityProjectDir);
var psi = new ProcessStartInfo(); if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
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); var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
if (claudeClient != null) if (claudeClient != null)
{ {
CheckClaudeCodeConfiguration(claudeClient); CheckClaudeCodeConfiguration(claudeClient);
} }
Repaint(); Repaint();
UnityEngine.Debug.Log("UnityMCP server successfully unregistered from Claude Code."); UnityEngine.Debug.Log("UnityMCP server successfully unregistered from Claude Code.");
} }
else if (!string.IsNullOrEmpty(errors)) else
{ {
UnityEngine.Debug.LogWarning($"Claude MCP removal errors: {errors}"); UnityEngine.Debug.LogWarning($"Claude MCP removal failed: {stderr}\n{stdout}");
}
}
catch (Exception e)
{
UnityEngine.Debug.LogError($"Claude CLI unregistration failed: {e.Message}");
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "com.coplaydev.unity-mcp", "name": "com.coplaydev.unity-mcp",
"version": "2.0.0", "version": "2.0.1",
"displayName": "Unity MCP Bridge", "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.", "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", "unity": "2020.3",