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/component.py CHANGED
@@ -7,7 +7,10 @@ from typing import Optional, 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_value_safe, parse_json_dict_or_exit
12
+ from cli.utils.constants import SEARCH_METHOD_CHOICE_BASIC
13
+ from cli.utils.confirmation import confirm_destructive_action
11
14
 
12
15
 
13
16
  @click.group()
@@ -21,7 +24,7 @@ def component():
21
24
  @click.argument("component_type")
22
25
  @click.option(
23
26
  "--search-method",
24
- type=click.Choice(["by_id", "by_name", "by_path"]),
27
+ type=SEARCH_METHOD_CHOICE_BASIC,
25
28
  default=None,
26
29
  help="How to find the target GameObject."
27
30
  )
@@ -30,6 +33,7 @@ def component():
30
33
  default=None,
31
34
  help='Initial properties as JSON (e.g., \'{"mass": 5.0}\').'
32
35
  )
36
+ @handle_unity_errors
33
37
  def add(target: str, component_type: str, search_method: Optional[str], properties: Optional[str]):
34
38
  """Add a component to a GameObject.
35
39
 
@@ -50,20 +54,12 @@ def add(target: str, component_type: str, search_method: Optional[str], properti
50
54
  if search_method:
51
55
  params["searchMethod"] = search_method
52
56
  if properties:
53
- try:
54
- params["properties"] = json.loads(properties)
55
- except json.JSONDecodeError as e:
56
- print_error(f"Invalid JSON for properties: {e}")
57
- sys.exit(1)
58
-
59
- try:
60
- result = run_command("manage_components", params, config)
61
- click.echo(format_output(result, config.format))
62
- if result.get("success"):
63
- print_success(f"Added {component_type} to '{target}'")
64
- except UnityConnectionError as e:
65
- print_error(str(e))
66
- sys.exit(1)
57
+ params["properties"] = parse_json_dict_or_exit(properties, "properties")
58
+
59
+ result = run_command("manage_components", params, config)
60
+ click.echo(format_output(result, config.format))
61
+ if result.get("success"):
62
+ print_success(f"Added {component_type} to '{target}'")
67
63
 
68
64
 
69
65
  @component.command("remove")
@@ -71,7 +67,7 @@ def add(target: str, component_type: str, search_method: Optional[str], properti
71
67
  @click.argument("component_type")
72
68
  @click.option(
73
69
  "--search-method",
74
- type=click.Choice(["by_id", "by_name", "by_path"]),
70
+ type=SEARCH_METHOD_CHOICE_BASIC,
75
71
  default=None,
76
72
  help="How to find the target GameObject."
77
73
  )
@@ -80,6 +76,7 @@ def add(target: str, component_type: str, search_method: Optional[str], properti
80
76
  is_flag=True,
81
77
  help="Skip confirmation prompt."
82
78
  )
79
+ @handle_unity_errors
83
80
  def remove(target: str, component_type: str, search_method: Optional[str], force: bool):
84
81
  """Remove a component from a GameObject.
85
82
 
@@ -90,8 +87,7 @@ def remove(target: str, component_type: str, search_method: Optional[str], force
90
87
  """
91
88
  config = get_config()
92
89
 
93
- if not force:
94
- click.confirm(f"Remove {component_type} from '{target}'?", abort=True)
90
+ confirm_destructive_action("Remove", component_type, target, force, "from")
95
91
 
96
92
  params: dict[str, Any] = {
97
93
  "action": "remove",
@@ -102,14 +98,10 @@ def remove(target: str, component_type: str, search_method: Optional[str], force
102
98
  if search_method:
103
99
  params["searchMethod"] = search_method
104
100
 
105
- try:
106
- result = run_command("manage_components", params, config)
107
- click.echo(format_output(result, config.format))
108
- if result.get("success"):
109
- print_success(f"Removed {component_type} from '{target}'")
110
- except UnityConnectionError as e:
111
- print_error(str(e))
112
- sys.exit(1)
101
+ result = run_command("manage_components", params, config)
102
+ click.echo(format_output(result, config.format))
103
+ if result.get("success"):
104
+ print_success(f"Removed {component_type} from '{target}'")
113
105
 
114
106
 
115
107
  @component.command("set")
@@ -119,10 +111,11 @@ def remove(target: str, component_type: str, search_method: Optional[str], force
119
111
  @click.argument("value")
120
112
  @click.option(
121
113
  "--search-method",
122
- type=click.Choice(["by_id", "by_name", "by_path"]),
114
+ type=SEARCH_METHOD_CHOICE_BASIC,
123
115
  default=None,
124
116
  help="How to find the target GameObject."
125
117
  )
118
+ @handle_unity_errors
126
119
  def set_property(target: str, component_type: str, property_name: str, value: str, search_method: Optional[str]):
127
120
  """Set a single property on a component.
128
121
 
@@ -135,11 +128,7 @@ def set_property(target: str, component_type: str, property_name: str, value: st
135
128
  config = get_config()
136
129
 
137
130
  # Try to parse value as JSON for complex types
138
- try:
139
- parsed_value = json.loads(value)
140
- except json.JSONDecodeError:
141
- # Keep as string if not valid JSON
142
- parsed_value = value
131
+ parsed_value = parse_value_safe(value)
143
132
 
144
133
  params: dict[str, Any] = {
145
134
  "action": "set_property",
@@ -152,14 +141,10 @@ def set_property(target: str, component_type: str, property_name: str, value: st
152
141
  if search_method:
153
142
  params["searchMethod"] = search_method
154
143
 
155
- try:
156
- result = run_command("manage_components", params, config)
157
- click.echo(format_output(result, config.format))
158
- if result.get("success"):
159
- print_success(f"Set {component_type}.{property_name} = {value}")
160
- except UnityConnectionError as e:
161
- print_error(str(e))
162
- sys.exit(1)
144
+ result = run_command("manage_components", params, config)
145
+ click.echo(format_output(result, config.format))
146
+ if result.get("success"):
147
+ print_success(f"Set {component_type}.{property_name} = {value}")
163
148
 
164
149
 
165
150
  @component.command("modify")
@@ -172,10 +157,11 @@ def set_property(target: str, component_type: str, property_name: str, value: st
172
157
  )
173
158
  @click.option(
174
159
  "--search-method",
175
- type=click.Choice(["by_id", "by_name", "by_path"]),
160
+ type=SEARCH_METHOD_CHOICE_BASIC,
176
161
  default=None,
177
162
  help="How to find the target GameObject."
178
163
  )
164
+ @handle_unity_errors
179
165
  def modify(target: str, component_type: str, properties: str, search_method: Optional[str]):
180
166
  """Set multiple properties on a component at once.
181
167
 
@@ -186,11 +172,7 @@ def modify(target: str, component_type: str, properties: str, search_method: Opt
186
172
  """
187
173
  config = get_config()
188
174
 
189
- try:
190
- props_dict = json.loads(properties)
191
- except json.JSONDecodeError as e:
192
- print_error(f"Invalid JSON for properties: {e}")
193
- sys.exit(1)
175
+ props_dict = parse_json_dict_or_exit(properties, "properties")
194
176
 
195
177
  params: dict[str, Any] = {
196
178
  "action": "set_property",
@@ -202,11 +184,7 @@ def modify(target: str, component_type: str, properties: str, search_method: Opt
202
184
  if search_method:
203
185
  params["searchMethod"] = search_method
204
186
 
205
- try:
206
- result = run_command("manage_components", params, config)
207
- click.echo(format_output(result, config.format))
208
- if result.get("success"):
209
- print_success(f"Modified {component_type} on '{target}'")
210
- except UnityConnectionError as e:
211
- print_error(str(e))
212
- sys.exit(1)
187
+ result = run_command("manage_components", params, config)
188
+ click.echo(format_output(result, config.format))
189
+ if result.get("success"):
190
+ print_success(f"Modified {component_type} on '{target}'")
cli/commands/editor.py CHANGED
@@ -6,8 +6,9 @@ from typing import Optional, Any
6
6
 
7
7
  from cli.utils.config import get_config
8
8
  from cli.utils.output import format_output, print_error, print_success, print_info
9
- from cli.utils.connection import run_command, run_list_custom_tools, UnityConnectionError
9
+ from cli.utils.connection import run_command, run_list_custom_tools, handle_unity_errors, UnityConnectionError
10
10
  from cli.utils.suggestions import suggest_matches, format_suggestions
11
+ from cli.utils.parsers import parse_json_dict_or_exit
11
12
 
12
13
 
13
14
  @click.group()
@@ -17,48 +18,36 @@ def editor():
17
18
 
18
19
 
19
20
  @editor.command("play")
21
+ @handle_unity_errors
20
22
  def play():
21
23
  """Enter play mode."""
22
24
  config = get_config()
23
-
24
- try:
25
- result = run_command("manage_editor", {"action": "play"}, config)
26
- click.echo(format_output(result, config.format))
27
- if result.get("success"):
28
- print_success("Entered play mode")
29
- except UnityConnectionError as e:
30
- print_error(str(e))
31
- sys.exit(1)
25
+ result = run_command("manage_editor", {"action": "play"}, config)
26
+ click.echo(format_output(result, config.format))
27
+ if result.get("success"):
28
+ print_success("Entered play mode")
32
29
 
33
30
 
34
31
  @editor.command("pause")
32
+ @handle_unity_errors
35
33
  def pause():
36
34
  """Pause play mode."""
37
35
  config = get_config()
38
-
39
- try:
40
- result = run_command("manage_editor", {"action": "pause"}, config)
41
- click.echo(format_output(result, config.format))
42
- if result.get("success"):
43
- print_success("Paused play mode")
44
- except UnityConnectionError as e:
45
- print_error(str(e))
46
- sys.exit(1)
36
+ result = run_command("manage_editor", {"action": "pause"}, config)
37
+ click.echo(format_output(result, config.format))
38
+ if result.get("success"):
39
+ print_success("Paused play mode")
47
40
 
48
41
 
49
42
  @editor.command("stop")
43
+ @handle_unity_errors
50
44
  def stop():
51
45
  """Stop play mode."""
52
46
  config = get_config()
53
-
54
- try:
55
- result = run_command("manage_editor", {"action": "stop"}, config)
56
- click.echo(format_output(result, config.format))
57
- if result.get("success"):
58
- print_success("Stopped play mode")
59
- except UnityConnectionError as e:
60
- print_error(str(e))
61
- sys.exit(1)
47
+ result = run_command("manage_editor", {"action": "stop"}, config)
48
+ click.echo(format_output(result, config.format))
49
+ if result.get("success"):
50
+ print_success("Stopped play mode")
62
51
 
63
52
 
64
53
  @editor.command("console")
@@ -92,6 +81,7 @@ def stop():
92
81
  is_flag=True,
93
82
  help="Clear the console instead of reading."
94
83
  )
84
+ @handle_unity_errors
95
85
  def console(log_types: tuple, count: int, filter_text: Optional[str], stacktrace: bool, clear: bool):
96
86
  """Read or clear the Unity console.
97
87
 
@@ -105,14 +95,10 @@ def console(log_types: tuple, count: int, filter_text: Optional[str], stacktrace
105
95
  config = get_config()
106
96
 
107
97
  if clear:
108
- try:
109
- result = run_command("read_console", {"action": "clear"}, config)
110
- click.echo(format_output(result, config.format))
111
- if result.get("success"):
112
- print_success("Console cleared")
113
- except UnityConnectionError as e:
114
- print_error(str(e))
115
- sys.exit(1)
98
+ result = run_command("read_console", {"action": "clear"}, config)
99
+ click.echo(format_output(result, config.format))
100
+ if result.get("success"):
101
+ print_success("Console cleared")
116
102
  return
117
103
 
118
104
  params: dict[str, Any] = {
@@ -125,16 +111,13 @@ def console(log_types: tuple, count: int, filter_text: Optional[str], stacktrace
125
111
  if filter_text:
126
112
  params["filter_text"] = filter_text
127
113
 
128
- try:
129
- result = run_command("read_console", params, config)
130
- click.echo(format_output(result, config.format))
131
- except UnityConnectionError as e:
132
- print_error(str(e))
133
- sys.exit(1)
114
+ result = run_command("read_console", params, config)
115
+ click.echo(format_output(result, config.format))
134
116
 
135
117
 
136
118
  @editor.command("add-tag")
137
119
  @click.argument("tag_name")
120
+ @handle_unity_errors
138
121
  def add_tag(tag_name: str):
139
122
  """Add a new tag.
140
123
 
@@ -144,20 +127,16 @@ def add_tag(tag_name: str):
144
127
  unity-mcp editor add-tag "Collectible"
145
128
  """
146
129
  config = get_config()
147
-
148
- try:
149
- result = run_command(
150
- "manage_editor", {"action": "add_tag", "tagName": tag_name}, config)
151
- click.echo(format_output(result, config.format))
152
- if result.get("success"):
153
- print_success(f"Added tag: {tag_name}")
154
- except UnityConnectionError as e:
155
- print_error(str(e))
156
- sys.exit(1)
130
+ result = run_command(
131
+ "manage_editor", {"action": "add_tag", "tagName": tag_name}, config)
132
+ click.echo(format_output(result, config.format))
133
+ if result.get("success"):
134
+ print_success(f"Added tag: {tag_name}")
157
135
 
158
136
 
159
137
  @editor.command("remove-tag")
160
138
  @click.argument("tag_name")
139
+ @handle_unity_errors
161
140
  def remove_tag(tag_name: str):
162
141
  """Remove a tag.
163
142
 
@@ -166,20 +145,16 @@ def remove_tag(tag_name: str):
166
145
  unity-mcp editor remove-tag "OldTag"
167
146
  """
168
147
  config = get_config()
169
-
170
- try:
171
- result = run_command(
172
- "manage_editor", {"action": "remove_tag", "tagName": tag_name}, config)
173
- click.echo(format_output(result, config.format))
174
- if result.get("success"):
175
- print_success(f"Removed tag: {tag_name}")
176
- except UnityConnectionError as e:
177
- print_error(str(e))
178
- sys.exit(1)
148
+ result = run_command(
149
+ "manage_editor", {"action": "remove_tag", "tagName": tag_name}, config)
150
+ click.echo(format_output(result, config.format))
151
+ if result.get("success"):
152
+ print_success(f"Removed tag: {tag_name}")
179
153
 
180
154
 
181
155
  @editor.command("add-layer")
182
156
  @click.argument("layer_name")
157
+ @handle_unity_errors
183
158
  def add_layer(layer_name: str):
184
159
  """Add a new layer.
185
160
 
@@ -188,20 +163,16 @@ def add_layer(layer_name: str):
188
163
  unity-mcp editor add-layer "Interactable"
189
164
  """
190
165
  config = get_config()
191
-
192
- try:
193
- result = run_command(
194
- "manage_editor", {"action": "add_layer", "layerName": layer_name}, config)
195
- click.echo(format_output(result, config.format))
196
- if result.get("success"):
197
- print_success(f"Added layer: {layer_name}")
198
- except UnityConnectionError as e:
199
- print_error(str(e))
200
- sys.exit(1)
166
+ result = run_command(
167
+ "manage_editor", {"action": "add_layer", "layerName": layer_name}, config)
168
+ click.echo(format_output(result, config.format))
169
+ if result.get("success"):
170
+ print_success(f"Added layer: {layer_name}")
201
171
 
202
172
 
203
173
  @editor.command("remove-layer")
204
174
  @click.argument("layer_name")
175
+ @handle_unity_errors
205
176
  def remove_layer(layer_name: str):
206
177
  """Remove a layer.
207
178
 
@@ -210,20 +181,16 @@ def remove_layer(layer_name: str):
210
181
  unity-mcp editor remove-layer "OldLayer"
211
182
  """
212
183
  config = get_config()
213
-
214
- try:
215
- result = run_command(
216
- "manage_editor", {"action": "remove_layer", "layerName": layer_name}, config)
217
- click.echo(format_output(result, config.format))
218
- if result.get("success"):
219
- print_success(f"Removed layer: {layer_name}")
220
- except UnityConnectionError as e:
221
- print_error(str(e))
222
- sys.exit(1)
184
+ result = run_command(
185
+ "manage_editor", {"action": "remove_layer", "layerName": layer_name}, config)
186
+ click.echo(format_output(result, config.format))
187
+ if result.get("success"):
188
+ print_success(f"Removed layer: {layer_name}")
223
189
 
224
190
 
225
191
  @editor.command("tool")
226
192
  @click.argument("tool_name")
193
+ @handle_unity_errors
227
194
  def set_tool(tool_name: str):
228
195
  """Set the active editor tool.
229
196
 
@@ -234,20 +201,16 @@ def set_tool(tool_name: str):
234
201
  unity-mcp editor tool "Scale"
235
202
  """
236
203
  config = get_config()
237
-
238
- try:
239
- result = run_command(
240
- "manage_editor", {"action": "set_active_tool", "toolName": tool_name}, config)
241
- click.echo(format_output(result, config.format))
242
- if result.get("success"):
243
- print_success(f"Set active tool: {tool_name}")
244
- except UnityConnectionError as e:
245
- print_error(str(e))
246
- sys.exit(1)
204
+ result = run_command(
205
+ "manage_editor", {"action": "set_active_tool", "toolName": tool_name}, config)
206
+ click.echo(format_output(result, config.format))
207
+ if result.get("success"):
208
+ print_success(f"Set active tool: {tool_name}")
247
209
 
248
210
 
249
211
  @editor.command("menu")
250
212
  @click.argument("menu_path")
213
+ @handle_unity_errors
251
214
  def execute_menu(menu_path: str):
252
215
  """Execute a menu item.
253
216
 
@@ -258,16 +221,10 @@ def execute_menu(menu_path: str):
258
221
  unity-mcp editor menu "GameObject/Create Empty"
259
222
  """
260
223
  config = get_config()
261
-
262
- try:
263
- result = run_command("execute_menu_item", {
264
- "menu_path": menu_path}, config)
265
- click.echo(format_output(result, config.format))
266
- if result.get("success"):
267
- print_success(f"Executed: {menu_path}")
268
- except UnityConnectionError as e:
269
- print_error(str(e))
270
- sys.exit(1)
224
+ result = run_command("execute_menu_item", {"menu_path": menu_path}, config)
225
+ click.echo(format_output(result, config.format))
226
+ if result.get("success"):
227
+ print_success(f"Executed: {menu_path}")
271
228
 
272
229
 
273
230
  @editor.command("tests")
@@ -298,6 +255,7 @@ def execute_menu(menu_path: str):
298
255
  is_flag=True,
299
256
  help="Include details for failed/skipped tests only."
300
257
  )
258
+ @handle_unity_errors
301
259
  def run_tests(mode: str, async_mode: bool, wait: Optional[int], details: bool, failed_only: bool):
302
260
  """Run Unity tests.
303
261
 
@@ -318,21 +276,17 @@ def run_tests(mode: str, async_mode: bool, wait: Optional[int], details: bool, f
318
276
  if failed_only:
319
277
  params["include_failed_tests"] = True
320
278
 
321
- try:
322
- result = run_command("run_tests", params, config)
279
+ result = run_command("run_tests", params, config)
323
280
 
324
- # For async mode, just show job ID
325
- if async_mode and result.get("success"):
326
- job_id = result.get("data", {}).get("job_id")
327
- if job_id:
328
- click.echo(f"Test job started: {job_id}")
329
- print_info("Poll with: unity-mcp editor poll-test " + job_id)
330
- return
281
+ # For async mode, just show job ID
282
+ if async_mode and result.get("success"):
283
+ job_id = result.get("data", {}).get("job_id")
284
+ if job_id:
285
+ click.echo(f"Test job started: {job_id}")
286
+ print_info("Poll with: unity-mcp editor poll-test " + job_id)
287
+ return
331
288
 
332
- click.echo(format_output(result, config.format))
333
- except UnityConnectionError as e:
334
- print_error(str(e))
335
- sys.exit(1)
289
+ click.echo(format_output(result, config.format))
336
290
 
337
291
 
338
292
  @editor.command("poll-test")
@@ -353,6 +307,7 @@ def run_tests(mode: str, async_mode: bool, wait: Optional[int], details: bool, f
353
307
  is_flag=True,
354
308
  help="Include details for failed/skipped tests only."
355
309
  )
310
+ @handle_unity_errors
356
311
  def poll_test(job_id: str, wait: int, details: bool, failed_only: bool):
357
312
  """Poll an async test job for status/results.
358
313
 
@@ -372,27 +327,23 @@ def poll_test(job_id: str, wait: int, details: bool, failed_only: bool):
372
327
  if failed_only:
373
328
  params["include_failed_tests"] = True
374
329
 
375
- try:
376
- result = run_command("get_test_job", params, config)
377
- click.echo(format_output(result, config.format))
378
-
379
- if isinstance(result, dict) and result.get("success"):
380
- data = result.get("data", {})
381
- status = data.get("status", "unknown")
382
- if status == "succeeded":
383
- print_success("Tests completed successfully")
384
- elif status == "failed":
385
- summary = data.get("result", {}).get("summary", {})
386
- failed = summary.get("failed", 0)
387
- print_error(f"Tests failed: {failed} failures")
388
- elif status == "running":
389
- progress = data.get("progress", {})
390
- completed = progress.get("completed", 0)
391
- total = progress.get("total", 0)
392
- print_info(f"Tests running: {completed}/{total}")
393
- except UnityConnectionError as e:
394
- print_error(str(e))
395
- sys.exit(1)
330
+ result = run_command("get_test_job", params, config)
331
+ click.echo(format_output(result, config.format))
332
+
333
+ if isinstance(result, dict) and result.get("success"):
334
+ data = result.get("data", {})
335
+ status = data.get("status", "unknown")
336
+ if status == "succeeded":
337
+ print_success("Tests completed successfully")
338
+ elif status == "failed":
339
+ summary = data.get("result", {}).get("summary", {})
340
+ failed = summary.get("failed", 0)
341
+ print_error(f"Tests failed: {failed} failures")
342
+ elif status == "running":
343
+ progress = data.get("progress", {})
344
+ completed = progress.get("completed", 0)
345
+ total = progress.get("total", 0)
346
+ print_info(f"Tests running: {completed}/{total}")
396
347
 
397
348
 
398
349
  @editor.command("refresh")
@@ -418,6 +369,7 @@ def poll_test(job_id: str, wait: int, details: bool, failed_only: bool):
418
369
  is_flag=True,
419
370
  help="Don't wait for refresh to complete."
420
371
  )
372
+ @handle_unity_errors
421
373
  def refresh(mode: str, scope: str, compile: bool, no_wait: bool):
422
374
  """Force Unity to refresh assets/scripts.
423
375
 
@@ -438,15 +390,11 @@ def refresh(mode: str, scope: str, compile: bool, no_wait: bool):
438
390
  if compile:
439
391
  params["compile"] = "request"
440
392
 
441
- try:
442
- click.echo("Refreshing Unity...")
443
- result = run_command("refresh_unity", params, config)
444
- click.echo(format_output(result, config.format))
445
- if result.get("success"):
446
- print_success("Unity refreshed")
447
- except UnityConnectionError as e:
448
- print_error(str(e))
449
- sys.exit(1)
393
+ click.echo("Refreshing Unity...")
394
+ result = run_command("refresh_unity", params, config)
395
+ click.echo(format_output(result, config.format))
396
+ if result.get("success"):
397
+ print_success("Unity refreshed")
450
398
 
451
399
 
452
400
  @editor.command("custom-tool")
@@ -456,6 +404,7 @@ def refresh(mode: str, scope: str, compile: bool, no_wait: bool):
456
404
  default="{}",
457
405
  help="Tool parameters as JSON."
458
406
  )
407
+ @handle_unity_errors
459
408
  def custom_tool(tool_name: str, params: str):
460
409
  """Execute a custom Unity tool.
461
410
 
@@ -466,48 +415,33 @@ def custom_tool(tool_name: str, params: str):
466
415
  unity-mcp editor custom-tool "MyCustomTool"
467
416
  unity-mcp editor custom-tool "BuildPipeline" --params '{"target": "Android"}'
468
417
  """
469
- import json
470
418
  config = get_config()
471
419
 
472
- try:
473
- params_dict = json.loads(params)
474
- except json.JSONDecodeError as e:
475
- print_error(f"Invalid JSON for params: {e}")
476
- print_info("Example: --params '{\"key\":\"value\"}'")
477
- print_info(
478
- "Tip: wrap JSON in single quotes to avoid shell escaping issues.")
479
- sys.exit(1)
480
-
481
- try:
482
- result = run_command("execute_custom_tool", {
483
- "tool_name": tool_name,
484
- "parameters": params_dict,
485
- }, config)
486
- click.echo(format_output(result, config.format))
487
- if result.get("success"):
488
- print_success(f"Executed custom tool: {tool_name}")
489
- else:
490
- message = (result.get("message")
491
- or result.get("error") or "").lower()
492
- if "not found" in message and "tool" in message:
493
- try:
494
- tools_result = run_list_custom_tools(config)
495
- tools = tools_result.get("tools")
496
- if tools is None:
497
- data = tools_result.get("data", {})
498
- tools = data.get("tools") if isinstance(
499
- data, dict) else None
500
- names = [
501
- t.get("name") for t in tools if isinstance(t, dict) and t.get("name")
502
- ] if isinstance(tools, list) else []
503
- matches = suggest_matches(tool_name, names)
504
- suggestion = format_suggestions(matches)
505
- if suggestion:
506
- print_info(suggestion)
507
- print_info(
508
- f'Example: unity-mcp editor custom-tool "{matches[0]}"')
509
- except UnityConnectionError:
510
- pass
511
- except UnityConnectionError as e:
512
- print_error(str(e))
513
- sys.exit(1)
420
+ params_dict = parse_json_dict_or_exit(params, "params")
421
+
422
+ result = run_command("execute_custom_tool", {
423
+ "tool_name": tool_name,
424
+ "parameters": params_dict,
425
+ }, config)
426
+ click.echo(format_output(result, config.format))
427
+ if result.get("success"):
428
+ print_success(f"Executed custom tool: {tool_name}")
429
+ else:
430
+ message = (result.get("message") or result.get("error") or "").lower()
431
+ if "not found" in message and "tool" in message:
432
+ try:
433
+ tools_result = run_list_custom_tools(config)
434
+ tools = tools_result.get("tools")
435
+ if tools is None:
436
+ data = tools_result.get("data", {})
437
+ tools = data.get("tools") if isinstance(data, dict) else None
438
+ names = [
439
+ t.get("name") for t in tools if isinstance(t, dict) and t.get("name")
440
+ ] if isinstance(tools, list) else []
441
+ matches = suggest_matches(tool_name, names)
442
+ suggestion = format_suggestions(matches)
443
+ if suggestion:
444
+ print_info(suggestion)
445
+ print_info(f'Example: unity-mcp editor custom-tool "{matches[0]}"')
446
+ except UnityConnectionError:
447
+ pass