mcpforunityserver 8.7.1__py3-none-any.whl → 9.0.1__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.
- main.py +4 -3
- {mcpforunityserver-8.7.1.dist-info → mcpforunityserver-9.0.1.dist-info}/METADATA +2 -2
- mcpforunityserver-9.0.1.dist-info/RECORD +72 -0
- {mcpforunityserver-8.7.1.dist-info → mcpforunityserver-9.0.1.dist-info}/top_level.txt +0 -1
- services/custom_tool_service.py +13 -8
- services/resources/active_tool.py +1 -1
- services/resources/custom_tools.py +2 -2
- services/resources/editor_state.py +283 -30
- services/resources/gameobject.py +243 -0
- services/resources/layers.py +1 -1
- services/resources/prefab_stage.py +1 -1
- services/resources/project_info.py +1 -1
- services/resources/selection.py +1 -1
- services/resources/tags.py +1 -1
- services/resources/unity_instances.py +1 -1
- services/resources/windows.py +1 -1
- services/state/external_changes_scanner.py +3 -4
- services/tools/batch_execute.py +24 -9
- services/tools/debug_request_context.py +8 -2
- services/tools/execute_custom_tool.py +6 -1
- services/tools/execute_menu_item.py +6 -3
- services/tools/find_gameobjects.py +89 -0
- services/tools/find_in_file.py +26 -19
- services/tools/manage_asset.py +13 -44
- services/tools/manage_components.py +131 -0
- services/tools/manage_editor.py +9 -8
- services/tools/manage_gameobject.py +115 -79
- services/tools/manage_material.py +80 -31
- services/tools/manage_prefabs.py +7 -1
- services/tools/manage_scene.py +30 -13
- services/tools/manage_script.py +62 -19
- services/tools/manage_scriptable_object.py +22 -10
- services/tools/manage_shader.py +8 -1
- services/tools/manage_vfx.py +738 -0
- services/tools/preflight.py +15 -12
- services/tools/read_console.py +11 -4
- services/tools/refresh_unity.py +24 -14
- services/tools/run_tests.py +162 -53
- services/tools/script_apply_edits.py +15 -7
- services/tools/set_active_instance.py +12 -7
- services/tools/utils.py +60 -6
- transport/legacy/port_discovery.py +2 -2
- transport/legacy/unity_connection.py +1 -1
- transport/plugin_hub.py +24 -16
- transport/unity_instance_middleware.py +4 -3
- transport/unity_transport.py +2 -1
- mcpforunityserver-8.7.1.dist-info/RECORD +0 -71
- routes/__init__.py +0 -0
- services/resources/editor_state_v2.py +0 -270
- services/tools/test_jobs.py +0 -94
- {mcpforunityserver-8.7.1.dist-info → mcpforunityserver-9.0.1.dist-info}/WHEEL +0 -0
- {mcpforunityserver-8.7.1.dist-info → mcpforunityserver-9.0.1.dist-info}/entry_points.txt +0 -0
- {mcpforunityserver-8.7.1.dist-info → mcpforunityserver-9.0.1.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)
|
services/resources/layers.py
CHANGED
|
@@ -13,7 +13,7 @@ class LayersResponse(MCPResponse):
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@mcp_for_unity_resource(
|
|
16
|
-
uri="
|
|
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="
|
|
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="
|
|
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
|
)
|
services/resources/selection.py
CHANGED
|
@@ -39,7 +39,7 @@ class SelectionResponse(MCPResponse):
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
@mcp_for_unity_resource(
|
|
42
|
-
uri="
|
|
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
|
)
|
services/resources/tags.py
CHANGED
|
@@ -14,7 +14,7 @@ class TagsResponse(MCPResponse):
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@mcp_for_unity_resource(
|
|
17
|
-
uri="
|
|
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="
|
|
14
|
+
uri="mcpforunity://instances",
|
|
15
15
|
name="unity_instances",
|
|
16
16
|
description="Lists all running Unity Editor instances with their details."
|
|
17
17
|
)
|
services/resources/windows.py
CHANGED
|
@@ -31,7 +31,7 @@ class WindowsResponse(MCPResponse):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
@mcp_for_unity_resource(
|
|
34
|
-
uri="
|
|
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(
|
|
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:")
|
|
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
|
-
|
services/tools/batch_execute.py
CHANGED
|
@@ -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
|
-
"
|
|
20
|
-
"
|
|
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,
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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}"}
|