From ad5c3112ca63ba7e22cd6afcb633adfec5de55a8 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Sun, 24 Aug 2025 14:18:08 -0700 Subject: [PATCH] MCP: Fix macOS paths and VSCode manual setup Normalize macOS to Application Support; use AppSupport symlink for Cursor args Map XDG (~/.local/share, ~/.config) to Application Support VSCode manual: show mcp.json path; use top-level servers JSON VSCode macOS path: ~/Library/Application Support/Code/User/mcp.json --- UnityMcpBridge/Editor/Data/McpClients.cs | 27 +++-- .../Editor/Helpers/ConfigJsonBuilder.cs | 36 +++++- .../Editor/Helpers/ServerInstaller.cs | 107 ++++++++++-------- .../Editor/Windows/MCPForUnityEditorWindow.cs | 31 +++-- .../Editor/Windows/VSCodeManualSetupWindow.cs | 18 ++- 5 files changed, 142 insertions(+), 77 deletions(-) diff --git a/UnityMcpBridge/Editor/Data/McpClients.cs b/UnityMcpBridge/Editor/Data/McpClients.cs index f52fa4a..be21770 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 @@ -82,19 +83,31 @@ namespace MCPForUnity.Editor.Data 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 94ba5d9..deb2970 100644 --- a/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs +++ b/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs @@ -50,7 +50,41 @@ namespace MCPForUnity.Editor.Helpers 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/ServerInstaller.cs b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs index 9fc34d2..f6ddeaf 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs @@ -23,6 +23,7 @@ namespace MCPForUnity.Editor.Helpers try { string saveLocation = GetSaveLocation(); + TryCreateMacSymlinkForAppSupport(); string destRoot = Path.Combine(saveLocation, ServerFolder); string destSrc = Path.Combine(destRoot, "src"); @@ -117,57 +118,79 @@ namespace MCPForUnity.Editor.Helpers /// private static string GetSaveLocation() { - // Prefer Unity's platform first (more reliable under Mono/macOS), then fallback - try - { - if (Application.platform == RuntimePlatform.OSXEditor) - { - string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty; - string appSupport = Path.Combine(home, "Library", "Application Support"); - return Path.Combine(appSupport, RootFolder); - } - if (Application.platform == RuntimePlatform.WindowsEditor) - { - var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) - ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local"); - return Path.Combine(localAppData, RootFolder); - } - if (Application.platform == RuntimePlatform.LinuxEditor) - { - var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); - if (string.IsNullOrEmpty(xdg)) - { - xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, ".local", "share"); - } - return Path.Combine(xdg, RootFolder); - } - } - catch { } - - // Fallback to RuntimeInformation 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, RootFolder); } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); if (string.IsNullOrEmpty(xdg)) { - xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, ".local", "share"); + xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, + ".local", "share"); } return Path.Combine(xdg, RootFolder); } - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty; - return Path.Combine(home, "Library", "Application Support", 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 @@ -302,11 +325,10 @@ namespace MCPForUnity.Editor.Helpers if (string.IsNullOrEmpty(serverSrcPath)) return; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; - string safePath = EscapeForPgrep(serverSrcPath); var psi = new System.Diagnostics.ProcessStartInfo { FileName = "/usr/bin/pgrep", - Arguments = $"-f \"uv .*--directory {safePath}\"", + Arguments = $"-f \"uv .*--directory {serverSrcPath}\"", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -330,21 +352,6 @@ namespace MCPForUnity.Editor.Helpers catch { } } - // Escape regex metacharacters so the path is treated literally by pgrep -f - private static string EscapeForPgrep(string path) - { - if (string.IsNullOrEmpty(path)) return path; - string s = path.Replace("\\", "\\\\"); - char[] meta = new[] {'.','+','*','?','^','$','(',')','[',']','{','}','|'}; - var sb = new StringBuilder(s.Length * 2); - foreach (char c in s) - { - if (Array.IndexOf(meta, c) >= 0) sb.Append('\\'); - sb.Append(c); - } - return sb.ToString().Replace("\"", "\\\""); - } - private static string ReadVersionFile(string path) { try diff --git a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs index f17a14c..f29a192 100644 --- a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs @@ -961,18 +961,15 @@ namespace MCPForUnity.Editor.Windows 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" } } } }; @@ -1157,6 +1154,24 @@ namespace MCPForUnity.Editor.Windows } } + // 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) diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs index 4935798..e554451 100644 --- a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs +++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs @@ -90,25 +90,21 @@ namespace MCPForUnity.Editor.Windows 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 @@ namespace MCPForUnity.Editor.Windows System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), "Code", "User", - "settings.json" + "mcp.json" ); } else @@ -132,7 +128,7 @@ namespace MCPForUnity.Editor.Windows "Application Support", "Code", "User", - "settings.json" + "mcp.json" ); } @@ -205,7 +201,7 @@ namespace MCPForUnity.Editor.Windows EditorGUILayout.Space(10); EditorGUILayout.LabelField( - "4. Add this configuration to your settings.json:", + "4. Add this configuration to your mcp.json:", EditorStyles.boldLabel );