diff --git a/UnityMcpBridge/Editor/Data/McpClients.cs b/UnityMcpBridge/Editor/Data/McpClients.cs
index f52fa4ac..7b150b7a 100644
--- a/UnityMcpBridge/Editor/Data/McpClients.cs
+++ b/UnityMcpBridge/Editor/Data/McpClients.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Runtime.InteropServices;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Data
@@ -69,12 +70,22 @@ public class McpClients
"Claude",
"claude_desktop_config.json"
),
- linuxConfigPath = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
- ".config",
- "Claude",
- "claude_desktop_config.json"
- ),
+ // 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),
+ ".config",
+ "Claude",
+ "claude_desktop_config.json"
+ ),
mcpType = McpTypes.ClaudeDesktop,
configStatus = "Not Configured",
},
@@ -82,19 +93,31 @@ public class McpClients
new()
{
name = "VSCode GitHub Copilot",
+ // Windows path is canonical under %AppData%\Code\User
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Code",
"User",
"mcp.json"
),
- linuxConfigPath = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
- ".config",
- "Code",
- "User",
- "mcp.json"
- ),
+ // 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),
+ ".config",
+ "Code",
+ "User",
+ "mcp.json"
+ ),
mcpType = McpTypes.VSCode,
configStatus = "Not Configured",
},
diff --git a/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs b/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs
index 94ba5d97..deb29708 100644
--- a/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs
+++ b/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs
@@ -50,7 +50,41 @@ public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPa
private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode)
{
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)
{
diff --git a/UnityMcpBridge/Editor/Helpers/McpLog.cs b/UnityMcpBridge/Editor/Helpers/McpLog.cs
new file mode 100644
index 00000000..7e467187
--- /dev/null
+++ b/UnityMcpBridge/Editor/Helpers/McpLog.cs
@@ -0,0 +1,33 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace MCPForUnity.Editor.Helpers
+{
+ internal static class McpLog
+ {
+ private const string Prefix = "MCP-FOR-UNITY:";
+
+ 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($"{Prefix} {message}");
+ }
+
+ public static void Error(string message)
+ {
+ Debug.LogError($"{Prefix} {message}");
+ }
+ }
+}
+
+
diff --git a/UnityMcpBridge/Editor/Helpers/McpLog.cs.meta b/UnityMcpBridge/Editor/Helpers/McpLog.cs.meta
new file mode 100644
index 00000000..b9e0fc38
--- /dev/null
+++ b/UnityMcpBridge/Editor/Helpers/McpLog.cs.meta
@@ -0,0 +1,13 @@
+fileFormatVersion: 2
+guid: 9e2c3f8a4f4f48d8a4c1b7b8e3f5a1c2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+
+
diff --git a/UnityMcpBridge/Editor/Helpers/PackageDetector.cs b/UnityMcpBridge/Editor/Helpers/PackageDetector.cs
new file mode 100644
index 00000000..0a672003
--- /dev/null
+++ b/UnityMcpBridge/Editor/Helpers/PackageDetector.cs
@@ -0,0 +1,96 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace MCPForUnity.Editor.Helpers
+{
+ ///
+ /// Auto-runs legacy/older install detection on package load/update (log-only).
+ /// Runs once per embedded server version using an EditorPrefs version-scoped key.
+ ///
+ [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;
+ }
+ }
+}
+
+
diff --git a/UnityMcpBridge/Editor/Helpers/PackageDetector.cs.meta b/UnityMcpBridge/Editor/Helpers/PackageDetector.cs.meta
new file mode 100644
index 00000000..af305308
--- /dev/null
+++ b/UnityMcpBridge/Editor/Helpers/PackageDetector.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b82eaef548d164ca095f17db64d15af8
\ No newline at end of file
diff --git a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs
index e32be859..f6ddeaf0 100644
--- a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs
+++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs
@@ -2,6 +2,7 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
+using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
@@ -11,6 +12,7 @@ public static class ServerInstaller
{
private const string RootFolder = "UnityMCP";
private const string ServerFolder = "UnityMcpServer";
+ private const string VersionFileName = "server_version.txt";
///
/// Ensures the mcp-for-unity-server is installed locally by copying from the embedded package source.
@@ -21,25 +23,74 @@ public static void EnsureServerInstalled()
try
{
string saveLocation = GetSaveLocation();
+ TryCreateMacSymlinkForAppSupport();
string destRoot = Path.Combine(saveLocation, ServerFolder);
string destSrc = Path.Combine(destRoot, "src");
- if (File.Exists(Path.Combine(destSrc, "server.py")))
- {
- return; // Already installed
- }
+ // Detect legacy installs and version state (logs)
+ DetectAndLogLegacyInstallStates(destRoot);
+ // Resolve embedded source and versions
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
{
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
Directory.CreateDirectory(destRoot);
- // Copy the entire UnityMcpServer folder (parent of src)
- string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
- CopyDirectoryRecursive(embeddedRoot, destRoot);
+ if (needOverwrite)
+ {
+ // Copy the entire UnityMcpServer folder (parent of src)
+ string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
+ 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)
{
@@ -49,11 +100,11 @@ public static void EnsureServerInstalled()
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;
}
- Debug.LogError($"Failed to ensure server installation: {ex.Message}");
+ McpLog.Error($"Failed to ensure server installation: {ex.Message}");
}
}
@@ -69,9 +120,10 @@ private static string GetSaveLocation()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
+ // Use per-user LocalApplicationData for canonical install location
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
?? 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))
{
@@ -85,15 +137,60 @@ private static string GetSaveLocation()
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
- // Use Application Support for a stable, user-writable location
- return Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
- RootFolder
- );
+ // On macOS, use LocalApplicationData (~/Library/Application Support)
+ var localAppSupport = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ // Unity/Mono may map LocalApplicationData to ~/.local/share on macOS; normalize to Application Support
+ 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.");
}
+ ///
+ /// 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.
+ ///
+ 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)
{
try
@@ -117,6 +214,173 @@ private static bool IsServerInstalled(string location)
&& File.Exists(Path.Combine(location, ServerFolder, "src", "server.py"));
}
+ ///
+ /// Detects legacy installs or older versions and logs findings (no deletion yet).
+ ///
+ 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 GetLegacyRootsForDetection()
+ {
+ var roots = new System.Collections.Generic.List();
+ 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; }
+ }
+
///
/// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
/// or common development locations.
diff --git a/UnityMcpBridge/Editor/MCPForUnityBridge.cs b/UnityMcpBridge/Editor/MCPForUnityBridge.cs
index 82905cfe..7d75908b 100644
--- a/UnityMcpBridge/Editor/MCPForUnityBridge.cs
+++ b/UnityMcpBridge/Editor/MCPForUnityBridge.cs
@@ -310,7 +310,9 @@ public static void Start()
isRunning = true;
isAutoConnectMode = false;
- Debug.Log($"MCP-FOR-UNITY: MCPForUnityBridge started on port {currentUnityPort}.");
+ string platform = Application.platform.ToString();
+ string serverVer = ReadInstalledServerVersionSafe();
+ Debug.Log($"MCP-FOR-UNITY: MCPForUnityBridge started on port {currentUnityPort}. (OS={platform}, server={serverVer})");
Task.Run(ListenerLoop);
EditorApplication.update += ProcessCommands;
// Write initial heartbeat immediately
@@ -727,6 +729,22 @@ private static void WriteHeartbeat(bool reloading, string reason = null)
}
}
+ 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)
{
try
diff --git a/UnityMcpBridge/Editor/Models/McpClient.cs b/UnityMcpBridge/Editor/Models/McpClient.cs
index 895e2d61..bbc15da7 100644
--- a/UnityMcpBridge/Editor/Models/McpClient.cs
+++ b/UnityMcpBridge/Editor/Models/McpClient.cs
@@ -5,6 +5,7 @@ public class McpClient
public string name;
public string windowsConfigPath;
public string linuxConfigPath;
+ public string macConfigPath; // optional macOS-specific config path
public McpTypes mcpType;
public string configStatus;
public McpStatus status = McpStatus.NotConfigured;
diff --git a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs
index e1aa073c..96d5038c 100644
--- a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs
+++ b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs
@@ -59,6 +59,10 @@ private void OnEnable()
isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
autoRegisterEnabled = EditorPrefs.GetBool("MCPForUnity.AutoRegisterEnabled", true);
debugLogsEnabled = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
+ if (debugLogsEnabled)
+ {
+ LogDebugPrefsState();
+ }
foreach (McpClient mcpClient in mcpClients.clients)
{
CheckMcpConfiguration(mcpClient);
@@ -243,10 +247,79 @@ private void DrawHeader()
{
debugLogsEnabled = newDebug;
EditorPrefs.SetBool("MCPForUnity.DebugLogs", debugLogsEnabled);
+ if (debugLogsEnabled)
+ {
+ LogDebugPrefsState();
+ }
}
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()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
@@ -505,7 +578,7 @@ private void AutoFirstRunSetup()
}
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();
@@ -522,7 +595,7 @@ private void AutoFirstRunSetup()
}
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 @@ private void AutoFirstRunSetup()
}
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,18 +961,15 @@ private void DrawClientConfigurationCompact(McpClient mcpClient)
UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode.");
return;
}
-
+ // VSCode now reads from mcp.json with a top-level "servers" block
var vscodeConfig = new
{
- mcp = new
+ servers = new
{
- servers = new
+ unityMCP = new
{
- unityMCP = new
- {
- command = uvPath,
- args = new[] { "run", "--directory", pythonDir, "server.py" }
- }
+ command = uvPath,
+ args = new[] { "run", "--directory", pythonDir, "server.py" }
}
}
};
@@ -1001,7 +1071,7 @@ private static bool ArgsEqual(string[] a, string[] b)
return true;
}
- private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null)
+ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null)
{
// 0) Respect explicit lock (hidden pref or UI toggle)
try { if (UnityEditor.EditorPrefs.GetBool("MCPForUnity.LockCursorConfig", false)) return "Skipped (locked)"; } catch { }
@@ -1073,11 +1143,38 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC
&& System.IO.File.Exists(System.IO.Path.Combine(serverSrc, "server.py"));
if (!serverValid)
{
- serverSrc = ResolveServerSrc();
+ // 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();
+ }
+ }
+
+ // 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
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ && !string.IsNullOrEmpty(serverSrc)
&& serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
&& !UnityEditor.EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
{
@@ -1105,13 +1202,52 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC
existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient);
string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
+
+ // Use a more robust atomic write pattern
string tmp = configPath + ".tmp";
- // Write UTF-8 without BOM to avoid issues on Windows editors/tools
- System.IO.File.WriteAllText(tmp, mergedJson, new System.Text.UTF8Encoding(false));
- if (System.IO.File.Exists(configPath))
- System.IO.File.Replace(tmp, configPath, null);
- else
+ string backup = configPath + ".backup";
+
+ try
+ {
+ // Write to temp file first
+ 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))
+ {
+ 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);
+
+ // 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
{
if (IsValidUv(uvPath)) UnityEditor.EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
@@ -1277,7 +1413,14 @@ private string ConfigureMcpClient(McpClient mcpClient)
}
else if (
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;
@@ -1319,7 +1462,14 @@ private string ConfigureMcpClient(McpClient mcpClient)
}
else if (
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;
@@ -1431,7 +1581,14 @@ private void CheckMcpConfiguration(McpClient mcpClient)
}
else if (
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;
@@ -1490,7 +1647,8 @@ private void CheckMcpConfiguration(McpClient mcpClient)
// Common logic for checking configuration status
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)
{
mcpClient.SetStatus(McpStatus.Configured);
@@ -1673,31 +1831,7 @@ private void UnregisterWithClaudeCode()
}
}
- private bool ParseTextOutput(string claudePath, string projectDir, string pathPrepend)
- {
- 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;
- }
+ // Removed unused ParseTextOutput
private string FindUvPath()
{
@@ -1979,93 +2113,7 @@ private string FindWindowsUvPath()
return null; // Will fallback to using 'uv' from PATH
}
- private string 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";
- }
- }
+ // Removed unused FindClaudeCommand
private void CheckClaudeCodeConfiguration(McpClient mcpClient)
{
diff --git a/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs b/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs
index e7806a94..9fe776a9 100644
--- a/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs
+++ b/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs
@@ -116,10 +116,13 @@ protected virtual void OnGUI()
{
displayPath = mcpClient.windowsConfigPath;
}
- else if (
- RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
- || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
- )
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ displayPath = string.IsNullOrEmpty(mcpClient.macConfigPath)
+ ? mcpClient.linuxConfigPath
+ : mcpClient.macConfigPath;
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
displayPath = mcpClient.linuxConfigPath;
}
diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs
index 49357982..e5544510 100644
--- a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs
+++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs
@@ -90,25 +90,21 @@ protected override void OnGUI()
EditorStyles.boldLabel
);
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
);
EditorGUILayout.LabelField(
- "b) Click on the 'Open Settings (JSON)' button in the top right",
+ "b) Paste the JSON shown below into mcp.json",
instructionStyle
);
EditorGUILayout.LabelField(
- "c) Add the MCP configuration shown below to your settings.json file",
- instructionStyle
- );
- EditorGUILayout.LabelField(
- "d) Save the file and restart VSCode",
+ "c) Save the file and restart VSCode",
instructionStyle
);
EditorGUILayout.Space(5);
EditorGUILayout.LabelField(
- "3. VSCode settings.json location:",
+ "3. VSCode mcp.json location:",
EditorStyles.boldLabel
);
@@ -121,7 +117,7 @@ protected override void OnGUI()
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
"Code",
"User",
- "settings.json"
+ "mcp.json"
);
}
else
@@ -132,7 +128,7 @@ protected override void OnGUI()
"Application Support",
"Code",
"User",
- "settings.json"
+ "mcp.json"
);
}
@@ -205,7 +201,7 @@ protected override void OnGUI()
EditorGUILayout.Space(10);
EditorGUILayout.LabelField(
- "4. Add this configuration to your settings.json:",
+ "4. Add this configuration to your mcp.json:",
EditorStyles.boldLabel
);
diff --git a/UnityMcpBridge/UnityMcpServer~/src/server_version.txt b/UnityMcpBridge/UnityMcpServer~/src/server_version.txt
new file mode 100644
index 00000000..cb2b00e4
--- /dev/null
+++ b/UnityMcpBridge/UnityMcpServer~/src/server_version.txt
@@ -0,0 +1 @@
+3.0.1
diff --git a/UnityMcpBridge/package.json b/UnityMcpBridge/package.json
index a96a6f00..473ceda2 100644
--- a/UnityMcpBridge/package.json
+++ b/UnityMcpBridge/package.json
@@ -1,6 +1,6 @@
{
"name": "com.coplaydev.unity-mcp",
- "version": "3.0.0",
+ "version": "3.0.1",
"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",
"unity": "2021.3",