Merge pull request #24 from justinpbarnett/feature/increase-commands

Feature/increase commands
main
Justin P Barnett 2025-03-21 06:35:48 -04:00 committed by GitHub
commit 4aff62fe5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 548 additions and 2 deletions

View File

@ -32,6 +32,7 @@ namespace UnityMCP.Editor.Commands
"BUILD" => HandleBuild(commandParams), "BUILD" => HandleBuild(commandParams),
"EXECUTE_COMMAND" => HandleExecuteCommand(commandParams), "EXECUTE_COMMAND" => HandleExecuteCommand(commandParams),
"READ_CONSOLE" => ReadConsole(commandParams), "READ_CONSOLE" => ReadConsole(commandParams),
"GET_AVAILABLE_COMMANDS" => GetAvailableCommands(),
_ => new { error = $"Unknown editor control command: {command}" }, _ => new { error = $"Unknown editor control command: {command}" },
}; };
} }
@ -583,5 +584,367 @@ namespace UnityMCP.Editor.Commands
return result; return result;
} }
/// <summary>
/// Gets a comprehensive list of available Unity commands, including editor menu items,
/// internal commands, utility methods, and other actionable operations that can be executed.
/// </summary>
/// <returns>Object containing categorized lists of available command paths</returns>
private static object GetAvailableCommands()
{
var menuCommands = new HashSet<string>();
var utilityCommands = new HashSet<string>();
var assetCommands = new HashSet<string>();
var sceneCommands = new HashSet<string>();
var gameObjectCommands = new HashSet<string>();
var prefabCommands = new HashSet<string>();
var shortcutCommands = new HashSet<string>();
var otherCommands = new HashSet<string>();
// 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<string, List<string>>
{
{ "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
};
}
}
} }
} }

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

@ -266,4 +266,30 @@ def register_editor_tools(mcp: FastMCP):
"type": "Error", "type": "Error",
"message": f"Error reading console: {str(e)}", "message": f"Error reading console: {str(e)}",
"stackTrace": "" "stackTrace": ""
}] }]
@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)}"]

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)}"}