mcp-venus-os
MCP Venus OS
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_secondsper 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 successSafety 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-osOr for development:
git clone https://github.com/4alvit/mcp-venus-os
cd mcp-venus-os
uv syncPrerequisites (Cerbo GX)
Settings → Services → MQTT Gateway, mode = Local network (listens on LAN :1883)
Note the portal ID shown on the MQTT Gateway page (also
com.victronenergy.system/Serial)Verify:
mosquitto_sub -h <cerbo-ip> -t 'N/<portalId>/system/#' -vreturns 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 modeRunning the Server
uv run mcp-venus-os # stdio (Claude Code launches this)
uv run mcp-venus-os --transport http # or SERVER_TRANSPORT=httpDeployment Matrix
Target | Transport backend | Server transport | Notes |
Synology Docker (primary) |
|
| shared endpoint for all machines on the LAN |
macOS (fallback) |
|
| local process via |
On-device (Cerbo) |
|
| 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 answersHTTP 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 upClients 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-osDSM 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 |
| every push to |
Docker Hub |
| every |
docker pull alvit/mcp-venus-os:v0.2.0The 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 |
| Battery SoC, voltage, current, power, temperature (+ |
| PV/solar charger power, voltage, current, yields (power falls back to V×I) |
| Grid power, voltage, current, frequency from the |
| Inverter mode, state, AC/DC power, temperature |
| Devices discovered from received MQTT topics |
Write Tools (Requires Confirmation)
Tool | Writes to | Notes |
|
| mode name → enum code via per-device table; unknown combos rejected before publishing |
|
| Amps |
|
| % — confirm exact BMS path on target battery |
MQTT Tools
Tool | Description |
| Connect to the Cerbo gateway and prime the read cache |
| Disconnect; cancels all write keepalives |
| 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 |
|
|
|
|
|
|
|
| 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 |
| reachability, firmware version, addresses |
| firmware dry run |
🔒 | download + apply firmware |
🔒 | set root password (stdin→chpasswd) |
| SetupHelper + installed packages |
🔒 | SetupHelper package lifecycle |
🔒 | 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 passedDocker 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 -dTo 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 levelReads: on connect we subscribe
N/<portalId>/#and cache the last value per topic with its receive time; tool output carriesstale+age_seconds(thresholdMQTT_STALE_AFTER_SECONDS, default 60).Writes: value published as JSON to
W/…; Venus expires writes unless<Path>/Keepalivereceives 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:
Confirmation gate (
SAFETY_REQUIRE_CONFIRMATION=true): first call withoutconfirmed=truereturns a confirmation prompt instead of writing.Hard limits: charge/discharge current ≤ configured maxima; SoC limits clamped to
[SAFETY_MIN_SOC_LIMIT, SAFETY_MAX_SOC_LIMIT]; inverter modes restricted toSAFETY_ALLOWED_MODES.Mode enum mapping: only modes with a known device-type enum code reach the wire; anything else is rejected pre-publish.
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| OTHERDevelopment
# Install dev dependencies
uv sync --dev
# Run linter
uv run ruff check src/
# Run type checker
uv run mypy src/
# Run tests
uv run pytestLicense
MIT License - see LICENSE file for details.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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