815 lines
34 KiB
C#
815 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 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";
|
|
try
|
|
{
|
|
EditorPrefs.SetString(EditorPrefKeys.HttpTransportScope, scope);
|
|
}
|
|
catch
|
|
{
|
|
McpLog.Debug("Failed to set HttpTransportScope pref.");
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
// Clear any stale resume flags when user manually changes transport
|
|
try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); } catch { }
|
|
try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeHttpAfterReload); } catch { }
|
|
|
|
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);
|
|
// Use EditorPrefs as source of truth for stdio selection - more reliable after domain reload
|
|
// than checking the dropdown which may not be initialized yet
|
|
bool stdioSelected = !EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
|
|
|
|
// Keep the Start/Stop Server button label in sync even when the session is not running
|
|
// (e.g., orphaned server after a domain reload).
|
|
// NOTE: This also updates lastLocalServerRunning which is used below for session toggle visibility.
|
|
UpdateStartHttpButtonState();
|
|
|
|
// Detect orphaned session: if HTTP Local session thinks it's running but the server is gone,
|
|
// automatically end the session to keep UI in sync with reality.
|
|
if (showLocalServerControls && isRunning && !lastLocalServerRunning && !connectionToggleInProgress)
|
|
{
|
|
McpLog.Info("Server no longer running; ending orphaned session.");
|
|
_ = EndOrphanedSessionAsync();
|
|
isRunning = false; // Update local state for the rest of this method
|
|
}
|
|
|
|
// For HTTP Local: show session toggle button only when server is running (so user can manually start/end session).
|
|
// For Stdio/HTTP Remote: always show the session toggle button.
|
|
// This separates server lifecycle from session lifecycle for multi-instance scenarios.
|
|
// We use lastLocalServerRunning which was just refreshed by UpdateStartHttpButtonState() above.
|
|
if (connectionToggleButton != null)
|
|
{
|
|
bool showSessionToggle = !showLocalServerControls || lastLocalServerRunning;
|
|
connectionToggleButton.style.display = showSessionToggle ? DisplayStyle.Flex : DisplayStyle.None;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
// Show instance name (project folder name) for better identification in multi-instance scenarios.
|
|
// Defensive: handle edge cases where path parsing might return null/empty.
|
|
string projectDir = System.IO.Path.GetDirectoryName(Application.dataPath);
|
|
string instanceName = !string.IsNullOrEmpty(projectDir)
|
|
? System.IO.Path.GetFileName(projectDir)
|
|
: "Unity";
|
|
if (string.IsNullOrEmpty(instanceName)) instanceName = "Unity";
|
|
connectionStatusLabel.text = $"Session Active ({instanceName})";
|
|
statusIndicator.RemoveFromClassList("disconnected");
|
|
statusIndicator.AddToClassList("connected");
|
|
connectionToggleButton.text = "End Session";
|
|
connectionToggleButton.SetEnabled(true); // Re-enable in case it was disabled during resumption
|
|
|
|
// Force the UI to reflect the actual port being used
|
|
unityPortField.value = bridgeService.CurrentPort.ToString();
|
|
unityPortField.SetEnabled(false);
|
|
}
|
|
else
|
|
{
|
|
// Check if we're resuming the stdio bridge after a domain reload.
|
|
// During this brief window, show "Resuming..." instead of "No Session" to avoid UI flicker.
|
|
bool isStdioResuming = stdioSelected
|
|
&& EditorPrefs.GetBool(EditorPrefKeys.ResumeStdioAfterReload, false);
|
|
|
|
if (isStdioResuming)
|
|
{
|
|
connectionStatusLabel.text = "Resuming...";
|
|
// Keep the indicator in a neutral/transitional state
|
|
statusIndicator.RemoveFromClassList("connected");
|
|
statusIndicator.RemoveFromClassList("disconnected");
|
|
connectionToggleButton.text = "Start Session";
|
|
connectionToggleButton.SetEnabled(false);
|
|
}
|
|
else
|
|
{
|
|
connectionStatusLabel.text = "No Session";
|
|
statusIndicator.RemoveFromClassList("connected");
|
|
statusIndicator.AddToClassList("disconnected");
|
|
connectionToggleButton.text = "Start Session";
|
|
connectionToggleButton.SetEnabled(true);
|
|
}
|
|
|
|
unityPortField.SetEnabled(!isStdioResuming);
|
|
|
|
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 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;
|
|
}
|
|
|
|
// Server button only controls server lifecycle (Start/Stop Server).
|
|
// Session lifecycle is handled by the separate connectionToggleButton.
|
|
bool shouldShowStop = 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", localServerRunning);
|
|
startHttpServerButton.SetEnabled(
|
|
!httpServerToggleInProgress && (shouldShowStop || canStartLocalServer));
|
|
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
|
|
{
|
|
// Check if a local server is running.
|
|
bool serverRunning = IsHttpLocalSelected() && MCPServiceLocator.Server.IsLocalHttpServerRunning();
|
|
|
|
if (serverRunning)
|
|
{
|
|
// Stop Server: end session first (if active), then stop the 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");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Start Server: launch the local HTTP server.
|
|
// When WE start the server, auto-start our session (we clearly want to use it).
|
|
// This differs from detecting an already-running server, where we require manual session start.
|
|
bool serverStarted = MCPServiceLocator.Server.StartLocalHttpServer();
|
|
if (serverStarted)
|
|
{
|
|
await TryAutoStartSessionAsync();
|
|
}
|
|
else
|
|
{
|
|
McpLog.Warn("Failed to start local HTTP server");
|
|
}
|
|
}
|
|
}
|
|
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 async Task TryAutoStartSessionAsync()
|
|
{
|
|
// Wait briefly for the HTTP server to become ready, then start the session.
|
|
// This is called when THIS instance starts the server (not when detecting an external server).
|
|
var bridgeService = MCPServiceLocator.Bridge;
|
|
// Windows/dev mode may take much longer due to uv package resolution, fresh downloads, antivirus scans, etc.
|
|
const int maxAttempts = 30;
|
|
// Use shorter delays initially, then longer delays to allow server startup
|
|
var shortDelay = TimeSpan.FromMilliseconds(500);
|
|
var longDelay = TimeSpan.FromSeconds(3);
|
|
|
|
for (int attempt = 0; attempt < maxAttempts; attempt++)
|
|
{
|
|
var delay = attempt < 6 ? shortDelay : longDelay;
|
|
|
|
// Check if server is actually accepting connections
|
|
bool serverDetected = MCPServiceLocator.Server.IsLocalHttpServerRunning();
|
|
|
|
if (serverDetected)
|
|
{
|
|
// Server detected - try to connect
|
|
bool started = await bridgeService.StartAsync();
|
|
if (started)
|
|
{
|
|
await VerifyBridgeConnectionAsync();
|
|
UpdateConnectionStatus();
|
|
return;
|
|
}
|
|
}
|
|
else if (attempt >= 20)
|
|
{
|
|
// After many attempts without detection, try connecting anyway as a last resort.
|
|
// This handles cases where process detection fails but the server is actually running.
|
|
// Only try once every 3 attempts to avoid spamming connection errors (at attempts 20, 23, 26, 29).
|
|
if ((attempt - 20) % 3 != 0) continue;
|
|
|
|
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 session after launching the HTTP server.");
|
|
}
|
|
|
|
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 EndOrphanedSessionAsync()
|
|
{
|
|
// Fire-and-forget cleanup of orphaned session when server is no longer running.
|
|
// This prevents the UI from showing "Session Active" when the underlying server is gone.
|
|
try
|
|
{
|
|
connectionToggleInProgress = true;
|
|
connectionToggleButton?.SetEnabled(false);
|
|
await MCPServiceLocator.Bridge.StopAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
McpLog.Warn($"Failed to end orphaned session: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
connectionToggleInProgress = false;
|
|
connectionToggleButton?.SetEnabled(true);
|
|
UpdateConnectionStatus();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|