Skip to content

Commit 1b5cdaf

Browse files
HivemindMinionHivemindOverlordclaude
authored
fix: remove vestigial find/component params from manage_gameobject (#693)
* fix: remove vestigial find/component params from manage_gameobject tool The manage_gameobject tool retained parameters (search_term, find_all, search_in_children, search_inactive, component_name, page_size, cursor, max_components, include_properties, includeNonPublicSerialized) from when it was a monolith tool. These params now belong to the separate find_gameobjects tool, manage_components tool, and gameobject_components resource. Their presence on manage_gameobject caused LLMs to hallucinate non-existent actions (e.g. action="find", action="get_components"), leading to ~46 failed tool calls in a single session. Also tightens search_method Literal to only values relevant for target resolution (by_id, by_name, by_path) and improves the tool description to explicitly cross-reference the correct tools and resources. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: update search_method Literal and docstring per review Expand search_method to include by_tag, by_layer, by_component and clarify that Unity infers the method when omitted. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: HivemindOverlord <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent 50c6ed4 commit 1b5cdaf

File tree

2 files changed

+29
-65
lines changed

2 files changed

+29
-65
lines changed

Server/src/services/tools/manage_gameobject.py

Lines changed: 16 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import json
21
from typing import Annotated, Any, Literal
32

43
from fastmcp import Context
@@ -8,7 +7,7 @@
87
from services.tools import get_unity_instance_from_context
98
from transport.unity_transport import send_with_unity_instance
109
from 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
1211
from 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,

Server/tests/integration/test_tool_signatures_paging.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,23 @@ def test_manage_scene_signature_includes_paging_params():
1919
assert "include_transform" in names
2020

2121

22-
def test_manage_gameobject_signature_includes_paging_params():
22+
def test_manage_gameobject_signature_excludes_vestigial_params():
23+
"""Paging/find/component params were removed — they belong to separate tools."""
2324
import services.tools.manage_gameobject as mod
2425

2526
sig = inspect.signature(mod.manage_gameobject)
2627
names = list(sig.parameters.keys())
2728

28-
assert "page_size" in names
29-
assert "cursor" in names
30-
assert "max_components" in names
31-
assert "include_properties" in names
29+
# These params now live on find_gameobjects / manage_components / gameobject_components resource
30+
assert "page_size" not in names
31+
assert "cursor" not in names
32+
assert "max_components" not in names
33+
assert "include_properties" not in names
34+
assert "search_term" not in names
35+
assert "find_all" not in names
36+
assert "search_in_children" not in names
37+
assert "search_inactive" not in names
38+
assert "component_name" not in names
39+
assert "includeNonPublicSerialized" not in names
3240

3341

0 commit comments

Comments
 (0)