From 505779653ed347eb616c997df4a76d2e3a55b6d8 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:36:32 -0400 Subject: [PATCH 1/3] Initial update --- .../Editor/Constants/EditorPrefKeys.cs | 1 + .../Editor/Services/HttpAutoStartHandler.cs | 171 ++++++++++++++++++ .../Services/HttpAutoStartHandler.cs.meta | 2 + .../Services/IServerManagementService.cs | 3 +- .../Services/ServerManagementService.cs | 81 +++++---- .../Transport/Transports/StdioBridgeHost.cs | 5 +- .../Components/Advanced/McpAdvancedSection.cs | 21 +++ .../Advanced/McpAdvancedSection.uxml | 5 + 8 files changed, 251 insertions(+), 38 deletions(-) create mode 100644 MCPForUnity/Editor/Services/HttpAutoStartHandler.cs create mode 100644 MCPForUnity/Editor/Services/HttpAutoStartHandler.cs.meta diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index b5b1aaa64..a513aca9a 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -66,6 +66,7 @@ internal static class EditorPrefKeys internal const string ApiKey = "MCPForUnity.ApiKey"; + internal const string AutoStartOnLoad = "MCPForUnity.AutoStartOnLoad"; internal const string BatchExecuteMaxCommands = "MCPForUnity.BatchExecute.MaxCommands"; internal const string LogRecordEnabled = "MCPForUnity.LogRecordEnabled"; } diff --git a/MCPForUnity/Editor/Services/HttpAutoStartHandler.cs b/MCPForUnity/Editor/Services/HttpAutoStartHandler.cs new file mode 100644 index 000000000..24394eba6 --- /dev/null +++ b/MCPForUnity/Editor/Services/HttpAutoStartHandler.cs @@ -0,0 +1,171 @@ +using System; +using System.Threading.Tasks; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services.Transport; +using MCPForUnity.Editor.Windows; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Automatically starts the HTTP MCP bridge on editor load when the user has opted in + /// via the "Auto-Start on Editor Load" toggle in Advanced Settings. + /// This complements HttpBridgeReloadHandler (which only resumes after domain reloads). + /// + [InitializeOnLoad] + internal static class HttpAutoStartHandler + { + static HttpAutoStartHandler() + { + if (Application.isBatchMode && + string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("UNITY_MCP_ALLOW_BATCH"))) + { + return; + } + + // Only check lightweight EditorPrefs here — services like EditorConfigurationCache + // and MCPServiceLocator may not be initialized yet on fresh editor launch. + bool autoStartEnabled = EditorPrefs.GetBool(EditorPrefKeys.AutoStartOnLoad, true); + if (!autoStartEnabled) return; + + // Delay to let the editor and services finish initialization. + EditorApplication.delayCall += OnEditorReady; + } + + private static void OnEditorReady() + { + try + { + bool autoStartEnabled = EditorPrefs.GetBool(EditorPrefKeys.AutoStartOnLoad, true); + if (!autoStartEnabled) return; + + bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; + if (!useHttp) return; + + // Don't auto-start if HttpBridgeReloadHandler will resume (avoids double-start). + bool resumingAfterReload = EditorPrefs.GetBool(EditorPrefKeys.ResumeHttpAfterReload, false); + if (resumingAfterReload) return; + + // Don't auto-start if bridge is already running. + if (MCPServiceLocator.TransportManager.IsRunning(TransportMode.Http)) return; + + _ = AutoStartAsync(); + } + catch (Exception ex) + { + McpLog.Debug($"[HTTP Auto-Start] Deferred check failed: {ex.Message}"); + } + } + + private static async Task AutoStartAsync() + { + try + { + bool isLocal = !HttpEndpointUtility.IsRemoteScope(); + + if (isLocal) + { + // For HTTP Local: launch the server process first, then connect the bridge. + // This mirrors what the UI "Start Server" button does. + if (!HttpEndpointUtility.IsHttpLocalUrlAllowedForLaunch( + HttpEndpointUtility.GetLocalBaseUrl(), out string policyError)) + { + McpLog.Debug($"[HTTP Auto-Start] Local URL blocked by security policy: {policyError}"); + return; + } + + // Check if server is already reachable (e.g. user started it externally). + if (!MCPServiceLocator.Server.IsLocalHttpServerReachable()) + { + bool serverStarted = MCPServiceLocator.Server.StartLocalHttpServer(quiet: true); + if (!serverStarted) + { + McpLog.Warn("[HTTP Auto-Start] Failed to start local HTTP server"); + return; + } + } + + // Wait for the server to become reachable, then connect. + await WaitForServerAndConnectAsync(); + } + else + { + // For HTTP Remote: server is external, just connect the bridge. + await ConnectBridgeAsync(); + } + } + catch (Exception ex) + { + McpLog.Warn($"[HTTP Auto-Start] Failed: {ex.Message}"); + } + } + + /// + /// Waits for the local HTTP server to accept connections, then connects the bridge. + /// Mirrors TryAutoStartSessionAsync in McpConnectionSection. + /// + private static async Task WaitForServerAndConnectAsync() + { + const int maxAttempts = 30; + var shortDelay = TimeSpan.FromMilliseconds(500); + var longDelay = TimeSpan.FromSeconds(3); + + for (int attempt = 0; attempt < maxAttempts; attempt++) + { + // Abort if user changed settings while we were waiting. + if (!EditorPrefs.GetBool(EditorPrefKeys.AutoStartOnLoad, true)) return; + if (!EditorConfigurationCache.Instance.UseHttpTransport) return; + if (MCPServiceLocator.TransportManager.IsRunning(TransportMode.Http)) return; + + bool reachable = MCPServiceLocator.Server.IsLocalHttpServerReachable(); + + if (reachable) + { + bool started = await MCPServiceLocator.Bridge.StartAsync(); + if (started) + { + McpLog.Info("[HTTP Auto-Start] Bridge started successfully"); + MCPForUnityEditorWindow.RequestHealthVerification(); + return; + } + } + else if (attempt >= 20 && (attempt - 20) % 3 == 0) + { + // Last-resort: try connecting even if not detected (process detection may fail). + bool started = await MCPServiceLocator.Bridge.StartAsync(); + if (started) + { + McpLog.Info("[HTTP Auto-Start] Bridge started successfully (late connect)"); + MCPForUnityEditorWindow.RequestHealthVerification(); + return; + } + } + + var delay = attempt < 6 ? shortDelay : longDelay; + try { await Task.Delay(delay); } + catch { return; } + } + + McpLog.Warn("[HTTP Auto-Start] Server did not become reachable after launch"); + } + + /// + /// Connects the bridge directly (for remote HTTP where the server is already running). + /// + private static async Task ConnectBridgeAsync() + { + bool started = await MCPServiceLocator.Bridge.StartAsync(); + if (started) + { + McpLog.Info("[HTTP Auto-Start] Bridge started successfully (remote)"); + MCPForUnityEditorWindow.RequestHealthVerification(); + } + else + { + McpLog.Warn("[HTTP Auto-Start] Failed to connect to remote HTTP server"); + } + } + } +} diff --git a/MCPForUnity/Editor/Services/HttpAutoStartHandler.cs.meta b/MCPForUnity/Editor/Services/HttpAutoStartHandler.cs.meta new file mode 100644 index 000000000..29a8a5f9e --- /dev/null +++ b/MCPForUnity/Editor/Services/HttpAutoStartHandler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3d8f1790992fe0742938d8a879056ee6 \ No newline at end of file diff --git a/MCPForUnity/Editor/Services/IServerManagementService.cs b/MCPForUnity/Editor/Services/IServerManagementService.cs index 2da66738f..dea0c6ddd 100644 --- a/MCPForUnity/Editor/Services/IServerManagementService.cs +++ b/MCPForUnity/Editor/Services/IServerManagementService.cs @@ -15,8 +15,9 @@ public interface IServerManagementService /// Start the local HTTP server in a new terminal window. /// Stops any existing server on the port and clears the uvx cache first. /// + /// When true, skip confirmation dialogs (used by auto-start). /// True if server was started successfully, false otherwise - bool StartLocalHttpServer(); + bool StartLocalHttpServer(bool quiet = false); /// /// Stop the local HTTP server by finding the process listening on the configured port diff --git a/MCPForUnity/Editor/Services/ServerManagementService.cs b/MCPForUnity/Editor/Services/ServerManagementService.cs index 0b7799fad..d1f6a0fc2 100644 --- a/MCPForUnity/Editor/Services/ServerManagementService.cs +++ b/MCPForUnity/Editor/Services/ServerManagementService.cs @@ -233,17 +233,20 @@ private string GetPlatformSpecificPathPrepend() /// Start the local HTTP server in a separate terminal window. /// Stops any existing server on the port and clears the uvx cache first. /// - public bool StartLocalHttpServer() + public bool StartLocalHttpServer(bool quiet = false) { /// Clean stale Python build artifacts when using a local dev server path AssetPathUtility.CleanLocalServerBuildArtifacts(); if (!TryGetLocalHttpServerCommandParts(out _, out _, out var displayCommand, out var error)) { - EditorUtility.DisplayDialog( - "Cannot Start HTTP Server", - error ?? "The server command could not be constructed with the current settings.", - "OK"); + if (!quiet) + { + EditorUtility.DisplayDialog( + "Cannot Start HTTP Server", + error ?? "The server command could not be constructed with the current settings.", + "OK"); + } return false; } @@ -259,12 +262,15 @@ public bool StartLocalHttpServer() var remaining = GetListeningProcessIdsForPort(uri.Port); if (remaining.Count > 0) { - EditorUtility.DisplayDialog( - "Port In Use", - $"Cannot start the local HTTP server because port {uri.Port} is already in use by PID(s): " + - $"{string.Join(", ", remaining)}\n\n" + - "MCP For Unity will not terminate unrelated processes. Stop the owning process manually or change the HTTP URL.", - "OK"); + if (!quiet) + { + EditorUtility.DisplayDialog( + "Port In Use", + $"Cannot start the local HTTP server because port {uri.Port} is already in use by PID(s): " + + $"{string.Join(", ", remaining)}\n\n" + + "MCP For Unity will not terminate unrelated processes. Stop the owning process manually or change the HTTP URL.", + "OK"); + } return false; } } @@ -286,50 +292,53 @@ public bool StartLocalHttpServer() launchCommand = $"{displayCommand} --pidfile {QuoteIfNeeded(pidFilePath)} --unity-instance-token {instanceToken}"; } - if (EditorUtility.DisplayDialog( + if (!quiet && !EditorUtility.DisplayDialog( "Start Local HTTP Server", $"This will start the MCP server in HTTP mode in a new terminal window:\n\n{launchCommand}\n\n" + "Continue?", "Start Server", "Cancel")) { + return false; + } + + try + { + // Clear any stale handshake state from prior launches. + ClearLocalServerPidTracking(); + + // Best-effort: delete stale pidfile if it exists. try { - // Clear any stale handshake state from prior launches. - ClearLocalServerPidTracking(); - - // Best-effort: delete stale pidfile if it exists. - try + if (!string.IsNullOrEmpty(pidFilePath) && File.Exists(pidFilePath)) { - if (!string.IsNullOrEmpty(pidFilePath) && File.Exists(pidFilePath)) - { - DeletePidFile(pidFilePath); - } + DeletePidFile(pidFilePath); } - catch { } + } + catch { } - // Launch the server in a new terminal window (keeps user-visible logs). - var startInfo = CreateTerminalProcessStartInfo(launchCommand); - System.Diagnostics.Process.Start(startInfo); - if (!string.IsNullOrEmpty(pidFilePath)) - { - StoreLocalHttpServerHandshake(pidFilePath, instanceToken); - } - McpLog.Info($"Started local HTTP server in terminal: {launchCommand}"); - return true; + // Launch the server in a new terminal window (keeps user-visible logs). + var startInfo = CreateTerminalProcessStartInfo(launchCommand); + System.Diagnostics.Process.Start(startInfo); + if (!string.IsNullOrEmpty(pidFilePath)) + { + StoreLocalHttpServerHandshake(pidFilePath, instanceToken); } - catch (Exception ex) + McpLog.Info($"Started local HTTP server in terminal: {launchCommand}"); + return true; + } + catch (Exception ex) + { + McpLog.Error($"Failed to start server: {ex.Message}"); + if (!quiet) { - McpLog.Error($"Failed to start server: {ex.Message}"); EditorUtility.DisplayDialog( "Error", $"Failed to start server: {ex.Message}", "OK"); - return false; } + return false; } - - return false; } /// diff --git a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs index 0e9674d65..f92f59120 100644 --- a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs +++ b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs @@ -183,7 +183,10 @@ private static bool ShouldAutoStartBridge() try { bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport; - return !useHttpTransport; + if (useHttpTransport) return false; + + bool autoStart = EditorPrefs.GetBool(EditorPrefKeys.AutoStartOnLoad, true); + return autoStart; } catch { diff --git a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs index 89261c074..0bd40840c 100644 --- a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs @@ -24,6 +24,7 @@ public class McpAdvancedSection private TextField gitUrlOverride; private Button browseGitUrlButton; private Button clearGitUrlButton; + private Toggle autoStartOnLoadToggle; private Toggle debugLogsToggle; private Toggle logRecordToggle; private Toggle devModeForceRefreshToggle; @@ -66,6 +67,7 @@ private void CacheUIElements() gitUrlOverride = Root.Q("git-url-override"); browseGitUrlButton = Root.Q