diff --git a/README.md b/README.md
index c8aecf26..f7beb958 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ MCP for Unity acts as a bridge, allowing AI assistants (like Claude, Cursor) to
* `manage_asset`: Performs asset operations (import, create, modify, delete, etc.).
* `manage_shader`: Performs shader CRUD operations (create, read, modify, delete).
* `manage_gameobject`: Manages GameObjects: create, modify, delete, find, and component operations.
- * `execute_menu_item`: Executes a menu item via its path (e.g., "File/Save Project").
+ * `manage_menu_item`: List Unity Editor menu items; and check for their existence or execute them (e.g., execute "File/Save Project").
* `apply_text_edits`: Precise text edits with precondition hashes and atomic multi-edit batches.
* `script_apply_edits`: Structured C# method/class edits (insert/replace/delete) with safer boundaries.
* `validate_script`: Fast validation (basic/standard) to catch syntax/structure issues before/after writes.
diff --git a/TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs.meta b/TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs.meta
index 3d95d986..1261b65c 100644
--- a/TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs.meta
+++ b/TestProjects/UnityMCPTests/Assets/Scripts/LongUnityScriptClaudeTest.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: dfbabf507ab1245178d1a8e745d8d283
\ No newline at end of file
+guid: dfbabf507ab1245178d1a8e745d8d283
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef.meta b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef.meta
new file mode 100644
index 00000000..6376d072
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e5441db2ad88a4bc3a8f0868c9471142
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs.meta
index 31bddd75..26284a21 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs.meta
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 9e4468da1a15349029e52570b84ec4b0
\ No newline at end of file
+guid: 9e4468da1a15349029e52570b84ec4b0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs.meta
index c4c339a8..9e5900a5 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs.meta
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: c15ba6502927e4901a43826c43debd7c
\ No newline at end of file
+guid: c15ba6502927e4901a43826c43debd7c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs.meta
index cd9b0d92..8f11d54b 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs.meta
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 5931268353eab4ea5baa054e6231e824
\ No newline at end of file
+guid: 5931268353eab4ea5baa054e6231e824
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems.meta
new file mode 100644
index 00000000..fd11c223
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c01321ff6339b4763807adb979c5c427
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs
new file mode 100644
index 00000000..d4188040
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs
@@ -0,0 +1,47 @@
+using NUnit.Framework;
+using Newtonsoft.Json.Linq;
+using MCPForUnity.Editor.Tools.MenuItems;
+
+namespace MCPForUnityTests.Editor.Tools.MenuItems
+{
+ public class ManageMenuItemTests
+ {
+ private static JObject ToJO(object o) => JObject.FromObject(o);
+
+ [Test]
+ public void HandleCommand_UnknownAction_ReturnsError()
+ {
+ var res = ManageMenuItem.HandleCommand(new JObject { ["action"] = "unknown_action" });
+ var jo = ToJO(res);
+ Assert.IsFalse((bool)jo["success"], "Expected success false for unknown action");
+ StringAssert.Contains("Unknown action", (string)jo["error"]);
+ }
+
+ [Test]
+ public void HandleCommand_List_RoutesAndReturnsArray()
+ {
+ var res = ManageMenuItem.HandleCommand(new JObject { ["action"] = "list" });
+ var jo = ToJO(res);
+ Assert.IsTrue((bool)jo["success"], "Expected success true");
+ Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array");
+ }
+
+ [Test]
+ public void HandleCommand_Execute_Blacklisted_RoutesAndErrors()
+ {
+ var res = ManageMenuItem.HandleCommand(new JObject { ["action"] = "execute", ["menuPath"] = "File/Quit" });
+ var jo = ToJO(res);
+ Assert.IsFalse((bool)jo["success"], "Expected success false");
+ StringAssert.Contains("blocked for safety", (string)jo["error"], "Expected blacklist message");
+ }
+
+ [Test]
+ public void HandleCommand_Exists_MissingParam_ReturnsError()
+ {
+ var res = ManageMenuItem.HandleCommand(new JObject { ["action"] = "exists" });
+ var jo = ToJO(res);
+ Assert.IsFalse((bool)jo["success"], "Expected success false when missing menuPath");
+ StringAssert.Contains("Required parameter", (string)jo["error"]);
+ }
+ }
+}
diff --git a/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs.meta
similarity index 83%
rename from UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs.meta
rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs.meta
index d9520d98..6f1a8c2b 100644
--- a/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs.meta
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 896e8045986eb0d449ee68395479f1d6
+guid: 2b36e5f577aa1481c8758831c49d8f9d
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemExecutorTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemExecutorTests.cs
new file mode 100644
index 00000000..495f429d
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemExecutorTests.cs
@@ -0,0 +1,39 @@
+using NUnit.Framework;
+using Newtonsoft.Json.Linq;
+using MCPForUnity.Editor.Tools.MenuItems;
+
+namespace MCPForUnityTests.Editor.Tools.MenuItems
+{
+ public class MenuItemExecutorTests
+ {
+ private static JObject ToJO(object o) => JObject.FromObject(o);
+
+ [Test]
+ public void Execute_MissingParam_ReturnsError()
+ {
+ var res = MenuItemExecutor.Execute(new JObject());
+ var jo = ToJO(res);
+ Assert.IsFalse((bool)jo["success"], "Expected success false");
+ StringAssert.Contains("Required parameter", (string)jo["error"]);
+ }
+
+ [Test]
+ public void Execute_Blacklisted_ReturnsError()
+ {
+ var res = MenuItemExecutor.Execute(new JObject { ["menuPath"] = "File/Quit" });
+ var jo = ToJO(res);
+ Assert.IsFalse((bool)jo["success"], "Expected success false for blacklisted menu");
+ StringAssert.Contains("blocked for safety", (string)jo["error"], "Expected blacklist message");
+ }
+
+ [Test]
+ public void Execute_NonBlacklisted_ReturnsImmediateSuccess()
+ {
+ // We don't rely on the menu actually existing; execution is delayed and we only check the immediate response shape
+ var res = MenuItemExecutor.Execute(new JObject { ["menuPath"] = "File/Save Project" });
+ var jo = ToJO(res);
+ Assert.IsTrue((bool)jo["success"], "Expected immediate success response");
+ StringAssert.Contains("Attempted to execute menu item", (string)jo["message"], "Expected attempt message");
+ }
+ }
+}
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemExecutorTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemExecutorTests.cs.meta
new file mode 100644
index 00000000..6c6db472
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemExecutorTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ae694b6ac48824768a319eb378e7fb63
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemsReaderTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemsReaderTests.cs
new file mode 100644
index 00000000..e13e1b90
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemsReaderTests.cs
@@ -0,0 +1,88 @@
+using NUnit.Framework;
+using Newtonsoft.Json.Linq;
+using MCPForUnity.Editor.Tools.MenuItems;
+using System;
+using System.Linq;
+
+namespace MCPForUnityTests.Editor.Tools.MenuItems
+{
+ public class MenuItemsReaderTests
+ {
+ private static JObject ToJO(object o) => JObject.FromObject(o);
+
+ [Test]
+ public void List_NoSearch_ReturnsSuccessAndArray()
+ {
+ var res = MenuItemsReader.List(new JObject());
+ var jo = ToJO(res);
+ Assert.IsTrue((bool)jo["success"], "Expected success true");
+ Assert.IsNotNull(jo["data"], "Expected data field present");
+ Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array");
+
+ // Validate list is sorted ascending when there are multiple items
+ var arr = (JArray)jo["data"];
+ if (arr.Count >= 2)
+ {
+ var original = arr.Select(t => (string)t).ToList();
+ var sorted = original.OrderBy(s => s, StringComparer.Ordinal).ToList();
+ CollectionAssert.AreEqual(sorted, original, "Expected menu items to be sorted ascending");
+ }
+ }
+
+ [Test]
+ public void List_SearchNoMatch_ReturnsEmpty()
+ {
+ var res = MenuItemsReader.List(new JObject { ["search"] = "___unlikely___term___" });
+ var jo = ToJO(res);
+ Assert.IsTrue((bool)jo["success"], "Expected success true");
+ Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array");
+ Assert.AreEqual(0, jo["data"].Count(), "Expected no results for unlikely search term");
+ }
+
+ [Test]
+ public void List_SearchMatchesExistingItem_ReturnsContainingItem()
+ {
+ // Get the full list first
+ var listRes = MenuItemsReader.List(new JObject());
+ var listJo = ToJO(listRes);
+ if (listJo["data"] is JArray arr && arr.Count > 0)
+ {
+ var first = (string)arr[0];
+ // Use a mid-substring (case-insensitive) to avoid edge cases
+ var term = first.Length > 4 ? first.Substring(1, Math.Min(3, first.Length - 2)) : first;
+ term = term.ToLowerInvariant();
+
+ var res = MenuItemsReader.List(new JObject { ["search"] = term });
+ var jo = ToJO(res);
+ Assert.IsTrue((bool)jo["success"], "Expected success true");
+ Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array");
+ // Expect at least the original item to be present
+ var names = ((JArray)jo["data"]).Select(t => (string)t).ToList();
+ CollectionAssert.Contains(names, first, "Expected search results to include the sampled item");
+ }
+ else
+ {
+ Assert.Pass("No menu items available to perform a content-based search assertion.");
+ }
+ }
+
+ [Test]
+ public void Exists_MissingParam_ReturnsError()
+ {
+ var res = MenuItemsReader.Exists(new JObject());
+ var jo = ToJO(res);
+ Assert.IsFalse((bool)jo["success"], "Expected success false");
+ StringAssert.Contains("Required parameter", (string)jo["error"]);
+ }
+
+ [Test]
+ public void Exists_Bogus_ReturnsFalse()
+ {
+ var res = MenuItemsReader.Exists(new JObject { ["menuPath"] = "Nonexistent/Menu/___unlikely___" });
+ var jo = ToJO(res);
+ Assert.IsTrue((bool)jo["success"], "Expected success true");
+ Assert.IsNotNull(jo["data"], "Expected data field present");
+ Assert.IsFalse((bool)jo["data"]["exists"], "Expected exists false for bogus menu path");
+ }
+ }
+}
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemsReaderTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemsReaderTests.cs.meta
new file mode 100644
index 00000000..4c2a398e
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemsReaderTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: dbae8d670978f4a2bb525d7da9ed9f34
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json b/TestProjects/UnityMCPTests/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json
index 3c7b4c18..ad11087f 100644
--- a/TestProjects/UnityMCPTests/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json
+++ b/TestProjects/UnityMCPTests/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json
@@ -1,4 +1,6 @@
{
+ "m_Name": "Settings",
+ "m_Path": "ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json",
"m_Dictionary": {
"m_DictionaryValues": []
}
diff --git a/UnityMcpBridge/Editor/AssemblyInfo.cs.meta b/UnityMcpBridge/Editor/AssemblyInfo.cs.meta
index 343ff10e..72bf5f72 100644
--- a/UnityMcpBridge/Editor/AssemblyInfo.cs.meta
+++ b/UnityMcpBridge/Editor/AssemblyInfo.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: be61633e00d934610ac1ff8192ffbe3d
\ No newline at end of file
+guid: be61633e00d934610ac1ff8192ffbe3d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Helpers/PackageDetector.cs.meta b/UnityMcpBridge/Editor/Helpers/PackageDetector.cs.meta
index af305308..f1a5dbe4 100644
--- a/UnityMcpBridge/Editor/Helpers/PackageDetector.cs.meta
+++ b/UnityMcpBridge/Editor/Helpers/PackageDetector.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: b82eaef548d164ca095f17db64d15af8
\ No newline at end of file
+guid: b82eaef548d164ca095f17db64d15af8
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/MCPForUnityBridge.cs b/UnityMcpBridge/Editor/MCPForUnityBridge.cs
index 2c92b9ce..88b30deb 100644
--- a/UnityMcpBridge/Editor/MCPForUnityBridge.cs
+++ b/UnityMcpBridge/Editor/MCPForUnityBridge.cs
@@ -14,6 +14,7 @@
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Tools;
+using MCPForUnity.Editor.Tools.MenuItems;
namespace MCPForUnity.Editor
{
@@ -1053,7 +1054,7 @@ private static string ExecuteCommand(Command command)
"manage_asset" => ManageAsset.HandleCommand(paramsObject),
"manage_shader" => ManageShader.HandleCommand(paramsObject),
"read_console" => ReadConsole.HandleCommand(paramsObject),
- "execute_menu_item" => ExecuteMenuItem.HandleCommand(paramsObject),
+ "manage_menu_item" => ManageMenuItem.HandleCommand(paramsObject),
_ => throw new ArgumentException(
$"Unknown or unsupported command type: {command.type}"
),
diff --git a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs
index 55c7425b..912ddf59 100644
--- a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs
+++ b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
+using MCPForUnity.Editor.Tools.MenuItems;
namespace MCPForUnity.Editor.Tools
{
@@ -19,7 +20,7 @@ public static class CommandRegistry
{ "HandleManageGameObject", ManageGameObject.HandleCommand },
{ "HandleManageAsset", ManageAsset.HandleCommand },
{ "HandleReadConsole", ReadConsole.HandleCommand },
- { "HandleExecuteMenuItem", ExecuteMenuItem.HandleCommand },
+ { "HandleManageMenuItem", ManageMenuItem.HandleCommand },
{ "HandleManageShader", ManageShader.HandleCommand},
};
diff --git a/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs b/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs
deleted file mode 100644
index 306cf8d3..00000000
--- a/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using System;
-using System.Collections.Generic; // Added for HashSet
-using Newtonsoft.Json.Linq;
-using UnityEditor;
-using UnityEngine;
-using MCPForUnity.Editor.Helpers; // For Response class
-
-namespace MCPForUnity.Editor.Tools
-{
- ///
- /// Handles executing Unity Editor menu items by path.
- ///
- public static class ExecuteMenuItem
- {
- // Basic blacklist to prevent accidental execution of potentially disruptive menu items.
- // This can be expanded based on needs.
- private static readonly HashSet _menuPathBlacklist = new HashSet(
- StringComparer.OrdinalIgnoreCase
- )
- {
- "File/Quit",
- // Add other potentially dangerous items like "Edit/Preferences...", "File/Build Settings..." if needed
- };
-
- ///
- /// Main handler for executing menu items or getting available ones.
- ///
- public static object HandleCommand(JObject @params)
- {
- string action = (@params["action"]?.ToString())?.ToLowerInvariant() ?? "execute"; // Default action
-
- try
- {
- switch (action)
- {
- case "execute":
- return ExecuteItem(@params);
- case "get_available_menus":
- // Getting a comprehensive list of *all* menu items dynamically is very difficult
- // and often requires complex reflection or maintaining a manual list.
- // Returning a placeholder/acknowledgement for now.
- Debug.LogWarning(
- "[ExecuteMenuItem] 'get_available_menus' action is not fully implemented. Dynamically listing all menu items is complex."
- );
- // Returning an empty list as per the refactor plan's requirements.
- return Response.Success(
- "'get_available_menus' action is not fully implemented. Returning empty list.",
- new List()
- );
- // TODO: Consider implementing a basic list of common/known menu items or exploring reflection techniques if this feature becomes critical.
- default:
- return Response.Error(
- $"Unknown action: '{action}'. Valid actions are 'execute', 'get_available_menus'."
- );
- }
- }
- catch (Exception e)
- {
- Debug.LogError($"[ExecuteMenuItem] Action '{action}' failed: {e}");
- return Response.Error($"Internal error processing action '{action}': {e.Message}");
- }
- }
-
- ///
- /// Executes a specific menu item.
- ///
- private static object ExecuteItem(JObject @params)
- {
- // Try both naming conventions: snake_case and camelCase
- string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString();
- // Optional future param retained for API compatibility; not used in synchronous mode
- // int timeoutMs = Math.Max(0, (@params["timeout_ms"]?.ToObject() ?? 2000));
-
- // string alias = @params["alias"]?.ToString(); // TODO: Implement alias mapping based on refactor plan requirements.
- // JObject parameters = @params["parameters"] as JObject; // TODO: Investigate parameter passing (often not directly supported by ExecuteMenuItem).
-
- if (string.IsNullOrWhiteSpace(menuPath))
- {
- return Response.Error("Required parameter 'menu_path' or 'menuPath' is missing or empty.");
- }
-
- // Validate against blacklist
- if (_menuPathBlacklist.Contains(menuPath))
- {
- return Response.Error(
- $"Execution of menu item '{menuPath}' is blocked for safety reasons."
- );
- }
-
- // TODO: Implement alias lookup here if needed (Map alias to actual menuPath).
- // if (!string.IsNullOrEmpty(alias)) { menuPath = LookupAlias(alias); if(menuPath == null) return Response.Error(...); }
-
- // TODO: Handle parameters ('parameters' object) if a viable method is found.
- // This is complex as EditorApplication.ExecuteMenuItem doesn't take arguments directly.
- // It might require finding the underlying EditorWindow or command if parameters are needed.
-
- try
- {
- // Trace incoming execute requests (debug-gated)
- McpLog.Info($"[ExecuteMenuItem] Request to execute menu: '{menuPath}'", always: false);
-
- // Execute synchronously. This code runs on the Editor main thread in our bridge path.
- bool executed = EditorApplication.ExecuteMenuItem(menuPath);
- if (executed)
- {
- // Success trace (debug-gated)
- McpLog.Info($"[ExecuteMenuItem] Executed successfully: '{menuPath}'", always: false);
- return Response.Success(
- $"Executed menu item: '{menuPath}'",
- new { executed = true, menuPath }
- );
- }
- Debug.LogWarning($"[ExecuteMenuItem] Failed (not found/disabled): '{menuPath}'");
- return Response.Error(
- $"Failed to execute menu item (not found or disabled): '{menuPath}'",
- new { executed = false, menuPath }
- );
- }
- catch (Exception e)
- {
- Debug.LogError($"[ExecuteMenuItem] Error executing '{menuPath}': {e}");
- return Response.Error($"Error executing menu item '{menuPath}': {e.Message}");
- }
- }
-
- // TODO: Add helper for alias lookup if implementing aliases.
- // private static string LookupAlias(string alias) { ... return actualMenuPath or null ... }
- }
-}
-
diff --git a/UnityMcpBridge/Editor/Tools/MenuItems.meta b/UnityMcpBridge/Editor/Tools/MenuItems.meta
new file mode 100644
index 00000000..ffbda8e7
--- /dev/null
+++ b/UnityMcpBridge/Editor/Tools/MenuItems.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2df8f144c6e684ec3bfd53e4a48f06ee
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/MenuItems/ManageMenuItem.cs b/UnityMcpBridge/Editor/Tools/MenuItems/ManageMenuItem.cs
new file mode 100644
index 00000000..8cca35a6
--- /dev/null
+++ b/UnityMcpBridge/Editor/Tools/MenuItems/ManageMenuItem.cs
@@ -0,0 +1,47 @@
+using System;
+using Newtonsoft.Json.Linq;
+using UnityEditor;
+using UnityEngine;
+using MCPForUnity.Editor.Helpers;
+
+namespace MCPForUnity.Editor.Tools.MenuItems
+{
+ ///
+ /// Facade handler for managing Unity Editor menu items.
+ /// Routes actions to read or execute implementations.
+ ///
+ public static class ManageMenuItem
+ {
+ ///
+ /// Routes actions: execute, list, exists, refresh
+ ///
+ public static object HandleCommand(JObject @params)
+ {
+ string action = @params["action"]?.ToString()?.ToLowerInvariant();
+ if (string.IsNullOrEmpty(action))
+ {
+ return Response.Error("Action parameter is required. Valid actions are: execute, list, exists, refresh.");
+ }
+
+ try
+ {
+ switch (action)
+ {
+ case "execute":
+ return MenuItemExecutor.Execute(@params);
+ case "list":
+ return MenuItemsReader.List(@params);
+ case "exists":
+ return MenuItemsReader.Exists(@params);
+ default:
+ return Response.Error($"Unknown action: '{action}'. Valid actions are: execute, list, exists, refresh.");
+ }
+ }
+ catch (Exception e)
+ {
+ McpLog.Error($"[ManageMenuItem] Action '{action}' failed: {e}");
+ return Response.Error($"Internal error: {e.Message}");
+ }
+ }
+ }
+}
diff --git a/UnityMcpBridge/Editor/Tools/MenuItems/ManageMenuItem.cs.meta b/UnityMcpBridge/Editor/Tools/MenuItems/ManageMenuItem.cs.meta
new file mode 100644
index 00000000..aba1f496
--- /dev/null
+++ b/UnityMcpBridge/Editor/Tools/MenuItems/ManageMenuItem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 77808278b21a6474a90f3abb91483f71
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/MenuItems/MenuItemExecutor.cs b/UnityMcpBridge/Editor/Tools/MenuItems/MenuItemExecutor.cs
new file mode 100644
index 00000000..fe6180f7
--- /dev/null
+++ b/UnityMcpBridge/Editor/Tools/MenuItems/MenuItemExecutor.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+using UnityEditor;
+using UnityEngine;
+using MCPForUnity.Editor.Helpers;
+
+namespace MCPForUnity.Editor.Tools.MenuItems
+{
+ ///
+ /// Executes Unity Editor menu items by path with safety checks.
+ ///
+ public static class MenuItemExecutor
+ {
+ // Basic blacklist to prevent execution of disruptive menu items.
+ private static readonly HashSet _menuPathBlacklist = new HashSet(
+ StringComparer.OrdinalIgnoreCase)
+ {
+ "File/Quit",
+ };
+
+ ///
+ /// Execute a specific menu item. Expects 'menu_path' or 'menuPath' in params.
+ ///
+ public static object Execute(JObject @params)
+ {
+ string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString();
+ if (string.IsNullOrWhiteSpace(menuPath))
+ {
+ return Response.Error("Required parameter 'menu_path' or 'menuPath' is missing or empty.");
+ }
+
+ if (_menuPathBlacklist.Contains(menuPath))
+ {
+ return Response.Error($"Execution of menu item '{menuPath}' is blocked for safety reasons.");
+ }
+
+ try
+ {
+ // Execute on main thread using delayCall
+ EditorApplication.delayCall += () =>
+ {
+ try
+ {
+ bool executed = EditorApplication.ExecuteMenuItem(menuPath);
+ if (!executed)
+ {
+ McpLog.Error($"[MenuItemExecutor] Failed to execute menu item via delayCall: '{menuPath}'. It might be invalid, disabled, or context-dependent.");
+ }
+ }
+ catch (Exception delayEx)
+ {
+ McpLog.Error($"[MenuItemExecutor] Exception during delayed execution of '{menuPath}': {delayEx}");
+ }
+ };
+
+ return Response.Success($"Attempted to execute menu item: '{menuPath}'. Check Unity logs for confirmation or errors.");
+ }
+ catch (Exception e)
+ {
+ McpLog.Error($"[MenuItemExecutor] Failed to setup execution for '{menuPath}': {e}");
+ return Response.Error($"Error setting up execution for menu item '{menuPath}': {e.Message}");
+ }
+ }
+ }
+}
diff --git a/UnityMcpBridge/Editor/Tools/MenuItems/MenuItemExecutor.cs.meta b/UnityMcpBridge/Editor/Tools/MenuItems/MenuItemExecutor.cs.meta
new file mode 100644
index 00000000..2e9f4223
--- /dev/null
+++ b/UnityMcpBridge/Editor/Tools/MenuItems/MenuItemExecutor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1ccc7c6ff549542e1ae4ba3463ae79d2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/MenuItems/MenuItemsReader.cs b/UnityMcpBridge/Editor/Tools/MenuItems/MenuItemsReader.cs
new file mode 100644
index 00000000..db91feb3
--- /dev/null
+++ b/UnityMcpBridge/Editor/Tools/MenuItems/MenuItemsReader.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json.Linq;
+using UnityEditor;
+using UnityEngine;
+using MCPForUnity.Editor.Helpers;
+
+namespace MCPForUnity.Editor.Tools.MenuItems
+{
+ ///
+ /// Provides read/list/exists capabilities for Unity menu items with caching.
+ ///
+ public static class MenuItemsReader
+ {
+ private static List _cached;
+
+ [InitializeOnLoadMethod]
+ private static void Build() => Refresh();
+
+ ///
+ /// Returns the cached list, refreshing if necessary.
+ ///
+ public static IReadOnlyList AllMenuItems() => _cached ??= Refresh();
+
+ ///
+ /// Rebuilds the cached list from reflection.
+ ///
+ private static List Refresh()
+ {
+ try
+ {
+ var methods = TypeCache.GetMethodsWithAttribute