mcpforunityserver 8.2.3__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.
- __init__.py +0 -0
- core/__init__.py +0 -0
- core/config.py +56 -0
- core/logging_decorator.py +37 -0
- core/telemetry.py +533 -0
- core/telemetry_decorator.py +164 -0
- main.py +411 -0
- mcpforunityserver-8.2.3.dist-info/METADATA +222 -0
- mcpforunityserver-8.2.3.dist-info/RECORD +65 -0
- mcpforunityserver-8.2.3.dist-info/WHEEL +5 -0
- mcpforunityserver-8.2.3.dist-info/entry_points.txt +2 -0
- mcpforunityserver-8.2.3.dist-info/licenses/LICENSE +21 -0
- mcpforunityserver-8.2.3.dist-info/top_level.txt +8 -0
- models/__init__.py +4 -0
- models/models.py +56 -0
- models/unity_response.py +47 -0
- routes/__init__.py +0 -0
- services/__init__.py +0 -0
- services/custom_tool_service.py +339 -0
- services/registry/__init__.py +22 -0
- services/registry/resource_registry.py +53 -0
- services/registry/tool_registry.py +51 -0
- services/resources/__init__.py +81 -0
- services/resources/active_tool.py +47 -0
- services/resources/custom_tools.py +57 -0
- services/resources/editor_state.py +42 -0
- services/resources/layers.py +29 -0
- services/resources/menu_items.py +34 -0
- services/resources/prefab_stage.py +39 -0
- services/resources/project_info.py +39 -0
- services/resources/selection.py +55 -0
- services/resources/tags.py +30 -0
- services/resources/tests.py +55 -0
- services/resources/unity_instances.py +122 -0
- services/resources/windows.py +47 -0
- services/tools/__init__.py +76 -0
- services/tools/batch_execute.py +78 -0
- services/tools/debug_request_context.py +71 -0
- services/tools/execute_custom_tool.py +38 -0
- services/tools/execute_menu_item.py +29 -0
- services/tools/find_in_file.py +174 -0
- services/tools/manage_asset.py +129 -0
- services/tools/manage_editor.py +63 -0
- services/tools/manage_gameobject.py +240 -0
- services/tools/manage_material.py +95 -0
- services/tools/manage_prefabs.py +62 -0
- services/tools/manage_scene.py +75 -0
- services/tools/manage_script.py +602 -0
- services/tools/manage_shader.py +64 -0
- services/tools/read_console.py +115 -0
- services/tools/run_tests.py +108 -0
- services/tools/script_apply_edits.py +998 -0
- services/tools/set_active_instance.py +112 -0
- services/tools/utils.py +60 -0
- transport/__init__.py +0 -0
- transport/legacy/port_discovery.py +329 -0
- transport/legacy/stdio_port_registry.py +65 -0
- transport/legacy/unity_connection.py +785 -0
- transport/models.py +62 -0
- transport/plugin_hub.py +412 -0
- transport/plugin_registry.py +123 -0
- transport/unity_instance_middleware.py +141 -0
- transport/unity_transport.py +103 -0
- utils/module_discovery.py +55 -0
- utils/reload_sentinel.py +9 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import math
|
|
3
|
+
from typing import Annotated, Any, Literal, Union
|
|
4
|
+
|
|
5
|
+
from fastmcp import Context
|
|
6
|
+
from services.registry import mcp_for_unity_tool
|
|
7
|
+
from services.tools import get_unity_instance_from_context
|
|
8
|
+
from transport.unity_transport import send_with_unity_instance
|
|
9
|
+
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
10
|
+
from services.tools.utils import coerce_bool, parse_json_payload
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp_for_unity_tool(
|
|
14
|
+
description="Performs CRUD operations on GameObjects and components."
|
|
15
|
+
)
|
|
16
|
+
async def manage_gameobject(
|
|
17
|
+
ctx: Context,
|
|
18
|
+
action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components", "get_component", "duplicate", "move_relative"], "Perform CRUD operations on GameObjects and components."] | None = None,
|
|
19
|
+
target: Annotated[str,
|
|
20
|
+
"GameObject identifier by name or path for modify/delete/component actions"] | None = None,
|
|
21
|
+
search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"],
|
|
22
|
+
"How to find objects. Used with 'find' and some 'target' lookups."] | None = None,
|
|
23
|
+
name: Annotated[str,
|
|
24
|
+
"GameObject name for 'create' (initial name) and 'modify' (rename) actions ONLY. For 'find' action, use 'search_term' instead."] | None = None,
|
|
25
|
+
tag: Annotated[str,
|
|
26
|
+
"Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None,
|
|
27
|
+
parent: Annotated[str,
|
|
28
|
+
"Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None,
|
|
29
|
+
position: Annotated[Union[list[float], str],
|
|
30
|
+
"Position - [x,y,z] or string '[x,y,z]' for client compatibility"] | None = None,
|
|
31
|
+
rotation: Annotated[Union[list[float], str],
|
|
32
|
+
"Rotation - [x,y,z] or string '[x,y,z]' for client compatibility"] | None = None,
|
|
33
|
+
scale: Annotated[Union[list[float], str],
|
|
34
|
+
"Scale - [x,y,z] or string '[x,y,z]' for client compatibility"] | None = None,
|
|
35
|
+
components_to_add: Annotated[list[str],
|
|
36
|
+
"List of component names to add"] | None = None,
|
|
37
|
+
primitive_type: Annotated[str,
|
|
38
|
+
"Primitive type for 'create' action"] | None = None,
|
|
39
|
+
save_as_prefab: Annotated[bool | str,
|
|
40
|
+
"If True, saves the created GameObject as a prefab (accepts true/false or 'true'/'false')"] | None = None,
|
|
41
|
+
prefab_path: Annotated[str, "Path for prefab creation"] | None = None,
|
|
42
|
+
prefab_folder: Annotated[str,
|
|
43
|
+
"Folder for prefab creation"] | None = None,
|
|
44
|
+
# --- Parameters for 'modify' ---
|
|
45
|
+
set_active: Annotated[bool | str,
|
|
46
|
+
"If True, sets the GameObject active (accepts true/false or 'true'/'false')"] | None = None,
|
|
47
|
+
layer: Annotated[str, "Layer name"] | None = None,
|
|
48
|
+
components_to_remove: Annotated[list[str],
|
|
49
|
+
"List of component names to remove"] | None = None,
|
|
50
|
+
component_properties: Annotated[Union[dict[str, dict[str, Any]], str],
|
|
51
|
+
"""Dictionary of component names to their properties to set. For example:
|
|
52
|
+
`{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject
|
|
53
|
+
`{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component
|
|
54
|
+
Example set nested property:
|
|
55
|
+
- Access shared material: `{"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}}`"""] | None = None,
|
|
56
|
+
# --- Parameters for 'find' ---
|
|
57
|
+
search_term: Annotated[str,
|
|
58
|
+
"Search term for 'find' action ONLY. Use this (not 'name') when searching for GameObjects."] | None = None,
|
|
59
|
+
find_all: Annotated[bool | str,
|
|
60
|
+
"If True, finds all GameObjects matching the search term (accepts true/false or 'true'/'false')"] | None = None,
|
|
61
|
+
search_in_children: Annotated[bool | str,
|
|
62
|
+
"If True, searches in children of the GameObject (accepts true/false or 'true'/'false')"] | None = None,
|
|
63
|
+
search_inactive: Annotated[bool | str,
|
|
64
|
+
"If True, searches inactive GameObjects (accepts true/false or 'true'/'false')"] | None = None,
|
|
65
|
+
# -- Component Management Arguments --
|
|
66
|
+
component_name: Annotated[str,
|
|
67
|
+
"Component name for 'add_component' and 'remove_component' actions"] | None = None,
|
|
68
|
+
# Controls whether serialization of private [SerializeField] fields is included
|
|
69
|
+
includeNonPublicSerialized: Annotated[bool | str,
|
|
70
|
+
"Controls whether serialization of private [SerializeField] fields is included (accepts true/false or 'true'/'false')"] | None = None,
|
|
71
|
+
# --- Parameters for 'duplicate' ---
|
|
72
|
+
new_name: Annotated[str,
|
|
73
|
+
"New name for the duplicated object (default: SourceName_Copy)"] | None = None,
|
|
74
|
+
offset: Annotated[Union[list[float], str],
|
|
75
|
+
"Offset from original/reference position - [x,y,z] or string '[x,y,z]'"] | None = None,
|
|
76
|
+
# --- Parameters for 'move_relative' ---
|
|
77
|
+
reference_object: Annotated[str,
|
|
78
|
+
"Reference object for relative movement (required for move_relative)"] | None = None,
|
|
79
|
+
direction: Annotated[Literal["left", "right", "up", "down", "forward", "back", "front", "backward", "behind"],
|
|
80
|
+
"Direction for relative movement (e.g., 'right', 'up', 'forward')"] | None = None,
|
|
81
|
+
distance: Annotated[float,
|
|
82
|
+
"Distance to move in the specified direction (default: 1.0)"] | None = None,
|
|
83
|
+
world_space: Annotated[bool | str,
|
|
84
|
+
"If True (default), use world space directions; if False, use reference object's local directions"] | None = None,
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
# Get active instance from session state
|
|
87
|
+
# Removed session_state import
|
|
88
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
89
|
+
|
|
90
|
+
if action is None:
|
|
91
|
+
return {
|
|
92
|
+
"success": False,
|
|
93
|
+
"message": "Missing required parameter 'action'. Valid actions: create, modify, delete, find, add_component, remove_component, set_component_property, get_components, get_component, duplicate, move_relative"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Coercers to tolerate stringified booleans and vectors
|
|
97
|
+
def _coerce_vec(value, default=None):
|
|
98
|
+
if value is None:
|
|
99
|
+
return default
|
|
100
|
+
|
|
101
|
+
# First try to parse if it's a string
|
|
102
|
+
val = parse_json_payload(value)
|
|
103
|
+
|
|
104
|
+
def _to_vec3(parts):
|
|
105
|
+
try:
|
|
106
|
+
vec = [float(parts[0]), float(parts[1]), float(parts[2])]
|
|
107
|
+
except (ValueError, TypeError):
|
|
108
|
+
return default
|
|
109
|
+
return vec if all(math.isfinite(n) for n in vec) else default
|
|
110
|
+
|
|
111
|
+
if isinstance(val, list) and len(val) == 3:
|
|
112
|
+
return _to_vec3(val)
|
|
113
|
+
|
|
114
|
+
# Handle legacy comma-separated strings "1,2,3" that parse_json_payload doesn't handle (since they aren't JSON arrays)
|
|
115
|
+
if isinstance(val, str):
|
|
116
|
+
s = val.strip()
|
|
117
|
+
# minimal tolerant parse for "[x,y,z]" or "x,y,z"
|
|
118
|
+
if s.startswith("[") and s.endswith("]"):
|
|
119
|
+
s = s[1:-1]
|
|
120
|
+
# support "x,y,z" and "x y z"
|
|
121
|
+
parts = [p.strip()
|
|
122
|
+
for p in (s.split(",") if "," in s else s.split())]
|
|
123
|
+
if len(parts) == 3:
|
|
124
|
+
return _to_vec3(parts)
|
|
125
|
+
return default
|
|
126
|
+
|
|
127
|
+
position = _coerce_vec(position, default=position)
|
|
128
|
+
rotation = _coerce_vec(rotation, default=rotation)
|
|
129
|
+
scale = _coerce_vec(scale, default=scale)
|
|
130
|
+
offset = _coerce_vec(offset, default=offset)
|
|
131
|
+
save_as_prefab = coerce_bool(save_as_prefab)
|
|
132
|
+
set_active = coerce_bool(set_active)
|
|
133
|
+
find_all = coerce_bool(find_all)
|
|
134
|
+
search_in_children = coerce_bool(search_in_children)
|
|
135
|
+
search_inactive = coerce_bool(search_inactive)
|
|
136
|
+
includeNonPublicSerialized = coerce_bool(includeNonPublicSerialized)
|
|
137
|
+
world_space = coerce_bool(world_space, default=True)
|
|
138
|
+
|
|
139
|
+
# Coerce 'component_properties' from JSON string to dict for client compatibility
|
|
140
|
+
component_properties = parse_json_payload(component_properties)
|
|
141
|
+
|
|
142
|
+
# Ensure final type is a dict (object) if provided
|
|
143
|
+
if component_properties is not None and not isinstance(component_properties, dict):
|
|
144
|
+
return {"success": False, "message": "component_properties must be a JSON object (dict)."}
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
# Map tag to search_term when search_method is by_tag for backward compatibility
|
|
148
|
+
if action == "find" and search_method == "by_tag" and tag is not None and search_term is None:
|
|
149
|
+
search_term = tag
|
|
150
|
+
|
|
151
|
+
# Validate parameter usage to prevent silent failures
|
|
152
|
+
if action == "find":
|
|
153
|
+
if name is not None:
|
|
154
|
+
return {
|
|
155
|
+
"success": False,
|
|
156
|
+
"message": "For 'find' action, use 'search_term' parameter, not 'name'. Remove 'name' parameter. Example: search_term='Player', search_method='by_name'"
|
|
157
|
+
}
|
|
158
|
+
if search_term is None:
|
|
159
|
+
return {
|
|
160
|
+
"success": False,
|
|
161
|
+
"message": "For 'find' action, 'search_term' parameter is required. Use search_term (not 'name') to specify what to find."
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if action in ["create", "modify"]:
|
|
165
|
+
if search_term is not None:
|
|
166
|
+
return {
|
|
167
|
+
"success": False,
|
|
168
|
+
"message": f"For '{action}' action, use 'name' parameter, not 'search_term'."
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Prepare parameters, removing None values
|
|
172
|
+
params = {
|
|
173
|
+
"action": action,
|
|
174
|
+
"target": target,
|
|
175
|
+
"searchMethod": search_method,
|
|
176
|
+
"name": name,
|
|
177
|
+
"tag": tag,
|
|
178
|
+
"parent": parent,
|
|
179
|
+
"position": position,
|
|
180
|
+
"rotation": rotation,
|
|
181
|
+
"scale": scale,
|
|
182
|
+
"componentsToAdd": components_to_add,
|
|
183
|
+
"primitiveType": primitive_type,
|
|
184
|
+
"saveAsPrefab": save_as_prefab,
|
|
185
|
+
"prefabPath": prefab_path,
|
|
186
|
+
"prefabFolder": prefab_folder,
|
|
187
|
+
"setActive": set_active,
|
|
188
|
+
"layer": layer,
|
|
189
|
+
"componentsToRemove": components_to_remove,
|
|
190
|
+
"componentProperties": component_properties,
|
|
191
|
+
"searchTerm": search_term,
|
|
192
|
+
"findAll": find_all,
|
|
193
|
+
"searchInChildren": search_in_children,
|
|
194
|
+
"searchInactive": search_inactive,
|
|
195
|
+
"componentName": component_name,
|
|
196
|
+
"includeNonPublicSerialized": includeNonPublicSerialized,
|
|
197
|
+
# Parameters for 'duplicate'
|
|
198
|
+
"new_name": new_name,
|
|
199
|
+
"offset": offset,
|
|
200
|
+
# Parameters for 'move_relative'
|
|
201
|
+
"reference_object": reference_object,
|
|
202
|
+
"direction": direction,
|
|
203
|
+
"distance": distance,
|
|
204
|
+
"world_space": world_space,
|
|
205
|
+
}
|
|
206
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
207
|
+
|
|
208
|
+
# --- Handle Prefab Path Logic ---
|
|
209
|
+
# Check if 'saveAsPrefab' is explicitly True in params
|
|
210
|
+
if action == "create" and params.get("saveAsPrefab"):
|
|
211
|
+
if "prefabPath" not in params:
|
|
212
|
+
if "name" not in params or not params["name"]:
|
|
213
|
+
return {"success": False, "message": "Cannot create default prefab path: 'name' parameter is missing."}
|
|
214
|
+
# Use the provided prefab_folder (which has a default) and the name to construct the path
|
|
215
|
+
constructed_path = f"{prefab_folder}/{params['name']}.prefab"
|
|
216
|
+
# Ensure clean path separators (Unity prefers '/')
|
|
217
|
+
params["prefabPath"] = constructed_path.replace("\\", "/")
|
|
218
|
+
elif not params["prefabPath"].lower().endswith(".prefab"):
|
|
219
|
+
return {"success": False, "message": f"Invalid prefab_path: '{params['prefabPath']}' must end with .prefab"}
|
|
220
|
+
# Ensure prefabFolder itself isn't sent if prefabPath was constructed or provided
|
|
221
|
+
# The C# side only needs the final prefabPath
|
|
222
|
+
params.pop("prefabFolder", None)
|
|
223
|
+
# --------------------------------
|
|
224
|
+
|
|
225
|
+
# Use centralized retry helper with instance routing
|
|
226
|
+
response = await send_with_unity_instance(
|
|
227
|
+
async_send_command_with_retry,
|
|
228
|
+
unity_instance,
|
|
229
|
+
"manage_gameobject",
|
|
230
|
+
params,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Check if the response indicates success
|
|
234
|
+
# If the response is not successful, raise an exception with the error message
|
|
235
|
+
if isinstance(response, dict) and response.get("success"):
|
|
236
|
+
return {"success": True, "message": response.get("message", "GameObject operation successful."), "data": response.get("data")}
|
|
237
|
+
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
return {"success": False, "message": f"Python error managing GameObject: {e!s}"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Defines the manage_material tool for interacting with Unity materials.
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
from typing import Annotated, Any, Literal, Union
|
|
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 services.tools.utils import parse_json_payload
|
|
11
|
+
from transport.unity_transport import send_with_unity_instance
|
|
12
|
+
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@mcp_for_unity_tool(
|
|
16
|
+
description="Manages Unity materials (set properties, colors, shaders, etc)."
|
|
17
|
+
)
|
|
18
|
+
async def manage_material(
|
|
19
|
+
ctx: Context,
|
|
20
|
+
action: Annotated[Literal[
|
|
21
|
+
"ping",
|
|
22
|
+
"create",
|
|
23
|
+
"set_material_shader_property",
|
|
24
|
+
"set_material_color",
|
|
25
|
+
"assign_material_to_renderer",
|
|
26
|
+
"set_renderer_color",
|
|
27
|
+
"get_material_info"
|
|
28
|
+
], "Action to perform."],
|
|
29
|
+
|
|
30
|
+
# Common / Shared
|
|
31
|
+
material_path: Annotated[str, "Path to material asset (Assets/...)"] | None = None,
|
|
32
|
+
property: Annotated[str, "Shader property name (e.g., _BaseColor, _MainTex)"] | None = None,
|
|
33
|
+
|
|
34
|
+
# create
|
|
35
|
+
shader: Annotated[str, "Shader name (default: Standard)"] | None = None,
|
|
36
|
+
properties: Annotated[Union[dict[str, Any], str], "Initial properties to set {name: value}."] | None = None,
|
|
37
|
+
|
|
38
|
+
# set_material_shader_property
|
|
39
|
+
value: Annotated[Union[list, float, int, str, bool, None], "Value to set (color array, float, texture path/instruction)"] | None = None,
|
|
40
|
+
|
|
41
|
+
# set_material_color / set_renderer_color
|
|
42
|
+
color: Annotated[Union[list[float], list[int], str], "Color as [r,g,b] or [r,g,b,a]."] | None = None,
|
|
43
|
+
|
|
44
|
+
# assign_material_to_renderer / set_renderer_color
|
|
45
|
+
target: Annotated[str, "Target GameObject (name, path, or find instruction)"] | None = None,
|
|
46
|
+
search_method: Annotated[Literal["by_name", "by_path", "by_tag", "by_layer", "by_component"], "Search method for target"] | None = None,
|
|
47
|
+
slot: Annotated[int | str, "Material slot index"] | None = None,
|
|
48
|
+
mode: Annotated[Literal["shared", "instance", "property_block"], "Assignment/modification mode"] | None = None,
|
|
49
|
+
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
52
|
+
|
|
53
|
+
# Parse inputs that might be stringified JSON
|
|
54
|
+
color = parse_json_payload(color)
|
|
55
|
+
properties = parse_json_payload(properties)
|
|
56
|
+
value = parse_json_payload(value)
|
|
57
|
+
|
|
58
|
+
# Coerce slot to int if it's a string
|
|
59
|
+
if slot is not None:
|
|
60
|
+
if isinstance(slot, str):
|
|
61
|
+
try:
|
|
62
|
+
slot = int(slot)
|
|
63
|
+
except ValueError:
|
|
64
|
+
return {
|
|
65
|
+
"success": False,
|
|
66
|
+
"message": f"Invalid slot value: '{slot}' must be a valid integer"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Prepare parameters for the C# handler
|
|
70
|
+
params_dict = {
|
|
71
|
+
"action": action.lower(),
|
|
72
|
+
"materialPath": material_path,
|
|
73
|
+
"shader": shader,
|
|
74
|
+
"properties": properties,
|
|
75
|
+
"property": property,
|
|
76
|
+
"value": value,
|
|
77
|
+
"color": color,
|
|
78
|
+
"target": target,
|
|
79
|
+
"searchMethod": search_method,
|
|
80
|
+
"slot": slot,
|
|
81
|
+
"mode": mode
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Remove None values
|
|
85
|
+
params_dict = {k: v for k, v in params_dict.items() if v is not None}
|
|
86
|
+
|
|
87
|
+
# Use centralized async retry helper with instance routing
|
|
88
|
+
result = await send_with_unity_instance(
|
|
89
|
+
async_send_command_with_retry,
|
|
90
|
+
unity_instance,
|
|
91
|
+
"manage_material",
|
|
92
|
+
params_dict,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return result if isinstance(result, dict) else {"success": False, "message": str(result)}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Annotated, Any, Literal
|
|
2
|
+
|
|
3
|
+
from fastmcp import Context
|
|
4
|
+
from services.registry import mcp_for_unity_tool
|
|
5
|
+
from services.tools import get_unity_instance_from_context
|
|
6
|
+
from transport.unity_transport import send_with_unity_instance
|
|
7
|
+
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
8
|
+
from services.tools.utils import coerce_bool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@mcp_for_unity_tool(
|
|
12
|
+
description="Performs prefab operations (open_stage, close_stage, save_open_stage, create_from_gameobject)."
|
|
13
|
+
)
|
|
14
|
+
async def manage_prefabs(
|
|
15
|
+
ctx: Context,
|
|
16
|
+
action: Annotated[Literal["open_stage", "close_stage", "save_open_stage", "create_from_gameobject"], "Perform prefab operations."],
|
|
17
|
+
prefab_path: Annotated[str,
|
|
18
|
+
"Prefab asset path relative to Assets e.g. Assets/Prefabs/favorite.prefab"] | None = None,
|
|
19
|
+
mode: Annotated[str,
|
|
20
|
+
"Optional prefab stage mode (only 'InIsolation' is currently supported)"] | None = None,
|
|
21
|
+
save_before_close: Annotated[bool,
|
|
22
|
+
"When true, `close_stage` will save the prefab before exiting the stage."] | None = None,
|
|
23
|
+
target: Annotated[str,
|
|
24
|
+
"Scene GameObject name required for create_from_gameobject"] | None = None,
|
|
25
|
+
allow_overwrite: Annotated[bool,
|
|
26
|
+
"Allow replacing an existing prefab at the same path"] | None = None,
|
|
27
|
+
search_inactive: Annotated[bool,
|
|
28
|
+
"Include inactive objects when resolving the target name"] | None = None,
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
# Get active instance from session state
|
|
31
|
+
# Removed session_state import
|
|
32
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
params: dict[str, Any] = {"action": action}
|
|
36
|
+
|
|
37
|
+
if prefab_path:
|
|
38
|
+
params["prefabPath"] = prefab_path
|
|
39
|
+
if mode:
|
|
40
|
+
params["mode"] = mode
|
|
41
|
+
save_before_close_val = coerce_bool(save_before_close)
|
|
42
|
+
if save_before_close_val is not None:
|
|
43
|
+
params["saveBeforeClose"] = save_before_close_val
|
|
44
|
+
if target:
|
|
45
|
+
params["target"] = target
|
|
46
|
+
allow_overwrite_val = coerce_bool(allow_overwrite)
|
|
47
|
+
if allow_overwrite_val is not None:
|
|
48
|
+
params["allowOverwrite"] = allow_overwrite_val
|
|
49
|
+
search_inactive_val = coerce_bool(search_inactive)
|
|
50
|
+
if search_inactive_val is not None:
|
|
51
|
+
params["searchInactive"] = search_inactive_val
|
|
52
|
+
response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_prefabs", params)
|
|
53
|
+
|
|
54
|
+
if isinstance(response, dict) and response.get("success"):
|
|
55
|
+
return {
|
|
56
|
+
"success": True,
|
|
57
|
+
"message": response.get("message", "Prefab operation successful."),
|
|
58
|
+
"data": response.get("data"),
|
|
59
|
+
}
|
|
60
|
+
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
return {"success": False, "message": f"Python error managing prefabs: {exc}"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import Annotated, Literal, Any
|
|
2
|
+
|
|
3
|
+
from fastmcp import Context
|
|
4
|
+
from services.registry import mcp_for_unity_tool
|
|
5
|
+
from services.tools import get_unity_instance_from_context
|
|
6
|
+
from transport.unity_transport import send_with_unity_instance
|
|
7
|
+
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp_for_unity_tool(
|
|
11
|
+
description="Performs CRUD operations on Unity scenes."
|
|
12
|
+
)
|
|
13
|
+
async def manage_scene(
|
|
14
|
+
ctx: Context,
|
|
15
|
+
action: Annotated[Literal[
|
|
16
|
+
"create",
|
|
17
|
+
"load",
|
|
18
|
+
"save",
|
|
19
|
+
"get_hierarchy",
|
|
20
|
+
"get_active",
|
|
21
|
+
"get_build_settings",
|
|
22
|
+
"screenshot",
|
|
23
|
+
], "Perform CRUD operations on Unity scenes, and capture a screenshot."],
|
|
24
|
+
name: Annotated[str, "Scene name."] | None = None,
|
|
25
|
+
path: Annotated[str, "Scene path."] | None = None,
|
|
26
|
+
build_index: Annotated[int | str,
|
|
27
|
+
"Unity build index (quote as string, e.g., '0')."] | None = None,
|
|
28
|
+
screenshot_file_name: Annotated[str, "Screenshot file name (optional). Defaults to timestamp when omitted."] | None = None,
|
|
29
|
+
screenshot_super_size: Annotated[int | str, "Screenshot supersize multiplier (integer ≥1). Optional." ] | None = None,
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
# Get active instance from session state
|
|
32
|
+
# Removed session_state import
|
|
33
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
34
|
+
try:
|
|
35
|
+
# Coerce numeric inputs defensively
|
|
36
|
+
def _coerce_int(value, default=None):
|
|
37
|
+
if value is None:
|
|
38
|
+
return default
|
|
39
|
+
try:
|
|
40
|
+
if isinstance(value, bool):
|
|
41
|
+
return default
|
|
42
|
+
if isinstance(value, int):
|
|
43
|
+
return int(value)
|
|
44
|
+
s = str(value).strip()
|
|
45
|
+
if s.lower() in ("", "none", "null"):
|
|
46
|
+
return default
|
|
47
|
+
return int(float(s))
|
|
48
|
+
except Exception:
|
|
49
|
+
return default
|
|
50
|
+
|
|
51
|
+
coerced_build_index = _coerce_int(build_index, default=None)
|
|
52
|
+
coerced_super_size = _coerce_int(screenshot_super_size, default=None)
|
|
53
|
+
|
|
54
|
+
params: dict[str, Any] = {"action": action}
|
|
55
|
+
if name:
|
|
56
|
+
params["name"] = name
|
|
57
|
+
if path:
|
|
58
|
+
params["path"] = path
|
|
59
|
+
if coerced_build_index is not None:
|
|
60
|
+
params["buildIndex"] = coerced_build_index
|
|
61
|
+
if screenshot_file_name:
|
|
62
|
+
params["fileName"] = screenshot_file_name
|
|
63
|
+
if coerced_super_size is not None:
|
|
64
|
+
params["superSize"] = coerced_super_size
|
|
65
|
+
|
|
66
|
+
# Use centralized retry helper with instance routing
|
|
67
|
+
response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_scene", params)
|
|
68
|
+
|
|
69
|
+
# Preserve structured failure data; unwrap success into a friendlier shape
|
|
70
|
+
if isinstance(response, dict) and response.get("success"):
|
|
71
|
+
return {"success": True, "message": response.get("message", "Scene operation successful."), "data": response.get("data")}
|
|
72
|
+
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return {"success": False, "message": f"Python error managing scene: {str(e)}"}
|