diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index eb4ba2b52..43562e5c8 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -334,7 +334,24 @@ public ClaudeCliMcpConfigurator(McpClient client) : base(client) { } public override string GetConfigPath() => "Managed via Claude CLI"; + /// + /// Checks the Claude CLI registration status. + /// MUST be called from the main Unity thread due to EditorPrefs and Application.dataPath access. + /// public override McpStatus CheckStatus(bool attemptAutoRewrite = true) + { + // Capture main-thread-only values before delegating to thread-safe method + string projectDir = Path.GetDirectoryName(Application.dataPath); + bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); + return CheckStatusWithProjectDir(projectDir, useHttpTransport, attemptAutoRewrite); + } + + /// + /// Internal thread-safe version of CheckStatus. + /// Can be called from background threads because all main-thread-only values are passed as parameters. + /// Both projectDir and useHttpTransport are REQUIRED (non-nullable) to enforce thread safety at compile time. + /// + internal McpStatus CheckStatusWithProjectDir(string projectDir, bool useHttpTransport, bool attemptAutoRewrite = true) { try { @@ -347,8 +364,11 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) return client.status; } - string args = "mcp list"; - string projectDir = Path.GetDirectoryName(Application.dataPath); + // projectDir is required - no fallback to Application.dataPath + if (string.IsNullOrEmpty(projectDir)) + { + throw new ArgumentNullException(nameof(projectDir), "Project directory must be provided for thread-safe execution"); + } string pathPrepend = null; if (Application.platform == RuntimePlatform.OSXEditor) @@ -372,10 +392,35 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) } catch { } - if (ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out _, 10000, pathPrepend)) + // Check if UnityMCP exists + if (ExecPath.TryRun(claudePath, "mcp list", projectDir, out var listStdout, out var listStderr, 10000, pathPrepend)) { - if (!string.IsNullOrEmpty(stdout) && stdout.IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0) + 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 + if (ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out var getStdout, out var getStderr, 7000, pathPrepend)) + { + // 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); + + // Check for transport mismatch + if ((currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp)) + { + string registeredTransport = registeredWithHttp ? "HTTP" : "stdio"; + string currentTransport = currentUseHttp ? "HTTP" : "stdio"; + string errorMsg = $"Transport mismatch: Claude Code is registered with {registeredTransport} but current setting is {currentTransport}. Click Configure to re-register."; + client.SetStatus(McpStatus.Error, errorMsg); + McpLog.Warn(errorMsg); + return client.status; + } + } + client.SetStatus(McpStatus.Configured); return client.status; } @@ -452,26 +497,29 @@ private void Register() } catch { } - bool already = false; - if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend)) + // Check if UnityMCP already exists and remove it first to ensure clean registration + // This ensures we always use the current transport mode setting + bool serverExists = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out _, out _, 7000, pathPrepend); + if (serverExists) { - string combined = ($"{stdout}\n{stderr}") ?? string.Empty; - if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0) + McpLog.Info("Existing UnityMCP registration found - removing to ensure transport mode is up-to-date"); + if (!ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var removeStdout, out var removeStderr, 10000, pathPrepend)) { - already = true; - } - else - { - throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}"); + McpLog.Warn($"Failed to remove existing UnityMCP registration: {removeStderr}. Attempting to register anyway..."); } } - if (!already) + // Now add the registration with the current transport mode + if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend)) { - McpLog.Info("Successfully registered with Claude Code."); + throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}"); } - CheckStatus(); + McpLog.Info($"Successfully registered with Claude Code using {(useHttpTransport ? "HTTP" : "stdio")} transport."); + + // Set status to Configured immediately after successful registration + // The UI will trigger an async verification check separately to avoid blocking + client.SetStatus(McpStatus.Configured); } private void Unregister() @@ -514,7 +562,7 @@ private void Unregister() } client.SetStatus(McpStatus.NotConfigured); - CheckStatus(); + // Status is already set - no need for blocking CheckStatus() call } public override string GetManualSnippet() diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs index 781d1c0ed..9c2dd795b 100644 --- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs +++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using MCPForUnity.Editor.Clients; +using MCPForUnity.Editor.Constants; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; using MCPForUnity.Editor.Services; @@ -288,12 +289,14 @@ private void OnCopyJsonClicked() McpLog.Info("Configuration copied to clipboard"); } - public void RefreshSelectedClient() + public void RefreshSelectedClient(bool forceImmediate = false) { if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count) { var client = configurators[selectedClientIndex]; - RefreshClientStatus(client, forceImmediate: true); + // Force immediate for non-Claude CLI, or when explicitly requested + bool shouldForceImmediate = forceImmediate || client is not ClaudeCliMcpConfigurator; + RefreshClientStatus(client, shouldForceImmediate); UpdateManualConfiguration(); UpdateClaudeCliPathVisibility(); } @@ -318,14 +321,6 @@ private void RefreshClientStatus(IMcpClientConfigurator client, bool forceImmedi private void RefreshClaudeCliStatus(IMcpClientConfigurator client, bool forceImmediate) { - if (forceImmediate) - { - MCPServiceLocator.Client.CheckClientStatus(client, attemptAutoRewrite: false); - lastStatusChecks[client] = DateTime.UtcNow; - ApplyStatusToUi(client); - return; - } - bool hasStatus = lastStatusChecks.ContainsKey(client); bool needsRefresh = !hasStatus || ShouldRefreshClient(client); @@ -338,14 +333,21 @@ private void RefreshClaudeCliStatus(IMcpClientConfigurator client, bool forceImm ApplyStatusToUi(client); } - if (needsRefresh && !statusRefreshInFlight.Contains(client)) + if ((forceImmediate || needsRefresh) && !statusRefreshInFlight.Contains(client)) { statusRefreshInFlight.Add(client); ApplyStatusToUi(client, showChecking: true); + // Capture main-thread-only values before async task + string projectDir = Path.GetDirectoryName(Application.dataPath); + bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); + Task.Run(() => { - MCPServiceLocator.Client.CheckClientStatus(client, attemptAutoRewrite: false); + // This method is only called for Claude CLI configurators, so we can safely cast + // Use thread-safe version with captured main-thread values + var claudeConfigurator = (ClaudeCliMcpConfigurator)client; + claudeConfigurator.CheckStatusWithProjectDir(projectDir, useHttpTransport, attemptAutoRewrite: false); }).ContinueWith(t => { bool faulted = false; diff --git a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs index 9b2cc9335..d19bab2b4 100644 --- a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs @@ -55,6 +55,7 @@ private enum TransportProtocol // Events public event Action OnManualConfigUpdateRequested; + public event Action OnTransportChanged; public VisualElement Root { get; private set; } @@ -115,6 +116,7 @@ private void RegisterCallbacks() UpdateHttpFieldVisibility(); RefreshHttpUi(); OnManualConfigUpdateRequested?.Invoke(); + OnTransportChanged?.Invoke(); McpLog.Info($"Transport changed to: {evt.newValue}"); }); diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs index b82f03c12..d818edd05 100644 --- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs @@ -175,6 +175,8 @@ public void CreateGUI() connectionSection = new McpConnectionSection(connectionRoot); connectionSection.OnManualConfigUpdateRequested += () => clientConfigSection?.UpdateManualConfiguration(); + connectionSection.OnTransportChanged += () => + clientConfigSection?.RefreshSelectedClient(forceImmediate: true); } // Load and initialize Client Configuration section