Skip to main content
Glama
4alvit

mcp-venus-os

by 4alvit

MCP Venus OS

CodeQL Scorecards Dependency Review License: MIT Python 3.11+ Development Status GitHub last commit Maintenance Made with Python MCP

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.

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:

pip install git+https://github.com/4alvit/mcp-venus-os

Or for development:

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 or set environment variables:

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

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:

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:

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:

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, which reads the token from the VENUS_MCP_TOKEN environment variable (export VENUS_MCP_TOKEN=<hex> before launching Claude Code):

{
  "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:

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/):

  • 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

Two registries, both multi-arch (linux/amd64 + linux/arm64):

Registry

Image

Updated on

GitHub Container Registry

ghcr.io/4alvit/mcp-venus-os:latest (+ :<sha>)

every push to main

Docker Hub

alvit/mcp-venus-os:vX.Y.Z + :latest

every v* tag push

docker pull alvit/mcp-venus-os:v0.2.0

The Docker Hub publish workflow (docker-hub-release.yml) needs repository secrets DOCKERHUB_USERNAME and DOCKERHUB_TOKEN (Docker Hub → Account Settings → Security → New Access Token, Read & Write).

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)

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

# .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:

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.

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

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

# 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.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/4alvit/mcp-venus-os'

If you have feedback or need assistance with the MCP directory API, please join our Discord server