Skip to main content
Glama
vasilyevstan

solar-mcp

by vasilyevstan

solar-mcp and solar-stats

Two independent stdio MCP servers: solar-mcp connects locally to an inverter; solar-stats reads historical plant production from an authenticated FusionSolar browser session. The cloud-history server never connects to or controls the inverter.

solar-mcp: local inverter

A local stdio Model Context Protocol server for Huawei SUN2000 inverter telemetry, read-only by default, with optional percentage and fixed-watt generation-cap control.

Tool

Availability

Purpose

get_solar_status

Always

Generation, energy counters, device status, and freshness

get_generation_limit

Always

Fresh mode-aware percentage or watt-cap readbacks

set_generation_limit

Explicit local opt-in

Change the percentage power cap with expected-value and readback checks

set_power_limit

Explicit local opt-in

Select a percentage or whole-watt cap, including guarded mode changes and restoration

Generation is not household consumption, grid export, or available surplus. There are no arbitrary-register, start/stop, grid-code, reactive-power, discovery, HTTP, or scheduling tools. Installer login and one session heartbeat are used only for an explicit cap change when an installer password has been configured.

Related MCP server: solax-cloud-mcp

Requirements

  • Python 3.12 or newer and uv.

  • A Huawei SUN2000 inverter reachable through a compatible local Modbus TCP connection, with Modbus access enabled by the device owner/installer.

  • The inverter's unit ID. The verified SUN2000-8KTL-M0 / SDongleA-05 installation uses unit 1, not the library's unit-0 default.

Telemetry, percentage-cap writes/restoration, and fixed-watt writes/readbacks on that inverter/dongle combination have been exercised on hardware. On-grid output curtailment following a fixed-watt command has been observed; instantaneous power can fluctuate around the setting rather than equal it exactly. Other SUN2000 models must provide the same registers; missing data is an error rather than an invented reading. Software updates and commissioning are outside this server's scope. Do not start it during an active firmware update.

Run one server instance per inverter endpoint, not separate instances in several MCP clients. Close competing commissioning sessions when troubleshooting. The server serializes its own requests, but does not coordinate other processes. Keep Modbus on a trusted LAN; never expose port 502 to the internet.

Install and configure

git clone https://github.com/vasilyevstan/solar.git
cd solar
uv sync --frozen --no-dev
cp .env.example .env

Edit the ignored .env with your actual address. 192.0.2.10 in the example is a documentation placeholder, not a discoverable device.

Setting

Default

Meaning

SOLAR_INVERTER_HOST

Required

Hostname or LAN IP, without a URL scheme

SOLAR_INVERTER_PORT

502

Modbus TCP port

SOLAR_INVERTER_UNIT_ID

1

Inverter unit, between 0 and 247

SOLAR_EXPECTED_SERIAL

Unset

Optional expected inverter serial; kept local and never returned by the tool

SOLAR_ALLOW_CONTROL

0

Set exactly 1 to register the cap setters; requires SOLAR_EXPECTED_SERIAL

SOLAR_CONTROL_LOG

~/.local/state/solar-mcp/generation-caps.jsonl

Private, durable cap-change journal; overrides must be absolute paths

SOLAR_INSTALLER_PASSWORD

Unset

Optional local installer password, used only during an explicit cap change

The server reads environment variables, not .env files itself. For a direct stdio launch, let uv load the local file:

uv run --frozen --no-dev --env-file .env solar-mcp

This starts an MCP protocol process, not an interactive dashboard. Use an MCP client to send requests. Host, port, and unit can alternatively be set with --inverter-host, --inverter-port, and --unit-id; command-line values take precedence over environment variables.

MCP client configuration

Use the following stdio entry in your client's MCP server configuration, replacing the directory and placeholder IP. The exact outer configuration file is client-specific. Ensure uv is on the client's executable search path.

{
  "mcpServers": {
    "solar-mcp": {
      "command": "uv",
      "args": [
        "run", "--directory", "/absolute/path/to/solar",
        "--frozen", "--no-dev", "solar-mcp"
      ],
      "env": {
        "SOLAR_INVERTER_HOST": "192.0.2.10",
        "SOLAR_INVERTER_UNIT_ID": "1"
      }
    }
  }
}

Tool result

get_solar_status takes no arguments. It returns structured data with:

Field

Meaning

model, software_version

Inverter model and running inverter firmware, not dongle firmware

generation_w

Signed active power in watts; zero is a valid observation

daily_yield_kwh

Daily inverter generation counter

lifetime_yield_kwh

Accumulated inverter generation counter

device_status_code, device_status

Raw operating code and description

observed_at

UTC time at the start of the telemetry read sequence

age_seconds

Age since that observation, measured with a monotonic clock

from_cache

Whether this call reused an observation less than 30 seconds old

source

Always huawei_modbus

Readings are sequential, not an atomic meter snapshot. The daily counter follows the inverter's own day boundary; the observation timestamp is UTC. The server does not persist telemetry or contact FusionSolar.

Successful readings are cached on demand for less than 30 seconds. Cache hits do not move the observation timestamp. There is no background polling. A known disconnected connection invalidates cache use. Call again after the cache window for another live observation.

Each new connection waits one second before reading and verifies the model and serial. The first accepted identity is retained for the process lifetime, so a different device after reconnect is rejected. Set SOLAR_EXPECTED_SERIAL to enforce that identity across process restarts as well.

Connection/response timeouts are 10 seconds. A tool call, including waiting for another request, has a 30-second deadline, plus at most two seconds for connection cleanup. There are no hidden reconnect/retry loops: after a failed read, the connection is closed and the next tool call makes one fresh attempt.

Failures produce an MCP isError response, never successful stale values or fabricated zeroes. Invalid register sentinels and missing/unsupported data are errors. Unknown operating codes are preserved and explicitly labelled unknown. Logs go to stderr; stdout is reserved for MCP messages.

Generation caps: power, not energy

A cap limits instantaneous output power (W or kW), not daily energy (kWh). For example, output held at 4 kW for one hour generates 4 kWh; at 2 kW for half an hour it generates 1 kWh. A cap cannot force more production than sunlight makes available and is not a household export limit.

Supported controls are percentage mode at 40125 (0.1% steps) and fixed-watt mode at 40126 (one unsigned 32-bit value, 1 W steps). A percentage uses the inverter's own power reference; it is not converted into promised watts. Whole-watt targets must be between zero and the freshly read hardware Pmax at 30075. The server does not change Pmax or override other scheduling modes. The active mode/value/command block at 35300-35303 is always read together.

get_generation_limit has no arguments and never uses a cache. Its mode is percent or watts; the corresponding configured/active fields are percent/active_percent or watts/active_watts. Fields in the other unit are null. Results also contain observed_at (UTC) and control_enabled. Unsupported mode/command combinations and invalid register values are explicit errors. A stored cap and its active adjustment can differ.

Enable and use control

Control remains off in the example configuration. To opt in, set the following in your private environment and restart the MCP:

SOLAR_ALLOW_CONTROL=1
SOLAR_EXPECTED_SERIAL=your-inverter-serial

Keep the installer password in private local configuration only if your device requires authentication. It is never a tool argument or result, is not logged, and is not used by telemetry or cap reads. An authentication failure is an error; the server does not guess credentials or retry failed writes.

For an explicitly requested percentage change:

  1. Call get_generation_limit and inspect both percent and active_percent.

  2. Call set_generation_limit with the requested percent, expected_current_percent from the configured readback, and expected_active_percent from the active readback. All are numeric values in 0.1% steps, from 0 to 100.

  3. Keep the result's previous_percent if you intend to restore it. Restore explicitly using the same setter with that previous percentage and both freshly read expectations.

expected_active_percent defaults to expected_current_percent for callers whose readbacks agree. When they differ, supply the actual active expectation explicitly; otherwise the setter rejects the request without writing. This also allows guarded restoration of a stored change that has not become active.

For example, only when the current cap is 100% and you actually want 50%:

{
  "percent": 50.0,
  "expected_current_percent": 100.0,
  "expected_active_percent": 100.0
}

Zero can stop generation. 100% removes this percentage restriction, not other inverter limits. A request for the already active value returns changed: false without writing or authenticating; it does not prove write permission.

Whole-watt caps and mode changes

Use set_power_limit for a whole-watt cap or an explicit transition between the two supported modes. Read get_generation_limit first. Pass its mode as expected_mode, and both configured/active values in that mode's unit as expected_current_value and expected_active_value.

For example, only when the fresh configured and active readbacks are both 100% and you actually want a 222 W maximum:

{
  "mode": "watts",
  "value": 222,
  "expected_mode": "percent",
  "expected_current_value": 100,
  "expected_active_value": 100
}

Save the result's previous mode and value for restoration. To restore a previous 100% cap after a fresh read confirms configured/active values of 222 W, call this same tool with mode: "percent", value: 100, expected_mode: "watts", and both expected values set to 222. The original set_generation_limit remains a percentage-mode-only convenience; it will not implicitly switch out of watt mode.

Unlike that convenience tool's no-op optimization, set_power_limit deliberately sends one command even when its target matches the visible state. This permits explicit reassertion/restoration when another mode's command is still pending. write_performed: true means the command was sent and its stored value verified. It does not promise constant generation or prove that physical curtailment has already occurred.

Verification and persistence

Both setters share the same serialization, identity, fresh-expectation, optional installer-login, durable-journal, and readback safeguards. They send one named Huawei-library write: function 06 to 40125 for percentages, or function 16 with two words at 40126 for watts. No separate mode register is written. They check the acknowledgment and stored target, then inspect the active adjustment up to three times, but never automatically retry a cap write.

changed: true means the stored setting changed, not that output is already limited. active_readback_matches reports whether the requested mode and active adjustment agree. For set_power_limit, configuration_verified refers to requested_mode/requested_value; limit still describes the active mode's readbacks, which may be the previous mode while a transition is pending. If it does not, the result includes a warning: the cap may be pending or overridden, and its output restriction is unconfirmed. This distinction is necessary because the two readbacks can differ while the inverter is in no-irradiation standby. Physical curtailment must be checked separately while sufficient sunlight exists. An earlier standby check changed the stored cap from 100% to 99% and restored 100%, while the active readback stayed at 100%. A later on-grid fixed-watt check verified both configured/active watt values and observed output curtailment.

Treat changes as persistent. Exiting or disconnecting does not trigger a restore. If a write times out, is cancelled, or fails verification, it may still have applied. Read the current cap before any further action; do not blindly retry or assume the original cap was restored. The private journal contains paired prepared/verified records, including previous/requested modes and values, the target register's previous value, active readback, and a request ID. verified means the configured value was verified; its active_readback_matches field records the separate active result. An unpaired prepared record has an uncertain outcome and preserves the information needed for an explicit recovery.

The journal is permission-restricted and must be writable; inability to persist intent blocks the cap write. It contains device identity, so do not publish it. The repository ignores *.jsonl files.

Do not run competing controllers, including simultaneous commissioning/cloud setting changes. Expected-value checking is not an atomic lock against other Modbus clients or FusionSolar; a later external change can override the result.

solar-stats: FusionSolar history

solar-stats exposes two read-only tools:

get_generation(start_date, end_date=None, format="json", refresh=False)
get_hourly_generation(start_date, end_date=None, format="json", refresh=False)

Dates use YYYY-MM-DD. Omitting the end date selects one day; a period is inclusive and may cross years. The default structured result includes:

  • generation_kwh: the requested period's sum, in kWh, not instantaneous kW.

  • daily: each date, its generation_kwh, source_missing, from_cache, and original source_retrieved_at.

  • missing_dates, source_complete, source/plant identity, and UTC retrieved_at (when this result was assembled).

  • retrieval_mode: stored_file, portal, or mixed, with cached_days and portal_days counting returned days from each source.

  • date_basis: "plant_report_calendar": source daily labels are not shifted to UTC.

The metric is Plant Report / PV Yield (kWh), not inverter lifetime counters, household consumption, export, or revenue.

Saved-file preference

Queries now prefer existing date records in local yearly CSVs, including genuine measured zeroes and explicitly flagged missing-value zero fills. They validate the matching metadata's plant identity, metric, units, date coverage, missing flags, total, and CSV checksum. A complete file hit does not launch Chrome, contact FusionSolar, or read Keychain credentials.

Only dates not covered by saved files are requested from the portal. Requests are grouped by the monthly reporting periods actually needed; saved measurements are retained rather than overwritten by unrelated rows returned in the same monthly report. Portal failures remain errors, never partial results or fabricated zeroes. refresh=True in MCP, or --refresh in the CLI, bypasses local files entirely when you need updated measurements, to recheck known source gaps, or to refresh a still-changing current day's generation. A stored missing-value zero remains source_missing: true; using it from a file never turns it into a confirmed measurement.

Configure SOLAR_STATS_DATA_DIR (default: state/solar-stats under the process working directory). Preferred files are generation-YEAR.csv with matching .metadata.json sidecars. If the canonical file is absent, matching partial exports such as generation-2026-jan-sep.csv are accepted; conflicting overlapping measurements are errors. An invalid file or mismatched pair fails explicitly. Use refresh to bypass it, and an explicit export to replace it when appropriate. Original files without per-date timestamps remain readable using their original retrieval timestamp; new sidecars retain per-date source timestamps.

Storage management remains deferred. A query reads existing files but does not automatically create a database, synchronize files, or rewrite exports. Use --output for a standalone snapshot. Use --output-dir for ongoing yearly files: it merges new dates into stable generation-YEAR.csv names, preserving earlier months, existing measured values when a new reading is missing, and newer measurements when an older cached result overlaps. Appends must cover intervening dates rather than inventing values for unqueried gaps. Coordinated reads/writes use a local file lock. All plant data remains private and untracked.

GitHub Secrets and private Actions fetching

For a local MCP that must not read the FusionSolar password from macOS Keychain, set SOLAR_STATS_BACKEND=github_actions. Saved-file hits remain entirely local. Uncovered dates are fetched by a dispatch-only job in a private GitHub repository; the local process never downloads the login credentials.

Copy automation/solar-stats.yml into .github/workflows/solar-stats.yml in the private automation repository. Replace both __SOLAR_SOURCE_SHA__ placeholders with the reviewed, published 40-character source commit. The workflow checks out that exact revision, uses pinned actions, has read-only repository permissions, and accepts only dates, daily/hourly granularity and a request ID. Do not enable this data-producing workflow in the public source repository.

Create these repository Actions secrets through GitHub's secret forms or gh secret set's private input prompt:

Secret

Value

FUSIONSOLAR_USERNAME

The FusionSolar login account

FUSIONSOLAR_PASSWORD

The FusionSolar password, not the Mac/Keychain unlock password

SOLAR_STATS_PLANT_URL

The configured plant page, without query parameters or tokens

SOLAR_STATS_TIMEZONE

The plant report's IANA time zone

Configure the local MCP's existing private environment:

SOLAR_STATS_BACKEND=github_actions
SOLAR_STATS_ACTIONS_REPOSITORY=your-owner/private-solar-runner
SOLAR_STATS_ACTIONS_SOURCE_SHA=the-published-40-character-source-commit
SOLAR_STATS_GH_COMMAND=/absolute/path/to/gh
SOLAR_STATS_TIMEOUT_SECONDS=600

Keep the existing plant URL, time zone and data directory for local file validation. gh must already be authenticated to GitHub with access to dispatch Actions, read private artifacts and delete consumed artifacts in that repository. The browser-profile setting is optional and unused by this backend. No FusionSolar password is stored in local configuration, and no Keychain fallback is attempted.

The local client verifies repository privacy, workflow revision, caller/run ownership, unique request ID, artifact digest, pinned source revision and report shape. Reports travel as private artifacts, are deleted after consumption, and otherwise expire after one day. Browser profiles and login state are never uploaded. Artifacts are temporary transport, not the permanent data-storage solution; CSV output and explicit yearly append behavior remain unchanged.

Each live miss uses GitHub Actions runner time; file hits do not. Concurrent portal jobs are serialized, and cancellations/timeouts remain explicit failures. Update the workflow's source pin and local SOLAR_STATS_ACTIONS_SOURCE_SHA together when deploying new source. GitHub Secrets removes the Keychain prompt from portal fetching, but cannot bypass FusionSolar MFA, CAPTCHA, denied access, or missing historical measurements.

Inside the runner only, SOLAR_STATS_LOGIN_SOURCE=environment selects SOLAR_STATS_USERNAME and SOLAR_STATS_PASSWORD injected from the secrets. Missing credentials fail before opening Chrome. The same login-origin checks, single-attempt authentication and disabled browser-debug logging apply. Fresh sign-ins can first land on the plant overview while the application initializes. The client waits for that overview to be ready and follows the configured plant's report route without repeating login or dismissing unrelated dialogs.

Browser-session setup

Two browser modes are available. The default attach mode preserves existing behavior: reuse a dedicated, signed-in Chrome session through loopback remote debugging. managed mode starts installed Google Chrome automatically, with a persistent private profile, and closes only that owned browser after the query. Managed mode is headless by default and does not expose a debugging port.

Do not use your normal browsing profile. For attach mode, keep your configured plant open and do not expose its debugging port to the network. For example, on macOS, launch a separate Chrome window:

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --user-data-dir=/absolute/private/path/solar-stats-browser \
  --remote-debugging-port=0 --remote-debugging-address=127.0.0.1 \
  "https://your-region.fusionsolar.huawei.com"

Sign in directly in that window and open your plant. Copy .env.solar-stats.example to the ignored .env.solar-stats and configure:

Setting

Meaning

SOLAR_STATS_PLANT_URL

Your authenticated plant page containing /view/station/NE=...; no query parameters, passwords, or tokens

SOLAR_STATS_PROFILE_DIR

Absolute dedicated profile path; managed mode creates it privately and requires directory mode 0700

SOLAR_STATS_TIMEOUT_SECONDS

Whole-query deadline, including waiting for the shared profile; default 300

SOLAR_STATS_DATA_DIR

Directory of private yearly CSVs and metadata; default state/solar-stats in the working directory

SOLAR_STATS_TIMEZONE

Hourly queries only: the plant report's IANA time zone, validated against the portal request

SOLAR_STATS_BROWSER_MODE

attach (default) or managed; never run both modes against the same profile concurrently

SOLAR_STATS_HEADLESS

true (default) or false, used by managed mode

SOLAR_STATS_KEYCHAIN_SERVICE

Optional macOS generic-password item name for automatic login; unset by default

For automatic startup, set SOLAR_STATS_BROWSER_MODE=managed and use a separate private profile on persistent storage. Chrome must already be installed; no browser download, desktop, scheduler, or background service is required. macOS and Linux browser sessions are supported (flock is required); the optional credential reader described below is macOS-only.

Treat the profile as sensitive: it retains normal browser session credentials. The server does not export cookies or copy another browser's authentication storage. Missing permissions, changed portal layouts, and rate limits remain explicit errors. This is a web-report integration, not an official northbound/OpenAPI account.

The adapter opens a native child tab from the authenticated plant page, retaining the browser's normal tab-scoped session without extracting it. It reads monthly reports and checks all reported pages, dates, units, and plant identity. A local profile lock serializes simultaneous solar-stats processes. Attach mode closes only its own tab and connection, never the shared Chrome browser. Managed mode restores its own profile's last session before attempting login.

Optional macOS Keychain login

In Keychain Access, select the login keychain and create a new password item named solar-stats.fusionsolar. Its account name is your FusionSolar username/email; its password is your FusionSolar password. Enter both directly in Keychain Access, not in chat, shell arguments, environment variables, or Git. Configure only this reference:

SOLAR_STATS_KEYCHAIN_SERVICE=solar-stats.fusionsolar

The reader uses the native /usr/bin/security helper. Authorize that application for this specific item through Keychain Access when prompted; do not allow all applications. That authorization trusts the helper executable, not just this Python project. The login Keychain must be accessible to the signed-in macOS account. No new Python credential dependency or plaintext fallback is used.

Queries reuse an authenticated session first. Only a recognized login page on the configured FusionSolar region triggers a Keychain read and one login attempt. The adapter follows the application's login popup as well as same-tab redirects; a login URL carrying the plant's return address is not treated as a plant tab. If a restored application tab stays blank, configured Keychain login may reopen the normal regional sign-in page once before attempting authentication. The username/password stay in local process memory and the browser login form; the helper's output is captured privately, never returned through MCP or logs. DEBUG/PWDEBUG must be unset when Keychain login is enabled to avoid browser diagnostics exposing form values.

Keychain access is bounded; denial, a locked Keychain, rejected credentials, MFA, and CAPTCHA are explicit errors, not zero generation. There are no automatic login retries. Resolve an authentication error before repeating queries. For an interactive challenge, stop managed queries, open that profile manually with the Chrome command above, and sign in normally. Close that manual browser before resuming managed queries. Challenges are never bypassed. Without the Keychain option, sign in manually and reuse the profile. Headless Linux credential provisioning is not implemented by this macOS integration.

MCP and terminal use

With no subcommand, this starts the stdio MCP:

uv run --no-sync --env-file .env.solar-stats solar-stats

For other MCP clients, use the same command with absolute project/configuration paths and enable get_generation and get_hourly_generation. This registration is independent of solar-mcp; it requires no inverter address or installer password.

Terminal query mode prints raw JSON or CSV to stdout; diagnostics go to stderr:

uv run --no-sync --env-file .env.solar-stats solar-stats query \
  --start-date 2025-01-01

uv run --no-sync --env-file .env.solar-stats solar-stats query \
  --start-date 2024-12-31 --end-date 2025-01-02

uv run --no-sync --env-file .env.solar-stats solar-stats query \
  --start-date 2025-01-01 --end-date 2025-12-31 --format csv \
  --output state/solar-stats/generation-2025.csv

uv run --no-sync --env-file .env.solar-stats solar-stats query \
  --start-date 2021-10-01 --end-date 2026-09-30 --format csv \
  --output-dir state/solar-stats

uv run --no-sync --env-file .env.solar-stats solar-stats query \
  --start-date 2025-01-01 --refresh

uv run --no-sync --env-file .env.solar-stats solar-stats query \
  --start-date 2026-07-01 --end-date 2026-07-31 --granularity hour \
  --format csv --output-dir state/solar-stats

--output-dir requires CSV format and updates a separate generation-YEAR.csv and metadata sidecar for every intersecting year. The first/last year may cover only part of that year, such as October-December 2021 or January-September 2026. The filename stays generation-2026.csv as subsequent months are added. --output writes a single result instead of printing it. CSV output also creates a .metadata.json sidecar with the source, interval, missing dates, original retrieval timestamps, and CSV checksum. Keep both together: the checksum detects an interrupted export or subsequent edits. Existing CSVs are not modified if fetching fails.

In MCP mode, format="csv" places CSV in the tool's text content and retains structured data/provenance. It does not print raw CSV into the MCP protocol.

Missing data and matrix interpretation

Missing source readings for real requested dates are deliberately returned as 0, with source_missing: true. A measured zero has source_missing: false. When any day is missing, source_complete is false and the total is a sum with missing readings treated as zero, not a verified total of all electricity actually produced. No interpolation or historical repair is done.

Authentication errors, failed requests, incomplete pagination, invalid values, or wrong-period data are not converted to zero.

The CSV columns are year,month,1,2,...,31. Rows follow chronological year/month order. A full year has January at the top and December at the bottom; partial and multi-year periods use only their intersecting months. Cells outside the requested period or on nonexistent dates remain blank. Thus 2024 has 366 numeric date cells (including February 29) and six nonexistent-date blanks; 2025 has 365 numeric date cells and seven blanks.

Exports contain private plant data. The repository excludes state/, real .env files, and vendor documentation; never commit report data, profiles, cookies, or credentials.

Hourly history

get_hourly_generation (CLI: query --granularity hour) reads PV Yield in kWh per hour, not instantaneous power or an estimate from daily totals. It uses the portal's normal By time range / Hourly statistics view, splits live periods into at most 31 days, and reads all returned pages. It validates dates, labeled units, source time zone and daylight-saving labels before accepting data. Only completed plant-calendar days are supported. Large uncached requests remain subject to the configured query deadline. With --output-dir, the CLI saves each month-sized batch before continuing, so rerunning resumes from saved dates. Explicit empty unsuccessful reports are listed as unavailable, not zero-filled periods; other failures stop the run. If any period is unavailable the command exits with an error after retaining completed batches.

Hourly data is separate: generation-hourly-YEAR.csv and its metadata sidecar. The CSV has a date column, columns 00:00 through 23:00, and an extra #2 column for a repeated clock hour when that year's time zone requires one. For example, 03:00#2 retains the second occurrence without combining its energy with the first. Nonexistent clock hours and unused repeated-hour cells are blank. Real source-missing hours are zero with an explicit metadata flag. IANA time-zone data must be available to Python; configure the zone shown by the actual plant report, not the computer's current offset.

Saved hourly measurements retain original per-hour source timestamps. Appending can fill gaps, but a missing new reading never erases a measured value, and an older result never replaces a newer measurement. Partial yearly hourly files list their actual covered_dates and unavailable_dates between the first and last saved days. An unavailable date means it is not covered by that file, not proof that its generation was zero or that the portal never stored it.

The normal query fails if any requested live range fails; it does not turn HTTP, authentication, or unsuccessful report responses into zero-hour rows. Historical hourly availability can differ from daily availability. A successful daily report does not imply that the same date's hourly measurements can be recovered, and the server never distributes a daily total into invented hourly values.

Development

uv sync --frozen --group dev
uv run --frozen pytest
uv build

Tests use fake clients and a loopback Modbus server, not physical hardware. The stdio integration tests exercise the actual SDK and Huawei library, verify schemas and errors, assert that default-mode traffic is only function-03 reads, and check exact function-06 percentage and function-16 two-word watt writes in opt-in mode. Tests also cover Pmax bounds, guarded mode transitions/reassertion, identity and expected-value checks, durable intent, permission failures, explicit restoration with differing stored/active readbacks, cancellation, serialization, and unchanged monitoring behavior.

License and dependencies

Licensed under AGPL-3.0-only; see LICENSE.

Register decoding and Modbus operations use huawei-solar (AGPLv3). The transport is composed with its tmodbus dependency so automatic reconnect cannot bypass identity checks. The official MCP Python SDK is MIT-licensed. Dependency versions are pinned and resolved in uv.lock.

Vendor documents and firmware, local settings, device identifiers, and diagnostic artifacts are not distributed here. The local docs/ directory is ignored.

Available Tools

1 tool
get_solar_statusA
Read-onlyIdempotent

Read inverter identity, software, power, energy, and status; cache for at most 30 seconds.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
modelYes
sourceNo
from_cacheYes
age_secondsYes
observed_atYes
generation_wYes
device_statusYes
daily_yield_kwhYes
software_versionYes
device_status_codeYes
lifetime_yield_kwhYes

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint, destructiveHint=false, and openWorldHint, so safety is covered. The description adds genuinely new behavioral context beyond them: a 30-second cache bound that tells the agent data may be slightly stale.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single tight sentence that front-loads the resource and payload, then appends the caching caveat. Every clause earns its place with no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a no-arg read-only tool with a full output schema and rich annotations, the description covers the remaining gap (cache staleness) that structured fields do not. Nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes no parameters, so there is nothing to document; baseline is 4. The description correctly spends no words on parameters, focusing instead on output and caching.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (Read) and resource (inverter) and enumerates exactly what is returned: identity, software, power, energy, and status. An agent can tell immediately what this tool yields; with no siblings, no differentiation is needed.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no explicit when-to-use/when-not statement, but with zero sibling tools there is no alternative to route between. The 'cache for at most 30 seconds' clause gives implicit freshness guidance, which is the closest thing to usage context here.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

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

  1. 1 tool updatev0.1.0
    • First observedget_solar_status

TDQS

A4.2/5.0

Scored across 1 tool

Disambiguation5/5

Only one tool is exposed, so there is no possibility of confusing it with another tool. Its purpose—reading solar inverter status—is clearly distinct.

Naming Consistency5/5

The single tool follows a clear snake_case verb_noun pattern: get_solar_status. With only one name, there is no inconsistency across the set.

Tool Count3/5

One tool is borderline thin for a server named solar-mcp. It may be sufficient if the server is intentionally scoped to status retrieval only, but it offers little surface for broader solar operations.

Completeness3/5

The tool covers a comprehensive current snapshot: identity, software, power, energy, and status. However, it lacks notable related operations such as historical data, fault/alert listing, device discovery, or configuration/control.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides access to Tesla vehicle telemetry data via the Tessie API, enabling real-time monitoring of battery status, charging state, climate controls, location, and other vehicle metrics through 30+ tools with intelligent caching.
    -
  • A
    license
    A
    quality
    D
    maintenance
    Provides real-time access to solar inverter data from the SolaX Cloud API, enabling queries of power output, energy yields, battery status, and grid import/export data.
    2
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    Enables AI assistants to monitor and control Growatt solar installations via the ShineServer API, including reading PV generation, load, grid import/export, battery SOC, and adjusting inverter settings.
    26
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables Claude or any MCP client to read live and historical Sunsynk inverter, battery, and solar data, plus inverter settings, through the Sunsynk Connect cloud API. It is read-only and lets users ask natural-language questions about generation, consumption, charging, and configuration.
    7
    MIT