Skip to main content
Glama
4alvit

mcp-venus-os

by 4alvit
README.md
# 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.


[![CodeQL](https://github.com/4alvit/mcp-venus-os/actions/workflows/codeql.yml/badge.svg)](https://github.com/4alvit/mcp-venus-os/actions/workflows/codeql.yml)
[![Scorecards](https://github.com/4alvit/mcp-venus-os/actions/workflows/scorecards.yml/badge.svg)](https://github.com/4alvit/mcp-venus-os/actions/workflows/scorecards.yml)
[![Dependency Review](https://github.com/4alvit/mcp-venus-os/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/4alvit/mcp-venus-os/actions/workflows/dependency-review.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![Development Status](https://img.shields.io/badge/Status-Alpha-orange.svg)]()
[![GitHub last commit](https://img.shields.io/github/last-commit/4alvit/mcp-venus-os)](https://github.com/4alvit/mcp-venus-os/commits/main)
[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/4alvit/mcp-venus-os/graphs/commit-activity)
[![Made with Python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/)
[![MCP](https://img.shields.io/badge/MCP-Model%20Context%20Protocol-blueviolet)](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

A3.7/5.0

Scored across 11 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness3/5

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.

Maintenance

ActivityActive
ResponsivenessNo issues