Skip to content

Conversation

@msanatan
Copy link
Member

@msanatan msanatan commented Jan 21, 2026

Builds of @Scriptwonder's CLI, but formats the files and include some fixes for the main branch

Summary by Sourcery

Introduce a dedicated Unity MCP command-line interface and corresponding HTTP endpoints for issuing commands and managing instances, while tightening handling of configuration and built‑in tools.

New Features:

  • Add a full-featured unity-mcp CLI with grouped commands for game objects, components, scenes, assets, scripts, shaders, materials, editor control, VFX, audio, lighting, UI, batch execution, and instance management.
  • Expose new REST endpoints for CLI usage to send generic commands and list connected Unity instances.
  • Provide a comprehensive CLI usage guide and user-facing documentation under the Server and docs trees.
  • Register a new console entry point for the CLI in the server package.

Bug Fixes:

  • Harden HTTP port parsing from environment variables to avoid crashes on invalid values.
  • Prevent global registration of custom tools that shadow built-in tools.
  • Update Rider configurator MCP config paths to match the GitHub Copilot IntelliJ locations.
  • Adjust default editor preference for project-scoped tools local HTTP toggle to be disabled by default.

Enhancements:

  • Improve tool-result pruning script formatting and robustness.
  • Tweak Unity connection retry configuration to respect minimum retry counts.

Documentation:

  • Add detailed CLI usage and command reference documentation for both AI assistants and human users.

Tests:

  • Add extensive unit and integration-style tests covering CLI configuration, output formatting, connection helpers, and the majority of CLI subcommands and error paths.

Summary by CodeRabbit

  • New Features

    • Added a full-feature CLI (many command groups) and a new unity-mcp entry point for local Unity management.
    • Added batch execution and instance management.
  • Documentation

    • Added comprehensive CLI usage guides and reference docs.
  • Bug Fixes

    • Adjusted default UI toggle behavior for project-scoped tools.
    • Updated IDE configuration paths.
  • Chores

    • Added Click runtime dependency.
  • Tests

    • Added extensive CLI unit test suite.

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

Scriptwonder and others added 21 commits January 10, 2026 22:09
- 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
* Log a message with implicit URI changes

Small update for CoplayDev#542

* Log a message with implicit URI changes

Small update for CoplayDev#542

* Add helper scripts to update forks

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

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

Small update for CoplayDev#542

* Update docker container to default to stdio

Replaces CoplayDev#541
- Fix RiderConfigurator to use correct GitHub Copilot config path:
  - Windows: %LOCALAPPDATA%\github-copilot\intellij\mcp.json
  - macOS: ~/Library/Application Support/github-copilot/intellij/mcp.json
  - Linux: ~/.config/github-copilot/intellij/mcp.json
- Add mcp.json for GitHub MCP Registry support:
  - Enables users to install via coplaydev/unity-mcp
  - Uses uvx with mcpforunityserver from PyPI
@msanatan msanatan self-assigned this Jan 21, 2026
@msanatan msanatan changed the title Cli Add CLI Jan 21, 2026
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 21, 2026

Reviewer's Guide

Adds a fully featured unity-mcp Click-based CLI and HTTP endpoints for driving Unity via MCP, including rich subcommand groups, connection/config/output utilities, extensive tests and docs, plus minor server/runtime fixes and configuration tweaks.

Sequence diagram for executing a unity-mcp CLI command via HTTP MCP server

sequenceDiagram
    actor User
    participant Shell as Shell
    participant CLI as unity_mcp_cli
    participant Conn as cli_utils_connection
    participant HTTP as FastAPI_http_server
    participant Hub as PluginHub
    participant Unity as Unity_instance

    User->>Shell: type "unity-mcp gameobject create \n\"MyCube\" --primitive Cube"
    Shell->>CLI: invoke cli.main.main()

    CLI->>CLI: parse global options\n(host, port, format, instance)
    CLI->>CLI: build params for manage_gameobject
    CLI->>Conn: run_command("manage_gameobject", params, config)

    Conn->>HTTP: POST /api/command\n{ type: manage_gameobject,\n  params: {...},\n  unity_instance? }

    HTTP->>Hub: PluginHub.get_sessions()
    Hub-->>HTTP: sessions (session_id -> details)
    HTTP->>HTTP: select session_id\n(matching unity_instance or first)

    HTTP->>Hub: send_command(session_id,\n  command_type, params)
    Hub->>Unity: execute MCP tool\n(manage_gameobject)
    Unity-->>Hub: tool result JSON
    Hub-->>HTTP: MCPResponse / dict
    HTTP-->>Conn: 200 OK + JSON body

    Conn-->>CLI: parsed result dict
    CLI->>CLI: format_output(result, config.format)
    CLI-->>Shell: formatted output
    Shell-->>User: display result
Loading

Class diagram for CLI core configuration, connection, and output utilities

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 {
        +UnityConnectionError
        +warn_if_remote_host(config CLIConfig) void
        +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
    }

    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) void
        +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) void
        +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) void
        +delete(target str, search_method str, force bool) void
        +duplicate(target str, name str, offset tuple, search_method str) void
        +move(target str, reference str, direction str, distance float, local bool, search_method str) void
    }

    class SceneCommands {
        +scene()
        +hierarchy(parent str, max_depth int, include_transform bool, limit int, cursor int) void
        +active() void
        +load(scene str, by_index bool) void
        +save(path str) void
        +create(name str, path str) void
        +build_settings() void
        +screenshot(filename str, supersize int) void
    }

    class EditorCommands {
        +editor()
        +play() void
        +pause() void
        +stop() void
        +console(log_types tuple, count int, filter_text str, stacktrace bool, clear bool) void
        +add_tag(tag_name str) void
        +remove_tag(tag_name str) void
        +add_layer(layer_name str) void
        +remove_layer(layer_name str) void
        +set_tool(tool_name str) void
        +execute_menu(menu_path str) void
        +run_tests(mode str, async_mode bool, wait int, details bool, failed_only bool) void
        +poll_test(job_id str, wait int, details bool, failed_only bool) void
        +refresh(mode str, scope str, compile bool, no_wait bool) void
        +custom_tool(tool_name str, params str) void
    }

    class AssetCommands {
        +asset()
        +search(pattern str, path str, filter_type str, limit int, page int) void
        +info(path str, preview bool) void
        +create(path str, asset_type str, properties str) void
        +delete(path str, force bool) void
        +duplicate(source str, destination str) void
        +move(source str, destination str) void
        +rename(path str, new_name str) void
        +import_asset(path str) void
        +mkdir(path str) void
    }

    class MaterialCommands {
        +material()
        +info(path str) void
        +create(path str, shader str, properties str) void
        +set_color(path str, r float, g float, b float, a float, property str) void
        +set_property(path str, property_name str, value str) void
        +assign(material_path str, target str, search_method str, slot int, mode str) void
        +set_renderer_color(target str, r float, g float, b float, a float, search_method str, mode str) void
    }

    CLIConfig <.. ConfigModule : used by
    CLIConfig <.. ConnectionModule : used by
    CLIConfig <.. OutputModule : used by
    Context o-- CLIConfig

    MainCLI ..> CLIConfig : config
    MainCLI ..> Context : uses
    MainCLI ..> ConnectionModule : uses
    MainCLI ..> OutputModule : uses

    GameobjectCommands ..> ConnectionModule : run_command
    GameobjectCommands ..> OutputModule : format_output, print_*

    SceneCommands ..> ConnectionModule
    SceneCommands ..> OutputModule

    EditorCommands ..> ConnectionModule
    EditorCommands ..> OutputModule

    AssetCommands ..> ConnectionModule
    AssetCommands ..> OutputModule

    MaterialCommands ..> ConnectionModule
    MaterialCommands ..> OutputModule
Loading

File-Level Changes

Change Details Files
Expose HTTP endpoints for CLI commands and Unity instance discovery, and harden HTTP port configuration.
  • Add POST /api/command route that validates request body, selects Unity session (by hash/project or first available), and forwards commands via PluginHub.send_command.
  • Add GET /api/instances route that returns a normalized list of connected Unity sessions for use by the CLI.
  • Refactor HTTP port selection to safely parse UNITY_MCP_HTTP_PORT with logging on invalid values and reuse that parsed value for later host/port configuration.
Server/src/main.py
Introduce a Click-based Unity MCP CLI with modular command groups for editor/scene/gameobject/assets/materials/scripts/shaders/VFX, including connection/config/output utilities.
  • Create cli.main entrypoint with global options (host, port, timeout, format, instance, verbose), status/instances/raw commands, and dynamic registration of subcommand modules.
  • Implement CLI config, connection, and output helpers including env-based config, HTTP client wrappers to /api/command, /api/instances, /plugin/sessions, and text/json/table formatting with convenience printing helpers.
  • Add command groups implementing concrete behaviors for gameobjects, components, scenes, assets, scripts, shaders, editor, prefabs, materials, lighting, animation, audio, UI, VFX, batch, code, and instance management, each translating CLI flags/args into appropriate MCP tool invocations.
Server/src/cli/main.py
Server/src/cli/__init__.py
Server/src/cli/CLI_USAGE_GUIDE.md
docs/CLI_USAGE.md
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/cli/commands/__init__.py
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/shader.py
Server/src/cli/commands/editor.py
Server/src/cli/commands/prefab.py
Server/src/cli/commands/material.py
Server/src/cli/commands/ui.py
Server/src/cli/commands/audio.py
Server/src/cli/commands/lighting.py
Server/src/cli/commands/instance.py
Server/src/cli/commands/animation.py
Server/src/cli/commands/vfx.py
Server/src/cli/commands/batch.py
mcp.json
Add an extensive automated test suite for the CLI covering configuration, output formatting, connection behavior, and most subcommands.
  • Introduce tests/test_cli.py with fixtures and end-to-end style tests using click.CliRunner and mocking of HTTP and command helpers.
  • Cover status/instances/raw plus nearly all command groups (gameobject, component, scene, asset, editor, prefab, material, script, shader, vfx, batch, instance, code, global options, and error handling).
Server/tests/test_cli.py
Prevent custom tool registry from registering tools that shadow built-in tools.
  • Extend CustomToolService.register_global_tools to compute built-in tool names via get_registered_tools and skip registration when a custom tool shares a name, logging the skip.
  • Add helper _get_builtin_tool_names for lookup.
Server/src/services/custom_tool_service.py
Adjust IDE configurator and Unity editor defaults to align with Copilot MCP integration and project-scoped tools behavior.
  • Change JetBrains Rider MCP config paths to GitHub Copilot-specific locations for Windows/macOS/Linux in RiderConfigurator.
  • Flip default for projectScopedToolsToggle in McpConnectionSection from true to false so project-scoped tools are disabled by default for local HTTP connections.
MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
Miscellaneous formatting, dependency, and script wiring fixes.
  • Reformat prune_tool_results.py for readability and PEP8 compliance without changing behavior.
  • Add click dependency and unity-mcp console script entrypoint in pyproject.toml to wire the CLI into the Python package.
  • Apply minor line-wrapping/formatting changes in legacy unity_connection to improve readability.
prune_tool_results.py
Server/pyproject.toml
Server/src/transport/legacy/unity_connection.py
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 21, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a Python Click-based CLI for Unity MCP, including CLI modules, config/connection/output utilities, server HTTP endpoints for command execution, docs, tests, and small Unity editor configurator/UI tweaks.

Changes

Cohort / File(s) Summary
CLI Core
Server/src/cli/__init__.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
New Click entrypoint, CLIConfig (env-driven), HTTP connection helpers (send/run/check/list), output formatting (text/json/table), and central utils exports.
CLI Commands
Server/src/cli/commands/... (18 modules)
Adds 18 command groups (animation, asset, audio, batch, code, component, editor, gameobject, instance, lighting, material, prefab, scene, script, shader, ui, vfx, plus commands.init) implementing parameter parsing, backend requests, and standardized output/error handling.
Server HTTP & Services
Server/src/main.py, Server/src/services/custom_tool_service.py, Server/src/transport/legacy/unity_connection.py
Adds /api/command and /api/instances endpoints, Windows-safe log rotate handler, filters registration of built-in tools when registering global tools, and minor formatting changes in legacy connection code.
Packaging & Entry
Server/pyproject.toml, mcp.json
Adds dependency click>=8.1.0, new unity-mcp console script, and an mcp.json stdio server manifest.
Editor Client tweaks
MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs, MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
Updates Rider/IntelliJ mcp.json paths; changes default projectScopedToolsToggle pref from true→false.
Docs
Server/src/cli/CLI_USAGE_GUIDE.md, docs/CLI_USAGE.md
Adds comprehensive CLI usage guides and examples.
Tests
Server/tests/test_cli.py
Large new test suite covering config, output formatting, connection utilities, and CLI command workflows (success and error paths).
Misc / Formatting
prune_tool_results.py
Styling and whitespace adjustments only.

Sequence Diagram(s)

sequenceDiagram
    actor User as CLI User
    participant CLI as unity-mcp (Click)
    participant Server as MCP HTTP Server
    participant Router as Command Router
    participant Tool as MCP Tool Handler
    participant Unity as Unity Editor

    User->>CLI: unity-mcp <command> ...
    CLI->>CLI: parse args / get_config
    CLI->>Server: POST /api/command (command_type, params)
    Server->>Router: dispatch command
    Router->>Tool: call manage_* handler
    Tool->>Unity: send MCP request
    Unity-->>Tool: result
    Tool-->>Router: result
    Router-->>Server: response
    Server-->>CLI: JSON result
    CLI->>CLI: format_output
    CLI->>User: display
Loading
sequenceDiagram
    participant Env as Environment
    participant CFG as CLIConfig.from_env
    participant Cache as get_config()
    participant CLI as any command

    Env->>CFG: UNITY_MCP_HOST/PORT/TIMEOUT/FORMAT/INSTANCE
    CFG->>CFG: validate port/timeout
    CFG->>Cache: create CLIConfig instance
    CLI->>Cache: get_config() (reuse instance)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • justinpbarnett

Poem

🐰 I hopped through code with nimble feet,
A CLI garden, tidy and neat.
Click and HTTP—commands in bloom,
Unity answers, lighting the room,
Hop, run, format — celebrate the feat!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is comprehensive with a detailed summary by Sourcery covering all major changes, but does not follow the template's required sections (Type of Change, Changes Made, Testing/Screenshots, Related Issues are missing or incomplete). Add missing template sections: explicitly specify Type of Change (New feature), organize Changes Made by file/subsystem, add Testing/Screenshots if applicable, and link Related Issues.
Title check ❓ Inconclusive The title 'Add CLI' is vague and overly generic, failing to convey meaningful information about the changeset's scope despite substantial additions of a complete CLI system. Use a more descriptive title like 'Add unity-mcp CLI with HTTP endpoints and command groups' to better reflect the substantial changes made.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 94.55% 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.

@msanatan
Copy link
Member Author

Based off #544

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 5 issues, and left some high level feedback:

  • In Server/src/main.py's /api/command route, when a unity_instance is provided but doesn't match any session you silently fall back to the first available session; consider returning a 400/404-style error instead to avoid accidentally targeting the wrong Unity instance.
  • In cli/utils/config.py, CLIConfig.from_env raises ValueError on malformed UNITY_MCP_HTTP_PORT/UNITY_MCP_TIMEOUT, which will crash the CLI; it might be safer to log a warning and fall back to defaults so a bad env var doesn't make the tool unusable.
  • In cli/main.py's register_commands, swallowing ImportError without logging can obscure real issues (e.g., typos or packaging problems); consider at least logging which command module failed to import so debugging broken installations is easier.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Server/src/main.py`'s `/api/command` route, when a `unity_instance` is provided but doesn't match any session you silently fall back to the first available session; consider returning a 400/404-style error instead to avoid accidentally targeting the wrong Unity instance.
- In `cli/utils/config.py`, `CLIConfig.from_env` raises `ValueError` on malformed `UNITY_MCP_HTTP_PORT`/`UNITY_MCP_TIMEOUT`, which will crash the CLI; it might be safer to log a warning and fall back to defaults so a bad env var doesn't make the tool unusable.
- In `cli/main.py`'s `register_commands`, swallowing `ImportError` without logging can obscure real issues (e.g., typos or packaging problems); consider at least logging which command module failed to import so debugging broken installations is easier.

## Individual Comments

### Comment 1
<location> `Server/src/main.py:349-360` </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>
**suggestion (bug_risk):** Avoid silently falling back to the first Unity session when a specific `unity_instance` was requested but not found.

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

```suggestion
            # Find target session
            session_id = None
            if unity_instance:
                # Try to match by hash or project name
                for sid, details in sessions.sessions.items():
                    if details.hash == unity_instance or details.project == unity_instance:
                        session_id = sid
                        break

                # If a specific unity_instance was requested but not found, return an error
                if not session_id:
                    return JSONResponse(
                        {
                            "success": False,
                            "error": f"Unity instance '{unity_instance}' not found",
                        },
                        status_code=404,
                    )
            else:
                # No specific unity_instance requested: use first available session
                session_id = next(iter(sessions.sessions.keys()))
```
</issue_to_address>

### Comment 2
<location> `Server/src/cli/main.py:185-194` </location>
<code_context>
+def register_commands():
</code_context>

<issue_to_address>
**issue (bug_risk):** Swallowing all `ImportError`s when registering commands can hide real bugs in command modules.

This pattern also hides import-time errors within the command modules themselves (e.g., syntax errors or missing dependencies), making broken commands hard to detect. To support optional modules without masking real failures, catch `ModuleNotFoundError` instead and verify `e.name` matches the optional module you expect, or at least log the exception so genuine errors in `cli.commands.*` are visible.
</issue_to_address>

### Comment 3
<location> `Server/src/cli/commands/scene.py:19-28` </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):** Validate `supersize` argument range for `scene screenshot` to match the documented limits.

The help text says `supersize` should be `1-4`, but the code only checks `supersize > 1` and passes it straight through. Values like `0` or very large numbers would violate the documented range and could behave unpredictably in Unity. Please enforce `1 <= supersize <= 4` and either clamp or fail with a clear error when the value is out of range.
</issue_to_address>

### Comment 4
<location> `Server/src/cli/utils/config.py:19-28` </location>
<code_context>
+    unity_instance: Optional[str] = None
+
+    @classmethod
+    def from_env(cls) -> "CLIConfig":
+        port_raw = os.environ.get("UNITY_MCP_HTTP_PORT", "8080")
+        try:
+            port = int(port_raw)
+        except (ValueError, TypeError):
+            raise ValueError(
+                f"Invalid UNITY_MCP_HTTP_PORT value: {port_raw!r}")
+
+        timeout_raw = os.environ.get("UNITY_MCP_TIMEOUT", "30")
+        try:
+            timeout = int(timeout_raw)
+        except (ValueError, TypeError):
+            raise ValueError(
+                f"Invalid UNITY_MCP_TIMEOUT value: {timeout_raw!r}")
+
+        return cls(
</code_context>

<issue_to_address>
**suggestion:** Failing hard on malformed env vars may make the CLI brittle; consider falling back to defaults with a warning.

`UNITY_MCP_HTTP_PORT` and `UNITY_MCP_TIMEOUT` parsing currently raises `ValueError` in `from_env`, aborting all CLI commands when misconfigured. For a CLI, it’s usually preferable to emit a warning (e.g., via logging or `click.echo` to stderr) and continue with the default values so the tool remains usable despite imperfect env configuration.

Suggested implementation:

```python
import logging


class CLIConfig:

```

```python
    @classmethod
    def from_env(cls) -> "CLIConfig":
        port_raw = os.environ.get("UNITY_MCP_HTTP_PORT", "8080")
        try:
            port = int(port_raw)
        except (ValueError, TypeError):
            logging.warning(
                "Invalid UNITY_MCP_HTTP_PORT value %r; falling back to default 8080",
                port_raw,
            )
            port = 8080

        timeout_raw = os.environ.get("UNITY_MCP_TIMEOUT", "30")
        try:
            timeout = int(timeout_raw)
        except (ValueError, TypeError):
            logging.warning(
                "Invalid UNITY_MCP_TIMEOUT value %r; falling back to default 30",
                timeout_raw,
            )
            timeout = 30

        return cls(
            host=os.environ.get("UNITY_MCP_HOST", "127.0.0.1"),
            port=port,
            timeout=timeout,
            format=os.environ.get("UNITY_MCP_FORMAT", "text"),
            unity_instance=os.environ.get("UNITY_MCP_INSTANCE"),
        )

```

If this module already defines imports in a grouped block (e.g., `import os`, `from typing import Optional`), you may want to move `import logging` into that existing import section rather than directly above `class CLIConfig:` to match your style. The logging configuration (handlers/format) should be set up once in your CLI entry point so that these warnings are visible to users; if you don't configure logging anywhere yet, add a basic configuration there (e.g., `logging.basicConfig(level=logging.WARNING)`).
</issue_to_address>

### Comment 5
<location> `docs/CLI_USAGE.md:3-5` </location>
<code_context>
+# Unity MCP CLI Usage Guide
+
+The Unity MCP CLI provides command-line access to control Unity Editor through the Model Context Protocol. Currently only supports local HTTP.
+
+Note: Some tools are still experimental and might fail under circumstances. Please submit an issue for us to make it better.
+
+## Installation
</code_context>

<issue_to_address>
**suggestion (typo):** Minor grammar improvements in the introductory sentences.

Suggested tweaks for clarity:
- "The Unity MCP CLI provides command-line access to control **the** Unity Editor through the Model Context Protocol. **It** currently only supports local HTTP."
- "Note: Some tools are still experimental and might fail **under some circumstances**. Please submit an issue to help us make it better."

```suggestion
The Unity MCP CLI provides command-line access to control the Unity Editor through the Model Context Protocol. It currently only supports local HTTP.

Note: Some tools are still experimental and might fail under some circumstances. Please submit an issue to help us make it better.
```
</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 +19 to +28
@click.option(
"--parent",
default=None,
help="Parent GameObject to list children of (name, path, or instance ID)."
)
@click.option(
"--max-depth", "-d",
default=None,
type=int,
help="Maximum depth to traverse."
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): Validate supersize argument range for scene screenshot to match the documented limits.

The help text says supersize should be 1-4, but the code only checks supersize > 1 and passes it straight through. Values like 0 or very large numbers would violate the documented range and could behave unpredictably in Unity. Please enforce 1 <= supersize <= 4 and either clamp or fail with a clear error when the value is out of range.

Comment on lines +19 to +28
def from_env(cls) -> "CLIConfig":
port_raw = os.environ.get("UNITY_MCP_HTTP_PORT", "8080")
try:
port = int(port_raw)
except (ValueError, TypeError):
raise ValueError(
f"Invalid UNITY_MCP_HTTP_PORT value: {port_raw!r}")

timeout_raw = os.environ.get("UNITY_MCP_TIMEOUT", "30")
try:
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Failing hard on malformed env vars may make the CLI brittle; consider falling back to defaults with a warning.

UNITY_MCP_HTTP_PORT and UNITY_MCP_TIMEOUT parsing currently raises ValueError in from_env, aborting all CLI commands when misconfigured. For a CLI, it’s usually preferable to emit a warning (e.g., via logging or click.echo to stderr) and continue with the default values so the tool remains usable despite imperfect env configuration.

Suggested implementation:

import logging


class CLIConfig:
    @classmethod
    def from_env(cls) -> "CLIConfig":
        port_raw = os.environ.get("UNITY_MCP_HTTP_PORT", "8080")
        try:
            port = int(port_raw)
        except (ValueError, TypeError):
            logging.warning(
                "Invalid UNITY_MCP_HTTP_PORT value %r; falling back to default 8080",
                port_raw,
            )
            port = 8080

        timeout_raw = os.environ.get("UNITY_MCP_TIMEOUT", "30")
        try:
            timeout = int(timeout_raw)
        except (ValueError, TypeError):
            logging.warning(
                "Invalid UNITY_MCP_TIMEOUT value %r; falling back to default 30",
                timeout_raw,
            )
            timeout = 30

        return cls(
            host=os.environ.get("UNITY_MCP_HOST", "127.0.0.1"),
            port=port,
            timeout=timeout,
            format=os.environ.get("UNITY_MCP_FORMAT", "text"),
            unity_instance=os.environ.get("UNITY_MCP_INSTANCE"),
        )

If this module already defines imports in a grouped block (e.g., import os, from typing import Optional), you may want to move import logging into that existing import section rather than directly above class CLIConfig: to match your style. The logging configuration (handlers/format) should be set up once in your CLI entry point so that these warnings are visible to users; if you don't configure logging anywhere yet, add a basic configuration there (e.g., logging.basicConfig(level=logging.WARNING)).

msanatan and others added 2 commits January 21, 2026 18:55
…c unity_instance was requested but not found.


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

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
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: 13

🤖 Fix all issues with AI agents
In `@Server/src/cli/CLI_USAGE_GUIDE.md`:
- Around line 345-347: The example in the usage guide uses the wrong command
name; update the snippet to use the CLI group and subcommand defined in
Server/src/cli/commands/instance.py by changing "unity-mcp instances" to the
correct "unity-mcp instance list" so it matches the instance command group and
its list subcommand.
- Around line 83-89: Add a language identifier to the fenced code blocks in the
CLI usage examples so markdownlint MD040 is satisfied: change the
triple-backtick fences that contain the CLI output ("Checking connection to
127.0.0.1:8080... ✅ Connected to Unity MCP server..." block) and the usage
synopsis ("unity-mcp [GLOBAL_OPTIONS] COMMAND_GROUP [SUBCOMMAND]...") to use
```text instead of plain ``` so they are treated as non-code text blocks.

In `@Server/src/cli/commands/audio.py`:
- Around line 31-60: The play/stop CLI commands call manage_components with
action "set_property" for property "Play"/"Stop", but AudioSource.Play()/Stop()
are methods and ComponentOps.SetProperty cannot invoke methods—this will fail;
update the fix by either extending the backend manage_components to support a
new action (e.g., "invoke_method") that takes "methodName" and optional args and
calls ComponentOps.InvokeMethod (or equivalent), or special-case audio in the
CLI to call a new action like "invoke_method" with methodName "Play"/"Stop";
change the play() and stop() functions to send that action and parameters
instead of "set_property", and update any backend handler names referenced
(ComponentOps.SetProperty -> implement ComponentOps.InvokeMethod or
method-invocation path) so method invocation is performed rather than property
assignment.

In `@Server/src/cli/commands/batch.py`:
- Around line 179-184: The file-writing block that uses output, json_output, and
print_success lacks error handling; wrap the open/write in a try/except that
catches OSError/IOError (or generic Exception), and on failure log/echo a clear
error using the existing CLI utilities (e.g., print_error or click.echo)
including the exception message and the target path, then exit non‑zero (e.g.,
raise click.Abort or sys.exit(1)); also ensure the file is opened with an
explicit encoding (utf-8) to avoid encoding issues.

In `@Server/src/cli/commands/code.py`:
- Around line 59-69: The read path after calling run_command("manage_script",
params, config) should normalize nested/encoded responses like the search
implementation: first unwrap a top-level "result" key if present (i.e., use
result = result.get("result", result)), then extract the "data" payload, check
for "contents" inside that data (or inside data.get("data") if nested), and if
the contents are base64-encoded decode them before calling click.echo; otherwise
fall back to format_output(result, config.format). Update the logic around
result/data handling in the read flow (the block using run_command, result,
data, format_output) to perform this normalization and decoding so file contents
are printed correctly when nested or encoded.

In `@Server/src/cli/commands/editor.py`:
- Around line 272-335: The CLI never adds the async intent to the request
payload; update the run_tests function to set the expected async flag on the
params dict before calling run_command (e.g., if async_mode: params["async"] =
True—or whichever key the Unity backend expects, like "run_async"), so the
backend receives explicit async intent; modify the params population in
run_tests (where params: dict[str, Any] is built) to include this key when
async_mode is True.

In `@Server/src/cli/commands/material.py`:
- Around line 171-177: The CLI option "--search-method" advertises "by_id" in
examples but the click.Choice for that option omits "by_id", causing the example
to fail; update the click.option declarations that define "--search-method" (the
click.Choice(...) entries in material.py) to include "by_id" alongside
"by_name", "by_path", "by_tag", "by_layer", and "by_component" so the validator
accepts the documented value (ensure you update both occurrences referenced in
the diff).

In `@Server/src/cli/commands/ui.py`:
- Around line 37-71: The code currently ignores failures from the component
addition and render-mode steps, so update the block using
run_command("manage_components", ...) and the render-mode run_command to check
each returned result (e.g., comp_result or render_result) and fail-fast: if
result.get("success") is falsy, call print_error with a clear message including
the component name or property and result.get("error", "Unknown error"), then
sys.exit(1); ensure you still echo the original creation result via
format_output only after all steps succeed and keep the UnityConnectionError
except block unchanged.
- Around line 102-135: The add-component and set-property calls (run_command
with "manage_components") are not checked for success, so if either fails the
Text element can be left incomplete; after each run_command for component
addition and for property setting (the two calls where "componentType":
"TextMeshProUGUI" and "action": "add"/"set_property" are used) inspect the
returned result like you do for manage_gameobject (check result.get("success")
or result.get("data") or result.get("result")), echo format_output(result,
config.format) and exit with sys.exit(1) (or raise UnityConnectionError) on
failure, and only print_success("Created Text: {name}") after all steps succeed;
keep existing UnityConnectionError except block as-is.

In `@Server/src/cli/commands/vfx.py`:
- Around line 417-431: The code currently blindly merges extra_params into
request_params which will raise if --params parses to a non-object (e.g., list);
validate that extra_params is a dict before updating: after the json.loads into
extra_params in vfx.py, check isinstance(extra_params, dict) and if not call
print_error with a friendly message like "params must be a JSON object" and
sys.exit(1); only then perform request_params.update(extra_params). Reference
extra_params, request_params, print_error, and the json.JSONDecodeError handling
to place the check.

In `@Server/src/cli/utils/__init__.py`:
- Around line 18-31: The __all__ list is not sorted which triggers Ruff RUF022;
update the __all__ declaration to either be alphabetically sorted or add a
per-line or list-level noqa to silence RUF022 while preserving grouping.
Specifically modify the __all__ variable (containing "CLIConfig", "get_config",
"set_config", "run_command", "run_check_connection", "run_list_instances",
"UnityConnectionError", "format_output", "print_success", "print_error",
"print_warning", "print_info") to be alphabetically ordered OR append a comment
like  # noqa: RUF022 to the __all__ assignment so the linter stops flagging it.
Ensure you keep the same exported names and only change ordering or add the noqa
suppression.

In `@Server/src/cli/utils/output.py`:
- Around line 193-195: The info icon in print_info triggers Ruff RUF001 for
ambiguous Unicode; update the print_info function to either use an ASCII
alternative (e.g., "INFO:" or "i:") instead of the ℹ emoji, or add a local Ruff
suppression comment (e.g., # noqa: RUF001) directly above the print_info
definition to silence the rule; locate the function print_info in output.py and
apply the chosen change so the linter no longer flags the line.

In `@Server/src/main.py`:
- Around line 328-368: The cli_command_route handler currently catches a bare
Exception and returns the raw exception to the client; change it to catch
specific expected errors (e.g., json.JSONDecodeError, KeyError, ValueError) and
handle them with logger.exception(...) returning a 400 with a safe message (like
"Invalid request payload"), then add a final broad except Exception as e that
uses logger.exception("CLI command unexpected error") and returns a 500 with a
generic message (e.g., "Internal server error") without including str(e); update
imports to reference json.JSONDecodeError if needed and leave
PluginHub.get_sessions and PluginHub.send_command usage unchanged.
🧹 Nitpick comments (11)
Server/src/cli/__init__.py (1)

1-3: Consider single-sourcing version in the future.

The CLI module defines its own version (1.0.0) separate from the server version (9.0.8 in pyproject.toml). This is acceptable for a new feature, but maintaining two independent versions may lead to confusion over time. Consider single-sourcing from pyproject.toml using importlib.metadata if you want to keep versions aligned.

prune_tool_results.py (1)

6-10: Consider catching json.JSONDecodeError instead of Exception.

The broad except Exception catch works as a fallback, but catching json.JSONDecodeError would be more precise and avoids masking unexpected errors (e.g., MemoryError).

♻️ Suggested refinement
 def summarize(txt):
     try:
         obj = json.loads(txt)
-    except Exception:
+    except json.JSONDecodeError:
         return f"tool_result: {len(txt)} bytes"
Server/src/main.py (1)

370-386: Consider consistent error handling for the instances endpoint.

Similar to the command endpoint, this catches bare Exception. While less critical for a read-only endpoint, consider logging with logger.exception for better debugging.

Proposed improvement
         except Exception as e:
+            logger.exception("Error listing Unity instances")
             return JSONResponse({"success": False, "error": str(e)}, status_code=500)
Server/src/cli/commands/animation.py (1)

84-87: Remove unused config variable in placeholder command.

The config variable is assigned but never used since this is a placeholder implementation. Either remove it or use it if formatting is intended for the output.

Proposed fix
 def set_parameter(target: str, param_name: str, value: str, param_type: str):
     """Set an Animator parameter.
 
     \b
     Examples:
         unity-mcp animation set-parameter "Player" "Speed" 5.0
         unity-mcp animation set-parameter "Player" "IsRunning" true --type bool
         unity-mcp animation set-parameter "Player" "Jump" "" --type trigger
     """
-    config = get_config()
     print_info(
         "Animation parameter command - requires custom Unity implementation")
     click.echo(f"Would set {param_name}={value} ({param_type}) on {target}")
Server/src/cli/commands/audio.py (1)

8-8: Remove unused import print_info.

The print_info function is imported but never used in this module.

Proposed fix
-from cli.utils.output import format_output, print_error, print_info
+from cli.utils.output import format_output, print_error
Server/src/cli/commands/scene.py (1)

219-255: Consider validating the supersize range.

The help text indicates supersize should be 1-4, but there's no validation enforcing this. Invalid values could cause unexpected behavior on the Unity side.

♻️ Suggested fix using Click's IntRange
 `@click.option`(
     "--supersize", "-s",
     default=1,
-    type=int,
+    type=click.IntRange(1, 4),
     help="Supersize multiplier (1-4)."
 )
Server/src/cli/utils/config.py (1)

18-40: Consider adding exception chaining for better debugging.

The static analysis correctly identifies that using raise ... from err preserves the original exception traceback, which aids debugging when users provide invalid environment variable values.

♻️ Suggested fix with exception chaining
         try:
             port = int(port_raw)
         except (ValueError, TypeError):
-            raise ValueError(
-                f"Invalid UNITY_MCP_HTTP_PORT value: {port_raw!r}")
+            raise ValueError(
+                f"Invalid UNITY_MCP_HTTP_PORT value: {port_raw!r}") from None

         timeout_raw = os.environ.get("UNITY_MCP_TIMEOUT", "30")
         try:
             timeout = int(timeout_raw)
         except (ValueError, TypeError):
-            raise ValueError(
-                f"Invalid UNITY_MCP_TIMEOUT value: {timeout_raw!r}")
+            raise ValueError(
+                f"Invalid UNITY_MCP_TIMEOUT value: {timeout_raw!r}") from None

Using from None explicitly suppresses the original exception chain (since the new message is self-explanatory), which is cleaner than an implicit chain.

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

1-1: Update the module docstring - this is a full implementation, not a placeholder.

The docstring says "placeholder for future implementation" but the module contains a complete implementation with four functional commands.

♻️ Suggested fix
-"""UI CLI commands - placeholder for future implementation."""
+"""UI CLI commands - create and modify UI elements."""

179-185: Remove or use the unused label_result variable.

Static analysis correctly identified that label_result is assigned but never used. Either remove the assignment or use it for error checking.

♻️ Option 1: Remove unused assignment
         # Step 3: Create child label GameObject
         label_name = f"{name}_Label"
-        label_result = run_command("manage_gameobject", {
+        run_command("manage_gameobject", {
             "action": "create",
             "name": label_name,
             "parent": name,
         }, config)
♻️ Option 2: Use for error checking (preferred)
         # Step 3: Create child label GameObject
         label_name = f"{name}_Label"
         label_result = run_command("manage_gameobject", {
             "action": "create",
             "name": label_name,
             "parent": name,
         }, config)
+        
+        if not label_result.get("success"):
+            print_error(f"Failed to create button label: {label_result.get('error', 'Unknown error')}")
+            sys.exit(1)
Server/src/cli/utils/connection.py (1)

80-96: Preserve exception context when wrapping httpx errors.
Currently the root cause is lost; chaining will keep traceback intact.

♻️ Suggested fix
-    except httpx.ConnectError as e:
-        raise UnityConnectionError(
+    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(
+    except httpx.TimeoutException as e:
+        raise UnityConnectionError(
             f"Connection to Unity timed out after {timeout or cfg.timeout}s. "
             f"Unity may be busy or unresponsive."
-        )
-    except httpx.HTTPStatusError as e:
-        raise UnityConnectionError(
+        ) from e
+    except httpx.HTTPStatusError as e:
+        raise UnityConnectionError(
             f"HTTP error from server: {e.response.status_code} - {e.response.text}"
-        )
-    except Exception as e:
-        raise UnityConnectionError(f"Unexpected error: {e}")
+        ) from e
+    except Exception as e:
+        raise UnityConnectionError(f"Unexpected error: {e}") from e
Server/src/cli/commands/gameobject.py (1)

183-195: Consider using a unique identifier when adding components post-create.
Unity allows duplicate names; targeting by name can attach components to the wrong object. If manage_gameobject create returns an ID, prefer that for manage_components.

Comment on lines +83 to +89
```
Checking connection to 127.0.0.1:8080...
✅ Connected to Unity MCP server at 127.0.0.1:8080
Connected Unity instances:
• MyProject (Unity 6000.2.10f1) [09abcc51]
```
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

Add language identifiers to fenced blocks (MD040).
Markdownlint will flag these; use a text fence for non-code output/diagrams.

🧩 Suggested fix
-```
+```text
 Checking connection to 127.0.0.1:8080...
 ✅ Connected to Unity MCP server at 127.0.0.1:8080

 Connected Unity instances:
   • MyProject (Unity 6000.2.10f1) [09abcc51]

```diff
-```
+```text
 unity-mcp [GLOBAL_OPTIONS] COMMAND_GROUP [SUBCOMMAND] [ARGUMENTS] [OPTIONS]
</details>


Also applies to: 129-131

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

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

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @Server/src/cli/CLI_USAGE_GUIDE.md around lines 83 - 89, Add a language
identifier to the fenced code blocks in the CLI usage examples so markdownlint
MD040 is satisfied: change the triple-backtick fences that contain the CLI
output ("Checking connection to 127.0.0.1:8080... ✅ Connected to Unity MCP
server..." block) and the usage synopsis ("unity-mcp [GLOBAL_OPTIONS]
COMMAND_GROUP [SUBCOMMAND]...") to use text instead of plain so they are
treated as non-code text blocks.


</details>

<!-- fingerprinting:phantom:poseidon:eagle -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +345 to +347
# List connected Unity instances
unity-mcp instances
```
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

Command name typo: instances vs instance list.
The CLI group is instance (see Server/src/cli/commands/instance.py). Update the example to avoid confusion.

🧩 Suggested fix
-# List connected Unity instances
-unity-mcp instances
+# List connected Unity instances
+unity-mcp instance list
🤖 Prompt for AI Agents
In `@Server/src/cli/CLI_USAGE_GUIDE.md` around lines 345 - 347, The example in the
usage guide uses the wrong command name; update the snippet to use the CLI group
and subcommand defined in Server/src/cli/commands/instance.py by changing
"unity-mcp instances" to the correct "unity-mcp instance list" so it matches the
instance command group and its list subcommand.

Comment on lines +31 to +60
def play(target: str, clip: Optional[str], search_method: Optional[str]):
"""Play audio on a target's AudioSource.
\b
Examples:
unity-mcp audio play "MusicPlayer"
unity-mcp audio play "SFXSource" --clip "Assets/Audio/explosion.wav"
"""
config = get_config()

params: dict[str, Any] = {
"action": "set_property",
"target": target,
"componentType": "AudioSource",
"property": "Play",
"value": True,
}

if clip:
params["clip"] = clip

if search_method:
params["searchMethod"] = search_method

try:
result = run_command("manage_components", params, config)
click.echo(format_output(result, config.format))
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
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:

# Find the manage_components implementation
fd -t f -n "manage_components" --exec cat {} \;

Repository: CoplayDev/unity-mcp

Length of output: 293


🏁 Script executed:

# Search for manage_components function/handler in the codebase
rg -n "def manage_components|manage_components" --type py -B 2 -A 10 | head -100

Repository: CoplayDev/unity-mcp

Length of output: 6717


🏁 Script executed:

# Look for AudioSource handling in the backend
rg -n "AudioSource" --type py -C 5 | head -100

Repository: CoplayDev/unity-mcp

Length of output: 4762


🏁 Script executed:

# Find all Python files that define manage_components function
rg -t py "def manage_components" -B 2 -A 20

Repository: CoplayDev/unity-mcp

Length of output: 2114


🏁 Script executed:

# Search for backend implementation - look for C# or non-Python manage_components
fd -t f "manage_components"

Repository: CoplayDev/unity-mcp

Length of output: 160


🏁 Script executed:

# Look in common backend directories
find . -type d -name "backend" -o -type d -name "server" -o -type d -name "handlers" | head -20

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

# Search for where manage_components is handled - likely RPC handler
rg "manage_components" --type py -l

Repository: CoplayDev/unity-mcp

Length of output: 495


🏁 Script executed:

wc -l Server/src/services/tools/manage_components.py

Repository: CoplayDev/unity-mcp

Length of output: 113


🏁 Script executed:

cat -n Server/src/services/tools/manage_components.py

Repository: CoplayDev/unity-mcp

Length of output: 6004


🏁 Script executed:

# Also check the test file for audio-related tests
cat -n Server/tests/integration/test_manage_components.py

Repository: CoplayDev/unity-mcp

Length of output: 8543


🏁 Script executed:

# Find C# or backend files that might handle manage_components
find . -type f \( -name "*.cs" -o -name "*.csproj" \) | head -20

Repository: CoplayDev/unity-mcp

Length of output: 1337


🏁 Script executed:

# Look for any documentation or comment about manage_components
rg "manage_components" -t cs -C 5

Repository: CoplayDev/unity-mcp

Length of output: 1351


🏁 Script executed:

# Check if there's a separate backend directory
find . -type d -name "*backend*" -o -type d -name "*unity*" -o -type d -name "*handler*"

Repository: CoplayDev/unity-mcp

Length of output: 219


🏁 Script executed:

wc -l MCPForUnity/Editor/Tools/ManageComponents.cs

Repository: CoplayDev/unity-mcp

Length of output: 111


🏁 Script executed:

cat -n MCPForUnity/Editor/Tools/ManageComponents.cs

Repository: CoplayDev/unity-mcp

Length of output: 15008


🏁 Script executed:

find . -type f -name "*ComponentOps*"

Repository: CoplayDev/unity-mcp

Length of output: 157


🏁 Script executed:

rg "class ComponentOps" -B 2 -A 50

Repository: CoplayDev/unity-mcp

Length of output: 4367


🏁 Script executed:

wc -l MCPForUnity/Editor/Helpers/ComponentOps.cs

Repository: CoplayDev/unity-mcp

Length of output: 109


🏁 Script executed:

cat -n MCPForUnity/Editor/Helpers/ComponentOps.cs | head -250

Repository: CoplayDev/unity-mcp

Length of output: 11669


🏁 Script executed:

cat -n MCPForUnity/Editor/Helpers/ComponentOps.cs | tail -150

Repository: CoplayDev/unity-mcp

Length of output: 7399


🏁 Script executed:

# Search for any special handling of method invocation like "Play" or "Stop"
rg "Play\|Stop" MCPForUnity/Editor/Helpers/ -C 3

Repository: CoplayDev/unity-mcp

Length of output: 45


🌐 Web query:

Unity AudioSource Play method documentation API reference

💡 Result:

AudioSource.Play — signature and key notes

  • Signature: public void Play(ulong delay = 0);. [1]
  • What it does: starts playback of the AudioSource's clip. If the same clip is already assigned and playing, calling Play restarts it. [1][2]
  • Deprecated detail: the delay parameter (number of samples at 44.1 kHz) is deprecated — use AudioSource.PlayDelayed (seconds) or AudioSource.PlayScheduled for more accurate timing. [1][2]

References:
[1] Unity Scripting API — AudioSource (method list and Play link).
[2] Unity Scripting API — AudioSource.Play documentation.


🏁 Script executed:

# Search for any method invocation handling in the codebase
rg "MethodInfo\|Invoke" MCPForUnity/Editor/ -l

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

# Check if there's any alternative approach for invoking methods
rg "Play|Stop" MCPForUnity/ --type cs -l

Repository: CoplayDev/unity-mcp

Length of output: 1695


🏁 Script executed:

# Search for any indication of method invocation support
rg "invoke\|Invoke|MethodInfo" MCPForUnity/Editor/Tools/ -i -C 2

Repository: CoplayDev/unity-mcp

Length of output: 1555


🏁 Script executed:

# Check the audio.py implementation more carefully
cat -n Server/src/cli/commands/audio.py

Repository: CoplayDev/unity-mcp

Length of output: 4326


The current implementation cannot work because Play() and Stop() are methods, not properties.

In Unity, AudioSource.Play() and AudioSource.Stop() are public methods, not properties. The backend's ComponentOps.SetProperty only handles property and field assignments via reflection—it has no mechanism to invoke methods. When this code executes, it will fail with an error like "Property or field 'Play' not found on component 'AudioSource'."

To fix this, the code needs to either:

  1. Add method invocation support to the manage_components backend (recommended for flexibility)
  2. Implement audio-specific handlers that invoke Play/Stop directly
  3. Use a different action type that routes to method invocation instead of property assignment

The same issue affects the stop() command.

🤖 Prompt for AI Agents
In `@Server/src/cli/commands/audio.py` around lines 31 - 60, The play/stop CLI
commands call manage_components with action "set_property" for property
"Play"/"Stop", but AudioSource.Play()/Stop() are methods and
ComponentOps.SetProperty cannot invoke methods—this will fail; update the fix by
either extending the backend manage_components to support a new action (e.g.,
"invoke_method") that takes "methodName" and optional args and calls
ComponentOps.InvokeMethod (or equivalent), or special-case audio in the CLI to
call a new action like "invoke_method" with methodName "Play"/"Stop"; change the
play() and stop() functions to send that action and parameters instead of
"set_property", and update any backend handler names referenced
(ComponentOps.SetProperty -> implement ComponentOps.InvokeMethod or
method-invocation path) so method invocation is performed rather than property
assignment.

Comment on lines +179 to +184
if output:
with open(output, 'w') as f:
f.write(json_output)
print_success(f"Template written to: {output}")
else:
click.echo(json_output)
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

Add error handling for file write operation.

The template command writes to a file without handling potential I/O errors. This could fail silently or crash if the path is invalid or permissions are denied.

Proposed fix
     if output:
-        with open(output, 'w') as f:
-            f.write(json_output)
-        print_success(f"Template written to: {output}")
+        try:
+            with open(output, 'w') as f:
+                f.write(json_output)
+            print_success(f"Template written to: {output}")
+        except IOError as e:
+            print_error(f"Error writing file: {e}")
+            sys.exit(1)
     else:
         click.echo(json_output)
🤖 Prompt for AI Agents
In `@Server/src/cli/commands/batch.py` around lines 179 - 184, The file-writing
block that uses output, json_output, and print_success lacks error handling;
wrap the open/write in a try/except that catches OSError/IOError (or generic
Exception), and on failure log/echo a clear error using the existing CLI
utilities (e.g., print_error or click.echo) including the exception message and
the target path, then exit non‑zero (e.g., raise click.Abort or sys.exit(1));
also ensure the file is opened with an explicit encoding (utf-8) to avoid
encoding issues.

Comment on lines +59 to +69
try:
result = run_command("manage_script", params, config)
# For read, output content directly if available
if result.get("success") and result.get("data"):
data = result.get("data", {})
if isinstance(data, dict) and "contents" in data:
click.echo(data["contents"])
else:
click.echo(format_output(result, config.format))
else:
click.echo(format_output(result, config.format))
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

Normalize read response before printing contents.
read assumes success/data at the top level, but search shows responses can be nested under result and/or base64-encoded. In those cases, read won’t print file contents as intended.

🐛 Suggested fix (align with search)
     try:
         result = run_command("manage_script", params, config)
-        # For read, output content directly if available
-        if result.get("success") and result.get("data"):
-            data = result.get("data", {})
-            if isinstance(data, dict) and "contents" in data:
-                click.echo(data["contents"])
-            else:
-                click.echo(format_output(result, config.format))
-        else:
-            click.echo(format_output(result, config.format))
+        # Normalize response shape (some endpoints return {status, result:{...}})
+        inner_result = result.get("result", result)
+        if inner_result.get("success") and inner_result.get("data"):
+            data = inner_result.get("data", {})
+            contents = data.get("contents")
+            if not contents and data.get("encodedContents"):
+                try:
+                    import base64
+                    contents = base64.b64decode(
+                        data["encodedContents"]
+                    ).decode("utf-8", "replace")
+                except (ValueError, TypeError):
+                    contents = None
+            if contents is not None:
+                click.echo(contents)
+                return
+        click.echo(format_output(result, config.format))
     except UnityConnectionError as e:
         print_error(str(e))
         sys.exit(1)
📝 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
try:
result = run_command("manage_script", params, config)
# For read, output content directly if available
if result.get("success") and result.get("data"):
data = result.get("data", {})
if isinstance(data, dict) and "contents" in data:
click.echo(data["contents"])
else:
click.echo(format_output(result, config.format))
else:
click.echo(format_output(result, config.format))
try:
result = run_command("manage_script", params, config)
# Normalize response shape (some endpoints return {status, result:{...}})
inner_result = result.get("result", result)
if inner_result.get("success") and inner_result.get("data"):
data = inner_result.get("data", {})
contents = data.get("contents")
if not contents and data.get("encodedContents"):
try:
import base64
contents = base64.b64decode(
data["encodedContents"]
).decode("utf-8", "replace")
except (ValueError, TypeError):
contents = None
if contents is not None:
click.echo(contents)
return
click.echo(format_output(result, config.format))
🤖 Prompt for AI Agents
In `@Server/src/cli/commands/code.py` around lines 59 - 69, The read path after
calling run_command("manage_script", params, config) should normalize
nested/encoded responses like the search implementation: first unwrap a
top-level "result" key if present (i.e., use result = result.get("result",
result)), then extract the "data" payload, check for "contents" inside that data
(or inside data.get("data") if nested), and if the contents are base64-encoded
decode them before calling click.echo; otherwise fall back to
format_output(result, config.format). Update the logic around result/data
handling in the read flow (the block using run_command, result, data,
format_output) to perform this normalization and decoding so file contents are
printed correctly when nested or encoded.

Comment on lines +102 to +135
try:
# Step 1: Create empty GameObject with parent
result = run_command("manage_gameobject", {
"action": "create",
"name": name,
"parent": parent,
"position": list(position),
}, config)

if not (result.get("success") or result.get("data") or result.get("result")):
click.echo(format_output(result, config.format))
return

# Step 2: Add RectTransform and TextMeshProUGUI
run_command("manage_components", {
"action": "add",
"target": name,
"componentType": "TextMeshProUGUI",
}, config)

# Step 3: Set text content
run_command("manage_components", {
"action": "set_property",
"target": name,
"componentType": "TextMeshProUGUI",
"property": "text",
"value": text,
}, config)

click.echo(format_output(result, config.format))
print_success(f"Created Text: {name}")
except UnityConnectionError as e:
print_error(str(e))
sys.exit(1)
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 intermediate error handling gap as create_canvas.

The component addition and property setting steps don't check for failures, potentially leaving the text element in an incomplete state.

🤖 Prompt for AI Agents
In `@Server/src/cli/commands/ui.py` around lines 102 - 135, The add-component and
set-property calls (run_command with "manage_components") are not checked for
success, so if either fails the Text element can be left incomplete; after each
run_command for component addition and for property setting (the two calls where
"componentType": "TextMeshProUGUI" and "action": "add"/"set_property" are used)
inspect the returned result like you do for manage_gameobject (check
result.get("success") or result.get("data") or result.get("result")), echo
format_output(result, config.format) and exit with sys.exit(1) (or raise
UnityConnectionError) on failure, and only print_success("Created Text: {name}")
after all steps succeed; keep existing UnityConnectionError except block as-is.

Comment on lines +193 to +195
def print_info(message: str) -> None:
"""Print an info message."""
click.echo(f"ℹ️ {message}")
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

Ruff RUF001: ambiguous Unicode in info icon.
If Ruff is enforced, the ℹ emoji will trigger RUF001. Either replace with ASCII or suppress the rule locally.

🧩 Minimal suppression
 def print_info(message: str) -> None:
     """Print an info message."""
-    click.echo(f"ℹ️  {message}")
+    click.echo(f"ℹ️  {message}")  # noqa: RUF001
📝 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 print_info(message: str) -> None:
"""Print an info message."""
click.echo(f"ℹ️ {message}")
def print_info(message: str) -> None:
"""Print an info message."""
click.echo(f"ℹ️ {message}") # noqa: RUF001
🧰 Tools
🪛 Ruff (0.14.13)

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

(RUF001)

🤖 Prompt for AI Agents
In `@Server/src/cli/utils/output.py` around lines 193 - 195, The info icon in
print_info triggers Ruff RUF001 for ambiguous Unicode; update the print_info
function to either use an ASCII alternative (e.g., "INFO:" or "i:") instead of
the ℹ emoji, or add a local Ruff suppression comment (e.g., # noqa: RUF001)
directly above the print_info definition to silence the rule; locate the
function print_info in output.py and apply the chosen change so the linter no
longer flags the line.

Comment on lines 328 to 368
@mcp.custom_route("/api/command", methods=["POST"])
async def cli_command_route(request: Request) -> JSONResponse:
"""REST endpoint for CLI commands to Unity."""
try:
body = await request.json()

command_type = body.get("type")
params = body.get("params", {})
unity_instance = body.get("unity_instance")

if not command_type:
return JSONResponse({"success": False, "error": "Missing 'type' field"}, status_code=400)

# Get available sessions
sessions = await PluginHub.get_sessions()
if not sessions.sessions:
return JSONResponse({
"success": False,
"error": "No Unity instances connected. Make sure Unity is running with MCP plugin."
}, status_code=503)

# Find target session
session_id = None
if unity_instance:
# Try to match by hash or project name
for sid, details in sessions.sessions.items():
if details.hash == unity_instance or details.project == unity_instance:
session_id = sid
break

if not session_id:
# Use first available session
session_id = next(iter(sessions.sessions.keys()))

# Send command to Unity
result = await PluginHub.send_command(session_id, command_type, params)
return JSONResponse(result)

except Exception as e:
logger.error(f"CLI command error: {e}")
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
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

Improve exception handling and logging in the CLI command endpoint.

The static analysis correctly flags two issues:

  1. Catching bare Exception is overly broad and may mask unexpected errors
  2. logging.exception is preferred over logging.error when logging exceptions as it automatically includes the traceback

Additionally, the error response exposes the raw exception message to the client, which could leak internal details.

Proposed fix
         except Exception as e:
-            logger.error(f"CLI command error: {e}")
-            return JSONResponse({"success": False, "error": str(e)}, status_code=500)
+            logger.exception("CLI command error")
+            return JSONResponse({"success": False, "error": "Internal server error"}, status_code=500)
🧰 Tools
🪛 Ruff (0.14.13)

366-366: Do not catch blind exception: Exception

(BLE001)


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

Replace with exception

(TRY400)

🤖 Prompt for AI Agents
In `@Server/src/main.py` around lines 328 - 368, The cli_command_route handler
currently catches a bare Exception and returns the raw exception to the client;
change it to catch specific expected errors (e.g., json.JSONDecodeError,
KeyError, ValueError) and handle them with logger.exception(...) returning a 400
with a safe message (like "Invalid request payload"), then add a final broad
except Exception as e that uses logger.exception("CLI command unexpected error")
and returns a 500 with a generic message (e.g., "Internal server error") without
including str(e); update imports to reference json.JSONDecodeError if needed and
leave PluginHub.get_sessions and PluginHub.send_command usage unchanged.

msanatan and others added 3 commits January 21, 2026 19:01
… modules and to surface real import-time failures, so broken command modules don’t get silently ignored.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
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: 1

🤖 Fix all issues with AI agents
In `@Server/src/cli/commands/vfx.py`:
- Around line 211-215: After parsing positions with json.loads into
positions_list, validate that positions_list is an actual list (use
isinstance(positions_list, list)); if it is not a list, call print_error with a
clear message like "positions must be a JSON list" and sys.exit(1). Update the
try/except block around json.loads in the vfx command (where positions and
positions_list are used) to perform this type check immediately after successful
parse and before sending to Unity.
♻️ Duplicate comments (1)
Server/src/main.py (1)

375-377: Improve exception logging and avoid exposing internal error details.

Per static analysis and the previous review comment (still applicable), use logger.exception instead of logger.error to capture the full traceback, and avoid returning raw exception messages to clients.

🛡️ Proposed fix
         except Exception as e:
-            logger.error(f"CLI command error: {e}")
-            return JSONResponse({"success": False, "error": str(e)}, status_code=500)
+            logger.exception("CLI command error")
+            return JSONResponse({"success": False, "error": "Internal server error"}, status_code=500)
🧹 Nitpick comments (2)
Server/src/cli/main.py (1)

171-175: Validate that params is a dict to match the type contract.

The run_command function signature expects params: Dict[str, Any], but this code doesn't validate that the parsed JSON is actually a dict. For consistency with the vfx raw command and type safety, add validation.

🛡️ Proposed fix
     try:
         params_dict = json.loads(params)
     except json.JSONDecodeError as e:
         print_error(f"Invalid JSON params: {e}")
         sys.exit(1)
+    if not isinstance(params_dict, dict):
+        print_error("Invalid JSON params: expected an object")
+        sys.exit(1)
Server/src/main.py (1)

394-395: Apply consistent error handling.

For consistency with the /api/command endpoint fix, avoid exposing raw exception messages.

🛡️ Proposed fix
         except Exception as e:
-            return JSONResponse({"success": False, "error": str(e)}, status_code=500)
+            logger.exception("Failed to list instances")
+            return JSONResponse({"success": False, "error": "Internal server error"}, status_code=500)

Comment on lines +211 to +215
try:
positions_list = json.loads(positions)
except json.JSONDecodeError as e:
print_error(f"Invalid JSON for positions: {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

Validate that positions parses to a list.

Unlike the vfx_raw command which validates that params is a dict, this code doesn't verify that the parsed JSON is actually a list. If a user passes a non-list JSON (e.g., --positions '{"x": 1}'), it will be sent to Unity and likely cause a confusing error.

🛡️ Proposed fix
     try:
         positions_list = json.loads(positions)
     except json.JSONDecodeError as e:
         print_error(f"Invalid JSON for positions: {e}")
         sys.exit(1)
+    if not isinstance(positions_list, list):
+        print_error("Invalid JSON for positions: expected an array")
+        sys.exit(1)
🤖 Prompt for AI Agents
In `@Server/src/cli/commands/vfx.py` around lines 211 - 215, After parsing
positions with json.loads into positions_list, validate that positions_list is
an actual list (use isinstance(positions_list, list)); if it is not a list, call
print_error with a clear message like "positions must be a JSON list" and
sys.exit(1). Update the try/except block around json.loads in the vfx command
(where positions and positions_list are used) to perform this type check
immediately after successful parse and before sending to Unity.

@msanatan msanatan merged commit 7f44e4b into CoplayDev:main Jan 22, 2026
2 checks passed
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.

3 participants