Skip to main content
Glama
achouwer

shelly-readonly-mcp

by achouwer
README.md
# Shelly Read-Only MCP

A fail-closed, local-only MCP server for inspecting Shelly devices discovered
through Home Assistant. It exposes status and redacted configuration, but no
switching, control, generic RPC, or configuration tools.

## Development disclosure

This project was designed and developed with assistance from **OpenAI Codex**.
Codex was used to help create the implementation, security controls, automated
tests, Docker configuration, and documentation. The project is independently
maintained and is not an official OpenAI, Home Assistant, or Shelly product.

AI-assisted code can contain mistakes. Review changes, run the test suite, and
validate security-sensitive behavior before deploying or contributing.

## Requirements

- Docker Engine with Docker Compose
- Home Assistant reachable on the local network
- The official **Shelly integration must already be installed and running in
  Home Assistant**
- Shelly devices must be present in the Home Assistant device registry
- A dedicated, non-administrator Home Assistant user with **local access only**
- A separate Home Assistant long-lived access token created while signed in as
  that dedicated user

Do not use an owner or administrator token. A Home Assistant token is a bearer
credential and is not inherently read-only; the server limits itself to three
fixed read commands, but a stolen token retains the permissions of its user.

## Home Assistant account setup

1. In Home Assistant, create a user such as `shelly_inventory`.
2. Enable **local access only**.
3. Leave **administrator** disabled.
4. Sign in locally as that user.
5. Create a dedicated long-lived access token from the user's Security page.
6. Store the token in a file outside the repository:

   ```bash
   mkdir -p ~/.secrets/shelly-readonly-mcp
   chmod 700 ~/.secrets ~/.secrets/shelly-readonly-mcp
   nano ~/.secrets/shelly-readonly-mcp/ha_token
   chmod 600 ~/.secrets/shelly-readonly-mcp/ha_token
   ```

Never paste the token into `.env`, `compose.yaml`, Git, logs, screenshots, or
support requests.

## Installation

```bash
git clone https://github.com/YOUR-ACCOUNT/shelly-readonly-mcp.git
cd shelly-readonly-mcp
cp .env.example .env
nano .env
docker compose up -d --build
```

Set `APP_UID` to the owner of the token file. Find it with `id -u` on the
Docker host. This lets the non-root container read the mode-`600` secret
without weakening its filesystem permissions.

Validate the resolved configuration before starting:

```bash
docker compose config
```

Example `.env`:

```dotenv
APP_UID=1000
HA_HOST=192.168.1.10
MCP_BIND_IP=192.168.1.20
MCP_PORT=3001
HA_INVENTORY_TTL=600
HA_TOKEN_FILE_HOST=/home/your-user/.secrets/shelly-readonly-mcp/ha_token
```

The MCP endpoint is then:

```text
http://MCP_BIND_IP:3001/mcp
```

## MCP tools

- `shelly_list_devices`
- `shelly_refresh_inventory`
- `shelly_get_summary`
- `shelly_get_snapshot`

There is deliberately no generic RPC, service-call, switch, light, cover, or
configuration tool.

## Discovery behavior

Home Assistant is the source of device names, models, entities, and integration
membership. Local IP addresses are obtained from state attributes associated
with the same Home Assistant device. Only private IPv4 targets are accepted.

- Gen1 devices use exact query-free `GET /shelly`, `/status`, and `/settings`.
- Gen2/Gen3/Gen4 devices use a small allowlist of read-only RPC methods.
- Shelly BLU devices have no directly reachable IP and remain available through
  Home Assistant rather than direct device access.
- The inventory is refreshed lazily when used and is at most ten minutes old by
  default.
- The last valid inventory is stored in a Docker volume and survives restarts.
- A failed or empty refresh never overwrites the last valid inventory.

## Network security

Bind the MCP port to a specific LAN address and restrict it to trusted clients.
Example for a Docker host using `DOCKER-USER` (replace the addresses):

```bash
sudo iptables -I DOCKER-USER 1 -p tcp -s 192.168.1.50 --dport 3001 -j ACCEPT
sudo iptables -I DOCKER-USER 2 -p tcp --dport 3001 -j DROP
sudo netfilter-persistent save
```

Do not expose the MCP endpoint or Home Assistant token to the public internet.

## Security model

- Deny by default; permit only exact allowlisted read operations.
- No arbitrary host, path, query string, RPC method, or Home Assistant command.
- Private IPv4 destinations only.
- Response size and timeout limits.
- Recursive redaction of passwords, tokens, private keys, and URL credentials.
- Home Assistant token mounted as a Docker secret, never built into the image.
- Persistent cache contains device metadata and local IPs, but no token.
- Container runs as a non-root user with a read-only root filesystem, no Linux
  capabilities, and `no-new-privileges`.

See [SECURITY.md](SECURITY.md) for reporting and limitations.

## Tests

```bash
python -m unittest discover -s tests -v
```

## License

Distributed under the [MIT License](LICENSE).