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.
main
Kirill Kuvaldin 2025-03-20 14:20:47 -07:00
parent f334b0a6db
commit 8102460c7c
3 changed files with 158 additions and 1 deletions

View File

@ -7,6 +7,7 @@ using UnityEditor;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement; using UnityEditor.SceneManagement;
using UnityMCP.Editor.Helpers; using UnityMCP.Editor.Helpers;
using System.Reflection;
namespace UnityMCP.Editor.Commands namespace UnityMCP.Editor.Commands
{ {
@ -398,5 +399,107 @@ namespace UnityMCP.Editor.Commands
light.shadows = LightShadows.Soft; light.shadows = LightShadows.Soft;
return obj; return obj;
} }
/// <summary>
/// Executes a context menu method on a component of a game object
/// </summary>
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<ContextMenu>()
.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;
}
} }
} }

View File

@ -286,6 +286,7 @@ namespace UnityMCP.Editor
"CREATE_OBJECT" => ObjectCommandHandler.CreateObject(command.@params), "CREATE_OBJECT" => ObjectCommandHandler.CreateObject(command.@params),
"MODIFY_OBJECT" => ObjectCommandHandler.ModifyObject(command.@params), "MODIFY_OBJECT" => ObjectCommandHandler.ModifyObject(command.@params),
"DELETE_OBJECT" => ObjectCommandHandler.DeleteObject(command.@params), "DELETE_OBJECT" => ObjectCommandHandler.DeleteObject(command.@params),
"EXECUTE_CONTEXT_MENU_ITEM" => ObjectCommandHandler.ExecuteContextMenuItem(command.@params),
"GET_OBJECT_PROPERTIES" => ObjectCommandHandler.GetObjectProperties(command.@params), "GET_OBJECT_PROPERTIES" => ObjectCommandHandler.GetObjectProperties(command.@params),
"GET_COMPONENT_PROPERTIES" => ObjectCommandHandler.GetComponentProperties(command.@params), "GET_COMPONENT_PROPERTIES" => ObjectCommandHandler.GetComponentProperties(command.@params),
"FIND_OBJECTS_BY_NAME" => ObjectCommandHandler.FindObjectsByName(command.@params), "FIND_OBJECTS_BY_NAME" => ObjectCommandHandler.FindObjectsByName(command.@params),

View File

@ -194,4 +194,57 @@ def register_object_tools(mcp: FastMCP):
}) })
return response.get("assets", []) return response.get("assets", [])
except Exception as e: except Exception as e:
return [{"error": f"Failed to get asset list: {str(e)}"}] 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)}"}