Skip to content

Commit a7af0cd

Browse files
authored
Merge pull request #209 from dsarno/patch/uv-links-prefer
Windows UV stability: prefer WinGet Links, preserve pinned uv path; Claude Code unregister UI; add generic mcp_source.py
2 parents ce8ab83 + f8c76db commit a7af0cd

File tree

7 files changed

+453
-57
lines changed

7 files changed

+453
-57
lines changed

README-DEV.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Deploys your development code to the actual installation locations for testing.
3636
3. Enter server path (or use default: `%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src`)
3737
4. Enter backup location (or use default: `%USERPROFILE%\Desktop\unity-mcp-backup`)
3838

39+
**Note:** Dev deploy skips `.venv`, `__pycache__`, `.pytest_cache`, `.mypy_cache`, `.git`; reduces churn and avoids copying virtualenvs.
40+
3941
### `restore-dev.bat`
4042
Restores original files from backup.
4143

@@ -73,6 +75,23 @@ Note: In recent builds, the Python server sources are also bundled inside the pa
7375
5. **Restore** original files when done using `restore-dev.bat`
7476

7577

78+
## Switching MCP package sources quickly
79+
80+
Use `mcp_source.py` to quickly switch between different Unity MCP package sources:
81+
82+
**Usage:**
83+
```bash
84+
python mcp_source.py [--manifest /path/to/manifest.json] [--repo /path/to/unity-mcp] [--choice 1|2|3]
85+
```
86+
87+
**Options:**
88+
- **1** Upstream main (CoplayDev/unity-mcp)
89+
- **2** Remote current branch (origin + branch)
90+
- **3** Local workspace (file: UnityMcpBridge)
91+
92+
After switching, open Package Manager and Refresh to re-resolve packages.
93+
94+
7695
## Troubleshooting
7796

7897
### "Path not found" errors running the .bat file
@@ -88,4 +107,7 @@ Note: In recent builds, the Python server sources are also bundled inside the pa
88107
### "Backup not found" errors
89108
- Run `deploy-dev.bat` first to create initial backup
90109
- Check backup directory permissions
91-
- Verify backup directory path is correct
110+
- Verify backup directory path is correct
111+
112+
### Windows uv path issues
113+
- On Windows, when testing GUI clients, prefer the WinGet Links `uv.exe`; if multiple `uv.exe` exist, use "Choose UV Install Location" to pin the Links shim.

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,13 @@ Connect your MCP Client (Claude, Cursor, etc.) to the Python server you installe
124124
125125
1. In Unity, go to `Window > Unity MCP`.
126126
2. Click `Auto-Setup`.
127-
3. Look for a green status indicator 🟢 and "Connected ✓". *(This attempts to modify the MCP Client\'s config file automatically)*.
127+
3. Look for a green status indicator 🟢 and "Connected ✓". *(This attempts to modify the MCP Client\'s config file automatically).*
128+
129+
Client-specific notes
130+
131+
- **VSCode**: uses `Code/User/mcp.json` with top-level `servers.unityMCP` and `"type": "stdio"`. On Windows, Unity MCP writes an absolute `uv.exe` (prefers WinGet Links shim) to avoid PATH issues.
132+
- **Cursor / Windsurf**: if `uv` is missing, the Unity MCP window shows "uv Not Found" with a quick [HELP] link and a "Choose UV Install Location" button.
133+
- **Claude Code**: if `claude` isn't found, the window shows "Claude Not Found" with [HELP] and a "Choose Claude Location" button. Unregister now updates the UI immediately.
128134
129135
130136
**Option B: Manual Configuration**
@@ -137,7 +143,23 @@ If Auto-Setup fails or you use a different client:
137143
2. **Edit the file** to add/update the `mcpServers` section, using the *exact* paths from Step 1.
138144

139145
<details>
140-
<summary><strong>Click for OS-Specific JSON Configuration Snippets...</strong></summary>
146+
<summary><strong>Click for Client-Specific JSON Configuration Snippets...</strong></summary>
147+
148+
**VSCode (all OS)**
149+
150+
```json
151+
{
152+
"servers": {
153+
"unityMCP": {
154+
"command": "uv",
155+
"args": ["--directory","<ABSOLUTE_PATH_TO>/UnityMcpServer/src","run","server.py"],
156+
"type": "stdio"
157+
}
158+
}
159+
}
160+
```
161+
162+
On Windows, set `command` to the absolute shim, e.g. `C:\\Users\\YOU\\AppData\\Local\\Microsoft\\WinGet\\Links\\uv.exe`.
141163

142164
**Windows:**
143165

UnityMcpBridge/Editor/Helpers/ExecPath.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,15 @@ internal static string ResolveClaude()
5353
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
5454
string[] candidates =
5555
{
56+
// Prefer .cmd (most reliable from non-interactive processes)
5657
Path.Combine(appData, "npm", "claude.cmd"),
5758
Path.Combine(localAppData, "npm", "claude.cmd"),
59+
// Fall back to PowerShell shim if only .ps1 is present
60+
Path.Combine(appData, "npm", "claude.ps1"),
61+
Path.Combine(localAppData, "npm", "claude.ps1"),
5862
};
5963
foreach (string c in candidates) { if (File.Exists(c)) return c; }
60-
string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude");
64+
string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude");
6165
if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
6266
#endif
6367
return null;
@@ -172,10 +176,16 @@ internal static bool TryRun(
172176
stderr = string.Empty;
173177
try
174178
{
179+
// Handle PowerShell scripts on Windows by invoking through powershell.exe
180+
bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
181+
file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase);
182+
175183
var psi = new ProcessStartInfo
176184
{
177-
FileName = file,
178-
Arguments = args,
185+
FileName = isPs1 ? "powershell.exe" : file,
186+
Arguments = isPs1
187+
? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim()
188+
: args,
179189
WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
180190
UseShellExecute = false,
181191
RedirectStandardOutput = true,

UnityMcpBridge/Editor/Helpers/ServerInstaller.cs

Lines changed: 60 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.IO;
33
using System.Runtime.InteropServices;
44
using System.Text;
5-
using System.Reflection;
65
using UnityEditor;
76
using UnityEngine;
87

@@ -70,21 +69,19 @@ private static string GetSaveLocation()
7069
{
7170
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
7271
{
73-
return Path.Combine(
74-
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
75-
"AppData",
76-
"Local",
77-
"Programs",
78-
RootFolder
79-
);
72+
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
73+
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local");
74+
return Path.Combine(localAppData, "Programs", RootFolder);
8075
}
8176
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
8277
{
83-
return Path.Combine(
84-
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
85-
"bin",
86-
RootFolder
87-
);
78+
var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
79+
if (string.IsNullOrEmpty(xdg))
80+
{
81+
xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty,
82+
".local", "share");
83+
}
84+
return Path.Combine(xdg, RootFolder);
8885
}
8986
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
9087
{
@@ -270,19 +267,58 @@ internal static string FindUvPath()
270267
string[] candidates;
271268
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
272269
{
270+
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
271+
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty;
272+
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
273+
string programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) ?? string.Empty; // optional fallback
274+
275+
// Fast path: resolve from PATH first
276+
try
277+
{
278+
var wherePsi = new System.Diagnostics.ProcessStartInfo
279+
{
280+
FileName = "where",
281+
Arguments = "uv.exe",
282+
UseShellExecute = false,
283+
RedirectStandardOutput = true,
284+
RedirectStandardError = true,
285+
CreateNoWindow = true
286+
};
287+
using var wp = System.Diagnostics.Process.Start(wherePsi);
288+
string output = wp.StandardOutput.ReadToEnd().Trim();
289+
wp.WaitForExit(1500);
290+
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
291+
{
292+
foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
293+
{
294+
string path = line.Trim();
295+
if (File.Exists(path) && ValidateUvBinary(path)) return path;
296+
}
297+
}
298+
}
299+
catch { }
300+
273301
candidates = new[]
274302
{
303+
// Preferred: WinGet Links shims (stable entrypoints)
304+
Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"),
305+
Path.Combine(programFiles, "WinGet", "Links", "uv.exe"),
306+
// Optional low-priority fallback for atypical images
307+
Path.Combine(programData, "Microsoft", "WinGet", "Links", "uv.exe"),
308+
275309
// Common per-user installs
276-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python313\Scripts\uv.exe"),
277-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python312\Scripts\uv.exe"),
278-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python311\Scripts\uv.exe"),
279-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python310\Scripts\uv.exe"),
280-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python313\Scripts\uv.exe"),
281-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python312\Scripts\uv.exe"),
282-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python311\Scripts\uv.exe"),
283-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python310\Scripts\uv.exe"),
310+
Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"),
311+
Path.Combine(localAppData, @"Programs\Python\Python312\Scripts\uv.exe"),
312+
Path.Combine(localAppData, @"Programs\Python\Python311\Scripts\uv.exe"),
313+
Path.Combine(localAppData, @"Programs\Python\Python310\Scripts\uv.exe"),
314+
Path.Combine(appData, @"Python\Python313\Scripts\uv.exe"),
315+
Path.Combine(appData, @"Python\Python312\Scripts\uv.exe"),
316+
Path.Combine(appData, @"Python\Python311\Scripts\uv.exe"),
317+
Path.Combine(appData, @"Python\Python310\Scripts\uv.exe"),
318+
284319
// Program Files style installs (if a native installer was used)
285-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty, @"uv\uv.exe"),
320+
Path.Combine(programFiles, @"uv\uv.exe"),
321+
286322
// Try simple name resolution later via PATH
287323
"uv.exe",
288324
"uv"
@@ -315,33 +351,10 @@ internal static string FindUvPath()
315351
catch { /* ignore */ }
316352
}
317353

318-
// Use platform-appropriate which/where to resolve from PATH
354+
// Use platform-appropriate which/where to resolve from PATH (non-Windows handled here; Windows tried earlier)
319355
try
320356
{
321-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
322-
{
323-
var wherePsi = new System.Diagnostics.ProcessStartInfo
324-
{
325-
FileName = "where",
326-
Arguments = "uv.exe",
327-
UseShellExecute = false,
328-
RedirectStandardOutput = true,
329-
RedirectStandardError = true,
330-
CreateNoWindow = true
331-
};
332-
using var wp = System.Diagnostics.Process.Start(wherePsi);
333-
string output = wp.StandardOutput.ReadToEnd().Trim();
334-
wp.WaitForExit(3000);
335-
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
336-
{
337-
foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
338-
{
339-
string path = line.Trim();
340-
if (File.Exists(path) && ValidateUvBinary(path)) return path;
341-
}
342-
}
343-
}
344-
else
357+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
345358
{
346359
var whichPsi = new System.Diagnostics.ProcessStartInfo
347360
{

0 commit comments

Comments
 (0)