From 6f3b869f3d1f00a78028275fa44c53bd5b30bb24 Mon Sep 17 00:00:00 2001 From: dsarno Date: Tue, 3 Feb 2026 14:48:44 -0800 Subject: [PATCH] fix: speed up Claude Code config check by reading JSON directly (#682) * fix: speed up Claude Code config check by reading JSON directly Instead of running `claude mcp list` (15+ seconds due to health checks), read the config directly from ~/.claude.json (instant). Changes: - Add ReadClaudeCodeConfig() to parse Claude's JSON config file - Walk up directory tree to find config at parent directories - Handle duplicate path entries (forward/backslash variants) - Add beta/stable version mismatch detection with clear messages - Add IsBetaPackageSource() to detect PyPI beta versions and prerelease ranges - Change button label from "Register" to "Configure" for consistency Co-Authored-By: Claude Opus 4.5 * fix: speed up Claude Code config check by reading JSON directly Instead of running `claude mcp list` (15+ seconds due to health checks), read the config directly from ~/.claude.json (instant). Changes: - Add ReadClaudeCodeConfig() to parse Claude's JSON config file - Walk up directory tree to find config at parent directories - Handle duplicate path entries (forward/backslash variants) - Add beta/stable version mismatch detection with clear messages - Add IsBetaPackageSource() to detect PyPI beta versions and prerelease ranges - Change button label from "Register" to "Configure" for consistency - Refresh client status when switching to Connect tab or toggling beta mode Co-Authored-By: Claude Opus 4.5 * feat: add VersionMismatch status for Claude Code config detection - Add McpStatus.VersionMismatch enum value for version mismatch cases - Show "Version Mismatch" with yellow warning indicator instead of "Error" - Use VersionMismatch for beta/stable package source mismatches - Keep Error status for transport mismatches and general errors Co-Authored-By: Claude Opus 4.5 * feat: add version mismatch warning banner in Server section - Add version-mismatch-warning banner to McpConnectionSection.uxml - Add UpdateVersionMismatchWarning method to show/hide the banner - Fire OnClientConfigMismatch event when VersionMismatch status detected - Wire up event in main window to update the warning banner - Store mismatch details in configStatus for both Error and VersionMismatch Co-Authored-By: Claude Opus 4.5 * fix: simplify version mismatch messages for non-technical users Before: "Beta/stable mismatch: registered with beta 'mcpforunityserver>=0.0.0a0' but plugin is stable 'mcpforunityserver==9.4.0'." After: "Configured for beta server, but 'Use Beta Server' is disabled in Advanced settings." Co-Authored-By: Claude Opus 4.5 * fix: address PR review feedback - Treat missing ~/.claude.json as "not configured" instead of error (distinguishes "no Claude Code installed" from actual read failures) - Handle --from=VALUE format in ExtractPackageSourceFromConfig (in addition to existing --from VALUE format) Co-Authored-By: Claude Opus 4.5 * feat: add beta/stable version mismatch detection to all JSON-based clients - Move GetExpectedPackageSourceForValidation() and IsBetaPackageSource() to base class so all configurators can use them - Update JsonFileMcpConfigurator.CheckStatus() to use beta-aware comparison - Show VersionMismatch status with clear messaging for Claude Desktop, Cursor, Windsurf, VS Code, and other JSON-based clients - Auto-rewrite still attempts to fix mismatches automatically Co-Authored-By: Claude Opus 4.5 * fix: add beta-aware validation to CodexMcpConfigurator CodexMcpConfigurator was still using the non-beta-aware package source comparison. Now uses GetExpectedPackageSourceForValidation() and shows VersionMismatch status with clear messaging like other configurators. Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- .../Configurators/ClaudeCodeConfigurator.cs | 2 +- .../Clients/McpClientConfiguratorBase.cs | 561 +++++++++++++----- MCPForUnity/Editor/Models/McpClient.cs | 5 +- MCPForUnity/Editor/Models/McpStatus.cs | 1 + .../ClientConfig/McpClientConfigSection.cs | 24 +- .../Connection/McpConnectionSection.cs | 33 ++ .../Connection/McpConnectionSection.uxml | 3 + .../Editor/Windows/MCPForUnityEditorWindow.cs | 8 + 8 files changed, 482 insertions(+), 155 deletions(-) diff --git a/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs index c890d7c..d2545b8 100644 --- a/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs @@ -19,7 +19,7 @@ namespace MCPForUnity.Editor.Clients.Configurators public override IList GetInstallationSteps() => new List { "Ensure Claude CLI is installed (comes with Claude Code)", - "Click Register to add UnityMCP via 'claude mcp add'", + "Click Configure to add UnityMCP via 'claude mcp add'", "The server will be automatically available in Claude Code", "Use Unregister to remove via 'claude mcp remove'" }; diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index afa8b25..4265695 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -78,6 +78,74 @@ namespace MCPForUnity.Editor.Clients string Normalize(string value) => value.Trim().TrimEnd('/'); return string.Equals(Normalize(a), Normalize(b), StringComparison.OrdinalIgnoreCase); } + + /// + /// Gets the expected package source for validation, accounting for beta mode. + /// This should match what Configure() would actually use for the --from argument. + /// MUST be called from the main thread due to EditorPrefs access. + /// + protected static string GetExpectedPackageSourceForValidation() + { + // Check for explicit override first + string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, ""); + if (!string.IsNullOrEmpty(gitUrlOverride)) + { + return gitUrlOverride; + } + + // Check beta mode using the same logic as GetUseBetaServerWithDynamicDefault + // (bypass cache to ensure fresh read) + bool useBetaServer; + bool hasPrefKey = EditorPrefs.HasKey(EditorPrefKeys.UseBetaServer); + if (hasPrefKey) + { + useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, false); + } + else + { + // Dynamic default based on package version + useBetaServer = AssetPathUtility.IsPreReleaseVersion(); + } + + if (useBetaServer) + { + return "mcpforunityserver>=0.0.0a0"; + } + + // Standard mode uses exact version from package.json + return AssetPathUtility.GetMcpServerPackageSource(); + } + + /// + /// Checks if a package source string represents a beta/prerelease version. + /// Beta versions include: + /// - PyPI beta: "mcpforunityserver==9.4.0b20250203..." (contains 'b' before timestamp) + /// - PyPI prerelease range: "mcpforunityserver>=0.0.0a0" (used when beta mode is enabled) + /// - Git beta branch: contains "@beta" or "-beta" + /// + protected static bool IsBetaPackageSource(string packageSource) + { + if (string.IsNullOrEmpty(packageSource)) + return false; + + // PyPI beta format: mcpforunityserver==X.Y.Zb + // The 'b' suffix before numbers indicates a PEP 440 beta version + if (System.Text.RegularExpressions.Regex.IsMatch(packageSource, @"==\d+\.\d+\.\d+b\d+")) + return true; + + // PyPI prerelease range: >=0.0.0a0 (used when "Use Beta Server" is enabled in Unity settings) + if (packageSource.Contains(">=0.0.0a0", StringComparison.OrdinalIgnoreCase)) + return true; + + // Git-based beta references + if (packageSource.Contains("@beta", StringComparison.OrdinalIgnoreCase)) + return true; + + if (packageSource.Contains("-beta", StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } } /// JSON-file based configurator (Cursor, Windsurf, VS Code, etc.). @@ -174,12 +242,44 @@ namespace MCPForUnity.Editor.Clients } bool matches = false; + bool hasVersionMismatch = false; + string mismatchReason = null; + if (args != null && args.Length > 0) { - string expectedUvxUrl = AssetPathUtility.GetMcpServerPackageSource(); + // Use beta-aware expected package source for comparison + string expectedUvxUrl = GetExpectedPackageSourceForValidation(); string configuredUvxUrl = McpConfigurationHelper.ExtractUvxUrl(args); - matches = !string.IsNullOrEmpty(configuredUvxUrl) && - McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl); + + if (!string.IsNullOrEmpty(configuredUvxUrl) && !string.IsNullOrEmpty(expectedUvxUrl)) + { + if (McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl)) + { + matches = true; + } + else + { + // Check for beta/stable mismatch + bool configuredIsBeta = IsBetaPackageSource(configuredUvxUrl); + bool expectedIsBeta = IsBetaPackageSource(expectedUvxUrl); + + if (configuredIsBeta && !expectedIsBeta) + { + hasVersionMismatch = true; + mismatchReason = "Configured for beta server, but 'Use Beta Server' is disabled in Advanced settings."; + } + else if (!configuredIsBeta && expectedIsBeta) + { + hasVersionMismatch = true; + mismatchReason = "Configured for stable server, but 'Use Beta Server' is enabled in Advanced settings."; + } + else + { + hasVersionMismatch = true; + mismatchReason = "Server version doesn't match the plugin. Re-configure to update."; + } + } + } } else if (!string.IsNullOrEmpty(configuredUrl)) { @@ -194,7 +294,27 @@ namespace MCPForUnity.Editor.Clients return client.status; } - if (attemptAutoRewrite) + if (hasVersionMismatch) + { + if (attemptAutoRewrite) + { + var result = McpConfigurationHelper.WriteMcpConfiguration(path, client); + if (result == "Configured successfully") + { + client.SetStatus(McpStatus.Configured); + client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport(); + } + else + { + client.SetStatus(McpStatus.VersionMismatch, mismatchReason); + } + } + else + { + client.SetStatus(McpStatus.VersionMismatch, mismatchReason); + } + } + else if (attemptAutoRewrite) { var result = McpConfigurationHelper.WriteMcpConfiguration(path, client); if (result == "Configured successfully") @@ -300,6 +420,9 @@ namespace MCPForUnity.Editor.Clients } bool matches = false; + bool hasVersionMismatch = false; + string mismatchReason = null; + if (!string.IsNullOrEmpty(url)) { // Match against the active scope's URL @@ -307,10 +430,39 @@ namespace MCPForUnity.Editor.Clients } else if (args != null && args.Length > 0) { - string expected = AssetPathUtility.GetMcpServerPackageSource(); + // Use beta-aware expected package source for comparison + string expected = GetExpectedPackageSourceForValidation(); string configured = McpConfigurationHelper.ExtractUvxUrl(args); - matches = !string.IsNullOrEmpty(configured) && - McpConfigurationHelper.PathsEqual(configured, expected); + + if (!string.IsNullOrEmpty(configured) && !string.IsNullOrEmpty(expected)) + { + if (McpConfigurationHelper.PathsEqual(configured, expected)) + { + matches = true; + } + else + { + // Check for beta/stable mismatch + bool configuredIsBeta = IsBetaPackageSource(configured); + bool expectedIsBeta = IsBetaPackageSource(expected); + + if (configuredIsBeta && !expectedIsBeta) + { + hasVersionMismatch = true; + mismatchReason = "Configured for beta server, but 'Use Beta Server' is disabled in Advanced settings."; + } + else if (!configuredIsBeta && expectedIsBeta) + { + hasVersionMismatch = true; + mismatchReason = "Configured for stable server, but 'Use Beta Server' is enabled in Advanced settings."; + } + else + { + hasVersionMismatch = true; + mismatchReason = "Server version doesn't match the plugin. Re-configure to update."; + } + } + } } if (matches) @@ -318,6 +470,22 @@ namespace MCPForUnity.Editor.Clients client.SetStatus(McpStatus.Configured); return client.status; } + + if (hasVersionMismatch) + { + if (attemptAutoRewrite) + { + string result = McpConfigurationHelper.ConfigureCodexClient(path, client); + if (result == "Configured successfully") + { + client.SetStatus(McpStatus.Configured); + client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport(); + return client.status; + } + } + client.SetStatus(McpStatus.VersionMismatch, mismatchReason); + return client.status; + } } else { @@ -394,7 +562,7 @@ namespace MCPForUnity.Editor.Clients public ClaudeCliMcpConfigurator(McpClient client) : base(client) { } public override bool SupportsAutoConfigure => true; - public override string GetConfigureActionLabel() => client.status == McpStatus.Configured ? "Unregister" : "Register"; + public override string GetConfigureActionLabel() => client.status == McpStatus.Configured ? "Unregister" : "Configure"; public override string GetConfigPath() => "Managed via Claude CLI"; @@ -445,131 +613,130 @@ namespace MCPForUnity.Editor.Clients throw new ArgumentNullException(nameof(projectDir), "Project directory must be provided for thread-safe execution"); } - string pathPrepend = null; - if (platform == RuntimePlatform.OSXEditor) + // Read Claude Code config directly from ~/.claude.json instead of using slow CLI + // This is instant vs 15+ seconds for `claude mcp list` which does health checks + var configResult = ReadClaudeCodeConfig(projectDir); + if (configResult.error != null) { - pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; - } - else if (platform == RuntimePlatform.LinuxEditor) - { - pathPrepend = "/usr/local/bin:/usr/bin:/bin"; + client.SetStatus(McpStatus.NotConfigured, configResult.error); + client.configuredTransport = Models.ConfiguredTransport.Unknown; + return client.status; } - try + if (configResult.serverConfig == null) { - string claudeDir = Path.GetDirectoryName(claudePath); - if (!string.IsNullOrEmpty(claudeDir)) + // UnityMCP not found in config + client.SetStatus(McpStatus.NotConfigured); + client.configuredTransport = Models.ConfiguredTransport.Unknown; + return client.status; + } + + // UnityMCP is registered - check transport and version + bool currentUseHttp = useHttpTransport; + var serverConfig = configResult.serverConfig; + + // Determine registered transport type + string registeredType = serverConfig["type"]?.ToString()?.ToLowerInvariant() ?? ""; + bool registeredWithHttp = registeredType == "http"; + bool registeredWithStdio = registeredType == "stdio"; + + // Set the configured transport based on what we detected + if (registeredWithHttp) + { + client.configuredTransport = isRemoteScope + ? Models.ConfiguredTransport.HttpRemote + : Models.ConfiguredTransport.Http; + } + else if (registeredWithStdio) + { + client.configuredTransport = Models.ConfiguredTransport.Stdio; + } + else + { + client.configuredTransport = Models.ConfiguredTransport.Unknown; + } + + // Check for transport mismatch + bool hasTransportMismatch = (currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp); + + // For stdio transport, also check package version + bool hasVersionMismatch = false; + string configuredPackageSource = null; + string mismatchReason = null; + if (registeredWithStdio) + { + configuredPackageSource = ExtractPackageSourceFromConfig(serverConfig); + if (!string.IsNullOrEmpty(configuredPackageSource) && !string.IsNullOrEmpty(expectedPackageSource)) { - pathPrepend = string.IsNullOrEmpty(pathPrepend) - ? claudeDir - : $"{claudeDir}:{pathPrepend}"; - } - } - catch { } - - // Check if UnityMCP exists (handles both "UnityMCP" and legacy "unityMCP") - if (ExecPath.TryRun(claudePath, "mcp list", projectDir, out var listStdout, out var listStderr, 10000, pathPrepend)) - { - if (!string.IsNullOrEmpty(listStdout) && listStdout.IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0) - { - // UnityMCP is registered - now verify transport mode matches - // useHttpTransport parameter is required (non-nullable) to ensure thread safety - bool currentUseHttp = useHttpTransport; - - // Get detailed info about the registration to check transport type - // Try both "UnityMCP" and "unityMCP" (legacy naming) - string getStdout = null, getStderr = null; - bool gotInfo = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out getStdout, out getStderr, 7000, pathPrepend) - || ExecPath.TryRun(claudePath, "mcp get unityMCP", projectDir, out getStdout, out getStderr, 7000, pathPrepend); - if (gotInfo) + // Check for exact match first + if (!string.Equals(configuredPackageSource, expectedPackageSource, StringComparison.OrdinalIgnoreCase)) { - // Parse the output to determine registered transport mode - // The CLI output format contains "Type: http" or "Type: stdio" - bool registeredWithHttp = getStdout.Contains("Type: http", StringComparison.OrdinalIgnoreCase); - bool registeredWithStdio = getStdout.Contains("Type: stdio", StringComparison.OrdinalIgnoreCase); + hasVersionMismatch = true; - // Set the configured transport based on what we detected - // For HTTP, we can't distinguish local/remote from CLI output alone, - // so infer from the current scope setting when HTTP is detected. - if (registeredWithHttp) + // Provide more specific mismatch reason for beta/stable differences + bool configuredIsBeta = IsBetaPackageSource(configuredPackageSource); + bool expectedIsBeta = IsBetaPackageSource(expectedPackageSource); + + if (configuredIsBeta && !expectedIsBeta) { - client.configuredTransport = isRemoteScope - ? Models.ConfiguredTransport.HttpRemote - : Models.ConfiguredTransport.Http; + mismatchReason = "Configured for beta server, but 'Use Beta Server' is disabled in Advanced settings."; } - else if (registeredWithStdio) + else if (!configuredIsBeta && expectedIsBeta) { - client.configuredTransport = Models.ConfiguredTransport.Stdio; + mismatchReason = "Configured for stable server, but 'Use Beta Server' is enabled in Advanced settings."; } else { - client.configuredTransport = Models.ConfiguredTransport.Unknown; - } - - // Check for transport mismatch (3-way: Stdio, Http, HttpRemote) - bool hasTransportMismatch = (currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp); - - // For stdio transport, also check package version - bool hasVersionMismatch = false; - string configuredPackageSource = null; - if (registeredWithStdio) - { - // expectedPackageSource was captured on main thread and passed as parameter - configuredPackageSource = ExtractPackageSourceFromCliOutput(getStdout); - hasVersionMismatch = !string.IsNullOrEmpty(configuredPackageSource) && - !string.Equals(configuredPackageSource, expectedPackageSource, StringComparison.OrdinalIgnoreCase); - } - - // If there's any mismatch and auto-rewrite is enabled, re-register - if (hasTransportMismatch || hasVersionMismatch) - { - // Configure() requires main thread (accesses EditorPrefs, Application.dataPath) - // Only attempt auto-rewrite if we're on the main thread - bool isMainThread = System.Threading.Thread.CurrentThread.ManagedThreadId == 1; - if (attemptAutoRewrite && isMainThread) - { - string reason = hasTransportMismatch - ? $"Transport mismatch (registered: {(registeredWithHttp ? "HTTP" : "stdio")}, expected: {(currentUseHttp ? "HTTP" : "stdio")})" - : $"Package version mismatch (registered: {configuredPackageSource}, expected: {expectedPackageSource})"; - McpLog.Info($"{reason}. Re-registering..."); - try - { - // Force re-register by ensuring status is not Configured (which would toggle to Unregister) - client.SetStatus(McpStatus.IncorrectPath); - Configure(); - return client.status; - } - catch (Exception ex) - { - McpLog.Warn($"Auto-reregister failed: {ex.Message}"); - client.SetStatus(McpStatus.IncorrectPath, $"Configuration mismatch. Click Configure to re-register."); - return client.status; - } - } - else - { - if (hasTransportMismatch) - { - string errorMsg = $"Transport mismatch: Claude Code is registered with {(registeredWithHttp ? "HTTP" : "stdio")} but current setting is {(currentUseHttp ? "HTTP" : "stdio")}. Click Configure to re-register."; - client.SetStatus(McpStatus.Error, errorMsg); - McpLog.Warn(errorMsg); - } - else - { - client.SetStatus(McpStatus.IncorrectPath, $"Package version mismatch: registered with '{configuredPackageSource}' but current version is '{expectedPackageSource}'."); - } - return client.status; - } + mismatchReason = "Server version doesn't match the plugin. Re-configure to update."; } } + } + } - client.SetStatus(McpStatus.Configured); + // If there's any mismatch and auto-rewrite is enabled, re-register + if (hasTransportMismatch || hasVersionMismatch) + { + // Configure() requires main thread (accesses EditorPrefs, Application.dataPath) + // Only attempt auto-rewrite if we're on the main thread + bool isMainThread = System.Threading.Thread.CurrentThread.ManagedThreadId == 1; + if (attemptAutoRewrite && isMainThread) + { + string reason = hasTransportMismatch + ? $"Transport mismatch (registered: {(registeredWithHttp ? "HTTP" : "stdio")}, expected: {(currentUseHttp ? "HTTP" : "stdio")})" + : mismatchReason ?? $"Package version mismatch"; + McpLog.Info($"{reason}. Re-registering..."); + try + { + // Force re-register by ensuring status is not Configured (which would toggle to Unregister) + client.SetStatus(McpStatus.IncorrectPath); + Configure(); + return client.status; + } + catch (Exception ex) + { + McpLog.Warn($"Auto-reregister failed: {ex.Message}"); + client.SetStatus(McpStatus.IncorrectPath, $"Configuration mismatch. Click Configure to re-register."); + return client.status; + } + } + else + { + if (hasTransportMismatch) + { + string errorMsg = $"Transport mismatch: Claude Code is registered with {(registeredWithHttp ? "HTTP" : "stdio")} but current setting is {(currentUseHttp ? "HTTP" : "stdio")}. Click Configure to re-register."; + client.SetStatus(McpStatus.Error, errorMsg); + McpLog.Warn(errorMsg); + } + else + { + client.SetStatus(McpStatus.VersionMismatch, mismatchReason); + } return client.status; } } - client.SetStatus(McpStatus.NotConfigured); - client.configuredTransport = Models.ConfiguredTransport.Unknown; + client.SetStatus(McpStatus.Configured); + return client.status; } catch (Exception ex) { @@ -854,7 +1021,7 @@ namespace MCPForUnity.Editor.Clients public override IList GetInstallationSteps() => new List { "Ensure Claude CLI is installed", - "Use Register to add UnityMCP (or run claude mcp add UnityMCP)", + "Use Configure to add UnityMCP (or run claude mcp add UnityMCP)", "Restart Claude Code" }; @@ -911,43 +1078,6 @@ namespace MCPForUnity.Editor.Clients return sb.ToString(); } - /// - /// Gets the expected package source for validation, accounting for beta mode. - /// This should match what Register() would actually use for the --from argument. - /// MUST be called from the main thread due to EditorPrefs access. - /// - private static string GetExpectedPackageSourceForValidation() - { - // Check for explicit override first - string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, ""); - if (!string.IsNullOrEmpty(gitUrlOverride)) - { - return gitUrlOverride; - } - - // Check beta mode using the same logic as GetUseBetaServerWithDynamicDefault - // (bypass cache to ensure fresh read) - bool useBetaServer; - bool hasPrefKey = EditorPrefs.HasKey(EditorPrefKeys.UseBetaServer); - if (hasPrefKey) - { - useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, false); - } - else - { - // Dynamic default based on package version - useBetaServer = AssetPathUtility.IsPreReleaseVersion(); - } - - if (useBetaServer) - { - return "mcpforunityserver>=0.0.0a0"; - } - - // Standard mode uses exact version from package.json - return AssetPathUtility.GetMcpServerPackageSource(); - } - /// /// Extracts the package source (--from argument value) from claude mcp get output. /// The output format includes args like: --from "mcpforunityserver==9.0.1" @@ -993,5 +1123,134 @@ namespace MCPForUnity.Editor.Clients return null; } + + /// + /// Reads Claude Code configuration directly from ~/.claude.json file. + /// This is much faster than running `claude mcp list` which does health checks on all servers. + /// + private static (JObject serverConfig, string error) ReadClaudeCodeConfig(string projectDir) + { + try + { + // Find the Claude config file + string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string configPath = Path.Combine(homeDir, ".claude.json"); + + if (!File.Exists(configPath)) + { + // Missing config file is "not configured", not an error + // (Claude Code may not be installed or just hasn't been configured yet) + return (null, null); + } + + string configJson = File.ReadAllText(configPath); + var config = JObject.Parse(configJson); + + var projects = config["projects"] as JObject; + if (projects == null) + { + return (null, null); // No projects configured + } + + // Build a dictionary of normalized paths for quick lookup + // Use last entry for duplicates (forward/backslash variants) as it's typically more recent + var normalizedProjects = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var project in projects.Properties()) + { + string normalizedPath = NormalizePath(project.Name); + normalizedProjects[normalizedPath] = project.Value as JObject; + } + + // Walk up the directory tree to find a matching project config + // Claude Code may be configured at a parent directory (e.g., repo root) + // while Unity project is in a subdirectory (e.g., TestProjects/UnityMCPTests) + string currentDir = NormalizePath(projectDir); + while (!string.IsNullOrEmpty(currentDir)) + { + if (normalizedProjects.TryGetValue(currentDir, out var projectConfig)) + { + var mcpServers = projectConfig?["mcpServers"] as JObject; + if (mcpServers != null) + { + // Look for UnityMCP (case-insensitive) + foreach (var server in mcpServers.Properties()) + { + if (string.Equals(server.Name, "UnityMCP", StringComparison.OrdinalIgnoreCase)) + { + return (server.Value as JObject, null); + } + } + } + // Found the project but no UnityMCP - don't continue walking up + return (null, null); + } + + // Move up one directory + int lastSlash = currentDir.LastIndexOf('/'); + if (lastSlash <= 0) + break; + currentDir = currentDir.Substring(0, lastSlash); + } + + return (null, null); // Project not found in config + } + catch (Exception ex) + { + return (null, $"Error reading Claude config: {ex.Message}"); + } + } + + /// + /// Normalizes a file path for comparison (handles forward/back slashes, trailing slashes). + /// + private static string NormalizePath(string path) + { + if (string.IsNullOrEmpty(path)) + return path; + + // Replace backslashes with forward slashes and remove trailing slashes + return path.Replace('\\', '/').TrimEnd('/'); + } + + /// + /// Extracts the package source from Claude Code JSON config. + /// For stdio servers, this is in the args array after "--from". + /// + private static string ExtractPackageSourceFromConfig(JObject serverConfig) + { + if (serverConfig == null) + return null; + + var args = serverConfig["args"] as JArray; + if (args == null) + return null; + + // Look for --from argument (either "--from VALUE" or "--from=VALUE" format) + bool foundFrom = false; + foreach (var arg in args) + { + string argStr = arg?.ToString(); + if (argStr == null) + continue; + + if (foundFrom) + { + // This is the package source following --from + return argStr; + } + + if (argStr == "--from") + { + foundFrom = true; + } + else if (argStr.StartsWith("--from=", StringComparison.OrdinalIgnoreCase)) + { + // Handle --from=VALUE format + return argStr.Substring(7).Trim('"', '\''); + } + } + + return null; + } } } diff --git a/MCPForUnity/Editor/Models/McpClient.cs b/MCPForUnity/Editor/Models/McpClient.cs index 832bb8a..5f2a1c7 100644 --- a/MCPForUnity/Editor/Models/McpClient.cs +++ b/MCPForUnity/Editor/Models/McpClient.cs @@ -35,6 +35,7 @@ namespace MCPForUnity.Editor.Models McpStatus.UnsupportedOS => "Unsupported OS", McpStatus.MissingConfig => "Missing MCPForUnity Config", McpStatus.Error => configStatus?.StartsWith("Error:") == true ? configStatus : "Error", + McpStatus.VersionMismatch => "Version Mismatch", _ => "Unknown", }; } @@ -44,9 +45,9 @@ namespace MCPForUnity.Editor.Models { status = newStatus; - if (newStatus == McpStatus.Error && !string.IsNullOrEmpty(errorDetails)) + if ((newStatus == McpStatus.Error || newStatus == McpStatus.VersionMismatch) && !string.IsNullOrEmpty(errorDetails)) { - configStatus = $"Error: {errorDetails}"; + configStatus = errorDetails; } else { diff --git a/MCPForUnity/Editor/Models/McpStatus.cs b/MCPForUnity/Editor/Models/McpStatus.cs index c23bc81..dfc04fd 100644 --- a/MCPForUnity/Editor/Models/McpStatus.cs +++ b/MCPForUnity/Editor/Models/McpStatus.cs @@ -13,6 +13,7 @@ namespace MCPForUnity.Editor.Models MissingConfig, // Config file exists but missing required elements UnsupportedOS, // OS is not supported Error, // General error state + VersionMismatch, // Configuration version doesn't match expected version } /// diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs index 21d2f69..575650f 100644 --- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs +++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs @@ -53,6 +53,12 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig /// public event Action OnClientTransportDetected; + /// + /// Fired when a config mismatch is detected (e.g., version mismatch). + /// The parameter contains the client name and the mismatch message (null if no mismatch). + /// + public event Action OnClientConfigMismatch; + public VisualElement Root { get; private set; } public McpClientConfigSection(VisualElement root) @@ -167,6 +173,7 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig McpStatus.UnsupportedOS => "Unsupported OS", McpStatus.MissingConfig => "Missing MCPForUnity Config", McpStatus.Error => "Error", + McpStatus.VersionMismatch => "Version Mismatch", _ => "Unknown", }; } @@ -286,7 +293,7 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig statusRefreshInFlight.Add(client); bool isCurrentlyConfigured = client.Status == McpStatus.Configured; - ApplyStatusToUi(client, showChecking: true, customMessage: isCurrentlyConfigured ? "Unregistering..." : "Registering..."); + ApplyStatusToUi(client, showChecking: true, customMessage: isCurrentlyConfigured ? "Unregistering..." : "Configuring..."); // Capture ALL main-thread-only values before async task string projectDir = Path.GetDirectoryName(Application.dataPath); @@ -581,6 +588,8 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig case McpStatus.IncorrectPath: case McpStatus.CommunicationError: case McpStatus.NoResponse: + case McpStatus.Error: + case McpStatus.VersionMismatch: clientStatusIndicator.AddToClassList("warning"); break; default: @@ -594,6 +603,19 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig // Notify listeners about the client's configured transport OnClientTransportDetected?.Invoke(client.DisplayName, client.ConfiguredTransport); + + // Notify listeners about version mismatch if applicable + if (client.Status == McpStatus.VersionMismatch && client is McpClientConfiguratorBase baseConfigurator) + { + // Get the mismatch reason from the configStatus field + string mismatchReason = baseConfigurator.Client.configStatus; + OnClientConfigMismatch?.Invoke(client.DisplayName, mismatchReason); + } + else + { + // Clear any previous mismatch warning + OnClientConfigMismatch?.Invoke(client.DisplayName, null); + } } /// diff --git a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs index 36f5647..7f14781 100644 --- a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs @@ -30,6 +30,8 @@ namespace MCPForUnity.Editor.Windows.Components.Connection private EnumField transportDropdown; private VisualElement transportMismatchWarning; private Label transportMismatchText; + private VisualElement versionMismatchWarning; + private Label versionMismatchText; private VisualElement httpUrlRow; private VisualElement httpServerControlRow; private Foldout manualCommandFoldout; @@ -86,6 +88,8 @@ namespace MCPForUnity.Editor.Windows.Components.Connection transportDropdown = Root.Q("transport-dropdown"); transportMismatchWarning = Root.Q("transport-mismatch-warning"); transportMismatchText = Root.Q