diff --git a/README-DEV.md b/README-DEV.md index 98dafae6..f6bb942d 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 673837b8..17d63c86 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:** diff --git a/UnityMcpBridge/Editor/Helpers/ExecPath.cs b/UnityMcpBridge/Editor/Helpers/ExecPath.cs index ab55fd6a..e3a03b43 100644 --- a/UnityMcpBridge/Editor/Helpers/ExecPath.cs +++ b/UnityMcpBridge/Editor/Helpers/ExecPath.cs @@ -53,11 +53,15 @@ internal static string ResolveClaude() string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; string[] candidates = { + // Prefer .cmd (most reliable from non-interactive processes) Path.Combine(appData, "npm", "claude.cmd"), Path.Combine(localAppData, "npm", "claude.cmd"), + // Fall back to PowerShell shim if only .ps1 is present + Path.Combine(appData, "npm", "claude.ps1"), + Path.Combine(localAppData, "npm", "claude.ps1"), }; foreach (string c in candidates) { if (File.Exists(c)) return c; } - string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude"); + string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude"); if (!string.IsNullOrEmpty(fromWhere)) return fromWhere; #endif return null; @@ -172,10 +176,16 @@ internal static bool TryRun( stderr = string.Empty; try { + // Handle PowerShell scripts on Windows by invoking through powershell.exe + bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase); + var psi = new ProcessStartInfo { - FileName = file, - Arguments = args, + FileName = isPs1 ? "powershell.exe" : file, + Arguments = isPs1 + ? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim() + : args, WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir, UseShellExecute = false, RedirectStandardOutput = true, diff --git a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs index dbdfb743..a2c28fe5 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs @@ -2,7 +2,6 @@ using System.IO; using System.Runtime.InteropServices; using System.Text; -using System.Reflection; using UnityEditor; using UnityEngine; @@ -70,21 +69,19 @@ private static string GetSaveLocation() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "AppData", - "Local", - "Programs", - RootFolder - ); + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local"); + return Path.Combine(localAppData, "Programs", RootFolder); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "bin", - RootFolder - ); + var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + if (string.IsNullOrEmpty(xdg)) + { + xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, + ".local", "share"); + } + return Path.Combine(xdg, RootFolder); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { @@ -270,19 +267,58 @@ internal static string FindUvPath() string[] candidates; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; + string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty; + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; + string programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) ?? string.Empty; // optional fallback + + // Fast path: resolve from PATH first + try + { + var wherePsi = new System.Diagnostics.ProcessStartInfo + { + FileName = "where", + Arguments = "uv.exe", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var wp = System.Diagnostics.Process.Start(wherePsi); + string output = wp.StandardOutput.ReadToEnd().Trim(); + wp.WaitForExit(1500); + if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output)) + { + foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) + { + string path = line.Trim(); + if (File.Exists(path) && ValidateUvBinary(path)) return path; + } + } + } + catch { } + candidates = new[] { + // Preferred: WinGet Links shims (stable entrypoints) + Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"), + Path.Combine(programFiles, "WinGet", "Links", "uv.exe"), + // Optional low-priority fallback for atypical images + Path.Combine(programData, "Microsoft", "WinGet", "Links", "uv.exe"), + // Common per-user installs - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python313\Scripts\uv.exe"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python312\Scripts\uv.exe"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python311\Scripts\uv.exe"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python310\Scripts\uv.exe"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python313\Scripts\uv.exe"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python312\Scripts\uv.exe"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python311\Scripts\uv.exe"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python310\Scripts\uv.exe"), + Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"), + Path.Combine(localAppData, @"Programs\Python\Python312\Scripts\uv.exe"), + Path.Combine(localAppData, @"Programs\Python\Python311\Scripts\uv.exe"), + Path.Combine(localAppData, @"Programs\Python\Python310\Scripts\uv.exe"), + Path.Combine(appData, @"Python\Python313\Scripts\uv.exe"), + Path.Combine(appData, @"Python\Python312\Scripts\uv.exe"), + Path.Combine(appData, @"Python\Python311\Scripts\uv.exe"), + Path.Combine(appData, @"Python\Python310\Scripts\uv.exe"), + // Program Files style installs (if a native installer was used) - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty, @"uv\uv.exe"), + Path.Combine(programFiles, @"uv\uv.exe"), + // Try simple name resolution later via PATH "uv.exe", "uv" @@ -315,33 +351,10 @@ internal static string FindUvPath() catch { /* ignore */ } } - // Use platform-appropriate which/where to resolve from PATH + // Use platform-appropriate which/where to resolve from PATH (non-Windows handled here; Windows tried earlier) try { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var wherePsi = new System.Diagnostics.ProcessStartInfo - { - FileName = "where", - Arguments = "uv.exe", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using var wp = System.Diagnostics.Process.Start(wherePsi); - string output = wp.StandardOutput.ReadToEnd().Trim(); - wp.WaitForExit(3000); - if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output)) - { - foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) - { - string path = line.Trim(); - if (File.Exists(path) && ValidateUvBinary(path)) return path; - } - } - } - else + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { var whichPsi = new System.Diagnostics.ProcessStartInfo { diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index e5354bad..234a3a09 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -1023,6 +1023,84 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC 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( + 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( + 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 @@ private void UnregisterWithClaudeCode() 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 (!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,10 +1656,45 @@ private void UnregisterWithClaudeCode() } 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; diff --git a/UnityMcpBridge/package.json b/UnityMcpBridge/package.json index ba4add49..445f448b 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 new file mode 100755 index 00000000..1cd708e3 --- /dev/null +++ b/mcp_source.py @@ -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() \ No newline at end of file