diff --git a/Editor/Commands/EditorControlHandler.cs b/Editor/Commands/EditorControlHandler.cs index 5e14aa5..be48fe2 100644 --- a/Editor/Commands/EditorControlHandler.cs +++ b/Editor/Commands/EditorControlHandler.cs @@ -32,6 +32,7 @@ namespace UnityMCP.Editor.Commands "BUILD" => HandleBuild(commandParams), "EXECUTE_COMMAND" => HandleExecuteCommand(commandParams), "READ_CONSOLE" => ReadConsole(commandParams), + "GET_AVAILABLE_COMMANDS" => GetAvailableCommands(), _ => new { error = $"Unknown editor control command: {command}" }, }; } @@ -583,5 +584,367 @@ namespace UnityMCP.Editor.Commands return result; } + + /// + /// 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 categorized lists of available command paths + private static object GetAvailableCommands() + { + 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 + { + // 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)) + { + 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()) + { + 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 + }; + } + } } } \ No newline at end of file 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/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 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