Skip to content

Commit cf8f5d4

Browse files
authored
Merge pull request CoplayDev#210 from dsarno/fix/config-stability
Config stability: preserve user configs, prefer installed server, safer uv resolution, XDG paths (Linux)
2 parents a7af0cd + 80d311e commit cf8f5d4

File tree

9 files changed

+414
-159
lines changed

9 files changed

+414
-159
lines changed

CursorHelp.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
### Cursor/VSCode/Windsurf: UV path issue on Windows (diagnosis and fix)
2+
3+
#### The issue
4+
- Some Windows machines have multiple `uv.exe` locations. Our auto-config sometimes picked a less stable path, causing the MCP client to fail to launch the Unity MCP Server or for the path to be auto-rewritten on repaint/restart.
5+
6+
#### Typical symptoms
7+
- Cursor shows the UnityMCP server but never connects or reports it “can’t start.”
8+
- Your `%USERPROFILE%\\.cursor\\mcp.json` flips back to a different `command` path when Unity or the Unity MCP window refreshes.
9+
10+
#### Real-world example
11+
- Wrong/fragile path (auto-picked):
12+
- `C:\Users\mrken.local\bin\uv.exe` (malformed, not standard)
13+
- `C:\Users\mrken\AppData\Local\Microsoft\WinGet\Packages\astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe\uv.exe`
14+
- Correct/stable path (works with Cursor):
15+
- `C:\Users\mrken\AppData\Local\Microsoft\WinGet\Links\uv.exe`
16+
17+
#### Quick fix (recommended)
18+
1) In Unity: `Window > Unity MCP` → select your MCP client (Cursor or Windsurf)
19+
2) If you see “uv Not Found,” click “Choose UV Install Location” and browse to:
20+
- `C:\Users\<YOU>\AppData\Local\Microsoft\WinGet\Links\uv.exe`
21+
3) If uv is already found but wrong, still click “Choose UV Install Location” and select the `Links\uv.exe` path above. This saves a persistent override.
22+
4) Click “Auto Configure” (or re-open the client) and restart Cursor.
23+
24+
This sets an override stored in the Editor (key: `UnityMCP.UvPath`) so UnityMCP won’t auto-rewrite the config back to a different `uv.exe` later.
25+
26+
#### Verify the fix
27+
- Confirm global Cursor config is at: `%USERPROFILE%\\.cursor\\mcp.json`
28+
- You should see something like:
29+
30+
```json
31+
{
32+
"mcpServers": {
33+
"unityMCP": {
34+
"command": "C:\\Users\\YOU\\AppData\\Local\\Microsoft\\WinGet\\Links\\uv.exe",
35+
"args": [
36+
"--directory",
37+
"C:\\Users\\YOU\\AppData\\Local\\Programs\\UnityMCP\\UnityMcpServer\\src",
38+
"run",
39+
"server.py"
40+
]
41+
}
42+
}
43+
}
44+
```
45+
46+
- Manually run the same command in PowerShell to confirm it launches:
47+
48+
```powershell
49+
"C:\Users\YOU\AppData\Local\Microsoft\WinGet\Links\uv.exe" --directory "C:\Users\YOU\AppData\Local\Programs\UnityMCP\UnityMcpServer\src" run server.py
50+
```
51+
52+
If that runs without error, restart Cursor and it should connect.
53+
54+
#### Why this happens
55+
- On Windows, multiple `uv.exe` can exist (WinGet Packages path, a WinGet Links shim, Python Scripts, etc.). The Links shim is the most stable target for GUI apps to launch.
56+
- Prior versions of the auto-config could pick the first found path and re-write config on refresh. Choosing a path via the MCP window pins a known‑good absolute path and prevents auto-rewrites.
57+
58+
#### Extra notes
59+
- Restart Cursor after changing `mcp.json`; it doesn’t always hot-reload that file.
60+
- If you also have a project-scoped `.cursor\\mcp.json` in your Unity project folder, that file overrides the global one.
61+
62+
63+
### Why pin the WinGet Links shim (and not the Packages path)
64+
65+
- Windows often has multiple `uv.exe` installs and GUI clients (Cursor/Windsurf/VSCode) may launch with a reduced `PATH`. Using an absolute path is safer than `"command": "uv"`.
66+
- WinGet publishes stable launch shims in these locations:
67+
- User scope: `%LOCALAPPDATA%\Microsoft\WinGet\Links\uv.exe`
68+
- Machine scope: `C:\Program Files\WinGet\Links\uv.exe`
69+
These shims survive upgrades and are intended as the portable entrypoints. See the WinGet notes: [discussion](https://github.com/microsoft/winget-pkgs/discussions/184459)[how to find installs](https://superuser.com/questions/1739292/how-to-know-where-winget-installed-a-program)
70+
- The `Packages` root is where payloads live and can change across updates, so avoid pointing your config at it.
71+
72+
Recommended practice
73+
74+
- Prefer the WinGet Links shim paths above. If present, select one via “Choose UV Install Location”.
75+
- If the unity window keeps rewriting to a different `uv.exe`, pick the Links shim again; Unity MCP saves a pinned override and will stop auto-rewrites.
76+
- If neither Links path exists, a reasonable fallback is `~/.local/bin/uv.exe` (uv tools bin) or a Scoop shim, but Links is preferred for stability.
77+
78+
References
79+
80+
- WinGet portable Links: [GitHub discussion](https://github.com/microsoft/winget-pkgs/discussions/184459)
81+
- WinGet install locations: [Super User](https://superuser.com/questions/1739292/how-to-know-where-winget-installed-a-program)
82+
- GUI client PATH caveats (Cursor): [Cursor community thread](https://forum.cursor.com/t/mcp-feature-client-closed-fix/54651?page=4)
83+
- uv tools install location (`~/.local/bin`): [Astral docs](https://docs.astral.sh/uv/concepts/tools/)
84+
85+

UnityMcpBridge/Editor/Data/McpClients.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@ public class McpClients
7171
),
7272
linuxConfigPath = Path.Combine(
7373
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
74-
"Library",
75-
"Application Support",
74+
".config",
7675
"Claude",
7776
"claude_desktop_config.json"
7877
),
@@ -91,8 +90,7 @@ public class McpClients
9190
),
9291
linuxConfigPath = Path.Combine(
9392
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
94-
"Library",
95-
"Application Support",
93+
".config",
9694
"Code",
9795
"User",
9896
"mcp.json"

UnityMcpBridge/Editor/Helpers/ServerInstaller.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ private static string GetSaveLocation()
8888
// Use Application Support for a stable, user-writable location
8989
return Path.Combine(
9090
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
91-
"UnityMCP"
91+
RootFolder
9292
);
9393
}
9494
throw new Exception("Unsupported operating system.");
@@ -126,6 +126,7 @@ private static bool TryGetEmbeddedServerSource(out string srcPath)
126126
return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath);
127127
}
128128

129+
private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" };
129130
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
130131
{
131132
Directory.CreateDirectory(destinationDir);
@@ -140,8 +141,15 @@ private static void CopyDirectoryRecursive(string sourceDir, string destinationD
140141
foreach (string dirPath in Directory.GetDirectories(sourceDir))
141142
{
142143
string dirName = Path.GetFileName(dirPath);
144+
foreach (var skip in _skipDirs)
145+
{
146+
if (dirName.Equals(skip, StringComparison.OrdinalIgnoreCase))
147+
goto NextDir;
148+
}
149+
try { if ((File.GetAttributes(dirPath) & FileAttributes.ReparsePoint) != 0) continue; } catch { }
143150
string destSubDir = Path.Combine(destinationDir, dirName);
144151
CopyDirectoryRecursive(dirPath, destSubDir);
152+
NextDir: ;
145153
}
146154
}
147155

@@ -270,7 +278,6 @@ internal static string FindUvPath()
270278
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
271279
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty;
272280
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
273-
string programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) ?? string.Empty; // optional fallback
274281

275282
// Fast path: resolve from PATH first
276283
try
@@ -301,10 +308,9 @@ internal static string FindUvPath()
301308
candidates = new[]
302309
{
303310
// Preferred: WinGet Links shims (stable entrypoints)
311+
// Per-user shim (LOCALAPPDATA) → machine-wide shim (Program Files\WinGet\Links)
304312
Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"),
305313
Path.Combine(programFiles, "WinGet", "Links", "uv.exe"),
306-
// Optional low-priority fallback for atypical images
307-
Path.Combine(programData, "Microsoft", "WinGet", "Links", "uv.exe"),
308314

309315
// Common per-user installs
310316
Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"),

0 commit comments

Comments
 (0)