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.
Files changed (61) hide show
  1. cli/commands/animation.py +6 -9
  2. cli/commands/asset.py +50 -80
  3. cli/commands/audio.py +14 -22
  4. cli/commands/batch.py +20 -33
  5. cli/commands/code.py +63 -70
  6. cli/commands/component.py +33 -55
  7. cli/commands/editor.py +122 -188
  8. cli/commands/gameobject.py +60 -83
  9. cli/commands/instance.py +28 -36
  10. cli/commands/lighting.py +54 -59
  11. cli/commands/material.py +39 -68
  12. cli/commands/prefab.py +63 -81
  13. cli/commands/scene.py +30 -54
  14. cli/commands/script.py +32 -50
  15. cli/commands/shader.py +43 -55
  16. cli/commands/texture.py +53 -51
  17. cli/commands/tool.py +24 -27
  18. cli/commands/ui.py +125 -130
  19. cli/commands/vfx.py +84 -138
  20. cli/utils/confirmation.py +37 -0
  21. cli/utils/connection.py +32 -2
  22. cli/utils/constants.py +23 -0
  23. cli/utils/parsers.py +112 -0
  24. core/config.py +0 -4
  25. core/telemetry.py +20 -2
  26. {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/METADATA +21 -1
  27. mcpforunityserver-9.3.0b20260129121506.dist-info/RECORD +103 -0
  28. services/resources/active_tool.py +1 -1
  29. services/resources/custom_tools.py +1 -1
  30. services/resources/editor_state.py +1 -1
  31. services/resources/gameobject.py +4 -4
  32. services/resources/layers.py +1 -1
  33. services/resources/menu_items.py +1 -1
  34. services/resources/prefab.py +3 -3
  35. services/resources/prefab_stage.py +1 -1
  36. services/resources/project_info.py +1 -1
  37. services/resources/selection.py +1 -1
  38. services/resources/tags.py +1 -1
  39. services/resources/tests.py +40 -8
  40. services/resources/unity_instances.py +1 -1
  41. services/resources/windows.py +1 -1
  42. services/tools/__init__.py +3 -1
  43. services/tools/find_gameobjects.py +32 -11
  44. services/tools/manage_gameobject.py +11 -66
  45. services/tools/manage_material.py +4 -37
  46. services/tools/manage_prefabs.py +51 -7
  47. services/tools/manage_script.py +1 -1
  48. services/tools/manage_texture.py +10 -96
  49. services/tools/run_tests.py +67 -4
  50. services/tools/utils.py +217 -0
  51. transport/models.py +1 -0
  52. transport/plugin_hub.py +2 -1
  53. transport/plugin_registry.py +3 -0
  54. transport/unity_transport.py +0 -51
  55. utils/focus_nudge.py +291 -23
  56. mcpforunityserver-9.3.0b20260128055651.dist-info/RECORD +0 -101
  57. utils/reload_sentinel.py +0 -9
  58. {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/WHEEL +0 -0
  59. {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/entry_points.txt +0 -0
  60. {mcpforunityserver-9.3.0b20260128055651.dist-info → mcpforunityserver-9.3.0b20260129121506.dist-info}/licenses/LICENSE +0 -0
  61. {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, UnityConnectionError
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
67
- result = run_command(
68
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
95
- result = run_command(
96
- "manage_vfx", _normalize_vfx_params(params), config)
97
- click.echo(format_output(result, config.format))
98
- if result.get("success"):
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
119
- result = run_command(
120
- "manage_vfx", _normalize_vfx_params(params), config)
121
- click.echo(format_output(result, config.format))
122
- if result.get("success"):
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
140
- result = run_command(
141
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
162
- result = run_command(
163
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
184
- result = run_command(
185
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
218
- result = run_command(
219
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
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
- try:
254
- result = run_command(
255
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
285
- result = run_command(
286
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
319
- result = run_command(
320
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
338
- result = run_command(
339
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
367
- result = run_command(
368
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
396
- result = run_command(
397
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
415
- result = run_command(
416
- "manage_vfx", _normalize_vfx_params(params), config)
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=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
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
- try:
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
- try:
470
- result = run_command(
471
- "manage_vfx", _normalize_vfx_params(request_params), config)
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 json
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()