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