# Sprint 4a Implementation Plan — Foundation + Connection Layer

## Overview

This plan implements the GodotIQ bridge layer: a bidirectional WebSocket connection between the Python MCP server and a Godot 4.x editor addon. The bridge enables 4 new MCP tools that interact with the live Godot editor and running game, bringing the total from 13 to 17 tools.

### Architecture

```
Python MCP Server  <──WebSocket──>  Godot Editor Addon  <──EngineDebugger──>  Running Game
(bridge/*.py)                       (godotiq_server.gd)                       (godotiq_runtime.gd)
(tools/bridge/*.py)                 (godotiq_debugger.gd)
```

The communication flows through three layers:
1. **Python bridge** sends JSON requests over WebSocket to the addon
2. **Godot addon** (editor-side) handles editor operations directly, or forwards game operations via EngineDebugger IPC
3. **Game runtime** (autoload) executes game-side operations (screenshots, perf) and sends results back through EngineDebugger

### Constraints

- **DO NOT TOUCH** any Phase 1 files (parsers, tools/code, tools/spatial, tools/animation, tools/assets, tools/memory, resolvers, index.py, project_index.py, session.py, existing tests)
- Only ADD new files and modify `pyproject.toml` + `server.py`
- Single new dependency: `websockets>=14.0`

---

## Section 1: Godot Addon — Plugin Configuration and Lifecycle

### Purpose
Create the addon directory structure and the EditorPlugin that orchestrates startup/shutdown of all addon components.

### Files
```
godot-addon/addons/godotiq/
  plugin.cfg              # Plugin metadata (name, version, script path)
  godotiq_plugin.gd       # EditorPlugin lifecycle manager
```

### Design

**plugin.cfg** declares the plugin with name "GodotIQ", version "2.0.0", pointing to `godotiq_plugin.gd` as the entry script.

**godotiq_plugin.gd** is an `@tool` EditorPlugin that:
- `_enter_tree()`: Creates WebSocket server node (child), registers EditorDebuggerPlugin, registers runtime autoload singleton
- `_exit_tree()`: Cleans up in reverse order — remove autoload, remove debugger plugin, free server node

Cross-references: The debugger plugin gets a reference to the server (`_debugger.server = _server`) so it can forward game responses.

Constants: `AUTOLOAD_NAME = "GodotIQRuntime"`, `RUNTIME_PATH = "res://addons/godotiq/godotiq_runtime.gd"`

---

## Section 2: Godot Addon — WebSocket Server

### Purpose
Central hub that accepts WebSocket connections from the Python MCP server and dispatches requests to appropriate handlers.

### File
```
godot-addon/addons/godotiq/godotiq_server.gd
```

### Design

**Server architecture**: Uses Godot 4 pattern of `TCPServer` + `WebSocketPeer.accept_stream()` (since `WebSocketServer` was removed in Godot 4).

**Core state**:
- `_tcp_server: TCPServer` — listens on configurable port (default 6007, from `.godotiq.json`)
- `_peers: Dictionary` — maps peer_id → WebSocketPeer
- `_peer_tcp: Dictionary` — keeps StreamPeerTCP references alive (prevents GC)
- `_pending_game_requests: Dictionary` — maps request_id → {peer_id, method, timeout_at}
- `_game_running: bool` — tracks game state (updated by debugger events)

**`_process()` loop** (called every frame):
1. Accept new TCP connections → upgrade to WebSocket via `accept_stream()`
2. Poll all WebSocket peers for state changes and incoming packets
3. Remove disconnected peers (STATE_CLOSED) — clean up both `_peers` and `_peer_tcp` dictionaries
4. Check for timed-out game-side requests

**Request dispatch** (`_dispatch()`):
- Editor-side (handled directly): `ping`, `editor_context`, `run`, `stop`
- Game-side (forwarded via debugger): `screenshot`, `perf_snapshot`

**Editor-side handlers**:
- `ping` → returns editor version, game state, addon version
- `editor_context` → returns open scenes, selected nodes, game state, project path via `EditorInterface`
- `run` → calls `EditorInterface.play_main_scene()` / `play_current_scene()` / `play_custom_scene()`
- `stop` → calls `EditorInterface.stop_playing_scene()`

**Game-side forwarding**: Stores request in `_pending_game_requests` with timeout, sends to game via `debugger.send_to_game()`. When `handle_game_response()` is called (by debugger), matches response to pending request and sends to correct WebSocket peer.

**Event broadcasting**: `on_game_started()` / `on_game_stopped()` broadcast events to all connected WebSocket peers. Game stop clears all pending requests with GAME_STOPPED errors.

**Port configuration**: Reads from `.godotiq.json` at `server.addon_port`, defaults to 6007. Binds to `127.0.0.1` only (localhost).

**JSON protocol**:
- Response: `{"id": "...", "status": "ok", "result": {...}}`
- Error: `{"id": "...", "status": "error", "error": {"code": "...", "message": "..."}}`
- Event: `{"event": "...", "data": {...}}`

---

## Section 3: Godot Addon — EngineDebugger Bridge and Game Runtime

### Purpose
Two files that handle editor↔game IPC: the debugger plugin (editor-side) and the runtime autoload (game-side).

### Files
```
godot-addon/addons/godotiq/godotiq_debugger.gd   # EditorDebuggerPlugin
godot-addon/addons/godotiq/godotiq_runtime.gd     # Game autoload
```

### Design

**godotiq_debugger.gd** — `@tool extends EditorDebuggerPlugin`:
- `_has_capture("godotiq") -> true` — captures all `"godotiq:"` prefixed messages
- `_capture()` — routes incoming game messages to `server.handle_game_response()`:
  - `godotiq:screenshot_result` → `server.handle_game_response("screenshot", data)`
  - `godotiq:perf_result` → `server.handle_game_response("perf", data)`
  - `godotiq:error` → broadcasts as `runtime_error` event
  - Future: state_result, input_result, exec_result, nav_result, watch_update
- `_setup_session(session_id)` — stores session_id, connects `started`/`stopped` signals
- `send_to_game(message, data)` — wraps `get_session(id).send_message()`
- Untyped `server` var to avoid circular dependency with godotiq_server.gd

**godotiq_runtime.gd** — `extends Node` (game autoload):
- `_ready()`: Guards with `EngineDebugger.is_active()`, registers `"godotiq"` message capture
- Message handler dispatches:
  - `godotiq:screenshot` → `_take_screenshot(params)`
  - `godotiq:query_perf` → `_send_perf_snapshot()`
- `_take_screenshot()`: Awaits one frame, gets viewport image, resizes by scale, encodes to format (webp/png/jpg), sends base64 via `EngineDebugger.send_message("godotiq:screenshot_result", [b64, fmt, w, h])`
- `_send_perf_snapshot()`: Collects FPS, draw calls, triangles, objects, memory (texture/buffer/video), node count, orphan nodes via `Engine`, `RenderingServer`, `Performance` APIs. Sends JSON string via `EngineDebugger.send_message("godotiq:perf_result", [json])`

---

## Section 4: Python Bridge Layer — Protocol and Connection

### Purpose
Python-side WebSocket communication: message encoding/decoding and the async connection client.

### Files
```
src/godotiq/bridge/__init__.py     # Package docstring
src/godotiq/bridge/protocol.py     # Dataclasses + JSON encoding
src/godotiq/bridge/connection.py   # Async WebSocket client
```

### Design

**protocol.py** — Three frozen dataclasses:
- `BridgeRequest(id, method, params)` with `to_json()` method
- `BridgeResponse(id, status, result, error)` with `is_ok`, `error_code`, `error_message` properties
- `BridgeEvent(event, data)` for push events

`parse_message(text) -> BridgeResponse | BridgeEvent` — parses raw JSON, returns appropriate type. Raises `ValueError` on malformed messages. Routes by presence of "id" (response) vs "event" (event) keys.

Per-tool timeout configuration dict `TOOL_TIMEOUTS` with sensible defaults (screenshot=30s, ping=2s, perf=5s, etc.). `get_timeout(method)` returns the configured timeout or 5.0s default.

Screenshot retry constants: `SCREENSHOT_MAX_RETRIES=3`, `SCREENSHOT_RETRY_DELAY=0.5`.

**connection.py** — `BridgeConnection` class:

Three custom exceptions: `BridgeConnectionError`, `BridgeTimeoutError`, `BridgeError(code, message)`.

Constructor: `BridgeConnection(host="127.0.0.1", port=6007)`. State: `_ws`, `_pending` dict (id → Future), `_event_handlers`, `_request_counter`, `_connected`, `_game_running`, `_listen_task`.

Key methods:
- `connect(timeout=5.0)` — imports websockets, creates connection with `max_size=16MB`, starts background `_listen_loop` task, registers game state event handlers
- `disconnect()` — cancels listen task, closes websocket, fails all pending futures
- `request(method, params, timeout)` — creates BridgeRequest with incrementing ID, sends JSON, awaits Future with per-tool timeout
- `request_with_retry(method, params, max_retries, retry_delay)` — wraps `request()` with retry loop for screenshot use case
- `on_event(event, handler)` — registers push event callback

`_listen_loop()` — background async task that iterates over incoming messages, resolves pending futures for responses, dispatches events to handlers. On connection loss, fails all pending and sets `_connected = False`.

Game state tracked via `_on_game_started`/`_on_game_stopped` event handlers.

---

## Section 5: Python Bridge Layer — Session Manager and Tool Registration

### Purpose
Shared connection manager for bridge tools, and integration with the MCP server.

### Files
```
src/godotiq/bridge/session.py          # Singleton connection manager
src/godotiq/tools/bridge/__init__.py   # Tool registration with @mcp.tool()
```

### Modifications
```
src/godotiq/server.py                  # Add bridge import
pyproject.toml                         # Add websockets dependency
```

### Design

**session.py** — Module-level singleton pattern with async lock:
- `_bridge: Optional[BridgeConnection]` module global
- `_bridge_lock: asyncio.Lock` — prevents race conditions when concurrent tool calls both try to create/reconnect
- `get_bridge()` — acquires lock, returns existing connection or creates new one. Reads port from `GODOTIQ_ADDON_PORT` env var (default 6007). Auto-reconnects if connection was lost (checks `_bridge.is_connected`, disconnects stale connection first).
- `get_bridge_or_none()` — wraps `get_bridge()` in try/except, returns None on failure. Used by editor_context for graceful fallback.
- `close_bridge()` — disconnects and clears singleton.

**Note on port configuration:** The Python side reads port ONLY from `GODOTIQ_ADDON_PORT` env var (not from `.godotiq.json`). The GDScript addon reads `.godotiq.json` independently. `config.py` is NOT modified.

**tools/bridge/__init__.py** — The registrar file. Imports `mcp` from `godotiq.server` and defines all 4 bridge tool wrappers using `@mcp.tool()` decorator. Each wrapper calls the corresponding function from its module file (screenshot.py, run.py, etc.) and catches bridge exceptions, returning user-friendly error dicts:
- `BridgeConnectionError` → `{"error": "...", "hint": "Enable the GodotIQ addon..."}`
- `BridgeTimeoutError` → `{"error": "...", "hint": "The game may not be running..."}`
- `BridgeError` → `{"error": "[CODE] message"}`

**Pattern clarification:** `@mcp.tool()` decorators live in `tools/bridge/__init__.py`. The individual files (screenshot.py, run.py, etc.) export plain async functions without decorators. This matches the existing pattern where `__init__.py` is the integration point.

All tools accept `ctx: Context = None` for consistency with existing tools (not currently used by bridge tools, but enables future access to session context).

**Tool annotations** (per-tool, not one-size-fits-all):
- `godotiq_screenshot`: `readOnlyHint=True, destructiveHint=False, idempotentHint=False, openWorldHint=True`
- `godotiq_run`: `readOnlyHint=False, destructiveHint=True, idempotentHint=False, openWorldHint=True`
- `godotiq_perf_snapshot`: `readOnlyHint=True, destructiveHint=False, idempotentHint=False, openWorldHint=True`
- `godotiq_editor_context`: `readOnlyHint=True, destructiveHint=False, idempotentHint=True, openWorldHint=True`

**server.py modification**: Add `from godotiq.tools import bridge  # noqa: E402, F401` alongside existing tool imports. Also add `close_bridge()` call in the lifespan's finally block for clean shutdown.

**pyproject.toml modification**: Add `"websockets>=14.0"` to `[project] dependencies`.

---

## Section 6: Bridge MCP Tool Implementations

### Purpose
The 4 MCP tool functions that bridge AI requests to the Godot addon.

### Files
```
src/godotiq/tools/bridge/screenshot.py      # godotiq_screenshot
src/godotiq/tools/bridge/run.py             # godotiq_run
src/godotiq/tools/bridge/perf_snapshot.py   # godotiq_perf_snapshot
src/godotiq/tools/bridge/editor_context.py  # godotiq_editor_context
```

### Design

**screenshot.py** — `godotiq_screenshot(viewport="game", scale=0.5, format="webp")`:
- Validates scale (clamp to 0.5 if <=0 or >1.0) and format (fallback to "webp")
- Calls `bridge.request_with_retry("screenshot", ...)` with 3 retries, 0.5s delay, 30s timeout
- Returns: `{image: base64, format, width, height}`

**run.py** — `godotiq_run(action="play", scene="main")`:
- action="stop" → `bridge.request("stop")`
- action="play" → `bridge.request("run", params={"scene": scene})`
- Returns: `{action, scene}`

**perf_snapshot.py** — `godotiq_perf_snapshot()`:
- Calls `bridge.request("perf_snapshot")`
- Returns: `{fps, draw_calls, triangles, objects, texture_mem, buffer_mem, video_mem, total_nodes, orphan_nodes}`

**editor_context.py** — `godotiq_editor_context()`:
- Tries `get_bridge_or_none()` first
- Connected: calls `bridge.request("editor_context")`, adds `addon_connected: true`
- Not connected: returns filesystem-based fallback with `addon_connected: false`, project_path from `GODOTIQ_PROJECT_ROOT` env var, and a hint note

---

## Section 7: Tests

### Purpose
Unit tests for the bridge protocol, connection, and tool layers. All use mocks — no real WebSocket server needed.

### Files
```
tests/test_bridge/__init__.py         # Package init
tests/test_bridge/test_protocol.py    # Protocol encode/decode tests
tests/test_bridge/test_connection.py  # Connection mock tests
tests/test_bridge/test_tools.py       # Tool validation tests
```

### Design

**test_protocol.py** — Tests for:
- `BridgeRequest.to_json()` — minimal and with params
- `parse_message()` — ok response, error response, event, event with data, invalid JSON, missing id+event, non-object
- `get_timeout()` — known methods, default fallback, all positive

**test_connection.py** — Tests for:
- Default and custom constructor params
- `request()` without connect raises `BridgeConnectionError`
- Game state tracking (`_on_game_started`/`_on_game_stopped`)
- Event handler registration and dispatch (single and multiple handlers)
- `BridgeError` exception attributes

**test_tools.py** — Tests for each tool with mocked bridge:
- Screenshot: default params, invalid scale clamping, invalid format fallback
- Run: play main, stop
- Perf snapshot: basic call
- Editor context: connected (addon_connected=true), disconnected (fallback with note)

All tests use `unittest.mock.AsyncMock` and `patch()` to mock the bridge connection. Tests use class-based organization (`class TestX:`).

---

## Verification Checklist

1. `pytest tests/ -v` — ALL Phase 1 tests still pass (regression)
2. `pytest tests/test_bridge/ -v` — ALL new bridge tests pass
3. `python -c "from godotiq.bridge.connection import BridgeConnection; print('OK')"` — import check
4. `python -c "from godotiq.bridge.protocol import parse_message; print('OK')"` — import check
5. `python -c "from godotiq.tools.bridge.screenshot import screenshot; print('OK')"` — tool import check
6. GDScript files have no Python syntax leaks
