# Section 08: Tool Registration

## Overview

This section adds MCP tool registration wrappers for the 4 new Sprint 4b tools in `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/bridge/__init__.py`. Each wrapper imports its implementation function, decorates it with `@mcp.tool()` and appropriate annotations, and provides exception handling.

The existing file already contains 4 tool wrappers from Sprint 4a (`godotiq_screenshot`, `godotiq_run`, `godotiq_perf_snapshot`, `godotiq_editor_context`). This section adds 4 more:

- **`godotiq_scene_tree`** -- bridge tool, catches `BridgeConnectionError` / `BridgeTimeoutError` / `BridgeError`
- **`godotiq_node_ops`** -- bridge tool, same exception handling pattern
- **`godotiq_script_ops`** -- filesystem tool, catches generic `Exception`
- **`godotiq_file_ops`** -- filesystem tool, catches generic `Exception`

**File to modify:** `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/bridge/__init__.py`

**Dependencies:**
- Section 03 (scene_tree Python tool) -- provides `scene_tree` function in `src/godotiq/tools/bridge/scene_tree.py`
- Section 05 (node_ops Python tool) -- provides `node_ops` function in `src/godotiq/tools/bridge/node_ops.py`
- Section 06 (script_ops) -- provides `script_ops` function in `src/godotiq/tools/bridge/script_ops.py`
- Section 07 (file_ops) -- provides `file_ops` function in `src/godotiq/tools/bridge/file_ops.py`

---

## Tests First

The registration tests verify that each MCP wrapper correctly catches exceptions and returns user-friendly error dicts. These tests go in `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_bridge/test_registration.py`.

```python
"""Tests for MCP tool registration wrappers in bridge/__init__.py."""

from __future__ import annotations

import pytest
from unittest.mock import AsyncMock, patch

from godotiq.tools.bridge import (
    godotiq_scene_tree,
    godotiq_node_ops,
    godotiq_script_ops,
    godotiq_file_ops,
)


class TestSceneTreeRegistration:
    """scene_tree MCP wrapper exception handling."""

    async def test_catches_bridge_connection_error(self) -> None:
        """BridgeConnectionError is caught and returns a user-friendly message with hint."""
        from godotiq.bridge.connection import BridgeConnectionError

        with patch(
            "godotiq.tools.bridge.scene_tree.get_bridge",
            side_effect=BridgeConnectionError("not connected"),
        ):
            result = await godotiq_scene_tree()

        assert "error" in result
        assert "hint" in result

    async def test_catches_bridge_timeout_error(self) -> None:
        """BridgeTimeoutError is caught and returns a user-friendly message with hint."""
        from godotiq.bridge.connection import BridgeTimeoutError

        mock_bridge = AsyncMock()
        mock_bridge.request.side_effect = BridgeTimeoutError("timed out")
        with patch(
            "godotiq.tools.bridge.scene_tree.get_bridge",
            return_value=mock_bridge,
        ):
            result = await godotiq_scene_tree()

        assert "error" in result
        assert "hint" in result

    async def test_catches_bridge_error(self) -> None:
        """BridgeError is caught and returns error with code."""
        from godotiq.bridge.connection import BridgeError

        mock_bridge = AsyncMock()
        mock_bridge.request.side_effect = BridgeError("NOT_FOUND", "No scene open")
        with patch(
            "godotiq.tools.bridge.scene_tree.get_bridge",
            return_value=mock_bridge,
        ):
            result = await godotiq_scene_tree()

        assert "error" in result
        assert "NOT_FOUND" in result["error"]


class TestNodeOpsRegistration:
    """node_ops MCP wrapper exception handling."""

    async def test_catches_bridge_connection_error(self) -> None:
        """BridgeConnectionError is caught and returns a user-friendly message."""
        from godotiq.bridge.connection import BridgeConnectionError

        with patch(
            "godotiq.tools.bridge.node_ops.get_bridge",
            side_effect=BridgeConnectionError("not connected"),
        ):
            result = await godotiq_node_ops(
                operations=[{"op": "move", "node": "X", "position": [0, 0, 0]}]
            )

        assert "error" in result
        assert "hint" in result

    async def test_catches_bridge_timeout_error(self) -> None:
        """BridgeTimeoutError is caught and returns a user-friendly message."""
        from godotiq.bridge.connection import BridgeTimeoutError

        mock_bridge = AsyncMock()
        mock_bridge.request.side_effect = BridgeTimeoutError("timed out")
        with patch(
            "godotiq.tools.bridge.node_ops.get_bridge",
            return_value=mock_bridge,
        ):
            result = await godotiq_node_ops(
                operations=[{"op": "move", "node": "X", "position": [0, 0, 0]}]
            )

        assert "error" in result
        assert "hint" in result


class TestScriptOpsRegistration:
    """script_ops MCP wrapper exception handling."""

    async def test_catches_generic_exception(self) -> None:
        """Any unexpected Exception is caught and returns an error dict."""
        with patch(
            "godotiq.tools.bridge.script_ops.script_ops",
            side_effect=RuntimeError("disk full"),
        ):
            # The wrapper calls the imported _script_ops, so we patch at that level
            result = await godotiq_script_ops(op="read", path="test.gd")

        assert "error" in result


class TestFileOpsRegistration:
    """file_ops MCP wrapper exception handling."""

    async def test_catches_generic_exception(self) -> None:
        """Any unexpected Exception is caught and returns an error dict."""
        with patch(
            "godotiq.tools.bridge.file_ops.file_ops",
            side_effect=RuntimeError("permission denied"),
        ):
            result = await godotiq_file_ops(op="list", path=".")

        assert "error" in result
```

### Notes on the test approach

- **Bridge tools** (`scene_tree`, `node_ops`): The tests mock `get_bridge` within the implementation module. The wrapper calls the implementation function, which calls `get_bridge()`, which raises the exception. The wrapper catches it. This exercises the real call chain through the wrapper.

- **Filesystem tools** (`script_ops`, `file_ops`): The tests patch the implementation function itself (at its module location) to raise an unexpected exception. The wrapper catches the generic `Exception`. The exact patch target depends on the import alias pattern -- the wrapper imports `script_ops as _script_ops` and calls `_script_ops(...)`, so patching the original module function at `godotiq.tools.bridge.script_ops.script_ops` ensures the mock propagates through the alias. Alternatively, the test may need to patch `godotiq.tools.bridge._script_ops` directly. The implementer should verify the correct patch target based on the final import pattern.

- The `ctx: Context = None` parameter is omitted from test calls since it defaults to `None` and is unused.

---

## Implementation Details

### File to Modify

`/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/bridge/__init__.py`

### What to Add

Add 4 new import lines and 4 new `@mcp.tool()` decorated wrapper functions. The existing 4 wrappers remain untouched.

### New Imports

Add these alongside the existing implementation imports at the top of the file:

```python
from godotiq.tools.bridge.scene_tree import scene_tree as _scene_tree
from godotiq.tools.bridge.node_ops import node_ops as _node_ops
from godotiq.tools.bridge.script_ops import script_ops as _script_ops
from godotiq.tools.bridge.file_ops import file_ops as _file_ops
```

The underscore-prefixed aliases follow the existing convention (e.g., `screenshot as _screenshot`) to avoid name collisions with the `@mcp.tool()` wrapper functions.

### Wrapper 1: `godotiq_scene_tree`

This is a **bridge tool** (read-only), using the same exception handling pattern as `godotiq_screenshot`, `godotiq_run`, and `godotiq_perf_snapshot`.

```python
@mcp.tool(
    name="godotiq_scene_tree",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def godotiq_scene_tree(
    root: str = "",
    depth: int = 3,
    filter_type: str = "",
    include: list[str] | None = None,
    ctx: Context = None,
) -> dict:
    """Read the live scene tree from the Godot editor.

    Returns the editor's active scene tree with spatial, script, group, and
    visibility context per node. Unlike scene_map (which reads .tscn files
    from disk), this reads the actual live editor state.

    Args:
        root: Node path to start from (e.g. "Level/Enemies"). Empty = scene root.
        depth: Max tree depth (1-10, default 3).
        filter_type: Only include nodes of this type (e.g. "MeshInstance3D").
        include: Data fields per node. Default: ["transform","script","groups","visibility"].
        ctx: MCP context (unused).

    Returns:
        Dict with root, scene_path, total_nodes, returned_nodes, and tree.
    """
    try:
        return await _scene_tree(root=root, depth=depth, filter_type=filter_type, include=include)
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The editor may not be responding or the scene is too large."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}
```

Key annotations:
- `readOnlyHint: True` -- reads the tree but does not modify it
- `idempotentHint: True` -- same call returns same result (given no editor changes)
- `openWorldHint: True` -- requires a live Godot editor connection

### Wrapper 2: `godotiq_node_ops`

This is a **bridge tool** (destructive, non-idempotent), using the same exception chain. The wrapper also needs to handle the `ValueError` that the implementation raises for empty lists or exceeding the 50-operation limit.

```python
@mcp.tool(
    name="godotiq_node_ops",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def godotiq_node_ops(
    operations: list[dict] | None = None,
    ctx: Context = None,
) -> dict:
    """Execute batch node operations in the Godot editor with undo support.

    All operations are grouped as a single undo action (one Ctrl+Z to revert).
    Maximum 50 operations per call.

    Supported ops: move, rotate, scale, set_property, add_child, delete,
    duplicate, reparent.

    Args:
        operations: List of operation dicts, each with an 'op' key.
        ctx: MCP context (unused).

    Returns:
        Dict with results array containing per-operation status.
    """
    try:
        return await _node_ops(operations=operations or [])
    except BridgeConnectionError as e:
        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
    except BridgeTimeoutError as e:
        return {"error": str(e), "hint": "The operation timed out. Try fewer operations per batch."}
    except BridgeError as e:
        return {"error": f"[{e.code}] {e.error_message}"}
    except ValueError as e:
        return {"error": str(e)}
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}
```

Key differences from `scene_tree`:
- `destructiveHint: True` -- modifies the scene tree
- `idempotentHint: False` -- running twice duplicates effects
- The `operations` parameter defaults to `None` in the MCP signature (FastMCP parameter handling) and is coerced to `[]` before passing to the implementation, which then raises `ValueError` for empty lists
- `ValueError` is caught separately to return a clean error message for validation failures (empty list, too many operations)

### Wrapper 3: `godotiq_script_ops`

This is a **filesystem tool** -- no bridge connection needed. Uses a generic `Exception` handler rather than bridge-specific exception types.

```python
@mcp.tool(
    name="godotiq_script_ops",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": False,
    },
)
async def godotiq_script_ops(
    op: str = "read",
    path: str = "",
    content: str = "",
    patches: list[dict] | None = None,
    validate_before_write: bool = True,
    auto_fix: bool = False,
    dry_run: bool = False,
    ctx: Context = None,
) -> dict:
    """Read, write, patch, or create GDScript files with validation.

    Modes:
      - read: Return file content with line count and size.
      - write: Write entire file (with optional GDScript convention validation).
      - patch: Find-and-replace with uniqueness checking (safest edit mode).
      - create: Create new file (fails if file already exists).

    Use this tool for .gd files. For non-script files, use file_ops instead.

    Args:
        op: Operation mode ("read", "write", "patch", "create").
        path: File path (supports res:// and relative paths).
        content: File content for write/create modes.
        patches: List of {search, replace} dicts for patch mode.
        validate_before_write: Run GDScript convention checks before writing.
        auto_fix: Automatically fix detected issues (future feature).
        dry_run: Validate without writing to disk.
        ctx: MCP context (unused).

    Returns:
        Dict with operation result, status, and any validation issues.
    """
    try:
        return await _script_ops(
            op=op,
            path=path,
            content=content,
            patches=patches,
            validate_before_write=validate_before_write,
            auto_fix=auto_fix,
            dry_run=dry_run,
        )
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}
```

Key annotations:
- `openWorldHint: False` -- operates only on the local filesystem, no external connection
- `destructiveHint: True` -- can write/patch/create files

The implementation function (`_script_ops`) returns error information via status fields in the response dict (e.g., `"status": "ERROR"`, `"status": "BLOCKED"`). The `except Exception` clause is only a safety net for truly unexpected failures (disk errors, permission issues, bugs).

### Wrapper 4: `godotiq_file_ops`

This is a **filesystem tool** with the same exception pattern as `script_ops`.

```python
@mcp.tool(
    name="godotiq_file_ops",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": False,
    },
)
async def godotiq_file_ops(
    op: str = "list",
    path: str = "",
    query: str = "",
    filter: str = "all",
    recursive: bool = False,
    context_lines: int = 2,
    max_depth: int = 3,
    show_sizes: bool = False,
    content: str = "",
    destination: str = "",
    ctx: Context = None,
) -> dict:
    """Execute filesystem operations within the Godot project.

    Operations:
      - list: List directory entries with optional type filter and recursion.
      - read: Read text file content (fails on binary files).
      - write: Write file content (use script_ops for .gd files).
      - move: Move/rename a file.
      - delete: Delete a file.
      - search: Text search across files with context lines.
      - tree: Directory tree with depth control and optional size info.

    For .gd files, prefer godotiq_script_ops which provides GDScript validation.

    Args:
        op: Operation ("list", "read", "write", "move", "delete", "search", "tree").
        path: Target path (supports res:// and relative paths).
        query: Search query string (for search operation).
        filter: Type filter ("all", "scenes", "scripts", "images", "audio", etc.).
        recursive: Include subdirectories in list operation.
        context_lines: Lines of context around search matches (default 2).
        max_depth: Maximum depth for tree operation (default 3).
        show_sizes: Include file sizes in tree output.
        content: File content for write operation.
        destination: Destination path for move operation.
        ctx: MCP context (unused).

    Returns:
        Dict with operation-specific results.
    """
    try:
        return await _file_ops(
            op=op,
            path=path,
            query=query,
            filter=filter,
            recursive=recursive,
            context_lines=context_lines,
            max_depth=max_depth,
            show_sizes=show_sizes,
            content=content,
            destination=destination,
        )
    except Exception as e:
        return {"error": f"Unexpected error: {e}"}
```

Key design decisions:
- The tool description explicitly directs the agent to use `godotiq_script_ops` for `.gd` files. This is important because `file_ops.write` does not validate GDScript conventions.
- `openWorldHint: False` -- purely local filesystem operations.

### Complete File Structure After Modification

The final `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/bridge/__init__.py` should contain:

1. Module docstring
2. `from __future__ import annotations`
3. Import of `Context` from `mcp.server.fastmcp`
4. Import of `mcp` from `godotiq.server`
5. Import of `BridgeConnectionError`, `BridgeTimeoutError`, `BridgeError` from `godotiq.bridge.connection`
6. 8 implementation imports (4 existing + 4 new):
   - `screenshot as _screenshot`
   - `run as _run`
   - `perf_snapshot as _perf_snapshot`
   - `editor_context as _editor_context`
   - `scene_tree as _scene_tree` (new)
   - `node_ops as _node_ops` (new)
   - `script_ops as _script_ops` (new)
   - `file_ops as _file_ops` (new)
7. 8 `@mcp.tool()` decorated functions (4 existing + 4 new)

### Exception Handling Pattern Summary

| Tool | Exception Types Caught |
|------|----------------------|
| `godotiq_scene_tree` | `BridgeConnectionError`, `BridgeTimeoutError`, `BridgeError`, `Exception` |
| `godotiq_node_ops` | `BridgeConnectionError`, `BridgeTimeoutError`, `BridgeError`, `ValueError`, `Exception` |
| `godotiq_script_ops` | `Exception` |
| `godotiq_file_ops` | `Exception` |

Bridge tools catch the three bridge-specific exception types with tailored hint messages, then fall through to a generic `Exception` catch. Filesystem tools only need the generic `Exception` catch because they return errors via the response dict's `status`/`error` fields.

### Tool Count After This Section

After this section, GodotIQ registers **21 MCP tools** total:
- 1 ping tool (`godotiq_ping` in `server.py`)
- 12 filesystem analysis tools (memory, code, spatial, assets, animation modules)
- 4 Sprint 4a bridge tools (screenshot, run, perf_snapshot, editor_context)
- 4 Sprint 4b tools (scene_tree, node_ops, script_ops, file_ops) -- added by this section

---

## File Listing

| File | Action |
|------|--------|
| `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/bridge/__init__.py` | Modify (add 4 imports + 4 wrappers) |
| `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_bridge/test_registration.py` | Create |

## Verification

After implementation, run:

```bash
cd /Users/salvatorefarruggio/Desktop/godotiq && uv run pytest tests/test_bridge/test_registration.py -v
```

All 9 tests should pass. The tests mock the implementation functions and bridge calls, so no live Godot connection is needed.

Additionally, verify the existing Sprint 4a tool tests still pass:

```bash
cd /Users/salvatorefarruggio/Desktop/godotiq && uv run pytest tests/test_bridge/test_tools.py -v
```

---

## Implementation Notes

**Status:** Complete
**Files modified:**
- `src/godotiq/tools/bridge/__init__.py` — Added 4 imports + 4 MCP wrapper functions
**Files created:**
- `tests/test_bridge/test_registration.py` — 9 tests, all passing

**Deviations from plan:**
- Plan specified 6 tests; implementation has 9 (added BridgeError + ValueError tests for node_ops, total: 3 scene_tree + 4 node_ops + 1 script_ops + 1 file_ops)
- Mock targets for filesystem tools use `godotiq.tools.bridge._script_ops` / `_file_ops` (the imported alias) instead of the module-level function — this is the correct approach per the plan's own guidance
- Removed unused `import pytest` from test file