io.github.malkreide/swiss-electricity-mcp

السجل الرسمي أخرى

BFE energy dashboard, ElCom tariffs, public consumption data

التثبيت

أضف هذا إلى إعدادات MCP في عميلك (Claude Desktop: claude_desktop_config.json، وCursor: ‏.cursor/mcp.json، ويستخدم VS Code وغيره الصيغة نفسها «mcpServers»).

PYPI package swiss-electricity-mcp
Claude Code
claude mcp add swiss-electricity-mcp -- uvx swiss-electricity-mcp==0.3.0
إعداد JSON (Claude Desktop وCursor وVS Code)
{
    "mcpServers": {
        "swiss-electricity-mcp": {
            "command": "uvx",
            "args": [
                "swiss-electricity-mcp==0.3.0"
            ]
        }
    }
}

التوثيق

مأخوذ من ملف README للمشروع على GitHub ويُحدَّث أسبوعيًا. تفتح الروابط على GitHub.

المحتويات
  1. swiss-electricity-mcp
  2. Anchor demo query
  3. What's inside
  4. Tools (12)
  5. Installation
  6. Use with Claude Desktop
  7. Cloud deployment (Streamable HTTP)
  8. Observability & configuration
  9. Architecture
  10. MCP Protocol Version
  11. Testing
  12. Known limitations
  13. Portfolio synergy
  14. Data sources & licensing
  15. Contributing
  16. Security
  17. License
  18. Author
  19. Changelog
  20. Installation

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:

SourceWhat it providesProvenance
Energiedashboard.ch (Bundesamt für Energie)National production mix, consumption forecast, storage-lake fill, consumer price indexlive_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 with SWISS_ELECTRICITY_HOST=0.0.0.0 only inside a container, where the network boundary is the container, not the host. Setting 0.0.0.0 on 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 varDefaultPurpose
SWISS_ELECTRICITY_TRANSPORTstdiostdio or streamable-http
SWISS_ELECTRICITY_HOST127.0.0.1HTTP bind host (0.0.0.0 in containers only)
SWISS_ELECTRICITY_PORT8000HTTP port
SWISS_ELECTRICITY_LOG_LEVELINFOLog 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_ENVunknowndeployment.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-mcp

    You 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)
                                          ┌────────▼──────────┐
                                          │  MCPServer (mcp)  │  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:

  1. 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.
  2. 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.
  3. Domain separation from swiss-energy-mcp: that server covers geo and infrastructure data (power plants, grid lines). swiss-electricity-mcp covers 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 of live_api / sparql / cached / weekly_dump / stale_cache_fallback.
  • retrieved_at — ISO-8601 UTC timestamp.

This makes accidental misattribution structurally impossible.

Resilience
  • Retry: up to 3 retries, i.e. at most 4 attempts. Waits grow exponentially (base 2 s / 4 s / 8 s), each jittered to 0.5–1.5× so that clients do not return in lockstep after an outage. No single wait exceeds 20 s. A Retry-After sent with 429 or 503 takes precedence (plus 0–25 %, also capped at 20 s).
  • Time budget: 25 s for the whole call, all attempts and waits together. A wait that would overrun the budget ends the call instead, so there may be fewer than 4 attempts.
  • 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.


MCP Protocol Version

This server speaks two protocol eras over the same endpoint. The client's first request on a connection decides which one applies; a later claim from the other era is refused.

EraRevisionWho reaches it
initialize handshake2024-11-05 … 2025-11-25What today's clients speak. The server answers with the revision asked for, or with the 2025-11-25 ceiling when the request asks for something newer.
Per-request envelope2026-07-28A request carrying the 2026-07-28 _meta envelope opens a modern connection.

Both revisions are pinned in tests/test_protocol_version.py and asserted against the installed SDK, so a Dependabot bump of mcp cannot move either one silently. The handshake ceiling is measured against a live initialize through the assembled ASGI stack, not read off a constant name.

Note that the SDK's LATEST_PROTOCOL_VERSION is an alias for the modern era, not for the handshake era — pinning against it alone would leave the era that current clients actually negotiate free to drift.

Update policy. When the gate fails, do not edit the constant blindly: read the spec changelog between the two revisions, verify the server still behaves, then move the constant, this section, README.de.md and CHANGELOG.md together. A spec bump is adopted only through an explicit mcp minor/major bump, recorded in CHANGELOG.md and verified against the tool-definition lock (tool-definitions.lock.json).

What the server sets natively for 2026-07-28

The SDK reaching a revision is not the same as a server speaking it. Two surfaces the SDK leaves to the server, and what happens when it is left alone:

SurfaceWhat this server setsWhat the SDK does without it
serverInfo — stamped into the _meta of every result, not just the initialize replyname, title, version, websiteUrlSubstitutes nothing. An unversioned server reports "version": "" to every caller, on both eras and both transports.
ttlMs / cacheScope on the cacheable methods (SEP-2549)300000 ms, scope public, on tools/list, server/discover, prompts/list, resources/list, resources/templates/listCacheHint() defaults to ttl_ms=0, scope="private" — the wire form of "already stale, never share". Every client then re-lists on every connection.

2026-07-28 moved serverInfo from a once-per-connection handshake footnote to a stamp on the running traffic, which is what makes the empty version worth a gate rather than a shrug.

The three empty directories carry a hint on purpose. MCPServer registers their handlers unconditionally and server/discover lists prompts and resources among its capabilities, so the surface exists on the wire — it is just empty. This server has no way to register a prompt or a resource at runtime, so it stays empty for the life of the process, which makes it the safest thing here to cache.

Both are measured off a real response rather than read back off the constructor: tests/test_server_identity.py checks each identity field separately on each era, and tests/test_cache_hints.py reads the hints out of a live client session. Each has a negative control against a bare MCPServer("kontrolle") — without it, an assertion that the SDK one day starts satisfying by itself would keep reading as proof that this server sets it.


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.

Auditing the ruff pin across the portfolio

scripts/pin_audit.py checks whether a server's own pin guards actually hold. It is not a CI gate — it needs the sibling repositories on disk — but it is worth running whenever a pin convention changes or a new server joins:

python scripts/pin_audit.py ../*-mcp

It measures black-box: prepend an ordinary second pre-commit hook with its own rev:, run the guard, read the exit code, restore the file. Two guards in the portfolio used to report that hook's version as the ruff pin, turning CI red with a number nobody had written. A positive control (misconfigure the ruff hook's own rev) separates "correctly scoped" from "never reads the file" — without it, a guard that ignores the config looks like a clean bill of health.

Where the test data comes from

The fixtures under tests/fixtures/ are recorded from the live sources and dated. Source, retrieval date, selection rule and SHA-256 for every file: tests/fixtures/PROVENANCE.md.

python scripts/record_fixtures.py   # re-record

The requests are built by the production code. The script calls ElComSparqlClient and EnergyDashboardClient and captures the answer through an httpx transport, rather than retyping the SPARQL alongside. A fixture that answers a slightly different question than the server asks proves the wrong answer — quietly, because it looks plausible. At 40 lines of SPARQL, "slightly different" is the normal case, not the exception.

Two selection rules are deliberately more than "the first N":

  • The storage-lake series runs into the future. After the last measured day come rows with a null measurement — 94 of them on the recording day. They are kept on purpose: without them, no test could show that the tool skips them.
  • What counts as a measurement is named per file, not guessed. The first version of this used "any field other than date is non-null", which is wrong: those future rows do carry values — the five-year reference curves — just no measurement.

Where a search is trimmed, count keeps its real value: it says how much is not in the file.


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-mcp or 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"
      ]
    }
  }
}

الأسئلة الشائعة

ما هو io.github.malkreide/swiss-electricity-mcp؟

BFE energy dashboard, ElCom tariffs, public consumption data

كيف أثبّت io.github.malkreide/swiss-electricity-mcp؟

يعمل على جهازك. في Claude Code: claude mcp add swiss-electricity-mcp -- uvx swiss-electricity-mcp==0.3.0. وفي العملاء الآخرين، الصق إعداد JSON من هذه الصفحة في إعدادات MCP الخاصة بهم.

هل يحتاج io.github.malkreide/swiss-electricity-mcp إلى مفتاح API؟

لا يذكر إدراجه في السجل أي مفتاح أو سر إلزامي. قد تتطلب بعض الميزات حسابًا لدى الخدمة التي يتصل بها.

هل io.github.malkreide/swiss-electricity-mcp مفتوح المصدر؟

نعم. شيفرته متاحة على GitHub بترخيص MIT.

من يصون io.github.malkreide/swiss-electricity-mcp؟

ينشره malkreide. يدرجه AI.tn لكنه لا يصونه ولا يوصي به.

من سجل MCP الرسمي. الإدراج ليس توصية: راجع شيفرة الخادم وصلاحياته قبل منحه الوصول إلى بياناتك.