Skip to content
Merged
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
1 change: 1 addition & 0 deletions MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down
175 changes: 175 additions & 0 deletions MCPForUnity/Editor/Services/HttpAutoStartHandler.cs
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");
}
}
}
}
2 changes: 2 additions & 0 deletions MCPForUnity/Editor/Services/HttpAutoStartHandler.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion MCPForUnity/Editor/Services/IServerManagementService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
/// <param name="quiet">When true, skip confirmation dialogs (used by auto-start).</param>
/// <returns>True if server was started successfully, false otherwise</returns>
bool StartLocalHttpServer();
bool StartLocalHttpServer(bool quiet = false);

/// <summary>
/// Stop the local HTTP server by finding the process listening on the configured port
Expand Down
81 changes: 45 additions & 36 deletions MCPForUnity/Editor/Services/ServerManagementService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Comment on lines +243 to +249
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When quiet=true, this returns false without logging error if 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.

Suggested change
if (!quiet)
{
EditorUtility.DisplayDialog(
"Cannot Start HTTP Server",
error ?? "The server command could not be constructed with the current settings.",
"OK");
}
var message = error ?? "The server command could not be constructed with the current settings.";
if (!quiet)
{
EditorUtility.DisplayDialog(
"Cannot Start HTTP Server",
message,
"OK");
}
else
{
Debug.LogError($"Cannot Start HTTP Server: {message}");
}

Copilot uses AI. Check for mistakes.
return false;
}

Expand All @@ -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");
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When quiet=true, a port-in-use failure returns false without any log output. Auto-start will then fail silently aside from a generic message. Consider logging the port and remaining PIDs (or at least that the port is in use) when dialogs are suppressed.

Suggested change
}
}
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.");
}

Copilot uses AI. Check for mistakes.
return false;
}
}
Expand All @@ -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;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,6 +67,7 @@ private void CacheUIElements()
gitUrlOverride = Root.Q<TextField>("git-url-override");
browseGitUrlButton = Root.Q<Button>("browse-git-url-button");
clearGitUrlButton = Root.Q<Button>("clear-git-url-button");
autoStartOnLoadToggle = Root.Q<Toggle>("auto-start-on-load-toggle");
debugLogsToggle = Root.Q<Toggle>("debug-logs-toggle");
logRecordToggle = Root.Q<Toggle>("log-record-toggle");
devModeForceRefreshToggle = Root.Q<Toggle>("dev-mode-force-refresh-toggle");
Expand Down Expand Up @@ -149,6 +151,15 @@ private void InitializeUI()
if (deployRestoreButton != null)
deployRestoreButton.tooltip = "Restore the last backup before deployment";

if (autoStartOnLoadToggle != null)
{
autoStartOnLoadToggle.tooltip = "Automatically start the local HTTP server and connect the MCP bridge when the Unity Editor opens. Only applies to HTTP transport (stdio always auto-starts).";
var autoStartLabel = autoStartOnLoadToggle.parent?.Q<Label>();
if (autoStartLabel != null)
autoStartLabel.tooltip = autoStartOnLoadToggle.tooltip;
autoStartOnLoadToggle.SetValueWithoutNotify(EditorPrefs.GetBool(EditorPrefKeys.AutoStartOnLoad, false));
}

gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");

bool debugEnabled = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
Expand Down Expand Up @@ -219,6 +230,14 @@ private void RegisterCallbacks()
});
}

if (autoStartOnLoadToggle != null)
{
autoStartOnLoadToggle.RegisterValueChangedCallback(evt =>
{
EditorPrefs.SetBool(EditorPrefKeys.AutoStartOnLoad, evt.newValue);
});
}

devModeForceRefreshToggle.RegisterValueChangedCallback(evt =>
{
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, evt.newValue);
Expand Down Expand Up @@ -343,6 +362,8 @@ public void UpdatePathOverrides()
}

gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
if (autoStartOnLoadToggle != null)
autoStartOnLoadToggle.value = EditorPrefs.GetBool(EditorPrefKeys.AutoStartOnLoad, false);
debugLogsToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
if (logRecordToggle != null)
logRecordToggle.value = McpLogRecord.IsEnabled;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
<ui:Button name="test-connection-button" text="Test" class="action-button" />
</ui:VisualElement>

<ui:VisualElement class="setting-row">
<ui:Label text="Auto-Start on Editor Load:" class="setting-label" />
<ui:Toggle name="auto-start-on-load-toggle" class="setting-toggle" />
</ui:VisualElement>

<ui:VisualElement class="setting-row">
<ui:Label text="Force Fresh Install:" class="setting-label" />
<ui:Toggle name="dev-mode-force-refresh-toggle" class="setting-toggle" />
Expand Down