From 6b8cf0eab2dac06edee275cb16470aefc2c99e00 Mon Sep 17 00:00:00 2001 From: Justin Barnett Date: Thu, 20 Mar 2025 07:24:31 -0400 Subject: [PATCH] added cursor auto config --- Editor/Commands/AssetCommandHandler.cs | 40 +- Editor/Commands/CommandRegistry.cs | 2 +- Editor/Commands/EditorControlHandler.cs | 997 +++++++++--------- Editor/Commands/MaterialCommandHandler.cs | 4 +- Editor/Commands/ObjectCommandHandler.cs | 78 +- Editor/Commands/SceneCommandHandler.cs | 12 +- Editor/Commands/ScriptCommandHandler.cs | 88 +- Editor/Data.meta | 8 + Editor/Data/DefaultServerConfig.cs | 17 + Editor/Data/DefaultServerConfig.cs.meta | 2 + Editor/Data/McpClients.cs | 57 + Editor/Data/McpClients.cs.meta | 2 + Editor/Helpers/Vector3Helper.cs | 2 +- Editor/MCPEditorWindow.cs | 622 ----------- Editor/Models/Command.cs | 2 +- Editor/Models/MCPConfig.cs | 12 + Editor/Models/MCPConfig.cs.meta | 2 + Editor/Models/MCPConfigServer.cs | 15 + Editor/Models/MCPConfigServer.cs.meta | 2 + Editor/Models/MCPConfigServers.cs | 12 + Editor/Models/MCPConfigServers.cs.meta | 2 + Editor/Models/McpClient.cs | 48 + Editor/Models/McpClient.cs.meta | 2 + Editor/Models/McpStatus.cs | 17 + Editor/Models/McpStatus.cs.meta | 2 + Editor/Models/McpTypes.cs | 8 + Editor/Models/McpTypes.cs.meta | 2 + Editor/Models/ServerConfig.cs | 36 + Editor/Models/ServerConfig.cs.meta | 2 + Editor/UnityMCPBridge.cs | 567 +++++----- Editor/Windows.meta | 8 + Editor/Windows/ManualConfigEditorWindow.cs | 201 ++++ .../Windows/ManualConfigEditorWindow.cs.meta | 2 + Editor/Windows/UnityMCPEditorWindow.cs | 663 ++++++++++++ .../UnityMCPEditorWindow.cs.meta} | 0 Python/unity_mcp.egg-info.meta | 8 + README.md | 122 +-- 37 files changed, 2039 insertions(+), 1627 deletions(-) create mode 100644 Editor/Data.meta create mode 100644 Editor/Data/DefaultServerConfig.cs create mode 100644 Editor/Data/DefaultServerConfig.cs.meta create mode 100644 Editor/Data/McpClients.cs create mode 100644 Editor/Data/McpClients.cs.meta delete mode 100644 Editor/MCPEditorWindow.cs create mode 100644 Editor/Models/MCPConfig.cs create mode 100644 Editor/Models/MCPConfig.cs.meta create mode 100644 Editor/Models/MCPConfigServer.cs create mode 100644 Editor/Models/MCPConfigServer.cs.meta create mode 100644 Editor/Models/MCPConfigServers.cs create mode 100644 Editor/Models/MCPConfigServers.cs.meta create mode 100644 Editor/Models/McpClient.cs create mode 100644 Editor/Models/McpClient.cs.meta create mode 100644 Editor/Models/McpStatus.cs create mode 100644 Editor/Models/McpStatus.cs.meta create mode 100644 Editor/Models/McpTypes.cs create mode 100644 Editor/Models/McpTypes.cs.meta create mode 100644 Editor/Models/ServerConfig.cs create mode 100644 Editor/Models/ServerConfig.cs.meta create mode 100644 Editor/Windows.meta create mode 100644 Editor/Windows/ManualConfigEditorWindow.cs create mode 100644 Editor/Windows/ManualConfigEditorWindow.cs.meta create mode 100644 Editor/Windows/UnityMCPEditorWindow.cs rename Editor/{MCPEditorWindow.cs.meta => Windows/UnityMCPEditorWindow.cs.meta} (100%) create mode 100644 Python/unity_mcp.egg-info.meta diff --git a/Editor/Commands/AssetCommandHandler.cs b/Editor/Commands/AssetCommandHandler.cs index ea57405..77f953e 100644 --- a/Editor/Commands/AssetCommandHandler.cs +++ b/Editor/Commands/AssetCommandHandler.cs @@ -2,10 +2,9 @@ using UnityEngine; using UnityEditor; using System.IO; using Newtonsoft.Json.Linq; -using System.Linq; using System.Collections.Generic; -namespace MCPServer.Editor.Commands +namespace UnityMCP.Editor.Commands { /// /// Handles asset-related commands for the MCP Server @@ -51,10 +50,11 @@ namespace MCPServer.Editor.Commands } catch (System.Exception e) { - return new { - success = false, - error = $"Failed to import asset: {e.Message}", - stackTrace = e.StackTrace + return new + { + success = false, + error = $"Failed to import asset: {e.Message}", + stackTrace = e.StackTrace }; } } @@ -67,16 +67,16 @@ namespace MCPServer.Editor.Commands try { string prefabPath = (string)@params["prefab_path"]; - + if (string.IsNullOrEmpty(prefabPath)) return new { success = false, error = "Prefab path cannot be empty" }; - Vector3 position = new Vector3( + Vector3 position = new( (float)@params["position_x"], (float)@params["position_y"], (float)@params["position_z"] ); - Vector3 rotation = new Vector3( + Vector3 rotation = new( (float)@params["rotation_x"], (float)@params["rotation_y"], (float)@params["rotation_z"] @@ -93,7 +93,7 @@ namespace MCPServer.Editor.Commands { return new { success = false, error = $"Failed to instantiate prefab: {prefabPath}" }; } - + instance.transform.position = position; instance.transform.rotation = Quaternion.Euler(rotation); @@ -106,10 +106,11 @@ namespace MCPServer.Editor.Commands } catch (System.Exception e) { - return new { - success = false, - error = $"Failed to instantiate prefab: {e.Message}", - stackTrace = e.StackTrace + return new + { + success = false, + error = $"Failed to instantiate prefab: {e.Message}", + stackTrace = e.StackTrace }; } } @@ -162,9 +163,10 @@ namespace MCPServer.Editor.Commands } catch (System.Exception e) { - return new { - success = false, - error = $"Failed to create prefab: {e.Message}", + return new + { + success = false, + error = $"Failed to create prefab: {e.Message}", stackTrace = e.StackTrace, sourceInfo = $"Object: {@params["object_name"]}, Path: {@params["prefab_path"]}" }; @@ -218,9 +220,9 @@ namespace MCPServer.Editor.Commands assets.Add(new { name = Path.GetFileNameWithoutExtension(path), - path = path, + path, type = assetType?.Name ?? "Unknown", - guid = guid + guid }); } diff --git a/Editor/Commands/CommandRegistry.cs b/Editor/Commands/CommandRegistry.cs index 3878e15..1998386 100644 --- a/Editor/Commands/CommandRegistry.cs +++ b/Editor/Commands/CommandRegistry.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; -namespace MCPServer.Editor.Commands +namespace UnityMCP.Editor.Commands { /// /// Registry for all MCP command handlers diff --git a/Editor/Commands/EditorControlHandler.cs b/Editor/Commands/EditorControlHandler.cs index 7525711..5e14aa5 100644 --- a/Editor/Commands/EditorControlHandler.cs +++ b/Editor/Commands/EditorControlHandler.cs @@ -5,295 +5,334 @@ using Newtonsoft.Json.Linq; using System; using System.Reflection; using System.Collections.Generic; -using System.Linq; // Add LINQ namespace for Select extension method -using System.Globalization; +using System.Linq; -/// -/// Handles editor control commands like undo, redo, play, pause, stop, and build operations. -/// -public static class EditorControlHandler +namespace UnityMCP.Editor.Commands { /// - /// Handles editor control commands + /// Handles editor control commands like undo, redo, play, pause, stop, and build operations. /// - public static object HandleEditorControl(JObject @params) + public static class EditorControlHandler { - string command = (string)@params["command"]; - JObject commandParams = (JObject)@params["params"]; - - switch (command.ToUpper()) + /// + /// Handles editor control commands + /// + public static object HandleEditorControl(JObject @params) { - case "UNDO": - return HandleUndo(); - case "REDO": - return HandleRedo(); - case "PLAY": - return HandlePlay(); - case "PAUSE": - return HandlePause(); - case "STOP": - return HandleStop(); - case "BUILD": - return HandleBuild(commandParams); - case "EXECUTE_COMMAND": - return HandleExecuteCommand(commandParams); - case "READ_CONSOLE": - return ReadConsole(commandParams); - default: - return new { error = $"Unknown editor control command: {command}" }; - } - } + string command = (string)@params["command"]; + JObject commandParams = (JObject)@params["params"]; - private static object HandleUndo() - { - Undo.PerformUndo(); - return new { message = "Undo performed successfully" }; - } - - private static object HandleRedo() - { - Undo.PerformRedo(); - return new { message = "Redo performed successfully" }; - } - - private static object HandlePlay() - { - if (!EditorApplication.isPlaying) - { - EditorApplication.isPlaying = true; - return new { message = "Entered play mode" }; - } - return new { message = "Already in play mode" }; - } - - private static object HandlePause() - { - if (EditorApplication.isPlaying) - { - EditorApplication.isPaused = !EditorApplication.isPaused; - return new { message = EditorApplication.isPaused ? "Game paused" : "Game resumed" }; - } - return new { message = "Not in play mode" }; - } - - private static object HandleStop() - { - if (EditorApplication.isPlaying) - { - EditorApplication.isPlaying = false; - return new { message = "Exited play mode" }; - } - return new { message = "Not in play mode" }; - } - - private static object HandleBuild(JObject @params) - { - string platform = (string)@params["platform"]; - string buildPath = (string)@params["buildPath"]; - - try - { - BuildTarget target = GetBuildTarget(platform); - if ((int)target == -1) + return command.ToUpper() switch { - return new { error = $"Unsupported platform: {platform}" }; - } - - BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions(); - buildPlayerOptions.scenes = GetEnabledScenes(); - buildPlayerOptions.target = target; - buildPlayerOptions.locationPathName = buildPath; - - BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions); - return new - { - message = "Build completed successfully", - summary = report.summary + "UNDO" => HandleUndo(), + "REDO" => HandleRedo(), + "PLAY" => HandlePlay(), + "PAUSE" => HandlePause(), + "STOP" => HandleStop(), + "BUILD" => HandleBuild(commandParams), + "EXECUTE_COMMAND" => HandleExecuteCommand(commandParams), + "READ_CONSOLE" => ReadConsole(commandParams), + _ => new { error = $"Unknown editor control command: {command}" }, }; } - catch (System.Exception e) - { - return new { error = $"Build failed: {e.Message}" }; - } - } - private static object HandleExecuteCommand(JObject @params) - { - string commandName = (string)@params["commandName"]; - try + private static object HandleUndo() { - EditorApplication.ExecuteMenuItem(commandName); - return new { message = $"Executed command: {commandName}" }; - } - catch (System.Exception e) - { - return new { error = $"Failed to execute command: {e.Message}" }; - } - } - - /// - /// Reads log messages from the Unity Console - /// - /// Parameters containing filtering options - /// Object containing console messages filtered by type - public static object ReadConsole(JObject @params) - { - // Default values for show flags - bool showLogs = true; - bool showWarnings = true; - bool showErrors = true; - string searchTerm = string.Empty; - - // Get filter parameters if provided - if (@params != null) - { - if (@params["show_logs"] != null) showLogs = (bool)@params["show_logs"]; - if (@params["show_warnings"] != null) showWarnings = (bool)@params["show_warnings"]; - if (@params["show_errors"] != null) showErrors = (bool)@params["show_errors"]; - if (@params["search_term"] != null) searchTerm = (string)@params["search_term"]; + Undo.PerformUndo(); + return new { message = "Undo performed successfully" }; } - try + private static object HandleRedo() { - // Get required types and methods via reflection - Type logEntriesType = Type.GetType("UnityEditor.LogEntries,UnityEditor"); - Type logEntryType = Type.GetType("UnityEditor.LogEntry,UnityEditor"); + Undo.PerformRedo(); + return new { message = "Redo performed successfully" }; + } - if (logEntriesType == null || logEntryType == null) - return new { error = "Could not find required Unity logging types", entries = new List() }; - - // Get essential methods - MethodInfo getCountMethod = logEntriesType.GetMethod("GetCount", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); - MethodInfo getEntryMethod = logEntriesType.GetMethod("GetEntryAt", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? - logEntriesType.GetMethod("GetEntryInternal", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); - - if (getCountMethod == null || getEntryMethod == null) - return new { error = "Could not find required Unity logging methods", entries = new List() }; - - // Get stack trace method if available - MethodInfo getStackTraceMethod = logEntriesType.GetMethod("GetEntryStackTrace", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, - null, new[] { typeof(int) }, null) ?? logEntriesType.GetMethod("GetStackTrace", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, - null, new[] { typeof(int) }, null); - - // Get entry count and prepare result list - int count = (int)getCountMethod.Invoke(null, null); - var entries = new List(); - - // Create LogEntry instance to populate - object logEntryInstance = Activator.CreateInstance(logEntryType); - - // Find properties on LogEntry type - PropertyInfo modeProperty = logEntryType.GetProperty("mode") ?? logEntryType.GetProperty("Mode"); - PropertyInfo messageProperty = logEntryType.GetProperty("message") ?? logEntryType.GetProperty("Message"); - - // Parse search terms if provided - string[] searchWords = !string.IsNullOrWhiteSpace(searchTerm) ? - searchTerm.ToLower().Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) : null; - - // Process each log entry - for (int i = 0; i < count; i++) + private static object HandlePlay() + { + if (!EditorApplication.isPlaying) { - try + EditorApplication.isPlaying = true; + return new { message = "Entered play mode" }; + } + return new { message = "Already in play mode" }; + } + + private static object HandlePause() + { + if (EditorApplication.isPlaying) + { + EditorApplication.isPaused = !EditorApplication.isPaused; + return new { message = EditorApplication.isPaused ? "Game paused" : "Game resumed" }; + } + return new { message = "Not in play mode" }; + } + + private static object HandleStop() + { + if (EditorApplication.isPlaying) + { + EditorApplication.isPlaying = false; + return new { message = "Exited play mode" }; + } + return new { message = "Not in play mode" }; + } + + private static object HandleBuild(JObject @params) + { + string platform = (string)@params["platform"]; + string buildPath = (string)@params["buildPath"]; + + try + { + BuildTarget target = GetBuildTarget(platform); + if ((int)target == -1) { - // Get log entry at index i - var methodParams = getEntryMethod.GetParameters(); - if (methodParams.Length == 2 && methodParams[1].ParameterType == logEntryType) + return new { error = $"Unsupported platform: {platform}" }; + } + + BuildPlayerOptions buildPlayerOptions = new() + { + scenes = GetEnabledScenes(), + target = target, + locationPathName = buildPath + }; + + BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions); + return new + { + message = "Build completed successfully", + report.summary + }; + } + catch (Exception e) + { + return new { error = $"Build failed: {e.Message}" }; + } + } + + private static object HandleExecuteCommand(JObject @params) + { + string commandName = (string)@params["commandName"]; + try + { + EditorApplication.ExecuteMenuItem(commandName); + return new { message = $"Executed command: {commandName}" }; + } + catch (Exception e) + { + return new { error = $"Failed to execute command: {e.Message}" }; + } + } + + /// + /// Reads log messages from the Unity Console + /// + /// Parameters containing filtering options + /// Object containing console messages filtered by type + public static object ReadConsole(JObject @params) + { + // Default values for show flags + bool showLogs = true; + bool showWarnings = true; + bool showErrors = true; + string searchTerm = string.Empty; + + // Get filter parameters if provided + if (@params != null) + { + if (@params["show_logs"] != null) showLogs = (bool)@params["show_logs"]; + if (@params["show_warnings"] != null) showWarnings = (bool)@params["show_warnings"]; + if (@params["show_errors"] != null) showErrors = (bool)@params["show_errors"]; + if (@params["search_term"] != null) searchTerm = (string)@params["search_term"]; + } + + try + { + // Get required types and methods via reflection + Type logEntriesType = Type.GetType("UnityEditor.LogEntries,UnityEditor"); + Type logEntryType = Type.GetType("UnityEditor.LogEntry,UnityEditor"); + + if (logEntriesType == null || logEntryType == null) + return new { error = "Could not find required Unity logging types", entries = new List() }; + + // Get essential methods + MethodInfo getCountMethod = logEntriesType.GetMethod("GetCount", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + MethodInfo getEntryMethod = logEntriesType.GetMethod("GetEntryAt", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? + logEntriesType.GetMethod("GetEntryInternal", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + if (getCountMethod == null || getEntryMethod == null) + return new { error = "Could not find required Unity logging methods", entries = new List() }; + + // Get stack trace method if available + MethodInfo getStackTraceMethod = logEntriesType.GetMethod("GetEntryStackTrace", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, + null, new[] { typeof(int) }, null) ?? logEntriesType.GetMethod("GetStackTrace", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, + null, new[] { typeof(int) }, null); + + // Get entry count and prepare result list + int count = (int)getCountMethod.Invoke(null, null); + var entries = new List(); + + // Create LogEntry instance to populate + object logEntryInstance = Activator.CreateInstance(logEntryType); + + // Find properties on LogEntry type + PropertyInfo modeProperty = logEntryType.GetProperty("mode") ?? logEntryType.GetProperty("Mode"); + PropertyInfo messageProperty = logEntryType.GetProperty("message") ?? logEntryType.GetProperty("Message"); + + // Parse search terms if provided + string[] searchWords = !string.IsNullOrWhiteSpace(searchTerm) ? + searchTerm.ToLower().Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) : null; + + // Process each log entry + for (int i = 0; i < count; i++) + { + try { - getEntryMethod.Invoke(null, new object[] { i, logEntryInstance }); - } - else if (methodParams.Length >= 1 && methodParams[0].ParameterType == typeof(int)) - { - var parameters = new object[methodParams.Length]; - parameters[0] = i; - for (int p = 1; p < parameters.Length; p++) + // Get log entry at index i + var methodParams = getEntryMethod.GetParameters(); + if (methodParams.Length == 2 && methodParams[1].ParameterType == logEntryType) { - parameters[p] = methodParams[p].ParameterType.IsValueType ? - Activator.CreateInstance(methodParams[p].ParameterType) : null; + getEntryMethod.Invoke(null, new object[] { i, logEntryInstance }); } - getEntryMethod.Invoke(null, parameters); - } - else continue; - - // Extract log data - int logType = modeProperty != null ? - Convert.ToInt32(modeProperty.GetValue(logEntryInstance) ?? 0) : 0; - - string message = messageProperty != null ? - (messageProperty.GetValue(logEntryInstance)?.ToString() ?? "") : ""; - - // If message is empty, try to get it via a field - if (string.IsNullOrEmpty(message)) - { - var msgField = logEntryType.GetField("message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (msgField != null) + else if (methodParams.Length >= 1 && methodParams[0].ParameterType == typeof(int)) { - object msgValue = msgField.GetValue(logEntryInstance); - message = msgValue != null ? msgValue.ToString() : ""; + var parameters = new object[methodParams.Length]; + parameters[0] = i; + for (int p = 1; p < parameters.Length; p++) + { + parameters[p] = methodParams[p].ParameterType.IsValueType ? + Activator.CreateInstance(methodParams[p].ParameterType) : null; + } + getEntryMethod.Invoke(null, parameters); } + else continue; - // If still empty, try alternate approach with Console window + // Extract log data + int logType = modeProperty != null ? + Convert.ToInt32(modeProperty.GetValue(logEntryInstance) ?? 0) : 0; + + string message = messageProperty != null ? + (messageProperty.GetValue(logEntryInstance)?.ToString() ?? "") : ""; + + // If message is empty, try to get it via a field if (string.IsNullOrEmpty(message)) { - // Access ConsoleWindow and its data - Type consoleWindowType = Type.GetType("UnityEditor.ConsoleWindow,UnityEditor"); - if (consoleWindowType != null) + var msgField = logEntryType.GetField("message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (msgField != null) { - try + object msgValue = msgField.GetValue(logEntryInstance); + message = msgValue != null ? msgValue.ToString() : ""; + } + + // If still empty, try alternate approach with Console window + if (string.IsNullOrEmpty(message)) + { + // Access ConsoleWindow and its data + Type consoleWindowType = Type.GetType("UnityEditor.ConsoleWindow,UnityEditor"); + if (consoleWindowType != null) { - // Get Console window instance - var getWindowMethod = consoleWindowType.GetMethod("GetWindow", - BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, - null, new[] { typeof(bool) }, null) ?? - consoleWindowType.GetMethod("GetConsoleWindow", - BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); - - if (getWindowMethod != null) + try { - object consoleWindow = getWindowMethod.Invoke(null, - getWindowMethod.GetParameters().Length > 0 ? new object[] { false } : null); + // Get Console window instance + var getWindowMethod = consoleWindowType.GetMethod("GetWindow", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, + null, new[] { typeof(bool) }, null) ?? + consoleWindowType.GetMethod("GetConsoleWindow", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); - if (consoleWindow != null) + if (getWindowMethod != null) { - // Try to find log entries collection - foreach (var prop in consoleWindowType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + object consoleWindow = getWindowMethod.Invoke(null, + getWindowMethod.GetParameters().Length > 0 ? new object[] { false } : null); + + if (consoleWindow != null) { - if (prop.PropertyType.IsArray || - (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>))) + // Try to find log entries collection + foreach (var prop in consoleWindowType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { - try + if (prop.PropertyType.IsArray || + (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>))) { - var logItems = prop.GetValue(consoleWindow); - if (logItems != null) + try { - if (logItems.GetType().IsArray && i < ((Array)logItems).Length) + var logItems = prop.GetValue(consoleWindow); + if (logItems != null) { - var entry = ((Array)logItems).GetValue(i); - if (entry != null) + if (logItems.GetType().IsArray && i < ((Array)logItems).Length) { - var entryType = entry.GetType(); - var entryMessageProp = entryType.GetProperty("message") ?? - entryType.GetProperty("Message"); - if (entryMessageProp != null) + var entry = ((Array)logItems).GetValue(i); + if (entry != null) { - object value = entryMessageProp.GetValue(entry); - if (value != null) + var entryType = entry.GetType(); + var entryMessageProp = entryType.GetProperty("message") ?? + entryType.GetProperty("Message"); + if (entryMessageProp != null) { - message = value.ToString(); - break; + object value = entryMessageProp.GetValue(entry); + if (value != null) + { + message = value.ToString(); + break; + } } } } } } - } - catch - { - // Ignore errors in this fallback approach + catch + { + // Ignore errors in this fallback approach + } } } } } } + catch + { + // Ignore errors in this fallback approach + } + } + } + + // If still empty, try one more approach with log files + if (string.IsNullOrEmpty(message)) + { + // This is our last resort - try to get log messages from the most recent Unity log file + try + { + string logPath = string.Empty; + + // Determine the log file path based on the platform + if (Application.platform == RuntimePlatform.WindowsEditor) + { + logPath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Unity", "Editor", "Editor.log"); + } + else if (Application.platform == RuntimePlatform.OSXEditor) + { + logPath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Personal), + "Library", "Logs", "Unity", "Editor.log"); + } + else if (Application.platform == RuntimePlatform.LinuxEditor) + { + logPath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Personal), + ".config", "unity3d", "logs", "Editor.log"); + } + + if (!string.IsNullOrEmpty(logPath) && System.IO.File.Exists(logPath)) + { + // Read last few lines from the log file + var logLines = ReadLastLines(logPath, 100); + if (logLines.Count > i) + { + message = logLines[logLines.Count - 1 - i]; + } + } } catch { @@ -302,291 +341,247 @@ public static class EditorControlHandler } } - // If still empty, try one more approach with log files - if (string.IsNullOrEmpty(message)) + // Get stack trace if method available + string stackTrace = ""; + if (getStackTraceMethod != null) { - // This is our last resort - try to get log messages from the most recent Unity log file - try - { - string logPath = string.Empty; + stackTrace = getStackTraceMethod.Invoke(null, new object[] { i })?.ToString() ?? ""; + } - // Determine the log file path based on the platform - if (Application.platform == RuntimePlatform.WindowsEditor) - { - logPath = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "Unity", "Editor", "Editor.log"); - } - else if (Application.platform == RuntimePlatform.OSXEditor) - { - logPath = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Personal), - "Library", "Logs", "Unity", "Editor.log"); - } - else if (Application.platform == RuntimePlatform.LinuxEditor) - { - logPath = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Personal), - ".config", "unity3d", "logs", "Editor.log"); - } + // Filter by type + bool typeMatch = (logType == 0 && showLogs) || + (logType == 1 && showWarnings) || + (logType == 2 && showErrors); + if (!typeMatch) continue; - if (!string.IsNullOrEmpty(logPath) && System.IO.File.Exists(logPath)) - { - // Read last few lines from the log file - var logLines = ReadLastLines(logPath, 100); - if (logLines.Count > i) - { - message = logLines[logLines.Count - 1 - i]; - } - } - } - catch + // Filter by search term + bool searchMatch = true; + if (searchWords != null && searchWords.Length > 0) + { + string lowerMessage = message.ToLower(); + string lowerStackTrace = stackTrace.ToLower(); + + foreach (string word in searchWords) { - // Ignore errors in this fallback approach + if (!lowerMessage.Contains(word) && !lowerStackTrace.Contains(word)) + { + searchMatch = false; + break; + } } } - } + if (!searchMatch) continue; - // Get stack trace if method available - string stackTrace = ""; - if (getStackTraceMethod != null) - { - stackTrace = getStackTraceMethod.Invoke(null, new object[] { i })?.ToString() ?? ""; - } - - // Filter by type - bool typeMatch = (logType == 0 && showLogs) || - (logType == 1 && showWarnings) || - (logType == 2 && showErrors); - if (!typeMatch) continue; - - // Filter by search term - bool searchMatch = true; - if (searchWords != null && searchWords.Length > 0) - { - string lowerMessage = message.ToLower(); - string lowerStackTrace = stackTrace.ToLower(); - - foreach (string word in searchWords) + // Add matching entry to results + string typeStr = logType == 0 ? "Log" : logType == 1 ? "Warning" : "Error"; + entries.Add(new { - if (!lowerMessage.Contains(word) && !lowerStackTrace.Contains(word)) - { - searchMatch = false; - break; - } - } + type = typeStr, + message, + stackTrace + }); } - if (!searchMatch) continue; - - // Add matching entry to results - string typeStr = logType == 0 ? "Log" : logType == 1 ? "Warning" : "Error"; - entries.Add(new + catch (Exception) { - type = typeStr, - message = message, - stackTrace = stackTrace - }); + // Skip entries that cause errors + continue; + } + } + + // Return filtered results + return new + { + message = "Console logs retrieved successfully", + entries, + total_entries = count, + filtered_count = entries.Count, + show_logs = showLogs, + show_warnings = showWarnings, + show_errors = showErrors + }; + } + catch (Exception e) + { + return new + { + error = $"Failed to read console logs: {e.Message}", + entries = new List() + }; + } + } + + private static MethodInfo FindMethod(Type type, string[] methodNames) + { + foreach (var methodName in methodNames) + { + var method = type.GetMethod(methodName, + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (method != null) + return method; + } + return null; + } + + private static BuildTarget GetBuildTarget(string platform) + { + BuildTarget target; + switch (platform.ToLower()) + { + case "windows": target = BuildTarget.StandaloneWindows64; break; + case "mac": target = BuildTarget.StandaloneOSX; break; + case "linux": target = BuildTarget.StandaloneLinux64; break; + case "android": target = BuildTarget.Android; break; + case "ios": target = BuildTarget.iOS; break; + case "webgl": target = BuildTarget.WebGL; break; + default: target = (BuildTarget)(-1); break; // Invalid target + } + return target; + } + + private static string[] GetEnabledScenes() + { + var scenes = new List(); + for (int i = 0; i < EditorBuildSettings.scenes.Length; i++) + { + if (EditorBuildSettings.scenes[i].enabled) + { + scenes.Add(EditorBuildSettings.scenes[i].path); + } + } + return scenes.ToArray(); + } + + /// + /// Helper method to get information about available properties and fields in a type + /// + private static Dictionary GetTypeInfo(Type type) + { + var result = new Dictionary(); + + // Get all public and non-public properties + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static | BindingFlags.Instance); + var propList = new List(); + foreach (var prop in properties) + { + propList.Add($"{prop.PropertyType.Name} {prop.Name}"); + } + result["Properties"] = propList; + + // Get all public and non-public fields + var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static | BindingFlags.Instance); + var fieldList = new List(); + foreach (var field in fields) + { + fieldList.Add($"{field.FieldType.Name} {field.Name}"); + } + result["Fields"] = fieldList; + + // Get all public and non-public methods + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static | BindingFlags.Instance); + var methodList = new List(); + foreach (var method in methods) + { + if (!method.Name.StartsWith("get_") && !method.Name.StartsWith("set_")) + { + var parameters = string.Join(", ", method.GetParameters() + .Select(p => $"{p.ParameterType.Name} {p.Name}")); + methodList.Add($"{method.ReturnType.Name} {method.Name}({parameters})"); + } + } + result["Methods"] = methodList; + + return result; + } + + /// + /// Helper method to get all property and field values from an object + /// + private static Dictionary GetObjectValues(object obj) + { + if (obj == null) return new Dictionary(); + + var result = new Dictionary(); + var type = obj.GetType(); + + // Get all property values + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + foreach (var prop in properties) + { + try + { + var value = prop.GetValue(obj); + result[$"Property:{prop.Name}"] = value?.ToString() ?? "null"; } catch (Exception) { - // Skip entries that cause errors - continue; + result[$"Property:{prop.Name}"] = "ERROR"; } } - // Return filtered results - return new + // Get all field values + var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + foreach (var field in fields) { - message = "Console logs retrieved successfully", - entries = entries, - total_entries = count, - filtered_count = entries.Count, - show_logs = showLogs, - show_warnings = showWarnings, - show_errors = showErrors - }; - } - catch (Exception e) - { - return new - { - error = $"Failed to read console logs: {e.Message}", - entries = new List() - }; - } - } - - private static MethodInfo FindMethod(Type type, string[] methodNames) - { - foreach (var methodName in methodNames) - { - var method = type.GetMethod(methodName, - BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); - if (method != null) - return method; - } - return null; - } - - private static BuildTarget GetBuildTarget(string platform) - { - BuildTarget target; - switch (platform.ToLower()) - { - case "windows": target = BuildTarget.StandaloneWindows64; break; - case "mac": target = BuildTarget.StandaloneOSX; break; - case "linux": target = BuildTarget.StandaloneLinux64; break; - case "android": target = BuildTarget.Android; break; - case "ios": target = BuildTarget.iOS; break; - case "webgl": target = BuildTarget.WebGL; break; - default: target = (BuildTarget)(-1); break; // Invalid target - } - return target; - } - - private static string[] GetEnabledScenes() - { - var scenes = new System.Collections.Generic.List(); - for (int i = 0; i < EditorBuildSettings.scenes.Length; i++) - { - if (EditorBuildSettings.scenes[i].enabled) - { - scenes.Add(EditorBuildSettings.scenes[i].path); - } - } - return scenes.ToArray(); - } - - /// - /// Helper method to get information about available properties and fields in a type - /// - private static Dictionary GetTypeInfo(Type type) - { - var result = new Dictionary(); - - // Get all public and non-public properties - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static | BindingFlags.Instance); - var propList = new List(); - foreach (var prop in properties) - { - propList.Add($"{prop.PropertyType.Name} {prop.Name}"); - } - result["Properties"] = propList; - - // Get all public and non-public fields - var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static | BindingFlags.Instance); - var fieldList = new List(); - foreach (var field in fields) - { - fieldList.Add($"{field.FieldType.Name} {field.Name}"); - } - result["Fields"] = fieldList; - - // Get all public and non-public methods - var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static | BindingFlags.Instance); - var methodList = new List(); - foreach (var method in methods) - { - if (!method.Name.StartsWith("get_") && !method.Name.StartsWith("set_")) - { - var parameters = string.Join(", ", method.GetParameters() - .Select(p => $"{p.ParameterType.Name} {p.Name}")); - methodList.Add($"{method.ReturnType.Name} {method.Name}({parameters})"); - } - } - result["Methods"] = methodList; - - return result; - } - - /// - /// Helper method to get all property and field values from an object - /// - private static Dictionary GetObjectValues(object obj) - { - if (obj == null) return new Dictionary(); - - var result = new Dictionary(); - var type = obj.GetType(); - - // Get all property values - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - foreach (var prop in properties) - { - try - { - var value = prop.GetValue(obj); - result[$"Property:{prop.Name}"] = value?.ToString() ?? "null"; - } - catch (Exception) - { - result[$"Property:{prop.Name}"] = "ERROR"; - } - } - - // Get all field values - var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - foreach (var field in fields) - { - try - { - var value = field.GetValue(obj); - result[$"Field:{field.Name}"] = value?.ToString() ?? "null"; - } - catch (Exception) - { - result[$"Field:{field.Name}"] = "ERROR"; - } - } - - return result; - } - - /// - /// Reads the last N lines from a file - /// - private static List ReadLastLines(string filePath, int lineCount) - { - var result = new List(); - - using (var stream = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite)) - using (var reader = new System.IO.StreamReader(stream)) - { - string line; - var circularBuffer = new List(lineCount); - int currentIndex = 0; - - // Read all lines keeping only the last N in a circular buffer - while ((line = reader.ReadLine()) != null) - { - if (circularBuffer.Count < lineCount) + try { - circularBuffer.Add(line); + var value = field.GetValue(obj); + result[$"Field:{field.Name}"] = value?.ToString() ?? "null"; + } + catch (Exception) + { + result[$"Field:{field.Name}"] = "ERROR"; + } + } + + return result; + } + + /// + /// Reads the last N lines from a file + /// + private static List ReadLastLines(string filePath, int lineCount) + { + var result = new List(); + + using (var stream = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite)) + using (var reader = new System.IO.StreamReader(stream)) + { + string line; + var circularBuffer = new List(lineCount); + int currentIndex = 0; + + // Read all lines keeping only the last N in a circular buffer + while ((line = reader.ReadLine()) != null) + { + if (circularBuffer.Count < lineCount) + { + circularBuffer.Add(line); + } + else + { + circularBuffer[currentIndex] = line; + currentIndex = (currentIndex + 1) % lineCount; + } + } + + // Reorder the circular buffer so that lines are returned in order + if (circularBuffer.Count == lineCount) + { + for (int i = 0; i < lineCount; i++) + { + result.Add(circularBuffer[(currentIndex + i) % lineCount]); + } } else { - circularBuffer[currentIndex] = line; - currentIndex = (currentIndex + 1) % lineCount; + result.AddRange(circularBuffer); } } - // Reorder the circular buffer so that lines are returned in order - if (circularBuffer.Count == lineCount) - { - for (int i = 0; i < lineCount; i++) - { - result.Add(circularBuffer[(currentIndex + i) % lineCount]); - } - } - else - { - result.AddRange(circularBuffer); - } + return result; } - - return result; } } \ No newline at end of file diff --git a/Editor/Commands/MaterialCommandHandler.cs b/Editor/Commands/MaterialCommandHandler.cs index 66f8bf0..e47a403 100644 --- a/Editor/Commands/MaterialCommandHandler.cs +++ b/Editor/Commands/MaterialCommandHandler.cs @@ -5,7 +5,7 @@ using UnityEngine.Rendering; using UnityEditor; using System.IO; -namespace MCPServer.Editor.Commands +namespace UnityMCP.Editor.Commands { /// /// Handles material-related commands @@ -70,7 +70,7 @@ namespace MCPServer.Editor.Commands if (colorArray.Count < 3 || colorArray.Count > 4) throw new System.Exception("Color must be an array of 3 (RGB) or 4 (RGBA) floats."); - Color color = new Color( + Color color = new( (float)colorArray[0], (float)colorArray[1], (float)colorArray[2], diff --git a/Editor/Commands/ObjectCommandHandler.cs b/Editor/Commands/ObjectCommandHandler.cs index 99fd258..eecfcf2 100644 --- a/Editor/Commands/ObjectCommandHandler.cs +++ b/Editor/Commands/ObjectCommandHandler.cs @@ -1,14 +1,14 @@ using UnityEngine; using Newtonsoft.Json.Linq; -using MCPServer.Editor.Helpers; using System; using System.Linq; using System.Collections.Generic; using UnityEditor; using UnityEngine.SceneManagement; using UnityEditor.SceneManagement; +using UnityMCP.Editor.Helpers; -namespace MCPServer.Editor.Commands +namespace UnityMCP.Editor.Commands { /// /// Handles object-related commands @@ -20,11 +20,11 @@ namespace MCPServer.Editor.Commands /// public static object GetObjectInfo(JObject @params) { - string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required."); - var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found."); + string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required."); + var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found."); return new { - name = obj.name, + obj.name, position = new[] { obj.transform.position.x, obj.transform.position.y, obj.transform.position.z }, rotation = new[] { obj.transform.eulerAngles.x, obj.transform.eulerAngles.y, obj.transform.eulerAngles.z }, scale = new[] { obj.transform.localScale.x, obj.transform.localScale.y, obj.transform.localScale.z } @@ -36,7 +36,7 @@ namespace MCPServer.Editor.Commands /// public static object CreateObject(JObject @params) { - string type = (string)@params["type"] ?? throw new System.Exception("Parameter 'type' is required."); + string type = (string)@params["type"] ?? throw new Exception("Parameter 'type' is required."); GameObject obj = type.ToUpper() switch { "CUBE" => GameObject.CreatePrimitive(PrimitiveType.Cube), @@ -48,7 +48,7 @@ namespace MCPServer.Editor.Commands "CAMERA" => new GameObject("Camera") { }.AddComponent().gameObject, "LIGHT" => new GameObject("Light") { }.AddComponent().gameObject, "DIRECTIONAL_LIGHT" => CreateDirectionalLight(), - _ => throw new System.Exception($"Unsupported object type: {type}") + _ => throw new Exception($"Unsupported object type: {type}") }; if (@params.ContainsKey("name")) obj.name = (string)@params["name"]; @@ -56,7 +56,7 @@ namespace MCPServer.Editor.Commands if (@params.ContainsKey("rotation")) obj.transform.eulerAngles = Vector3Helper.ParseVector3((JArray)@params["rotation"]); if (@params.ContainsKey("scale")) obj.transform.localScale = Vector3Helper.ParseVector3((JArray)@params["scale"]); - return new { name = obj.name }; + return new { obj.name }; } /// @@ -64,8 +64,8 @@ namespace MCPServer.Editor.Commands /// public static object ModifyObject(JObject @params) { - string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required."); - var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found."); + string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required."); + var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found."); // Handle basic transform properties if (@params.ContainsKey("location")) obj.transform.position = Vector3Helper.ParseVector3((JArray)@params["location"]); @@ -77,7 +77,7 @@ namespace MCPServer.Editor.Commands if (@params.ContainsKey("set_parent")) { string parentName = (string)@params["set_parent"]; - var parent = GameObject.Find(parentName) ?? throw new System.Exception($"Parent object '{parentName}' not found."); + var parent = GameObject.Find(parentName) ?? throw new Exception($"Parent object '{parentName}' not found."); obj.transform.SetParent(parent.transform); } @@ -109,7 +109,7 @@ namespace MCPServer.Editor.Commands "TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI), _ => Type.GetType($"UnityEngine.{componentType}") ?? Type.GetType(componentType) ?? - throw new System.Exception($"Component type '{componentType}' not found.") + throw new Exception($"Component type '{componentType}' not found.") }; obj.AddComponent(type); } @@ -119,7 +119,7 @@ namespace MCPServer.Editor.Commands string componentType = (string)@params["remove_component"]; Type type = Type.GetType($"UnityEngine.{componentType}") ?? Type.GetType(componentType) ?? - throw new System.Exception($"Component type '{componentType}' not found."); + throw new Exception($"Component type '{componentType}' not found."); var component = obj.GetComponent(type); if (component != null) UnityEngine.Object.DestroyImmediate(component); @@ -137,12 +137,12 @@ namespace MCPServer.Editor.Commands if (componentType == "GameObject") { var gameObjectProperty = typeof(GameObject).GetProperty(propertyName) ?? - throw new System.Exception($"Property '{propertyName}' not found on GameObject."); + throw new Exception($"Property '{propertyName}' not found on GameObject."); // Convert value based on property type object gameObjectValue = Convert.ChangeType(value, gameObjectProperty.PropertyType); gameObjectProperty.SetValue(obj, gameObjectValue); - return new { name = obj.name }; + return new { obj.name }; } // Handle component properties @@ -170,21 +170,21 @@ namespace MCPServer.Editor.Commands "TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI), _ => Type.GetType($"UnityEngine.{componentType}") ?? Type.GetType(componentType) ?? - throw new System.Exception($"Component type '{componentType}' not found.") + throw new Exception($"Component type '{componentType}' not found.") }; var component = obj.GetComponent(type) ?? - throw new System.Exception($"Component '{componentType}' not found on object '{name}'."); + throw new Exception($"Component '{componentType}' not found on object '{name}'."); var property = type.GetProperty(propertyName) ?? - throw new System.Exception($"Property '{propertyName}' not found on component '{componentType}'."); + throw new Exception($"Property '{propertyName}' not found on component '{componentType}'."); // Convert value based on property type object propertyValue = Convert.ChangeType(value, property.PropertyType); property.SetValue(component, propertyValue); } - return new { name = obj.name }; + return new { obj.name }; } /// @@ -192,8 +192,8 @@ namespace MCPServer.Editor.Commands /// public static object DeleteObject(JObject @params) { - string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required."); - var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found."); + string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required."); + var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found."); UnityEngine.Object.DestroyImmediate(obj); return new { name }; } @@ -203,8 +203,8 @@ namespace MCPServer.Editor.Commands /// public static object GetObjectProperties(JObject @params) { - string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required."); - var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found."); + string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required."); + var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found."); var components = obj.GetComponents() .Select(c => new @@ -216,9 +216,9 @@ namespace MCPServer.Editor.Commands return new { - name = obj.name, - tag = obj.tag, - layer = obj.layer, + obj.name, + obj.tag, + obj.layer, active = obj.activeSelf, transform = new { @@ -235,11 +235,11 @@ namespace MCPServer.Editor.Commands /// public static object GetComponentProperties(JObject @params) { - string objectName = (string)@params["object_name"] ?? throw new System.Exception("Parameter 'object_name' is required."); - string componentType = (string)@params["component_type"] ?? throw new System.Exception("Parameter 'component_type' is required."); + string objectName = (string)@params["object_name"] ?? throw new Exception("Parameter 'object_name' is required."); + string componentType = (string)@params["component_type"] ?? throw new Exception("Parameter 'component_type' is required."); - var obj = GameObject.Find(objectName) ?? throw new System.Exception($"Object '{objectName}' not found."); - var component = obj.GetComponent(componentType) ?? throw new System.Exception($"Component '{componentType}' not found on object '{objectName}'."); + var obj = GameObject.Find(objectName) ?? throw new Exception($"Object '{objectName}' not found."); + var component = obj.GetComponent(componentType) ?? throw new Exception($"Component '{componentType}' not found on object '{objectName}'."); return GetComponentProperties(component); } @@ -249,12 +249,12 @@ namespace MCPServer.Editor.Commands /// public static object FindObjectsByName(JObject @params) { - string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required."); + string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required."); var objects = GameObject.FindObjectsByType(FindObjectsSortMode.None) .Where(o => o.name.Contains(name)) .Select(o => new { - name = o.name, + o.name, path = GetGameObjectPath(o) }) .ToList(); @@ -267,11 +267,11 @@ namespace MCPServer.Editor.Commands /// public static object FindObjectsByTag(JObject @params) { - string tag = (string)@params["tag"] ?? throw new System.Exception("Parameter 'tag' is required."); + string tag = (string)@params["tag"] ?? throw new Exception("Parameter 'tag' is required."); var objects = GameObject.FindGameObjectsWithTag(tag) .Select(o => new { - name = o.name, + o.name, path = GetGameObjectPath(o) }) .ToList(); @@ -295,11 +295,11 @@ namespace MCPServer.Editor.Commands /// public static object SelectObject(JObject @params) { - string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required."); - var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found."); + string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required."); + var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found."); Selection.activeGameObject = obj; - return new { name = obj.name }; + return new { obj.name }; } /// @@ -315,7 +315,7 @@ namespace MCPServer.Editor.Commands { selected = new { - name = selected.name, + selected.name, path = GetGameObjectPath(selected) } }; @@ -379,7 +379,7 @@ namespace MCPServer.Editor.Commands { return new { - name = obj.name, + obj.name, children = Enumerable.Range(0, obj.transform.childCount) .Select(i => BuildHierarchyNode(obj.transform.GetChild(i).gameObject)) .ToList() diff --git a/Editor/Commands/SceneCommandHandler.cs b/Editor/Commands/SceneCommandHandler.cs index 1a9324d..8f5dbc5 100644 --- a/Editor/Commands/SceneCommandHandler.cs +++ b/Editor/Commands/SceneCommandHandler.cs @@ -1,11 +1,11 @@ -using UnityEngine; using UnityEngine.SceneManagement; using System.Linq; +using System; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditor.SceneManagement; -namespace MCPServer.Editor.Commands +namespace UnityMCP.Editor.Commands { /// /// Handles scene-related commands for the MCP Server @@ -42,7 +42,7 @@ namespace MCPServer.Editor.Commands EditorSceneManager.OpenScene(scenePath); return new { success = true, message = $"Opened scene: {scenePath}" }; } - catch (System.Exception e) + catch (Exception e) { return new { success = false, error = $"Failed to open scene: {e.Message}", stackTrace = e.StackTrace }; } @@ -60,7 +60,7 @@ namespace MCPServer.Editor.Commands EditorSceneManager.SaveScene(scene); return new { success = true, message = $"Saved scene: {scene.path}" }; } - catch (System.Exception e) + catch (Exception e) { return new { success = false, error = $"Failed to save scene: {e.Message}", stackTrace = e.StackTrace }; } @@ -96,7 +96,7 @@ namespace MCPServer.Editor.Commands return new { success = true, message = $"Created new scene at: {scenePath}" }; } - catch (System.Exception e) + catch (Exception e) { return new { success = false, error = $"Failed to create new scene: {e.Message}", stackTrace = e.StackTrace }; } @@ -131,7 +131,7 @@ namespace MCPServer.Editor.Commands EditorSceneManager.OpenScene(scenePath); return new { success = true, message = $"Changed to scene: {scenePath}" }; } - catch (System.Exception e) + catch (Exception e) { return new { success = false, error = $"Failed to change scene: {e.Message}", stackTrace = e.StackTrace }; } diff --git a/Editor/Commands/ScriptCommandHandler.cs b/Editor/Commands/ScriptCommandHandler.cs index 18055f7..6b6af57 100644 --- a/Editor/Commands/ScriptCommandHandler.cs +++ b/Editor/Commands/ScriptCommandHandler.cs @@ -5,9 +5,8 @@ using System.IO; using System.Text; using System.Linq; using Newtonsoft.Json.Linq; -using MCPServer.Editor.Helpers; -namespace MCPServer.Editor.Commands +namespace UnityMCP.Editor.Commands { /// /// Handles script-related commands for Unity @@ -19,12 +18,9 @@ namespace MCPServer.Editor.Commands /// public static object ViewScript(JObject @params) { - string scriptPath = (string)@params["script_path"] ?? throw new System.Exception("Parameter 'script_path' is required."); + string scriptPath = (string)@params["script_path"] ?? throw new Exception("Parameter 'script_path' is required."); bool requireExists = (bool?)@params["require_exists"] ?? true; - - // Debug to help diagnose issues - Debug.Log($"ViewScript - Original script path: {scriptPath}"); - + // Handle path correctly to avoid double "Assets" folder issue string relativePath; if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) @@ -36,16 +32,14 @@ namespace MCPServer.Editor.Commands { relativePath = scriptPath; } - + string fullPath = Path.Combine(Application.dataPath, relativePath); - Debug.Log($"ViewScript - Relative path: {relativePath}"); - Debug.Log($"ViewScript - Full path: {fullPath}"); if (!File.Exists(fullPath)) { if (requireExists) { - throw new System.Exception($"Script file not found: {scriptPath}"); + throw new Exception($"Script file not found: {scriptPath}"); } else { @@ -76,7 +70,7 @@ namespace MCPServer.Editor.Commands /// public static object CreateScript(JObject @params) { - string scriptName = (string)@params["script_name"] ?? throw new System.Exception("Parameter 'script_name' is required."); + string scriptName = (string)@params["script_name"] ?? throw new Exception("Parameter 'script_name' is required."); string scriptType = (string)@params["script_type"] ?? "MonoBehaviour"; string namespaceName = (string)@params["namespace"]; string template = (string)@params["template"]; @@ -93,7 +87,7 @@ namespace MCPServer.Editor.Commands // Determine the script path string scriptPath; - + // Handle the script folder parameter if (string.IsNullOrEmpty(scriptFolder)) { @@ -105,7 +99,7 @@ namespace MCPServer.Editor.Commands { // Use provided folder path scriptPath = scriptFolder; - + // If scriptFolder starts with "Assets/", remove it for local path operations if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) { @@ -115,10 +109,7 @@ namespace MCPServer.Editor.Commands // Create the full directory path, avoiding Assets/Assets issue string folderPath = Path.Combine(Application.dataPath, scriptPath); - Debug.Log($"CreateScript - Script name: {scriptName}"); - Debug.Log($"CreateScript - Script path: {scriptPath}"); - Debug.Log($"CreateScript - Creating script in folder path: {folderPath}"); - + // Create directory if it doesn't exist if (!Directory.Exists(folderPath)) { @@ -129,7 +120,7 @@ namespace MCPServer.Editor.Commands } catch (Exception ex) { - throw new System.Exception($"Failed to create directory '{scriptPath}': {ex.Message}"); + throw new Exception($"Failed to create directory '{scriptPath}': {ex.Message}"); } } @@ -137,7 +128,7 @@ namespace MCPServer.Editor.Commands string fullFilePath = Path.Combine(folderPath, scriptName); if (File.Exists(fullFilePath) && !overwrite) { - throw new System.Exception($"Script file '{scriptName}' already exists in '{scriptPath}' and overwrite is not enabled."); + throw new Exception($"Script file '{scriptName}' already exists in '{scriptPath}' and overwrite is not enabled."); } try @@ -151,7 +142,7 @@ namespace MCPServer.Editor.Commands else { // Otherwise generate content based on template and parameters - StringBuilder contentBuilder = new StringBuilder(); + StringBuilder contentBuilder = new(); // Add using directives contentBuilder.AppendLine("using UnityEngine;"); @@ -212,8 +203,9 @@ namespace MCPServer.Editor.Commands { relativePath = $"Assets/{relativePath}"; } - - return new { + + return new + { message = $"Created script: {Path.Combine(relativePath, scriptName).Replace('\\', '/')}", script_path = Path.Combine(relativePath, scriptName).Replace('\\', '/') }; @@ -221,7 +213,7 @@ namespace MCPServer.Editor.Commands catch (Exception ex) { Debug.LogError($"Failed to create script: {ex.Message}\n{ex.StackTrace}"); - throw new System.Exception($"Failed to create script '{scriptName}': {ex.Message}"); + throw new Exception($"Failed to create script '{scriptName}': {ex.Message}"); } } @@ -230,8 +222,8 @@ namespace MCPServer.Editor.Commands /// public static object UpdateScript(JObject @params) { - string scriptPath = (string)@params["script_path"] ?? throw new System.Exception("Parameter 'script_path' is required."); - string content = (string)@params["content"] ?? throw new System.Exception("Parameter 'content' is required."); + string scriptPath = (string)@params["script_path"] ?? throw new Exception("Parameter 'script_path' is required."); + string content = (string)@params["content"] ?? throw new Exception("Parameter 'content' is required."); bool createIfMissing = (bool?)@params["create_if_missing"] ?? false; bool createFolderIfMissing = (bool?)@params["create_folder_if_missing"] ?? false; @@ -246,14 +238,12 @@ namespace MCPServer.Editor.Commands { relativePath = scriptPath; } - + string fullPath = Path.Combine(Application.dataPath, relativePath); string directory = Path.GetDirectoryName(fullPath); - + // Debug the paths to help diagnose issues - Debug.Log($"UpdateScript - Original script path: {scriptPath}"); - Debug.Log($"UpdateScript - Relative path: {relativePath}"); - Debug.Log($"UpdateScript - Full path: {fullPath}"); + // Check if file exists, create if requested if (!File.Exists(fullPath)) @@ -267,7 +257,7 @@ namespace MCPServer.Editor.Commands } else if (!Directory.Exists(directory)) { - throw new System.Exception($"Directory does not exist: {Path.GetDirectoryName(scriptPath)}"); + throw new Exception($"Directory does not exist: {Path.GetDirectoryName(scriptPath)}"); } // Create the file with content @@ -277,7 +267,7 @@ namespace MCPServer.Editor.Commands } else { - throw new System.Exception($"Script file not found: {scriptPath}"); + throw new Exception($"Script file not found: {scriptPath}"); } } @@ -316,7 +306,7 @@ namespace MCPServer.Editor.Commands } if (!Directory.Exists(fullPath)) - throw new System.Exception($"Folder not found: {folderPath}"); + throw new Exception($"Folder not found: {folderPath}"); string[] scripts = Directory.GetFiles(fullPath, "*.cs", SearchOption.AllDirectories) .Select(path => path.Replace(Application.dataPath, "Assets")) @@ -330,14 +320,14 @@ namespace MCPServer.Editor.Commands /// public static object AttachScript(JObject @params) { - string objectName = (string)@params["object_name"] ?? throw new System.Exception("Parameter 'object_name' is required."); - string scriptName = (string)@params["script_name"] ?? throw new System.Exception("Parameter 'script_name' is required."); + string objectName = (string)@params["object_name"] ?? throw new Exception("Parameter 'object_name' is required."); + string scriptName = (string)@params["script_name"] ?? throw new Exception("Parameter 'script_name' is required."); string scriptPath = (string)@params["script_path"]; // Optional // Find the target object GameObject targetObject = GameObject.Find(objectName); if (targetObject == null) - throw new System.Exception($"Object '{objectName}' not found in scene."); + throw new Exception($"Object '{objectName}' not found in scene."); // Ensure script name ends with .cs if (!scriptName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) @@ -349,7 +339,7 @@ namespace MCPServer.Editor.Commands // Find the script asset string[] guids; - + if (!string.IsNullOrEmpty(scriptPath)) { // If a specific path is provided, try that first @@ -359,7 +349,7 @@ namespace MCPServer.Editor.Commands MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath(scriptPath); if (scriptAsset != null) { - System.Type scriptType = scriptAsset.GetClass(); + Type scriptType = scriptAsset.GetClass(); if (scriptType != null) { try @@ -378,7 +368,7 @@ namespace MCPServer.Editor.Commands catch (Exception ex) { Debug.LogError($"Error attaching script component: {ex.Message}"); - throw new System.Exception($"Failed to add component: {ex.Message}"); + throw new Exception($"Failed to add component: {ex.Message}"); } } } @@ -387,36 +377,36 @@ namespace MCPServer.Editor.Commands // Use the file name for searching if direct path didn't work guids = AssetDatabase.FindAssets(scriptNameWithoutExtension + " t:script"); - + if (guids.Length == 0) { // Try a broader search if exact match fails guids = AssetDatabase.FindAssets(scriptNameWithoutExtension); - + if (guids.Length == 0) - throw new System.Exception($"Script '{scriptFileName}' not found in project."); + throw new Exception($"Script '{scriptFileName}' not found in project."); } // Check each potential script until we find one that can be attached foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); - + // Filter to only consider .cs files if (!path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) continue; - + // Double check the file name to avoid false matches string foundFileName = Path.GetFileName(path); - if (!string.Equals(foundFileName, scriptFileName, StringComparison.OrdinalIgnoreCase) && + if (!string.Equals(foundFileName, scriptFileName, StringComparison.OrdinalIgnoreCase) && !string.Equals(Path.GetFileNameWithoutExtension(foundFileName), scriptNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) continue; - + MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath(path); if (scriptAsset == null) continue; - System.Type scriptType = scriptAsset.GetClass(); + Type scriptType = scriptAsset.GetClass(); if (scriptType == null || !typeof(MonoBehaviour).IsAssignableFrom(scriptType)) continue; @@ -452,7 +442,7 @@ namespace MCPServer.Editor.Commands } // If we've tried all possibilities and nothing worked - throw new System.Exception($"Could not attach script '{scriptFileName}' to object '{objectName}'. No valid script found or component creation failed."); + throw new Exception($"Could not attach script '{scriptFileName}' to object '{objectName}'. No valid script found or component creation failed."); } } } \ No newline at end of file diff --git a/Editor/Data.meta b/Editor/Data.meta new file mode 100644 index 0000000..bb714ec --- /dev/null +++ b/Editor/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e59036660cc33d24596fbbf6d4657a83 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Data/DefaultServerConfig.cs b/Editor/Data/DefaultServerConfig.cs new file mode 100644 index 0000000..2df0185 --- /dev/null +++ b/Editor/Data/DefaultServerConfig.cs @@ -0,0 +1,17 @@ +using UnityMCP.Editor.Models; + +namespace UnityMCP.Editor.Data +{ + public class DefaultServerConfig : ServerConfig + { + public new string unityHost = "localhost"; + public new int unityPort = 6400; + public new int mcpPort = 6500; + public new float connectionTimeout = 15.0f; + public new int bufferSize = 32768; + public new string logLevel = "INFO"; + public new string logFormat = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"; + public new int maxRetries = 3; + public new float retryDelay = 1.0f; + } +} \ No newline at end of file diff --git a/Editor/Data/DefaultServerConfig.cs.meta b/Editor/Data/DefaultServerConfig.cs.meta new file mode 100644 index 0000000..6df0a87 --- /dev/null +++ b/Editor/Data/DefaultServerConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: de8f5721c34f7194392e9d8c7d0226c0 \ No newline at end of file diff --git a/Editor/Data/McpClients.cs b/Editor/Data/McpClients.cs new file mode 100644 index 0000000..0c692da --- /dev/null +++ b/Editor/Data/McpClients.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.IO; +using UnityMCP.Editor.Models; + +namespace UnityMCP.Editor.Data +{ + public class McpClients + { + public List clients = new() { + new() { + name = "Claude Desktop", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Claude", + "claude_desktop_config.json" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Library", + "Application Support", + "Claude", + "claude_desktop_config.json" + ), + mcpType = McpTypes.ClaudeDesktop, + configStatus = "Not Configured" + }, + new() { + name = "Cursor", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cursor", + "mcp.json" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cursor", + "mcp.json" + ), + mcpType = McpTypes.Cursor, + configStatus = "Not Configured" + } + }; + + // Initialize status enums after construction + public McpClients() + { + foreach (var client in clients) + { + if (client.configStatus == "Not Configured") + { + client.status = McpStatus.NotConfigured; + } + } + } + } +} \ No newline at end of file diff --git a/Editor/Data/McpClients.cs.meta b/Editor/Data/McpClients.cs.meta new file mode 100644 index 0000000..3c8449a --- /dev/null +++ b/Editor/Data/McpClients.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 711b86bbc1f661e4fb2c822e14970e16 \ No newline at end of file diff --git a/Editor/Helpers/Vector3Helper.cs b/Editor/Helpers/Vector3Helper.cs index 4056eff..55cb68c 100644 --- a/Editor/Helpers/Vector3Helper.cs +++ b/Editor/Helpers/Vector3Helper.cs @@ -1,7 +1,7 @@ using UnityEngine; using Newtonsoft.Json.Linq; -namespace MCPServer.Editor.Helpers +namespace UnityMCP.Editor.Helpers { /// /// Helper class for Vector3 operations diff --git a/Editor/MCPEditorWindow.cs b/Editor/MCPEditorWindow.cs deleted file mode 100644 index f581d28..0000000 --- a/Editor/MCPEditorWindow.cs +++ /dev/null @@ -1,622 +0,0 @@ -using UnityEngine; -using UnityEditor; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System; -using Newtonsoft.Json; -using System.Net.Sockets; -using System.Threading.Tasks; -using System.Text; -using System.Collections.Generic; - -public class DefaultServerConfig : ServerConfig -{ - public new string unityHost = "localhost"; - public new int unityPort = 6400; - public new int mcpPort = 6500; - public new float connectionTimeout = 15.0f; - public new int bufferSize = 32768; - public new string logLevel = "INFO"; - public new string logFormat = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"; - public new int maxRetries = 3; - public new float retryDelay = 1.0f; - -} - -[Serializable] -public class MCPConfig -{ - [JsonProperty("mcpServers")] - public MCPConfigServers mcpServers; -} - -[Serializable] -public class MCPConfigServers -{ - [JsonProperty("unityMCP")] - public MCPConfigServer unityMCP; -} - -[Serializable] -public class MCPConfigServer -{ - [JsonProperty("command")] - public string command; - - [JsonProperty("args")] - public string[] args; -} - -[Serializable] -public class ServerConfig -{ - [JsonProperty("unity_host")] - public string unityHost = "localhost"; - - [JsonProperty("unity_port")] - public int unityPort; - - [JsonProperty("mcp_port")] - public int mcpPort; - - [JsonProperty("connection_timeout")] - public float connectionTimeout; - - [JsonProperty("buffer_size")] - public int bufferSize; - - [JsonProperty("log_level")] - public string logLevel; - - [JsonProperty("log_format")] - public string logFormat; - - [JsonProperty("max_retries")] - public int maxRetries; - - [JsonProperty("retry_delay")] - public float retryDelay; -} - -public class MCPEditorWindow : EditorWindow -{ - private bool isUnityBridgeRunning = false; - private Vector2 scrollPosition; - private string claudeConfigStatus = "Not configured"; - private string pythonServerStatus = "Not Connected"; - private Color pythonServerStatusColor = Color.red; - private const int unityPort = 6400; // Hardcoded Unity port - private const int mcpPort = 6500; // Hardcoded MCP port - private const float CONNECTION_CHECK_INTERVAL = 2f; // Check every 2 seconds - private float lastCheckTime = 0f; - - [MenuItem("Window/Unity MCP")] - public static void ShowWindow() - { - GetWindow("MCP Editor"); - } - - private void OnEnable() - { - // Check initial states - isUnityBridgeRunning = UnityMCPBridge.IsRunning; - CheckPythonServerConnection(); - } - - private void Update() - { - // Check Python server connection periodically - if (Time.realtimeSinceStartup - lastCheckTime >= CONNECTION_CHECK_INTERVAL) - { - CheckPythonServerConnection(); - lastCheckTime = Time.realtimeSinceStartup; - } - } - - private async void CheckPythonServerConnection() - { - try - { - using (var client = new TcpClient()) - { - // Try to connect with a short timeout - var connectTask = client.ConnectAsync("localhost", unityPort); - if (await Task.WhenAny(connectTask, Task.Delay(1000)) == connectTask) - { - // Try to send a ping message to verify connection is alive - try - { - NetworkStream stream = client.GetStream(); - byte[] pingMessage = Encoding.UTF8.GetBytes("ping"); - await stream.WriteAsync(pingMessage, 0, pingMessage.Length); - - // Wait for response with timeout - byte[] buffer = new byte[1024]; - var readTask = stream.ReadAsync(buffer, 0, buffer.Length); - if (await Task.WhenAny(readTask, Task.Delay(1000)) == readTask) - { - // Connection successful and responsive - pythonServerStatus = "Connected"; - pythonServerStatusColor = Color.green; - UnityEngine.Debug.Log($"Python server connected successfully on port {unityPort}"); - } - else - { - // No response received - pythonServerStatus = "No Response"; - pythonServerStatusColor = Color.yellow; - UnityEngine.Debug.LogWarning($"Python server not responding on port {unityPort}"); - } - } - catch (Exception e) - { - // Connection established but communication failed - pythonServerStatus = "Communication Error"; - pythonServerStatusColor = Color.yellow; - UnityEngine.Debug.LogWarning($"Error communicating with Python server: {e.Message}"); - } - } - else - { - // Connection failed - pythonServerStatus = "Not Connected"; - pythonServerStatusColor = Color.red; - UnityEngine.Debug.LogWarning($"Python server is not running or not accessible on port {unityPort}"); - } - client.Close(); - } - } - catch (Exception e) - { - pythonServerStatus = "Connection Error"; - pythonServerStatusColor = Color.red; - UnityEngine.Debug.LogError($"Error checking Python server connection: {e.Message}"); - } - } - - private void OnGUI() - { - scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); - - EditorGUILayout.Space(10); - EditorGUILayout.LabelField("MCP Editor", EditorStyles.boldLabel); - EditorGUILayout.Space(10); - - // Python Server Status Section - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.LabelField("Python Server Status", EditorStyles.boldLabel); - - // Status bar - var statusRect = EditorGUILayout.BeginHorizontal(); - EditorGUI.DrawRect(new Rect(statusRect.x, statusRect.y, 10, 20), pythonServerStatusColor); - EditorGUILayout.LabelField(pythonServerStatus); - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.LabelField($"Unity Port: {unityPort}"); - EditorGUILayout.LabelField($"MCP Port: {mcpPort}"); - EditorGUILayout.HelpBox("Start the Python server using command line: 'uv run server.py' in the Python directory", MessageType.Info); - EditorGUILayout.EndVertical(); - - EditorGUILayout.Space(10); - - // Unity Bridge Section - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.LabelField("Unity MCP Bridge", EditorStyles.boldLabel); - EditorGUILayout.LabelField($"Status: {(isUnityBridgeRunning ? "Running" : "Stopped")}"); - EditorGUILayout.LabelField($"Port: {unityPort}"); - - if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge")) - { - ToggleUnityBridge(); - } - EditorGUILayout.EndVertical(); - - EditorGUILayout.Space(10); - - // Claude Desktop Configuration Section - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.LabelField("Claude Desktop Configuration", EditorStyles.boldLabel); - EditorGUILayout.LabelField($"Status: {claudeConfigStatus}"); - - if (GUILayout.Button("Configure Claude Desktop")) - { - ConfigureClaudeDesktop(); - } - EditorGUILayout.EndVertical(); - - EditorGUILayout.EndScrollView(); - } - - private void ToggleUnityBridge() - { - if (isUnityBridgeRunning) - { - UnityMCPBridge.Stop(); - } - else - { - UnityMCPBridge.Start(); - } - isUnityBridgeRunning = !isUnityBridgeRunning; - } - - private void ConfigureClaudeDesktop() - { - try - { - // Determine the config file path based on OS - string configPath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - configPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "Claude", - "claude_desktop_config.json" - ); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - configPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "Library", - "Application Support", - "Claude", - "claude_desktop_config.json" - ); - } - else - { - claudeConfigStatus = "Unsupported OS"; - return; - } - - // Create directory if it doesn't exist - Directory.CreateDirectory(Path.GetDirectoryName(configPath)); - - // Find the server.py file location - string serverPath = null; - string pythonDir = null; - - // List of possible locations to search - var possiblePaths = new List - { - // Search in Assets folder - Manual installation - Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python", "server.py")), - Path.GetFullPath(Path.Combine(Application.dataPath, "Packages", "com.justinpbarnett.unity-mcp", "Python", "server.py")), - - // Search in package cache - Package manager installation - Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Library", "PackageCache", "com.justinpbarnett.unity-mcp@*", "Python", "server.py")), - - // Search in package manager packages - Git installation - Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Packages", "com.justinpbarnett.unity-mcp", "Python", "server.py")) - }; - - UnityEngine.Debug.Log("Searching for server.py in the following locations:"); - - // First try with explicit paths - foreach (var path in possiblePaths) - { - // Skip wildcard paths for now - if (path.Contains("*")) continue; - - UnityEngine.Debug.Log($"Checking: {path}"); - if (File.Exists(path)) - { - serverPath = path; - pythonDir = Path.GetDirectoryName(serverPath); - UnityEngine.Debug.Log($"Found server.py at: {serverPath}"); - break; - } - } - - // If not found, try with wildcard paths (package cache with version) - if (serverPath == null) - { - foreach (var path in possiblePaths) - { - if (!path.Contains("*")) continue; - - string directoryPath = Path.GetDirectoryName(path); - string searchPattern = Path.GetFileName(Path.GetDirectoryName(path)); - string parentDir = Path.GetDirectoryName(directoryPath); - - if (Directory.Exists(parentDir)) - { - var matchingDirs = Directory.GetDirectories(parentDir, searchPattern); - UnityEngine.Debug.Log($"Searching in: {parentDir} for pattern: {searchPattern}, found {matchingDirs.Length} matches"); - - foreach (var dir in matchingDirs) - { - string candidatePath = Path.Combine(dir, "Python", "server.py"); - UnityEngine.Debug.Log($"Checking: {candidatePath}"); - - if (File.Exists(candidatePath)) - { - serverPath = candidatePath; - pythonDir = Path.GetDirectoryName(serverPath); - UnityEngine.Debug.Log($"Found server.py at: {serverPath}"); - break; - } - } - - if (serverPath != null) break; - } - } - } - - if (serverPath == null || !File.Exists(serverPath)) - { - ShowManualConfigurationInstructions(configPath); - return; - } - - UnityEngine.Debug.Log($"Using server.py at: {serverPath}"); - UnityEngine.Debug.Log($"Python directory: {pythonDir}"); - - // Create configuration object - var config = new MCPConfig - { - mcpServers = new MCPConfigServers - { - unityMCP = new MCPConfigServer - { - command = "uv", - args = new[] - { - "--directory", - pythonDir, - "run", - "server.py" - } - } - } - }; - - // Serialize and write to file with proper formatting - var jsonSettings = new JsonSerializerSettings - { - Formatting = Formatting.Indented - }; - string jsonConfig = JsonConvert.SerializeObject(config, jsonSettings); - File.WriteAllText(configPath, jsonConfig); - - claudeConfigStatus = "Configured successfully"; - UnityEngine.Debug.Log($"Claude Desktop configuration saved to: {configPath}"); - UnityEngine.Debug.Log($"Configuration contents:\n{jsonConfig}"); - } - catch (Exception e) - { - // Determine the config file path based on OS for error message - string configPath = ""; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - configPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "Claude", - "claude_desktop_config.json" - ); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - configPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "Library", - "Application Support", - "Claude", - "claude_desktop_config.json" - ); - } - - ShowManualConfigurationInstructions(configPath); - UnityEngine.Debug.LogError($"Failed to configure Claude Desktop: {e.Message}\n{e.StackTrace}"); - } - } - - private void ShowManualConfigurationInstructions(string configPath) - { - claudeConfigStatus = "Error: Manual configuration required"; - - // Get the Python directory path using Package Manager API - string pythonDir = FindPackagePythonDirectory(); - - // Create the manual configuration message - var jsonConfig = new MCPConfig - { - mcpServers = new MCPConfigServers - { - unityMCP = new MCPConfigServer - { - command = "uv", - args = new[] - { - "--directory", - pythonDir, - "run", - "server.py" - } - } - } - }; - - var jsonSettings = new JsonSerializerSettings - { - Formatting = Formatting.Indented - }; - string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); - - // Show a dedicated configuration window instead of console logs - ManualConfigWindow.ShowWindow(configPath, manualConfigJson); - } - - private string FindPackagePythonDirectory() - { - string pythonDir = "/path/to/your/unity-mcp/Python"; - - try - { - // Try to find the package using Package Manager API - var request = UnityEditor.PackageManager.Client.List(); - while (!request.IsCompleted) { } // Wait for the request to complete - - if (request.Status == UnityEditor.PackageManager.StatusCode.Success) - { - foreach (var package in request.Result) - { - UnityEngine.Debug.Log($"Package: {package.name}, Path: {package.resolvedPath}"); - - if (package.name == "com.justinpbarnett.unity-mcp") - { - string packagePath = package.resolvedPath; - string potentialPythonDir = Path.Combine(packagePath, "Python"); - - if (Directory.Exists(potentialPythonDir) && - File.Exists(Path.Combine(potentialPythonDir, "server.py"))) - { - UnityEngine.Debug.Log($"Found package Python directory at: {potentialPythonDir}"); - return potentialPythonDir; - } - } - } - } - else if (request.Error != null) - { - UnityEngine.Debug.LogError("Failed to list packages: " + request.Error.message); - } - - // If not found via Package Manager, try manual approaches - // First check for local installation - string[] possibleDirs = { - Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python")) - }; - - foreach (var dir in possibleDirs) - { - UnityEngine.Debug.Log($"Checking local directory: {dir}"); - if (Directory.Exists(dir) && File.Exists(Path.Combine(dir, "server.py"))) - { - UnityEngine.Debug.Log($"Found local Python directory at: {dir}"); - return dir; - } - } - - // If still not found, return the placeholder path - UnityEngine.Debug.LogWarning("Could not find Python directory, using placeholder path"); - } - catch (Exception e) - { - UnityEngine.Debug.LogError($"Error finding package path: {e.Message}"); - } - - return pythonDir; - } -} - -// Editor window to display manual configuration instructions -public class ManualConfigWindow : EditorWindow -{ - private string configPath; - private string configJson; - private Vector2 scrollPos; - private bool pathCopied = false; - private bool jsonCopied = false; - private float copyFeedbackTimer = 0; - - public static void ShowWindow(string configPath, string configJson) - { - var window = GetWindow("Manual Configuration"); - window.configPath = configPath; - window.configJson = configJson; - window.minSize = new Vector2(500, 400); - window.Show(); - } - - private void OnGUI() - { - scrollPos = EditorGUILayout.BeginScrollView(scrollPos); - - // Header - EditorGUILayout.Space(10); - EditorGUILayout.LabelField("Claude Desktop Manual Configuration", EditorStyles.boldLabel); - EditorGUILayout.Space(10); - - // Instructions - EditorGUILayout.LabelField("The automatic configuration failed. Please follow these steps:", EditorStyles.boldLabel); - EditorGUILayout.Space(5); - - EditorGUILayout.LabelField("1. Open Claude Desktop and go to Settings > Developer > Edit Config", EditorStyles.wordWrappedLabel); - EditorGUILayout.LabelField("2. Create or edit the configuration file at:", EditorStyles.wordWrappedLabel); - - // Config path section with copy button - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.SelectableLabel(configPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight)); - - if (GUILayout.Button("Copy Path", GUILayout.Width(80))) - { - EditorGUIUtility.systemCopyBuffer = configPath; - pathCopied = true; - copyFeedbackTimer = 2f; - } - - EditorGUILayout.EndHorizontal(); - - if (pathCopied) - { - EditorGUILayout.LabelField("Path copied to clipboard!", EditorStyles.miniLabel); - } - - EditorGUILayout.Space(10); - - // JSON configuration - EditorGUILayout.LabelField("3. Paste the following JSON configuration:", EditorStyles.wordWrappedLabel); - EditorGUILayout.Space(5); - - EditorGUILayout.LabelField("Make sure to replace the Python path if necessary:", EditorStyles.wordWrappedLabel); - EditorGUILayout.Space(5); - - // JSON text area with copy button - GUIStyle textAreaStyle = new GUIStyle(EditorStyles.textArea) - { - wordWrap = true, - richText = true - }; - - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.SelectableLabel(configJson, textAreaStyle, GUILayout.MinHeight(200)); - EditorGUILayout.EndHorizontal(); - - if (GUILayout.Button("Copy JSON Configuration")) - { - EditorGUIUtility.systemCopyBuffer = configJson; - jsonCopied = true; - copyFeedbackTimer = 2f; - } - - if (jsonCopied) - { - EditorGUILayout.LabelField("JSON copied to clipboard!", EditorStyles.miniLabel); - } - - EditorGUILayout.Space(10); - - // Additional note - EditorGUILayout.HelpBox("After configuring, restart Claude Desktop to apply the changes.", MessageType.Info); - - EditorGUILayout.EndScrollView(); - } - - private void Update() - { - // Handle the feedback message timer - if (copyFeedbackTimer > 0) - { - copyFeedbackTimer -= Time.deltaTime; - if (copyFeedbackTimer <= 0) - { - pathCopied = false; - jsonCopied = false; - Repaint(); - } - } - } -} \ No newline at end of file diff --git a/Editor/Models/Command.cs b/Editor/Models/Command.cs index a7048b6..4f153f4 100644 --- a/Editor/Models/Command.cs +++ b/Editor/Models/Command.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json.Linq; -namespace MCPServer.Editor.Models +namespace UnityMCP.Editor.Models { /// /// Represents a command received from the MCP client diff --git a/Editor/Models/MCPConfig.cs b/Editor/Models/MCPConfig.cs new file mode 100644 index 0000000..9372b22 --- /dev/null +++ b/Editor/Models/MCPConfig.cs @@ -0,0 +1,12 @@ +using System; +using Newtonsoft.Json; + +namespace UnityMCP.Editor.Models +{ + [Serializable] + public class MCPConfig + { + [JsonProperty("mcpServers")] + public MCPConfigServers mcpServers; + } +} diff --git a/Editor/Models/MCPConfig.cs.meta b/Editor/Models/MCPConfig.cs.meta new file mode 100644 index 0000000..1f70925 --- /dev/null +++ b/Editor/Models/MCPConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c17c09908f0c1524daa8b6957ce1f7f5 \ No newline at end of file diff --git a/Editor/Models/MCPConfigServer.cs b/Editor/Models/MCPConfigServer.cs new file mode 100644 index 0000000..d182f67 --- /dev/null +++ b/Editor/Models/MCPConfigServer.cs @@ -0,0 +1,15 @@ +using System; +using Newtonsoft.Json; + +namespace UnityMCP.Editor.Models +{ + [Serializable] + public class MCPConfigServer + { + [JsonProperty("command")] + public string command; + + [JsonProperty("args")] + public string[] args; + } +} diff --git a/Editor/Models/MCPConfigServer.cs.meta b/Editor/Models/MCPConfigServer.cs.meta new file mode 100644 index 0000000..4dad0b4 --- /dev/null +++ b/Editor/Models/MCPConfigServer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5fae9d995f514e9498e9613e2cdbeca9 \ No newline at end of file diff --git a/Editor/Models/MCPConfigServers.cs b/Editor/Models/MCPConfigServers.cs new file mode 100644 index 0000000..de9e875 --- /dev/null +++ b/Editor/Models/MCPConfigServers.cs @@ -0,0 +1,12 @@ +using System; +using Newtonsoft.Json; + +namespace UnityMCP.Editor.Models +{ + [Serializable] + public class MCPConfigServers + { + [JsonProperty("unityMCP")] + public MCPConfigServer unityMCP; + } +} diff --git a/Editor/Models/MCPConfigServers.cs.meta b/Editor/Models/MCPConfigServers.cs.meta new file mode 100644 index 0000000..9ef1310 --- /dev/null +++ b/Editor/Models/MCPConfigServers.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bcb583553e8173b49be71a5c43bd9502 \ No newline at end of file diff --git a/Editor/Models/McpClient.cs b/Editor/Models/McpClient.cs new file mode 100644 index 0000000..d900f41 --- /dev/null +++ b/Editor/Models/McpClient.cs @@ -0,0 +1,48 @@ +namespace UnityMCP.Editor.Models +{ + public class McpClient + { + public string name; + public string windowsConfigPath; + public string linuxConfigPath; + public McpTypes mcpType; + public string configStatus; + public McpStatus status = McpStatus.NotConfigured; + + // Helper method to convert the enum to a display string + public string GetStatusDisplayString() + { + return status switch + { + McpStatus.NotConfigured => "Not Configured", + McpStatus.Configured => "Configured", + McpStatus.Running => "Running", + McpStatus.Connected => "Connected", + McpStatus.IncorrectPath => "Incorrect Path", + McpStatus.CommunicationError => "Communication Error", + McpStatus.NoResponse => "No Response", + McpStatus.UnsupportedOS => "Unsupported OS", + McpStatus.MissingConfig => "Missing UnityMCP Config", + McpStatus.Error => configStatus.StartsWith("Error:") ? configStatus : "Error", + _ => "Unknown" + }; + } + + // Helper method to set both status enum and string for backward compatibility + public void SetStatus(McpStatus newStatus, string errorDetails = null) + { + status = newStatus; + + if (newStatus == McpStatus.Error && !string.IsNullOrEmpty(errorDetails)) + { + configStatus = $"Error: {errorDetails}"; + } + else + { + configStatus = GetStatusDisplayString(); + } + } + } +} + + diff --git a/Editor/Models/McpClient.cs.meta b/Editor/Models/McpClient.cs.meta new file mode 100644 index 0000000..a11df35 --- /dev/null +++ b/Editor/Models/McpClient.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b1afa56984aec0d41808edcebf805e6a \ No newline at end of file diff --git a/Editor/Models/McpStatus.cs b/Editor/Models/McpStatus.cs new file mode 100644 index 0000000..36308e5 --- /dev/null +++ b/Editor/Models/McpStatus.cs @@ -0,0 +1,17 @@ +namespace UnityMCP.Editor.Models +{ + // Enum representing the various status states for MCP clients + public enum McpStatus + { + NotConfigured, // Not set up yet + Configured, // Successfully configured + Running, // Service is running + Connected, // Successfully connected + IncorrectPath, // Configuration has incorrect paths + CommunicationError, // Connected but communication issues + NoResponse, // Connected but not responding + MissingConfig, // Config file exists but missing required elements + UnsupportedOS, // OS is not supported + Error // General error state + } +} \ No newline at end of file diff --git a/Editor/Models/McpStatus.cs.meta b/Editor/Models/McpStatus.cs.meta new file mode 100644 index 0000000..4e5feb5 --- /dev/null +++ b/Editor/Models/McpStatus.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: aa63057c9e5282d4887352578bf49971 \ No newline at end of file diff --git a/Editor/Models/McpTypes.cs b/Editor/Models/McpTypes.cs new file mode 100644 index 0000000..2c08e82 --- /dev/null +++ b/Editor/Models/McpTypes.cs @@ -0,0 +1,8 @@ +namespace UnityMCP.Editor.Models +{ + public enum McpTypes + { + ClaudeDesktop, + Cursor + } +} \ No newline at end of file diff --git a/Editor/Models/McpTypes.cs.meta b/Editor/Models/McpTypes.cs.meta new file mode 100644 index 0000000..d20128c --- /dev/null +++ b/Editor/Models/McpTypes.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1 \ No newline at end of file diff --git a/Editor/Models/ServerConfig.cs b/Editor/Models/ServerConfig.cs new file mode 100644 index 0000000..4dc729d --- /dev/null +++ b/Editor/Models/ServerConfig.cs @@ -0,0 +1,36 @@ +using System; +using Newtonsoft.Json; + +namespace UnityMCP.Editor.Models +{ + [Serializable] + public class ServerConfig + { + [JsonProperty("unity_host")] + public string unityHost = "localhost"; + + [JsonProperty("unity_port")] + public int unityPort; + + [JsonProperty("mcp_port")] + public int mcpPort; + + [JsonProperty("connection_timeout")] + public float connectionTimeout; + + [JsonProperty("buffer_size")] + public int bufferSize; + + [JsonProperty("log_level")] + public string logLevel; + + [JsonProperty("log_format")] + public string logFormat; + + [JsonProperty("max_retries")] + public int maxRetries; + + [JsonProperty("retry_delay")] + public float retryDelay; + } +} diff --git a/Editor/Models/ServerConfig.cs.meta b/Editor/Models/ServerConfig.cs.meta new file mode 100644 index 0000000..0c4b377 --- /dev/null +++ b/Editor/Models/ServerConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e4e45386fcc282249907c2e3c7e5d9c6 \ No newline at end of file diff --git a/Editor/UnityMCPBridge.cs b/Editor/UnityMCPBridge.cs index 5b6eda6..3d4c600 100644 --- a/Editor/UnityMCPBridge.cs +++ b/Editor/UnityMCPBridge.cs @@ -3,344 +3,343 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; -using System.Threading; using System.Threading.Tasks; using UnityEditor; using UnityEngine; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using MCPServer.Editor.Models; -using MCPServer.Editor.Commands; -using MCPServer.Editor.Helpers; using System.IO; +using UnityMCP.Editor.Models; +using UnityMCP.Editor.Commands; -[InitializeOnLoad] -public static partial class UnityMCPBridge +namespace UnityMCP.Editor { - private static TcpListener listener; - private static bool isRunning = false; - private static readonly object lockObj = new object(); - private static Dictionary tcs)> commandQueue = new(); - private static readonly int unityPort = 6400; // Hardcoded port - - // Add public property to expose running state - public static bool IsRunning => isRunning; - - // Add method to check existence of a folder - public static bool FolderExists(string path) + [InitializeOnLoad] + public static partial class UnityMCPBridge { - if (string.IsNullOrEmpty(path)) - return false; + 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 - if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase)) - return true; + public static bool IsRunning => isRunning; - 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) + public static bool FolderExists(string path) { - try - { - var client = await listener.AcceptTcpClientAsync(); - // Enable basic socket keepalive - client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + if (string.IsNullOrEmpty(path)) + return false; - // Set longer receive timeout to prevent quick disconnections - client.ReceiveTimeout = 60000; // 60 seconds + if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase)) + return true; - // Fire and forget each client connection - _ = HandleClientAsync(client); - } - catch (Exception ex) - { - if (isRunning) Debug.LogError($"Listener error: {ex.Message}"); - } + string fullPath = Path.Combine(Application.dataPath, path.StartsWith("Assets/") ? path.Substring(7) : path); + return Directory.Exists(fullPath); } - } - private static async Task HandleClientAsync(TcpClient client) - { - using (client) - using (var stream = client.GetStream()) + 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() { - var buffer = new byte[8192]; while (isRunning) { try { - int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); - if (bytesRead == 0) break; // Client disconnected + var client = await listener.AcceptTcpClientAsync(); + // Enable basic socket keepalive + client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - string commandText = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead); - string commandId = Guid.NewGuid().ToString(); - var tcs = new TaskCompletionSource(); + // Set longer receive timeout to prevent quick disconnections + client.ReceiveTimeout = 60000; // 60 seconds - // 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); + // Fire and forget each client connection + _ = HandleClientAsync(client); } catch (Exception ex) { - Debug.LogError($"Client handler error: {ex.Message}"); - break; + if (isRunning) Debug.LogError($"Listener error: {ex.Message}"); } } } - } - private static void ProcessCommands() - { - List processedIds = new(); - lock (lockObj) + private static async Task HandleClientAsync(TcpClient client) { - foreach (var kvp in commandQueue.ToList()) + using (client) + using (var stream = client.GetStream()) { - string id = kvp.Key; - string commandText = kvp.Value.commandJson; - var tcs = kvp.Value.tcs; - - try + var buffer = new byte[8192]; + while (isRunning) { - // Special case handling - if (string.IsNullOrEmpty(commandText)) + try { - var emptyResponse = new + 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 = "Empty command received" + error = ex.Message, + commandType = "Unknown (error during processing)", + receivedText = commandText?.Length > 50 ? commandText.Substring(0, 50) + "..." : commandText }; - 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); + string responseJson = JsonConvert.SerializeObject(response); tcs.SetResult(responseJson); } + + processedIds.Add(id); } - catch (Exception ex) + + foreach (var id in processedIds) { - 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); + commandQueue.Remove(id); } - - 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)) + // 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; + } - text = text.Trim(); - if ((text.StartsWith("{") && text.EndsWith("}")) || // Object - (text.StartsWith("[") && text.EndsWith("]"))) // Array + private static string ExecuteCommand(Command command) { try { - JToken.Parse(text); - return true; + 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 == "ping") + { + var pingResponse = new { status = "success", result = new { message = "pong" } }; + return JsonConvert.SerializeObject(pingResponse); + } + + object result = command.type switch + { + "GET_SCENE_INFO" => SceneCommandHandler.GetSceneInfo(), + "OPEN_SCENE" => SceneCommandHandler.OpenScene(command.@params), + "SAVE_SCENE" => SceneCommandHandler.SaveScene(), + "NEW_SCENE" => SceneCommandHandler.NewScene(command.@params), + "CHANGE_SCENE" => SceneCommandHandler.ChangeScene(command.@params), + "GET_OBJECT_INFO" => ObjectCommandHandler.GetObjectInfo(command.@params), + "CREATE_OBJECT" => ObjectCommandHandler.CreateObject(command.@params), + "MODIFY_OBJECT" => ObjectCommandHandler.ModifyObject(command.@params), + "DELETE_OBJECT" => ObjectCommandHandler.DeleteObject(command.@params), + "GET_OBJECT_PROPERTIES" => ObjectCommandHandler.GetObjectProperties(command.@params), + "GET_COMPONENT_PROPERTIES" => ObjectCommandHandler.GetComponentProperties(command.@params), + "FIND_OBJECTS_BY_NAME" => ObjectCommandHandler.FindObjectsByName(command.@params), + "FIND_OBJECTS_BY_TAG" => ObjectCommandHandler.FindObjectsByTag(command.@params), + "GET_HIERARCHY" => ObjectCommandHandler.GetHierarchy(), + "SELECT_OBJECT" => ObjectCommandHandler.SelectObject(command.@params), + "GET_SELECTED_OBJECT" => ObjectCommandHandler.GetSelectedObject(), + "SET_MATERIAL" => MaterialCommandHandler.SetMaterial(command.@params), + "VIEW_SCRIPT" => ScriptCommandHandler.ViewScript(command.@params), + "CREATE_SCRIPT" => ScriptCommandHandler.CreateScript(command.@params), + "UPDATE_SCRIPT" => ScriptCommandHandler.UpdateScript(command.@params), + "LIST_SCRIPTS" => ScriptCommandHandler.ListScripts(command.@params), + "ATTACH_SCRIPT" => ScriptCommandHandler.AttachScript(command.@params), + "IMPORT_ASSET" => AssetCommandHandler.ImportAsset(command.@params), + "INSTANTIATE_PREFAB" => AssetCommandHandler.InstantiatePrefab(command.@params), + "CREATE_PREFAB" => AssetCommandHandler.CreatePrefab(command.@params), + "APPLY_PREFAB" => AssetCommandHandler.ApplyPrefab(command.@params), + "GET_ASSET_LIST" => AssetCommandHandler.GetAssetList(command.@params), + "EDITOR_CONTROL" => EditorControlHandler.HandleEditorControl(command.@params), + _ => throw new Exception($"Unknown command type: {command.type}") + }; + + var response = new { status = "success", result }; + return JsonConvert.SerializeObject(response); + } + catch (Exception ex) + { + Debug.LogError($"Error executing command {command.type}: {ex.Message}\n{ex.StackTrace}"); + var response = new + { + status = "error", + error = ex.Message, + command = command.type, + stackTrace = ex.StackTrace, + paramsSummary = command.@params != null ? GetParamsSummary(command.@params) : "No parameters" + }; + 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 false; + return "Could not summarize parameters"; } } - - 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 == "ping") - { - var pingResponse = new { status = "success", result = new { message = "pong" } }; - return JsonConvert.SerializeObject(pingResponse); - } - - object result = command.type switch - { - "GET_SCENE_INFO" => SceneCommandHandler.GetSceneInfo(), - "OPEN_SCENE" => SceneCommandHandler.OpenScene(command.@params), - "SAVE_SCENE" => SceneCommandHandler.SaveScene(), - "NEW_SCENE" => SceneCommandHandler.NewScene(command.@params), - "CHANGE_SCENE" => SceneCommandHandler.ChangeScene(command.@params), - "GET_OBJECT_INFO" => ObjectCommandHandler.GetObjectInfo(command.@params), - "CREATE_OBJECT" => ObjectCommandHandler.CreateObject(command.@params), - "MODIFY_OBJECT" => ObjectCommandHandler.ModifyObject(command.@params), - "DELETE_OBJECT" => ObjectCommandHandler.DeleteObject(command.@params), - "GET_OBJECT_PROPERTIES" => ObjectCommandHandler.GetObjectProperties(command.@params), - "GET_COMPONENT_PROPERTIES" => ObjectCommandHandler.GetComponentProperties(command.@params), - "FIND_OBJECTS_BY_NAME" => ObjectCommandHandler.FindObjectsByName(command.@params), - "FIND_OBJECTS_BY_TAG" => ObjectCommandHandler.FindObjectsByTag(command.@params), - "GET_HIERARCHY" => ObjectCommandHandler.GetHierarchy(), - "SELECT_OBJECT" => ObjectCommandHandler.SelectObject(command.@params), - "GET_SELECTED_OBJECT" => ObjectCommandHandler.GetSelectedObject(), - "SET_MATERIAL" => MaterialCommandHandler.SetMaterial(command.@params), - "VIEW_SCRIPT" => ScriptCommandHandler.ViewScript(command.@params), - "CREATE_SCRIPT" => ScriptCommandHandler.CreateScript(command.@params), - "UPDATE_SCRIPT" => ScriptCommandHandler.UpdateScript(command.@params), - "LIST_SCRIPTS" => ScriptCommandHandler.ListScripts(command.@params), - "ATTACH_SCRIPT" => ScriptCommandHandler.AttachScript(command.@params), - "IMPORT_ASSET" => AssetCommandHandler.ImportAsset(command.@params), - "INSTANTIATE_PREFAB" => AssetCommandHandler.InstantiatePrefab(command.@params), - "CREATE_PREFAB" => AssetCommandHandler.CreatePrefab(command.@params), - "APPLY_PREFAB" => AssetCommandHandler.ApplyPrefab(command.@params), - "GET_ASSET_LIST" => AssetCommandHandler.GetAssetList(command.@params), - "EDITOR_CONTROL" => EditorControlHandler.HandleEditorControl(command.@params), - _ => throw new Exception($"Unknown command type: {command.type}") - }; - - var response = new { status = "success", result }; - return JsonConvert.SerializeObject(response); - } - catch (Exception ex) - { - Debug.LogError($"Error executing command {command.type}: {ex.Message}\n{ex.StackTrace}"); - var response = new - { - status = "error", - error = ex.Message, - command = command.type, - stackTrace = ex.StackTrace, - paramsSummary = command.@params != null ? GetParamsSummary(command.@params) : "No parameters" - }; - 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"; - } } } \ No newline at end of file diff --git a/Editor/Windows.meta b/Editor/Windows.meta new file mode 100644 index 0000000..eda016e --- /dev/null +++ b/Editor/Windows.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d2ee39f5d4171184eb208e865c1ef4c1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Windows/ManualConfigEditorWindow.cs b/Editor/Windows/ManualConfigEditorWindow.cs new file mode 100644 index 0000000..dd1ca05 --- /dev/null +++ b/Editor/Windows/ManualConfigEditorWindow.cs @@ -0,0 +1,201 @@ +using UnityEngine; +using UnityEditor; +using System.Runtime.InteropServices; +using UnityMCP.Editor.Models; + +namespace UnityMCP.Editor.Windows +{ + // Editor window to display manual configuration instructions + public class ManualConfigEditorWindow : EditorWindow + { + private string configPath; + private string configJson; + private Vector2 scrollPos; + private bool pathCopied = false; + private bool jsonCopied = false; + private float copyFeedbackTimer = 0; + private McpClient mcpClient; + + public static void ShowWindow(string configPath, string configJson, McpClient mcpClient) + { + var window = GetWindow("Manual Configuration"); + window.configPath = configPath; + window.configJson = configJson; + window.mcpClient = mcpClient; + window.minSize = new Vector2(500, 400); + window.Show(); + } + + private void OnGUI() + { + scrollPos = EditorGUILayout.BeginScrollView(scrollPos); + + // Header with improved styling + EditorGUILayout.Space(10); + Rect titleRect = EditorGUILayout.GetControlRect(false, 30); + EditorGUI.DrawRect(new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height), new Color(0.2f, 0.2f, 0.2f, 0.1f)); + GUI.Label(new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height), + mcpClient.name + " Manual Configuration", EditorStyles.boldLabel); + EditorGUILayout.Space(10); + + // Instructions with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + Rect headerRect = EditorGUILayout.GetControlRect(false, 24); + EditorGUI.DrawRect(new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height), new Color(0.1f, 0.1f, 0.1f, 0.2f)); + GUI.Label(new Rect(headerRect.x + 8, headerRect.y + 4, headerRect.width - 16, headerRect.height), + "The automatic configuration failed. Please follow these steps:", EditorStyles.boldLabel); + EditorGUILayout.Space(10); + + GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel) + { + margin = new RectOffset(10, 10, 5, 5) + }; + + EditorGUILayout.LabelField("1. Open " + mcpClient.name + " config file by either:", instructionStyle); + if (mcpClient.mcpType == McpTypes.ClaudeDesktop) + { + EditorGUILayout.LabelField(" a) Going to Settings > Developer > Edit Config", instructionStyle); + } + else if (mcpClient.mcpType == McpTypes.Cursor) + { + EditorGUILayout.LabelField(" a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server", instructionStyle); + } + EditorGUILayout.LabelField(" OR", instructionStyle); + EditorGUILayout.LabelField(" b) Opening the configuration file at:", instructionStyle); + + // Path section with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + string displayPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + displayPath = mcpClient.windowsConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + displayPath = mcpClient.linuxConfigPath; + } + else + { + displayPath = configPath; + } + + // Prevent text overflow by allowing the text field to wrap + GUIStyle pathStyle = new(EditorStyles.textField) + { + wordWrap = true + }; + + EditorGUILayout.TextField(displayPath, pathStyle, GUILayout.Height(EditorGUIUtility.singleLineHeight)); + + // Copy button with improved styling + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUIStyle copyButtonStyle = new(GUI.skin.button) + { + padding = new RectOffset(15, 15, 5, 5), + margin = new RectOffset(10, 10, 5, 5) + }; + + if (GUILayout.Button("Copy Path", copyButtonStyle, GUILayout.Height(25), GUILayout.Width(100))) + { + EditorGUIUtility.systemCopyBuffer = displayPath; + pathCopied = true; + copyFeedbackTimer = 2f; + } + + if (GUILayout.Button("Open File", copyButtonStyle, GUILayout.Height(25), GUILayout.Width(100))) + { + // Open the file using the system's default application + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = displayPath, + UseShellExecute = true + }); + } + + if (pathCopied) + { + GUIStyle feedbackStyle = new(EditorStyles.label); + feedbackStyle.normal.textColor = Color.green; + EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60)); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + EditorGUILayout.LabelField("2. Paste the following JSON configuration:", instructionStyle); + + // JSON section with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // Improved text area for JSON with syntax highlighting colors + GUIStyle jsonStyle = new(EditorStyles.textArea) + { + font = EditorStyles.boldFont, + wordWrap = true + }; + jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue + + // Draw the JSON in a text area with a taller height for better readability + EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200)); + + // Copy JSON button with improved styling + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + if (GUILayout.Button("Copy JSON", copyButtonStyle, GUILayout.Height(25), GUILayout.Width(100))) + { + EditorGUIUtility.systemCopyBuffer = configJson; + jsonCopied = true; + copyFeedbackTimer = 2f; + } + + if (jsonCopied) + { + GUIStyle feedbackStyle = new(EditorStyles.label); + feedbackStyle.normal.textColor = Color.green; + EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60)); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + EditorGUILayout.LabelField("3. Save the file and restart " + mcpClient.name, instructionStyle); + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + // Close button at the bottom + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100))) + { + Close(); + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.EndScrollView(); + } + + private void Update() + { + // Handle the feedback message timer + if (copyFeedbackTimer > 0) + { + copyFeedbackTimer -= Time.deltaTime; + if (copyFeedbackTimer <= 0) + { + pathCopied = false; + jsonCopied = false; + Repaint(); + } + } + } + } +} diff --git a/Editor/Windows/ManualConfigEditorWindow.cs.meta b/Editor/Windows/ManualConfigEditorWindow.cs.meta new file mode 100644 index 0000000..b5797cc --- /dev/null +++ b/Editor/Windows/ManualConfigEditorWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 36798bd7b867b8e43ac86885e94f928f \ No newline at end of file diff --git a/Editor/Windows/UnityMCPEditorWindow.cs b/Editor/Windows/UnityMCPEditorWindow.cs new file mode 100644 index 0000000..999761a --- /dev/null +++ b/Editor/Windows/UnityMCPEditorWindow.cs @@ -0,0 +1,663 @@ +using UnityEngine; +using UnityEditor; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System; +using Newtonsoft.Json; +using System.Net.Sockets; +using System.Threading.Tasks; +using System.Text; +using System.Collections.Generic; +using System.Linq; +using UnityMCP.Editor.Models; +using UnityMCP.Editor.Data; + +namespace UnityMCP.Editor.Windows +{ + public class UnityMCPEditorWindow : EditorWindow + { + private bool isUnityBridgeRunning = false; + private Vector2 scrollPosition; + private string claudeConfigStatus = "Not configured"; + private string cursorConfigStatus = "Not configured"; + private string pythonServerStatus = "Not Connected"; + private Color pythonServerStatusColor = Color.red; + private const int unityPort = 6400; // Hardcoded Unity port + private const int mcpPort = 6500; // Hardcoded MCP port + private const float CONNECTION_CHECK_INTERVAL = 2f; // Check every 2 seconds + private float lastCheckTime = 0f; + private McpClients mcpClients = new(); + + private List possiblePaths = new() + { + Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python", "server.py")), + Path.GetFullPath(Path.Combine(Application.dataPath, "Packages", "com.justinpbarnett.unity-mcp", "Python", "server.py")), + Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Library", "PackageCache", "com.justinpbarnett.unity-mcp@*", "Python", "server.py")), + Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Packages", "com.justinpbarnett.unity-mcp", "Python", "server.py")) + }; + + [MenuItem("Window/Unity MCP")] + public static void ShowWindow() + { + GetWindow("MCP Editor"); + } + + private void OnEnable() + { + // Check initial states + isUnityBridgeRunning = UnityMCPBridge.IsRunning; + CheckPythonServerConnection(); + foreach (McpClient mcpClient in mcpClients.clients) + { + CheckMcpConfiguration(mcpClient); + } + } + + private void Update() + { + // Check Python server connection periodically + if (Time.realtimeSinceStartup - lastCheckTime >= CONNECTION_CHECK_INTERVAL) + { + CheckPythonServerConnection(); + lastCheckTime = Time.realtimeSinceStartup; + } + } + + private async void CheckPythonServerConnection() + { + try + { + using (var client = new TcpClient()) + { + // Try to connect with a short timeout + var connectTask = client.ConnectAsync("localhost", unityPort); + if (await Task.WhenAny(connectTask, Task.Delay(1000)) == connectTask) + { + // Try to send a ping message to verify connection is alive + try + { + NetworkStream stream = client.GetStream(); + byte[] pingMessage = Encoding.UTF8.GetBytes("ping"); + await stream.WriteAsync(pingMessage, 0, pingMessage.Length); + + // Wait for response with timeout + byte[] buffer = new byte[1024]; + var readTask = stream.ReadAsync(buffer, 0, buffer.Length); + if (await Task.WhenAny(readTask, Task.Delay(1000)) == readTask) + { + int bytesRead = await readTask; + if (bytesRead <= 0) + { + // Received empty response + pythonServerStatus = "Invalid Response"; + pythonServerStatusColor = GetStatusColor(McpStatus.NoResponse); + return; + } + + // Validate the response is actually from our server + string response = Encoding.UTF8.GetString(buffer, 0, bytesRead); + if (response.Contains("pong")) + { + // Connection successful and responsive with valid response + pythonServerStatus = "Connected"; + pythonServerStatusColor = GetStatusColor(McpStatus.Connected); + } + else + { + // Received response but not the expected one + pythonServerStatus = "Invalid Server"; + pythonServerStatusColor = GetStatusColor(McpStatus.CommunicationError); + } + } + else + { + // No response received + pythonServerStatus = "No Response"; + pythonServerStatusColor = GetStatusColor(McpStatus.NoResponse); + UnityEngine.Debug.LogWarning($"Python server not responding on port {unityPort}"); + } + } + catch (Exception e) + { + // Connection established but communication failed + pythonServerStatus = "Communication Error"; + pythonServerStatusColor = GetStatusColor(McpStatus.CommunicationError); + UnityEngine.Debug.LogWarning($"Error communicating with Python server: {e.Message}"); + } + } + else + { + // Connection failed + pythonServerStatus = "Not Connected"; + pythonServerStatusColor = GetStatusColor(McpStatus.NotConfigured); + UnityEngine.Debug.LogWarning($"Python server is not running or not accessible on port {unityPort}"); + } + client.Close(); + } + } + catch (Exception e) + { + pythonServerStatus = "Connection Error"; + pythonServerStatusColor = GetStatusColor(McpStatus.Error); + UnityEngine.Debug.LogError($"Error checking Python server connection: {e.Message}"); + } + } + + private Color GetStatusColor(McpStatus status) + { + // Return appropriate color based on the status enum + return status switch + { + McpStatus.Configured => Color.green, + McpStatus.Running => Color.green, + McpStatus.Connected => Color.green, + McpStatus.IncorrectPath => Color.yellow, + McpStatus.CommunicationError => Color.yellow, + McpStatus.NoResponse => Color.yellow, + _ => Color.red // Default to red for error states or not configured + }; + } + + private void ConfigurationSection(McpClient mcpClient) + { + // Calculate if we should use half-width layout + // Minimum width for half-width layout is 400 pixels + bool useHalfWidth = position.width >= 800; + float sectionWidth = useHalfWidth ? position.width / 2 - 15 : position.width - 20; + + // Begin horizontal layout if using half-width + if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) % 2 == 0) + { + EditorGUILayout.BeginHorizontal(); + } + + // Begin section with fixed width + EditorGUILayout.BeginVertical(EditorStyles.helpBox, GUILayout.Width(sectionWidth)); + + // Header with improved styling + EditorGUILayout.Space(5); + Rect headerRect = EditorGUILayout.GetControlRect(false, 24); + GUI.Label(new Rect(headerRect.x + 8, headerRect.y + 4, headerRect.width - 16, headerRect.height), + mcpClient.name + " Configuration", EditorStyles.boldLabel); + EditorGUILayout.Space(5); + + // Status indicator with colored dot + Rect statusRect = EditorGUILayout.BeginHorizontal(GUILayout.Height(20)); + Color statusColor = GetStatusColor(mcpClient.status); + + // Draw status dot + DrawStatusDot(statusRect, statusColor); + + // Status text with some padding + EditorGUILayout.LabelField(new GUIContent(" " + mcpClient.configStatus), GUILayout.Height(20), GUILayout.MinWidth(100)); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(8); + + // Configure button with improved styling + GUIStyle buttonStyle = new(GUI.skin.button); + buttonStyle.padding = new RectOffset(15, 15, 5, 5); + buttonStyle.margin = new RectOffset(10, 10, 5, 5); + + // Create muted button style for Manual Setup + GUIStyle mutedButtonStyle = new(buttonStyle); + + if (GUILayout.Button($"Auto Configure {mcpClient.name}", buttonStyle, GUILayout.Height(28))) + { + ConfigureMcpClient(mcpClient); + } + + if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28))) + { + // Get the appropriate config path based on OS + string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? mcpClient.windowsConfigPath + : mcpClient.linuxConfigPath; + ShowManualInstructionsWindow(configPath, mcpClient); + } + EditorGUILayout.Space(5); + + EditorGUILayout.EndVertical(); + + // End horizontal layout if using half-width and at the end of a row + if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) % 2 == 1) + { + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(5); + } + // Add space and end the horizontal layout if last item is odd + else if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) == mcpClients.clients.Count - 1) + { + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(5); + } + } + + private void DrawStatusDot(Rect statusRect, Color statusColor) + { + Rect dotRect = new(statusRect.x + 6, statusRect.y + 4, 12, 12); + Vector3 center = new(dotRect.x + dotRect.width / 2, dotRect.y + dotRect.height / 2, 0); + float radius = dotRect.width / 2; + + // Draw the main dot + Handles.color = statusColor; + Handles.DrawSolidDisc(center, Vector3.forward, radius); + + // Draw the border + Color borderColor = new(statusColor.r * 0.7f, statusColor.g * 0.7f, statusColor.b * 0.7f); + Handles.color = borderColor; + Handles.DrawWireDisc(center, Vector3.forward, radius); + } + + private void OnGUI() + { + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + + EditorGUILayout.Space(10); + // Title with improved styling + Rect titleRect = EditorGUILayout.GetControlRect(false, 30); + EditorGUI.DrawRect(new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height), new Color(0.2f, 0.2f, 0.2f, 0.1f)); + GUI.Label(new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height), + "MCP Editor", EditorStyles.boldLabel); + EditorGUILayout.Space(10); + + // Python Server Status Section + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("Python Server Status", EditorStyles.boldLabel); + + // Status indicator with colored dot + var statusRect = EditorGUILayout.BeginHorizontal(GUILayout.Height(20)); + DrawStatusDot(statusRect, pythonServerStatusColor); + EditorGUILayout.LabelField(" " + pythonServerStatus); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.LabelField($"Unity Port: {unityPort}"); + EditorGUILayout.LabelField($"MCP Port: {mcpPort}"); + EditorGUILayout.HelpBox("Your MCP client (e.g. Cursor or Claude Desktop) will start the server automatically when you start it.", MessageType.Info); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + // Unity Bridge Section + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("Unity MCP Bridge", EditorStyles.boldLabel); + EditorGUILayout.LabelField($"Status: {(isUnityBridgeRunning ? "Running" : "Stopped")}"); + EditorGUILayout.LabelField($"Port: {unityPort}"); + + if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge")) + { + ToggleUnityBridge(); + } + EditorGUILayout.EndVertical(); + + foreach (McpClient mcpClient in mcpClients.clients) + { + EditorGUILayout.Space(10); + ConfigurationSection(mcpClient); + } + + EditorGUILayout.EndScrollView(); + } + + private void ToggleUnityBridge() + { + if (isUnityBridgeRunning) + { + UnityMCPBridge.Stop(); + } + else + { + UnityMCPBridge.Start(); + } + + isUnityBridgeRunning = !isUnityBridgeRunning; + } + + + private string GetPythonDirectory(List possiblePaths) + { + foreach (var path in possiblePaths) + { + // Skip wildcard paths for now + if (path.Contains("*")) continue; + + if (File.Exists(path)) + { + return Path.GetDirectoryName(path); + } + } + + foreach (var path in possiblePaths) + { + if (!path.Contains("*")) continue; + + string directoryPath = Path.GetDirectoryName(path); + string searchPattern = Path.GetFileName(Path.GetDirectoryName(path)); + string parentDir = Path.GetDirectoryName(directoryPath); + + if (Directory.Exists(parentDir)) + { + var matchingDirs = Directory.GetDirectories(parentDir, searchPattern); + + foreach (var dir in matchingDirs) + { + string candidatePath = Path.Combine(dir, "Python", "server.py"); + + if (File.Exists(candidatePath)) + { + return Path.GetDirectoryName(candidatePath); + } + } + } + } + + return null; + } + + private string WriteToConfig(string pythonDir, string configPath) + { + // Create configuration object for unityMCP + var unityMCPConfig = new MCPConfigServer + { + command = "uv", + args = new[] + { + "--directory", + pythonDir, + "run", + "server.py" + } + }; + + var jsonSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented + }; + + // Read existing config if it exists + string existingJson = "{}"; + if (File.Exists(configPath)) + { + try + { + existingJson = File.ReadAllText(configPath); + } + catch (Exception e) + { + UnityEngine.Debug.LogWarning($"Error reading existing config: {e.Message}."); + } + } + + // Parse the existing JSON while preserving all properties + dynamic existingConfig = JsonConvert.DeserializeObject(existingJson); + if (existingConfig == null) + { + existingConfig = new Newtonsoft.Json.Linq.JObject(); + } + + // Ensure mcpServers object exists + if (existingConfig.mcpServers == null) + { + existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject(); + } + + // Add/update unityMCP while preserving other servers + existingConfig.mcpServers.unityMCP = JsonConvert.DeserializeObject( + JsonConvert.SerializeObject(unityMCPConfig) + ); + + // Write the merged configuration back to file + string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings); + File.WriteAllText(configPath, mergedJson); + + return "Configured successfully"; + } + + private void ShowManualConfigurationInstructions(string configPath, McpClient mcpClient) + { + mcpClient.SetStatus(McpStatus.Error, "Manual configuration required"); + + ShowManualInstructionsWindow(configPath, mcpClient); + } + + // New method to show manual instructions without changing status + private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient) + { + // Get the Python directory path using Package Manager API + string pythonDir = FindPackagePythonDirectory(); + + // Create the manual configuration message + var jsonConfig = new MCPConfig + { + mcpServers = new MCPConfigServers + { + unityMCP = new MCPConfigServer + { + command = "uv", + args = new[] + { + "--directory", + pythonDir, + "run", + "server.py" + } + } + } + }; + + var jsonSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented + }; + string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); + + ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); + } + + private string FindPackagePythonDirectory() + { + string pythonDir = "/path/to/your/unity-mcp/Python"; + + try + { + // Try to find the package using Package Manager API + var request = UnityEditor.PackageManager.Client.List(); + while (!request.IsCompleted) { } // Wait for the request to complete + + if (request.Status == UnityEditor.PackageManager.StatusCode.Success) + { + foreach (var package in request.Result) + { + if (package.name == "com.justinpbarnett.unity-mcp") + { + string packagePath = package.resolvedPath; + string potentialPythonDir = Path.Combine(packagePath, "Python"); + + if (Directory.Exists(potentialPythonDir) && + File.Exists(Path.Combine(potentialPythonDir, "server.py"))) + { + return potentialPythonDir; + } + } + } + } + else if (request.Error != null) + { + UnityEngine.Debug.LogError("Failed to list packages: " + request.Error.message); + } + + // If not found via Package Manager, try manual approaches + // First check for local installation + string[] possibleDirs = { + Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python")) + }; + + foreach (var dir in possibleDirs) + { + if (Directory.Exists(dir) && File.Exists(Path.Combine(dir, "server.py"))) + { + return dir; + } + } + + // If still not found, return the placeholder path + UnityEngine.Debug.LogWarning("Could not find Python directory, using placeholder path"); + } + catch (Exception e) + { + UnityEngine.Debug.LogError($"Error finding package path: {e.Message}"); + } + + return pythonDir; + } + + private string ConfigureMcpClient(McpClient mcpClient) + { + try + { + // Determine the config file path based on OS + string configPath; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + configPath = mcpClient.windowsConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + configPath = mcpClient.linuxConfigPath; + } + else + { + return "Unsupported OS"; + } + + // Create directory if it doesn't exist + Directory.CreateDirectory(Path.GetDirectoryName(configPath)); + + // Find the server.py file location + string pythonDir = GetPythonDirectory(possiblePaths); + + if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py"))) + { + ShowManualInstructionsWindow(configPath, mcpClient); + return "Manual Configuration Required"; + } + + string result = WriteToConfig(pythonDir, configPath); + + // Update the client status after successful configuration + if (result == "Configured successfully") + { + mcpClient.SetStatus(McpStatus.Configured); + } + + return result; + } + catch (Exception e) + { + // Determine the config file path based on OS for error message + string configPath = ""; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + configPath = mcpClient.windowsConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + configPath = mcpClient.linuxConfigPath; + } + + ShowManualInstructionsWindow(configPath, mcpClient); + UnityEngine.Debug.LogError($"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}"); + return $"Failed to configure {mcpClient.name}"; + } + } + + + private void ShowCursorManualConfigurationInstructions(string configPath, McpClient mcpClient) + { + mcpClient.SetStatus(McpStatus.Error, "Manual configuration required"); + + // Get the Python directory path using Package Manager API + string pythonDir = FindPackagePythonDirectory(); + + // Create the manual configuration message + var jsonConfig = new MCPConfig + { + mcpServers = new MCPConfigServers + { + unityMCP = new MCPConfigServer + { + command = "uv", + args = new[] + { + "--directory", + pythonDir, + "run", + "server.py" + } + } + } + }; + + var jsonSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented + }; + string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); + + ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); + } + + private void CheckMcpConfiguration(McpClient mcpClient) + { + try + { + string configPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + configPath = mcpClient.windowsConfigPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + configPath = mcpClient.linuxConfigPath; + } + else + { + mcpClient.SetStatus(McpStatus.UnsupportedOS); + return; + } + + if (!File.Exists(configPath)) + { + mcpClient.SetStatus(McpStatus.NotConfigured); + return; + } + + string configJson = File.ReadAllText(configPath); + var config = JsonConvert.DeserializeObject(configJson); + + if (config?.mcpServers?.unityMCP != null) + { + string pythonDir = GetPythonDirectory(possiblePaths); + if (pythonDir != null && Array.Exists(config.mcpServers.unityMCP.args, arg => arg.Contains(pythonDir, StringComparison.Ordinal))) + { + mcpClient.SetStatus(McpStatus.Configured); + } + else + { + mcpClient.SetStatus(McpStatus.IncorrectPath); + } + } + else + { + mcpClient.SetStatus(McpStatus.MissingConfig); + } + } + catch (Exception e) + { + mcpClient.SetStatus(McpStatus.Error, e.Message); + } + } + } +} \ No newline at end of file diff --git a/Editor/MCPEditorWindow.cs.meta b/Editor/Windows/UnityMCPEditorWindow.cs.meta similarity index 100% rename from Editor/MCPEditorWindow.cs.meta rename to Editor/Windows/UnityMCPEditorWindow.cs.meta diff --git a/Python/unity_mcp.egg-info.meta b/Python/unity_mcp.egg-info.meta new file mode 100644 index 0000000..21940ff --- /dev/null +++ b/Python/unity_mcp.egg-info.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 714de9c710feb1a42878a16b7a4e7a6f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index b997a98..68251f8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Unity package that enables seamless communication between Unity and Large Language Models (LLMs) like Claude Desktop via the **Model Context Protocol (MCP)**. This server acts as a bridge, allowing Unity to send commands to and receive responses from MCP-compliant tools, empowering developers to automate workflows, manipulate assets, and control the Unity Editor programmatically. -Welcome to the initial release of this open-source project! Whether you're looking to integrate LLMs into your Unity workflow or contribute to an exciting new tool, we're thrilled to have you here. +Welcome to the initial release of this open-source project! Whether you're looking to integrate LLMs into your Unity workflow or contribute to an exciting new tool, I appreciate you taking the time to check out my project. ## Overview @@ -20,7 +20,7 @@ This project is perfect for developers who want to leverage LLMs to enhance thei ### Prerequisites -- Unity 2020.3 LTS or newer +- Unity 2020.3 LTS or newer (⚠️ only works in URP projects currently) - Python 3.7 or newer - uv package manager @@ -70,16 +70,17 @@ Otherwise, installation instructions are on their website: [Install uv](https:// uv pip install -e . ``` -### Claude Desktop Integration +### MCP Client Integration 1. Open the Unity MCP window (`Window > Unity MCP`) -2. Click the "Configure Claude" button -3. Follow the on-screen instructions to set up the integration +2. Click the "Auto Configure" button for your desired MCP client +3. Status indicator should show green and a "Configured" message -Alternatively, manually configure Claude Desktop: +Alternatively, manually configure your MCP client: -1. Go to Claude > Settings > Developer > Edit Config -2. Edit `claude_desktop_config.json` to include: +1. Open the Unity MCP window (`Window > Unity MCP`) +2. Click the "Manually Configure" button for your desired MCP client +3. Copy the JSON code below to the config file ```json { @@ -99,100 +100,15 @@ Alternatively, manually configure Claude Desktop: Replace `/path/to/your/unity-mcp/Python` with the actual path to the Unity MCP Python directory. -### Cursor Integration - -1. Open the Unity MCP window (`Window > Unity MCP`) -2. Click the "Configure Cursor" button -3. Follow the on-screen instructions to set up the integration - -Alternatively, go to Cursor Settings > MCP and paste this as a command: - -```bash -uv --directory "/path/to/your/unity-mcp/Python" run server.py -``` - -Replace `/path/to/your/unity-mcp/Python` with the actual path to the Unity MCP Python directory. - **⚠️ Only run one instance of the MCP server (either on Cursor or Claude Desktop), not both** 4. **Start Claude Desktop or Cursor** - Launch your preferred tool - - The Unity MCP Server will automatically connect - -## Configuration - -To connect the MCP Server to tools like Claude Desktop or Cursor: - -1. **Open the Unity MCP Window** - In Unity, go to `Window > Unity MCP` to open the editor window. - -2. **Configure Your Tools** - - - In the Unity MCP window, you'll see buttons to configure **Claude Desktop** or **Cursor**. - - Click the appropriate button and follow the on-screen instructions to set up the integration. - -3. **Verify Server Status** - - Check the server status in the Unity MCP window. It will display: - - **Unity Bridge**: Should show "Running" when active. - - **Python Server**: Should show "Connected" (green) when successfully linked. - -## Manual Configuration for MCP Clients - -If you prefer to manually configure your MCP client (like Claude Desktop or Cursor), you can create the configuration file yourself: - -1. **Locate the Configuration Directory** - - - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` - - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` - -2. **Create the Configuration File** - Create a JSON file with the following structure: - - ```json - { - "mcpServers": { - "unityMCP": { - "command": "uv", - "args": [ - "--directory", - "/path/to/your/unity-mcp/Python", - "run", - "server.py" - ] - } - } - } - ``` - -3. **Find the Correct Python Path** - - - If installed as a package: Look in `Library/PackageCache/com.justinpbarnett.unity-mcp/Python` - - If installed locally: Look in `Assets/unity-mcp/Python` - -4. **Verify Configuration** - - Ensure the Python path points to the correct directory containing `server.py` - - Make sure the `uv` command is available in your system PATH - - Test the connection using the Unity MCP window + - The Unity MCP Server will automatically start and connect ## Usage -Once configured, you can use the MCP Server to interact with LLMs directly from Unity or Python. Here are a couple of examples: - -### Creating a Cube in the Scene - -```python -# Send a command to create a cube at position (0, 0, 0) -create_primitive(primitive_type="Cube", position=[0, 0, 0]) -``` - -### Changing a Material's Color - -```python -# Set a material's color to red (RGBA: 1, 0, 0, 1) -set_material_color(material_name="MyMaterial", color=[1, 0, 0, 1]) -``` - -Explore more commands in the [HOW_TO_ADD_A_TOOL.md](HOW_TO_ADD_A_TOOL.md) file for detailed examples and instructions on extending functionality. +Once configured, you can use the MCP Client to interact with Unity directly through their chat interface. ## Features @@ -205,7 +121,7 @@ Explore more commands in the [HOW_TO_ADD_A_TOOL.md](HOW_TO_ADD_A_TOOL.md) file f ## Contributing -We'd love your help to make the Unity MCP Server even better! Here's how to contribute: +I'd love your help to make the Unity MCP Server even better! Here's how to contribute: 1. **Fork the Repository** Fork [github.com/justinpbarnett/unity-mcp](https://github.com/justinpbarnett/unity-mcp) to your GitHub account. @@ -216,8 +132,14 @@ We'd love your help to make the Unity MCP Server even better! Here's how to cont git checkout -b feature/your-feature-name ``` + OR + + ```bash + git checkout -b bugfix/your-bugfix-name + ``` + 3. **Make Changes** - Implement your feature or fix, following the project's coding standards (see [HOW_TO_ADD_A_TOOL.md](HOW_TO_ADD_A_TOOL.md) for guidance). + Implement your feature or fix. 4. **Commit and Push** Use clear, descriptive commit messages: @@ -230,8 +152,6 @@ We'd love your help to make the Unity MCP Server even better! Here's how to cont 5. **Submit a Pull Request** Open a pull request to the `master` branch. Include a description of your changes and any relevant details. -For more details, check out [CONTRIBUTING.md](CONTRIBUTING.md) (to be created). - ## License This project is licensed under the **MIT License**. Feel free to use, modify, and distribute it as you see fit. See the full license [here](https://github.com/justinpbarnett/unity-mcp/blob/master/LICENSE). @@ -259,11 +179,9 @@ For additional help, check the [issue tracker](https://github.com/justinpbarnett Have questions or want to chat about the project? Reach out! - **X**: [@justinpbarnett](https://x.com/justinpbarnett) -- **GitHub**: [justinpbarnett](https://github.com/justinpbarnett) -- **Discord**: Join our community (link coming soon!). ## Acknowledgments -A huge thanks to everyone who's supported this project's initial release. Special shoutout to Unity Technologies for inspiring tools that push creative boundaries, and to the open-source community for making projects like this possible. +A huge thanks to everyone who's supported this project's initial release. Special shoutout to Unity Technologies for having an excellent Editor API. Happy coding, and enjoy integrating LLMs with Unity!