diff --git a/UnityMcpBridge/Editor/Data/McpClients.cs b/UnityMcpBridge/Editor/Data/McpClients.cs index 362ecdcc..9d9cbae5 100644 --- a/UnityMcpBridge/Editor/Data/McpClients.cs +++ b/UnityMcpBridge/Editor/Data/McpClients.cs @@ -87,7 +87,7 @@ public class McpClients Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", - "settings.json" + "mcp.json" ), linuxConfigPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), @@ -95,7 +95,7 @@ public class McpClients "Application Support", "Code", "User", - "settings.json" + "mcp.json" ), mcpType = McpTypes.VSCode, configStatus = "Not Configured", diff --git a/UnityMcpBridge/Editor/Helpers/ExecPath.cs b/UnityMcpBridge/Editor/Helpers/ExecPath.cs index 537962e6..ab55fd6a 100644 --- a/UnityMcpBridge/Editor/Helpers/ExecPath.cs +++ b/UnityMcpBridge/Editor/Helpers/ExecPath.cs @@ -35,6 +35,9 @@ internal static string ResolveClaude() Path.Combine(home, ".local", "bin", "claude"), }; foreach (string c in candidates) { if (File.Exists(c)) return c; } + // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude + string nvmClaude = ResolveClaudeFromNvm(home); + if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude; #if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"); #else @@ -70,6 +73,9 @@ internal static string ResolveClaude() Path.Combine(home, ".local", "bin", "claude"), }; foreach (string c in candidates) { if (File.Exists(c)) return c; } + // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude + string nvmClaude = ResolveClaudeFromNvm(home); + if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude; #if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX return Which("claude", "/usr/local/bin:/usr/bin:/bin"); #else @@ -78,6 +84,75 @@ internal static string ResolveClaude() } } + // Attempt to resolve claude from NVM-managed Node installations, choosing the newest version + private static string ResolveClaudeFromNvm(string home) + { + try + { + if (string.IsNullOrEmpty(home)) return null; + string nvmNodeDir = Path.Combine(home, ".nvm", "versions", "node"); + if (!Directory.Exists(nvmNodeDir)) return null; + + string bestPath = null; + Version bestVersion = null; + foreach (string versionDir in Directory.EnumerateDirectories(nvmNodeDir)) + { + string name = Path.GetFileName(versionDir); + if (string.IsNullOrEmpty(name)) continue; + if (name.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + { + // Extract numeric portion: e.g., v18.19.0-nightly -> 18.19.0 + string versionStr = name.Substring(1); + int dashIndex = versionStr.IndexOf('-'); + if (dashIndex > 0) + { + versionStr = versionStr.Substring(0, dashIndex); + } + if (Version.TryParse(versionStr, out Version parsed)) + { + string candidate = Path.Combine(versionDir, "bin", "claude"); + if (File.Exists(candidate)) + { + if (bestVersion == null || parsed > bestVersion) + { + bestVersion = parsed; + bestPath = candidate; + } + } + } + } + } + return bestPath; + } + catch { return null; } + } + + // Explicitly set the Claude CLI absolute path override in EditorPrefs + internal static void SetClaudeCliPath(string absolutePath) + { + try + { + if (!string.IsNullOrEmpty(absolutePath) && File.Exists(absolutePath)) + { + EditorPrefs.SetString(PrefClaude, absolutePath); + } + } + catch { } + } + + // Clear any previously set Claude CLI override path + internal static void ClearClaudeCliPath() + { + try + { + if (EditorPrefs.HasKey(PrefClaude)) + { + EditorPrefs.DeleteKey(PrefClaude); + } + } + catch { } + } + // Use existing UV resolver; returns absolute path or null. internal static string ResolveUv() { diff --git a/UnityMcpBridge/Editor/Models/MCPConfigServer.cs b/UnityMcpBridge/Editor/Models/MCPConfigServer.cs index 87d953d8..2c2596ff 100644 --- a/UnityMcpBridge/Editor/Models/MCPConfigServer.cs +++ b/UnityMcpBridge/Editor/Models/MCPConfigServer.cs @@ -11,5 +11,9 @@ public class McpConfigServer [JsonProperty("args")] public string[] args; + + // VSCode expects a transport type; include only when explicitly set + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string type; } } diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index b212719d..e5354bad 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -438,14 +438,7 @@ private void DrawUnifiedClientConfiguration() EditorGUILayout.LabelField("MCP Client Configuration", sectionTitleStyle); EditorGUILayout.Space(10); - // Auto-connect toggle (moved from Server Status) - bool newAuto = EditorGUILayout.ToggleLeft("Auto-connect to MCP Clients", autoRegisterEnabled); - if (newAuto != autoRegisterEnabled) - { - autoRegisterEnabled = newAuto; - EditorPrefs.SetBool("UnityMCP.AutoRegisterEnabled", autoRegisterEnabled); - } - EditorGUILayout.Space(6); + // (Auto-connect toggle removed per design) // Client selector string[] clientNames = mcpClients.clients.Select(c => c.name).ToArray(); @@ -697,6 +690,31 @@ private static bool VerifyBridgePing(int port) private void DrawClientConfigurationCompact(McpClient mcpClient) { + // Special pre-check for Claude Code: if CLI missing, reflect in status UI + if (mcpClient.mcpType == McpTypes.ClaudeCode) + { + string claudeCheck = ExecPath.ResolveClaude(); + if (string.IsNullOrEmpty(claudeCheck)) + { + mcpClient.configStatus = "Claude Not Found"; + mcpClient.status = McpStatus.NotConfigured; + } + } + + // Pre-check for clients that require uv (all except Claude Code) + bool uvRequired = mcpClient.mcpType != McpTypes.ClaudeCode; + bool uvMissingEarly = false; + if (uvRequired) + { + string uvPathEarly = FindUvPath(); + if (string.IsNullOrEmpty(uvPathEarly)) + { + uvMissingEarly = true; + mcpClient.configStatus = "uv Not Found"; + mcpClient.status = McpStatus.NotConfigured; + } + } + // Status display EditorGUILayout.BeginHorizontal(); Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); @@ -710,8 +728,64 @@ private void DrawClientConfigurationCompact(McpClient mcpClient) }; EditorGUILayout.LabelField(mcpClient.configStatus, clientStatusStyle, GUILayout.Height(28)); EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(10); + // When Claude CLI is missing, show a clear install hint directly below status + if (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) + { + GUIStyle installHintStyle = new GUIStyle(clientStatusStyle); + installHintStyle.normal.textColor = new Color(1f, 0.5f, 0f); // orange + EditorGUILayout.BeginHorizontal(); + GUIContent installText = new GUIContent("Make sure Claude Code is installed!"); + Vector2 textSize = installHintStyle.CalcSize(installText); + EditorGUILayout.LabelField(installText, installHintStyle, GUILayout.Height(22), GUILayout.Width(textSize.x + 2), GUILayout.ExpandWidth(false)); + GUIStyle helpLinkStyle = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; + GUILayout.Space(6); + if (GUILayout.Button("[HELP]", helpLinkStyle, GUILayout.Height(22), GUILayout.ExpandWidth(false))) + { + Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Claude-Code"); + } + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.Space(10); + + // If uv is missing for required clients, show hint and picker then exit early to avoid showing other controls + if (uvRequired && uvMissingEarly) + { + GUIStyle installHintStyle2 = new GUIStyle(EditorStyles.label) + { + fontSize = 12, + fontStyle = FontStyle.Bold, + wordWrap = false + }; + installHintStyle2.normal.textColor = new Color(1f, 0.5f, 0f); + EditorGUILayout.BeginHorizontal(); + GUIContent installText2 = new GUIContent("Make sure uv is installed!"); + Vector2 sz = installHintStyle2.CalcSize(installText2); + EditorGUILayout.LabelField(installText2, installHintStyle2, GUILayout.Height(22), GUILayout.Width(sz.x + 2), GUILayout.ExpandWidth(false)); + GUIStyle helpLinkStyle2 = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; + GUILayout.Space(6); + if (GUILayout.Button("[HELP]", helpLinkStyle2, GUILayout.Height(22), GUILayout.ExpandWidth(false))) + { + Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Cursor,-VSCode-&-Windsurf"); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(8); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Choose UV Install Location", GUILayout.Width(260), GUILayout.Height(22))) + { + string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string picked = EditorUtility.OpenFilePanel("Select 'uv' binary", suggested, ""); + if (!string.IsNullOrEmpty(picked)) + { + EditorPrefs.SetString("UnityMCP.UvPath", picked); + ConfigureMcpClient(mcpClient); + Repaint(); + } + } + EditorGUILayout.EndHorizontal(); + return; + } // Action buttons in horizontal layout EditorGUILayout.BeginHorizontal(); @@ -723,23 +797,57 @@ private void DrawClientConfigurationCompact(McpClient mcpClient) ConfigureMcpClient(mcpClient); } } - else if (mcpClient.mcpType == McpTypes.ClaudeCode) - { - bool isConfigured = mcpClient.status == McpStatus.Configured; - string buttonText = isConfigured ? "Unregister UnityMCP with Claude Code" : "Register with Claude Code"; - if (GUILayout.Button(buttonText, GUILayout.Height(32))) - { - if (isConfigured) - { - UnregisterWithClaudeCode(); - } - else - { - string pythonDir = FindPackagePythonDirectory(); - RegisterWithClaudeCode(pythonDir); - } - } - } + else if (mcpClient.mcpType == McpTypes.ClaudeCode) + { + bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); + if (claudeAvailable) + { + bool isConfigured = mcpClient.status == McpStatus.Configured; + string buttonText = isConfigured ? "Unregister UnityMCP with Claude Code" : "Register with Claude Code"; + if (GUILayout.Button(buttonText, GUILayout.Height(32))) + { + if (isConfigured) + { + UnregisterWithClaudeCode(); + } + else + { + string pythonDir = FindPackagePythonDirectory(); + RegisterWithClaudeCode(pythonDir); + } + } + // Hide the picker once a valid binary is available + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + GUIStyle pathLabelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true }; + string resolvedClaude = ExecPath.ResolveClaude(); + EditorGUILayout.LabelField($"Claude CLI: {resolvedClaude}", pathLabelStyle); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + } + // CLI picker row (only when not found) + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + if (!claudeAvailable) + { + // Only show the picker button in not-found state (no redundant "not found" label) + if (GUILayout.Button("Choose Claude Install Location", GUILayout.Width(260), GUILayout.Height(22))) + { + string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string picked = EditorUtility.OpenFilePanel("Select 'claude' CLI", suggested, ""); + if (!string.IsNullOrEmpty(picked)) + { + ExecPath.SetClaudeCliPath(picked); + // Auto-register after setting a valid path + string pythonDir = FindPackagePythonDirectory(); + RegisterWithClaudeCode(pythonDir); + Repaint(); + } + } + } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + } else { if (GUILayout.Button($"Auto Configure", GUILayout.Height(32))) @@ -793,13 +901,19 @@ private void DrawClientConfigurationCompact(McpClient mcpClient) EditorGUILayout.EndHorizontal(); - EditorGUILayout.Space(8); - // Quick info - GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel) - { - fontSize = 10 - }; - EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle); + EditorGUILayout.Space(8); + // Quick info (hide when Claude is not found to avoid confusion) + bool hideConfigInfo = + (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) + || ((mcpClient.mcpType != McpTypes.ClaudeCode) && string.IsNullOrEmpty(FindUvPath())); + if (!hideConfigInfo) + { + GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel) + { + fontSize = 10 + }; + EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle); + } } private void ToggleUnityBridge() @@ -831,6 +945,10 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC command = uvPath, args = new[] { "--directory", pythonDir, "run", "server.py" }, }; + if (mcpClient?.mcpType == McpTypes.VSCode) + { + unityMCPConfig.type = "stdio"; + } JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; @@ -849,29 +967,41 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC } // Parse the existing JSON while preserving all properties - dynamic existingConfig = JsonConvert.DeserializeObject(existingJson); - existingConfig ??= new Newtonsoft.Json.Linq.JObject(); + dynamic existingConfig; + try + { + if (string.IsNullOrWhiteSpace(existingJson)) + { + existingConfig = new Newtonsoft.Json.Linq.JObject(); + } + else + { + existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new Newtonsoft.Json.Linq.JObject(); + } + } + catch + { + // If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object + if (!string.IsNullOrWhiteSpace(existingJson)) + { + UnityEngine.Debug.LogWarning("UnityMCP: VSCode mcp.json could not be parsed; rewriting servers block."); + } + existingConfig = new Newtonsoft.Json.Linq.JObject(); + } // Handle different client types with a switch statement //Comments: Interestingly, VSCode has mcp.servers.unityMCP while others have mcpServers.unityMCP, which is why we need to prevent this switch (mcpClient?.mcpType) { case McpTypes.VSCode: - // VSCode specific configuration - // Ensure mcp object exists - if (existingConfig.mcp == null) - { - existingConfig.mcp = new Newtonsoft.Json.Linq.JObject(); - } - - // Ensure mcp.servers object exists - if (existingConfig.mcp.servers == null) + // VSCode-specific configuration (top-level "servers") + if (existingConfig.servers == null) { - existingConfig.mcp.servers = new Newtonsoft.Json.Linq.JObject(); + existingConfig.servers = new Newtonsoft.Json.Linq.JObject(); } - // Add/update UnityMCP server in VSCode settings - existingConfig.mcp.servers.unityMCP = + // Add/update UnityMCP server in VSCode mcp.json + existingConfig.servers.unityMCP = JsonConvert.DeserializeObject( JsonConvert.SerializeObject(unityMCPConfig) ); @@ -923,24 +1053,29 @@ private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient // Use switch statement to handle different client types switch (mcpClient.mcpType) { - case McpTypes.VSCode: - // Create VSCode-specific configuration with proper format - var vscodeConfig = new - { - mcp = new - { - servers = new - { - unityMCP = new - { - command = "uv", - args = new[] { "--directory", pythonDir, "run", "server.py" } - } - } - } - }; - manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); - break; + case McpTypes.VSCode: + // Resolve uv so VSCode launches the correct executable even if not on PATH + string uvPathManual = FindUvPath(); + if (uvPathManual == null) + { + UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration."); + return; + } + // Create VSCode-specific configuration with proper format + var vscodeConfig = new + { + servers = new + { + unityMCP = new + { + command = uvPathManual, + args = new[] { "--directory", pythonDir, "run", "server.py" }, + type = "stdio" + } + } + }; + manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); + break; default: // Create standard MCP configuration for other clients @@ -1244,9 +1379,15 @@ private void CheckMcpConfiguration(McpClient mcpClient) case McpTypes.VSCode: dynamic config = JsonConvert.DeserializeObject(configJson); - if (config?.mcp?.servers?.unityMCP != null) + // New schema: top-level servers + if (config?.servers?.unityMCP != null) + { + args = config.servers.unityMCP.args.ToObject(); + configExists = true; + } + // Back-compat: legacy mcp.servers + else if (config?.mcp?.servers?.unityMCP != null) { - // Extract args from VSCode config format args = config.mcp.servers.unityMCP.args.ToObject(); configExists = true; } diff --git a/UnityMcpBridge/package.json b/UnityMcpBridge/package.json index 1091f69f..ba4add49 100644 --- a/UnityMcpBridge/package.json +++ b/UnityMcpBridge/package.json @@ -1,6 +1,6 @@ { "name": "com.coplaydev.unity-mcp", - "version": "2.0.1", + "version": "2.0.2", "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",