Merge pull request #239 from dsarno/fix/installer-cleanup-v2
feat: installer cleanup, auto-migration of legacy server location to canonical, server logging normalizationmain
commit
c7d421842c
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using MCPForUnity.Editor.Models;
|
using MCPForUnity.Editor.Models;
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Data
|
namespace MCPForUnity.Editor.Data
|
||||||
|
|
@ -69,7 +70,17 @@ namespace MCPForUnity.Editor.Data
|
||||||
"Claude",
|
"Claude",
|
||||||
"claude_desktop_config.json"
|
"claude_desktop_config.json"
|
||||||
),
|
),
|
||||||
linuxConfigPath = Path.Combine(
|
// For macOS, Claude Desktop stores config under ~/Library/Application Support/Claude
|
||||||
|
// For Linux, it remains under ~/.config/Claude
|
||||||
|
linuxConfigPath = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||||
|
? Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"Claude",
|
||||||
|
"claude_desktop_config.json"
|
||||||
|
)
|
||||||
|
: Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
".config",
|
".config",
|
||||||
"Claude",
|
"Claude",
|
||||||
|
|
@ -82,13 +93,25 @@ namespace MCPForUnity.Editor.Data
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
name = "VSCode GitHub Copilot",
|
name = "VSCode GitHub Copilot",
|
||||||
|
// Windows path is canonical under %AppData%\Code\User
|
||||||
windowsConfigPath = Path.Combine(
|
windowsConfigPath = Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
"Code",
|
"Code",
|
||||||
"User",
|
"User",
|
||||||
"mcp.json"
|
"mcp.json"
|
||||||
),
|
),
|
||||||
linuxConfigPath = Path.Combine(
|
// For macOS, VSCode stores user config under ~/Library/Application Support/Code/User
|
||||||
|
// For Linux, it remains under ~/.config/Code/User
|
||||||
|
linuxConfigPath = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||||
|
? Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"Code",
|
||||||
|
"User",
|
||||||
|
"mcp.json"
|
||||||
|
)
|
||||||
|
: Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
".config",
|
".config",
|
||||||
"Code",
|
"Code",
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,41 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode)
|
private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode)
|
||||||
{
|
{
|
||||||
unity["command"] = uvPath;
|
unity["command"] = uvPath;
|
||||||
unity["args"] = JArray.FromObject(new[] { "run", "--directory", directory, "server.py" });
|
|
||||||
|
// For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners
|
||||||
|
string effectiveDir = directory;
|
||||||
|
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
|
||||||
|
bool isCursor = !isVSCode && (client == null || client.mcpType != Models.McpTypes.VSCode);
|
||||||
|
if (isCursor && !string.IsNullOrEmpty(directory))
|
||||||
|
{
|
||||||
|
// Replace canonical path segment with the symlink path if present
|
||||||
|
const string canonical = "/Library/Application Support/";
|
||||||
|
const string symlinkSeg = "/Library/AppSupport/";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Normalize to full path style
|
||||||
|
if (directory.Contains(canonical))
|
||||||
|
{
|
||||||
|
effectiveDir = directory.Replace(canonical, symlinkSeg);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If installer returned XDG-style on macOS, map to canonical symlink
|
||||||
|
string norm = directory.Replace('\\', '/');
|
||||||
|
int idx = norm.IndexOf("/.local/share/UnityMCP/", System.StringComparison.Ordinal);
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) ?? string.Empty;
|
||||||
|
string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
|
||||||
|
effectiveDir = System.IO.Path.Combine(home, "Library", "AppSupport", suffix).Replace('\\', '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* fallback to original directory on any error */ }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" });
|
||||||
|
|
||||||
if (isVSCode)
|
if (isVSCode)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MCPForUnity.Editor.Helpers
|
||||||
|
{
|
||||||
|
internal static class McpLog
|
||||||
|
{
|
||||||
|
private const string Prefix = "<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>:";
|
||||||
|
|
||||||
|
private static bool IsDebugEnabled()
|
||||||
|
{
|
||||||
|
try { return EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); } catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Info(string message, bool always = true)
|
||||||
|
{
|
||||||
|
if (!always && !IsDebugEnabled()) return;
|
||||||
|
Debug.Log($"{Prefix} {message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Warn(string message)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"<color=#cc7a00>{Prefix} {message}</color>");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Error(string message)
|
||||||
|
{
|
||||||
|
Debug.LogError($"<color=#cc3333>{Prefix} {message}</color>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9e2c3f8a4f4f48d8a4c1b7b8e3f5a1c2
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MCPForUnity.Editor.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Auto-runs legacy/older install detection on package load/update (log-only).
|
||||||
|
/// Runs once per embedded server version using an EditorPrefs version-scoped key.
|
||||||
|
/// </summary>
|
||||||
|
[InitializeOnLoad]
|
||||||
|
public static class PackageDetector
|
||||||
|
{
|
||||||
|
private const string DetectOnceFlagKeyPrefix = "MCPForUnity.LegacyDetectLogged:";
|
||||||
|
|
||||||
|
static PackageDetector()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string pkgVer = ReadPackageVersionOrFallback();
|
||||||
|
string key = DetectOnceFlagKeyPrefix + pkgVer;
|
||||||
|
|
||||||
|
// Always force-run if legacy roots exist or canonical install is missing
|
||||||
|
bool legacyPresent = LegacyRootsExist();
|
||||||
|
bool canonicalMissing = !System.IO.File.Exists(System.IO.Path.Combine(ServerInstaller.GetServerPath(), "server.py"));
|
||||||
|
|
||||||
|
if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing)
|
||||||
|
{
|
||||||
|
EditorApplication.delayCall += () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ServerInstaller.EnsureServerInstalled();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("MCP for Unity: Auto-detect on load failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EditorPrefs.SetBool(key, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadEmbeddedVersionOrFallback()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
|
||||||
|
{
|
||||||
|
var p = System.IO.Path.Combine(embeddedSrc, "server_version.txt");
|
||||||
|
if (System.IO.File.Exists(p))
|
||||||
|
return (System.IO.File.ReadAllText(p)?.Trim() ?? "unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadPackageVersionOrFallback()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(PackageDetector).Assembly);
|
||||||
|
if (info != null && !string.IsNullOrEmpty(info.version)) return info.version;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
// Fallback to embedded server version if package info unavailable
|
||||||
|
return ReadEmbeddedVersionOrFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LegacyRootsExist()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||||
|
string[] roots =
|
||||||
|
{
|
||||||
|
System.IO.Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
|
||||||
|
System.IO.Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
|
||||||
|
};
|
||||||
|
foreach (var r in roots)
|
||||||
|
{
|
||||||
|
try { if (System.IO.File.Exists(System.IO.Path.Combine(r, "server.py"))) return true; } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b82eaef548d164ca095f17db64d15af8
|
||||||
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Collections.Generic;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
|
|
@ -11,6 +12,7 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
{
|
{
|
||||||
private const string RootFolder = "UnityMCP";
|
private const string RootFolder = "UnityMCP";
|
||||||
private const string ServerFolder = "UnityMcpServer";
|
private const string ServerFolder = "UnityMcpServer";
|
||||||
|
private const string VersionFileName = "server_version.txt";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ensures the mcp-for-unity-server is installed locally by copying from the embedded package source.
|
/// Ensures the mcp-for-unity-server is installed locally by copying from the embedded package source.
|
||||||
|
|
@ -21,25 +23,74 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string saveLocation = GetSaveLocation();
|
string saveLocation = GetSaveLocation();
|
||||||
|
TryCreateMacSymlinkForAppSupport();
|
||||||
string destRoot = Path.Combine(saveLocation, ServerFolder);
|
string destRoot = Path.Combine(saveLocation, ServerFolder);
|
||||||
string destSrc = Path.Combine(destRoot, "src");
|
string destSrc = Path.Combine(destRoot, "src");
|
||||||
|
|
||||||
if (File.Exists(Path.Combine(destSrc, "server.py")))
|
// Detect legacy installs and version state (logs)
|
||||||
{
|
DetectAndLogLegacyInstallStates(destRoot);
|
||||||
return; // Already installed
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Resolve embedded source and versions
|
||||||
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
|
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
|
||||||
{
|
{
|
||||||
throw new Exception("Could not find embedded UnityMcpServer/src in the package.");
|
throw new Exception("Could not find embedded UnityMcpServer/src in the package.");
|
||||||
}
|
}
|
||||||
|
string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
|
||||||
|
string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName));
|
||||||
|
|
||||||
|
bool destHasServer = File.Exists(Path.Combine(destSrc, "server.py"));
|
||||||
|
bool needOverwrite = !destHasServer
|
||||||
|
|| string.IsNullOrEmpty(installedVer)
|
||||||
|
|| (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0);
|
||||||
|
|
||||||
// Ensure destination exists
|
// Ensure destination exists
|
||||||
Directory.CreateDirectory(destRoot);
|
Directory.CreateDirectory(destRoot);
|
||||||
|
|
||||||
|
if (needOverwrite)
|
||||||
|
{
|
||||||
// Copy the entire UnityMcpServer folder (parent of src)
|
// Copy the entire UnityMcpServer folder (parent of src)
|
||||||
string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
|
string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
|
||||||
CopyDirectoryRecursive(embeddedRoot, destRoot);
|
CopyDirectoryRecursive(embeddedRoot, destRoot);
|
||||||
|
// Write/refresh version file
|
||||||
|
try { File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer ?? "unknown"); } catch { }
|
||||||
|
McpLog.Info($"Installed/updated server to {destRoot} (version {embeddedVer}).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup legacy installs that are missing version or older than embedded
|
||||||
|
foreach (var legacyRoot in GetLegacyRootsForDetection())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string legacySrc = Path.Combine(legacyRoot, "src");
|
||||||
|
if (!File.Exists(Path.Combine(legacySrc, "server.py"))) continue;
|
||||||
|
string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName));
|
||||||
|
bool legacyOlder = string.IsNullOrEmpty(legacyVer)
|
||||||
|
|| (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0);
|
||||||
|
if (legacyOlder)
|
||||||
|
{
|
||||||
|
TryKillUvForPath(legacySrc);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(legacyRoot, recursive: true);
|
||||||
|
McpLog.Info($"Removed legacy server at '{legacyRoot}'.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear overrides that might point at legacy locations
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EditorPrefs.DeleteKey("MCPForUnity.ServerSrc");
|
||||||
|
EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride");
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -49,11 +100,11 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
|
|
||||||
if (hasInstalled || TryGetEmbeddedServerSource(out _))
|
if (hasInstalled || TryGetEmbeddedServerSource(out _))
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"MCP for Unity: Using existing server; skipped install. Details: {ex.Message}");
|
McpLog.Warn($"Using existing server; skipped install. Details: {ex.Message}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.LogError($"Failed to ensure server installation: {ex.Message}");
|
McpLog.Error($"Failed to ensure server installation: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,9 +120,10 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
{
|
{
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
|
// Use per-user LocalApplicationData for canonical install location
|
||||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
|
||||||
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local");
|
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local");
|
||||||
return Path.Combine(localAppData, "Programs", RootFolder);
|
return Path.Combine(localAppData, RootFolder);
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
{
|
{
|
||||||
|
|
@ -85,15 +137,60 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
{
|
{
|
||||||
// Use Application Support for a stable, user-writable location
|
// On macOS, use LocalApplicationData (~/Library/Application Support)
|
||||||
return Path.Combine(
|
var localAppSupport = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
// Unity/Mono may map LocalApplicationData to ~/.local/share on macOS; normalize to Application Support
|
||||||
RootFolder
|
bool looksLikeXdg = !string.IsNullOrEmpty(localAppSupport) && localAppSupport.Replace('\\', '/').Contains("/.local/share");
|
||||||
);
|
if (string.IsNullOrEmpty(localAppSupport) || looksLikeXdg)
|
||||||
|
{
|
||||||
|
// Fallback: construct from $HOME
|
||||||
|
var home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
|
||||||
|
localAppSupport = Path.Combine(home, "Library", "Application Support");
|
||||||
|
}
|
||||||
|
TryCreateMacSymlinkForAppSupport();
|
||||||
|
return Path.Combine(localAppSupport, RootFolder);
|
||||||
}
|
}
|
||||||
throw new Exception("Unsupported operating system.");
|
throw new Exception("Unsupported operating system.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On macOS, create a no-spaces symlink ~/Library/AppSupport -> ~/Library/Application Support
|
||||||
|
/// to mitigate arg parsing and quoting issues in some MCP clients.
|
||||||
|
/// Safe to call repeatedly.
|
||||||
|
/// </summary>
|
||||||
|
private static void TryCreateMacSymlinkForAppSupport()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return;
|
||||||
|
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
|
||||||
|
if (string.IsNullOrEmpty(home)) return;
|
||||||
|
|
||||||
|
string canonical = Path.Combine(home, "Library", "Application Support");
|
||||||
|
string symlink = Path.Combine(home, "Library", "AppSupport");
|
||||||
|
|
||||||
|
// If symlink exists already, nothing to do
|
||||||
|
if (Directory.Exists(symlink) || File.Exists(symlink)) return;
|
||||||
|
|
||||||
|
// Create symlink only if canonical exists
|
||||||
|
if (!Directory.Exists(canonical)) return;
|
||||||
|
|
||||||
|
// Use 'ln -s' to create a directory symlink (macOS)
|
||||||
|
var psi = new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "/bin/ln",
|
||||||
|
Arguments = $"-s \"{canonical}\" \"{symlink}\"",
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
using var p = System.Diagnostics.Process.Start(psi);
|
||||||
|
p?.WaitForExit(2000);
|
||||||
|
}
|
||||||
|
catch { /* best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsDirectoryWritable(string path)
|
private static bool IsDirectoryWritable(string path)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -117,6 +214,173 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
&& File.Exists(Path.Combine(location, ServerFolder, "src", "server.py"));
|
&& File.Exists(Path.Combine(location, ServerFolder, "src", "server.py"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detects legacy installs or older versions and logs findings (no deletion yet).
|
||||||
|
/// </summary>
|
||||||
|
private static void DetectAndLogLegacyInstallStates(string canonicalRoot)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string canonicalSrc = Path.Combine(canonicalRoot, "src");
|
||||||
|
// Normalize canonical root for comparisons
|
||||||
|
string normCanonicalRoot = NormalizePathSafe(canonicalRoot);
|
||||||
|
string embeddedSrc = null;
|
||||||
|
TryGetEmbeddedServerSource(out embeddedSrc);
|
||||||
|
|
||||||
|
string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc ?? string.Empty, VersionFileName));
|
||||||
|
string installedVer = ReadVersionFile(Path.Combine(canonicalSrc, VersionFileName));
|
||||||
|
|
||||||
|
// Legacy paths (macOS/Linux .config; Windows roaming as example)
|
||||||
|
foreach (var legacyRoot in GetLegacyRootsForDetection())
|
||||||
|
{
|
||||||
|
// Skip logging for the canonical root itself
|
||||||
|
if (PathsEqualSafe(legacyRoot, normCanonicalRoot))
|
||||||
|
continue;
|
||||||
|
string legacySrc = Path.Combine(legacyRoot, "src");
|
||||||
|
bool hasServer = File.Exists(Path.Combine(legacySrc, "server.py"));
|
||||||
|
string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName));
|
||||||
|
|
||||||
|
if (hasServer)
|
||||||
|
{
|
||||||
|
// Case 1: No version file
|
||||||
|
if (string.IsNullOrEmpty(legacyVer))
|
||||||
|
{
|
||||||
|
McpLog.Info("Detected legacy install without version file at: " + legacyRoot, always: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Lives in legacy path
|
||||||
|
McpLog.Info("Detected legacy install path: " + legacyRoot, always: false);
|
||||||
|
|
||||||
|
// Case 3: Has version but appears older than embedded
|
||||||
|
if (!string.IsNullOrEmpty(embeddedVer) && !string.IsNullOrEmpty(legacyVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0)
|
||||||
|
{
|
||||||
|
McpLog.Info($"Legacy install version {legacyVer} is older than embedded {embeddedVer}", always: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also log if canonical is missing version (treated as older)
|
||||||
|
if (Directory.Exists(canonicalRoot))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(installedVer))
|
||||||
|
{
|
||||||
|
McpLog.Info("Canonical install missing version file (treat as older). Path: " + canonicalRoot, always: false);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0)
|
||||||
|
{
|
||||||
|
McpLog.Info($"Canonical install version {installedVer} is older than embedded {embeddedVer}", always: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
McpLog.Warn("Detect legacy/version state failed: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizePathSafe(string path)
|
||||||
|
{
|
||||||
|
try { return string.IsNullOrEmpty(path) ? path : Path.GetFullPath(path.Trim()); }
|
||||||
|
catch { return path; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool PathsEqualSafe(string a, string b)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
|
||||||
|
string na = NormalizePathSafe(a);
|
||||||
|
string nb = NormalizePathSafe(b);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
return string.Equals(na, nb, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> GetLegacyRootsForDetection()
|
||||||
|
{
|
||||||
|
var roots = new System.Collections.Generic.List<string>();
|
||||||
|
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||||
|
// macOS/Linux legacy
|
||||||
|
roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer"));
|
||||||
|
roots.Add(Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer"));
|
||||||
|
// Windows roaming example
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string roaming = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
|
||||||
|
if (!string.IsNullOrEmpty(roaming))
|
||||||
|
roots.Add(Path.Combine(roaming, "UnityMCP", "UnityMcpServer"));
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryKillUvForPath(string serverSrcPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(serverSrcPath)) return;
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
|
||||||
|
|
||||||
|
var psi = new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "/usr/bin/pgrep",
|
||||||
|
Arguments = $"-f \"uv .*--directory {serverSrcPath}\"",
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
using var p = System.Diagnostics.Process.Start(psi);
|
||||||
|
if (p == null) return;
|
||||||
|
string outp = p.StandardOutput.ReadToEnd();
|
||||||
|
p.WaitForExit(1500);
|
||||||
|
if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
|
||||||
|
{
|
||||||
|
foreach (var line in outp.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
if (int.TryParse(line.Trim(), out int pid))
|
||||||
|
{
|
||||||
|
try { System.Diagnostics.Process.GetProcessById(pid).Kill(); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadVersionFile(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null;
|
||||||
|
string v = File.ReadAllText(path).Trim();
|
||||||
|
return string.IsNullOrEmpty(v) ? null : v;
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CompareSemverSafe(string a, string b)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return 0;
|
||||||
|
var ap = a.Split('.');
|
||||||
|
var bp = b.Split('.');
|
||||||
|
for (int i = 0; i < Math.Max(ap.Length, bp.Length); i++)
|
||||||
|
{
|
||||||
|
int ai = (i < ap.Length && int.TryParse(ap[i], out var t1)) ? t1 : 0;
|
||||||
|
int bi = (i < bp.Length && int.TryParse(bp[i], out var t2)) ? t2 : 0;
|
||||||
|
if (ai != bi) return ai.CompareTo(bi);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch { return 0; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
|
/// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
|
||||||
/// or common development locations.
|
/// or common development locations.
|
||||||
|
|
|
||||||
|
|
@ -310,7 +310,9 @@ namespace MCPForUnity.Editor
|
||||||
|
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
isAutoConnectMode = false;
|
isAutoConnectMode = false;
|
||||||
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: MCPForUnityBridge started on port {currentUnityPort}.");
|
string platform = Application.platform.ToString();
|
||||||
|
string serverVer = ReadInstalledServerVersionSafe();
|
||||||
|
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: MCPForUnityBridge started on port {currentUnityPort}. (OS={platform}, server={serverVer})");
|
||||||
Task.Run(ListenerLoop);
|
Task.Run(ListenerLoop);
|
||||||
EditorApplication.update += ProcessCommands;
|
EditorApplication.update += ProcessCommands;
|
||||||
// Write initial heartbeat immediately
|
// Write initial heartbeat immediately
|
||||||
|
|
@ -727,6 +729,22 @@ namespace MCPForUnity.Editor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ReadInstalledServerVersionSafe()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string serverSrc = ServerInstaller.GetServerPath();
|
||||||
|
string verFile = Path.Combine(serverSrc, "server_version.txt");
|
||||||
|
if (File.Exists(verFile))
|
||||||
|
{
|
||||||
|
string v = File.ReadAllText(verFile)?.Trim();
|
||||||
|
if (!string.IsNullOrEmpty(v)) return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
private static string ComputeProjectHash(string input)
|
private static string ComputeProjectHash(string input)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace MCPForUnity.Editor.Models
|
||||||
public string name;
|
public string name;
|
||||||
public string windowsConfigPath;
|
public string windowsConfigPath;
|
||||||
public string linuxConfigPath;
|
public string linuxConfigPath;
|
||||||
|
public string macConfigPath; // optional macOS-specific config path
|
||||||
public McpTypes mcpType;
|
public McpTypes mcpType;
|
||||||
public string configStatus;
|
public string configStatus;
|
||||||
public McpStatus status = McpStatus.NotConfigured;
|
public McpStatus status = McpStatus.NotConfigured;
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,10 @@ namespace MCPForUnity.Editor.Windows
|
||||||
isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
|
isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
|
||||||
autoRegisterEnabled = EditorPrefs.GetBool("MCPForUnity.AutoRegisterEnabled", true);
|
autoRegisterEnabled = EditorPrefs.GetBool("MCPForUnity.AutoRegisterEnabled", true);
|
||||||
debugLogsEnabled = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
|
debugLogsEnabled = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
|
||||||
|
if (debugLogsEnabled)
|
||||||
|
{
|
||||||
|
LogDebugPrefsState();
|
||||||
|
}
|
||||||
foreach (McpClient mcpClient in mcpClients.clients)
|
foreach (McpClient mcpClient in mcpClients.clients)
|
||||||
{
|
{
|
||||||
CheckMcpConfiguration(mcpClient);
|
CheckMcpConfiguration(mcpClient);
|
||||||
|
|
@ -243,10 +247,79 @@ namespace MCPForUnity.Editor.Windows
|
||||||
{
|
{
|
||||||
debugLogsEnabled = newDebug;
|
debugLogsEnabled = newDebug;
|
||||||
EditorPrefs.SetBool("MCPForUnity.DebugLogs", debugLogsEnabled);
|
EditorPrefs.SetBool("MCPForUnity.DebugLogs", debugLogsEnabled);
|
||||||
|
if (debugLogsEnabled)
|
||||||
|
{
|
||||||
|
LogDebugPrefsState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
EditorGUILayout.Space(15);
|
EditorGUILayout.Space(15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LogDebugPrefsState()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string pythonDirOverridePref = SafeGetPrefString("MCPForUnity.PythonDirOverride");
|
||||||
|
string uvPathPref = SafeGetPrefString("MCPForUnity.UvPath");
|
||||||
|
string serverSrcPref = SafeGetPrefString("MCPForUnity.ServerSrc");
|
||||||
|
bool useEmbedded = SafeGetPrefBool("MCPForUnity.UseEmbeddedServer");
|
||||||
|
|
||||||
|
// Version-scoped detection key
|
||||||
|
string embeddedVer = ReadEmbeddedVersionOrFallback();
|
||||||
|
string detectKey = $"MCPForUnity.LegacyDetectLogged:{embeddedVer}";
|
||||||
|
bool detectLogged = SafeGetPrefBool(detectKey);
|
||||||
|
|
||||||
|
// Project-scoped auto-register key
|
||||||
|
string projectPath = Application.dataPath ?? string.Empty;
|
||||||
|
string autoKey = $"MCPForUnity.AutoRegistered.{ComputeSha1(projectPath)}";
|
||||||
|
bool autoRegistered = SafeGetPrefBool(autoKey);
|
||||||
|
|
||||||
|
MCPForUnity.Editor.Helpers.McpLog.Info(
|
||||||
|
"MCP Debug Prefs:\n" +
|
||||||
|
$" DebugLogs: {debugLogsEnabled}\n" +
|
||||||
|
$" PythonDirOverride: '{pythonDirOverridePref}'\n" +
|
||||||
|
$" UvPath: '{uvPathPref}'\n" +
|
||||||
|
$" ServerSrc: '{serverSrcPref}'\n" +
|
||||||
|
$" UseEmbeddedServer: {useEmbedded}\n" +
|
||||||
|
$" DetectOnceKey: '{detectKey}' => {detectLogged}\n" +
|
||||||
|
$" AutoRegisteredKey: '{autoKey}' => {autoRegistered}",
|
||||||
|
always: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogWarning($"MCP Debug Prefs logging failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SafeGetPrefString(string key)
|
||||||
|
{
|
||||||
|
try { return EditorPrefs.GetString(key, string.Empty) ?? string.Empty; } catch { return string.Empty; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SafeGetPrefBool(string key)
|
||||||
|
{
|
||||||
|
try { return EditorPrefs.GetBool(key, false); } catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadEmbeddedVersionOrFallback()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
|
||||||
|
{
|
||||||
|
var p = Path.Combine(embeddedSrc, "server_version.txt");
|
||||||
|
if (File.Exists(p))
|
||||||
|
{
|
||||||
|
var s = File.ReadAllText(p)?.Trim();
|
||||||
|
if (!string.IsNullOrEmpty(s)) return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawServerStatusSection()
|
private void DrawServerStatusSection()
|
||||||
{
|
{
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
@ -505,7 +578,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
UnityEngine.Debug.LogWarning($"Auto-setup client '{client.name}' failed: {ex.Message}");
|
MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup client '{client.name}' failed: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastClientRegisteredOk = anyRegistered || IsCursorConfigured(pythonDir) || IsClaudeConfigured();
|
lastClientRegisteredOk = anyRegistered || IsCursorConfigured(pythonDir) || IsClaudeConfigured();
|
||||||
|
|
@ -522,7 +595,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
UnityEngine.Debug.LogWarning($"Auto-setup StartAutoConnect failed: {ex.Message}");
|
MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup StartAutoConnect failed: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -533,7 +606,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
UnityEngine.Debug.LogWarning($"MCP for Unity auto-setup skipped: {e.Message}");
|
MCPForUnity.Editor.Helpers.McpLog.Warn($"MCP for Unity auto-setup skipped: {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -888,10 +961,8 @@ namespace MCPForUnity.Editor.Windows
|
||||||
UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode.");
|
UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// VSCode now reads from mcp.json with a top-level "servers" block
|
||||||
var vscodeConfig = new
|
var vscodeConfig = new
|
||||||
{
|
|
||||||
mcp = new
|
|
||||||
{
|
{
|
||||||
servers = new
|
servers = new
|
||||||
{
|
{
|
||||||
|
|
@ -901,7 +972,6 @@ namespace MCPForUnity.Editor.Windows
|
||||||
args = new[] { "run", "--directory", pythonDir, "server.py" }
|
args = new[] { "run", "--directory", pythonDir, "server.py" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
|
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
|
||||||
string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings);
|
string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings);
|
||||||
|
|
@ -1072,12 +1142,39 @@ namespace MCPForUnity.Editor.Windows
|
||||||
bool serverValid = !string.IsNullOrEmpty(serverSrc)
|
bool serverValid = !string.IsNullOrEmpty(serverSrc)
|
||||||
&& System.IO.File.Exists(System.IO.Path.Combine(serverSrc, "server.py"));
|
&& System.IO.File.Exists(System.IO.Path.Combine(serverSrc, "server.py"));
|
||||||
if (!serverValid)
|
if (!serverValid)
|
||||||
|
{
|
||||||
|
// Prefer the provided pythonDir if valid; fall back to resolver
|
||||||
|
if (!string.IsNullOrEmpty(pythonDir) && System.IO.File.Exists(System.IO.Path.Combine(pythonDir, "server.py")))
|
||||||
|
{
|
||||||
|
serverSrc = pythonDir;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
serverSrc = ResolveServerSrc();
|
serverSrc = ResolveServerSrc();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS normalization: map XDG-style ~/.local/share to canonical Application Support
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX)
|
||||||
|
&& !string.IsNullOrEmpty(serverSrc))
|
||||||
|
{
|
||||||
|
string norm = serverSrc.Replace('\\', '/');
|
||||||
|
int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal);
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
|
||||||
|
string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
|
||||||
|
serverSrc = System.IO.Path.Combine(home, "Library", "Application Support", suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
// Hard-block PackageCache on Windows unless dev override is set
|
// Hard-block PackageCache on Windows unless dev override is set
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||||
|
&& !string.IsNullOrEmpty(serverSrc)
|
||||||
&& serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
|
&& serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
&& !UnityEditor.EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
|
&& !UnityEditor.EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
|
||||||
{
|
{
|
||||||
|
|
@ -1105,13 +1202,52 @@ namespace MCPForUnity.Editor.Windows
|
||||||
existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient);
|
existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient);
|
||||||
|
|
||||||
string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
|
string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
|
||||||
|
|
||||||
|
// Use a more robust atomic write pattern
|
||||||
string tmp = configPath + ".tmp";
|
string tmp = configPath + ".tmp";
|
||||||
// Write UTF-8 without BOM to avoid issues on Windows editors/tools
|
string backup = configPath + ".backup";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Write to temp file first
|
||||||
System.IO.File.WriteAllText(tmp, mergedJson, new System.Text.UTF8Encoding(false));
|
System.IO.File.WriteAllText(tmp, mergedJson, new System.Text.UTF8Encoding(false));
|
||||||
|
|
||||||
|
// Create backup of existing file if it exists
|
||||||
if (System.IO.File.Exists(configPath))
|
if (System.IO.File.Exists(configPath))
|
||||||
System.IO.File.Replace(tmp, configPath, null);
|
{
|
||||||
else
|
System.IO.File.Copy(configPath, backup, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic move operation (more reliable than Replace on macOS)
|
||||||
|
if (System.IO.File.Exists(configPath))
|
||||||
|
{
|
||||||
|
System.IO.File.Delete(configPath);
|
||||||
|
}
|
||||||
System.IO.File.Move(tmp, configPath);
|
System.IO.File.Move(tmp, configPath);
|
||||||
|
|
||||||
|
// Clean up backup
|
||||||
|
if (System.IO.File.Exists(backup))
|
||||||
|
{
|
||||||
|
System.IO.File.Delete(backup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Clean up temp file
|
||||||
|
try { if (System.IO.File.Exists(tmp)) System.IO.File.Delete(tmp); } catch { }
|
||||||
|
// Restore backup if it exists
|
||||||
|
try {
|
||||||
|
if (System.IO.File.Exists(backup))
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(configPath))
|
||||||
|
{
|
||||||
|
System.IO.File.Delete(configPath);
|
||||||
|
}
|
||||||
|
System.IO.File.Move(backup, configPath);
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
throw new Exception($"Failed to write config file '{configPath}': {ex.Message}", ex);
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (IsValidUv(uvPath)) UnityEditor.EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
|
if (IsValidUv(uvPath)) UnityEditor.EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
|
||||||
|
|
@ -1277,7 +1413,14 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
else if (
|
else if (
|
||||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||||
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
)
|
||||||
|
{
|
||||||
|
configPath = string.IsNullOrEmpty(mcpClient.macConfigPath)
|
||||||
|
? mcpClient.linuxConfigPath
|
||||||
|
: mcpClient.macConfigPath;
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
configPath = mcpClient.linuxConfigPath;
|
configPath = mcpClient.linuxConfigPath;
|
||||||
|
|
@ -1319,7 +1462,14 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
else if (
|
else if (
|
||||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||||
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
)
|
||||||
|
{
|
||||||
|
configPath = string.IsNullOrEmpty(mcpClient.macConfigPath)
|
||||||
|
? mcpClient.linuxConfigPath
|
||||||
|
: mcpClient.macConfigPath;
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
configPath = mcpClient.linuxConfigPath;
|
configPath = mcpClient.linuxConfigPath;
|
||||||
|
|
@ -1431,7 +1581,14 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
else if (
|
else if (
|
||||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||||
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
)
|
||||||
|
{
|
||||||
|
configPath = string.IsNullOrEmpty(mcpClient.macConfigPath)
|
||||||
|
? mcpClient.linuxConfigPath
|
||||||
|
: mcpClient.macConfigPath;
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
configPath = mcpClient.linuxConfigPath;
|
configPath = mcpClient.linuxConfigPath;
|
||||||
|
|
@ -1490,7 +1647,8 @@ namespace MCPForUnity.Editor.Windows
|
||||||
// Common logic for checking configuration status
|
// Common logic for checking configuration status
|
||||||
if (configExists)
|
if (configExists)
|
||||||
{
|
{
|
||||||
bool matches = pythonDir != null && Array.Exists(args, arg => arg.Contains(pythonDir, StringComparison.Ordinal));
|
string configuredDir = ExtractDirectoryArg(args);
|
||||||
|
bool matches = !string.IsNullOrEmpty(configuredDir) && PathsEqual(configuredDir, pythonDir);
|
||||||
if (matches)
|
if (matches)
|
||||||
{
|
{
|
||||||
mcpClient.SetStatus(McpStatus.Configured);
|
mcpClient.SetStatus(McpStatus.Configured);
|
||||||
|
|
@ -1673,31 +1831,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ParseTextOutput(string claudePath, string projectDir, string pathPrepend)
|
// Removed unused ParseTextOutput
|
||||||
{
|
|
||||||
if (ExecPath.TryRun(claudePath, "mcp list", projectDir, out var listStdout, out var listStderr, 10000, pathPrepend))
|
|
||||||
{
|
|
||||||
UnityEngine.Debug.Log($"Claude MCP servers (text): {listStdout}");
|
|
||||||
|
|
||||||
// Check if output indicates no servers or contains "UnityMCP" variants
|
|
||||||
if (listStdout.Contains("No MCP servers configured") ||
|
|
||||||
listStdout.Contains("no servers") ||
|
|
||||||
listStdout.Contains("No servers") ||
|
|
||||||
string.IsNullOrWhiteSpace(listStdout) ||
|
|
||||||
listStdout.Trim().Length == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for "UnityMCP" variants in the output
|
|
||||||
return listStdout.Contains("UnityMCP") ||
|
|
||||||
listStdout.Contains("unityMCP") ||
|
|
||||||
listStdout.Contains("unity-mcp");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If command failed, assume no servers
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string FindUvPath()
|
private string FindUvPath()
|
||||||
{
|
{
|
||||||
|
|
@ -1979,93 +2113,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
return null; // Will fallback to using 'uv' from PATH
|
return null; // Will fallback to using 'uv' from PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FindClaudeCommand()
|
// Removed unused FindClaudeCommand
|
||||||
{
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
{
|
|
||||||
// Common locations for Claude CLI on Windows
|
|
||||||
string[] possiblePaths = {
|
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "npm", "claude.cmd"),
|
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "npm", "claude.cmd"),
|
|
||||||
"claude.cmd", // Fallback to PATH
|
|
||||||
"claude" // Final fallback
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (string path in possiblePaths)
|
|
||||||
{
|
|
||||||
if (path.Contains("\\") && File.Exists(path))
|
|
||||||
{
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find via where command (PowerShell compatible)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var psi = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "where.exe",
|
|
||||||
Arguments = "claude",
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
using var process = Process.Start(psi);
|
|
||||||
string output = process.StandardOutput.ReadToEnd().Trim();
|
|
||||||
process.WaitForExit();
|
|
||||||
|
|
||||||
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output))
|
|
||||||
{
|
|
||||||
string[] lines = output.Split('\n');
|
|
||||||
foreach (string line in lines)
|
|
||||||
{
|
|
||||||
string cleanPath = line.Trim();
|
|
||||||
if (File.Exists(cleanPath))
|
|
||||||
{
|
|
||||||
return cleanPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// If where.exe fails, try PowerShell's Get-Command as fallback
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var psi = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "powershell.exe",
|
|
||||||
Arguments = "-Command \"(Get-Command claude -ErrorAction SilentlyContinue).Source\"",
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
using var process = Process.Start(psi);
|
|
||||||
string output = process.StandardOutput.ReadToEnd().Trim();
|
|
||||||
process.WaitForExit();
|
|
||||||
|
|
||||||
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
|
|
||||||
{
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Ignore PowerShell errors too
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "claude"; // Final fallback to PATH
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return "/usr/local/bin/claude";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CheckClaudeCodeConfiguration(McpClient mcpClient)
|
private void CheckClaudeCodeConfiguration(McpClient mcpClient)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -116,10 +116,13 @@ namespace MCPForUnity.Editor.Windows
|
||||||
{
|
{
|
||||||
displayPath = mcpClient.windowsConfigPath;
|
displayPath = mcpClient.windowsConfigPath;
|
||||||
}
|
}
|
||||||
else if (
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
{
|
||||||
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
displayPath = string.IsNullOrEmpty(mcpClient.macConfigPath)
|
||||||
)
|
? mcpClient.linuxConfigPath
|
||||||
|
: mcpClient.macConfigPath;
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
{
|
{
|
||||||
displayPath = mcpClient.linuxConfigPath;
|
displayPath = mcpClient.linuxConfigPath;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,25 +90,21 @@ namespace MCPForUnity.Editor.Windows
|
||||||
EditorStyles.boldLabel
|
EditorStyles.boldLabel
|
||||||
);
|
);
|
||||||
EditorGUILayout.LabelField(
|
EditorGUILayout.LabelField(
|
||||||
"a) Open VSCode Settings (File > Preferences > Settings)",
|
"a) Open or create your VSCode MCP config file (mcp.json) at the path below",
|
||||||
instructionStyle
|
instructionStyle
|
||||||
);
|
);
|
||||||
EditorGUILayout.LabelField(
|
EditorGUILayout.LabelField(
|
||||||
"b) Click on the 'Open Settings (JSON)' button in the top right",
|
"b) Paste the JSON shown below into mcp.json",
|
||||||
instructionStyle
|
instructionStyle
|
||||||
);
|
);
|
||||||
EditorGUILayout.LabelField(
|
EditorGUILayout.LabelField(
|
||||||
"c) Add the MCP configuration shown below to your settings.json file",
|
"c) Save the file and restart VSCode",
|
||||||
instructionStyle
|
|
||||||
);
|
|
||||||
EditorGUILayout.LabelField(
|
|
||||||
"d) Save the file and restart VSCode",
|
|
||||||
instructionStyle
|
instructionStyle
|
||||||
);
|
);
|
||||||
EditorGUILayout.Space(5);
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
EditorGUILayout.LabelField(
|
EditorGUILayout.LabelField(
|
||||||
"3. VSCode settings.json location:",
|
"3. VSCode mcp.json location:",
|
||||||
EditorStyles.boldLabel
|
EditorStyles.boldLabel
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -121,7 +117,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
|
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
|
||||||
"Code",
|
"Code",
|
||||||
"User",
|
"User",
|
||||||
"settings.json"
|
"mcp.json"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -132,7 +128,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
"Application Support",
|
"Application Support",
|
||||||
"Code",
|
"Code",
|
||||||
"User",
|
"User",
|
||||||
"settings.json"
|
"mcp.json"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,7 +201,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
EditorGUILayout.LabelField(
|
EditorGUILayout.LabelField(
|
||||||
"4. Add this configuration to your settings.json:",
|
"4. Add this configuration to your mcp.json:",
|
||||||
EditorStyles.boldLabel
|
EditorStyles.boldLabel
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
3.0.1
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "com.coplaydev.unity-mcp",
|
"name": "com.coplaydev.unity-mcp",
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"displayName": "MCP for Unity",
|
"displayName": "MCP for Unity",
|
||||||
"description": "A bridge that connects an LLM to Unity via the MCP (Model Context Protocol). This allows MCP Clients like Claude Desktop or Cursor to directly control your Unity Editor.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4",
|
"description": "A bridge that connects an LLM to Unity via the MCP (Model Context Protocol). This allows MCP Clients like Claude Desktop or Cursor to directly control your Unity Editor.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4",
|
||||||
"unity": "2021.3",
|
"unity": "2021.3",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue