Skip to main content
Glama
sigenergy-mcp

sigenergy-mcp

by aydho

sigenergy-mcp

An MCP server for monitoring and controlling a Sigenergy energy system (SigenStor, Sigen hybrid inverters) locally over Modbus TCP — no cloud account needed.

It exposes live PV, battery, grid and load data, inverter details and Sigen EV charger status as MCP tools, and (optionally) control tools for remote EMS mode, battery charge/discharge limits, grid export/import limits, SOC limits and EV charging.

Prerequisites

  1. Enable Modbus TCP on your Sigenergy system. In the mySigen app (installer account) go to the device → Settings → Communication → Modbus TCP Server and turn it on. Your installer can do this for you if you don't have access.

  2. Give the system a static IP / DHCP reservation on your LAN.

  3. Note the Modbus device ids: the plant (whole system, aggregated by the EMS) is 247; inverters start at 1 (multiple inverters are 1, 2, …). A Sigen AC charger normally takes id 1, so the inverter is moved to another id (e.g. 2). A DC charger answers on its inverter's id. If you're unsure, run the discover_devices tool once — it reports the ids and the settings to use.

  4. Python 3.10+.

Related MCP server: EG4 MCP Server

Install

pip install git+https://github.com/aydho/sigenergy-mcp
# or, from a checkout
pip install -e .

Or run without installing using uv:

uvx --from git+https://github.com/aydho/sigenergy-mcp sigenergy-mcp --host 192.168.1.50

Configuration

Environment variable

CLI flag

Default

Description

SIGENERGY_HOST

--host

—

IP address of the Sigenergy system (required)

SIGENERGY_PORT

--port

502

Modbus TCP port

SIGENERGY_PLANT_ID

--plant-id

247

Plant device id

SIGENERGY_INVERTER_IDS

--inverter-ids

1

Comma-separated inverter device ids

SIGENERGY_AC_CHARGER_IDS

--ac-charger-ids

—

AC charger (EVAC) device ids, usually 1

SIGENERGY_DC_CHARGER_IDS

--dc-charger-ids

—

DC charger (EVDC) ids = the inverter's id

SIGENERGY_ALLOW_WRITES

--allow-writes

false

Enable control (write) tools

SIGENERGY_AUDIT_LOG

--audit-log

—

File to append a JSON line to for every write

SIGENERGY_ACCESS_TEAM_DOMAIN

—

Cloudflare Access team (acme or acme.cloudflareaccess.com); enables Access JWT validation on HTTP

SIGENERGY_ACCESS_AUD

—

Cloudflare Access application AUD tag (required with the team domain)

SIGENERGY_HTTP_ALLOWED_HOSTS

—

Allowed Host headers for HTTP, e.g. pi.local:8000,192.168.1.20:8000

SIGENERGY_TIMEOUT

10

Modbus request timeout, seconds

SIGENERGY_RETRIES

1

Retries for a read that gets no response

SIGENERGY_MIN_REQUEST_INTERVAL

1.0

Minimum seconds between Modbus requests

SIGENERGY_HISTORY_DB

~/.local/share/sigenergy-mcp/history.db

SQLite file for recorded history (HTTP transport only); empty to disable

SIGENERGY_HISTORY_INTERVAL

60

Seconds between history samples; 0 to disable

SIGENERGY_TIMEZONE

system

IANA time zone for daily history buckets, e.g. Australia/Melbourne

SIGENERGY_LOG_LEVEL

INFO

Server log level (INFO logs each tool call and history poll)

Writes are off by default: the control tools aren't even listed to the client until you opt in.

History

When run with the HTTP transport, the server samples the plant once a minute (3 Modbus requests) into a local SQLite database:

Table

Contents

Kept

raw

Every sample

30 days

hourly

Each completed hour: avg/min/max per gauge, kWh per counter

90 days

daily

Each completed local day, same columns

Forever

Gauges are PV, battery, grid and load power and battery SOC; counters are the lifetime PV, load, battery charge/discharge, grid import/export and EV charger energy, reported as the kWh in each interval. The get_history tool reads it, choosing a resolution to suit the range and filling in the hour or day in progress.

History is only recorded by a long-lived HTTP server, never with the stdio transport: a stdio server is started for each client session, so recording there would poll the system once per open session. Run a single HTTP server per system. A stdio server's get_history still reads the database that HTTP server writes.

Request pacing

The Sigenergy Modbus protocol (section 4.2, "Interaction timeout") sets a minimum request period of 1000 ms, so the server sends one request at a time and waits at least SIGENERGY_MIN_REQUEST_INTERVAL seconds between them, including for retries and discover_devices. Nearby registers are read in a single request to keep the count down, but a tool still takes roughly one second per request it needs: get_plant_status takes about 3 s, and discover_devices about 2-4 s per id scanned. Raise the interval on a slow or busy link; lowering it below 1 s goes against the protocol.

Claude Desktop / Claude Code

{
  "mcpServers": {
    "sigenergy": {
      "command": "sigenergy-mcp",
      "env": {
        "SIGENERGY_HOST": "192.168.1.50",
        "SIGENERGY_INVERTER_IDS": "2",
        "SIGENERGY_AC_CHARGER_IDS": "1",
        "SIGENERGY_ALLOW_WRITES": "false"
      }
    }
  }
}

With Claude Code:

claude mcp add sigenergy -e SIGENERGY_HOST=192.168.1.50 -- sigenergy-mcp

HTTP transport

To run it as a long-lived network service (e.g. on a box next to the inverter), bind loopback and put an authenticating proxy in front — this is how the Mac mini deploy works, with Cloudflare Access terminating auth:

export SIGENERGY_HTTP_ALLOWED_HOSTS=pi.local:8000
sigenergy-mcp --host 192.168.1.50 --transport streamable-http --http-host 127.0.0.1 --http-port 8000
# MCP endpoint: http://127.0.0.1:8000/mcp  (reached through the proxy/tunnel)

The server refuses to listen on a non-loopback address unless Cloudflare Access JWT validation is configured (SIGENERGY_ACCESS_TEAM_DOMAIN + SIGENERGY_ACCESS_AUD), or you pass --allow-unauthenticated (only when a different authenticating proxy sits in front).

The HTTP app also answers GET /health with 200 ok, before any auth — for deploy and load-balancer health checks.

Authentication (Cloudflare Access)

The HTTP transport has no token of its own; it trusts Cloudflare Access to authenticate clients at the edge. Set both of these and every request must carry a valid Access JWT (the Cf-Access-Jwt-Assertion header Access injects, or the CF_Authorization cookie):

export SIGENERGY_ACCESS_TEAM_DOMAIN=acme            # your <team>.cloudflareaccess.com
export SIGENERGY_ACCESS_AUD=<application-AUD-tag>   # from the Access application

The server verifies the JWT's signature against your team's JWKS (https://<team>.cloudflareaccess.com/cdn-cgi/access/certs) and checks its issuer and audience, so the origin only trusts requests that came through Access. Enable Managed OAuth on the Access application and clients (Claude, MCP Inspector, …) authenticate with an interactive OAuth login — no static token or custom headers. GET /health stays open for health checks.

Deploy to the Mac mini

Pushing to main deploys to the Mac mini automatically, via the self-hosted GitHub Actions runner (labels self-hosted, mini) and .github/workflows/mac-mini.yml — the same pattern as the shopping-agent, agentic and claude-pro-proxy services. Each push rsyncs the repo to /srv/sigenergy-mcp, builds/updates a venv, pip install -e ., restarts the service and health-checks GET /health. workflow_dispatch offers deploy / restart / logs.

The service listens on 8003 (8000/8001/8002/8080 are taken) and binds 127.0.0.1; it's reached through the mini's cloudflared tunnel, so authentication is enforced at the edge rather than by the app. History recording is on (the server samples once a minute, see History).

Two files in deploy/ are the version-controlled reference:

  • deploy/sigenergy-mcp.service — the systemd unit. Install it by hand once (the runner isn't given sudo to write /etc/systemd/system):

    sudo cp /srv/sigenergy-mcp/deploy/sigenergy-mcp.service /etc/systemd/system/
    sudo systemctl daemon-reload && sudo systemctl enable --now sigenergy-mcp
  • deploy/sigenergy-mcp.env — just the PORT, rsynced on every deploy.

Connection settings and secrets go in /etc/sigenergy-mcp/env (untracked, root:andrewsaunders 0640), populated once by hand:

SIGENERGY_HOST=sigenstor.vista.aydho.com
SIGENERGY_TIMEZONE=Australia/Melbourne
SIGENERGY_HISTORY_DB=/srv/sigenergy-mcp/data/history.db
# Cloudflare Access JWT validation (see below). The server validates every request's Access JWT.
SIGENERGY_ACCESS_TEAM_DOMAIN=acme
SIGENERGY_ACCESS_AUD=<application-AUD-tag>
SIGENERGY_HTTP_ALLOWED_HOSTS=sigenergy.aydho.com

Expose it with cloudflared + Cloudflare Access (GitHub)

Add an ingress rule to the mini's /etc/cloudflared/config.yml (above the http_status:404 catch-all) and restart cloudflared:

  - hostname: sigenergy.aydho.com
    service: http://localhost:8003
cloudflared tunnel route dns 85dac53c-cdd5-4122-8fc5-47c0736130c8 sigenergy.aydho.com
sudo systemctl restart cloudflared

Then protect sigenergy.aydho.com in the Cloudflare Zero Trust → Access controls dashboard:

  1. Add the application and attach your identity provider. The purpose-built path is AI controls → MCP servers → Add an MCP server (HTTP URL https://sigenergy.aydho.com/mcp); a self-hosted application for the hostname works too.

  2. Add an Allow policy that includes your GitHub identity (your account/email). This is the interactive browser sign-in.

  3. Enable Managed OAuth on the application (⋯ → Edit → Advanced settings → Managed OAuth, with dynamic client registration on). Access then runs the OAuth login that MCP clients like Claude need — no static token or custom headers. Managed OAuth forwards a signed JWT to the origin, which this server validates via SIGENERGY_ACCESS_TEAM_DOMAIN + SIGENERGY_ACCESS_AUD (copy the AUD tag from the application's settings).

Add it in Claude (chat: Settings → Connectors → Add custom connector; or the CLI below) with just the URL — the OAuth flow handles sign-in:

claude mcp add --transport http sigenergy https://sigenergy.aydho.com/mcp

For a non-interactive CLI client you can instead create a service token (Access → Service Auth), add a Service Auth policy that includes it, and pass its two headers — Access still forwards a valid JWT that the server validates:

claude mcp add --transport http sigenergy https://sigenergy.aydho.com/mcp \
  --header "CF-Access-Client-Id: <id>.access" \
  --header "CF-Access-Client-Secret: <secret>"

Security

Modbus TCP itself has no authentication or encryption. Anything that can reach the Sigenergy system on port 502 can read every register and change its settings; the device id is just an address, not a secret. This server can't change that, so:

  • Never port-forward 502 to the inverter. Keep it on your LAN, ideally on its own VLAN/IoT network with firewall rules so only the machine running this server (or Home Assistant) can reach port 502.

  • SIGENERGY_ALLOW_WRITES only controls what this server will do. It doesn't stop other devices on your network writing to the inverter directly.

What the server does to protect its own interface:

  • Read-only by default. Without SIGENERGY_ALLOW_WRITES=true the control tools are removed from the server, so a client can't see or call them. Every write is still range- and enum-checked. Tools that write several registers report which writes already took effect if a later one fails. Plant and inverter start/stop can't be written at all.

  • Audit log. Set SIGENERGY_AUDIT_LOG=~/sigenergy-audit.jsonl to record every write attempt (time, device, register, previous value, new value, success or error), so you can see and undo what was changed.

  • HTTP transport. It binds to 127.0.0.1 by default, with Host/Origin checks against DNS rebinding, and refuses a non-loopback bind unless authentication is configured. Authentication is delegated to Cloudflare Access: set SIGENERGY_ACCESS_TEAM_DOMAIN + SIGENERGY_ACCESS_AUD and every request must carry a valid Access JWT (signature checked against your team's JWKS, plus issuer and audience), so the origin only trusts traffic that came through Access. Set SIGENERGY_HTTP_ALLOWED_HOSTS to the public hostname to keep the Host/Origin checks too.

  • TLS. The server speaks plain HTTP. For HTTPS or access from outside your home, put it behind Tailscale / WireGuard or a reverse proxy such as Caddy rather than opening a port.

Tools

Monitoring (read-only)

Tool

Description

get_plant_status

PV, battery, grid and load power, SOC, EMS mode, running state, decoded active alarms

get_battery_status

SOC, SOH, power, rated/available energy, power limits, cut-off SOCs

get_grid_status

Grid power (total and per phase), on/off-grid status

get_energy_totals

kWh counters: PV today/yesterday/lifetime, load, battery charge/discharge, grid import/export

get_inverter_status

Per-inverter power, battery, energy counters, temperatures, AC phases, PV strings

get_inverter_info

Model, serial number, firmware, ratings

get_ev_charger_status

AC charger state (no vehicle / connected / charging…), power, energy, alarms; DC charger state, power, vehicle SOC, session energy

get_history

Recorded history from the local database: raw, hourly or daily rows plus kWh totals

discover_devices

Scan device ids 1..10 (or max_device_id) to find the plant, inverters and EV chargers, with suggested settings

get_control_settings

Current remote EMS settings, power limits and SOC limits

list_registers

All named registers with address, type, unit and gain

read_registers

Read named registers

read_raw_registers

Read any address (for registers not in the map)

Control (requires SIGENERGY_ALLOW_WRITES=true)

Tool

Description

set_remote_ems_mode

Enable remote EMS and set mode (standby, self-consumption, command charge/discharge…)

set_remote_ems_enabled

Enable/disable remote EMS (disable to hand control back to the built-in EMS)

set_battery_power_limits

Max charge / discharge power (kW)

set_grid_limits

Max grid export / import (kW)

set_pv_power_limit

Limit PV output (kW)

set_soc_limits

Backup SOC, charge cut-off and discharge cut-off (%)

start_ev_charging / stop_ev_charging

Start or stop charging on the AC or DC charger

set_ac_charger_current

AC charging current (A), checked against the charger's rated and breaker current

set_dc_charger_power_limits

DC charger max charge / discharge (V2G) power (kW)

write_register

Write any writable register in the map by name (except start/stop registers)

Sign conventions

  • grid_active_power > 0 = importing from the grid, < 0 = exporting.

  • ess_power > 0 = battery charging, < 0 = discharging.

  • load_power is read from the system's load register (30282) on newer firmware, otherwise derived as plant AC output + grid power + third-party PV − AC charger power (so it excludes EV charging).

Example: force-charge the battery from the grid at 5 kW

  1. set_remote_ems_mode(mode=3) — command charging (grid first)

  2. set_battery_power_limits(max_charge_kw=5)

  3. Later, set_remote_ems_enabled(false) to return to normal operation.

Register map

The register map lives in src/sigenergy_mcp/registers.py and follows the Sigenergy Modbus Protocol (V2.x). Available registers can vary with firmware version; registers the device rejects are reported per-register in an errors field instead of failing the whole call. Registers that answer "illegal data address" are remembered per device and skipped on later calls. Use read_raw_registers for anything not yet mapped.

Addresses, types and gains are cross-checked against TypQxQ/Sigenergy-Local-Modbus, the field-tested Home Assistant integration; alarm bit descriptions are adapted from it (MIT).

Development

pip install -e '.[dev]'
pytest

The tests run the MCP tools end-to-end against an in-process pymodbus simulator.

Disclaimer

Not affiliated with Sigenergy. Writing Modbus registers changes how your system operates — use the control tools at your own risk and check settings in the mySigen app afterwards.

Available Tools

12 tools
discover_devicesA
Read-only

Scan Modbus device ids to find the plant, inverters, DC chargers and AC chargers.

Probes ids 1..max_device_id in order and stops at the first id that doesn't answer. Use the result to set SIGENERGY_INVERTER_IDS, SIGENERGY_AC_CHARGER_IDS and SIGENERGY_DC_CHARGER_IDS.

Inputs: max_device_id. Returns: object

get_battery_statusB
Read-only

Battery (ESS) state for the whole plant: SOC, SOH, power, capacity, available energy and limits.

Inputs: no named parameters. Returns: object

get_control_settingsB
Read-only

Current plant control settings: remote EMS enable/mode, power limits and SOC limits.

Inputs: no named parameters. Returns: object

get_energy_totalsA
Read-only

Plant energy counters (kWh): PV, load, battery, grid import/export and EV charger totals.

These registers need Modbus protocol V2.5+ firmware; any the system doesn't support are listed in errors.

Inputs: no named parameters. Returns: object

get_ev_charger_statusA
Read-only

Status of every configured Sigen EV charger.

AC chargers (EVAC): state (IEC 61851: no vehicle / connected / charging...), charging power, energy, ratings, alarms. DC chargers (EVDC, built into a SigenStor): state, output power, vehicle SOC, session energy and duration, lifetime totals and power limits.

Inputs: no named parameters. Returns: object

get_grid_statusB
Read-only

Grid connection point: total and per-phase active/reactive power and on/off-grid status.

Inputs: no named parameters. Returns: object

get_inverter_infoA
Read-only

Static inverter information: model, serial number, firmware and ratings.

Args: inverter_id: Modbus device id of the inverter. Omit to read every configured inverter.

Inputs: inverter_id. Returns: object

get_inverter_statusA
Read-only

Detailed live data for a hybrid inverter: power, battery, temperatures, AC voltages/currents and PV strings.

Args: inverter_id: Modbus device id of the inverter. Omit to read every configured inverter.

Inputs: inverter_id. Returns: object

get_plant_statusA
Read-only

Live overview of the whole system: PV, battery, grid and load power, battery SOC, mode and active alarms.

load_power comes from the system's own load register on newer firmware; otherwise it is derived as plant AC output + grid power + third-party PV - AC EV charger power.

Inputs: no named parameters. Returns: object

list_registersB
Read-only

List the named registers known to this server for the plant or an inverter.

Inputs: scope. Returns: result

read_raw_registersA
Read-only

Read arbitrary registers by address, for registers not in the built-in map.

Args: address: Start register address as given in the Sigenergy Modbus protocol (e.g. 30014). count: Number of 16-bit words to read (1-125). device_id: Modbus device id. Defaults to the plant id (247). register_type: "input" (function 0x04) or "holding" (0x03). Defaults to input for 3xxxx, holding otherwise. data_type: Optionally decode the words as this type.

Inputs: address (required), count, data_type, device_id, +1 more. Returns: object

read_registersA
Read-only

Read named registers (see list_registers) and return scaled values with units.

Args: names: Register names, e.g. ["ess_soc", "pv_power"]. scope: "plant" (system level), "inverter", "ac_charger" or "dc_charger". device_id: Modbus device id. Defaults to the plant id (247) or the first configured device of that kind.

Inputs: names (required), scope, device_id. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.8/5.0

    Scored across 12 tools

    Disambiguation4/5

    Each tool targets a distinct subsystem (plant, grid, battery, inverter, EV charger, energy totals, registers), so boundaries are mostly clear. The only mild overlap is that get_plant_status summarizes data also exposed by the individual status tools, but descriptions clarify the live-overview vs subsystem-detail distinction.

    Naming Consistency5/5

    Names follow a consistent verb_noun snake_case pattern (get_*_status, get_*_info, list_registers, read_registers, read_raw_registers, discover_devices). The verbs (get/list/read/discover) map predictably to action type.

    Tool Count5/5

    12 tools is well within the sweet spot for a monitoring/inspection domain, and each earns its place by covering a distinct device or data access layer.

    Completeness4/5

    The read surface is thorough: live status, static info, energy counters, settings, named-register and raw-register access, device discovery, and EV charging. The notable gap is write/control operations (e.g. setting power or SOC limits) implied by get_control_settings, though read_raw_registers partially mitigates unmapped reads.

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      Connect AI assistants to Victron Energy systems to read real-time solar, battery, grid, and inverter data from your local network via Modbus TCP or MQTT.
      32
      134 npm
      7
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      MCP server for EG4 solar inverters, enabling real-time monitoring, performance analysis, battery health, alerts, maintenance insights, and historical data via natural language.
      15 PyPI
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables Entra-authorized monitoring and control of an EG4 inverter via MCP, with OBO delegation and safety gates.
      MIT