Metadata-Version: 2.5
Name: myrohome-helper-mcp
Version: 0.5.2
Summary: MCP server that lets an AI assistant read and control an HAI / Leviton Omni or NESS / ELK controller through Myro:Home Helper
Project-URL: Homepage, https://www.myrocontrol.com
Project-URL: Repository, https://gitlab.com/MyroControl/myrohome-helper-mcp
Project-URL: Myro:Home Helper (Mac App Store), https://apps.apple.com/app/id1334844181
Author: Myro Control, LLC
License: MIT
License-File: LICENSE
Keywords: elk,hai,home-automation,leviton,mcp,myrohome,ness,omni
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: keyring>=25.0
Requires-Dist: mcp<2,>=1.2
Description-Content-Type: text/markdown

# Myro:Home Helper MCP server

Lets an AI assistant (Claude Desktop, Claude Code, ChatGPT desktop, anything
that speaks [MCP](https://modelcontextprotocol.io)) read and control an
HAI / Leviton Omni controller (or, with Helper 5.0, a NESS / ELK M1 panel)
through **Myro:Home Helper**, using the Helper's
HTTP API. The assistant sees typed tools with descriptions instead of guessing
URL shapes.

## What you need

- [Myro:Home Helper](https://apps.apple.com/app/id1334844181) (Mac App Store)
  4.06 or later on a Mac on your network (4.07 or later for
  the read tools: `list_objects`, `get_object`, `helper_log`).
- An API key for your controller: Helper → **API** tab → Generate.
- [`uv`](https://docs.astral.sh/uv/) on the machine running the assistant
  (`brew install uv` or the one-line installer). `uvx` fetches and runs this
  server on demand and keeps it updated.

## NESS / ELK (Helper 5.0)

Helper 5.0 adds NESS / ELK M1 panels alongside HAI / Leviton Omni. The tools and
their parameters are the same for both; `controller_status` reports
`controllerFamily` (`"HAI / Leviton"` or `"NESS / ELK"`, derived from the
controller's `protocol`), and the live `scripting_reference` is scoped to that
family, so the assistant should read it before writing scripts. On ELK, user
codes may be 4 or 6 digits (`security-code set` accepts either), there is no
energy-cost level, and audio is a legacy M1XEP integration.

## Configure

Claude Desktop (`claude_desktop_config.json`) or any client that takes the
standard MCP config:

```json
{
  "mcpServers": {
    "myrohome-helper": {
      "command": "uvx",
      "args": ["myrohome-helper-mcp"],
      "env": {
        "MHH_URL": "http://192.168.1.76:9393",
        "MHH_API_KEY": "your-api-key"
      }
    }
  }
}
```

Claude Code:

```
claude mcp add myrohome-helper -e MHH_URL=http://192.168.1.76:9393 -e MHH_API_KEY=your-api-key -- uvx myrohome-helper-mcp
```

The controller ID is fetched from the Helper automatically. Set
`MHH_CONTROLLER_ID` only if you want to pin it.

## Tools

Read:

| Tool | What it returns |
|---|---|
| `helper_info` | Helper version and identity, and which controller the key is for |
| `controller_status` | Name, model, firmware, connection, power, battery, phone line, energy cost, troubles, `controllerFamily` |
| `controller_property(name)` | One controller property |
| `list_objects(type?)` | Every object with decoded state; filter by Zone, Unit, Flag, Output, Thermostat, Auxiliary Sensor, … |
| `get_object(type, number)` | One object; for a door lock, its locked state and the reader's last access |
| `helper_log(lines)` | The Helper's recent log lines |
| `ethernet_quality(since?)` | The controller connection's health record |
| `connection_status` | Helper 4.07+: live connected/reconnecting state and the panel messages showing now |
| `list_logged_sensors` | Which sensors record history, with the type and number |
| `sensor_history(type, number, points)` | Logged readings for one sensor or thermostat |
| `history_window(type, number, start, end, points, remove_outliers, runtime_bucket, time_zone)` | Helper 4.50+: bounded live/archived samples, statistics before display reduction, and hourly/daily/weekly reported HVAC intervals with unknown coverage and archive warnings |
| `panel_events(...)` | Helper 4.50+: cached panel history with zone/area/type/month/day filters and user attribution |
| `refresh_panel_events(scope)` | Helper 4.50+: recent history by default; explicit `all` reads up to 2000 available panel records. Poll `panel_events` while `refreshing` is true; check `panelEndReached` and `refreshError` before claiming completeness |

### Door access (HAI / Leviton)

An access-control reader and the lock it releases share one number.
`list_objects("Access Control Lock")` and `get_object("Access Control Lock", n)`
return `locked` and, once the reader has reported, the last access:

| Field | Meaning |
|---|---|
| `lastAccessGranted` | `true` granted, `false` denied |
| `lastAccessUser` | Omni user number: 0 none, 251 Duress, 252 Keyswitch, 253 Quick Arm, 254 PC Access, 255 Programming |
| `lastAccessBy` | Added by this server: that number in words (`"user 7"`, `"Duress"`, `"none"`) |
| `lastAccessAt` | When Helper received the reader status (ISO 8601) |

`Access Control Reader` objects carry the same result as `granted`, `lastUser`
and `lastAccessAt`. The panel does not timestamp accesses, so `lastAccessAt` is
"last reported", not necessarily the moment of the swipe. Helper 5.01 and later
read reader status at startup and on every status refresh; older Helpers report
it only after the next badge or keypad code. User codes are never exposed.
Reading door access is a read: it adds no write tool, and unlocking stays
behind the owner opt-in below.

The new history/activity tools require adapter 0.4.0 and Helper 4.50. Panel event
dates contain no year; `receivedAt` is download time, not event time. History
indexing is incremental: retry when `indexing` is true. HVAC intervals are
estimates from reported action, and `unknownSeconds` must not be interpreted as
equipment being off. These additions do not change the existing security tools.
Recent event refreshes are limited to once every 30 seconds per controller;
full downloads to once every 10 minutes. A `429` means wait and use the cached
`panel_events` result, not repeat the download immediately.

Write (each changes the physical home; tool descriptions tell the assistant to confirm first):

| Tool | Effect |
|---|---|
| `set_unit(number, value, timer?)` | 0 off, 1 or 100 on, 2–99 dimmer level; optional timer 1–99 s for on/off |
| `set_blind(number, value, timer?)` | Blind/shade 0–100 |
| `set_flag(number, value, timer?)` | Flag to 0–255; optional timer 1–99 s for 0/1 |
| `increment_flag(number)` / `decrement_flag(number)` | Updated Helper 4.50+: one native counter step, without cached read/modify/write |
| `dim_unit(number, steps?)` / `brighten_unit(number, steps?)` | Updated Helper 4.50+: 1-9 native lighting steps (default 1), not percentage points |
| `set_output(number, on, timer?)` | Output relay on or off; optional timer 1–99 s |
| `set_thermostat(number, mode?, heat_setpoint?, cool_setpoint?, humidify_setpoint?, dehumidify_setpoint?, fan?, hold?)` | Mode off/heat/cool/auto/emergency, setpoints, fan auto/on, hold |
| `set_area_lights(on, area)` | All lights in an area on or off (area 0 = all) |
| `run_button(number)` | Run a controller button (macro) |
| `set_upb_link(number, state)` | UPB link on / off / store |
| `show_panel_message(number, presentation)` | Show a programmed message on the keypads: beep_led / led / silent |
| `clear_panel_message(number)` | Clear one message, or 0 for all |
| `lock_door(number)` | Lock an access-control lock (unlocking is opt-in, below) |
| `set_user_setting(number, value)` | User setting 1–255 to a raw value 0–65535 |
| `set_energy_cost(level)` | low / medium / high / critical (HAI / Leviton only) |
| `send_notification(message)` | Push to the Myro:Home apps |
| `send_test_notification` | The Helper's test push, to check delivery |
| `speak(message)` | Speak on the Mac |
| `log_message(message)` | Write to the Helper log |
| `log_to_file(filename, message)` | Append to a file in the Helper's Logs folder |
| `sync_now` | Re-sync from the controller |

Adapter 0.5.0 adds typed `/control` tools for blinds, thermostats, area lights,
buttons, UPB links, messages, locks, user settings and audio transport, plus
timers on units, flags and outputs (Helper 4.07 or later). Existing untimed
unit/flag/output, audio, energy, notification, speech, log and sync tools retain
their existing routes. Timed dim levels, intermediate blind positions and flag
counters are rejected because the panel ignores their timer. Thermostat
setpoints use the controller's temperature format; Helper enforces that format's
range. A controller ACK means acceptance, not physical completion. After an
error or timeout, read the state before considering another write; commands are
never automatically retried.

Scripts (save, enable and reload need a key with **remote script management** enabled;
listing, reading and validating do not):

| Tool | What it does |
|---|---|
| `list_scripts` | Every script: name, id, enabled, schedule |
| `get_script(identifier)` | One script's full source |
| `scripting_reference(compact)` | The live scripting API, with your real device names |
| `validate_script(code)` | Syntax and warnings, without running it |
| `save_script(name, code, script_id, enabled)` | Create or replace; disabled by default |
| `set_script_enabled(identifier, enabled)` | Enabling runs the script immediately |
| `reload_scripts` | Re-runs every enabled script |

### Helper 4.50 relative controls

Adapter 0.5.0 adds the four relative tools above, requiring an updated Helper
4.50 build with these actions. Earlier 4.50 builds may not support them. Discover the exact Flag or
Unit number first. They validate input locally and use the authenticated typed
control endpoint; the Helper also checks that the object has the right type.
Each counter call requests one step. Lighting steps are not percentage points;
device support and resulting levels depend on the panel and lighting technology.
The panel defines behavior at counter endpoints 0/255. Flags can trigger panel
programs, so confirm intent before changing them.

No automatic retries: an ACK timeout may mean the command already applied.
`accepted` means the panel acknowledged it, not that a particular final value was
observed. Check reported state after uncertainty. These additions do not expand
security permissions. Older Helpers return explicit failure with an updated
4.50 build requirement; the adapter never substitutes a cached-value write.

### Helper 4.50 scripting features

The live `scripting_reference` exposes `MyroLibraries`, `MyroControllers`, and
`MyroSystem.speakQueued` / `cancelSpeech` / `cancelAllSpeech`. MCP can generate,
validate and save scripts using these APIs through the existing script tools;
it does not need a separate tool for every JavaScript method.

Library files must be installed locally in Helper's Scripts/Libraries folder.
Cross-controller access requires local **Send To** and **Accept From** grants
at the respective controllers. MCP cannot upload library files, change grants,
or silently switch its controller API key. A grant extends the origin scripts'
authority, including remotely managed scripts, so enable it only deliberately.
Validation checks the script body without loading dependencies or proving
permissions, device state or speech playback. Save disabled and review before
enabling. Script-owned speech cancellation does not cancel the separate HTTP
speech queue used by MCP's `speak` tool.

Browser login, themes and live page navigation are UI features, not MCP tools.
History queries include archived data automatically; archive deletion policy
remains local configuration, not an MCP write capability.

Audio, Lutron and displays:

| Tool | What it does |
|---|---|
| `list_audio_sources` | Sources and their cached metadata |
| `audio_source_metadata(source, refresh)` | What one source is playing |
| `set_audio_zone(zone, power, volume, mute, source)` | Control a zone |
| `audio_zone_key(zone, transport or key)` | Russound transport (play, pause, next, …) or a raw audio key 1–40 |
| `list_lutron` | Bridges, devices and Picos |
| `set_lutron_level(bridge_id, level, ...)` | Set a Lutron device 0-100 |
| `activate_lutron_scene(bridge_id, scene_href)` | Activate a scene |
| `press_lutron_button(bridge_id, integration_id, component)` | Press a keypad or Pico button |
| `list_displays` | Vestaboards and LaMetric clocks |
| `send_to_vestaboard(text | lines | characters, ...)` | Show text on a Vestaboard: auto-fit, per-row, or a raw code grid |
| `send_to_lametric(text, ...)` | Show a notification on a LaMetric |
| `sync_clock` | Set the controller's clock from this Mac |

Helper Mac:

| Tool | What it does |
|---|---|
| `beep` | System beep on the Helper Mac |
| `show_mac_alert(message)` | Alert dialog on the Helper Mac |
| `set_speech_delays(start_delay_ms, between_delay_ms)` | Tune the speech queue |
| `play_sound(filename, queue_with_speech)` | Play a sound from the Helper's Scripts folder |
| `stop_sound(filename)` | Stop it |

Security (all three need an API key with arm/disarm enabled; `arm` also needs a panel code stored locally):

| Tool | Effect |
|---|---|
| `arm(mode, area)` | day, night, away, vacation, day_instant, night_delayed |
| `bypass_zone(zone, bypassed, area)` | Bypass or restore one zone |
| `restore_all_zones(area)` | Restore every bypassed zone |

`arm` stays disabled until the owner stores a panel code on the machine
running this server. `bypass_zone` and `restore_all_zones` work without one on
Helper 4.13 or later, running as the panel's Programmed user; with a stored code
they run as that user instead. To store or remove the code:

```
uvx myrohome-helper-mcp security-code set      # prompts; nothing is echoed
uvx myrohome-helper-mcp security-code delete
```

The code goes into the operating system credential store (macOS Keychain,
Windows Credential Locker, Linux Secret Service) under the controller's
identity. It is never placed in the MCP config, an environment variable, or a
conversation, and it is read only when a security tool is actually called.
Helper validates it against the panel itself.

### Disarming and unlocking (off by default)

An assistant cannot turn the alarm off or unlock a door unless you allow it, on
the computer running this server:

```
uvx myrohome-helper-mcp allow disarm        # adds the disarm tool
uvx myrohome-helper-mcp allow unlock        # adds the unlock_door tool
uvx myrohome-helper-mcp disallow disarm     # takes it away again
```

Each asks you to type the word to confirm, and applies only to the controller
that API key belongs to. Restart your MCP client afterwards. Until you allow
them, the tools are not registered at all, so an instruction hidden in an
email, web page or document the assistant reads has nothing to call. Disarm
still also needs an API key with arm/disarm enabled and a stored panel code.
Revoking takes effect on the next call, without a restart.

AI assistants can be misled by text they read. Allow these only if you
understand that, and prefer the Myro:Home app for disarming.

## Security

This server runs on your LAN like every other Myro:Home client and holds one
controller's API key. Anyone who can run the assistant with this config can do
what the key allows. Keep it on trusted machines. The Helper's HTTP API is
plain HTTP on the local network; do not expose port 9393 to the internet.

## Developing

```
uv sync
MHH_URL=http://192.168.1.76:9393 MHH_API_KEY=... uv run myrohome-helper-mcp   # stdio server
uv run mcp dev src/myrohome_helper_mcp/server.py                             # MCP inspector
```

## Releasing

Bump `version` in `pyproject.toml` and `__init__.py`, commit, then tag and
push: `git tag v0.1.0 && git push origin v0.1.0`. The GitLab CI job builds
and publishes to PyPI through trusted publishing (one-time setup: PyPI
project -> Publishing -> add GitLab publisher `MyroControl/myrohome-helper-mcp`,
environment `pypi`).

MIT licensed. Myro Control, LLC.
