Loxone MCP Proxy
by ivantichy
README.md
# Loxone MCP Proxy
A tiny, **zero-dependency** local proxy that gives Claude Desktop (or any
stdio-based MCP client) a **stable** connection to a **Loxone Miniserver's
native MCP Server** β with a fully **headless, credential-based login** (no
browser, no manual OAuth click).
> Loxone Gen 2 Miniservers (firmware **17.1+**) can run a built-in **MCP Server**
> plugin. Connecting a *local* MCP client to it normally has two pain points that
> this proxy solves for you.
## Why this exists
1. **The Miniserver's MCP URL is not stable.** Loxone publishes the endpoint
through a Cloud "connect" service that **redirects to a relay address that
rotates over time** (it changes on Miniserver restarts and relay reshuffles).
If you hard-code the redirected `β¦dyndns.loxonecloud.com:PORT/mcp` URL into a
client, it breaks a while later.
2. **Auth is interactive by default.** The MCP endpoint speaks OAuth 2.1 and the
official flow pops a browser login. That's awkward for an always-on client and
impossible for a headless setup.
**This proxy** exposes a normal local **stdio** MCP server to Claude, and on the
other side it:
- resolves the **stable** `connect.loxonecloud.com/<serial>/mcp` entry point to
the **current** relay, and **re-resolves automatically** whenever the relay
moves or dies (so your Claude connection just keeps working);
- performs the **OAuth login headlessly** using a Loxone username/password from a
local `.env` (it drives the Miniserver's HTML login form directly β no browser),
then caches and refreshes the token;
- proxies the MCP protocol (streamable HTTP β stdio) transparently.
## Features
- π **Stable** β hides the rotating relay address behind automatic re-resolution.
- π€ **Headless** β logs in from `.env` credentials, no browser, no clicking.
- β»οΈ **Self-healing** β re-resolves the relay and refreshes/re-issues tokens on
failure, and transparently re-initializes a dropped session.
- πͺΆ **Zero runtime dependencies** β plain Node.js (β₯ 20). Nothing to build.
- π **Least-privilege friendly** β log in as a dedicated, restricted Loxone user;
tokens are cached with `0600` permissions.
## How it works
```
Claude Desktop ββstdio(JSON-RPC)βββΆ loxone-mcp-proxy ββHTTPS(streamable MCP)βββΆ Loxone relay βββΆ Miniserver
β
1) GET connect.loxonecloud.com/<serial>/mcp β 307 β current relay "entry"
2) headless OAuth on the entry:
register client β GET authorize (HTML login form)
β POST username+password (+hidden fields) β ?code=β¦
β exchange code (PKCE) β access + refresh token
3) speak MCP to the entry with the Bearer token
4) on relay death / 401 / lost session β re-resolve / refresh / re-init
```
A subtle but important detail: the OAuth **`resource`** advertised by the server
is only the token **audience identifier** β it frequently points at a *different,
sometimes unreachable* relay. MCP traffic is therefore sent to the **reachable
connect-resolved entry**, not to `resource`. The proxy handles this for you.
## Requirements
- **Node.js β₯ 20** (uses the built-in `fetch`, no packages to install).
- A **Loxone Gen 2 Miniserver, firmware 17.1+**, with the **MCP Server** plugin
added and running (Loxone Config β Network Periphery β *MCP Server*).
- A **Loxone user account** for the assistant. Create a **dedicated** one with the
minimum rights it needs (ideally read-only to start).
## Setup
### 1. Get the code
```bash
git clone https://github.com/ivantichy/loxone-mcp-proxy.git
cd loxone-mcp-proxy
# no "npm install" needed β there are no dependencies
```
### 2. Configure credentials
```bash
cp .env.example .env
```
Edit `.env`:
```ini
LOXONE_SERIAL=504F94A1AE7A # your Miniserver serial (MAC without colons)
LOXONE_USER=mcp # a DEDICATED, minimal-rights Loxone user
LOXONE_PASSWORD=your-password
```
**Finding your serial:** open `http://<miniserver-ip>/jdev/cfg/api` in a browser
and read the `snr` field (e.g. `50:4F:94:A1:AE:7A` β `504F94A1AE7A`), or read it
in Loxone Config.
### 3. Verify it works
```bash
npm run login-check
```
Expected:
```
OK Authenticated & connected. Server: MCP Server 1.0.0
OK 11 tools available: control_find, control_command, system_status, ...
All good - this configuration works in Claude Desktop.
```
If this fails, see [Troubleshooting](#troubleshooting).
### 4. Add it to Claude Desktop
Open **Settings β Developer β Edit Config** and add a server that runs this proxy
over stdio. Point it at `src/index.js` with an absolute path:
```jsonc
{
"mcpServers": {
"loxone": {
"command": "node",
"args": ["/absolute/path/to/loxone-mcp-proxy/src/index.js"]
}
}
}
```
On **Windows**, escape the backslashes:
```jsonc
{
"mcpServers": {
"loxone": {
"command": "node",
"args": ["C:\\path\\to\\loxone-mcp-proxy\\src\\index.js"]
}
}
}
```
Credentials are read from the project's `.env`, so you don't put secrets in the
Claude config. Fully **quit and reopen** Claude Desktop, then ask it something
like *"what's on in Loxone?"*.
## What Claude can do
The tools come from your Miniserver's MCP plugin and act with **your Loxone
user's permissions**. Typical set:
| Tool | Type | Purpose |
|------|------|---------|
| `control_find`, `control_describe` | read | discover controls, rooms, categories |
| `control_state`, `system_status` | read | read current values / installation status |
| `control_history`, `control_statistics` | read | historical data & aggregates |
| `weather` | read | Loxone weather data |
| `control_command`, `control_secured_command` | write | operate controls (lights, blinds, scenesβ¦) |
| `code_run`, `sdk_describe` | advanced | scripted access (use with care) |
To keep the assistant **read-only**, either restrict the Loxone user's rights or
disable the write tools in your MCP client's connector settings.
## State warm-up (optional)
The Miniserver's MCP plugin (tech preview) fills its `control_state` table only
from **state-change deltas**, and it only starts collecting them once the first
state query of a server-side session epoch arrives β there is no initial
snapshot. Practical symptom: in a fresh session, *"what's the temperature at
home?"* returns no data until each value physically changes.
Set `LOXONE_STATE_WARMUP=1` and the proxy issues one tiny `control_state` nudge
right after every (re-)initialize, so the Miniserver starts accumulating values
immediately at connect time instead of at your first question. By the time you
ask, slow-moving states (room temperatures etc.) have usually re-broadcast and
are present. This cannot conjure a value that has not changed or re-broadcast
yet β that part only Loxone can fix server-side β but it removes the
"assistant starts blind" effect for a long-running Claude Desktop.
`LOXONE_WARMUP_SWEEP=1` additionally touches every control individually after
the nudge (one `control_state` per control, spaced out). Under currently
observed firmware semantics the delta subscription is global, so the sweep is
redundant β it exists as belt-and-braces for future firmware.
## Security
- **Use a dedicated Loxone user** with the least privilege necessary. The
assistant can only see and do what that user can.
- Credentials live only in your local `.env` (git-ignored). Tokens are cached at
`~/.loxone-mcp-proxy/cache.json` with `0600` permissions. **Windows note:**
POSIX file modes are effectively ignored on Windows β the cache is only as
private as your user profile directory (NTFS ACLs), so keep the machine
account personal.
- Nothing is sent to any third-party cloud β the proxy talks only to Loxone's own
connect/relay infrastructure and your Miniserver.
## Troubleshooting
Run with verbose logging (to stderr):
```bash
LOXONE_DEBUG=1 npm run login-check # macOS / Linux
```
```powershell
$env:LOXONE_DEBUG='1'; npm run login-check # Windows PowerShell
```
- **`login was rejected (form re-rendered)`** β wrong `LOXONE_USER` /
`LOXONE_PASSWORD`, or that user may not access this Miniserver.
- **`connect is rate-limited (HTTP 429)`** β the connect endpoint allows only a
handful of requests per window; wait a minute. Normal operation caches the
endpoint and rarely hits it.
- **`could not authenticate after N attempts`** β check the MCP Server plugin is
added and running in Loxone Config, and that the Miniserver has external access
/ Remote Connect enabled.
- **Claude shows the server but no tools** β fully quit Claude Desktop (from the
tray) and relaunch; confirm `npm run login-check` passes.
## Configuration reference
All optional, via `.env` or environment:
| Variable | Default | Meaning |
|----------|---------|---------|
| `LOXONE_SERIAL` | β | **required** β Miniserver serial (MAC, no colons) |
| `LOXONE_USER` | β | **required** β Loxone username |
| `LOXONE_PASSWORD` | β | **required** β Loxone password |
| `LOXONE_CONNECT_BASE` | `https://connect.loxonecloud.com` | Loxone Cloud connect base |
| `LOXONE_CACHE_PATH` | `~/.loxone-mcp-proxy/cache.json` | token cache location |
| `LOXONE_REDIRECT_PORT` | `41678` | loopback port used only as the OAuth redirect target |
| `LOXONE_REQUEST_TIMEOUT_MS` | `120000` | per-request deadline for MCP calls to the relay |
| `LOXONE_STATE_WARMUP` | `0` | set `1` to nudge state-delta collection after each (re-)initialize |
| `LOXONE_WARMUP_SWEEP` | `0` | with warm-up: also touch every control individually |
| `LOXONE_WARMUP_DELAY_MS` | `1000` | delay between session establishment and the warm-up |
| `LOXONE_WARMUP_SPACING_MS` | `150` | spacing between per-control sweep calls |
| `LOXONE_WARMUP_MAX_CONTROLS` | `500` | sweep enumeration cap |
| `LOXONE_MAX_ATTEMPTS` | `6` | max authentication attempts before giving up |
| `LOXONE_CONNECT_MIN_INTERVAL_MS` | `2000` | min. spacing between connect-endpoint calls |
| `LOXONE_ENV_FILE` | β | explicit path to the `.env` file to load |
| `LOXONE_NO_DOTENV` | `0` | set `1` to skip `.env` loading entirely |
| `LOXONE_DEBUG` | `0` | set `1` for verbose stderr logging |
| `LOXONE_LOG_LEVEL` | `info` | `error` / `warn` / `info` / `debug` (ignored if `LOXONE_DEBUG=1`) |
## Development
```bash
npm test # unit tests (node:test, no deps)
# live integration test (needs .env + a reachable Miniserver) β opt in via env:
LOXONE_INTEGRATION=1 npm run test:integration # macOS / Linux
```
```powershell
$env:LOXONE_INTEGRATION='1'; npm run test:integration # Windows PowerShell
```
Layout:
```
src/
index.js entry point (loads config, starts the stdio proxy)
proxy.js stdio <-> Connection bridge (newline-delimited JSON-RPC)
connection.js endpoint resolution, token lifecycle, session, recovery
loxoneAuth.js headless OAuth (register, HTML-form login, token, refresh)
resolver.js connect.loxonecloud.com -> current relay, cached
cache.js on-disk token cache
config.js .env + environment config
logger.js stderr logging (stdout is reserved for MCP)
scripts/
login-check.js standalone connectivity/credentials check
```
## License
[MIT](./LICENSE) Β© Ivan Tichy
## Acknowledgements
Built for connecting Claude to Loxone's native MCP Server. Not affiliated with or
endorsed by Loxone.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues