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.pymain
commit
a7af0cd9b0
|
|
@ -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`)
|
3. Enter server path (or use default: `%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src`)
|
||||||
4. Enter backup location (or use default: `%USERPROFILE%\Desktop\unity-mcp-backup`)
|
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`
|
### `restore-dev.bat`
|
||||||
Restores original files from backup.
|
Restores original files from backup.
|
||||||
|
|
||||||
|
|
@ -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`
|
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
|
## Troubleshooting
|
||||||
|
|
||||||
### "Path not found" errors running the .bat file
|
### "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
|
||||||
### "Backup not found" errors
|
### "Backup not found" errors
|
||||||
- Run `deploy-dev.bat` first to create initial backup
|
- Run `deploy-dev.bat` first to create initial backup
|
||||||
- Check backup directory permissions
|
- 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
README.md
26
README.md
|
|
@ -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`.
|
1. In Unity, go to `Window > Unity MCP`.
|
||||||
2. Click `Auto-Setup`.
|
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**
|
**Option B: Manual Configuration**
|
||||||
|
|
@ -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.
|
2. **Edit the file** to add/update the `mcpServers` section, using the *exact* paths from Step 1.
|
||||||
|
|
||||||
<details>
|
<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:**
|
**Windows:**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,15 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
|
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
|
||||||
string[] candidates =
|
string[] candidates =
|
||||||
{
|
{
|
||||||
|
// Prefer .cmd (most reliable from non-interactive processes)
|
||||||
Path.Combine(appData, "npm", "claude.cmd"),
|
Path.Combine(appData, "npm", "claude.cmd"),
|
||||||
Path.Combine(localAppData, "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; }
|
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;
|
if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
|
||||||
#endif
|
#endif
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -172,10 +176,16 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
stderr = string.Empty;
|
stderr = string.Empty;
|
||||||
try
|
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
|
var psi = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = file,
|
FileName = isPs1 ? "powershell.exe" : file,
|
||||||
Arguments = args,
|
Arguments = isPs1
|
||||||
|
? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim()
|
||||||
|
: args,
|
||||||
WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
|
WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Reflection;
|
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
|
|
@ -70,21 +69,19 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
{
|
{
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
return Path.Combine(
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local");
|
||||||
"AppData",
|
return Path.Combine(localAppData, "Programs", RootFolder);
|
||||||
"Local",
|
|
||||||
"Programs",
|
|
||||||
RootFolder
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
{
|
{
|
||||||
return Path.Combine(
|
var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
if (string.IsNullOrEmpty(xdg))
|
||||||
"bin",
|
{
|
||||||
RootFolder
|
xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty,
|
||||||
);
|
".local", "share");
|
||||||
|
}
|
||||||
|
return Path.Combine(xdg, RootFolder);
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
{
|
{
|
||||||
|
|
@ -270,19 +267,58 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
string[] candidates;
|
string[] candidates;
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
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[]
|
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
|
// Common per-user installs
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python313\Scripts\uv.exe"),
|
Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"),
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python312\Scripts\uv.exe"),
|
Path.Combine(localAppData, @"Programs\Python\Python312\Scripts\uv.exe"),
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python311\Scripts\uv.exe"),
|
Path.Combine(localAppData, @"Programs\Python\Python311\Scripts\uv.exe"),
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python310\Scripts\uv.exe"),
|
Path.Combine(localAppData, @"Programs\Python\Python310\Scripts\uv.exe"),
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python313\Scripts\uv.exe"),
|
Path.Combine(appData, @"Python\Python313\Scripts\uv.exe"),
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python312\Scripts\uv.exe"),
|
Path.Combine(appData, @"Python\Python312\Scripts\uv.exe"),
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python311\Scripts\uv.exe"),
|
Path.Combine(appData, @"Python\Python311\Scripts\uv.exe"),
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python310\Scripts\uv.exe"),
|
Path.Combine(appData, @"Python\Python310\Scripts\uv.exe"),
|
||||||
|
|
||||||
// Program Files style installs (if a native installer was used)
|
// 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
|
// Try simple name resolution later via PATH
|
||||||
"uv.exe",
|
"uv.exe",
|
||||||
"uv"
|
"uv"
|
||||||
|
|
@ -315,33 +351,10 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
catch { /* ignore */ }
|
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
|
try
|
||||||
{
|
{
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
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
|
|
||||||
{
|
{
|
||||||
var whichPsi = new System.Diagnostics.ProcessStartInfo
|
var whichPsi = new System.Diagnostics.ProcessStartInfo
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1023,6 +1023,84 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If config already has a working absolute uv path, avoid rewriting it on refresh
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (mcpClient?.mcpType != McpTypes.ClaudeCode)
|
||||||
|
{
|
||||||
|
// Inspect existing command for stability (Windows absolute path that exists)
|
||||||
|
string existingCommand = null;
|
||||||
|
if (mcpClient?.mcpType == McpTypes.VSCode)
|
||||||
|
{
|
||||||
|
existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(existingCommand))
|
||||||
|
{
|
||||||
|
bool keep = false;
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
// Consider absolute, existing paths as stable; prefer WinGet Links
|
||||||
|
if (Path.IsPathRooted(existingCommand) && File.Exists(existingCommand))
|
||||||
|
{
|
||||||
|
keep = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// On Unix, keep absolute existing path as well
|
||||||
|
if (Path.IsPathRooted(existingCommand) && File.Exists(existingCommand))
|
||||||
|
{
|
||||||
|
keep = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keep)
|
||||||
|
{
|
||||||
|
// Merge without replacing the existing command
|
||||||
|
if (mcpClient?.mcpType == McpTypes.VSCode)
|
||||||
|
{
|
||||||
|
if (existingConfig.servers == null)
|
||||||
|
{
|
||||||
|
existingConfig.servers = new Newtonsoft.Json.Linq.JObject();
|
||||||
|
}
|
||||||
|
if (existingConfig.servers.unityMCP == null)
|
||||||
|
{
|
||||||
|
existingConfig.servers.unityMCP = new Newtonsoft.Json.Linq.JObject();
|
||||||
|
}
|
||||||
|
existingConfig.servers.unityMCP.args =
|
||||||
|
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
|
||||||
|
JsonConvert.SerializeObject(unityMCPConfig.args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (existingConfig.mcpServers == null)
|
||||||
|
{
|
||||||
|
existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject();
|
||||||
|
}
|
||||||
|
if (existingConfig.mcpServers.unityMCP == null)
|
||||||
|
{
|
||||||
|
existingConfig.mcpServers.unityMCP = new Newtonsoft.Json.Linq.JObject();
|
||||||
|
}
|
||||||
|
existingConfig.mcpServers.unityMCP.args =
|
||||||
|
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
|
||||||
|
JsonConvert.SerializeObject(unityMCPConfig.args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
string mergedKeep = JsonConvert.SerializeObject(existingConfig, jsonSettings);
|
||||||
|
File.WriteAllText(configPath, mergedKeep);
|
||||||
|
return "Configured successfully";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* fall back to normal write */ }
|
||||||
|
|
||||||
// Write the merged configuration back to file
|
// Write the merged configuration back to file
|
||||||
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
|
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
|
||||||
File.WriteAllText(configPath, mergedJson);
|
File.WriteAllText(configPath, mergedJson);
|
||||||
|
|
@ -1516,13 +1594,61 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||||
string pathPrepend = Application.platform == RuntimePlatform.OSXEditor
|
string pathPrepend = Application.platform == RuntimePlatform.OSXEditor
|
||||||
? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
||||||
: "/usr/local/bin:/usr/bin:/bin";
|
: null; // On Windows, don't modify PATH - use system PATH as-is
|
||||||
|
|
||||||
if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
|
// Determine if Claude has a UnityMCP server registered by using exit codes from `claude mcp get <name>`
|
||||||
|
string[] candidateNamesForGet = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" };
|
||||||
|
List<string> existingNames = new List<string>();
|
||||||
|
foreach (var candidate in candidateNamesForGet)
|
||||||
|
{
|
||||||
|
if (ExecPath.TryRun(claudePath, $"mcp get {candidate}", projectDir, out var getStdout, out var getStderr, 7000, pathPrepend))
|
||||||
|
{
|
||||||
|
// Success exit code indicates the server exists
|
||||||
|
existingNames.Add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingNames.Count == 0)
|
||||||
|
{
|
||||||
|
// Nothing to unregister – set status and bail early
|
||||||
|
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||||
|
if (claudeClient != null)
|
||||||
|
{
|
||||||
|
claudeClient.SetStatus(McpStatus.NotConfigured);
|
||||||
|
UnityEngine.Debug.Log("Claude CLI reports no UnityMCP server via 'mcp get' – setting status to NotConfigured and aborting unregister.");
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try different possible server names
|
||||||
|
string[] possibleNames = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" };
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
foreach (string serverName in possibleNames)
|
||||||
|
{
|
||||||
|
if (ExecPath.TryRun(claudePath, $"mcp remove {serverName}", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
|
||||||
|
{
|
||||||
|
success = true;
|
||||||
|
UnityEngine.Debug.Log($"Successfully removed MCP server: {serverName}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(stderr) &&
|
||||||
|
!stderr.Contains("No MCP server found", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// If it's not a "not found" error, log it and stop trying
|
||||||
|
UnityEngine.Debug.LogWarning($"Error removing {serverName}: {stderr}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success)
|
||||||
{
|
{
|
||||||
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||||
if (claudeClient != null)
|
if (claudeClient != null)
|
||||||
{
|
{
|
||||||
|
// Optimistically flip to NotConfigured; then verify
|
||||||
|
claudeClient.SetStatus(McpStatus.NotConfigured);
|
||||||
CheckClaudeCodeConfiguration(claudeClient);
|
CheckClaudeCodeConfiguration(claudeClient);
|
||||||
}
|
}
|
||||||
Repaint();
|
Repaint();
|
||||||
|
|
@ -1530,10 +1656,45 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UnityEngine.Debug.LogWarning($"Claude MCP removal failed: {stderr}\n{stdout}");
|
// If no servers were found to remove, they're already unregistered
|
||||||
|
// Force status to NotConfigured and update the UI
|
||||||
|
UnityEngine.Debug.Log("No MCP servers found to unregister - already unregistered.");
|
||||||
|
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||||
|
if (claudeClient != null)
|
||||||
|
{
|
||||||
|
claudeClient.SetStatus(McpStatus.NotConfigured);
|
||||||
|
CheckClaudeCodeConfiguration(claudeClient);
|
||||||
|
}
|
||||||
|
Repaint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool ParseTextOutput(string claudePath, string projectDir, string pathPrepend)
|
||||||
|
{
|
||||||
|
if (ExecPath.TryRun(claudePath, "mcp list", projectDir, out var listStdout, out var listStderr, 10000, pathPrepend))
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.Log($"Claude MCP servers (text): {listStdout}");
|
||||||
|
|
||||||
|
// Check if output indicates no servers or contains UnityMCP variants
|
||||||
|
if (listStdout.Contains("No MCP servers configured") ||
|
||||||
|
listStdout.Contains("no servers") ||
|
||||||
|
listStdout.Contains("No servers") ||
|
||||||
|
string.IsNullOrWhiteSpace(listStdout) ||
|
||||||
|
listStdout.Trim().Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for UnityMCP variants in the output
|
||||||
|
return listStdout.Contains("UnityMCP") ||
|
||||||
|
listStdout.Contains("unityMCP") ||
|
||||||
|
listStdout.Contains("unity-mcp");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If command failed, assume no servers
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private string FindUvPath()
|
private string FindUvPath()
|
||||||
{
|
{
|
||||||
string uvPath = null;
|
string uvPath = null;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "com.coplaydev.unity-mcp",
|
"name": "com.coplaydev.unity-mcp",
|
||||||
"version": "2.0.2",
|
"version": "2.1.0",
|
||||||
"displayName": "Unity MCP Bridge",
|
"displayName": "Unity MCP Bridge",
|
||||||
"description": "A bridge that manages and communicates with the sister application, Unity MCP Server, which allows for communications with MCP Clients like Claude Desktop or Cursor.",
|
"description": "A bridge that manages and communicates with the sister application, Unity MCP Server, which allows for communications with MCP Clients like Claude Desktop or Cursor.",
|
||||||
"unity": "2020.3",
|
"unity": "2020.3",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generic helper to switch the Unity MCP package source in a Unity project's
|
||||||
|
Packages/manifest.json. This is useful for switching between upstream and local repos while working on the MCP.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python mcp_source.py [--manifest /abs/path/to/manifest.json] [--repo /abs/path/to/unity-mcp] [--choice 1|2|3]
|
||||||
|
|
||||||
|
Choices:
|
||||||
|
1) Upstream main (CoplayDev/unity-mcp)
|
||||||
|
2) Your remote current branch (derived from `origin` and current branch)
|
||||||
|
3) Local repo workspace (file: URL to UnityMcpBridge in your checkout)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
PKG_NAME = "com.coplaydev.unity-mcp"
|
||||||
|
BRIDGE_SUBPATH = "UnityMcpBridge"
|
||||||
|
|
||||||
|
|
||||||
|
def run_git(repo: pathlib.Path, *args: str) -> str:
|
||||||
|
result = subprocess.run([
|
||||||
|
"git", "-C", str(repo), *args
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(result.stderr.strip() or f"git {' '.join(args)} failed")
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_origin_to_https(url: str) -> str:
|
||||||
|
"""Map common SSH origin forms to https for Unity's git URL scheme."""
|
||||||
|
if url.startswith("git@github.com:"):
|
||||||
|
owner_repo = url.split(":", 1)[1]
|
||||||
|
if owner_repo.endswith(".git"):
|
||||||
|
owner_repo = owner_repo[:-4]
|
||||||
|
return f"https://github.com/{owner_repo}.git"
|
||||||
|
# already https or file: etc.
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def detect_repo_root(explicit: Optional[str]) -> pathlib.Path:
|
||||||
|
if explicit:
|
||||||
|
return pathlib.Path(explicit).resolve()
|
||||||
|
# Prefer the git toplevel from the script's directory
|
||||||
|
here = pathlib.Path(__file__).resolve().parent
|
||||||
|
try:
|
||||||
|
top = run_git(here, "rev-parse", "--show-toplevel")
|
||||||
|
return pathlib.Path(top)
|
||||||
|
except Exception:
|
||||||
|
return here
|
||||||
|
|
||||||
|
|
||||||
|
def detect_branch(repo: pathlib.Path) -> str:
|
||||||
|
return run_git(repo, "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
|
||||||
|
|
||||||
|
def detect_origin(repo: pathlib.Path) -> str:
|
||||||
|
url = run_git(repo, "remote", "get-url", "origin")
|
||||||
|
return normalize_origin_to_https(url)
|
||||||
|
|
||||||
|
|
||||||
|
def find_manifest(explicit: Optional[str]) -> pathlib.Path:
|
||||||
|
if explicit:
|
||||||
|
return pathlib.Path(explicit).resolve()
|
||||||
|
# Walk up from CWD looking for Packages/manifest.json
|
||||||
|
cur = pathlib.Path.cwd().resolve()
|
||||||
|
for parent in [cur, *cur.parents]:
|
||||||
|
candidate = parent / "Packages" / "manifest.json"
|
||||||
|
if candidate.exists():
|
||||||
|
return candidate
|
||||||
|
raise FileNotFoundError("Could not find Packages/manifest.json from current directory. Use --manifest to specify a path.")
|
||||||
|
|
||||||
|
|
||||||
|
def read_json(path: pathlib.Path) -> dict:
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path: pathlib.Path, data: dict) -> None:
|
||||||
|
with path.open("w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def build_options(repo_root: pathlib.Path, branch: str, origin_https: str):
|
||||||
|
upstream = "git+https://github.com/CoplayDev/unity-mcp.git?path=/UnityMcpBridge"
|
||||||
|
# Ensure origin is https
|
||||||
|
origin = origin_https
|
||||||
|
# If origin is a local file path or non-https, try to coerce to https github if possible
|
||||||
|
if origin.startswith("file:"):
|
||||||
|
# Not meaningful for remote option; keep upstream
|
||||||
|
origin_remote = upstream
|
||||||
|
else:
|
||||||
|
origin_remote = origin
|
||||||
|
return [
|
||||||
|
("[1] Upstream main", upstream),
|
||||||
|
("[2] Remote current branch", f"{origin_remote}?path=/{BRIDGE_SUBPATH}#{branch}"),
|
||||||
|
("[3] Local workspace", f"file:{(repo_root / BRIDGE_SUBPATH).as_posix()}"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
p = argparse.ArgumentParser(description="Switch Unity MCP package source")
|
||||||
|
p.add_argument("--manifest", help="Path to Packages/manifest.json")
|
||||||
|
p.add_argument("--repo", help="Path to unity-mcp repo root (for local file option)")
|
||||||
|
p.add_argument("--choice", choices=["1", "2", "3"], help="Pick option non-interactively")
|
||||||
|
return p.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
try:
|
||||||
|
repo_root = detect_repo_root(args.repo)
|
||||||
|
branch = detect_branch(repo_root)
|
||||||
|
origin = detect_origin(repo_root)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
options = build_options(repo_root, branch, origin)
|
||||||
|
|
||||||
|
try:
|
||||||
|
manifest_path = find_manifest(args.manifest)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Select MCP package source by number:")
|
||||||
|
for label, _ in options:
|
||||||
|
print(label)
|
||||||
|
|
||||||
|
if args.choice:
|
||||||
|
choice = args.choice
|
||||||
|
else:
|
||||||
|
choice = input("Enter 1-3: ").strip()
|
||||||
|
|
||||||
|
if choice not in {"1", "2", "3"}:
|
||||||
|
print("Invalid selection.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
idx = int(choice) - 1
|
||||||
|
_, chosen = options[idx]
|
||||||
|
|
||||||
|
data = read_json(manifest_path)
|
||||||
|
deps = data.get("dependencies", {})
|
||||||
|
if PKG_NAME not in deps:
|
||||||
|
print(f"Error: '{PKG_NAME}' not found in manifest dependencies.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"\nUpdating {PKG_NAME} → {chosen}")
|
||||||
|
deps[PKG_NAME] = chosen
|
||||||
|
data["dependencies"] = deps
|
||||||
|
write_json(manifest_path, data)
|
||||||
|
print(f"Done. Wrote to: {manifest_path}")
|
||||||
|
print("Tip: In Unity, open Package Manager and Refresh to re-resolve packages.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue