Verified against the live Virgo dev instance while testing Sophia: with no bearer token, 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 that never arrives on a headless host. The agent never finishes starting — so a missing PAT is a startup failure, not a per-tool one. Same trap as the Daedalus MCP server. Also noted that /mcp answers 307 to /mcp/, so the trailing slash avoids a redirect on every call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9.9 KiB
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
What It Is
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:
- Call
list_rooms/list_devicesbefore acting. Every session. The registry is not to be memorised across conversations — devices get added, renamed, and disabled. - If there's no obvious match for what the user said, ask. Do not pick the closest-sounding slug.
- Confirm before disruptive actions — whole-house changes (
room=""), powering off a receiver that's in use, running a multi-room routine. - 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.
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_timeris. - 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
Observationin 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_timeris not a kitchen timer. It requires a pre-existingroutine_idand 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_coloronly 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=-1means "don't change it." Passing0is 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
/mcpmount 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/)./mcpanswers 307 to/mcp/, so omitting it adds a redirect to every call. - The server sends its own instructions. Hecate's FastMCP server ships
_SERVER_INSTRUCTIONSdescribing room/device resolution to every client automatically. Don't duplicate that guidance verbatim in an agent prompt — add only what it doesn't say.