# Hecate > The house — lights, the receiver, IR devices, and saved routines. The MCP server is the actuator layer; the agent is the brain. - **MCP server name:** `hecate` - **Prompt snippet:** [prompts/tools/hecate.md](../../prompts/tools/hecate.md) ## What It Is [Hecate](https://git.helu.ca/r/hecate) is the estate's voice-first home-automation controller — a FastAPI + FastMCP service that owns the device registry and drives the physical devices in Robert's home: Tuya/SmartLife bulbs and outlets, HomeKit wall plugs (via the Propolos bridge, no vendor cloud), the Onkyo AV receiver over eISCP, and Broadlink IR blasters for TVs, soundbars, projectors, and legacy air conditioning. Named for the Greek goddess of the household threshold and the crossroads. Hecate deliberately contains no AI. It exposes tools; an agent decides what to call. It is the only MCP server on the personal team that changes physical state in the house — every other tool reads, writes text, or searches. **Two stores, as with Kairos and Periplus.** Hecate holds the **canonical live state** — which devices exist, what room they're in, whether the light is currently on, what the receiver's volume is. Neo4j holds the **interpretation** — that the reef tank in the living room matters, that this plant has been struggling since June. Hecate knows the lamp is on; it has no idea why anyone cares. When Sophia records that the aquarium heater failed, the *event* goes in Neo4j as an `Observation`; the *device* stays Hecate's. ## ⚠️ Critical Discipline: Never Guess a Device or Room The device registry lives in Hecate's database and is **per-install**. Room ids (`living_room`) and device ids (`lr_main`, `lr_fan_plug`) are human-authored slugs that exist only in that database. There is no fixed list, and none of them are guessable from conversation. Acting on a guessed slug either errors or — worse — actuates the wrong thing. Unlike a bad search result, this has physical consequences: the light someone is reading by goes out, the receiver powers down mid-film. The rules, which have no exceptions: 1. **Call `list_rooms` / `list_devices` before acting.** Every session. The registry is not to be memorised across conversations — devices get added, renamed, and disabled. 2. **If there's no obvious match for what the user said, ask.** Do not pick the closest-sounding slug. 3. **Confirm before disruptive actions** — whole-house changes (`room=""`), powering off a receiver that's in use, running a multi-room routine. 4. **Name the device when reporting failure.** "Can't reach the patio blaster — it's offline" beats "command failed." Hecate's own error strings do this; preserve it. ## MCP Tools ### Registry — always start here | Tool | Purpose | |---|---| | `list_rooms()` | Every room with its device count. The room-slug source of truth. | | `list_devices(room="")` | Every registered device with type, platform, and room. Optionally scoped to one room. | ### Lights and power | Tool | Purpose | |---|---| | `lights_on(room="", brightness=-1)` | Turn lights on. `brightness` 0–100; `-1` leaves it unchanged (and is ignored by outlets). `room=""` is the whole house. | | `lights_off(room="")` | Turn lights off, optionally scoped to a room. | | `lights_color(color, room="")` | Set colour. **RGB bulbs only** — white/dimmer bulbs and outlets are unaffected. Named colours: red, orange, yellow, green, cyan, blue, purple, magenta, pink, white, warmwhite, coolwhite. | | `device_power(device_id, state)` | One specific device by id. `state` is `"on"` or `"off"`. This is how you address a single plug when the room-scoped tools are too broad. | ### Onkyo receiver | Tool | Purpose | |---|---| | `receiver_power(state)` | On or off. | | `set_volume(level)` | Main-zone volume, 0–100. | | `set_input(source)` | Select an input, by eISCP name or friendly label. | | `get_receiver_status()` | Current power, volume, and input. | ### IR, routines, timing | Tool | Purpose | |---|---| | `ir_command(device_id, command)` | Fire a learned IR command through the target's blaster. Valid commands come from that device's learned command map — they are per-device, not a fixed vocabulary. | | `run_routine(routine_id)` | Execute a saved routine's steps in order; returns per-step outcomes. Steps are best-effort — one failure doesn't abort the rest. | | `list_routines()` | All routines with id, name, enabled state, trigger, and step count. | | `set_timer(routine_id, minutes=0, seconds=0, label="")` | Fire an **existing routine** after a delay. See the gotcha below — this is not a kitchen timer. | | `list_timers(status="")` | List timers, optionally filtered (`pending`, `fired`, `cancelled`). | | `wait(seconds=0, minutes=0)` | Pause inside a routine before the next step. | ### Wallboard (Phase 2) `list_wallboards`, `create_wallboard`, `get_wallboard`, `get_wallboard_version`, `get_wallboard_catalog`, `put_wallboard_version`, `activate_wallboard_version`, `comfy_generate`, `gallery_search` — the push-model kiosk display: an agent authors a full HTML board and pushes it. Not part of Sophia's day-to-day; see [hecate/docs/wallboard.md](https://git.helu.ca/r/hecate/src/branch/main/docs/wallboard.md). ## Canonical Workflows ### Turning something on ``` list_rooms() # or list_devices() if the user named a device → match what the user said to a real slug → lights_on(room="living_room", brightness=60) ``` Step 1 is non-negotiable. If the match is ambiguous, ask rather than guess. ### Addressing one device rather than a room ``` list_devices(room="living_room") # find the specific plug → device_power(device_id="lr_fan_plug", state="on") ``` Room-scoped `lights_*` tools hit every light-like device in the room. For "turn on the fan plug," use `device_power`. ### Running a saved sequence ``` list_routines() # routine ids are slugs: "movie_time", "goodnight" → run_routine(routine_id="movie_time") ``` Read the per-step outcomes in the response — a routine reports partial failure rather than throwing. ## Who Uses Hecate - **Sophia** — primary and effectively sole user. The house is her domain; she is the only personal agent wired to it. - No other agent has Hecate in its server list. If a request from another agent needs the house, it routes to Sophia by message. External non-agent clients (the Theia kiosk, a scheduled wallboard prompt) connect with their own PATs — out of scope here. ## What It's Good For - Actuating lights, plugs, the receiver, and IR devices by room or by device - Discovering what the house actually contains, live - Running saved multi-step routines ("movie time", "goodnight") - Authoring and serving the wallboard kiosk display ## What It's Not Good For - **Reading any sensor.** See the gotchas — nothing is exposed. - **Weather.** Not exposed as a tool yet. - **Conversational timers and reminders.** Not what `set_timer` is. - Remembering *why* something matters — that's Neo4j's job. - Scheduling in the human sense — cron/sun triggers are configured in Hecate's own UI; the household calendar is Kairos (Shawn). ## Known Gotchas - **Never guess a room or device slug.** See the Critical Discipline section — this is the failure mode with physical consequences. - **No sensor readings are exposed via MCP.** Hecate integrates **Demeter**, the estate's plant/aquarium sensor server (soil moisture, temperature), and sensor-capable Broadlink blasters report temperature/humidity — but all of it is reachable only over REST and Prometheus. There is **no MCP tool** for any reading. An agent cannot see soil moisture, water temperature, or room temperature. *On the Hecate backlog.* Until then the agent asks the user and records an `Observation` in Neo4j. - **No weather or forecast tool.** A weather data source exists inside Hecate for the wallboard (`sources/weather.py`, Open-Meteo) but is not exposed via MCP. *Also on the Hecate backlog.* - **`set_timer` is not a kitchen timer.** It requires a **pre-existing `routine_id`** and fires that routine after a delay — there is no tool to create a routine, and **no cancel-timer tool** (cancellation is REST-only). "Set a 10-minute timer" is not expressible. The conversational timer/reminder capability is planned for **Daedalus**, not Hecate. An agent must not imply it is holding a timer. - **`lights_color` only affects RGB bulbs.** White bulbs, dimmers, and outlets in the same room are silently unaffected — that's not a failure, but say so rather than claiming the room changed colour. - **`brightness=-1` means "don't change it."** Passing `0` is a real value and will dim the light to nothing. - **`room=""` is the whole house.** Easy to trigger accidentally by omitting the argument. - **Timers survive restarts; cron occurrences do not.** Past-due timers fire on startup; a 7 AM schedule missed during an outage is *not* replayed at 2 PM. Deliberate. - **Auth is owner-only.** The `/mcp` mount sits behind an ASGI guard — a valid non-owner credential gets **403**, not 401. Agents authenticate with an owner-scoped PAT (`hecate_pat_…`) as a static bearer header; Pallas does not forward inbound auth downstream. - **A missing PAT hangs agent startup — it does not just fail the tool.** With no bearer, Hecate answers 401; fast-agent reads that as an auth challenge and falls back to OAuth dynamic client registration, then blocks waiting for a browser callback on `127.0.0.1:3030`. On a headless host that callback never arrives and the agent never finishes starting. Verified 2026-07-26. The static bearer is the fix — this is the same trap as the Daedalus MCP server. - **Use the trailing slash** (`/mcp/`). `/mcp` answers 307 to `/mcp/`, so omitting it adds a redirect to every call. - **The server sends its own instructions.** Hecate's FastMCP server ships `_SERVER_INSTRUCTIONS` describing room/device resolution to every client automatically. Don't duplicate that guidance verbatim in an agent prompt — add only what it doesn't say.