mcpforunityserver 8.7.0__py3-none-any.whl → 9.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. main.py +4 -3
  2. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/METADATA +2 -2
  3. mcpforunityserver-9.0.0.dist-info/RECORD +72 -0
  4. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/top_level.txt +0 -1
  5. services/custom_tool_service.py +13 -8
  6. services/resources/active_tool.py +1 -1
  7. services/resources/custom_tools.py +2 -2
  8. services/resources/editor_state.py +283 -30
  9. services/resources/gameobject.py +243 -0
  10. services/resources/layers.py +1 -1
  11. services/resources/prefab_stage.py +1 -1
  12. services/resources/project_info.py +1 -1
  13. services/resources/selection.py +1 -1
  14. services/resources/tags.py +1 -1
  15. services/resources/unity_instances.py +1 -1
  16. services/resources/windows.py +1 -1
  17. services/state/external_changes_scanner.py +3 -4
  18. services/tools/batch_execute.py +24 -9
  19. services/tools/debug_request_context.py +8 -2
  20. services/tools/execute_custom_tool.py +6 -1
  21. services/tools/execute_menu_item.py +6 -3
  22. services/tools/find_gameobjects.py +89 -0
  23. services/tools/find_in_file.py +26 -19
  24. services/tools/manage_asset.py +13 -44
  25. services/tools/manage_components.py +131 -0
  26. services/tools/manage_editor.py +9 -8
  27. services/tools/manage_gameobject.py +115 -79
  28. services/tools/manage_material.py +80 -31
  29. services/tools/manage_prefabs.py +7 -1
  30. services/tools/manage_scene.py +30 -13
  31. services/tools/manage_script.py +62 -19
  32. services/tools/manage_scriptable_object.py +22 -10
  33. services/tools/manage_shader.py +8 -1
  34. services/tools/manage_vfx.py +738 -0
  35. services/tools/preflight.py +15 -12
  36. services/tools/read_console.py +31 -14
  37. services/tools/refresh_unity.py +28 -18
  38. services/tools/run_tests.py +162 -53
  39. services/tools/script_apply_edits.py +15 -7
  40. services/tools/set_active_instance.py +12 -7
  41. services/tools/utils.py +60 -6
  42. transport/legacy/port_discovery.py +2 -2
  43. transport/legacy/unity_connection.py +102 -17
  44. transport/plugin_hub.py +68 -24
  45. transport/unity_instance_middleware.py +4 -3
  46. transport/unity_transport.py +2 -1
  47. mcpforunityserver-8.7.0.dist-info/RECORD +0 -71
  48. routes/__init__.py +0 -0
  49. services/resources/editor_state_v2.py +0 -270
  50. services/tools/test_jobs.py +0 -94
  51. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/WHEEL +0 -0
  52. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/entry_points.txt +0 -0
  53. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,243 @@
1
+ """
2
+ MCP Resources for reading GameObject data from Unity scenes.
3
+
4
+ These resources provide read-only access to:
5
+ - Single GameObject data (mcpforunity://scene/gameobject/{id})
6
+ - All components on a GameObject (mcpforunity://scene/gameobject/{id}/components)
7
+ - Single component on a GameObject (mcpforunity://scene/gameobject/{id}/component/{name})
8
+ """
9
+ from typing import Any
10
+ from pydantic import BaseModel
11
+ from fastmcp import Context
12
+
13
+ from models import MCPResponse
14
+ from services.registry import mcp_for_unity_resource
15
+ from services.tools import get_unity_instance_from_context
16
+ from transport.unity_transport import send_with_unity_instance
17
+ from transport.legacy.unity_connection import async_send_command_with_retry
18
+
19
+
20
+ def _normalize_response(response: dict | Any) -> MCPResponse:
21
+ """Normalize Unity transport response to MCPResponse."""
22
+ if isinstance(response, dict):
23
+ return MCPResponse(**response)
24
+ return response
25
+
26
+
27
+ def _validate_instance_id(instance_id: str) -> tuple[int | None, MCPResponse | None]:
28
+ """
29
+ Validate and convert instance_id string to int.
30
+ Returns (id_int, None) on success or (None, error_response) on failure.
31
+ """
32
+ try:
33
+ return int(instance_id), None
34
+ except ValueError:
35
+ return None, MCPResponse(success=False, error=f"Invalid instance ID: {instance_id}")
36
+
37
+
38
+ # =============================================================================
39
+ # Static Helper Resource (shows in UI)
40
+ # =============================================================================
41
+
42
+ @mcp_for_unity_resource(
43
+ uri="mcpforunity://scene/gameobject-api",
44
+ name="gameobject_api",
45
+ description="Documentation for GameObject resources. Use find_gameobjects tool to get instance IDs, then access resources below."
46
+ )
47
+ async def get_gameobject_api_docs(_ctx: Context) -> MCPResponse:
48
+ """
49
+ Returns documentation for the GameObject resource API.
50
+
51
+ This is a helper resource that explains how to use the parameterized
52
+ GameObject resources which require an instance ID.
53
+ """
54
+ docs = {
55
+ "overview": "GameObject resources provide read-only access to Unity scene objects.",
56
+ "workflow": [
57
+ "1. Use find_gameobjects tool to search for GameObjects and get instance IDs",
58
+ "2. Use the instance ID to access detailed data via resources below"
59
+ ],
60
+ "best_practices": [
61
+ "⚡ Use batch_execute for multiple operations: Combine create/modify/component calls into one batch_execute call for 10-100x better performance",
62
+ "Example: Creating 5 cubes → 1 batch_execute with 5 manage_gameobject commands instead of 5 separate calls",
63
+ "Example: Adding components to 3 objects → 1 batch_execute with 3 manage_components commands"
64
+ ],
65
+ "resources": {
66
+ "mcpforunity://scene/gameobject/{instance_id}": {
67
+ "description": "Get basic GameObject data (name, tag, layer, transform, component type list)",
68
+ "example": "mcpforunity://scene/gameobject/-81840",
69
+ "returns": ["instanceID", "name", "tag", "layer", "transform", "componentTypes", "path", "parent", "children"]
70
+ },
71
+ "mcpforunity://scene/gameobject/{instance_id}/components": {
72
+ "description": "Get all components with full property serialization (paginated)",
73
+ "example": "mcpforunity://scene/gameobject/-81840/components",
74
+ "parameters": {
75
+ "page_size": "Number of components per page (default: 25)",
76
+ "cursor": "Pagination offset (default: 0)",
77
+ "include_properties": "Include full property data (default: true)"
78
+ }
79
+ },
80
+ "mcpforunity://scene/gameobject/{instance_id}/component/{component_name}": {
81
+ "description": "Get a single component by type name with full properties",
82
+ "example": "mcpforunity://scene/gameobject/-81840/component/Camera",
83
+ "note": "Use the component type name (e.g., 'Camera', 'Rigidbody', 'Transform')"
84
+ }
85
+ },
86
+ "related_tools": {
87
+ "find_gameobjects": "Search for GameObjects by name, tag, layer, component, or path",
88
+ "manage_components": "Add, remove, or modify components on GameObjects",
89
+ "manage_gameobject": "Create, modify, or delete GameObjects"
90
+ }
91
+ }
92
+ return MCPResponse(success=True, data=docs)
93
+
94
+
95
+ class TransformData(BaseModel):
96
+ """Transform component data."""
97
+ position: dict[str, float] = {"x": 0.0, "y": 0.0, "z": 0.0}
98
+ localPosition: dict[str, float] = {"x": 0.0, "y": 0.0, "z": 0.0}
99
+ rotation: dict[str, float] = {"x": 0.0, "y": 0.0, "z": 0.0}
100
+ localRotation: dict[str, float] = {"x": 0.0, "y": 0.0, "z": 0.0}
101
+ scale: dict[str, float] = {"x": 1.0, "y": 1.0, "z": 1.0}
102
+ lossyScale: dict[str, float] = {"x": 1.0, "y": 1.0, "z": 1.0}
103
+
104
+
105
+ class GameObjectData(BaseModel):
106
+ """Data for a single GameObject (without full component serialization)."""
107
+ instanceID: int
108
+ name: str
109
+ tag: str = "Untagged"
110
+ layer: int = 0
111
+ layerName: str = "Default"
112
+ active: bool = True
113
+ activeInHierarchy: bool = True
114
+ isStatic: bool = False
115
+ transform: TransformData = TransformData()
116
+ parent: int | None = None
117
+ children: list[int] = []
118
+ componentTypes: list[str] = []
119
+ path: str = ""
120
+
121
+
122
+ # TODO: Use these typed response classes for better type safety once
123
+ # we update the endpoints to validate response structure more strictly.
124
+ class GameObjectResponse(MCPResponse):
125
+ """Response containing GameObject data."""
126
+ data: GameObjectData | None = None
127
+
128
+
129
+ @mcp_for_unity_resource(
130
+ uri="mcpforunity://scene/gameobject/{instance_id}",
131
+ name="gameobject",
132
+ description="Get detailed information about a single GameObject by instance ID. Returns name, tag, layer, active state, transform data, parent/children IDs, and component type list (no full component properties)."
133
+ )
134
+ async def get_gameobject(ctx: Context, instance_id: str) -> MCPResponse:
135
+ """Get GameObject data by instance ID."""
136
+ unity_instance = get_unity_instance_from_context(ctx)
137
+
138
+ id_int, error = _validate_instance_id(instance_id)
139
+ if error:
140
+ return error
141
+
142
+ response = await send_with_unity_instance(
143
+ async_send_command_with_retry,
144
+ unity_instance,
145
+ "get_gameobject",
146
+ {"instanceID": id_int}
147
+ )
148
+
149
+ return _normalize_response(response)
150
+
151
+
152
+ class ComponentsData(BaseModel):
153
+ """Data for components on a GameObject."""
154
+ gameObjectID: int
155
+ gameObjectName: str
156
+ components: list[Any] = []
157
+ cursor: int = 0
158
+ pageSize: int = 25
159
+ nextCursor: int | None = None
160
+ totalCount: int = 0
161
+ hasMore: bool = False
162
+ includeProperties: bool = True
163
+
164
+
165
+ class ComponentsResponse(MCPResponse):
166
+ """Response containing components data."""
167
+ data: ComponentsData | None = None
168
+
169
+
170
+ @mcp_for_unity_resource(
171
+ uri="mcpforunity://scene/gameobject/{instance_id}/components",
172
+ name="gameobject_components",
173
+ description="Get all components on a GameObject with full property serialization. Supports pagination with pageSize and cursor parameters."
174
+ )
175
+ async def get_gameobject_components(
176
+ ctx: Context,
177
+ instance_id: str,
178
+ page_size: int = 25,
179
+ cursor: int = 0,
180
+ include_properties: bool = True
181
+ ) -> MCPResponse:
182
+ """Get all components on a GameObject."""
183
+ unity_instance = get_unity_instance_from_context(ctx)
184
+
185
+ id_int, error = _validate_instance_id(instance_id)
186
+ if error:
187
+ return error
188
+
189
+ response = await send_with_unity_instance(
190
+ async_send_command_with_retry,
191
+ unity_instance,
192
+ "get_gameobject_components",
193
+ {
194
+ "instanceID": id_int,
195
+ "pageSize": page_size,
196
+ "cursor": cursor,
197
+ "includeProperties": include_properties
198
+ }
199
+ )
200
+
201
+ return _normalize_response(response)
202
+
203
+
204
+ class SingleComponentData(BaseModel):
205
+ """Data for a single component."""
206
+ gameObjectID: int
207
+ gameObjectName: str
208
+ component: Any = None
209
+
210
+
211
+ class SingleComponentResponse(MCPResponse):
212
+ """Response containing single component data."""
213
+ data: SingleComponentData | None = None
214
+
215
+
216
+ @mcp_for_unity_resource(
217
+ uri="mcpforunity://scene/gameobject/{instance_id}/component/{component_name}",
218
+ name="gameobject_component",
219
+ description="Get a specific component on a GameObject by type name. Returns the fully serialized component with all properties."
220
+ )
221
+ async def get_gameobject_component(
222
+ ctx: Context,
223
+ instance_id: str,
224
+ component_name: str
225
+ ) -> MCPResponse:
226
+ """Get a specific component on a GameObject."""
227
+ unity_instance = get_unity_instance_from_context(ctx)
228
+
229
+ id_int, error = _validate_instance_id(instance_id)
230
+ if error:
231
+ return error
232
+
233
+ response = await send_with_unity_instance(
234
+ async_send_command_with_retry,
235
+ unity_instance,
236
+ "get_gameobject_component",
237
+ {
238
+ "instanceID": id_int,
239
+ "componentName": component_name
240
+ }
241
+ )
242
+
243
+ return _normalize_response(response)
@@ -13,7 +13,7 @@ class LayersResponse(MCPResponse):
13
13
 
14
14
 
15
15
  @mcp_for_unity_resource(
16
- uri="unity://project/layers",
16
+ uri="mcpforunity://project/layers",
17
17
  name="project_layers",
18
18
  description="All layers defined in the project's TagManager with their indices (0-31). Read this before using add_layer or remove_layer tools."
19
19
  )
@@ -23,7 +23,7 @@ class PrefabStageResponse(MCPResponse):
23
23
 
24
24
 
25
25
  @mcp_for_unity_resource(
26
- uri="unity://editor/prefab-stage",
26
+ uri="mcpforunity://editor/prefab-stage",
27
27
  name="editor_prefab_stage",
28
28
  description="Current prefab editing context if a prefab is open in isolation mode. Returns isOpen=false if no prefab is being edited."
29
29
  )
@@ -23,7 +23,7 @@ class ProjectInfoResponse(MCPResponse):
23
23
 
24
24
 
25
25
  @mcp_for_unity_resource(
26
- uri="unity://project/info",
26
+ uri="mcpforunity://project/info",
27
27
  name="project_info",
28
28
  description="Static project information including root path, Unity version, and platform. This data rarely changes."
29
29
  )
@@ -39,7 +39,7 @@ class SelectionResponse(MCPResponse):
39
39
 
40
40
 
41
41
  @mcp_for_unity_resource(
42
- uri="unity://editor/selection",
42
+ uri="mcpforunity://editor/selection",
43
43
  name="editor_selection",
44
44
  description="Detailed information about currently selected objects in the editor, including GameObjects, assets, and their properties."
45
45
  )
@@ -14,7 +14,7 @@ class TagsResponse(MCPResponse):
14
14
 
15
15
 
16
16
  @mcp_for_unity_resource(
17
- uri="unity://project/tags",
17
+ uri="mcpforunity://project/tags",
18
18
  name="project_tags",
19
19
  description="All tags defined in the project's TagManager. Read this before using add_tag or remove_tag tools."
20
20
  )
@@ -11,7 +11,7 @@ from transport.unity_transport import _current_transport
11
11
 
12
12
 
13
13
  @mcp_for_unity_resource(
14
- uri="unity://instances",
14
+ uri="mcpforunity://instances",
15
15
  name="unity_instances",
16
16
  description="Lists all running Unity Editor instances with their details."
17
17
  )
@@ -31,7 +31,7 @@ class WindowsResponse(MCPResponse):
31
31
 
32
32
 
33
33
  @mcp_for_unity_resource(
34
- uri="unity://editor/windows",
34
+ uri="mcpforunity://editor/windows",
35
35
  name="editor_windows",
36
36
  description="All currently open editor windows with their titles, types, positions, and focus state."
37
37
  )
@@ -116,7 +116,8 @@ class ExternalChangesScanner:
116
116
  st.manifest_last_mtime_ns = None
117
117
  return []
118
118
 
119
- mtime_ns = getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1_000_000_000))
119
+ mtime_ns = getattr(stat, "st_mtime_ns", int(
120
+ stat.st_mtime * 1_000_000_000))
120
121
  if st.extra_roots is not None and st.manifest_last_mtime_ns == mtime_ns:
121
122
  return [Path(p) for p in st.extra_roots if p]
122
123
 
@@ -143,7 +144,7 @@ class ExternalChangesScanner:
143
144
  v = ver.strip()
144
145
  if not v.startswith("file:"):
145
146
  continue
146
- suffix = v[len("file:") :].strip()
147
+ suffix = v[len("file:"):].strip()
147
148
  # Handle file:///abs/path or file:/abs/path
148
149
  if suffix.startswith("///"):
149
150
  candidate = Path("/" + suffix.lstrip("/"))
@@ -242,5 +243,3 @@ class ExternalChangesScanner:
242
243
 
243
244
  # Global singleton (simple, process-local)
244
245
  external_changes_scanner = ExternalChangesScanner()
245
-
246
-
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
  from typing import Annotated, Any
5
5
 
6
6
  from fastmcp import Context
7
+ from mcp.types import ToolAnnotations
7
8
 
8
9
  from services.registry import mcp_for_unity_tool
9
10
  from services.tools import get_unity_instance_from_context
@@ -16,22 +17,33 @@ MAX_COMMANDS_PER_BATCH = 25
16
17
  @mcp_for_unity_tool(
17
18
  name="batch_execute",
18
19
  description=(
19
- "Runs a list of MCP tool calls as one batch. Use it to send a full sequence of commands, "
20
- "inspect the results, then submit the next batch for the following step."
20
+ "Executes multiple MCP commands in a single batch for dramatically better performance. "
21
+ "STRONGLY RECOMMENDED when creating/modifying multiple objects, adding components to multiple targets, "
22
+ "or performing any repetitive operations. Reduces latency and token costs by 10-100x compared to "
23
+ "sequential tool calls. Supports up to 25 commands per batch. "
24
+ "Example: creating 5 cubes → use 1 batch_execute with 5 create commands instead of 5 separate calls."
25
+ ),
26
+ annotations=ToolAnnotations(
27
+ title="Batch Execute",
28
+ destructiveHint=True,
21
29
  ),
22
30
  )
23
31
  async def batch_execute(
24
32
  ctx: Context,
25
33
  commands: Annotated[list[dict[str, Any]], "List of commands with 'tool' and 'params' keys."],
26
- parallel: Annotated[bool | None, "Attempt to run read-only commands in parallel"] = None,
27
- fail_fast: Annotated[bool | None, "Stop processing after the first failure"] = None,
28
- max_parallelism: Annotated[int | None, "Hint for the maximum number of parallel workers"] = None,
34
+ parallel: Annotated[bool | None,
35
+ "Attempt to run read-only commands in parallel"] = None,
36
+ fail_fast: Annotated[bool | None,
37
+ "Stop processing after the first failure"] = None,
38
+ max_parallelism: Annotated[int | None,
39
+ "Hint for the maximum number of parallel workers"] = None,
29
40
  ) -> dict[str, Any]:
30
41
  """Proxy the batch_execute tool to the Unity Editor transporter."""
31
42
  unity_instance = get_unity_instance_from_context(ctx)
32
43
 
33
44
  if not isinstance(commands, list) or not commands:
34
- raise ValueError("'commands' must be a non-empty list of command specifications")
45
+ raise ValueError(
46
+ "'commands' must be a non-empty list of command specifications")
35
47
 
36
48
  if len(commands) > MAX_COMMANDS_PER_BATCH:
37
49
  raise ValueError(
@@ -41,18 +53,21 @@ async def batch_execute(
41
53
  normalized_commands: list[dict[str, Any]] = []
42
54
  for index, command in enumerate(commands):
43
55
  if not isinstance(command, dict):
44
- raise ValueError(f"Command at index {index} must be an object with 'tool' and 'params' keys")
56
+ raise ValueError(
57
+ f"Command at index {index} must be an object with 'tool' and 'params' keys")
45
58
 
46
59
  tool_name = command.get("tool")
47
60
  params = command.get("params", {})
48
61
 
49
62
  if not tool_name or not isinstance(tool_name, str):
50
- raise ValueError(f"Command at index {index} is missing a valid 'tool' name")
63
+ raise ValueError(
64
+ f"Command at index {index} is missing a valid 'tool' name")
51
65
 
52
66
  if params is None:
53
67
  params = {}
54
68
  if not isinstance(params, dict):
55
- raise ValueError(f"Command '{tool_name}' must specify parameters as an object/dict")
69
+ raise ValueError(
70
+ f"Command '{tool_name}' must specify parameters as an object/dict")
56
71
 
57
72
  normalized_commands.append({
58
73
  "tool": tool_name,
@@ -5,13 +5,19 @@ import sys
5
5
  from core.telemetry import get_package_version
6
6
 
7
7
  from fastmcp import Context
8
+ from mcp.types import ToolAnnotations
9
+
8
10
  from services.registry import mcp_for_unity_tool
9
11
  from transport.unity_instance_middleware import get_unity_instance_middleware
10
12
  from transport.plugin_hub import PluginHub
11
13
 
12
14
 
13
15
  @mcp_for_unity_tool(
14
- description="Return the current FastMCP request context details (client_id, session_id, and meta dump)."
16
+ description="Return the current FastMCP request context details (client_id, session_id, and meta dump).",
17
+ annotations=ToolAnnotations(
18
+ title="Debug Request Context",
19
+ readOnlyHint=True,
20
+ ),
15
21
  )
16
22
  def debug_request_context(ctx: Context) -> dict[str, Any]:
17
23
  # Check request_context properties
@@ -42,7 +48,7 @@ def debug_request_context(ctx: Context) -> dict[str, Any]:
42
48
  middleware = get_unity_instance_middleware()
43
49
  derived_key = middleware.get_session_key(ctx)
44
50
  active_instance = middleware.get_active_instance(ctx)
45
-
51
+
46
52
  # Debugging middleware internals
47
53
  # NOTE: These fields expose internal implementation details and may change between versions.
48
54
  with middleware._lock:
@@ -1,4 +1,5 @@
1
1
  from fastmcp import Context
2
+ from mcp.types import ToolAnnotations
2
3
  from models.models import MCPResponse
3
4
 
4
5
  from services.custom_tool_service import (
@@ -12,13 +13,17 @@ from services.tools import get_unity_instance_from_context
12
13
  @mcp_for_unity_tool(
13
14
  name="execute_custom_tool",
14
15
  description="Execute a project-scoped custom tool registered by Unity.",
16
+ annotations=ToolAnnotations(
17
+ title="Execute Custom Tool",
18
+ destructiveHint=True,
19
+ ),
15
20
  )
16
21
  async def execute_custom_tool(ctx: Context, tool_name: str, parameters: dict | None = None) -> MCPResponse:
17
22
  unity_instance = get_unity_instance_from_context(ctx)
18
23
  if not unity_instance:
19
24
  return MCPResponse(
20
25
  success=False,
21
- message="No active Unity instance. Call set_active_instance with Name@hash from unity://instances.",
26
+ message="No active Unity instance. Call set_active_instance with Name@hash from mcpforunity://instances.",
22
27
  )
23
28
 
24
29
  project_id = resolve_project_id_for_unity_instance(unity_instance)
@@ -4,6 +4,7 @@ Defines the execute_menu_item tool for executing and reading Unity Editor menu i
4
4
  from typing import Annotated, Any
5
5
 
6
6
  from fastmcp import Context
7
+ from mcp.types import ToolAnnotations
7
8
 
8
9
  from models import MCPResponse
9
10
  from services.registry import mcp_for_unity_tool
@@ -13,15 +14,17 @@ from transport.legacy.unity_connection import async_send_command_with_retry
13
14
 
14
15
 
15
16
  @mcp_for_unity_tool(
16
- description="Execute a Unity menu item by path."
17
+ description="Execute a Unity menu item by path.",
18
+ annotations=ToolAnnotations(
19
+ title="Execute Menu Item",
20
+ destructiveHint=True,
21
+ ),
17
22
  )
18
23
  async def execute_menu_item(
19
24
  ctx: Context,
20
25
  menu_path: Annotated[str,
21
26
  "Menu path for 'execute' or 'exists' (e.g., 'File/Save Project')"] | None = None,
22
27
  ) -> MCPResponse:
23
- # Get active instance from session state
24
- # Removed session_state import
25
28
  unity_instance = get_unity_instance_from_context(ctx)
26
29
  params_dict: dict[str, Any] = {"menuPath": menu_path}
27
30
  params_dict = {k: v for k, v in params_dict.items() if v is not None}
@@ -0,0 +1,89 @@
1
+ """
2
+ Tool for searching GameObjects in Unity scenes.
3
+ Returns only instance IDs with pagination support for efficient searches.
4
+ """
5
+ from typing import Annotated, Any, Literal
6
+
7
+ from fastmcp import Context
8
+ from services.registry import mcp_for_unity_tool
9
+ from services.tools import get_unity_instance_from_context
10
+ from transport.unity_transport import send_with_unity_instance
11
+ from transport.legacy.unity_connection import async_send_command_with_retry
12
+ from services.tools.utils import coerce_bool, coerce_int
13
+ from services.tools.preflight import preflight
14
+
15
+
16
+ @mcp_for_unity_tool(
17
+ description="Search for GameObjects in the scene. Returns instance IDs only (paginated) for efficient lookups. Use mcpforunity://scene/gameobject/{id} resource to get full GameObject data."
18
+ )
19
+ async def find_gameobjects(
20
+ ctx: Context,
21
+ search_term: Annotated[str, "The value to search for (name, tag, layer name, component type, or path)"],
22
+ search_method: Annotated[
23
+ Literal["by_name", "by_tag", "by_layer",
24
+ "by_component", "by_path", "by_id"],
25
+ "How to search for GameObjects"
26
+ ] = "by_name",
27
+ include_inactive: Annotated[bool | str,
28
+ "Include inactive GameObjects in search"] | None = None,
29
+ page_size: Annotated[int | str,
30
+ "Number of results per page (default: 50, max: 500)"] | None = None,
31
+ cursor: Annotated[int | str,
32
+ "Pagination cursor (offset for next page)"] | None = None,
33
+ ) -> dict[str, Any]:
34
+ """
35
+ Search for GameObjects and return their instance IDs.
36
+
37
+ This is a focused search tool optimized for finding GameObjects efficiently.
38
+ It returns only instance IDs to minimize payload size.
39
+
40
+ For detailed GameObject information, use the returned IDs with:
41
+ - mcpforunity://scene/gameobject/{id} - Get full GameObject data
42
+ - mcpforunity://scene/gameobject/{id}/components - Get all components
43
+ - mcpforunity://scene/gameobject/{id}/component/{name} - Get specific component
44
+ """
45
+ unity_instance = get_unity_instance_from_context(ctx)
46
+
47
+ # Validate required parameters before preflight I/O
48
+ if not search_term:
49
+ return {
50
+ "success": False,
51
+ "message": "Missing required parameter 'search_term'. Specify what to search for."
52
+ }
53
+
54
+ gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True)
55
+ if gate is not None:
56
+ return gate.model_dump()
57
+
58
+ # Coerce parameters
59
+ include_inactive = coerce_bool(include_inactive, default=False)
60
+ page_size = coerce_int(page_size, default=50)
61
+ cursor = coerce_int(cursor, default=0)
62
+
63
+ try:
64
+ params = {
65
+ "searchMethod": search_method,
66
+ "searchTerm": search_term,
67
+ "includeInactive": include_inactive,
68
+ "pageSize": page_size,
69
+ "cursor": cursor,
70
+ }
71
+ params = {k: v for k, v in params.items() if v is not None}
72
+
73
+ response = await send_with_unity_instance(
74
+ async_send_command_with_retry,
75
+ unity_instance,
76
+ "find_gameobjects",
77
+ params,
78
+ )
79
+
80
+ if isinstance(response, dict) and response.get("success"):
81
+ return {
82
+ "success": True,
83
+ "message": response.get("message", "Search completed."),
84
+ "data": response.get("data")
85
+ }
86
+ return response if isinstance(response, dict) else {"success": False, "message": str(response)}
87
+
88
+ except Exception as e:
89
+ return {"success": False, "message": f"Error searching GameObjects: {e!s}"}