Skip to content

Commit 8102460

Browse files
committed
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.
1 parent f334b0a commit 8102460

File tree

3 files changed

+158
-1
lines changed

3 files changed

+158
-1
lines changed

Editor/Commands/ObjectCommandHandler.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using UnityEngine.SceneManagement;
88
using UnityEditor.SceneManagement;
99
using UnityMCP.Editor.Helpers;
10+
using System.Reflection;
1011

1112
namespace UnityMCP.Editor.Commands
1213
{
@@ -398,5 +399,107 @@ private static GameObject CreateDirectionalLight()
398399
light.shadows = LightShadows.Soft;
399400
return obj;
400401
}
402+
403+
/// <summary>
404+
/// Executes a context menu method on a component of a game object
405+
/// </summary>
406+
public static object ExecuteContextMenuItem(JObject @params)
407+
{
408+
string objectName = (string)@params["object_name"] ?? throw new Exception("Parameter 'object_name' is required.");
409+
string componentName = (string)@params["component"] ?? throw new Exception("Parameter 'component' is required.");
410+
string contextMenuItemName = (string)@params["context_menu_item"] ?? throw new Exception("Parameter 'context_menu_item' is required.");
411+
412+
// Find the game object
413+
var obj = GameObject.Find(objectName) ?? throw new Exception($"Object '{objectName}' not found.");
414+
415+
// Find the component type
416+
Type componentType = FindTypeInLoadedAssemblies(componentName) ??
417+
throw new Exception($"Component type '{componentName}' not found.");
418+
419+
// Get the component from the game object
420+
var component = obj.GetComponent(componentType) ??
421+
throw new Exception($"Component '{componentName}' not found on object '{objectName}'.");
422+
423+
// Find methods with ContextMenu attribute matching the context menu item name
424+
var methods = componentType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
425+
.Where(m => m.GetCustomAttributes(typeof(ContextMenuItemAttribute), true).Any() ||
426+
m.GetCustomAttributes(typeof(ContextMenu), true)
427+
.Cast<ContextMenu>()
428+
.Any(attr => attr.menuItem == contextMenuItemName))
429+
.ToList();
430+
431+
// If no methods with ContextMenuItemAttribute are found, look for methods with name matching the context menu item
432+
if (methods.Count == 0)
433+
{
434+
methods = componentType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
435+
.Where(m => m.Name == contextMenuItemName)
436+
.ToList();
437+
}
438+
439+
if (methods.Count == 0)
440+
throw new Exception($"No context menu method '{contextMenuItemName}' found on component '{componentName}'.");
441+
442+
// If multiple methods match, use the first one and log a warning
443+
if (methods.Count > 1)
444+
{
445+
Debug.LogWarning($"Found multiple methods for context menu item '{contextMenuItemName}' on component '{componentName}'. Using the first one.");
446+
}
447+
448+
var method = methods[0];
449+
450+
// Execute the method
451+
try
452+
{
453+
method.Invoke(component, null);
454+
return new
455+
{
456+
success = true,
457+
message = $"Successfully executed context menu item '{contextMenuItemName}' on component '{componentName}' of object '{objectName}'."
458+
};
459+
}
460+
catch (Exception ex)
461+
{
462+
throw new Exception($"Error executing context menu item: {ex.Message}");
463+
}
464+
}
465+
466+
// Add this helper method to find types across all loaded assemblies
467+
private static Type FindTypeInLoadedAssemblies(string typeName)
468+
{
469+
// First try standard approach
470+
Type type = Type.GetType(typeName);
471+
if (type != null)
472+
return type;
473+
474+
type = Type.GetType($"UnityEngine.{typeName}");
475+
if (type != null)
476+
return type;
477+
478+
// Then search all loaded assemblies
479+
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
480+
{
481+
// Try with the simple name
482+
type = assembly.GetType(typeName);
483+
if (type != null)
484+
return type;
485+
486+
// Try with the fully qualified name (assembly.GetTypes() can be expensive, so we do this last)
487+
var types = assembly.GetTypes().Where(t => t.Name == typeName).ToArray();
488+
489+
if (types.Length > 0)
490+
{
491+
// If we found multiple types with the same name, log a warning
492+
if (types.Length > 1)
493+
{
494+
Debug.LogWarning(
495+
$"Found multiple types named '{typeName}'. Using the first one: {types[0].FullName}"
496+
);
497+
}
498+
return types[0];
499+
}
500+
}
501+
502+
return null;
503+
}
401504
}
402505
}

Editor/UnityMCPBridge.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ private static string ExecuteCommand(Command command)
286286
"CREATE_OBJECT" => ObjectCommandHandler.CreateObject(command.@params),
287287
"MODIFY_OBJECT" => ObjectCommandHandler.ModifyObject(command.@params),
288288
"DELETE_OBJECT" => ObjectCommandHandler.DeleteObject(command.@params),
289+
"EXECUTE_CONTEXT_MENU_ITEM" => ObjectCommandHandler.ExecuteContextMenuItem(command.@params),
289290
"GET_OBJECT_PROPERTIES" => ObjectCommandHandler.GetObjectProperties(command.@params),
290291
"GET_COMPONENT_PROPERTIES" => ObjectCommandHandler.GetComponentProperties(command.@params),
291292
"FIND_OBJECTS_BY_NAME" => ObjectCommandHandler.FindObjectsByName(command.@params),

Python/tools/object_tools.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,57 @@ def get_asset_list(
194194
})
195195
return response.get("assets", [])
196196
except Exception as e:
197-
return [{"error": f"Failed to get asset list: {str(e)}"}]
197+
return [{"error": f"Failed to get asset list: {str(e)}"}]
198+
199+
@mcp.tool()
200+
def execute_context_menu_item(
201+
ctx: Context,
202+
object_name: str,
203+
component: str,
204+
context_menu_item: str
205+
) -> Dict[str, Any]:
206+
"""Execute a specific [ContextMenu] method on a component of a given game object.
207+
208+
Args:
209+
ctx: The MCP context
210+
object_name: Name of the game object to call
211+
component: Name of the component type
212+
context_menu_item: Name of the context menu item to execute
213+
214+
Returns:
215+
Dict containing the result of the operation
216+
"""
217+
try:
218+
unity = get_unity_connection()
219+
220+
# Check if the object exists
221+
found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", {
222+
"name": object_name
223+
}).get("objects", [])
224+
225+
if not found_objects:
226+
return {"error": f"Object with name '{object_name}' not found in the scene."}
227+
228+
# Check if the component exists on the object
229+
object_props = unity.send_command("GET_OBJECT_PROPERTIES", {
230+
"name": object_name
231+
})
232+
233+
if "error" in object_props:
234+
return {"error": f"Failed to get object properties: {object_props['error']}"}
235+
236+
components = object_props.get("components", [])
237+
component_exists = any(comp.get("type") == component for comp in components)
238+
239+
if not component_exists:
240+
return {"error": f"Component '{component}' is not attached to object '{object_name}'."}
241+
242+
# Now execute the context menu item
243+
response = unity.send_command("EXECUTE_CONTEXT_MENU_ITEM", {
244+
"object_name": object_name,
245+
"component": component,
246+
"context_menu_item": context_menu_item
247+
})
248+
return response
249+
except Exception as e:
250+
return {"error": f"Failed to execute context menu item: {str(e)}"}

0 commit comments

Comments
 (0)