lightwave-mcp
README.md
# lightwave-mcp
An [MCP](https://modelcontextprotocol.io) server that lets an AI agent discover and control
LightwaveRF Smart Series ("Link Plus") smart home devices - light switches/dimmers, relays,
and smart power sockets - so a request like "turn off the kitchen light" made to an
MCP-connected agent results in the physical device being controlled.
> **Disclaimer**: this is an independent, community project. It is not affiliated with,
> endorsed by, or supported by LightwaveRF Technology Ltd. You need your own LightwaveRF
> Link Plus account and hardware to use it. LightwaveRF's public API is provided at their
> discretion and may change without notice.
## How it works
LightwaveRF publishes an official, documented REST API at `https://publicapi.lightwaverf.com/`
(separate from the undocumented protocol their own mobile app uses internally). This server
talks to that public API directly over plain HTTPS - there's no persistent connection, no
websocket, and no third-party LightwaveRF client library involved.
```
AI agent (Claude, etc.)
| MCP tool calls (stdio)
v
lightwave-mcp server
| HTTPS + bearer token
v
https://publicapi.lightwaverf.com
|
v
Your LightwaveRF Link Plus hub -> physical devices
```
**Authentication.** LightwaveRF uses a Basic token + rotating refresh token pair, generated
once by hand from your account. The server exchanges these for a short-lived access token
via `POST https://auth.lightwaverf.com/token`. Refresh tokens are **single-use**: every
refresh returns a new one that must be used next time. `src/lightwave_mcp/auth.py`
(`TokenManager`) handles this - it caches the live, rotating pair in a local JSON file
(`.lightwave_token_cache.json`, gitignored) so restarts don't reuse an already-consumed
refresh token, and it fails with a clear, actionable error if the pair ever goes dead rather
than retrying silently.
**Discovery.** On startup (and whenever `refresh_devices` is called), the server:
1. `GET /v1/structures` - lists the LightwaveRF "structures" (homes) on the account
2. `GET /v1/structure/{id}` - lists devices and their featureSets/features in each structure
3. `GET /v1/hierarchy/{id}` - maps featureSets to room names, so devices can be reported with
their room (e.g. "Kitchen")
Each device exposes a set of typed "features" (`switch`, `dimLevel`, `socketSetup`, etc.).
`src/lightwave_mcp/devices.py` classifies each featureSet into `"light"` (has `dimLevel`),
`"outlet"` (has `socketSetup`), or `"switch"` (has `switch` only) - anything else (thermostats,
TRVs, covers, sensors, remotes) is out of scope and filtered out of discovery entirely. A
`DeviceRegistry` builds an id- and name-indexed lookup so tools can resolve a device by either
its LightwaveRF id or the name you gave it in the Lightwave app, and raises a clear error if a
name is ambiguous (two devices sharing a name) or unrecognized.
**Control.** State reads and writes go through `/v1/feature/:featureId` (and the batch
variants `/v1/features/read` / `/v1/features/write`) - there's no push/streaming state, so the
server reads live on every call rather than trusting a cache.
**Errors.** Anything a calling agent needs to react to (device not found, ambiguous name,
wrong device type for an operation, out-of-range brightness, an underlying HTTP failure) is
raised as an MCP `ToolError` with a specific, actionable message, instead of a raw stack trace.
## Setup
### 1. Get a LightwaveRF API token pair
1. Go to [my.lightwaverf.com](https://my.lightwaverf.com) (or the Link Plus app) and sign in.
2. Open **Settings -> API Integration -> Get Token**. You'll be asked for your account
password again.
3. You'll be given two values: a **Basic token** and a **Refresh token**. Copy both - the
refresh token is only shown once here (the server keeps it up to date after that; see
"How it works" above).
Treat both values as secrets - anyone with them can control every device on your account.
### 2. Configure and install
```
cp .env.example .env
```
Edit `.env`:
```
LIGHTWAVE_BASIC_TOKEN=<basic token from step 1>
LIGHTWAVE_REFRESH_TOKEN=<refresh token from step 1>
```
```
python -m venv .venv
.venv/Scripts/activate # or `source .venv/bin/activate` on macOS/Linux
pip install -e ".[dev]"
```
`.env` and the rotating `.lightwave_token_cache.json` it produces are both gitignored -
never commit either.
### 3. Try it directly
```
python -m lightwave_mcp
```
Starts the stdio MCP server. It fails fast with a clear error if the token pair is missing
or invalid, rather than starting up with zero devices.
### 4. Register with an MCP client (e.g. Claude Code)
```
claude mcp add lightwave -e LIGHTWAVE_BASIC_TOKEN=<basic token> -e LIGHTWAVE_REFRESH_TOKEN=<refresh token> -- python -m lightwave_mcp
```
Then, from a session:
- `list_devices` - see every switch, relay, socket, and light discovered in your home
- `turn_on` / `turn_off` - control one, by name (e.g. "Kitchen Light") or id
- `set_brightness` - dim a light (0-100); turn it on first
- `get_device_state` - check a single device's current state
- `refresh_devices` - re-discover if you've added/removed devices in the Lightwave app
## Tools
| Tool | Description |
| --- | --- |
| `list_devices()` | List all in-scope devices with current state |
| `get_device_state(device)` | Current state of one device |
| `turn_on(device)` | Turn a device on |
| `turn_off(device)` | Turn a device off |
| `set_brightness(device, level)` | Set a light's brightness, 0-100 |
| `refresh_devices()` | Re-run discovery |
`device` accepts either the name shown by `list_devices` (case-insensitive) or its id. If a
name matches more than one device, the error lists the ids to disambiguate with.
## Project layout
```
src/lightwave_mcp/
auth.py TokenManager - token exchange/refresh/caching against auth.lightwaverf.com
client.py LightwaveClient - thin async HTTP wrapper over publicapi.lightwaverf.com
devices.py Device classification, room enrichment, id/name resolution
server.py FastMCP tool definitions + startup/shutdown lifecycle
__main__.py Entry point (stdio transport)
tests/
test_client.py TokenManager/LightwaveClient against mocked HTTP (respx)
test_devices.py Classification and registry resolution logic
test_server_tools.py MCP tool functions against a fake client
test_integration_live.py Real discovery round trip; skipped unless real tokens are set
```
## Development
```
pip install -e ".[dev]"
pytest
```
`tests/test_integration_live.py` performs a real discovery round trip against your actual
LightwaveRF account and is skipped automatically unless `LIGHTWAVE_BASIC_TOKEN` and
`LIGHTWAVE_REFRESH_TOKEN` are set in the environment - it never runs in CI unless you add
real secrets there deliberately.
## Security notes
- Never commit `.env` or `.lightwave_token_cache.json` - both hold live credentials.
`.gitignore` already excludes them.
- If you ever suspect a token has leaked, revoke it by generating a fresh pair from
`my.lightwaverf.com -> Settings -> API Integration -> Get Token` - this invalidates the old
refresh token.
- This server only exposes switches, relays, sockets, and dimmable lights by design; it never
reads or writes thermostats, TRVs, or other device types, even though the underlying API
account may have access to them.
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues