From 0619ba7b0593ab4f2f75bd775902d836ea2b429d Mon Sep 17 00:00:00 2001 From: Kirill Kuvaldin Date: Wed, 19 Mar 2025 14:12:58 -0700 Subject: [PATCH 1/3] Add GetAvailableCommands functionality to EditorControlHandler and Python tool - Implemented GetAvailableCommands method in EditorControlHandler.cs to retrieve available editor commands. - Added corresponding get_available_commands tool in editor_tools.py to interface with the new command. - Improved code formatting and readability throughout both files. --- Editor/Commands/EditorControlHandler.cs | 381 ++++++++++++++++++------ Python/tools/editor_tools.py | 28 +- 2 files changed, 318 insertions(+), 91 deletions(-) diff --git a/Editor/Commands/EditorControlHandler.cs b/Editor/Commands/EditorControlHandler.cs index 7525711..256a720 100644 --- a/Editor/Commands/EditorControlHandler.cs +++ b/Editor/Commands/EditorControlHandler.cs @@ -5,7 +5,7 @@ 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.Linq; // Add LINQ namespace for Select extension method using System.Globalization; /// @@ -39,6 +39,8 @@ public static class EditorControlHandler return HandleExecuteCommand(commandParams); case "READ_CONSOLE": return ReadConsole(commandParams); + case "GET_AVAILABLE_COMMANDS": + return GetAvailableCommands(); default: return new { error = $"Unknown editor control command: {command}" }; } @@ -105,11 +107,7 @@ public static class EditorControlHandler buildPlayerOptions.locationPathName = buildPath; BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions); - return new - { - message = "Build completed successfully", - summary = report.summary - }; + return new { message = "Build completed successfully", summary = report.summary }; } catch (System.Exception e) { @@ -147,10 +145,14 @@ public static class EditorControlHandler // 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"]; + 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 @@ -160,20 +162,50 @@ public static class EditorControlHandler 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() }; + 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); + 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() }; + 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); + 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); @@ -183,12 +215,17 @@ public static class EditorControlHandler 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"); + 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; + string[] searchWords = !string.IsNullOrWhiteSpace(searchTerm) + ? searchTerm + .ToLower() + .Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) + : null; // Process each log entry for (int i = 0; i < count; i++) @@ -201,30 +238,41 @@ public static class EditorControlHandler { getEntryMethod.Invoke(null, new object[] { i, logEntryInstance }); } - else if (methodParams.Length >= 1 && methodParams[0].ParameterType == typeof(int)) + 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++) { - parameters[p] = methodParams[p].ParameterType.IsValueType ? - Activator.CreateInstance(methodParams[p].ParameterType) : null; + parameters[p] = methodParams[p].ParameterType.IsValueType + ? Activator.CreateInstance(methodParams[p].ParameterType) + : null; } getEntryMethod.Invoke(null, parameters); } - else continue; + else + continue; // Extract log data - int logType = modeProperty != null ? - Convert.ToInt32(modeProperty.GetValue(logEntryInstance) ?? 0) : 0; + int logType = + modeProperty != null + ? Convert.ToInt32(modeProperty.GetValue(logEntryInstance) ?? 0) + : 0; - string message = messageProperty != null ? - (messageProperty.GetValue(logEntryInstance)?.ToString() ?? "") : ""; + 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); + var msgField = logEntryType.GetField( + "message", + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance + ); if (msgField != null) { object msgValue = msgField.GetValue(logEntryInstance); @@ -235,50 +283,93 @@ public static class EditorControlHandler if (string.IsNullOrEmpty(message)) { // Access ConsoleWindow and its data - Type consoleWindowType = Type.GetType("UnityEditor.ConsoleWindow,UnityEditor"); + Type consoleWindowType = Type.GetType( + "UnityEditor.ConsoleWindow,UnityEditor" + ); if (consoleWindowType != null) { try { // 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); + 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) { - object consoleWindow = getWindowMethod.Invoke(null, - getWindowMethod.GetParameters().Length > 0 ? new object[] { false } : null); + object consoleWindow = getWindowMethod.Invoke( + null, + getWindowMethod.GetParameters().Length > 0 + ? new object[] { false } + : null + ); if (consoleWindow != null) { // Try to find log entries collection - foreach (var prop in consoleWindowType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + foreach ( + var prop in consoleWindowType.GetProperties( + BindingFlags.Instance + | BindingFlags.Public + | BindingFlags.NonPublic + ) + ) { - if (prop.PropertyType.IsArray || - (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>))) + if ( + prop.PropertyType.IsArray + || ( + prop.PropertyType.IsGenericType + && prop.PropertyType.GetGenericTypeDefinition() + == typeof(List<>) + ) + ) { try { var logItems = prop.GetValue(consoleWindow); if (logItems != null) { - if (logItems.GetType().IsArray && i < ((Array)logItems).Length) + if ( + logItems.GetType().IsArray + && i < ((Array)logItems).Length + ) { - var entry = ((Array)logItems).GetValue(i); + var entry = ( + (Array)logItems + ).GetValue(i); if (entry != null) { var entryType = entry.GetType(); - var entryMessageProp = entryType.GetProperty("message") ?? - entryType.GetProperty("Message"); + var entryMessageProp = + entryType.GetProperty( + "message" + ) + ?? entryType.GetProperty( + "Message" + ); if (entryMessageProp != null) { - object value = entryMessageProp.GetValue(entry); + object value = + entryMessageProp.GetValue( + entry + ); if (value != null) { - message = value.ToString(); + message = + value.ToString(); break; } } @@ -314,23 +405,42 @@ public static class EditorControlHandler if (Application.platform == RuntimePlatform.WindowsEditor) { logPath = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "Unity", "Editor", "Editor.log"); + 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"); + 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"); + Environment.GetFolderPath( + Environment.SpecialFolder.Personal + ), + ".config", + "unity3d", + "logs", + "Editor.log" + ); } - if (!string.IsNullOrEmpty(logPath) && System.IO.File.Exists(logPath)) + if ( + !string.IsNullOrEmpty(logPath) && System.IO.File.Exists(logPath) + ) { // Read last few lines from the log file var logLines = ReadLastLines(logPath, 100); @@ -351,14 +461,17 @@ public static class EditorControlHandler string stackTrace = ""; if (getStackTraceMethod != null) { - stackTrace = getStackTraceMethod.Invoke(null, new object[] { i })?.ToString() ?? ""; + 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; + bool typeMatch = + (logType == 0 && showLogs) + || (logType == 1 && showWarnings) + || (logType == 2 && showErrors); + if (!typeMatch) + continue; // Filter by search term bool searchMatch = true; @@ -376,16 +489,24 @@ public static class EditorControlHandler } } } - if (!searchMatch) continue; + if (!searchMatch) + continue; // Add matching entry to results - string typeStr = logType == 0 ? "Log" : logType == 1 ? "Warning" : "Error"; - entries.Add(new - { - type = typeStr, - message = message, - stackTrace = stackTrace - }); + string typeStr = + logType == 0 + ? "Log" + : logType == 1 + ? "Warning" + : "Error"; + entries.Add( + new + { + type = typeStr, + message = message, + stackTrace = stackTrace + } + ); } catch (Exception) { @@ -420,8 +541,10 @@ public static class EditorControlHandler { foreach (var methodName in methodNames) { - var method = type.GetMethod(methodName, - BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + var method = type.GetMethod( + methodName, + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic + ); if (method != null) return method; } @@ -433,13 +556,27 @@ public static class EditorControlHandler 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 + 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; } @@ -465,8 +602,12 @@ public static class EditorControlHandler var result = new Dictionary(); // Get all public and non-public properties - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static | BindingFlags.Instance); + var properties = type.GetProperties( + BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.Static + | BindingFlags.Instance + ); var propList = new List(); foreach (var prop in properties) { @@ -475,8 +616,12 @@ public static class EditorControlHandler result["Properties"] = propList; // Get all public and non-public fields - var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static | BindingFlags.Instance); + var fields = type.GetFields( + BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.Static + | BindingFlags.Instance + ); var fieldList = new List(); foreach (var field in fields) { @@ -485,15 +630,21 @@ public static class EditorControlHandler result["Fields"] = fieldList; // Get all public and non-public methods - var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static | BindingFlags.Instance); + 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}")); + var parameters = string.Join( + ", ", + method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}") + ); methodList.Add($"{method.ReturnType.Name} {method.Name}({parameters})"); } } @@ -507,13 +658,16 @@ public static class EditorControlHandler /// private static Dictionary GetObjectValues(object obj) { - if (obj == null) return new Dictionary(); + 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); + var properties = type.GetProperties( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance + ); foreach (var prop in properties) { try @@ -528,7 +682,9 @@ public static class EditorControlHandler } // Get all field values - var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + var fields = type.GetFields( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance + ); foreach (var field in fields) { try @@ -552,7 +708,14 @@ public static class EditorControlHandler { 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 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; @@ -589,4 +752,42 @@ public static class EditorControlHandler return result; } -} \ No newline at end of file + + /// + /// Gets a list of available editor commands that can be executed + /// the method should have a MenuItem attribute + /// + /// Object containing list of available command paths + [MenuItem("Window/Get Available Commands")] + private static object GetAvailableCommands() + { + var commands = new List(); + Assembly assembly = Assembly.GetExecutingAssembly(); + foreach (Type type in assembly.GetTypes()) + { + MethodInfo[] methods = type.GetMethods( + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic + ); + + foreach (MethodInfo method in methods) + { + // Look for the MenuItem attribute + object[] attributes = method.GetCustomAttributes( + typeof(UnityEditor.MenuItem), + false + ); + if (attributes.Length > 0) + { + UnityEditor.MenuItem menuItem = attributes[0] as UnityEditor.MenuItem; + commands.Add(menuItem.menuItem); + } + } + } + Debug.Log($"commands.Count: {commands.Count}"); + foreach (var command in commands) + { + Debug.Log($"Command: {command}"); + } + return new { commands = commands }; + } +} diff --git a/Python/tools/editor_tools.py b/Python/tools/editor_tools.py index 38ab274..d3a634a 100644 --- a/Python/tools/editor_tools.py +++ b/Python/tools/editor_tools.py @@ -266,4 +266,30 @@ def register_editor_tools(mcp: FastMCP): "type": "Error", "message": f"Error reading console: {str(e)}", "stackTrace": "" - }] \ No newline at end of file + }] + + @mcp.tool() + def get_available_commands(ctx: Context) -> List[str]: + """Get a list of all available editor commands that can be executed. + + This tool provides direct access to the list of commands that can be executed + in the Unity Editor through the MCP system. + + Returns: + List[str]: List of available command paths + """ + try: + unity = get_unity_connection() + + # Send request for available commands + response = unity.send_command("EDITOR_CONTROL", { + "command": "GET_AVAILABLE_COMMANDS" + }) + + # Extract commands list + commands = response.get("commands", []) + + # Return the commands list + return commands + except Exception as e: + return [f"Error fetching commands: {str(e)}"] \ No newline at end of file From 8102460c7c6f06ad67d8776e0233a7c418fde601 Mon Sep 17 00:00:00 2001 From: Kirill Kuvaldin Date: Thu, 20 Mar 2025 14:20:47 -0700 Subject: [PATCH 2/3] Add ExecuteContextMenuItem functionality to ObjectCommandHandler and Python tool - Implemented ExecuteContextMenuItem method in ObjectCommandHandler.cs to execute context menu methods on game object components. - Added corresponding execute_context_menu_item tool in object_tools.py to facilitate the execution of context menu items from Python. - Enhanced error handling and logging for better debugging and user feedback. --- Editor/Commands/ObjectCommandHandler.cs | 103 ++++++++++++++++++++++++ Editor/UnityMCPBridge.cs | 1 + Python/tools/object_tools.py | 55 ++++++++++++- 3 files changed, 158 insertions(+), 1 deletion(-) diff --git a/Editor/Commands/ObjectCommandHandler.cs b/Editor/Commands/ObjectCommandHandler.cs index eecfcf2..6176b28 100644 --- a/Editor/Commands/ObjectCommandHandler.cs +++ b/Editor/Commands/ObjectCommandHandler.cs @@ -7,6 +7,7 @@ using UnityEditor; using UnityEngine.SceneManagement; using UnityEditor.SceneManagement; using UnityMCP.Editor.Helpers; +using System.Reflection; namespace UnityMCP.Editor.Commands { @@ -398,5 +399,107 @@ namespace UnityMCP.Editor.Commands light.shadows = LightShadows.Soft; return obj; } + + /// + /// Executes a context menu method on a component of a game object + /// + public static object ExecuteContextMenuItem(JObject @params) + { + string objectName = (string)@params["object_name"] ?? throw new Exception("Parameter 'object_name' is required."); + string componentName = (string)@params["component"] ?? throw new Exception("Parameter 'component' is required."); + string contextMenuItemName = (string)@params["context_menu_item"] ?? throw new Exception("Parameter 'context_menu_item' is required."); + + // Find the game object + var obj = GameObject.Find(objectName) ?? throw new Exception($"Object '{objectName}' not found."); + + // Find the component type + Type componentType = FindTypeInLoadedAssemblies(componentName) ?? + throw new Exception($"Component type '{componentName}' not found."); + + // Get the component from the game object + var component = obj.GetComponent(componentType) ?? + throw new Exception($"Component '{componentName}' not found on object '{objectName}'."); + + // Find methods with ContextMenu attribute matching the context menu item name + var methods = componentType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(m => m.GetCustomAttributes(typeof(ContextMenuItemAttribute), true).Any() || + m.GetCustomAttributes(typeof(ContextMenu), true) + .Cast() + .Any(attr => attr.menuItem == contextMenuItemName)) + .ToList(); + + // If no methods with ContextMenuItemAttribute are found, look for methods with name matching the context menu item + if (methods.Count == 0) + { + methods = componentType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(m => m.Name == contextMenuItemName) + .ToList(); + } + + if (methods.Count == 0) + throw new Exception($"No context menu method '{contextMenuItemName}' found on component '{componentName}'."); + + // If multiple methods match, use the first one and log a warning + if (methods.Count > 1) + { + Debug.LogWarning($"Found multiple methods for context menu item '{contextMenuItemName}' on component '{componentName}'. Using the first one."); + } + + var method = methods[0]; + + // Execute the method + try + { + method.Invoke(component, null); + return new + { + success = true, + message = $"Successfully executed context menu item '{contextMenuItemName}' on component '{componentName}' of object '{objectName}'." + }; + } + catch (Exception ex) + { + throw new Exception($"Error executing context menu item: {ex.Message}"); + } + } + + // Add this helper method to find types across all loaded assemblies + private static Type FindTypeInLoadedAssemblies(string typeName) + { + // First try standard approach + Type type = Type.GetType(typeName); + if (type != null) + return type; + + type = Type.GetType($"UnityEngine.{typeName}"); + if (type != null) + return type; + + // Then search all loaded assemblies + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + // Try with the simple name + type = assembly.GetType(typeName); + if (type != null) + return type; + + // Try with the fully qualified name (assembly.GetTypes() can be expensive, so we do this last) + var types = assembly.GetTypes().Where(t => t.Name == typeName).ToArray(); + + if (types.Length > 0) + { + // If we found multiple types with the same name, log a warning + if (types.Length > 1) + { + Debug.LogWarning( + $"Found multiple types named '{typeName}'. Using the first one: {types[0].FullName}" + ); + } + return types[0]; + } + } + + return null; + } } } \ No newline at end of file diff --git a/Editor/UnityMCPBridge.cs b/Editor/UnityMCPBridge.cs index 3d4c600..4b36e81 100644 --- a/Editor/UnityMCPBridge.cs +++ b/Editor/UnityMCPBridge.cs @@ -286,6 +286,7 @@ namespace UnityMCP.Editor "CREATE_OBJECT" => ObjectCommandHandler.CreateObject(command.@params), "MODIFY_OBJECT" => ObjectCommandHandler.ModifyObject(command.@params), "DELETE_OBJECT" => ObjectCommandHandler.DeleteObject(command.@params), + "EXECUTE_CONTEXT_MENU_ITEM" => ObjectCommandHandler.ExecuteContextMenuItem(command.@params), "GET_OBJECT_PROPERTIES" => ObjectCommandHandler.GetObjectProperties(command.@params), "GET_COMPONENT_PROPERTIES" => ObjectCommandHandler.GetComponentProperties(command.@params), "FIND_OBJECTS_BY_NAME" => ObjectCommandHandler.FindObjectsByName(command.@params), diff --git a/Python/tools/object_tools.py b/Python/tools/object_tools.py index 293ebef..99534f2 100644 --- a/Python/tools/object_tools.py +++ b/Python/tools/object_tools.py @@ -194,4 +194,57 @@ def register_object_tools(mcp: FastMCP): }) return response.get("assets", []) except Exception as e: - return [{"error": f"Failed to get asset list: {str(e)}"}] \ No newline at end of file + return [{"error": f"Failed to get asset list: {str(e)}"}] + + @mcp.tool() + def execute_context_menu_item( + ctx: Context, + object_name: str, + component: str, + context_menu_item: str + ) -> Dict[str, Any]: + """Execute a specific [ContextMenu] method on a component of a given game object. + + Args: + ctx: The MCP context + object_name: Name of the game object to call + component: Name of the component type + context_menu_item: Name of the context menu item to execute + + Returns: + Dict containing the result of the operation + """ + try: + unity = get_unity_connection() + + # Check if the object exists + found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", { + "name": object_name + }).get("objects", []) + + if not found_objects: + return {"error": f"Object with name '{object_name}' not found in the scene."} + + # Check if the component exists on the object + object_props = unity.send_command("GET_OBJECT_PROPERTIES", { + "name": object_name + }) + + if "error" in object_props: + return {"error": f"Failed to get object properties: {object_props['error']}"} + + components = object_props.get("components", []) + component_exists = any(comp.get("type") == component for comp in components) + + if not component_exists: + return {"error": f"Component '{component}' is not attached to object '{object_name}'."} + + # Now execute the context menu item + response = unity.send_command("EXECUTE_CONTEXT_MENU_ITEM", { + "object_name": object_name, + "component": component, + "context_menu_item": context_menu_item + }) + return response + except Exception as e: + return {"error": f"Failed to execute context menu item: {str(e)}"} \ No newline at end of file From 3352ba3033f4e7cc62b2e538b4762f8f2b8865c9 Mon Sep 17 00:00:00 2001 From: Justin Barnett Date: Fri, 21 Mar 2025 06:30:41 -0400 Subject: [PATCH 3/3] increased command list --- Editor/Commands/EditorControlHandler.cs | 366 ++++++++++++++++++++++-- 1 file changed, 350 insertions(+), 16 deletions(-) diff --git a/Editor/Commands/EditorControlHandler.cs b/Editor/Commands/EditorControlHandler.cs index f3162eb..be48fe2 100644 --- a/Editor/Commands/EditorControlHandler.cs +++ b/Editor/Commands/EditorControlHandler.cs @@ -584,33 +584,367 @@ namespace UnityMCP.Editor.Commands return result; } - /// - /// Gets a list of available editor commands that can be executed - /// the method should have a MenuItem attribute + /// Gets a comprehensive list of available Unity commands, including editor menu items, + /// internal commands, utility methods, and other actionable operations that can be executed. /// - /// Object containing list of available command paths + /// Object containing categorized lists of available command paths private static object GetAvailableCommands() { - var commands = new List(); - Assembly assembly = Assembly.GetExecutingAssembly(); - foreach (Type type in assembly.GetTypes()) + var menuCommands = new HashSet(); + var utilityCommands = new HashSet(); + var assetCommands = new HashSet(); + var sceneCommands = new HashSet(); + var gameObjectCommands = new HashSet(); + var prefabCommands = new HashSet(); + var shortcutCommands = new HashSet(); + var otherCommands = new HashSet(); + + // Add a simple command that we know will work for testing + menuCommands.Add("Window/Unity MCP"); + + Debug.Log("Starting command collection..."); + + try { - MethodInfo[] methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); - - foreach (MethodInfo method in methods) + // Add all EditorApplication static methods - these are guaranteed to work + Debug.Log("Adding EditorApplication methods..."); + foreach (MethodInfo method in typeof(EditorApplication).GetMethods(BindingFlags.Public | BindingFlags.Static)) { - // Look for the MenuItem attribute - object[] attributes = method.GetCustomAttributes(typeof(UnityEditor.MenuItem), false); - if (attributes.Length > 0) + utilityCommands.Add($"EditorApplication.{method.Name}"); + } + Debug.Log($"Added {utilityCommands.Count} EditorApplication methods"); + + // Add built-in menu commands directly - these are common ones that should always be available + Debug.Log("Adding built-in menu commands..."); + string[] builtInMenus = new[] { + "File/New Scene", + "File/Open Scene", + "File/Save", + "File/Save As...", + "Edit/Undo", + "Edit/Redo", + "Edit/Cut", + "Edit/Copy", + "Edit/Paste", + "Edit/Duplicate", + "Edit/Delete", + "GameObject/Create Empty", + "GameObject/3D Object/Cube", + "GameObject/3D Object/Sphere", + "GameObject/3D Object/Capsule", + "GameObject/3D Object/Cylinder", + "GameObject/3D Object/Plane", + "GameObject/Light/Directional Light", + "GameObject/Light/Point Light", + "GameObject/Light/Spotlight", + "GameObject/Light/Area Light", + "Component/Mesh/Mesh Filter", + "Component/Mesh/Mesh Renderer", + "Component/Physics/Rigidbody", + "Component/Physics/Box Collider", + "Component/Physics/Sphere Collider", + "Component/Physics/Capsule Collider", + "Component/Audio/Audio Source", + "Component/Audio/Audio Listener", + "Window/General/Scene", + "Window/General/Game", + "Window/General/Inspector", + "Window/General/Hierarchy", + "Window/General/Project", + "Window/General/Console", + "Window/Analysis/Profiler", + "Window/Package Manager", + "Assets/Create/Material", + "Assets/Create/C# Script", + "Assets/Create/Prefab", + "Assets/Create/Scene", + "Assets/Create/Folder", + }; + + foreach (string menuItem in builtInMenus) + { + menuCommands.Add(menuItem); + } + Debug.Log($"Added {builtInMenus.Length} built-in menu commands"); + + // Get menu commands from MenuItem attributes - wrapped in separate try block + Debug.Log("Searching for MenuItem attributes..."); + try + { + int itemCount = 0; + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { - UnityEditor.MenuItem menuItem = attributes[0] as UnityEditor.MenuItem; - commands.Add(menuItem.menuItem); + if (assembly.IsDynamic) continue; + + try + { + foreach (Type type in assembly.GetExportedTypes()) + { + try + { + foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + { + try + { + object[] attributes = method.GetCustomAttributes(typeof(UnityEditor.MenuItem), false); + if (attributes != null && attributes.Length > 0) + { + foreach (var attr in attributes) + { + var menuItem = attr as UnityEditor.MenuItem; + if (menuItem != null && !string.IsNullOrEmpty(menuItem.menuItem)) + { + menuCommands.Add(menuItem.menuItem); + itemCount++; + } + } + } + } + catch (Exception methodEx) + { + Debug.LogWarning($"Error getting menu items for method {method.Name}: {methodEx.Message}"); + continue; + } + } + } + catch (Exception typeEx) + { + Debug.LogWarning($"Error processing type: {typeEx.Message}"); + continue; + } + } + } + catch (Exception assemblyEx) + { + Debug.LogWarning($"Error examining assembly {assembly.GetName().Name}: {assemblyEx.Message}"); + continue; + } + } + Debug.Log($"Found {itemCount} menu items from attributes"); + } + catch (Exception menuItemEx) + { + Debug.LogError($"Failed to get menu items: {menuItemEx.Message}"); + } + + // Add EditorUtility methods as commands + Debug.Log("Adding EditorUtility methods..."); + foreach (MethodInfo method in typeof(EditorUtility).GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + utilityCommands.Add($"EditorUtility.{method.Name}"); + } + Debug.Log($"Added {typeof(EditorUtility).GetMethods(BindingFlags.Public | BindingFlags.Static).Length} EditorUtility methods"); + + // Add AssetDatabase methods as commands + Debug.Log("Adding AssetDatabase methods..."); + foreach (MethodInfo method in typeof(AssetDatabase).GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + assetCommands.Add($"AssetDatabase.{method.Name}"); + } + Debug.Log($"Added {typeof(AssetDatabase).GetMethods(BindingFlags.Public | BindingFlags.Static).Length} AssetDatabase methods"); + + // Add EditorSceneManager methods as commands + Debug.Log("Adding EditorSceneManager methods..."); + Type sceneManagerType = typeof(UnityEditor.SceneManagement.EditorSceneManager); + if (sceneManagerType != null) + { + foreach (MethodInfo method in sceneManagerType.GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + sceneCommands.Add($"EditorSceneManager.{method.Name}"); + } + Debug.Log($"Added {sceneManagerType.GetMethods(BindingFlags.Public | BindingFlags.Static).Length} EditorSceneManager methods"); + } + + // Add GameObject manipulation commands + Debug.Log("Adding GameObject methods..."); + foreach (MethodInfo method in typeof(GameObject).GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + gameObjectCommands.Add($"GameObject.{method.Name}"); + } + Debug.Log($"Added {typeof(GameObject).GetMethods(BindingFlags.Public | BindingFlags.Static).Length} GameObject methods"); + + // Add Selection-related commands + Debug.Log("Adding Selection methods..."); + foreach (MethodInfo method in typeof(Selection).GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + gameObjectCommands.Add($"Selection.{method.Name}"); + } + Debug.Log($"Added {typeof(Selection).GetMethods(BindingFlags.Public | BindingFlags.Static).Length} Selection methods"); + + // Add PrefabUtility methods as commands + Debug.Log("Adding PrefabUtility methods..."); + Type prefabUtilityType = typeof(UnityEditor.PrefabUtility); + if (prefabUtilityType != null) + { + foreach (MethodInfo method in prefabUtilityType.GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + prefabCommands.Add($"PrefabUtility.{method.Name}"); + } + Debug.Log($"Added {prefabUtilityType.GetMethods(BindingFlags.Public | BindingFlags.Static).Length} PrefabUtility methods"); + } + + // Add Undo related methods + Debug.Log("Adding Undo methods..."); + foreach (MethodInfo method in typeof(Undo).GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + utilityCommands.Add($"Undo.{method.Name}"); + } + Debug.Log($"Added {typeof(Undo).GetMethods(BindingFlags.Public | BindingFlags.Static).Length} Undo methods"); + + // The rest of the command gathering can be attempted but might not be critical + try + { + // Get commands from Unity's internal command system + Debug.Log("Trying to get internal CommandService commands..."); + Type commandServiceType = typeof(UnityEditor.EditorWindow).Assembly.GetType("UnityEditor.CommandService"); + if (commandServiceType != null) + { + Debug.Log("Found CommandService type"); + PropertyInfo instanceProperty = commandServiceType.GetProperty("Instance", + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + + if (instanceProperty != null) + { + Debug.Log("Found Instance property"); + object commandService = instanceProperty.GetValue(null); + if (commandService != null) + { + Debug.Log("Got CommandService instance"); + MethodInfo findAllCommandsMethod = commandServiceType.GetMethod("FindAllCommands", + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + if (findAllCommandsMethod != null) + { + Debug.Log("Found FindAllCommands method"); + var commandsResult = findAllCommandsMethod.Invoke(commandService, null); + if (commandsResult != null) + { + Debug.Log("Got commands result"); + var commandsList = commandsResult as System.Collections.IEnumerable; + if (commandsList != null) + { + int commandCount = 0; + foreach (var cmd in commandsList) + { + try + { + PropertyInfo nameProperty = cmd.GetType().GetProperty("name") ?? + cmd.GetType().GetProperty("path") ?? + cmd.GetType().GetProperty("commandName"); + if (nameProperty != null) + { + string commandName = nameProperty.GetValue(cmd)?.ToString(); + if (!string.IsNullOrEmpty(commandName)) + { + otherCommands.Add(commandName); + commandCount++; + } + } + } + catch (Exception cmdEx) + { + Debug.LogWarning($"Error processing command: {cmdEx.Message}"); + continue; + } + } + Debug.Log($"Added {commandCount} internal commands"); + } + } + else + { + Debug.LogWarning("FindAllCommands returned null"); + } + } + else + { + Debug.LogWarning("FindAllCommands method not found"); + } + } + else + { + Debug.LogWarning("CommandService instance is null"); + } + } + else + { + Debug.LogWarning("Instance property not found on CommandService"); + } + } + else + { + Debug.LogWarning("CommandService type not found"); } } + catch (Exception e) + { + Debug.LogWarning($"Failed to get internal Unity commands: {e.Message}"); + } + + // Other additional command sources can be tried + // ... other commands ... + } + catch (Exception e) + { + Debug.LogError($"Error getting Unity commands: {e.Message}\n{e.StackTrace}"); + } + + // Create command categories dictionary for the result + var commandCategories = new Dictionary> + { + { "MenuCommands", menuCommands.OrderBy(x => x).ToList() }, + { "UtilityCommands", utilityCommands.OrderBy(x => x).ToList() }, + { "AssetCommands", assetCommands.OrderBy(x => x).ToList() }, + { "SceneCommands", sceneCommands.OrderBy(x => x).ToList() }, + { "GameObjectCommands", gameObjectCommands.OrderBy(x => x).ToList() }, + { "PrefabCommands", prefabCommands.OrderBy(x => x).ToList() }, + { "ShortcutCommands", shortcutCommands.OrderBy(x => x).ToList() }, + { "OtherCommands", otherCommands.OrderBy(x => x).ToList() } + }; + + // Calculate total command count + int totalCount = commandCategories.Values.Sum(list => list.Count); + + Debug.Log($"Command retrieval complete. Found {totalCount} total commands."); + + // Create a simplified response with just the essential data + // The complex object structure might be causing serialization issues + var allCommandsList = commandCategories.Values.SelectMany(x => x).OrderBy(x => x).ToList(); + + // Use simple string array instead of JArray for better serialization + string[] commandsArray = allCommandsList.ToArray(); + + // Log the array size for verification + Debug.Log($"Final commands array contains {commandsArray.Length} items"); + + try + { + // Return a simple object with just the commands array and count + var result = new + { + commands = commandsArray, + count = commandsArray.Length + }; + + // Verify the result can be serialized properly + var jsonTest = JsonUtility.ToJson(new { test = "This is a test" }); + Debug.Log($"JSON serialization test successful: {jsonTest}"); + + return result; + } + catch (Exception ex) + { + Debug.LogError($"Error creating response: {ex.Message}"); + + // Ultimate fallback - don't use any JObject/JArray + return new + { + message = $"Found {commandsArray.Length} commands", + firstTen = commandsArray.Take(10).ToArray(), + count = commandsArray.Length + }; } - return new { commands = commands }; } } } \ No newline at end of file