Update for CLI (#636)
(1) Custom Tool fix (2) Include more tips and helps with CLI, including a CLI_EXAMPLE.md with @JohanHoltby's feedback!main
parent
17eb171e31
commit
6650e72cdf
|
|
@ -533,6 +533,18 @@ unity-mcp editor tests
|
||||||
unity-mcp editor tests --mode PlayMode
|
unity-mcp editor tests --mode PlayMode
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom Tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List custom tools / default tools for the active Unity project
|
||||||
|
unity-mcp tool list
|
||||||
|
unity-mcp custom_tool list
|
||||||
|
|
||||||
|
# Execute a custom tool by name
|
||||||
|
unity-mcp editor custom-tool "MyBuildTool"
|
||||||
|
unity-mcp editor custom-tool "Deploy" --params '{"target": "Android"}'
|
||||||
|
```
|
||||||
|
|
||||||
### Prefab Commands
|
### Prefab Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ from typing import Optional, Any
|
||||||
|
|
||||||
from cli.utils.config import get_config
|
from cli.utils.config import get_config
|
||||||
from cli.utils.output import format_output, print_error, print_success, print_info
|
from cli.utils.output import format_output, print_error, print_success, print_info
|
||||||
from cli.utils.connection import run_command, UnityConnectionError
|
from cli.utils.connection import run_command, run_list_custom_tools, UnityConnectionError
|
||||||
|
from cli.utils.suggestions import suggest_matches, format_suggestions
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
|
|
@ -472,6 +473,8 @@ def custom_tool(tool_name: str, params: str):
|
||||||
params_dict = json.loads(params)
|
params_dict = json.loads(params)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print_error(f"Invalid JSON for params: {e}")
|
print_error(f"Invalid JSON for params: {e}")
|
||||||
|
print_info("Example: --params '{\"key\":\"value\"}'")
|
||||||
|
print_info("Tip: wrap JSON in single quotes to avoid shell escaping issues.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -482,6 +485,26 @@ def custom_tool(tool_name: str, params: str):
|
||||||
click.echo(format_output(result, config.format))
|
click.echo(format_output(result, config.format))
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
print_success(f"Executed custom tool: {tool_name}")
|
print_success(f"Executed custom tool: {tool_name}")
|
||||||
|
else:
|
||||||
|
message = (result.get("message") or result.get("error") or "").lower()
|
||||||
|
if "not found" in message and "tool" in message:
|
||||||
|
try:
|
||||||
|
tools_result = run_list_custom_tools(config)
|
||||||
|
tools = tools_result.get("tools")
|
||||||
|
if tools is None:
|
||||||
|
data = tools_result.get("data", {})
|
||||||
|
tools = data.get("tools") if isinstance(data, dict) else None
|
||||||
|
names = [
|
||||||
|
t.get("name") for t in tools if isinstance(t, dict) and t.get("name")
|
||||||
|
] if isinstance(tools, list) else []
|
||||||
|
matches = suggest_matches(tool_name, names)
|
||||||
|
suggestion = format_suggestions(matches)
|
||||||
|
if suggestion:
|
||||||
|
print_info(suggestion)
|
||||||
|
print_info(
|
||||||
|
f'Example: unity-mcp editor custom-tool "{matches[0]}"')
|
||||||
|
except UnityConnectionError:
|
||||||
|
pass
|
||||||
except UnityConnectionError as e:
|
except UnityConnectionError as e:
|
||||||
print_error(str(e))
|
print_error(str(e))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
"""Tool CLI commands for listing custom tools."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import click
|
||||||
|
|
||||||
|
from cli.utils.config import get_config
|
||||||
|
from cli.utils.output import format_output, print_error
|
||||||
|
from cli.utils.connection import run_list_custom_tools, UnityConnectionError
|
||||||
|
|
||||||
|
|
||||||
|
def _list_custom_tools() -> None:
|
||||||
|
config = get_config()
|
||||||
|
try:
|
||||||
|
result = run_list_custom_tools(config)
|
||||||
|
if config.format != "text":
|
||||||
|
click.echo(format_output(result, config.format))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(result, dict) or not result.get("success", True):
|
||||||
|
click.echo(format_output(result, config.format))
|
||||||
|
return
|
||||||
|
|
||||||
|
tools = result.get("tools")
|
||||||
|
if tools is None:
|
||||||
|
data = result.get("data", {})
|
||||||
|
tools = data.get("tools") if isinstance(data, dict) else None
|
||||||
|
if not isinstance(tools, list):
|
||||||
|
click.echo(format_output(result, config.format))
|
||||||
|
return
|
||||||
|
|
||||||
|
click.echo(f"Custom tools ({len(tools)}):")
|
||||||
|
for i, tool in enumerate(tools):
|
||||||
|
name = tool.get("name") if isinstance(tool, dict) else str(tool)
|
||||||
|
click.echo(f" [{i}] {name}")
|
||||||
|
except UnityConnectionError as e:
|
||||||
|
print_error(str(e))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@click.group("tool")
|
||||||
|
def tool():
|
||||||
|
"""Tool management - list custom tools for the active Unity project."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@tool.command("list")
|
||||||
|
def list_tools():
|
||||||
|
"""List custom tools registered for the active Unity project."""
|
||||||
|
_list_custom_tools()
|
||||||
|
|
||||||
|
|
||||||
|
@click.group("custom_tool")
|
||||||
|
def custom_tool():
|
||||||
|
"""Alias for tool management (custom tools)."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@custom_tool.command("list")
|
||||||
|
def list_custom_tools():
|
||||||
|
"""List custom tools registered for the active Unity project."""
|
||||||
|
_list_custom_tools()
|
||||||
|
|
@ -8,6 +8,7 @@ from typing import Optional
|
||||||
|
|
||||||
from cli import __version__
|
from cli import __version__
|
||||||
from cli.utils.config import CLIConfig, set_config, get_config
|
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.output import format_output, print_error, print_success, print_info
|
||||||
from cli.utils.connection import (
|
from cli.utils.connection import (
|
||||||
run_command,
|
run_command,
|
||||||
|
|
@ -28,6 +29,35 @@ class Context:
|
||||||
pass_context = click.make_pass_decorator(Context, ensure=True)
|
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.group()
|
||||||
@click.version_option(version=__version__, prog_name="unity-mcp")
|
@click.version_option(version=__version__, prog_name="unity-mcp")
|
||||||
@click.option(
|
@click.option(
|
||||||
|
|
@ -212,6 +242,8 @@ def register_commands():
|
||||||
cli.add_command(command)
|
cli.add_command(command)
|
||||||
|
|
||||||
optional_commands = [
|
optional_commands = [
|
||||||
|
("cli.commands.tool", "tool"),
|
||||||
|
("cli.commands.tool", "custom_tool"),
|
||||||
("cli.commands.gameobject", "gameobject"),
|
("cli.commands.gameobject", "gameobject"),
|
||||||
("cli.commands.component", "component"),
|
("cli.commands.component", "component"),
|
||||||
("cli.commands.scene", "scene"),
|
("cli.commands.scene", "scene"),
|
||||||
|
|
|
||||||
|
|
@ -189,3 +189,40 @@ async def list_unity_instances(config: Optional[CLIConfig] = None) -> Dict[str,
|
||||||
def run_list_instances(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
|
def run_list_instances(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
|
||||||
"""Synchronous wrapper for list_unity_instances."""
|
"""Synchronous wrapper for list_unity_instances."""
|
||||||
return asyncio.run(list_unity_instances(config))
|
return asyncio.run(list_unity_instances(config))
|
||||||
|
|
||||||
|
|
||||||
|
async def list_custom_tools(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
|
||||||
|
"""List custom tools registered for the active Unity project."""
|
||||||
|
cfg = config or get_config()
|
||||||
|
url = f"http://{cfg.host}:{cfg.port}/api/custom-tools"
|
||||||
|
params: Dict[str, Any] = {}
|
||||||
|
if cfg.unity_instance:
|
||||||
|
params["instance"] = cfg.unity_instance
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(url, params=params, timeout=cfg.timeout)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except httpx.ConnectError as e:
|
||||||
|
raise UnityConnectionError(
|
||||||
|
f"Cannot connect to Unity MCP server at {cfg.host}:{cfg.port}. "
|
||||||
|
f"Make sure the server is running and Unity is connected.\n"
|
||||||
|
f"Error: {e}"
|
||||||
|
)
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
raise UnityConnectionError(
|
||||||
|
f"Connection to Unity timed out after {cfg.timeout}s. "
|
||||||
|
f"Unity may be busy or unresponsive."
|
||||||
|
)
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
raise UnityConnectionError(
|
||||||
|
f"HTTP error from server: {e.response.status_code} - {e.response.text}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise UnityConnectionError(f"Unexpected error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_list_custom_tools(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
|
||||||
|
"""Synchronous wrapper for list_custom_tools."""
|
||||||
|
return asyncio.run(list_custom_tools(config))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""Helpers for CLI suggestion messages."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import difflib
|
||||||
|
from typing import Iterable, List
|
||||||
|
|
||||||
|
|
||||||
|
def suggest_matches(
|
||||||
|
value: str,
|
||||||
|
choices: Iterable[str],
|
||||||
|
*,
|
||||||
|
limit: int = 3,
|
||||||
|
cutoff: float = 0.6,
|
||||||
|
) -> List[str]:
|
||||||
|
"""Return close matches for a value from a list of choices."""
|
||||||
|
try:
|
||||||
|
normalized = [c for c in choices if isinstance(c, str)]
|
||||||
|
except Exception:
|
||||||
|
normalized = []
|
||||||
|
if not value or not normalized:
|
||||||
|
return []
|
||||||
|
return difflib.get_close_matches(value, normalized, n=limit, cutoff=cutoff)
|
||||||
|
|
||||||
|
|
||||||
|
def format_suggestions(matches: Iterable[str]) -> str | None:
|
||||||
|
"""Format matches into a CLI-friendly suggestion string."""
|
||||||
|
items = [m for m in matches if m]
|
||||||
|
if not items:
|
||||||
|
return None
|
||||||
|
if len(items) == 1:
|
||||||
|
return f"Did you mean: {items[0]}"
|
||||||
|
joined = ", ".join(items)
|
||||||
|
return f"Did you mean one of: {joined}"
|
||||||
|
|
@ -9,7 +9,10 @@ from core.telemetry import record_milestone, record_telemetry, MilestoneType, Re
|
||||||
from services.resources import register_all_resources
|
from services.resources import register_all_resources
|
||||||
from transport.plugin_registry import PluginRegistry
|
from transport.plugin_registry import PluginRegistry
|
||||||
from transport.plugin_hub import PluginHub
|
from transport.plugin_hub import PluginHub
|
||||||
from services.custom_tool_service import CustomToolService
|
from services.custom_tool_service import (
|
||||||
|
CustomToolService,
|
||||||
|
resolve_project_id_for_unity_instance,
|
||||||
|
)
|
||||||
from core.config import config
|
from core.config import config
|
||||||
from starlette.routing import WebSocketRoute
|
from starlette.routing import WebSocketRoute
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
|
|
@ -325,6 +328,14 @@ def create_mcp_server(project_scoped_tools: bool) -> FastMCP:
|
||||||
"message": "MCP for Unity server is running"
|
"message": "MCP for Unity server is running"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def _normalize_instance_token(instance_token: str | None) -> tuple[str | None, str | None]:
|
||||||
|
if not instance_token:
|
||||||
|
return None, None
|
||||||
|
if "@" in instance_token:
|
||||||
|
name_part, _, hash_part = instance_token.partition("@")
|
||||||
|
return (name_part or None), (hash_part or None)
|
||||||
|
return None, instance_token
|
||||||
|
|
||||||
@mcp.custom_route("/api/command", methods=["POST"])
|
@mcp.custom_route("/api/command", methods=["POST"])
|
||||||
async def cli_command_route(request: Request) -> JSONResponse:
|
async def cli_command_route(request: Request) -> JSONResponse:
|
||||||
"""REST endpoint for CLI commands to Unity."""
|
"""REST endpoint for CLI commands to Unity."""
|
||||||
|
|
@ -348,11 +359,14 @@ def create_mcp_server(project_scoped_tools: bool) -> FastMCP:
|
||||||
|
|
||||||
# Find target session
|
# Find target session
|
||||||
session_id = None
|
session_id = None
|
||||||
|
session_details = None
|
||||||
|
instance_name, instance_hash = _normalize_instance_token(unity_instance)
|
||||||
if unity_instance:
|
if unity_instance:
|
||||||
# Try to match by hash or project name
|
# Try to match by hash or project name
|
||||||
for sid, details in sessions.sessions.items():
|
for sid, details in sessions.sessions.items():
|
||||||
if details.hash == unity_instance or details.project == unity_instance:
|
if details.hash == instance_hash or details.project in (instance_name, unity_instance):
|
||||||
session_id = sid
|
session_id = sid
|
||||||
|
session_details = details
|
||||||
break
|
break
|
||||||
|
|
||||||
# If a specific unity_instance was requested but not found, return an error
|
# If a specific unity_instance was requested but not found, return an error
|
||||||
|
|
@ -367,6 +381,46 @@ def create_mcp_server(project_scoped_tools: bool) -> FastMCP:
|
||||||
else:
|
else:
|
||||||
# No specific unity_instance requested: use first available session
|
# No specific unity_instance requested: use first available session
|
||||||
session_id = next(iter(sessions.sessions.keys()))
|
session_id = next(iter(sessions.sessions.keys()))
|
||||||
|
session_details = sessions.sessions.get(session_id)
|
||||||
|
|
||||||
|
if command_type == "execute_custom_tool":
|
||||||
|
tool_name = None
|
||||||
|
tool_params = {}
|
||||||
|
if isinstance(params, dict):
|
||||||
|
tool_name = params.get("tool_name") or params.get("name")
|
||||||
|
tool_params = params.get("parameters") or params.get("params") or {}
|
||||||
|
|
||||||
|
if not tool_name:
|
||||||
|
return JSONResponse(
|
||||||
|
{"success": False, "error": "Missing 'tool_name' for execute_custom_tool"},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
if tool_params is None:
|
||||||
|
tool_params = {}
|
||||||
|
if not isinstance(tool_params, dict):
|
||||||
|
return JSONResponse(
|
||||||
|
{"success": False, "error": "Tool parameters must be an object/dict"},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prefer a concrete hash for project-scoped tools.
|
||||||
|
unity_instance_hint = unity_instance
|
||||||
|
if session_details and session_details.hash:
|
||||||
|
unity_instance_hint = session_details.hash
|
||||||
|
|
||||||
|
project_id = resolve_project_id_for_unity_instance(
|
||||||
|
unity_instance_hint)
|
||||||
|
if not project_id:
|
||||||
|
return JSONResponse(
|
||||||
|
{"success": False, "error": "Could not resolve project id for custom tool"},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
service = CustomToolService.get_instance()
|
||||||
|
result = await service.execute_tool(
|
||||||
|
project_id, tool_name, unity_instance_hint, tool_params
|
||||||
|
)
|
||||||
|
return JSONResponse(result.model_dump())
|
||||||
|
|
||||||
# Send command to Unity
|
# Send command to Unity
|
||||||
result = await PluginHub.send_command(session_id, command_type, params)
|
result = await PluginHub.send_command(session_id, command_type, params)
|
||||||
|
|
@ -376,6 +430,67 @@ def create_mcp_server(project_scoped_tools: bool) -> FastMCP:
|
||||||
logger.error(f"CLI command error: {e}")
|
logger.error(f"CLI command error: {e}")
|
||||||
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
|
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
@mcp.custom_route("/api/custom-tools", methods=["GET"])
|
||||||
|
async def cli_custom_tools_route(request: Request) -> JSONResponse:
|
||||||
|
"""REST endpoint to list custom tools for the active Unity project."""
|
||||||
|
try:
|
||||||
|
unity_instance = request.query_params.get("instance")
|
||||||
|
instance_name, instance_hash = _normalize_instance_token(unity_instance)
|
||||||
|
|
||||||
|
sessions = await PluginHub.get_sessions()
|
||||||
|
if not sessions.sessions:
|
||||||
|
return JSONResponse({
|
||||||
|
"success": False,
|
||||||
|
"error": "No Unity instances connected. Make sure Unity is running with MCP plugin."
|
||||||
|
}, status_code=503)
|
||||||
|
|
||||||
|
session_details = None
|
||||||
|
if unity_instance:
|
||||||
|
# Try to match by hash or project name
|
||||||
|
for _, details in sessions.sessions.items():
|
||||||
|
if details.hash == instance_hash or details.project in (instance_name, unity_instance):
|
||||||
|
session_details = details
|
||||||
|
break
|
||||||
|
if not session_details:
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"error": f"Unity instance '{unity_instance}' not found",
|
||||||
|
},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# No specific unity_instance requested: use first available session
|
||||||
|
session_details = next(iter(sessions.sessions.values()))
|
||||||
|
|
||||||
|
unity_instance_hint = unity_instance
|
||||||
|
if session_details and session_details.hash:
|
||||||
|
unity_instance_hint = session_details.hash
|
||||||
|
|
||||||
|
project_id = resolve_project_id_for_unity_instance(
|
||||||
|
unity_instance_hint)
|
||||||
|
if not project_id:
|
||||||
|
return JSONResponse(
|
||||||
|
{"success": False, "error": "Could not resolve project id for custom tools"},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
service = CustomToolService.get_instance()
|
||||||
|
tools = await service.list_registered_tools(project_id)
|
||||||
|
tools_payload = [
|
||||||
|
tool.model_dump() if hasattr(tool, "model_dump") else tool for tool in tools
|
||||||
|
]
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"project_id": project_id,
|
||||||
|
"tool_count": len(tools_payload),
|
||||||
|
"tools": tools_payload,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"CLI custom tools error: {e}")
|
||||||
|
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
|
||||||
|
|
||||||
@mcp.custom_route("/api/instances", methods=["GET"])
|
@mcp.custom_route("/api/instances", methods=["GET"])
|
||||||
async def cli_instances_route(_: Request) -> JSONResponse:
|
async def cli_instances_route(_: Request) -> JSONResponse:
|
||||||
"""REST endpoint to list connected Unity instances."""
|
"""REST endpoint to list connected Unity instances."""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
## Unity MCP (CLI Mode)
|
||||||
|
|
||||||
|
We use Unity MCP via **CLI commands** instead of MCP server connection. This avoids the reconnection issues that occur when Unity restarts.
|
||||||
|
|
||||||
|
### Why CLI Instead of MCP Connection?
|
||||||
|
|
||||||
|
- MCP connection breaks when Unity restarts
|
||||||
|
- `/mcp reconnect` requires human intervention
|
||||||
|
- CLI works directly via HTTP to the MCP server - no persistent connection needed
|
||||||
|
- Claude can call CLI commands autonomously without reconnection issues
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Server # In unity-mcp repo
|
||||||
|
pip install -e .
|
||||||
|
# Or with uv:
|
||||||
|
uv pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Options
|
||||||
|
|
||||||
|
| Option | Description | Default | Env Variable |
|
||||||
|
|--------|-------------|---------|--------------|
|
||||||
|
| `-h, --host` | Server host | 127.0.0.1 | `UNITY_MCP_HOST` |
|
||||||
|
| `-p, --port` | Server port | 8080 | `UNITY_MCP_HTTP_PORT` |
|
||||||
|
| `-t, --timeout` | Timeout seconds | 30 | `UNITY_MCP_TIMEOUT` |
|
||||||
|
| `-f, --format` | Output: text, json, table | text | `UNITY_MCP_FORMAT` |
|
||||||
|
| `-i, --instance` | Target Unity instance | - | `UNITY_MCP_INSTANCE` |
|
||||||
|
|
||||||
|
### Core CLI Commands
|
||||||
|
|
||||||
|
**Status & Connection**
|
||||||
|
```bash
|
||||||
|
unity-mcp status # Check server + Unity connection
|
||||||
|
```
|
||||||
|
|
||||||
|
**Instance Management**
|
||||||
|
```bash
|
||||||
|
unity-mcp instance list # List connected Unity instances
|
||||||
|
unity-mcp instance set "ProjectName@abc" # Set active instance
|
||||||
|
unity-mcp instance current # Show current instance
|
||||||
|
```
|
||||||
|
|
||||||
|
**Editor Control**
|
||||||
|
```bash
|
||||||
|
unity-mcp editor play|pause|stop # Control play mode
|
||||||
|
unity-mcp editor console [--clear] # Get/clear console logs
|
||||||
|
unity-mcp editor refresh [--compile] # Refresh assets
|
||||||
|
unity-mcp editor menu "Edit/Project Settings..." # Execute menu item
|
||||||
|
unity-mcp editor add-tag "TagName" # Add tag
|
||||||
|
unity-mcp editor add-layer "LayerName" # Add layer
|
||||||
|
unity-mcp editor tests --mode PlayMode [--async]
|
||||||
|
unity-mcp editor poll-test <job_id> [--wait 60] [--details]
|
||||||
|
unity-mcp --instance "MyProject@abc123" editor play # Target a specific instance
|
||||||
|
```
|
||||||
|
|
||||||
|
**Custom Tools**
|
||||||
|
```bash
|
||||||
|
unity-mcp tool list
|
||||||
|
unity-mcp custom_tool list
|
||||||
|
unity-mcp editor custom-tool "bake_lightmaps"
|
||||||
|
unity-mcp editor custom-tool "capture_screenshot" --params '{"filename":"shot_01","width":1920,"height":1080}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scene Operations**
|
||||||
|
```bash
|
||||||
|
unity-mcp scene hierarchy [--limit 20] [--depth 3]
|
||||||
|
unity-mcp scene active
|
||||||
|
unity-mcp scene load "Assets/Scenes/Main.unity"
|
||||||
|
unity-mcp scene save
|
||||||
|
unity-mcp scene screenshot --name "capture"
|
||||||
|
unity-mcp --format json scene hierarchy
|
||||||
|
```
|
||||||
|
|
||||||
|
**GameObject Operations**
|
||||||
|
```bash
|
||||||
|
unity-mcp gameobject find "Name" [--method by_tag|by_name|by_layer|by_component]
|
||||||
|
unity-mcp gameobject create "Name" [--primitive Cube] [--position X Y Z]
|
||||||
|
unity-mcp gameobject modify "Name" [--position X Y Z] [--rotation X Y Z]
|
||||||
|
unity-mcp gameobject delete "Name" [--force]
|
||||||
|
unity-mcp gameobject duplicate "Name"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Component Operations**
|
||||||
|
```bash
|
||||||
|
unity-mcp component add "GameObject" ComponentType
|
||||||
|
unity-mcp component remove "GameObject" ComponentType
|
||||||
|
unity-mcp component set "GameObject" Component property value
|
||||||
|
```
|
||||||
|
|
||||||
|
**Script Operations**
|
||||||
|
```bash
|
||||||
|
unity-mcp script create "ScriptName" --path "Assets/Scripts"
|
||||||
|
unity-mcp script read "Assets/Scripts/File.cs"
|
||||||
|
unity-mcp script delete "Assets/Scripts/File.cs" [--force]
|
||||||
|
unity-mcp code search "pattern" "path/to/file.cs" [--max-results 20]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Asset Operations**
|
||||||
|
```bash
|
||||||
|
unity-mcp asset search --pattern "*.mat" --path "Assets/Materials"
|
||||||
|
unity-mcp asset info "Assets/Materials/File.mat"
|
||||||
|
unity-mcp asset mkdir "Assets/NewFolder"
|
||||||
|
unity-mcp asset move "Old/Path" "New/Path"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prefab Operations**
|
||||||
|
```bash
|
||||||
|
unity-mcp prefab open "Assets/Prefabs/File.prefab"
|
||||||
|
unity-mcp prefab save
|
||||||
|
unity-mcp prefab close
|
||||||
|
unity-mcp prefab create "GameObject" --path "Assets/Prefabs"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Material Operations**
|
||||||
|
```bash
|
||||||
|
unity-mcp material create "Assets/Materials/File.mat"
|
||||||
|
unity-mcp material set-color "File.mat" R G B
|
||||||
|
unity-mcp material assign "File.mat" "GameObject"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Shader Operations**
|
||||||
|
```bash
|
||||||
|
unity-mcp shader create "Name" --path "Assets/Shaders"
|
||||||
|
unity-mcp shader read "Assets/Shaders/Custom.shader"
|
||||||
|
unity-mcp shader update "Assets/Shaders/Custom.shader" --file local.shader
|
||||||
|
unity-mcp shader delete "Assets/Shaders/File.shader" [--force]
|
||||||
|
```
|
||||||
|
|
||||||
|
**VFX Operations**
|
||||||
|
```bash
|
||||||
|
unity-mcp vfx particle info|play|stop|pause|restart|clear "Name"
|
||||||
|
unity-mcp vfx line info "Name"
|
||||||
|
unity-mcp vfx line create-line "Name" --start X Y Z --end X Y Z
|
||||||
|
unity-mcp vfx line create-circle "Name" --radius N
|
||||||
|
unity-mcp vfx trail info|set-time|clear "Name" [time]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lighting & UI**
|
||||||
|
```bash
|
||||||
|
unity-mcp lighting create "Name" --type Point|Spot [--intensity N] [--position X Y Z]
|
||||||
|
unity-mcp ui create-canvas "Name"
|
||||||
|
unity-mcp ui create-text "Name" --parent "Canvas" --text "Content"
|
||||||
|
unity-mcp ui create-button "Name" --parent "Canvas" --text "Label"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Batch Operations**
|
||||||
|
```bash
|
||||||
|
unity-mcp batch run commands.json [--parallel] [--fail-fast]
|
||||||
|
unity-mcp batch inline '[{"tool": "manage_scene", "params": {...}}]'
|
||||||
|
unity-mcp batch template > commands.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Raw Access (Any Tool)**
|
||||||
|
```bash
|
||||||
|
unity-mcp raw tool_name 'JSON_params'
|
||||||
|
unity-mcp raw manage_scene '{"action":"get_active"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Note on MCP Server
|
||||||
|
|
||||||
|
The MCP HTTP server still needs to be running for CLI to work. Here is an example to run the server manually on Mac:
|
||||||
|
```bash
|
||||||
|
/opt/homebrew/bin/uvx --no-cache --refresh --from /XXX/unity-mcp/Server mcp-for-unity --transport http --http-url http://localhost:8080
|
||||||
|
```
|
||||||
|
|
@ -168,6 +168,10 @@ unity-mcp editor menu "Edit/Project Settings..."
|
||||||
# Custom tools
|
# Custom tools
|
||||||
unity-mcp editor custom-tool "MyBuildTool"
|
unity-mcp editor custom-tool "MyBuildTool"
|
||||||
unity-mcp editor custom-tool "Deploy" --params '{"target": "Android"}'
|
unity-mcp editor custom-tool "Deploy" --params '{"target": "Android"}'
|
||||||
|
|
||||||
|
# List custom tools for the active Unity project
|
||||||
|
unity-mcp tool list
|
||||||
|
unity-mcp custom_tool list
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
@ -344,6 +348,8 @@ unity-mcp raw read_console '{"count": 20}'
|
||||||
| `animation` | `play`, `set-parameter` |
|
| `animation` | `play`, `set-parameter` |
|
||||||
| `audio` | `play`, `stop`, `volume` |
|
| `audio` | `play`, `stop`, `volume` |
|
||||||
| `lighting` | `create` |
|
| `lighting` | `create` |
|
||||||
|
| `tool` | `list` |
|
||||||
|
| `custom_tool` | `list` |
|
||||||
| `ui` | `create-canvas`, `create-text`, `create-button`, `create-image` |
|
| `ui` | `create-canvas`, `create-text`, `create-button`, `create-image` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,22 @@ Once you've created your tool, you'll need to let your AI assistant know about i
|
||||||
|
|
||||||
**If that doesn't work:** Some clients (like Windsurf) may need you to remove and reconfigure the MCP for Unity server entirely. It's a bit more work, but it guarantees your new tools will appear.
|
**If that doesn't work:** Some clients (like Windsurf) may need you to remove and reconfigure the MCP for Unity server entirely. It's a bit more work, but it guarantees your new tools will appear.
|
||||||
|
|
||||||
|
## Step 3: List and Call Your Tool from the CLI
|
||||||
|
|
||||||
|
If you want to use the CLI directly, list custom tools for the active Unity project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
unity-mcp tool list
|
||||||
|
unity-mcp custom_tool list
|
||||||
|
```
|
||||||
|
|
||||||
|
Then call your tool by name:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
unity-mcp editor custom-tool "my_custom_tool"
|
||||||
|
unity-mcp editor custom-tool "my_custom_tool" --params '{"param1":"value"}'
|
||||||
|
```
|
||||||
|
|
||||||
## Complete Example: Screenshot Tool
|
## Complete Example: Screenshot Tool
|
||||||
|
|
||||||
### C# Handler (`Assets/Editor/ScreenShots/CaptureScreenshotTool.cs`)
|
### C# Handler (`Assets/Editor/ScreenShots/CaptureScreenshotTool.cs`)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue