swiss-electricity-mcp
MCP server for Swiss electricity data β three official sources, twelve tools, zero authentication.
π Read this in your language: π©πͺ Deutsch
Part of the Swiss Public Data MCP Portfolio β a coordinated set of MCP servers for Swiss public administration.
Anchor demo query
"How have ewz electricity tariffs for a typical school building (consumption category C3, β150'000 kWh/a) developed since 2019, and how do they compare to the Swiss median?"
A single conversation calls tariff_get_by_municipality (bfs_nr=261, category="C3") + tariff_get_median_swiss and returns a year-by-year comparison with full provenance β ready for a GeschΓ€ftsleitung slide.
Demo
What's inside
Three official Swiss data sources combined into one MCP server, each with its own dedicated tool group:
| Source | What it provides | Provenance |
|---|---|---|
| Energiedashboard.ch (Bundesamt fΓΌr Energie) | National production mix, consumption forecast, storage-lake fill, consumer price index | live_api |
| ElCom electricity-price cubes (via LINDAS SPARQL) | Tariffs per municipality, category, year, with full breakdown (energy + grid usage + KEV + Abgaben) | sparql |
| opendata.swiss + Stadt ZΓΌrich OGD (CKAN) | Dataset discovery for raw time series (e.g. quarter-hour NE5/NE7 consumption) | live_api |
No authentication required. All endpoints are public Swiss OGD.
Tools (12)
dashboard_* β Energiedashboard.ch (BFE)
dashboard_get_production_mixβ Production mix by year (TWh + %): Kernkraft, Wasserkraft, PV, Wind, thermal.dashboard_get_consumption_forecastβ Current consumption forecast + 5-day outlook + 5-year envelope.dashboard_get_storage_lakesβ Speichersee fill level (CH or per region: Wallis, Tessin, GraubΓΌnden, Zentral/Ost) β critical winter-supply indicator.dashboard_get_consumer_price_indexβ Endverbraucher-Strompreis-Index (2020-01-01 = 100).
tariff_* β ElCom (via LINDAS SPARQL)
tariff_list_categoriesβ H1βH8 (households) and C1βC7 (commercial). C3 β 150'000 kWh/a is the typical reference for school buildings.tariff_get_by_municipalityβ Tariffs for a BFS-Nr + category + year range, broken into energy / grid usage / KEV / Abgaben.tariff_get_median_swissβ National median benchmark.tariff_get_median_cantonβ Cantonal median (e.g. for Kanton ZΓΌrich).tariff_compare_municipalitiesβ Compare up to 20 municipalities side-by-side.
consumption_* β opendata.swiss + Stadt ZΓΌrich OGD
consumption_search_bfe_datasetsβ CKAN search across BFE-published datasets.consumption_search_zurichβ CKAN search across Stadt ZΓΌrich OGD (includes quarter-hour NE5/NE7 consumption).
Status
electricity_check_statusβ Liveness probe across all four upstreams (HTTP status + latency + overall-healthy flag).
Installation
From PyPI
pip install swiss-electricity-mcp
From source
git clone https://github.com/malkreide/swiss-electricity-mcp.git
cd swiss-electricity-mcp
pip install -e ".[dev]"
Use with Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"swiss-electricity": {
"command": "swiss-electricity-mcp"
}
}
}
Cloud deployment (Streamable HTTP)
SWISS_ELECTRICITY_TRANSPORT=streamable-http \
SWISS_ELECTRICITY_HOST=0.0.0.0 \
SWISS_ELECTRICITY_PORT=8000 \
swiss-electricity-mcp
Works on Render.com, Railway, Fly.io.
Host binding (security). In HTTP mode the host defaults to
127.0.0.1(loopback only). Bind to all interfaces withSWISS_ELECTRICITY_HOST=0.0.0.0only inside a container, where the network boundary is the container, not the host. Setting0.0.0.0on a developer machine exposes the server to the local network (NeighborJack).
Docker
A multi-stage Dockerfile is provided. It runs as a non-root user (UID 10001)
and sets SWISS_ELECTRICITY_HOST=0.0.0.0 explicitly for the containerised case.
docker build -t swiss-electricity-mcp .
docker run --rm -p 8000:8000 swiss-electricity-mcp
Observability & configuration
| Env var | Default | Purpose |
|---|---|---|
SWISS_ELECTRICITY_TRANSPORT | stdio | stdio or streamable-http |
SWISS_ELECTRICITY_HOST | 127.0.0.1 | HTTP bind host (0.0.0.0 in containers only) |
SWISS_ELECTRICITY_PORT | 8000 | HTTP port |
SWISS_ELECTRICITY_LOG_LEVEL | INFO | Log level (DEBUG/INFO/WARNING/ERROR) |
SWISS_ELECTRICITY_CORS_ORIGINS | (empty) | Comma-separated allowed CORS origins (browser clients); never * |
OTEL_EXPORTER_OTLP_ENDPOINT | (unset) | Enables OpenTelemetry tracing when set |
SWISS_ELECTRICITY_ENV | unknown | deployment.environment resource attribute for traces |
-
Logging is structured JSON on stderr (stdout is reserved for the stdio JSON-RPC channel). Upstream failures are logged in full server-side but masked in client-facing responses.
-
Tracing is opt-in. Install the extra and point it at a collector:
pip install "swiss-electricity-mcp[otel]" OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 swiss-electricity-mcpYou get one span per tool call (
mcp.tool.<name>) plus automatic httpx child spans for each upstream request. No argument values or PII are recorded.
Architecture
βββββββββββββββββββββββββββ MCP client (Claude etc.) βββββββββββββββββββββββββββ
β stdio or Streamable HTTP β
βββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β 12 read-only tools (annotated)
ββββββββββΌββββββββββ
β FastMCP server β egress allow-list + HTTPS gate
β + structlog/OTel β per-source TTL cache + retry
βββββ¬βββββββββ¬ββββ¬βββ
dashboard_* β tariff_* β β β consumption_*
βΌ βΌ βΌ βΌ
βββββββββββββββββββββ βββββββββββββ ββββββββββββββββ βββββββββββββββββββββββ
β Energiedashboard β β LINDAS β β opendata.swissβ β data.stadt-zuerich.chβ
β .admin.ch (BFE) β β SPARQL β β CKAN β β CKAN (OGD) β
βββββββββββββββββββββ βββββββββββββ ββββββββββββββββ βββββββββββββββββββββββ
Hybrid (live API + SPARQL + CKAN discovery), no authentication. Three reasons this is the right shape:
- Different latency profiles per source: Energiedashboard responds in ~200 ms (great live); LINDAS SPARQL is slower and occasionally returns 504 (longer timeout + 3 retries); CKAN is metadata-only and inherently safe.
- Different update cadences: Dashboard updates intraday; ElCom tariffs update once per year; OGD datasets are stable for months. Per-source TTL caching (600 s / 3600 s) reflects this.
- Domain separation from
swiss-energy-mcp: that server covers geo and infrastructure data (power plants, grid lines).swiss-electricity-mcpcovers time-series and tariffs. Both compose cleanly.
Provenance discipline
Every tool response is a Pydantic envelope carrying:
sourceβ full attribution string (e.g. "Daten: Bundesamt fΓΌr Energie (BFE)β¦").provenanceβ exactly one oflive_api/sparql/cached/weekly_dump/stale_cache_fallback.retrieved_atβ ISO-8601 UTC timestamp.
This makes accidental misattribution structurally impossible.
Resilience
- Retry: 3 attempts with exponential backoff (2 s / 4 s / 8 s).
- 5xx + 429: retried. 4xx (except 429): raised immediately (permanent client error).
- In-memory TTL cache: per-source TTLs reduce upstream load and round-trip during multi-step agent workflows.
MCP primitives β why Tools only
This server intentionally exposes only Tools, not Resources or Prompts. The
data is parametric and query-driven (a municipality BFS number, a category, a
year), which maps naturally to tool calls; there is no stable, enumerable set of
documents to expose as Resources, and no curated prompt templates to ship. If a
future use case needs, say, a fixed "national production mix" document, the
read-only dashboard_* tools are the obvious Resource-migration candidates.
Project phase
Phase 1 β read-only. All 12 tools are read-only (readOnlyHint=true) with no
write or destructive operations. Phase-transition criteria and the longer-term
plan live in docs/roadmap.md. Security posture (egress,
supply-chain, lethal-trifecta assessment) is documented in
docs/security-posture.md.
Testing
# Unit tests (mocked, fast, CI default) β tests/test_unit.py + tests/test_security.py
PYTHONPATH=src pytest -m "not live" -v
# Live tests (hits real upstreams) β tests/test_live.py
PYTHONPATH=src pytest -m live -v
Unit tests cover the contract layers: Happy (response parsing), Retry
(5xx, 429, 4xx), Timeout (network errors β clean UpstreamUnreachableError),
envelope/attribution invariants, plus security (egress allow-list, SPARQL
escaping, tool-definition lock). CI runs ruff + pytest -m "not live" on
Python 3.11β3.13.
MCP protocol version
This server is built on the official MCP Python SDK (mcp[cli]), pinned to
>=1.2.0,<2.0.0. The MCP protocol version is negotiated by the SDK at the
initialize handshake; the supported spec version tracks the pinned SDK
(currently MCP spec 2025-11-25).
Update policy: SDK updates arrive as weekly Dependabot PRs. A protocol-spec
bump is only adopted via an explicit SDK minor/major bump, recorded in
CHANGELOG.md, and verified against the tool-definition lock
(tool-definitions.lock.json).
Known limitations
- LINDAS SPARQL 504 timeouts: the LINDAS public endpoint occasionally returns 504 under load. The 3-retry policy handles transient cases; persistent unavailability surfaces as
UpstreamUnreachableError. - No historical PV/wind detail: Energiedashboard exposes only aggregated production mix at year level. For sub-yearly PV or wind, use
consumption_search_bfe_datasets. - No FHIR or smart-meter data: out of scope. Future work may add a
swiss-prosumer-mcpor similar. - Year coverage: ElCom tariff data starts in 2009. Energiedashboard mix starts in 2014.
Portfolio synergy
This server composes naturally with other portfolio servers:
- +
swiss-energy-mcpβ combine geo/asset data (power plants) with time-series and tariffs for full energy-infrastructure analysis. - +
meteoswiss-mcpβ correlate consumption forecasts with weather (temperature drives heating/cooling load). - +
fedlex-mcpβ pair tariff data with the Stromversorgungsgesetz (StromVG) for compliance/legal context. - +
zh-education-mcpβ Schulamt-relevant queries combining tariffs, school counts, infrastructure budgets.
Data sources & licensing
All upstream data is Open Government Data Switzerland (OGD-CH):
- Energiedashboard.ch Β© Bundesamt fΓΌr Energie BFE β Open data, free to use.
- ElCom / LINDAS Β© EidgenΓΆssische ElektrizitΓ€tskommission ElCom β CC BY 4.0.
- opendata.swiss Β© Various Swiss public bodies β Mostly CC0 / CC BY 4.0.
- Stadt ZΓΌrich OGD Β© Stadt ZΓΌrich β CC0.
This MCP server is MIT-licensed (see LICENSE). Always cite the original data source β the response envelope includes the proper attribution string automatically.
Contributing
See CONTRIBUTING.md.
Security
See SECURITY.md for the security policy and how to report a vulnerability.
License
MIT License β see LICENSE. The upstream data keeps the licences listed under Data sources & licensing above.
Author
Hayal Oezkan Β· github.com/malkreide
Changelog
See CHANGELOG.md.
Installation
Run via uv's uvx β no clone or manual install needed. Add to your MCP client config (mcpServers for Claude Desktop, Cursor and Windsurf; use a top-level servers key for VS Code in .vscode/mcp.json):
{
"mcpServers": {
"swiss-electricity-mcp": {
"command": "uvx",
"args": [
"swiss-electricity-mcp"
]
}
}
}