1- import json
21from typing import Annotated , Any , Literal
32
43from fastmcp import Context
87from services .tools import get_unity_instance_from_context
98from transport .unity_transport import send_with_unity_instance
109from transport .legacy .unity_connection import async_send_command_with_retry
11- from services .tools .utils import coerce_bool , parse_json_payload , coerce_int , normalize_vector3
10+ from services .tools .utils import coerce_bool , parse_json_payload , normalize_vector3
1211from services .tools .preflight import preflight
1312
1413
@@ -40,7 +39,13 @@ def _normalize_component_properties(value: Any) -> tuple[dict[str, dict[str, Any
4039
4140
4241@mcp_for_unity_tool (
43- description = "Performs CRUD operations on GameObjects. Actions: create, modify, delete, duplicate, move_relative. For finding GameObjects use find_gameobjects tool. For component operations use manage_components tool." ,
42+ description = (
43+ "Performs CRUD operations on GameObjects. "
44+ "Actions: create, modify, delete, duplicate, move_relative. "
45+ "To FIND GameObjects, use the find_gameobjects tool instead. "
46+ "To manage COMPONENTS (add/remove/set_property), use the manage_components tool instead. "
47+ "To READ component data, use the mcpforunity://scene/gameobject/{id}/components resource."
48+ ),
4449 annotations = ToolAnnotations (
4550 title = "Manage GameObject" ,
4651 destructiveHint = True ,
@@ -51,11 +56,14 @@ async def manage_gameobject(
5156 action : Annotated [Literal ["create" , "modify" , "delete" , "duplicate" ,
5257 "move_relative" ], "Action to perform on GameObject." ] | None = None ,
5358 target : Annotated [str ,
54- "GameObject identifier by name or path for modify/delete/component actions" ] | None = None ,
55- search_method : Annotated [Literal ["by_id" , "by_name" , "by_path" , "by_tag" , "by_layer" , "by_component" ],
56- "How to find objects. Used with 'find' and some 'target' lookups." ] | None = None ,
59+ "GameObject identifier by name, path, or instance ID for modify/delete/duplicate actions" ] | None = None ,
60+ search_method : Annotated [
61+ Literal ["by_id" , "by_name" , "by_path" , "by_tag" , "by_layer" , "by_component" ],
62+ "How to resolve 'target'. If omitted, Unity infers: instance ID -> by_id, "
63+ "path (contains '/') -> by_path, otherwise by_name."
64+ ] | None = None ,
5765 name : Annotated [str ,
58- "GameObject name for 'create' (initial name) and 'modify' (rename) actions ONLY. For 'find' action, use 'search_term' instead ." ] | None = None ,
66+ "GameObject name for 'create' (initial name) and 'modify' (rename) actions." ] | None = None ,
5967 tag : Annotated [str ,
6068 "Tag name - used for both 'create' (initial tag) and 'modify' (change tag)" ] | None = None ,
6169 parent : Annotated [str ,
@@ -67,7 +75,7 @@ async def manage_gameobject(
6775 scale : Annotated [list [float ] | dict [str , float ] | str ,
6876 "Scale as [x, y, z] array, {x, y, z} object, or JSON string" ] | None = None ,
6977 components_to_add : Annotated [list [str ],
70- "List of component names to add" ] | None = None ,
78+ "List of component names to add during 'create' or 'modify' " ] | None = None ,
7179 primitive_type : Annotated [str ,
7280 "Primitive type for 'create' action" ] | None = None ,
7381 save_as_prefab : Annotated [bool | str ,
@@ -87,30 +95,6 @@ async def manage_gameobject(
8795 `{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component
8896 Example set nested property:
8997 - Access shared material: `{"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}}`""" ] | None = None ,
90- # --- Parameters for 'find' ---
91- search_term : Annotated [str ,
92- "Search term for 'find' action ONLY. Use this (not 'name') when searching for GameObjects." ] | None = None ,
93- find_all : Annotated [bool | str ,
94- "If True, finds all GameObjects matching the search term (accepts true/false or 'true'/'false')" ] | None = None ,
95- search_in_children : Annotated [bool | str ,
96- "If True, searches in children of the GameObject (accepts true/false or 'true'/'false')" ] | None = None ,
97- search_inactive : Annotated [bool | str ,
98- "If True, searches inactive GameObjects (accepts true/false or 'true'/'false')" ] | None = None ,
99- # -- Component Management Arguments --
100- component_name : Annotated [str ,
101- "Component name for 'add_component' and 'remove_component' actions" ] | None = None ,
102- # Controls whether serialization of private [SerializeField] fields is included
103- includeNonPublicSerialized : Annotated [bool | str ,
104- "Controls whether serialization of private [SerializeField] fields is included (accepts true/false or 'true'/'false')" ] | None = None ,
105- # --- Paging/safety for get_components ---
106- page_size : Annotated [int | str ,
107- "Page size for get_components paging." ] | None = None ,
108- cursor : Annotated [int | str ,
109- "Opaque cursor for get_components paging (offset)." ] | None = None ,
110- max_components : Annotated [int | str ,
111- "Hard cap on returned components per request (safety)." ] | None = None ,
112- include_properties : Annotated [bool | str ,
113- "If true, include serialized component properties (bounded)." ] | None = None ,
11498 # --- Parameters for 'duplicate' ---
11599 new_name : Annotated [str ,
116100 "New name for the duplicated object (default: SourceName_Copy)" ] | None = None ,
@@ -157,33 +141,15 @@ async def manage_gameobject(
157141 # --- Normalize boolean parameters ---
158142 save_as_prefab = coerce_bool (save_as_prefab )
159143 set_active = coerce_bool (set_active )
160- find_all = coerce_bool (find_all )
161- search_in_children = coerce_bool (search_in_children )
162- search_inactive = coerce_bool (search_inactive )
163- includeNonPublicSerialized = coerce_bool (includeNonPublicSerialized )
164- include_properties = coerce_bool (include_properties )
165144 world_space = coerce_bool (world_space , default = True )
166145
167- # --- Normalize integer parameters ---
168- page_size = coerce_int (page_size , default = None )
169- cursor = coerce_int (cursor , default = None )
170- max_components = coerce_int (max_components , default = None )
171-
172146 # --- Normalize component_properties with detailed error handling ---
173147 component_properties , comp_props_error = _normalize_component_properties (
174148 component_properties )
175149 if comp_props_error :
176150 return {"success" : False , "message" : comp_props_error }
177151
178152 try :
179- # Validate parameter usage to prevent silent failures
180- if action in ["create" , "modify" ]:
181- if search_term is not None :
182- return {
183- "success" : False ,
184- "message" : f"For '{ action } ' action, use 'name' parameter, not 'search_term'."
185- }
186-
187153 # Prepare parameters, removing None values
188154 params = {
189155 "action" : action ,
@@ -204,16 +170,6 @@ async def manage_gameobject(
204170 "layer" : layer ,
205171 "componentsToRemove" : components_to_remove ,
206172 "componentProperties" : component_properties ,
207- "searchTerm" : search_term ,
208- "findAll" : find_all ,
209- "searchInChildren" : search_in_children ,
210- "searchInactive" : search_inactive ,
211- "componentName" : component_name ,
212- "includeNonPublicSerialized" : includeNonPublicSerialized ,
213- "pageSize" : page_size ,
214- "cursor" : cursor ,
215- "maxComponents" : max_components ,
216- "includeProperties" : include_properties ,
217173 # Parameters for 'duplicate'
218174 "new_name" : new_name ,
219175 "offset" : offset ,
0 commit comments