mcp-venus-os
# MCP Venus OS
## Venus OS deployment notes
Prefer the MQTT backend on a NAS, server, or workstation. It keeps the MCP/HTTP
runtime off a constrained GX while using the existing Venus MQTT gateway.
The Cerbo audit on 2026-09-12 found no native `mcp-venus-os` service; the documented
Synology deployment is a separate host and must be checked there.
SSH package refresh downloads into a temporary `/data` staging directory and
validates that `setup` exists before copying into the installed tree. It retains
files absent from the release, including virtualenvs and local configuration,
and calls `setup install` without interactive stdin. Package-owned uninstall
handles service removal; there is no recursive-delete fallback. A release can
still replace same-named tracked files, so keep local secrets in the package's
documented external configuration files and retain a backup before upgrades.
[](https://github.com/4alvit/mcp-venus-os/actions/workflows/codeql.yml)
[](https://github.com/4alvit/mcp-venus-os/actions/workflows/scorecards.yml)
[](https://github.com/4alvit/mcp-venus-os/actions/workflows/dependency-review.yml)
[](https://opensource.org/licenses/MIT)
[](https://www.python.org/downloads/)
[]()
[](https://github.com/4alvit/mcp-venus-os/commits/main)
[](https://github.com/4alvit/mcp-venus-os/graphs/commit-activity)
[](https://www.python.org/)
[](https://modelcontextprotocol.io/)
MCP (Model Context Protocol) server for Victron Venus OS management. Primary transport is the **Venus OS MQTT gateway** (`N/<portalId>/…` reads, `W/<portalId>/…` writes) so the server can run off-device; direct D-Bus remains available for on-device installs.
<!-- ci-release-process:start -->
## Release process
See the [release strategy](RELEASING.md) for validation, nightly, beta, RC and stable promotion rules, and the [operator runbook](docs/release-workflow.md) for local commands.
<!-- ci-release-process:end -->
## Features
- **MQTT read path**: subscribes `N/<portalId>/#` on the Cerbo GX gateway and serves tools from a stale-guarded cache (`stale`, `age_seconds` per reading)
- **Write tools over `W/` topics**: inverter mode, charge-current limit, SoC limit — each write is kept alive (≤60s expiry) and verified by read-back before reporting success
- **Safety constraints**: confirmation gate + hard limits enforced before any publish
- **Two server transports**: stdio (Claude Code launches the process) or streamable HTTP with optional bearer-token auth (Synology Docker / shared use)
- **Optional D-Bus backend**: unchanged behavior for installs running directly on the Cerbo
## Quick Start
### Installation
Not yet on PyPI. Install from GitHub:
```bash
pip install git+https://github.com/4alvit/mcp-venus-os
```
Or for development:
```bash
git clone https://github.com/4alvit/mcp-venus-os
cd mcp-venus-os
uv sync
```
### Prerequisites (Cerbo GX)
1. Settings → Services → **MQTT Gateway**, mode = *Local network* (listens on LAN :1883)
2. Note the **portal ID** shown on the MQTT Gateway page (also `com.victronenergy.system/Serial`)
3. Verify: `mosquitto_sub -h <cerbo-ip> -t 'N/<portalId>/system/#' -v` returns telemetry
### Configuration
Create a `.env` file from [`.env.sample`](.env.sample) or set environment variables:
```bash
TRANSPORT_BACKEND=mqtt # mqtt (default) | dbus (on-device only)
SERVER_TRANSPORT=stdio # stdio (default) | http
# MQTT — Venus OS gateway on the Cerbo
MQTT_HOST=<cerbo-ip>
MQTT_PORT=1883
MQTT_PORTAL_ID=<venus-portal-id>
MQTT_STALE_AFTER_SECONDS=60
# Safety
SAFETY_REQUIRE_CONFIRMATION=true
SAFETY_MAX_CHARGE_CURRENT=100
SAFETY_MAX_DISCHARGE_CURRENT=100
SAFETY_MIN_SOC_LIMIT=10
SAFETY_MAX_SOC_LIMIT=100
SAFETY_ALLOWED_MODES=on,off,charger_only,inverter_only,eco
SAFETY_ENABLE_WRITES=false # MUST be true to enable any write/control tools
# HTTP mode extras
SERVER_HOST=127.0.0.1 # 0.0.0.0 inside containers
SERVER_PORT=8000
SERVER_AUTH_TOKEN= # optional bearer token for HTTP mode
```
### Running the Server
```bash
uv run mcp-venus-os # stdio (Claude Code launches this)
uv run mcp-venus-os --transport http # or SERVER_TRANSPORT=http
```
### Deployment Matrix
| Target | Transport backend | Server transport | Notes |
|--------|-------------------|------------------|-------|
| **Synology Docker (primary)** | `mqtt` → Cerbo LAN | `http` :8080 | shared endpoint for all machines on the LAN |
| macOS (fallback) | `mqtt` → Cerbo LAN | `stdio` | local process via `claude mcp add`, no NAS dependency |
| On-device (Cerbo) | `dbus` | `stdio` | legacy mode, no gateway needed |
Docker:
```bash
cp .env.sample .env # fill in cerbo IP + portal id (+ token if exposing beyond LAN)
docker compose up -d # healthcheck hits GET /mcp until the MCP endpoint answers
```
### HTTP Auth Token
HTTP mode is protected by a static bearer token (`SERVER_AUTH_TOKEN`). Without it the
server runs unauthenticated — only sensible on a trusted home LAN.
Generate and apply:
```bash
openssl rand -hex 24 # generate once
echo 'SERVER_AUTH_TOKEN=<hex>' >> .env # add to the deployment .env
docker compose up -d # restart so the container picks it up
```
Clients then send `Authorization: Bearer <token>` on every request. Unauthenticated or
wrong-token requests get `401`. For Claude Code:
```bash
claude mcp add --scope user --transport http venus-os \
http://<synology-ip>:8080/mcp \
--header "Authorization: Bearer <token>"
```
Or via the project-level [`.mcp.json`](.mcp.json), which reads the token from the
`VENUS_MCP_TOKEN` environment variable (`export VENUS_MCP_TOKEN=<hex>` before launching
Claude Code):
```json
{
"mcpServers": {
"venus-os": {
"type": "http",
"url": "http://192.168.167.25:8080/mcp",
"headers": { "Authorization": "Bearer ${VENUS_MCP_TOKEN}" }
}
}
}
```
### Claude Code Registration
Primary (shared Synology HTTP endpoint — see above). Fallback: launch the server locally
so it works even when the NAS is down:
```bash
claude mcp add --scope user venus-os \
-e TRANSPORT_BACKEND=mqtt -e MQTT_HOST=<cerbo-ip> -e MQTT_PORTAL_ID=<id> \
-- uv --directory /path/to/mcp-venus-os run mcp-venus-os
```
DSM notes (deployed at `/volume1/docker/mcp-venus-os/`):
The 2026-09-12 audit found this directory still present, but no MCP container
in the NAS Docker inventory, including stopped containers. Treat the instructions
below as the saved deployment layout, not evidence of a currently running
endpoint. Verify the existing configuration and host before enabling it again.
- Plain `docker compose` (full path `/usr/local/bin/docker`) works fine; Container Manager is not required.
- Host port 8000 is taken by Portainer on typical DSM installs — remap in the compose `ports:` (e.g. `"8080:8000"`).
- SFTP/scp may be disabled; copy files via `ssh ... 'cat > file'`.
- The `.env` (portal id, token) lives only on the NAS, mode 600.
### Container Images
The reviewed publication mapping is `ghcr.io/4alvit/mcp-venus-os`. Candidate builds produce an OCI archive; approved stable bytes are promoted separately to the registry without rebuilding. Follow the [operator runbook](docs/release-workflow.md) for the versioned tag and optional `latest` update. The former main/tag-triggered GHCR and Docker Hub publishers are retired.
## Available Tools
### Read Tools
| Tool | Description |
|------|-------------|
| `get_battery_soc` | Battery SoC, voltage, current, power, temperature (+ `stale`, `age_seconds`) |
| `get_pv_power` | PV/solar charger power, voltage, current, yields (power falls back to V×I) |
| `get_grid_status` | Grid power, voltage, current, frequency from the `system/0` aggregates |
| `get_inverter_status` | Inverter mode, state, AC/DC power, temperature |
| `list_devices` | Devices discovered from received MQTT topics |
### Write Tools (Requires Confirmation)
| Tool | Writes to | Notes |
|------|-----------|-------|
| `set_inverter_mode` | `W/…/vebus/<instance>/Mode` | mode name → enum code via per-device table; unknown combos rejected before publishing |
| `set_charge_current_limit` | `W/…/vebus/<instance>/Dc/0/MaxChargeCurrent` | Amps |
| `set_soc_limit` | `W/…/battery/<instance>/SocLimit` | % — confirm exact BMS path on target battery |
### MQTT Tools
| Tool | Description |
|------|-------------|
| `mqtt_connect` | Connect to the Cerbo gateway and prime the read cache |
| `mqtt_disconnect` | Disconnect; cancels all write keepalives |
| `mqtt_subscribe` | Stub — reports "not yet implemented" rather than pretending success |
### Conditional Tool Groups (context-friendly)
Tools are registered **only when their service is present**, so installations
without them never pay tool-schema context:
| Group | Detected via | Tools |
|-------|--------------|-------|
| `control` | `inverter/state` topic ([inverter-control](https://github.com/4alvit/inverter-control)) | `get_control_state()` — grid, per-battery detail, MPPT breakdown, tasmota, EV, water level, booleans, inverter state/setpoint in one JSON |
| `pump` | `tank/<n>/…` topics (dbus-pump) | `get_tank_level(instance=0)` |
| `ssh` | `SSH_PASSWORD`/`SSH_KEY_PATH` set | Cerbo management toolkit (below) |
Multi-instance reads: `instance=0` → `{"readings": [...], "total_power": …}` for
every device of the type; explicit `instance=N` → single dict.
### Cerbo SSH Management
When SSH credentials are configured, these register alongside the broker-detected
groups; 🔒 = confirmation-gated:
| Tool | Purpose |
|------|---------|
| `cerbo_ssh_available` / `cerbo_version` / `cerbo_ip` | reachability, firmware version, addresses |
| `cerbo_check_updates` | firmware dry run |
| 🔒 `cerbo_firmware_update` | download + apply firmware |
| 🔒 `cerbo_enable_ssh` | set root password (stdin→chpasswd) |
| `setuphelper_status` | SetupHelper + installed packages |
| 🔒 `setuphelper_install_package(package, repo)` / 🔒 `setuphelper_remove_package(package)` | SetupHelper package lifecycle |
| 🔒 `cerbo_ssh_exec(command)` | arbitrary command, output capped |
```bash
# .env (local/stdio runs)
SSH_HOST= # defaults to MQTT_HOST
SSH_USER=root
SSH_KEY_PATH=~/.ssh/id_ed25519 # preferred…
# SSH_PASSWORD= # …or password
CERBO_ROOT_PASSWORD= # used by cerbo_enable_ssh when not passed
```
Docker deployments mount the key instead of passing secrets through `.env`:
```bash
mkdir keys && cp ~/.ssh/<cerbo-key> keys/cerbo_rsa
chown 999:999 keys/cerbo_rsa # uid of the container's app user
chmod 600 keys/cerbo_rsa # compose already mounts ./keys:/app/keys:ro
# compose sets SSH_KEY_PATH=/app/keys/cerbo_rsa; remove those lines to use
# SSH_PASSWORD from .env instead
docker compose up -d
```
To bootstrap access on a fresh Cerbo: GUI → Settings → General → set the root
password once (`cerbo_enable_ssh` automates it from then on).
Clients can discover the live surface at runtime via the MCP resource
**`venus-os://capabilities`** (also summarized in server instructions); full
reference in [docs/CAPABILITIES.md](docs/CAPABILITIES.md).
## MQTT Topic Map
The server speaks the Venus OS **MQTT-Gateway** protocol:
```
N/<portalId>/<type>/<instance>/<Path> reads (published by Venus)
W/<portalId>/<type>/<instance>/<Path> writes (published by us)
W/<portalId>/<type>/<instance>/<Path>/Keepalive empty payload every 50s while a written value must stay active
R/<portalId> request full re-publish
inverter/state inverter-control aggregate
tank/<n>/Level dbus-pump tank level
```
- Reads: on connect we subscribe `N/<portalId>/#` and cache the last value per
topic with its receive time; tool output carries `stale` + `age_seconds`
(threshold `MQTT_STALE_AFTER_SECONDS`, default 60).
- Writes: value published as JSON to `W/…`; Venus expires writes unless
`<Path>/Keepalive` receives an empty payload at least every 60s — we send
every 50s and cancel all keepalives on disconnect/shutdown.
- Verification: after each write the matching `N/…` topic is polled for up to
5s (`WRITE_VERIFY_TIMEOUT_S`); timeout → explicit error, never silent success.
## Safety Model
Defense runs in order, before any publish:
1. **Confirmation gate** (`SAFETY_REQUIRE_CONFIRMATION=true`): first call without
`confirmed=true` returns a confirmation prompt instead of writing.
2. **Hard limits**: charge/discharge current ≤ configured maxima; SoC limits
clamped to `[SAFETY_MIN_SOC_LIMIT, SAFETY_MAX_SOC_LIMIT]`; inverter modes
restricted to `SAFETY_ALLOWED_MODES`.
3. **Mode enum mapping**: only modes with a known device-type enum code reach
the wire; anything else is rejected pre-publish.
4. **Read-back verification** closes the loop — an unacknowledged write is
reported as failed.
Known caveats: vebus/inverter/solarcharger Mode enum tables come from Victron's
documented enums but should be sanity-checked against your firmware before
relying on non-default modes; the exact SoC-limit path depends on the battery
BMS. Also note that *acceptance ≠ persistence*: when another service owns a
path (e.g. a BMS driver continuously asserting `/Dc/0/MaxChargeCurrent`), Venus
acknowledges and echoes the written value but re-applies its own within seconds —
verified live, where a 45 A write to a BMS-owned 52 A limit echoed successfully
and snapped back ~3 s later despite keepalives. The tool reports acceptance;
whether the value sticks depends on which service owns the item.
Configuration options:
- `SAFETY_REQUIRE_CONFIRMATION` - Require confirmation for write operations (default: true)
- `SAFETY_MAX_CHARGE_CURRENT` - Maximum allowed charge current in Amps (default: 100)
- `SAFETY_MAX_DISCHARGE_CURRENT` - Maximum allowed discharge current in Amps (default: 100)
- `SAFETY_MIN_SOC_LIMIT` - Minimum allowed SoC limit % (default: 10)
- `SAFETY_MAX_SOC_LIMIT` - Maximum allowed SoC limit % (default: 100)
- `SAFETY_ALLOWED_MODES` - Comma-separated list of allowed inverter modes
## Architecture
```mermaid
graph TD
subgraph "Venus OS Hardware"
VOS[Venus OS / Cerbo GX]
DBUS[(D-Bus System Bus)]
MQTT_BROKER[(MQTT Broker)]
end
subgraph "MCP Server (mcp-venus-os)"
MCP[FastMCP Server]
DBUS_CLIENT[D-Bus Client]
MQTT_CLIENT[MQTT Client]
SAFETY[Safety Validator]
TOOLS[MCP Tools]
end
subgraph "Clients"
CLAUDE[Claude Desktop]
OTHER[Other MCP Clients]
end
VOS --> DBUS
VOS --> MQTT_BROKER
DBUS --> DBUS_CLIENT
MQTT_BROKER --> MQTT_CLIENT
DBUS_CLIENT --> TOOLS
MQTT_CLIENT --> TOOLS
SAFETY --> TOOLS
TOOLS --> MCP
MCP -.->|stdio/JSON-RPC| CLAUDE
MCP -.->|stdio/JSON-RPC| OTHER
```
## Development
```bash
# Install dev dependencies
uv sync --dev
# Run linter
uv run ruff check src/
# Run type checker
uv run mypy src/
# Run tests
uv run pytest
```
## License
MIT License - see LICENSE file for details.
TDQS
Scored across 11 tools
Each tool targets a distinct device metric, setting, or MQTT lifecycle action. Read, write, and connection verbs create clear boundaries, and no two tools appear to perform the same operation.
Tool names follow a consistent snake_case verb_noun pattern: get_* for reads, set_* for writes, and mqtt_* for connection controls. list_devices is the only slight deviation but remains conventional and predictable.
With 11 tools, the server is well-scoped for a Victron monitoring and control interface. The count is substantial enough to cover core operations without feeling bloated or redundant.
The main monitoring and control surface is solid, but there is no direct load/consumption tool, which is a notable gap for energy management. Additionally, mqtt_subscribe is explicitly unimplemented, creating a dead end for real-time streaming workflows.