Add CLI (#606)
* 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
parent
f54b1cb552
commit
7f44e4b53e
|
|
@ -132,7 +132,7 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
|
|||
{
|
||||
projectScopedToolsToggle.value = EditorPrefs.GetBool(
|
||||
EditorPrefKeys.ProjectScopedToolsLocalHttp,
|
||||
true
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
```
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
"""Unity MCP Command Line Interface."""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
"""CLI command modules."""
|
||||
|
||||
# Commands will be registered in main.py
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue