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`)
|
||||
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.
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -89,3 +108,6 @@ Note: In recent builds, the Python server sources are also bundled inside the pa
|
|||
- Run `deploy-dev.bat` first to create initial backup
|
||||
- Check backup directory permissions
|
||||
- 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`.
|
||||
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**
|
||||
|
|
@ -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:**
|
||||
|
||||
|
|
|
|||
|
|
@ -53,11 +53,15 @@ namespace UnityMcpBridge.Editor.Helpers
|
|||
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;
|
||||
|
|
@ -172,10 +176,16 @@ namespace UnityMcpBridge.Editor.Helpers
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using System;
|
|||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
|
|
@ -70,21 +69,19 @@ namespace UnityMcpBridge.Editor.Helpers
|
|||
{
|
||||
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))
|
||||
{
|
||||
|
|
@ -270,19 +267,58 @@ namespace UnityMcpBridge.Editor.Helpers
|
|||
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"
|
||||
|
|
@ -315,33 +351,10 @@ namespace UnityMcpBridge.Editor.Helpers
|
|||
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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1023,6 +1023,84 @@ namespace UnityMcpBridge.Editor.Windows
|
|||
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
|
||||
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
|
||||
File.WriteAllText(configPath, mergedJson);
|
||||
|
|
@ -1516,13 +1594,61 @@ namespace UnityMcpBridge.Editor.Windows
|
|||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
string pathPrepend = Application.platform == RuntimePlatform.OSXEditor
|
||||
? "/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);
|
||||
if (claudeClient != null)
|
||||
{
|
||||
// Optimistically flip to NotConfigured; then verify
|
||||
claudeClient.SetStatus(McpStatus.NotConfigured);
|
||||
CheckClaudeCodeConfiguration(claudeClient);
|
||||
}
|
||||
Repaint();
|
||||
|
|
@ -1530,8 +1656,43 @@ namespace UnityMcpBridge.Editor.Windows
|
|||
}
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "com.coplaydev.unity-mcp",
|
||||
"version": "2.0.2",
|
||||
"version": "2.1.0",
|
||||
"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.",
|
||||
"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