diff --git a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs index a4728901..a2a48b2a 100644 --- a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs +++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs @@ -123,24 +123,61 @@ private static TomlTable TryParseToml(string toml) } } + /// + /// Checks if we should use remote uvx (Asset Store install without embedded server) + /// + private static bool ShouldUseRemoteUvx(string serverSrc) + { + // If no serverSrc provided, use remote + if (string.IsNullOrEmpty(serverSrc)) + return true; + + // If no embedded server exists, use remote + if (!ServerInstaller.HasEmbeddedServer()) + return true; + + return false; + } + /// /// Creates a TomlTable for the unityMCP server configuration /// - /// Path to uv executable - /// Path to server source directory + /// Path to uv executable (can be null for remote uvx) + /// Path to server source directory (can be null for remote uvx) private static TomlTable CreateUnityMcpTable(string uvPath, string serverSrc) { var unityMCP = new TomlTable(); - unityMCP["command"] = new TomlString { Value = uvPath }; + bool useRemote = ShouldUseRemoteUvx(serverSrc); - var argsArray = new TomlArray(); - argsArray.Add(new TomlString { Value = "run" }); - argsArray.Add(new TomlString { Value = "--directory" }); - argsArray.Add(new TomlString { Value = serverSrc }); - argsArray.Add(new TomlString { Value = "server.py" }); - unityMCP["args"] = argsArray; + if (useRemote) + { + // Asset Store install - use remote uvx + string version = AssetPathUtility.GetPackageVersion(); + string remoteUrl = $"git+https://github.com/CoplayDev/unity-mcp@v{version}#subdirectory=MCPForUnity/UnityMcpServer~/src"; + + unityMCP["command"] = new TomlString { Value = "uvx" }; + + var argsArray = new TomlArray(); + argsArray.Add(new TomlString { Value = "--from" }); + argsArray.Add(new TomlString { Value = remoteUrl }); + argsArray.Add(new TomlString { Value = "mcp-for-unity" }); + unityMCP["args"] = argsArray; + } + else + { + // Git/embedded install - use local path + unityMCP["command"] = new TomlString { Value = uvPath }; + + var argsArray = new TomlArray(); + argsArray.Add(new TomlString { Value = "run" }); + argsArray.Add(new TomlString { Value = "--directory" }); + argsArray.Add(new TomlString { Value = serverSrc }); + argsArray.Add(new TomlString { Value = "server.py" }); + unityMCP["args"] = argsArray; + } // Add Windows-specific environment configuration, see: https://github.com/CoplayDev/unity-mcp/issues/315 + // Always include for Windows, even with remote uvx var platformService = MCPServiceLocator.Platform; if (platformService.IsWindows()) { diff --git a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs index 5889e4f6..8126b07c 100644 --- a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs +++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs @@ -40,19 +40,50 @@ public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPa return root; } + /// + /// Checks if we should use remote uvx (Asset Store install without embedded server) + /// + private static bool ShouldUseRemoteUvx(string directory) + { + // If no directory provided, use remote + if (string.IsNullOrEmpty(directory)) + return true; + + // If no embedded server exists, use remote + if (!ServerInstaller.HasEmbeddedServer()) + return true; + + return false; + } + /// /// Centralized builder that applies all caveats consistently. - /// - Sets command/args with provided directory + /// - Sets command/args with provided directory OR remote uvx for Asset Store /// - Ensures env exists /// - Adds type:"stdio" for VSCode /// - Adds disabled:false for Windsurf/Kiro only when missing /// private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode) { - unity["command"] = uvPath; + // Check if we should use remote uvx (Asset Store without embedded server) + bool useRemote = ShouldUseRemoteUvx(directory); - // For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners - string effectiveDir = directory; + if (useRemote) + { + // Asset Store install - use remote uvx + string version = AssetPathUtility.GetPackageVersion(); + string remoteUrl = $"git+https://github.com/CoplayDev/unity-mcp@v{version}#subdirectory=MCPForUnity/UnityMcpServer~/src"; + + unity["command"] = "uvx"; + unity["args"] = JArray.FromObject(new[] { "--from", remoteUrl, "mcp-for-unity" }); + } + else + { + // Git/embedded install - use local path + unity["command"] = uvPath; + + // For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners + string effectiveDir = directory; #if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX bool isCursor = !isVSCode && (client == null || client.mcpType != McpTypes.VSCode); if (isCursor && !string.IsNullOrEmpty(directory)) @@ -92,7 +123,8 @@ private static void PopulateUnityNode(JObject unity, string uvPath, string direc } #endif - unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" }); + unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" }); + } if (isVSCode) { diff --git a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs index 96ad7ec2..98f5ce3e 100644 --- a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs +++ b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs @@ -93,19 +93,27 @@ public static string WriteMcpConfiguration(string pythonDir, string configPath, } catch { } - // 1) Start from existing, only fill gaps (prefer trusted resolver) - string uvPath = ServerInstaller.FindUvPath(); - // Optionally trust existingCommand if it looks like uv/uv.exe - try + // 1) Check if we should use remote uvx (Asset Store without embedded server) + bool useRemote = !ServerInstaller.HasEmbeddedServer() || string.IsNullOrEmpty(pythonDir); + + string uvPath = null; + if (!useRemote) { - var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); - if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand)) + // Git/embedded install - need UV path + uvPath = ServerInstaller.FindUvPath(); + // Optionally trust existingCommand if it looks like uv/uv.exe + try { - uvPath = existingCommand; + var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); + if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand)) + { + uvPath = existingCommand; + } } + catch { } + if (uvPath == null) return "UV package manager not found. Please install UV first."; } - catch { } - if (uvPath == null) return "UV package manager not found. Please install UV first."; + string serverSrc = ResolveServerDirectory(pythonDir, existingArgs); // Ensure containers exist and write back configuration @@ -165,20 +173,28 @@ public static string ConfigureCodexClient(string pythonDir, string configPath, M CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs); } - string uvPath = ServerInstaller.FindUvPath(); - try + // Check if we should use remote uvx (Asset Store without embedded server) + bool useRemote = !ServerInstaller.HasEmbeddedServer() || string.IsNullOrEmpty(pythonDir); + + string uvPath = null; + if (!useRemote) { - var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); - if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand)) + // Git/embedded install - need UV path + uvPath = ServerInstaller.FindUvPath(); + try { - uvPath = existingCommand; + var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant(); + if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand)) + { + uvPath = existingCommand; + } } - } - catch { } + catch { } - if (uvPath == null) - { - return "UV package manager not found. Please install UV first."; + if (uvPath == null) + { + return "UV package manager not found. Please install UV first."; + } } string serverSrc = ResolveServerDirectory(pythonDir, existingArgs); diff --git a/MCPForUnity/Editor/Helpers/ServerInstaller.cs b/MCPForUnity/Editor/Helpers/ServerInstaller.cs index 10666342..f3064b96 100644 --- a/MCPForUnity/Editor/Helpers/ServerInstaller.cs +++ b/MCPForUnity/Editor/Helpers/ServerInstaller.cs @@ -866,103 +866,6 @@ private static bool ValidateUvBinary(string uvPath) return false; } - /// - /// Download and install server from GitHub release (Asset Store workflow) - /// - public static bool DownloadAndInstallServer() - { - string packageVersion = AssetPathUtility.GetPackageVersion(); - if (packageVersion == "unknown") - { - McpLog.Error("Cannot determine package version for download."); - return false; - } - - string downloadUrl = $"https://github.com/CoplayDev/unity-mcp/releases/download/v{packageVersion}/mcp-for-unity-server-v{packageVersion}.zip"; - string tempZip = Path.Combine(Path.GetTempPath(), $"mcp-server-v{packageVersion}.zip"); - string destRoot = Path.Combine(GetSaveLocation(), ServerFolder); - - try - { - EditorUtility.DisplayProgressBar("MCP for Unity", "Downloading server...", 0.3f); - - // Download - using (var client = new WebClient()) - { - client.DownloadFile(downloadUrl, tempZip); - } - - EditorUtility.DisplayProgressBar("MCP for Unity", "Extracting server...", 0.7f); - - // Kill any running UV processes - string destSrc = Path.Combine(destRoot, "src"); - TryKillUvForPath(destSrc); - - // Delete old installation - if (Directory.Exists(destRoot)) - { - if (!DeleteDirectoryWithRetry(destRoot, maxRetries: 5, delayMs: 1000)) - { - McpLog.Warn($"Could not fully delete old server (files may be in use)"); - } - } - - // Extract to temp location first - string tempExtractDir = Path.Combine(Path.GetTempPath(), $"mcp-server-extract-{Guid.NewGuid()}"); - Directory.CreateDirectory(tempExtractDir); - - try - { - ZipFile.ExtractToDirectory(tempZip, tempExtractDir); - - // The ZIP contains UnityMcpServer~ folder, find it and move its contents - string extractedServerFolder = Path.Combine(tempExtractDir, "UnityMcpServer~"); - Directory.CreateDirectory(destRoot); - CopyDirectoryRecursive(extractedServerFolder, destRoot); - } - finally - { - // Cleanup temp extraction directory - try - { - if (Directory.Exists(tempExtractDir)) - { - Directory.Delete(tempExtractDir, recursive: true); - } - } - catch (Exception ex) - { - McpLog.Warn($"Could not fully delete temp extraction directory: {ex.Message}"); - } - } - - EditorUtility.ClearProgressBar(); - McpLog.Info($"Server v{packageVersion} downloaded and installed successfully!"); - return true; - } - catch (Exception ex) - { - EditorUtility.ClearProgressBar(); - McpLog.Error($"Failed to download server: {ex.Message}"); - EditorUtility.DisplayDialog( - "Download Failed", - $"Could not download server from GitHub.\n\n{ex.Message}\n\nPlease check your internet connection or try again later.", - "OK" - ); - return false; - } - finally - { - try - { - if (File.Exists(tempZip)) File.Delete(tempZip); - } - catch (Exception ex) - { - McpLog.Warn($"Could not delete temp zip file: {ex.Message}"); - } - } - } /// /// Check if the package has an embedded server (Git install vs Asset Store) diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs b/MCPForUnity/Editor/Services/ClientConfigurationService.cs index 8a9c4caf..7fd3c263 100644 --- a/MCPForUnity/Editor/Services/ClientConfigurationService.cs +++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs @@ -27,7 +27,9 @@ public void ConfigureClient(McpClient client) string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath(); - if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py"))) + // For Asset Store installs without embedded server, pythonDir can be null (will use remote uvx) + bool useRemote = !ServerInstaller.HasEmbeddedServer(); + if (!useRemote && (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py")))) { throw new InvalidOperationException("Server not found. Please use manual configuration or set server path in Advanced Settings."); } @@ -235,19 +237,35 @@ public void RegisterClaudeCode() var pathService = MCPServiceLocator.Paths; string pythonDir = pathService.GetMcpServerPath(); - if (string.IsNullOrEmpty(pythonDir)) - { - throw new InvalidOperationException("Cannot register: Python directory not found"); - } - string claudePath = pathService.GetClaudeCliPath(); if (string.IsNullOrEmpty(claudePath)) { throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); } - string uvPath = pathService.GetUvPath() ?? "uv"; - string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; + // Check if we should use remote uvx (Asset Store without embedded server) + bool useRemote = !ServerInstaller.HasEmbeddedServer(); + string args; + + if (useRemote) + { + // Asset Store install - use remote uvx + string version = AssetPathUtility.GetPackageVersion(); + string remoteUrl = $"git+https://github.com/CoplayDev/unity-mcp@v{version}#subdirectory=MCPForUnity/UnityMcpServer~/src"; + args = $"mcp add UnityMCP -- uvx --from {remoteUrl} mcp-for-unity"; + } + else + { + // Git/embedded install - use local path + if (string.IsNullOrEmpty(pythonDir)) + { + throw new InvalidOperationException("Cannot register: Python directory not found"); + } + + string uvPath = pathService.GetUvPath() ?? "uv"; + args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; + } + string projectDir = Path.GetDirectoryName(Application.dataPath); string pathPrepend = null; @@ -368,17 +386,28 @@ public string GenerateConfigJson(McpClient client) { string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath(); string uvPath = MCPServiceLocator.Paths.GetUvPath(); + bool useRemote = !ServerInstaller.HasEmbeddedServer(); // Claude Code uses CLI commands, not JSON config if (client.mcpType == McpTypes.ClaudeCode) { - if (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath)) + string registerCommand; + if (useRemote) { - return "# Error: Configuration not available - check paths in Advanced Settings"; + // Asset Store install - use remote uvx + string version = AssetPathUtility.GetPackageVersion(); + string remoteUrl = $"git+https://github.com/CoplayDev/unity-mcp@v{version}#subdirectory=MCPForUnity/UnityMcpServer~/src"; + registerCommand = $"claude mcp add UnityMCP -- uvx --from {remoteUrl} mcp-for-unity"; + } + else + { + // Git/embedded install - use local path + if (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath)) + { + return "# Error: Configuration not available - check paths in Advanced Settings"; + } + registerCommand = $"claude mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; } - - // Show the actual command that RegisterClaudeCode() uses - string registerCommand = $"claude mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py"; return "# Register the MCP server with Claude Code:\n" + $"{registerCommand}\n\n" + @@ -388,7 +417,8 @@ public string GenerateConfigJson(McpClient client) "claude mcp list # Only works when claude is run in the project's directory"; } - if (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath)) + // For other clients, check if we need paths + if (!useRemote && (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath))) return "{ \"error\": \"Configuration not available - check paths in Advanced Settings\" }"; try diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs index b8fb3c3a..3702d54b 100644 --- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs @@ -50,7 +50,6 @@ private enum ConnectionProtocol private Button testConnectionButton; private VisualElement serverStatusBanner; private Label serverStatusMessage; - private Button downloadServerButton; private Button rebuildServerButton; // Client UI Elements @@ -218,7 +217,6 @@ private void CacheUIElements() testConnectionButton = rootVisualElement.Q