feat: local-only package resolution + Claude CLI resolver; quieter install logs; guarded auto-registration
parent
ae87e3f3b2
commit
8984ab95bc
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Reflection;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
|
|
@ -42,6 +43,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 +125,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)
|
||||||
|
|
@ -313,7 +227,7 @@ try
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FindUvPath()
|
internal static string FindUvPath()
|
||||||
{
|
{
|
||||||
// Allow user override via EditorPrefs
|
// Allow user override via EditorPrefs
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -987,65 +988,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 +1304,66 @@ 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;
|
||||||
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
|
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
|
UnityEngine.Debug.LogError($"UnityMCP: Failed to start Claude CLI.\n{stderr}\n{stdout}");
|
||||||
command = "/usr/local/bin/claude";
|
return;
|
||||||
args = $"mcp add UnityMCP -- uv --directory \"{pythonDir}\" run server.py";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
// Update status
|
||||||
{
|
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||||
// Get the Unity project directory (where the Assets folder is)
|
if (claudeClient != null) CheckClaudeCodeConfiguration(claudeClient);
|
||||||
string unityProjectDir = Application.dataPath;
|
Repaint();
|
||||||
string projectDir = Path.GetDirectoryName(unityProjectDir);
|
UnityEngine.Debug.Log("<b><color=#2EA3FF>UNITY-MCP</color></b>: Registered with Claude Code.");
|
||||||
|
|
||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.");
|
||||||
|
return;
|
||||||
if (string.IsNullOrEmpty(command))
|
}
|
||||||
|
|
||||||
|
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.");
|
CheckClaudeCodeConfiguration(claudeClient);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
Repaint();
|
||||||
|
UnityEngine.Debug.Log("UnityMCP server successfully unregistered from Claude Code.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Use full path to claude command
|
UnityEngine.Debug.LogWarning($"Claude MCP removal failed: {stderr}\n{stdout}");
|
||||||
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}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue