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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
beta
dsarno 2026-02-03 14:48:44 -08:00 committed by GitHub
parent a8e478a42a
commit 6f3b869f3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 482 additions and 155 deletions

View File

@ -19,7 +19,7 @@ namespace MCPForUnity.Editor.Clients.Configurators
public override IList<string> GetInstallationSteps() => new List<string> public override IList<string> GetInstallationSteps() => new List<string>
{ {
"Ensure Claude CLI is installed (comes with Claude Code)", "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", "The server will be automatically available in Claude Code",
"Use Unregister to remove via 'claude mcp remove'" "Use Unregister to remove via 'claude mcp remove'"
}; };

View File

@ -78,6 +78,74 @@ namespace MCPForUnity.Editor.Clients
string Normalize(string value) => value.Trim().TrimEnd('/'); string Normalize(string value) => value.Trim().TrimEnd('/');
return string.Equals(Normalize(a), Normalize(b), StringComparison.OrdinalIgnoreCase); return string.Equals(Normalize(a), Normalize(b), StringComparison.OrdinalIgnoreCase);
} }
/// <summary>
/// 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.
/// </summary>
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();
}
/// <summary>
/// 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"
/// </summary>
protected static bool IsBetaPackageSource(string packageSource)
{
if (string.IsNullOrEmpty(packageSource))
return false;
// PyPI beta format: mcpforunityserver==X.Y.Zb<timestamp>
// 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;
}
} }
/// <summary>JSON-file based configurator (Cursor, Windsurf, VS Code, etc.).</summary> /// <summary>JSON-file based configurator (Cursor, Windsurf, VS Code, etc.).</summary>
@ -174,12 +242,44 @@ namespace MCPForUnity.Editor.Clients
} }
bool matches = false; bool matches = false;
bool hasVersionMismatch = false;
string mismatchReason = null;
if (args != null && args.Length > 0) 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); 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)) else if (!string.IsNullOrEmpty(configuredUrl))
{ {
@ -194,7 +294,27 @@ namespace MCPForUnity.Editor.Clients
return client.status; 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); var result = McpConfigurationHelper.WriteMcpConfiguration(path, client);
if (result == "Configured successfully") if (result == "Configured successfully")
@ -300,6 +420,9 @@ namespace MCPForUnity.Editor.Clients
} }
bool matches = false; bool matches = false;
bool hasVersionMismatch = false;
string mismatchReason = null;
if (!string.IsNullOrEmpty(url)) if (!string.IsNullOrEmpty(url))
{ {
// Match against the active scope's URL // Match against the active scope's URL
@ -307,10 +430,39 @@ namespace MCPForUnity.Editor.Clients
} }
else if (args != null && args.Length > 0) 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); 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) if (matches)
@ -318,6 +470,22 @@ namespace MCPForUnity.Editor.Clients
client.SetStatus(McpStatus.Configured); client.SetStatus(McpStatus.Configured);
return client.status; 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 else
{ {
@ -394,7 +562,7 @@ namespace MCPForUnity.Editor.Clients
public ClaudeCliMcpConfigurator(McpClient client) : base(client) { } public ClaudeCliMcpConfigurator(McpClient client) : base(client) { }
public override bool SupportsAutoConfigure => true; 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"; 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"); throw new ArgumentNullException(nameof(projectDir), "Project directory must be provided for thread-safe execution");
} }
string pathPrepend = null; // Read Claude Code config directly from ~/.claude.json instead of using slow CLI
if (platform == RuntimePlatform.OSXEditor) // 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"; client.SetStatus(McpStatus.NotConfigured, configResult.error);
} client.configuredTransport = Models.ConfiguredTransport.Unknown;
else if (platform == RuntimePlatform.LinuxEditor) return client.status;
{
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
} }
try if (configResult.serverConfig == null)
{ {
string claudeDir = Path.GetDirectoryName(claudePath); // UnityMCP not found in config
if (!string.IsNullOrEmpty(claudeDir)) 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) // Check for exact match first
? claudeDir if (!string.Equals(configuredPackageSource, expectedPackageSource, StringComparison.OrdinalIgnoreCase))
: $"{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)
{ {
// Parse the output to determine registered transport mode hasVersionMismatch = true;
// 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);
// Set the configured transport based on what we detected // Provide more specific mismatch reason for beta/stable differences
// For HTTP, we can't distinguish local/remote from CLI output alone, bool configuredIsBeta = IsBetaPackageSource(configuredPackageSource);
// so infer from the current scope setting when HTTP is detected. bool expectedIsBeta = IsBetaPackageSource(expectedPackageSource);
if (registeredWithHttp)
if (configuredIsBeta && !expectedIsBeta)
{ {
client.configuredTransport = isRemoteScope mismatchReason = "Configured for beta server, but 'Use Beta Server' is disabled in Advanced settings.";
? Models.ConfiguredTransport.HttpRemote
: Models.ConfiguredTransport.Http;
} }
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 else
{ {
client.configuredTransport = Models.ConfiguredTransport.Unknown; mismatchReason = "Server version doesn't match the plugin. Re-configure to update.";
}
// 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;
}
} }
} }
}
}
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; return client.status;
} }
} }
client.SetStatus(McpStatus.NotConfigured); client.SetStatus(McpStatus.Configured);
client.configuredTransport = Models.ConfiguredTransport.Unknown; return client.status;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -854,7 +1021,7 @@ namespace MCPForUnity.Editor.Clients
public override IList<string> GetInstallationSteps() => new List<string> public override IList<string> GetInstallationSteps() => new List<string>
{ {
"Ensure Claude CLI is installed", "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" "Restart Claude Code"
}; };
@ -911,43 +1078,6 @@ namespace MCPForUnity.Editor.Clients
return sb.ToString(); return sb.ToString();
} }
/// <summary>
/// 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.
/// </summary>
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();
}
/// <summary> /// <summary>
/// Extracts the package source (--from argument value) from claude mcp get output. /// Extracts the package source (--from argument value) from claude mcp get output.
/// The output format includes args like: --from "mcpforunityserver==9.0.1" /// The output format includes args like: --from "mcpforunityserver==9.0.1"
@ -993,5 +1123,134 @@ namespace MCPForUnity.Editor.Clients
return null; return null;
} }
/// <summary>
/// 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.
/// </summary>
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<string, JObject>(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}");
}
}
/// <summary>
/// Normalizes a file path for comparison (handles forward/back slashes, trailing slashes).
/// </summary>
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('/');
}
/// <summary>
/// Extracts the package source from Claude Code JSON config.
/// For stdio servers, this is in the args array after "--from".
/// </summary>
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;
}
} }
} }

View File

@ -35,6 +35,7 @@ namespace MCPForUnity.Editor.Models
McpStatus.UnsupportedOS => "Unsupported OS", McpStatus.UnsupportedOS => "Unsupported OS",
McpStatus.MissingConfig => "Missing MCPForUnity Config", McpStatus.MissingConfig => "Missing MCPForUnity Config",
McpStatus.Error => configStatus?.StartsWith("Error:") == true ? configStatus : "Error", McpStatus.Error => configStatus?.StartsWith("Error:") == true ? configStatus : "Error",
McpStatus.VersionMismatch => "Version Mismatch",
_ => "Unknown", _ => "Unknown",
}; };
} }
@ -44,9 +45,9 @@ namespace MCPForUnity.Editor.Models
{ {
status = newStatus; 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 else
{ {

View File

@ -13,6 +13,7 @@ namespace MCPForUnity.Editor.Models
MissingConfig, // Config file exists but missing required elements MissingConfig, // Config file exists but missing required elements
UnsupportedOS, // OS is not supported UnsupportedOS, // OS is not supported
Error, // General error state Error, // General error state
VersionMismatch, // Configuration version doesn't match expected version
} }
/// <summary> /// <summary>

View File

@ -53,6 +53,12 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
/// </summary> /// </summary>
public event Action<string, ConfiguredTransport> OnClientTransportDetected; public event Action<string, ConfiguredTransport> OnClientTransportDetected;
/// <summary>
/// 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).
/// </summary>
public event Action<string, string> OnClientConfigMismatch;
public VisualElement Root { get; private set; } public VisualElement Root { get; private set; }
public McpClientConfigSection(VisualElement root) public McpClientConfigSection(VisualElement root)
@ -167,6 +173,7 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
McpStatus.UnsupportedOS => "Unsupported OS", McpStatus.UnsupportedOS => "Unsupported OS",
McpStatus.MissingConfig => "Missing MCPForUnity Config", McpStatus.MissingConfig => "Missing MCPForUnity Config",
McpStatus.Error => "Error", McpStatus.Error => "Error",
McpStatus.VersionMismatch => "Version Mismatch",
_ => "Unknown", _ => "Unknown",
}; };
} }
@ -286,7 +293,7 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
statusRefreshInFlight.Add(client); statusRefreshInFlight.Add(client);
bool isCurrentlyConfigured = client.Status == McpStatus.Configured; 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 // Capture ALL main-thread-only values before async task
string projectDir = Path.GetDirectoryName(Application.dataPath); string projectDir = Path.GetDirectoryName(Application.dataPath);
@ -581,6 +588,8 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
case McpStatus.IncorrectPath: case McpStatus.IncorrectPath:
case McpStatus.CommunicationError: case McpStatus.CommunicationError:
case McpStatus.NoResponse: case McpStatus.NoResponse:
case McpStatus.Error:
case McpStatus.VersionMismatch:
clientStatusIndicator.AddToClassList("warning"); clientStatusIndicator.AddToClassList("warning");
break; break;
default: default:
@ -594,6 +603,19 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
// Notify listeners about the client's configured transport // Notify listeners about the client's configured transport
OnClientTransportDetected?.Invoke(client.DisplayName, client.ConfiguredTransport); 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);
}
} }
/// <summary> /// <summary>

View File

@ -30,6 +30,8 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
private EnumField transportDropdown; private EnumField transportDropdown;
private VisualElement transportMismatchWarning; private VisualElement transportMismatchWarning;
private Label transportMismatchText; private Label transportMismatchText;
private VisualElement versionMismatchWarning;
private Label versionMismatchText;
private VisualElement httpUrlRow; private VisualElement httpUrlRow;
private VisualElement httpServerControlRow; private VisualElement httpServerControlRow;
private Foldout manualCommandFoldout; private Foldout manualCommandFoldout;
@ -86,6 +88,8 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
transportDropdown = Root.Q<EnumField>("transport-dropdown"); transportDropdown = Root.Q<EnumField>("transport-dropdown");
transportMismatchWarning = Root.Q<VisualElement>("transport-mismatch-warning"); transportMismatchWarning = Root.Q<VisualElement>("transport-mismatch-warning");
transportMismatchText = Root.Q<Label>("transport-mismatch-text"); transportMismatchText = Root.Q<Label>("transport-mismatch-text");
versionMismatchWarning = Root.Q<VisualElement>("version-mismatch-warning");
versionMismatchText = Root.Q<Label>("version-mismatch-text");
httpUrlRow = Root.Q<VisualElement>("http-url-row"); httpUrlRow = Root.Q<VisualElement>("http-url-row");
httpServerControlRow = Root.Q<VisualElement>("http-server-control-row"); httpServerControlRow = Root.Q<VisualElement>("http-server-control-row");
manualCommandFoldout = Root.Q<Foldout>("manual-command-foldout"); manualCommandFoldout = Root.Q<Foldout>("manual-command-foldout");
@ -1023,6 +1027,35 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
transportMismatchWarning?.RemoveFromClassList("visible"); transportMismatchWarning?.RemoveFromClassList("visible");
} }
/// <summary>
/// Updates the version mismatch warning banner based on the client's configuration status.
/// Shows a warning if the client is registered with a different package version than expected.
/// </summary>
/// <param name="clientName">The display name of the client being checked.</param>
/// <param name="mismatchMessage">The mismatch message, or null if no mismatch.</param>
public void UpdateVersionMismatchWarning(string clientName, string mismatchMessage)
{
if (versionMismatchWarning == null || versionMismatchText == null)
return;
if (string.IsNullOrEmpty(mismatchMessage))
{
versionMismatchWarning.RemoveFromClassList("visible");
return;
}
versionMismatchText.text = $"⚠ {clientName}: {mismatchMessage}";
versionMismatchWarning.AddToClassList("visible");
}
/// <summary>
/// Clears the version mismatch warning banner.
/// </summary>
public void ClearVersionMismatchWarning()
{
versionMismatchWarning?.RemoveFromClassList("visible");
}
private static string TransportDisplayName(ConfiguredTransport transport) private static string TransportDisplayName(ConfiguredTransport transport)
{ {
return transport switch return transport switch

View File

@ -10,6 +10,9 @@
<ui:VisualElement name="transport-mismatch-warning" class="warning-banner"> <ui:VisualElement name="transport-mismatch-warning" class="warning-banner">
<ui:Label name="transport-mismatch-text" class="warning-banner-text" /> <ui:Label name="transport-mismatch-text" class="warning-banner-text" />
</ui:VisualElement> </ui:VisualElement>
<ui:VisualElement name="version-mismatch-warning" class="warning-banner">
<ui:Label name="version-mismatch-text" class="warning-banner-text" />
</ui:VisualElement>
<ui:VisualElement class="setting-row" name="http-url-row"> <ui:VisualElement class="setting-row" name="http-url-row">
<ui:Label text="HTTP URL:" class="setting-label" /> <ui:Label text="HTTP URL:" class="setting-label" />
<ui:TextField name="http-url" class="url-field" /> <ui:TextField name="http-url" class="url-field" />

View File

@ -229,6 +229,11 @@ namespace MCPForUnity.Editor.Windows
// update the connection section's warning banner if there's a mismatch // update the connection section's warning banner if there's a mismatch
clientConfigSection.OnClientTransportDetected += (clientName, transport) => clientConfigSection.OnClientTransportDetected += (clientName, transport) =>
connectionSection?.UpdateTransportMismatchWarning(clientName, transport); connectionSection?.UpdateTransportMismatchWarning(clientName, transport);
// Wire up version mismatch detection: when client status is checked,
// update the connection section's warning banner if there's a version mismatch
clientConfigSection.OnClientConfigMismatch += (clientName, mismatchMessage) =>
connectionSection?.UpdateVersionMismatchWarning(clientName, mismatchMessage);
} }
// Load and initialize Validation section // Load and initialize Validation section
@ -263,6 +268,7 @@ namespace MCPForUnity.Editor.Windows
await connectionSection.VerifyBridgeConnectionAsync(); await connectionSection.VerifyBridgeConnectionAsync();
}; };
advancedSection.OnBetaModeChanged += UpdateVersionLabel; advancedSection.OnBetaModeChanged += UpdateVersionLabel;
advancedSection.OnBetaModeChanged += _ => clientConfigSection?.RefreshSelectedClient(forceImmediate: true);
// Wire up health status updates from Connection to Advanced // Wire up health status updates from Connection to Advanced
connectionSection?.SetHealthStatusUpdateCallback((isHealthy, statusText) => connectionSection?.SetHealthStatusUpdateCallback((isHealthy, statusText) =>
@ -552,6 +558,8 @@ namespace MCPForUnity.Editor.Windows
{ {
case ActivePanel.Clients: case ActivePanel.Clients:
if (clientsPanel != null) clientsPanel.style.display = DisplayStyle.Flex; if (clientsPanel != null) clientsPanel.style.display = DisplayStyle.Flex;
// Refresh client status when switching to Connect tab (e.g., after changing beta mode in Advanced)
clientConfigSection?.RefreshSelectedClient(forceImmediate: true);
break; break;
case ActivePanel.Validation: case ActivePanel.Validation:
if (validationPanel != null) validationPanel.style.display = DisplayStyle.Flex; if (validationPanel != null) validationPanel.style.display = DisplayStyle.Flex;