diff --git a/UnityMCPPlugin/Editor/UnityMCPConnection.cs b/UnityMCPPlugin/Editor/UnityMCPConnection.cs index 26aad6c..08aa1f9 100644 --- a/UnityMCPPlugin/Editor/UnityMCPConnection.cs +++ b/UnityMCPPlugin/Editor/UnityMCPConnection.cs @@ -248,6 +248,9 @@ private static void HandleMessage(string message) case "executeEditorCommand": ExecuteEditorCommand(data["data"].ToString()); break; + case "executeMenuItem": + ExecuteMenuItem(data["data"].ToString()); + break; } } catch (Exception e) @@ -256,6 +259,97 @@ private static void HandleMessage(string message) } } + private static void ExecuteMenuItem(string menuItemData) + { + var logs = new List(); + var errors = new List(); + var warnings = new List(); + + Application.logMessageReceived += LogHandler; + + try + { + var menuDataObj = JsonConvert.DeserializeObject(menuItemData); + var menuPath = menuDataObj.menuPath; + + Debug.Log($"[UnityMCP] Executing menu item: {menuPath}"); + + bool success = EditorApplication.ExecuteMenuItem(menuPath); + + var resultMessage = JsonConvert.SerializeObject(new + { + type = "menuItemResult", + data = new + { + success = success, + message = success + ? $"Successfully executed menu item: {menuPath}" + : $"Failed to execute menu item: {menuPath}", + logs = logs, + warnings = warnings, + errors = errors + } + }); + + var buffer = Encoding.UTF8.GetBytes(resultMessage); + webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cts.Token).Wait(); + } + catch (Exception e) + { + var error = $"[UnityMCP] Failed to execute menu item: {e.Message}\n{e.StackTrace}"; + Debug.LogError(error); + + // Send back error information + var errorMessage = JsonConvert.SerializeObject(new + { + type = "menuItemResult", + data = new + { + success = false, + message = $"Error executing menu item: {e.Message}", + logs = logs, + errors = new List(errors) { error }, + warnings = warnings, + errorDetails = new + { + message = e.Message, + stackTrace = e.StackTrace, + type = e.GetType().Name + } + } + }); + + var buffer = Encoding.UTF8.GetBytes(errorMessage); + webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cts.Token).Wait(); + } + finally + { + Application.logMessageReceived -= LogHandler; + } + + void LogHandler(string logMessage, string stackTrace, LogType type) + { + switch (type) + { + case LogType.Log: + logs.Add(logMessage); + break; + case LogType.Warning: + warnings.Add(logMessage); + break; + case LogType.Error: + case LogType.Exception: + errors.Add($"{logMessage}\n{stackTrace}"); + break; + } + } + } + + private class MenuItemData + { + public string menuPath { get; set; } + } + private static void ExecuteEditorCommand(string commandData) { var logs = new List(); diff --git a/unity-mcp-server/src/index.ts b/unity-mcp-server/src/index.ts index 7695e6f..d86091b 100644 --- a/unity-mcp-server/src/index.ts +++ b/unity-mcp-server/src/index.ts @@ -138,6 +138,7 @@ class UnityMCPServer { break; case 'commandResult': + case 'menuItemResult': // Resolve the pending command result promise if (this.commandResultPromise) { this.commandResultPromise.resolve(message.data); @@ -193,6 +194,50 @@ class UnityMCPServer { } ] }, + { + name: 'execute_menu_item', + description: 'Execute a Unity menu item by path. This allows you to trigger any menu command available in the Unity Editor, such as creating objects, running menu commands, or executing editor functions.', + category: 'Editor Control', + tags: ['unity', 'editor', 'menu', 'command'], + inputSchema: { + type: 'object', + properties: { + menuPath: { + type: 'string', + description: 'The path to the menu item to execute (e.g. "GameObject/Create Empty" or "Window/Package Manager")', + minLength: 1, + examples: [ + 'GameObject/Create Empty', + 'Window/Package Manager', + 'Assets/Create/C# Script' + ] + } + }, + required: ['menuPath'], + additionalProperties: false + }, + returns: { + type: 'object', + description: 'Returns the execution result including success status and message', + format: 'JSON object containing "success" and "message" fields' + }, + examples: [ + { + description: 'Create an empty GameObject', + input: { + menuPath: 'GameObject/Create Empty' + }, + output: '{ "success": true, "message": "Successfully executed menu item: GameObject/Create Empty" }' + }, + { + description: 'Open Package Manager', + input: { + menuPath: 'Window/Package Manager' + }, + output: '{ "success": true, "message": "Successfully executed menu item: Window/Package Manager" }' + } + ] + }, { name: 'execute_editor_command', description: 'Execute arbitrary C# code within the Unity Editor context. This powerful tool allows for direct manipulation of the Unity Editor, GameObjects, components, and project assets using the Unity Editor API.', @@ -346,7 +391,7 @@ class UnityMCPServer { const { name, arguments: args } = request.params; // Validate tool exists with helpful error message - const availableTools = ['get_editor_state', 'execute_editor_command', 'get_logs']; + const availableTools = ['get_editor_state', 'execute_editor_command', 'execute_menu_item', 'get_logs']; if (!availableTools.includes(name)) { throw new McpError( ErrorCode.MethodNotFound, @@ -403,6 +448,76 @@ class UnityMCPServer { } } + case 'execute_menu_item': { + // Validate menuPath parameter + if (!args?.menuPath) { + throw new McpError( + ErrorCode.InvalidParams, + 'The menuPath parameter is required' + ); + } + + if (typeof args.menuPath !== 'string') { + throw new McpError( + ErrorCode.InvalidParams, + 'The menuPath parameter must be a string' + ); + } + + if (args.menuPath.trim().length === 0) { + throw new McpError( + ErrorCode.InvalidParams, + 'The menuPath parameter cannot be empty' + ); + } + + try { + // Send command to Unity + this.unityConnection.send(JSON.stringify({ + type: 'executeMenuItem', + data: { menuPath: args.menuPath }, + })); + + // Wait for result with timeout handling + const timeoutMs = 5000; + const result = await Promise.race([ + new Promise((resolve, reject) => { + this.commandResultPromise = { resolve, reject }; + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error( + `Menu item execution timed out after ${timeoutMs/1000} seconds. This may indicate a long-running menu operation or that the menu item doesn't exist.` + )), timeoutMs) + ) + ]); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + // Enhanced error handling + if (error instanceof Error) { + if (error.message.includes('timed out')) { + throw new McpError( + ErrorCode.InternalError, + error.message + ); + } + } + + // Generic error fallback + throw new McpError( + ErrorCode.InternalError, + `Failed to execute menu item: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } + } + case 'execute_editor_command': { // Validate code parameter if (!args?.code) {