unity-mcp/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs

831 lines
34 KiB
C#

using System;
using System.Net.Sockets;
using System.Threading.Tasks;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Services.Transport;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace MCPForUnity.Editor.Windows.Components.Connection
{
/// <summary>
/// Controller for the Connection section of the MCP For Unity editor window.
/// Handles transport protocol, HTTP/stdio configuration, connection status, and health checks.
/// </summary>
public class McpConnectionSection
{
// Transport protocol enum
private enum TransportProtocol
{
HTTPLocal,
HTTPRemote,
Stdio
}
// UI Elements
private EnumField transportDropdown;
private VisualElement httpUrlRow;
private VisualElement httpServerCommandSection;
private TextField httpServerCommandField;
private Button copyHttpServerCommandButton;
private Label httpServerCommandHint;
private TextField httpUrlField;
private Button startHttpServerButton;
private Button stopHttpServerButton;
private VisualElement unitySocketPortRow;
private TextField unityPortField;
private VisualElement statusIndicator;
private Label connectionStatusLabel;
private Button connectionToggleButton;
private VisualElement healthIndicator;
private Label healthStatusLabel;
private VisualElement healthRow;
private Button testConnectionButton;
private bool connectionToggleInProgress;
private bool autoStartInProgress;
private bool httpServerToggleInProgress;
private Task verificationTask;
private string lastHealthStatus;
private double lastLocalServerRunningPollTime;
private bool lastLocalServerRunning;
// Health status constants
private const string HealthStatusUnknown = "Unknown";
private const string HealthStatusHealthy = "Healthy";
private const string HealthStatusPingFailed = "Ping Failed";
private const string HealthStatusUnhealthy = "Unhealthy";
// Events
public event Action OnManualConfigUpdateRequested;
public event Action OnTransportChanged;
public VisualElement Root { get; private set; }
public McpConnectionSection(VisualElement root)
{
Root = root;
CacheUIElements();
InitializeUI();
RegisterCallbacks();
}
private void CacheUIElements()
{
transportDropdown = Root.Q<EnumField>("transport-dropdown");
httpUrlRow = Root.Q<VisualElement>("http-url-row");
httpServerCommandSection = Root.Q<VisualElement>("http-server-command-section");
httpServerCommandField = Root.Q<TextField>("http-server-command");
copyHttpServerCommandButton = Root.Q<Button>("copy-http-server-command-button");
httpServerCommandHint = Root.Q<Label>("http-server-command-hint");
httpUrlField = Root.Q<TextField>("http-url");
startHttpServerButton = Root.Q<Button>("start-http-server-button");
stopHttpServerButton = Root.Q<Button>("stop-http-server-button");
unitySocketPortRow = Root.Q<VisualElement>("unity-socket-port-row");
unityPortField = Root.Q<TextField>("unity-port");
statusIndicator = Root.Q<VisualElement>("status-indicator");
connectionStatusLabel = Root.Q<Label>("connection-status");
connectionToggleButton = Root.Q<Button>("connection-toggle");
healthIndicator = Root.Q<VisualElement>("health-indicator");
healthStatusLabel = Root.Q<Label>("health-status");
healthRow = Root.Q<VisualElement>("health-row");
testConnectionButton = Root.Q<Button>("test-connection-button");
}
private void InitializeUI()
{
transportDropdown.Init(TransportProtocol.HTTPLocal);
bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
if (!useHttpTransport)
{
transportDropdown.value = TransportProtocol.Stdio;
}
else
{
// Back-compat: if scope pref isn't set yet, infer from current URL.
string scope = EditorPrefs.GetString(EditorPrefKeys.HttpTransportScope, string.Empty);
if (string.IsNullOrEmpty(scope))
{
scope = MCPServiceLocator.Server.IsLocalUrl() ? "local" : "remote";
}
transportDropdown.value = scope == "remote" ? TransportProtocol.HTTPRemote : TransportProtocol.HTTPLocal;
}
httpUrlField.value = HttpEndpointUtility.GetBaseUrl();
int unityPort = EditorPrefs.GetInt(EditorPrefKeys.UnitySocketPort, 0);
if (unityPort == 0)
{
unityPort = MCPServiceLocator.Bridge.CurrentPort;
}
unityPortField.value = unityPort.ToString();
UpdateHttpFieldVisibility();
RefreshHttpUi();
UpdateConnectionStatus();
// Explain what "Health" means (it is a separate verify/ping check and can differ from session state).
if (healthStatusLabel != null)
{
healthStatusLabel.tooltip = "Health is a lightweight verify/ping of the active transport. A session can be active while health is degraded.";
}
if (healthIndicator != null)
{
healthIndicator.tooltip = healthStatusLabel?.tooltip;
}
}
private void RegisterCallbacks()
{
transportDropdown.RegisterValueChangedCallback(evt =>
{
var previous = (TransportProtocol)evt.previousValue;
var selected = (TransportProtocol)evt.newValue;
bool useHttp = selected != TransportProtocol.Stdio;
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, useHttp);
if (useHttp)
{
string scope = selected == TransportProtocol.HTTPRemote ? "remote" : "local";
EditorPrefs.SetString(EditorPrefKeys.HttpTransportScope, scope);
}
UpdateHttpFieldVisibility();
RefreshHttpUi();
UpdateConnectionStatus();
OnManualConfigUpdateRequested?.Invoke();
OnTransportChanged?.Invoke();
McpLog.Info($"Transport changed to: {evt.newValue}");
// Best-effort: stop the deselected transport to avoid leaving duplicated sessions running.
// (Switching between HttpLocal/HttpRemote does not require stopping.)
bool prevWasHttp = previous != TransportProtocol.Stdio;
bool nextIsHttp = selected != TransportProtocol.Stdio;
if (prevWasHttp != nextIsHttp)
{
var stopMode = nextIsHttp ? TransportMode.Stdio : TransportMode.Http;
try
{
var stopTask = MCPServiceLocator.TransportManager.StopAsync(stopMode);
stopTask.ContinueWith(t =>
{
try
{
if (t.IsFaulted)
{
var msg = t.Exception?.GetBaseException()?.Message ?? "Unknown error";
McpLog.Warn($"Async stop of {stopMode} transport failed: {msg}");
}
}
catch { }
}, TaskScheduler.Default);
}
catch (Exception ex)
{
McpLog.Warn($"Failed to stop previous transport ({stopMode}) after selection change: {ex.Message}");
}
}
});
// Don't normalize/overwrite the URL on every keystroke (it fights the user and can duplicate schemes).
// Instead, persist + normalize on focus-out / Enter, then update UI once.
httpUrlField.RegisterCallback<FocusOutEvent>(_ => PersistHttpUrlFromField());
httpUrlField.RegisterCallback<KeyDownEvent>(evt =>
{
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
PersistHttpUrlFromField();
evt.StopPropagation();
}
});
if (startHttpServerButton != null)
{
startHttpServerButton.clicked += OnHttpServerToggleClicked;
}
if (stopHttpServerButton != null)
{
// Stop button removed from UXML as part of consolidated Start/Stop UX.
// Kept null-check for backward compatibility if older UXML is loaded.
stopHttpServerButton.clicked += () =>
{
// In older UXML layouts, route the stop button to the consolidated toggle behavior.
// If a session is active, this will end it and attempt to stop the local server.
OnHttpServerToggleClicked();
};
}
if (copyHttpServerCommandButton != null)
{
copyHttpServerCommandButton.clicked += () =>
{
if (!string.IsNullOrEmpty(httpServerCommandField?.value) && copyHttpServerCommandButton.enabledSelf)
{
EditorGUIUtility.systemCopyBuffer = httpServerCommandField.value;
McpLog.Info("HTTP server command copied to clipboard.");
}
};
}
unityPortField.RegisterCallback<FocusOutEvent>(_ => PersistUnityPortFromField());
unityPortField.RegisterCallback<KeyDownEvent>(evt =>
{
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
PersistUnityPortFromField();
evt.StopPropagation();
}
});
connectionToggleButton.clicked += OnConnectionToggleClicked;
testConnectionButton.clicked += OnTestConnectionClicked;
}
private void PersistHttpUrlFromField()
{
if (httpUrlField == null)
{
return;
}
HttpEndpointUtility.SaveBaseUrl(httpUrlField.text);
// Update displayed value to normalized form without re-triggering callbacks/caret jumps.
httpUrlField.SetValueWithoutNotify(HttpEndpointUtility.GetBaseUrl());
OnManualConfigUpdateRequested?.Invoke();
RefreshHttpUi();
}
public void UpdateConnectionStatus()
{
var bridgeService = MCPServiceLocator.Bridge;
bool isRunning = bridgeService.IsRunning;
bool showLocalServerControls = IsHttpLocalSelected();
bool debugMode = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
bool stdioSelected = transportDropdown != null && (TransportProtocol)transportDropdown.value == TransportProtocol.Stdio;
// Keep the consolidated Start/Stop Server button label in sync even when the session is not running
// (e.g., orphaned server after a domain reload).
UpdateStartHttpButtonState();
// If local-server controls are active, hide the manual session toggle controls and
// rely on the Start/Stop Server button. We still keep the session status text visible
// next to the dot for clarity.
if (connectionToggleButton != null)
{
connectionToggleButton.style.display = showLocalServerControls ? DisplayStyle.None : DisplayStyle.Flex;
}
// Hide "Test" buttons unless Debug Mode is enabled.
if (testConnectionButton != null)
{
testConnectionButton.style.display = debugMode ? DisplayStyle.Flex : DisplayStyle.None;
}
// Health is useful mainly for diagnostics: hide it once we're "Healthy" unless Debug Mode is enabled.
// If health is degraded, keep it visible even outside Debug Mode so it can act as a signal.
if (healthRow != null)
{
bool showHealth = debugMode || (isRunning && lastHealthStatus != HealthStatusHealthy);
healthRow.style.display = showHealth ? DisplayStyle.Flex : DisplayStyle.None;
}
if (isRunning)
{
connectionStatusLabel.text = "Session Active";
statusIndicator.RemoveFromClassList("disconnected");
statusIndicator.AddToClassList("connected");
connectionToggleButton.text = "End Session";
// Force the UI to reflect the actual port being used
unityPortField.value = bridgeService.CurrentPort.ToString();
unityPortField.SetEnabled(false);
}
else
{
connectionStatusLabel.text = "No Session";
statusIndicator.RemoveFromClassList("connected");
statusIndicator.AddToClassList("disconnected");
connectionToggleButton.text = "Start Session";
unityPortField.SetEnabled(true);
healthStatusLabel.text = HealthStatusUnknown;
healthIndicator.RemoveFromClassList("healthy");
healthIndicator.RemoveFromClassList("warning");
healthIndicator.AddToClassList("unknown");
int savedPort = EditorPrefs.GetInt(EditorPrefKeys.UnitySocketPort, 0);
unityPortField.value = (savedPort == 0
? bridgeService.CurrentPort
: savedPort).ToString();
}
// For stdio session toggling, make End Session visually "danger" (red).
// (HTTP Local uses the consolidated Start/Stop Server button instead.)
connectionToggleButton?.EnableInClassList("server-running", isRunning && stdioSelected);
}
public void UpdateHttpServerCommandDisplay()
{
if (httpServerCommandSection == null || httpServerCommandField == null)
{
return;
}
bool useHttp = transportDropdown != null && (TransportProtocol)transportDropdown.value != TransportProtocol.Stdio;
bool httpLocalSelected = IsHttpLocalSelected();
bool isLocalHttpUrl = MCPServiceLocator.Server.IsLocalUrl();
// Only show the local-server helper UI when HTTP Local is selected.
if (!useHttp || !httpLocalSelected)
{
httpServerCommandSection.style.display = DisplayStyle.None;
httpServerCommandField.value = string.Empty;
httpServerCommandField.tooltip = string.Empty;
if (httpServerCommandHint != null)
{
httpServerCommandHint.text = string.Empty;
}
if (copyHttpServerCommandButton != null)
{
copyHttpServerCommandButton.SetEnabled(false);
}
return;
}
httpServerCommandSection.style.display = DisplayStyle.Flex;
if (!isLocalHttpUrl)
{
httpServerCommandField.value = string.Empty;
httpServerCommandField.tooltip = string.Empty;
if (httpServerCommandHint != null)
{
httpServerCommandHint.text = "HTTP Local requires a localhost URL (localhost/127.0.0.1/0.0.0.0/::1).";
}
copyHttpServerCommandButton?.SetEnabled(false);
return;
}
if (MCPServiceLocator.Server.TryGetLocalHttpServerCommand(out var command, out var error))
{
httpServerCommandField.value = command;
httpServerCommandField.tooltip = command;
if (httpServerCommandHint != null)
{
httpServerCommandHint.text = "Run this command in your shell if you prefer to start the server manually.";
}
if (copyHttpServerCommandButton != null)
{
copyHttpServerCommandButton.SetEnabled(true);
}
}
else
{
httpServerCommandField.value = string.Empty;
httpServerCommandField.tooltip = string.Empty;
if (httpServerCommandHint != null)
{
httpServerCommandHint.text = error ?? "The command is not available with the current configuration.";
}
if (copyHttpServerCommandButton != null)
{
copyHttpServerCommandButton.SetEnabled(false);
}
}
}
private void UpdateHttpFieldVisibility()
{
bool useHttp = (TransportProtocol)transportDropdown.value != TransportProtocol.Stdio;
httpUrlRow.style.display = useHttp ? DisplayStyle.Flex : DisplayStyle.None;
unitySocketPortRow.style.display = useHttp ? DisplayStyle.None : DisplayStyle.Flex;
}
private bool IsHttpLocalSelected()
{
return transportDropdown != null && (TransportProtocol)transportDropdown.value == TransportProtocol.HTTPLocal;
}
private void UpdateStartHttpButtonState()
{
if (startHttpServerButton == null)
return;
bool useHttp = transportDropdown != null && (TransportProtocol)transportDropdown.value != TransportProtocol.Stdio;
if (!useHttp)
{
startHttpServerButton.SetEnabled(false);
startHttpServerButton.tooltip = string.Empty;
return;
}
bool httpLocalSelected = IsHttpLocalSelected();
bool canStartLocalServer = httpLocalSelected && MCPServiceLocator.Server.IsLocalUrl();
bool sessionRunning = MCPServiceLocator.Bridge.IsRunning;
bool localServerRunning = false;
// Avoid running expensive port/PID checks every UI tick.
if (httpLocalSelected)
{
double now = EditorApplication.timeSinceStartup;
if ((now - lastLocalServerRunningPollTime) > 0.75f || httpServerToggleInProgress)
{
lastLocalServerRunningPollTime = now;
lastLocalServerRunning = MCPServiceLocator.Server.IsLocalHttpServerRunning();
}
localServerRunning = lastLocalServerRunning;
}
// Single consolidated button: Start Server (launch local server + start session) or
// Stop Server (end session + attempt to stop local server).
bool shouldShowStop = sessionRunning || localServerRunning;
startHttpServerButton.text = shouldShowStop ? "Stop Server" : "Start Server";
// Note: Server logs may contain transient HTTP 400s on /mcp during startup probing and
// CancelledError stack traces on shutdown when streaming requests are cancelled; this is expected.
startHttpServerButton.EnableInClassList("server-running", shouldShowStop);
startHttpServerButton.SetEnabled(
(canStartLocalServer && !httpServerToggleInProgress && !autoStartInProgress) ||
(shouldShowStop && !httpServerToggleInProgress));
startHttpServerButton.tooltip = httpLocalSelected
? (canStartLocalServer ? string.Empty : "HTTP Local requires a localhost URL (localhost/127.0.0.1/0.0.0.0/::1).")
: string.Empty;
// Stop button is no longer used; it may be null depending on UXML version.
stopHttpServerButton?.SetEnabled(false);
}
private void RefreshHttpUi()
{
UpdateStartHttpButtonState();
UpdateHttpServerCommandDisplay();
}
private async void OnHttpServerToggleClicked()
{
if (httpServerToggleInProgress)
{
return;
}
var bridgeService = MCPServiceLocator.Bridge;
httpServerToggleInProgress = true;
startHttpServerButton?.SetEnabled(false);
try
{
// If a session is active, treat this as "Stop Server" (end session first, then try to stop server).
if (bridgeService.IsRunning)
{
await bridgeService.StopAsync();
bool stopped = MCPServiceLocator.Server.StopLocalHttpServer();
if (!stopped)
{
McpLog.Warn("Failed to stop HTTP server or no server was running");
}
return;
}
// If the session isn't running but the local server is, allow stopping the server directly.
if (IsHttpLocalSelected() && MCPServiceLocator.Server.IsLocalHttpServerRunning())
{
bool stopped = MCPServiceLocator.Server.StopLocalHttpServer();
if (!stopped)
{
McpLog.Warn("Failed to stop HTTP server or no server was running");
}
return;
}
// Otherwise, "Start Server" and then auto-start the session.
bool started = MCPServiceLocator.Server.StartLocalHttpServer();
if (started)
{
await TryAutoStartSessionAfterHttpServerAsync();
}
}
catch (Exception ex)
{
McpLog.Error($"HTTP server toggle failed: {ex.Message}");
EditorUtility.DisplayDialog("Error", $"Failed to toggle local HTTP server:\n\n{ex.Message}", "OK");
}
finally
{
httpServerToggleInProgress = false;
RefreshHttpUi();
UpdateConnectionStatus();
}
}
private void PersistUnityPortFromField()
{
if (unityPortField == null)
{
return;
}
string input = unityPortField.text?.Trim();
if (!int.TryParse(input, out int requestedPort) || requestedPort <= 0)
{
unityPortField.value = MCPServiceLocator.Bridge.CurrentPort.ToString();
return;
}
try
{
int storedPort = PortManager.SetPreferredPort(requestedPort);
EditorPrefs.SetInt(EditorPrefKeys.UnitySocketPort, storedPort);
unityPortField.value = storedPort.ToString();
}
catch (Exception ex)
{
McpLog.Warn($"Failed to persist Unity socket port: {ex.Message}");
EditorUtility.DisplayDialog(
"Port Unavailable",
$"The requested port could not be used:\n\n{ex.Message}\n\nReverting to the active Unity port.",
"OK");
unityPortField.value = MCPServiceLocator.Bridge.CurrentPort.ToString();
}
}
private async void OnConnectionToggleClicked()
{
if (connectionToggleInProgress)
{
return;
}
var bridgeService = MCPServiceLocator.Bridge;
connectionToggleInProgress = true;
connectionToggleButton?.SetEnabled(false);
try
{
if (bridgeService.IsRunning)
{
await bridgeService.StopAsync();
}
else
{
bool started = await bridgeService.StartAsync();
if (started)
{
await VerifyBridgeConnectionAsync();
}
else
{
McpLog.Warn("Failed to start MCP bridge");
}
}
}
catch (Exception ex)
{
McpLog.Error($"Connection toggle failed: {ex.Message}");
EditorUtility.DisplayDialog("Connection Error",
$"Failed to toggle the MCP connection:\n\n{ex.Message}",
"OK");
}
finally
{
connectionToggleInProgress = false;
connectionToggleButton?.SetEnabled(true);
UpdateConnectionStatus();
}
}
private async void OnTestConnectionClicked()
{
await VerifyBridgeConnectionAsync();
}
private async Task TryAutoStartSessionAfterHttpServerAsync()
{
if (autoStartInProgress)
{
return;
}
var bridgeService = MCPServiceLocator.Bridge;
if (bridgeService.IsRunning)
{
return;
}
autoStartInProgress = true;
connectionToggleButton?.SetEnabled(false);
const int maxAttempts = 10;
var delay = TimeSpan.FromSeconds(1);
try
{
// Wait until the HTTP server is actually accepting connections to reduce transient "unable to connect then recovers"
// behavior (session start attempts can race the server startup).
bool serverReady = await WaitForHttpServerAcceptingConnectionsAsync(TimeSpan.FromSeconds(10));
if (!serverReady)
{
McpLog.Warn("HTTP server did not become reachable within the expected startup window; will still attempt to start the session.");
}
for (int attempt = 0; attempt < maxAttempts; attempt++)
{
bool started = await bridgeService.StartAsync();
if (started)
{
await VerifyBridgeConnectionAsync();
UpdateConnectionStatus();
return;
}
if (attempt < maxAttempts - 1)
{
await Task.Delay(delay);
}
}
McpLog.Warn("Failed to auto-start MCP session after launching the HTTP server.");
}
finally
{
autoStartInProgress = false;
connectionToggleButton?.SetEnabled(true);
UpdateConnectionStatus();
}
}
private static async Task<bool> WaitForHttpServerAcceptingConnectionsAsync(TimeSpan timeout)
{
try
{
string baseUrl = HttpEndpointUtility.GetBaseUrl();
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri) || uri.Port <= 0)
{
return true; // Don't block if URL cannot be parsed
}
string host = uri.Host;
int port = uri.Port;
// Normalize wildcard/bind-all hosts to loopback for readiness checks.
// When the server binds to 0.0.0.0 or ::, clients typically connect via localhost/127.0.0.1.
string normalizedHost;
if (string.IsNullOrWhiteSpace(host)
|| string.Equals(host, "0.0.0.0", StringComparison.OrdinalIgnoreCase)
|| string.Equals(host, "::", StringComparison.OrdinalIgnoreCase)
|| string.Equals(host, "*", StringComparison.OrdinalIgnoreCase))
{
normalizedHost = "localhost";
}
else
{
normalizedHost = host;
}
var deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
try
{
using var client = new TcpClient();
var connectTask = client.ConnectAsync(normalizedHost, port);
var completed = await Task.WhenAny(connectTask, Task.Delay(250));
if (completed != connectTask)
{
// Avoid leaving a long-running ConnectAsync in-flight (default OS connect timeout can be very long),
// which can accumulate across retries and impact overall editor/network responsiveness.
// Closing the client will cause ConnectAsync to complete quickly (typically with an exception),
// which we then attempt to observe (bounded) by awaiting.
try { client.Close(); } catch { }
}
try
{
// Even after Close(), some platforms may take a moment to complete the connect task.
// Keep this bounded so the readiness loop can't hang here.
var connectCompleted = await Task.WhenAny(connectTask, Task.Delay(250));
if (connectCompleted == connectTask)
{
await connectTask;
}
else
{
_ = connectTask.ContinueWith(
t => _ = t.Exception,
System.Threading.CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.Default);
}
}
catch
{
// Ignore connection exceptions and retry until timeout.
}
if (client.Connected)
{
return true;
}
}
catch
{
// Ignore and retry until timeout
}
await Task.Delay(150);
}
return false;
}
catch
{
return false;
}
}
public async Task VerifyBridgeConnectionAsync()
{
// Prevent concurrent verification calls
if (verificationTask != null && !verificationTask.IsCompleted)
{
return;
}
verificationTask = VerifyBridgeConnectionInternalAsync();
await verificationTask;
}
private async Task VerifyBridgeConnectionInternalAsync()
{
var bridgeService = MCPServiceLocator.Bridge;
if (!bridgeService.IsRunning)
{
healthStatusLabel.text = HealthStatusUnknown;
healthIndicator.RemoveFromClassList("healthy");
healthIndicator.RemoveFromClassList("warning");
healthIndicator.AddToClassList("unknown");
// Only log if state changed
if (lastHealthStatus != HealthStatusUnknown)
{
McpLog.Warn("Cannot verify connection: Bridge is not running");
lastHealthStatus = HealthStatusUnknown;
}
return;
}
var result = await bridgeService.VerifyAsync();
healthIndicator.RemoveFromClassList("healthy");
healthIndicator.RemoveFromClassList("warning");
healthIndicator.RemoveFromClassList("unknown");
string newStatus;
if (result.Success && result.PingSucceeded)
{
newStatus = HealthStatusHealthy;
healthStatusLabel.text = newStatus;
healthIndicator.AddToClassList("healthy");
// Only log if state changed
if (lastHealthStatus != newStatus)
{
McpLog.Debug($"Connection verification successful: {result.Message}");
lastHealthStatus = newStatus;
}
}
else if (result.HandshakeValid)
{
newStatus = HealthStatusPingFailed;
healthStatusLabel.text = newStatus;
healthIndicator.AddToClassList("warning");
// Log once per distinct warning state
if (lastHealthStatus != newStatus)
{
McpLog.Warn($"Connection verification warning: {result.Message}");
lastHealthStatus = newStatus;
}
}
else
{
newStatus = HealthStatusUnhealthy;
healthStatusLabel.text = newStatus;
healthIndicator.AddToClassList("warning");
// Log once per distinct error state
if (lastHealthStatus != newStatus)
{
McpLog.Error($"Connection verification failed: {result.Message}");
lastHealthStatus = newStatus;
}
}
}
}
}