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