Skip to content

Conversation

@Scriptwonder
Copy link
Collaborator

@Scriptwonder Scriptwonder commented Jan 11, 2026

  • Add click-based CLI with 15+ command groups and USAGE_GUIDE.md for AI/users to follow
  • Commands: gameobject, component, scene, asset, script, editor, prefab, material, lighting, ui, audio, animation, code (Some still placeholder but most workable)
  • 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
截屏2026-01-10 下午10 17 16

Hi team! The reason for this CLI is that the current MCP repo - even accelerated by some optimization - still needs sometime waiting and sometime can be undeterministic, with a lot of details of the calls and parameters hidden from the user's sight to save space. I am recently working on a game-based skill benchmark, and this skill caught my attention. To make the generation more automated, CLI seems to be an intuitive way and a new, faster approach to executing user commands.

Some advantages I can think of:

  • Faster automation, so speed and efficiency (in terms of simpler commands of course)
  • Reproducibility with certainty
  • A quicker and token-saving approach when dealing with redundant operations

And some cons I can think of:

  • Safety issue: which is why I keep it completely local with HTTP at the current version, can discuss more to support remote
  • Confusion: when to use MCP and when to use CLI? I developed this with the skills in my mind, which is hard to achieve with MCP, but I think from a reg game dev perspective, these two do not make a difference, except that the MCP is more developed.

That would be all, looking forward to the team's feedback. Happy coding! @msanatan @dsarno

PS: Did a solid amount of AI coding with this one, then did throughful test myself (CLI tools are easy to test!)

Summary by Sourcery

Introduce a Click-based unity-mcp CLI for interacting with the Unity MCP server over HTTP, including routes on the server to accept CLI commands and list Unity instances, along with configuration, output formatting, and tests.

New Features:

  • Add a unity-mcp command-line entry point with global options and multiple command groups for game objects, components, scenes, assets, scripts, editor control, prefabs, materials, lighting, audio, UI, animation, code, and raw commands.
  • Expose new HTTP endpoints on the MCP server for executing typed commands against Unity instances and listing connected Unity sessions, enabling the CLI to target specific projects.

Enhancements:

  • Provide reusable CLI utilities for configuration management, HTTP connection handling, and rich output formatting supporting text, JSON, and table views.

Build:

  • Register the new unity-mcp console script in pyproject.toml and add Click as a runtime dependency.

Documentation:

  • Add a comprehensive CLI usage guide (CLI_USAGE_GUIDE.md) describing installation, command structure, common pitfalls, and detailed command reference for AI assistants and developers.

Tests:

  • Add extensive unit and integration-style tests for CLI configuration, output formatting, HTTP connection helpers, and the majority of CLI commands and global options.

Summary by CodeRabbit

  • New Features

    • Added comprehensive command-line interface for managing Unity projects remotely, supporting GameObject, component, scene, asset, animation, audio, material, script, lighting, UI, editor, and prefab operations.
    • Support for multiple output formats: text, JSON, and table.
    • Connection status checking and Unity instance discovery.
  • Documentation

    • Added CLI usage guide with installation steps, command reference, and examples.
  • Chores

    • Added Click dependency for CLI support.
    • Added new console script entry point.

✏️ Tip: You can customize this high-level summary in your review settings.

- 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
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 11, 2026

Reviewer's Guide

Introduce a new unity-mcp Click-based CLI that talks to the Unity MCP server over new HTTP endpoints, with rich command groups for editor/game manipulation, shared config/output utilities, and extensive tests and docs for AI/developer usage.

Sequence diagram for unity-mcp gameobject create command

sequenceDiagram
    actor User
    participant CLI as unity_mcp_CLI
    participant GameobjectCmd as gameobject_create_command
    participant Config as CLIConfig
    participant Conn as run_command
    participant HTTP as /api/command
    participant Hub as PluginHub
    participant Unity as Unity_Editor_instance

    User->>CLI: unity-mcp gameobject create "MyCube" --primitive Cube
    CLI->>Config: set_config from CLI options
    CLI->>GameobjectCmd: dispatch click subcommand

    GameobjectCmd->>Config: get_config
    GameobjectCmd->>Conn: run_command("manage_gameobject", params, config)

    Conn->>HTTP: POST /api/command {type: manage_gameobject, params, unity_instance?}
    HTTP->>Hub: PluginHub.get_sessions()
    Hub-->>HTTP: sessions list
    HTTP->>Hub: send_command(session_id, command_type, params)
    Hub-->>HTTP: result JSON
    HTTP-->>Conn: JSONResponse(result)
    Conn-->>GameobjectCmd: result dict

    GameobjectCmd->>Conn: run_command("manage_components", add Rigidbody, config) (optional)
    GameobjectCmd->>Conn: run_command("manage_components", add BoxCollider, config) (optional)

    GameobjectCmd->>CLI: formatted output via format_output
    CLI-->>User: text/json/table result
Loading

Class diagram for CLI configuration and utility modules

classDiagram
    class CLIConfig {
        +str host
        +int port
        +int timeout
        +str format
        +str unity_instance
        +from_env() CLIConfig
    }

    class Context {
        +CLIConfig config
        +bool verbose
        +__init__()
    }

    class ConfigModule {
        +CLIConfig _config
        +get_config() CLIConfig
        +set_config(config CLIConfig) void
    }

    class ConnectionModule {
        +send_command(command_type str, params dict, config CLIConfig, timeout int) dict
        +run_command(command_type str, params dict, config CLIConfig, timeout int) dict
        +check_connection(config CLIConfig) bool
        +run_check_connection(config CLIConfig) bool
        +list_unity_instances(config CLIConfig) dict
        +run_list_instances(config CLIConfig) dict
        +warn_if_remote_host(config CLIConfig) void
    }

    class UnityConnectionError {
        +UnityConnectionError(message str)
    }

    class OutputModule {
        +format_output(data any, format_type str) str
        +format_as_json(data any) str
        +format_as_text(data any, indent int) str
        +format_as_table(data any) str
        +print_success(message str) void
        +print_error(message str) void
        +print_warning(message str) void
        +print_info(message str) void
    }

    class MainCLI {
        +cli(host str, port int, timeout int, format str, instance str, verbose bool) void
        +status() void
        +instances() void
        +raw_command(command_type str, params str) void
        +register_commands() void
        +main() void
    }

    class GameobjectCommands {
        +gameobject()
        +find(search_term str, method str, include_inactive bool, limit int, cursor int)
        +create(name str, primitive str, position tuple, rotation tuple, scale tuple, parent str, tag str, layer str, components str, save_prefab bool, prefab_path str)
        +modify(target str, name str, position tuple, rotation tuple, scale tuple, parent str, tag str, layer str, active bool, add_components str, remove_components str, search_method str)
        +delete(target str, search_method str, force bool)
        +duplicate(target str, name str, offset tuple, search_method str)
        +move(target str, reference str, direction str, distance float, local bool, search_method str)
    }

    MainCLI --> Context : uses
    Context --> CLIConfig : holds

    MainCLI --> ConfigModule : uses
    MainCLI --> ConnectionModule : uses
    MainCLI --> OutputModule : uses

    ConfigModule --> CLIConfig : manages

    ConnectionModule --> CLIConfig : reads
    ConnectionModule --> UnityConnectionError : raises

    OutputModule ..> CLIConfig : uses format field

    GameobjectCommands --> ConfigModule : get_config
    GameobjectCommands --> ConnectionModule : run_command
    GameobjectCommands --> OutputModule : format_output
Loading

File-Level Changes

Change Details Files
Add HTTP REST endpoints on the MCP server for CLI commands and Unity instance enumeration.
  • Add POST /api/command route that validates payload, selects appropriate Unity session (optionally by instance identifier), and proxies commands via PluginHub.send_command.
  • Add GET /api/instances route that normalizes PluginHub session metadata into a simple instances list for clients.
  • Return structured JSON success/error payloads with appropriate HTTP status codes and logging on exceptions.
Server/src/main.py
Introduce a unity-mcp Click-based CLI entrypoint with global config, connection management, and a raw command interface.
  • Define a main Click group with global options (host, port, timeout, output format, instance, verbose) wired into a shared CLIConfig object and environment variables.
  • Implement status, instances, and raw subcommands that use connection utilities to ping the server, list Unity instances, or send arbitrary command payloads.
  • Register multiple command groups (gameobject, component, scene, asset, script, code, editor, prefab, material, lighting, animation, audio, ui) from separate modules and expose the CLI via the unity-mcp console script entry point.
Server/src/cli/main.py
Server/pyproject.toml
Server/src/cli/__init__.py
Provide shared CLI utilities for configuration, HTTP communication, and output formatting, including safety warnings for remote hosts.
  • Add CLIConfig dataclass with env-var-based construction and global getter/setter to share connection settings across commands.
  • Implement async/sync HTTP helpers for sending commands to /api/command, checking /health, and listing Unity instances via /api/instances with fallback to /plugin/sessions and normalized response format.
  • Add output helpers that can render responses as text, JSON, or simple tables plus convenience printers for success/error/info/warning, including special handling of common response shapes.
Server/src/cli/utils/config.py
Server/src/cli/utils/connection.py
Server/src/cli/utils/output.py
Server/src/cli/utils/__init__.py
Add focused Click command groups for common Unity editor domains (GameObjects, components, scenes, assets, scripts, materials, prefabs, editor, UI, lighting, audio, animation, code).
  • Implement gameobject commands (find/create/modify/delete/duplicate/move) that translate CLI flags into manage_gameobject/manage_components calls, including multi-value vector options and follow‑up component creation logic.
  • Implement component commands (add/remove/set/modify) that accept JSON for property bags and call manage_components with appropriate action and searchMethod parameters.
  • Implement scene commands (hierarchy/active/load/save/create/build-settings/screenshot) that wrap manage_scene actions with options for pagination, transform inclusion, build index vs path, and screenshot parameters.
  • Implement asset commands (search/info/create/delete/duplicate/move/rename/import/mkdir) over manage_asset, handling JSON properties, path manipulation, and confirmation prompts.
  • Implement script and code commands to create/read/delete/edit/validate C# scripts by wrapping manage_script/apply_text_edits/validate_script and printing source content directly when present.
  • Implement material commands (info/create/set-color/set-property/assign/set-renderer-color) that wrap manage_material/manage_components, including shader property naming and color vector handling.
  • Implement prefab commands (open/close/save/create) that call manage_prefabs with stage control or prefab creation from scene GameObjects.
  • Implement editor commands (play/pause/stop/console/add/remove tag/layer/tool/menu/tests) that orchestrate manage_editor, read_console, execute_menu_item, and run_tests tools.
  • Implement ui, lighting, audio, and animation commands that compose higher-level behaviors using underlying manage_gameobject/manage_components, with some commands still acting as thin or placeholder wrappers pending deeper Unity-side support.
Server/src/cli/commands/gameobject.py
Server/src/cli/commands/component.py
Server/src/cli/commands/scene.py
Server/src/cli/commands/asset.py
Server/src/cli/commands/script.py
Server/src/cli/commands/code.py
Server/src/cli/commands/material.py
Server/src/cli/commands/prefab.py
Server/src/cli/commands/editor.py
Server/src/cli/commands/ui.py
Server/src/cli/commands/lighting.py
Server/src/cli/commands/audio.py
Server/src/cli/commands/animation.py
Server/src/cli/commands/__init__.py
Add detailed CLI usage documentation and an extensive CLI test suite.
  • Introduce CLI_USAGE_GUIDE.md targeted at AI assistants and developers, documenting installation, global options, argument vs option syntax, multi-value pitfalls, command patterns, and concrete examples for each command group.
  • Add tests that exercise configuration/env handling, connection utilities, output formatting functions, error handling, and representative commands across groups using Click's CliRunner and mocked run_command/run_check_connection/run_list_instances.
  • Ensure tests cover invalid JSON handling, connection failures via UnityConnectionError, global option propagation, and integration-style workflows like create/modify/delete GameObject or listing scenes and instances.
Server/src/cli/CLI_USAGE_GUIDE.md
Server/tests/test_cli.py
Wire the new CLI into the project build and dependency graph.
  • Add click>=8.1.0 to runtime dependencies and ensure the unity-mcp script is exposed in pyproject.scripts next to the existing mcp-for-unity entry.
  • Update the lockfile to capture new dependency resolution (file mostly autogenerated).
Server/pyproject.toml
Server/uv.lock

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 11, 2026

📝 Walkthrough

Walkthrough

Introduces a comprehensive CLI for Unity MCP using Click framework. Adds 13 command groups (gameobject, component, scene, asset, script, code, editor, prefab, material, lighting, animation, audio, ui) with corresponding subcommands, HTTP server endpoints for CLI communication, configuration management, output formatting utilities, and extensive test coverage.

Changes

Cohort / File(s) Summary
Project Configuration
Server/pyproject.toml
Added click>=8.1.0 dependency and new unity-mcp console script entry pointing to cli.main:main
Documentation
Server/src/cli/CLI_USAGE_GUIDE.md
Comprehensive 727-line CLI usage guide covering installation, quick start, commands, options, syntax, output formats, examples, and reference cards
Core CLI Structure
Server/src/cli/__init__.py, Server/src/cli/commands/__init__.py, Server/src/cli/main.py
CLI package initialization with version 1.0.0, command registration mechanism, Click-based entry point with global options (host, port, timeout, format, instance, verbose), and built-in commands (status, instances, raw)
Command Groups: GameObject & Component
Server/src/cli/commands/gameobject.py, Server/src/cli/commands/component.py
GameObject operations (find, create, modify, delete, duplicate, move) and component management (add, remove, set-property, modify) with pagination, transform support, and JSON property parsing
Command Groups: Scene & Asset
Server/src/cli/commands/scene.py, Server/src/cli/commands/asset.py
Scene management (hierarchy, active, load, save, create, build-settings, screenshot) and asset operations (search, info, create, delete, duplicate, move, rename, import, mkdir) with filtering and pagination
Command Groups: Script & Code
Server/src/cli/commands/script.py, Server/src/cli/commands/code.py
Script creation/deletion/validation and code file reading with line range support
Command Groups: Editor, Prefab & Utilities
Server/src/cli/commands/editor.py, Server/src/cli/commands/prefab.py, Server/src/cli/commands/lighting.py, Server/src/cli/commands/material.py
Editor operations (play, pause, stop, console, tags, layers, tools, menus, tests), prefab staging/creation, light creation, and material management (info, create, set color/properties, assign to renderers)
Command Groups: Animation, Audio & UI
Server/src/cli/commands/animation.py, Server/src/cli/commands/audio.py, Server/src/cli/commands/ui.py
Animation state control and parameter setting, audio playback/volume control, and UI element creation (canvas, text, button, image)
Utilities: Configuration & Connection
Server/src/cli/utils/config.py, Server/src/cli/utils/connection.py, Server/src/cli/utils/__init__.py
CLIConfig dataclass with environment variable support, async/sync HTTP command sending with timeout/error handling, Unity instance discovery, and health checks
Utilities: Output Formatting
Server/src/cli/utils/output.py
Multi-format output (JSON, text, table) with intelligent data rendering, list item summarization, dynamic table widths, and standardized success/error/warning/info messaging
Server Integration
Server/src/main.py
Two new REST endpoints: POST /api/command for command execution with optional Unity instance selection, and GET /api/instances for listing connected sessions
Test Suite
Server/tests/test_cli.py
877-line comprehensive test suite covering configuration, output formatting, connection handling, CLI commands, command-specific workflows, error scenarios, and integration-style operations

Sequence Diagram(s)

sequenceDiagram
    participant User as User/CLI
    participant Click as Click CLI
    participant Config as Config
    participant Connection as Connection Handler
    participant Server as MCP Server
    participant Unity as Unity Instances

    User->>Click: Execute command (e.g., gameobject find)
    Click->>Config: get_config()
    Config-->>Click: CLIConfig
    Click->>Connection: run_command(type, params, config)
    Connection->>Connection: asyncio.run(send_command(...))
    Connection->>Server: POST /api/command
    Server->>Server: Select Unity instance
    Server->>Unity: Forward command (manage_gameobject)
    Unity-->>Server: Command result
    Server-->>Connection: JSON response
    Connection-->>Click: Dict result
    Click->>Click: format_output(result, format_type)
    Click->>User: Formatted output (text/JSON/table)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • msanatan
  • justinpbarnett

Poem

🐰 Hop, hop—a CLI is born,
With Click commands from dusk till morn!
GameObjects, scenes, and materials bright,
The rabbit's CLI shines with all its might!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: Add CLI for Unity MCP server' clearly and specifically summarizes the main change: adding a command-line interface for the Unity MCP server.
Docstring Coverage ✅ Passed Docstring coverage is 98.45% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 9 issues, and left some high level feedback:

  • The /api/command route silently falls back to the first available session when unity_instance is missing or doesn’t match, which can be surprising in multi-instance setups—consider returning a 400/409 with a clear error when there’s an explicit but unresolved unity_instance, and/or requiring disambiguation when multiple candidates exist.
  • Several commands inspect responses with ad‑hoc checks like result.get("success") or result.get("data") or result.get("result"); it would be more robust to centralize response normalization/validation (e.g., a helper that extracts success, data and standardizes error reporting) so all commands interpret server responses consistently.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `/api/command` route silently falls back to the first available session when `unity_instance` is missing or doesn’t match, which can be surprising in multi-instance setups—consider returning a 400/409 with a clear error when there’s an explicit but unresolved `unity_instance`, and/or requiring disambiguation when multiple candidates exist.
- Several commands inspect responses with ad‑hoc checks like `result.get("success") or result.get("data") or result.get("result")`; it would be more robust to centralize response normalization/validation (e.g., a helper that extracts `success`, `data` and standardizes error reporting) so all commands interpret server responses consistently.

## Individual Comments

### Comment 1
<location> `Server/src/main.py:328-330` </location>
<code_context>
+                    session_id = sid
+                    break
+        
+        if not session_id:
+            # Use first available session
+            session_id = next(iter(sessions.sessions.keys()))
+        
+        # Send command to Unity
</code_context>

<issue_to_address>
**issue (bug_risk):** Using the first available session when a specific `unity_instance` is provided but not found may be surprising behavior.

If `unity_instance` is provided but not found, silently falling back to the first session can cause commands to be sent to the wrong Unity instance, and the CLI has no easy way to detect this. It would be safer to return an explicit error in that case (e.g., `"Unity instance '<value>' not found"`), or otherwise handle "not provided" vs. "provided but missing" differently rather than auto-selecting.
</issue_to_address>

### Comment 2
<location> `Server/src/cli/commands/material.py:92-98` </location>
<code_context>
[email protected]("r", type=float)
[email protected]("g", type=float)
[email protected]("b", type=float)
[email protected]("a", type=float, default=1.0)
[email protected](
+    "--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.
+    
</code_context>

<issue_to_address>
**issue (bug_risk):** Using a default on a required Click argument can lead to confusing or unsupported behavior.

Here `a` is a positional argument but also has `default=1.0`. In Click, defaults are usually used with `required=False` and options rather than required positional arguments, and behavior can vary between Click versions. Consider either making `a` an option (e.g., `--alpha`) with a default, or removing the default and requiring all four components explicitly.
</issue_to_address>

### Comment 3
<location> `Server/src/cli/commands/ui.py:91` </location>
<code_context>
+    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).
+    
</code_context>

<issue_to_address>
**issue (bug_risk):** The `position` parameter for `create-text` is currently unused.

The CLI accepts `--position` and passes it to `create_text`, but the function never uses it (e.g., on a `RectTransform` anchored position). This makes the option misleading. Either apply the position (possibly via a `manage_components`/`manage_gameobject` call) or remove the option until it’s supported.
</issue_to_address>

### Comment 4
<location> `Server/src/cli/commands/audio.py:20-23` </location>
<code_context>
[email protected]("play")
[email protected]("target")
[email protected]("state_name")
[email protected](
+    "--layer", "-l",
+    default=0,
</code_context>

<issue_to_address>
**issue (bug_risk):** The `--clip` option in `audio play` is accepted but never used.

The `play` command takes a `clip` parameter but only toggles an `AudioSource` property and never sends the clip path to Unity. If clip selection isn’t supported yet, either remove this option or pass it through to `manage_components` (or equivalent) so the backend can actually use it.
</issue_to_address>

### Comment 5
<location> `Server/tests/test_cli.py:199-208` </location>
<code_context>
+            result = await check_connection()
+            assert result is False
+
+    @pytest.mark.asyncio
+    async def test_send_command_success(self, mock_unity_response):
+        """Test successful command sending."""
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.json.return_value = mock_unity_response
+
+        with patch("httpx.AsyncClient") as mock_client:
+            mock_client.return_value.__aenter__.return_value.post = AsyncMock(
+                return_value=mock_response
+            )
+            mock_response.raise_for_status = MagicMock()
+
+            result = await send_command("test_command", {"param": "value"})
+            assert result == mock_unity_response
+
+    @pytest.mark.asyncio
+    async def test_send_command_connection_error(self):
+        """Test command sending with connection error."""
+        with patch("httpx.AsyncClient") as mock_client:
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for the specific httpx error branches in send_command

Current tests only exercise the happy path and a generic `Exception` from `send_command`, but the code has specific handling for `httpx.ConnectError`, `httpx.TimeoutException`, and `httpx.HTTPStatusError` with distinct messages. Please add tests that mock `httpx.AsyncClient.post` to raise each of these exceptions and assert that `UnityConnectionError` is raised with the correct message fragment, so the detailed error handling is verified and doesn't regress.

Suggested implementation:

```python
# Connection Tests
# =============================================================================

class TestConnection:
=======
        assert "key" in table_result.lower() or "Key" in table_result


@pytest.mark.asyncio
async def test_send_command_httpx_connect_error():
    """send_command should wrap httpx.ConnectError in UnityConnectionError with a connection-related message."""
    # Simulate a low-level connection error from httpx
    with patch("httpx.AsyncClient") as mock_client:
        mock_client.return_value.__aenter__.return_value.post = AsyncMock(
            side_effect=httpx.ConnectError("Unable to connect")
        )

        with pytest.raises(UnityConnectionError) as exc_info:
            await send_command("test_command", {"param": "value"})

        message = str(exc_info.value).lower()
        assert "connect" in message or "connection" in message


@pytest.mark.asyncio
async def test_send_command_httpx_timeout_error():
    """send_command should wrap httpx.TimeoutException in UnityConnectionError with a timeout-related message."""
    with patch("httpx.AsyncClient") as mock_client:
        mock_client.return_value.__aenter__.return_value.post = AsyncMock(
            side_effect=httpx.TimeoutException("Request timed out")
        )

        with pytest.raises(UnityConnectionError) as exc_info:
            await send_command("test_command", {"param": "value"})

        message = str(exc_info.value).lower()
        assert "timeout" in message or "timed out" in message


@pytest.mark.asyncio
async def test_send_command_httpx_http_status_error():
    """send_command should wrap httpx.HTTPStatusError in UnityConnectionError with an HTTP-status-related message."""
    # Create a Response and Request to satisfy HTTPStatusError requirements
    request = httpx.Request("POST", "http://unity-endpoint")
    response = httpx.Response(status_code=500, request=request)

    http_status_error = httpx.HTTPStatusError(
        "Unity returned an error status",
        request=request,
        response=response,
    )

    with patch("httpx.AsyncClient") as mock_client:
        # send_command is expected to call response.raise_for_status(), which will raise HTTPStatusError
        mock_response = MagicMock()
        mock_response.raise_for_status.side_effect = http_status_error
        mock_client.return_value.__aenter__.return_value.post = AsyncMock(
            return_value=mock_response
        )

        with pytest.raises(UnityConnectionError) as exc_info:
            await send_command("test_command", {"param": "value"})

        message = str(exc_info.value).lower()
        assert "http" in message or "status" in message or "error response" in message


# =============================================================================
# Connection Tests
# =============================================================================

class TestConnection:

```

These tests assume:
1. `pytest`, `httpx`, `MagicMock`, `AsyncMock`, `patch`, `UnityConnectionError`, and `send_command` are already imported in `Server/tests/test_cli.py`. If any are missing, add appropriate imports at the top of the file.
2. The error messages raised by `send_command` for `httpx.ConnectError`, `httpx.TimeoutException`, and `httpx.HTTPStatusError` contain connection-, timeout-, and HTTP/status-related words respectively. If your actual messages differ, adjust the `assert` conditions in the tests to match the real message fragments (for example, assert for `"failed to connect to unity"` instead of the generic `"connect"` / `"connection"`).
</issue_to_address>

### Comment 6
<location> `Server/tests/test_cli.py:735-744` </location>
<code_context>
+class TestGlobalOptions:
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a test that asserts warn_if_remote_host is triggered for non-local hosts

Current global option tests cover `--host`, `--port`, `--timeout`, and `--format`, but don’t check that `warn_if_remote_host` is invoked for non-local hosts. Please add a CLI test (e.g., `unity-mcp --host 10.0.0.5 status`) that captures stderr/stdout to assert the warning text appears and that the command still returns a valid status. This will help prevent regressions in the remote-host safety warning wiring.

Suggested implementation:

```python
class TestGlobalOptions:
    """Tests for global CLI options."""

    def test_custom_host(self, runner, mock_unity_response):
        """Test custom host option."""
        with patch("cli.main.run_check_connection", return_value=True):
            with patch("cli.main.run_list_instances", return_value={"instances": []}):
                result = runner.invoke(cli, ["--host", "192.168.1.100", "status"])
                assert result.exit_code == 0

    def test_remote_host_warns_and_succeeds(self, runner, mock_unity_response):
        """Test that a non-local host triggers a warning but the command still succeeds."""
        with patch("cli.main.run_check_connection", return_value=True):
            with patch("cli.main.run_list_instances", return_value={"instances": []}):
                with patch("cli.main.warn_if_remote_host") as mock_warn_if_remote_host:
                    # Emit a known warning string to stderr when the remote host check runs
                    mock_warn_if_remote_host.side_effect = lambda host: click.echo(
                        "REMOTE HOST SAFETY WARNING", err=True
                    )
                    result = runner.invoke(cli, ["--host", "10.0.0.5", "status"])

        # Command should still complete successfully
        assert result.exit_code == 0
        # Ensure the remote host warning hook was invoked with the non-local host
        mock_warn_if_remote_host.assert_called_once_with("10.0.0.5")
        # Ensure the warning text is visible in the CLI output (stderr captured by Click test runner)
        assert "REMOTE HOST SAFETY WARNING" in result.output

    def test_custom_port(self, runner, mock_unity_response):

```

1. Ensure `click` is imported at the top of `Server/tests/test_cli.py`, for example:
   - Add `import click` alongside the other imports if it is not already present.
2. If the real `warn_if_remote_host` signature differs (e.g., additional parameters or different module path), adjust the patch target (`"cli.main.warn_if_remote_host"`) and `assert_called_once_with(...)` arguments accordingly.
3. If your test suite differentiates between `stdout` and `stderr` using a different mechanism than Click's `CliRunner`, adapt the `result.output` assertion to match your existing pattern (e.g., `result.stderr` if available).
</issue_to_address>

### Comment 7
<location> `Server/tests/test_cli.py:285-291` </location>
<code_context>
+        result = runner.invoke(cli, ["--version"])
+        assert result.exit_code == 0
+
+    def test_status_connected(self, runner, mock_instances_response):
+        """Test status command when connected."""
+        with patch("cli.main.run_check_connection", return_value=True):
+            with patch("cli.main.run_list_instances", return_value=mock_instances_response):
+                result = runner.invoke(cli, ["status"])
+                assert result.exit_code == 0
+                assert "Connected" in result.output
+
+    def test_status_disconnected(self, runner):
</code_context>

<issue_to_address>
**suggestion (testing):** Add a status test for when listing instances fails with UnityConnectionError

There’s an untested path where `run_check_connection` returns `True` but `run_list_instances` raises `UnityConnectionError`; in that case the CLI should print an informational message and exit cleanly. Please add a test that patches `run_check_connection` to return `True` and `run_list_instances` to raise `UnityConnectionError`, then assert exit code 0 and that the expected "Could not retrieve Unity instances" (or equivalent) message is printed.

Suggested implementation:

```python
    def test_status_connected(self, runner, mock_instances_response):
        """Test status command when connected."""
        with patch("cli.main.run_check_connection", return_value=True):
            with patch("cli.main.run_list_instances", return_value=mock_instances_response):
                result = runner.invoke(cli, ["status"])
                assert result.exit_code == 0
                assert "Connected" in result.output

    def test_status_instances_error(self, runner):
        """Test status command when listing instances fails."""
        with patch("cli.main.run_check_connection", return_value=True):
            with patch("cli.main.run_list_instances", side_effect=UnityConnectionError("test error")):
                result = runner.invoke(cli, ["status"])
                assert result.exit_code == 0
                assert "Could not retrieve Unity instances" in result.output

    def test_status_disconnected(self, runner):
        """Test status command when disconnected."""
        with patch("cli.main.run_check_connection", return_value=False):
            result = runner.invoke(cli, ["status"])
            assert result.exit_code == 1
            assert "Cannot connect" in result.output

```

1. Ensure `UnityConnectionError` is imported at the top of `Server/tests/test_cli.py`, for example:
   `from cli.main import UnityConnectionError` or from the module where it is defined (e.g. `from cli.exceptions import UnityConnectionError`), matching the existing codebase.
2. Confirm the exact message printed by the CLI when instance listing fails; if it differs from `"Could not retrieve Unity instances"`, update the assertion string in `test_status_instances_error` accordingly.
</issue_to_address>

### Comment 8
<location> `Server/tests/test_cli.py:306-277` </location>
<code_context>
+            result = runner.invoke(cli, ["instances"])
+            assert result.exit_code == 0
+
+    def test_raw_command(self, runner, mock_unity_response):
+        """Test raw command."""
+        with patch("cli.main.run_command", return_value=mock_unity_response):
+            result = runner.invoke(cli, ["raw", "test_command", '{"param": "value"}'])
+            assert result.exit_code == 0
+
+    def test_raw_command_invalid_json(self, runner):
</code_context>

<issue_to_address>
**suggestion (testing):** Add a raw command test that covers UnityConnectionError handling

Currently we only test the success and invalid-JSON paths. Since `cli.main.raw_command` should treat a `UnityConnectionError` from `run_command` as an error (message + exit code 1), please add a test that patches `cli.main.run_command` to raise `UnityConnectionError("some error")`, then asserts `runner.invoke` returns `exit_code == 1` and that the error message appears in the output/stderr.

Suggested implementation:

```python
# GameObject Command Tests
# =============================================================================
=======
    def test_raw_command_invalid_json(self, runner):
        """Test raw command with invalid JSON."""
        result = runner.invoke(cli, ["raw", "test_command", "invalid json"])
        assert result.exit_code == 1
        assert "Invalid JSON" in result.output

    def test_raw_command_unity_connection_error(self, runner):
        """Test raw command handling UnityConnectionError from run_command."""
        with patch("cli.main.run_command", side_effect=UnityConnectionError("some error")):
            result = runner.invoke(cli, ["raw", "test_command", '{"param": "value"}'])
            assert result.exit_code == 1
            assert "some error" in result.output


# =============================================================================
# GameObject Command Tests
# =============================================================================

```

If `UnityConnectionError` is not already imported in `Server/tests/test_cli.py`, add an import at the top of the file, for example:

- If it lives in `cli.exceptions`:
```python
from cli.exceptions import UnityConnectionError
```

or match whatever module is used elsewhere in this file for other `UnityConnectionError`-related tests.
</issue_to_address>

### Comment 9
<location> `Server/tests/test_cli.py:126-135` </location>
<code_context>
+class TestOutputFormatting:
</code_context>

<issue_to_address>
**suggestion (testing):** Add a test for format_as_text on successful responses that still include success/message fields

`format_as_text` has special handling for dicts with `success` and `data`/`result` fields where it recurses into the inner payload. Please add a test for an input like `{"success": True, "message": "OK", "data": {"foo": "bar"}}` that asserts only the inner `data` is rendered and `success`/`message` are omitted, to lock in this meta-field stripping behavior.

Suggested implementation:

```python
class TestOutputFormatting:
    """Tests for output formatting utilities."""

    def test_format_as_json(self):
        """Test JSON formatting."""
        data = {"key": "value", "number": 42}
        result = format_as_json(data)
        parsed = json.loads(result)
        assert parsed == data

    def test_format_as_text_strips_meta_fields_on_success(self):
        """format_as_text should render only inner data for success-wrapped payloads."""
        payload = {"success": True, "message": "OK", "data": {"foo": "bar"}}

        rendered = format_as_text(payload)

        # Only the inner data payload should be rendered; meta fields are stripped
        assert "foo" in rendered
        assert "bar" in rendered
        assert "success" not in rendered
        assert "message" not in rendered

    def test_format_as_json_with_complex_types(self):

```

If `format_as_text` is not yet imported in this test module, you will also need to add an import at the top of `Server/tests/test_cli.py`, matching how `format_as_json` is imported (for example, `from Server.cli import format_as_json, format_as_text` or similar, depending on the existing import style).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +328 to +330
if not session_id:
# Use first available session
session_id = next(iter(sessions.sessions.keys()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Using the first available session when a specific unity_instance is provided but not found may be surprising behavior.

If unity_instance is provided but not found, silently falling back to the first session can cause commands to be sent to the wrong Unity instance, and the CLI has no easy way to detect this. It would be safer to return an explicit error in that case (e.g., "Unity instance '<value>' not found"), or otherwise handle "not provided" vs. "provided but missing" differently rather than auto-selecting.

Comment on lines +92 to +98
@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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Using a default on a required Click argument can lead to confusing or unsupported behavior.

Here a is a positional argument but also has default=1.0. In Click, defaults are usually used with required=False and options rather than required positional arguments, and behavior can vary between Click versions. Consider either making a an option (e.g., --alpha) with a default, or removing the default and requiring all four components explicitly.

default=(0, 0),
help="Anchored position X Y."
)
def create_text(name: str, parent: str, text: str, position: tuple):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The position parameter for create-text is currently unused.

The CLI accepts --position and passes it to create_text, but the function never uses it (e.g., on a RectTransform anchored position). This makes the option misleading. Either apply the position (possibly via a manage_components/manage_gameobject call) or remove the option until it’s supported.

Comment on lines +20 to +23
@click.option(
"--clip", "-c",
default=None,
help="Audio clip path to play."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The --clip option in audio play is accepted but never used.

The play command takes a clip parameter but only toggles an AudioSource property and never sends the clip path to Unity. If clip selection isn’t supported yet, either remove this option or pass it through to manage_components (or equivalent) so the backend can actually use it.

Comment on lines +126 to +135
class TestOutputFormatting:
"""Tests for output formatting utilities."""

def test_format_as_json(self):
"""Test JSON formatting."""
data = {"key": "value", "number": 42}
result = format_as_json(data)
parsed = json.loads(result)
assert parsed == data

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test for format_as_text on successful responses that still include success/message fields

format_as_text has special handling for dicts with success and data/result fields where it recurses into the inner payload. Please add a test for an input like {"success": True, "message": "OK", "data": {"foo": "bar"}} that asserts only the inner data is rendered and success/message are omitted, to lock in this meta-field stripping behavior.

Suggested implementation:

class TestOutputFormatting:
    """Tests for output formatting utilities."""

    def test_format_as_json(self):
        """Test JSON formatting."""
        data = {"key": "value", "number": 42}
        result = format_as_json(data)
        parsed = json.loads(result)
        assert parsed == data

    def test_format_as_text_strips_meta_fields_on_success(self):
        """format_as_text should render only inner data for success-wrapped payloads."""
        payload = {"success": True, "message": "OK", "data": {"foo": "bar"}}

        rendered = format_as_text(payload)

        # Only the inner data payload should be rendered; meta fields are stripped
        assert "foo" in rendered
        assert "bar" in rendered
        assert "success" not in rendered
        assert "message" not in rendered

    def test_format_as_json_with_complex_types(self):

If format_as_text is not yet imported in this test module, you will also need to add an import at the top of Server/tests/test_cli.py, matching how format_as_json is imported (for example, from Server.cli import format_as_json, format_as_text or similar, depending on the existing import style).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Fix all issues with AI agents
In @Server/src/cli/commands/animation.py:
- Around line 33-60: The play function accepts a layer parameter but never sends
it to Unity; update the params dict in play to include the layer (e.g.,
params["layer"] = layer) so the "manage_components" call receives the layer
value, or if layer support is not yet intended remove the layer parameter from
play's signature or add a clear TODO comment and validation; locate the play
function and modify the params construction (or signature) accordingly to ensure
the parameter is used or intentionally omitted.
- Line 48: The PR is trying to set "Play" as a property but Animator.Play is a
method; update the code that emits the component change (the entry with
"property": "Play") to either (A) call a method-invocation path in the
manage_components backend (implement method invocation support there and handle
calling Animator.Play(...)) or (B) replace this manage_components call with an
animation-specific tool (similar to ManageVFX.cs) that invokes Animator.Play on
the target component; locate the emitter that writes the "property": "Play"
entry and change it to emit a method-invoke request (or swap to the custom
animation tool) and wire up any required parameters for Animator.Play
accordingly.

In @Server/src/cli/commands/audio.py:
- Around line 18-57: The play command accepts a --clip option but never uses it;
update the play function so that when clip is not None you add it to the params
payload (e.g., params["clip"] = clip) before calling
run_command("manage_components", params, config); if specifying a clip is not a
supported use case, remove the --clip Click option and the clip parameter from
play instead.

In @Server/src/cli/commands/code.py:
- Line 1: The module docstring claims "search and read source code" but only the
read command is implemented; update the top-level docstring in
Server/src/cli/commands/code.py to accurately reflect implemented functionality
(e.g., "Read source code" or "Read source code (search not yet implemented)") or
add a clear TODO note about implementing the search command, and mention the
implemented symbol 'read' so reviewers can verify consistency.

In @Server/src/cli/commands/gameobject.py:
- Around line 181-189: The component addition loop currently calls
run_command("manage_components", ...) for each entry in component_list without
per-item error handling; wrap each run_command call in a try/except (or check
its return value) inside the for-loop that iterates component_list, log or
collect failures (including component name and error/response) and continue to
the next component, and after the loop report any collected failures (or
raise/return a consolidated error) so callers can see which components failed;
references: components, result, component_list, run_command,
"manage_components", name, config.

In @Server/src/cli/commands/lighting.py:
- Around line 66-68: The current guard incorrectly uses "or" across fields so a
falsy success with truthy data/result lets execution continue; update the check
on create_result to only verify success (or success and data) — e.g., replace
the condition with if not create_result.get("success") (or if not
(create_result.get("success") and create_result.get("data"))) before calling
click.echo(format_output(create_result, config.format)) and returning so the
command aborts when creation failed.

In @Server/src/cli/commands/material.py:
- Around line 171-176: The docstring/example uses an invalid value "by_id" for
the --search-method Click option; update the example(s) to use one of the
allowed choices ("by_name", "by_path", "by_tag", "by_layer", "by_component") or
alternatively add "by_id" to the Click option choices array on the
--search-method declaration so they match; ensure you change all occurrences
(including the example at the other noted location) so the example and the
click.option choices remain consistent.

In @Server/src/cli/commands/script.py:
- Around line 147-150: The params construction is using fragile string-splitting
on path; update the logic that builds params (the "params" dict in the delete
command) to use robust path utilities instead of path.split: derive the script
name with pathlib.Path(path).stem (or
os.path.splitext(os.path.basename(path))[0]) and derive the parent with
pathlib.Path(path).parent.as_posix() (or os.path.dirname(path)), falling back to
"Assets" when parent is empty or "."; then assign those values into the existing
params dict ("action": "delete", "name", "path") so behavior matches the safer
parsing used in the read command.

In @Server/src/cli/commands/ui.py:
- Around line 190-228: The --sprite option is accepted by create_image but never
applied; after the existing run_command call that adds the Image component in
create_image, if sprite is not None call run_command again to set the Image
component's sprite property on the created GameObject (use the same config and
target name), e.g. invoke the manage_components command to set the Image
component's "sprite" (or include it in the add call as a properties/payload
field) so the provided sprite path is applied to the component.
- Around line 84-132: create_text accepts a position tuple but never applies it
to the RectTransform; after adding the TextMeshProUGUI component (in
create_text) call run_command("manage_components", { "action": "set_property",
"target": name, "componentType": "RectTransform", "property":
"anchoredPosition", "value": list(position) }, config) (or use
"anchoredPosition3D"/"localPosition" if your backend expects 3D) and ensure you
pass the position variable (converted to the expected list/format) so the
RectTransform receives the anchored position.

In @Server/src/cli/utils/config.py:
- Around line 18-27: The from_env classmethod on CLIConfig currently casts
UNITY_MCP_HTTP_PORT and UNITY_MCP_TIMEOUT to int without validation, which can
raise ValueError; wrap the int conversions in validation/error handling inside
CLIConfig.from_env (catch ValueError/TypeError), provide a clear error message
including the env var name and invalid value (and optionally fallback to
defaults), and re-raise or exit gracefully so the app doesn’t crash with an
opaque traceback; reference the symbols CLIConfig.from_env, UNITY_MCP_HTTP_PORT,
and UNITY_MCP_TIMEOUT when locating and updating the code.
🧹 Nitpick comments (19)
Server/src/cli/utils/connection.py (2)

80-96: Consider adding exception chaining for better debugging.

The exception handlers re-raise errors without preserving the exception chain. Adding from e (or from None where deliberate) improves debugging by preserving the original traceback.

♻️ Proposed fix to add exception chaining
     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}"
-        )
+        ) from 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."
-        )
+        ) from None
     except httpx.HTTPStatusError as e:
         raise UnityConnectionError(
             f"HTTP error from server: {e.response.status_code} - {e.response.text}"
-        )
+        ) from e
     except Exception as e:
-        raise UnityConnectionError(f"Unexpected error: {e}")
+        raise UnityConnectionError(f"Unexpected error: {e}") from e

161-183: Consider logging endpoint failures for easier debugging.

The try-except-continue pattern silently swallows errors when attempting each endpoint. If both endpoints fail, users only see the generic "No working endpoint found" message without knowing why each endpoint failed. Adding debug logging would aid troubleshooting.

💡 Suggested improvement with logging
+    import logging
+    logger = logging.getLogger(__name__)
+    
     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}
+                else:
+                    logger.debug(f"Endpoint {url} returned status {response.status_code}")
-            except Exception:
+            except Exception as e:
+                logger.debug(f"Failed to connect to {url}: {e}")
                 continue
Server/src/cli/commands/code.py (1)

44-44: Consider broader file type support.

The command strips .cs extensions, assuming C# files. However, the command group is named code (not script), which suggests it might be intended for broader source file reading. Consider whether this should support other file types or if the naming should be clarified.

Server/src/main.py (2)

336-338: Use logging.exception to preserve traceback.

Line 337 uses logger.error, which discards the exception traceback. For debugging production issues, use logger.exception instead to preserve the full stack trace.

🔍 Suggested fix
     except Exception as e:
-        logger.error(f"CLI command error: {e}")
+        logger.exception(f"CLI command error: {e}")
         return JSONResponse({"success": False, "error": str(e)}, status_code=500)

Based on static analysis hints.


356-357: Add error logging for consistency.

The cli_instances_route exception handler doesn't log errors, unlike cli_command_route at line 337. For operational visibility and debugging, consider logging the exception here as well.

📊 Suggested addition
     except Exception as e:
+        logger.exception("CLI instances query error")
         return JSONResponse({"success": False, "error": str(e)}, status_code=500)
Server/src/cli/utils/config.py (1)

25-25: Validate format value.

Line 25 accepts any string for the format field, but based on the codebase context (output.py uses "text", "json", "table"), only specific values are valid. Consider validating the format value and falling back to "text" or raising an error for invalid values.

✨ Suggested validation
+        format_value = os.environ.get("UNITY_MCP_FORMAT", "text")
+        if format_value not in ("text", "json", "table"):
+            raise ValueError(f"Invalid UNITY_MCP_FORMAT: {format_value}. Must be one of: text, json, table")
+        
         return cls(
             host=os.environ.get("UNITY_MCP_HOST", "127.0.0.1"),
             port=int(os.environ.get("UNITY_MCP_HTTP_PORT", "8080")),
             timeout=int(os.environ.get("UNITY_MCP_TIMEOUT", "30")),
-            format=os.environ.get("UNITY_MCP_FORMAT", "text"),
+            format=format_value,
             unity_instance=os.environ.get("UNITY_MCP_INSTANCE"),
         )
Server/src/cli/commands/animation.py (1)

63-85: Placeholder command doesn't execute.

The set-parameter command is a non-functional placeholder that only prints informational messages (lines 84-85) without executing any Unity operations. While this may be intentional per the PR description, consider either:

  1. Implementing the functionality
  2. Adding a clear warning in the help text that this is not yet implemented
  3. Hiding the command until implementation is complete
📋 Suggested help text clarification
 def set_parameter(target: str, param_name: str, value: str, param_type: str):
-    """Set an Animator parameter.
+    """Set an Animator parameter (placeholder - not yet implemented).
     
     \b
     Examples:
Server/tests/test_cli.py (1)

735-770: Remove unused mock_unity_response fixture arguments.

The mock_unity_response fixture is declared but never used in test_custom_host, test_custom_port, and test_timeout_option. These tests mock run_check_connection and run_list_instances directly, so the fixture is unnecessary.

♻️ Proposed fix
-    def test_custom_host(self, runner, mock_unity_response):
+    def test_custom_host(self, runner):
         """Test custom host option."""
         with patch("cli.main.run_check_connection", return_value=True):
             with patch("cli.main.run_list_instances", return_value={"instances": []}):
                 result = runner.invoke(cli, ["--host", "192.168.1.100", "status"])
                 assert result.exit_code == 0

-    def test_custom_port(self, runner, mock_unity_response):
+    def test_custom_port(self, runner):
         """Test custom port option."""
         with patch("cli.main.run_check_connection", return_value=True):
             with patch("cli.main.run_list_instances", return_value={"instances": []}):
                 result = runner.invoke(cli, ["--port", "9090", "status"])
                 assert result.exit_code == 0
...
-    def test_timeout_option(self, runner, mock_unity_response):
+    def test_timeout_option(self, runner):
         """Test timeout option."""
         with patch("cli.main.run_check_connection", return_value=True):
             with patch("cli.main.run_list_instances", return_value={"instances": []}):
                 result = runner.invoke(cli, ["--timeout", "60", "status"])
                 assert result.exit_code == 0
Server/src/cli/commands/audio.py (1)

96-130: Consider adding volume range validation.

Unity's AudioSource.volume is clamped between 0.0 and 1.0. Adding client-side validation would provide clearer feedback to users.

♻️ Suggested validation
 def volume(target: str, level: float, search_method: Optional[str]):
     """Set audio volume on a target's AudioSource.
     ...
     """
     config = get_config()
+    
+    if not 0.0 <= level <= 1.0:
+        print_error("Volume level must be between 0.0 and 1.0")
+        sys.exit(1)
     
     params: dict[str, Any] = {
Server/src/cli/commands/asset.py (1)

246-249: Move os import to module level.

The os import is inside the function, which is inconsistent with the module's import style. Move it to the top with other imports.

♻️ Proposed fix
 """Asset CLI commands."""

 import sys
+import os
 import json
 import click
 from typing import Optional, Any

Then remove line 247:

-    import os
     dir_path = os.path.dirname(path)
Server/src/cli/commands/prefab.py (1)

20-24: Consider constraining mode values with click.Choice.

The mode option accepts any string, but Unity's PrefabStage only supports specific modes. Using click.Choice would provide better validation and help text.

♻️ Suggested improvement
 @click.option(
     "--mode", "-m",
-    default="InIsolation",
-    help="Prefab stage mode (InIsolation)."
+    type=click.Choice(["Normal", "InIsolation", "InContext"]),
+    default="InIsolation",
+    help="Prefab stage mode."
 )
Server/src/cli/commands/ui.py (1)

43-45: Overly permissive success check may mask failures.

The condition result.get("success") or result.get("data") or result.get("result") could treat an error response as successful if it contains data or result keys. Consider using only result.get("success") for consistency with other command modules.

♻️ Proposed fix
-        if not (result.get("success") or result.get("data") or result.get("result")):
+        if not result.get("success"):
             click.echo(format_output(result, config.format))
             return

Apply this pattern to all occurrences in this file (lines 43, 108, 164, 213).

Server/src/cli/commands/scene.py (1)

224-245: Consider validating supersize range.

The help text indicates supersize should be 1-4, but no validation enforces this. Adding client-side validation would provide clearer feedback.

♻️ Suggested validation
 def screenshot(filename: Optional[str], supersize: int):
     """Capture a screenshot of the scene.
     ...
     """
     config = get_config()
+    
+    if not 1 <= supersize <= 4:
+        print_error("Supersize must be between 1 and 4")
+        sys.exit(1)
     
     params: dict[str, Any] = {"action": "screenshot"}
Server/src/cli/main.py (2)

31-36: Short option -h conflicts with Click's default --help.

Using -h for --host conflicts with the conventional -h / --help shortcut that many CLI users expect. Click doesn't add -h by default, but this could cause confusion.

Consider using a different short option
 @click.option(
-    "--host", "-h",
+    "--host", "-H",
     default="127.0.0.1",
     envvar="UNITY_MCP_HOST",
     help="MCP server host address."
 )

180-260: Repetitive command registration can be simplified with a loop.

The register_commands function repeats the same try/except pattern 13 times. This could be simplified using a loop, reducing maintenance burden and improving readability.

♻️ Suggested refactor using a loop
 def register_commands():
     """Register all command groups."""
-    try:
-        from cli.commands.gameobject import gameobject
-        cli.add_command(gameobject)
-    except ImportError:
-        pass
-    
-    try:
-        from cli.commands.component import component
-        cli.add_command(component)
-    except ImportError:
-        pass
-    
-    # ... (remaining repetitive blocks)
+    command_modules = [
+        ("gameobject", "gameobject"),
+        ("component", "component"),
+        ("scene", "scene"),
+        ("asset", "asset"),
+        ("script", "script"),
+        ("code", "code"),
+        ("editor", "editor"),
+        ("prefab", "prefab"),
+        ("material", "material"),
+        ("lighting", "lighting"),
+        ("animation", "animation"),
+        ("audio", "audio"),
+        ("ui", "ui"),
+    ]
+    
+    for module_name, command_name in command_modules:
+        try:
+            module = __import__(f"cli.commands.{module_name}", fromlist=[command_name])
+            cli.add_command(getattr(module, command_name))
+        except ImportError:
+            pass
Server/src/cli/commands/material.py (1)

6-6: Unused import Tuple.

Tuple is imported but not used in this module.

Remove unused import
-from typing import Optional, Any, Tuple
+from typing import Optional, Any
Server/src/cli/commands/gameobject.py (1)

291-294: Inconsistent type annotations on params dictionaries.

The params dictionary in modify, delete, duplicate, and move commands lacks type annotations, unlike find and create commands which use dict[str, Any].

Add consistent type annotations
-    params = {
+    params: dict[str, Any] = {
         "action": "modify",
         "target": target,
     }

Apply similar change to delete, duplicate, and move commands.

Also applies to: 354-357, 408-411, 478-485

Server/src/cli/utils/output.py (1)

5-5: Unused import Union.

Union is imported but not used in the module.

Remove unused import
-from typing import Any, Dict, List, Optional, Union
+from typing import Any, Dict, List, Optional
Server/src/cli/commands/editor.py (1)

8-8: Unused import print_info.

print_info is imported but not used in this module.

Remove unused import
-from cli.utils.output import format_output, print_error, print_success, print_info
+from cli.utils.output import format_output, print_error, print_success
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c9beaf and a9ef262.

⛔ Files ignored due to path filters (1)
  • Server/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • Server/pyproject.toml
  • Server/src/cli/CLI_USAGE_GUIDE.md
  • Server/src/cli/__init__.py
  • Server/src/cli/commands/__init__.py
  • Server/src/cli/commands/animation.py
  • Server/src/cli/commands/asset.py
  • Server/src/cli/commands/audio.py
  • Server/src/cli/commands/code.py
  • Server/src/cli/commands/component.py
  • Server/src/cli/commands/editor.py
  • Server/src/cli/commands/gameobject.py
  • Server/src/cli/commands/lighting.py
  • Server/src/cli/commands/material.py
  • Server/src/cli/commands/prefab.py
  • Server/src/cli/commands/scene.py
  • Server/src/cli/commands/script.py
  • Server/src/cli/commands/ui.py
  • Server/src/cli/main.py
  • Server/src/cli/utils/__init__.py
  • Server/src/cli/utils/config.py
  • Server/src/cli/utils/connection.py
  • Server/src/cli/utils/output.py
  • Server/src/main.py
  • Server/tests/test_cli.py
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 368
File: MCPForUnity/UnityMcpServer~/src/resources/menu_items.py:15-15
Timestamp: 2025-11-05T18:23:12.349Z
Learning: In Unity MCP, the `name` parameter in the `mcp_for_unity_resource` decorator is the external API name exposed to MCP clients (LLMs, AI agents). The command string passed to `async_send_command_with_retry` or `async_send_with_unity_instance` (e.g., "get_menu_items") is the internal command identifier that must match the C# side. These are decoupled, allowing external API naming to evolve independently of internal command routing.
Learnt from: JohanHoltby
Repo: CoplayDev/unity-mcp PR: 309
File: MCPForUnity/Editor/Helpers/ServerInstaller.cs:478-508
Timestamp: 2025-10-13T13:41:00.086Z
Learning: In the MCPForUnityTools feature (MCPForUnity/Editor/Helpers/ServerInstaller.cs), the design intentionally forces users to have only one .py file per MCPForUnityTools folder to keep file tracking simple. Package-style tools (subdirectories with __init__.py) are not supported.
📚 Learning: 2025-12-29T04:54:17.743Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 490
File: Server/pyproject.toml:33-33
Timestamp: 2025-12-29T04:54:17.743Z
Learning: Pin the fastmcp dependency to an exact version in Server/pyproject.toml (e.g., exact string 2.14.1). Avoid range pins like >=2.13.0 to prevent breaking changes affecting MCP tools. Apply the same exact-version pinning approach according to the syntax of the package tool in use (e.g., Poetry: fastmcp = '2.14.1' or equivalent exact-specifier).

Applied to files:

  • Server/pyproject.toml
📚 Learning: 2025-10-13T13:41:00.086Z
Learnt from: JohanHoltby
Repo: CoplayDev/unity-mcp PR: 309
File: MCPForUnity/Editor/Helpers/ServerInstaller.cs:478-508
Timestamp: 2025-10-13T13:41:00.086Z
Learning: In the MCPForUnityTools feature (MCPForUnity/Editor/Helpers/ServerInstaller.cs), the design intentionally forces users to have only one .py file per MCPForUnityTools folder to keep file tracking simple. Package-style tools (subdirectories with __init__.py) are not supported.

Applied to files:

  • Server/pyproject.toml
  • Server/src/cli/CLI_USAGE_GUIDE.md
  • Server/src/cli/__init__.py
📚 Learning: 2025-09-04T01:01:11.927Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 260
File: UnityMcpBridge/UnityMcpServer~/src/server_version.txt:1-1
Timestamp: 2025-09-04T01:01:11.927Z
Learning: The UnityMcpBridge project is not maintaining changelogs yet, so don't suggest adding changelog entries for version bumps.

Applied to files:

  • Server/pyproject.toml
📚 Learning: 2025-10-13T13:27:23.040Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 316
File: TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta:1-8
Timestamp: 2025-10-13T13:27:23.040Z
Learning: UnityMcpBridge is a legacy project kept for backwards compatibility; MCPForUnity is the only active Unity plugin project. GUID collisions between UnityMcpBridge and MCPForUnity are acceptable.

Applied to files:

  • Server/pyproject.toml
📚 Learning: 2025-11-05T18:23:12.349Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 368
File: MCPForUnity/UnityMcpServer~/src/resources/menu_items.py:15-15
Timestamp: 2025-11-05T18:23:12.349Z
Learning: In Unity MCP, the `name` parameter in the `mcp_for_unity_resource` decorator is the external API name exposed to MCP clients (LLMs, AI agents). The command string passed to `async_send_command_with_retry` or `async_send_with_unity_instance` (e.g., "get_menu_items") is the internal command identifier that must match the C# side. These are decoupled, allowing external API naming to evolve independently of internal command routing.

Applied to files:

  • Server/pyproject.toml
  • Server/src/main.py
  • Server/src/cli/CLI_USAGE_GUIDE.md
  • Server/src/cli/__init__.py
📚 Learning: 2025-10-03T22:11:46.002Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 301
File: docs/CUSTOM_TOOLS.md:54-62
Timestamp: 2025-10-03T22:11:46.002Z
Learning: In Unity MCP, the `description` parameter in the `mcp_for_unity_tool` decorator is technically optional but should always be included as a best practice. Without it, there's a higher chance that MCP clients will not parse the tool correctly. All Unity MCP tools should include the description in the decorator for compatibility.

Applied to files:

  • Server/pyproject.toml
  • Server/src/cli/CLI_USAGE_GUIDE.md
📚 Learning: 2025-09-05T16:22:04.960Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 265
File: README.md:204-204
Timestamp: 2025-09-05T16:22:04.960Z
Learning: In the Unity MCP project, the ServerInstaller.cs creates a symlink from ~/Library/AppSupport to ~/Library/Application Support on macOS to mitigate argument parsing and quoting issues in some MCP clients. The README documentation should use the shortened AppSupport path, not the full "Application Support" path with spaces.

Applied to files:

  • Server/src/cli/CLI_USAGE_GUIDE.md
📚 Learning: 2025-09-05T16:22:04.960Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 265
File: README.md:204-204
Timestamp: 2025-09-05T16:22:04.960Z
Learning: In the Unity MCP project, the ServerInstaller.cs creates a symlink from ~/Library/AppSupport to ~/Library/Application Support on macOS to avoid spaces in paths that can cause arg parsing and quoting issues in some MCP clients. The README documentation should use the shortened AppSupport path, not the full "Application Support" path.

Applied to files:

  • Server/src/cli/CLI_USAGE_GUIDE.md
🧬 Code graph analysis (10)
Server/src/cli/utils/connection.py (2)
Server/src/cli/main.py (1)
  • cli (70-103)
Server/src/cli/utils/config.py (2)
  • get_config (34-39)
  • CLIConfig (9-27)
Server/src/cli/main.py (16)
Server/src/cli/utils/config.py (3)
  • CLIConfig (9-27)
  • set_config (42-45)
  • get_config (34-39)
Server/src/cli/utils/output.py (4)
  • format_output (8-23)
  • print_error (176-178)
  • print_success (171-173)
  • print_info (186-188)
Server/src/cli/utils/connection.py (5)
  • run_command (99-116)
  • run_check_connection (139-141)
  • run_list_instances (188-190)
  • UnityConnectionError (13-15)
  • warn_if_remote_host (18-37)
Server/src/cli/commands/gameobject.py (1)
  • gameobject (14-16)
Server/src/cli/commands/component.py (1)
  • component (14-16)
Server/src/cli/commands/scene.py (1)
  • scene (13-15)
Server/src/cli/commands/asset.py (1)
  • asset (14-16)
Server/src/cli/commands/script.py (1)
  • script (14-16)
Server/src/cli/commands/code.py (1)
  • code (13-15)
Server/src/cli/commands/editor.py (1)
  • editor (13-15)
Server/src/cli/commands/prefab.py (1)
  • prefab (13-15)
Server/src/cli/commands/material.py (1)
  • material (14-16)
Server/src/cli/commands/lighting.py (1)
  • lighting (13-15)
Server/src/cli/commands/animation.py (1)
  • animation (13-15)
Server/src/cli/commands/audio.py (1)
  • audio (13-15)
Server/src/cli/commands/ui.py (1)
  • ui (13-15)
Server/src/cli/commands/code.py (4)
Server/src/cli/utils/config.py (1)
  • get_config (34-39)
Server/src/cli/utils/output.py (3)
  • format_output (8-23)
  • print_error (176-178)
  • print_info (186-188)
Server/src/cli/utils/connection.py (2)
  • run_command (99-116)
  • UnityConnectionError (13-15)
Server/src/cli/commands/script.py (1)
  • read (91-125)
Server/src/cli/commands/ui.py (5)
Server/src/cli/main.py (1)
  • cli (70-103)
Server/src/cli/utils/config.py (1)
  • get_config (34-39)
Server/src/cli/utils/output.py (3)
  • format_output (8-23)
  • print_error (176-178)
  • print_success (171-173)
Server/src/cli/utils/connection.py (2)
  • run_command (99-116)
  • UnityConnectionError (13-15)
Server/src/cli/commands/component.py (1)
  • component (14-16)
Server/src/cli/commands/gameobject.py (3)
Server/src/cli/utils/config.py (1)
  • get_config (34-39)
Server/src/cli/utils/output.py (3)
  • format_output (8-23)
  • print_error (176-178)
  • print_success (171-173)
Server/src/cli/utils/connection.py (2)
  • run_command (99-116)
  • UnityConnectionError (13-15)
Server/src/cli/commands/material.py (3)
Server/src/cli/utils/config.py (1)
  • get_config (34-39)
Server/src/cli/utils/output.py (3)
  • format_output (8-23)
  • print_error (176-178)
  • print_success (171-173)
Server/src/cli/utils/connection.py (2)
  • run_command (99-116)
  • UnityConnectionError (13-15)
Server/src/cli/commands/script.py (3)
Server/src/cli/utils/config.py (1)
  • get_config (34-39)
Server/src/cli/utils/output.py (2)
  • format_output (8-23)
  • print_success (171-173)
Server/src/cli/utils/connection.py (2)
  • run_command (99-116)
  • UnityConnectionError (13-15)
Server/src/cli/commands/asset.py (3)
Server/src/cli/utils/config.py (1)
  • get_config (34-39)
Server/src/cli/utils/output.py (3)
  • format_output (8-23)
  • print_error (176-178)
  • print_success (171-173)
Server/src/cli/utils/connection.py (2)
  • run_command (99-116)
  • UnityConnectionError (13-15)
Server/src/cli/utils/__init__.py (3)
Server/src/cli/utils/config.py (3)
  • CLIConfig (9-27)
  • get_config (34-39)
  • set_config (42-45)
Server/src/cli/utils/connection.py (4)
  • run_command (99-116)
  • run_check_connection (139-141)
  • run_list_instances (188-190)
  • UnityConnectionError (13-15)
Server/src/cli/utils/output.py (5)
  • format_output (8-23)
  • print_success (171-173)
  • print_error (176-178)
  • print_warning (181-183)
  • print_info (186-188)
Server/src/cli/commands/prefab.py (3)
Server/src/cli/utils/config.py (1)
  • get_config (34-39)
Server/src/cli/utils/output.py (3)
  • format_output (8-23)
  • print_error (176-178)
  • print_success (171-173)
Server/src/cli/utils/connection.py (2)
  • run_command (99-116)
  • UnityConnectionError (13-15)
🪛 markdownlint-cli2 (0.18.1)
Server/src/cli/CLI_USAGE_GUIDE.md

83-83: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


129-129: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.14.10)
Server/src/cli/commands/animation.py

33-33: Unused function argument: layer

(ARG001)


83-83: Local variable config is assigned to but never used

Remove assignment to unused variable config

(F841)

Server/src/main.py

336-336: Do not catch blind exception: Exception

(BLE001)


337-337: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


356-356: Do not catch blind exception: Exception

(BLE001)

Server/src/cli/utils/connection.py

29-29: Possible binding to all interfaces

(S104)


81-85: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


81-85: Avoid specifying long messages outside the exception class

(TRY003)


87-90: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


87-90: Avoid specifying long messages outside the exception class

(TRY003)


92-94: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


92-94: Avoid specifying long messages outside the exception class

(TRY003)


95-95: Do not catch blind exception: Exception

(BLE001)


96-96: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


96-96: Avoid specifying long messages outside the exception class

(TRY003)


135-135: Do not catch blind exception: Exception

(BLE001)


182-183: try-except-continue detected, consider logging the exception

(S112)


182-182: Do not catch blind exception: Exception

(BLE001)


185-185: Avoid specifying long messages outside the exception class

(TRY003)

Server/src/cli/commands/ui.py

91-91: Unused function argument: position

(ARG001)


147-147: Unused function argument: text

(ARG001)


195-195: Unused function argument: sprite

(ARG001)

Server/src/cli/commands/audio.py

31-31: Unused function argument: clip

(ARG001)

Server/tests/test_cli.py

738-738: Unused method argument: mock_unity_response

(ARG002)


745-745: Unused method argument: mock_unity_response

(ARG002)


764-764: Unused method argument: mock_unity_response

(ARG002)

Server/src/cli/utils/__init__.py

18-31: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

Server/src/cli/utils/output.py

188-188: String contains ambiguous (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?

(RUF001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: Sourcery review
🔇 Additional comments (34)
Server/pyproject.toml (2)

39-39: LGTM! Click dependency added appropriately.

The Click dependency is added with a reasonable version constraint. Unlike fastmcp (which requires exact pinning per project learnings), Click has strong semantic versioning guarantees and backwards compatibility, making the >=8.1.0 constraint appropriate for a CLI framework.

Based on learnings, only fastmcp requires exact version pinning to prevent breaking changes affecting MCP tools.


55-55: LGTM! Console script entry point configured correctly.

The new unity-mcp entry point is properly configured to invoke the CLI main function.

Server/src/cli/CLI_USAGE_GUIDE.md (1)

1-734: LGTM! Comprehensive and well-structured CLI documentation.

The usage guide is thorough, covering installation, syntax, common pitfalls, and extensive command examples. The documentation clearly explains argument vs. option syntax and multi-value parameter usage, which helps prevent common CLI mistakes.

Note: Static analysis warnings about missing language specifiers on lines 83 and 129 are false positives—those blocks show expected output and abstract patterns, not executable code.

Server/src/cli/utils/connection.py (6)

13-15: LGTM! Custom exception defined appropriately.


18-37: LGTM! Security warning appropriately implemented.

The function correctly warns users when connecting to non-localhost servers. The static analysis warning about "0.0.0.0" (S104) is a false positive—the code checks whether the configured host equals "0.0.0.0", it doesn't bind to that address.


99-116: LGTM! Synchronous wrapper implemented correctly.


119-136: LGTM! Connection check implemented appropriately.

The broad exception catch on line 135 is acceptable for a health check function—returning False on any error is the correct behavior for a simple connectivity test.


139-141: LGTM! Synchronous wrapper implemented correctly.


188-190: LGTM! Synchronous wrapper implemented correctly.

Server/src/cli/__init__.py (1)

1-3: LGTM! Package initializer properly configured.

The CLI package is initialized with appropriate docstring and version metadata.

Server/src/cli/commands/__init__.py (1)

1-3: LGTM! Commands package initializer properly configured.

The commands package is correctly initialized with a clear note that command registration happens in main.py, consistent with the pluggable architecture described in the PR.

Server/src/cli/commands/code.py (1)

63-64: code.py is correct; the bug is in script.py instead.

Line 63 correctly checks for "contents" (plural), which matches the actual response structure from the manage_script backend (see Server/src/services/tools/script_apply_edits.py:630). However, the related read command in Server/src/cli/commands/script.py:117 incorrectly checks for "content" (singular), which will cause it to fail silently and fall back to formatted output instead of displaying the script content directly. The script.py command should use "contents" to match the backend response and the code.py implementation.

Likely an incorrect or invalid review comment.

Server/tests/test_cli.py (2)

1-27: Well-structured test fixtures and imports.

The test suite is well-organized with clear section markers, appropriate fixtures for mocking CLI behavior, and comprehensive coverage of the CLI surface area.


806-874: Good integration-style test coverage with realistic workflows.

The integration tests properly simulate multi-step workflows (create → modify → delete) with realistic response payloads, verifying both successful exit codes and expected output content.

Server/src/cli/commands/asset.py (1)

1-16: Clean module structure with consistent patterns.

The asset command group follows a consistent pattern with proper error handling, JSON validation for properties, and appropriate confirmation prompts for destructive operations.

Server/src/cli/commands/prefab.py (1)

1-16: Well-implemented prefab command group.

The prefab commands follow the established CLI patterns with consistent error handling and appropriate options for each operation.

Server/src/cli/utils/__init__.py (1)

1-31: Clean re-export module consolidating CLI utilities.

The __init__.py properly centralizes imports for easier consumption by command modules. The __all__ list comprehensively covers the public API.

Server/src/cli/commands/scene.py (2)

1-16: Comprehensive scene command group with good option coverage.

The scene commands provide useful functionality with sensible defaults and consistent error handling patterns.


99-138: Smart scene loading with appropriate path/name detection.

The load command intelligently differentiates between paths (ending in .unity) and scene names, with proper validation for build index parsing.

Server/src/cli/commands/script.py (3)

1-16: LGTM - Clean module structure and imports.

The module setup follows the established patterns from other CLI command modules with appropriate imports and a well-defined Click group.


179-183: Good JSON validation with clear error messaging.

The edit command properly validates JSON input before attempting the command, providing a clear error message on parse failure.


218-222: Unused level parameter in validate command.

The level option is defined and parsed but not included in the params dictionary sent to the backend.

🐛 Proposed fix
     params: dict[str, Any] = {
         "uri": path,
+        "level": level,
         "include_diagnostics": True,
     }

Likely an incorrect or invalid review comment.

Server/src/cli/main.py (2)

106-134: Status command implementation is well-structured.

The status command provides useful feedback about server connectivity and displays connected Unity instances with relevant details (project, version, truncated hash). Good use of print_info for non-connected state.


163-170: Import inside function - acceptable for rarely-used command.

The json import inside raw_command is unconventional but acceptable since this is likely a power-user command that won't be called frequently.

Server/src/cli/commands/component.py (3)

1-16: LGTM - Standard module structure.

Clean imports and group definition following established patterns.


137-142: Good fallback strategy for value parsing.

The silent fallback to string when JSON parsing fails is appropriate here, as users may pass simple string values that don't need JSON encoding.


194-200: No action needed—backend handler supports both property signatures.

The manage_components backend handler already supports both the single property format (property and value fields) and batch properties format (properties dict). Integration tests confirm both signatures work correctly:

  • test_manage_components_set_property_single validates the single property path
  • test_manage_components_set_property_multiple validates the batch properties path

The implementation explicitly handles both cases in sequence, so there is no API incompatibility.

Server/src/cli/commands/material.py (1)

141-149: Robust 3-tier value parsing.

Good approach: try JSON first for complex types, then float for numeric values, then fall back to string. This provides flexibility for different property types.

Server/src/cli/commands/gameobject.py (1)

430-497: Well-designed relative move command.

The move command has good directional options and clear separation of world/local space. The required --reference and --direction options ensure the command has the necessary context.

Server/src/cli/utils/output.py (3)

26-31: Good defensive JSON serialization.

The fallback to error object on serialization failure is a good defensive pattern that prevents crashes while providing debugging information.


60-70: Large list truncation is well-implemented.

Shows first 5 and last 5 items with a clear indication of how many items are omitted. This balances information density with readability.


186-188: Intentional use of Unicode information symbol.

The static analysis warning about the ambiguous (INFORMATION SOURCE) character is a false positive - this is the intended emoji for the info message indicator. The character renders correctly in modern terminals.

Server/src/cli/commands/editor.py (2)

63-132: Well-designed console command with dual functionality.

The console command cleanly handles both reading and clearing the console using the --clear flag, with early return after clear operation. The multiple filtering options (type, count, filter text, stacktrace) provide good flexibility.


266-299: Tests command implementation looks good.

The test runner command appropriately handles optional timeout and mode selection. The results are formatted according to the global format setting.

Comment on lines +33 to +60
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,
}

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unused layer parameter - likely missing from params.

The layer parameter is accepted (line 33) but never used in the command execution. It's not included in the params dict sent to Unity. This is likely a bug - if layer support is intended, add it to params. If not yet implemented, consider removing the option or adding a TODO comment.

🎯 Proposed fix if layer should be used
     params: dict[str, Any] = {
         "action": "set_property",
         "target": target,
         "componentType": "Animator",
         "property": "Play",
         "value": state_name,
+        "layer": layer,
     }

Or if not yet implemented:

 @click.option(
     "--layer", "-l",
     default=0,
     type=int,
-    help="Animator layer."
+    help="Animator layer (not yet implemented)."
 )

Based on static analysis hints.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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,
}
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)
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)
🧰 Tools
🪛 Ruff (0.14.10)

33-33: Unused function argument: layer

(ARG001)

🤖 Prompt for AI Agents
In @Server/src/cli/commands/animation.py around lines 33 - 60, The play function
accepts a layer parameter but never sends it to Unity; update the params dict in
play to include the layer (e.g., params["layer"] = layer) so the
"manage_components" call receives the layer value, or if layer support is not
yet intended remove the layer parameter from play's signature or add a clear
TODO comment and validation; locate the play function and modify the params
construction (or signature) accordingly to ensure the parameter is used or
intentionally omitted.

"action": "set_property",
"target": target,
"componentType": "Animator",
"property": "Play",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's look at the animation.py file around line 48 with context
cat -n Server/src/cli/commands/animation.py | sed -n '40,60p'

Repository: CoplayDev/unity-mcp

Length of output: 782


🏁 Script executed:

#!/bin/bash
# Search for Animator usage and Play method/property in the codebase
rg -nP -C3 'Animator.*Play|Play.*property|property.*Play' --type=py

Repository: CoplayDev/unity-mcp

Length of output: 924


🏁 Script executed:

#!/bin/bash
# Look for Unity/MCP API documentation or definitions
find . -type f \( -name "*.py" -o -name "*.md" -o -name "*.json" \) | xargs rg -l "Animator|manage_components" | head -20

Repository: CoplayDev/unity-mcp

Length of output: 734


🏁 Script executed:

#!/bin/bash
# Check the manage_components implementation to understand the API
rg -nP -C5 'def manage_components|class.*Animator|Play' --type=py | head -100

Repository: CoplayDev/unity-mcp

Length of output: 6923


🏁 Script executed:

#!/bin/bash
# Check the manage_components.py implementation
cat -n Server/src/services/tools/manage_components.py | head -150

Repository: CoplayDev/unity-mcp

Length of output: 6004


🏁 Script executed:

#!/bin/bash
# Search for Play handling in C# or backend code
find . -type f -name "*.cs" | xargs rg -l "Play|Animator" | head -10

Repository: CoplayDev/unity-mcp

Length of output: 10282


🏁 Script executed:

#!/bin/bash
# Look for any API documentation or schema that defines valid properties
rg -nP "Play|Animator.*property|property.*Animator" --type=py | grep -E "(define|schema|valid|property)" | head -20

Repository: CoplayDev/unity-mcp

Length of output: 409


🏁 Script executed:

#!/bin/bash
# Search for manage_components in C# code
find . -type f -name "*.cs" | xargs rg -l "manage_components" | head -10

Repository: CoplayDev/unity-mcp

Length of output: 9512


🏁 Script executed:

#!/bin/bash
# Look for Animator handling in C# code
find MCPForUnity -type f -name "*.cs" | xargs rg -A10 -B2 "Animator|Play" | head -150

Repository: CoplayDev/unity-mcp

Length of output: 12347


🏁 Script executed:

#!/bin/bash
# Search for property handling/reflection in C# code
find MCPForUnity -type f -name "*.cs" | xargs rg -l "SetProperty|setProperty|property.*reflection" | head -10

Repository: CoplayDev/unity-mcp

Length of output: 300


🏁 Script executed:

#!/bin/bash
# Read the ManageComponents.cs file
wc -l MCPForUnity/Editor/Tools/ManageComponents.cs

Repository: CoplayDev/unity-mcp

Length of output: 111


🏁 Script executed:

#!/bin/bash
# Get the key parts of ManageComponents.cs
cat -n MCPForUnity/Editor/Tools/ManageComponents.cs | head -200

Repository: CoplayDev/unity-mcp

Length of output: 9723


🏁 Script executed:

#!/bin/bash
# Read ComponentOps.cs to understand property handling
cat -n MCPForUnity/Editor/Helpers/ComponentOps.cs | head -250

Repository: CoplayDev/unity-mcp

Length of output: 11688


🏁 Script executed:

#!/bin/bash
# Continue reading ManageComponents.cs to see full SetProperty implementation and any special handling
cat -n MCPForUnity/Editor/Tools/ManageComponents.cs | sed -n '200,330p'

Repository: CoplayDev/unity-mcp

Length of output: 5399


🏁 Script executed:

#!/bin/bash
# Search for any special Animator handling or custom properties
rg -n "Animator|Play" MCPForUnity/Editor/Tools/ManageComponents.cs MCPForUnity/Editor/Helpers/ComponentOps.cs

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if there are any integration tests that show expected behavior for Animator.Play
find . -type f -name "*test*.cs" -o -name "*Test*.cs" | xargs rg -l "Animator.*Play|animation.*play" -i 2>/dev/null | head -5

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if there's any custom Animator implementation or extension in MCPForUnity
find MCPForUnity -type f -name "*.cs" | xargs rg -l "Animator" | head -10

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for any tests or documentation about animation commands
find . -type f \( -name "*animation*" -o -name "*animator*" \) -type f | grep -E "\.(cs|py|md)$"

Repository: CoplayDev/unity-mcp

Length of output: 101


🏁 Script executed:

#!/bin/bash
# Check CLI_USAGE_GUIDE.md for animation command documentation
grep -A 20 -B 5 "animation\|animator" Server/src/cli/CLI_USAGE_GUIDE.md -i

Repository: CoplayDev/unity-mcp

Length of output: 1084


🏁 Script executed:

#!/bin/bash
# Let's examine the full animation.py file to understand the context
cat -n Server/src/cli/commands/animation.py

Repository: CoplayDev/unity-mcp

Length of output: 3080


🏁 Script executed:

#!/bin/bash
# Check if Animator is extended or wrapped anywhere in MCPForUnity
find MCPForUnity -type f -name "*.cs" | xargs rg -B5 -A10 "class.*Animator|Animator.*Play" 2>/dev/null | head -100

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check the test_manage_components.py to see what the expected behavior is
cat -n Server/tests/integration/test_manage_components.py | head -200

Repository: CoplayDev/unity-mcp

Length of output: 7161


🏁 Script executed:

#!/bin/bash
# Search for any method invocation or custom extension for Animator.Play in C#
find MCPForUnity -type f -name "*.cs" | xargs rg -B3 -A3 "\.Play\(" | grep -A3 -B3 -i animator

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if there's a way to invoke methods (not just properties) on components
rg -n "MethodInfo|InvokeMethod|Invoke" MCPForUnity/Editor/Helpers/ComponentOps.cs MCPForUnity/Editor/Tools/ManageComponents.cs

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Let's verify: does Animator actually have a public Play property?
web_search "Unity Animator Play property vs method C# API"

Repository: CoplayDev/unity-mcp

Length of output: 113


🌐 Web query:

Unity Animator Play property C# API documentation

💡 Result:

Animator.Play (C#) — signatures and description

  • Signatures:

    • void Play(string stateName, int layer = -1, float normalizedTime = float.NegativeInfinity);
    • void Play(int stateNameHash, int layer = -1, float normalizedTime = float.NegativeInfinity). [1]
  • Parameters:

    • stateName: state full name (include layer name, e.g. "Base Layer.Bounce") — string version calls Animator.StringToHash internally.
    • stateNameHash: precomputed hash (use 0 to change current state time).
    • layer: layer index (-1 to search first match).
    • normalizedTime: start time as 0..1 (NegativeInfinity means use default behavior). [1][2]
  • Behavior:

    • Plays the specified Animator state immediately; normalizedTime controls where in the state to start. Using the string form will convert to a hash internally; for frequent calls use a precomputed hash for better performance. If transitions exist, blending behavior depends on transition settings. [1][2]

References:
[1] Unity Scripting API — Animator.Play (Unity Docs).
[2] Unity Scripting API — Animator.Play (Unity Docs, alternate version / examples).


🏁 Script executed:

#!/bin/bash
# Final check: search for any MethodInfo or method invocation support in the codebase
find MCPForUnity Server -type f -name "*.cs" -o -name "*.py" | xargs rg -l "MethodInfo|InvokeMethod|\.Invoke\(" | head -10

Repository: CoplayDev/unity-mcp

Length of output: 382


🏁 Script executed:

#!/bin/bash
# Check if there's an alternative approach or fallback for animation in the codebase
rg -n "animation|animator" Server/src/cli/commands/ -i --type=py | grep -i "play\|state"

Repository: CoplayDev/unity-mcp

Length of output: 946


"Play" is a method, not a property—the animation play command will fail.

Animator.Play() is a void method, not a settable property. The current implementation attempts to set a property named "Play" via reflection, which will fail at runtime with "Property or field 'Play' not found on component 'Animator'."

The manage_components backend only supports setting properties and fields, not invoking methods. Either implement method invocation support in the manage_components tool or use an alternative approach (e.g., a custom animation-specific tool similar to ManageVFX.cs).

🤖 Prompt for AI Agents
In @Server/src/cli/commands/animation.py at line 48, The PR is trying to set
"Play" as a property but Animator.Play is a method; update the code that emits
the component change (the entry with "property": "Play") to either (A) call a
method-invocation path in the manage_components backend (implement method
invocation support there and handle calling Animator.Play(...)) or (B) replace
this manage_components call with an animation-specific tool (similar to
ManageVFX.cs) that invokes Animator.Play on the target component; locate the
emitter that writes the "property": "Play" entry and change it to emit a
method-invoke request (or swap to the custom animation tool) and wire up any
required parameters for Animator.Play accordingly.

Comment on lines +18 to +57
@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 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The --clip option is declared but never used.

The clip parameter is accepted but never included in the params dictionary sent to Unity. If specifying an audio clip is a valid use case, it should be added to the payload; otherwise, remove the option.

🔧 Proposed fix to use the clip option
     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
🧰 Tools
🪛 Ruff (0.14.10)

31-31: Unused function argument: clip

(ARG001)

🤖 Prompt for AI Agents
In @Server/src/cli/commands/audio.py around lines 18 - 57, The play command
accepts a --clip option but never uses it; update the play function so that when
clip is not None you add it to the params payload (e.g., params["clip"] = clip)
before calling run_command("manage_components", params, config); if specifying a
clip is not a supported use case, remove the --clip Click option and the clip
parameter from play instead.

@@ -0,0 +1,71 @@
"""Code CLI commands - search and read source code."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Docstring overpromises functionality.

The module docstring mentions "search and read source code" but only the read command is implemented. Consider updating the docstring to reflect the actual functionality or add a placeholder/TODO for the search command.

📝 Suggested docstring fix
-"""Code CLI commands - search and read source code."""
+"""Code CLI commands - read source code."""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""Code CLI commands - search and read source code."""
"""Code CLI commands - read source code."""
🤖 Prompt for AI Agents
In @Server/src/cli/commands/code.py at line 1, The module docstring claims
"search and read source code" but only the read command is implemented; update
the top-level docstring in Server/src/cli/commands/code.py to accurately reflect
implemented functionality (e.g., "Read source code" or "Read source code (search
not yet implemented)") or add a clear TODO note about implementing the search
command, and mention the implemented symbol 'read' so reviewers can verify
consistency.

Comment on lines +181 to +189
# 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(",")]
for component in component_list:
run_command("manage_components", {
"action": "add",
"target": name,
"componentType": component,
}, config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Component addition loop lacks individual error handling.

If any component fails to add, the error isn't reported and subsequent components aren't attempted. Consider wrapping each run_command in try/except or collecting failures.

🔧 Suggested improvement with error collection
         # 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:
-                run_command("manage_components", {
-                    "action": "add",
-                    "target": name,
-                    "componentType": component,
-                }, config)
+                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)}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 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(",")]
for component in component_list:
run_command("manage_components", {
"action": "add",
"target": name,
"componentType": component,
}, 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)}")
🤖 Prompt for AI Agents
In @Server/src/cli/commands/gameobject.py around lines 181 - 189, The component
addition loop currently calls run_command("manage_components", ...) for each
entry in component_list without per-item error handling; wrap each run_command
call in a try/except (or check its return value) inside the for-loop that
iterates component_list, log or collect failures (including component name and
error/response) and continue to the next component, and after the loop report
any collected failures (or raise/return a consolidated error) so callers can see
which components failed; references: components, result, component_list,
run_command, "manage_components", name, config.

Comment on lines +147 to +150
params: dict[str, Any] = {
"action": "delete",
"name": path.split("/")[-1].replace(".cs", ""),
"path": "/".join(path.split("/")[:-1]) or "Assets",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same path parsing fragility as in read command.

This uses the same path-splitting logic with the same edge case concerns.

🤖 Prompt for AI Agents
In @Server/src/cli/commands/script.py around lines 147 - 150, The params
construction is using fragile string-splitting on path; update the logic that
builds params (the "params" dict in the delete command) to use robust path
utilities instead of path.split: derive the script name with
pathlib.Path(path).stem (or os.path.splitext(os.path.basename(path))[0]) and
derive the parent with pathlib.Path(path).parent.as_posix() (or
os.path.dirname(path)), falling back to "Assets" when parent is empty or ".";
then assign those values into the existing params dict ("action": "delete",
"name", "path") so behavior matches the safer parsing used in the read command.

Comment on lines +84 to +132
@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,
}, 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The --position and --text options are declared but never used in create_text.

The position tuple is accepted but not applied to the RectTransform, and while text is correctly used in line 125, the position functionality is missing. The position should be applied after component creation.

🔧 Proposed fix to apply position
         # Step 3: Set text content
         run_command("manage_components", {
             "action": "set_property",
             "target": name,
             "componentType": "TextMeshProUGUI",
             "property": "text",
             "value": text,
         }, config)
+        
+        # Step 4: Set position if specified
+        if position != (0, 0):
+            run_command("manage_components", {
+                "action": "set_property",
+                "target": name,
+                "componentType": "RectTransform",
+                "property": "anchoredPosition",
+                "value": {"x": position[0], "y": position[1]},
+            }, config)
🧰 Tools
🪛 Ruff (0.14.10)

91-91: Unused function argument: position

(ARG001)

🤖 Prompt for AI Agents
In @Server/src/cli/commands/ui.py around lines 84 - 132, create_text accepts a
position tuple but never applies it to the RectTransform; after adding the
TextMeshProUGUI component (in create_text) call run_command("manage_components",
{ "action": "set_property", "target": name, "componentType": "RectTransform",
"property": "anchoredPosition", "value": list(position) }, config) (or use
"anchoredPosition3D"/"localPosition" if your backend expects 3D) and ensure you
pass the position variable (converted to the expected list/format) so the
RectTransform receives the anchored position.

Comment on lines +142 to +180
@click.option(
"--text", "-t",
default="Button",
help="Button label text."
)
def create_button(name: str, parent: str, text: str):
"""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)

click.echo(format_output(result, config.format))
print_success(f"Created Button: {name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The --text option for create-button is declared but never used.

The text parameter is accepted but not applied to the button. Unity UI Buttons typically have a child Text component for the label. Either implement the text functionality or remove the option to avoid confusion.

🔧 Proposed fix to implement button text
         # 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 text element if text specified
+        if text and text != "Button":
+            text_name = f"{name}_Text"
+            run_command("manage_gameobject", {
+                "action": "create",
+                "name": text_name,
+                "parent": name,
+            }, config)
+            run_command("manage_components", {
+                "action": "add",
+                "target": text_name,
+                "componentType": "TextMeshProUGUI",
+            }, config)
+            run_command("manage_components", {
+                "action": "set_property",
+                "target": text_name,
+                "componentType": "TextMeshProUGUI",
+                "property": "text",
+                "value": text,
+            }, config)
🧰 Tools
🪛 Ruff (0.14.10)

147-147: Unused function argument: text

(ARG001)

Comment on lines +190 to +228
@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)

click.echo(format_output(result, config.format))
print_success(f"Created Image: {name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The --sprite option for create-image is declared but never used.

The sprite parameter is accepted but not applied to the Image component. If a sprite path is provided, it should be set on the Image component.

🔧 Proposed fix to apply sprite
         # Step 2: Add Image component
         run_command("manage_components", {
             "action": "add",
             "target": name,
             "componentType": "Image",
         }, config)
+        
+        # Step 3: Set sprite if specified
+        if sprite:
+            run_command("manage_components", {
+                "action": "set_property",
+                "target": name,
+                "componentType": "Image",
+                "property": "sprite",
+                "value": sprite,
+            }, config)
🧰 Tools
🪛 Ruff (0.14.10)

195-195: Unused function argument: sprite

(ARG001)

🤖 Prompt for AI Agents
In @Server/src/cli/commands/ui.py around lines 190 - 228, The --sprite option is
accepted by create_image but never applied; after the existing run_command call
that adds the Image component in create_image, if sprite is not None call
run_command again to set the Image component's sprite property on the created
GameObject (use the same config and target name), e.g. invoke the
manage_components command to set the Image component's "sprite" (or include it
in the add call as a properties/payload field) so the provided sprite path is
applied to the component.

Comment on lines +18 to +27
@classmethod
def from_env(cls) -> "CLIConfig":
"""Create config from environment variables."""
return cls(
host=os.environ.get("UNITY_MCP_HOST", "127.0.0.1"),
port=int(os.environ.get("UNITY_MCP_HTTP_PORT", "8080")),
timeout=int(os.environ.get("UNITY_MCP_TIMEOUT", "30")),
format=os.environ.get("UNITY_MCP_FORMAT", "text"),
unity_instance=os.environ.get("UNITY_MCP_INSTANCE"),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling for environment variable parsing.

Lines 23-24 convert environment variables to integers without error handling. If UNITY_MCP_HTTP_PORT or UNITY_MCP_TIMEOUT contain invalid values, the conversion will raise ValueError and crash the application.

Consider adding try-except blocks or validation to provide clearer error messages.

🛡️ Suggested improvement
     @classmethod
     def from_env(cls) -> "CLIConfig":
         """Create config from environment variables."""
+        try:
+            port = int(os.environ.get("UNITY_MCP_HTTP_PORT", "8080"))
+        except ValueError:
+            raise ValueError(f"Invalid UNITY_MCP_HTTP_PORT value: {os.environ.get('UNITY_MCP_HTTP_PORT')}")
+        
+        try:
+            timeout = int(os.environ.get("UNITY_MCP_TIMEOUT", "30"))
+        except ValueError:
+            raise ValueError(f"Invalid UNITY_MCP_TIMEOUT value: {os.environ.get('UNITY_MCP_TIMEOUT')}")
+        
         return cls(
             host=os.environ.get("UNITY_MCP_HOST", "127.0.0.1"),
-            port=int(os.environ.get("UNITY_MCP_HTTP_PORT", "8080")),
-            timeout=int(os.environ.get("UNITY_MCP_TIMEOUT", "30")),
+            port=port,
+            timeout=timeout,
             format=os.environ.get("UNITY_MCP_FORMAT", "text"),
             unity_instance=os.environ.get("UNITY_MCP_INSTANCE"),
         )
🤖 Prompt for AI Agents
In @Server/src/cli/utils/config.py around lines 18 - 27, The from_env
classmethod on CLIConfig currently casts UNITY_MCP_HTTP_PORT and
UNITY_MCP_TIMEOUT to int without validation, which can raise ValueError; wrap
the int conversions in validation/error handling inside CLIConfig.from_env
(catch ValueError/TypeError), provide a clear error message including the env
var name and invalid value (and optionally fallback to defaults), and re-raise
or exit gracefully so the app doesn’t crash with an opaque traceback; reference
the symbols CLIConfig.from_env, UNITY_MCP_HTTP_PORT, and UNITY_MCP_TIMEOUT when
locating and updating the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant