# Section 03: MCP Wrapper Updates

## Overview

This section modifies the `godotiq_node_ops` MCP wrapper in `src/godotiq/tools/bridge/__init__.py` to:

1. Add a `scene` parameter for specifying which `.tscn` file to validate against.
2. Access the `GodotIQSession` via the MCP context to resolve scene spatial data.
3. Convert `WorldNode` objects to the validator's dict format.
4. Pass `scene_data` down to the `_node_ops()` call.

This is the **first bridge tool to access the session via MCP context**. All other bridge tools currently ignore `ctx`. This deliberately crosses the architectural boundary -- bridge tools (WebSocket) now also consult filesystem-parsed state when validation is requested.

**Depends on:** section-01-validator (spatial_validator.py must exist), section-02-node-ops-integration (node_ops must accept `scene_data` parameter).

## File to Modify

`src/godotiq/tools/bridge/__init__.py`

## Tests First

Add these tests to `tests/test_bridge/test_spatial_validator.py` (appending to the file created in sections 01 and 02). These tests verify the MCP wrapper's session-access and scene-resolution logic.

The wrapper tests require heavier mocking than the pure validator or node_ops integration tests because they exercise the full path: MCP context -> session -> resolve_scene_spatial -> WorldNode conversion -> node_ops call.

### Test stubs

```python
class TestMCPWrapperValidation:
    """Tests for godotiq_node_ops MCP wrapper's scene resolution and validation plumbing."""

    # Test: scene parameter not provided + no validate ops -> normal bridge call (no session access)
    # Setup: operations without validate:true, mock bridge
    # Assert: bridge called normally, ctx.request_context never accessed

    # Test: validate ops + ctx=None -> validation skipped, bridge called normally
    # Setup: operations with validate:true, ctx=None, mock bridge
    # Assert: bridge.request called, no error

    # Test: validate ops + session=None -> validation skipped, bridge called normally
    # Setup: operations with validate:true, ctx with session=None, mock bridge
    # Assert: bridge.request called, scene_data=None passed through

    # Test: validate ops + valid session + scene param -> resolve_scene_spatial called with correct res_path
    # Setup: mock session with to_res_path and resolve_scene_spatial returning WorldNode list, mock bridge
    # Assert: session.resolve_scene_spatial called, scene_data dict has correct "nodes" structure, node_ops called with scene_data

    # Test: resolve_scene_spatial returns None -> validation skipped gracefully
    # Setup: mock session.resolve_scene_spatial returning None, mock bridge
    # Assert: bridge called normally, no crash

    # Test: no scene param + no main scene found -> validation skipped
    # Setup: validate:true ops, session with project_root but no project.godot or missing run/main_scene
    # Assert: bridge called normally, scene_data=None

    # Test: WorldNode conversion produces correct dict shape
    # Setup: mock WorldNode with known fields
    # Assert: scene_data["nodes"][0] has keys: name, type, position, scale, depth
    # Assert: position and scale are plain lists (not tuples)
```

### Mocking strategy

The wrapper test needs to mock several layers:

1. **Bridge mock**: patch `godotiq.tools.bridge.node_ops.get_bridge` to return an `AsyncMock` with a `.request` return value.
2. **Session mock**: Create a `MagicMock` for `GodotIQSession` with `.to_res_path()`, `.resolve_scene_spatial()`, and `.project_root` attributes.
3. **Context mock**: Create a mock `ctx` object where `ctx.request_context.lifespan_context["session"]` returns the session mock.
4. **WorldNode mock**: Create mock objects with `.node.name`, `.node.type`, `.world_position`, `.world_scale`, `.depth` attributes.

The pattern for creating a mock context follows the established convention used by `godotiq_scene_map` and `godotiq_project_summary` in `src/godotiq/tools/spatial/__init__.py` and `src/godotiq/tools/memory/__init__.py`:

```python
ctx = MagicMock()
ctx.request_context.lifespan_context = {"session": mock_session}
```

## Implementation Details

### Updated Function Signature

The `godotiq_node_ops` wrapper adds a `scene` parameter:

```python
async def godotiq_node_ops(
    operations: list[dict] | None = None,
    scene: str | None = None,
    ctx: Context = None,
) -> dict:
```

### Logic Flow Inside the Wrapper

The wrapper's body, before calling `_node_ops()`, needs to:

1. **Check if any operation has `validate: true`** -- scan the `operations` list. If none do, skip all session work and call `_node_ops(operations=operations or [], scene_data=None)` directly.

2. **Guard: ctx is None** -- if `ctx` is `None`, skip validation. Pass `scene_data=None`.

3. **Guard: session is None** -- access `ctx.request_context.lifespan_context["session"]`. If `None`, skip validation. Pass `scene_data=None`.

4. **Resolve the scene res:// path**:
   - If `scene` parameter is provided: use `session.to_res_path(scene)` to normalize it.
   - If `scene` parameter is not provided: fall back to finding the main scene from `project.godot`. Reuse the `_read_main_scene` helper from `godotiq.tools.memory` (import it). If the main scene is empty string, skip validation.

5. **Call `session.resolve_scene_spatial(res_path)`** -- this returns `tuple[ResolvedScene, list[WorldNode]]` or `None`. If `None`, skip validation.

6. **Convert WorldNode list to validator dict format**:
   ```python
   scene_data = {"nodes": [
       {
           "name": wn.node.name,
           "type": wn.node.type,
           "position": list(wn.world_position),
           "scale": list(wn.world_scale),
           "depth": wn.depth,
       }
       for wn in world_nodes
   ]}
   ```
   Note: `wn.world_position` and `wn.world_scale` are tuples, so `list()` is needed. The `wn.node.name` and `wn.node.type` come from the `TscnNode` reference inside each `WorldNode`.

7. **Pass to node_ops**: call `_node_ops(operations=operations or [], scene_data=scene_data)`.

The entire validation-preparation block should be wrapped in a `try/except Exception` to ensure that any failure in scene resolution degrades gracefully to `scene_data=None` rather than crashing the tool. This matches the "best-effort" constraint.

### Import Additions

The following imports are needed at the top of `__init__.py`:

- `from godotiq.tools.memory import _read_main_scene` -- reuse the existing helper that parses `project.godot` for `run/main_scene`.

No other new imports are needed. The session is accessed dynamically from the context dict, so no type import for `GodotIQSession` is required (though adding it for clarity is fine).

### Updated Tool Docstring

The docstring should document:

- The `validate: true` flag on individual operations enables spatial validation.
- The `scene` parameter specifies which `.tscn` to validate against (optional; falls back to the project's main scene from `project.godot`).
- Three possible response shapes: normal (no validation), blocked (validation prevented execution), success-with-warnings (validation passed with warnings).
- Atomic batch behavior: if any single operation is blocked, no operations in the batch execute.

### Error Handling

All existing exception handlers in the wrapper (`BridgeConnectionError`, `BridgeTimeoutError`, `BridgeError`, `ValueError`, generic `Exception`) remain unchanged. The new scene-resolution code is entirely inside the `try` block. If `resolve_scene_spatial` raises, the generic `except Exception` catches it and returns an error dict -- but ideally the inline try/except around the scene resolution block catches it first and degrades to `scene_data=None`.

### Backward Compatibility

Existing calls to `godotiq_node_ops` without the `scene` parameter and without `validate: true` in any operation produce identical behavior to the current code. The `scene_data=None` default in the modified `_node_ops()` signature (from section-02) ensures no bridge call changes occur.

### Key Architectural Notes

- **Why `resolve_scene_spatial` instead of `_build_scene_map`**: The scene_map MCP tool filters to depth <= 1 and caps at 50 nodes. Walls deeper in the tree (e.g., `Room/Walls/Wall_East`) would be invisible. `resolve_scene_spatial` returns ALL nodes at ALL depths with world-space transforms already computed.

- **Stale data caveat**: Validation reads `.tscn` files from disk. Unsaved editor changes (from prior `node_ops` calls without `save_scene`) are not reflected. The tool docstring should note this.

- **This is the first bridge tool to use session state**: All other bridge tools treat `ctx` as unused. This sets a precedent for future bridge tools that may also need filesystem-parsed context.
