Skip to content

Commit ad5c311

Browse files
committed
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
1 parent 01ea2f4 commit ad5c311

File tree

5 files changed

+142
-77
lines changed

5 files changed

+142
-77
lines changed

UnityMcpBridge/Editor/Data/McpClients.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Runtime.InteropServices;
45
using MCPForUnity.Editor.Models;
56

67
namespace MCPForUnity.Editor.Data
@@ -82,19 +83,31 @@ public class McpClients
8283
new()
8384
{
8485
name = "VSCode GitHub Copilot",
86+
// Windows path is canonical under %AppData%\Code\User
8587
windowsConfigPath = Path.Combine(
8688
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
8789
"Code",
8890
"User",
8991
"mcp.json"
9092
),
91-
linuxConfigPath = Path.Combine(
92-
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
93-
".config",
94-
"Code",
95-
"User",
96-
"mcp.json"
97-
),
93+
// For macOS, VSCode stores user config under ~/Library/Application Support/Code/User
94+
// For Linux, it remains under ~/.config/Code/User
95+
linuxConfigPath = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
96+
? Path.Combine(
97+
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
98+
"Library",
99+
"Application Support",
100+
"Code",
101+
"User",
102+
"mcp.json"
103+
)
104+
: Path.Combine(
105+
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
106+
".config",
107+
"Code",
108+
"User",
109+
"mcp.json"
110+
),
98111
mcpType = McpTypes.VSCode,
99112
configStatus = "Not Configured",
100113
},

UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,41 @@ public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPa
5050
private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode)
5151
{
5252
unity["command"] = uvPath;
53-
unity["args"] = JArray.FromObject(new[] { "run", "--directory", directory, "server.py" });
53+
54+
// For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners
55+
string effectiveDir = directory;
56+
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
57+
bool isCursor = !isVSCode && (client == null || client.mcpType != Models.McpTypes.VSCode);
58+
if (isCursor && !string.IsNullOrEmpty(directory))
59+
{
60+
// Replace canonical path segment with the symlink path if present
61+
const string canonical = "/Library/Application Support/";
62+
const string symlinkSeg = "/Library/AppSupport/";
63+
try
64+
{
65+
// Normalize to full path style
66+
if (directory.Contains(canonical))
67+
{
68+
effectiveDir = directory.Replace(canonical, symlinkSeg);
69+
}
70+
else
71+
{
72+
// If installer returned XDG-style on macOS, map to canonical symlink
73+
string norm = directory.Replace('\\', '/');
74+
int idx = norm.IndexOf("/.local/share/UnityMCP/", System.StringComparison.Ordinal);
75+
if (idx >= 0)
76+
{
77+
string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) ?? string.Empty;
78+
string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
79+
effectiveDir = System.IO.Path.Combine(home, "Library", "AppSupport", suffix).Replace('\\', '/');
80+
}
81+
}
82+
}
83+
catch { /* fallback to original directory on any error */ }
84+
}
85+
#endif
86+
87+
unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" });
5488

5589
if (isVSCode)
5690
{

UnityMcpBridge/Editor/Helpers/ServerInstaller.cs

Lines changed: 57 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public static void EnsureServerInstalled()
2323
try
2424
{
2525
string saveLocation = GetSaveLocation();
26+
TryCreateMacSymlinkForAppSupport();
2627
string destRoot = Path.Combine(saveLocation, ServerFolder);
2728
string destSrc = Path.Combine(destRoot, "src");
2829

@@ -117,57 +118,79 @@ public static string GetServerPath()
117118
/// </summary>
118119
private static string GetSaveLocation()
119120
{
120-
// Prefer Unity's platform first (more reliable under Mono/macOS), then fallback
121-
try
122-
{
123-
if (Application.platform == RuntimePlatform.OSXEditor)
124-
{
125-
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
126-
string appSupport = Path.Combine(home, "Library", "Application Support");
127-
return Path.Combine(appSupport, RootFolder);
128-
}
129-
if (Application.platform == RuntimePlatform.WindowsEditor)
130-
{
131-
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
132-
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local");
133-
return Path.Combine(localAppData, RootFolder);
134-
}
135-
if (Application.platform == RuntimePlatform.LinuxEditor)
136-
{
137-
var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
138-
if (string.IsNullOrEmpty(xdg))
139-
{
140-
xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, ".local", "share");
141-
}
142-
return Path.Combine(xdg, RootFolder);
143-
}
144-
}
145-
catch { }
146-
147-
// Fallback to RuntimeInformation
148121
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
149122
{
123+
// Use per-user LocalApplicationData for canonical install location
150124
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
151125
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local");
152126
return Path.Combine(localAppData, RootFolder);
153127
}
154-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
128+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
155129
{
156130
var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
157131
if (string.IsNullOrEmpty(xdg))
158132
{
159-
xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, ".local", "share");
133+
xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty,
134+
".local", "share");
160135
}
161136
return Path.Combine(xdg, RootFolder);
162137
}
163-
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
138+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
164139
{
165-
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
166-
return Path.Combine(home, "Library", "Application Support", RootFolder);
140+
// On macOS, use LocalApplicationData (~/Library/Application Support)
141+
var localAppSupport = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
142+
// Unity/Mono may map LocalApplicationData to ~/.local/share on macOS; normalize to Application Support
143+
bool looksLikeXdg = !string.IsNullOrEmpty(localAppSupport) && localAppSupport.Replace('\\', '/').Contains("/.local/share");
144+
if (string.IsNullOrEmpty(localAppSupport) || looksLikeXdg)
145+
{
146+
// Fallback: construct from $HOME
147+
var home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
148+
localAppSupport = Path.Combine(home, "Library", "Application Support");
149+
}
150+
TryCreateMacSymlinkForAppSupport();
151+
return Path.Combine(localAppSupport, RootFolder);
167152
}
168153
throw new Exception("Unsupported operating system.");
169154
}
170155

156+
/// <summary>
157+
/// On macOS, create a no-spaces symlink ~/Library/AppSupport -> ~/Library/Application Support
158+
/// to mitigate arg parsing and quoting issues in some MCP clients.
159+
/// Safe to call repeatedly.
160+
/// </summary>
161+
private static void TryCreateMacSymlinkForAppSupport()
162+
{
163+
try
164+
{
165+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return;
166+
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
167+
if (string.IsNullOrEmpty(home)) return;
168+
169+
string canonical = Path.Combine(home, "Library", "Application Support");
170+
string symlink = Path.Combine(home, "Library", "AppSupport");
171+
172+
// If symlink exists already, nothing to do
173+
if (Directory.Exists(symlink) || File.Exists(symlink)) return;
174+
175+
// Create symlink only if canonical exists
176+
if (!Directory.Exists(canonical)) return;
177+
178+
// Use 'ln -s' to create a directory symlink (macOS)
179+
var psi = new System.Diagnostics.ProcessStartInfo
180+
{
181+
FileName = "/bin/ln",
182+
Arguments = $"-s \"{canonical}\" \"{symlink}\"",
183+
UseShellExecute = false,
184+
RedirectStandardOutput = true,
185+
RedirectStandardError = true,
186+
CreateNoWindow = true
187+
};
188+
using var p = System.Diagnostics.Process.Start(psi);
189+
p?.WaitForExit(2000);
190+
}
191+
catch { /* best-effort */ }
192+
}
193+
171194
private static bool IsDirectoryWritable(string path)
172195
{
173196
try
@@ -302,11 +325,10 @@ private static void TryKillUvForPath(string serverSrcPath)
302325
if (string.IsNullOrEmpty(serverSrcPath)) return;
303326
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
304327

305-
string safePath = EscapeForPgrep(serverSrcPath);
306328
var psi = new System.Diagnostics.ProcessStartInfo
307329
{
308330
FileName = "/usr/bin/pgrep",
309-
Arguments = $"-f \"uv .*--directory {safePath}\"",
331+
Arguments = $"-f \"uv .*--directory {serverSrcPath}\"",
310332
UseShellExecute = false,
311333
RedirectStandardOutput = true,
312334
RedirectStandardError = true,
@@ -330,21 +352,6 @@ private static void TryKillUvForPath(string serverSrcPath)
330352
catch { }
331353
}
332354

333-
// Escape regex metacharacters so the path is treated literally by pgrep -f
334-
private static string EscapeForPgrep(string path)
335-
{
336-
if (string.IsNullOrEmpty(path)) return path;
337-
string s = path.Replace("\\", "\\\\");
338-
char[] meta = new[] {'.','+','*','?','^','$','(',')','[',']','{','}','|'};
339-
var sb = new StringBuilder(s.Length * 2);
340-
foreach (char c in s)
341-
{
342-
if (Array.IndexOf(meta, c) >= 0) sb.Append('\\');
343-
sb.Append(c);
344-
}
345-
return sb.ToString().Replace("\"", "\\\"");
346-
}
347-
348355
private static string ReadVersionFile(string path)
349356
{
350357
try

UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -961,18 +961,15 @@ private void DrawClientConfigurationCompact(McpClient mcpClient)
961961
UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode.");
962962
return;
963963
}
964-
964+
// VSCode now reads from mcp.json with a top-level "servers" block
965965
var vscodeConfig = new
966966
{
967-
mcp = new
967+
servers = new
968968
{
969-
servers = new
969+
unityMCP = new
970970
{
971-
unityMCP = new
972-
{
973-
command = uvPath,
974-
args = new[] { "run", "--directory", pythonDir, "server.py" }
975-
}
971+
command = uvPath,
972+
args = new[] { "run", "--directory", pythonDir, "server.py" }
976973
}
977974
}
978975
};
@@ -1157,6 +1154,24 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC
11571154
}
11581155
}
11591156

1157+
// macOS normalization: map XDG-style ~/.local/share to canonical Application Support
1158+
try
1159+
{
1160+
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX)
1161+
&& !string.IsNullOrEmpty(serverSrc))
1162+
{
1163+
string norm = serverSrc.Replace('\\', '/');
1164+
int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal);
1165+
if (idx >= 0)
1166+
{
1167+
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
1168+
string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
1169+
serverSrc = System.IO.Path.Combine(home, "Library", "Application Support", suffix);
1170+
}
1171+
}
1172+
}
1173+
catch { }
1174+
11601175
// Hard-block PackageCache on Windows unless dev override is set
11611176
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
11621177
&& !string.IsNullOrEmpty(serverSrc)

UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,25 +90,21 @@ protected override void OnGUI()
9090
EditorStyles.boldLabel
9191
);
9292
EditorGUILayout.LabelField(
93-
"a) Open VSCode Settings (File > Preferences > Settings)",
93+
"a) Open or create your VSCode MCP config file (mcp.json) at the path below",
9494
instructionStyle
9595
);
9696
EditorGUILayout.LabelField(
97-
"b) Click on the 'Open Settings (JSON)' button in the top right",
97+
"b) Paste the JSON shown below into mcp.json",
9898
instructionStyle
9999
);
100100
EditorGUILayout.LabelField(
101-
"c) Add the MCP configuration shown below to your settings.json file",
102-
instructionStyle
103-
);
104-
EditorGUILayout.LabelField(
105-
"d) Save the file and restart VSCode",
101+
"c) Save the file and restart VSCode",
106102
instructionStyle
107103
);
108104
EditorGUILayout.Space(5);
109105

110106
EditorGUILayout.LabelField(
111-
"3. VSCode settings.json location:",
107+
"3. VSCode mcp.json location:",
112108
EditorStyles.boldLabel
113109
);
114110

@@ -121,7 +117,7 @@ protected override void OnGUI()
121117
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
122118
"Code",
123119
"User",
124-
"settings.json"
120+
"mcp.json"
125121
);
126122
}
127123
else
@@ -132,7 +128,7 @@ protected override void OnGUI()
132128
"Application Support",
133129
"Code",
134130
"User",
135-
"settings.json"
131+
"mcp.json"
136132
);
137133
}
138134

@@ -205,7 +201,7 @@ protected override void OnGUI()
205201
EditorGUILayout.Space(10);
206202

207203
EditorGUILayout.LabelField(
208-
"4. Add this configuration to your settings.json:",
204+
"4. Add this configuration to your mcp.json:",
209205
EditorStyles.boldLabel
210206
);
211207

0 commit comments

Comments
 (0)