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
cli/commands/ui.py ADDED
@@ -0,0 +1,258 @@
1
+ """UI CLI commands - placeholder for future implementation."""
2
+
3
+ import sys
4
+ import click
5
+ from typing import Optional, Any
6
+
7
+ from cli.utils.config import get_config
8
+ from cli.utils.output import format_output, print_error, print_success
9
+ from cli.utils.connection import run_command, handle_unity_errors
10
+
11
+
12
+ @click.group()
13
+ def ui():
14
+ """UI operations - create and modify UI elements."""
15
+ pass
16
+
17
+
18
+ @ui.command("create-canvas")
19
+ @click.argument("name")
20
+ @click.option(
21
+ "--render-mode",
22
+ type=click.Choice(
23
+ ["ScreenSpaceOverlay", "ScreenSpaceCamera", "WorldSpace"]),
24
+ default="ScreenSpaceOverlay",
25
+ help="Canvas render mode."
26
+ )
27
+ @handle_unity_errors
28
+ def create_canvas(name: str, render_mode: str):
29
+ """Create a new Canvas.
30
+
31
+ \b
32
+ Examples:
33
+ unity-mcp ui create-canvas "MainUI"
34
+ unity-mcp ui create-canvas "WorldUI" --render-mode WorldSpace
35
+ """
36
+ config = get_config()
37
+
38
+ # Step 1: Create empty GameObject
39
+ result = run_command("manage_gameobject", {
40
+ "action": "create",
41
+ "name": name,
42
+ }, config)
43
+
44
+ if not (result.get("success") or result.get("data") or result.get("result")):
45
+ click.echo(format_output(result, config.format))
46
+ return
47
+
48
+ # Step 2: Add Canvas components
49
+ failed_components = []
50
+ for component in ["Canvas", "CanvasScaler", "GraphicRaycaster"]:
51
+ comp_result = run_command("manage_components", {
52
+ "action": "add",
53
+ "target": name,
54
+ "componentType": component,
55
+ }, config)
56
+ if not (comp_result.get("success") or comp_result.get("data")):
57
+ failed_components.append((component, comp_result.get("error", "Unknown error")))
58
+
59
+ if failed_components:
60
+ error_details = "; ".join([f"{c}: {e}" for c, e in failed_components])
61
+ print_error(f"Failed to add components: {error_details}")
62
+
63
+ # Step 3: Set render mode
64
+ render_mode_value = {"ScreenSpaceOverlay": 0,
65
+ "ScreenSpaceCamera": 1, "WorldSpace": 2}.get(render_mode, 0)
66
+ run_command("manage_components", {
67
+ "action": "set_property",
68
+ "target": name,
69
+ "componentType": "Canvas",
70
+ "property": "renderMode",
71
+ "value": render_mode_value,
72
+ }, config)
73
+
74
+ click.echo(format_output(result, config.format))
75
+ print_success(f"Created Canvas: {name}")
76
+
77
+
78
+ @ui.command("create-text")
79
+ @click.argument("name")
80
+ @click.option(
81
+ "--parent", "-p",
82
+ required=True,
83
+ help="Parent Canvas or UI element."
84
+ )
85
+ @click.option(
86
+ "--text", "-t",
87
+ default="New Text",
88
+ help="Initial text content."
89
+ )
90
+ @click.option(
91
+ "--position",
92
+ nargs=2,
93
+ type=float,
94
+ default=(0, 0),
95
+ help="Anchored position X Y."
96
+ )
97
+ @handle_unity_errors
98
+ def create_text(name: str, parent: str, text: str, position: tuple):
99
+ """Create a UI Text element (TextMeshPro).
100
+
101
+ \b
102
+ Examples:
103
+ unity-mcp ui create-text "TitleText" --parent "MainUI" --text "Hello World"
104
+ """
105
+ config = get_config()
106
+
107
+ # Step 1: Create empty GameObject with parent
108
+ result = run_command("manage_gameobject", {
109
+ "action": "create",
110
+ "name": name,
111
+ "parent": parent,
112
+ "position": list(position),
113
+ }, config)
114
+
115
+ if not (result.get("success") or result.get("data") or result.get("result")):
116
+ click.echo(format_output(result, config.format))
117
+ return
118
+
119
+ # Step 2: Add RectTransform and TextMeshProUGUI
120
+ run_command("manage_components", {
121
+ "action": "add",
122
+ "target": name,
123
+ "componentType": "TextMeshProUGUI",
124
+ }, config)
125
+
126
+ # Step 3: Set text content
127
+ run_command("manage_components", {
128
+ "action": "set_property",
129
+ "target": name,
130
+ "componentType": "TextMeshProUGUI",
131
+ "property": "text",
132
+ "value": text,
133
+ }, config)
134
+
135
+ click.echo(format_output(result, config.format))
136
+ print_success(f"Created Text: {name}")
137
+
138
+
139
+ @ui.command("create-button")
140
+ @click.argument("name")
141
+ @click.option(
142
+ "--parent", "-p",
143
+ required=True,
144
+ help="Parent Canvas or UI element."
145
+ )
146
+ @click.option(
147
+ "--text", "-t",
148
+ default="Button",
149
+ help="Button label text."
150
+ )
151
+ @handle_unity_errors
152
+ def create_button(name: str, parent: str, text: str): # text current placeholder
153
+ """Create a UI Button.
154
+
155
+ \b
156
+ Examples:
157
+ unity-mcp ui create-button "StartButton" --parent "MainUI" --text "Start Game"
158
+ """
159
+ config = get_config()
160
+
161
+ # Step 1: Create empty GameObject with parent
162
+ result = run_command("manage_gameobject", {
163
+ "action": "create",
164
+ "name": name,
165
+ "parent": parent,
166
+ }, config)
167
+
168
+ if not (result.get("success") or result.get("data") or result.get("result")):
169
+ click.echo(format_output(result, config.format))
170
+ return
171
+
172
+ # Step 2: Add Button and Image components
173
+ for component in ["Image", "Button"]:
174
+ run_command("manage_components", {
175
+ "action": "add",
176
+ "target": name,
177
+ "componentType": component,
178
+ }, config)
179
+
180
+ # Step 3: Create child label GameObject
181
+ label_name = f"{name}_Label"
182
+ run_command("manage_gameobject", {
183
+ "action": "create",
184
+ "name": label_name,
185
+ "parent": name,
186
+ }, config)
187
+
188
+ # Step 4: Add TextMeshProUGUI to label and set text
189
+ run_command("manage_components", {
190
+ "action": "add",
191
+ "target": label_name,
192
+ "componentType": "TextMeshProUGUI",
193
+ }, config)
194
+ run_command("manage_components", {
195
+ "action": "set_property",
196
+ "target": label_name,
197
+ "componentType": "TextMeshProUGUI",
198
+ "property": "text",
199
+ "value": text,
200
+ }, config)
201
+
202
+ click.echo(format_output(result, config.format))
203
+ print_success(f"Created Button: {name} (with label '{text}')")
204
+
205
+
206
+ @ui.command("create-image")
207
+ @click.argument("name")
208
+ @click.option(
209
+ "--parent", "-p",
210
+ required=True,
211
+ help="Parent Canvas or UI element."
212
+ )
213
+ @click.option(
214
+ "--sprite", "-s",
215
+ default=None,
216
+ help="Sprite asset path."
217
+ )
218
+ @handle_unity_errors
219
+ def create_image(name: str, parent: str, sprite: Optional[str]):
220
+ """Create a UI Image.
221
+
222
+ \b
223
+ Examples:
224
+ unity-mcp ui create-image "Background" --parent "MainUI"
225
+ unity-mcp ui create-image "Icon" --parent "MainUI" --sprite "Assets/Sprites/icon.png"
226
+ """
227
+ config = get_config()
228
+
229
+ # Step 1: Create empty GameObject with parent
230
+ result = run_command("manage_gameobject", {
231
+ "action": "create",
232
+ "name": name,
233
+ "parent": parent,
234
+ }, config)
235
+
236
+ if not (result.get("success") or result.get("data") or result.get("result")):
237
+ click.echo(format_output(result, config.format))
238
+ return
239
+
240
+ # Step 2: Add Image component
241
+ run_command("manage_components", {
242
+ "action": "add",
243
+ "target": name,
244
+ "componentType": "Image",
245
+ }, config)
246
+
247
+ # Step 3: Set sprite if provided
248
+ if sprite:
249
+ run_command("manage_components", {
250
+ "action": "set_property",
251
+ "target": name,
252
+ "componentType": "Image",
253
+ "property": "sprite",
254
+ "value": sprite,
255
+ }, config)
256
+
257
+ click.echo(format_output(result, config.format))
258
+ print_success(f"Created Image: {name}")
cli/commands/vfx.py ADDED
@@ -0,0 +1,421 @@
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, 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
13
+
14
+
15
+ _VFX_TOP_LEVEL_KEYS = {"action", "target", "searchMethod", "properties"}
16
+
17
+
18
+ def _normalize_vfx_params(params: dict[str, Any]) -> dict[str, Any]:
19
+ params = dict(params)
20
+ properties: dict[str, Any] = {}
21
+ for key in list(params.keys()):
22
+ if key in _VFX_TOP_LEVEL_KEYS:
23
+ continue
24
+ properties[key] = params.pop(key)
25
+
26
+ if properties:
27
+ existing = params.get("properties")
28
+ if isinstance(existing, dict):
29
+ params["properties"] = {**properties, **existing}
30
+ else:
31
+ params["properties"] = properties
32
+
33
+ return {k: v for k, v in params.items() if v is not None}
34
+
35
+
36
+ @click.group()
37
+ def vfx():
38
+ """VFX operations - particle systems, line renderers, trails."""
39
+ pass
40
+
41
+
42
+ # =============================================================================
43
+ # Particle System Commands
44
+ # =============================================================================
45
+
46
+ @vfx.group()
47
+ def particle():
48
+ """Particle system operations."""
49
+ pass
50
+
51
+
52
+ @particle.command("info")
53
+ @click.argument("target")
54
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
55
+ @handle_unity_errors
56
+ def particle_info(target: str, search_method: Optional[str]):
57
+ """Get particle system info.
58
+
59
+ \\b
60
+ Examples:
61
+ unity-mcp vfx particle info "Fire"
62
+ unity-mcp vfx particle info "-12345" --search-method by_id
63
+ """
64
+ config = get_config()
65
+ params: dict[str, Any] = {"action": "particle_get_info", "target": target}
66
+ if search_method:
67
+ params["searchMethod"] = search_method
68
+
69
+ result = run_command(
70
+ "manage_vfx", _normalize_vfx_params(params), config)
71
+ click.echo(format_output(result, config.format))
72
+
73
+
74
+ @particle.command("play")
75
+ @click.argument("target")
76
+ @click.option("--with-children", is_flag=True, help="Also play child particle systems.")
77
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
78
+ @handle_unity_errors
79
+ def particle_play(target: str, with_children: bool, search_method: Optional[str]):
80
+ """Play a particle system.
81
+
82
+ \\b
83
+ Examples:
84
+ unity-mcp vfx particle play "Fire"
85
+ unity-mcp vfx particle play "Effects" --with-children
86
+ """
87
+ config = get_config()
88
+ params: dict[str, Any] = {"action": "particle_play", "target": target}
89
+ if with_children:
90
+ params["withChildren"] = True
91
+ if search_method:
92
+ params["searchMethod"] = search_method
93
+
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}")
99
+
100
+
101
+ @particle.command("stop")
102
+ @click.argument("target")
103
+ @click.option("--with-children", is_flag=True, help="Also stop child particle systems.")
104
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
105
+ @handle_unity_errors
106
+ def particle_stop(target: str, with_children: bool, search_method: Optional[str]):
107
+ """Stop a particle system."""
108
+ config = get_config()
109
+ params: dict[str, Any] = {"action": "particle_stop", "target": target}
110
+ if with_children:
111
+ params["withChildren"] = True
112
+ if search_method:
113
+ params["searchMethod"] = search_method
114
+
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}")
120
+
121
+
122
+ @particle.command("pause")
123
+ @click.argument("target")
124
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
125
+ @handle_unity_errors
126
+ def particle_pause(target: str, search_method: Optional[str]):
127
+ """Pause a particle system."""
128
+ config = get_config()
129
+ params: dict[str, Any] = {"action": "particle_pause", "target": target}
130
+ if search_method:
131
+ params["searchMethod"] = search_method
132
+
133
+ result = run_command(
134
+ "manage_vfx", _normalize_vfx_params(params), config)
135
+ click.echo(format_output(result, config.format))
136
+
137
+
138
+ @particle.command("restart")
139
+ @click.argument("target")
140
+ @click.option("--with-children", is_flag=True)
141
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
142
+ @handle_unity_errors
143
+ def particle_restart(target: str, with_children: bool, search_method: Optional[str]):
144
+ """Restart a particle system."""
145
+ config = get_config()
146
+ params: dict[str, Any] = {"action": "particle_restart", "target": target}
147
+ if with_children:
148
+ params["withChildren"] = True
149
+ if search_method:
150
+ params["searchMethod"] = search_method
151
+
152
+ result = run_command(
153
+ "manage_vfx", _normalize_vfx_params(params), config)
154
+ click.echo(format_output(result, config.format))
155
+
156
+
157
+ @particle.command("clear")
158
+ @click.argument("target")
159
+ @click.option("--with-children", is_flag=True)
160
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
161
+ @handle_unity_errors
162
+ def particle_clear(target: str, with_children: bool, search_method: Optional[str]):
163
+ """Clear all particles from a particle system."""
164
+ config = get_config()
165
+ params: dict[str, Any] = {"action": "particle_clear", "target": target}
166
+ if with_children:
167
+ params["withChildren"] = True
168
+ if search_method:
169
+ params["searchMethod"] = search_method
170
+
171
+ result = run_command(
172
+ "manage_vfx", _normalize_vfx_params(params), config)
173
+ click.echo(format_output(result, config.format))
174
+
175
+
176
+ # =============================================================================
177
+ # Line Renderer Commands
178
+ # =============================================================================
179
+
180
+ @vfx.group()
181
+ def line():
182
+ """Line renderer operations."""
183
+ pass
184
+
185
+
186
+ @line.command("info")
187
+ @click.argument("target")
188
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
189
+ @handle_unity_errors
190
+ def line_info(target: str, search_method: Optional[str]):
191
+ """Get line renderer info.
192
+
193
+ \\b
194
+ Examples:
195
+ unity-mcp vfx line info "LaserBeam"
196
+ """
197
+ config = get_config()
198
+ params: dict[str, Any] = {"action": "line_get_info", "target": target}
199
+ if search_method:
200
+ params["searchMethod"] = search_method
201
+
202
+ result = run_command(
203
+ "manage_vfx", _normalize_vfx_params(params), config)
204
+ click.echo(format_output(result, config.format))
205
+
206
+
207
+ @line.command("set-positions")
208
+ @click.argument("target")
209
+ @click.option("--positions", "-p", required=True, help='Positions as JSON array: [[0,0,0], [1,1,1], [2,0,0]]')
210
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
211
+ @handle_unity_errors
212
+ def line_set_positions(target: str, positions: str, search_method: Optional[str]):
213
+ """Set all positions on a line renderer.
214
+
215
+ \\b
216
+ Examples:
217
+ unity-mcp vfx line set-positions "Line" --positions "[[0,0,0], [5,2,0], [10,0,0]]"
218
+ """
219
+ config = get_config()
220
+
221
+ positions_list = parse_json_list_or_exit(positions, "positions")
222
+
223
+ params: dict[str, Any] = {
224
+ "action": "line_set_positions",
225
+ "target": target,
226
+ "positions": positions_list,
227
+ }
228
+ if search_method:
229
+ params["searchMethod"] = search_method
230
+
231
+ result = run_command(
232
+ "manage_vfx", _normalize_vfx_params(params), config)
233
+ click.echo(format_output(result, config.format))
234
+
235
+
236
+ @line.command("create-line")
237
+ @click.argument("target")
238
+ @click.option("--start", nargs=3, type=float, required=True, help="Start point X Y Z")
239
+ @click.option("--end", nargs=3, type=float, required=True, help="End point X Y Z")
240
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
241
+ @handle_unity_errors
242
+ def line_create_line(target: str, start: Tuple[float, float, float], end: Tuple[float, float, float], search_method: Optional[str]):
243
+ """Create a simple line between two points.
244
+
245
+ \\b
246
+ Examples:
247
+ unity-mcp vfx line create-line "MyLine" --start 0 0 0 --end 10 5 0
248
+ """
249
+ config = get_config()
250
+ params: dict[str, Any] = {
251
+ "action": "line_create_line",
252
+ "target": target,
253
+ "start": list(start),
254
+ "end": list(end),
255
+ }
256
+ if search_method:
257
+ params["searchMethod"] = search_method
258
+
259
+ result = run_command(
260
+ "manage_vfx", _normalize_vfx_params(params), config)
261
+ click.echo(format_output(result, config.format))
262
+
263
+
264
+ @line.command("create-circle")
265
+ @click.argument("target")
266
+ @click.option("--center", nargs=3, type=float, default=(0, 0, 0), help="Center point X Y Z")
267
+ @click.option("--radius", type=float, required=True, help="Circle radius")
268
+ @click.option("--segments", type=int, default=32, help="Number of segments")
269
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
270
+ @handle_unity_errors
271
+ def line_create_circle(target: str, center: Tuple[float, float, float], radius: float, segments: int, search_method: Optional[str]):
272
+ """Create a circle shape.
273
+
274
+ \\b
275
+ Examples:
276
+ unity-mcp vfx line create-circle "Circle" --radius 5 --segments 64
277
+ unity-mcp vfx line create-circle "Ring" --center 0 2 0 --radius 3
278
+ """
279
+ config = get_config()
280
+ params: dict[str, Any] = {
281
+ "action": "line_create_circle",
282
+ "target": target,
283
+ "center": list(center),
284
+ "radius": radius,
285
+ "segments": segments,
286
+ }
287
+ if search_method:
288
+ params["searchMethod"] = search_method
289
+
290
+ result = run_command(
291
+ "manage_vfx", _normalize_vfx_params(params), config)
292
+ click.echo(format_output(result, config.format))
293
+
294
+
295
+ @line.command("clear")
296
+ @click.argument("target")
297
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
298
+ @handle_unity_errors
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
+ result = run_command(
307
+ "manage_vfx", _normalize_vfx_params(params), config)
308
+ click.echo(format_output(result, config.format))
309
+
310
+
311
+ # =============================================================================
312
+ # Trail Renderer Commands
313
+ # =============================================================================
314
+
315
+ @vfx.group()
316
+ def trail():
317
+ """Trail renderer operations."""
318
+ pass
319
+
320
+
321
+ @trail.command("info")
322
+ @click.argument("target")
323
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
324
+ @handle_unity_errors
325
+ def trail_info(target: str, search_method: Optional[str]):
326
+ """Get trail renderer info."""
327
+ config = get_config()
328
+ params: dict[str, Any] = {"action": "trail_get_info", "target": target}
329
+ if search_method:
330
+ params["searchMethod"] = search_method
331
+
332
+ result = run_command(
333
+ "manage_vfx", _normalize_vfx_params(params), config)
334
+ click.echo(format_output(result, config.format))
335
+
336
+
337
+ @trail.command("set-time")
338
+ @click.argument("target")
339
+ @click.argument("duration", type=float)
340
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
341
+ @handle_unity_errors
342
+ def trail_set_time(target: str, duration: float, search_method: Optional[str]):
343
+ """Set trail duration.
344
+
345
+ \\b
346
+ Examples:
347
+ unity-mcp vfx trail set-time "PlayerTrail" 2.0
348
+ """
349
+ config = get_config()
350
+ params: dict[str, Any] = {
351
+ "action": "trail_set_time",
352
+ "target": target,
353
+ "time": duration,
354
+ }
355
+ if search_method:
356
+ params["searchMethod"] = search_method
357
+
358
+ result = run_command(
359
+ "manage_vfx", _normalize_vfx_params(params), config)
360
+ click.echo(format_output(result, config.format))
361
+
362
+
363
+ @trail.command("clear")
364
+ @click.argument("target")
365
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
366
+ @handle_unity_errors
367
+ def trail_clear(target: str, search_method: Optional[str]):
368
+ """Clear a trail renderer."""
369
+ config = get_config()
370
+ params: dict[str, Any] = {"action": "trail_clear", "target": target}
371
+ if search_method:
372
+ params["searchMethod"] = search_method
373
+
374
+ result = run_command(
375
+ "manage_vfx", _normalize_vfx_params(params), config)
376
+ click.echo(format_output(result, config.format))
377
+
378
+
379
+ # =============================================================================
380
+ # Raw Command (escape hatch for all VFX actions)
381
+ # =============================================================================
382
+
383
+ @vfx.command("raw")
384
+ @click.argument("action")
385
+ @click.argument("target", required=False)
386
+ @click.option("--params", "-p", default="{}", help="Additional parameters as JSON.")
387
+ @click.option("--search-method", type=SEARCH_METHOD_CHOICE_TAGGED, default=None)
388
+ @handle_unity_errors
389
+ def vfx_raw(action: str, target: Optional[str], params: str, search_method: Optional[str]):
390
+ """Execute any VFX action directly.
391
+
392
+ For advanced users who need access to all 60+ VFX actions.
393
+
394
+ \\b
395
+ Actions include:
396
+ particle_*: particle_set_main, particle_set_emission, particle_set_shape, ...
397
+ vfx_*: vfx_set_float, vfx_send_event, vfx_play, ...
398
+ line_*: line_create_arc, line_create_bezier, ...
399
+ trail_*: trail_set_width, trail_set_color, ...
400
+
401
+ \\b
402
+ Examples:
403
+ unity-mcp vfx raw particle_set_main "Fire" --params '{"duration": 5, "looping": true}'
404
+ unity-mcp vfx raw line_create_arc "Arc" --params '{"radius": 3, "startAngle": 0, "endAngle": 180}'
405
+ unity-mcp vfx raw vfx_send_event "Explosion" --params '{"eventName": "OnSpawn"}'
406
+ """
407
+ config = get_config()
408
+
409
+ extra_params = parse_json_dict_or_exit(params, "params")
410
+
411
+ request_params: dict[str, Any] = {"action": action}
412
+ if target:
413
+ request_params["target"] = target
414
+ if search_method:
415
+ request_params["searchMethod"] = search_method
416
+
417
+ # Merge extra params
418
+ request_params.update(extra_params)
419
+ result = run_command(
420
+ "manage_vfx", _normalize_vfx_params(request_params), config)
421
+ click.echo(format_output(result, config.format))