"""Unity MCP Command Line Interface - Main Entry Point."""
import sys
from importlib import import_module
import click
from typing import Optional
from cli import __version__
from cli.utils.config import CLIConfig, set_config, get_config
from cli.utils.suggestions import suggest_matches, format_suggestions
from cli.utils.output import format_output, print_error, print_success, print_info
from cli.utils.connection import (
run_command,
run_check_connection,
run_list_instances,
UnityConnectionError,
warn_if_remote_host,
)
# Context object to pass configuration between commands
class Context:
def __init__(self):
self.config: Optional[CLIConfig] = None
self.verbose: bool = False
pass_context = click.make_pass_decorator(Context, ensure=True)
_ORIGINAL_RESOLVE_COMMAND = click.Group.resolve_command
def _resolve_command_with_suggestions(self: click.Group, ctx: click.Context, args: list[str]):
try:
return _ORIGINAL_RESOLVE_COMMAND(self, ctx, args)
except click.exceptions.NoSuchCommand as e:
if not args or args[0].startswith("-"):
raise
matches = suggest_matches(args[0], self.list_commands(ctx))
suggestion = format_suggestions(matches)
if suggestion:
message = f"{e}\n{suggestion}"
raise click.exceptions.UsageError(message, ctx=ctx)
raise
except click.exceptions.UsageError as e:
if args and not args[0].startswith("-") and "No such command" in str(e):
matches = suggest_matches(args[0], self.list_commands(ctx))
suggestion = format_suggestions(matches)
if suggestion:
message = f"{e}\n{suggestion}"
raise click.exceptions.UsageError(message, ctx=ctx)
raise
# Install suggestion handling for all CLI command groups.
click.Group.resolve_command = _resolve_command_with_suggestions # type: ignore[assignment]
@click.group()
@click.version_option(version=__version__, prog_name="unity-mcp")
@click.option(
"--host", "-h",
default="127.0.0.1",
envvar="UNITY_MCP_HOST",
help="MCP server host address."
)
@click.option(
"--port", "-p",
default=8080,
type=int,
envvar="UNITY_MCP_HTTP_PORT",
help="MCP server port."
)
@click.option(
"--timeout", "-t",
default=30,
type=int,
envvar="UNITY_MCP_TIMEOUT",
help="Command timeout in seconds."
)
@click.option(
"--format", "-f",
type=click.Choice(["text", "json", "table"]),
default="text",
envvar="UNITY_MCP_FORMAT",
help="Output format."
)
@click.option(
"--instance", "-i",
default=None,
envvar="UNITY_MCP_INSTANCE",
help="Target Unity instance (hash or Name@hash)."
)
@click.option(
"--verbose", "-v",
is_flag=True,
help="Enable verbose output."
)
@pass_context
def cli(ctx: Context, host: str, port: int, timeout: int, format: str, instance: Optional[str], verbose: bool):
"""Unity MCP Command Line Interface.
Control Unity Editor directly from the command line using the Model Context Protocol.
\b
Examples:
unity-mcp status
unity-mcp gameobject find "Player"
unity-mcp scene hierarchy --format json
unity-mcp editor play
\b
Environment Variables:
UNITY_MCP_HOST Server host (default: 127.0.0.1)
UNITY_MCP_HTTP_PORT Server port (default: 8080)
UNITY_MCP_TIMEOUT Timeout in seconds (default: 30)
UNITY_MCP_FORMAT Output format (default: text)
UNITY_MCP_INSTANCE Target Unity instance
"""
config = CLIConfig(
host=host,
port=port,
timeout=timeout,
format=format,
unity_instance=instance,
)
# Security warning for non-localhost connections
warn_if_remote_host(config)
set_config(config)
ctx.config = config
ctx.verbose = verbose
@cli.command("status")
@pass_context
def status(ctx: Context):
"""Check connection status to Unity MCP server."""
config = ctx.config or get_config()
click.echo(f"Checking connection to {config.host}:{config.port}...")
if run_check_connection(config):
print_success(
f"Connected to Unity MCP server at {config.host}:{config.port}")
# Try to get Unity instances
try:
result = run_list_instances(config)
instances = result.get("instances", []) if isinstance(
result, dict) else []
if instances:
click.echo("\nConnected Unity instances:")
for inst in instances:
project = inst.get("project", "Unknown")
version = inst.get("unity_version", "Unknown")
hash_id = inst.get("hash", "")[:8]
click.echo(f" • {project} (Unity {version}) [{hash_id}]")
else:
print_info("No Unity instances currently connected")
except UnityConnectionError as e:
print_info(f"Could not retrieve Unity instances: {e}")
else:
print_error(
f"Cannot connect to Unity MCP server at {config.host}:{config.port}")
sys.exit(1)
@cli.command("instances")
@pass_context
def list_instances(ctx: Context):
"""List available Unity instances."""
config = ctx.config or get_config()
try:
instances = run_list_instances(config)
click.echo(format_output(instances, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@cli.command("raw")
@click.argument("command_type")
@click.argument("params", required=False, default="{}")
@pass_context
def raw_command(ctx: Context, command_type: str, params: str):
"""Send a raw command to Unity.
\b
Examples:
unity-mcp raw manage_scene '{"action": "get_hierarchy"}'
unity-mcp raw read_console '{"count": 10}'
"""
import json
config = ctx.config or get_config()
try:
params_dict = json.loads(params)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON params: {e}")
sys.exit(1)
try:
result = run_command(command_type, params_dict, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
# Import and register command groups
# These will be implemented in subsequent TODOs
def register_commands():
"""Register all command groups."""
def register_optional_command(module_name: str, command_name: str) -> None:
try:
module = import_module(module_name)
except ModuleNotFoundError as e:
if e.name == module_name:
return
print_error(
f"Failed to load command module '{module_name}': {e}"
)
return
except Exception as e:
print_error(
f"Failed to load command module '{module_name}': {e}"
)
return
command = getattr(module, command_name, None)
if command is None:
print_error(
f"Command '{command_name}' not found in '{module_name}'"
)
return
cli.add_command(command)
optional_commands = [
("cli.commands.tool", "tool"),
("cli.commands.tool", "custom_tool"),
("cli.commands.gameobject", "gameobject"),
("cli.commands.component", "component"),
("cli.commands.scene", "scene"),
("cli.commands.asset", "asset"),
("cli.commands.script", "script"),
("cli.commands.code", "code"),
("cli.commands.editor", "editor"),
("cli.commands.prefab", "prefab"),
("cli.commands.material", "material"),
("cli.commands.lighting", "lighting"),
("cli.commands.animation", "animation"),
("cli.commands.audio", "audio"),
("cli.commands.ui", "ui"),
("cli.commands.instance", "instance"),
("cli.commands.shader", "shader"),
("cli.commands.vfx", "vfx"),
("cli.commands.batch", "batch"),
("cli.commands.texture", "texture"),
]
for module_name, command_name in optional_commands:
register_optional_command(module_name, command_name)
# Register commands on import
register_commands()
def main():
"""Main entry point for the CLI."""
cli()
if __name__ == "__main__":
main()