Skip to content

Fix execute_custom_tool bypassed when unity_instance is specified#724

Merged
Scriptwonder merged 5 commits intoCoplayDev:betafrom
whatevertogo:fix/issue-649-execute-custom-tool-bypass-main
Feb 12, 2026
Merged

Fix execute_custom_tool bypassed when unity_instance is specified#724
Scriptwonder merged 5 commits intoCoplayDev:betafrom
whatevertogo:fix/issue-649-execute-custom-tool-bypass-main

Conversation

@whatevertogo
Copy link
Contributor

@whatevertogo whatevertogo commented Feb 11, 2026

Description

Fix bug where execute_custom_tool was bypassed when both unity_instance and command_type: "execute_custom_tool" were specified in the CLI /api/command REST endpoint.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Test update

Changes Made

  • Moved execute_custom_tool handling outside of the if/else block that checks for unity_instance
  • The custom tool execution path now runs regardless of whether a specific Unity instance is requested
  • Added session validation for execute_custom_tool to ensure a valid session exists
  • Preserved existing behavior: when no unity_instance is specified, the first available session is used

Root Cause: The original code had execute_custom_tool handling only in the else branch (when unity_instance is not specified). When unity_instance was provided, the code would find the session but then fall through to PluginHub.send_command, bypassing custom tool resolution.

Testing/Screenshots/Recordings

  • All existing CLI tests pass (123 tests)
  • Manual verification of code paths:
    • unity_instance specified + execute_custom_tool → now executes custom tool
    • unity_instance not specified + execute_custom_tool → still works
    • unity_instance specified + other commands → still routes to Unity
    • unity_instance not specified + other commands → still routes to Unity

Documentation Updates

  • I have added/removed/modified tools or resources
  • If yes, I have updated all documentation files using:
    • The LLM prompt at tools/UPDATE_DOCS_PROMPT.md (recommended)
    • Manual updates following the guide at tools/UPDATE_DOCS.md

Related Issues

Fixes #649

Additional Notes

This is a pre-existing logic issue that was identified during code review of PR #644 by CodeRabbit.

Summary by Sourcery

Ensure CLI custom tool execution is correctly routed through the custom tool handler before falling back to Unity command dispatch.

Bug Fixes:

  • Fix execute_custom_tool commands being bypassed when a unity_instance is specified in the CLI command endpoint.
  • Return a clear error when a requested unity_instance does not exist before attempting custom tool execution.
  • Validate that a usable Unity session exists before executing a custom tool and surface appropriate errors when it does not.

Enhancements:

  • Apply consistent session selection logic for custom tools by using the first available session when no unity_instance is specified.

Summary by CodeRabbit

  • Bug Fixes

    • Return 404 when a requested Unity instance is not found.
    • Improved validation and clearer error responses (400/503) for missing sessions, missing tool name, or invalid tool parameters.
    • Resolve project context for tool execution and surface errors when resolution fails.
  • Refactor

    • Streamlined custom-tool execution flow to use a single service and return tool results directly, improving reliability.

Move execute_custom_tool handling outside the if/else block so it's
executed regardless of whether unity_instance is provided.

Previously, when unity_instance was specified, the code would take the
if branch (match by hash/name) and return 404 if not found,
but the execute_custom_tool block was only in the else branch,
causing custom tools to fall through to PluginHub.send_command.

Fixes CoplayDev#649

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 11, 2026 20:21
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 11, 2026

Reviewer's Guide

Adjusts CLI /api/command routing so execute_custom_tool is always handled via CustomToolService with proper session validation, regardless of whether a unity_instance is specified, while preserving Unity command routing for all other command types.

Sequence diagram for updated CLI execute_custom_tool routing

sequenceDiagram
    actor CLI
    participant ApiServer
    participant SessionStore
    participant CustomToolService

    CLI->>ApiServer: POST /api/command
    activate ApiServer
    ApiServer->>SessionStore: resolve session_id, session_details (optional unity_instance)
    SessionStore-->>ApiServer: session_id or none, session_details

    alt unity_instance specified but not found
        ApiServer-->>CLI: 404 JSONResponse Unity instance not found
    else session resolved or first session selected
        alt command_type is execute_custom_tool
            ApiServer->>ApiServer: validate session_id and session_details
            alt invalid session
                ApiServer-->>CLI: 503 JSONResponse No valid Unity session
            else valid session
                ApiServer->>ApiServer: parse tool_name and tool_params from params
                alt missing tool_name
                    ApiServer-->>CLI: 400 JSONResponse Missing tool_name
                else invalid tool_params type
                    ApiServer-->>CLI: 400 JSONResponse Tool parameters must be dict
                else valid tool parameters
                    ApiServer->>ApiServer: compute unity_instance_hint from session_details.hash or unity_instance
                    ApiServer->>ApiServer: resolve_project_id_for_unity_instance
                    alt project_id not found
                        ApiServer-->>CLI: 400 JSONResponse Could not resolve project id
                    else project_id found
                        ApiServer->>CustomToolService: execute_tool(project_id, tool_name, unity_instance_hint, tool_params)
                        CustomToolService-->>ApiServer: result
                        ApiServer-->>CLI: JSONResponse result.model_dump()
                    end
                end
            end
        else command_type is not execute_custom_tool
            ApiServer->>ApiServer: fall through to Unity routing
            ApiServer-->>CLI: (handled by PluginHub in separate path)
        end
    end
    deactivate ApiServer
Loading

Flow diagram for CLI command routing with execute_custom_tool handling

flowchart TD
    A_start[Start cli_command_route] --> B_getParams[Read command_type, params, unity_instance]
    B_getParams --> C_hasUnityInstance{unity_instance specified?}
    C_hasUnityInstance -->|yes| D_findSession[Lookup session by unity_instance]
    C_hasUnityInstance -->|no| E_skipLookup[Skip direct lookup]

    D_findSession --> F_checkFound{session_id found?}
    F_checkFound -->|no| G_404[Return 404 Unity instance not found]
    F_checkFound -->|yes| H_haveSession[Have session_id and session_details]

    E_skipLookup --> I_hasSessionFromElse{session_id already set?}
    I_hasSessionFromElse -->|yes| H_haveSession
    I_hasSessionFromElse -->|no| J_useFirst[Set session_id, session_details to first available session]
    J_useFirst --> H_haveSession

    H_haveSession --> K_commandType{command_type == execute_custom_tool?}

    K_commandType -->|yes| L_validateSession{session_id and session_details valid?}
    L_validateSession -->|no| M_503[Return 503 No valid Unity session]

    L_validateSession -->|yes| N_parseParams[Extract tool_name, tool_params from params]
    N_parseParams --> O_hasToolName{tool_name present?}
    O_hasToolName -->|no| P_400_name[Return 400 Missing tool_name]

    O_hasToolName -->|yes| Q_paramsType{tool_params is dict?}
    Q_paramsType -->|no| R_400_params[Return 400 Tool parameters must be dict]

    Q_paramsType -->|yes| S_computeHint[Set unity_instance_hint from session_details.hash or unity_instance]
    S_computeHint --> T_resolveProject[resolve_project_id_for_unity_instance]
    T_resolveProject --> U_hasProject{project_id found?}
    U_hasProject -->|no| V_400_project[Return 400 Could not resolve project id]
    U_hasProject -->|yes| W_executeTool[CustomToolService.execute_tool]
    W_executeTool --> X_returnTool[Return JSONResponse result.model_dump]

    K_commandType -->|no| Y_unityRouting[Send command to Unity via PluginHub.send_command]
    Y_unityRouting --> Z_returnUnity[Return Unity command result]

    G_404 --> AA_end[End]
    M_503 --> AA_end
    P_400_name --> AA_end
    R_400_params --> AA_end
    V_400_project --> AA_end
    X_returnTool --> AA_end
    Z_returnUnity --> AA_end
Loading

File-Level Changes

Change Details Files
Ensure execute_custom_tool path runs regardless of unity_instance and before PluginHub.send_command, with added session and project resolution validation.
  • Move execute_custom_tool handling outside the unity_instance conditional so it executes for both specified and unspecified instances
  • Change the unity_instance-not-found check to run before custom tool handling and only when a unity_instance was explicitly requested
  • Default to the first available session when no session_id is resolved, even if no unity_instance is provided
  • Add explicit validation that a valid session_id and session_details exist before executing a custom tool, returning 503 when no valid Unity session is available
  • Normalize and validate tool_name and tool_params, including type checks and defaulting params to an empty dict
  • Use session_details.hash when available as the preferred unity_instance_hint for resolve_project_id_for_unity_instance, falling back to unity_instance
  • Return appropriate 4xx errors when the tool name is missing, tool parameters are invalid, or project id resolution fails, and return the CustomToolService result directly as JSONResponse
Server/src/main.py

Possibly linked issues


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 Feb 11, 2026

📝 Walkthrough

Walkthrough

Moves and expands execute_custom_tool handling in the /api/command POST route so custom tool requests run regardless of a supplied unity_instance. Adds validation for session, tool_name, and tool_params, resolves project_id, and invokes CustomToolService.execute_tool, returning its result; non-custom commands still go to PluginHub.send_command.

Changes

Cohort / File(s) Summary
Command endpoint logic
Server/src/main.py
Reworks /api/command POST flow: pulls out execute_custom_tool handling to run before falling through to PluginHub.send_command. Validates session selection, extracts tool_name/tool_params, checks types, determines unity_instance_hint (explicit unity_instance or session hash), resolves project_id, calls CustomToolService.execute_tool, and returns its model_dump. Adds 404/400/503 responses for missing resources or invalid payloads.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(200,200,255,0.5)
    participant Client
    participant API_Server as Server(/api/command)
    participant CTS as CustomToolService
    participant PH as PluginHub
    participant Unity
  end

  Client->>API_Server: POST /api/command { command_type, unity_instance?, params, ... }
  API_Server->>API_Server: validate session / unity_instance
  alt command_type == "execute_custom_tool"
    API_Server->>CTS: resolve project_id (unity_instance_hint) and execute_tool(tool_name, params)
    CTS-->>API_Server: model_dump (result)
    API_Server-->>Client: 200 { model_dump }
  else other command_type
    API_Server->>PH: PluginHub.send_command(command)
    PH-->>Unity: deliver command
    PH-->>API_Server: command result
    API_Server-->>Client: command result
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • fix: Multi-session UI improvements and HTTP instance recognition #517 — Adjusts execute_custom_tool flow to resolve and pass unity_instance/project_id into CustomToolService (closely related execution/project resolution changes).
  • Remote server auth #644 — Earlier change referenced in linked issue that altered /api/command flow; this PR fixes a logic regression introduced around that area.
  • (linked issue) #649 — Bug report describing execute_custom_tool being bypassed when unity_instance is provided; this PR implements the suggested fix.

Poem

🐰 I hopped through routes and fixed a hole,
Tools now run whether instance's in the scroll,
I nudged the flow, resolved the id,
Now commands and carrots both glide with pride 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main bug being fixed: execute_custom_tool bypassed when unity_instance is specified.
Description check ✅ Passed The description comprehensively covers the bug, root cause, changes made, testing performed, and related issue, following most template sections.
Linked Issues check ✅ Passed All code requirements from issue #649 are met: custom tool handling moved outside conditional, session validation added, params validated, unity_instance_hint computed, and tool execution properly routed.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the execute_custom_tool bypass bug by reorganizing the command routing logic in the /api/command endpoint.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
Server/src/main.py (1)

446-447: Nit: tool_params is None check is redundant.

Line 438 already defaults tool_params to {} via the or {} fallback, so tool_params can never be None when reaching line 446. This dead branch doesn't cause harm but could be removed for clarity.


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 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `Server/src/main.py:398-407` </location>
<code_context>
+                    )
+
+                # If no specific unity_instance requested, use first available session
+                if not session_id:
                     session_id = next(iter(sessions.sessions.keys()))
                     session_details = sessions.sessions.get(session_id)

</code_context>

<issue_to_address>
**issue (bug_risk):** Guard against empty `sessions.sessions` before calling `next(iter(...))` to avoid `StopIteration` at runtime.

In the no-`unity_instance` path, `session_id = next(iter(sessions.sessions.keys()))` will raise `StopIteration` when there are no active sessions. Since `execute_custom_tool` now guards on missing `session_id`/`session_details`, it would be better to mirror that behavior here by checking that `sessions.sessions` is non-empty before calling `next(...)`, and returning a 503 (or similar) if no sessions are available, instead of letting an exception bubble up.
</issue_to_address>

### Comment 2
<location> `Server/src/main.py:408-411` </location>
<code_context>
                     session_id = next(iter(sessions.sessions.keys()))
                     session_details = sessions.sessions.get(session_id)

-                    if command_type == "execute_custom_tool":
-                        tool_name = None
+                # Custom tool execution - must be checked BEFORE the final PluginHub.send_command call
+                # This applies to both cases: with or without explicit unity_instance
+                if command_type == "execute_custom_tool":
+                    if not session_id or not session_details:
+                        return JSONResponse(
+                            {"success": False,
</code_context>

<issue_to_address>
**suggestion:** Align the `execute_custom_tool` session validation with how `session_id` is derived so the guard can actually be effective.

The new guard is unlikely to run as intended because `session_id` is always set via `next(iter(sessions.sessions.keys()))` first: if that succeeds, `session_id` is truthy and `session_details` will almost never be `None`; if it fails (empty `sessions.sessions`), we never reach the guard because `next(...)` raises.

To make the 503 path reliably reachable:
- Move the default-session selection (`if not session_id: ...`) into the `execute_custom_tool` block and guard against an empty `sessions.sessions`, or
- Extract a helper that safely returns `(session_id, session_details)` or `None`, and let `execute_custom_tool` handle the `None` case consistently.

```suggestion
                # If no specific unity_instance requested, use first available session (if any)
                if not session_id:
                    try:
                        session_id = next(iter(sessions.sessions.keys()))
                    except StopIteration:
                        session_id = None
                        session_details = None
                    else:
                        session_details = sessions.sessions.get(session_id)
```
</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.

whatevertogo and others added 2 commits February 12, 2026 04:24
When no active sessions exist, next(iter(sessions.sessions.keys()))
raises StopIteration. Catch this exception and set session_id
and session_details to None, allowing execute_custom_tool guard to work
as intended.

Suggested by Sourcery review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Per Sourcery review: moving the default session selection
inside execute_custom_tool ensures the empty sessions guard works
correctly. If sessions becomes empty between the initial check
and this code, the StopIteration will now properly trigger the
session validation guard and return a 503 error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a routing bug in the local CLI /api/command endpoint where command_type: "execute_custom_tool" could be skipped when unity_instance was provided, ensuring custom tool execution is handled consistently regardless of instance selection.

Changes:

  • Moves execute_custom_tool handling so it runs before the fallback PluginHub.send_command path.
  • Ensures unity_instance resolution/validation happens in a way that supports both explicit and implicit (first available) session selection.
  • Adds an explicit “valid session required” check before executing a custom tool.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@whatevertogo whatevertogo changed the base branch from main to beta February 11, 2026 20:29
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/main.py`:
- Around line 410-472: The default session selection logic (the block that sets
session_id = next(iter(sessions.sessions.keys())) and session_details) currently
lives inside the execute_custom_tool branch causing non-custom commands to get a
None session_id; move that default-selection block so it runs before the
execute_custom_tool check (i.e., apply to all command types) so
PluginHub.send_command receives a valid session_id; ensure you keep the
StopIteration fallback (setting session_id/session_details to None) and the
subsequent existing checks that return 503/400 unchanged, and update any
references to unity_instance_hint/project resolution
(resolve_project_id_for_unity_instance and CustomToolService.execute_tool) to
run only inside the execute_custom_tool branch as before.
🧹 Nitpick comments (1)
Server/src/main.py (1)

414-420: StopIteration handler is unreachable.

At this point, sessions.sessions is guaranteed non-empty (line 378 returns 503 otherwise), and we only enter this block when unity_instance was not provided (the 404 at line 399 handles the "provided but not found" case). So next(iter(sessions.sessions.keys())) will always succeed. The try/except StopIteration is dead code.

Not harmful, but if you adopt the fix suggested above (moving default session selection out of the custom-tool block), this becomes moot.

@whatevertogo
Copy link
Contributor Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@Scriptwonder Scriptwonder merged commit 18e7a32 into CoplayDev:beta Feb 12, 2026
2 checks passed
msanatan pushed a commit to msanatan/unity-mcp that referenced this pull request Feb 25, 2026
…playDev#724)

* Fix execute_custom_tool bypassed when unity_instance is specified

Move execute_custom_tool handling outside the if/else block so it's
executed regardless of whether unity_instance is provided.

Previously, when unity_instance was specified, the code would take the
if branch (match by hash/name) and return 404 if not found,
but the execute_custom_tool block was only in the else branch,
causing custom tools to fall through to PluginHub.send_command.

Fixes CoplayDev#649

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Handle empty sessions gracefully when selecting default session

When no active sessions exist, next(iter(sessions.sessions.keys()))
raises StopIteration. Catch this exception and set session_id
and session_details to None, allowing execute_custom_tool guard to work
as intended.

Suggested by Sourcery review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move default session selection into execute_custom_tool block

Per Sourcery review: moving the default session selection
inside execute_custom_tool ensures the empty sessions guard works
correctly. If sessions becomes empty between the initial check
and this code, the StopIteration will now properly trigger the
session validation guard and return a 503 error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove unintended uv.lock changes from PR

* 优化 execute_custom_tool 逻辑以优先使用可用会话

---------

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
msanatan pushed a commit to msanatan/unity-mcp that referenced this pull request Feb 25, 2026
…playDev#724)

* Fix execute_custom_tool bypassed when unity_instance is specified

Move execute_custom_tool handling outside the if/else block so it's
executed regardless of whether unity_instance is provided.

Previously, when unity_instance was specified, the code would take the
if branch (match by hash/name) and return 404 if not found,
but the execute_custom_tool block was only in the else branch,
causing custom tools to fall through to PluginHub.send_command.

Fixes CoplayDev#649

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Handle empty sessions gracefully when selecting default session

When no active sessions exist, next(iter(sessions.sessions.keys()))
raises StopIteration. Catch this exception and set session_id
and session_details to None, allowing execute_custom_tool guard to work
as intended.

Suggested by Sourcery review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move default session selection into execute_custom_tool block

Per Sourcery review: moving the default session selection
inside execute_custom_tool ensures the empty sessions guard works
correctly. If sessions becomes empty between the initial check
and this code, the StopIteration will now properly trigger the
session validation guard and return a 503 error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove unintended uv.lock changes from PR

* 优化 execute_custom_tool 逻辑以优先使用可用会话

---------

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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.

Bug: execute_custom_tool bypassed when unity_instance is specified in CLI /api/command endpoint

3 participants