# Section 01: Python-side Tool -- `build_scene.py`

## Overview

This section creates the Python-side implementation of the `godotiq_build_scene` MCP tool. The tool enables AI agents to create multiple Godot nodes in a single call using high-level patterns (grid, line, scatter, nodes), solving the problem where `node_ops` requires one operation dict per node, making complex scenes impractical.

The architecture is: AI Agent calls MCP tool (Python) which validates parameters, then sends a single `build_scene` WebSocket request to the Godot addon (GDScript) which does the actual pattern expansion and node creation. This section covers only the Python side -- the GDScript handler is in section-02.

## Dependencies

- **section-02-gdscript-handler**: The GDScript addon must handle the `build_scene` method for this tool to work end-to-end. However, this section is independently implementable and testable (validation logic needs no bridge).
- **section-04-tests**: Tests for `validate_build_params` are defined in this section. section-04 covers backward compatibility and integration verification.

## Files to Create/Modify

| File | Action |
|------|--------|
| `src/godotiq/tools/bridge/build_scene.py` | **CREATE** |
| `src/godotiq/tools/bridge/__init__.py` | **MODIFY** (add import + `@mcp.tool()` wrapper) |
| `src/godotiq/bridge/protocol.py` | **MODIFY** (add timeout entry) |
| `tests/test_tools/test_build_scene.py` | **CREATE** |

## Tests (Write First)

Create `tests/test_tools/test_build_scene.py`.

The tests exercise `validate_build_params` directly -- no bridge connection or running Godot addon is needed. This function was extracted specifically to enable unit testing.

```python
"""Tests for build_scene validation logic."""

from __future__ import annotations

from godotiq.tools.bridge.build_scene import validate_build_params


class TestModeExclusivity:
    """Exactly one of grid/line/scatter/nodes must be provided."""

    def test_no_mode_returns_error(self) -> None:
        """Passing zero modes returns an error dict."""

    def test_multiple_modes_returns_error(self) -> None:
        """Passing grid + scatter simultaneously returns an error."""


class TestGridValidation:
    """Grid mode parameter validation."""

    def test_valid_grid(self) -> None:
        """Grid with rows=3, cols=3, spacing=2.0 passes; returned dict has 'grid' key."""

    def test_single_node_grid(self) -> None:
        """Grid 1x1 passes validation (count=1)."""

    def test_grid_missing_rows(self) -> None:
        """Grid without 'rows' key returns error."""

    def test_grid_zero_cols(self) -> None:
        """Grid with cols=0 returns error."""

    def test_grid_exceeds_node_limit(self) -> None:
        """Grid with rows=17, cols=16 (272 nodes) returns error mentioning 256 limit."""


class TestLineValidation:
    """Line mode parameter validation."""

    def test_valid_line(self) -> None:
        """Line with 2 points passes validation."""

    def test_line_insufficient_points(self) -> None:
        """Line with only 1 point returns error (needs >= 2)."""


class TestScatterValidation:
    """Scatter mode parameter validation."""

    def test_valid_scatter(self) -> None:
        """Scatter with 3 items passes; returned dict has 'scatter' key."""

    def test_scatter_missing_items(self) -> None:
        """Scatter without 'items' key returns error."""

    def test_scatter_empty_items(self) -> None:
        """Scatter with empty items list returns error."""

    def test_scatter_exceeds_node_limit(self) -> None:
        """Scatter with 257 items returns error mentioning 256."""


class TestNodesValidation:
    """Nodes mode parameter validation."""

    def test_valid_nodes(self) -> None:
        """Nodes list with 4 items passes validation."""

    def test_nodes_empty_list(self) -> None:
        """Nodes as empty list returns error."""
```

**Key assertions:**

- **Mode exclusivity tests**: Call `validate_build_params(grid=None, line=None, scatter=None, nodes=None)` for zero-mode. Both modes return `(None, error_dict)` where `error_dict` has an `"error"` key.
- **Valid-mode tests**: Return `(params_dict, None)`. The `params_dict` contains the mode key.
- **Invalid sub-field tests**: Return `(None, error_dict)` with descriptive error messages.
- **Node count limit tests**: The error message should mention `256`.

## Implementation Details

### 1. Create `src/godotiq/tools/bridge/build_scene.py`

Two functions: a pure validation function and an async bridge caller.

**`validate_build_params` function:**

```python
def validate_build_params(
    grid: dict | None,
    line: dict | None,
    scatter: dict | None,
    nodes: list[dict] | None,
) -> tuple[dict, dict | None]:
    """Validate build_scene parameters.

    Returns (params_dict, None) on success, or (None, error_dict) on failure.
    """
```

Validation logic:

1. **Mode exclusivity**: Count non-None among grid, line, scatter, nodes. Must be exactly 1.

2. **Required sub-fields per mode:**
   - Grid: `rows` (int > 0), `cols` (int > 0) required
   - Line: `points` array with len >= 2
   - Scatter: `items` key with non-empty list
   - Nodes: non-empty list

3. **Node count limit (256 max):**
   - Grid: `rows * cols`
   - Scatter: `len(scatter["items"])`
   - Nodes: `len(nodes)`
   - Line: Sum Euclidean distances between consecutive points, divide by `spacing` (default 1.0), add 1 for endpoint. Uses `math.sqrt` for 3D distance.

4. On success, return `({"grid": grid}, None)` (or whichever mode). On failure, `(None, {"error": "message"})`.

**`build_scene` async function:**

```python
async def build_scene(
    grid: dict | None = None,
    line: dict | None = None,
    scatter: dict | None = None,
    nodes: list[dict] | None = None,
    parent: str = "",
    target_scene: str = "",
) -> dict:
    """Send a build_scene request to the Godot addon."""
```

1. Call `validate_build_params(grid, line, scatter, nodes)`.
2. If error, return error dict directly.
3. Call `get_bridge()` then `bridge.request("build_scene", params={**params_dict, "parent": parent, "target_scene": target_scene})`.
4. Return bridge response. Does NOT catch bridge exceptions (wrapper handles that).

### 2. Modify `src/godotiq/tools/bridge/__init__.py`

Add import:

```python
from godotiq.tools.bridge.build_scene import build_scene as _build_scene
```

Add wrapper:

```python
@mcp.tool(
    name="godotiq_build_scene",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_build_scene(
    target_scene: str = "",
    parent: str = "",
    detail: str = "brief",
    grid: dict | None = None,
    line: dict | None = None,
    scatter: dict | None = None,
    nodes: list[dict] | None = None,
    ctx: Context = None,
) -> dict:
    """Create multiple nodes in a scene using high-level patterns. ..."""
```

Key details:
- `target_scene` (not `scene`) avoids collision with per-node `scene` key
- Error handling: standard try/except for BridgeConnectionError, BridgeTimeoutError, BridgeError, Exception
- Output limiting: `apply_output_limit(result, detail, list_keys=["errors"])`
- If `_build_scene` returns dict with `"error"` key (validation failure), return directly without output limit
- Comprehensive docstring describing all 4 modes for AI agent tool descriptions

### 3. Modify `src/godotiq/bridge/protocol.py`

Add to `TOOL_TIMEOUTS`:

```python
"build_scene": 30.0,  # Large batch creation may take time
```

## Design Decisions

- **`validate_build_params` is a pure function** (not async) for unit testability
- **Line count estimation** slightly overestimates vs GDScript (safe side). GDScript 256 cap is defense-in-depth.
- **`build_scene` raises bridge exceptions** — wrapper in `__init__.py` handles conversion
- **`apply_output_limit` uses `list_keys=["errors"]`** — only errors array can grow large

## Implementation Notes (Post-Build)

**Actual files created/modified:**
- `src/godotiq/tools/bridge/build_scene.py` — Created as planned
- `src/godotiq/tools/bridge/__init__.py` — Modified: added import + MCP wrapper
- `src/godotiq/bridge/protocol.py` — Modified: added `build_scene: 30.0` timeout
- `tests/test_tools/test_build_scene.py` — Created with 18 tests (3 more than planned)

**Deviations from plan:**
- Return type annotation uses `tuple[dict | None, dict | None]` (more correct than plan's `tuple[dict, dict | None]`)
- Line validation adds explicit point structure check (must be list/tuple with >= 2 numbers) to prevent IndexError on malformed input
- Added 3 extra tests from code review: `test_nodes_exceeds_node_limit`, `test_line_exceeds_node_limit`, `test_line_malformed_point`

**Final test count: 18 (all passing)**
