mcpforunityserver 8.7.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.
Files changed (81) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +3 -0
  3. cli/commands/animation.py +87 -0
  4. cli/commands/asset.py +310 -0
  5. cli/commands/audio.py +133 -0
  6. cli/commands/batch.py +184 -0
  7. cli/commands/code.py +189 -0
  8. cli/commands/component.py +212 -0
  9. cli/commands/editor.py +487 -0
  10. cli/commands/gameobject.py +510 -0
  11. cli/commands/instance.py +101 -0
  12. cli/commands/lighting.py +128 -0
  13. cli/commands/material.py +268 -0
  14. cli/commands/prefab.py +144 -0
  15. cli/commands/scene.py +255 -0
  16. cli/commands/script.py +240 -0
  17. cli/commands/shader.py +238 -0
  18. cli/commands/ui.py +263 -0
  19. cli/commands/vfx.py +439 -0
  20. cli/main.py +248 -0
  21. cli/utils/__init__.py +31 -0
  22. cli/utils/config.py +58 -0
  23. cli/utils/connection.py +191 -0
  24. cli/utils/output.py +195 -0
  25. main.py +177 -62
  26. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/METADATA +4 -2
  27. mcpforunityserver-9.1.0.dist-info/RECORD +96 -0
  28. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/WHEEL +1 -1
  29. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/entry_points.txt +1 -0
  30. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/top_level.txt +1 -2
  31. services/custom_tool_service.py +179 -19
  32. services/resources/__init__.py +6 -1
  33. services/resources/active_tool.py +1 -1
  34. services/resources/custom_tools.py +2 -2
  35. services/resources/editor_state.py +283 -30
  36. services/resources/gameobject.py +243 -0
  37. services/resources/layers.py +1 -1
  38. services/resources/prefab_stage.py +1 -1
  39. services/resources/project_info.py +1 -1
  40. services/resources/selection.py +1 -1
  41. services/resources/tags.py +1 -1
  42. services/resources/unity_instances.py +1 -1
  43. services/resources/windows.py +1 -1
  44. services/state/external_changes_scanner.py +3 -4
  45. services/tools/__init__.py +6 -1
  46. services/tools/batch_execute.py +24 -9
  47. services/tools/debug_request_context.py +8 -2
  48. services/tools/execute_custom_tool.py +6 -1
  49. services/tools/execute_menu_item.py +6 -3
  50. services/tools/find_gameobjects.py +89 -0
  51. services/tools/find_in_file.py +26 -19
  52. services/tools/manage_asset.py +13 -44
  53. services/tools/manage_components.py +131 -0
  54. services/tools/manage_editor.py +9 -8
  55. services/tools/manage_gameobject.py +115 -79
  56. services/tools/manage_material.py +80 -31
  57. services/tools/manage_prefabs.py +7 -1
  58. services/tools/manage_scene.py +30 -13
  59. services/tools/manage_script.py +62 -19
  60. services/tools/manage_scriptable_object.py +22 -10
  61. services/tools/manage_shader.py +8 -1
  62. services/tools/manage_vfx.py +738 -0
  63. services/tools/preflight.py +15 -12
  64. services/tools/read_console.py +70 -17
  65. services/tools/refresh_unity.py +92 -29
  66. services/tools/run_tests.py +187 -53
  67. services/tools/script_apply_edits.py +15 -7
  68. services/tools/set_active_instance.py +12 -7
  69. services/tools/utils.py +60 -6
  70. transport/legacy/port_discovery.py +2 -2
  71. transport/legacy/unity_connection.py +129 -26
  72. transport/plugin_hub.py +85 -24
  73. transport/unity_instance_middleware.py +4 -3
  74. transport/unity_transport.py +2 -1
  75. utils/focus_nudge.py +321 -0
  76. __init__.py +0 -0
  77. mcpforunityserver-8.7.0.dist-info/RECORD +0 -71
  78. routes/__init__.py +0 -0
  79. services/resources/editor_state_v2.py +0 -270
  80. services/tools/test_jobs.py +0 -94
  81. {mcpforunityserver-8.7.0.dist-info → mcpforunityserver-9.1.0.dist-info}/licenses/LICENSE +0 -0
cli/commands/vfx.py ADDED
@@ -0,0 +1,439 @@
1
+ """VFX CLI commands for managing Unity visual effects."""
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
10
+ from cli.utils.connection import run_command, UnityConnectionError
11
+
12
+
13
+ @click.group()
14
+ def vfx():
15
+ """VFX operations - particle systems, line renderers, trails."""
16
+ pass
17
+
18
+
19
+ # =============================================================================
20
+ # Particle System Commands
21
+ # =============================================================================
22
+
23
+ @vfx.group()
24
+ def particle():
25
+ """Particle system operations."""
26
+ pass
27
+
28
+
29
+ @particle.command("info")
30
+ @click.argument("target")
31
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
32
+ def particle_info(target: str, search_method: Optional[str]):
33
+ """Get particle system info.
34
+
35
+ \\b
36
+ Examples:
37
+ unity-mcp vfx particle info "Fire"
38
+ unity-mcp vfx particle info "-12345" --search-method by_id
39
+ """
40
+ config = get_config()
41
+ params: dict[str, Any] = {"action": "particle_get_info", "target": target}
42
+ if search_method:
43
+ params["searchMethod"] = search_method
44
+
45
+ try:
46
+ result = run_command("manage_vfx", params, config)
47
+ click.echo(format_output(result, config.format))
48
+ except UnityConnectionError as e:
49
+ print_error(str(e))
50
+ sys.exit(1)
51
+
52
+
53
+ @particle.command("play")
54
+ @click.argument("target")
55
+ @click.option("--with-children", is_flag=True, help="Also play child particle systems.")
56
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
57
+ def particle_play(target: str, with_children: bool, search_method: Optional[str]):
58
+ """Play a particle system.
59
+
60
+ \\b
61
+ Examples:
62
+ unity-mcp vfx particle play "Fire"
63
+ unity-mcp vfx particle play "Effects" --with-children
64
+ """
65
+ config = get_config()
66
+ params: dict[str, Any] = {"action": "particle_play", "target": target}
67
+ if with_children:
68
+ params["withChildren"] = True
69
+ if search_method:
70
+ params["searchMethod"] = search_method
71
+
72
+ try:
73
+ result = run_command("manage_vfx", params, config)
74
+ click.echo(format_output(result, config.format))
75
+ if result.get("success"):
76
+ print_success(f"Playing particle system: {target}")
77
+ except UnityConnectionError as e:
78
+ print_error(str(e))
79
+ sys.exit(1)
80
+
81
+
82
+ @particle.command("stop")
83
+ @click.argument("target")
84
+ @click.option("--with-children", is_flag=True, help="Also stop child particle systems.")
85
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
86
+ def particle_stop(target: str, with_children: bool, search_method: Optional[str]):
87
+ """Stop a particle system."""
88
+ config = get_config()
89
+ params: dict[str, Any] = {"action": "particle_stop", "target": target}
90
+ if with_children:
91
+ params["withChildren"] = True
92
+ if search_method:
93
+ params["searchMethod"] = search_method
94
+
95
+ try:
96
+ result = run_command("manage_vfx", params, config)
97
+ click.echo(format_output(result, config.format))
98
+ if result.get("success"):
99
+ print_success(f"Stopped particle system: {target}")
100
+ except UnityConnectionError as e:
101
+ print_error(str(e))
102
+ sys.exit(1)
103
+
104
+
105
+ @particle.command("pause")
106
+ @click.argument("target")
107
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
108
+ def particle_pause(target: str, search_method: Optional[str]):
109
+ """Pause a particle system."""
110
+ config = get_config()
111
+ params: dict[str, Any] = {"action": "particle_pause", "target": target}
112
+ if search_method:
113
+ params["searchMethod"] = search_method
114
+
115
+ try:
116
+ result = run_command("manage_vfx", params, config)
117
+ click.echo(format_output(result, config.format))
118
+ except UnityConnectionError as e:
119
+ print_error(str(e))
120
+ sys.exit(1)
121
+
122
+
123
+ @particle.command("restart")
124
+ @click.argument("target")
125
+ @click.option("--with-children", is_flag=True)
126
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
127
+ def particle_restart(target: str, with_children: bool, search_method: Optional[str]):
128
+ """Restart a particle system."""
129
+ config = get_config()
130
+ params: dict[str, Any] = {"action": "particle_restart", "target": target}
131
+ if with_children:
132
+ params["withChildren"] = True
133
+ if search_method:
134
+ params["searchMethod"] = search_method
135
+
136
+ try:
137
+ result = run_command("manage_vfx", params, config)
138
+ click.echo(format_output(result, config.format))
139
+ except UnityConnectionError as e:
140
+ print_error(str(e))
141
+ sys.exit(1)
142
+
143
+
144
+ @particle.command("clear")
145
+ @click.argument("target")
146
+ @click.option("--with-children", is_flag=True)
147
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
148
+ def particle_clear(target: str, with_children: bool, search_method: Optional[str]):
149
+ """Clear all particles from a particle system."""
150
+ config = get_config()
151
+ params: dict[str, Any] = {"action": "particle_clear", "target": target}
152
+ if with_children:
153
+ params["withChildren"] = True
154
+ if search_method:
155
+ params["searchMethod"] = search_method
156
+
157
+ try:
158
+ result = run_command("manage_vfx", params, config)
159
+ click.echo(format_output(result, config.format))
160
+ except UnityConnectionError as e:
161
+ print_error(str(e))
162
+ sys.exit(1)
163
+
164
+
165
+ # =============================================================================
166
+ # Line Renderer Commands
167
+ # =============================================================================
168
+
169
+ @vfx.group()
170
+ def line():
171
+ """Line renderer operations."""
172
+ pass
173
+
174
+
175
+ @line.command("info")
176
+ @click.argument("target")
177
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
178
+ def line_info(target: str, search_method: Optional[str]):
179
+ """Get line renderer info.
180
+
181
+ \\b
182
+ Examples:
183
+ unity-mcp vfx line info "LaserBeam"
184
+ """
185
+ config = get_config()
186
+ params: dict[str, Any] = {"action": "line_get_info", "target": target}
187
+ if search_method:
188
+ params["searchMethod"] = search_method
189
+
190
+ try:
191
+ result = run_command("manage_vfx", params, config)
192
+ click.echo(format_output(result, config.format))
193
+ except UnityConnectionError as e:
194
+ print_error(str(e))
195
+ sys.exit(1)
196
+
197
+
198
+ @line.command("set-positions")
199
+ @click.argument("target")
200
+ @click.option("--positions", "-p", required=True, help='Positions as JSON array: [[0,0,0], [1,1,1], [2,0,0]]')
201
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
202
+ def line_set_positions(target: str, positions: str, search_method: Optional[str]):
203
+ """Set all positions on a line renderer.
204
+
205
+ \\b
206
+ Examples:
207
+ unity-mcp vfx line set-positions "Line" --positions "[[0,0,0], [5,2,0], [10,0,0]]"
208
+ """
209
+ config = get_config()
210
+
211
+ try:
212
+ positions_list = json.loads(positions)
213
+ except json.JSONDecodeError as e:
214
+ print_error(f"Invalid JSON for positions: {e}")
215
+ sys.exit(1)
216
+
217
+ params: dict[str, Any] = {
218
+ "action": "line_set_positions",
219
+ "target": target,
220
+ "positions": positions_list,
221
+ }
222
+ if search_method:
223
+ params["searchMethod"] = search_method
224
+
225
+ try:
226
+ result = run_command("manage_vfx", params, config)
227
+ click.echo(format_output(result, config.format))
228
+ except UnityConnectionError as e:
229
+ print_error(str(e))
230
+ sys.exit(1)
231
+
232
+
233
+ @line.command("create-line")
234
+ @click.argument("target")
235
+ @click.option("--start", nargs=3, type=float, required=True, help="Start point X Y Z")
236
+ @click.option("--end", nargs=3, type=float, required=True, help="End point X Y Z")
237
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
238
+ def line_create_line(target: str, start: Tuple[float, float, float], end: Tuple[float, float, float], search_method: Optional[str]):
239
+ """Create a simple line between two points.
240
+
241
+ \\b
242
+ Examples:
243
+ unity-mcp vfx line create-line "MyLine" --start 0 0 0 --end 10 5 0
244
+ """
245
+ config = get_config()
246
+ params: dict[str, Any] = {
247
+ "action": "line_create_line",
248
+ "target": target,
249
+ "start": list(start),
250
+ "end": list(end),
251
+ }
252
+ if search_method:
253
+ params["searchMethod"] = search_method
254
+
255
+ try:
256
+ result = run_command("manage_vfx", params, config)
257
+ click.echo(format_output(result, config.format))
258
+ except UnityConnectionError as e:
259
+ print_error(str(e))
260
+ sys.exit(1)
261
+
262
+
263
+ @line.command("create-circle")
264
+ @click.argument("target")
265
+ @click.option("--center", nargs=3, type=float, default=(0, 0, 0), help="Center point X Y Z")
266
+ @click.option("--radius", type=float, required=True, help="Circle radius")
267
+ @click.option("--segments", type=int, default=32, help="Number of segments")
268
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
269
+ def line_create_circle(target: str, center: Tuple[float, float, float], radius: float, segments: int, search_method: Optional[str]):
270
+ """Create a circle shape.
271
+
272
+ \\b
273
+ Examples:
274
+ unity-mcp vfx line create-circle "Circle" --radius 5 --segments 64
275
+ unity-mcp vfx line create-circle "Ring" --center 0 2 0 --radius 3
276
+ """
277
+ config = get_config()
278
+ params: dict[str, Any] = {
279
+ "action": "line_create_circle",
280
+ "target": target,
281
+ "center": list(center),
282
+ "radius": radius,
283
+ "segments": segments,
284
+ }
285
+ if search_method:
286
+ params["searchMethod"] = search_method
287
+
288
+ try:
289
+ result = run_command("manage_vfx", params, config)
290
+ click.echo(format_output(result, config.format))
291
+ except UnityConnectionError as e:
292
+ print_error(str(e))
293
+ sys.exit(1)
294
+
295
+
296
+ @line.command("clear")
297
+ @click.argument("target")
298
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
299
+ def line_clear(target: str, search_method: Optional[str]):
300
+ """Clear all positions from a line renderer."""
301
+ config = get_config()
302
+ params: dict[str, Any] = {"action": "line_clear", "target": target}
303
+ if search_method:
304
+ params["searchMethod"] = search_method
305
+
306
+ try:
307
+ result = run_command("manage_vfx", params, config)
308
+ click.echo(format_output(result, config.format))
309
+ except UnityConnectionError as e:
310
+ print_error(str(e))
311
+ sys.exit(1)
312
+
313
+
314
+ # =============================================================================
315
+ # Trail Renderer Commands
316
+ # =============================================================================
317
+
318
+ @vfx.group()
319
+ def trail():
320
+ """Trail renderer operations."""
321
+ pass
322
+
323
+
324
+ @trail.command("info")
325
+ @click.argument("target")
326
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
327
+ def trail_info(target: str, search_method: Optional[str]):
328
+ """Get trail renderer info."""
329
+ config = get_config()
330
+ params: dict[str, Any] = {"action": "trail_get_info", "target": target}
331
+ if search_method:
332
+ params["searchMethod"] = search_method
333
+
334
+ try:
335
+ result = run_command("manage_vfx", params, config)
336
+ click.echo(format_output(result, config.format))
337
+ except UnityConnectionError as e:
338
+ print_error(str(e))
339
+ sys.exit(1)
340
+
341
+
342
+ @trail.command("set-time")
343
+ @click.argument("target")
344
+ @click.argument("duration", type=float)
345
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
346
+ def trail_set_time(target: str, duration: float, search_method: Optional[str]):
347
+ """Set trail duration.
348
+
349
+ \\b
350
+ Examples:
351
+ unity-mcp vfx trail set-time "PlayerTrail" 2.0
352
+ """
353
+ config = get_config()
354
+ params: dict[str, Any] = {
355
+ "action": "trail_set_time",
356
+ "target": target,
357
+ "time": duration,
358
+ }
359
+ if search_method:
360
+ params["searchMethod"] = search_method
361
+
362
+ try:
363
+ result = run_command("manage_vfx", params, config)
364
+ click.echo(format_output(result, config.format))
365
+ except UnityConnectionError as e:
366
+ print_error(str(e))
367
+ sys.exit(1)
368
+
369
+
370
+ @trail.command("clear")
371
+ @click.argument("target")
372
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
373
+ def trail_clear(target: str, search_method: Optional[str]):
374
+ """Clear a trail renderer."""
375
+ config = get_config()
376
+ params: dict[str, Any] = {"action": "trail_clear", "target": target}
377
+ if search_method:
378
+ params["searchMethod"] = search_method
379
+
380
+ try:
381
+ result = run_command("manage_vfx", params, config)
382
+ click.echo(format_output(result, config.format))
383
+ except UnityConnectionError as e:
384
+ print_error(str(e))
385
+ sys.exit(1)
386
+
387
+
388
+ # =============================================================================
389
+ # Raw Command (escape hatch for all VFX actions)
390
+ # =============================================================================
391
+
392
+ @vfx.command("raw")
393
+ @click.argument("action")
394
+ @click.argument("target", required=False)
395
+ @click.option("--params", "-p", default="{}", help="Additional parameters as JSON.")
396
+ @click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
397
+ def vfx_raw(action: str, target: Optional[str], params: str, search_method: Optional[str]):
398
+ """Execute any VFX action directly.
399
+
400
+ For advanced users who need access to all 60+ VFX actions.
401
+
402
+ \\b
403
+ Actions include:
404
+ particle_*: particle_set_main, particle_set_emission, particle_set_shape, ...
405
+ vfx_*: vfx_set_float, vfx_send_event, vfx_play, ...
406
+ line_*: line_create_arc, line_create_bezier, ...
407
+ trail_*: trail_set_width, trail_set_color, ...
408
+
409
+ \\b
410
+ Examples:
411
+ unity-mcp vfx raw particle_set_main "Fire" --params '{"duration": 5, "looping": true}'
412
+ unity-mcp vfx raw line_create_arc "Arc" --params '{"radius": 3, "startAngle": 0, "endAngle": 180}'
413
+ unity-mcp vfx raw vfx_send_event "Explosion" --params '{"eventName": "OnSpawn"}'
414
+ """
415
+ config = get_config()
416
+
417
+ try:
418
+ extra_params = json.loads(params)
419
+ except json.JSONDecodeError as e:
420
+ print_error(f"Invalid JSON for params: {e}")
421
+ sys.exit(1)
422
+ if not isinstance(extra_params, dict):
423
+ print_error("Invalid JSON for params: expected an object")
424
+ sys.exit(1)
425
+
426
+ request_params: dict[str, Any] = {"action": action}
427
+ if target:
428
+ request_params["target"] = target
429
+ if search_method:
430
+ request_params["searchMethod"] = search_method
431
+
432
+ # Merge extra params
433
+ request_params.update(extra_params)
434
+ try:
435
+ result = run_command("manage_vfx", request_params, config)
436
+ click.echo(format_output(result, config.format))
437
+ except UnityConnectionError as e:
438
+ print_error(str(e))
439
+ sys.exit(1)
cli/main.py ADDED
@@ -0,0 +1,248 @@
1
+ """Unity MCP Command Line Interface - Main Entry Point."""
2
+
3
+ import sys
4
+ from importlib import import_module
5
+
6
+ import click
7
+ from typing import Optional
8
+
9
+ from cli import __version__
10
+ from cli.utils.config import CLIConfig, set_config, get_config
11
+ from cli.utils.output import format_output, print_error, print_success, print_info
12
+ from cli.utils.connection import (
13
+ run_command,
14
+ run_check_connection,
15
+ run_list_instances,
16
+ UnityConnectionError,
17
+ warn_if_remote_host,
18
+ )
19
+
20
+
21
+ # Context object to pass configuration between commands
22
+ class Context:
23
+ def __init__(self):
24
+ self.config: Optional[CLIConfig] = None
25
+ self.verbose: bool = False
26
+
27
+
28
+ pass_context = click.make_pass_decorator(Context, ensure=True)
29
+
30
+
31
+ @click.group()
32
+ @click.version_option(version=__version__, prog_name="unity-mcp")
33
+ @click.option(
34
+ "--host", "-h",
35
+ default="127.0.0.1",
36
+ envvar="UNITY_MCP_HOST",
37
+ help="MCP server host address."
38
+ )
39
+ @click.option(
40
+ "--port", "-p",
41
+ default=8080,
42
+ type=int,
43
+ envvar="UNITY_MCP_HTTP_PORT",
44
+ help="MCP server port."
45
+ )
46
+ @click.option(
47
+ "--timeout", "-t",
48
+ default=30,
49
+ type=int,
50
+ envvar="UNITY_MCP_TIMEOUT",
51
+ help="Command timeout in seconds."
52
+ )
53
+ @click.option(
54
+ "--format", "-f",
55
+ type=click.Choice(["text", "json", "table"]),
56
+ default="text",
57
+ envvar="UNITY_MCP_FORMAT",
58
+ help="Output format."
59
+ )
60
+ @click.option(
61
+ "--instance", "-i",
62
+ default=None,
63
+ envvar="UNITY_MCP_INSTANCE",
64
+ help="Target Unity instance (hash or Name@hash)."
65
+ )
66
+ @click.option(
67
+ "--verbose", "-v",
68
+ is_flag=True,
69
+ help="Enable verbose output."
70
+ )
71
+ @pass_context
72
+ def cli(ctx: Context, host: str, port: int, timeout: int, format: str, instance: Optional[str], verbose: bool):
73
+ """Unity MCP Command Line Interface.
74
+
75
+ Control Unity Editor directly from the command line using the Model Context Protocol.
76
+
77
+ \b
78
+ Examples:
79
+ unity-mcp status
80
+ unity-mcp gameobject find "Player"
81
+ unity-mcp scene hierarchy --format json
82
+ unity-mcp editor play
83
+
84
+ \b
85
+ Environment Variables:
86
+ UNITY_MCP_HOST Server host (default: 127.0.0.1)
87
+ UNITY_MCP_HTTP_PORT Server port (default: 8080)
88
+ UNITY_MCP_TIMEOUT Timeout in seconds (default: 30)
89
+ UNITY_MCP_FORMAT Output format (default: text)
90
+ UNITY_MCP_INSTANCE Target Unity instance
91
+ """
92
+ config = CLIConfig(
93
+ host=host,
94
+ port=port,
95
+ timeout=timeout,
96
+ format=format,
97
+ unity_instance=instance,
98
+ )
99
+
100
+ # Security warning for non-localhost connections
101
+ warn_if_remote_host(config)
102
+
103
+ set_config(config)
104
+ ctx.config = config
105
+ ctx.verbose = verbose
106
+
107
+
108
+ @cli.command("status")
109
+ @pass_context
110
+ def status(ctx: Context):
111
+ """Check connection status to Unity MCP server."""
112
+ config = ctx.config or get_config()
113
+
114
+ click.echo(f"Checking connection to {config.host}:{config.port}...")
115
+
116
+ if run_check_connection(config):
117
+ print_success(
118
+ f"Connected to Unity MCP server at {config.host}:{config.port}")
119
+
120
+ # Try to get Unity instances
121
+ try:
122
+ result = run_list_instances(config)
123
+ instances = result.get("instances", []) if isinstance(
124
+ result, dict) else []
125
+ if instances:
126
+ click.echo("\nConnected Unity instances:")
127
+ for inst in instances:
128
+ project = inst.get("project", "Unknown")
129
+ version = inst.get("unity_version", "Unknown")
130
+ hash_id = inst.get("hash", "")[:8]
131
+ click.echo(f" • {project} (Unity {version}) [{hash_id}]")
132
+ else:
133
+ print_info("No Unity instances currently connected")
134
+ except UnityConnectionError as e:
135
+ print_info(f"Could not retrieve Unity instances: {e}")
136
+ else:
137
+ print_error(
138
+ f"Cannot connect to Unity MCP server at {config.host}:{config.port}")
139
+ sys.exit(1)
140
+
141
+
142
+ @cli.command("instances")
143
+ @pass_context
144
+ def list_instances(ctx: Context):
145
+ """List available Unity instances."""
146
+ config = ctx.config or get_config()
147
+
148
+ try:
149
+ instances = run_list_instances(config)
150
+ click.echo(format_output(instances, config.format))
151
+ except UnityConnectionError as e:
152
+ print_error(str(e))
153
+ sys.exit(1)
154
+
155
+
156
+ @cli.command("raw")
157
+ @click.argument("command_type")
158
+ @click.argument("params", required=False, default="{}")
159
+ @pass_context
160
+ def raw_command(ctx: Context, command_type: str, params: str):
161
+ """Send a raw command to Unity.
162
+
163
+ \b
164
+ Examples:
165
+ unity-mcp raw manage_scene '{"action": "get_hierarchy"}'
166
+ unity-mcp raw read_console '{"count": 10}'
167
+ """
168
+ import json
169
+ config = ctx.config or get_config()
170
+
171
+ try:
172
+ params_dict = json.loads(params)
173
+ except json.JSONDecodeError as e:
174
+ print_error(f"Invalid JSON params: {e}")
175
+ sys.exit(1)
176
+
177
+ try:
178
+ result = run_command(command_type, params_dict, config)
179
+ click.echo(format_output(result, config.format))
180
+ except UnityConnectionError as e:
181
+ print_error(str(e))
182
+ sys.exit(1)
183
+
184
+
185
+ # Import and register command groups
186
+ # These will be implemented in subsequent TODOs
187
+ def register_commands():
188
+ """Register all command groups."""
189
+ def register_optional_command(module_name: str, command_name: str) -> None:
190
+ try:
191
+ module = import_module(module_name)
192
+ except ModuleNotFoundError as e:
193
+ if e.name == module_name:
194
+ return
195
+ print_error(
196
+ f"Failed to load command module '{module_name}': {e}"
197
+ )
198
+ return
199
+ except Exception as e:
200
+ print_error(
201
+ f"Failed to load command module '{module_name}': {e}"
202
+ )
203
+ return
204
+
205
+ command = getattr(module, command_name, None)
206
+ if command is None:
207
+ print_error(
208
+ f"Command '{command_name}' not found in '{module_name}'"
209
+ )
210
+ return
211
+
212
+ cli.add_command(command)
213
+
214
+ optional_commands = [
215
+ ("cli.commands.gameobject", "gameobject"),
216
+ ("cli.commands.component", "component"),
217
+ ("cli.commands.scene", "scene"),
218
+ ("cli.commands.asset", "asset"),
219
+ ("cli.commands.script", "script"),
220
+ ("cli.commands.code", "code"),
221
+ ("cli.commands.editor", "editor"),
222
+ ("cli.commands.prefab", "prefab"),
223
+ ("cli.commands.material", "material"),
224
+ ("cli.commands.lighting", "lighting"),
225
+ ("cli.commands.animation", "animation"),
226
+ ("cli.commands.audio", "audio"),
227
+ ("cli.commands.ui", "ui"),
228
+ ("cli.commands.instance", "instance"),
229
+ ("cli.commands.shader", "shader"),
230
+ ("cli.commands.vfx", "vfx"),
231
+ ("cli.commands.batch", "batch"),
232
+ ]
233
+
234
+ for module_name, command_name in optional_commands:
235
+ register_optional_command(module_name, command_name)
236
+
237
+
238
+ # Register commands on import
239
+ register_commands()
240
+
241
+
242
+ def main():
243
+ """Main entry point for the CLI."""
244
+ cli()
245
+
246
+
247
+ if __name__ == "__main__":
248
+ main()