CoplayDev/unity-mcp · error · ValueError

'commands' must be a non-empty list of command specification

Error message

'commands' must be a non-empty list of command specifications

What it means

Input validation in the batch_execute MCP tool: the `commands` argument must be a non-empty Python list. The check `not isinstance(commands, list) or not commands` rejects None, dicts, tuples, strings, and empty lists. batch_execute proxies a list of {tool, params} sub-commands to Unity's batch transporter, so an empty list has nothing to execute and a non-list is a caller contract violation.

Source

Thrown at Server/src/services/tools/batch_execute.py:91

        title="Batch Execute",
        destructiveHint=True,
    ),
)
async def batch_execute(
    ctx: Context,
    commands: Annotated[list[dict[str, Any]], "List of commands with 'tool' and 'params' keys."],
    parallel: Annotated[bool | None,
                        "Attempt to run read-only commands in parallel"] = None,
    fail_fast: Annotated[bool | None,
                         "Stop processing after the first failure"] = None,
    max_parallelism: Annotated[int | None,
                               "Hint for the maximum number of parallel workers"] = None,
) -> dict[str, Any]:
    """Proxy the batch_execute tool to the Unity Editor transporter."""
    unity_instance = await get_unity_instance_from_context(ctx)

    if not isinstance(commands, list) or not commands:
        raise ValueError(
            "'commands' must be a non-empty list of command specifications")

    max_commands = await _get_max_commands_from_editor_state(ctx)
    if len(commands) > max_commands:
        raise ValueError(
            f"batch_execute supports up to {max_commands} commands (configured in Unity); received {len(commands)}"
        )

    normalized_commands: list[dict[str, Any]] = []
    for index, command in enumerate(commands):
        if not isinstance(command, dict):
            raise ValueError(
                f"Command at index {index} must be an object with 'tool' and 'params' keys")

        tool_name = command.get("tool")
        params = command.get("params", {})

        if not tool_name or not isinstance(tool_name, str):

View on GitHub (pinned to c21bf496bc)

Solutions

  1. Ensure commands is a list with at least one {"tool": "...", "params": {...}} entry.
  2. If building the list dynamically, guard against empty before calling: only call batch_execute when len(commands) > 0.
  3. Use the correct shape: [{"tool": "manage_gameobject", "params": {"action": "create", "name": "Cube"}}].

Example fix

// before (caller)
await batch_execute(ctx, commands=[])
// after
await batch_execute(ctx, commands=[{"tool": "manage_gameobject", "params": {"action": "create", "name": "Cube"}}])
Defensive patterns

Strategy: validation

Validate before calling

# Validate commands before calling batch_execute
def validate_batch_commands(commands: list) -> None:
    if not isinstance(commands, list) or not commands:
        raise ValueError("commands must be a non-empty list")
    for i, cmd in enumerate(commands):
        if not isinstance(cmd, dict):
            raise ValueError(f"Command {i} must be a dict")
        if not cmd.get("tool") or not isinstance(cmd.get("tool"), str):
            raise ValueError(f"Command {i} missing valid 'tool'")
        params = cmd.get("params", {})
        if params is None:
            params = {}
        if not isinstance(params, dict):
            raise ValueError(f"Command {i} params must be a dict")

validate_batch_commands(commands)

Type guard

def is_valid_command_list(commands: object) -> bool:
    if not isinstance(commands, list) or len(commands) == 0:
        return False
    return all(
        isinstance(c, dict)
        and isinstance(c.get("tool"), str)
        and c.get("tool", "") != ""
        and (c.get("params", {}) is None or isinstance(c.get("params"), dict))
        for c in commands
    )

Prevention

When it happens

Trigger: An AI assistant or caller passes commands=null, commands={}, commands="tool_name", or commands=[]; a JSON-RPC payload where 'commands' is omitted (None after default) or explicitly empty; a caller that builds the list dynamically and it ended up empty due to a filtering bug.

Common situations: An LLM generates an empty batch by mistake; a programmatic caller passes a single tool name as a string instead of a list of dicts; a pipeline that filters commands down to zero entries before calling batch_execute; a caller confusing batch_execute with a single-command tool.

Related errors


AI-assisted analysis of CoplayDev/unity-mcp@c21bf496bc (2026-08-13). Data as JSON: /api/errors/f8cb9637577763f9. Report an issue: GitHub.