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/main.py ADDED
@@ -0,0 +1,281 @@
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.suggestions import suggest_matches, format_suggestions
12
+ from cli.utils.output import format_output, print_error, print_success, print_info
13
+ from cli.utils.connection import (
14
+ run_command,
15
+ run_check_connection,
16
+ run_list_instances,
17
+ UnityConnectionError,
18
+ warn_if_remote_host,
19
+ )
20
+
21
+
22
+ # Context object to pass configuration between commands
23
+ class Context:
24
+ def __init__(self):
25
+ self.config: Optional[CLIConfig] = None
26
+ self.verbose: bool = False
27
+
28
+
29
+ pass_context = click.make_pass_decorator(Context, ensure=True)
30
+
31
+
32
+ _ORIGINAL_RESOLVE_COMMAND = click.Group.resolve_command
33
+
34
+
35
+ def _resolve_command_with_suggestions(self: click.Group, ctx: click.Context, args: list[str]):
36
+ try:
37
+ return _ORIGINAL_RESOLVE_COMMAND(self, ctx, args)
38
+ except click.exceptions.NoSuchCommand as e:
39
+ if not args or args[0].startswith("-"):
40
+ raise
41
+ matches = suggest_matches(args[0], self.list_commands(ctx))
42
+ suggestion = format_suggestions(matches)
43
+ if suggestion:
44
+ message = f"{e}\n{suggestion}"
45
+ raise click.exceptions.UsageError(message, ctx=ctx)
46
+ raise
47
+ except click.exceptions.UsageError as e:
48
+ if args and not args[0].startswith("-") and "No such command" in str(e):
49
+ matches = suggest_matches(args[0], self.list_commands(ctx))
50
+ suggestion = format_suggestions(matches)
51
+ if suggestion:
52
+ message = f"{e}\n{suggestion}"
53
+ raise click.exceptions.UsageError(message, ctx=ctx)
54
+ raise
55
+
56
+
57
+ # Install suggestion handling for all CLI command groups.
58
+ click.Group.resolve_command = _resolve_command_with_suggestions # type: ignore[assignment]
59
+
60
+
61
+ @click.group()
62
+ @click.version_option(version=__version__, prog_name="unity-mcp")
63
+ @click.option(
64
+ "--host", "-h",
65
+ default="127.0.0.1",
66
+ envvar="UNITY_MCP_HOST",
67
+ help="MCP server host address."
68
+ )
69
+ @click.option(
70
+ "--port", "-p",
71
+ default=8080,
72
+ type=int,
73
+ envvar="UNITY_MCP_HTTP_PORT",
74
+ help="MCP server port."
75
+ )
76
+ @click.option(
77
+ "--timeout", "-t",
78
+ default=30,
79
+ type=int,
80
+ envvar="UNITY_MCP_TIMEOUT",
81
+ help="Command timeout in seconds."
82
+ )
83
+ @click.option(
84
+ "--format", "-f",
85
+ type=click.Choice(["text", "json", "table"]),
86
+ default="text",
87
+ envvar="UNITY_MCP_FORMAT",
88
+ help="Output format."
89
+ )
90
+ @click.option(
91
+ "--instance", "-i",
92
+ default=None,
93
+ envvar="UNITY_MCP_INSTANCE",
94
+ help="Target Unity instance (hash or Name@hash)."
95
+ )
96
+ @click.option(
97
+ "--verbose", "-v",
98
+ is_flag=True,
99
+ help="Enable verbose output."
100
+ )
101
+ @pass_context
102
+ def cli(ctx: Context, host: str, port: int, timeout: int, format: str, instance: Optional[str], verbose: bool):
103
+ """Unity MCP Command Line Interface.
104
+
105
+ Control Unity Editor directly from the command line using the Model Context Protocol.
106
+
107
+ \b
108
+ Examples:
109
+ unity-mcp status
110
+ unity-mcp gameobject find "Player"
111
+ unity-mcp scene hierarchy --format json
112
+ unity-mcp editor play
113
+
114
+ \b
115
+ Environment Variables:
116
+ UNITY_MCP_HOST Server host (default: 127.0.0.1)
117
+ UNITY_MCP_HTTP_PORT Server port (default: 8080)
118
+ UNITY_MCP_TIMEOUT Timeout in seconds (default: 30)
119
+ UNITY_MCP_FORMAT Output format (default: text)
120
+ UNITY_MCP_INSTANCE Target Unity instance
121
+ """
122
+ config = CLIConfig(
123
+ host=host,
124
+ port=port,
125
+ timeout=timeout,
126
+ format=format,
127
+ unity_instance=instance,
128
+ )
129
+
130
+ # Security warning for non-localhost connections
131
+ warn_if_remote_host(config)
132
+
133
+ set_config(config)
134
+ ctx.config = config
135
+ ctx.verbose = verbose
136
+
137
+
138
+ @cli.command("status")
139
+ @pass_context
140
+ def status(ctx: Context):
141
+ """Check connection status to Unity MCP server."""
142
+ config = ctx.config or get_config()
143
+
144
+ click.echo(f"Checking connection to {config.host}:{config.port}...")
145
+
146
+ if run_check_connection(config):
147
+ print_success(
148
+ f"Connected to Unity MCP server at {config.host}:{config.port}")
149
+
150
+ # Try to get Unity instances
151
+ try:
152
+ result = run_list_instances(config)
153
+ instances = result.get("instances", []) if isinstance(
154
+ result, dict) else []
155
+ if instances:
156
+ click.echo("\nConnected Unity instances:")
157
+ for inst in instances:
158
+ project = inst.get("project", "Unknown")
159
+ version = inst.get("unity_version", "Unknown")
160
+ hash_id = inst.get("hash", "")[:8]
161
+ click.echo(f" • {project} (Unity {version}) [{hash_id}]")
162
+ else:
163
+ print_info("No Unity instances currently connected")
164
+ except UnityConnectionError as e:
165
+ print_info(f"Could not retrieve Unity instances: {e}")
166
+ else:
167
+ print_error(
168
+ f"Cannot connect to Unity MCP server at {config.host}:{config.port}")
169
+ sys.exit(1)
170
+
171
+
172
+ @cli.command("instances")
173
+ @pass_context
174
+ def list_instances(ctx: Context):
175
+ """List available Unity instances."""
176
+ config = ctx.config or get_config()
177
+
178
+ try:
179
+ instances = run_list_instances(config)
180
+ click.echo(format_output(instances, config.format))
181
+ except UnityConnectionError as e:
182
+ print_error(str(e))
183
+ sys.exit(1)
184
+
185
+
186
+ @cli.command("raw")
187
+ @click.argument("command_type")
188
+ @click.argument("params", required=False, default="{}")
189
+ @pass_context
190
+ def raw_command(ctx: Context, command_type: str, params: str):
191
+ """Send a raw command to Unity.
192
+
193
+ \b
194
+ Examples:
195
+ unity-mcp raw manage_scene '{"action": "get_hierarchy"}'
196
+ unity-mcp raw read_console '{"count": 10}'
197
+ """
198
+ import json
199
+ config = ctx.config or get_config()
200
+
201
+ try:
202
+ params_dict = json.loads(params)
203
+ except json.JSONDecodeError as e:
204
+ print_error(f"Invalid JSON params: {e}")
205
+ sys.exit(1)
206
+
207
+ try:
208
+ result = run_command(command_type, params_dict, config)
209
+ click.echo(format_output(result, config.format))
210
+ except UnityConnectionError as e:
211
+ print_error(str(e))
212
+ sys.exit(1)
213
+
214
+
215
+ # Import and register command groups
216
+ # These will be implemented in subsequent TODOs
217
+ def register_commands():
218
+ """Register all command groups."""
219
+ def register_optional_command(module_name: str, command_name: str) -> None:
220
+ try:
221
+ module = import_module(module_name)
222
+ except ModuleNotFoundError as e:
223
+ if e.name == module_name:
224
+ return
225
+ print_error(
226
+ f"Failed to load command module '{module_name}': {e}"
227
+ )
228
+ return
229
+ except Exception as e:
230
+ print_error(
231
+ f"Failed to load command module '{module_name}': {e}"
232
+ )
233
+ return
234
+
235
+ command = getattr(module, command_name, None)
236
+ if command is None:
237
+ print_error(
238
+ f"Command '{command_name}' not found in '{module_name}'"
239
+ )
240
+ return
241
+
242
+ cli.add_command(command)
243
+
244
+ optional_commands = [
245
+ ("cli.commands.tool", "tool"),
246
+ ("cli.commands.tool", "custom_tool"),
247
+ ("cli.commands.gameobject", "gameobject"),
248
+ ("cli.commands.component", "component"),
249
+ ("cli.commands.scene", "scene"),
250
+ ("cli.commands.asset", "asset"),
251
+ ("cli.commands.script", "script"),
252
+ ("cli.commands.code", "code"),
253
+ ("cli.commands.editor", "editor"),
254
+ ("cli.commands.prefab", "prefab"),
255
+ ("cli.commands.material", "material"),
256
+ ("cli.commands.lighting", "lighting"),
257
+ ("cli.commands.animation", "animation"),
258
+ ("cli.commands.audio", "audio"),
259
+ ("cli.commands.ui", "ui"),
260
+ ("cli.commands.instance", "instance"),
261
+ ("cli.commands.shader", "shader"),
262
+ ("cli.commands.vfx", "vfx"),
263
+ ("cli.commands.batch", "batch"),
264
+ ("cli.commands.texture", "texture"),
265
+ ]
266
+
267
+ for module_name, command_name in optional_commands:
268
+ register_optional_command(module_name, command_name)
269
+
270
+
271
+ # Register commands on import
272
+ register_commands()
273
+
274
+
275
+ def main():
276
+ """Main entry point for the CLI."""
277
+ cli()
278
+
279
+
280
+ if __name__ == "__main__":
281
+ main()
cli/utils/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """CLI utility modules."""
2
+
3
+ from cli.utils.config import CLIConfig, get_config, set_config
4
+ from cli.utils.connection import (
5
+ run_command,
6
+ run_check_connection,
7
+ run_list_instances,
8
+ UnityConnectionError,
9
+ )
10
+ from cli.utils.output import (
11
+ format_output,
12
+ print_success,
13
+ print_error,
14
+ print_warning,
15
+ print_info,
16
+ )
17
+
18
+ __all__ = [
19
+ "CLIConfig",
20
+ "UnityConnectionError",
21
+ "format_output",
22
+ "get_config",
23
+ "print_error",
24
+ "print_info",
25
+ "print_success",
26
+ "print_warning",
27
+ "run_check_connection",
28
+ "run_command",
29
+ "run_list_instances",
30
+ "set_config",
31
+ ]
cli/utils/config.py ADDED
@@ -0,0 +1,58 @@
1
+ """CLI Configuration utilities."""
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import Optional
6
+
7
+
8
+ @dataclass
9
+ class CLIConfig:
10
+ """Configuration for CLI connection to Unity."""
11
+
12
+ host: str = "127.0.0.1"
13
+ port: int = 8080
14
+ timeout: int = 30
15
+ format: str = "text" # text, json, table
16
+ unity_instance: Optional[str] = None
17
+
18
+ @classmethod
19
+ def from_env(cls) -> "CLIConfig":
20
+ port_raw = os.environ.get("UNITY_MCP_HTTP_PORT", "8080")
21
+ try:
22
+ port = int(port_raw)
23
+ except (ValueError, TypeError):
24
+ raise ValueError(
25
+ f"Invalid UNITY_MCP_HTTP_PORT value: {port_raw!r}")
26
+
27
+ timeout_raw = os.environ.get("UNITY_MCP_TIMEOUT", "30")
28
+ try:
29
+ timeout = int(timeout_raw)
30
+ except (ValueError, TypeError):
31
+ raise ValueError(
32
+ f"Invalid UNITY_MCP_TIMEOUT value: {timeout_raw!r}")
33
+
34
+ return cls(
35
+ host=os.environ.get("UNITY_MCP_HOST", "127.0.0.1"),
36
+ port=port,
37
+ timeout=timeout,
38
+ format=os.environ.get("UNITY_MCP_FORMAT", "text"),
39
+ unity_instance=os.environ.get("UNITY_MCP_INSTANCE"),
40
+ )
41
+
42
+
43
+ # Global config instance
44
+ _config: Optional[CLIConfig] = None
45
+
46
+
47
+ def get_config() -> CLIConfig:
48
+ """Get the current CLI configuration."""
49
+ global _config
50
+ if _config is None:
51
+ _config = CLIConfig.from_env()
52
+ return _config
53
+
54
+
55
+ def set_config(config: CLIConfig) -> None:
56
+ """Set the CLI configuration."""
57
+ global _config
58
+ _config = config
@@ -0,0 +1,37 @@
1
+ """Confirmation dialog utilities for CLI commands."""
2
+
3
+ import click
4
+
5
+
6
+ def confirm_destructive_action(
7
+ action: str,
8
+ item_type: str,
9
+ item_name: str,
10
+ force: bool,
11
+ extra_context: str = ""
12
+ ) -> None:
13
+ """Prompt user to confirm destructive action unless --force flag is set.
14
+
15
+ Args:
16
+ action: The action being performed (e.g., "Delete", "Remove")
17
+ item_type: The type of item (e.g., "script", "GameObject", "asset")
18
+ item_name: The name/path of the item
19
+ force: If True, skip confirmation prompt
20
+ extra_context: Optional additional context (e.g., "from 'Player'")
21
+
22
+ Raises:
23
+ click.Abort: If user declines confirmation
24
+
25
+ Examples:
26
+ confirm_destructive_action("Delete", "script", "MyScript.cs", force=False)
27
+ # Prompts: "Delete script 'MyScript.cs'?"
28
+
29
+ confirm_destructive_action("Remove", "Rigidbody", "Player", force=False, extra_context="from")
30
+ # Prompts: "Remove Rigidbody from 'Player'?"
31
+ """
32
+ if not force:
33
+ if extra_context:
34
+ message = f"{action} {item_type} {extra_context} '{item_name}'?"
35
+ else:
36
+ message = f"{action} {item_type} '{item_name}'?"
37
+ click.confirm(message, abort=True)
@@ -0,0 +1,254 @@
1
+ """Connection utilities for CLI to communicate with Unity via MCP server."""
2
+
3
+ import asyncio
4
+ import functools
5
+ import sys
6
+ from typing import Any, Callable, Dict, Optional, TypeVar
7
+
8
+ import httpx
9
+
10
+ from cli.utils.config import get_config, CLIConfig
11
+
12
+
13
+ class UnityConnectionError(Exception):
14
+ """Raised when connection to Unity fails."""
15
+ pass
16
+
17
+
18
+ F = TypeVar("F", bound=Callable[..., Any])
19
+
20
+
21
+ def handle_unity_errors(func: F) -> F:
22
+ """Decorator that handles UnityConnectionError consistently.
23
+
24
+ Wraps a CLI command function and catches UnityConnectionError,
25
+ printing a formatted error message and exiting with code 1.
26
+
27
+ Usage:
28
+ @scene.command("active")
29
+ @handle_unity_errors
30
+ def active():
31
+ config = get_config()
32
+ result = run_command("manage_scene", {"action": "get_active"}, config)
33
+ click.echo(format_output(result, config.format))
34
+ """
35
+ from cli.utils.output import print_error
36
+
37
+ @functools.wraps(func)
38
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
39
+ try:
40
+ return func(*args, **kwargs)
41
+ except UnityConnectionError as e:
42
+ print_error(str(e))
43
+ sys.exit(1)
44
+
45
+ return wrapper # type: ignore[return-value]
46
+
47
+
48
+ def warn_if_remote_host(config: CLIConfig) -> None:
49
+ """Warn user if connecting to a non-localhost server.
50
+
51
+ This is a security measure to alert users that connecting to remote
52
+ servers exposes Unity control to potential network attacks.
53
+
54
+ Args:
55
+ config: CLI configuration with host setting
56
+ """
57
+ import click
58
+
59
+ local_hosts = ("127.0.0.1", "localhost", "::1", "0.0.0.0")
60
+ if config.host.lower() not in local_hosts:
61
+ click.echo(
62
+ "⚠️ Security Warning: Connecting to non-localhost server.\n"
63
+ " The MCP CLI has no authentication. Anyone on the network could\n"
64
+ " intercept commands or send unauthorized commands to Unity.\n"
65
+ " Only proceed if you trust this network.\n",
66
+ err=True
67
+ )
68
+
69
+
70
+ async def send_command(
71
+ command_type: str,
72
+ params: Dict[str, Any],
73
+ config: Optional[CLIConfig] = None,
74
+ timeout: Optional[int] = None,
75
+ ) -> Dict[str, Any]:
76
+ """Send a command to Unity via the MCP HTTP server.
77
+
78
+ Args:
79
+ command_type: The command type (e.g., 'manage_gameobject', 'manage_scene')
80
+ params: Command parameters
81
+ config: Optional CLI configuration
82
+ timeout: Optional timeout override
83
+
84
+ Returns:
85
+ Response dict from Unity
86
+
87
+ Raises:
88
+ UnityConnectionError: If connection fails
89
+ """
90
+ cfg = config or get_config()
91
+ url = f"http://{cfg.host}:{cfg.port}/api/command"
92
+
93
+ payload = {
94
+ "type": command_type,
95
+ "params": params,
96
+ }
97
+
98
+ if cfg.unity_instance:
99
+ payload["unity_instance"] = cfg.unity_instance
100
+
101
+ try:
102
+ async with httpx.AsyncClient() as client:
103
+ response = await client.post(
104
+ url,
105
+ json=payload,
106
+ timeout=timeout or cfg.timeout,
107
+ )
108
+ response.raise_for_status()
109
+ return response.json()
110
+ except httpx.ConnectError as e:
111
+ raise UnityConnectionError(
112
+ f"Cannot connect to Unity MCP server at {cfg.host}:{cfg.port}. "
113
+ f"Make sure the server is running and Unity is connected.\n"
114
+ f"Error: {e}"
115
+ )
116
+ except httpx.TimeoutException:
117
+ raise UnityConnectionError(
118
+ f"Connection to Unity timed out after {timeout or cfg.timeout}s. "
119
+ f"Unity may be busy or unresponsive."
120
+ )
121
+ except httpx.HTTPStatusError as e:
122
+ raise UnityConnectionError(
123
+ f"HTTP error from server: {e.response.status_code} - {e.response.text}"
124
+ )
125
+ except Exception as e:
126
+ raise UnityConnectionError(f"Unexpected error: {e}")
127
+
128
+
129
+ def run_command(
130
+ command_type: str,
131
+ params: Dict[str, Any],
132
+ config: Optional[CLIConfig] = None,
133
+ timeout: Optional[int] = None,
134
+ ) -> Dict[str, Any]:
135
+ """Synchronous wrapper for send_command.
136
+
137
+ Args:
138
+ command_type: The command type
139
+ params: Command parameters
140
+ config: Optional CLI configuration
141
+ timeout: Optional timeout override
142
+
143
+ Returns:
144
+ Response dict from Unity
145
+ """
146
+ return asyncio.run(send_command(command_type, params, config, timeout))
147
+
148
+
149
+ async def check_connection(config: Optional[CLIConfig] = None) -> bool:
150
+ """Check if we can connect to the Unity MCP server.
151
+
152
+ Args:
153
+ config: Optional CLI configuration
154
+
155
+ Returns:
156
+ True if connection successful, False otherwise
157
+ """
158
+ cfg = config or get_config()
159
+ url = f"http://{cfg.host}:{cfg.port}/health"
160
+
161
+ try:
162
+ async with httpx.AsyncClient() as client:
163
+ response = await client.get(url, timeout=5)
164
+ return response.status_code == 200
165
+ except Exception:
166
+ return False
167
+
168
+
169
+ def run_check_connection(config: Optional[CLIConfig] = None) -> bool:
170
+ """Synchronous wrapper for check_connection."""
171
+ return asyncio.run(check_connection(config))
172
+
173
+
174
+ async def list_unity_instances(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
175
+ """List available Unity instances.
176
+
177
+ Args:
178
+ config: Optional CLI configuration
179
+
180
+ Returns:
181
+ Dict with list of Unity instances
182
+ """
183
+ cfg = config or get_config()
184
+
185
+ url = f"http://{cfg.host}:{cfg.port}/api/instances"
186
+
187
+ try:
188
+ async with httpx.AsyncClient() as client:
189
+ response = await client.get(url, timeout=10)
190
+ response.raise_for_status()
191
+ data = response.json()
192
+ if "instances" in data:
193
+ return data
194
+ except httpx.ConnectError as e:
195
+ raise UnityConnectionError(
196
+ f"Cannot connect to Unity MCP server at {cfg.host}:{cfg.port}. "
197
+ f"Make sure the server is running and Unity is connected.\n"
198
+ f"Error: {e}"
199
+ )
200
+ except httpx.TimeoutException:
201
+ raise UnityConnectionError(
202
+ "Connection to Unity timed out while listing instances. "
203
+ "Unity may be busy or unresponsive."
204
+ )
205
+ except httpx.HTTPStatusError as e:
206
+ raise UnityConnectionError(
207
+ f"HTTP error from server: {e.response.status_code} - {e.response.text}"
208
+ )
209
+ except Exception as e:
210
+ raise UnityConnectionError(f"Unexpected error: {e}")
211
+
212
+ raise UnityConnectionError("Failed to list Unity instances")
213
+
214
+
215
+ def run_list_instances(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
216
+ """Synchronous wrapper for list_unity_instances."""
217
+ return asyncio.run(list_unity_instances(config))
218
+
219
+
220
+ async def list_custom_tools(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
221
+ """List custom tools registered for the active Unity project."""
222
+ cfg = config or get_config()
223
+ url = f"http://{cfg.host}:{cfg.port}/api/custom-tools"
224
+ params: Dict[str, Any] = {}
225
+ if cfg.unity_instance:
226
+ params["instance"] = cfg.unity_instance
227
+
228
+ try:
229
+ async with httpx.AsyncClient() as client:
230
+ response = await client.get(url, params=params, timeout=cfg.timeout)
231
+ response.raise_for_status()
232
+ return response.json()
233
+ except httpx.ConnectError as e:
234
+ raise UnityConnectionError(
235
+ f"Cannot connect to Unity MCP server at {cfg.host}:{cfg.port}. "
236
+ f"Make sure the server is running and Unity is connected.\n"
237
+ f"Error: {e}"
238
+ )
239
+ except httpx.TimeoutException:
240
+ raise UnityConnectionError(
241
+ f"Connection to Unity timed out after {cfg.timeout}s. "
242
+ f"Unity may be busy or unresponsive."
243
+ )
244
+ except httpx.HTTPStatusError as e:
245
+ raise UnityConnectionError(
246
+ f"HTTP error from server: {e.response.status_code} - {e.response.text}"
247
+ )
248
+ except Exception as e:
249
+ raise UnityConnectionError(f"Unexpected error: {e}")
250
+
251
+
252
+ def run_list_custom_tools(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
253
+ """Synchronous wrapper for list_custom_tools."""
254
+ return asyncio.run(list_custom_tools(config))
cli/utils/constants.py ADDED
@@ -0,0 +1,23 @@
1
+ """Common constants for CLI commands."""
2
+ import click
3
+
4
+ # Search method constants used across various CLI commands
5
+ # These define how GameObjects and other Unity objects can be located
6
+
7
+ # Full set of search methods (used by gameobject commands)
8
+ SEARCH_METHODS_FULL = ["by_name", "by_path", "by_id", "by_tag", "by_layer", "by_component"]
9
+
10
+ # Basic search methods (used by component, animation, audio commands)
11
+ SEARCH_METHODS_BASIC = ["by_id", "by_name", "by_path"]
12
+
13
+ # Extended search methods for renderer-based commands (material commands)
14
+ SEARCH_METHODS_RENDERER = ["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"]
15
+
16
+ # Tagged search methods (used by VFX commands)
17
+ SEARCH_METHODS_TAGGED = ["by_name", "by_path", "by_id", "by_tag"]
18
+
19
+ # Click choice options for each set
20
+ SEARCH_METHOD_CHOICE_FULL = click.Choice(SEARCH_METHODS_FULL)
21
+ SEARCH_METHOD_CHOICE_BASIC = click.Choice(SEARCH_METHODS_BASIC)
22
+ SEARCH_METHOD_CHOICE_RENDERER = click.Choice(SEARCH_METHODS_RENDERER)
23
+ SEARCH_METHOD_CHOICE_TAGGED = click.Choice(SEARCH_METHODS_TAGGED)