mcpforunityserver 8.5.0__py3-none-any.whl → 9.1.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.
- cli/__init__.py +3 -0
- cli/commands/__init__.py +3 -0
- cli/commands/animation.py +87 -0
- cli/commands/asset.py +310 -0
- cli/commands/audio.py +133 -0
- cli/commands/batch.py +184 -0
- cli/commands/code.py +189 -0
- cli/commands/component.py +212 -0
- cli/commands/editor.py +487 -0
- cli/commands/gameobject.py +510 -0
- cli/commands/instance.py +101 -0
- cli/commands/lighting.py +128 -0
- cli/commands/material.py +268 -0
- cli/commands/prefab.py +144 -0
- cli/commands/scene.py +255 -0
- cli/commands/script.py +240 -0
- cli/commands/shader.py +238 -0
- cli/commands/ui.py +263 -0
- cli/commands/vfx.py +439 -0
- cli/main.py +248 -0
- cli/utils/__init__.py +31 -0
- cli/utils/config.py +58 -0
- cli/utils/connection.py +191 -0
- cli/utils/output.py +195 -0
- main.py +207 -62
- {mcpforunityserver-8.5.0.dist-info → mcpforunityserver-9.1.0.dist-info}/METADATA +4 -2
- mcpforunityserver-9.1.0.dist-info/RECORD +96 -0
- {mcpforunityserver-8.5.0.dist-info → mcpforunityserver-9.1.0.dist-info}/WHEEL +1 -1
- {mcpforunityserver-8.5.0.dist-info → mcpforunityserver-9.1.0.dist-info}/entry_points.txt +1 -0
- {mcpforunityserver-8.5.0.dist-info → mcpforunityserver-9.1.0.dist-info}/top_level.txt +1 -2
- services/custom_tool_service.py +179 -19
- services/resources/__init__.py +6 -1
- services/resources/active_tool.py +1 -1
- services/resources/custom_tools.py +2 -2
- services/resources/editor_state.py +283 -21
- 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 +245 -0
- services/tools/__init__.py +6 -1
- 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 +19 -43
- services/tools/manage_components.py +131 -0
- services/tools/manage_editor.py +9 -8
- services/tools/manage_gameobject.py +120 -79
- services/tools/manage_material.py +80 -31
- services/tools/manage_prefabs.py +7 -1
- services/tools/manage_scene.py +34 -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 +110 -0
- services/tools/read_console.py +81 -18
- services/tools/refresh_unity.py +153 -0
- services/tools/run_tests.py +202 -41
- 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 +129 -26
- transport/plugin_hub.py +191 -19
- transport/unity_instance_middleware.py +93 -2
- transport/unity_transport.py +17 -6
- utils/focus_nudge.py +321 -0
- __init__.py +0 -0
- mcpforunityserver-8.5.0.dist-info/RECORD +0 -66
- routes/__init__.py +0 -0
- {mcpforunityserver-8.5.0.dist-info → mcpforunityserver-9.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,21 +1,98 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import math
|
|
3
|
-
from typing import Annotated, Any, Literal
|
|
3
|
+
from typing import Annotated, Any, Literal
|
|
4
4
|
|
|
5
5
|
from fastmcp import Context
|
|
6
|
+
from mcp.types import ToolAnnotations
|
|
7
|
+
|
|
6
8
|
from services.registry import mcp_for_unity_tool
|
|
7
9
|
from services.tools import get_unity_instance_from_context
|
|
8
10
|
from transport.unity_transport import send_with_unity_instance
|
|
9
11
|
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
10
12
|
from services.tools.utils import coerce_bool, parse_json_payload, coerce_int
|
|
13
|
+
from services.tools.preflight import preflight
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalize_vector(value: Any, default: Any = None) -> list[float] | None:
|
|
17
|
+
"""
|
|
18
|
+
Robustly normalize a vector parameter to [x, y, z] format.
|
|
19
|
+
Handles: list, tuple, JSON string, comma-separated string.
|
|
20
|
+
Returns None if parsing fails.
|
|
21
|
+
"""
|
|
22
|
+
if value is None:
|
|
23
|
+
return default
|
|
24
|
+
|
|
25
|
+
# If already a list/tuple with 3 elements, convert to floats
|
|
26
|
+
if isinstance(value, (list, tuple)) and len(value) == 3:
|
|
27
|
+
try:
|
|
28
|
+
vec = [float(value[0]), float(value[1]), float(value[2])]
|
|
29
|
+
return vec if all(math.isfinite(n) for n in vec) else default
|
|
30
|
+
except (ValueError, TypeError):
|
|
31
|
+
return default
|
|
32
|
+
|
|
33
|
+
# Try parsing as JSON string
|
|
34
|
+
if isinstance(value, str):
|
|
35
|
+
parsed = parse_json_payload(value)
|
|
36
|
+
if isinstance(parsed, list) and len(parsed) == 3:
|
|
37
|
+
try:
|
|
38
|
+
vec = [float(parsed[0]), float(parsed[1]), float(parsed[2])]
|
|
39
|
+
return vec if all(math.isfinite(n) for n in vec) else default
|
|
40
|
+
except (ValueError, TypeError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# Handle legacy comma-separated strings "1,2,3" or "[1,2,3]"
|
|
44
|
+
s = value.strip()
|
|
45
|
+
if s.startswith("[") and s.endswith("]"):
|
|
46
|
+
s = s[1:-1]
|
|
47
|
+
parts = [p.strip() for p in (s.split(",") if "," in s else s.split())]
|
|
48
|
+
if len(parts) == 3:
|
|
49
|
+
try:
|
|
50
|
+
vec = [float(parts[0]), float(parts[1]), float(parts[2])]
|
|
51
|
+
return vec if all(math.isfinite(n) for n in vec) else default
|
|
52
|
+
except (ValueError, TypeError):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
return default
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _normalize_component_properties(value: Any) -> tuple[dict[str, dict[str, Any]] | None, str | None]:
|
|
59
|
+
"""
|
|
60
|
+
Robustly normalize component_properties to a dict.
|
|
61
|
+
Returns (parsed_dict, error_message). If error_message is set, parsed_dict is None.
|
|
62
|
+
"""
|
|
63
|
+
if value is None:
|
|
64
|
+
return None, None
|
|
65
|
+
|
|
66
|
+
# Already a dict - validate structure
|
|
67
|
+
if isinstance(value, dict):
|
|
68
|
+
return value, None
|
|
69
|
+
|
|
70
|
+
# Try parsing as JSON string
|
|
71
|
+
if isinstance(value, str):
|
|
72
|
+
# Check for obviously invalid values
|
|
73
|
+
if value in ("[object Object]", "undefined", "null", ""):
|
|
74
|
+
return None, f"component_properties received invalid value: '{value}'. Expected a JSON object like {{\"ComponentName\": {{\"property\": value}}}}"
|
|
75
|
+
|
|
76
|
+
parsed = parse_json_payload(value)
|
|
77
|
+
if isinstance(parsed, dict):
|
|
78
|
+
return parsed, None
|
|
79
|
+
|
|
80
|
+
return None, f"component_properties must be a JSON object (dict), got string that parsed to {type(parsed).__name__}"
|
|
81
|
+
|
|
82
|
+
return None, f"component_properties must be a dict or JSON string, got {type(value).__name__}"
|
|
11
83
|
|
|
12
84
|
|
|
13
85
|
@mcp_for_unity_tool(
|
|
14
|
-
description="Performs CRUD operations on GameObjects
|
|
86
|
+
description="Performs CRUD operations on GameObjects. Actions: create, modify, delete, duplicate, move_relative. For finding GameObjects use find_gameobjects tool. For component operations use manage_components tool.",
|
|
87
|
+
annotations=ToolAnnotations(
|
|
88
|
+
title="Manage GameObject",
|
|
89
|
+
destructiveHint=True,
|
|
90
|
+
),
|
|
15
91
|
)
|
|
16
92
|
async def manage_gameobject(
|
|
17
93
|
ctx: Context,
|
|
18
|
-
action: Annotated[Literal["create", "modify", "delete", "
|
|
94
|
+
action: Annotated[Literal["create", "modify", "delete", "duplicate",
|
|
95
|
+
"move_relative"], "Action to perform on GameObject."] | None = None,
|
|
19
96
|
target: Annotated[str,
|
|
20
97
|
"GameObject identifier by name or path for modify/delete/component actions"] | None = None,
|
|
21
98
|
search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"],
|
|
@@ -26,12 +103,12 @@ async def manage_gameobject(
|
|
|
26
103
|
"Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None,
|
|
27
104
|
parent: Annotated[str,
|
|
28
105
|
"Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None,
|
|
29
|
-
position: Annotated[
|
|
30
|
-
"Position
|
|
31
|
-
rotation: Annotated[
|
|
32
|
-
"Rotation
|
|
33
|
-
scale: Annotated[
|
|
34
|
-
"Scale
|
|
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,
|
|
35
112
|
components_to_add: Annotated[list[str],
|
|
36
113
|
"List of component names to add"] | None = None,
|
|
37
114
|
primitive_type: Annotated[str,
|
|
@@ -47,7 +124,7 @@ async def manage_gameobject(
|
|
|
47
124
|
layer: Annotated[str, "Layer name"] | None = None,
|
|
48
125
|
components_to_remove: Annotated[list[str],
|
|
49
126
|
"List of component names to remove"] | None = None,
|
|
50
|
-
component_properties: Annotated[
|
|
127
|
+
component_properties: Annotated[dict[str, dict[str, Any]],
|
|
51
128
|
"""Dictionary of component names to their properties to set. For example:
|
|
52
129
|
`{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject
|
|
53
130
|
`{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component
|
|
@@ -69,70 +146,50 @@ async def manage_gameobject(
|
|
|
69
146
|
includeNonPublicSerialized: Annotated[bool | str,
|
|
70
147
|
"Controls whether serialization of private [SerializeField] fields is included (accepts true/false or 'true'/'false')"] | None = None,
|
|
71
148
|
# --- Paging/safety for get_components ---
|
|
72
|
-
page_size: Annotated[int | str,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
149
|
+
page_size: Annotated[int | str,
|
|
150
|
+
"Page size for get_components paging."] | None = None,
|
|
151
|
+
cursor: Annotated[int | str,
|
|
152
|
+
"Opaque cursor for get_components paging (offset)."] | None = None,
|
|
153
|
+
max_components: Annotated[int | str,
|
|
154
|
+
"Hard cap on returned components per request (safety)."] | None = None,
|
|
155
|
+
include_properties: Annotated[bool | str,
|
|
156
|
+
"If true, include serialized component properties (bounded)."] | None = None,
|
|
76
157
|
# --- Parameters for 'duplicate' ---
|
|
77
158
|
new_name: Annotated[str,
|
|
78
159
|
"New name for the duplicated object (default: SourceName_Copy)"] | None = None,
|
|
79
|
-
offset: Annotated[
|
|
80
|
-
"Offset from original/reference position
|
|
160
|
+
offset: Annotated[list[float],
|
|
161
|
+
"Offset from original/reference position as [x, y, z] array"] | None = None,
|
|
81
162
|
# --- Parameters for 'move_relative' ---
|
|
82
163
|
reference_object: Annotated[str,
|
|
83
|
-
|
|
164
|
+
"Reference object for relative movement (required for move_relative)"] | None = None,
|
|
84
165
|
direction: Annotated[Literal["left", "right", "up", "down", "forward", "back", "front", "backward", "behind"],
|
|
85
|
-
|
|
166
|
+
"Direction for relative movement (e.g., 'right', 'up', 'forward')"] | None = None,
|
|
86
167
|
distance: Annotated[float,
|
|
87
|
-
|
|
168
|
+
"Distance to move in the specified direction (default: 1.0)"] | None = None,
|
|
88
169
|
world_space: Annotated[bool | str,
|
|
89
|
-
|
|
170
|
+
"If True (default), use world space directions; if False, use reference object's local directions"] | None = None,
|
|
90
171
|
) -> dict[str, Any]:
|
|
91
172
|
# Get active instance from session state
|
|
92
173
|
# Removed session_state import
|
|
93
174
|
unity_instance = get_unity_instance_from_context(ctx)
|
|
94
175
|
|
|
176
|
+
gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True)
|
|
177
|
+
if gate is not None:
|
|
178
|
+
return gate.model_dump()
|
|
179
|
+
|
|
95
180
|
if action is None:
|
|
96
181
|
return {
|
|
97
182
|
"success": False,
|
|
98
|
-
"message": "Missing required parameter 'action'. Valid actions: create, modify, delete,
|
|
183
|
+
"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."
|
|
99
184
|
}
|
|
100
185
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# First try to parse if it's a string
|
|
107
|
-
val = parse_json_payload(value)
|
|
108
|
-
|
|
109
|
-
def _to_vec3(parts):
|
|
110
|
-
try:
|
|
111
|
-
vec = [float(parts[0]), float(parts[1]), float(parts[2])]
|
|
112
|
-
except (ValueError, TypeError):
|
|
113
|
-
return default
|
|
114
|
-
return vec if all(math.isfinite(n) for n in vec) else default
|
|
115
|
-
|
|
116
|
-
if isinstance(val, list) and len(val) == 3:
|
|
117
|
-
return _to_vec3(val)
|
|
118
|
-
|
|
119
|
-
# Handle legacy comma-separated strings "1,2,3" that parse_json_payload doesn't handle (since they aren't JSON arrays)
|
|
120
|
-
if isinstance(val, str):
|
|
121
|
-
s = val.strip()
|
|
122
|
-
# minimal tolerant parse for "[x,y,z]" or "x,y,z"
|
|
123
|
-
if s.startswith("[") and s.endswith("]"):
|
|
124
|
-
s = s[1:-1]
|
|
125
|
-
# support "x,y,z" and "x y z"
|
|
126
|
-
parts = [p.strip()
|
|
127
|
-
for p in (s.split(",") if "," in s else s.split())]
|
|
128
|
-
if len(parts) == 3:
|
|
129
|
-
return _to_vec3(parts)
|
|
130
|
-
return default
|
|
186
|
+
# --- Normalize vector parameters using robust helper ---
|
|
187
|
+
position = _normalize_vector(position)
|
|
188
|
+
rotation = _normalize_vector(rotation)
|
|
189
|
+
scale = _normalize_vector(scale)
|
|
190
|
+
offset = _normalize_vector(offset)
|
|
131
191
|
|
|
132
|
-
|
|
133
|
-
rotation = _coerce_vec(rotation, default=rotation)
|
|
134
|
-
scale = _coerce_vec(scale, default=scale)
|
|
135
|
-
offset = _coerce_vec(offset, default=offset)
|
|
192
|
+
# --- Normalize boolean parameters ---
|
|
136
193
|
save_as_prefab = coerce_bool(save_as_prefab)
|
|
137
194
|
set_active = coerce_bool(set_active)
|
|
138
195
|
find_all = coerce_bool(find_all)
|
|
@@ -141,36 +198,20 @@ async def manage_gameobject(
|
|
|
141
198
|
includeNonPublicSerialized = coerce_bool(includeNonPublicSerialized)
|
|
142
199
|
include_properties = coerce_bool(include_properties)
|
|
143
200
|
world_space = coerce_bool(world_space, default=True)
|
|
144
|
-
|
|
201
|
+
|
|
202
|
+
# --- Normalize integer parameters ---
|
|
145
203
|
page_size = coerce_int(page_size, default=None)
|
|
146
204
|
cursor = coerce_int(cursor, default=None)
|
|
147
205
|
max_components = coerce_int(max_components, default=None)
|
|
148
206
|
|
|
149
|
-
#
|
|
150
|
-
component_properties =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return {"success": False, "message": "component_properties must be a JSON object (dict)."}
|
|
155
|
-
|
|
156
|
-
try:
|
|
157
|
-
# Map tag to search_term when search_method is by_tag for backward compatibility
|
|
158
|
-
if action == "find" and search_method == "by_tag" and tag is not None and search_term is None:
|
|
159
|
-
search_term = tag
|
|
207
|
+
# --- Normalize component_properties with detailed error handling ---
|
|
208
|
+
component_properties, comp_props_error = _normalize_component_properties(
|
|
209
|
+
component_properties)
|
|
210
|
+
if comp_props_error:
|
|
211
|
+
return {"success": False, "message": comp_props_error}
|
|
160
212
|
|
|
213
|
+
try:
|
|
161
214
|
# Validate parameter usage to prevent silent failures
|
|
162
|
-
if action == "find":
|
|
163
|
-
if name is not None:
|
|
164
|
-
return {
|
|
165
|
-
"success": False,
|
|
166
|
-
"message": "For 'find' action, use 'search_term' parameter, not 'name'. Remove 'name' parameter. Example: search_term='Player', search_method='by_name'"
|
|
167
|
-
}
|
|
168
|
-
if search_term is None:
|
|
169
|
-
return {
|
|
170
|
-
"success": False,
|
|
171
|
-
"message": "For 'find' action, 'search_term' parameter is required. Use search_term (not 'name') to specify what to find."
|
|
172
|
-
}
|
|
173
|
-
|
|
174
215
|
if action in ["create", "modify"]:
|
|
175
216
|
if search_term is not None:
|
|
176
217
|
return {
|
|
@@ -2,18 +2,57 @@
|
|
|
2
2
|
Defines the manage_material tool for interacting with Unity materials.
|
|
3
3
|
"""
|
|
4
4
|
import json
|
|
5
|
-
from typing import Annotated, Any, Literal
|
|
5
|
+
from typing import Annotated, Any, Literal
|
|
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 services.tools import get_unity_instance_from_context
|
|
10
|
-
from services.tools.utils import parse_json_payload
|
|
12
|
+
from services.tools.utils import parse_json_payload, coerce_int, normalize_properties
|
|
11
13
|
from transport.unity_transport import send_with_unity_instance
|
|
12
14
|
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
def _normalize_color(value: Any) -> tuple[list[float] | None, str | None]:
|
|
18
|
+
"""
|
|
19
|
+
Normalize color parameter to [r, g, b] or [r, g, b, a] format.
|
|
20
|
+
Returns (parsed_color, error_message).
|
|
21
|
+
"""
|
|
22
|
+
if value is None:
|
|
23
|
+
return None, None
|
|
24
|
+
|
|
25
|
+
# Already a list - validate
|
|
26
|
+
if isinstance(value, (list, tuple)):
|
|
27
|
+
if len(value) in (3, 4):
|
|
28
|
+
try:
|
|
29
|
+
return [float(c) for c in value], None
|
|
30
|
+
except (ValueError, TypeError):
|
|
31
|
+
return None, f"color values must be numbers, got {value}"
|
|
32
|
+
return None, f"color must have 3 or 4 components, got {len(value)}"
|
|
33
|
+
|
|
34
|
+
# Try parsing as string
|
|
35
|
+
if isinstance(value, str):
|
|
36
|
+
if value in ("[object Object]", "undefined", "null", ""):
|
|
37
|
+
return None, f"color received invalid value: '{value}'. Expected [r, g, b] or [r, g, b, a]"
|
|
38
|
+
|
|
39
|
+
parsed = parse_json_payload(value)
|
|
40
|
+
if isinstance(parsed, (list, tuple)) and len(parsed) in (3, 4):
|
|
41
|
+
try:
|
|
42
|
+
return [float(c) for c in parsed], None
|
|
43
|
+
except (ValueError, TypeError):
|
|
44
|
+
return None, f"color values must be numbers, got {parsed}"
|
|
45
|
+
return None, f"Failed to parse color string: {value}"
|
|
46
|
+
|
|
47
|
+
return None, f"color must be a list or JSON string, got {type(value).__name__}"
|
|
48
|
+
|
|
49
|
+
|
|
15
50
|
@mcp_for_unity_tool(
|
|
16
|
-
description="Manages Unity materials (set properties, colors, shaders, etc)."
|
|
51
|
+
description="Manages Unity materials (set properties, colors, shaders, etc). Read-only actions: ping, get_material_info. Modifying actions: create, set_material_shader_property, set_material_color, assign_material_to_renderer, set_renderer_color.",
|
|
52
|
+
annotations=ToolAnnotations(
|
|
53
|
+
title="Manage Material",
|
|
54
|
+
destructiveHint=True,
|
|
55
|
+
),
|
|
17
56
|
)
|
|
18
57
|
async def manage_material(
|
|
19
58
|
ctx: Context,
|
|
@@ -26,45 +65,55 @@ async def manage_material(
|
|
|
26
65
|
"set_renderer_color",
|
|
27
66
|
"get_material_info"
|
|
28
67
|
], "Action to perform."],
|
|
29
|
-
|
|
68
|
+
|
|
30
69
|
# Common / Shared
|
|
31
|
-
material_path: Annotated[str,
|
|
32
|
-
|
|
70
|
+
material_path: Annotated[str,
|
|
71
|
+
"Path to material asset (Assets/...)"] | None = None,
|
|
72
|
+
property: Annotated[str,
|
|
73
|
+
"Shader property name (e.g., _BaseColor, _MainTex)"] | None = None,
|
|
33
74
|
|
|
34
75
|
# create
|
|
35
76
|
shader: Annotated[str, "Shader name (default: Standard)"] | None = None,
|
|
36
|
-
properties: Annotated[
|
|
37
|
-
|
|
77
|
+
properties: Annotated[dict[str, Any],
|
|
78
|
+
"Initial properties to set as {name: value} dict."] | None = None,
|
|
79
|
+
|
|
38
80
|
# set_material_shader_property
|
|
39
|
-
value: Annotated[
|
|
40
|
-
|
|
81
|
+
value: Annotated[list | float | int | str | bool | None,
|
|
82
|
+
"Value to set (color array, float, texture path/instruction)"] | None = None,
|
|
83
|
+
|
|
41
84
|
# set_material_color / set_renderer_color
|
|
42
|
-
color: Annotated[
|
|
43
|
-
|
|
85
|
+
color: Annotated[list[float],
|
|
86
|
+
"Color as [r, g, b] or [r, g, b, a] array."] | None = None,
|
|
87
|
+
|
|
44
88
|
# assign_material_to_renderer / set_renderer_color
|
|
45
|
-
target: Annotated[str,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
89
|
+
target: Annotated[str,
|
|
90
|
+
"Target GameObject (name, path, or find instruction)"] | None = None,
|
|
91
|
+
search_method: Annotated[Literal["by_name", "by_path", "by_tag",
|
|
92
|
+
"by_layer", "by_component"], "Search method for target"] | None = None,
|
|
93
|
+
slot: Annotated[int, "Material slot index (0-based)"] | None = None,
|
|
94
|
+
mode: Annotated[Literal["shared", "instance", "property_block"],
|
|
95
|
+
"Assignment/modification mode"] | None = None,
|
|
96
|
+
|
|
50
97
|
) -> dict[str, Any]:
|
|
51
98
|
unity_instance = get_unity_instance_from_context(ctx)
|
|
52
99
|
|
|
53
|
-
#
|
|
54
|
-
color =
|
|
55
|
-
|
|
100
|
+
# --- Normalize color with validation ---
|
|
101
|
+
color, color_error = _normalize_color(color)
|
|
102
|
+
if color_error:
|
|
103
|
+
return {"success": False, "message": color_error}
|
|
104
|
+
|
|
105
|
+
# --- Normalize properties with validation ---
|
|
106
|
+
properties, props_error = normalize_properties(properties)
|
|
107
|
+
if props_error:
|
|
108
|
+
return {"success": False, "message": props_error}
|
|
109
|
+
|
|
110
|
+
# --- Normalize value (parse JSON if string) ---
|
|
56
111
|
value = parse_json_payload(value)
|
|
112
|
+
if isinstance(value, str) and value in ("[object Object]", "undefined"):
|
|
113
|
+
return {"success": False, "message": f"value received invalid input: '{value}'"}
|
|
57
114
|
|
|
58
|
-
#
|
|
59
|
-
|
|
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
|
-
}
|
|
115
|
+
# --- Normalize slot to int ---
|
|
116
|
+
slot = coerce_int(slot)
|
|
68
117
|
|
|
69
118
|
# Prepare parameters for the C# handler
|
|
70
119
|
params_dict = {
|
|
@@ -91,5 +140,5 @@ async def manage_material(
|
|
|
91
140
|
"manage_material",
|
|
92
141
|
params_dict,
|
|
93
142
|
)
|
|
94
|
-
|
|
143
|
+
|
|
95
144
|
return result if isinstance(result, dict) else {"success": False, "message": str(result)}
|
services/tools/manage_prefabs.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from typing import Annotated, Any, Literal
|
|
2
2
|
|
|
3
3
|
from fastmcp import Context
|
|
4
|
+
from mcp.types import ToolAnnotations
|
|
5
|
+
|
|
4
6
|
from services.registry import mcp_for_unity_tool
|
|
5
7
|
from services.tools import get_unity_instance_from_context
|
|
6
8
|
from transport.unity_transport import send_with_unity_instance
|
|
@@ -9,7 +11,11 @@ from services.tools.utils import coerce_bool
|
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
@mcp_for_unity_tool(
|
|
12
|
-
description="Performs prefab operations (open_stage, close_stage, save_open_stage, create_from_gameobject)."
|
|
14
|
+
description="Performs prefab operations (open_stage, close_stage, save_open_stage, create_from_gameobject).",
|
|
15
|
+
annotations=ToolAnnotations(
|
|
16
|
+
title="Manage Prefabs",
|
|
17
|
+
destructiveHint=True,
|
|
18
|
+
),
|
|
13
19
|
)
|
|
14
20
|
async def manage_prefabs(
|
|
15
21
|
ctx: Context,
|
services/tools/manage_scene.py
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
from typing import Annotated, Literal, Any
|
|
2
2
|
|
|
3
3
|
from fastmcp import Context
|
|
4
|
+
from mcp.types import ToolAnnotations
|
|
5
|
+
|
|
4
6
|
from services.registry import mcp_for_unity_tool
|
|
5
7
|
from services.tools import get_unity_instance_from_context
|
|
6
8
|
from services.tools.utils import coerce_int, coerce_bool
|
|
7
9
|
from transport.unity_transport import send_with_unity_instance
|
|
8
10
|
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
11
|
+
from services.tools.preflight import preflight
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
@mcp_for_unity_tool(
|
|
12
|
-
description="Performs CRUD operations on Unity scenes."
|
|
15
|
+
description="Performs CRUD operations on Unity scenes. Read-only actions: get_hierarchy, get_active, get_build_settings, screenshot. Modifying actions: create, load, save.",
|
|
16
|
+
annotations=ToolAnnotations(
|
|
17
|
+
title="Manage Scene",
|
|
18
|
+
destructiveHint=True,
|
|
19
|
+
),
|
|
13
20
|
)
|
|
14
21
|
async def manage_scene(
|
|
15
22
|
ctx: Context,
|
|
@@ -26,20 +33,32 @@ async def manage_scene(
|
|
|
26
33
|
path: Annotated[str, "Scene path."] | None = None,
|
|
27
34
|
build_index: Annotated[int | str,
|
|
28
35
|
"Unity build index (quote as string, e.g., '0')."] | None = None,
|
|
29
|
-
screenshot_file_name: Annotated[str,
|
|
30
|
-
|
|
36
|
+
screenshot_file_name: Annotated[str,
|
|
37
|
+
"Screenshot file name (optional). Defaults to timestamp when omitted."] | None = None,
|
|
38
|
+
screenshot_super_size: Annotated[int | str,
|
|
39
|
+
"Screenshot supersize multiplier (integer ≥1). Optional."] | None = None,
|
|
31
40
|
# --- get_hierarchy paging/safety ---
|
|
32
|
-
parent: Annotated[str | int,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
parent: Annotated[str | int,
|
|
42
|
+
"Optional parent GameObject reference (name/path/instanceID) to list direct children."] | None = None,
|
|
43
|
+
page_size: Annotated[int | str,
|
|
44
|
+
"Page size for get_hierarchy paging."] | None = None,
|
|
45
|
+
cursor: Annotated[int | str,
|
|
46
|
+
"Opaque cursor for paging (offset)."] | None = None,
|
|
47
|
+
max_nodes: Annotated[int | str,
|
|
48
|
+
"Hard cap on returned nodes per request (safety)."] | None = None,
|
|
49
|
+
max_depth: Annotated[int | str,
|
|
50
|
+
"Accepted for forward-compatibility; current paging returns a single level."] | None = None,
|
|
51
|
+
max_children_per_node: Annotated[int | str,
|
|
52
|
+
"Child paging hint (safety)."] | None = None,
|
|
53
|
+
include_transform: Annotated[bool | str,
|
|
54
|
+
"If true, include local transform in node summaries."] | None = None,
|
|
39
55
|
) -> dict[str, Any]:
|
|
40
56
|
# Get active instance from session state
|
|
41
57
|
# Removed session_state import
|
|
42
58
|
unity_instance = get_unity_instance_from_context(ctx)
|
|
59
|
+
gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True)
|
|
60
|
+
if gate is not None:
|
|
61
|
+
return gate.model_dump()
|
|
43
62
|
try:
|
|
44
63
|
coerced_build_index = coerce_int(build_index, default=None)
|
|
45
64
|
coerced_super_size = coerce_int(screenshot_super_size, default=None)
|
|
@@ -47,8 +66,10 @@ async def manage_scene(
|
|
|
47
66
|
coerced_cursor = coerce_int(cursor, default=None)
|
|
48
67
|
coerced_max_nodes = coerce_int(max_nodes, default=None)
|
|
49
68
|
coerced_max_depth = coerce_int(max_depth, default=None)
|
|
50
|
-
coerced_max_children_per_node = coerce_int(
|
|
51
|
-
|
|
69
|
+
coerced_max_children_per_node = coerce_int(
|
|
70
|
+
max_children_per_node, default=None)
|
|
71
|
+
coerced_include_transform = coerce_bool(
|
|
72
|
+
include_transform, default=None)
|
|
52
73
|
|
|
53
74
|
params: dict[str, Any] = {"action": action}
|
|
54
75
|
if name:
|
|
@@ -61,7 +82,7 @@ async def manage_scene(
|
|
|
61
82
|
params["fileName"] = screenshot_file_name
|
|
62
83
|
if coerced_super_size is not None:
|
|
63
84
|
params["superSize"] = coerced_super_size
|
|
64
|
-
|
|
85
|
+
|
|
65
86
|
# get_hierarchy paging/safety params (optional)
|
|
66
87
|
if parent is not None:
|
|
67
88
|
params["parent"] = parent
|