diff --git a/.github/workflows/github-repo-stats.yml b/.github/workflows/github-repo-stats.yml index fb130a1b..d61f894b 100644 --- a/.github/workflows/github-repo-stats.yml +++ b/.github/workflows/github-repo-stats.yml @@ -1,10 +1,10 @@ name: github-repo-stats on: - schedule: + # schedule: # Run this once per day, towards the end of the day for keeping the most # recent data point most meaningful (hours are interpreted in UTC). - - cron: "0 23 * * *" + #- cron: "0 23 * * *" workflow_dispatch: # Allow for running this manually. jobs: diff --git a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs index 084e2a7e..b86f15cb 100644 --- a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs +++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs @@ -85,15 +85,31 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl // Stdio mode: Use uvx command var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts(); - unity["command"] = uvxPath; + var args = new List(); - var args = new List { packageName }; - if (!string.IsNullOrEmpty(fromUrl)) + // Fix for Windows GUI apps (Claude Desktop, Cursor, etc.): + // Wrap in cmd /c to ensure PATH and environment are properly resolved. + if (UnityEngine.Application.platform == UnityEngine.RuntimePlatform.WindowsEditor) + { + unity["command"] = "cmd"; + args.Add("/c"); + + // If uvxPath contains spaces, we might need to ensure it's treated as a command. + // But typically in JSON args, it's just the next argument. + args.Add(uvxPath); + } + else { - args.Insert(0, fromUrl); - args.Insert(0, "--from"); + unity["command"] = uvxPath; } + if (!string.IsNullOrEmpty(fromUrl)) + { + args.Add("--from"); + args.Add(fromUrl); + } + + args.Add(packageName); args.Add("--transport"); args.Add("stdio"); diff --git a/MCPForUnity/Editor/Helpers/ExecPath.cs b/MCPForUnity/Editor/Helpers/ExecPath.cs index 9190ec38..ac3ec83a 100644 --- a/MCPForUnity/Editor/Helpers/ExecPath.cs +++ b/MCPForUnity/Editor/Helpers/ExecPath.cs @@ -132,6 +132,56 @@ private static string ResolveClaudeFromNvm(string home) catch { return null; } } + // Resolve uvx absolute path. Pref -> env -> common locations -> PATH. + internal static string ResolveUvx() + { + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; + string[] candidates = + { + Path.Combine(home, ".local", "bin", "uvx"), + Path.Combine(home, ".cargo", "bin", "uvx"), + "/usr/local/bin/uvx", + "/opt/homebrew/bin/uvx", + "/usr/bin/uvx" + }; + foreach (string c in candidates) { if (File.Exists(c)) return c; } + +#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX + return Which("uvx", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"); +#else + return null; +#endif + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { +#if UNITY_EDITOR_WIN + string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + string[] candidates = + { + Path.Combine(userProfile, ".cargo", "bin", "uvx.exe"), + Path.Combine(localAppData, "uv", "uvx.exe"), + Path.Combine(userProfile, "uv", "uvx.exe"), + }; + foreach (string c in candidates) { if (File.Exists(c)) return c; } + + string fromWhere = Where("uvx.exe") ?? Where("uvx"); + if (!string.IsNullOrEmpty(fromWhere)) return fromWhere; +#endif + return null; + } + } + catch { } + + return null; + } + // Explicitly set the Claude CLI absolute path override in EditorPrefs internal static void SetClaudeCliPath(string absolutePath) { diff --git a/MCPForUnity/Editor/Services/PathResolverService.cs b/MCPForUnity/Editor/Services/PathResolverService.cs index 4b6b07fb..b8444eb5 100644 --- a/MCPForUnity/Editor/Services/PathResolverService.cs +++ b/MCPForUnity/Editor/Services/PathResolverService.cs @@ -34,6 +34,13 @@ public string GetUvxPath() McpLog.Debug("No uvx path override found, falling back to default command"); } + // Auto-discovery of absolute path + string discovered = ExecPath.ResolveUvx(); + if (!string.IsNullOrEmpty(discovered)) + { + return discovered; + } + return "uvx"; } diff --git a/MCPForUnity/Editor/Services/ServerManagementService.cs b/MCPForUnity/Editor/Services/ServerManagementService.cs index 8a323aba..09315a8f 100644 --- a/MCPForUnity/Editor/Services/ServerManagementService.cs +++ b/MCPForUnity/Editor/Services/ServerManagementService.cs @@ -14,6 +14,30 @@ namespace MCPForUnity.Editor.Services /// public class ServerManagementService : IServerManagementService { + /// + /// Convert a uvx path to a uv path by replacing "uvx" with "uv" while preserving the extension + /// + private string ConvertUvxToUv(string uvxPath) + { + if (string.IsNullOrEmpty(uvxPath)) + return uvxPath; + + // Handle case-insensitive replacement of "uvx" with "uv" + // This works for paths like: + // - /usr/bin/uvx -> /usr/bin/uv + // - C:\path\to\uvx.exe -> C:\path\to\uv.exe + // - uvx -> uv + + int lastIndex = uvxPath.LastIndexOf("uvx", StringComparison.OrdinalIgnoreCase); + if (lastIndex >= 0) + { + return uvxPath.Substring(0, lastIndex) + "uv" + uvxPath.Substring(lastIndex + 3); + } + + // Fallback: if "uvx" not found, try removing last character (original behavior) + return uvxPath.Length > 0 ? uvxPath.Remove(uvxPath.Length - 1, 1) : uvxPath; + } + /// /// Clear the local uvx cache for the MCP server package /// @@ -23,7 +47,7 @@ public bool ClearUvxCache() try { string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); - string uvCommand = uvxPath.Remove(uvxPath.Length - 1, 1); + string uvCommand = ConvertUvxToUv(uvxPath); // Get the package name string packageName = "mcp-for-unity"; @@ -65,7 +89,7 @@ private bool ExecuteUvCommand(string uvCommand, string args, out string stdout, stderr = null; string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); - string uvPath = uvxPath.Remove(uvxPath.Length - 1, 1); + string uvPath = ConvertUvxToUv(uvxPath); if (!string.Equals(uvCommand, uvPath, StringComparison.OrdinalIgnoreCase)) { diff --git a/MCPForUnity/README.md b/MCPForUnity/README.md index b4048f5e..e70d2c49 100644 --- a/MCPForUnity/README.md +++ b/MCPForUnity/README.md @@ -82,6 +82,10 @@ Notes: - Help: [Fix MCP for Unity with Cursor, VS Code & Windsurf](https://github.com/CoplayDev/unity-mcp/wiki/1.-Fix-Unity-MCP-and-Cursor,-VSCode-&-Windsurf) - Claude CLI not found: - Help: [Fix MCP for Unity with Claude Code](https://github.com/CoplayDev/unity-mcp/wiki/2.-Fix-Unity-MCP-and-Claude-Code) +- Claude Desktop "spawn uvx ENOENT" error on macOS: + - Claude Desktop may not inherit your shell's PATH. + - The MCP for Unity plugin attempts to automatically resolve the absolute path to `uvx`. + - If this fails, use the "Choose UV Install Location" button in the MCP for Unity window to select your `uvx` executable (typically `~/.local/bin/uvx`), or manually update your Claude Desktop config to use the absolute path to `uvx`. --- diff --git a/Server/src/services/resources/unity_instances.py b/Server/src/services/resources/unity_instances.py index 74cb42b9..511e7f91 100644 --- a/Server/src/services/resources/unity_instances.py +++ b/Server/src/services/resources/unity_instances.py @@ -9,6 +9,11 @@ from transport.plugin_hub import PluginHub from transport.unity_transport import _is_http_transport +try: + pass +except: pass + + @mcp_for_unity_resource( uri="unity://instances", diff --git a/Server/src/transport/legacy/port_discovery.py b/Server/src/transport/legacy/port_discovery.py index fe7de5bf..50ec5913 100644 --- a/Server/src/transport/legacy/port_discovery.py +++ b/Server/src/transport/legacy/port_discovery.py @@ -14,6 +14,8 @@ import glob import json import logging +import os +import struct from datetime import datetime from pathlib import Path import socket @@ -27,7 +29,7 @@ class PortDiscovery: """Handles port discovery from Unity Bridge registry""" REGISTRY_FILE = "unity-mcp-port.json" # legacy single-project file DEFAULT_PORT = 6400 - CONNECT_TIMEOUT = 0.3 # seconds, keep this snappy during discovery + CONNECT_TIMEOUT = 1.0 # seconds, keep this snappy during discovery @staticmethod def get_registry_path() -> Path: @@ -100,6 +102,7 @@ def _recv_exact(expected: int) -> bytes | None: response = _recv_exact(response_length) if response is None: return False + return b'"message":"pong"' in response except Exception as e: logger.debug(f"Port probe failed for {port}: {e}") @@ -306,7 +309,7 @@ def discover_all_unity_instances() -> list[UnityInstanceInfo]: deduped_instances = [entry[0] for entry in sorted( instances_by_port.values(), key=lambda item: item[1], reverse=True)] - + logger.info( f"Discovered {len(deduped_instances)} Unity instances (after de-duplication by port)") return deduped_instances diff --git a/Server/uv.lock b/Server/uv.lock index 26152be7..94423e54 100644 --- a/Server/uv.lock +++ b/Server/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" [[package]] @@ -694,7 +694,7 @@ wheels = [ [[package]] name = "mcpforunityserver" -version = "7.0.0" +version = "8.0.1" source = { editable = "." } dependencies = [ { name = "fastapi" },