using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; using UnityEditor; using UnityEngine; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.IO; using UnityMCP.Editor.Models; using UnityMCP.Editor.Tools; namespace UnityMCP.Editor { [InitializeOnLoad] public static partial class UnityMCPBridge { private static TcpListener listener; private static bool isRunning = false; private static readonly object lockObj = new(); private static Dictionary tcs)> commandQueue = new(); private static readonly int unityPort = 6400; // Hardcoded port public static bool IsRunning => isRunning; public static bool FolderExists(string path) { if (string.IsNullOrEmpty(path)) return false; if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase)) return true; string fullPath = Path.Combine(Application.dataPath, path.StartsWith("Assets/") ? path.Substring(7) : path); return Directory.Exists(fullPath); } static UnityMCPBridge() { Start(); EditorApplication.quitting += Stop; } public static void Start() { if (isRunning) return; isRunning = true; listener = new TcpListener(IPAddress.Loopback, unityPort); listener.Start(); Debug.Log($"UnityMCPBridge started on port {unityPort}."); Task.Run(ListenerLoop); EditorApplication.update += ProcessCommands; } public static void Stop() { if (!isRunning) return; isRunning = false; listener.Stop(); EditorApplication.update -= ProcessCommands; Debug.Log("UnityMCPBridge stopped."); } private static async Task ListenerLoop() { while (isRunning) { try { var client = await listener.AcceptTcpClientAsync(); // Enable basic socket keepalive client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); // Set longer receive timeout to prevent quick disconnections client.ReceiveTimeout = 60000; // 60 seconds // Fire and forget each client connection _ = HandleClientAsync(client); } catch (Exception ex) { if (isRunning) Debug.LogError($"Listener error: {ex.Message}"); } } } private static async Task HandleClientAsync(TcpClient client) { using (client) using (var stream = client.GetStream()) { var buffer = new byte[8192]; while (isRunning) { try { int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); if (bytesRead == 0) break; // Client disconnected string commandText = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead); string commandId = Guid.NewGuid().ToString(); var tcs = new TaskCompletionSource(); // Special handling for ping command to avoid JSON parsing if (commandText.Trim() == "ping") { // Direct response to ping without going through JSON parsing byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes("{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}"); await stream.WriteAsync(pingResponseBytes, 0, pingResponseBytes.Length); continue; } lock (lockObj) { commandQueue[commandId] = (commandText, tcs); } string response = await tcs.Task; byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(response); await stream.WriteAsync(responseBytes, 0, responseBytes.Length); } catch (Exception ex) { Debug.LogError($"Client handler error: {ex.Message}"); break; } } } } private static void ProcessCommands() { List processedIds = new(); lock (lockObj) { foreach (var kvp in commandQueue.ToList()) { string id = kvp.Key; string commandText = kvp.Value.commandJson; var tcs = kvp.Value.tcs; try { // Special case handling if (string.IsNullOrEmpty(commandText)) { var emptyResponse = new { status = "error", error = "Empty command received" }; tcs.SetResult(JsonConvert.SerializeObject(emptyResponse)); processedIds.Add(id); continue; } // Trim the command text to remove any whitespace commandText = commandText.Trim(); // Non-JSON direct commands handling (like ping) if (commandText == "ping") { var pingResponse = new { status = "success", result = new { message = "pong" } }; tcs.SetResult(JsonConvert.SerializeObject(pingResponse)); processedIds.Add(id); continue; } // Check if the command is valid JSON before attempting to deserialize if (!IsValidJson(commandText)) { var invalidJsonResponse = new { status = "error", error = "Invalid JSON format", receivedText = commandText.Length > 50 ? commandText.Substring(0, 50) + "..." : commandText }; tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse)); processedIds.Add(id); continue; } // Normal JSON command processing var command = JsonConvert.DeserializeObject(commandText); if (command == null) { var nullCommandResponse = new { status = "error", error = "Command deserialized to null", details = "The command was valid JSON but could not be deserialized to a Command object" }; tcs.SetResult(JsonConvert.SerializeObject(nullCommandResponse)); } else { string responseJson = ExecuteCommand(command); tcs.SetResult(responseJson); } } catch (Exception ex) { Debug.LogError($"Error processing command: {ex.Message}\n{ex.StackTrace}"); var response = new { status = "error", error = ex.Message, commandType = "Unknown (error during processing)", receivedText = commandText?.Length > 50 ? commandText.Substring(0, 50) + "..." : commandText }; string responseJson = JsonConvert.SerializeObject(response); tcs.SetResult(responseJson); } processedIds.Add(id); } foreach (var id in processedIds) { commandQueue.Remove(id); } } } // Helper method to check if a string is valid JSON private static bool IsValidJson(string text) { if (string.IsNullOrWhiteSpace(text)) return false; text = text.Trim(); if ((text.StartsWith("{") && text.EndsWith("}")) || // Object (text.StartsWith("[") && text.EndsWith("]"))) // Array { try { JToken.Parse(text); return true; } catch { return false; } } return false; } private static string ExecuteCommand(Command command) { try { if (string.IsNullOrEmpty(command.type)) { var errorResponse = new { status = "error", error = "Command type cannot be empty", details = "A valid command type is required for processing" }; return JsonConvert.SerializeObject(errorResponse); } // Handle ping command for connection verification if (command.type.Equals("ping", StringComparison.OrdinalIgnoreCase)) { var pingResponse = new { status = "success", result = new { message = "pong" } }; return JsonConvert.SerializeObject(pingResponse); } // Use JObject for parameters as the new handlers likely expect this JObject paramsObject = command.@params ?? new JObject(); // Route command based on the new tool structure from the refactor plan object result = command.type switch { // Maps the command type (tool name) to the corresponding handler's static HandleCommand method // Assumes each handler class has a static method named 'HandleCommand' that takes JObject parameters "manage_script" => ManageScript.HandleCommand(paramsObject), "manage_scene" => ManageScene.HandleCommand(paramsObject), "manage_editor" => ManageEditor.HandleCommand(paramsObject), "manage_gameobject" => ManageGameObject.HandleCommand(paramsObject), "manage_asset" => ManageAsset.HandleCommand(paramsObject), "read_console" => ReadConsole.HandleCommand(paramsObject), "execute_menu_item" => ExecuteMenuItem.HandleCommand(paramsObject), _ => throw new ArgumentException($"Unknown or unsupported command type: {command.type}") }; // Standard success response format var response = new { status = "success", result }; return JsonConvert.SerializeObject(response); } catch (Exception ex) { // Log the detailed error in Unity for debugging Debug.LogError($"Error executing command '{command?.type ?? "Unknown"}': {ex.Message}\n{ex.StackTrace}"); // Standard error response format var response = new { status = "error", error = ex.Message, // Provide the specific error message command = command?.type ?? "Unknown", // Include the command type if available stackTrace = ex.StackTrace, // Include stack trace for detailed debugging paramsSummary = command?.@params != null ? GetParamsSummary(command.@params) : "No parameters" // Summarize parameters for context }; return JsonConvert.SerializeObject(response); } } // Helper method to get a summary of parameters for error reporting private static string GetParamsSummary(JObject @params) { try { if (@params == null || !@params.HasValues) return "No parameters"; return string.Join(", ", @params.Properties().Select(p => $"{p.Name}: {p.Value?.ToString()?.Substring(0, Math.Min(20, p.Value?.ToString()?.Length ?? 0))}")); } catch { return "Could not summarize parameters"; } } } }