* feat: Add CLI for Unity MCP server

- Add click-based CLI with 15+ command groups
- Commands: gameobject, component, scene, asset, script, editor, prefab, material, lighting, ui, audio, animation, code
- HTTP transport to communicate with Unity via MCP server
- Output formats: text, json, table
- Configuration via environment variables or CLI options
- Comprehensive usage guide and unit tests

* Update based on AI feedback

* Fixes main.py error

* Update for further error fix

* Update based on AI

* Update script.py

* Update with better coverage and Tool Readme

* Log a message with implicit URI changes

Small update for #542

* Minor fixes (#602)

* Log a message with implicit URI changes

Small update for #542

* Log a message with implicit URI changes

Small update for #542

* Add helper scripts to update forks

* fix: improve HTTP Local URL validation UX and styling specificity

- Rename CSS class from generic "error" to "http-local-url-error" for better specificity
- Rename "invalid-url" class to "http-local-invalid-url" for clarity
- Disable httpServerCommandField when URL is invalid or transport not HTTP Local
- Clear field value and tooltip when showing validation errors
- Ensure field is re-enabled when URL becomes valid

* Docker mcp gateway (#603)

* Log a message with implicit URI changes

Small update for #542

* Update docker container to default to stdio

Replaces #541

* fix: Rider config path and add MCP registry manifest (#604)

- Fix RiderConfigurator to use correct GitHub Copilot config path:
  - Windows: %LOCALAPPDATA%\github-copilot\intellij\mcp.json
  - macOS: ~/Library/Application Support/github-copilot/intellij/mcp.json
  - Linux: ~/.config/github-copilot/intellij/mcp.json
- Add mcp.json for GitHub MCP Registry support:
  - Enables users to install via coplaydev/unity-mcp
  - Uses uvx with mcpforunityserver from PyPI

* Use click.echo instead of print statements

* Standardize whitespace

* Minor tweak in docs

* Use `wait` params

* Unrelated but project scoped tools should be off by default

* Update lock file

* Whitespace cleanup

* Update custom_tool_service.py to skip global registration for any tool name that already exists as a built‑in.

* Avoid silently falling back to the first Unity session when a specific unity_instance was requested but not found.

If a client passes a unity_instance that doesn’t match any session, this code will still route the command to the first available session, which can send commands to the wrong project in multi‑instance environments. Instead, when a unity_instance is provided but no matching session_id is found, return an error (e.g. 400/404 with "Unity instance '' not found") and only default to the first session when no unity_instance was specified.

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

* Update docs/CLI_USAGE.md

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

* Updated the CLI command registration to only swallow missing optional modules and to surface real import-time failures, so broken command modules don’t get silently ignored.

* Sorted __all__ alphabetically to satisfy RUF022 in __init__.py.

* Validate --params is a JSON object before merging.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Co-authored-by: dsarno <david@lighthaus.us>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
main
Marcus Sanatan 2026-01-21 20:53:13 -04:00 committed by GitHub
parent f54b1cb552
commit 7f44e4b53e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 7410 additions and 43 deletions

View File

@ -132,7 +132,7 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
{
projectScopedToolsToggle.value = EditorPrefs.GetBool(
EditorPrefKeys.ProjectScopedToolsLocalHttp,
true
false
);
}

View File

@ -36,6 +36,7 @@ dependencies = [
"tomli>=2.3.0",
"fastapi>=0.104.0",
"uvicorn>=0.35.0",
"click>=8.1.0",
]
[project.optional-dependencies]
@ -51,6 +52,7 @@ Issues = "https://github.com/CoplayDev/unity-mcp/issues"
[project.scripts]
mcp-for-unity = "main:main"
unity-mcp = "cli.main:main"
[build-system]
requires = ["setuptools>=64.0.0", "wheel"]

View File

@ -0,0 +1,727 @@
# Unity MCP CLI Usage Guide
> **For AI Assistants and Developers**: This document explains the correct syntax and common pitfalls when using the Unity MCP CLI.
## Table of Contents
1. [Installation](#installation)
2. [Quick Start](#quick-start)
3. [Command Structure](#command-structure)
4. [Global Options](#global-options)
5. [Argument vs Option Syntax](#argument-vs-option-syntax)
6. [Common Mistakes and Corrections](#common-mistakes-and-corrections)
7. [Output Formats](#output-formats)
8. [Command Reference by Category](#command-reference-by-category)
---
## Installation
### Prerequisites
- **Python 3.10+** installed
- **Unity Editor** running with the MCP plugin enabled
- **MCP Server** running (HTTP transport on port 8080)
### Install via pip (from source)
```bash
# Navigate to the Server directory
cd /path/to/unity-mcp/Server
# Install in development mode
pip install -e .
# Or install with uv (recommended)
uv pip install -e .
```
### Install via uv tool
```bash
# Run directly without installing
uvx --from /path/to/unity-mcp/Server unity-mcp --help
# Or install as a tool
uv tool install /path/to/unity-mcp/Server
```
### Verify Installation
```bash
# Check version
unity-mcp --version
# Check help
unity-mcp --help
# Test connection to Unity
unity-mcp status
```
---
## Quick Start
### 1. Start the MCP Server
Make sure the Unity MCP server is running with HTTP transport:
```bash
# The server is typically started via the Unity-MCP window, select HTTP local, and start server, or try this manually:
cd /path/to/unity-mcp/Server
uv run mcp-for-unity --transport http --http-url http://localhost:8080
```
### 2. Verify Connection
```bash
unity-mcp status
```
Expected output:
```
Checking connection to 127.0.0.1:8080...
✅ Connected to Unity MCP server at 127.0.0.1:8080
Connected Unity instances:
• MyProject (Unity 6000.2.10f1) [09abcc51]
```
### 3. Run Your First Commands
```bash
# Get scene hierarchy
unity-mcp scene hierarchy
# Create a cube
unity-mcp gameobject create "MyCube" --primitive Cube
# Move the cube
unity-mcp gameobject modify "MyCube" --position 0 2 0
# Take a screenshot
unity-mcp scene screenshot
# Enter play mode
unity-mcp editor play
```
### 4. Get Help on Any Command
```bash
# List all commands
unity-mcp --help
# Help for a command group
unity-mcp gameobject --help
# Help for a specific command
unity-mcp gameobject create --help
```
---
## Command Structure
The CLI follows this general pattern:
```
unity-mcp [GLOBAL_OPTIONS] COMMAND_GROUP [SUBCOMMAND] [ARGUMENTS] [OPTIONS]
```
**Example breakdown:**
```bash
unity-mcp -f json gameobject create "MyCube" --primitive Cube --position 0 1 0
# ^^^^^^^ ^^^^^^^^^^^ ^^^^^^ ^^^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
# global cmd group subcmd argument option multi-value option
```
---
## Global Options
Global options come **BEFORE** the command group:
| Option | Short | Description | Default |
|--------|-------|-------------|---------|
| `--host` | `-h` | MCP server host | `127.0.0.1` |
| `--port` | `-p` | MCP server port | `8080` |
| `--format` | `-f` | Output format: `text`, `json`, `table` | `text` |
| `--timeout` | `-t` | Command timeout in seconds | `30` |
| `--instance` | `-i` | Target Unity instance (hash or Name@hash) | auto |
| `--verbose` | `-v` | Enable verbose output | `false` |
**✅ Correct:**
```bash
unity-mcp -f json scene hierarchy
unity-mcp --format json --timeout 60 gameobject find "Player"
```
**❌ Wrong:**
```bash
unity-mcp scene hierarchy -f json # Global option after command
```
---
## Argument vs Option Syntax
### Arguments (Positional)
Arguments are **required values** that come in a specific order, **without** flags.
```bash
unity-mcp gameobject find "Player"
# ^^^^^^^^ This is an ARGUMENT (positional)
```
### Options (Named)
Options use `--name` or `-n` flags and can appear in any order after arguments.
```bash
unity-mcp gameobject create "MyCube" --primitive Cube
# ^^^^^^^^^^^ ^^^^ This is an OPTION with value
```
### Multi-Value Options
Some options accept multiple values. **Do NOT use commas** - use spaces:
**✅ Correct:**
```bash
unity-mcp gameobject modify "Cube" --position 1 2 3
unity-mcp gameobject modify "Cube" --rotation 0 45 0
unity-mcp gameobject modify "Cube" --scale 2 2 2
```
**❌ Wrong:**
```bash
unity-mcp gameobject modify "Cube" --position "1,2,3" # Wrong: comma-separated string
unity-mcp gameobject modify "Cube" --position 1,2,3 # Wrong: comma-separated
unity-mcp gameobject modify "Cube" -pos "1 2 3" # Wrong: quoted as single string
```
---
## Common Mistakes and Corrections
### 1. Multi-Value Options (Position, Rotation, Scale, Color)
These options expect **separate float arguments**, not comma-separated strings:
| Option | ❌ Wrong | ✅ Correct |
|--------|----------|-----------|
| `--position` | `--position "2,1,0"` | `--position 2 1 0` |
| `--rotation` | `--rotation "0,45,0"` | `--rotation 0 45 0` |
| `--scale` | `--scale "1,1,1"` | `--scale 1 1 1` |
| Color args | `1,0,0,1` | `1 0 0 1` |
**Example - Moving a GameObject:**
```bash
# Wrong - will error "requires 3 arguments"
unity-mcp gameobject modify "Cube" --position "2,1,0"
# Correct
unity-mcp gameobject modify "Cube" --position 2 1 0
```
**Example - Setting material color:**
```bash
# Wrong
unity-mcp material set-color "Assets/Mat.mat" 1,0,0,1
# Correct (R G B or R G B A as separate args)
unity-mcp material set-color "Assets/Mat.mat" 1 0 0
unity-mcp material set-color "Assets/Mat.mat" 1 0 0 1
```
### 2. Argument Order Matters
Some commands have multiple positional arguments. Check `--help` to see the order:
**Material assign:**
```bash
# Wrong - arguments in wrong order
unity-mcp material assign "TestCube" "Assets/Materials/Red.mat"
# ✅ Correct - MATERIAL_PATH comes before TARGET
unity-mcp material assign "Assets/Materials/Red.mat" "TestCube"
```
**Prefab create:**
```bash
# Wrong - using --path option that doesn't exist
unity-mcp prefab create "Cube" --path "Assets/Prefabs/Cube.prefab"
# Correct - PATH is a positional argument
unity-mcp prefab create "Cube" "Assets/Prefabs/Cube.prefab"
```
### 3. Using Options That Don't Exist
Always check `--help` before assuming an option exists:
```bash
# Check available options for any command
unity-mcp gameobject modify --help
unity-mcp material assign --help
unity-mcp prefab create --help
```
### 4. Property Names for Materials
Different shaders use different property names. Use `material info` to discover them:
```bash
# First, check what properties exist
unity-mcp material info "Assets/Materials/MyMat.mat"
# Then use the correct property name
# For URP shaders, often "_BaseColor" instead of "_Color"
unity-mcp material set-color "Assets/Mat.mat" 1 0 0 --property "_BaseColor"
```
### 5. Search Methods
When targeting GameObjects, specify how to search:
```bash
# By name (default)
unity-mcp gameobject modify "Player" --position 0 0 0
# By instance ID (use --search-method)
unity-mcp gameobject modify "-81840" --search-method by_id --position 0 0 0
# By path
unity-mcp gameobject modify "/Canvas/Panel/Button" --search-method by_path --active
# By tag
unity-mcp gameobject find "Player" --search-method by_tag
```
---
## Output Formats
### Text (Default)
Human-readable nested format:
```bash
unity-mcp scene active
# Output:
# status: success
# result:
# name: New Scene
# path: Assets/Scenes/New Scene.unity
# ...
```
### JSON
Machine-readable JSON:
```bash
unity-mcp -f json scene active
# Output: {"status": "success", "result": {...}}
```
### Table
Key-value table format:
```bash
unity-mcp -f table scene active
# Output:
# Key | Value
# -------+------
# status | success
# ...
```
---
## Command Reference by Category
### Status & Connection
```bash
# Check server connection and Unity instances
unity-mcp status
# List connected Unity instances
unity-mcp instances
```
### Scene Commands
```bash
# Get scene hierarchy
unity-mcp scene hierarchy
# Get active scene info
unity-mcp scene active
# Get build settings
unity-mcp scene build-settings
# Create new scene
unity-mcp scene create "MyScene"
# Load scene
unity-mcp scene load "Assets/Scenes/MyScene.unity"
# Save current scene
unity-mcp scene save
# Take screenshot
unity-mcp scene screenshot
unity-mcp scene screenshot --filename "my_screenshot" --supersize 2
```
### GameObject Commands
```bash
# Find GameObjects
unity-mcp gameobject find "Player"
unity-mcp gameobject find "Enemy" --method by_tag
unity-mcp gameobject find "-81840" --method by_id
unity-mcp gameobject find "Rigidbody" --method by_component
# Create GameObject
unity-mcp gameobject create "Empty" # Empty object
unity-mcp gameobject create "MyCube" --primitive Cube # Primitive
unity-mcp gameobject create "MyObj" --position 0 5 0 # With position
unity-mcp gameobject create "Player" --components "Rigidbody,BoxCollider" # With components
# Modify GameObject
unity-mcp gameobject modify "Cube" --position 1 2 3
unity-mcp gameobject modify "Cube" --rotation 0 45 0
unity-mcp gameobject modify "Cube" --scale 2 2 2
unity-mcp gameobject modify "Cube" --name "NewName"
unity-mcp gameobject modify "Cube" --active # Enable
unity-mcp gameobject modify "Cube" --inactive # Disable
unity-mcp gameobject modify "Cube" --tag "Player"
unity-mcp gameobject modify "Cube" --parent "Parent"
# Delete GameObject
unity-mcp gameobject delete "Cube"
unity-mcp gameobject delete "Cube" --force # Skip confirmation
# Duplicate GameObject
unity-mcp gameobject duplicate "Cube"
# Move relative to another object
unity-mcp gameobject move "Cube" --reference "Player" --direction up --distance 2
```
### Component Commands
```bash
# Add component
unity-mcp component add "Cube" Rigidbody
unity-mcp component add "Cube" BoxCollider
# Remove component
unity-mcp component remove "Cube" Rigidbody
unity-mcp component remove "Cube" Rigidbody --force # Skip confirmation
# Set single property
unity-mcp component set "Cube" Rigidbody mass 5
unity-mcp component set "Cube" Rigidbody useGravity false
unity-mcp component set "Cube" Light intensity 2.5
# Set multiple properties at once
unity-mcp component modify "Cube" Rigidbody --properties '{"mass": 5, "drag": 0.5}'
```
### Asset Commands
```bash
# Search assets
unity-mcp asset search "Player"
unity-mcp asset search "t:Material" # By type
unity-mcp asset search "t:Prefab Player" # Combined
# Get asset info
unity-mcp asset info "Assets/Materials/Red.mat"
# Create asset
unity-mcp asset create "Assets/Materials/New.mat" Material
# Delete asset
unity-mcp asset delete "Assets/Materials/Old.mat"
unity-mcp asset delete "Assets/Materials/Old.mat" --force # Skip confirmation
# Move/Rename asset
unity-mcp asset move "Assets/Old/Mat.mat" "Assets/New/Mat.mat"
unity-mcp asset rename "Assets/Materials/Old.mat" "New"
# Create folder
unity-mcp asset mkdir "Assets/NewFolder"
# Import/reimport
unity-mcp asset import "Assets/Textures/image.png"
```
### Script Commands
```bash
# Create script
unity-mcp script create "MyScript" --path "Assets/Scripts"
unity-mcp script create "MyScript" --path "Assets/Scripts" --type MonoBehaviour
# Read script
unity-mcp script read "Assets/Scripts/MyScript.cs"
# Delete script
unity-mcp script delete "Assets/Scripts/MyScript.cs"
# Validate script
unity-mcp script validate "Assets/Scripts/MyScript.cs"
```
### Material Commands
```bash
# Create material
unity-mcp material create "Assets/Materials/New.mat"
unity-mcp material create "Assets/Materials/New.mat" --shader "Standard"
# Get material info
unity-mcp material info "Assets/Materials/Mat.mat"
# Set color (R G B or R G B A)
unity-mcp material set-color "Assets/Materials/Mat.mat" 1 0 0
unity-mcp material set-color "Assets/Materials/Mat.mat" 1 0 0 --property "_BaseColor"
# Set shader property
unity-mcp material set-property "Assets/Materials/Mat.mat" "_Metallic" 0.5
# Assign to GameObject
unity-mcp material assign "Assets/Materials/Mat.mat" "Cube"
unity-mcp material assign "Assets/Materials/Mat.mat" "Cube" --slot 1
# Set renderer color directly
unity-mcp material set-renderer-color "Cube" 1 0 0 1
```
### Editor Commands
```bash
# Play mode control
unity-mcp editor play
unity-mcp editor pause
unity-mcp editor stop
# Console
unity-mcp editor console # Read console
unity-mcp editor console --count 20 # Last 20 entries
unity-mcp editor console --clear # Clear console
unity-mcp editor console --types error,warning # Filter by type
# Menu items
unity-mcp editor menu "Edit/Preferences"
unity-mcp editor menu "GameObject/Create Empty"
# Tags and Layers
unity-mcp editor add-tag "Enemy"
unity-mcp editor remove-tag "Enemy"
unity-mcp editor add-layer "Interactable"
unity-mcp editor remove-layer "Interactable"
# Editor tool
unity-mcp editor tool View
unity-mcp editor tool Move
unity-mcp editor tool Rotate
# Run tests
unity-mcp editor tests
unity-mcp editor tests --mode PlayMode
```
### Prefab Commands
```bash
# Create prefab from scene object
unity-mcp prefab create "Cube" "Assets/Prefabs/Cube.prefab"
unity-mcp prefab create "Cube" "Assets/Prefabs/Cube.prefab" --overwrite
# Open prefab for editing
unity-mcp prefab open "Assets/Prefabs/Player.prefab"
# Save open prefab
unity-mcp prefab save
# Close prefab stage
unity-mcp prefab close
```
### UI Commands
```bash
# Create a Canvas (adds Canvas, CanvasScaler, GraphicRaycaster)
unity-mcp ui create-canvas "MainCanvas"
unity-mcp ui create-canvas "WorldUI" --render-mode WorldSpace
# Create UI elements (must have a parent Canvas)
unity-mcp ui create-text "TitleText" --parent "MainCanvas" --text "Hello World"
unity-mcp ui create-button "StartButton" --parent "MainCanvas" --text "Click Me"
unity-mcp ui create-image "Background" --parent "MainCanvas"
```
### Lighting Commands
```bash
# Create lights with type, color, intensity
unity-mcp lighting create "Sun" --type Directional
unity-mcp lighting create "Lamp" --type Point --intensity 2 --position 0 5 0
unity-mcp lighting create "Spot" --type Spot --color 1 0 0 --intensity 3
unity-mcp lighting create "GreenLight" --type Point --color 0 1 0
```
### Audio Commands
```bash
# Control AudioSource (target must have AudioSource component)
unity-mcp audio play "MusicPlayer"
unity-mcp audio stop "MusicPlayer"
unity-mcp audio volume "MusicPlayer" 0.5
```
### Animation Commands
```bash
# Control Animator (target must have Animator component)
unity-mcp animation play "Character" "Walk"
unity-mcp animation set-parameter "Character" "Speed" 1.5 --type float
unity-mcp animation set-parameter "Character" "IsRunning" true --type bool
unity-mcp animation set-parameter "Character" "Jump" "" --type trigger
```
### Code Commands
```bash
# Read source files
unity-mcp code read "Assets/Scripts/Player.cs"
unity-mcp code read "Assets/Scripts/Player.cs" --start-line 10 --line-count 20
```
### Raw Commands
For advanced usage, send raw tool calls:
```bash
# Send any MCP tool directly
unity-mcp raw manage_scene '{"action": "get_active"}'
unity-mcp raw manage_gameobject '{"action": "create", "name": "Test"}'
unity-mcp raw manage_components '{"action": "add", "target": "Test", "componentType": "Rigidbody"}'
unity-mcp raw manage_editor '{"action": "play"}'
```
---
## Known Behaviors
### Component Creation
When creating GameObjects with components, the CLI creates the object first, then adds components separately. This is the correct workflow for Unity MCP.
```bash
# This works correctly - creates object then adds components
unity-mcp gameobject create "Player" --components "Rigidbody,BoxCollider"
# Equivalent to:
unity-mcp gameobject create "Player"
unity-mcp component add "Player" Rigidbody
unity-mcp component add "Player" BoxCollider
```
### Light Creation
The `lighting create` command creates a complete light with the specified type, color, and intensity:
```bash
# Creates Point light with green color and intensity 5
unity-mcp lighting create "GreenLight" --type Point --color 0 1 0 --intensity 5
```
### UI Element Creation
UI commands automatically add the required components:
```bash
# create-canvas adds: Canvas, CanvasScaler, GraphicRaycaster
unity-mcp ui create-canvas "MainUI"
# create-button adds: Image, Button
unity-mcp ui create-button "MyButton" --parent "MainUI"
```
---
## Quick Reference Card
### Multi-Value Syntax
```bash
--position X Y Z # not "X,Y,Z"
--rotation X Y Z # not "X,Y,Z"
--scale X Y Z # not "X,Y,Z"
--color R G B # not "R,G,B"
```
### Argument Order (check --help)
```bash
material assign MATERIAL_PATH TARGET
prefab create TARGET PATH
component set TARGET COMPONENT PROPERTY VALUE
```
### Search Methods
```bash
--method by_name # default for gameobject find
--method by_id
--method by_path
--method by_tag
--method by_component
```
### Global Options Position
```bash
unity-mcp [GLOBAL_OPTIONS] command subcommand [ARGS] [OPTIONS]
# ^^^^^^^^^^^^^^^^
# Must come BEFORE command!
```
---
## Debugging Tips
1. **Always check `--help`** for any command:
```bash
unity-mcp gameobject --help
unity-mcp gameobject modify --help
```
2. **Use verbose mode** to see what's happening:
```bash
unity-mcp -v scene hierarchy
```
3. **Use JSON output** for programmatic parsing:
```bash
unity-mcp -f json gameobject find "Player" | jq '.result'
```
4. **Check connection first**:
```bash
unity-mcp status
```
5. **When in doubt about properties**, use info commands:
```bash
unity-mcp material info "Assets/Materials/Mat.mat"
unity-mcp asset info "Assets/Prefabs/Player.prefab"
```

View File

@ -0,0 +1,3 @@
"""Unity MCP Command Line Interface."""
__version__ = "1.0.0"

View File

@ -0,0 +1,3 @@
"""CLI command modules."""
# Commands will be registered in main.py

View File

@ -0,0 +1,87 @@
"""Animation CLI commands - placeholder for future implementation."""
import sys
import click
from typing import Optional, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_info
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def animation():
"""Animation operations - control Animator, play animations."""
pass
@animation.command("play")
@click.argument("target")
@click.argument("state_name")
@click.option(
"--layer", "-l",
default=0,
type=int,
help="Animator layer(TODO)."
)
@click.option(
"--search-method",
type=click.Choice(["by_name", "by_path", "by_id"]),
default=None,
help="How to find the target."
)
def play(target: str, state_name: str, layer: int, search_method: Optional[str]):
"""Play an animation state on a target's Animator.
\b
Examples:
unity-mcp animation play "Player" "Walk"
unity-mcp animation play "Enemy" "Attack" --layer 1
"""
config = get_config()
# Set Animator parameter to trigger state
params: dict[str, Any] = {
"action": "set_property",
"target": target,
"componentType": "Animator",
"property": "Play",
"value": state_name,
"layer": layer,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_components", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@animation.command("set-parameter")
@click.argument("target")
@click.argument("param_name")
@click.argument("value")
@click.option(
"--type", "-t",
"param_type",
type=click.Choice(["float", "int", "bool", "trigger"]),
default="float",
help="Parameter type."
)
def set_parameter(target: str, param_name: str, value: str, param_type: str):
"""Set an Animator parameter.
\b
Examples:
unity-mcp animation set-parameter "Player" "Speed" 5.0
unity-mcp animation set-parameter "Player" "IsRunning" true --type bool
unity-mcp animation set-parameter "Player" "Jump" "" --type trigger
"""
config = get_config()
print_info(
"Animation parameter command - requires custom Unity implementation")
click.echo(f"Would set {param_name}={value} ({param_type}) on {target}")

View File

@ -0,0 +1,310 @@
"""Asset CLI commands."""
import sys
import json
import click
from typing import Optional, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def asset():
"""Asset operations - search, import, create, delete assets."""
pass
@asset.command("search")
@click.argument("pattern", default="*")
@click.option(
"--path", "-p",
default="Assets",
help="Folder path to search in."
)
@click.option(
"--type", "-t",
"filter_type",
default=None,
help="Filter by asset type (e.g., Material, Prefab, MonoScript)."
)
@click.option(
"--limit", "-l",
default=25,
type=int,
help="Maximum results per page."
)
@click.option(
"--page",
default=1,
type=int,
help="Page number (1-based)."
)
def search(pattern: str, path: str, filter_type: Optional[str], limit: int, page: int):
"""Search for assets.
\b
Examples:
unity-mcp asset search "*.prefab"
unity-mcp asset search "Player*" --path "Assets/Characters"
unity-mcp asset search "*" --type Material
unity-mcp asset search "t:MonoScript" --path "Assets/Scripts"
"""
config = get_config()
params: dict[str, Any] = {
"action": "search",
"path": path,
"searchPattern": pattern,
"pageSize": limit,
"pageNumber": page,
}
if filter_type:
params["filterType"] = filter_type
try:
result = run_command("manage_asset", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@asset.command("info")
@click.argument("path")
@click.option(
"--preview",
is_flag=True,
help="Generate preview thumbnail (may be large)."
)
def info(path: str, preview: bool):
"""Get detailed information about an asset.
\b
Examples:
unity-mcp asset info "Assets/Materials/Red.mat"
unity-mcp asset info "Assets/Prefabs/Player.prefab" --preview
"""
config = get_config()
params: dict[str, Any] = {
"action": "get_info",
"path": path,
"generatePreview": preview,
}
try:
result = run_command("manage_asset", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@asset.command("create")
@click.argument("path")
@click.argument("asset_type")
@click.option(
"--properties", "-p",
default=None,
help='Initial properties as JSON.'
)
def create(path: str, asset_type: str, properties: Optional[str]):
"""Create a new asset.
\b
Examples:
unity-mcp asset create "Assets/Materials/Blue.mat" Material
unity-mcp asset create "Assets/NewFolder" Folder
unity-mcp asset create "Assets/Materials/Custom.mat" Material --properties '{"color": [0,0,1,1]}'
"""
config = get_config()
params: dict[str, Any] = {
"action": "create",
"path": path,
"assetType": asset_type,
}
if properties:
try:
params["properties"] = json.loads(properties)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON for properties: {e}")
sys.exit(1)
try:
result = run_command("manage_asset", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Created {asset_type}: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@asset.command("delete")
@click.argument("path")
@click.option(
"--force", "-f",
is_flag=True,
help="Skip confirmation prompt."
)
def delete(path: str, force: bool):
"""Delete an asset.
\b
Examples:
unity-mcp asset delete "Assets/OldMaterial.mat"
unity-mcp asset delete "Assets/Unused" --force
"""
config = get_config()
if not force:
click.confirm(f"Delete asset '{path}'?", abort=True)
try:
result = run_command(
"manage_asset", {"action": "delete", "path": path}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Deleted: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@asset.command("duplicate")
@click.argument("source")
@click.argument("destination")
def duplicate(source: str, destination: str):
"""Duplicate an asset.
\b
Examples:
unity-mcp asset duplicate "Assets/Materials/Red.mat" "Assets/Materials/RedCopy.mat"
"""
config = get_config()
params: dict[str, Any] = {
"action": "duplicate",
"path": source,
"destination": destination,
}
try:
result = run_command("manage_asset", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Duplicated to: {destination}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@asset.command("move")
@click.argument("source")
@click.argument("destination")
def move(source: str, destination: str):
"""Move an asset to a new location.
\b
Examples:
unity-mcp asset move "Assets/Old/Material.mat" "Assets/New/Material.mat"
"""
config = get_config()
params: dict[str, Any] = {
"action": "move",
"path": source,
"destination": destination,
}
try:
result = run_command("manage_asset", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Moved to: {destination}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@asset.command("rename")
@click.argument("path")
@click.argument("new_name")
def rename(path: str, new_name: str):
"""Rename an asset.
\b
Examples:
unity-mcp asset rename "Assets/Materials/Old.mat" "New.mat"
"""
config = get_config()
# Construct destination path
import os
dir_path = os.path.dirname(path)
destination = os.path.join(dir_path, new_name).replace("\\", "/")
params: dict[str, Any] = {
"action": "rename",
"path": path,
"destination": destination,
}
try:
result = run_command("manage_asset", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Renamed to: {new_name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@asset.command("import")
@click.argument("path")
def import_asset(path: str):
"""Import/reimport an asset.
\b
Examples:
unity-mcp asset import "Assets/Textures/NewTexture.png"
"""
config = get_config()
try:
result = run_command(
"manage_asset", {"action": "import", "path": path}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Imported: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@asset.command("mkdir")
@click.argument("path")
def mkdir(path: str):
"""Create a folder.
\b
Examples:
unity-mcp asset mkdir "Assets/NewFolder"
unity-mcp asset mkdir "Assets/Levels/Chapter1"
"""
config = get_config()
try:
result = run_command(
"manage_asset", {"action": "create_folder", "path": path}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Created folder: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,133 @@
"""Audio CLI commands - placeholder for future implementation."""
import sys
import click
from typing import Optional, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_info
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def audio():
"""Audio operations - AudioSource control, audio settings."""
pass
@audio.command("play")
@click.argument("target")
@click.option(
"--clip", "-c",
default=None,
help="Audio clip path to play."
)
@click.option(
"--search-method",
type=click.Choice(["by_name", "by_path", "by_id"]),
default=None,
help="How to find the target."
)
def play(target: str, clip: Optional[str], search_method: Optional[str]):
"""Play audio on a target's AudioSource.
\b
Examples:
unity-mcp audio play "MusicPlayer"
unity-mcp audio play "SFXSource" --clip "Assets/Audio/explosion.wav"
"""
config = get_config()
params: dict[str, Any] = {
"action": "set_property",
"target": target,
"componentType": "AudioSource",
"property": "Play",
"value": True,
}
if clip:
params["clip"] = clip
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_components", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@audio.command("stop")
@click.argument("target")
@click.option(
"--search-method",
type=click.Choice(["by_name", "by_path", "by_id"]),
default=None,
help="How to find the target."
)
def stop(target: str, search_method: Optional[str]):
"""Stop audio on a target's AudioSource.
\b
Examples:
unity-mcp audio stop "MusicPlayer"
"""
config = get_config()
params: dict[str, Any] = {
"action": "set_property",
"target": target,
"componentType": "AudioSource",
"property": "Stop",
"value": True,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_components", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@audio.command("volume")
@click.argument("target")
@click.argument("level", type=float)
@click.option(
"--search-method",
type=click.Choice(["by_name", "by_path", "by_id"]),
default=None,
help="How to find the target."
)
def volume(target: str, level: float, search_method: Optional[str]):
"""Set audio volume on a target's AudioSource.
\b
Examples:
unity-mcp audio volume "MusicPlayer" 0.5
"""
config = get_config()
params: dict[str, Any] = {
"action": "set_property",
"target": target,
"componentType": "AudioSource",
"property": "volume",
"value": level,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_components", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,184 @@
"""Batch CLI commands for executing multiple Unity operations efficiently."""
import sys
import json
import click
from typing import Optional, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success, print_info
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def batch():
"""Batch operations - execute multiple commands efficiently."""
pass
@batch.command("run")
@click.argument("file", type=click.Path(exists=True))
@click.option("--parallel", is_flag=True, help="Execute read-only commands in parallel.")
@click.option("--fail-fast", is_flag=True, help="Stop on first failure.")
def batch_run(file: str, parallel: bool, fail_fast: bool):
"""Execute commands from a JSON file.
The JSON file should contain an array of command objects with 'tool' and 'params' keys.
\\b
File format:
[
{"tool": "manage_gameobject", "params": {"action": "create", "name": "Cube1"}},
{"tool": "manage_gameobject", "params": {"action": "create", "name": "Cube2"}},
{"tool": "manage_components", "params": {"action": "add", "target": "Cube1", "componentType": "Rigidbody"}}
]
\\b
Examples:
unity-mcp batch run commands.json
unity-mcp batch run setup.json --parallel
unity-mcp batch run critical.json --fail-fast
"""
config = get_config()
try:
with open(file, 'r') as f:
commands = json.load(f)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON in file: {e}")
sys.exit(1)
except IOError as e:
print_error(f"Error reading file: {e}")
sys.exit(1)
if not isinstance(commands, list):
print_error("JSON file must contain an array of commands")
sys.exit(1)
if len(commands) > 25:
print_error(f"Maximum 25 commands per batch, got {len(commands)}")
sys.exit(1)
params: dict[str, Any] = {"commands": commands}
if parallel:
params["parallel"] = True
if fail_fast:
params["failFast"] = True
click.echo(f"Executing {len(commands)} commands...")
try:
result = run_command("batch_execute", params, config)
click.echo(format_output(result, config.format))
if isinstance(result, dict):
results = result.get("data", {}).get("results", [])
succeeded = sum(1 for r in results if r.get("success"))
failed = len(results) - succeeded
if failed == 0:
print_success(
f"All {succeeded} commands completed successfully")
else:
print_info(f"{succeeded} succeeded, {failed} failed")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@batch.command("inline")
@click.argument("commands_json")
@click.option("--parallel", is_flag=True, help="Execute read-only commands in parallel.")
@click.option("--fail-fast", is_flag=True, help="Stop on first failure.")
def batch_inline(commands_json: str, parallel: bool, fail_fast: bool):
"""Execute commands from inline JSON.
\\b
Examples:
unity-mcp batch inline '[{"tool": "manage_scene", "params": {"action": "get_active"}}]'
unity-mcp batch inline '[
{"tool": "manage_gameobject", "params": {"action": "create", "name": "A", "primitiveType": "Cube"}},
{"tool": "manage_gameobject", "params": {"action": "create", "name": "B", "primitiveType": "Sphere"}}
]'
"""
config = get_config()
try:
commands = json.loads(commands_json)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON: {e}")
sys.exit(1)
if not isinstance(commands, list):
print_error("Commands must be an array")
sys.exit(1)
if len(commands) > 25:
print_error(f"Maximum 25 commands per batch, got {len(commands)}")
sys.exit(1)
params: dict[str, Any] = {"commands": commands}
if parallel:
params["parallel"] = True
if fail_fast:
params["failFast"] = True
try:
result = run_command("batch_execute", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@batch.command("template")
@click.option("--output", "-o", type=click.Path(), help="Output file (default: stdout)")
def batch_template(output: Optional[str]):
"""Generate a sample batch commands file.
\\b
Examples:
unity-mcp batch template > commands.json
unity-mcp batch template -o my_batch.json
"""
template = [
{
"tool": "manage_scene",
"params": {"action": "get_active"}
},
{
"tool": "manage_gameobject",
"params": {
"action": "create",
"name": "BatchCube",
"primitiveType": "Cube",
"position": [0, 1, 0]
}
},
{
"tool": "manage_components",
"params": {
"action": "add",
"target": "BatchCube",
"componentType": "Rigidbody"
}
},
{
"tool": "manage_gameobject",
"params": {
"action": "modify",
"target": "BatchCube",
"position": [0, 5, 0]
}
}
]
json_output = json.dumps(template, indent=2)
if output:
with open(output, 'w') as f:
f.write(json_output)
print_success(f"Template written to: {output}")
else:
click.echo(json_output)

View File

@ -0,0 +1,189 @@
"""Code CLI commands - read source code. search might be implemented later (but can be totally achievable with AI)."""
import sys
import os
import click
from typing import Optional, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_info
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def code():
"""Code operations - read source files."""
pass
@code.command("read")
@click.argument("path")
@click.option(
"--start-line", "-s",
default=None,
type=int,
help="Starting line number (1-based)."
)
@click.option(
"--line-count", "-n",
default=None,
type=int,
help="Number of lines to read."
)
def read(path: str, start_line: Optional[int], line_count: Optional[int]):
"""Read a source file.
\b
Examples:
unity-mcp code read "Assets/Scripts/Player.cs"
unity-mcp code read "Assets/Scripts/Player.cs" --start-line 10 --line-count 20
"""
config = get_config()
# Extract name and directory from path
parts = path.replace("\\", "/").split("/")
filename = os.path.splitext(parts[-1])[0]
directory = "/".join(parts[:-1]) or "Assets"
params: dict[str, Any] = {
"action": "read",
"name": filename,
"path": directory,
}
if start_line:
params["startLine"] = start_line
if line_count:
params["lineCount"] = line_count
try:
result = run_command("manage_script", params, config)
# For read, output content directly if available
if result.get("success") and result.get("data"):
data = result.get("data", {})
if isinstance(data, dict) and "contents" in data:
click.echo(data["contents"])
else:
click.echo(format_output(result, config.format))
else:
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@code.command("search")
@click.argument("pattern")
@click.argument("path")
@click.option(
"--max-results", "-n",
default=50,
type=int,
help="Maximum number of results (default: 50)."
)
@click.option(
"--case-sensitive", "-c",
is_flag=True,
help="Make search case-sensitive (default: case-insensitive)."
)
def search(pattern: str, path: str, max_results: int, case_sensitive: bool):
"""Search for patterns in Unity scripts using regex.
PATTERN is a regex pattern to search for.
PATH is the script path (e.g., Assets/Scripts/Player.cs).
\\b
Examples:
unity-mcp code search "class.*Player" "Assets/Scripts/Player.cs"
unity-mcp code search "private.*int" "Assets/Scripts/GameManager.cs"
unity-mcp code search "TODO|FIXME" "Assets/Scripts/Utils.cs"
"""
import re
import base64
config = get_config()
# Extract name and directory from path
parts = path.replace("\\", "/").split("/")
filename = os.path.splitext(parts[-1])[0]
directory = "/".join(parts[:-1]) or "Assets"
# Step 1: Read the file via Unity's manage_script
read_params: dict[str, Any] = {
"action": "read",
"name": filename,
"path": directory,
}
try:
result = run_command("manage_script", read_params, config)
# Handle nested response structure: {status, result: {success, data}}
inner_result = result.get("result", result)
if not inner_result.get("success") and result.get("status") != "success":
click.echo(format_output(result, config.format))
return
# Get file contents from nested data
data = inner_result.get("data", {})
contents = data.get("contents")
# Handle base64 encoded content
if not contents and data.get("contentsEncoded") and data.get("encodedContents"):
try:
contents = base64.b64decode(
data["encodedContents"]).decode("utf-8", "replace")
except (ValueError, TypeError):
pass
if not contents:
print_error(f"Could not read file content from {path}")
sys.exit(1)
# Step 2: Perform regex search locally
flags = re.MULTILINE
if not case_sensitive:
flags |= re.IGNORECASE
try:
regex = re.compile(pattern, flags)
except re.error as e:
print_error(f"Invalid regex pattern: {e}")
sys.exit(1)
found = list(regex.finditer(contents))
if not found:
print_info(f"No matches found for pattern: {pattern}")
return
results = []
for m in found[:max_results]:
start_idx = m.start()
# Calculate line number
line_num = contents.count('\n', 0, start_idx) + 1
# Get line content
line_start = contents.rfind('\n', 0, start_idx) + 1
line_end = contents.find('\n', start_idx)
if line_end == -1:
line_end = len(contents)
line_content = contents[line_start:line_end].strip()
results.append({
"line": line_num,
"content": line_content,
"match": m.group(0),
})
# Display results
click.echo(f"Found {len(results)} matches (total: {len(found)}):\n")
for match in results:
click.echo(f" Line {match['line']}: {match['content']}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,212 @@
"""Component CLI commands."""
import sys
import json
import click
from typing import Optional, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def component():
"""Component operations - add, remove, modify components on GameObjects."""
pass
@component.command("add")
@click.argument("target")
@click.argument("component_type")
@click.option(
"--search-method",
type=click.Choice(["by_id", "by_name", "by_path"]),
default=None,
help="How to find the target GameObject."
)
@click.option(
"--properties", "-p",
default=None,
help='Initial properties as JSON (e.g., \'{"mass": 5.0}\').'
)
def add(target: str, component_type: str, search_method: Optional[str], properties: Optional[str]):
"""Add a component to a GameObject.
\b
Examples:
unity-mcp component add "Player" Rigidbody
unity-mcp component add "-81840" BoxCollider --search-method by_id
unity-mcp component add "Enemy" Rigidbody --properties '{"mass": 5.0, "useGravity": true}'
"""
config = get_config()
params: dict[str, Any] = {
"action": "add",
"target": target,
"componentType": component_type,
}
if search_method:
params["searchMethod"] = search_method
if properties:
try:
params["properties"] = json.loads(properties)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON for properties: {e}")
sys.exit(1)
try:
result = run_command("manage_components", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Added {component_type} to '{target}'")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@component.command("remove")
@click.argument("target")
@click.argument("component_type")
@click.option(
"--search-method",
type=click.Choice(["by_id", "by_name", "by_path"]),
default=None,
help="How to find the target GameObject."
)
@click.option(
"--force", "-f",
is_flag=True,
help="Skip confirmation prompt."
)
def remove(target: str, component_type: str, search_method: Optional[str], force: bool):
"""Remove a component from a GameObject.
\b
Examples:
unity-mcp component remove "Player" Rigidbody
unity-mcp component remove "-81840" BoxCollider --search-method by_id --force
"""
config = get_config()
if not force:
click.confirm(f"Remove {component_type} from '{target}'?", abort=True)
params: dict[str, Any] = {
"action": "remove",
"target": target,
"componentType": component_type,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_components", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Removed {component_type} from '{target}'")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@component.command("set")
@click.argument("target")
@click.argument("component_type")
@click.argument("property_name")
@click.argument("value")
@click.option(
"--search-method",
type=click.Choice(["by_id", "by_name", "by_path"]),
default=None,
help="How to find the target GameObject."
)
def set_property(target: str, component_type: str, property_name: str, value: str, search_method: Optional[str]):
"""Set a single property on a component.
\b
Examples:
unity-mcp component set "Player" Rigidbody mass 5.0
unity-mcp component set "Enemy" Transform position "[0, 5, 0]"
unity-mcp component set "-81840" Light intensity 2.5 --search-method by_id
"""
config = get_config()
# Try to parse value as JSON for complex types
try:
parsed_value = json.loads(value)
except json.JSONDecodeError:
# Keep as string if not valid JSON
parsed_value = value
params: dict[str, Any] = {
"action": "set_property",
"target": target,
"componentType": component_type,
"property": property_name,
"value": parsed_value,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_components", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Set {component_type}.{property_name} = {value}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@component.command("modify")
@click.argument("target")
@click.argument("component_type")
@click.option(
"--properties", "-p",
required=True,
help='Properties to set as JSON (e.g., \'{"mass": 5.0, "useGravity": false}\').'
)
@click.option(
"--search-method",
type=click.Choice(["by_id", "by_name", "by_path"]),
default=None,
help="How to find the target GameObject."
)
def modify(target: str, component_type: str, properties: str, search_method: Optional[str]):
"""Set multiple properties on a component at once.
\b
Examples:
unity-mcp component modify "Player" Rigidbody --properties '{"mass": 5.0, "useGravity": false}'
unity-mcp component modify "Light" Light --properties '{"intensity": 2.0, "color": [1, 0, 0, 1]}'
"""
config = get_config()
try:
props_dict = json.loads(properties)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON for properties: {e}")
sys.exit(1)
params: dict[str, Any] = {
"action": "set_property",
"target": target,
"componentType": component_type,
"properties": props_dict,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_components", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Modified {component_type} on '{target}'")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,487 @@
"""Editor CLI commands."""
import sys
import click
from typing import Optional, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success, print_info
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def editor():
"""Editor operations - play mode, console, tags, layers."""
pass
@editor.command("play")
def play():
"""Enter play mode."""
config = get_config()
try:
result = run_command("manage_editor", {"action": "play"}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success("Entered play mode")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("pause")
def pause():
"""Pause play mode."""
config = get_config()
try:
result = run_command("manage_editor", {"action": "pause"}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success("Paused play mode")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("stop")
def stop():
"""Stop play mode."""
config = get_config()
try:
result = run_command("manage_editor", {"action": "stop"}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success("Stopped play mode")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("console")
@click.option(
"--type", "-t",
"log_types",
multiple=True,
type=click.Choice(["error", "warning", "log", "all"]),
default=["error", "warning", "log"],
help="Message types to retrieve."
)
@click.option(
"--count", "-n",
default=10,
type=int,
help="Number of messages to retrieve."
)
@click.option(
"--filter", "-f",
"filter_text",
default=None,
help="Filter messages containing this text."
)
@click.option(
"--stacktrace", "-s",
is_flag=True,
help="Include stack traces."
)
@click.option(
"--clear",
is_flag=True,
help="Clear the console instead of reading."
)
def console(log_types: tuple, count: int, filter_text: Optional[str], stacktrace: bool, clear: bool):
"""Read or clear the Unity console.
\b
Examples:
unity-mcp editor console
unity-mcp editor console --type error --count 20
unity-mcp editor console --filter "NullReference" --stacktrace
unity-mcp editor console --clear
"""
config = get_config()
if clear:
try:
result = run_command("read_console", {"action": "clear"}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success("Console cleared")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
return
params: dict[str, Any] = {
"action": "get",
"types": list(log_types),
"count": count,
"include_stacktrace": stacktrace,
}
if filter_text:
params["filter_text"] = filter_text
try:
result = run_command("read_console", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("add-tag")
@click.argument("tag_name")
def add_tag(tag_name: str):
"""Add a new tag.
\b
Examples:
unity-mcp editor add-tag "Enemy"
unity-mcp editor add-tag "Collectible"
"""
config = get_config()
try:
result = run_command(
"manage_editor", {"action": "add_tag", "tagName": tag_name}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Added tag: {tag_name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("remove-tag")
@click.argument("tag_name")
def remove_tag(tag_name: str):
"""Remove a tag.
\b
Examples:
unity-mcp editor remove-tag "OldTag"
"""
config = get_config()
try:
result = run_command(
"manage_editor", {"action": "remove_tag", "tagName": tag_name}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Removed tag: {tag_name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("add-layer")
@click.argument("layer_name")
def add_layer(layer_name: str):
"""Add a new layer.
\b
Examples:
unity-mcp editor add-layer "Interactable"
"""
config = get_config()
try:
result = run_command(
"manage_editor", {"action": "add_layer", "layerName": layer_name}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Added layer: {layer_name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("remove-layer")
@click.argument("layer_name")
def remove_layer(layer_name: str):
"""Remove a layer.
\b
Examples:
unity-mcp editor remove-layer "OldLayer"
"""
config = get_config()
try:
result = run_command(
"manage_editor", {"action": "remove_layer", "layerName": layer_name}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Removed layer: {layer_name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("tool")
@click.argument("tool_name")
def set_tool(tool_name: str):
"""Set the active editor tool.
\b
Examples:
unity-mcp editor tool "Move"
unity-mcp editor tool "Rotate"
unity-mcp editor tool "Scale"
"""
config = get_config()
try:
result = run_command(
"manage_editor", {"action": "set_active_tool", "toolName": tool_name}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Set active tool: {tool_name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("menu")
@click.argument("menu_path")
def execute_menu(menu_path: str):
"""Execute a menu item.
\b
Examples:
unity-mcp editor menu "File/Save"
unity-mcp editor menu "Edit/Undo"
unity-mcp editor menu "GameObject/Create Empty"
"""
config = get_config()
try:
result = run_command("execute_menu_item", {
"menu_path": menu_path}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Executed: {menu_path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("tests")
@click.option(
"--mode", "-m",
type=click.Choice(["EditMode", "PlayMode"]),
default="EditMode",
help="Test mode to run."
)
@click.option(
"--async", "async_mode",
is_flag=True,
help="Run asynchronously and return job ID for polling."
)
@click.option(
"--wait", "-w",
type=int,
default=None,
help="Wait up to N seconds for completion (default: no wait)."
)
@click.option(
"--details",
is_flag=True,
help="Include detailed results for all tests."
)
@click.option(
"--failed-only",
is_flag=True,
help="Include details for failed/skipped tests only."
)
def run_tests(mode: str, async_mode: bool, wait: Optional[int], details: bool, failed_only: bool):
"""Run Unity tests.
\b
Examples:
unity-mcp editor tests
unity-mcp editor tests --mode PlayMode
unity-mcp editor tests --async
unity-mcp editor tests --wait 60 --failed-only
"""
config = get_config()
params: dict[str, Any] = {"mode": mode}
if wait is not None:
params["wait_timeout"] = wait
if details:
params["include_details"] = True
if failed_only:
params["include_failed_tests"] = True
try:
result = run_command("run_tests", params, config)
# For async mode, just show job ID
if async_mode and result.get("success"):
job_id = result.get("data", {}).get("job_id")
if job_id:
click.echo(f"Test job started: {job_id}")
print_info("Poll with: unity-mcp editor poll-test " + job_id)
return
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("poll-test")
@click.argument("job_id")
@click.option(
"--wait", "-w",
type=int,
default=30,
help="Wait up to N seconds for completion (default: 30)."
)
@click.option(
"--details",
is_flag=True,
help="Include detailed results for all tests."
)
@click.option(
"--failed-only",
is_flag=True,
help="Include details for failed/skipped tests only."
)
def poll_test(job_id: str, wait: int, details: bool, failed_only: bool):
"""Poll an async test job for status/results.
\b
Examples:
unity-mcp editor poll-test abc123
unity-mcp editor poll-test abc123 --wait 60
unity-mcp editor poll-test abc123 --failed-only
"""
config = get_config()
params: dict[str, Any] = {"job_id": job_id}
if wait:
params["wait_timeout"] = wait
if details:
params["include_details"] = True
if failed_only:
params["include_failed_tests"] = True
try:
result = run_command("get_test_job", params, config)
click.echo(format_output(result, config.format))
if isinstance(result, dict) and result.get("success"):
data = result.get("data", {})
status = data.get("status", "unknown")
if status == "succeeded":
print_success("Tests completed successfully")
elif status == "failed":
summary = data.get("result", {}).get("summary", {})
failed = summary.get("failed", 0)
print_error(f"Tests failed: {failed} failures")
elif status == "running":
progress = data.get("progress", {})
completed = progress.get("completed", 0)
total = progress.get("total", 0)
print_info(f"Tests running: {completed}/{total}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("refresh")
@click.option(
"--mode",
type=click.Choice(["if_dirty", "force"]),
default="if_dirty",
help="Refresh mode."
)
@click.option(
"--scope",
type=click.Choice(["assets", "scripts", "all"]),
default="all",
help="What to refresh."
)
@click.option(
"--compile",
is_flag=True,
help="Request script compilation."
)
@click.option(
"--no-wait",
is_flag=True,
help="Don't wait for refresh to complete."
)
def refresh(mode: str, scope: str, compile: bool, no_wait: bool):
"""Force Unity to refresh assets/scripts.
\b
Examples:
unity-mcp editor refresh
unity-mcp editor refresh --mode force
unity-mcp editor refresh --compile
unity-mcp editor refresh --scope scripts --compile
"""
config = get_config()
params: dict[str, Any] = {
"mode": mode,
"scope": scope,
"wait_for_ready": not no_wait,
}
if compile:
params["compile"] = "request"
try:
click.echo("Refreshing Unity...")
result = run_command("refresh_unity", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success("Unity refreshed")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@editor.command("custom-tool")
@click.argument("tool_name")
@click.option(
"--params", "-p",
default="{}",
help="Tool parameters as JSON."
)
def custom_tool(tool_name: str, params: str):
"""Execute a custom Unity tool.
Custom tools are registered by Unity projects via the MCP plugin.
\b
Examples:
unity-mcp editor custom-tool "MyCustomTool"
unity-mcp editor custom-tool "BuildPipeline" --params '{"target": "Android"}'
"""
import json
config = get_config()
try:
params_dict = json.loads(params)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON for params: {e}")
sys.exit(1)
try:
result = run_command("execute_custom_tool", {
"tool_name": tool_name,
"parameters": params_dict,
}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Executed custom tool: {tool_name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,510 @@
"""GameObject CLI commands."""
import sys
import json
import click
from typing import Optional, Tuple, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success, print_warning
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def gameobject():
"""GameObject operations - create, find, modify, delete GameObjects."""
pass
@gameobject.command("find")
@click.argument("search_term")
@click.option(
"--method", "-m",
type=click.Choice(["by_name", "by_tag", "by_layer",
"by_component", "by_path", "by_id"]),
default="by_name",
help="Search method."
)
@click.option(
"--include-inactive", "-i",
is_flag=True,
help="Include inactive GameObjects."
)
@click.option(
"--limit", "-l",
default=50,
type=int,
help="Maximum results to return."
)
@click.option(
"--cursor", "-c",
default=0,
type=int,
help="Pagination cursor (offset)."
)
def find(search_term: str, method: str, include_inactive: bool, limit: int, cursor: int):
"""Find GameObjects by search criteria.
\b
Examples:
unity-mcp gameobject find "Player"
unity-mcp gameobject find "Enemy" --method by_tag
unity-mcp gameobject find "-81840" --method by_id
unity-mcp gameobject find "Rigidbody" --method by_component
unity-mcp gameobject find "/Canvas/Panel" --method by_path
"""
config = get_config()
try:
result = run_command("find_gameobjects", {
"searchMethod": method,
"searchTerm": search_term,
"includeInactive": include_inactive,
"pageSize": limit,
"cursor": cursor,
}, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@gameobject.command("create")
@click.argument("name")
@click.option(
"--primitive", "-p",
type=click.Choice(["Cube", "Sphere", "Cylinder",
"Plane", "Capsule", "Quad"]),
help="Create a primitive type."
)
@click.option(
"--position", "-pos",
nargs=3,
type=float,
default=None,
help="Position as X Y Z."
)
@click.option(
"--rotation", "-rot",
nargs=3,
type=float,
default=None,
help="Rotation as X Y Z (euler angles)."
)
@click.option(
"--scale", "-s",
nargs=3,
type=float,
default=None,
help="Scale as X Y Z."
)
@click.option(
"--parent",
default=None,
help="Parent GameObject name or path."
)
@click.option(
"--tag", "-t",
default=None,
help="Tag to assign."
)
@click.option(
"--layer",
default=None,
help="Layer to assign."
)
@click.option(
"--components",
default=None,
help="Comma-separated list of components to add."
)
@click.option(
"--save-prefab",
is_flag=True,
help="Save as prefab after creation."
)
@click.option(
"--prefab-path",
default=None,
help="Path for prefab (e.g., Assets/Prefabs/MyPrefab.prefab)."
)
def create(
name: str,
primitive: Optional[str],
position: Optional[Tuple[float, float, float]],
rotation: Optional[Tuple[float, float, float]],
scale: Optional[Tuple[float, float, float]],
parent: Optional[str],
tag: Optional[str],
layer: Optional[str],
components: Optional[str],
save_prefab: bool,
prefab_path: Optional[str],
):
"""Create a new GameObject.
\b
Examples:
unity-mcp gameobject create "MyCube" --primitive Cube
unity-mcp gameobject create "Player" --position 0 1 0
unity-mcp gameobject create "Enemy" --primitive Sphere --tag Enemy
unity-mcp gameobject create "Child" --parent "ParentObject"
unity-mcp gameobject create "Item" --components "Rigidbody,BoxCollider"
"""
config = get_config()
params: dict[str, Any] = {
"action": "create",
"name": name,
}
if primitive:
params["primitiveType"] = primitive
if position:
params["position"] = list(position)
if rotation:
params["rotation"] = list(rotation)
if scale:
params["scale"] = list(scale)
if parent:
params["parent"] = parent
if tag:
params["tag"] = tag
if layer:
params["layer"] = layer
if save_prefab:
params["saveAsPrefab"] = True
if prefab_path:
params["prefabPath"] = prefab_path
try:
result = run_command("manage_gameobject", params, config)
# Add components separately since componentsToAdd doesn't work
if components and (result.get("success") or result.get("data") or result.get("result")):
component_list = [c.strip() for c in components.split(",")]
failed_components = []
for component in component_list:
try:
run_command("manage_components", {
"action": "add",
"target": name,
"componentType": component,
}, config)
except UnityConnectionError:
failed_components.append(component)
if failed_components:
print_warning(
f"Failed to add components: {', '.join(failed_components)}")
click.echo(format_output(result, config.format))
if result.get("success") or result.get("result"):
print_success(f"Created GameObject '{name}'")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@gameobject.command("modify")
@click.argument("target")
@click.option(
"--name", "-n",
default=None,
help="New name for the GameObject."
)
@click.option(
"--position", "-pos",
nargs=3,
type=float,
default=None,
help="New position as X Y Z."
)
@click.option(
"--rotation", "-rot",
nargs=3,
type=float,
default=None,
help="New rotation as X Y Z (euler angles)."
)
@click.option(
"--scale", "-s",
nargs=3,
type=float,
default=None,
help="New scale as X Y Z."
)
@click.option(
"--parent",
default=None,
help="New parent GameObject."
)
@click.option(
"--tag", "-t",
default=None,
help="New tag."
)
@click.option(
"--layer",
default=None,
help="New layer."
)
@click.option(
"--active/--inactive",
default=None,
help="Set active state."
)
@click.option(
"--add-components",
default=None,
help="Comma-separated list of components to add."
)
@click.option(
"--remove-components",
default=None,
help="Comma-separated list of components to remove."
)
@click.option(
"--search-method",
type=click.Choice(["by_name", "by_path", "by_tag", "by_id"]),
default=None,
help="How to find the target GameObject."
)
def modify(
target: str,
name: Optional[str],
position: Optional[Tuple[float, float, float]],
rotation: Optional[Tuple[float, float, float]],
scale: Optional[Tuple[float, float, float]],
parent: Optional[str],
tag: Optional[str],
layer: Optional[str],
active: Optional[bool],
add_components: Optional[str],
remove_components: Optional[str],
search_method: Optional[str],
):
"""Modify an existing GameObject.
TARGET can be a name, path, instance ID, or tag depending on --search-method.
\b
Examples:
unity-mcp gameobject modify "Player" --position 0 5 0
unity-mcp gameobject modify "Enemy" --name "Boss" --tag "Boss"
unity-mcp gameobject modify "-81840" --search-method by_id --active
unity-mcp gameobject modify "/Canvas/Panel" --search-method by_path --inactive
unity-mcp gameobject modify "Cube" --add-components "Rigidbody,BoxCollider"
"""
config = get_config()
params: dict[str, Any] = {
"action": "modify",
"target": target,
}
if name:
params["name"] = name
if position:
params["position"] = list(position)
if rotation:
params["rotation"] = list(rotation)
if scale:
params["scale"] = list(scale)
if parent:
params["parent"] = parent
if tag:
params["tag"] = tag
if layer:
params["layer"] = layer
if active is not None:
params["setActive"] = active
if add_components:
params["componentsToAdd"] = [c.strip()
for c in add_components.split(",")]
if remove_components:
params["componentsToRemove"] = [c.strip()
for c in remove_components.split(",")]
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_gameobject", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@gameobject.command("delete")
@click.argument("target")
@click.option(
"--search-method",
type=click.Choice(["by_name", "by_path", "by_tag", "by_id"]),
default=None,
help="How to find the target GameObject."
)
@click.option(
"--force", "-f",
is_flag=True,
help="Skip confirmation prompt."
)
def delete(target: str, search_method: Optional[str], force: bool):
"""Delete a GameObject.
\b
Examples:
unity-mcp gameobject delete "OldObject"
unity-mcp gameobject delete "-81840" --search-method by_id
unity-mcp gameobject delete "TempObjects" --search-method by_tag --force
"""
config = get_config()
if not force:
click.confirm(f"Delete GameObject '{target}'?", abort=True)
params = {
"action": "delete",
"target": target,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_gameobject", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Deleted GameObject '{target}'")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@gameobject.command("duplicate")
@click.argument("target")
@click.option(
"--name", "-n",
default=None,
help="Name for the duplicate (default: OriginalName_Copy)."
)
@click.option(
"--offset",
nargs=3,
type=float,
default=None,
help="Position offset from original as X Y Z."
)
@click.option(
"--search-method",
type=click.Choice(["by_name", "by_path", "by_tag", "by_id"]),
default=None,
help="How to find the target GameObject."
)
def duplicate(
target: str,
name: Optional[str],
offset: Optional[Tuple[float, float, float]],
search_method: Optional[str],
):
"""Duplicate a GameObject.
\b
Examples:
unity-mcp gameobject duplicate "Player"
unity-mcp gameobject duplicate "Enemy" --name "Enemy2" --offset 5 0 0
unity-mcp gameobject duplicate "-81840" --search-method by_id
"""
config = get_config()
params: dict[str, Any] = {
"action": "duplicate",
"target": target,
}
if name:
params["new_name"] = name
if offset:
params["offset"] = list(offset)
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_gameobject", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Duplicated GameObject '{target}'")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@gameobject.command("move")
@click.argument("target")
@click.option(
"--reference", "-r",
required=True,
help="Reference object for relative movement."
)
@click.option(
"--direction", "-d",
type=click.Choice(["left", "right", "up", "down", "forward",
"back", "front", "backward", "behind"]),
required=True,
help="Direction to move."
)
@click.option(
"--distance",
type=float,
default=1.0,
help="Distance to move (default: 1.0)."
)
@click.option(
"--local",
is_flag=True,
help="Use reference object's local space instead of world space."
)
@click.option(
"--search-method",
type=click.Choice(["by_name", "by_path", "by_tag", "by_id"]),
default=None,
help="How to find the target GameObject."
)
def move(
target: str,
reference: str,
direction: str,
distance: float,
local: bool,
search_method: Optional[str],
):
"""Move a GameObject relative to another object.
\b
Examples:
unity-mcp gameobject move "Chair" --reference "Table" --direction right --distance 2
unity-mcp gameobject move "Light" --reference "Player" --direction up --distance 3
unity-mcp gameobject move "NPC" --reference "Player" --direction forward --local
"""
config = get_config()
params: dict[str, Any] = {
"action": "move_relative",
"target": target,
"reference_object": reference,
"direction": direction,
"distance": distance,
"world_space": not local,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_gameobject", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(
f"Moved '{target}' {direction} of '{reference}' by {distance} units")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,101 @@
"""Instance CLI commands for managing Unity instances."""
import sys
import click
from typing import Optional
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success, print_info
from cli.utils.connection import run_command, run_list_instances, UnityConnectionError
@click.group()
def instance():
"""Unity instance management - list, select, and view instances."""
pass
@instance.command("list")
def list_instances():
"""List available Unity instances.
\\b
Examples:
unity-mcp instance list
"""
config = get_config()
try:
result = run_list_instances(config)
instances = result.get("instances", []) if isinstance(
result, dict) else []
if not instances:
print_info("No Unity instances currently connected")
return
click.echo("Available Unity instances:")
for inst in instances:
project = inst.get("project", "Unknown")
version = inst.get("unity_version", "Unknown")
hash_id = inst.get("hash", "")
session_id = inst.get("session_id", "")
# Format: ProjectName@hash (Unity version)
display_id = f"{project}@{hash_id}" if hash_id else project
click.echo(f"{display_id} (Unity {version})")
if session_id:
click.echo(f" Session: {session_id[:8]}...")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@instance.command("set")
@click.argument("instance_id")
def set_instance(instance_id: str):
"""Set the active Unity instance.
INSTANCE_ID can be Name@hash or just a hash prefix.
\\b
Examples:
unity-mcp instance set "MyProject@abc123"
unity-mcp instance set abc123
"""
config = get_config()
try:
result = run_command("set_active_instance", {
"instance": instance_id,
}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
data = result.get("data", {})
active = data.get("instance", instance_id)
print_success(f"Active instance set to: {active}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@instance.command("current")
def current_instance():
"""Show the currently selected Unity instance.
\\b
Examples:
unity-mcp instance current
"""
config = get_config()
# The current instance is typically shown in telemetry or needs to be tracked
# For now, we can show the configured instance from CLI options
if config.unity_instance:
click.echo(f"Configured instance: {config.unity_instance}")
else:
print_info(
"No instance explicitly set. Using default (auto-select single instance).")
print_info("Use 'unity-mcp instance list' to see available instances.")
print_info("Use 'unity-mcp instance set <id>' to select one.")

View File

@ -0,0 +1,128 @@
"""Lighting CLI commands."""
import sys
import click
from typing import Optional, Tuple
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def lighting():
"""Lighting operations - create, modify lights and lighting settings."""
pass
@lighting.command("create")
@click.argument("name")
@click.option(
"--type", "-t",
"light_type",
type=click.Choice(["Directional", "Point", "Spot", "Area"]),
default="Point",
help="Type of light to create."
)
@click.option(
"--position", "-pos",
nargs=3,
type=float,
default=(0, 3, 0),
help="Position as X Y Z."
)
@click.option(
"--color", "-c",
nargs=3,
type=float,
default=None,
help="Color as R G B (0-1)."
)
@click.option(
"--intensity", "-i",
default=None,
type=float,
help="Light intensity."
)
def create(name: str, light_type: str, position: Tuple[float, float, float], color: Optional[Tuple[float, float, float]], intensity: Optional[float]):
"""Create a new light.
\b
Examples:
unity-mcp lighting create "MainLight" --type Directional
unity-mcp lighting create "PointLight1" --position 0 5 0 --intensity 2
unity-mcp lighting create "RedLight" --type Spot --color 1 0 0
"""
config = get_config()
try:
# Step 1: Create empty GameObject with position
create_result = run_command("manage_gameobject", {
"action": "create",
"name": name,
"position": list(position),
}, config)
if not (create_result.get("success")):
click.echo(format_output(create_result, config.format))
return
# Step 2: Add Light component using manage_components
add_result = run_command("manage_components", {
"action": "add",
"target": name,
"componentType": "Light",
}, config)
if not add_result.get("success"):
click.echo(format_output(add_result, config.format))
return
# Step 3: Set light type using manage_components set_property
type_result = run_command("manage_components", {
"action": "set_property",
"target": name,
"componentType": "Light",
"property": "type",
"value": light_type,
}, config)
if not type_result.get("success"):
click.echo(format_output(type_result, config.format))
return
# Step 4: Set color if provided
if color:
color_result = run_command("manage_components", {
"action": "set_property",
"target": name,
"componentType": "Light",
"property": "color",
"value": {"r": color[0], "g": color[1], "b": color[2], "a": 1},
}, config)
if not color_result.get("success"):
click.echo(format_output(color_result, config.format))
return
# Step 5: Set intensity if provided
if intensity is not None:
intensity_result = run_command("manage_components", {
"action": "set_property",
"target": name,
"componentType": "Light",
"property": "intensity",
"value": intensity,
}, config)
if not intensity_result.get("success"):
click.echo(format_output(intensity_result, config.format))
return
# Output the result
click.echo(format_output(create_result, config.format))
print_success(f"Created {light_type} light: {name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,268 @@
"""Material CLI commands."""
import sys
import json
import click
from typing import Optional, Any, Tuple
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def material():
"""Material operations - create, modify, assign materials."""
pass
@material.command("info")
@click.argument("path")
def info(path: str):
"""Get information about a material.
\b
Examples:
unity-mcp material info "Assets/Materials/Red.mat"
"""
config = get_config()
try:
result = run_command("manage_material", {
"action": "get_material_info",
"materialPath": path,
}, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@material.command("create")
@click.argument("path")
@click.option(
"--shader", "-s",
default="Standard",
help="Shader to use (default: Standard)."
)
@click.option(
"--properties", "-p",
default=None,
help='Initial properties as JSON.'
)
def create(path: str, shader: str, properties: Optional[str]):
"""Create a new material.
\b
Examples:
unity-mcp material create "Assets/Materials/NewMat.mat"
unity-mcp material create "Assets/Materials/Red.mat" --shader "Universal Render Pipeline/Lit"
unity-mcp material create "Assets/Materials/Blue.mat" --properties '{"_Color": [0,0,1,1]}'
"""
config = get_config()
params: dict[str, Any] = {
"action": "create",
"materialPath": path,
"shader": shader,
}
if properties:
try:
params["properties"] = json.loads(properties)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON for properties: {e}")
sys.exit(1)
try:
result = run_command("manage_material", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Created material: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@material.command("set-color")
@click.argument("path")
@click.argument("r", type=float)
@click.argument("g", type=float)
@click.argument("b", type=float)
@click.argument("a", type=float, default=1.0)
@click.option(
"--property", "-p",
default="_Color",
help="Color property name (default: _Color)."
)
def set_color(path: str, r: float, g: float, b: float, a: float, property: str):
"""Set a material's color.
\b
Examples:
unity-mcp material set-color "Assets/Materials/Red.mat" 1 0 0
unity-mcp material set-color "Assets/Materials/Blue.mat" 0 0 1 0.5
unity-mcp material set-color "Assets/Materials/Mat.mat" 1 1 0 --property "_BaseColor"
"""
config = get_config()
params: dict[str, Any] = {
"action": "set_material_color",
"materialPath": path,
"property": property,
"color": [r, g, b, a],
}
try:
result = run_command("manage_material", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Set color on: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@material.command("set-property")
@click.argument("path")
@click.argument("property_name")
@click.argument("value")
def set_property(path: str, property_name: str, value: str):
"""Set a shader property on a material.
\b
Examples:
unity-mcp material set-property "Assets/Materials/Mat.mat" _Metallic 0.5
unity-mcp material set-property "Assets/Materials/Mat.mat" _Smoothness 0.8
unity-mcp material set-property "Assets/Materials/Mat.mat" _MainTex "Assets/Textures/Tex.png"
"""
config = get_config()
# Try to parse value as JSON for complex types
try:
parsed_value = json.loads(value)
except json.JSONDecodeError:
# Try to parse as number
try:
parsed_value = float(value)
except ValueError:
parsed_value = value
params: dict[str, Any] = {
"action": "set_material_shader_property",
"materialPath": path,
"property": property_name,
"value": parsed_value,
}
try:
result = run_command("manage_material", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Set {property_name} on: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@material.command("assign")
@click.argument("material_path")
@click.argument("target")
@click.option(
"--search-method",
type=click.Choice(["by_name", "by_path", "by_tag",
"by_layer", "by_component"]),
default=None,
help="How to find the target GameObject."
)
@click.option(
"--slot", "-s",
default=0,
type=int,
help="Material slot index (default: 0)."
)
@click.option(
"--mode", "-m",
type=click.Choice(["shared", "instance", "property_block"]),
default="shared",
help="Assignment mode."
)
def assign(material_path: str, target: str, search_method: Optional[str], slot: int, mode: str):
"""Assign a material to a GameObject's renderer.
\b
Examples:
unity-mcp material assign "Assets/Materials/Red.mat" "Cube"
unity-mcp material assign "Assets/Materials/Blue.mat" "Player" --mode instance
unity-mcp material assign "Assets/Materials/Mat.mat" "-81840" --search-method by_id --slot 1
"""
config = get_config()
params: dict[str, Any] = {
"action": "assign_material_to_renderer",
"materialPath": material_path,
"target": target,
"slot": slot,
"mode": mode,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_material", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Assigned material to: {target}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@material.command("set-renderer-color")
@click.argument("target")
@click.argument("r", type=float)
@click.argument("g", type=float)
@click.argument("b", type=float)
@click.argument("a", type=float, default=1.0)
@click.option(
"--search-method",
type=click.Choice(["by_name", "by_path", "by_tag",
"by_layer", "by_component"]),
default=None,
help="How to find the target GameObject."
)
@click.option(
"--mode", "-m",
type=click.Choice(["shared", "instance", "property_block"]),
default="property_block",
help="Modification mode."
)
def set_renderer_color(target: str, r: float, g: float, b: float, a: float, search_method: Optional[str], mode: str):
"""Set a renderer's material color directly.
\b
Examples:
unity-mcp material set-renderer-color "Cube" 1 0 0
unity-mcp material set-renderer-color "Player" 0 1 0 --mode instance
"""
config = get_config()
params: dict[str, Any] = {
"action": "set_renderer_color",
"target": target,
"color": [r, g, b, a],
"mode": mode,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_material", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Set renderer color on: {target}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,144 @@
"""Prefab CLI commands."""
import sys
import click
from typing import Optional, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def prefab():
"""Prefab operations - open, save, create prefabs."""
pass
@prefab.command("open")
@click.argument("path")
@click.option(
"--mode", "-m",
default="InIsolation",
help="Prefab stage mode (InIsolation)."
)
def open_stage(path: str, mode: str):
"""Open a prefab in the prefab stage for editing.
\b
Examples:
unity-mcp prefab open "Assets/Prefabs/Player.prefab"
"""
config = get_config()
params: dict[str, Any] = {
"action": "open_stage",
"prefabPath": path,
"mode": mode,
}
try:
result = run_command("manage_prefabs", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Opened prefab: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@prefab.command("close")
@click.option(
"--save", "-s",
is_flag=True,
help="Save the prefab before closing."
)
def close_stage(save: bool):
"""Close the current prefab stage.
\b
Examples:
unity-mcp prefab close
unity-mcp prefab close --save
"""
config = get_config()
params: dict[str, Any] = {
"action": "close_stage",
}
if save:
params["saveBeforeClose"] = True
try:
result = run_command("manage_prefabs", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success("Closed prefab stage")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@prefab.command("save")
def save_stage():
"""Save the currently open prefab stage.
\b
Examples:
unity-mcp prefab save
"""
config = get_config()
try:
result = run_command("manage_prefabs", {
"action": "save_open_stage"}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success("Saved prefab")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@prefab.command("create")
@click.argument("target")
@click.argument("path")
@click.option(
"--overwrite",
is_flag=True,
help="Overwrite existing prefab at path."
)
@click.option(
"--include-inactive",
is_flag=True,
help="Include inactive objects when finding target."
)
def create(target: str, path: str, overwrite: bool, include_inactive: bool):
"""Create a prefab from a scene GameObject.
\b
Examples:
unity-mcp prefab create "Player" "Assets/Prefabs/Player.prefab"
unity-mcp prefab create "Enemy" "Assets/Prefabs/Enemy.prefab" --overwrite
"""
config = get_config()
params: dict[str, Any] = {
"action": "create_from_gameobject",
"target": target,
"prefabPath": path,
}
if overwrite:
params["allowOverwrite"] = True
if include_inactive:
params["searchInactive"] = True
try:
result = run_command("manage_prefabs", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Created prefab: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,255 @@
"""Scene CLI commands."""
import sys
import click
from typing import Optional, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def scene():
"""Scene operations - hierarchy, load, save, create scenes."""
pass
@scene.command("hierarchy")
@click.option(
"--parent",
default=None,
help="Parent GameObject to list children of (name, path, or instance ID)."
)
@click.option(
"--max-depth", "-d",
default=None,
type=int,
help="Maximum depth to traverse."
)
@click.option(
"--include-transform", "-t",
is_flag=True,
help="Include transform data for each node."
)
@click.option(
"--limit", "-l",
default=50,
type=int,
help="Maximum nodes to return."
)
@click.option(
"--cursor", "-c",
default=0,
type=int,
help="Pagination cursor."
)
def hierarchy(
parent: Optional[str],
max_depth: Optional[int],
include_transform: bool,
limit: int,
cursor: int,
):
"""Get the scene hierarchy.
\b
Examples:
unity-mcp scene hierarchy
unity-mcp scene hierarchy --max-depth 3
unity-mcp scene hierarchy --parent "Canvas" --include-transform
unity-mcp scene hierarchy --format json
"""
config = get_config()
params: dict[str, Any] = {
"action": "get_hierarchy",
"pageSize": limit,
"cursor": cursor,
}
if parent:
params["parent"] = parent
if max_depth is not None:
params["maxDepth"] = max_depth
if include_transform:
params["includeTransform"] = True
try:
result = run_command("manage_scene", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@scene.command("active")
def active():
"""Get information about the active scene."""
config = get_config()
try:
result = run_command("manage_scene", {"action": "get_active"}, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@scene.command("load")
@click.argument("scene")
@click.option(
"--by-index", "-i",
is_flag=True,
help="Load by build index instead of path/name."
)
def load(scene: str, by_index: bool):
"""Load a scene.
\b
Examples:
unity-mcp scene load "Assets/Scenes/Main.unity"
unity-mcp scene load "MainScene"
unity-mcp scene load 0 --by-index
"""
config = get_config()
params: dict[str, Any] = {"action": "load"}
if by_index:
try:
params["buildIndex"] = int(scene)
except ValueError:
print_error(f"Invalid build index: {scene}")
sys.exit(1)
else:
if scene.endswith(".unity"):
params["path"] = scene
else:
params["name"] = scene
try:
result = run_command("manage_scene", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Loaded scene: {scene}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@scene.command("save")
@click.option(
"--path",
default=None,
help="Path to save the scene to (for new scenes)."
)
def save(path: Optional[str]):
"""Save the current scene.
\b
Examples:
unity-mcp scene save
unity-mcp scene save --path "Assets/Scenes/NewScene.unity"
"""
config = get_config()
params: dict[str, Any] = {"action": "save"}
if path:
params["path"] = path
try:
result = run_command("manage_scene", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success("Scene saved")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@scene.command("create")
@click.argument("name")
@click.option(
"--path",
default=None,
help="Path to create the scene at."
)
def create(name: str, path: Optional[str]):
"""Create a new scene.
\b
Examples:
unity-mcp scene create "NewLevel"
unity-mcp scene create "TestScene" --path "Assets/Scenes/Test"
"""
config = get_config()
params: dict[str, Any] = {
"action": "create",
"name": name,
}
if path:
params["path"] = path
try:
result = run_command("manage_scene", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Created scene: {name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@scene.command("build-settings")
def build_settings():
"""Get scenes in build settings."""
config = get_config()
try:
result = run_command(
"manage_scene", {"action": "get_build_settings"}, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@scene.command("screenshot")
@click.option(
"--filename", "-f",
default=None,
help="Output filename (default: timestamp)."
)
@click.option(
"--supersize", "-s",
default=1,
type=int,
help="Supersize multiplier (1-4)."
)
def screenshot(filename: Optional[str], supersize: int):
"""Capture a screenshot of the scene.
\b
Examples:
unity-mcp scene screenshot
unity-mcp scene screenshot --filename "level_preview"
unity-mcp scene screenshot --supersize 2
"""
config = get_config()
params: dict[str, Any] = {"action": "screenshot"}
if filename:
params["fileName"] = filename
if supersize > 1:
params["superSize"] = supersize
try:
result = run_command("manage_scene", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success("Screenshot captured")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,240 @@
"""Script CLI commands."""
import sys
import json
import click
from typing import Optional, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def script():
"""Script operations - create, read, edit C# scripts."""
pass
@script.command("create")
@click.argument("name")
@click.option(
"--path", "-p",
default="Assets/Scripts",
help="Directory to create the script in."
)
@click.option(
"--type", "-t",
"script_type",
type=click.Choice(["MonoBehaviour", "ScriptableObject",
"Editor", "EditorWindow", "Plain"]),
default="MonoBehaviour",
help="Type of script to create."
)
@click.option(
"--namespace", "-n",
default=None,
help="Namespace for the script."
)
@click.option(
"--contents", "-c",
default=None,
help="Full script contents (overrides template)."
)
def create(name: str, path: str, script_type: str, namespace: Optional[str], contents: Optional[str]):
"""Create a new C# script.
\b
Examples:
unity-mcp script create "PlayerController"
unity-mcp script create "GameManager" --path "Assets/Scripts/Managers"
unity-mcp script create "EnemyData" --type ScriptableObject
unity-mcp script create "CustomEditor" --type Editor --namespace "MyGame.Editor"
"""
config = get_config()
params: dict[str, Any] = {
"action": "create",
"name": name,
"path": path,
"scriptType": script_type,
}
if namespace:
params["namespace"] = namespace
if contents:
params["contents"] = contents
try:
result = run_command("manage_script", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Created script: {name}.cs")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@script.command("read")
@click.argument("path")
@click.option(
"--start-line", "-s",
default=None,
type=int,
help="Starting line number (1-based)."
)
@click.option(
"--line-count", "-n",
default=None,
type=int,
help="Number of lines to read."
)
def read(path: str, start_line: Optional[int], line_count: Optional[int]):
"""Read a C# script file.
\b
Examples:
unity-mcp script read "Assets/Scripts/Player.cs"
unity-mcp script read "Assets/Scripts/Player.cs" --start-line 10 --line-count 20
"""
config = get_config()
parts = path.rsplit("/", 1)
filename = parts[-1]
directory = parts[0] if len(parts) > 1 else "Assets"
name = filename[:-3] if filename.endswith(".cs") else filename
params: dict[str, Any] = {
"action": "read",
"name": name,
"path": directory,
}
if start_line:
params["startLine"] = start_line
if line_count:
params["lineCount"] = line_count
try:
result = run_command("manage_script", params, config)
# For read, just output the content directly
if result.get("success") and result.get("data"):
data = result.get("data", {})
if isinstance(data, dict) and "contents" in data:
click.echo(data["contents"])
else:
click.echo(format_output(result, config.format))
else:
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@script.command("delete")
@click.argument("path")
@click.option(
"--force", "-f",
is_flag=True,
help="Skip confirmation prompt."
)
def delete(path: str, force: bool):
"""Delete a C# script.
\b
Examples:
unity-mcp script delete "Assets/Scripts/OldScript.cs"
"""
config = get_config()
if not force:
click.confirm(f"Delete script '{path}'?", abort=True)
parts = path.rsplit("/", 1)
filename = parts[-1]
directory = parts[0] if len(parts) > 1 else "Assets"
name = filename[:-3] if filename.endswith(".cs") else filename
params: dict[str, Any] = {
"action": "delete",
"name": name,
"path": directory,
}
try:
result = run_command("manage_script", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Deleted: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@script.command("edit")
@click.argument("path")
@click.option(
"--edits", "-e",
required=True,
help='Edits as JSON array of {startLine, startCol, endLine, endCol, newText}.'
)
def edit(path: str, edits: str):
"""Apply text edits to a script.
\b
Examples:
unity-mcp script edit "Assets/Scripts/Player.cs" --edits '[{"startLine": 10, "startCol": 1, "endLine": 10, "endCol": 20, "newText": "// Modified"}]'
"""
config = get_config()
try:
edits_list = json.loads(edits)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON for edits: {e}")
sys.exit(1)
params: dict[str, Any] = {
"uri": path,
"edits": edits_list,
}
try:
result = run_command("apply_text_edits", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Applied edits to: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@script.command("validate")
@click.argument("path")
@click.option(
"--level", "-l",
type=click.Choice(["basic", "standard"]),
default="basic",
help="Validation level."
)
def validate(path: str, level: str):
"""Validate a C# script for errors.
\b
Examples:
unity-mcp script validate "Assets/Scripts/Player.cs"
unity-mcp script validate "Assets/Scripts/Player.cs" --level standard
"""
config = get_config()
params: dict[str, Any] = {
"uri": path,
"level": level,
"include_diagnostics": True,
}
try:
result = run_command("validate_script", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,238 @@
"""Shader CLI commands for managing Unity shaders."""
import sys
import click
from typing import Optional
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def shader():
"""Shader operations - create, read, update, delete shaders."""
pass
@shader.command("read")
@click.argument("path")
def read_shader(path: str):
"""Read a shader file.
\\b
Examples:
unity-mcp shader read "Assets/Shaders/MyShader.shader"
"""
config = get_config()
# Extract name from path
import os
name = os.path.splitext(os.path.basename(path))[0]
directory = os.path.dirname(path)
try:
result = run_command("manage_shader", {
"action": "read",
"name": name,
"path": directory or "Assets/",
}, config)
# If successful, display the contents nicely
if result.get("success") and result.get("data", {}).get("contents"):
click.echo(result["data"]["contents"])
else:
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@shader.command("create")
@click.argument("name")
@click.option(
"--path", "-p",
default="Assets/Shaders",
help="Directory to create shader in."
)
@click.option(
"--contents", "-c",
default=None,
help="Shader code (reads from stdin if not provided)."
)
@click.option(
"--file", "-f",
"file_path",
default=None,
type=click.Path(exists=True),
help="Read shader code from file."
)
def create_shader(name: str, path: str, contents: Optional[str], file_path: Optional[str]):
"""Create a new shader.
\\b
Examples:
unity-mcp shader create "MyShader" --path "Assets/Shaders"
unity-mcp shader create "MyShader" --file local_shader.shader
echo "Shader code..." | unity-mcp shader create "MyShader"
"""
config = get_config()
# Get contents from file, option, or stdin
if file_path:
with open(file_path, 'r') as f:
shader_contents = f.read()
elif contents:
shader_contents = contents
else:
# Read from stdin if available
import sys
if not sys.stdin.isatty():
shader_contents = sys.stdin.read()
else:
# Provide default shader template
shader_contents = f'''Shader "Custom/{name}"
{{
Properties
{{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {{}}
}}
SubShader
{{
Tags {{ "RenderType"="Opaque" }}
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
fixed4 _Color;
struct Input
{{
float2 uv_MainTex;
}};
void surf (Input IN, inout SurfaceOutputStandard o)
{{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a;
}}
ENDCG
}}
FallBack "Diffuse"
}}
'''
try:
result = run_command("manage_shader", {
"action": "create",
"name": name,
"path": path,
"contents": shader_contents,
}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Created shader: {path}/{name}.shader")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@shader.command("update")
@click.argument("path")
@click.option(
"--contents", "-c",
default=None,
help="New shader code."
)
@click.option(
"--file", "-f",
"file_path",
default=None,
type=click.Path(exists=True),
help="Read shader code from file."
)
def update_shader(path: str, contents: Optional[str], file_path: Optional[str]):
"""Update an existing shader.
\\b
Examples:
unity-mcp shader update "Assets/Shaders/MyShader.shader" --file updated.shader
echo "New shader code" | unity-mcp shader update "Assets/Shaders/MyShader.shader"
"""
config = get_config()
import os
name = os.path.splitext(os.path.basename(path))[0]
directory = os.path.dirname(path)
# Get contents from file, option, or stdin
if file_path:
with open(file_path, 'r') as f:
shader_contents = f.read()
elif contents:
shader_contents = contents
else:
import sys
if not sys.stdin.isatty():
shader_contents = sys.stdin.read()
else:
print_error(
"No shader contents provided. Use --contents, --file, or pipe via stdin.")
sys.exit(1)
try:
result = run_command("manage_shader", {
"action": "update",
"name": name,
"path": directory or "Assets/",
"contents": shader_contents,
}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Updated shader: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@shader.command("delete")
@click.argument("path")
@click.option(
"--force", "-f",
is_flag=True,
help="Skip confirmation prompt."
)
def delete_shader(path: str, force: bool):
"""Delete a shader.
\\b
Examples:
unity-mcp shader delete "Assets/Shaders/OldShader.shader"
unity-mcp shader delete "Assets/Shaders/OldShader.shader" --force
"""
config = get_config()
if not force:
click.confirm(f"Delete shader '{path}'?", abort=True)
import os
name = os.path.splitext(os.path.basename(path))[0]
directory = os.path.dirname(path)
try:
result = run_command("manage_shader", {
"action": "delete",
"name": name,
"path": directory or "Assets/",
}, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Deleted shader: {path}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,263 @@
"""UI CLI commands - placeholder for future implementation."""
import sys
import click
from typing import Optional, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def ui():
"""UI operations - create and modify UI elements."""
pass
@ui.command("create-canvas")
@click.argument("name")
@click.option(
"--render-mode",
type=click.Choice(
["ScreenSpaceOverlay", "ScreenSpaceCamera", "WorldSpace"]),
default="ScreenSpaceOverlay",
help="Canvas render mode."
)
def create_canvas(name: str, render_mode: str):
"""Create a new Canvas.
\b
Examples:
unity-mcp ui create-canvas "MainUI"
unity-mcp ui create-canvas "WorldUI" --render-mode WorldSpace
"""
config = get_config()
try:
# Step 1: Create empty GameObject
result = run_command("manage_gameobject", {
"action": "create",
"name": name,
}, config)
if not (result.get("success") or result.get("data") or result.get("result")):
click.echo(format_output(result, config.format))
return
# Step 2: Add Canvas components
for component in ["Canvas", "CanvasScaler", "GraphicRaycaster"]:
run_command("manage_components", {
"action": "add",
"target": name,
"componentType": component,
}, config)
# Step 3: Set render mode
render_mode_value = {"ScreenSpaceOverlay": 0,
"ScreenSpaceCamera": 1, "WorldSpace": 2}.get(render_mode, 0)
run_command("manage_components", {
"action": "set_property",
"target": name,
"componentType": "Canvas",
"property": "renderMode",
"value": render_mode_value,
}, config)
click.echo(format_output(result, config.format))
print_success(f"Created Canvas: {name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@ui.command("create-text")
@click.argument("name")
@click.option(
"--parent", "-p",
required=True,
help="Parent Canvas or UI element."
)
@click.option(
"--text", "-t",
default="New Text",
help="Initial text content."
)
@click.option(
"--position",
nargs=2,
type=float,
default=(0, 0),
help="Anchored position X Y."
)
def create_text(name: str, parent: str, text: str, position: tuple):
"""Create a UI Text element (TextMeshPro).
\b
Examples:
unity-mcp ui create-text "TitleText" --parent "MainUI" --text "Hello World"
"""
config = get_config()
try:
# Step 1: Create empty GameObject with parent
result = run_command("manage_gameobject", {
"action": "create",
"name": name,
"parent": parent,
"position": list(position),
}, config)
if not (result.get("success") or result.get("data") or result.get("result")):
click.echo(format_output(result, config.format))
return
# Step 2: Add RectTransform and TextMeshProUGUI
run_command("manage_components", {
"action": "add",
"target": name,
"componentType": "TextMeshProUGUI",
}, config)
# Step 3: Set text content
run_command("manage_components", {
"action": "set_property",
"target": name,
"componentType": "TextMeshProUGUI",
"property": "text",
"value": text,
}, config)
click.echo(format_output(result, config.format))
print_success(f"Created Text: {name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@ui.command("create-button")
@click.argument("name")
@click.option(
"--parent", "-p",
required=True,
help="Parent Canvas or UI element."
)
@click.option(
"--text", "-t",
default="Button",
help="Button label text."
)
def create_button(name: str, parent: str, text: str): # text current placeholder
"""Create a UI Button.
\b
Examples:
unity-mcp ui create-button "StartButton" --parent "MainUI" --text "Start Game"
"""
config = get_config()
try:
# Step 1: Create empty GameObject with parent
result = run_command("manage_gameobject", {
"action": "create",
"name": name,
"parent": parent,
}, config)
if not (result.get("success") or result.get("data") or result.get("result")):
click.echo(format_output(result, config.format))
return
# Step 2: Add Button and Image components
for component in ["Image", "Button"]:
run_command("manage_components", {
"action": "add",
"target": name,
"componentType": component,
}, config)
# Step 3: Create child label GameObject
label_name = f"{name}_Label"
label_result = run_command("manage_gameobject", {
"action": "create",
"name": label_name,
"parent": name,
}, config)
# Step 4: Add TextMeshProUGUI to label and set text
run_command("manage_components", {
"action": "add",
"target": label_name,
"componentType": "TextMeshProUGUI",
}, config)
run_command("manage_components", {
"action": "set_property",
"target": label_name,
"componentType": "TextMeshProUGUI",
"property": "text",
"value": text,
}, config)
click.echo(format_output(result, config.format))
print_success(f"Created Button: {name} (with label '{text}')")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@ui.command("create-image")
@click.argument("name")
@click.option(
"--parent", "-p",
required=True,
help="Parent Canvas or UI element."
)
@click.option(
"--sprite", "-s",
default=None,
help="Sprite asset path."
)
def create_image(name: str, parent: str, sprite: Optional[str]):
"""Create a UI Image.
\b
Examples:
unity-mcp ui create-image "Background" --parent "MainUI"
unity-mcp ui create-image "Icon" --parent "MainUI" --sprite "Assets/Sprites/icon.png"
"""
config = get_config()
try:
# Step 1: Create empty GameObject with parent
result = run_command("manage_gameobject", {
"action": "create",
"name": name,
"parent": parent,
}, config)
if not (result.get("success") or result.get("data") or result.get("result")):
click.echo(format_output(result, config.format))
return
# Step 2: Add Image component
run_command("manage_components", {
"action": "add",
"target": name,
"componentType": "Image",
}, config)
# Step 3: Set sprite if provided
if sprite:
run_command("manage_components", {
"action": "set_property",
"target": name,
"componentType": "Image",
"property": "sprite",
"value": sprite,
}, config)
click.echo(format_output(result, config.format))
print_success(f"Created Image: {name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

View File

@ -0,0 +1,439 @@
"""VFX CLI commands for managing Unity visual effects."""
import sys
import json
import click
from typing import Optional, Tuple, Any
from cli.utils.config import get_config
from cli.utils.output import format_output, print_error, print_success
from cli.utils.connection import run_command, UnityConnectionError
@click.group()
def vfx():
"""VFX operations - particle systems, line renderers, trails."""
pass
# =============================================================================
# Particle System Commands
# =============================================================================
@vfx.group()
def particle():
"""Particle system operations."""
pass
@particle.command("info")
@click.argument("target")
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def particle_info(target: str, search_method: Optional[str]):
"""Get particle system info.
\\b
Examples:
unity-mcp vfx particle info "Fire"
unity-mcp vfx particle info "-12345" --search-method by_id
"""
config = get_config()
params: dict[str, Any] = {"action": "particle_get_info", "target": target}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@particle.command("play")
@click.argument("target")
@click.option("--with-children", is_flag=True, help="Also play child particle systems.")
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def particle_play(target: str, with_children: bool, search_method: Optional[str]):
"""Play a particle system.
\\b
Examples:
unity-mcp vfx particle play "Fire"
unity-mcp vfx particle play "Effects" --with-children
"""
config = get_config()
params: dict[str, Any] = {"action": "particle_play", "target": target}
if with_children:
params["withChildren"] = True
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Playing particle system: {target}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@particle.command("stop")
@click.argument("target")
@click.option("--with-children", is_flag=True, help="Also stop child particle systems.")
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def particle_stop(target: str, with_children: bool, search_method: Optional[str]):
"""Stop a particle system."""
config = get_config()
params: dict[str, Any] = {"action": "particle_stop", "target": target}
if with_children:
params["withChildren"] = True
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Stopped particle system: {target}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@particle.command("pause")
@click.argument("target")
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def particle_pause(target: str, search_method: Optional[str]):
"""Pause a particle system."""
config = get_config()
params: dict[str, Any] = {"action": "particle_pause", "target": target}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@particle.command("restart")
@click.argument("target")
@click.option("--with-children", is_flag=True)
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def particle_restart(target: str, with_children: bool, search_method: Optional[str]):
"""Restart a particle system."""
config = get_config()
params: dict[str, Any] = {"action": "particle_restart", "target": target}
if with_children:
params["withChildren"] = True
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@particle.command("clear")
@click.argument("target")
@click.option("--with-children", is_flag=True)
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def particle_clear(target: str, with_children: bool, search_method: Optional[str]):
"""Clear all particles from a particle system."""
config = get_config()
params: dict[str, Any] = {"action": "particle_clear", "target": target}
if with_children:
params["withChildren"] = True
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
# =============================================================================
# Line Renderer Commands
# =============================================================================
@vfx.group()
def line():
"""Line renderer operations."""
pass
@line.command("info")
@click.argument("target")
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def line_info(target: str, search_method: Optional[str]):
"""Get line renderer info.
\\b
Examples:
unity-mcp vfx line info "LaserBeam"
"""
config = get_config()
params: dict[str, Any] = {"action": "line_get_info", "target": target}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@line.command("set-positions")
@click.argument("target")
@click.option("--positions", "-p", required=True, help='Positions as JSON array: [[0,0,0], [1,1,1], [2,0,0]]')
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def line_set_positions(target: str, positions: str, search_method: Optional[str]):
"""Set all positions on a line renderer.
\\b
Examples:
unity-mcp vfx line set-positions "Line" --positions "[[0,0,0], [5,2,0], [10,0,0]]"
"""
config = get_config()
try:
positions_list = json.loads(positions)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON for positions: {e}")
sys.exit(1)
params: dict[str, Any] = {
"action": "line_set_positions",
"target": target,
"positions": positions_list,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@line.command("create-line")
@click.argument("target")
@click.option("--start", nargs=3, type=float, required=True, help="Start point X Y Z")
@click.option("--end", nargs=3, type=float, required=True, help="End point X Y Z")
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def line_create_line(target: str, start: Tuple[float, float, float], end: Tuple[float, float, float], search_method: Optional[str]):
"""Create a simple line between two points.
\\b
Examples:
unity-mcp vfx line create-line "MyLine" --start 0 0 0 --end 10 5 0
"""
config = get_config()
params: dict[str, Any] = {
"action": "line_create_line",
"target": target,
"start": list(start),
"end": list(end),
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@line.command("create-circle")
@click.argument("target")
@click.option("--center", nargs=3, type=float, default=(0, 0, 0), help="Center point X Y Z")
@click.option("--radius", type=float, required=True, help="Circle radius")
@click.option("--segments", type=int, default=32, help="Number of segments")
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def line_create_circle(target: str, center: Tuple[float, float, float], radius: float, segments: int, search_method: Optional[str]):
"""Create a circle shape.
\\b
Examples:
unity-mcp vfx line create-circle "Circle" --radius 5 --segments 64
unity-mcp vfx line create-circle "Ring" --center 0 2 0 --radius 3
"""
config = get_config()
params: dict[str, Any] = {
"action": "line_create_circle",
"target": target,
"center": list(center),
"radius": radius,
"segments": segments,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@line.command("clear")
@click.argument("target")
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def line_clear(target: str, search_method: Optional[str]):
"""Clear all positions from a line renderer."""
config = get_config()
params: dict[str, Any] = {"action": "line_clear", "target": target}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
# =============================================================================
# Trail Renderer Commands
# =============================================================================
@vfx.group()
def trail():
"""Trail renderer operations."""
pass
@trail.command("info")
@click.argument("target")
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def trail_info(target: str, search_method: Optional[str]):
"""Get trail renderer info."""
config = get_config()
params: dict[str, Any] = {"action": "trail_get_info", "target": target}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@trail.command("set-time")
@click.argument("target")
@click.argument("duration", type=float)
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def trail_set_time(target: str, duration: float, search_method: Optional[str]):
"""Set trail duration.
\\b
Examples:
unity-mcp vfx trail set-time "PlayerTrail" 2.0
"""
config = get_config()
params: dict[str, Any] = {
"action": "trail_set_time",
"target": target,
"time": duration,
}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
@trail.command("clear")
@click.argument("target")
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def trail_clear(target: str, search_method: Optional[str]):
"""Clear a trail renderer."""
config = get_config()
params: dict[str, Any] = {"action": "trail_clear", "target": target}
if search_method:
params["searchMethod"] = search_method
try:
result = run_command("manage_vfx", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
# =============================================================================
# Raw Command (escape hatch for all VFX actions)
# =============================================================================
@vfx.command("raw")
@click.argument("action")
@click.argument("target", required=False)
@click.option("--params", "-p", default="{}", help="Additional parameters as JSON.")
@click.option("--search-method", type=click.Choice(["by_name", "by_path", "by_id", "by_tag"]), default=None)
def vfx_raw(action: str, target: Optional[str], params: str, search_method: Optional[str]):
"""Execute any VFX action directly.
For advanced users who need access to all 60+ VFX actions.
\\b
Actions include:
particle_*: particle_set_main, particle_set_emission, particle_set_shape, ...
vfx_*: vfx_set_float, vfx_send_event, vfx_play, ...
line_*: line_create_arc, line_create_bezier, ...
trail_*: trail_set_width, trail_set_color, ...
\\b
Examples:
unity-mcp vfx raw particle_set_main "Fire" --params '{"duration": 5, "looping": true}'
unity-mcp vfx raw line_create_arc "Arc" --params '{"radius": 3, "startAngle": 0, "endAngle": 180}'
unity-mcp vfx raw vfx_send_event "Explosion" --params '{"eventName": "OnSpawn"}'
"""
config = get_config()
try:
extra_params = json.loads(params)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON for params: {e}")
sys.exit(1)
if not isinstance(extra_params, dict):
print_error("Invalid JSON for params: expected an object")
sys.exit(1)
request_params: dict[str, Any] = {"action": action}
if target:
request_params["target"] = target
if search_method:
request_params["searchMethod"] = search_method
# Merge extra params
request_params.update(extra_params)
try:
result = run_command("manage_vfx", request_params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)

248
Server/src/cli/main.py Normal file
View File

@ -0,0 +1,248 @@
"""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.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)
@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.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"),
]
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()

View File

@ -0,0 +1,31 @@
"""CLI utility modules."""
from cli.utils.config import CLIConfig, get_config, set_config
from cli.utils.connection import (
run_command,
run_check_connection,
run_list_instances,
UnityConnectionError,
)
from cli.utils.output import (
format_output,
print_success,
print_error,
print_warning,
print_info,
)
__all__ = [
"CLIConfig",
"UnityConnectionError",
"format_output",
"get_config",
"print_error",
"print_info",
"print_success",
"print_warning",
"run_check_connection",
"run_command",
"run_list_instances",
"set_config",
]

View File

@ -0,0 +1,58 @@
"""CLI Configuration utilities."""
import os
from dataclasses import dataclass
from typing import Optional
@dataclass
class CLIConfig:
"""Configuration for CLI connection to Unity."""
host: str = "127.0.0.1"
port: int = 8080
timeout: int = 30
format: str = "text" # text, json, table
unity_instance: Optional[str] = None
@classmethod
def from_env(cls) -> "CLIConfig":
port_raw = os.environ.get("UNITY_MCP_HTTP_PORT", "8080")
try:
port = int(port_raw)
except (ValueError, TypeError):
raise ValueError(
f"Invalid UNITY_MCP_HTTP_PORT value: {port_raw!r}")
timeout_raw = os.environ.get("UNITY_MCP_TIMEOUT", "30")
try:
timeout = int(timeout_raw)
except (ValueError, TypeError):
raise ValueError(
f"Invalid UNITY_MCP_TIMEOUT value: {timeout_raw!r}")
return cls(
host=os.environ.get("UNITY_MCP_HOST", "127.0.0.1"),
port=port,
timeout=timeout,
format=os.environ.get("UNITY_MCP_FORMAT", "text"),
unity_instance=os.environ.get("UNITY_MCP_INSTANCE"),
)
# Global config instance
_config: Optional[CLIConfig] = None
def get_config() -> CLIConfig:
"""Get the current CLI configuration."""
global _config
if _config is None:
_config = CLIConfig.from_env()
return _config
def set_config(config: CLIConfig) -> None:
"""Set the CLI configuration."""
global _config
_config = config

View File

@ -0,0 +1,191 @@
"""Connection utilities for CLI to communicate with Unity via MCP server."""
import asyncio
import json
import sys
from typing import Any, Dict, Optional
import httpx
from cli.utils.config import get_config, CLIConfig
class UnityConnectionError(Exception):
"""Raised when connection to Unity fails."""
pass
def warn_if_remote_host(config: CLIConfig) -> None:
"""Warn user if connecting to a non-localhost server.
This is a security measure to alert users that connecting to remote
servers exposes Unity control to potential network attacks.
Args:
config: CLI configuration with host setting
"""
import click
local_hosts = ("127.0.0.1", "localhost", "::1", "0.0.0.0")
if config.host.lower() not in local_hosts:
click.echo(
"⚠️ Security Warning: Connecting to non-localhost server.\n"
" The MCP CLI has no authentication. Anyone on the network could\n"
" intercept commands or send unauthorized commands to Unity.\n"
" Only proceed if you trust this network.\n",
err=True
)
async def send_command(
command_type: str,
params: Dict[str, Any],
config: Optional[CLIConfig] = None,
timeout: Optional[int] = None,
) -> Dict[str, Any]:
"""Send a command to Unity via the MCP HTTP server.
Args:
command_type: The command type (e.g., 'manage_gameobject', 'manage_scene')
params: Command parameters
config: Optional CLI configuration
timeout: Optional timeout override
Returns:
Response dict from Unity
Raises:
UnityConnectionError: If connection fails
"""
cfg = config or get_config()
url = f"http://{cfg.host}:{cfg.port}/api/command"
payload = {
"type": command_type,
"params": params,
}
if cfg.unity_instance:
payload["unity_instance"] = cfg.unity_instance
try:
async with httpx.AsyncClient() as client:
response = await client.post(
url,
json=payload,
timeout=timeout or 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 {timeout or 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_command(
command_type: str,
params: Dict[str, Any],
config: Optional[CLIConfig] = None,
timeout: Optional[int] = None,
) -> Dict[str, Any]:
"""Synchronous wrapper for send_command.
Args:
command_type: The command type
params: Command parameters
config: Optional CLI configuration
timeout: Optional timeout override
Returns:
Response dict from Unity
"""
return asyncio.run(send_command(command_type, params, config, timeout))
async def check_connection(config: Optional[CLIConfig] = None) -> bool:
"""Check if we can connect to the Unity MCP server.
Args:
config: Optional CLI configuration
Returns:
True if connection successful, False otherwise
"""
cfg = config or get_config()
url = f"http://{cfg.host}:{cfg.port}/health"
try:
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=5)
return response.status_code == 200
except Exception:
return False
def run_check_connection(config: Optional[CLIConfig] = None) -> bool:
"""Synchronous wrapper for check_connection."""
return asyncio.run(check_connection(config))
async def list_unity_instances(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
"""List available Unity instances.
Args:
config: Optional CLI configuration
Returns:
Dict with list of Unity instances
"""
cfg = config or get_config()
# Try the new /api/instances endpoint first, fall back to /plugin/sessions
urls_to_try = [
f"http://{cfg.host}:{cfg.port}/api/instances",
f"http://{cfg.host}:{cfg.port}/plugin/sessions",
]
async with httpx.AsyncClient() as client:
for url in urls_to_try:
try:
response = await client.get(url, timeout=10)
if response.status_code == 200:
data = response.json()
# Normalize response format
if "instances" in data:
return data
elif "sessions" in data:
# Convert sessions format to instances format
instances = []
for session_id, details in data["sessions"].items():
instances.append({
"session_id": session_id,
"project": details.get("project", "Unknown"),
"hash": details.get("hash", ""),
"unity_version": details.get("unity_version", "Unknown"),
"connected_at": details.get("connected_at", ""),
})
return {"success": True, "instances": instances}
except Exception:
continue
raise UnityConnectionError(
"Failed to list Unity instances: No working endpoint found")
def run_list_instances(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
"""Synchronous wrapper for list_unity_instances."""
return asyncio.run(list_unity_instances(config))

View File

@ -0,0 +1,195 @@
"""Output formatting utilities for CLI."""
import json
from typing import Any
import click
def format_output(data: Any, format_type: str = "text") -> str:
"""Format output based on requested format type.
Args:
data: Data to format
format_type: One of 'text', 'json', 'table'
Returns:
Formatted string
"""
if format_type == "json":
return format_as_json(data)
elif format_type == "table":
return format_as_table(data)
else:
return format_as_text(data)
def format_as_json(data: Any) -> str:
"""Format data as pretty-printed JSON."""
try:
return json.dumps(data, indent=2, default=str)
except (TypeError, ValueError) as e:
return json.dumps({"error": f"JSON serialization failed: {e}", "raw": str(data)})
def format_as_text(data: Any, indent: int = 0) -> str:
"""Format data as human-readable text."""
prefix = " " * indent
if data is None:
return f"{prefix}(none)"
if isinstance(data, dict):
# Check for error response
if "success" in data and not data.get("success"):
error = data.get("error") or data.get("message") or "Unknown error"
return f"{prefix}❌ Error: {error}"
# Check for success response with data
if "success" in data and data.get("success"):
result = data.get("data") or data.get("result") or data
if result != data:
return format_as_text(result, indent)
lines = []
for key, value in data.items():
if key in ("success", "error", "message") and "success" in data:
continue # Skip meta fields
if isinstance(value, dict):
lines.append(f"{prefix}{key}:")
lines.append(format_as_text(value, indent + 1))
elif isinstance(value, list):
lines.append(f"{prefix}{key}: [{len(value)} items]")
if len(value) <= 10:
for i, item in enumerate(value):
lines.append(
f"{prefix} [{i}] {_format_list_item(item)}")
else:
for i, item in enumerate(value[:5]):
lines.append(
f"{prefix} [{i}] {_format_list_item(item)}")
lines.append(f"{prefix} ... ({len(value) - 10} more)")
for i, item in enumerate(value[-5:], len(value) - 5):
lines.append(
f"{prefix} [{i}] {_format_list_item(item)}")
else:
lines.append(f"{prefix}{key}: {value}")
return "\n".join(lines)
if isinstance(data, list):
if not data:
return f"{prefix}(empty list)"
lines = [f"{prefix}[{len(data)} items]"]
for i, item in enumerate(data[:20]):
lines.append(f"{prefix} [{i}] {_format_list_item(item)}")
if len(data) > 20:
lines.append(f"{prefix} ... ({len(data) - 20} more)")
return "\n".join(lines)
return f"{prefix}{data}"
def _format_list_item(item: Any) -> str:
"""Format a single list item."""
if isinstance(item, dict):
# Try to find a name/id field for display
name = item.get("name") or item.get(
"Name") or item.get("id") or item.get("Id")
if name:
extra = ""
if "instanceID" in item:
extra = f" (ID: {item['instanceID']})"
elif "path" in item:
extra = f" ({item['path']})"
return f"{name}{extra}"
# Fallback to compact representation
return json.dumps(item, default=str)[:80]
return str(item)[:80]
def format_as_table(data: Any) -> str:
"""Format data as an ASCII table."""
if isinstance(data, dict):
# Check for success response with data
if "success" in data and data.get("success"):
result = data.get("data") or data.get(
"result") or data.get("items")
if isinstance(result, list):
return _build_table(result)
# Single dict as key-value table
rows = [[str(k), str(v)[:60]] for k, v in data.items()]
return _build_table(rows, headers=["Key", "Value"])
if isinstance(data, list):
return _build_table(data)
return str(data)
def _build_table(data: list[Any], headers: list[str] | None = None) -> str:
"""Build an ASCII table from list data."""
if not data:
return "(no data)"
# Convert list of dicts to rows
if isinstance(data[0], dict):
if headers is None:
headers = list(data[0].keys())
rows = [[str(item.get(h, ""))[:40] for h in headers] for item in data]
elif isinstance(data[0], (list, tuple)):
rows = [[str(cell)[:40] for cell in row] for row in data]
if headers is None:
headers = [f"Col{i}" for i in range(len(data[0]))]
else:
rows = [[str(item)[:60]] for item in data]
headers = headers or ["Value"]
# Calculate column widths
col_widths = [len(h) for h in headers]
for row in rows:
for i, cell in enumerate(row):
if i < len(col_widths):
col_widths[i] = max(col_widths[i], len(cell))
# Build table
lines = []
# Header
header_line = " | ".join(
h.ljust(col_widths[i]) for i, h in enumerate(headers))
lines.append(header_line)
lines.append("-+-".join("-" * w for w in col_widths))
# Rows
for row in rows[:50]: # Limit rows
row_line = " | ".join(
(row[i] if i < len(row) else "").ljust(col_widths[i])
for i in range(len(headers))
)
lines.append(row_line)
if len(rows) > 50:
lines.append(f"... ({len(rows) - 50} more rows)")
return "\n".join(lines)
def print_success(message: str) -> None:
"""Print a success message."""
click.echo(f"{message}")
def print_error(message: str) -> None:
"""Print an error message to stderr."""
click.echo(f"{message}", err=True)
def print_warning(message: str) -> None:
"""Print a warning message."""
click.echo(f"⚠️ {message}")
def print_info(message: str) -> None:
"""Print an info message."""
click.echo(f" {message}")

View File

@ -1,3 +1,18 @@
from starlette.requests import Request
from transport.unity_instance_middleware import (
UnityInstanceMiddleware,
get_unity_instance_middleware
)
from transport.legacy.unity_connection import get_unity_connection_pool, UnityConnectionPool
from services.tools import register_all_tools
from core.telemetry import record_milestone, record_telemetry, MilestoneType, RecordType, get_package_version
from services.resources import register_all_resources
from transport.plugin_registry import PluginRegistry
from transport.plugin_hub import PluginHub
from services.custom_tool_service import CustomToolService
from core.config import config
from starlette.routing import WebSocketRoute
from starlette.responses import JSONResponse
import argparse
import asyncio
import logging
@ -50,22 +65,7 @@ class WindowsSafeRotatingFileHandler(RotatingFileHandler):
# On Windows, another process may have the log file open.
# Skip rotation this time - we'll try again on the next rollover.
pass
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import WebSocketRoute
from core.config import config
from services.custom_tool_service import CustomToolService
from transport.plugin_hub import PluginHub
from transport.plugin_registry import PluginRegistry
from services.resources import register_all_resources
from core.telemetry import record_milestone, record_telemetry, MilestoneType, RecordType, get_package_version
from services.tools import register_all_tools
from transport.legacy.unity_connection import get_unity_connection_pool, UnityConnectionPool
from transport.unity_instance_middleware import (
UnityInstanceMiddleware,
get_unity_instance_middleware
)
# Configure logging using settings from config
logging.basicConfig(
@ -325,6 +325,75 @@ def create_mcp_server(project_scoped_tools: bool) -> FastMCP:
"message": "MCP for Unity server is running"
})
@mcp.custom_route("/api/command", methods=["POST"])
async def cli_command_route(request: Request) -> JSONResponse:
"""REST endpoint for CLI commands to Unity."""
try:
body = await request.json()
command_type = body.get("type")
params = body.get("params", {})
unity_instance = body.get("unity_instance")
if not command_type:
return JSONResponse({"success": False, "error": "Missing 'type' field"}, status_code=400)
# Get available sessions
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)
# Find target session
session_id = None
if unity_instance:
# Try to match by hash or project name
for sid, details in sessions.sessions.items():
if details.hash == unity_instance or details.project == unity_instance:
session_id = sid
break
# If a specific unity_instance was requested but not found, return an error
if not session_id:
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_id = next(iter(sessions.sessions.keys()))
# Send command to Unity
result = await PluginHub.send_command(session_id, command_type, params)
return JSONResponse(result)
except Exception as e:
logger.error(f"CLI command error: {e}")
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
@mcp.custom_route("/api/instances", methods=["GET"])
async def cli_instances_route(_: Request) -> JSONResponse:
"""REST endpoint to list connected Unity instances."""
try:
sessions = await PluginHub.get_sessions()
instances = []
for session_id, details in sessions.sessions.items():
instances.append({
"session_id": session_id,
"project": details.project,
"hash": details.hash,
"unity_version": details.unity_version,
"connected_at": details.connected_at,
})
return JSONResponse({"success": True, "instances": instances})
except Exception as e:
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
@mcp.custom_route("/plugin/sessions", methods=["GET"])
async def plugin_sessions_route(_: Request) -> JSONResponse:
data = await PluginHub.get_sessions()
@ -464,8 +533,17 @@ Examples:
# Allow individual host/port to override URL components
http_host = args.http_host or os.environ.get(
"UNITY_MCP_HTTP_HOST") or parsed_url.hostname or "localhost"
http_port = args.http_port or (int(os.environ.get("UNITY_MCP_HTTP_PORT")) if os.environ.get(
"UNITY_MCP_HTTP_PORT") else None) or parsed_url.port or 8080
# Safely parse optional environment port (may be None or non-numeric)
_env_port_str = os.environ.get("UNITY_MCP_HTTP_PORT")
try:
_env_port = int(_env_port_str) if _env_port_str is not None else None
except ValueError:
logger.warning(
"Invalid UNITY_MCP_HTTP_PORT value '%s', ignoring", _env_port_str)
_env_port = None
http_port = args.http_port or _env_port or parsed_url.port or 8080
os.environ["UNITY_MCP_HTTP_HOST"] = http_host
os.environ["UNITY_MCP_HTTP_PORT"] = str(http_port)
@ -502,8 +580,7 @@ Examples:
parsed_url = urlparse(http_url)
host = args.http_host or os.environ.get(
"UNITY_MCP_HTTP_HOST") or parsed_url.hostname or "localhost"
port = args.http_port or (int(os.environ.get("UNITY_MCP_HTTP_PORT")) if os.environ.get(
"UNITY_MCP_HTTP_PORT") else None) or parsed_url.port or 8080
port = args.http_port or _env_port or parsed_url.port or 8080
logger.info(f"Starting FastMCP with HTTP transport on {host}:{port}")
mcp.run(transport=transport, host=host, port=port)
else:

View File

@ -20,6 +20,7 @@ from transport.legacy.unity_connection import (
)
from transport.plugin_hub import PluginHub
from services.tools import get_unity_instance_from_context
from services.registry import get_registered_tools
logger = logging.getLogger("mcp-for-unity-server")
@ -287,9 +288,19 @@ class CustomToolService:
def register_global_tools(self, tools: list[ToolDefinitionModel]) -> None:
if self._project_scoped_tools:
return
builtin_names = self._get_builtin_tool_names()
for tool in tools:
if tool.name in builtin_names:
logger.info(
"Skipping global custom tool registration for built-in tool '%s'",
tool.name,
)
continue
self._register_global_tool(tool)
def _get_builtin_tool_names(self) -> set[str]:
return {tool["name"] for tool in get_registered_tools()}
def _register_global_tool(self, definition: ToolDefinitionModel) -> None:
existing = self._global_tools.get(definition.name)
if existing:

View File

@ -246,7 +246,8 @@ class UnityConnection:
raise ValueError("MCP call missing command_type")
if params is None:
return MCPResponse(success=False, error="MCP call received with no parameters (client placeholder?)")
attempts = max(config.max_retries, 5) if max_attempts is None else max_attempts
attempts = max(config.max_retries,
5) if max_attempts is None else max_attempts
base_backoff = max(0.5, config.retry_delay)
def read_status_file(target_hash: str | None = None) -> dict | None:
@ -781,7 +782,8 @@ def send_command_with_retry(
# Commands that trigger compilation/reload shouldn't retry on disconnect
send_max_attempts = None if retry_on_reload else 0
response = conn.send_command(command_type, params, max_attempts=send_max_attempts)
response = conn.send_command(
command_type, params, max_attempts=send_max_attempts)
retries = 0
wait_started = None
reason = _extract_response_reason(response)

1225
Server/tests/test_cli.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -912,9 +912,10 @@ wheels = [
[[package]]
name = "mcpforunityserver"
version = "9.0.3"
version = "9.0.8"
source = { editable = "." }
dependencies = [
{ name = "click" },
{ name = "fastapi" },
{ name = "fastmcp" },
{ name = "httpx" },
@ -933,6 +934,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.1.0" },
{ name = "fastapi", specifier = ">=0.104.0" },
{ name = "fastmcp", specifier = "==2.14.1" },
{ name = "httpx", specifier = ">=0.27.2" },

393
docs/CLI_USAGE.md Normal file
View File

@ -0,0 +1,393 @@
# Unity MCP CLI Usage Guide
The Unity MCP CLI provides command-line access to control the Unity Editor through the Model Context Protocol. It currently only supports local HTTP.
Note: Some tools are still experimental and might fail under some circumstances. Please submit an issue to help us make it better.
## Installation
```bash
cd Server
pip install -e .
# Or with uv:
uv pip install -e .
```
## Quick Start
```bash
# Check connection
unity-mcp status
# List Unity instances
unity-mcp instance list
# Get scene hierarchy
unity-mcp scene hierarchy
# Find a GameObject
unity-mcp gameobject find "Player"
```
## Global Options
| Option | Env Variable | Description |
|--------|--------------|-------------|
| `-h, --host` | `UNITY_MCP_HOST` | Server host (default: 127.0.0.1) |
| `-p, --port` | `UNITY_MCP_HTTP_PORT` | Server port (default: 8080) |
| `-t, --timeout` | `UNITY_MCP_TIMEOUT` | Timeout in seconds (default: 30) |
| `-f, --format` | `UNITY_MCP_FORMAT` | Output format: text, json, table |
| `-i, --instance` | `UNITY_MCP_INSTANCE` | Target Unity instance |
## Command Reference
### Instance Management
```bash
# List connected Unity instances
unity-mcp instance list
# Set active instance
unity-mcp instance set "ProjectName@abc123"
# Show current instance
unity-mcp instance current
```
### Scene Operations
```bash
# Get scene hierarchy
unity-mcp scene hierarchy
unity-mcp scene hierarchy --limit 20 --depth 3
# Get active scene info
unity-mcp scene active
# Load/save scenes
unity-mcp scene load "Assets/Scenes/Main.unity"
unity-mcp scene save
# Take screenshot
unity-mcp scene screenshot --name "capture"
```
### GameObject Operations
```bash
# Find GameObjects
unity-mcp gameobject find "Player"
unity-mcp gameobject find "Enemy" --method by_tag
# Create GameObjects
unity-mcp gameobject create "NewCube" --primitive Cube
unity-mcp gameobject create "Empty" --position 0 5 0
# Modify GameObjects
unity-mcp gameobject modify "Cube" --position 1 2 3 --rotation 0 45 0
# Delete/duplicate
unity-mcp gameobject delete "OldObject" --force
unity-mcp gameobject duplicate "Template"
```
### Component Operations
```bash
# Add component
unity-mcp component add "Player" Rigidbody
# Remove component
unity-mcp component remove "Player" Rigidbody
# Set property
unity-mcp component set "Player" Rigidbody mass 10
```
### Script Operations
```bash
# Create script
unity-mcp script create "PlayerController" --path "Assets/Scripts"
# Read script
unity-mcp script read "Assets/Scripts/Player.cs"
# Delete script
unity-mcp script delete "Assets/Scripts/Old.cs" --force
```
### Code Search
```bash
# Search with regex
unity-mcp code search "class.*Player" "Assets/Scripts/Player.cs"
unity-mcp code search "TODO|FIXME" "Assets/Scripts/Utils.cs"
unity-mcp code search "void Update" "Assets/Scripts/Game.cs" --max-results 20
```
### Shader Operations
```bash
# Create shader
unity-mcp shader create "MyShader" --path "Assets/Shaders"
# Read shader
unity-mcp shader read "Assets/Shaders/Custom.shader"
# Update from file
unity-mcp shader update "Assets/Shaders/Custom.shader" --file local.shader
# Delete shader
unity-mcp shader delete "Assets/Shaders/Old.shader" --force
```
### Editor Controls
```bash
# Play mode
unity-mcp editor play
unity-mcp editor pause
unity-mcp editor stop
# Refresh assets
unity-mcp editor refresh
unity-mcp editor refresh --compile
# Console
unity-mcp editor console
unity-mcp editor console --clear
# Tags and layers
unity-mcp editor add-tag "Enemy"
unity-mcp editor add-layer "Projectiles"
# Menu items
unity-mcp editor menu "Edit/Project Settings..."
# Custom tools
unity-mcp editor custom-tool "MyBuildTool"
unity-mcp editor custom-tool "Deploy" --params '{"target": "Android"}'
```
### Testing
```bash
# Run tests synchronously
unity-mcp editor tests --mode EditMode
# Run tests asynchronously
unity-mcp editor tests --mode PlayMode --async
# Poll test job
unity-mcp editor poll-test <job_id>
unity-mcp editor poll-test <job_id> --wait 60 --details
```
### Material Operations
```bash
# Create material
unity-mcp material create "Assets/Materials/Red.mat"
# Set color
unity-mcp material set-color "Assets/Materials/Red.mat" 1 0 0
# Assign to object
unity-mcp material assign "Assets/Materials/Red.mat" "Cube"
```
### VFX Operations
```bash
# Particle systems
unity-mcp vfx particle info "Fire"
unity-mcp vfx particle play "Fire" --with-children
unity-mcp vfx particle stop "Fire"
# Line renderers
unity-mcp vfx line info "LaserBeam"
unity-mcp vfx line create-line "Line" --start 0 0 0 --end 10 5 0
unity-mcp vfx line create-circle "Circle" --radius 5
# Trail renderers
unity-mcp vfx trail info "PlayerTrail"
unity-mcp vfx trail set-time "Trail" 2.0
# Raw VFX actions (access all 60+ actions)
unity-mcp vfx raw particle_set_main "Fire" --params '{"duration": 5}'
```
### Batch Operations
```bash
# Execute from JSON file
unity-mcp batch run commands.json
unity-mcp batch run commands.json --parallel --fail-fast
# Execute inline JSON
unity-mcp batch inline '[{"tool": "manage_scene", "params": {"action": "get_active"}}]'
# Generate template
unity-mcp batch template > my_commands.json
```
### Prefab Operations
```bash
# Open prefab for editing
unity-mcp prefab open "Assets/Prefabs/Player.prefab"
# Save and close
unity-mcp prefab save
unity-mcp prefab close
# Create from GameObject
unity-mcp prefab create "Player" --path "Assets/Prefabs"
```
### Asset Operations
```bash
# Search assets
unity-mcp asset search --pattern "*.mat" --path "Assets/Materials"
# Get asset info
unity-mcp asset info "Assets/Materials/Red.mat"
# Create folder
unity-mcp asset mkdir "Assets/NewFolder"
# Move/rename
unity-mcp asset move "Assets/Old.mat" "Assets/Materials/"
```
### Animation Operations
```bash
# Play animation state
unity-mcp animation play "Player" "Run"
# Set animator parameter
unity-mcp animation set-parameter "Player" Speed 1.5
unity-mcp animation set-parameter "Player" IsRunning true
```
### Audio Operations
```bash
# Play audio
unity-mcp audio play "AudioPlayer"
# Stop audio
unity-mcp audio stop "AudioPlayer"
# Set volume
unity-mcp audio volume "AudioPlayer" 0.5
```
### Lighting Operations
```bash
# Create light
unity-mcp lighting create "NewLight" --type Point --position 0 5 0
unity-mcp lighting create "Spotlight" --type Spot --intensity 2
```
### UI Operations
```bash
# Create canvas
unity-mcp ui create-canvas "MainCanvas"
# Create text
unity-mcp ui create-text "Title" --parent "MainCanvas" --text "Hello World"
# Create button
unity-mcp ui create-button "StartBtn" --parent "MainCanvas" --text "Start"
# Create image
unity-mcp ui create-image "Background" --parent "MainCanvas"
```
### Raw Commands
For any MCP tool not covered by dedicated commands:
```bash
unity-mcp raw manage_scene '{"action": "get_hierarchy", "max_nodes": 100}'
unity-mcp raw read_console '{"count": 20}'
```
---
## Complete Command Reference
| Group | Subcommands |
|-------|-------------|
| `instance` | `list`, `set`, `current` |
| `scene` | `hierarchy`, `active`, `load`, `save`, `create`, `screenshot`, `build-settings` |
| `gameobject` | `find`, `create`, `modify`, `delete`, `duplicate`, `move` |
| `component` | `add`, `remove`, `set`, `modify` |
| `script` | `create`, `read`, `delete`, `edit`, `validate` |
| `code` | `read`, `search` |
| `shader` | `create`, `read`, `update`, `delete` |
| `editor` | `play`, `pause`, `stop`, `refresh`, `console`, `menu`, `tool`, `add-tag`, `remove-tag`, `add-layer`, `remove-layer`, `tests`, `poll-test`, `custom-tool` |
| `asset` | `search`, `info`, `create`, `delete`, `duplicate`, `move`, `rename`, `import`, `mkdir` |
| `prefab` | `open`, `close`, `save`, `create` |
| `material` | `info`, `create`, `set-color`, `set-property`, `assign`, `set-renderer-color` |
| `vfx particle` | `info`, `play`, `stop`, `pause`, `restart`, `clear` |
| `vfx line` | `info`, `set-positions`, `create-line`, `create-circle`, `clear` |
| `vfx trail` | `info`, `set-time`, `clear` |
| `vfx` | `raw` (access all 60+ actions) |
| `batch` | `run`, `inline`, `template` |
| `animation` | `play`, `set-parameter` |
| `audio` | `play`, `stop`, `volume` |
| `lighting` | `create` |
| `ui` | `create-canvas`, `create-text`, `create-button`, `create-image` |
---
## Output Formats
```bash
# Text (default) - human readable
unity-mcp scene hierarchy
# JSON - for scripting
unity-mcp --format json scene hierarchy
# Table - structured display
unity-mcp --format table instance list
```
## Environment Variables
Set defaults via environment:
```bash
export UNITY_MCP_HOST=192.168.1.100
export UNITY_MCP_HTTP_PORT=8080
export UNITY_MCP_FORMAT=json
export UNITY_MCP_INSTANCE=MyProject@abc123
```
## Troubleshooting
### Connection Issues
```bash
# Check server status
unity-mcp status
# Verify Unity is running with MCP plugin
# Check Unity console for MCP connection messages
```
### Common Errors
| Error | Solution |
|-------|----------|
| Cannot connect to server | Ensure Unity MCP server is running |
| Unknown command type | Unity plugin may not support this tool |
| Timeout | Increase timeout with `-t 60` |

View File

@ -1,5 +1,7 @@
#!/usr/bin/env python3
import sys, json
import sys
import json
def summarize(txt):
try:
@ -15,7 +17,8 @@ def summarize(txt):
if "diagnostics" in data:
diags = data["diagnostics"] or []
w = sum(d.get("severity", "").lower() == "warning" for d in diags)
e = sum(d.get("severity","" ).lower() in ("error","fatal") for d in diags)
e = sum(d.get("severity", "").lower() in ("error", "fatal")
for d in diags)
ok = "OK" if not e else "FAIL"
return f"validate: {ok} (warnings={w}, errors={e})"
if "matches" in data:
@ -28,26 +31,32 @@ def summarize(txt):
lines = data["lines"] or []
lvls = {"info": 0, "warning": 0, "error": 0}
for L in lines:
lvls[L.get("level","" ).lower()] = lvls.get(L.get("level","" ).lower(),0)+1
lvls[L.get("level", "").lower()] = lvls.get(
L.get("level", "").lower(), 0)+1
return f"console: {len(lines)} lines (info={lvls.get('info', 0)},warn={lvls.get('warning', 0)},err={lvls.get('error', 0)})"
# Fallback: short status
return (msg or "tool_result")[:80]
def prune_message(msg):
if "content" not in msg: return msg
if "content" not in msg:
return msg
newc = []
for c in msg["content"]:
if c.get("type") == "tool_result" and c.get("content"):
out = []
for chunk in c["content"]:
if chunk.get("type") == "text":
out.append({"type":"text","text":summarize(chunk.get("text","" ))})
newc.append({"type":"tool_result","tool_use_id":c.get("tool_use_id"),"content":out})
out.append(
{"type": "text", "text": summarize(chunk.get("text", ""))})
newc.append({"type": "tool_result", "tool_use_id": c.get(
"tool_use_id"), "content": out})
else:
newc.append(c)
msg["content"] = newc
return msg
def main():
convo = json.load(sys.stdin)
if isinstance(convo, dict) and "messages" in convo:
@ -55,4 +64,6 @@ def main():
elif isinstance(convo, list):
convo = [prune_message(m) for m in convo]
json.dump(convo, sys.stdout, ensure_ascii=False)
main()