Skip to main content
Glama
adambenhassen

euenergy-mcp

README.md
<p align="center">
  <img src="assets/banner.svg" alt="euenergy-mcp — European day-ahead electricity prices, a read-only MCP server for AI agents" width="100%">
</p>

<p align="center">
  <a href="#"><img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-3776AB?logo=python&logoColor=white"></a>
  <a href="https://modelcontextprotocol.io"><img alt="MCP" src="https://img.shields.io/badge/MCP-streamable--http-22C55E"></a>
  <a href="Dockerfile"><img alt="Docker" src="https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white"></a>
  <a href="#"><img alt="Access: read-only" src="https://img.shields.io/badge/access-read--only-475569"></a>
</p>

# euenergy-mcp

A small [Model Context Protocol](https://modelcontextprotocol.io) server that wraps the
**[euenergy.live](https://euenergy.live)** day-ahead electricity price API as curated, **read-only**
tools for AI agents — hourly prices, the cheapest hours, the current price, and a cross-zone
snapshot for ~41 European bidding zones.

Day-ahead market results (sourced from ENTSO-E) are published around **midday CEST** for the next
delivery day and are static thereafter — so the data only changes about once per day. The server
caches upstream responses with a long TTL (default 1 hour) keyed by endpoint + params.

## Features

- **Six read-only tools** covering the whole API surface: zones, prices (today/tomorrow), the
  current hour, cheapest hours, a cross-zone summary, and a historical date range.
- **EUR/MWh and EUR/kWh** — every price is surfaced as the raw `price_eur_mwh` (exactly as
  published) *and* a `price_eur_kwh` convenience value (`price / 1000`).
- **Never fakes a zero** — a missing or unparseable upstream price/load is surfaced as `null` with a
  `warnings` entry and `partial: true`, never silently coerced to `0`.
- **Honest about time** — hour timestamps are passed through as **UTC**, never silently shifted (see
  the timezone caveat below).
- **Long-TTL cache** keyed by zone + date, appropriate for once-a-day data.
- **Structured errors** — every tool returns `{"error": …, "error_type": …}` instead of throwing
  across the tool boundary.

## Tools

| Tool | What it answers |
|------|-----------------|
| `list_zones()` | The ≈41 bidding zones: `code`, ENTSO-E `eic`, `country`, `label`, `resolution`, `currency`, `lat`/`lon`. |
| `get_prices(zone, day?)` | Hourly day-ahead prices for a zone. `day` = `today` (default) or `tomorrow`. |
| `get_current_price(zone)` | The price for the current UTC hour (and the next hour). |
| `cheapest_hours(zone, hours?)` | The cheapest individual hours **today** (default 4; upstream clamps to 24) — for load-shifting. |
| `price_summary()` | Cross-zone snapshot: per-zone average price, average load (MW), and day-over-day change. |
| `price_history(zone, date_from, date_to)` | Hourly prices over a date range (`YYYY-MM-DD`); pass equal dates for a single past day. |

All prices are **EUR/MWh** on the wire (plus a `price_eur_kwh` convenience value). All timestamps and
day boundaries are **UTC**. The server is strictly read-only — no write/control endpoints exist.

## Prerequisites

- A **free euenergy.live API token**, minted with your email:
  ```bash
  curl -X POST 'https://euenergy.live/api/v1/keys?email=you@example.com&label=euenergy-mcp'
  ```
  The token is returned/emailed; pass it as a Bearer token (the server does this for you).
  When using this data publicly, attribute *"Data via euenergy.live (CC-BY-4.0)"*.
- [Docker](https://docs.docker.com/get-docker/) (recommended) **or** Python 3.10+.

## Configuration

Configuration is entirely via environment variables:

| Variable | Required | Default | Description |
|----------|:--------:|---------|-------------|
| `EUENERGY_TOKEN` | ✅ | — | Your euenergy.live API token. |
| `EUENERGY_BASE_URL` | | `https://euenergy.live/api/v1` | API base URL (override only for testing/proxying). |
| `EUENERGY_TIMEOUT` | | `20` | HTTP timeout, seconds. |
| `EUENERGY_CACHE_TTL` | | `3600` | Response cache TTL, seconds. Long by design — data changes ~once/day. |
| `MCP_PORT` | | `3000` | Port the server listens on. |
| `TZ` | | `UTC` | Only affects log readability — prices and timestamps are always UTC. |

The token is read **lazily**: importing the module never requires it; only making a request does.
A missing token fails fast with a clear message.

Put the secret in a `secret.env` file (git-ignored) for Docker Compose:

```dotenv
EUENERGY_TOKEN=your-euenergy-token
```

## Running

### Docker Compose (recommended)

```bash
docker compose up -d --build
```

The MCP endpoint is then available at **`http://localhost:3000/mcp`**. Override the host port with
`HOST_PORT`:

```bash
HOST_PORT=8080 docker compose up -d --build
```

### Docker

```bash
docker build -t euenergy-mcp .
docker run --rm -p 3000:3000 --env-file secret.env euenergy-mcp
```

### Local (Python)

```bash
pip install -r requirements.txt
export EUENERGY_TOKEN=your-euenergy-token
python server.py
```

## Connecting an MCP client

The server speaks **streamable HTTP**. Point any MCP client at the `/mcp` endpoint:

```json
{
  "mcpServers": {
    "euenergy": {
      "url": "http://localhost:3000/mcp"
    }
  }
}
```

## How it works

```
MCP client ──HTTP /mcp──> euenergy MCP (FastMCP) ──> euenergy.live API (v1)
                                                     GET /zones
                                                     GET /summary/latest
                                                     GET /prices/{today,tomorrow}
                                                     GET /prices?zone&from&to
                                                     GET /cheapest-hours
```

## Units & timezone caveats

> [!IMPORTANT]
> **EUR/MWh → EUR/kWh.** Prices are published in **EUR/MWh**. Each tool returns the raw
> `price_eur_mwh` (the source of truth, unrounded) plus a `price_eur_kwh` convenience value
> (`price / 1000`, rounded to 5 decimal places, banker's rounding). To compare against a home
> electricity bill, use the kWh value.

> [!IMPORTANT]
> **Everything is UTC.** Hour timestamps look like `2026-06-22T10:00:00.000Z` and the API's "day"
> is a **UTC calendar day** (00:00Z–23:00Z). The day-ahead market itself clears in **CET/CEST**, so a
> zone's *local* midnight does **not** line up with hour 0 of the returned day. Timestamps are
> surfaced exactly as received — never silently converted. If you need local-hour reasoning, convert
> from the UTC `ts` yourself using the zone's offset (CET = UTC+1, CEST = UTC+2).

## Development

```bash
pip install -r requirements.txt pytest
pytest -q                                  # network-free unit tests
EUENERGY_TOKEN=... python live_smoke.py    # paced end-to-end smoke test against the real API
```

`live_smoke.py` exercises every tool and underlying endpoint against the live API and exits non-zero
if any call fails.

## Notes & limitations

- **Read-only** by design — no writes, no control endpoints.
- `cheapest_hours` and `get_current_price` cover **today** only; use `get_prices(zone, "tomorrow")`
  or `price_history` for other days.
- `price_change_pct` in `price_summary` is the fractional change vs the previous published day,
  **as provided by the API** (e.g. `0.5 ≈ +50%`) — passed through verbatim.
- Data is **CC-BY-4.0**: attribute *"Data via euenergy.live (CC-BY-4.0)"* when publishing it.
- Not affiliated with euenergy.live.