-
Notifications
You must be signed in to change notification settings - Fork 867
auto-start server option #923
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| 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 | ||
| { | ||
| /// <summary> | ||
| /// 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). | ||
| /// </summary> | ||
| [InitializeOnLoad] | ||
| internal static class HttpAutoStartHandler | ||
| { | ||
| private const string SessionInitKey = "HttpAutoStartHandler.SessionInitialized"; | ||
|
|
||
| static HttpAutoStartHandler() | ||
| { | ||
| // SessionState resets on editor process start but persists across domain reloads. | ||
| // Only run once per session — let HttpBridgeReloadHandler handle reload-resume cases. | ||
| if (SessionState.GetBool(SessionInitKey, false)) return; | ||
|
|
||
| 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, false); | ||
| if (!autoStartEnabled) return; | ||
|
|
||
| SessionState.SetBool(SessionInitKey, true); | ||
|
|
||
| // Delay to let the editor and services finish initialization. | ||
| EditorApplication.delayCall += OnEditorReady; | ||
| } | ||
|
|
||
| private static void OnEditorReady() | ||
| { | ||
| try | ||
| { | ||
| bool autoStartEnabled = EditorPrefs.GetBool(EditorPrefKeys.AutoStartOnLoad, false); | ||
| if (!autoStartEnabled) return; | ||
|
|
||
| bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; | ||
| if (!useHttp) 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}"); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Waits for the local HTTP server to accept connections, then connects the bridge. | ||
| /// Mirrors TryAutoStartSessionAsync in McpConnectionSection. | ||
| /// </summary> | ||
| 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, false)) 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"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Connects the bridge directly (for remote HTTP where the server is already running). | ||
| /// </summary> | ||
| 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"); | ||
| } | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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. | ||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||
| 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"); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
||||||||||||||||||||
| } | |
| } | |
| else | |
| { | |
| Debug.LogWarning( | |
| $"[MCP For Unity] Cannot start the local HTTP server because port {uri.Port} is already in use by PID(s): " + | |
| $"{string.Join(\", \", remaining)}. MCP For Unity will not terminate unrelated processes. " + | |
| "Stop the owning process manually or change the HTTP URL."); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
quiet=true, this returnsfalsewithout loggingerrorif the command can’t be constructed. For auto-start, that makes failures hard to diagnose. Consider logging a warning/error with the reason even when dialogs are suppressed.