Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 65 additions & 17 deletions MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,24 @@ public ClaudeCliMcpConfigurator(McpClient client) : base(client) { }

public override string GetConfigPath() => "Managed via Claude CLI";

/// <summary>
/// Checks the Claude CLI registration status.
/// MUST be called from the main Unity thread due to EditorPrefs and Application.dataPath access.
/// </summary>
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);
}

/// <summary>
/// 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.
/// </summary>
internal McpStatus CheckStatusWithProjectDir(string projectDir, bool useHttpTransport, bool attemptAutoRewrite = true)
{
try
{
Expand All @@ -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)
Expand All @@ -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;
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand All @@ -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);

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ private enum TransportProtocol

// Events
public event Action OnManualConfigUpdateRequested;
public event Action OnTransportChanged;

public VisualElement Root { get; private set; }

Expand Down Expand Up @@ -115,6 +116,7 @@ private void RegisterCallbacks()
UpdateHttpFieldVisibility();
RefreshHttpUi();
OnManualConfigUpdateRequested?.Invoke();
OnTransportChanged?.Invoke();
McpLog.Info($"Transport changed to: {evt.newValue}");
});

Expand Down
2 changes: 2 additions & 0 deletions MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down