Files
koios/docs/tools/hecate.md
Robert Helewka 616d7580a2 docs(hecate): note the OAuth-DCR startup hang and trailing slash
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>
2026-07-26 19:34:00 -04:00

9.9 KiB
Raw Permalink Blame History

Hecate

The house — lights, the receiver, IR devices, and saved routines. The MCP server is the actuator layer; the agent is the brain.

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:

  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 0100; -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, 0100.
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_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.