mcpforunityserver 9.4.0b20260203025228__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 (105) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +3 -0
  3. cli/commands/animation.py +84 -0
  4. cli/commands/asset.py +280 -0
  5. cli/commands/audio.py +125 -0
  6. cli/commands/batch.py +171 -0
  7. cli/commands/code.py +182 -0
  8. cli/commands/component.py +190 -0
  9. cli/commands/editor.py +447 -0
  10. cli/commands/gameobject.py +487 -0
  11. cli/commands/instance.py +93 -0
  12. cli/commands/lighting.py +123 -0
  13. cli/commands/material.py +239 -0
  14. cli/commands/prefab.py +248 -0
  15. cli/commands/scene.py +231 -0
  16. cli/commands/script.py +222 -0
  17. cli/commands/shader.py +226 -0
  18. cli/commands/texture.py +540 -0
  19. cli/commands/tool.py +58 -0
  20. cli/commands/ui.py +258 -0
  21. cli/commands/vfx.py +421 -0
  22. cli/main.py +281 -0
  23. cli/utils/__init__.py +31 -0
  24. cli/utils/config.py +58 -0
  25. cli/utils/confirmation.py +37 -0
  26. cli/utils/connection.py +254 -0
  27. cli/utils/constants.py +23 -0
  28. cli/utils/output.py +195 -0
  29. cli/utils/parsers.py +112 -0
  30. cli/utils/suggestions.py +34 -0
  31. core/__init__.py +0 -0
  32. core/config.py +67 -0
  33. core/constants.py +4 -0
  34. core/logging_decorator.py +37 -0
  35. core/telemetry.py +551 -0
  36. core/telemetry_decorator.py +164 -0
  37. main.py +845 -0
  38. mcpforunityserver-9.4.0b20260203025228.dist-info/METADATA +328 -0
  39. mcpforunityserver-9.4.0b20260203025228.dist-info/RECORD +105 -0
  40. mcpforunityserver-9.4.0b20260203025228.dist-info/WHEEL +5 -0
  41. mcpforunityserver-9.4.0b20260203025228.dist-info/entry_points.txt +3 -0
  42. mcpforunityserver-9.4.0b20260203025228.dist-info/licenses/LICENSE +21 -0
  43. mcpforunityserver-9.4.0b20260203025228.dist-info/top_level.txt +7 -0
  44. models/__init__.py +4 -0
  45. models/models.py +56 -0
  46. models/unity_response.py +70 -0
  47. services/__init__.py +0 -0
  48. services/api_key_service.py +235 -0
  49. services/custom_tool_service.py +499 -0
  50. services/registry/__init__.py +22 -0
  51. services/registry/resource_registry.py +53 -0
  52. services/registry/tool_registry.py +51 -0
  53. services/resources/__init__.py +86 -0
  54. services/resources/active_tool.py +48 -0
  55. services/resources/custom_tools.py +57 -0
  56. services/resources/editor_state.py +304 -0
  57. services/resources/gameobject.py +243 -0
  58. services/resources/layers.py +30 -0
  59. services/resources/menu_items.py +35 -0
  60. services/resources/prefab.py +191 -0
  61. services/resources/prefab_stage.py +40 -0
  62. services/resources/project_info.py +40 -0
  63. services/resources/selection.py +56 -0
  64. services/resources/tags.py +31 -0
  65. services/resources/tests.py +88 -0
  66. services/resources/unity_instances.py +125 -0
  67. services/resources/windows.py +48 -0
  68. services/state/external_changes_scanner.py +245 -0
  69. services/tools/__init__.py +83 -0
  70. services/tools/batch_execute.py +93 -0
  71. services/tools/debug_request_context.py +86 -0
  72. services/tools/execute_custom_tool.py +43 -0
  73. services/tools/execute_menu_item.py +32 -0
  74. services/tools/find_gameobjects.py +110 -0
  75. services/tools/find_in_file.py +181 -0
  76. services/tools/manage_asset.py +119 -0
  77. services/tools/manage_components.py +131 -0
  78. services/tools/manage_editor.py +64 -0
  79. services/tools/manage_gameobject.py +260 -0
  80. services/tools/manage_material.py +111 -0
  81. services/tools/manage_prefabs.py +209 -0
  82. services/tools/manage_scene.py +111 -0
  83. services/tools/manage_script.py +645 -0
  84. services/tools/manage_scriptable_object.py +87 -0
  85. services/tools/manage_shader.py +71 -0
  86. services/tools/manage_texture.py +581 -0
  87. services/tools/manage_vfx.py +120 -0
  88. services/tools/preflight.py +110 -0
  89. services/tools/read_console.py +151 -0
  90. services/tools/refresh_unity.py +153 -0
  91. services/tools/run_tests.py +317 -0
  92. services/tools/script_apply_edits.py +1006 -0
  93. services/tools/set_active_instance.py +120 -0
  94. services/tools/utils.py +348 -0
  95. transport/__init__.py +0 -0
  96. transport/legacy/port_discovery.py +329 -0
  97. transport/legacy/stdio_port_registry.py +65 -0
  98. transport/legacy/unity_connection.py +910 -0
  99. transport/models.py +68 -0
  100. transport/plugin_hub.py +787 -0
  101. transport/plugin_registry.py +182 -0
  102. transport/unity_instance_middleware.py +262 -0
  103. transport/unity_transport.py +94 -0
  104. utils/focus_nudge.py +589 -0
  105. utils/module_discovery.py +55 -0
@@ -0,0 +1,487 @@
1
+ """GameObject CLI commands."""
2
+
3
+ import sys
4
+ import json
5
+ import click
6
+ from typing import Optional, Tuple, Any
7
+
8
+ from cli.utils.config import get_config
9
+ from cli.utils.output import format_output, print_error, print_success, print_warning
10
+ from cli.utils.connection import run_command, handle_unity_errors, UnityConnectionError
11
+ from cli.utils.constants import SEARCH_METHOD_CHOICE_FULL, SEARCH_METHOD_CHOICE_TAGGED
12
+ from cli.utils.confirmation import confirm_destructive_action
13
+
14
+
15
+ @click.group()
16
+ def gameobject():
17
+ """GameObject operations - create, find, modify, delete GameObjects."""
18
+ pass
19
+
20
+
21
+ @gameobject.command("find")
22
+ @click.argument("search_term")
23
+ @click.option(
24
+ "--method", "-m",
25
+ type=SEARCH_METHOD_CHOICE_FULL,
26
+ default="by_name",
27
+ help="Search method."
28
+ )
29
+ @click.option(
30
+ "--include-inactive", "-i",
31
+ is_flag=True,
32
+ help="Include inactive GameObjects."
33
+ )
34
+ @click.option(
35
+ "--limit", "-l",
36
+ default=50,
37
+ type=int,
38
+ help="Maximum results to return."
39
+ )
40
+ @click.option(
41
+ "--cursor", "-c",
42
+ default=0,
43
+ type=int,
44
+ help="Pagination cursor (offset)."
45
+ )
46
+ @handle_unity_errors
47
+ def find(search_term: str, method: str, include_inactive: bool, limit: int, cursor: int):
48
+ """Find GameObjects by search criteria.
49
+
50
+ \b
51
+ Examples:
52
+ unity-mcp gameobject find "Player"
53
+ unity-mcp gameobject find "Enemy" --method by_tag
54
+ unity-mcp gameobject find "-81840" --method by_id
55
+ unity-mcp gameobject find "Rigidbody" --method by_component
56
+ unity-mcp gameobject find "/Canvas/Panel" --method by_path
57
+ """
58
+ config = get_config()
59
+ result = run_command("find_gameobjects", {
60
+ "searchMethod": method,
61
+ "searchTerm": search_term,
62
+ "includeInactive": include_inactive,
63
+ "pageSize": limit,
64
+ "cursor": cursor,
65
+ }, config)
66
+ click.echo(format_output(result, config.format))
67
+
68
+
69
+ @gameobject.command("create")
70
+ @click.argument("name")
71
+ @click.option(
72
+ "--primitive", "-p",
73
+ type=click.Choice(["Cube", "Sphere", "Cylinder",
74
+ "Plane", "Capsule", "Quad"]),
75
+ help="Create a primitive type."
76
+ )
77
+ @click.option(
78
+ "--position", "-pos",
79
+ nargs=3,
80
+ type=float,
81
+ default=None,
82
+ help="Position as X Y Z."
83
+ )
84
+ @click.option(
85
+ "--rotation", "-rot",
86
+ nargs=3,
87
+ type=float,
88
+ default=None,
89
+ help="Rotation as X Y Z (euler angles)."
90
+ )
91
+ @click.option(
92
+ "--scale", "-s",
93
+ nargs=3,
94
+ type=float,
95
+ default=None,
96
+ help="Scale as X Y Z."
97
+ )
98
+ @click.option(
99
+ "--parent",
100
+ default=None,
101
+ help="Parent GameObject name or path."
102
+ )
103
+ @click.option(
104
+ "--tag", "-t",
105
+ default=None,
106
+ help="Tag to assign."
107
+ )
108
+ @click.option(
109
+ "--layer",
110
+ default=None,
111
+ help="Layer to assign."
112
+ )
113
+ @click.option(
114
+ "--components",
115
+ default=None,
116
+ help="Comma-separated list of components to add."
117
+ )
118
+ @click.option(
119
+ "--save-prefab",
120
+ is_flag=True,
121
+ help="Save as prefab after creation."
122
+ )
123
+ @click.option(
124
+ "--prefab-path",
125
+ default=None,
126
+ help="Path for prefab (e.g., Assets/Prefabs/MyPrefab.prefab)."
127
+ )
128
+ @handle_unity_errors
129
+ def create(
130
+ name: str,
131
+ primitive: Optional[str],
132
+ position: Optional[Tuple[float, float, float]],
133
+ rotation: Optional[Tuple[float, float, float]],
134
+ scale: Optional[Tuple[float, float, float]],
135
+ parent: Optional[str],
136
+ tag: Optional[str],
137
+ layer: Optional[str],
138
+ components: Optional[str],
139
+ save_prefab: bool,
140
+ prefab_path: Optional[str],
141
+ ):
142
+ """Create a new GameObject.
143
+
144
+ \b
145
+ Examples:
146
+ unity-mcp gameobject create "MyCube" --primitive Cube
147
+ unity-mcp gameobject create "Player" --position 0 1 0
148
+ unity-mcp gameobject create "Enemy" --primitive Sphere --tag Enemy
149
+ unity-mcp gameobject create "Child" --parent "ParentObject"
150
+ unity-mcp gameobject create "Item" --components "Rigidbody,BoxCollider"
151
+ """
152
+ config = get_config()
153
+
154
+ params: dict[str, Any] = {
155
+ "action": "create",
156
+ "name": name,
157
+ }
158
+
159
+ if primitive:
160
+ params["primitiveType"] = primitive
161
+ if position:
162
+ params["position"] = list(position)
163
+ if rotation:
164
+ params["rotation"] = list(rotation)
165
+ if scale:
166
+ params["scale"] = list(scale)
167
+ if parent:
168
+ params["parent"] = parent
169
+ if tag:
170
+ params["tag"] = tag
171
+ if layer:
172
+ params["layer"] = layer
173
+ if save_prefab:
174
+ params["saveAsPrefab"] = True
175
+ if prefab_path:
176
+ params["prefabPath"] = prefab_path
177
+
178
+ result = run_command("manage_gameobject", params, config)
179
+
180
+ # Add components separately since componentsToAdd doesn't work
181
+ if components and (result.get("success") or result.get("data") or result.get("result")):
182
+ component_list = [c.strip() for c in components.split(",")]
183
+ failed_components = []
184
+ for component in component_list:
185
+ try:
186
+ run_command("manage_components", {
187
+ "action": "add",
188
+ "target": name,
189
+ "componentType": component,
190
+ }, config)
191
+ except UnityConnectionError:
192
+ failed_components.append(component)
193
+ if failed_components:
194
+ print_warning(f"Failed to add components: {', '.join(failed_components)}")
195
+
196
+ click.echo(format_output(result, config.format))
197
+ if result.get("success") or result.get("result"):
198
+ print_success(f"Created GameObject '{name}'")
199
+
200
+
201
+ @gameobject.command("modify")
202
+ @click.argument("target")
203
+ @click.option(
204
+ "--name", "-n",
205
+ default=None,
206
+ help="New name for the GameObject."
207
+ )
208
+ @click.option(
209
+ "--position", "-pos",
210
+ nargs=3,
211
+ type=float,
212
+ default=None,
213
+ help="New position as X Y Z."
214
+ )
215
+ @click.option(
216
+ "--rotation", "-rot",
217
+ nargs=3,
218
+ type=float,
219
+ default=None,
220
+ help="New rotation as X Y Z (euler angles)."
221
+ )
222
+ @click.option(
223
+ "--scale", "-s",
224
+ nargs=3,
225
+ type=float,
226
+ default=None,
227
+ help="New scale as X Y Z."
228
+ )
229
+ @click.option(
230
+ "--parent",
231
+ default=None,
232
+ help="New parent GameObject."
233
+ )
234
+ @click.option(
235
+ "--tag", "-t",
236
+ default=None,
237
+ help="New tag."
238
+ )
239
+ @click.option(
240
+ "--layer",
241
+ default=None,
242
+ help="New layer."
243
+ )
244
+ @click.option(
245
+ "--active/--inactive",
246
+ default=None,
247
+ help="Set active state."
248
+ )
249
+ @click.option(
250
+ "--add-components",
251
+ default=None,
252
+ help="Comma-separated list of components to add."
253
+ )
254
+ @click.option(
255
+ "--remove-components",
256
+ default=None,
257
+ help="Comma-separated list of components to remove."
258
+ )
259
+ @click.option(
260
+ "--search-method",
261
+ type=SEARCH_METHOD_CHOICE_TAGGED,
262
+ default=None,
263
+ help="How to find the target GameObject."
264
+ )
265
+ @handle_unity_errors
266
+ def modify(
267
+ target: str,
268
+ name: Optional[str],
269
+ position: Optional[Tuple[float, float, float]],
270
+ rotation: Optional[Tuple[float, float, float]],
271
+ scale: Optional[Tuple[float, float, float]],
272
+ parent: Optional[str],
273
+ tag: Optional[str],
274
+ layer: Optional[str],
275
+ active: Optional[bool],
276
+ add_components: Optional[str],
277
+ remove_components: Optional[str],
278
+ search_method: Optional[str],
279
+ ):
280
+ """Modify an existing GameObject.
281
+
282
+ TARGET can be a name, path, instance ID, or tag depending on --search-method.
283
+
284
+ \b
285
+ Examples:
286
+ unity-mcp gameobject modify "Player" --position 0 5 0
287
+ unity-mcp gameobject modify "Enemy" --name "Boss" --tag "Boss"
288
+ unity-mcp gameobject modify "-81840" --search-method by_id --active
289
+ unity-mcp gameobject modify "/Canvas/Panel" --search-method by_path --inactive
290
+ unity-mcp gameobject modify "Cube" --add-components "Rigidbody,BoxCollider"
291
+ """
292
+ config = get_config()
293
+
294
+ params: dict[str, Any] = {
295
+ "action": "modify",
296
+ "target": target,
297
+ }
298
+
299
+ if name:
300
+ params["name"] = name
301
+ if position:
302
+ params["position"] = list(position)
303
+ if rotation:
304
+ params["rotation"] = list(rotation)
305
+ if scale:
306
+ params["scale"] = list(scale)
307
+ if parent:
308
+ params["parent"] = parent
309
+ if tag:
310
+ params["tag"] = tag
311
+ if layer:
312
+ params["layer"] = layer
313
+ if active is not None:
314
+ params["setActive"] = active
315
+ if add_components:
316
+ params["componentsToAdd"] = [c.strip() for c in add_components.split(",")]
317
+ if remove_components:
318
+ params["componentsToRemove"] = [c.strip() for c in remove_components.split(",")]
319
+ if search_method:
320
+ params["searchMethod"] = search_method
321
+
322
+ result = run_command("manage_gameobject", params, config)
323
+ click.echo(format_output(result, config.format))
324
+
325
+
326
+ @gameobject.command("delete")
327
+ @click.argument("target")
328
+ @click.option(
329
+ "--search-method",
330
+ type=SEARCH_METHOD_CHOICE_TAGGED,
331
+ default=None,
332
+ help="How to find the target GameObject."
333
+ )
334
+ @click.option(
335
+ "--force", "-f",
336
+ is_flag=True,
337
+ help="Skip confirmation prompt."
338
+ )
339
+ @handle_unity_errors
340
+ def delete(target: str, search_method: Optional[str], force: bool):
341
+ """Delete a GameObject.
342
+
343
+ \b
344
+ Examples:
345
+ unity-mcp gameobject delete "OldObject"
346
+ unity-mcp gameobject delete "-81840" --search-method by_id
347
+ unity-mcp gameobject delete "TempObjects" --search-method by_tag --force
348
+ """
349
+ config = get_config()
350
+
351
+ confirm_destructive_action("Delete", "GameObject", target, force)
352
+
353
+ params = {
354
+ "action": "delete",
355
+ "target": target,
356
+ }
357
+
358
+ if search_method:
359
+ params["searchMethod"] = search_method
360
+
361
+ result = run_command("manage_gameobject", params, config)
362
+ click.echo(format_output(result, config.format))
363
+ if result.get("success"):
364
+ print_success(f"Deleted GameObject '{target}'")
365
+
366
+
367
+ @gameobject.command("duplicate")
368
+ @click.argument("target")
369
+ @click.option(
370
+ "--name", "-n",
371
+ default=None,
372
+ help="Name for the duplicate (default: OriginalName_Copy)."
373
+ )
374
+ @click.option(
375
+ "--offset",
376
+ nargs=3,
377
+ type=float,
378
+ default=None,
379
+ help="Position offset from original as X Y Z."
380
+ )
381
+ @click.option(
382
+ "--search-method",
383
+ type=SEARCH_METHOD_CHOICE_TAGGED,
384
+ default=None,
385
+ help="How to find the target GameObject."
386
+ )
387
+ @handle_unity_errors
388
+ def duplicate(
389
+ target: str,
390
+ name: Optional[str],
391
+ offset: Optional[Tuple[float, float, float]],
392
+ search_method: Optional[str],
393
+ ):
394
+ """Duplicate a GameObject.
395
+
396
+ \b
397
+ Examples:
398
+ unity-mcp gameobject duplicate "Player"
399
+ unity-mcp gameobject duplicate "Enemy" --name "Enemy2" --offset 5 0 0
400
+ unity-mcp gameobject duplicate "-81840" --search-method by_id
401
+ """
402
+ config = get_config()
403
+
404
+ params: dict[str, Any] = {
405
+ "action": "duplicate",
406
+ "target": target,
407
+ }
408
+
409
+ if name:
410
+ params["new_name"] = name
411
+ if offset:
412
+ params["offset"] = list(offset)
413
+ if search_method:
414
+ params["searchMethod"] = search_method
415
+
416
+ result = run_command("manage_gameobject", params, config)
417
+ click.echo(format_output(result, config.format))
418
+ if result.get("success"):
419
+ print_success(f"Duplicated GameObject '{target}'")
420
+
421
+
422
+ @gameobject.command("move")
423
+ @click.argument("target")
424
+ @click.option(
425
+ "--reference", "-r",
426
+ required=True,
427
+ help="Reference object for relative movement."
428
+ )
429
+ @click.option(
430
+ "--direction", "-d",
431
+ type=click.Choice(["left", "right", "up", "down", "forward",
432
+ "back", "front", "backward", "behind"]),
433
+ required=True,
434
+ help="Direction to move."
435
+ )
436
+ @click.option(
437
+ "--distance",
438
+ type=float,
439
+ default=1.0,
440
+ help="Distance to move (default: 1.0)."
441
+ )
442
+ @click.option(
443
+ "--local",
444
+ is_flag=True,
445
+ help="Use reference object's local space instead of world space."
446
+ )
447
+ @click.option(
448
+ "--search-method",
449
+ type=SEARCH_METHOD_CHOICE_TAGGED,
450
+ default=None,
451
+ help="How to find the target GameObject."
452
+ )
453
+ @handle_unity_errors
454
+ def move(
455
+ target: str,
456
+ reference: str,
457
+ direction: str,
458
+ distance: float,
459
+ local: bool,
460
+ search_method: Optional[str],
461
+ ):
462
+ """Move a GameObject relative to another object.
463
+
464
+ \b
465
+ Examples:
466
+ unity-mcp gameobject move "Chair" --reference "Table" --direction right --distance 2
467
+ unity-mcp gameobject move "Light" --reference "Player" --direction up --distance 3
468
+ unity-mcp gameobject move "NPC" --reference "Player" --direction forward --local
469
+ """
470
+ config = get_config()
471
+
472
+ params: dict[str, Any] = {
473
+ "action": "move_relative",
474
+ "target": target,
475
+ "reference_object": reference,
476
+ "direction": direction,
477
+ "distance": distance,
478
+ "world_space": not local,
479
+ }
480
+
481
+ if search_method:
482
+ params["searchMethod"] = search_method
483
+
484
+ result = run_command("manage_gameobject", params, config)
485
+ click.echo(format_output(result, config.format))
486
+ if result.get("success"):
487
+ print_success(f"Moved '{target}' {direction} of '{reference}' by {distance} units")
@@ -0,0 +1,93 @@
1
+ """Instance CLI commands for managing Unity instances."""
2
+
3
+ import click
4
+ from typing import Optional
5
+
6
+ from cli.utils.config import get_config
7
+ from cli.utils.output import format_output, print_error, print_success, print_info
8
+ from cli.utils.connection import run_command, run_list_instances, handle_unity_errors
9
+
10
+
11
+ @click.group()
12
+ def instance():
13
+ """Unity instance management - list, select, and view instances."""
14
+ pass
15
+
16
+
17
+ @instance.command("list")
18
+ @handle_unity_errors
19
+ def list_instances():
20
+ """List available Unity instances.
21
+
22
+ \\b
23
+ Examples:
24
+ unity-mcp instance list
25
+ """
26
+ config = get_config()
27
+
28
+ result = run_list_instances(config)
29
+ instances = result.get("instances", []) if isinstance(
30
+ result, dict) else []
31
+
32
+ if not instances:
33
+ print_info("No Unity instances currently connected")
34
+ return
35
+
36
+ click.echo("Available Unity instances:")
37
+ for inst in instances:
38
+ project = inst.get("project", "Unknown")
39
+ version = inst.get("unity_version", "Unknown")
40
+ hash_id = inst.get("hash", "")
41
+ session_id = inst.get("session_id", "")
42
+
43
+ # Format: ProjectName@hash (Unity version)
44
+ display_id = f"{project}@{hash_id}" if hash_id else project
45
+ click.echo(f" • {display_id} (Unity {version})")
46
+ if session_id:
47
+ click.echo(f" Session: {session_id[:8]}...")
48
+
49
+
50
+ @instance.command("set")
51
+ @click.argument("instance_id")
52
+ @handle_unity_errors
53
+ def set_instance(instance_id: str):
54
+ """Set the active Unity instance.
55
+
56
+ INSTANCE_ID can be Name@hash or just a hash prefix.
57
+
58
+ \\b
59
+ Examples:
60
+ unity-mcp instance set "MyProject@abc123"
61
+ unity-mcp instance set abc123
62
+ """
63
+ config = get_config()
64
+
65
+ result = run_command("set_active_instance", {
66
+ "instance": instance_id,
67
+ }, config)
68
+ click.echo(format_output(result, config.format))
69
+ if result.get("success"):
70
+ data = result.get("data", {})
71
+ active = data.get("instance", instance_id)
72
+ print_success(f"Active instance set to: {active}")
73
+
74
+
75
+ @instance.command("current")
76
+ def current_instance():
77
+ """Show the currently selected Unity instance.
78
+
79
+ \\b
80
+ Examples:
81
+ unity-mcp instance current
82
+ """
83
+ config = get_config()
84
+
85
+ # The current instance is typically shown in telemetry or needs to be tracked
86
+ # For now, we can show the configured instance from CLI options
87
+ if config.unity_instance:
88
+ click.echo(f"Configured instance: {config.unity_instance}")
89
+ else:
90
+ print_info(
91
+ "No instance explicitly set. Using default (auto-select single instance).")
92
+ print_info("Use 'unity-mcp instance list' to see available instances.")
93
+ print_info("Use 'unity-mcp instance set <id>' to select one.")
@@ -0,0 +1,123 @@
1
+ """Lighting CLI commands."""
2
+
3
+ import click
4
+ from typing import Optional, Tuple
5
+
6
+ from cli.utils.config import get_config
7
+ from cli.utils.output import format_output, print_error, print_success
8
+ from cli.utils.connection import run_command, handle_unity_errors
9
+
10
+
11
+ @click.group()
12
+ def lighting():
13
+ """Lighting operations - create, modify lights and lighting settings."""
14
+ pass
15
+
16
+
17
+ @lighting.command("create")
18
+ @click.argument("name")
19
+ @click.option(
20
+ "--type", "-t",
21
+ "light_type",
22
+ type=click.Choice(["Directional", "Point", "Spot", "Area"]),
23
+ default="Point",
24
+ help="Type of light to create."
25
+ )
26
+ @click.option(
27
+ "--position", "-pos",
28
+ nargs=3,
29
+ type=float,
30
+ default=(0, 3, 0),
31
+ help="Position as X Y Z."
32
+ )
33
+ @click.option(
34
+ "--color", "-c",
35
+ nargs=3,
36
+ type=float,
37
+ default=None,
38
+ help="Color as R G B (0-1)."
39
+ )
40
+ @click.option(
41
+ "--intensity", "-i",
42
+ default=None,
43
+ type=float,
44
+ help="Light intensity."
45
+ )
46
+ @handle_unity_errors
47
+ def create(name: str, light_type: str, position: Tuple[float, float, float], color: Optional[Tuple[float, float, float]], intensity: Optional[float]):
48
+ """Create a new light.
49
+
50
+ \b
51
+ Examples:
52
+ unity-mcp lighting create "MainLight" --type Directional
53
+ unity-mcp lighting create "PointLight1" --position 0 5 0 --intensity 2
54
+ unity-mcp lighting create "RedLight" --type Spot --color 1 0 0
55
+ """
56
+ config = get_config()
57
+
58
+ # Step 1: Create empty GameObject with position
59
+ create_result = run_command("manage_gameobject", {
60
+ "action": "create",
61
+ "name": name,
62
+ "position": list(position),
63
+ }, config)
64
+
65
+ if not (create_result.get("success")):
66
+ click.echo(format_output(create_result, config.format))
67
+ return
68
+
69
+ # Step 2: Add Light component using manage_components
70
+ add_result = run_command("manage_components", {
71
+ "action": "add",
72
+ "target": name,
73
+ "componentType": "Light",
74
+ }, config)
75
+
76
+ if not add_result.get("success"):
77
+ click.echo(format_output(add_result, config.format))
78
+ return
79
+
80
+ # Step 3: Set light type using manage_components set_property
81
+ type_result = run_command("manage_components", {
82
+ "action": "set_property",
83
+ "target": name,
84
+ "componentType": "Light",
85
+ "property": "type",
86
+ "value": light_type,
87
+ }, config)
88
+
89
+ if not type_result.get("success"):
90
+ click.echo(format_output(type_result, config.format))
91
+ return
92
+
93
+ # Step 4: Set color if provided
94
+ if color:
95
+ color_result = run_command("manage_components", {
96
+ "action": "set_property",
97
+ "target": name,
98
+ "componentType": "Light",
99
+ "property": "color",
100
+ "value": {"r": color[0], "g": color[1], "b": color[2], "a": 1},
101
+ }, config)
102
+
103
+ if not color_result.get("success"):
104
+ click.echo(format_output(color_result, config.format))
105
+ return
106
+
107
+ # Step 5: Set intensity if provided
108
+ if intensity is not None:
109
+ intensity_result = run_command("manage_components", {
110
+ "action": "set_property",
111
+ "target": name,
112
+ "componentType": "Light",
113
+ "property": "intensity",
114
+ "value": intensity,
115
+ }, config)
116
+
117
+ if not intensity_result.get("success"):
118
+ click.echo(format_output(intensity_result, config.format))
119
+ return
120
+
121
+ # Output the result
122
+ click.echo(format_output(create_result, config.format))
123
+ print_success(f"Created {light_type} light: {name}")