using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEditor; using UnityEditor.Compilation; using Newtonsoft.Json; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Linq; using SynapticAIPro; // v1.2.23 staged: enableMCP default true, AutoReconnectEnabled property, ConfigureAwait fix. namespace SynapticPro { /// /// Editor-only MCP Service - Independent service that doesn't depend on scenes /// Not affected by PlayMode switching or scene changes /// [InitializeOnLoad] public static class NexusEditorMCPService { private static ClientWebSocket webSocket; private static CancellationTokenSource cancellationTokenSource; private static bool isConnected = false; private static Queue messageQueue = new Queue(); private static string serverUrl = null; // Set dynamically private static bool isInitialized = false; private static bool shouldReconnect = true; private static int reconnectAttempts = 0; private static int maxReconnectAttempts = 5; private static float lastReconnectTime = 0; private static float lastReconnectAttempt = 0f; private const float MIN_RECONNECT_INTERVAL = 10f; // Hard minimum between reconnect attempts // スレッドセーフな時刻取得用 (Time.realtimeSinceStartup はメインスレッドからしか呼べないため、 // OnConnectionLost等の非同期コンテキストからはこちらを使う) // // ⚠️ 重要: 戻り値は Time.realtimeSinceStartup と同一エポック (Editor 起動時起点) に // 揃える必要がある。生の Stopwatch.Elapsed はクラス load 起点でドメインリロード毎に // 0 リセットされるため、Time.realtimeSinceStartup と混在比較すると差分が常に // ~Editor起動時間 となり再接続ループが永久発火する (ESC-0102 真因)。 // 解決: 初回メインスレッド呼び出し時に Time.realtimeSinceStartup でベースラインを記録、 // 以降は (ベースライン + Stopwatch差分) を返す。ベースライン未取得時はメインスレッド // からの呼び出しのみで取得し、それまでは Stopwatch 直値 (フォールバック)。 private static readonly System.Diagnostics.Stopwatch _threadSafeClock = System.Diagnostics.Stopwatch.StartNew(); private static double _threadSafeBaselineOffset = -1.0; private static float ThreadSafeTime() { // Time.realtimeSinceStartup は メインスレッド限定なので、安全側に倒して // Stopwatch 直値を返す。CalibrateThreadSafeTime() がメインスレッドから // 1回呼ばれていればオフセット適用で Time.realtimeSinceStartup と同一エポック。 double elapsed = _threadSafeClock.Elapsed.TotalSeconds; if (_threadSafeBaselineOffset >= 0) { return (float)(_threadSafeBaselineOffset + elapsed); } return (float)elapsed; } private static void CalibrateThreadSafeTime() { // メインスレッドから呼ばれる前提。Time.realtimeSinceStartup を読み、 // Stopwatch 経過分を差し引いてベースラインオフセットを確定。 try { double elapsed = _threadSafeClock.Elapsed.TotalSeconds; _threadSafeBaselineOffset = Time.realtimeSinceStartup - elapsed; } catch { /* Time.realtimeSinceStartup access failure — keep raw stopwatch */ } } private const int CONNECT_TIMEOUT_SECONDS = 5; private static float lastConnectionCheckTime = 0; private static bool isReconnecting = false; private static int reconnectPhase = 0; // 0: Standby, 1: Light test (2s), 2: Full reconnect (5s), 3: Retry on failure (10s) // Play Mode auto-reconnect related private static bool wasConnectedBeforePlayMode = false; private static bool enableAutoReconnect = true; // Master switch for the MCP CLIENT (Unity always connects as client to // the index-supersave.js / hub-server.js running on port 8090 — Unity // is never the host). Default true so the install is functional out of // the box; advanced users can toggle off via the legacy "MCP Server: // Stop" menu (the name is historical, kept for menu-item discoverability). // The HTTP server (port 8086) is independent and unaffected by this flag. private static bool enableMCP = true; private static string connectionStateKey = "NexusMCP_ConnectionState"; private static string autoReconnectKey = "NexusMCP_AutoReconnect"; private const string mcpEnabledKey = "NexusMCP_Enabled"; public static bool IsConnected => isConnected && webSocket != null && webSocket.State == WebSocketState.Open; /// /// Public accessor for the auto-reconnect toggle so the Setup window /// can render a checkbox without going through the menu items. /// Persisted in EditorPrefs under . /// public static bool AutoReconnectEnabled { get => EditorPrefs.GetBool(autoReconnectKey, true); set { enableAutoReconnect = value; EditorPrefs.SetBool(autoReconnectKey, value); // Turning Auto Reconnect ON implies the user wants MCP active. // Without this, the Update() gate would still skip reconnect // because of the separate `enableMCP` master switch — a UX // trap users hit constantly (Auto checkbox looks like it should // be the only switch they need). if (value && !enableMCP) { enableMCP = true; EditorPrefs.SetBool(mcpEnabledKey, true); } } } public static string GetServerUrl() => serverUrl ?? "ws://127.0.0.1:8090"; public static event Action OnMessageReceived; public static event Action OnConnected; public static event Action OnDisconnected; public static event Action OnError; [Serializable] public class MCPMessage { public string type; public string id; public string provider; public string content; public Dictionary parameters; public string tool; public string command; public object data; } static NexusEditorMCPService() { Initialize(); } private static void Initialize() { if (isInitialized) return; SynLog.Info("[Nexus Editor MCP] Initializing Editor MCP Service"); // Auto-detect available port DetectAndSetAvailablePort(); // Subscribe to EditorApplication events EditorApplication.update += Update; EditorApplication.quitting += OnEditorQuitting; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; // Subscribe to compilation-related events CompilationPipeline.compilationStarted += OnCompilationStarted; CompilationPipeline.compilationFinished += OnCompilationFinished; // Load settings. Both default true — Unity is a client of the // MCP server (port 8090) and Auto Reconnect is the normal flow. // Users who don't use MCP can disable via the legacy menu items // or just rely on verbose-log filtering to hide retry noise. enableAutoReconnect = EditorPrefs.GetBool(autoReconnectKey, true); enableMCP = EditorPrefs.GetBool(mcpEnabledKey, true); if (!enableMCP) { SynLog.Info("[Nexus Editor MCP] MCP client disabled (persisted). Use Tools > Synaptic Pro > MCP Server: Start to re-enable."); } // Delayed auto-connect (after 0.1s) - Connect reliably even in closed state EditorApplication.delayCall += () => { RunTracked(Task.Run(async () => { await Task.Delay(100); // Wait 0.1 seconds (connect immediately) if (enableMCP && enableAutoReconnect && !isConnected) { try { await ConnectToMCPServer(); SynLog.Info("[Nexus MCP] 🚀 Initial auto-connect completed"); } catch (Exception e) { SynLog.Warn($"[Nexus MCP] Initial auto-connect failed: {e.Message}"); isConnected = false; // On failure, gradual reconnection takes over OnConnectionLost(); } } }), "InitialAutoConnect"); }; isInitialized = true; SynLog.Info("[Nexus Editor MCP] Service initialized - auto-connection will start immediately"); } /// /// Set server URL to fixed port 8090 (v1.1.0) /// Both index.js and hub-server.js use port 8090 /// private static void DetectAndSetAvailablePort() { try { // Fixed port 8090 for all MCP servers (index.js and hub-server.js) const int mcpPort = 8090; serverUrl = $"ws://localhost:{mcpPort}"; SynLog.Info($"[Nexus Editor MCP] Using MCP server at {serverUrl}"); // Auto-update Claude Desktop settings UpdateClaudeDesktopConfigForPort(mcpPort); } catch (Exception e) { SynLog.Warn($"[Nexus Editor MCP] Setup failed: {e.Message}, using default port"); serverUrl = "ws://127.0.0.1:8090"; } } /// /// Check if MCP server responds on that port /// private static bool IsPortInUse(int port) { try { // Verify MCP server existence with simple TCP connection test using (var client = new System.Net.Sockets.TcpClient()) { var result = client.BeginConnect("localhost", port, null, null); var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(500)); if (success && client.Connected) { client.Close(); return true; // MCP server responded } return false; // Cannot connect } } catch { return false; // Consider unavailable on error } } // HTTP WebSocket auto-connect private static float lastHttpCheckTime = 0; private static bool httpAutoConnecting = false; private static void Update() { // Calibrate ThreadSafeTime on first main-thread tick so it shares the // same epoch as Time.realtimeSinceStartup. Without this the // OnConnectionLost path stores stopwatch-since-classload values that // never match Time.realtimeSinceStartup-since-editor-start, causing // the reconnect gate (currentTime - lastConnectionCheckTime > 2f) to // be permanently true after the first domain reload — root cause of // the v1.2.21 MCP-timeout regression. if (_threadSafeBaselineOffset < 0) CalibrateThreadSafeTime(); // Process messages on main thread while (messageQueue.Count > 0) { var message = messageQueue.Dequeue(); ProcessMessage(message); } // HTTP WebSocket auto-connect: check every 10 seconds float currentTimeForHttp = Time.realtimeSinceStartup; if (!httpAutoConnecting && currentTimeForHttp - lastHttpCheckTime > 10f) { lastHttpCheckTime = currentTimeForHttp; if (!NexusHTTPWebSocketClient.Instance.IsConnected) { // Read EditorPrefs on main thread before Task.Run int httpPort = 8086; try { httpPort = EditorPrefs.GetInt($"SynapticPro_HTTP_Port_{Application.dataPath.GetHashCode():X8}", 8086); } catch { } httpAutoConnecting = true; int portCapture = httpPort; RunTracked(Task.Run(async () => { try { using (var client = new System.Net.WebClient()) { client.Headers.Add("User-Agent", "Unity-AutoConnect"); var response = client.DownloadString($"http://localhost:{portCapture}/health"); if (response.Contains("ok") || response.Contains("Synaptic")) { SynLog.Info($"[Nexus MCP] HTTP Server detected on port {portCapture}, auto-connecting..."); await NexusHTTPWebSocketClient.Instance.Connect(portCapture); } } } catch { } finally { httpAutoConnecting = false; } }), "HttpAutoConnect"); } else { // Already connected, no need to check frequently lastHttpCheckTime = currentTimeForHttp + 50f; // Next check in 60s } } // Gradual reconnection processing if (!enableMCP || !enableAutoReconnect || isReconnecting) { // Set reconnect flag if connection is lost even during compilation if (enableMCP && EditorApplication.isCompiling && !isConnected) { EditorPrefs.SetBool("NexusMCP_NeedsReconnectAfterCompile", true); } return; } float currentTime = Time.realtimeSinceStartup; // Include WebSocket in closed state as reconnection target bool needsReconnection = !isConnected || (webSocket != null && (webSocket.State == WebSocketState.Closed || webSocket.State == WebSocketState.Aborted || webSocket.State == WebSocketState.None)); if (needsReconnection) { // Hard gate: never spawn a reconnect work task more often than MIN_RECONNECT_INTERVAL. if (currentTime - lastReconnectAttempt < MIN_RECONNECT_INTERVAL) { return; } switch (reconnectPhase) { case 0: // After disconnect detection, wait 2 seconds then light test if (currentTime - lastConnectionCheckTime > 2f) { reconnectPhase = 1; lastReconnectTime = currentTime; lastReconnectAttempt = currentTime; RunTracked(Task.Run(LightConnectionTest), "LightConnectionTest"); } break; case 1: // Full reconnect 5 seconds after light test if (currentTime - lastReconnectTime > 5f) { reconnectPhase = 2; lastReconnectTime = currentTime; lastReconnectAttempt = currentTime; RunTracked(Task.Run(FullReconnectAttempt), "FullReconnectAttempt"); } break; case 2: // Retry 10 seconds after full reconnect failure if (currentTime - lastReconnectTime > 10f) { reconnectPhase = 3; lastReconnectTime = currentTime; lastReconnectAttempt = currentTime; RunTracked(Task.Run(RetryReconnect), "RetryReconnect"); } break; case 3: // Wait another 10 seconds after retry, then return to phase 1 if (currentTime - lastReconnectTime > 10f) { reconnectPhase = 1; lastReconnectTime = currentTime; } break; } } else { // Reset phase on successful connection reconnectPhase = 0; // Check WebSocket state even while connected (start reconnection immediately if Closed) if (webSocket != null && webSocket.State == WebSocketState.Closed) { SynLog.Info("[Nexus MCP] WebSocket closed state detected, starting reconnection"); OnConnectionLost(); } } } private static void OnEditorQuitting() { SynLog.Info("[Nexus Editor MCP] Editor quitting, disconnecting MCP service"); DisconnectFromMCPServer(); // Unsubscribe from events EditorApplication.update -= Update; EditorApplication.quitting -= OnEditorQuitting; EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; CompilationPipeline.compilationFinished -= OnCompilationFinished; } private static void OnPlayModeStateChanged(PlayModeStateChange state) { if (!enableAutoReconnect) return; switch (state) { case PlayModeStateChange.ExitingEditMode: // When exiting Edit Mode - save connection state wasConnectedBeforePlayMode = IsConnected; if (wasConnectedBeforePlayMode) { SynLog.Info("[Nexus Editor MCP] 🎮 Transitioning to Play Mode. Connection state saved."); EditorPrefs.SetBool(connectionStateKey, true); EditorPrefs.SetString(connectionStateKey + "_ServerUrl", serverUrl); } break; case PlayModeStateChange.EnteredPlayMode: SynLog.Info("[Nexus Editor MCP] 🎮 Entered Play Mode. Basic tools are available but editing functions are restricted."); // Reconnect if connection lost even in Play Mode if (wasConnectedBeforePlayMode && !IsConnected) { SynLog.Info("[Nexus Editor MCP] 🔄 Connection lost in Play Mode. Reconnecting..."); EditorApplication.delayCall += () => { RunTracked(Task.Run(async () => await ConnectToMCPServer()), "PlayModeReconnect"); }; } break; case PlayModeStateChange.ExitingPlayMode: SynLog.Info("[Nexus Editor MCP] ⏹️ Exiting Play Mode..."); // Save current connection state before exiting Play Mode if (IsConnected) { EditorPrefs.SetBool(connectionStateKey, true); EditorPrefs.SetString(connectionStateKey + "_ServerUrl", serverUrl); } break; case PlayModeStateChange.EnteredEditMode: // When returning to Edit Mode - auto-reconnect if (EditorPrefs.GetBool(connectionStateKey, false)) { var savedServerUrl = EditorPrefs.GetString(connectionStateKey + "_ServerUrl", ""); if (!string.IsNullOrEmpty(savedServerUrl)) { serverUrl = savedServerUrl; } SynLog.Info("[Nexus Editor MCP] ⏹️ Play Mode ended. Starting auto-reconnect..."); // Reconnect with slight delay EditorApplication.delayCall += () => { RunTracked(Task.Run(async () => await ConnectToMCPServer()), "ExitPlayModeReconnect"); EditorPrefs.DeleteKey(connectionStateKey); EditorPrefs.DeleteKey(connectionStateKey + "_ServerUrl"); }; } break; } } /// /// Handler for compilation start /// private static void OnCompilationStarted(object context) { SynLog.Info("[Nexus MCP] 🔨 Compilation start detected"); // Keep current connection as compilation errors may occur if (isConnected) { EditorPrefs.SetBool("NexusMCP_WasConnectedBeforeCompile", true); } } /// /// Handler for compilation completion /// private static void OnCompilationFinished(object context) { if (!enableMCP || !enableAutoReconnect) return; // In Unity 2022.3, CompilationPipeline.compilationFinished event is passed as object type // Need to check for errors using a different method bool hasErrors = UnityEditor.EditorUtility.scriptCompilationFailed; if (hasErrors) { Debug.LogError("[Nexus MCP] ❌ Compilation errors detected"); } SynLog.Info($"[Nexus MCP] 🔨 Compilation completed (Errors: {hasErrors}) - Starting fast reconnection"); // Reconnect 0.5 seconds after compilation completion (including when errors exist) EditorApplication.delayCall += () => { // Attempt reconnection if previously connected, even with compilation errors bool wasConnectedBeforeCompile = EditorPrefs.GetBool("NexusMCP_WasConnectedBeforeCompile", false); if (wasConnectedBeforeCompile || !isConnected || (webSocket != null && webSocket.State != WebSocketState.Open)) { // Reset current reconnection phase and reconnect immediately reconnectPhase = 0; lastConnectionCheckTime = 0; lastReconnectTime = 0; // Execute fast reconnection EditorApplication.delayCall += async () => { try { SynLog.Info("[Nexus MCP] 🔄 Starting post-compilation reconnection..."); await ConnectToMCPServer(); SynLog.Info("[Nexus MCP] ⚡ Fast reconnection after compilation successful!"); if (hasErrors) { SynLog.Warn("[Nexus MCP] ⚠️ Compilation had errors but connection was restored"); } } catch (Exception e) { SynLog.Warn($"[Nexus MCP] Post-compilation reconnection failed: {e.Message}"); // Fall back to normal gradual reconnection on failure OnConnectionLost(); } finally { // Clear flag EditorPrefs.DeleteKey("NexusMCP_WasConnectedBeforeCompile"); } }; } }; } public static async Task ConnectToMCPServer() { try { if (isConnected) { SynLog.Info("[Nexus Editor MCP] Already connected"); return; } // Re-detect if serverUrl is null if (serverUrl == null) { DetectAndSetAvailablePort(); } // Only log first attempt and every 5th attempt to reduce console noise if (reconnectAttempts == 0 || reconnectAttempts % 5 == 0) { SynLog.Info($"[Nexus MCP] Connecting to {serverUrl}... (Attempt {reconnectAttempts + 1}/{maxReconnectAttempts})"); } // Clean up existing connection if (webSocket != null) { webSocket.Dispose(); } if (cancellationTokenSource != null) { cancellationTokenSource.Cancel(); cancellationTokenSource.Dispose(); } webSocket = new ClientWebSocket(); webSocket.Options.SetRequestHeader("x-client-type", "unity"); // Identify Unity client to MCP server cancellationTokenSource = new CancellationTokenSource(); // Bound the connect handshake so an unreachable host cannot hang us for 30+s. using (var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource.Token)) { connectCts.CancelAfter(TimeSpan.FromSeconds(CONNECT_TIMEOUT_SECONDS)); await webSocket.ConnectAsync(new Uri(serverUrl), connectCts.Token); } SynLog.Info($"[Nexus Editor MCP] WebSocket State after connect: {webSocket.State}"); isConnected = true; reconnectAttempts = 0; // Reset on success // Successful connection => MCP is in use. Persist so the next // domain reload doesn't wake up with enableMCP=false (default) // and silently skip auto-reconnect. The user only opts OUT // explicitly via `Tools > Synaptic Pro > MCP Server: Stop`. if (!enableMCP) { enableMCP = true; EditorPrefs.SetBool(mcpEnabledKey, true); } OnConnected?.Invoke(); SynLog.Info("[Nexus Editor MCP] Connected to MCP Server successfully"); // Start message listener first to avoid race conditions on some platforms RunTracked(Task.Run(async () => await ListenForMessages()), "ListenForMessages"); // Wait for listener to initialize before sending ping (fixes Windows WebSocket Aborted issue) await Task.Delay(200); // Send connection confirmation message await SendConnectionPing(); } catch (Exception e) { reconnectAttempts++; // Only log every 5th failure to reduce console noise if (reconnectAttempts == 1 || reconnectAttempts % 5 == 0) { SynLog.Warn($"[Nexus MCP] Connection failed (attempt {reconnectAttempts}/{maxReconnectAttempts}): {e.Message}"); } OnError?.Invoke(e.Message); isConnected = false; // Start auto-reconnect on connection failure OnConnectionLost(); if (reconnectAttempts >= maxReconnectAttempts) { SynLog.Warn("[Nexus MCP] Cannot connect to MCP server. Make sure Claude Desktop, Cursor, or another MCP client is running.\nUse Tools > Synaptic Pro > AI Reconnect to retry."); // Reset attempts to allow future retries reconnectAttempts = 0; } } } private static async Task ListenForMessages() { SynLog.Info("[Nexus Editor MCP] Starting message listener"); var buffer = new byte[1024 * 16]; // Increased buffer size // Snapshot the connection so a concurrent Connect() reassigning the // static webSocket/cancellationTokenSource fields cannot make this // loop silently read from a different (live) socket while pointing // metadata at a stale one. var ws = webSocket; var cts = cancellationTokenSource; if (ws == null) return; try { while (ws.State == WebSocketState.Open && !cts.Token.IsCancellationRequested) { var messageBuffer = new List(); WebSocketReceiveResult result; // Loop until entire message is received do { var segment = new ArraySegment(buffer); result = await ws.ReceiveAsync(segment, cts.Token); if (result.Count > 0) { messageBuffer.AddRange(buffer.Take(result.Count)); } } while (!result.EndOfMessage); if (result.MessageType == WebSocketMessageType.Text && messageBuffer.Count > 0) { var messageText = Encoding.UTF8.GetString(messageBuffer.ToArray()); SynLog.Info($"[Nexus Editor MCP] ⚡ RAW MESSAGE RECEIVED: {messageText}"); try { var message = JsonConvert.DeserializeObject(messageText); if (message != null) { // Add to queue for processing on main thread messageQueue.Enqueue(message); } } catch (Exception e) { Debug.LogError($"[Nexus Editor MCP] Failed to parse message: {e.Message}"); } } else if (result.MessageType == WebSocketMessageType.Close) { SynLog.Info("[Nexus Editor MCP] WebSocket closed by server"); break; } } } catch (Exception e) { var wsException = e as WebSocketException; string state = webSocket?.State.ToString() ?? "null"; string closeStatus = webSocket?.CloseStatus?.ToString() ?? "none"; string closeDescription = webSocket?.CloseStatusDescription ?? "n/a"; string errorCode = wsException?.WebSocketErrorCode.ToString() ?? "n/a"; string nativeError = wsException?.NativeErrorCode.ToString() ?? "n/a"; Debug.LogError( $"[Nexus Editor MCP] Message listener error: {e.Message} " + $"(State: {state}, CloseStatus: {closeStatus}, CloseDesc: {closeDescription}, " + $"WebSocketError: {errorCode}, NativeError: {nativeError})\n{e}"); // Attempt auto-reconnect if WebSocket exception if (e is WebSocketException || e.Message.Contains("WebSocket")) { SynLog.Info("[Nexus Editor MCP] WebSocket error detected, will attempt reconnection"); } } finally { // Only process disconnection if WebSocket is not properly closed if (webSocket?.State != WebSocketState.Open) { OnConnectionLost(); OnDisconnected?.Invoke(); } } } private static void ProcessMessage(MCPMessage message) { SynLog.Info($"[Nexus Editor MCP] Processing message type: {message.type}, tool: {message.tool}, command: {message.command}"); switch (message.type) { case "unity_operation": case "tool_call": ExecuteUnityOperation(message); break; case "ai_response": OnMessageReceived?.Invoke(message.content); break; case "error": SynLog.Warn($"[Nexus Editor MCP] {message.content}"); OnMessageReceived?.Invoke($"❗ {message.content}"); break; default: SynLog.Info($"[Nexus Editor MCP] Unknown message type: {message.type}"); break; } } private static void ExecuteUnityOperation(MCPMessage message) { SynLog.Info($"[Nexus Editor MCP] Executing Unity operation: {message.tool} with command: {message.command}"); try { // Map MCP tool name to Unity operation string operationType = message.command ?? message.tool ?? ""; // Convert tool name to existing operation type operationType = ConvertMCPToolToOperation(operationType); SynLog.Info($"[Nexus Editor MCP] Converted operation type: {operationType}"); var operation = new NexusUnityOperation { type = operationType, parameters = new Dictionary() }; // Parameter conversion if (message.parameters != null) { foreach (var kvp in message.parameters) { if (kvp.Value != null) { // Processing nested objects if (kvp.Value is Newtonsoft.Json.Linq.JArray jArray) { // Keep array as JSON string (for search tools) operation.parameters[kvp.Key] = jArray.ToString(Newtonsoft.Json.Formatting.None); } else if (kvp.Value is Newtonsoft.Json.Linq.JObject jObj) { // Processing structs like Vector3 if (jObj.ContainsKey("x") && jObj.ContainsKey("y") && jObj.ContainsKey("z")) { operation.parameters[kvp.Key] = $"{jObj["x"]},{jObj["y"]},{jObj["z"]}"; } else if (jObj.ContainsKey("x") && jObj.ContainsKey("y")) { operation.parameters[kvp.Key] = $"{jObj["x"]},{jObj["y"]}"; } else if (jObj.ContainsKey("r") && jObj.ContainsKey("g") && jObj.ContainsKey("b")) { operation.parameters[kvp.Key] = $"{jObj["r"]},{jObj["g"]},{jObj["b"]}"; } else { operation.parameters[kvp.Key] = jObj.ToString(); } } else { operation.parameters[kvp.Key] = kvp.Value.ToString(); } } } } SynLog.Info($"[Nexus Editor MCP] About to execute operation with parameters: {operation.parameters.Count}"); foreach (var param in operation.parameters) { SynLog.Info($"[Nexus Editor MCP] Parameter: {param.Key} = '{param.Value}'"); } // Get message ID from either message.id or parameters.operationId string messageId = message.id; if (string.IsNullOrEmpty(messageId) && message.parameters != null && message.parameters.ContainsKey("operationId")) { messageId = message.parameters["operationId"]?.ToString(); } // Execute Unity operation (synchronous execution on main thread) ExecuteOperationAsync(operation, messageId); } catch (Exception e) { Debug.LogError($"[Nexus Editor MCP] Unity operation error: {e.Message}"); _ = SendOperationResult(message.id, false, $"Error: {e.Message}"); } } private static async void ExecuteOperationAsync(NexusUnityOperation operation, string messageId) { try { var executor = new NexusUnityExecutor(); string result = await executor.ExecuteOperation(operation); bool success = !result.StartsWith("Error:") && !result.StartsWith("Failed:"); SynLog.Info($"[Nexus Editor MCP] Operation result: {result}"); SynLog.Info($"[Nexus Editor MCP] Operation success: {success}"); // Send result to MCP server await SendOperationResult(messageId, success, result); // Output result to log if (success) { SynLog.Info($"[Nexus Editor MCP] SUCCESS: {result}"); } else { Debug.LogError($"[Nexus Editor MCP] FAILED: {result}"); } } catch (Exception e) { Debug.LogError($"[Nexus Editor MCP] Async operation error: {e.Message}"); await SendOperationResult(messageId, false, $"Error: {e.Message}"); } } private static async Task SendConnectionPing() { try { var pingMessage = new MCPMessage { type = "ping", id = Guid.NewGuid().ToString(), content = "Unity Editor connected" }; var json = JsonConvert.SerializeObject(pingMessage); var buffer = Encoding.UTF8.GetBytes(json); await webSocket.SendAsync( new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None ); SynLog.Info("[Nexus Editor MCP] Sent connection ping"); } catch (Exception e) { SynLog.Warn($"[Nexus Editor MCP] Failed to send ping: {e.Message}"); } } private static async Task SendOperationResult(string messageId, bool success, string result) { if (!IsConnected) return; object structuredData = null; string displayContent = result; // Attempt JSON parsing and send as structured data try { // Send as structured data if result is JSON if (result.TrimStart().StartsWith("{") || result.TrimStart().StartsWith("[")) { structuredData = JsonConvert.DeserializeObject(result); displayContent = success ? "Structured data retrieved" : result; } } catch (Exception e) { SynLog.Warn($"[Nexus Editor MCP] JSON parse failed: {e.Message}"); } // Store result in content field according to MCP protocol var response = new MCPMessage { type = "operation_result", id = messageId, content = result, // Return original result (JSON string) as is data = new { success = success } }; try { var json = JsonConvert.SerializeObject(response, Formatting.Indented); var buffer = Encoding.UTF8.GetBytes(json); await webSocket.SendAsync( new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationTokenSource.Token ); SynLog.Info($"[Nexus Editor MCP] Sent operation result: {success}"); SynLog.Info($"[Nexus Editor MCP] Response JSON: {json}"); } catch (Exception e) { Debug.LogError($"[Nexus Editor MCP] Failed to send operation result: {e.Message}"); } } public static void DisconnectFromMCPServer() { try { shouldReconnect = false; // Stop auto-reconnect on manual disconnect if (webSocket != null && isConnected) { isConnected = false; cancellationTokenSource?.Cancel(); if (webSocket.State == WebSocketState.Open) { webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", CancellationToken.None); } webSocket.Dispose(); webSocket = null; OnDisconnected?.Invoke(); SynLog.Info("[Nexus Editor MCP] Disconnected from MCP Server"); } } catch (Exception e) { Debug.LogError($"[Nexus Editor MCP] Error during disconnect: {e.Message}"); } } public static void SetServerUrl(string url) { serverUrl = url; SynLog.Info($"[Nexus Editor MCP] Server URL changed to: {url}"); } public static async void ReconnectToMCPServer() { shouldReconnect = true; // Enable reconnection reconnectAttempts = 0; // Reset counter // Manual reconnect implies the user wants MCP enabled. Without this, // auto-reconnect after the next domain reload silently no-ops because // the Update() gate checks `enableMCP`, which defaults to false and // is only ever flipped on via `Tools > Synaptic Pro > MCP Server: Start`. // Users hit this when they install the package, never see that menu // item, and assume the Auto Reconnect checkbox is the master switch. enableMCP = true; EditorPrefs.SetBool(mcpEnabledKey, true); DisconnectFromMCPServer(); shouldReconnect = true; // Re-enable as Disconnect sets it to false await Task.Delay(1000); // Wait 1 second await ConnectToMCPServer(); } public static string ConvertMCPToolToOperation(string mcpTool) { switch (mcpTool) { // GameObject operations case "unity_create_gameobject": case "create_gameobject": return "CREATE_GAMEOBJECT"; case "unity_update_gameobject": case "update_gameobject": return "UPDATE_GAMEOBJECT"; case "unity_delete_gameobject": case "delete_gameobject": return "DELETE_GAMEOBJECT"; case "unity_set_transform": case "set_transform": return "SET_TRANSFORM"; // Components case "unity_add_component": case "add_component": return "ADD_COMPONENT"; case "unity_update_component": case "update_component": return "UPDATE_COMPONENT"; // UI case "unity_create_ui": case "create_ui": return "CREATE_UI"; // Terrain case "unity_create_terrain": case "create_terrain": return "CREATE_TERRAIN"; case "unity_modify_terrain": case "modify_terrain": return "MODIFY_TERRAIN"; // Camera case "unity_setup_camera": case "setup_camera": return "SETUP_CAMERA"; // Cinemachine case "unity_create_virtual_camera": case "create_virtual_camera": return "CREATE_VIRTUAL_CAMERA"; case "unity_create_freelook_camera": case "create_freelook_camera": return "CREATE_FREELOOK_CAMERA"; case "unity_setup_cinemachine_brain": case "setup_cinemachine_brain": return "SETUP_CINEMACHINE_BRAIN"; case "unity_update_virtual_camera": case "update_virtual_camera": return "UPDATE_VIRTUAL_CAMERA"; case "unity_create_dolly_track": case "create_dolly_track": return "CREATE_DOLLY_TRACK"; case "unity_add_collider_extension": case "add_collider_extension": return "ADD_COLLIDER_EXTENSION"; case "unity_add_confiner_extension": case "add_confiner_extension": return "ADD_CONFINER_EXTENSION"; case "unity_create_state_driven_camera": case "create_state_driven_camera": return "CREATE_STATE_DRIVEN_CAMERA"; case "unity_create_clear_shot_camera": case "create_clear_shot_camera": return "CREATE_CLEAR_SHOT_CAMERA"; case "unity_create_impulse_source": case "create_impulse_source": return "CREATE_IMPULSE_SOURCE"; case "unity_add_impulse_listener": case "add_impulse_listener": return "ADD_IMPULSE_LISTENER"; case "unity_create_blend_list_camera": case "create_blend_list_camera": return "CREATE_BLEND_LIST_CAMERA"; case "unity_create_target_group": case "create_target_group": return "CREATE_TARGET_GROUP"; case "unity_add_target_to_group": case "add_target_to_group": return "ADD_TARGET_TO_GROUP"; case "unity_set_camera_priority": case "set_camera_priority": return "SET_CAMERA_PRIORITY"; case "unity_set_camera_enabled": case "set_camera_enabled": return "SET_CAMERA_ENABLED"; case "unity_create_mixing_camera": case "create_mixing_camera": return "CREATE_MIXING_CAMERA"; case "unity_update_camera_target": case "update_camera_target": return "UPDATE_CAMERA_TARGET"; case "unity_update_brain_blend_settings": case "update_brain_blend_settings": return "UPDATE_BRAIN_BLEND_SETTINGS"; case "unity_get_active_camera_info": case "get_active_camera_info": return "GET_ACTIVE_CAMERA_INFO"; // Placement case "unity_place_objects": case "place_objects": return "PLACE_OBJECTS"; // Lighting case "unity_setup_lighting": case "setup_lighting": return "SETUP_LIGHTING"; // Material case "unity_create_material": case "create_material": return "CREATE_MATERIAL"; // Prefab case "unity_create_prefab": case "create_prefab": return "CREATE_PREFAB"; // Script case "unity_create_script": case "create_script": return "CREATE_SCRIPT"; // Scene case "unity_manage_scene": case "manage_scene": return "MANAGE_SCENE"; // Animation case "unity_create_animation": case "create_animation": return "CREATE_ANIMATION"; // Physics case "unity_setup_physics": case "setup_physics": return "SETUP_PHYSICS"; // Particle/VFX case "unity_create_particle_system": case "create_particle_system": return "CREATE_PARTICLE_SYSTEM"; case "unity_create_vfx_graph": case "create_vfx_graph": return "CREATE_VFX_GRAPH"; case "unity_create_shader_graph": case "create_shader_graph": return "CREATE_SHADER_GRAPH"; case "unity_setup_post_processing": case "setup_post_processing": return "SETUP_POST_PROCESSING"; case "unity_setup_lighting_scenarios": case "setup_lighting_scenarios": return "SETUP_LIGHTING_SCENARIOS"; case "unity_set_vfx_property": case "set_vfx_property": return "SET_VFX_PROPERTY"; case "unity_get_vfx_properties": case "get_vfx_properties": return "GET_VFX_PROPERTIES"; case "unity_trigger_vfx_event": case "trigger_vfx_event": return "TRIGGER_VFX_EVENT"; // VFX Graph Builder API case "unity_vfx_create": case "vfx_create": return "VFX_CREATE"; case "unity_vfx_add_context": case "vfx_add_context": return "VFX_ADD_CONTEXT"; case "unity_vfx_add_block": case "vfx_add_block": return "VFX_ADD_BLOCK"; case "unity_vfx_add_operator": case "vfx_add_operator": return "VFX_ADD_OPERATOR"; case "unity_vfx_link_contexts": case "vfx_link_contexts": return "VFX_LINK_CONTEXTS"; case "unity_vfx_get_structure": case "vfx_get_structure": return "VFX_GET_STRUCTURE"; case "unity_vfx_compile": case "vfx_compile": return "VFX_COMPILE"; case "unity_vfx_get_available_types": case "vfx_get_available_types": return "VFX_GET_AVAILABLE_TYPES"; case "unity_vfx_add_parameter": case "vfx_add_parameter": return "VFX_ADD_PARAMETER"; case "unity_vfx_connect_slots": case "vfx_connect_slots": return "VFX_CONNECT_SLOTS"; case "unity_vfx_set_attribute": case "vfx_set_attribute": return "VFX_SET_ATTRIBUTE"; case "unity_vfx_create_preset": case "vfx_create_preset": return "VFX_CREATE_PRESET"; // Navigation case "unity_setup_navmesh": case "setup_navmesh": return "SETUP_NAVMESH"; // Audio case "unity_create_audio_mixer": case "create_audio_mixer": return "CREATE_AUDIO_MIXER"; // Operation History/Undo/Redo case "unity_get_operation_history": return "GET_OPERATION_HISTORY"; case "unity_undo_operation": return "UNDO_OPERATION"; case "unity_redo_operation": return "REDO_OPERATION"; case "unity_create_checkpoint": return "CREATE_CHECKPOINT"; case "unity_restore_checkpoint": return "RESTORE_CHECKPOINT"; // Real-time Event Monitoring case "unity_monitor_play_state": return "MONITOR_PLAY_STATE"; case "unity_monitor_file_changes": return "MONITOR_FILE_CHANGES"; case "unity_monitor_compile": return "MONITOR_COMPILE"; case "unity_subscribe_events": return "SUBSCRIBE_EVENTS"; case "unity_get_events": return "GET_EVENTS"; case "unity_get_monitoring_status": return "GET_MONITORING_STATUS"; // Project Settings case "unity_get_build_settings": return "GET_BUILD_SETTINGS"; case "unity_get_player_settings": return "GET_PLAYER_SETTINGS"; case "unity_get_quality_settings": return "GET_QUALITY_SETTINGS"; case "unity_get_input_settings": return "GET_INPUT_SETTINGS"; case "unity_get_physics_settings": return "GET_PHYSICS_SETTINGS"; case "unity_get_project_summary": return "GET_PROJECT_SUMMARY"; // Scene Information case "unity_get_scene_info": return "GET_SCENE_INFO"; // Screenshot Capture Tools case "unity_capture_game_view": case "capture_game_view": return "CAPTURE_GAME_VIEW"; case "unity_capture_scene_view": case "capture_scene_view": return "CAPTURE_SCENE_VIEW"; case "unity_capture_region": case "capture_region": return "CAPTURE_REGION"; // Asset and Script Management case "unity_force_refresh_assets": case "force_refresh_assets": return "FORCE_REFRESH_ASSETS"; case "unity_invoke_context_menu": case "invoke_context_menu": return "INVOKE_CONTEXT_MENU"; // Inspector Information case "unity_get_inspector_info": return "GET_INSPECTOR_INFO"; case "unity_get_selected_object_info": return "GET_SELECTED_OBJECT_INFO"; case "unity_get_component_details": return "GET_COMPONENT_DETAILS"; // Asset Management case "unity_list_assets": case "unity_list_project_assets": case "list_assets": return "LIST_ASSETS"; // Folder Management case "unity_check_folder": case "check_folder": return "CHECK_FOLDER"; case "unity_create_folder": case "create_folder": return "CREATE_FOLDER"; case "unity_list_folders": case "list_folders": return "LIST_FOLDERS"; // New Tool Set case "unity_duplicate_gameobject": case "duplicate_gameobject": return "DUPLICATE_GAMEOBJECT"; case "unity_find_gameobjects_by_component": case "find_gameobjects_by_component": case "find_by_component": return "FIND_BY_COMPONENT"; case "unity_cleanup_empty_objects": case "cleanup_empty_objects": return "CLEANUP_EMPTY_OBJECTS"; case "unity_group_gameobjects": case "group_gameobjects": return "GROUP_GAMEOBJECTS"; case "unity_rename_asset": case "rename_asset": return "RENAME_ASSET"; case "unity_move_asset": case "move_asset": return "MOVE_ASSET"; case "unity_delete_asset": case "delete_asset": return "DELETE_ASSET"; case "unity_pause_scene": case "pause_scene": return "PAUSE_SCENE"; case "unity_optimize_textures_batch": case "optimize_textures_batch": return "OPTIMIZE_TEXTURES_BATCH"; case "unity_analyze_draw_calls": case "analyze_draw_calls": return "ANALYZE_DRAW_CALLS"; case "unity_create_project_snapshot": case "create_project_snapshot": return "CREATE_PROJECT_SNAPSHOT"; case "unity_analyze_dependencies": case "analyze_dependencies": return "ANALYZE_DEPENDENCIES"; case "unity_export_project_structure": case "export_project_structure": return "EXPORT_PROJECT_STRUCTURE"; case "unity_validate_naming_conventions": case "validate_naming_conventions": return "VALIDATE_NAMING_CONVENTIONS"; case "unity_extract_all_text": case "extract_all_text": return "EXTRACT_ALL_TEXT"; case "unity_batch_rename": case "batch_rename": return "BATCH_RENAME"; case "unity_batch_import_settings": case "batch_import_settings": return "BATCH_IMPORT_SETTINGS"; case "unity_batch_prefab_update": case "batch_prefab_update": return "BATCH_PREFAB_UPDATE"; case "unity_find_unused_assets": case "find_unused_assets": return "FIND_UNUSED_ASSETS"; case "unity_estimate_build_size": case "estimate_build_size": return "ESTIMATE_BUILD_SIZE"; case "unity_performance_report": case "performance_report": return "PERFORMANCE_REPORT"; case "unity_auto_organize_folders": case "auto_organize_folders": return "AUTO_ORGANIZE_FOLDERS"; case "unity_generate_lod": case "generate_lod": return "GENERATE_LOD"; case "unity_auto_atlas_textures": case "auto_atlas_textures": return "AUTO_ATLAS_TEXTURES"; // Game development features case "unity_create_game_controller": case "create_game_controller": return "CREATE_GAME_CONTROLLER"; case "unity_setup_input_system": case "setup_input_system": return "SETUP_INPUT_SYSTEM"; case "unity_create_state_machine": case "create_state_machine": return "CREATE_STATE_MACHINE"; case "unity_setup_inventory_system": case "setup_inventory_system": return "SETUP_INVENTORY_SYSTEM"; // Prototyping features case "unity_create_game_template": case "create_game_template": return "CREATE_GAME_TEMPLATE"; case "unity_quick_prototype": case "quick_prototype": return "QUICK_PROTOTYPE"; // AI and machine learning case "unity_setup_ml_agent": case "setup_ml_agent": return "SETUP_ML_AGENT"; case "unity_create_neural_network": case "create_neural_network": return "CREATE_NEURAL_NETWORK"; case "unity_setup_behavior_tree": case "setup_behavior_tree": return "SETUP_BEHAVIOR_TREE"; case "unity_create_ai_pathfinding": case "create_ai_pathfinding": return "CREATE_AI_PATHFINDING"; // Script editing features case "unity_modify_script": case "modify_script": return "MODIFY_SCRIPT"; case "unity_edit_script_line": case "edit_script_line": return "EDIT_SCRIPT_LINE"; case "unity_add_script_method": case "add_script_method": return "ADD_SCRIPT_METHOD"; case "unity_update_script_variable": case "update_script_variable": return "UPDATE_SCRIPT_VARIABLE"; // Debug and test tools case "unity_control_game_speed": case "control_game_speed": return "CONTROL_GAME_SPEED"; case "unity_profile_performance": case "profile_performance": return "PROFILE_PERFORMANCE"; case "unity_debug_draw": case "debug_draw": return "DEBUG_DRAW"; case "unity_run_tests": case "run_unity_tests": return "RUN_UNITY_TESTS"; case "unity_manage_breakpoints": case "manage_breakpoints": return "MANAGE_BREAKPOINTS"; // Animation tools case "unity_create_animator_controller": case "create_animator_controller": return "CREATE_ANIMATOR_CONTROLLER"; case "unity_add_animation_state": case "add_animation_state": return "ADD_ANIMATION_STATE"; case "unity_create_animation_clip": case "create_animation_clip": return "CREATE_ANIMATION_CLIP"; case "unity_setup_blend_tree": case "setup_blend_tree": return "SETUP_BLEND_TREE"; case "unity_add_animation_transition": case "add_animation_transition": return "ADD_ANIMATION_TRANSITION"; case "unity_setup_animation_layer": case "setup_animation_layer": return "SETUP_ANIMATION_LAYER"; case "unity_create_animation_event": case "create_animation_event": return "CREATE_ANIMATION_EVENT"; case "unity_setup_avatar": case "setup_avatar": return "SETUP_AVATAR"; case "unity_create_timeline": case "create_timeline": return "CREATE_TIMELINE"; case "unity_bake_animation": case "bake_animation": return "BAKE_ANIMATION"; // Others case "unity_search": case "search_objects": return "SEARCH_OBJECTS"; case "unity_console": case "console_operation": return "CONSOLE_OPERATION"; case "unity_analyze_console_logs": case "analyze_console_logs": return "ANALYZE_CONSOLE_LOGS"; // Code search and exploration tools case "unity_grep_scripts": case "grep_scripts": return "GREP_SCRIPTS"; case "unity_read_script_range": case "read_script_range": return "READ_SCRIPT_RANGE"; case "unity_search_code": case "search_code": return "SEARCH_CODE"; case "unity_list_script_files": case "list_script_files": return "LIST_SCRIPT_FILES"; // Scene manipulation tools case "unity_load_scene": case "load_scene": return "LOAD_SCENE"; case "unity_unload_scene": case "unload_scene": return "UNLOAD_SCENE"; case "unity_set_active_scene": case "set_active_scene": return "SET_ACTIVE_SCENE"; case "unity_list_all_scenes": case "list_all_scenes": return "LIST_ALL_SCENES"; case "unity_add_scene_to_build": case "add_scene_to_build": return "ADD_SCENE_TO_BUILD"; // Asset search tools case "unity_search_prefabs_by_component": case "search_prefabs_by_component": return "SEARCH_PREFABS_BY_COMPONENT"; case "unity_find_material_usage": case "find_material_usage": return "FIND_MATERIAL_USAGE"; case "unity_find_texture_usage": case "find_texture_usage": return "FIND_TEXTURE_USAGE"; case "unity_get_asset_dependencies": case "get_asset_dependencies": return "GET_ASSET_DEPENDENCIES"; case "unity_find_missing_references": case "find_missing_references": return "FIND_MISSING_REFERENCES"; // Skybox case "unity_create_skybox_from_image": case "create_skybox_from_image": return "CREATE_SKYBOX_FROM_IMAGE"; case "unity_create_skybox_blend": case "create_skybox_blend": return "CREATE_SKYBOX_BLEND"; // Dynamic Meta-Tools case "unity_dynamic_inspect": case "dynamic_inspect": case "inspect": return "DYNAMIC_INSPECT"; case "unity_dynamic_modify": case "dynamic_modify": case "modify": return "DYNAMIC_MODIFY"; case "unity_dynamic_create": case "dynamic_create": case "create": return "DYNAMIC_CREATE"; case "unity_execute_batch": case "execute_batch": return "EXECUTE_BATCH"; default: // Strip unity_ prefix if present and convert to uppercase if (mcpTool.StartsWith("unity_")) return mcpTool.Substring(6).ToUpper(); return mcpTool.ToUpper(); } } /// /// Start MCP: enable auto-connect and attempt connection now. /// Persisted — MCP will auto-connect on future editor sessions. /// HTTP server client is independent of this toggle. /// [MenuItem("Tools/Synaptic Pro/MCP Server: Start", false, 10)] public static void StartMCP() { enableMCP = true; EditorPrefs.SetBool(mcpEnabledKey, true); SynLog.Info("[Nexus Editor MCP] ▶️ MCP enabled (persisted). Connecting..."); // Reset reconnect state so the first attempt fires immediately. reconnectPhase = 0; lastReconnectTime = 0f; lastReconnectAttempt = 0f; lastConnectionCheckTime = 0f; if (!isConnected) { RunTracked(Task.Run(async () => await ConnectToMCPServer()), "StartMCP"); } } [MenuItem("Tools/Synaptic Pro/MCP Server: Start", true)] public static bool StartMCPValidate() { enableMCP = EditorPrefs.GetBool(mcpEnabledKey, true); return !enableMCP; // show Start only when currently stopped } /// /// Stop MCP: disconnect and skip all future auto-connect/reconnect attempts. /// Persisted across editor sessions. HTTP server client is unaffected. /// [MenuItem("Tools/Synaptic Pro/MCP Server: Stop", false, 10)] public static void StopMCP() { enableMCP = false; EditorPrefs.SetBool(mcpEnabledKey, false); SynLog.Info("[Nexus Editor MCP] ⏹️ MCP disabled (persisted). HTTP server client unaffected."); DisconnectFromMCPServer(); } [MenuItem("Tools/Synaptic Pro/MCP Server: Stop", true)] public static bool StopMCPValidate() { enableMCP = EditorPrefs.GetBool(mcpEnabledKey, true); return enableMCP; // show Stop only when currently running } /// /// MCP Service status for debugging /// [MenuItem("Tools/Synaptic Pro/AI Connection Status", false, 11)] public static void ShowMCPStatus() { string connectionStatus = IsConnected ? "✅ Connected" : "❌ Disconnected"; string webSocketState = webSocket?.State.ToString() ?? "null"; string status = $@"🔗 AI Connection Status {connectionStatus} Details: • Initialized: {(isInitialized ? "✅" : "❌")} • Server URL: {serverUrl ?? "Not configured"} • Message Queue: {messageQueue.Count} items • WebSocket State: {webSocketState} • Reconnect Attempts: {reconnectAttempts}/{maxReconnectAttempts} • Auto Reconnect: {(shouldReconnect ? "Enabled" : "Disabled")} If you have issues, try 'AI Reconnect'."; SynLog.Info(status); EditorUtility.DisplayDialog("AI Connection Status", status, "OK"); } /// /// Connection status display for toolbar /// // [MenuItem("Tools/AI Connection Status", false, 1)] public static void QuickStatus() { string status = IsConnected ? "✅ AI: Connected" : "❌ AI: Disconnected"; SynLog.Info($"[Nexus MCP] {status}"); EditorUtility.DisplayDialog("Connection Status", status, "OK"); } /// /// Manual reconnect for debugging /// [MenuItem("Tools/Synaptic Pro/AI Reconnect", false, 12)] public static void ManualReconnect() { SynLog.Info("[Nexus Editor MCP] Manual reconnect requested"); // Display confirmation dialog to user bool reconnect = EditorUtility.DisplayDialog( "Repair AI Connection", "Re-establish connection with Claude and other AI.\n\n" + "Use in the following cases:\n" + "• No response from AI\n" + "• Unity tools have operation timeout\n" + "• Connection became unstable\n\n" + "Execute reconnection?", "Reconnect", "Cancel" ); if (reconnect) { ReconnectToMCPServer(); EditorUtility.DisplayDialog( "Reconnection Complete", "Re-established connection with Claude and other AI.\n" + "Check console log for connection status.", "OK" ); } } /// /// Simple reconnect button (for toolbar) /// // [MenuItem("Tools/🔗 AI Reconnect", false, 0)] public static void QuickReconnect() { SynLog.Info("[Nexus Editor MCP] Quick reconnect requested"); ReconnectToMCPServer(); } /// /// Enable auto-reconnect (shown when currently OFF) /// [MenuItem("Tools/Synaptic Pro/Auto Reconnect: Enable", false, 13)] public static void EnableAutoReconnect() { enableAutoReconnect = true; EditorPrefs.SetBool(autoReconnectKey, enableAutoReconnect); SynLog.Info("[Nexus Editor MCP] Auto-reconnect: enabled"); } [MenuItem("Tools/Synaptic Pro/Auto Reconnect: Enable", true)] public static bool EnableAutoReconnectValidate() { // Load from EditorPrefs to ensure correct state enableAutoReconnect = EditorPrefs.GetBool(autoReconnectKey, true); // Show "Enable" option only when currently OFF return !enableAutoReconnect; } /// /// Disable auto-reconnect (shown when currently ON) /// [MenuItem("Tools/Synaptic Pro/Auto Reconnect: Disable", false, 13)] public static void DisableAutoReconnect() { enableAutoReconnect = false; EditorPrefs.SetBool(autoReconnectKey, enableAutoReconnect); SynLog.Info("[Nexus Editor MCP] Auto-reconnect: disabled"); } [MenuItem("Tools/Synaptic Pro/Auto Reconnect: Disable", true)] public static bool DisableAutoReconnectValidate() { // Load from EditorPrefs to ensure correct state enableAutoReconnect = EditorPrefs.GetBool(autoReconnectKey, true); // Show "Disable" option only when currently ON return enableAutoReconnect; } /// /// Update Claude Desktop config file port /// private static void UpdateClaudeDesktopConfigForPort(int newPort) { try { string configPath; // Detect platform-specific Claude Desktop config path if (UnityEngine.Application.platform == UnityEngine.RuntimePlatform.OSXEditor) { configPath = System.IO.Path.Combine( System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude", "claude_desktop_config.json" ); } else if (UnityEngine.Application.platform == UnityEngine.RuntimePlatform.WindowsEditor) { configPath = System.IO.Path.Combine( System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), "Claude", "claude_desktop_config.json" ); } else { SynLog.Warn("[Nexus Editor MCP] Unsupported platform for Claude Desktop config update"); return; } if (!System.IO.File.Exists(configPath)) { SynLog.Warn($"[Nexus Editor MCP] Claude Desktop config file not found: {configPath}"); return; } string configContent = System.IO.File.ReadAllText(configPath); // Update WebSocket port (supports multiple patterns) bool updated = false; string newPattern = $"ws://localhost:{newPort}"; // Check all known port patterns string[] oldPatterns = { "ws://localhost:8090", "ws://localhost:8081", "ws://localhost:8082", "ws://localhost:8083", "ws://localhost:8084" }; foreach (string oldPattern in oldPatterns) { if (configContent.Contains(oldPattern) && oldPattern != newPattern) { configContent = configContent.Replace(oldPattern, newPattern); updated = true; SynLog.Info($"[Nexus Editor MCP] 🔄 Auto-updated Claude Desktop config: {oldPattern} → {newPattern}"); } } if (updated) { // Create backup string backupPath = configPath + $".backup_{System.DateTime.Now:yyyyMMdd_HHmmss}"; System.IO.File.Copy(configPath, backupPath); // Write new config System.IO.File.WriteAllText(configPath, configContent); SynLog.Info($"[Nexus Editor MCP] 🔄 Auto-updated Claude Desktop config"); SynLog.Info($"[Nexus Editor MCP] 📁 Backup: {backupPath}"); SynLog.Info($"[Nexus Editor MCP] ⚠️ Claude Desktop restart required"); } } catch (System.Exception e) { Debug.LogError($"[Nexus Editor MCP] Claude Desktop config update error: {e.Message}"); } } /// /// Light connection test (WebSocket state check only) /// private static async Task LightConnectionTest() { try { if (webSocket?.State == WebSocketState.Open) { // WebSocket is open but test actual communication var testMessage = JsonConvert.SerializeObject(new { type = "ping", id = "test" }); var buffer = Encoding.UTF8.GetBytes(testMessage); await webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); // Connection check successful isConnected = true; reconnectPhase = 0; return; } else if (webSocket?.State == WebSocketState.Closed || webSocket?.State == WebSocketState.Aborted) { // If Closed state, immediately proceed to full reconnect SynLog.Info("[Nexus MCP] WebSocket is closed, skipping to full reconnection"); reconnectPhase = 2; lastReconnectTime = Time.realtimeSinceStartup; return; } } catch (Exception) { // Light test failed, proceed to next phase } } /// /// Full reconnect attempt /// private static async Task FullReconnectAttempt() { isReconnecting = true; try { // Clean up existing connection if (webSocket != null) { try { webSocket.Dispose(); } catch { } webSocket = null; } await Task.Delay(1000); // Wait 1 second // Attempt new connection await ConnectToMCPServer(); if (isConnected) { reconnectPhase = 0; SynLog.Info("[Nexus MCP] 🔄 Auto-reconnect successful"); } } catch (Exception e) { SynLog.Warn($"[Nexus MCP] Reconnect failed: {e.Message}"); } finally { isReconnecting = false; } } /// /// Retry after failure /// private static async Task RetryReconnect() { isReconnecting = true; try { // Rescan port and reconnect DetectAndSetAvailablePort(); await Task.Delay(50); // Immediate reconnect await ConnectToMCPServer(); if (isConnected) { reconnectPhase = 0; SynLog.Info("[Nexus MCP] 🔄 Retry reconnect successful"); } } catch (Exception e) { SynLog.Warn($"[Nexus MCP] Retry failed: {e.Message}"); } finally { isReconnecting = false; } } /// /// Initialize when connection loss detected /// private static void OnConnectionLost() { isConnected = false; // スレッドセーフな時刻取得(非同期コンテキストから呼ばれるため Time.realtimeSinceStartup は使えない) lastConnectionCheckTime = ThreadSafeTime(); reconnectPhase = 0; } /// /// About Nexus - Version information and credits /// [MenuItem("Tools/Synaptic Pro/Join Discord Community", false, 20)] public static void OpenDiscord() { Application.OpenURL("https://discord.gg/MXwHCVWmPe"); } [MenuItem("Tools/Synaptic Pro/About Synaptic Pro", false, 21)] public static void ShowAbout() { EditorUtility.DisplayDialog( "About Synaptic AI Pro for Unity", "Synaptic AI Pro for Unity\n" + $"Version {NexusVersion.Current}\n\n" + "Control Unity Editor with natural language through Claude AI\n\n" + "Features:\n" + "• 235+ Professional Tools\n" + "• Scene Management & GameObject Control\n" + "• Lighting, Cinemachine, Physics, Animation\n" + "• Natural Language Automation\n" + "• Unity 2022.3+ and Unity 6.0+ Compatible\n\n" + "Website: https://synaptic-ai.net\n" + "Discord: https://discord.gg/MXwHCVWmPe\n" + "Support: sekiguchimiu@gmail.com\n\n" + "© 2025 Miu Sekiguchi", "OK" ); } /// /// Attach fault logging to a fire-and-forget Task. Without this, unobserved exceptions /// make the phase machine advance as if the attempt succeeded, triggering a reconnect loop. /// private static void RunTracked(Task task, string label) { task.ContinueWith( t => SynLog.Warn($"[Nexus MCP] {label} faulted: {t.Exception?.GetBaseException().Message}"), TaskContinuationOptions.OnlyOnFaulted); } } }