From ee5db9285e728cf18be76dd497203f7287ffa6a3 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Fri, 12 Sep 2025 11:03:18 -0700 Subject: [PATCH 1/8] CI: update unity-tests workflow --- .github/workflows/unity-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index e1dea5a26..bd246ea1d 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -7,6 +7,7 @@ on: - TestProjects/UnityMCPTests/** - UnityMcpBridge/Editor/** - .github/workflows/unity-tests.yml + workflow_dispatch: jobs: testAllModes: From 8ee1f50001feea8ab28103fea8d7c189743b9084 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Fri, 12 Sep 2025 11:07:44 -0700 Subject: [PATCH 2/8] CI: Unity tests use EBL only (drop UNITY_LICENSE) --- .github/workflows/unity-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index bd246ea1d..ab1e164ce 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -45,7 +45,6 @@ jobs: env: UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} with: projectPath: ${{ matrix.projectPath }} unityVersion: ${{ matrix.unityVersion }} From 692d7199cb13513cd2a4b8e4fb0e7e99cd755e50 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Fri, 12 Sep 2025 11:12:15 -0700 Subject: [PATCH 3/8] CI: Unity tests use serial/license again (restore UNITY_LICENSE) --- .github/workflows/unity-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index ab1e164ce..bd246ea1d 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -45,6 +45,7 @@ jobs: env: UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} with: projectPath: ${{ matrix.projectPath }} unityVersion: ${{ matrix.unityVersion }} From a940741bf11cfb05a9a6f17a18039e7d06176b6e Mon Sep 17 00:00:00 2001 From: David Sarno Date: Fri, 12 Sep 2025 11:33:53 -0700 Subject: [PATCH 4/8] CI: revert unity-tests.yml to upstream/main version --- .github/workflows/unity-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index bd246ea1d..e1dea5a26 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -7,7 +7,6 @@ on: - TestProjects/UnityMCPTests/** - UnityMcpBridge/Editor/** - .github/workflows/unity-tests.yml - workflow_dispatch: jobs: testAllModes: From d5292567a33f900c26eae510e474bbaf25d043c3 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Mon, 29 Sep 2025 19:59:01 -0700 Subject: [PATCH 5/8] fix: add parameter validation to manage_gameobject tool - Prevent silent failures when using 'name' instead of 'search_term' for find action - Add clear error messages guiding users to correct parameter usage - Validate that 'search_term' is only used with 'find' action - Update parameter annotations to clarify when each parameter should be used --- .../src/tools/manage_gameobject.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py index 285b04613..27c0c0a13 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py @@ -16,10 +16,10 @@ def manage_gameobject( action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components"], "Perform CRUD operations on GameObjects and components."], target: Annotated[str, "GameObject identifier by name or path for modify/delete/component actions"] | None = None, - search_method: Annotated[str, - "How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups."] | None = None, + search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"], + "How to find objects. Used with 'find' and some 'target' lookups."] | None = None, name: Annotated[str, - "GameObject name - used for both 'create' (initial name) and 'modify' (rename)"] | None = None, + "GameObject name for 'create' (initial name) and 'modify' (rename) actions ONLY. For 'find' action, use 'search_term' instead."] | None = None, tag: Annotated[str, "Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None, parent: Annotated[str, @@ -53,7 +53,7 @@ def manage_gameobject( - Access shared material: `{"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}}`"""] | None = None, # --- Parameters for 'find' --- search_term: Annotated[str, - "Search term for 'find' action"] | None = None, + "Search term for 'find' action ONLY. Use this (not 'name') when searching for GameObjects."] | None = None, find_all: Annotated[bool, "If True, finds all GameObjects matching the search term"] | None = None, search_in_children: Annotated[bool, @@ -69,6 +69,26 @@ def manage_gameobject( ) -> dict[str, Any]: ctx.info(f"Processing manage_gameobject: {action}") try: + # Validate parameter usage to prevent silent failures + if action == "find": + if name is not None and search_term is None: + return { + "success": False, + "message": "For 'find' action, use 'search_term' parameter, not 'name'. Example: search_term='Player', search_method='by_name'" + } + if search_term is None: + return { + "success": False, + "message": "For 'find' action, 'search_term' parameter is required. Use search_term (not 'name') to specify what to find." + } + + if action in ["create", "modify"]: + if search_term is not None: + return { + "success": False, + "message": f"For '{action}' action, use 'name' parameter, not 'search_term'." + } + # Prepare parameters, removing None values params = { "action": action, From c38fc330d2d18ba1ca205e061a84ed36777e971b Mon Sep 17 00:00:00 2001 From: David Sarno Date: Mon, 29 Sep 2025 20:10:52 -0700 Subject: [PATCH 6/8] fix: reject 'name' parameter for find action in all cases - Simplify validation to reject 'name' parameter whenever present for find action - Remove ambiguity when both 'name' and 'search_term' are provided - Update error message to clarify that 'name' should be removed --- UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py index 27c0c0a13..c36a09d51 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py @@ -71,10 +71,10 @@ def manage_gameobject( try: # Validate parameter usage to prevent silent failures if action == "find": - if name is not None and search_term is None: + if name is not None: return { "success": False, - "message": "For 'find' action, use 'search_term' parameter, not 'name'. Example: search_term='Player', search_method='by_name'" + "message": "For 'find' action, use 'search_term' parameter, not 'name'. Remove 'name' parameter. Example: search_term='Player', search_method='by_name'" } if search_term is None: return { From 1ad8c6e5041d64a6862cbbfdfa2ddda9811ef9d1 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Mon, 29 Sep 2025 20:14:46 -0700 Subject: [PATCH 7/8] resolve merge conflicts from main --- UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py index c36a09d51..eb766ff41 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py @@ -145,4 +145,4 @@ def manage_gameobject( return response if isinstance(response, dict) else {"success": False, "message": str(response)} except Exception as e: - return {"success": False, "message": f"Python error managing GameObject: {str(e)}"} + return {"success": False, "message": f"Python error managing GameObject: {str(e)}"} \ No newline at end of file From a435973f782ef1db3548cac7719569586a4495ee Mon Sep 17 00:00:00 2001 From: David Sarno Date: Mon, 29 Sep 2025 20:33:58 -0700 Subject: [PATCH 8/8] Add get_component action to manage_gameobject Adds a new 'get_component' action that retrieves a single component's serialized data instead of all components, improving efficiency and avoiding token limits when only specific component data is needed. --- .../Editor/Tools/ManageGameObject.cs | 76 +++++++++++++++++++ .../src/tools/manage_gameobject.py | 4 +- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index c3357ed99..4c11f3435 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -155,6 +155,18 @@ public static object HandleCommand(JObject @params) ); // Pass the includeNonPublicSerialized flag here return GetComponentsFromTarget(getCompTarget, searchMethod, includeNonPublicSerialized); + case "get_component": + string getSingleCompTarget = targetToken?.ToString(); + if (getSingleCompTarget == null) + return Response.Error( + "'target' parameter required for get_component." + ); + string componentName = @params["componentName"]?.ToString(); + if (string.IsNullOrEmpty(componentName)) + return Response.Error( + "'componentName' parameter required for get_component." + ); + return GetSingleComponentFromTarget(getSingleCompTarget, searchMethod, componentName, includeNonPublicSerialized); case "add_component": return AddComponentToTarget(@params, targetToken, searchMethod); case "remove_component": @@ -1008,6 +1020,70 @@ private static object GetComponentsFromTarget(string target, string searchMethod } } + private static object GetSingleComponentFromTarget(string target, string searchMethod, string componentName, bool includeNonPublicSerialized = true) + { + GameObject targetGo = FindObjectInternal(target, searchMethod); + if (targetGo == null) + { + return Response.Error( + $"Target GameObject ('{target}') not found using method '{searchMethod ?? "default"}'." + ); + } + + try + { + // Try to find the component by name + Component targetComponent = targetGo.GetComponent(componentName); + + // If not found directly, try to find by type name (handle namespaces) + if (targetComponent == null) + { + Component[] allComponents = targetGo.GetComponents(); + foreach (Component comp in allComponents) + { + if (comp != null) + { + string typeName = comp.GetType().Name; + string fullTypeName = comp.GetType().FullName; + + if (typeName == componentName || fullTypeName == componentName) + { + targetComponent = comp; + break; + } + } + } + } + + if (targetComponent == null) + { + return Response.Error( + $"Component '{componentName}' not found on GameObject '{targetGo.name}'." + ); + } + + var componentData = Helpers.GameObjectSerializer.GetComponentData(targetComponent, includeNonPublicSerialized); + + if (componentData == null) + { + return Response.Error( + $"Failed to serialize component '{componentName}' on GameObject '{targetGo.name}'." + ); + } + + return Response.Success( + $"Retrieved component '{componentName}' from '{targetGo.name}'.", + componentData + ); + } + catch (Exception e) + { + return Response.Error( + $"Error getting component '{componentName}' from '{targetGo.name}': {e.Message}" + ); + } + } + private static object AddComponentToTarget( JObject @params, JToken targetToken, diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py index eb766ff41..41d4a1c09 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py @@ -9,11 +9,11 @@ def register_manage_gameobject_tools(mcp: FastMCP): """Register all GameObject management tools with the MCP server.""" - @mcp.tool(name="manage_gameobject", description="Manage GameObjects. Note: for 'get_components', the `data` field contains a dictionary of component names and their serialized properties.") + @mcp.tool(name="manage_gameobject", description="Manage GameObjects. Note: for 'get_components', the `data` field contains a dictionary of component names and their serialized properties. For 'get_component', specify 'component_name' to retrieve only that component's serialized data.") @telemetry_tool("manage_gameobject") def manage_gameobject( ctx: Context, - action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components"], "Perform CRUD operations on GameObjects and components."], + action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components", "get_component"], "Perform CRUD operations on GameObjects and components."], target: Annotated[str, "GameObject identifier by name or path for modify/delete/component actions"] | None = None, search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"],