Skip to main content
Glama
adambenhassen

shelly-api-mcp

README.md
<p align="center">
  <img src="assets/banner.svg" alt="Shelly API MCP — read-only MCP server for Shelly Cloud energy data" width="100%">
</p>

<p align="center">
  <a href="#"><img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-3776AB?logo=python&logoColor=white"></a>
  <a href="https://modelcontextprotocol.io"><img alt="MCP" src="https://img.shields.io/badge/MCP-streamable--http-22C55E"></a>
  <a href="Dockerfile"><img alt="Docker" src="https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white"></a>
  <a href="#"><img alt="Access: read-only" src="https://img.shields.io/badge/access-read--only-475569"></a>
</p>

<p align="center">
  A small <a href="https://modelcontextprotocol.io">Model Context Protocol</a> server that wraps the
  <strong>Shelly Cloud Control API</strong> as curated, read-only electricity tools for AI agents —
  live power, lifetime energy, and historical consumption with cost.
</p>

---

## Why this exists

There is no official MCP for reading **your own** Shelly device data. The official Shelly MCP
(`shelly-api-docs.mcp.shelly.link`) is documentation-only and never touches your account, and the
community servers are either DeskAgent plugins or control-focused with no real consumption tooling.

Shelly MCP talks to the Shelly Cloud Control API directly — the same cloud your devices already
report to — and exposes **read-only** consumption tools. No control endpoints are wrapped: it can
read your meters, never switch anything.

## Features

- **Generic across device families** — Gen2 energy meters (`em:N` / `emdata:N`), Gen2 switches &
  plugs (`switch:N` / `pm1:N`), and best-effort Gen1 (`meters[]` / `emeters[]`).
- **Never fakes a zero** — missing or unparseable readings are flagged as `partial` with warnings
  instead of being silently summed as `0`, so a total is never quietly too low.
- **Historical buckets with cost** — hourly/daily/monthly consumption and net-energy series, summed
  across all three phases, with range totals.
- **Honest about gaps** — incomplete ranges report `gaps` / `partial_buckets` rather than presenting
  a partial range as whole.
- **Brief response cache** to soften the Cloud's ~1 req/s rate limit.

## Tools

| Tool | What it answers |
|------|-----------------|
| `list_devices()` | Inventory: id, name, model, generation, mode, online state, local IP. |
| `get_power(device_id?)` | Live electricity draw **now** (W) — total + per-phase / per-channel. |
| `get_energy(device_id?)` | Lifetime energy counters (kWh) — consumed, returned, per-phase. |
| `get_consumption_summary(device_id?)` | One-shot per-device summary: name, model, live W, lifetime kWh, meter temp. |
| `get_consumption_history(device_id?, date_range, date_from?, date_to?)` | Historical grid consumption over time **with cost**. |
| `get_net_energy_history(device_id?, date_range, date_from?, date_to?)` | Historical **net** energy (import − export); `consumption_kwh` is signed. |
| `get_device_status(device_id)` | Raw, complete `device_status` JSON — escape hatch for uncurated fields. |

`device_id` is optional on most tools (omit to cover all devices); it is **required** on
`get_device_status`. History tools require an explicit id when the account has more than one device.
For `date_range` use `day` \| `week` \| `month` \| `year` \| `custom` (custom requires both
`date_from` and `date_to` as `YYYY-MM-DD HH:MM:SS`). Only 3-phase meters (`em-3p`, e.g. Shelly Pro
3EM) are supported by the history tools.

## Prerequisites

- A Shelly Cloud account with at least one device.
- Your **Cloud Control API auth key** and **server host**, both from the Shelly app/web:
  *Settings → Authorization Cloud Key* (the key, e.g. `MWE...`) and the server it lists
  (e.g. `shelly-NN-eu.shelly.cloud`).
- [Docker](https://docs.docker.com/get-docker/) (recommended) **or** Python 3.10+.

## Configuration

Configuration is entirely via environment variables:

| Variable | Required | Default | Description |
|----------|:--------:|---------|-------------|
| `SHELLY_SERVER` | ✅ | — | Cloud server host or URL (bare host is fine; `https://` is assumed). |
| `SHELLY_AUTH_KEY` | ✅ | — | Cloud Control API auth key. |
| `SHELLY_TIMEOUT` | | `20` | HTTP timeout, seconds. |
| `SHELLY_CACHE_TTL` | | `5` | Response cache TTL, seconds. |
| `MCP_PORT` | | `3000` | Port the server listens on. |
| `TZ` | | _(UTC)_ | Timezone for named date ranges — **see the note below**. |

Put the secrets in a `secret.env` file (git-ignored) for Docker Compose:

```dotenv
SHELLY_SERVER=shelly-NN-eu.shelly.cloud
SHELLY_AUTH_KEY=your-cloud-control-api-key
```

> [!IMPORTANT]
> Named date ranges (`day`/`week`/…) are computed in **local time**, then read by the cloud in the
> **device's timezone**. Set `TZ` to your device's zone (e.g. `TZ=Europe/Paris`) or ranges shift by
> the UTC offset. With Docker Compose, `TZ` defaults to `Europe/Paris` and is overridable.

## Running

### Docker Compose (recommended)

```bash
docker compose up -d --build
```

The MCP endpoint is then available at **`http://localhost:3000/mcp`**. Override the host port with
`HOST_PORT` and the timezone with `TZ`:

```bash
HOST_PORT=8080 TZ=Europe/Berlin docker compose up -d --build
```

### Docker

```bash
docker build -t shelly-api-mcp .
docker run --rm -p 3000:3000 --env-file secret.env -e TZ=Europe/Paris shelly-api-mcp
```

### Local (Python)

```bash
pip install -r requirements.txt
export SHELLY_SERVER=shelly-NN-eu.shelly.cloud
export SHELLY_AUTH_KEY=your-cloud-control-api-key
python server.py
```

## Connecting an MCP client

The server speaks **streamable HTTP**. Point any MCP client at the `/mcp` endpoint:

```json
{
  "mcpServers": {
    "shelly": {
      "url": "http://localhost:3000/mcp"
    }
  }
}
```

## How it works

```
MCP client ──HTTP /mcp──> Shelly MCP (FastMCP) ──> Shelly Cloud Control API
                                                    POST /interface/device/list
                                                    POST /device/all_status
                                                    POST /device/status
                                                    GET  /v2/statistics/{power-consumption,net-energy}/em-3p
```

Energy counters arrive in **Wh** on the wire and are surfaced in **kWh**. The Cloud Control API is
rate-limited (~1 req/s); the short response cache is the only client-side mitigation. The server is
strictly read-only — no control endpoints are wrapped.

## Development

```bash
pip install -r requirements.txt pytest
pytest -q                 # 59 network-free unit tests over the parsing internals
python live_smoke.py      # paced end-to-end smoke test against the real cloud (needs creds)
```

`live_smoke.py` exercises every tool and underlying endpoint against the live API, paced to respect
the rate limit, and exits non-zero if any call fails.

## Related

- [**euenergy-mcp**](https://github.com/adambenhassen/euenergy-mcp) — an MCP server exposing
  European day-ahead electricity prices (ENTSO-E, ~41 bidding zones). Pairs well with this server:
  your own consumption here, market prices there.

## Notes & limitations

- **Read-only** by design — no switching, no configuration writes.
- **History tools cover 3-phase meters (`em-3p`) only**; other families return a structured error.
- Cost is reported in the account's tariff currency.
- Not affiliated with Allterco / Shelly.