"""MCP resources -- read-only state queries exposed as esp32:// URIs.""" from __future__ import annotations import json from fastmcp import FastMCP from .serial_client import NotConnected, get_client # Static persona catalog. Mirrors the presets defined in the ESP32 firmware. _PERSONAS: dict[str, dict] = { "headset": { "name": "ESP32-Headset", "io_cap": "no_io", "transport": "classic", "services": ["handsfree", "a2dp_sink"], }, "speaker": { "name": "ESP32-Speaker", "io_cap": "no_io", "transport": "classic", "services": ["a2dp_sink"], }, "keyboard": { "name": "ESP32-Keyboard", "io_cap": "keyboard_display", "transport": "ble", "services": ["hid"], }, "sensor": { "name": "ESP32-Sensor", "io_cap": "no_io", "transport": "ble", "services": ["battery", "environmental_sensing"], }, "phone": { "name": "ESP32-Phone", "io_cap": "display_yesno", "transport": "both", "services": ["gap", "gatt"], }, "bare": { "name": "ESP32-BT", "io_cap": "no_io", "transport": "both", "services": [], }, } def _event_to_dict(e) -> dict: return {"event": e.event, "data": e.data, "ts": e.ts} def register_resources(mcp: FastMCP) -> None: @mcp.resource("esp32://status") async def device_status() -> str: """Device connection status and Bluetooth state.""" try: client = get_client() resp = await client.send_command("get_status") data = {"connected": True, **resp.data} except NotConnected: data = {"connected": False} return json.dumps(data) @mcp.resource("esp32://events") async def recent_events() -> str: """Recent event history (last 50 events).""" try: client = get_client() events = client.event_queue.get_events(limit=50) data = [_event_to_dict(e) for e in events] except NotConnected: data = [] return json.dumps(data) @mcp.resource("esp32://personas") async def available_personas() -> str: """Available device personas and their configurations.""" return json.dumps(_PERSONAS)