mcpforunityserver 9.3.0b20260128055651__py3-none-any.whl → 9.3.0b20260129121506__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/animation.py +6 -9
- cli/commands/asset.py +50 -80
- cli/commands/audio.py +14 -22
- cli/commands/batch.py +20 -33
- cli/commands/code.py +63 -70
- cli/commands/component.py +33 -55
- cli/commands/editor.py +122 -188
- cli/commands/gameobject.py +60 -83
- cli/commands/instance.py +28 -36
- cli/commands/lighting.py +54 -59
- cli/commands/material.py +39 -68
- cli/commands/prefab.py +63 -81
- cli/commands/scene.py +30 -54
- cli/commands/script.py +32 -50
- cli/commands/shader.py +43 -55
- cli/commands/texture.py +53 -51
- cli/commands/tool.py +24 -27
- cli/commands/ui.py +125 -130
- cli/commands/vfx.py +84 -138
- cli/utils/confirmation.py +37 -0
- cli/utils/connection.py +32 -2
- cli/utils/constants.py +23 -0
- cli/utils/parsers.py +112 -0
- core/config.py +0 -4
- core/telemetry.py +20 -2
- {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/METADATA +21 -1
- mcpforunityserver-9.3.0b20260129121506.dist-info/RECORD +103 -0
- services/resources/active_tool.py +1 -1
- services/resources/custom_tools.py +1 -1
- services/resources/editor_state.py +1 -1
- services/resources/gameobject.py +4 -4
- services/resources/layers.py +1 -1
- services/resources/menu_items.py +1 -1
- services/resources/prefab.py +3 -3
- 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/tests.py +40 -8
- services/resources/unity_instances.py +1 -1
- services/resources/windows.py +1 -1
- services/tools/__init__.py +3 -1
- services/tools/find_gameobjects.py +32 -11
- services/tools/manage_gameobject.py +11 -66
- services/tools/manage_material.py +4 -37
- services/tools/manage_prefabs.py +51 -7
- services/tools/manage_script.py +1 -1
- services/tools/manage_texture.py +10 -96
- services/tools/run_tests.py +67 -4
- services/tools/utils.py +217 -0
- transport/models.py +1 -0
- transport/plugin_hub.py +2 -1
- transport/plugin_registry.py +3 -0
- transport/unity_transport.py +0 -51
- utils/focus_nudge.py +291 -23
- mcpforunityserver-9.3.0b20260128055651.dist-info/RECORD +0 -101
- utils/reload_sentinel.py +0 -9
- {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/WHEEL +0 -0
- {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/entry_points.txt +0 -0
- {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/licenses/LICENSE +0 -0
- {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/top_level.txt +0 -0
cli/commands/vfx.py
CHANGED
|
@@ -7,7 +7,9 @@ from typing import Optional, Tuple, Any
|
|
|
7
7
|
|
|
8
8
|
from cli.utils.config import get_config
|
|
9
9
|
from cli.utils.output import format_output, print_error, print_success
|
|
10
|
-
from cli.utils.connection import run_command,
|
|
10
|
+
from cli.utils.connection import run_command, handle_unity_errors
|
|
11
|
+
from cli.utils.parsers import parse_json_list_or_exit, parse_json_dict_or_exit
|
|
12
|
+
from cli.utils.constants import SEARCH_METHOD_CHOICE_TAGGED
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
_VFX_TOP_LEVEL_KEYS = {"action", "target", "searchMethod", "properties"}
|
|
@@ -49,7 +51,8 @@ def particle():
|
|
|
49
51
|
|
|
50
52
|
@particle.command("info")
|
|
51
53
|
@click.argument("target")
|
|
52
|
-
@click.option("--search-method", type=
|
|
54
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
55
|
+
@handle_unity_errors
|
|
53
56
|
def particle_info(target: str, search_method: Optional[str]):
|
|
54
57
|
"""Get particle system info.
|
|
55
58
|
|
|
@@ -63,19 +66,16 @@ def particle_info(target: str, search_method: Optional[str]):
|
|
|
63
66
|
if search_method:
|
|
64
67
|
params["searchMethod"] = search_method
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
click.echo(format_output(result, config.format))
|
|
70
|
-
except UnityConnectionError as e:
|
|
71
|
-
print_error(str(e))
|
|
72
|
-
sys.exit(1)
|
|
69
|
+
result = run_command(
|
|
70
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
71
|
+
click.echo(format_output(result, config.format))
|
|
73
72
|
|
|
74
73
|
|
|
75
74
|
@particle.command("play")
|
|
76
75
|
@click.argument("target")
|
|
77
76
|
@click.option("--with-children", is_flag=True, help="Also play child particle systems.")
|
|
78
|
-
@click.option("--search-method", type=
|
|
77
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
78
|
+
@handle_unity_errors
|
|
79
79
|
def particle_play(target: str, with_children: bool, search_method: Optional[str]):
|
|
80
80
|
"""Play a particle system.
|
|
81
81
|
|
|
@@ -91,21 +91,18 @@ def particle_play(target: str, with_children: bool, search_method: Optional[str]
|
|
|
91
91
|
if search_method:
|
|
92
92
|
params["searchMethod"] = search_method
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
print_success(f"Playing particle system: {target}")
|
|
100
|
-
except UnityConnectionError as e:
|
|
101
|
-
print_error(str(e))
|
|
102
|
-
sys.exit(1)
|
|
94
|
+
result = run_command(
|
|
95
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
96
|
+
click.echo(format_output(result, config.format))
|
|
97
|
+
if result.get("success"):
|
|
98
|
+
print_success(f"Playing particle system: {target}")
|
|
103
99
|
|
|
104
100
|
|
|
105
101
|
@particle.command("stop")
|
|
106
102
|
@click.argument("target")
|
|
107
103
|
@click.option("--with-children", is_flag=True, help="Also stop child particle systems.")
|
|
108
|
-
@click.option("--search-method", type=
|
|
104
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
105
|
+
@handle_unity_errors
|
|
109
106
|
def particle_stop(target: str, with_children: bool, search_method: Optional[str]):
|
|
110
107
|
"""Stop a particle system."""
|
|
111
108
|
config = get_config()
|
|
@@ -115,20 +112,17 @@ def particle_stop(target: str, with_children: bool, search_method: Optional[str]
|
|
|
115
112
|
if search_method:
|
|
116
113
|
params["searchMethod"] = search_method
|
|
117
114
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
print_success(f"Stopped particle system: {target}")
|
|
124
|
-
except UnityConnectionError as e:
|
|
125
|
-
print_error(str(e))
|
|
126
|
-
sys.exit(1)
|
|
115
|
+
result = run_command(
|
|
116
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
117
|
+
click.echo(format_output(result, config.format))
|
|
118
|
+
if result.get("success"):
|
|
119
|
+
print_success(f"Stopped particle system: {target}")
|
|
127
120
|
|
|
128
121
|
|
|
129
122
|
@particle.command("pause")
|
|
130
123
|
@click.argument("target")
|
|
131
|
-
@click.option("--search-method", type=
|
|
124
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
125
|
+
@handle_unity_errors
|
|
132
126
|
def particle_pause(target: str, search_method: Optional[str]):
|
|
133
127
|
"""Pause a particle system."""
|
|
134
128
|
config = get_config()
|
|
@@ -136,19 +130,16 @@ def particle_pause(target: str, search_method: Optional[str]):
|
|
|
136
130
|
if search_method:
|
|
137
131
|
params["searchMethod"] = search_method
|
|
138
132
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
click.echo(format_output(result, config.format))
|
|
143
|
-
except UnityConnectionError as e:
|
|
144
|
-
print_error(str(e))
|
|
145
|
-
sys.exit(1)
|
|
133
|
+
result = run_command(
|
|
134
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
135
|
+
click.echo(format_output(result, config.format))
|
|
146
136
|
|
|
147
137
|
|
|
148
138
|
@particle.command("restart")
|
|
149
139
|
@click.argument("target")
|
|
150
140
|
@click.option("--with-children", is_flag=True)
|
|
151
|
-
@click.option("--search-method", type=
|
|
141
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
142
|
+
@handle_unity_errors
|
|
152
143
|
def particle_restart(target: str, with_children: bool, search_method: Optional[str]):
|
|
153
144
|
"""Restart a particle system."""
|
|
154
145
|
config = get_config()
|
|
@@ -158,19 +149,16 @@ def particle_restart(target: str, with_children: bool, search_method: Optional[s
|
|
|
158
149
|
if search_method:
|
|
159
150
|
params["searchMethod"] = search_method
|
|
160
151
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
click.echo(format_output(result, config.format))
|
|
165
|
-
except UnityConnectionError as e:
|
|
166
|
-
print_error(str(e))
|
|
167
|
-
sys.exit(1)
|
|
152
|
+
result = run_command(
|
|
153
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
154
|
+
click.echo(format_output(result, config.format))
|
|
168
155
|
|
|
169
156
|
|
|
170
157
|
@particle.command("clear")
|
|
171
158
|
@click.argument("target")
|
|
172
159
|
@click.option("--with-children", is_flag=True)
|
|
173
|
-
@click.option("--search-method", type=
|
|
160
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
161
|
+
@handle_unity_errors
|
|
174
162
|
def particle_clear(target: str, with_children: bool, search_method: Optional[str]):
|
|
175
163
|
"""Clear all particles from a particle system."""
|
|
176
164
|
config = get_config()
|
|
@@ -180,13 +168,9 @@ def particle_clear(target: str, with_children: bool, search_method: Optional[str
|
|
|
180
168
|
if search_method:
|
|
181
169
|
params["searchMethod"] = search_method
|
|
182
170
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
click.echo(format_output(result, config.format))
|
|
187
|
-
except UnityConnectionError as e:
|
|
188
|
-
print_error(str(e))
|
|
189
|
-
sys.exit(1)
|
|
171
|
+
result = run_command(
|
|
172
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
173
|
+
click.echo(format_output(result, config.format))
|
|
190
174
|
|
|
191
175
|
|
|
192
176
|
# =============================================================================
|
|
@@ -201,7 +185,8 @@ def line():
|
|
|
201
185
|
|
|
202
186
|
@line.command("info")
|
|
203
187
|
@click.argument("target")
|
|
204
|
-
@click.option("--search-method", type=
|
|
188
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
189
|
+
@handle_unity_errors
|
|
205
190
|
def line_info(target: str, search_method: Optional[str]):
|
|
206
191
|
"""Get line renderer info.
|
|
207
192
|
|
|
@@ -214,19 +199,16 @@ def line_info(target: str, search_method: Optional[str]):
|
|
|
214
199
|
if search_method:
|
|
215
200
|
params["searchMethod"] = search_method
|
|
216
201
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
click.echo(format_output(result, config.format))
|
|
221
|
-
except UnityConnectionError as e:
|
|
222
|
-
print_error(str(e))
|
|
223
|
-
sys.exit(1)
|
|
202
|
+
result = run_command(
|
|
203
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
204
|
+
click.echo(format_output(result, config.format))
|
|
224
205
|
|
|
225
206
|
|
|
226
207
|
@line.command("set-positions")
|
|
227
208
|
@click.argument("target")
|
|
228
209
|
@click.option("--positions", "-p", required=True, help='Positions as JSON array: [[0,0,0], [1,1,1], [2,0,0]]')
|
|
229
|
-
@click.option("--search-method", type=
|
|
210
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
211
|
+
@handle_unity_errors
|
|
230
212
|
def line_set_positions(target: str, positions: str, search_method: Optional[str]):
|
|
231
213
|
"""Set all positions on a line renderer.
|
|
232
214
|
|
|
@@ -236,11 +218,7 @@ def line_set_positions(target: str, positions: str, search_method: Optional[str]
|
|
|
236
218
|
"""
|
|
237
219
|
config = get_config()
|
|
238
220
|
|
|
239
|
-
|
|
240
|
-
positions_list = json.loads(positions)
|
|
241
|
-
except json.JSONDecodeError as e:
|
|
242
|
-
print_error(f"Invalid JSON for positions: {e}")
|
|
243
|
-
sys.exit(1)
|
|
221
|
+
positions_list = parse_json_list_or_exit(positions, "positions")
|
|
244
222
|
|
|
245
223
|
params: dict[str, Any] = {
|
|
246
224
|
"action": "line_set_positions",
|
|
@@ -250,20 +228,17 @@ def line_set_positions(target: str, positions: str, search_method: Optional[str]
|
|
|
250
228
|
if search_method:
|
|
251
229
|
params["searchMethod"] = search_method
|
|
252
230
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
click.echo(format_output(result, config.format))
|
|
257
|
-
except UnityConnectionError as e:
|
|
258
|
-
print_error(str(e))
|
|
259
|
-
sys.exit(1)
|
|
231
|
+
result = run_command(
|
|
232
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
233
|
+
click.echo(format_output(result, config.format))
|
|
260
234
|
|
|
261
235
|
|
|
262
236
|
@line.command("create-line")
|
|
263
237
|
@click.argument("target")
|
|
264
238
|
@click.option("--start", nargs=3, type=float, required=True, help="Start point X Y Z")
|
|
265
239
|
@click.option("--end", nargs=3, type=float, required=True, help="End point X Y Z")
|
|
266
|
-
@click.option("--search-method", type=
|
|
240
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
241
|
+
@handle_unity_errors
|
|
267
242
|
def line_create_line(target: str, start: Tuple[float, float, float], end: Tuple[float, float, float], search_method: Optional[str]):
|
|
268
243
|
"""Create a simple line between two points.
|
|
269
244
|
|
|
@@ -281,13 +256,9 @@ def line_create_line(target: str, start: Tuple[float, float, float], end: Tuple[
|
|
|
281
256
|
if search_method:
|
|
282
257
|
params["searchMethod"] = search_method
|
|
283
258
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
click.echo(format_output(result, config.format))
|
|
288
|
-
except UnityConnectionError as e:
|
|
289
|
-
print_error(str(e))
|
|
290
|
-
sys.exit(1)
|
|
259
|
+
result = run_command(
|
|
260
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
261
|
+
click.echo(format_output(result, config.format))
|
|
291
262
|
|
|
292
263
|
|
|
293
264
|
@line.command("create-circle")
|
|
@@ -295,7 +266,8 @@ def line_create_line(target: str, start: Tuple[float, float, float], end: Tuple[
|
|
|
295
266
|
@click.option("--center", nargs=3, type=float, default=(0, 0, 0), help="Center point X Y Z")
|
|
296
267
|
@click.option("--radius", type=float, required=True, help="Circle radius")
|
|
297
268
|
@click.option("--segments", type=int, default=32, help="Number of segments")
|
|
298
|
-
@click.option("--search-method", type=
|
|
269
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
270
|
+
@handle_unity_errors
|
|
299
271
|
def line_create_circle(target: str, center: Tuple[float, float, float], radius: float, segments: int, search_method: Optional[str]):
|
|
300
272
|
"""Create a circle shape.
|
|
301
273
|
|
|
@@ -315,18 +287,15 @@ def line_create_circle(target: str, center: Tuple[float, float, float], radius:
|
|
|
315
287
|
if search_method:
|
|
316
288
|
params["searchMethod"] = search_method
|
|
317
289
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
click.echo(format_output(result, config.format))
|
|
322
|
-
except UnityConnectionError as e:
|
|
323
|
-
print_error(str(e))
|
|
324
|
-
sys.exit(1)
|
|
290
|
+
result = run_command(
|
|
291
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
292
|
+
click.echo(format_output(result, config.format))
|
|
325
293
|
|
|
326
294
|
|
|
327
295
|
@line.command("clear")
|
|
328
296
|
@click.argument("target")
|
|
329
|
-
@click.option("--search-method", type=
|
|
297
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
298
|
+
@handle_unity_errors
|
|
330
299
|
def line_clear(target: str, search_method: Optional[str]):
|
|
331
300
|
"""Clear all positions from a line renderer."""
|
|
332
301
|
config = get_config()
|
|
@@ -334,13 +303,9 @@ def line_clear(target: str, search_method: Optional[str]):
|
|
|
334
303
|
if search_method:
|
|
335
304
|
params["searchMethod"] = search_method
|
|
336
305
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
click.echo(format_output(result, config.format))
|
|
341
|
-
except UnityConnectionError as e:
|
|
342
|
-
print_error(str(e))
|
|
343
|
-
sys.exit(1)
|
|
306
|
+
result = run_command(
|
|
307
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
308
|
+
click.echo(format_output(result, config.format))
|
|
344
309
|
|
|
345
310
|
|
|
346
311
|
# =============================================================================
|
|
@@ -355,7 +320,8 @@ def trail():
|
|
|
355
320
|
|
|
356
321
|
@trail.command("info")
|
|
357
322
|
@click.argument("target")
|
|
358
|
-
@click.option("--search-method", type=
|
|
323
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
324
|
+
@handle_unity_errors
|
|
359
325
|
def trail_info(target: str, search_method: Optional[str]):
|
|
360
326
|
"""Get trail renderer info."""
|
|
361
327
|
config = get_config()
|
|
@@ -363,19 +329,16 @@ def trail_info(target: str, search_method: Optional[str]):
|
|
|
363
329
|
if search_method:
|
|
364
330
|
params["searchMethod"] = search_method
|
|
365
331
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
click.echo(format_output(result, config.format))
|
|
370
|
-
except UnityConnectionError as e:
|
|
371
|
-
print_error(str(e))
|
|
372
|
-
sys.exit(1)
|
|
332
|
+
result = run_command(
|
|
333
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
334
|
+
click.echo(format_output(result, config.format))
|
|
373
335
|
|
|
374
336
|
|
|
375
337
|
@trail.command("set-time")
|
|
376
338
|
@click.argument("target")
|
|
377
339
|
@click.argument("duration", type=float)
|
|
378
|
-
@click.option("--search-method", type=
|
|
340
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
341
|
+
@handle_unity_errors
|
|
379
342
|
def trail_set_time(target: str, duration: float, search_method: Optional[str]):
|
|
380
343
|
"""Set trail duration.
|
|
381
344
|
|
|
@@ -392,18 +355,15 @@ def trail_set_time(target: str, duration: float, search_method: Optional[str]):
|
|
|
392
355
|
if search_method:
|
|
393
356
|
params["searchMethod"] = search_method
|
|
394
357
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
click.echo(format_output(result, config.format))
|
|
399
|
-
except UnityConnectionError as e:
|
|
400
|
-
print_error(str(e))
|
|
401
|
-
sys.exit(1)
|
|
358
|
+
result = run_command(
|
|
359
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
360
|
+
click.echo(format_output(result, config.format))
|
|
402
361
|
|
|
403
362
|
|
|
404
363
|
@trail.command("clear")
|
|
405
364
|
@click.argument("target")
|
|
406
|
-
@click.option("--search-method", type=
|
|
365
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
366
|
+
@handle_unity_errors
|
|
407
367
|
def trail_clear(target: str, search_method: Optional[str]):
|
|
408
368
|
"""Clear a trail renderer."""
|
|
409
369
|
config = get_config()
|
|
@@ -411,13 +371,9 @@ def trail_clear(target: str, search_method: Optional[str]):
|
|
|
411
371
|
if search_method:
|
|
412
372
|
params["searchMethod"] = search_method
|
|
413
373
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
click.echo(format_output(result, config.format))
|
|
418
|
-
except UnityConnectionError as e:
|
|
419
|
-
print_error(str(e))
|
|
420
|
-
sys.exit(1)
|
|
374
|
+
result = run_command(
|
|
375
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
376
|
+
click.echo(format_output(result, config.format))
|
|
421
377
|
|
|
422
378
|
|
|
423
379
|
# =============================================================================
|
|
@@ -428,7 +384,8 @@ def trail_clear(target: str, search_method: Optional[str]):
|
|
|
428
384
|
@click.argument("action")
|
|
429
385
|
@click.argument("target", required=False)
|
|
430
386
|
@click.option("--params", "-p", default="{}", help="Additional parameters as JSON.")
|
|
431
|
-
@click.option("--search-method", type=
|
|
387
|
+
@click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
|
|
388
|
+
@handle_unity_errors
|
|
432
389
|
def vfx_raw(action: str, target: Optional[str], params: str, search_method: Optional[str]):
|
|
433
390
|
"""Execute any VFX action directly.
|
|
434
391
|
|
|
@@ -449,14 +406,7 @@ def vfx_raw(action: str, target: Optional[str], params: str, search_method: Opti
|
|
|
449
406
|
"""
|
|
450
407
|
config = get_config()
|
|
451
408
|
|
|
452
|
-
|
|
453
|
-
extra_params = json.loads(params)
|
|
454
|
-
except json.JSONDecodeError as e:
|
|
455
|
-
print_error(f"Invalid JSON for params: {e}")
|
|
456
|
-
sys.exit(1)
|
|
457
|
-
if not isinstance(extra_params, dict):
|
|
458
|
-
print_error("Invalid JSON for params: expected an object")
|
|
459
|
-
sys.exit(1)
|
|
409
|
+
extra_params = parse_json_dict_or_exit(params, "params")
|
|
460
410
|
|
|
461
411
|
request_params: dict[str, Any] = {"action": action}
|
|
462
412
|
if target:
|
|
@@ -466,10 +416,6 @@ def vfx_raw(action: str, target: Optional[str], params: str, search_method: Opti
|
|
|
466
416
|
|
|
467
417
|
# Merge extra params
|
|
468
418
|
request_params.update(extra_params)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
click.echo(format_output(result, config.format))
|
|
473
|
-
except UnityConnectionError as e:
|
|
474
|
-
print_error(str(e))
|
|
475
|
-
sys.exit(1)
|
|
419
|
+
result = run_command(
|
|
420
|
+
"manage_vfx", _normalize_vfx_params(request_params), config)
|
|
421
|
+
click.echo(format_output(result, config.format))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Confirmation dialog utilities for CLI commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def confirm_destructive_action(
|
|
7
|
+
action: str,
|
|
8
|
+
item_type: str,
|
|
9
|
+
item_name: str,
|
|
10
|
+
force: bool,
|
|
11
|
+
extra_context: str = ""
|
|
12
|
+
) -> None:
|
|
13
|
+
"""Prompt user to confirm destructive action unless --force flag is set.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
action: The action being performed (e.g., "Delete", "Remove")
|
|
17
|
+
item_type: The type of item (e.g., "script", "GameObject", "asset")
|
|
18
|
+
item_name: The name/path of the item
|
|
19
|
+
force: If True, skip confirmation prompt
|
|
20
|
+
extra_context: Optional additional context (e.g., "from 'Player'")
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
click.Abort: If user declines confirmation
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
confirm_destructive_action("Delete", "script", "MyScript.cs", force=False)
|
|
27
|
+
# Prompts: "Delete script 'MyScript.cs'?"
|
|
28
|
+
|
|
29
|
+
confirm_destructive_action("Remove", "Rigidbody", "Player", force=False, extra_context="from")
|
|
30
|
+
# Prompts: "Remove Rigidbody from 'Player'?"
|
|
31
|
+
"""
|
|
32
|
+
if not force:
|
|
33
|
+
if extra_context:
|
|
34
|
+
message = f"{action} {item_type} {extra_context} '{item_name}'?"
|
|
35
|
+
else:
|
|
36
|
+
message = f"{action} {item_type} '{item_name}'?"
|
|
37
|
+
click.confirm(message, abort=True)
|
cli/utils/connection.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Connection utilities for CLI to communicate with Unity via MCP server."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import
|
|
4
|
+
import functools
|
|
5
5
|
import sys
|
|
6
|
-
from typing import Any, Dict, Optional
|
|
6
|
+
from typing import Any, Callable, Dict, Optional, TypeVar
|
|
7
7
|
|
|
8
8
|
import httpx
|
|
9
9
|
|
|
@@ -15,6 +15,36 @@ class UnityConnectionError(Exception):
|
|
|
15
15
|
pass
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handle_unity_errors(func: F) -> F:
|
|
22
|
+
"""Decorator that handles UnityConnectionError consistently.
|
|
23
|
+
|
|
24
|
+
Wraps a CLI command function and catches UnityConnectionError,
|
|
25
|
+
printing a formatted error message and exiting with code 1.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
@scene.command("active")
|
|
29
|
+
@handle_unity_errors
|
|
30
|
+
def active():
|
|
31
|
+
config = get_config()
|
|
32
|
+
result = run_command("manage_scene", {"action": "get_active"}, config)
|
|
33
|
+
click.echo(format_output(result, config.format))
|
|
34
|
+
"""
|
|
35
|
+
from cli.utils.output import print_error
|
|
36
|
+
|
|
37
|
+
@functools.wraps(func)
|
|
38
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
39
|
+
try:
|
|
40
|
+
return func(*args, **kwargs)
|
|
41
|
+
except UnityConnectionError as e:
|
|
42
|
+
print_error(str(e))
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
return wrapper # type: ignore[return-value]
|
|
46
|
+
|
|
47
|
+
|
|
18
48
|
def warn_if_remote_host(config: CLIConfig) -> None:
|
|
19
49
|
"""Warn user if connecting to a non-localhost server.
|
|
20
50
|
|
cli/utils/constants.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Common constants for CLI commands."""
|
|
2
|
+
import click
|
|
3
|
+
|
|
4
|
+
# Search method constants used across various CLI commands
|
|
5
|
+
# These define how GameObjects and other Unity objects can be located
|
|
6
|
+
|
|
7
|
+
# Full set of search methods (used by gameobject commands)
|
|
8
|
+
SEARCH_METHODS_FULL = ["by_name", "by_path", "by_id", "by_tag", "by_layer", "by_component"]
|
|
9
|
+
|
|
10
|
+
# Basic search methods (used by component, animation, audio commands)
|
|
11
|
+
SEARCH_METHODS_BASIC = ["by_id", "by_name", "by_path"]
|
|
12
|
+
|
|
13
|
+
# Extended search methods for renderer-based commands (material commands)
|
|
14
|
+
SEARCH_METHODS_RENDERER = ["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"]
|
|
15
|
+
|
|
16
|
+
# Tagged search methods (used by VFX commands)
|
|
17
|
+
SEARCH_METHODS_TAGGED = ["by_name", "by_path", "by_id", "by_tag"]
|
|
18
|
+
|
|
19
|
+
# Click choice options for each set
|
|
20
|
+
SEARCH_METHOD_CHOICE_FULL = click.Choice(SEARCH_METHODS_FULL)
|
|
21
|
+
SEARCH_METHOD_CHOICE_BASIC = click.Choice(SEARCH_METHODS_BASIC)
|
|
22
|
+
SEARCH_METHOD_CHOICE_RENDERER = click.Choice(SEARCH_METHODS_RENDERER)
|
|
23
|
+
SEARCH_METHOD_CHOICE_TAGGED = click.Choice(SEARCH_METHODS_TAGGED)
|
cli/utils/parsers.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""JSON and value parsing utilities for CLI commands."""
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from cli.utils.output import print_error, print_info
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_value_safe(value: str) -> Any:
|
|
10
|
+
"""Parse a value, trying JSON → float → string fallback.
|
|
11
|
+
|
|
12
|
+
This is used for property values that could be JSON objects/arrays,
|
|
13
|
+
numbers, or strings. Never raises an exception.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
value: The string value to parse
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Parsed JSON object/array, float, or original string
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
>>> parse_value_safe('{"x": 1}')
|
|
23
|
+
{'x': 1}
|
|
24
|
+
>>> parse_value_safe('3.14')
|
|
25
|
+
3.14
|
|
26
|
+
>>> parse_value_safe('hello')
|
|
27
|
+
'hello'
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
return json.loads(value)
|
|
31
|
+
except json.JSONDecodeError:
|
|
32
|
+
# Try to parse as number
|
|
33
|
+
try:
|
|
34
|
+
return float(value)
|
|
35
|
+
except ValueError:
|
|
36
|
+
# Keep as string
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_json_or_exit(value: str, context: str = "parameter") -> Any:
|
|
41
|
+
"""Parse JSON string, trying to fix common issues, or exit with error.
|
|
42
|
+
|
|
43
|
+
Attempts to parse JSON with automatic fixes for:
|
|
44
|
+
- Single quotes instead of double quotes
|
|
45
|
+
- Python-style True/False instead of true/false
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
value: The JSON string to parse
|
|
49
|
+
context: Description of what's being parsed (for error messages)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Parsed JSON object
|
|
53
|
+
|
|
54
|
+
Exits:
|
|
55
|
+
Calls sys.exit(1) if JSON is invalid after attempting fixes
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
return json.loads(value)
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
# Try to fix common shell quoting issues (single quotes, Python bools)
|
|
61
|
+
try:
|
|
62
|
+
fixed = value.replace("'", '"').replace("True", "true").replace("False", "false")
|
|
63
|
+
return json.loads(fixed)
|
|
64
|
+
except json.JSONDecodeError as e:
|
|
65
|
+
print_error(f"Invalid JSON for {context}: {e}")
|
|
66
|
+
print_info("Example: --params '{\"key\":\"value\"}'")
|
|
67
|
+
print_info("Tip: wrap JSON in single quotes to avoid shell escaping issues.")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_json_dict_or_exit(value: str, context: str = "parameter") -> dict[str, Any]:
|
|
72
|
+
"""Parse JSON object (dict), or exit with error.
|
|
73
|
+
|
|
74
|
+
Like parse_json_or_exit, but ensures result is a dictionary.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
value: The JSON string to parse
|
|
78
|
+
context: Description of what's being parsed (for error messages)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Parsed JSON object as dictionary
|
|
82
|
+
|
|
83
|
+
Exits:
|
|
84
|
+
Calls sys.exit(1) if JSON is invalid or not an object
|
|
85
|
+
"""
|
|
86
|
+
result = parse_json_or_exit(value, context)
|
|
87
|
+
if not isinstance(result, dict):
|
|
88
|
+
print_error(f"Invalid JSON for {context}: expected an object, got {type(result).__name__}")
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_json_list_or_exit(value: str, context: str = "parameter") -> list[Any]:
|
|
94
|
+
"""Parse JSON array (list), or exit with error.
|
|
95
|
+
|
|
96
|
+
Like parse_json_or_exit, but ensures result is a list.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
value: The JSON string to parse
|
|
100
|
+
context: Description of what's being parsed (for error messages)
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Parsed JSON array as list
|
|
104
|
+
|
|
105
|
+
Exits:
|
|
106
|
+
Calls sys.exit(1) if JSON is invalid or not an array
|
|
107
|
+
"""
|
|
108
|
+
result = parse_json_or_exit(value, context)
|
|
109
|
+
if not isinstance(result, list):
|
|
110
|
+
print_error(f"Invalid JSON for {context}: expected an array, got {type(result).__name__}")
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
return result
|
core/config.py
CHANGED
|
@@ -47,10 +47,6 @@ class ServerConfig:
|
|
|
47
47
|
# Align with telemetry.py default Cloud Run endpoint
|
|
48
48
|
telemetry_endpoint: str = "https://api-prod.coplay.dev/telemetry/events"
|
|
49
49
|
|
|
50
|
-
def configure_logging(self) -> None:
|
|
51
|
-
level = getattr(logging, self.log_level, logging.INFO)
|
|
52
|
-
logging.basicConfig(level=level, format=self.log_format)
|
|
53
|
-
|
|
54
50
|
|
|
55
51
|
# Create a global config instance
|
|
56
52
|
config = ServerConfig()
|