Skip to main content
Glama
README.md
# iaqualink-mcp

MCP server for **iAqualink** (Zodiac / Jandy / Fluidra) pool and spa equipment.
Read temperatures, set points, pumps, heaters, lights, aux circuits and salt
chlorinator state, and switch equipment — every change confirmation-gated and
verified by a re-read. It signs in with your own iAqualink account (the same
email and password as the iAqualink app); no browser, extension or bridge.

Supports the two system types the reference client supports: **iAqua**
(Jandy iAquaLink panels) and **eXO** (Zodiac salt chlorinators). Other device
types on the account (robotic cleaners, …) are listed but not controlled.

> **Status: partly verified live.** Request shapes come from
> [iaqualink-py](https://pypi.org/project/iaqualink/) 0.7.0 (what Home
> Assistant uses) and a WebTouch capture (see
> [docs/IAQUALINK-API.md](docs/IAQUALINK-API.md)).
> - **Seen working on a real iAqua (RS) panel, 2026-10-11/12:** sign-in, the
>   system list (`devices.json`), the iAqua status reads (`get_home`,
>   `get_devices`) and WebTouch (location lookup, init, button presses and the
>   display stream).
> - **Still derived, not yet exercised live:** the iAqua `set_*` commands (pump,
>   heater, aux, light, temperature), every eXO request (shadow read and
>   writes), `iaqualink_set_chlorinator`'s inferred shape, and the iQ20 schedule
>   fallback. Every write still re-reads to confirm what happened.

This project was developed and is maintained by AI (Claude Code). Use at your
own discretion.

## Tools

| Tool | What it does | Annotation |
| --- | --- | --- |
| `iaqualink_list_systems` | Systems on the account and their serials | read |
| `iaqualink_get_status` | Temperatures, set points, pumps, heaters, aux/lights, chlorinator, salinity/pH/ORP (`view`: compact · full · raw) | read |
| `iaqualink_healthcheck` | Whether a sign-in is configured and accepted | read |
| `iaqualink_set_equipment` | Pool/spa pump, pool/spa/solar heater; eXO filter pump, production, boost, low, heating — on/off | write, not destructive |
| `iaqualink_set_aux` | Aux circuit or light on/off (by number, `aux_<n>` or label) | write, not destructive |
| `iaqualink_set_light` | Dimmable brightness (0/25/50/75/100) or color effect (iAqua) | write, not destructive |
| `iaqualink_set_temperature` | Pool and/or spa set point (iAqua °F/°C; eXO °C pool) | write, not destructive |
| `iaqualink_set_chlorinator` | eXO chlorinator output % — **inferred shape, not in iaqualink-py** | write, not destructive |
| `iaqualink_list_schedules` | RS panel programs via WebTouch (iAqua; iQ20 fallback probe); eXO timers | read |
| `iaqualink_add_schedule` | Add an RS panel program (circuit, start, stop, run days) via WebTouch | write, not destructive |
| `iaqualink_edit_schedule` | Change an RS panel program's start / stop / run days via WebTouch | write, not destructive |
| `iaqualink_delete_schedule` | Delete an RS panel program via WebTouch — **no undo** | write, **destructive** |
| `iaqualink_set_schedule` | Set / re-time / disable an eXO timer (start–stop, rpm on variable-speed pumps) — **eXO only, unverified live** | write, not destructive |
| `iaqualink_check_panel` | Is an RS system online, and what is its panel showing (screen, open dialog, Edit mode with unsaved changes)? Run it when the app and these tools disagree | read |
| `iaqualink_list_pump_speeds` | RS panel variable-speed pump: the named speeds, their RPM and which one runs, via WebTouch | read |
| `iaqualink_set_pump_speed` | Run the pump at a named speed via WebTouch | write, not destructive |
| `iaqualink_set_pump_speed_rpm` | Change a named speed's RPM (600–3450) via WebTouch; a speed that is not running runs briefly while set, then the running speed is restored | write, not destructive |
| `iaqualink_list_onetouch` | RS panel OneTouch scenes (Spa Mode, All OFF, …) and their state, via WebTouch | read |
| `iaqualink_set_onetouch` | Turn a OneTouch scene on/off via WebTouch — **toggle not exercised live yet** | write, not destructive |

**Systems by name.** At start the server lists the account's systems once;
when that works, every `serial` argument advertises the supported systems'
names (e.g. "Pool", "Spa") as an enum, and every tool accepts a name or a
serial — an unknown name is refused with the list of known systems. If the
listing fails (no sign-in yet, iAqualink down, past 5 s), the argument is a
plain serial and nothing else changes. Every tool also carries `_meta.mint`
hints (domain `home`; `lookup` for reads, `turn-on-off` for the `set_*`
switches, `change` for schedule edits) that mcp-host's context groups read and
other clients ignore.

Every write has an inverse in this set and reaches no other person — except
deleting a panel program, which the panel stores immediately with no undo, so
that one is marked destructive.

**Schedules and OneTouch on a Jandy AquaLink RS panel** go through
**WebTouch**, the panel emulation the iAqualink web app uses: the server opens
the panel's display stream, presses the same buttons a person would (checking
before every press that the panel is on the expected screen and row, and
aborting otherwise), returns to Home, and re-reads to verify. Only one WebTouch
session per system runs at a time. The add / edit / delete sequences follow a
live capture on an RS panel (2026-10-11) — see
[docs/IAQUALINK-API.md](docs/IAQUALINK-API.md#webtouch-aqualink-rs-panel-emulation).
If WebTouch is unavailable, `iaqualink_list_schedules` falls back to iQ20
schedules (adapter firmware 3+), probing `r-api.iaqualink.net/v2` (Bearer),
`p-api.iaqualink.net/v2` (Bearer), `p-api.iaqualink.net/v2` (bare token) and
`prm.iaqualink.net/v1` (bare token) once each and reporting every attempt.
iQ20 schedule *writes* are not built.

**How a write works.** It reads the current state first and sends nothing if
it already matches — iAqua's pump/heater/aux commands are *toggles*, so a blind
send would flip whatever the state happened to be. It then asks you to confirm
(an elicitation prompt where the client supports one; otherwise a preview plus
a `confirmToken`, bound to the state it was planned against), sends, and
re-reads until the requested value shows up. The result is `confirmed`,
`unchanged`, or `sent-unconfirmed` with what was observed (eXO applies changes
asynchronously — check again rather than retrying).

## Setup

```sh
npm i -g @chrischall/iaqualink-mcp
```

| Variable | Required | Meaning |
| --- | --- | --- |
| `IAQUALINK_EMAIL` | for every tool | Your iAqualink app sign-in email |
| `IAQUALINK_PASSWORD` | for every tool | Your iAqualink password (not trimmed) |
| `IAQUALINK_STATUS_CACHE_SECONDS` | no | Seconds a status read is reused (default 5; 0 disables) |
| `IAQUALINK_WEBTOUCH_STREAM_HOSTS` | no | Extra hosts (comma-separated) the WebTouch display stream may come from; default only `prm.iaqualink.net` |
| `IAQUALINK_VERIFY_TIMEOUT_MS` | no | Milliseconds a write re-reads to confirm the change before reporting `sent-unconfirmed`, for iAqua and eXO alike (positive integer; default 15000 iAqua, 30000 eXO) |
| `MCP_CONFIRM_MODE` | no | `ask-user` (default) / `auto` / `refuse` |
| `MCP_CONFIRM_TTL_SECONDS` | no | confirmToken lifetime (default 600) |
| `MCP_CONFIRM_ELICITATION` | no | `off` skips the prompt even on clients that declare elicitation |

The server starts without credentials (so hosts can list its tools) and
reports them missing on the first call. **A rejected password is never
retried**: the refusal is remembered until the server restarts, so a typo
cannot lock the account.

Claude Code: `claude mcp add iaqualink -e IAQUALINK_EMAIL=… -e IAQUALINK_PASSWORD=… -- npx -y @chrischall/iaqualink-mcp`

### Hosted (claude.ai)

`mint.yaml` declares per-user children with an email/password form on the
connector's authorize page (each person signs in as themselves), egress to
exactly `prod.zodiac-io.com`, `r-api.iaqualink.net`, `p-api.iaqualink.net`
(iQ20 probe only) and `prm.iaqualink.net` (WebTouch, and the iQ20 probe); the
WebTouch display stream is refused on any other host unless it is added to
`IAQUALINK_WEBTOUCH_STREAM_HOSTS` (and egress). No persistent
state — the session lives in memory and one sign-in rebuilds it after a cold
start.

## Skills

- `skills/iaqualink` — how to use these tools.
- `skills/iaqualink-curl` — the same reads straight from a shell with `curl`,
  credentials from the macOS Keychain (the sign-in, system list and iAqua reads
  are seen live; the eXO shadow read is still derived from iaqualink-py).

## Development

```sh
npm install
npm run build
npm run test:coverage   # typecheck (src + tests) + vitest, 100% coverage — what CI runs
```

Tests run the real client against an in-memory iAqualink
(`tests/support/fake-server.ts`) built from iaqualink-py's fixtures.

TDQS

A4/5.0

Scored across 19 tools

Disambiguation4/5

Most tools target a distinct resource+action (systems, status, schedules, OneTouch, pump speeds), and descriptions explicitly scope iAqua vs eXO behavior. Two real overlap points remain: iaqualink_set_equipment covers eXO 'production'/'boost' chlorine control while iaqualink_set_chlorinator sets output percentage, and iaqualink_set_aux turns light circuits on/off while iaqualink_set_light sets brightness/color. iaqualink_check_panel vs iaqualink_healthcheck is also a fine distinction an agent must reason about.

Naming Consistency4/5

All names share the iaqualink_ prefix and snake_case verb_noun structure (list_systems, get_status, set_equipment, add/edit/delete_schedule). The only deviation is iaqualink_healthcheck, which omits a verb, and the schedule tools split awkwardly across set_schedule (eXO) versus add/edit/delete_schedule (iAqua) for what is nominally one concept.

Tool Count4/5

Nineteen tools is on the heavy side, but the server genuinely spans two hardware families plus schedules, OneTouch scenes, VSP speeds, and diagnostics, so most tools earn their place. A few could be collapsed — set_chlorinator largely duplicates the eXO branch of set_equipment, and check_panel/healthcheck are adjacent diagnostics.

Completeness4/5

Coverage is strong for a control server: discovery, status, equipment/aux/light/temperature writes, full schedule CRUD for iAqua panels, OneTouch and pump-speed management, plus healthcheck and panel diagnostics. Gaps exist on the eXO side, which has list+set schedule but no add or true delete (only a disable-to-00:00), and there is no history/energy/log surface.

Maintenance

ActivityMaintained
ResponsivenessLimited data