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
main
David Sarno 2025-08-24 14:18:08 -07:00
parent 01ea2f46ac
commit ad5c3112ca
5 changed files with 142 additions and 77 deletions

View File

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

View File

@ -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)
{

View File

@ -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
/// </summary>
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.");
}
/// <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)
{
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

View File

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

View File

@ -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
);