# Section 05: Python Bridge Layer -- Session Manager and Tool Registration

## Overview

This section implements the shared bridge connection manager (singleton pattern with async lock) and the MCP tool registration layer that wires bridge tools into the server. It also modifies `server.py` to import bridge tools and `pyproject.toml` to add the `websockets` dependency.

## Dependencies

- **Section 04** provides `BridgeConnection`, `BridgeConnectionError`, `BridgeTimeoutError`, `BridgeError` in `src/godotiq/bridge/connection.py`

## Blocks

- **Section 06** (Bridge MCP Tool Implementations) imports from `bridge.session` and is registered in `tools/bridge/__init__.py`

## Files to Create

| File | Purpose |
|------|---------|
| `src/godotiq/bridge/session.py` | Singleton connection manager with async lock |
| `src/godotiq/tools/bridge/__init__.py` | Tool registration with `@mcp.tool()` decorators |

## Files to Modify

| File | Change |
|------|--------|
| `src/godotiq/server.py` | Add bridge import + close_bridge() in lifespan finally |
| `pyproject.toml` | Add `websockets>=14.0` to dependencies |

## Tests

No separate test file for session manager. Behavior is covered by tool tests in Section 07 via mocked `get_bridge()` / `get_bridge_or_none()`.

---

## Implementation Details

### 1. Session Manager (`src/godotiq/bridge/session.py`)

Module-level singleton with `asyncio.Lock` to prevent race conditions when concurrent tool calls try to create/reconnect.

**Module state:**
- `_bridge: Optional[BridgeConnection] = None`
- `_bridge_lock: Optional[asyncio.Lock] = None` -- initialized lazily (no event loop at import time)

**Public functions:**

`async def get_bridge() -> BridgeConnection:`
- Lazily initializes `_bridge_lock` if None
- Acquires lock
- If `_bridge` exists and `is_connected`, returns it
- If `_bridge` exists but NOT connected, calls `await _bridge.disconnect()` (cleanup stale)
- Creates new `BridgeConnection(port=port)` where port from `GODOTIQ_ADDON_PORT` env var (default 6007)
- Calls `await _bridge.connect()`
- Returns the connected bridge

`async def get_bridge_or_none() -> Optional[BridgeConnection]:`
- Wraps `get_bridge()` in try/except
- Returns None on any exception
- Used by `editor_context` for graceful fallback

`async def close_bridge() -> None:`
- Acquires lock
- If `_bridge` is not None, disconnects and sets to None
- Called in server lifespan's finally block

**Imports:** `asyncio`, `os`, `Optional` from typing, `BridgeConnection` and `BridgeConnectionError` from `godotiq.bridge.connection`

**Port configuration:** Python side reads ONLY from `GODOTIQ_ADDON_PORT` env var. Does NOT read `.godotiq.json`. Does NOT modify `config.py`.

### 2. Tool Registration (`src/godotiq/tools/bridge/__init__.py`)

**Note:** This file already exists with a stub docstring. Overwrite it entirely.

Imports `mcp` from `godotiq.server` and defines 4 `@mcp.tool()` decorated async wrapper functions. Each wrapper:
1. Calls the corresponding implementation function from its module (Section 06 files)
2. Catches bridge exceptions and converts to user-friendly error dicts

**Exception handling pattern** (for screenshot, run, perf_snapshot):
```python
try:
    result = await _implementation_function(...)
    return result
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 game may not be running or the operation timed out."}
except BridgeError as e:
    return {"error": f"[{e.code}] {e.error_message}"}
```

**`editor_context` is special:** Uses `get_bridge_or_none()` internally and handles fallback itself. Its wrapper does NOT need try/except — just returns the result directly.

**Tool declarations with annotations:**

| Tool | name | readOnlyHint | destructiveHint | idempotentHint | openWorldHint |
|------|------|-------------|----------------|---------------|--------------|
| godotiq_screenshot | `"godotiq_screenshot"` | True | False | False | True |
| godotiq_run | `"godotiq_run"` | False | True | False | True |
| godotiq_perf_snapshot | `"godotiq_perf_snapshot"` | True | False | False | True |
| godotiq_editor_context | `"godotiq_editor_context"` | True | False | True | True |

All tools accept `ctx: Context = None` for consistency.

**Imports:**
- `from mcp.server.fastmcp import Context`
- `from godotiq.server import mcp`
- `from godotiq.bridge.connection import BridgeConnectionError, BridgeTimeoutError, BridgeError`
- Tool functions from their modules (aliased with underscore prefix)

### 3. Server Modification (`src/godotiq/server.py`)

**Change 1:** Add after existing tool imports:
```python
from godotiq.tools import bridge  # noqa: E402, F401
```

**Change 2:** Modify lifespan to add cleanup:
```python
@asynccontextmanager
async def godotiq_lifespan(server):
    # ... existing session setup ...
    try:
        yield {"session": session}
    finally:
        from godotiq.bridge.session import close_bridge
        await close_bridge()
```

The import inside `finally` avoids circular imports at module load time.

### 4. Dependency Addition (`pyproject.toml`)

Change:
```toml
dependencies = ["mcp>=1.0.0"]
```
To:
```toml
dependencies = ["mcp>=1.0.0", "websockets>=14.0"]
```

No other changes.

## Key Design Decisions

1. **Lazy lock initialization**: `asyncio.Lock()` cannot be created at import time (no event loop). Created on first `get_bridge()` call.
2. **Auto-reconnect**: `get_bridge()` checks `is_connected`. If lost, disconnects stale instance and creates fresh one.
3. **Exception conversion**: Bridge exceptions become error dicts with hints. MCP client never sees raw exceptions.
4. **`editor_context` special case**: Uses `get_bridge_or_none()` for graceful fallback when addon not running.

## Verification

1. `python -c "from godotiq.bridge.session import get_bridge, close_bridge; print('OK')"`
2. `pyproject.toml` has `websockets>=14.0` in dependencies
3. `server.py` has `from godotiq.tools import bridge` in imports
4. All Phase 1 tests pass: `pytest tests/ -v --ignore=tests/test_bridge`

---

## Post-Implementation Notes

### Files Created
- `src/godotiq/bridge/session.py` — as planned
- `src/godotiq/tools/bridge/__init__.py` — as planned (overwritten stub)
- `src/godotiq/tools/bridge/screenshot.py` — **stub** returning error dict (Section 06 will overwrite)
- `src/godotiq/tools/bridge/run.py` — **stub** returning error dict (Section 06 will overwrite)
- `src/godotiq/tools/bridge/perf_snapshot.py` — **stub** returning error dict (Section 06 will overwrite)
- `src/godotiq/tools/bridge/editor_context.py` — **stub** returning error dict (Section 06 will overwrite)

### Files Modified
- `src/godotiq/server.py` — added bridge import + try/finally lifespan cleanup
- `pyproject.toml` — added `websockets>=14.0` dependency

### Deviations from Plan
1. **Stub files for Section 06 tools**: Created 4 stub files (not in plan) so that the `tools/bridge/__init__.py` imports resolve at import time. Stubs return `{"error": "Not yet implemented", ...}` instead of raising.
2. **Catch-all exception handler**: Added `except Exception` fallback to all 4 tool wrappers (user-approved during review). The plan only specified 3 bridge-specific exception types.
3. **Port parsing error handling**: Added try/except around `int(GODOTIQ_ADDON_PORT)` with fallback to DEFAULT_PORT (review fix).
4. **close_bridge error handling**: Wrapped `disconnect()` in try/except pass to ensure graceful cleanup (review fix).

### Test Results
- 460 tests passed (all Phase 1 tests, ignoring test_bridge)
