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/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Unity MCP Command Line Interface."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,3 @@
1
+ """CLI command modules."""
2
+
3
+ # Commands will be registered in main.py
@@ -0,0 +1,84 @@
1
+ """Animation CLI commands - placeholder for future implementation."""
2
+
3
+ import click
4
+ from typing import Optional, Any
5
+
6
+ from cli.utils.config import get_config
7
+ from cli.utils.output import format_output, print_error, print_info
8
+ from cli.utils.connection import run_command, handle_unity_errors
9
+ from cli.utils.constants import SEARCH_METHOD_CHOICE_BASIC
10
+
11
+
12
+ @click.group()
13
+ def animation():
14
+ """Animation operations - control Animator, play animations."""
15
+ pass
16
+
17
+
18
+ @animation.command("play")
19
+ @click.argument("target")
20
+ @click.argument("state_name")
21
+ @click.option(
22
+ "--layer", "-l",
23
+ default=0,
24
+ type=int,
25
+ help="Animator layer(TODO)."
26
+ )
27
+ @click.option(
28
+ "--search-method",
29
+ type=SEARCH_METHOD_CHOICE_BASIC,
30
+ default=None,
31
+ help="How to find the target."
32
+ )
33
+ @handle_unity_errors
34
+ def play(target: str, state_name: str, layer: int, search_method: Optional[str]):
35
+ """Play an animation state on a target's Animator.
36
+
37
+ \b
38
+ Examples:
39
+ unity-mcp animation play "Player" "Walk"
40
+ unity-mcp animation play "Enemy" "Attack" --layer 1
41
+ """
42
+ config = get_config()
43
+
44
+ # Set Animator parameter to trigger state
45
+ params: dict[str, Any] = {
46
+ "action": "set_property",
47
+ "target": target,
48
+ "componentType": "Animator",
49
+ "property": "Play",
50
+ "value": state_name,
51
+ "layer": layer,
52
+ }
53
+
54
+ if search_method:
55
+ params["searchMethod"] = search_method
56
+
57
+ result = run_command("manage_components", params, config)
58
+ click.echo(format_output(result, config.format))
59
+
60
+
61
+ @animation.command("set-parameter")
62
+ @click.argument("target")
63
+ @click.argument("param_name")
64
+ @click.argument("value")
65
+ @click.option(
66
+ "--type", "-t",
67
+ "param_type",
68
+ type=click.Choice(["float", "int", "bool", "trigger"]),
69
+ default="float",
70
+ help="Parameter type."
71
+ )
72
+ def set_parameter(target: str, param_name: str, value: str, param_type: str):
73
+ """Set an Animator parameter.
74
+
75
+ \b
76
+ Examples:
77
+ unity-mcp animation set-parameter "Player" "Speed" 5.0
78
+ unity-mcp animation set-parameter "Player" "IsRunning" true --type bool
79
+ unity-mcp animation set-parameter "Player" "Jump" "" --type trigger
80
+ """
81
+ config = get_config()
82
+ print_info(
83
+ "Animation parameter command - requires custom Unity implementation")
84
+ click.echo(f"Would set {param_name}={value} ({param_type}) on {target}")
cli/commands/asset.py ADDED
@@ -0,0 +1,280 @@
1
+ """Asset CLI commands."""
2
+
3
+ import sys
4
+ import json
5
+ import click
6
+ from typing import Optional, 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_dict_or_exit
12
+ from cli.utils.confirmation import confirm_destructive_action
13
+
14
+
15
+ @click.group()
16
+ def asset():
17
+ """Asset operations - search, import, create, delete assets."""
18
+ pass
19
+
20
+
21
+ @asset.command("search")
22
+ @click.argument("pattern", default="*")
23
+ @click.option(
24
+ "--path", "-p",
25
+ default="Assets",
26
+ help="Folder path to search in."
27
+ )
28
+ @click.option(
29
+ "--type", "-t",
30
+ "filter_type",
31
+ default=None,
32
+ help="Filter by asset type (e.g., Material, Prefab, MonoScript)."
33
+ )
34
+ @click.option(
35
+ "--limit", "-l",
36
+ default=25,
37
+ type=int,
38
+ help="Maximum results per page."
39
+ )
40
+ @click.option(
41
+ "--page",
42
+ default=1,
43
+ type=int,
44
+ help="Page number (1-based)."
45
+ )
46
+ @handle_unity_errors
47
+ def search(pattern: str, path: str, filter_type: Optional[str], limit: int, page: int):
48
+ """Search for assets.
49
+
50
+ \b
51
+ Examples:
52
+ unity-mcp asset search "*.prefab"
53
+ unity-mcp asset search "Player*" --path "Assets/Characters"
54
+ unity-mcp asset search "*" --type Material
55
+ unity-mcp asset search "t:MonoScript" --path "Assets/Scripts"
56
+ """
57
+ config = get_config()
58
+
59
+ params: dict[str, Any] = {
60
+ "action": "search",
61
+ "path": path,
62
+ "searchPattern": pattern,
63
+ "pageSize": limit,
64
+ "pageNumber": page,
65
+ }
66
+
67
+ if filter_type:
68
+ params["filterType"] = filter_type
69
+
70
+ result = run_command("manage_asset", params, config)
71
+ click.echo(format_output(result, config.format))
72
+
73
+
74
+ @asset.command("info")
75
+ @click.argument("path")
76
+ @click.option(
77
+ "--preview",
78
+ is_flag=True,
79
+ help="Generate preview thumbnail (may be large)."
80
+ )
81
+ @handle_unity_errors
82
+ def info(path: str, preview: bool):
83
+ """Get detailed information about an asset.
84
+
85
+ \b
86
+ Examples:
87
+ unity-mcp asset info "Assets/Materials/Red.mat"
88
+ unity-mcp asset info "Assets/Prefabs/Player.prefab" --preview
89
+ """
90
+ config = get_config()
91
+
92
+ params: dict[str, Any] = {
93
+ "action": "get_info",
94
+ "path": path,
95
+ "generatePreview": preview,
96
+ }
97
+
98
+ result = run_command("manage_asset", params, config)
99
+ click.echo(format_output(result, config.format))
100
+
101
+
102
+ @asset.command("create")
103
+ @click.argument("path")
104
+ @click.argument("asset_type")
105
+ @click.option(
106
+ "--properties", "-p",
107
+ default=None,
108
+ help='Initial properties as JSON.'
109
+ )
110
+ @handle_unity_errors
111
+ def create(path: str, asset_type: str, properties: Optional[str]):
112
+ """Create a new asset.
113
+
114
+ \b
115
+ Examples:
116
+ unity-mcp asset create "Assets/Materials/Blue.mat" Material
117
+ unity-mcp asset create "Assets/NewFolder" Folder
118
+ unity-mcp asset create "Assets/Materials/Custom.mat" Material --properties '{"color": [0,0,1,1]}'
119
+ """
120
+ config = get_config()
121
+
122
+ params: dict[str, Any] = {
123
+ "action": "create",
124
+ "path": path,
125
+ "assetType": asset_type,
126
+ }
127
+
128
+ if properties:
129
+ params["properties"] = parse_json_dict_or_exit(properties, "properties")
130
+
131
+ result = run_command("manage_asset", params, config)
132
+ click.echo(format_output(result, config.format))
133
+ if result.get("success"):
134
+ print_success(f"Created {asset_type}: {path}")
135
+
136
+
137
+ @asset.command("delete")
138
+ @click.argument("path")
139
+ @click.option(
140
+ "--force", "-f",
141
+ is_flag=True,
142
+ help="Skip confirmation prompt."
143
+ )
144
+ @handle_unity_errors
145
+ def delete(path: str, force: bool):
146
+ """Delete an asset.
147
+
148
+ \b
149
+ Examples:
150
+ unity-mcp asset delete "Assets/OldMaterial.mat"
151
+ unity-mcp asset delete "Assets/Unused" --force
152
+ """
153
+ config = get_config()
154
+
155
+ confirm_destructive_action("Delete", "asset", path, force)
156
+
157
+ result = run_command(
158
+ "manage_asset", {"action": "delete", "path": path}, config)
159
+ click.echo(format_output(result, config.format))
160
+ if result.get("success"):
161
+ print_success(f"Deleted: {path}")
162
+
163
+
164
+ @asset.command("duplicate")
165
+ @click.argument("source")
166
+ @click.argument("destination")
167
+ @handle_unity_errors
168
+ def duplicate(source: str, destination: str):
169
+ """Duplicate an asset.
170
+
171
+ \b
172
+ Examples:
173
+ unity-mcp asset duplicate "Assets/Materials/Red.mat" "Assets/Materials/RedCopy.mat"
174
+ """
175
+ config = get_config()
176
+
177
+ params: dict[str, Any] = {
178
+ "action": "duplicate",
179
+ "path": source,
180
+ "destination": destination,
181
+ }
182
+
183
+ result = run_command("manage_asset", params, config)
184
+ click.echo(format_output(result, config.format))
185
+ if result.get("success"):
186
+ print_success(f"Duplicated to: {destination}")
187
+
188
+
189
+ @asset.command("move")
190
+ @click.argument("source")
191
+ @click.argument("destination")
192
+ @handle_unity_errors
193
+ def move(source: str, destination: str):
194
+ """Move an asset to a new location.
195
+
196
+ \b
197
+ Examples:
198
+ unity-mcp asset move "Assets/Old/Material.mat" "Assets/New/Material.mat"
199
+ """
200
+ config = get_config()
201
+
202
+ params: dict[str, Any] = {
203
+ "action": "move",
204
+ "path": source,
205
+ "destination": destination,
206
+ }
207
+
208
+ result = run_command("manage_asset", params, config)
209
+ click.echo(format_output(result, config.format))
210
+ if result.get("success"):
211
+ print_success(f"Moved to: {destination}")
212
+
213
+
214
+ @asset.command("rename")
215
+ @click.argument("path")
216
+ @click.argument("new_name")
217
+ @handle_unity_errors
218
+ def rename(path: str, new_name: str):
219
+ """Rename an asset.
220
+
221
+ \b
222
+ Examples:
223
+ unity-mcp asset rename "Assets/Materials/Old.mat" "New.mat"
224
+ """
225
+ config = get_config()
226
+
227
+ # Construct destination path
228
+ import os
229
+ dir_path = os.path.dirname(path)
230
+ destination = os.path.join(dir_path, new_name).replace("\\", "/")
231
+
232
+ params: dict[str, Any] = {
233
+ "action": "rename",
234
+ "path": path,
235
+ "destination": destination,
236
+ }
237
+
238
+ result = run_command("manage_asset", params, config)
239
+ click.echo(format_output(result, config.format))
240
+ if result.get("success"):
241
+ print_success(f"Renamed to: {new_name}")
242
+
243
+
244
+ @asset.command("import")
245
+ @click.argument("path")
246
+ @handle_unity_errors
247
+ def import_asset(path: str):
248
+ """Import/reimport an asset.
249
+
250
+ \b
251
+ Examples:
252
+ unity-mcp asset import "Assets/Textures/NewTexture.png"
253
+ """
254
+ config = get_config()
255
+
256
+ result = run_command(
257
+ "manage_asset", {"action": "import", "path": path}, config)
258
+ click.echo(format_output(result, config.format))
259
+ if result.get("success"):
260
+ print_success(f"Imported: {path}")
261
+
262
+
263
+ @asset.command("mkdir")
264
+ @click.argument("path")
265
+ @handle_unity_errors
266
+ def mkdir(path: str):
267
+ """Create a folder.
268
+
269
+ \b
270
+ Examples:
271
+ unity-mcp asset mkdir "Assets/NewFolder"
272
+ unity-mcp asset mkdir "Assets/Levels/Chapter1"
273
+ """
274
+ config = get_config()
275
+
276
+ result = run_command(
277
+ "manage_asset", {"action": "create_folder", "path": path}, config)
278
+ click.echo(format_output(result, config.format))
279
+ if result.get("success"):
280
+ print_success(f"Created folder: {path}")
cli/commands/audio.py ADDED
@@ -0,0 +1,125 @@
1
+ """Audio 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_info
9
+ from cli.utils.connection import run_command, handle_unity_errors
10
+ from cli.utils.constants import SEARCH_METHOD_CHOICE_BASIC
11
+
12
+
13
+ @click.group()
14
+ def audio():
15
+ """Audio operations - AudioSource control, audio settings."""
16
+ pass
17
+
18
+
19
+ @audio.command("play")
20
+ @click.argument("target")
21
+ @click.option(
22
+ "--clip", "-c",
23
+ default=None,
24
+ help="Audio clip path to play."
25
+ )
26
+ @click.option(
27
+ "--search-method",
28
+ type=SEARCH_METHOD_CHOICE_BASIC,
29
+ default=None,
30
+ help="How to find the target."
31
+ )
32
+ @handle_unity_errors
33
+ def play(target: str, clip: Optional[str], search_method: Optional[str]):
34
+ """Play audio on a target's AudioSource.
35
+
36
+ \b
37
+ Examples:
38
+ unity-mcp audio play "MusicPlayer"
39
+ unity-mcp audio play "SFXSource" --clip "Assets/Audio/explosion.wav"
40
+ """
41
+ config = get_config()
42
+
43
+ params: dict[str, Any] = {
44
+ "action": "set_property",
45
+ "target": target,
46
+ "componentType": "AudioSource",
47
+ "property": "Play",
48
+ "value": True,
49
+ }
50
+
51
+ if clip:
52
+ params["clip"] = clip
53
+
54
+ if search_method:
55
+ params["searchMethod"] = search_method
56
+
57
+ result = run_command("manage_components", params, config)
58
+ click.echo(format_output(result, config.format))
59
+
60
+
61
+ @audio.command("stop")
62
+ @click.argument("target")
63
+ @click.option(
64
+ "--search-method",
65
+ type=SEARCH_METHOD_CHOICE_BASIC,
66
+ default=None,
67
+ help="How to find the target."
68
+ )
69
+ @handle_unity_errors
70
+ def stop(target: str, search_method: Optional[str]):
71
+ """Stop audio on a target's AudioSource.
72
+
73
+ \b
74
+ Examples:
75
+ unity-mcp audio stop "MusicPlayer"
76
+ """
77
+ config = get_config()
78
+
79
+ params: dict[str, Any] = {
80
+ "action": "set_property",
81
+ "target": target,
82
+ "componentType": "AudioSource",
83
+ "property": "Stop",
84
+ "value": True,
85
+ }
86
+
87
+ if search_method:
88
+ params["searchMethod"] = search_method
89
+
90
+ result = run_command("manage_components", params, config)
91
+ click.echo(format_output(result, config.format))
92
+
93
+
94
+ @audio.command("volume")
95
+ @click.argument("target")
96
+ @click.argument("level", type=float)
97
+ @click.option(
98
+ "--search-method",
99
+ type=SEARCH_METHOD_CHOICE_BASIC,
100
+ default=None,
101
+ help="How to find the target."
102
+ )
103
+ @handle_unity_errors
104
+ def volume(target: str, level: float, search_method: Optional[str]):
105
+ """Set audio volume on a target's AudioSource.
106
+
107
+ \b
108
+ Examples:
109
+ unity-mcp audio volume "MusicPlayer" 0.5
110
+ """
111
+ config = get_config()
112
+
113
+ params: dict[str, Any] = {
114
+ "action": "set_property",
115
+ "target": target,
116
+ "componentType": "AudioSource",
117
+ "property": "volume",
118
+ "value": level,
119
+ }
120
+
121
+ if search_method:
122
+ params["searchMethod"] = search_method
123
+
124
+ result = run_command("manage_components", params, config)
125
+ click.echo(format_output(result, config.format))
cli/commands/batch.py ADDED
@@ -0,0 +1,171 @@
1
+ """Batch CLI commands for executing multiple Unity operations efficiently."""
2
+
3
+ import sys
4
+ import json
5
+ import click
6
+ from typing import Optional, Any
7
+
8
+ from cli.utils.config import get_config
9
+ from cli.utils.output import format_output, print_error, print_success, print_info
10
+ from cli.utils.connection import run_command, handle_unity_errors
11
+ from cli.utils.parsers import parse_json_list_or_exit
12
+
13
+
14
+ @click.group()
15
+ def batch():
16
+ """Batch operations - execute multiple commands efficiently."""
17
+ pass
18
+
19
+
20
+ @batch.command("run")
21
+ @click.argument("file", type=click.Path(exists=True))
22
+ @click.option("--parallel", is_flag=True, help="Execute read-only commands in parallel.")
23
+ @click.option("--fail-fast", is_flag=True, help="Stop on first failure.")
24
+ @handle_unity_errors
25
+ def batch_run(file: str, parallel: bool, fail_fast: bool):
26
+ """Execute commands from a JSON file.
27
+
28
+ The JSON file should contain an array of command objects with 'tool' and 'params' keys.
29
+
30
+ \\b
31
+ File format:
32
+ [
33
+ {"tool": "manage_gameobject", "params": {"action": "create", "name": "Cube1"}},
34
+ {"tool": "manage_gameobject", "params": {"action": "create", "name": "Cube2"}},
35
+ {"tool": "manage_components", "params": {"action": "add", "target": "Cube1", "componentType": "Rigidbody"}}
36
+ ]
37
+
38
+ \\b
39
+ Examples:
40
+ unity-mcp batch run commands.json
41
+ unity-mcp batch run setup.json --parallel
42
+ unity-mcp batch run critical.json --fail-fast
43
+ """
44
+ config = get_config()
45
+
46
+ try:
47
+ with open(file, 'r') as f:
48
+ commands = json.load(f)
49
+ except json.JSONDecodeError as e:
50
+ print_error(f"Invalid JSON in file: {e}")
51
+ sys.exit(1)
52
+ except IOError as e:
53
+ print_error(f"Error reading file: {e}")
54
+ sys.exit(1)
55
+
56
+ if not isinstance(commands, list):
57
+ print_error("JSON file must contain an array of commands")
58
+ sys.exit(1)
59
+
60
+ if len(commands) > 25:
61
+ print_error(f"Maximum 25 commands per batch, got {len(commands)}")
62
+ sys.exit(1)
63
+
64
+ params: dict[str, Any] = {"commands": commands}
65
+ if parallel:
66
+ params["parallel"] = True
67
+ if fail_fast:
68
+ params["failFast"] = True
69
+
70
+ click.echo(f"Executing {len(commands)} commands...")
71
+
72
+ result = run_command("batch_execute", params, config)
73
+ click.echo(format_output(result, config.format))
74
+
75
+ if isinstance(result, dict):
76
+ results = result.get("data", {}).get("results", [])
77
+ succeeded = sum(1 for r in results if r.get("success"))
78
+ failed = len(results) - succeeded
79
+
80
+ if failed == 0:
81
+ print_success(
82
+ f"All {succeeded} commands completed successfully")
83
+ else:
84
+ print_info(f"{succeeded} succeeded, {failed} failed")
85
+
86
+
87
+ @batch.command("inline")
88
+ @click.argument("commands_json")
89
+ @click.option("--parallel", is_flag=True, help="Execute read-only commands in parallel.")
90
+ @click.option("--fail-fast", is_flag=True, help="Stop on first failure.")
91
+ @handle_unity_errors
92
+ def batch_inline(commands_json: str, parallel: bool, fail_fast: bool):
93
+ """Execute commands from inline JSON.
94
+
95
+ \\b
96
+ Examples:
97
+ unity-mcp batch inline '[{"tool": "manage_scene", "params": {"action": "get_active"}}]'
98
+
99
+ unity-mcp batch inline '[
100
+ {"tool": "manage_gameobject", "params": {"action": "create", "name": "A", "primitiveType": "Cube"}},
101
+ {"tool": "manage_gameobject", "params": {"action": "create", "name": "B", "primitiveType": "Sphere"}}
102
+ ]'
103
+ """
104
+ config = get_config()
105
+
106
+ commands = parse_json_list_or_exit(commands_json, "commands")
107
+
108
+ if len(commands) > 25:
109
+ print_error(f"Maximum 25 commands per batch, got {len(commands)}")
110
+ sys.exit(1)
111
+
112
+ params: dict[str, Any] = {"commands": commands}
113
+ if parallel:
114
+ params["parallel"] = True
115
+ if fail_fast:
116
+ params["failFast"] = True
117
+
118
+ result = run_command("batch_execute", params, config)
119
+ click.echo(format_output(result, config.format))
120
+
121
+
122
+ @batch.command("template")
123
+ @click.option("--output", "-o", type=click.Path(), help="Output file (default: stdout)")
124
+ def batch_template(output: Optional[str]):
125
+ """Generate a sample batch commands file.
126
+
127
+ \\b
128
+ Examples:
129
+ unity-mcp batch template > commands.json
130
+ unity-mcp batch template -o my_batch.json
131
+ """
132
+ template = [
133
+ {
134
+ "tool": "manage_scene",
135
+ "params": {"action": "get_active"}
136
+ },
137
+ {
138
+ "tool": "manage_gameobject",
139
+ "params": {
140
+ "action": "create",
141
+ "name": "BatchCube",
142
+ "primitiveType": "Cube",
143
+ "position": [0, 1, 0]
144
+ }
145
+ },
146
+ {
147
+ "tool": "manage_components",
148
+ "params": {
149
+ "action": "add",
150
+ "target": "BatchCube",
151
+ "componentType": "Rigidbody"
152
+ }
153
+ },
154
+ {
155
+ "tool": "manage_gameobject",
156
+ "params": {
157
+ "action": "modify",
158
+ "target": "BatchCube",
159
+ "position": [0, 5, 0]
160
+ }
161
+ }
162
+ ]
163
+
164
+ json_output = json.dumps(template, indent=2)
165
+
166
+ if output:
167
+ with open(output, 'w') as f:
168
+ f.write(json_output)
169
+ print_success(f"Template written to: {output}")
170
+ else:
171
+ click.echo(json_output)