mqtt-readonly-mcp
by OdorDecoder
README.md
# mqtt-readonly-mcp
> **First attempt.** This is an early, hobby-grade release: it works on the author's own Home Assistant setup and has a test suite, but it has not been used by many people or on many brokers. Expect rough edges, and please read the [disclaimer](#disclaimer-use-at-your-own-risk) before you point it at anything you care about.
A small **listen-only** MQTT tool for [Claude Code](https://claude.com/claude-code) and other MCP clients.
## Purpose
The goal is to let an AI assistant **watch the traffic on a smart-home MQTT broker live**, understand it, and then help you **bring that traffic under control through Home Assistant**.
The tool itself only listens. It shows the assistant what Home Assistant, Zigbee2MQTT, Frigate and your devices are publishing: which topics are chatty, what a sensor sends when something happens, how busy the broker is. The assistant then uses that picture, together with Home Assistant, to change things: for example lowering a presence radar's update rate or turning down a noisy Zigbee2MQTT log level. Those changes are made through Home Assistant (or Zigbee2MQTT), not through this tool, and they need your approval like any other change the assistant proposes.
- `server.py`: an MCP server (stdio). Keeps a rolling buffer of what the broker publishes, so the assistant can ask "what did topic X send in the last N minutes?".
- `stream.py`: prints one line per message, for Claude Code's **Monitor** tool (live notifications in the chat).
- `mqtt_common.py`: shared code (buffer, redaction, payload formatting, connection).
Neither program contains any publish code. `tests/test_no_publish.py` fails if publish-like code appears in them.
## What it is good for
It was built to answer questions like these on a real smart home:
- Which device is flooding the broker? (`mqtt_rates` found that one presence radar produced 87% of about 1,400 messages per minute; after the radar's update rate was lowered in Home Assistant, traffic fell to about a fifth.)
- Did the Zigbee2MQTT change I just made actually reduce traffic?
- What exactly does this sensor publish when the door opens? (`mqtt_wait`)
## Redaction: on by default, how to switch it
Message payloads end up in the AI conversation, and MQTT carries secrets (the Zigbee2MQTT network key, camera passwords in URLs, API tokens). So **redaction is on by default**.
| You want | Set |
|---|---|
| Secrets scrubbed (default) | nothing, or `MQTT_RO_REDACT=1` |
| Payloads exactly as published | `MQTT_RO_REDACT=0` |
Set it in the environment of the server, for example when registering with Claude Code:
```
claude mcp add --scope user mqtt-readonly -e MQTT_RO_HOST=192.168.1.10 -e MQTT_RO_REDACT=0 -- /path/to/.venv/bin/python /path/to/server.py
```
To turn redaction back on, remove that variable or set `MQTT_RO_REDACT=1`, then restart the server (in Claude Code, reload the session). The same variable controls `stream.py`. Details of what is scrubbed are in [Redaction](#redaction) below. Turn it off only on a broker whose traffic you are happy to paste into a chat.
## Requirements
- Python 3.10 or newer
- An MQTT broker you can log in to. Create a separate login for this tool.
- `pip install -r requirements.txt` (`mcp` is pinned below 2 because version 2 renamed the server API)
## Setup
1. Create a virtual environment and install the requirements.
```
python -m venv .venv
.venv/bin/pip install -r requirements.txt # Windows: .venv\Scripts\pip
```
2. Put the broker login in a credentials file that is **not** inside this project. The default location is `~/.config/mqtt-readonly/credentials.json`:
```json
{"username": "mqtt_readonly", "password": "..."}
```
Restrict it to your own account (`chmod 600`, or `icacls` on Windows). The tool never prints it.
3. Register the server with Claude Code. The password is not on the command line:
```
claude mcp add --scope user mqtt-readonly \
-e MQTT_RO_HOST=192.168.1.10 -e MQTT_RO_PORT=1883 \
-- /path/to/.venv/bin/python /path/to/server.py
```
Any other MCP client works the same way: run `python server.py` over stdio with the environment variables below.
### Environment variables
| Variable | Default | Meaning |
|---|---|---|
| `MQTT_RO_HOST` | `127.0.0.1` | Broker address |
| `MQTT_RO_PORT` | `1883` | Broker port |
| `MQTT_RO_CRED_FILE` | `~/.config/mqtt-readonly/credentials.json` | Credentials file |
| `MQTT_RO_NO_AUTH` | unset | `1` connects without a login (local test broker only) |
| `MQTT_RO_FILTERS` | `#` | Comma-separated subscription filters |
| `MQTT_RO_ALLOW` | empty (allow all) | Comma-separated topic filters this tool may see at all (see below) |
| `MQTT_RO_IGNORE` | empty | Comma-separated topic filters to skip (noisy sources) |
| `MQTT_RO_BUFFER` | `5000` | Messages kept in the rolling buffer |
| `MQTT_RO_REDACT` | `1` | `0` reports payloads exactly as published (see below) |
The connection is plain MQTT. There is no TLS option yet, so use it on a trusted network.
## Tools
| Tool | What it does |
|---|---|
| `mqtt_status` | Connection state, buffer fill, uptime |
| `mqtt_topics(prefix, limit)` | Topics seen, newest first, with counts |
| `mqtt_tree(prefix, depth, limit)` | Topic tree: topics grouped by their first `depth` levels, with topic and message counts, busiest first |
| `mqtt_broker_stats` | Broker statistics from `$SYS`: version, uptime, clients, subscriptions, retained messages, messages per minute |
| `mqtt_last(topic_filter, limit)` | Latest value per matching topic (includes retained values) |
| `mqtt_recent(topic_filter, minutes, limit)` | Messages from the buffer, oldest first |
| `mqtt_rates(minutes, limit, group_levels)` | Messages per minute, busiest first: finds chatty topics |
| `mqtt_wait(topic_filter, seconds, max_messages)` | Waits for NEW messages (max 60 s) |
MQTT wildcards `+` and `#` work in every filter. Retained messages (replayed on connect) go only into the last-value map, not the buffer. Responses are capped at 20,000 characters.
## Limiting what it can see (`MQTT_RO_ALLOW`)
By default the tool subscribes to `#` and can read every topic your login can. Set `MQTT_RO_ALLOW` to a comma-separated list of topic filters to make that a hard limit:
```
MQTT_RO_ALLOW=zigbee2mqtt/#,alarm/#,$SYS/#
```
- The server only subscribes to filters inside the allow list, and drops any message outside it before it is stored.
- `stream.py` reads the same variable and refuses (`[ERROR]`, exit code 2) any `--filter` the allow list does not cover, so the Monitor tool cannot widen access.
- `MQTT_RO_FILTERS` only chooses what the server subscribes to. `MQTT_RO_ALLOW` is the limit that also constrains `stream.py`.
- Include `$SYS/#` if you want `mqtt_broker_stats` to work.
This restricts the tool, not your broker login. For enforcement on the broker itself, use an ACL (see "What read-only means here").
## Broker statistics
The server also subscribes to `$SYS/#` (the `#` filter alone does not match `$SYS` topics). Mosquitto publishes these roughly every 10 seconds, so only the latest value of each is kept and they never fill the message buffer. `mqtt_broker_stats` reports a short list of the useful figures. If the broker publishes no `$SYS` topics, or your login may not read them, the tool says so rather than failing. Other brokers use different `$SYS` names, so the output may be sparse there.
## Redaction
Message payloads end up in the AI conversation, and MQTT carries secrets: the Zigbee2MQTT network key, camera passwords in URLs, API tokens. So redaction is **on by default**:
- JSON values whose key contains `pass`, `secret`, `token`, `key`, `psk`, `auth` or `credential` become `[REDACTED]`, at any depth.
- The same pattern is applied to non-JSON text (`key=value`), and `user:password@` inside URLs is masked.
- `zigbee2mqtt/bridge/*` topics (except `bridge/state`) report their size only, because they carry network and device configuration.
- Redaction runs before a payload is trimmed or stored, so a secret cannot survive a cut.
- Binary payloads (camera JPEGs) are shown as `[binary N bytes]`.
Redaction is pattern based. It will miss a secret that sits under an unusual key name, so treat it as a safety net, not a guarantee. Set `MQTT_RO_REDACT=0` only on a broker whose traffic you are happy to paste into a chat. JSON is re-serialised compactly when redaction is on, so spacing differs from the original.
## Monitor (live stream)
```
python stream.py --filter "zigbee2mqtt/#" --exclude "zigbee2mqtt/bridge/#" --max-per-minute 30 --seconds 120
```
Each printed line becomes a chat notification when run through Claude Code's Monitor tool, so a per-minute cap applies and the excess is summarised as `[suppressed N messages ...]`. It prints `[ERROR] ...` and exits if credentials are missing, the login fails, or the broker cannot be reached. Retained replay is skipped unless you pass `--retained`.
## What "read-only" means here
The code has no publish path, and a test enforces that. But the **broker login can still publish** unless you restrict it on the broker. For real enforcement, give the login read access only. With Mosquitto, an ACL file like this does it:
```
user mqtt_readonly
topic read #
```
Be careful: once you enable `acl_file`, every other user and client must also be listed in it, or they lose access. Back up the broker configuration first.
## Known limits
- The buffer holds 5,000 messages by default. A very chatty device shortens the window; use `MQTT_RO_IGNORE` or lower that device's update rate. `mqtt_rates` reports the effective window.
- The buffer only holds what arrived while the server was running.
- No TLS and no MQTT 5 specific features.
## Tests
Most tests run without a broker. The integration tests need a throwaway broker and skip themselves when it is not running:
```
docker run -d --rm --name mqtt-ro-test -p 18830:1883 eclipse-mosquitto:2 mosquitto -c /mosquitto-no-auth.conf
python -m unittest discover -s tests -t .
docker stop mqtt-ro-test
```
(Git Bash on Windows needs `MSYS_NO_PATHCONV=1` in front of `docker run`.) `tests/demo_publisher.py` publishes sample messages to that test broker so the Monitor tool has something to stream. It is a test helper and is not part of the server.
## Acknowledgments
- **Pat ([OdorDecoder](https://github.com/OdorDecoder))**: idea, requirements, testing on a real Home Assistant installation, and every decision about what the tool should and should not do.
- **Claude** (Anthropic's AI assistant, used through Claude Code): wrote the code, tests and documentation in collaboration with Pat.
- Built on the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk), [Eclipse Paho MQTT client](https://github.com/eclipse-paho/paho.mqtt.python) and [Eclipse Mosquitto](https://mosquitto.org/) (used as the throwaway test broker).
- Ideas for features came from looking at descriptions of other MQTT MCP servers: [mcmqtt](https://git.supported.systems/MCP/mcmqtt/src/branch/main/llms.txt), [tspspi/mcpMQTT](https://lobehub.com/mcp/tspspi-mcpmqtt) and [rsbohn/mqtt-bridge](https://lobehub.com/mcp/rsbohn-mqtt-bridge). No code was copied from them, and thanks to their authors for showing what is useful.
## Disclaimer: use at your own risk
This is a first attempt, shared as is, under the MIT license, which already says the software comes **without warranty of any kind**. In plain words:
- **The author takes no responsibility for anything that goes wrong** from using, misusing or relying on this software. That includes, without limit, exposed secrets or private data, a broker or smart-home device that misbehaves or loses access, wrong advice from an AI assistant that uses this tool, and any cost or damage that follows.
- The tool is read-only in its own code, but your broker login may still be able to publish unless you restrict it on the broker. See "What read-only means here".
- Redaction is pattern based and can miss a secret. Do not rely on it as your only protection. Review what you send to any AI service.
- Test on a non-critical setup first, and take a backup before changing your broker or Home Assistant configuration.
## A note on personal information
Care was taken to scrub this repository of personal information: the author's network addresses, file paths, credentials and device names were removed from the code, tests and documentation, and the full git history was checked for secrets before publishing. The example values you see (such as `192.168.1.10`, `mqtt_readonly` or the fake passwords in the tests) are placeholders. No scrub is perfect, so if you spot something that should not be here, please open an issue.
## License
MIT, see `LICENSE`.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues