# Sprint 4a Research — Foundation + Connection Layer

## Codebase Research

### Tool Registration Pattern

Tools self-register using the `@mcp.tool()` decorator imported from `godotiq.server`:

```python
from godotiq.server import mcp
from mcp.server.fastmcp import Context

@mcp.tool(
    name="godotiq_tool_name",
    annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False},
)
async def godotiq_tool_name(param: str = "default", ctx: Context = None) -> dict:
    session = ctx.request_context.lifespan_context["session"]
    ...
```

Tools are auto-registered via imports at the bottom of `server.py`:
```python
from godotiq.tools import memory  # noqa: E402, F401
from godotiq.tools import code    # noqa: E402, F401
from godotiq.tools import spatial # noqa: E402, F401
from godotiq.tools import assets  # noqa: E402, F401
from godotiq.tools import animation # noqa: E402, F401
# NEED TO ADD: from godotiq.tools import bridge
```

### pyproject.toml

Current dependencies:
- Runtime: `mcp>=1.0.0` (single dependency)
- Dev: `pytest>=7.0`, `pytest-asyncio>=0.21`
- pytest config: `asyncio_mode = "auto"`

Need to add: `websockets>=12.0` to runtime deps.

### Session and Lifespan

`GodotIQSession` in `session.py` manages project state. Lifespan context provides session to tools via `ctx.request_context.lifespan_context["session"]`. Bridge tools will NOT need the session — they talk directly to the addon via WebSocket.

### Config Pattern

`GodotIQConfig` in `config.py` reads `.godotiq.json`. The bridge port can be configured there (`server.addon_port`), or via `GODOTIQ_ADDON_PORT` env var on the Python side.

### Test Setup

- pytest with `asyncio_mode = "auto"` (no need for `@pytest.mark.asyncio` — but it still works)
- Class-based test organization (`class TestX:`)
- Fixture-based project loading
- Mock-friendly patterns

### Error Handling Convention

All tools return dicts — never raise exceptions to the MCP client. Errors are returned as `{"error": "message", "hint": "..."}`.

---

## Web Research

### 1. Godot 4.x EditorDebuggerPlugin API

**Architecture:** Three components work together:
1. `EditorPlugin` — registers debugger plugin + autoload
2. `EditorDebuggerPlugin` — runs in editor, handles messages from game
3. Autoload script — runs in game, uses `EngineDebugger` for IPC

**Message format is strict:** Must use `"prefix:action"` with colon separator.
- `_has_capture(prefix)` receives only the prefix part
- `_capture(message, data, session_id)` receives the full message string

**Key methods:**
| Component | Method | Description |
|-----------|--------|-------------|
| EditorDebuggerPlugin | `_has_capture(prefix) -> bool` | Route by prefix |
| EditorDebuggerPlugin | `_capture(message, data, session_id) -> bool` | Handle message, return true |
| EditorDebuggerPlugin | `_setup_session(session_id)` | Connect started/stopped signals |
| EditorDebuggerSession | `send_message(message, data)` | Send to running game |
| EngineDebugger | `register_message_capture(name, callable)` | Game-side handler |
| EngineDebugger | `send_message(message, data)` | Send from game to editor |
| EngineDebugger | `is_active() -> bool` | Guard for non-editor runs |

**Gotchas:**
- Callable registered with `register_message_capture` must return `bool` (true = handled)
- Guard game-side code with `EngineDebugger.is_active()`
- Known stability issues — potential silent editor crashes
- Clean up in `_exit_tree()` to avoid leaks

**Sources:**
- [EditorDebuggerPlugin Docs](https://docs.godotengine.org/en/4.4/classes/class_editordebuggerplugin.html)
- [EditorDebuggerSession Docs](https://docs.godotengine.org/en/4.4/classes/class_editordebuggersession.html)
- [EngineDebugger Docs](https://docs.godotengine.org/en/stable/classes/class_enginedebugger.html)

### 2. Godot 4.x WebSocketPeer Server Pattern

`WebSocketServer` was removed in Godot 4.0 (PR #66594). The replacement is `TCPServer` + `WebSocketPeer.accept_stream()`.

**Key points:**
- `poll()` must be called every frame in `_process()` — no signals emitted
- Track peers in a Dictionary, clean up `STATE_CLOSED` peers each frame
- Use `send_text()` for JSON/text, `send()` for binary
- After `accept_stream()`, peer starts in `STATE_CONNECTING`, handshake completes during poll
- Use `was_string_packet()` before decoding received packets

**Server pattern:**
```gdscript
var _tcp_server := TCPServer.new()
var _peers: Dictionary = {}

func _ready():
    _tcp_server.listen(PORT, "127.0.0.1")

func _process(_delta):
    while _tcp_server.is_connection_available():
        var ws := WebSocketPeer.new()
        ws.accept_stream(_tcp_server.take_connection())
        _peers[id] = ws

    for peer_id in _peers.keys():
        var peer = _peers[peer_id]
        peer.poll()
        match peer.get_ready_state():
            WebSocketPeer.STATE_OPEN:
                while peer.get_available_packet_count() > 0:
                    # handle packets
            WebSocketPeer.STATE_CLOSED:
                _peers.erase(peer_id)
```

**Sources:**
- [WebSocketPeer Docs](https://docs.godotengine.org/en/stable/classes/class_websocketpeer.html)
- [WebSocket Refactor PR #66594](https://github.com/godotengine/godot/pull/66594)

### 3. Python websockets >= 12.0 Async Patterns

**Version landscape:**
| Version | Key Change |
|---------|-----------|
| 12.0 | Last before asyncio rewrite |
| 13.0 | New `websockets.asyncio` introduced (opt-in) |
| 14.0 | New implementation becomes default, legacy deprecated |
| 15.0-16.0 | Current stable, Python >= 3.9/3.10 required |

**Recommendation for our spec:** Use `websockets>=12.0` as minimum, but import via `websockets.connect()` which works across all versions. The spec's `connection.py` uses the top-level `websockets.connect()` which is fine.

**Key connect() parameters:**
- `open_timeout`: 10s default
- `ping_interval`: 20s keepalive
- `ping_timeout`: 20s pong timeout
- `close_timeout`: 10s
- `max_size`: 1MB (increase for screenshots)

**Auto-reconnection pattern:**
```python
async for ws in websockets.connect(uri):
    try:
        async for message in ws:
            await process(message)
    except websockets.ConnectionClosed:
        continue  # auto-reconnects with backoff
```

**Critical asyncio rules:**
- Never use `time.sleep()` — always `await asyncio.sleep()`
- Use `asyncio.create_task()` for background listeners
- Handle `ConnectionClosed` properly
- Use `async for message in ws` for receiving

**Sources:**
- [websockets Documentation](https://websockets.readthedocs.io/)
- [Client API Reference](https://websockets.readthedocs.io/en/stable/reference/asyncio/client.html)
- [GitHub](https://github.com/python-websockets/websockets)

---

## Validation of Spec Against Research

The Sprint 4a spec code is well-aligned with all researched best practices:

1. **EditorDebuggerPlugin usage** — correct `_has_capture`/`_capture`/`_setup_session` pattern, proper `"godotiq:"` prefix, `EngineDebugger.is_active()` guard in runtime
2. **WebSocketPeer server** — correct `TCPServer` + `accept_stream()` pattern, polling in `_process()`, dictionary peer tracking, proper cleanup
3. **Python websockets** — uses `websockets.connect()` (compatible across versions), proper async/await patterns, background listener task

**One consideration:** For large screenshots, may need to increase `max_size` on the websockets connection beyond the 1MB default. The spec's `connection.py` doesn't set this explicitly — worth noting for implementation.
