Skip to content
Merged
24 changes: 23 additions & 1 deletion README-DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Deploys your development code to the actual installation locations for testing.
3. Enter server path (or use default: `%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src`)
4. Enter backup location (or use default: `%USERPROFILE%\Desktop\unity-mcp-backup`)

**Note:** Dev deploy skips `.venv`, `__pycache__`, `.pytest_cache`, `.mypy_cache`, `.git`; reduces churn and avoids copying virtualenvs.

### `restore-dev.bat`
Restores original files from backup.

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


## Switching MCP package sources quickly

Use `mcp_source.py` to quickly switch between different Unity MCP package sources:

**Usage:**
```bash
python mcp_source.py [--manifest /path/to/manifest.json] [--repo /path/to/unity-mcp] [--choice 1|2|3]
```

**Options:**
- **1** Upstream main (CoplayDev/unity-mcp)
- **2** Remote current branch (origin + branch)
- **3** Local workspace (file: UnityMcpBridge)

After switching, open Package Manager and Refresh to re-resolve packages.


## Troubleshooting

### "Path not found" errors running the .bat file
Expand All @@ -88,4 +107,7 @@ Note: In recent builds, the Python server sources are also bundled inside the pa
### "Backup not found" errors
- Run `deploy-dev.bat` first to create initial backup
- Check backup directory permissions
- Verify backup directory path is correct
- Verify backup directory path is correct

### Windows uv path issues
- 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.
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,13 @@ Connect your MCP Client (Claude, Cursor, etc.) to the Python server you installe

1. In Unity, go to `Window > Unity MCP`.
2. Click `Auto-Setup`.
3. Look for a green status indicator 🟢 and "Connected ✓". *(This attempts to modify the MCP Client\'s config file automatically)*.
3. Look for a green status indicator 🟢 and "Connected ✓". *(This attempts to modify the MCP Client\'s config file automatically).*

Client-specific notes

- **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.
- **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.
- **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.


**Option B: Manual Configuration**
Expand All @@ -137,7 +143,23 @@ If Auto-Setup fails or you use a different client:
2. **Edit the file** to add/update the `mcpServers` section, using the *exact* paths from Step 1.

<details>
<summary><strong>Click for OS-Specific JSON Configuration Snippets...</strong></summary>
<summary><strong>Click for Client-Specific JSON Configuration Snippets...</strong></summary>

**VSCode (all OS)**

```json
{
"servers": {
"unityMCP": {
"command": "uv",
"args": ["--directory","<ABSOLUTE_PATH_TO>/UnityMcpServer/src","run","server.py"],
"type": "stdio"
}
}
}
```

On Windows, set `command` to the absolute shim, e.g. `C:\\Users\\YOU\\AppData\\Local\\Microsoft\\WinGet\\Links\\uv.exe`.

**Windows:**

Expand Down
16 changes: 13 additions & 3 deletions UnityMcpBridge/Editor/Helpers/ExecPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,15 @@ internal static string ResolveClaude()
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
string[] candidates =
{
// Prefer .cmd (most reliable from non-interactive processes)
Path.Combine(appData, "npm", "claude.cmd"),
Path.Combine(localAppData, "npm", "claude.cmd"),
// Fall back to PowerShell shim if only .ps1 is present
Path.Combine(appData, "npm", "claude.ps1"),
Path.Combine(localAppData, "npm", "claude.ps1"),
};
foreach (string c in candidates) { if (File.Exists(c)) return c; }
string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude");
string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude");
if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
#endif
return null;
Expand Down Expand Up @@ -172,10 +176,16 @@ internal static bool TryRun(
stderr = string.Empty;
try
{
// Handle PowerShell scripts on Windows by invoking through powershell.exe
bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase);

var psi = new ProcessStartInfo
{
FileName = file,
Arguments = args,
FileName = isPs1 ? "powershell.exe" : file,
Arguments = isPs1
? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim()
: args,
WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
UseShellExecute = false,
RedirectStandardOutput = true,
Expand Down
107 changes: 60 additions & 47 deletions UnityMcpBridge/Editor/Helpers/ServerInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Reflection;
using UnityEditor;
using UnityEngine;

Expand Down Expand Up @@ -70,21 +69,19 @@ private static string GetSaveLocation()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"AppData",
"Local",
"Programs",
RootFolder
);
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local");
return Path.Combine(localAppData, "Programs", RootFolder);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"bin",
RootFolder
);
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);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Expand Down Expand Up @@ -270,19 +267,58 @@ internal static string FindUvPath()
string[] candidates;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty;
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
string programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) ?? string.Empty; // optional fallback

// Fast path: resolve from PATH first
try
{
var wherePsi = new System.Diagnostics.ProcessStartInfo
{
FileName = "where",
Arguments = "uv.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var wp = System.Diagnostics.Process.Start(wherePsi);
string output = wp.StandardOutput.ReadToEnd().Trim();
wp.WaitForExit(1500);
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
{
foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
string path = line.Trim();
if (File.Exists(path) && ValidateUvBinary(path)) return path;
}
}
}
catch { }

candidates = new[]
{
// Preferred: WinGet Links shims (stable entrypoints)
Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"),
Path.Combine(programFiles, "WinGet", "Links", "uv.exe"),
// Optional low-priority fallback for atypical images
Path.Combine(programData, "Microsoft", "WinGet", "Links", "uv.exe"),

// Common per-user installs
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python313\Scripts\uv.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python312\Scripts\uv.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python311\Scripts\uv.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python310\Scripts\uv.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python313\Scripts\uv.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python312\Scripts\uv.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python311\Scripts\uv.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python310\Scripts\uv.exe"),
Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"),
Path.Combine(localAppData, @"Programs\Python\Python312\Scripts\uv.exe"),
Path.Combine(localAppData, @"Programs\Python\Python311\Scripts\uv.exe"),
Path.Combine(localAppData, @"Programs\Python\Python310\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python313\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python312\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python311\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python310\Scripts\uv.exe"),

// Program Files style installs (if a native installer was used)
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty, @"uv\uv.exe"),
Path.Combine(programFiles, @"uv\uv.exe"),

// Try simple name resolution later via PATH
"uv.exe",
"uv"
Expand Down Expand Up @@ -315,33 +351,10 @@ internal static string FindUvPath()
catch { /* ignore */ }
}

// Use platform-appropriate which/where to resolve from PATH
// Use platform-appropriate which/where to resolve from PATH (non-Windows handled here; Windows tried earlier)
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var wherePsi = new System.Diagnostics.ProcessStartInfo
{
FileName = "where",
Arguments = "uv.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var wp = System.Diagnostics.Process.Start(wherePsi);
string output = wp.StandardOutput.ReadToEnd().Trim();
wp.WaitForExit(3000);
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
{
foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
string path = line.Trim();
if (File.Exists(path) && ValidateUvBinary(path)) return path;
}
}
}
else
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var whichPsi = new System.Diagnostics.ProcessStartInfo
{
Expand Down
Loading