Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions UnityMCPPlugin/Editor/UnityMCPConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -256,6 +259,97 @@ private static void HandleMessage(string message)
}
}

private static void ExecuteMenuItem(string menuItemData)
{
var logs = new List<string>();
var errors = new List<string>();
var warnings = new List<string>();

Application.logMessageReceived += LogHandler;

try
{
var menuDataObj = JsonConvert.DeserializeObject<MenuItemData>(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<byte>(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<string>(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<byte>(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<string>();
Expand Down
117 changes: 116 additions & 1 deletion unity-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down