mcpforunityserver 9.3.0__py3-none-any.whl → 9.3.0b20260128055651__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.
- cli/commands/editor.py +27 -1
- cli/commands/prefab.py +134 -12
- cli/commands/texture.py +538 -0
- cli/commands/tool.py +61 -0
- cli/commands/vfx.py +51 -15
- cli/main.py +33 -0
- cli/utils/connection.py +37 -0
- cli/utils/suggestions.py +34 -0
- main.py +125 -6
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/METADATA +2 -2
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/RECORD +22 -17
- services/resources/prefab.py +191 -0
- services/tools/manage_components.py +1 -1
- services/tools/manage_gameobject.py +43 -23
- services/tools/manage_material.py +2 -2
- services/tools/manage_prefabs.py +128 -31
- services/tools/manage_texture.py +667 -0
- services/tools/manage_vfx.py +15 -633
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/WHEEL +0 -0
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/entry_points.txt +0 -0
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/licenses/LICENSE +0 -0
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Resources for reading Prefab data from Unity.
|
|
3
|
+
|
|
4
|
+
These resources provide read-only access to:
|
|
5
|
+
- Prefab info by asset path (mcpforunity://prefab/{path})
|
|
6
|
+
- Prefab hierarchy by asset path (mcpforunity://prefab/{path}/hierarchy)
|
|
7
|
+
- Currently open prefab stage (mcpforunity://editor/prefab-stage - see prefab_stage.py)
|
|
8
|
+
"""
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.parse import unquote
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
from fastmcp import Context
|
|
13
|
+
|
|
14
|
+
from models import MCPResponse
|
|
15
|
+
from services.registry import mcp_for_unity_resource
|
|
16
|
+
from services.tools import get_unity_instance_from_context
|
|
17
|
+
from transport.unity_transport import send_with_unity_instance
|
|
18
|
+
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _normalize_response(response: dict | MCPResponse | Any) -> MCPResponse:
|
|
22
|
+
"""Normalize Unity transport response to MCPResponse."""
|
|
23
|
+
if isinstance(response, dict):
|
|
24
|
+
return MCPResponse(**response)
|
|
25
|
+
if isinstance(response, MCPResponse):
|
|
26
|
+
return response
|
|
27
|
+
# Fallback: wrap unexpected types in an error response
|
|
28
|
+
return MCPResponse(success=False, error=f"Unexpected response type: {type(response).__name__}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _decode_prefab_path(encoded_path: str) -> str:
|
|
32
|
+
"""
|
|
33
|
+
Decode a URL-encoded prefab path.
|
|
34
|
+
Handles paths like 'Assets%2FPrefabs%2FMyPrefab.prefab' -> 'Assets/Prefabs/MyPrefab.prefab'
|
|
35
|
+
"""
|
|
36
|
+
return unquote(encoded_path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# Static Helper Resource (shows in UI)
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
@mcp_for_unity_resource(
|
|
44
|
+
uri="mcpforunity://prefab-api",
|
|
45
|
+
name="prefab_api",
|
|
46
|
+
description="Documentation for Prefab resources. Use manage_asset action=search filterType=Prefab to find prefabs, then access resources below."
|
|
47
|
+
)
|
|
48
|
+
async def get_prefab_api_docs(_ctx: Context) -> MCPResponse:
|
|
49
|
+
"""
|
|
50
|
+
Returns documentation for the Prefab resource API.
|
|
51
|
+
|
|
52
|
+
This is a helper resource that explains how to use the parameterized
|
|
53
|
+
Prefab resources which require an asset path.
|
|
54
|
+
"""
|
|
55
|
+
docs = {
|
|
56
|
+
"overview": "Prefab resources provide read-only access to Unity prefab assets.",
|
|
57
|
+
"workflow": [
|
|
58
|
+
"1. Use manage_asset action=search filterType=Prefab to find prefabs",
|
|
59
|
+
"2. Use the asset path to access detailed data via resources below",
|
|
60
|
+
"3. Use manage_prefabs tool for prefab stage operations (open, save, close)"
|
|
61
|
+
],
|
|
62
|
+
"path_encoding": {
|
|
63
|
+
"note": "Prefab paths must be URL-encoded when used in resource URIs",
|
|
64
|
+
"example": "Assets/Prefabs/MyPrefab.prefab -> Assets%2FPrefabs%2FMyPrefab.prefab"
|
|
65
|
+
},
|
|
66
|
+
"resources": {
|
|
67
|
+
"mcpforunity://prefab/{encoded_path}": {
|
|
68
|
+
"description": "Get prefab asset info (type, root name, components, variant info)",
|
|
69
|
+
"example": "mcpforunity://prefab/Assets%2FPrefabs%2FPlayer.prefab",
|
|
70
|
+
"returns": ["assetPath", "guid", "prefabType", "rootObjectName", "rootComponentTypes", "childCount", "isVariant", "parentPrefab"]
|
|
71
|
+
},
|
|
72
|
+
"mcpforunity://prefab/{encoded_path}/hierarchy": {
|
|
73
|
+
"description": "Get full prefab hierarchy with nested prefab information",
|
|
74
|
+
"example": "mcpforunity://prefab/Assets%2FPrefabs%2FPlayer.prefab/hierarchy",
|
|
75
|
+
"returns": ["prefabPath", "total", "items (with name, instanceId, path, componentTypes, prefab nesting info)"]
|
|
76
|
+
},
|
|
77
|
+
"mcpforunity://editor/prefab-stage": {
|
|
78
|
+
"description": "Get info about the currently open prefab stage (if any)",
|
|
79
|
+
"returns": ["isOpen", "assetPath", "prefabRootName", "mode", "isDirty"]
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"related_tools": {
|
|
83
|
+
"manage_prefabs": "Open/close prefab stages, save changes, create prefabs from GameObjects",
|
|
84
|
+
"manage_asset": "Search for prefab assets, get asset info",
|
|
85
|
+
"manage_gameobject": "Modify GameObjects in open prefab stage",
|
|
86
|
+
"manage_components": "Add/remove/modify components on prefab GameObjects"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return MCPResponse(success=True, data=docs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# =============================================================================
|
|
93
|
+
# Prefab Info Resource
|
|
94
|
+
# =============================================================================
|
|
95
|
+
|
|
96
|
+
# TODO: Use these typed response classes for better type safety once
|
|
97
|
+
# we update the endpoints to validate response structure more strictly.
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class PrefabInfoData(BaseModel):
|
|
101
|
+
"""Data for a prefab asset."""
|
|
102
|
+
assetPath: str
|
|
103
|
+
guid: str = ""
|
|
104
|
+
prefabType: str = "Regular"
|
|
105
|
+
rootObjectName: str = ""
|
|
106
|
+
rootComponentTypes: list[str] = []
|
|
107
|
+
childCount: int = 0
|
|
108
|
+
isVariant: bool = False
|
|
109
|
+
parentPrefab: str | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PrefabInfoResponse(MCPResponse):
|
|
113
|
+
"""Response containing prefab info data."""
|
|
114
|
+
data: PrefabInfoData | None = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@mcp_for_unity_resource(
|
|
118
|
+
uri="mcpforunity://prefab/{encoded_path}",
|
|
119
|
+
name="prefab_info",
|
|
120
|
+
description="Get detailed information about a prefab asset by URL-encoded path. Returns prefab type, root object name, component types, child count, and variant info."
|
|
121
|
+
)
|
|
122
|
+
async def get_prefab_info(ctx: Context, encoded_path: str) -> MCPResponse:
|
|
123
|
+
"""Get prefab asset info by path."""
|
|
124
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
125
|
+
|
|
126
|
+
# Decode the URL-encoded path
|
|
127
|
+
decoded_path = _decode_prefab_path(encoded_path)
|
|
128
|
+
|
|
129
|
+
response = await send_with_unity_instance(
|
|
130
|
+
async_send_command_with_retry,
|
|
131
|
+
unity_instance,
|
|
132
|
+
"manage_prefabs",
|
|
133
|
+
{
|
|
134
|
+
"action": "get_info",
|
|
135
|
+
"prefabPath": decoded_path
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return _normalize_response(response)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# =============================================================================
|
|
143
|
+
# Prefab Hierarchy Resource
|
|
144
|
+
# =============================================================================
|
|
145
|
+
|
|
146
|
+
class PrefabHierarchyItem(BaseModel):
|
|
147
|
+
"""Single item in prefab hierarchy."""
|
|
148
|
+
name: str
|
|
149
|
+
instanceId: int
|
|
150
|
+
path: str
|
|
151
|
+
activeSelf: bool = True
|
|
152
|
+
childCount: int = 0
|
|
153
|
+
componentTypes: list[str] = []
|
|
154
|
+
prefab: dict[str, Any] = {}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class PrefabHierarchyData(BaseModel):
|
|
158
|
+
"""Data for prefab hierarchy."""
|
|
159
|
+
prefabPath: str
|
|
160
|
+
total: int = 0
|
|
161
|
+
items: list[PrefabHierarchyItem] = []
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class PrefabHierarchyResponse(MCPResponse):
|
|
165
|
+
"""Response containing prefab hierarchy data."""
|
|
166
|
+
data: PrefabHierarchyData | None = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@mcp_for_unity_resource(
|
|
170
|
+
uri="mcpforunity://prefab/{encoded_path}/hierarchy",
|
|
171
|
+
name="prefab_hierarchy",
|
|
172
|
+
description="Get the full hierarchy of a prefab with nested prefab information. Returns all GameObjects with their components and nesting depth."
|
|
173
|
+
)
|
|
174
|
+
async def get_prefab_hierarchy(ctx: Context, encoded_path: str) -> MCPResponse:
|
|
175
|
+
"""Get prefab hierarchy by path."""
|
|
176
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
177
|
+
|
|
178
|
+
# Decode the URL-encoded path
|
|
179
|
+
decoded_path = _decode_prefab_path(encoded_path)
|
|
180
|
+
|
|
181
|
+
response = await send_with_unity_instance(
|
|
182
|
+
async_send_command_with_retry,
|
|
183
|
+
unity_instance,
|
|
184
|
+
"manage_prefabs",
|
|
185
|
+
{
|
|
186
|
+
"action": "get_hierarchy",
|
|
187
|
+
"prefabPath": decoded_path
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return _normalize_response(response)
|
|
@@ -37,7 +37,7 @@ async def manage_components(
|
|
|
37
37
|
# For set_property action - single property
|
|
38
38
|
property: Annotated[str,
|
|
39
39
|
"Property name to set (for set_property action)"] | None = None,
|
|
40
|
-
value: Annotated[
|
|
40
|
+
value: Annotated[str | int | float | bool | dict | list ,
|
|
41
41
|
"Value to set (for set_property action)"] | None = None,
|
|
42
42
|
# For add/set_property - multiple properties
|
|
43
43
|
properties: Annotated[
|
|
@@ -13,32 +13,40 @@ from services.tools.utils import coerce_bool, parse_json_payload, coerce_int
|
|
|
13
13
|
from services.tools.preflight import preflight
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def _normalize_vector(value: Any,
|
|
16
|
+
def _normalize_vector(value: Any, param_name: str = "vector") -> tuple[list[float] | None, str | None]:
|
|
17
17
|
"""
|
|
18
18
|
Robustly normalize a vector parameter to [x, y, z] format.
|
|
19
19
|
Handles: list, tuple, JSON string, comma-separated string.
|
|
20
|
-
Returns
|
|
20
|
+
Returns (parsed_vector, error_message). If error_message is set, parsed_vector is None.
|
|
21
21
|
"""
|
|
22
22
|
if value is None:
|
|
23
|
-
return
|
|
23
|
+
return None, None
|
|
24
24
|
|
|
25
25
|
# If already a list/tuple with 3 elements, convert to floats
|
|
26
26
|
if isinstance(value, (list, tuple)) and len(value) == 3:
|
|
27
27
|
try:
|
|
28
28
|
vec = [float(value[0]), float(value[1]), float(value[2])]
|
|
29
|
-
|
|
29
|
+
if all(math.isfinite(n) for n in vec):
|
|
30
|
+
return vec, None
|
|
31
|
+
return None, f"{param_name} values must be finite numbers, got {value}"
|
|
30
32
|
except (ValueError, TypeError):
|
|
31
|
-
return
|
|
33
|
+
return None, f"{param_name} values must be numbers, got {value}"
|
|
32
34
|
|
|
33
35
|
# Try parsing as JSON string
|
|
34
36
|
if isinstance(value, str):
|
|
37
|
+
# Check for obviously invalid values
|
|
38
|
+
if value in ("[object Object]", "undefined", "null", ""):
|
|
39
|
+
return None, f"{param_name} received invalid value: '{value}'. Expected [x, y, z] array (list or JSON string)"
|
|
40
|
+
|
|
35
41
|
parsed = parse_json_payload(value)
|
|
36
42
|
if isinstance(parsed, list) and len(parsed) == 3:
|
|
37
43
|
try:
|
|
38
44
|
vec = [float(parsed[0]), float(parsed[1]), float(parsed[2])]
|
|
39
|
-
|
|
45
|
+
if all(math.isfinite(n) for n in vec):
|
|
46
|
+
return vec, None
|
|
47
|
+
return None, f"{param_name} values must be finite numbers, got {parsed}"
|
|
40
48
|
except (ValueError, TypeError):
|
|
41
|
-
|
|
49
|
+
return None, f"{param_name} values must be numbers, got {parsed}"
|
|
42
50
|
|
|
43
51
|
# Handle legacy comma-separated strings "1,2,3" or "[1,2,3]"
|
|
44
52
|
s = value.strip()
|
|
@@ -48,11 +56,15 @@ def _normalize_vector(value: Any, default: Any = None) -> list[float] | None:
|
|
|
48
56
|
if len(parts) == 3:
|
|
49
57
|
try:
|
|
50
58
|
vec = [float(parts[0]), float(parts[1]), float(parts[2])]
|
|
51
|
-
|
|
59
|
+
if all(math.isfinite(n) for n in vec):
|
|
60
|
+
return vec, None
|
|
61
|
+
return None, f"{param_name} values must be finite numbers, got {value}"
|
|
52
62
|
except (ValueError, TypeError):
|
|
53
|
-
|
|
63
|
+
return None, f"{param_name} values must be numbers, got {value}"
|
|
64
|
+
|
|
65
|
+
return None, f"{param_name} must be a [x, y, z] array (list or JSON string), got: {value}"
|
|
54
66
|
|
|
55
|
-
return
|
|
67
|
+
return None, f"{param_name} must be a list or JSON string, got {type(value).__name__}"
|
|
56
68
|
|
|
57
69
|
|
|
58
70
|
def _normalize_component_properties(value: Any) -> tuple[dict[str, dict[str, Any]] | None, str | None]:
|
|
@@ -103,12 +115,12 @@ async def manage_gameobject(
|
|
|
103
115
|
"Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None,
|
|
104
116
|
parent: Annotated[str,
|
|
105
117
|
"Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None,
|
|
106
|
-
position: Annotated[list[float],
|
|
107
|
-
"Position as [x, y, z] array"] | None = None,
|
|
108
|
-
rotation: Annotated[list[float],
|
|
109
|
-
"Rotation as [x, y, z] euler angles array"] | None = None,
|
|
110
|
-
scale: Annotated[list[float],
|
|
111
|
-
"Scale as [x, y, z] array"] | None = None,
|
|
118
|
+
position: Annotated[list[float] | str,
|
|
119
|
+
"Position as [x, y, z] array (list or JSON string)"] | None = None,
|
|
120
|
+
rotation: Annotated[list[float] | str,
|
|
121
|
+
"Rotation as [x, y, z] euler angles array (list or JSON string)"] | None = None,
|
|
122
|
+
scale: Annotated[list[float] | str,
|
|
123
|
+
"Scale as [x, y, z] array (list or JSON string)"] | None = None,
|
|
112
124
|
components_to_add: Annotated[list[str],
|
|
113
125
|
"List of component names to add"] | None = None,
|
|
114
126
|
primitive_type: Annotated[str,
|
|
@@ -157,8 +169,8 @@ async def manage_gameobject(
|
|
|
157
169
|
# --- Parameters for 'duplicate' ---
|
|
158
170
|
new_name: Annotated[str,
|
|
159
171
|
"New name for the duplicated object (default: SourceName_Copy)"] | None = None,
|
|
160
|
-
offset: Annotated[list[float],
|
|
161
|
-
"Offset from original/reference position as [x, y, z] array"] | None = None,
|
|
172
|
+
offset: Annotated[list[float] | str,
|
|
173
|
+
"Offset from original/reference position as [x, y, z] array (list or JSON string)"] | None = None,
|
|
162
174
|
# --- Parameters for 'move_relative' ---
|
|
163
175
|
reference_object: Annotated[str,
|
|
164
176
|
"Reference object for relative movement (required for move_relative)"] | None = None,
|
|
@@ -183,11 +195,19 @@ async def manage_gameobject(
|
|
|
183
195
|
"message": "Missing required parameter 'action'. Valid actions: create, modify, delete, duplicate, move_relative. For finding GameObjects use find_gameobjects tool. For component operations use manage_components tool."
|
|
184
196
|
}
|
|
185
197
|
|
|
186
|
-
# --- Normalize vector parameters
|
|
187
|
-
position = _normalize_vector(position)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
# --- Normalize vector parameters with detailed error handling ---
|
|
199
|
+
position, position_error = _normalize_vector(position, "position")
|
|
200
|
+
if position_error:
|
|
201
|
+
return {"success": False, "message": position_error}
|
|
202
|
+
rotation, rotation_error = _normalize_vector(rotation, "rotation")
|
|
203
|
+
if rotation_error:
|
|
204
|
+
return {"success": False, "message": rotation_error}
|
|
205
|
+
scale, scale_error = _normalize_vector(scale, "scale")
|
|
206
|
+
if scale_error:
|
|
207
|
+
return {"success": False, "message": scale_error}
|
|
208
|
+
offset, offset_error = _normalize_vector(offset, "offset")
|
|
209
|
+
if offset_error:
|
|
210
|
+
return {"success": False, "message": offset_error}
|
|
191
211
|
|
|
192
212
|
# --- Normalize boolean parameters ---
|
|
193
213
|
save_as_prefab = coerce_bool(save_as_prefab)
|
|
@@ -82,8 +82,8 @@ async def manage_material(
|
|
|
82
82
|
"Value to set (color array, float, texture path/instruction)"] | None = None,
|
|
83
83
|
|
|
84
84
|
# set_material_color / set_renderer_color
|
|
85
|
-
color: Annotated[list[float],
|
|
86
|
-
"Color as [r, g, b] or [r, g, b, a] array."] | None = None,
|
|
85
|
+
color: Annotated[list[float] | str,
|
|
86
|
+
"Color as [r, g, b] or [r, g, b, a] array (list or JSON string)."] | None = None,
|
|
87
87
|
|
|
88
88
|
# assign_material_to_renderer / set_renderer_color
|
|
89
89
|
target: Annotated[str,
|
services/tools/manage_prefabs.py
CHANGED
|
@@ -5,13 +5,28 @@ from mcp.types import ToolAnnotations
|
|
|
5
5
|
|
|
6
6
|
from services.registry import mcp_for_unity_tool
|
|
7
7
|
from services.tools import get_unity_instance_from_context
|
|
8
|
+
from services.tools.utils import coerce_bool
|
|
8
9
|
from transport.unity_transport import send_with_unity_instance
|
|
9
10
|
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
10
|
-
from services.tools.
|
|
11
|
+
from services.tools.preflight import preflight
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Required parameters for each action
|
|
15
|
+
REQUIRED_PARAMS = {
|
|
16
|
+
"get_info": ["prefab_path"],
|
|
17
|
+
"get_hierarchy": ["prefab_path"],
|
|
18
|
+
"create_from_gameobject": ["target", "prefab_path"],
|
|
19
|
+
"modify_contents": ["prefab_path"],
|
|
20
|
+
}
|
|
11
21
|
|
|
12
22
|
|
|
13
23
|
@mcp_for_unity_tool(
|
|
14
|
-
description=
|
|
24
|
+
description=(
|
|
25
|
+
"Manages Unity Prefab assets via headless operations (no UI, no prefab stages). "
|
|
26
|
+
"Actions: get_info, get_hierarchy, create_from_gameobject, modify_contents. "
|
|
27
|
+
"Use modify_contents for headless prefab editing - ideal for automated workflows. "
|
|
28
|
+
"Use manage_asset action=search filterType=Prefab to list prefabs."
|
|
29
|
+
),
|
|
15
30
|
annotations=ToolAnnotations(
|
|
16
31
|
title="Manage Prefabs",
|
|
17
32
|
destructiveHint=True,
|
|
@@ -19,50 +34,132 @@ from services.tools.utils import coerce_bool
|
|
|
19
34
|
)
|
|
20
35
|
async def manage_prefabs(
|
|
21
36
|
ctx: Context,
|
|
22
|
-
action: Annotated[
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
action: Annotated[
|
|
38
|
+
Literal[
|
|
39
|
+
"create_from_gameobject",
|
|
40
|
+
"get_info",
|
|
41
|
+
"get_hierarchy",
|
|
42
|
+
"modify_contents",
|
|
43
|
+
],
|
|
44
|
+
"Prefab operation to perform.",
|
|
45
|
+
],
|
|
46
|
+
prefab_path: Annotated[str, "Prefab asset path (e.g., Assets/Prefabs/MyPrefab.prefab)."] | None = None,
|
|
47
|
+
target: Annotated[str, "Target GameObject: scene object for create_from_gameobject, or object within prefab for modify_contents (name or path like 'Parent/Child')."] | None = None,
|
|
48
|
+
allow_overwrite: Annotated[bool, "Allow replacing existing prefab."] | None = None,
|
|
49
|
+
search_inactive: Annotated[bool, "Include inactive GameObjects in search."] | None = None,
|
|
50
|
+
unlink_if_instance: Annotated[bool, "Unlink from existing prefab before creating new one."] | None = None,
|
|
51
|
+
# modify_contents parameters
|
|
52
|
+
position: Annotated[list[float], "New local position [x, y, z] for modify_contents."] | None = None,
|
|
53
|
+
rotation: Annotated[list[float], "New local rotation (euler angles) [x, y, z] for modify_contents."] | None = None,
|
|
54
|
+
scale: Annotated[list[float], "New local scale [x, y, z] for modify_contents."] | None = None,
|
|
55
|
+
name: Annotated[str, "New name for the target object in modify_contents."] | None = None,
|
|
56
|
+
tag: Annotated[str, "New tag for the target object in modify_contents."] | None = None,
|
|
57
|
+
layer: Annotated[str, "New layer name for the target object in modify_contents."] | None = None,
|
|
58
|
+
set_active: Annotated[bool, "Set active state of target object in modify_contents."] | None = None,
|
|
59
|
+
parent: Annotated[str, "New parent object name/path within prefab for modify_contents."] | None = None,
|
|
60
|
+
components_to_add: Annotated[list[str], "Component types to add in modify_contents."] | None = None,
|
|
61
|
+
components_to_remove: Annotated[list[str], "Component types to remove in modify_contents."] | None = None,
|
|
35
62
|
) -> dict[str, Any]:
|
|
36
|
-
#
|
|
37
|
-
|
|
63
|
+
# Back-compat: map 'name' → 'target' for create_from_gameobject (Unity accepts both)
|
|
64
|
+
if action == "create_from_gameobject" and target is None and name is not None:
|
|
65
|
+
target = name
|
|
66
|
+
|
|
67
|
+
# Validate required parameters
|
|
68
|
+
required = REQUIRED_PARAMS.get(action, [])
|
|
69
|
+
for param_name in required:
|
|
70
|
+
# Use updated local value for target after back-compat mapping
|
|
71
|
+
param_value = target if param_name == "target" else locals().get(param_name)
|
|
72
|
+
# Check for None and empty/whitespace strings
|
|
73
|
+
if param_value is None or (isinstance(param_value, str) and not param_value.strip()):
|
|
74
|
+
return {
|
|
75
|
+
"success": False,
|
|
76
|
+
"message": f"Action '{action}' requires parameter '{param_name}'."
|
|
77
|
+
}
|
|
78
|
+
|
|
38
79
|
unity_instance = get_unity_instance_from_context(ctx)
|
|
39
80
|
|
|
81
|
+
# Preflight check for operations to ensure Unity is ready
|
|
40
82
|
try:
|
|
83
|
+
gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True)
|
|
84
|
+
if gate is not None:
|
|
85
|
+
return gate.model_dump()
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
return {
|
|
88
|
+
"success": False,
|
|
89
|
+
"message": f"Unity preflight check failed: {exc}"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# Build parameters dictionary
|
|
41
94
|
params: dict[str, Any] = {"action": action}
|
|
42
95
|
|
|
96
|
+
# Handle prefab path parameter
|
|
43
97
|
if prefab_path:
|
|
44
98
|
params["prefabPath"] = prefab_path
|
|
45
|
-
|
|
46
|
-
params["mode"] = mode
|
|
47
|
-
save_before_close_val = coerce_bool(save_before_close)
|
|
48
|
-
if save_before_close_val is not None:
|
|
49
|
-
params["saveBeforeClose"] = save_before_close_val
|
|
99
|
+
|
|
50
100
|
if target:
|
|
51
101
|
params["target"] = target
|
|
102
|
+
|
|
52
103
|
allow_overwrite_val = coerce_bool(allow_overwrite)
|
|
53
104
|
if allow_overwrite_val is not None:
|
|
54
105
|
params["allowOverwrite"] = allow_overwrite_val
|
|
106
|
+
|
|
55
107
|
search_inactive_val = coerce_bool(search_inactive)
|
|
56
108
|
if search_inactive_val is not None:
|
|
57
109
|
params["searchInactive"] = search_inactive_val
|
|
58
|
-
response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_prefabs", params)
|
|
59
110
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
111
|
+
unlink_if_instance_val = coerce_bool(unlink_if_instance)
|
|
112
|
+
if unlink_if_instance_val is not None:
|
|
113
|
+
params["unlinkIfInstance"] = unlink_if_instance_val
|
|
114
|
+
|
|
115
|
+
# modify_contents parameters
|
|
116
|
+
if position is not None:
|
|
117
|
+
params["position"] = position
|
|
118
|
+
if rotation is not None:
|
|
119
|
+
params["rotation"] = rotation
|
|
120
|
+
if scale is not None:
|
|
121
|
+
params["scale"] = scale
|
|
122
|
+
if name is not None:
|
|
123
|
+
params["name"] = name
|
|
124
|
+
if tag is not None:
|
|
125
|
+
params["tag"] = tag
|
|
126
|
+
if layer is not None:
|
|
127
|
+
params["layer"] = layer
|
|
128
|
+
set_active_val = coerce_bool(set_active)
|
|
129
|
+
if set_active_val is not None:
|
|
130
|
+
params["setActive"] = set_active_val
|
|
131
|
+
if parent is not None:
|
|
132
|
+
params["parent"] = parent
|
|
133
|
+
if components_to_add is not None:
|
|
134
|
+
params["componentsToAdd"] = components_to_add
|
|
135
|
+
if components_to_remove is not None:
|
|
136
|
+
params["componentsToRemove"] = components_to_remove
|
|
137
|
+
|
|
138
|
+
# Send command to Unity
|
|
139
|
+
response = await send_with_unity_instance(
|
|
140
|
+
async_send_command_with_retry, unity_instance, "manage_prefabs", params
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Return Unity response directly; ensure success field exists
|
|
144
|
+
# Handle MCPResponse objects (returned on error) by converting to dict
|
|
145
|
+
if hasattr(response, 'model_dump'):
|
|
146
|
+
return response.model_dump()
|
|
147
|
+
if isinstance(response, dict):
|
|
148
|
+
if "success" not in response:
|
|
149
|
+
response["success"] = False
|
|
150
|
+
return response
|
|
151
|
+
return {
|
|
152
|
+
"success": False,
|
|
153
|
+
"message": f"Unexpected response type: {type(response).__name__}"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
except TimeoutError:
|
|
157
|
+
return {
|
|
158
|
+
"success": False,
|
|
159
|
+
"message": "Unity connection timeout. Please check if Unity is running and responsive."
|
|
160
|
+
}
|
|
67
161
|
except Exception as exc:
|
|
68
|
-
return {
|
|
162
|
+
return {
|
|
163
|
+
"success": False,
|
|
164
|
+
"message": f"Error managing prefabs: {exc}"
|
|
165
|
+
}
|