From 9a9267c1280c70dda576ac41eb6702a982e66860 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Wed, 13 Aug 2025 11:35:31 -0700 Subject: [PATCH 1/8] Windows: prefer WinGet Links uv.exe and preserve existing absolute uv command during config writes --- .../Editor/Helpers/ServerInstaller.cs | 28 ++++++--- .../Editor/Windows/UnityMcpEditorWindow.cs | 62 +++++++++++++++++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs index dbdfb74..aa84589 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs @@ -270,19 +270,29 @@ 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; + candidates = new[] { + // Preferred: WinGet Links shims (stable entrypoints) + Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"), + Path.Combine(programFiles, "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" diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index e5354ba..821e03c 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -1023,6 +1023,68 @@ 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) + { + existingConfig.servers.unityMCP.args = + JsonConvert.DeserializeObject( + JsonConvert.SerializeObject(unityMCPConfig.args) + ); + } + else + { + existingConfig.mcpServers.unityMCP.args = + JsonConvert.DeserializeObject( + 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); From a2a14c179cc874d798bc5b392b0e95fd9272d8a7 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Wed, 13 Aug 2025 11:54:07 -0700 Subject: [PATCH 2/8] Claude Code: after unregister, set NotConfigured, re-check, and repaint so button toggles and status updates --- UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 821e03c..079e3e9 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -1585,6 +1585,8 @@ namespace UnityMcpBridge.Editor.Windows 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(); @@ -1593,6 +1595,12 @@ namespace UnityMcpBridge.Editor.Windows else { UnityEngine.Debug.LogWarning($"Claude MCP removal failed: {stderr}\n{stdout}"); + var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (claudeClient != null) + { + CheckClaudeCodeConfiguration(claudeClient); + } + Repaint(); } } From b6b8d47dfed6a2131e164fe4a77d55a0f8926e04 Mon Sep 17 00:00:00 2001 From: dsarno Date: Wed, 13 Aug 2025 12:36:24 -0700 Subject: [PATCH 3/8] Windows: robust Claude CLI resolution (prefer .cmd, fallback .ps1, where.exe); Unregister UX: use 'claude mcp get' exit codes; stop PATH prepend on Windows; safer detection when unregistered --- UnityMcpBridge/Editor/Helpers/ExecPath.cs | 6 +- .../Editor/Windows/UnityMcpEditorWindow.cs | 80 ++++++++++++++++++- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/UnityMcpBridge/Editor/Helpers/ExecPath.cs b/UnityMcpBridge/Editor/Helpers/ExecPath.cs index ab55fd6..99dcf5a 100644 --- a/UnityMcpBridge/Editor/Helpers/ExecPath.cs +++ b/UnityMcpBridge/Editor/Helpers/ExecPath.cs @@ -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; diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 079e3e9..ab8ab32 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -1578,9 +1578,54 @@ 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 ` + string[] candidateNamesForGet = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" }; + List existingNames = new List(); + 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 (!stderr.Contains("No MCP server found")) + { + // 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) @@ -1594,16 +1639,45 @@ 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() { string uvPath = null; From cd707284d7f395291739fcc53d5825a66c6ed104 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Wed, 13 Aug 2025 12:42:07 -0700 Subject: [PATCH 4/8] dev: add generic mcp_source.py helper to switch MCP package source (upstream/remote/local) --- mcp_source.py | 241 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100755 mcp_source.py diff --git a/mcp_source.py b/mcp_source.py new file mode 100755 index 0000000..548b2a9 --- /dev/null +++ b/mcp_source.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +""" +Generic helper to switch the Unity MCP package source in a Unity project's +Packages/manifest.json without embedding any personal paths. + +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 = "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() + +#!/usr/bin/env python3 +import json +import os +import sys +import subprocess + +# Defaults for your environment +UNITY_PROJECT = "/Users/davidsarno/ramble" # change if needed +MANIFEST = os.path.join(UNITY_PROJECT, "Packages", "manifest.json") +LOCAL_REPO = "/Users/davidsarno/unity-mcp" # local repo root +PKG_NAME = "com.coplaydev.unity-mcp" + +def get_current_branch(repo_path: str) -> str: + result = subprocess.run( + ["git", "-C", repo_path, "rev-parse", "--abbrev-ref", "HEAD"], + capture_output=True, text=True + ) + if result.returncode != 0: + print("Error: unable to detect current branch from local repo.", file=sys.stderr) + sys.exit(1) + return result.stdout.strip() + +def build_options(branch: str): + return [ + ("[1] Upstream main", "https://github.com/CoplayDev/unity-mcp.git?path=/UnityMcpBridge"), + (f"[2] Remote {branch}", f"https://github.com/dsarno/unity-mcp.git?path=/UnityMcpBridge#{branch}"), + (f"[3] Local {branch}", f"file:{os.path.join(LOCAL_REPO, 'UnityMcpBridge')}"), + ] + +def read_manifest(path: str): + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + +def write_manifest(path: str, data: dict): + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + f.write("\n") + +def main(): + # Allow overrides via args: python mcp_source.py [manifest.json] [local_repo] + manifest_path = MANIFEST if len(sys.argv) < 2 else sys.argv[1] + repo_path = LOCAL_REPO if len(sys.argv) < 3 else sys.argv[2] + + branch = get_current_branch(repo_path) + options = build_options(branch) + + print("Select MCP package source by number:") + for label, _ in options: + print(label) + 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_manifest(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_manifest(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() \ No newline at end of file From 5583327a0355b36ff84fb7f983d594a3cf076dbe Mon Sep 17 00:00:00 2001 From: dsarno Date: Wed, 13 Aug 2025 13:10:47 -0700 Subject: [PATCH 5/8] mcp_source.py: remove duplicate mac-only script; keep cross-platform argparse version (auto-detect manifest/repo; supports interactive/non-interactive) --- mcp_source.py | 73 --------------------------------------------------- 1 file changed, 73 deletions(-) diff --git a/mcp_source.py b/mcp_source.py index 548b2a9..15f2ff4 100755 --- a/mcp_source.py +++ b/mcp_source.py @@ -164,78 +164,5 @@ def main() -> None: print("Tip: In Unity, open Package Manager and Refresh to re-resolve packages.") -if __name__ == "__main__": - main() - -#!/usr/bin/env python3 -import json -import os -import sys -import subprocess - -# Defaults for your environment -UNITY_PROJECT = "/Users/davidsarno/ramble" # change if needed -MANIFEST = os.path.join(UNITY_PROJECT, "Packages", "manifest.json") -LOCAL_REPO = "/Users/davidsarno/unity-mcp" # local repo root -PKG_NAME = "com.coplaydev.unity-mcp" - -def get_current_branch(repo_path: str) -> str: - result = subprocess.run( - ["git", "-C", repo_path, "rev-parse", "--abbrev-ref", "HEAD"], - capture_output=True, text=True - ) - if result.returncode != 0: - print("Error: unable to detect current branch from local repo.", file=sys.stderr) - sys.exit(1) - return result.stdout.strip() - -def build_options(branch: str): - return [ - ("[1] Upstream main", "https://github.com/CoplayDev/unity-mcp.git?path=/UnityMcpBridge"), - (f"[2] Remote {branch}", f"https://github.com/dsarno/unity-mcp.git?path=/UnityMcpBridge#{branch}"), - (f"[3] Local {branch}", f"file:{os.path.join(LOCAL_REPO, 'UnityMcpBridge')}"), - ] - -def read_manifest(path: str): - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - -def write_manifest(path: str, data: dict): - with open(path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2) - f.write("\n") - -def main(): - # Allow overrides via args: python mcp_source.py [manifest.json] [local_repo] - manifest_path = MANIFEST if len(sys.argv) < 2 else sys.argv[1] - repo_path = LOCAL_REPO if len(sys.argv) < 3 else sys.argv[2] - - branch = get_current_branch(repo_path) - options = build_options(branch) - - print("Select MCP package source by number:") - for label, _ in options: - print(label) - 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_manifest(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_manifest(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() \ No newline at end of file From 4e1b905ea02e9ef3a4e010a2b7c4f24752c00c69 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Wed, 13 Aug 2025 14:02:19 -0700 Subject: [PATCH 6/8] chore: bump version to 2.1.0; Windows uv resolver improvements; preserve existing uv command; Claude unregister UI fix; .ps1 handling; add generic mcp_source.py --- UnityMcpBridge/Editor/Helpers/ExecPath.cs | 10 ++- .../Editor/Helpers/ServerInstaller.cs | 79 ++++++++++--------- .../Editor/Windows/UnityMcpEditorWindow.cs | 19 ++++- UnityMcpBridge/package.json | 2 +- mcp_source.py | 2 +- 5 files changed, 69 insertions(+), 43 deletions(-) diff --git a/UnityMcpBridge/Editor/Helpers/ExecPath.cs b/UnityMcpBridge/Editor/Helpers/ExecPath.cs index 99dcf5a..e3a03b4 100644 --- a/UnityMcpBridge/Editor/Helpers/ExecPath.cs +++ b/UnityMcpBridge/Editor/Helpers/ExecPath.cs @@ -176,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, diff --git a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs index aa84589..a2c28fe 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs @@ -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)) { @@ -273,12 +270,41 @@ namespace UnityMcpBridge.Editor.Helpers 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(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"), @@ -325,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 { diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index ab8ab32..234a3a0 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -1064,6 +1064,14 @@ namespace UnityMcpBridge.Editor.Windows // 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( JsonConvert.SerializeObject(unityMCPConfig.args) @@ -1071,6 +1079,14 @@ namespace UnityMcpBridge.Editor.Windows } 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( JsonConvert.SerializeObject(unityMCPConfig.args) @@ -1617,7 +1633,8 @@ namespace UnityMcpBridge.Editor.Windows UnityEngine.Debug.Log($"Successfully removed MCP server: {serverName}"); break; } - else if (!stderr.Contains("No MCP server found")) + 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}"); diff --git a/UnityMcpBridge/package.json b/UnityMcpBridge/package.json index ba4add4..445f448 100644 --- a/UnityMcpBridge/package.json +++ b/UnityMcpBridge/package.json @@ -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", diff --git a/mcp_source.py b/mcp_source.py index 15f2ff4..535dbae 100755 --- a/mcp_source.py +++ b/mcp_source.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ Generic helper to switch the Unity MCP package source in a Unity project's -Packages/manifest.json without embedding any personal paths. +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] From 370a36044dc580e5283e2f051955b4ef69fa9930 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Wed, 13 Aug 2025 14:23:52 -0700 Subject: [PATCH 7/8] docs: update README with client-specific config flows and mcp_source.py documentation --- README-DEV.md | 24 +++++++++++++++++++++++- README.md | 26 ++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/README-DEV.md b/README-DEV.md index 98dafae..f6bb942 100644 --- a/README-DEV.md +++ b/README-DEV.md @@ -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 @@ -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 \ No newline at end of file +- 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. \ No newline at end of file diff --git a/README.md b/README.md index 673837b..17d63c8 100644 --- a/README.md +++ b/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.
-Click for OS-Specific JSON Configuration Snippets... +Click for Client-Specific JSON Configuration Snippets... + +**VSCode (all OS)** + +```json +{ + "servers": { + "unityMCP": { + "command": "uv", + "args": ["--directory","/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:** From f8c76db9ca84a250befc7c7e8acefc24aba7d7c4 Mon Sep 17 00:00:00 2001 From: dsarno Date: Wed, 13 Aug 2025 14:31:10 -0700 Subject: [PATCH 8/8] Fix Unity Package Manager Git URL format in mcp_source.py --- mcp_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp_source.py b/mcp_source.py index 535dbae..1cd708e 100755 --- a/mcp_source.py +++ b/mcp_source.py @@ -92,7 +92,7 @@ def write_json(path: pathlib.Path, data: dict) -> None: def build_options(repo_root: pathlib.Path, branch: str, origin_https: str): - upstream = "https://github.com/CoplayDev/unity-mcp.git?path=/UnityMcpBridge" + 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