# entsoe-mcp

> European power-market data: day-ahead & balancing prices, load, generation, flows, outages. 47 zones

- name: com.entsoemcp/entsoe-mcp
- version: 1.0.0
- connection class: R0 (autonomous)
- trust: 80/100 flags: no-repository
- quality: 88/100 (good)
- owner: not claimed
- descriptor: https://api.protogrid.dev/v1/servers/com.entsoemcp%2Fentsoe-mcp
- tools: https://api.protogrid.dev/v1/servers/com.entsoemcp%2Fentsoe-mcp/tools

**An agent can connect right now, no human step.** Remote endpoint, no authentication, reachable on the last probe.

## Connection (mcpServers)

```json
{
  "mcpServers": {
    "entsoe-mcp": {
      "type": "http",
      "url": "https://entsoemcp.com/mcp"
    }
  }
}
```


## Trust

- hygiene: 80
- liveness: 100
- freshness: 85
- provenance: 55
- drivers: +dns-namespace +website +reachable +uptime-99% ~updated-53d-ago -no-repository
- Derived from observable signals (official registry feed and our own credential-free probes); no code audit performed.

## Quality 88/100 (good)

### protocol: 75 (weight 25)

- warn `protocol.modern`: newest supported version is 2025-11-25; 2026-07-28 not supported, older versions are deprecated until 2027-07-28
- n/a `protocol.stateless`: only applies to 2026-07-28 servers
- pass `protocol.transport`: streamable HTTP
- n/a `protocol.list_ttl`: only applies to 2026-07-28 servers

### authorization: n/a (weight 15)

- n/a `auth.prm`, `auth.as_metadata`, `auth.cimd`: no remote uses OAuth
- n/a `auth.secret_in_url`: no templated URL

### tool hygiene: 93 (weight 30)

- pass `tools.descriptions`: every tool has a description
- warn `tools.description_length`: 2 tools have descriptions over 1,024 characters, which crowds the context
- pass `tools.schemas`: every tool has an object input schema
- pass `tools.annotations`: 14 of 14 tools declare readOnlyHint or destructiveHint
- pass `tools.directory_hints`: every tool declares readOnlyHint, destructiveHint, idempotentHint and openWorldHint
- pass `tools.token_cost`: about 4,230 tokens to load every tool
- pass `tools.api_dump`: tools are not a one-to-one API dump

### stability: 100 (weight 15)

- pass `stability.changes`: 5 tool changes in 30 days
- pass `stability.rug_pull`: no tool changed its meaning under the same name

### dependencies: n/a (weight 15)

- n/a `deps.known_vulns`, `deps.mcp_sdk_version`, `deps.resolvable`: no npm or PyPI package

- tools: 14, about 4,230 tokens to load them all
- tool set hash: f3a89e00e053fd6832a061d2ff60c09d
- tools last changed: 2026-10-19T11:23:38.364Z
- badge: [![protogrid quality](https://protogrid.dev/badge/com.entsoemcp/entsoe-mcp.svg)](https://protogrid.dev/servers/com.entsoemcp/entsoe-mcp)
- Checks run on what our credential-free, read-only probes observe; tools are never called and no code audit is performed.

### Tool changes

- 2026-10-19 `get_series` changed (description)
  - before: Generic time-series query for ANY registered series endpoint. One tool covers every (non-outage) endpoint in the registry, so adding a new dataset (call `list_endpoints()` to see the current 14) gets an MCP surface automatically — no new tool to learn. Argument shape adapts to the endpoint: • single-zone (day_ahead_price, actual_load, generation_per_type, …) → pass `zone="DE_LU"` • cross-zone (crossborder_flow, scheduled_exchanges, net_transfer_capacity_dayahead) → pass `from_zone="DE_LU"` AND `to_zone="FR"` • psr-dependent (generation_per_type, wind_solar_forecast, installed_generation_capacity) → optionally filter via `psr_types=["solar","wind_onshore"]` `start`/`end`: UTC by default; `start` inclusive, `end` EXCLUSIVE (for "all of April 2026" use end=2026-05-01). Pass `tz="local"` or an IANA name to interpret as wall-clock in that timezone. `aggregation`: 'raw' (default — native PT15M/PT60M per endpoint), 'hourly' (AVG over quarters → one row per hour, useful for the growing list of PT15M-stored endpoints like DE_LU day_ahead_price), 'daily', or 'monthly'. For day-ahead prices specifically the auction still clears hourly even where stored at PT15M, so AVG=any-quarter; SUM would 4× over-count. Currency: price columns keep `_eur_` in their names for stability. An endpoint whose rows carry a `currency` column (day-ahead and intraday prices, the A81/A15 capacity prices, activated_balancing_price, imbalance_prices) labels each row (PLN, HUF, RON, CZK, GBP, …) and the response `unit` follows it; `unit` is null when the window mixes currencies (`mixed_currency`) or, for activated_balancing_price and imbalance_prices, holds rows of unknown currency (`currency_unknown_rows`). Never average a price across rows with different `currency`. Split periods: when a response carries `split_periods`, some periods hold more than one row — different published series told apart by the listed columns (activated_balancing_price: `product`, e.g. CZ/SK mFRR standard A01 vs specific A02; or `currency`). Never average across them. Keep one with `filters={"product": ["A01"]}` — `filters` takes any categorical column of the endpoint (`{column: [values]}`). Process types: activated balancing prices are one endpoint per ENTSO-E processType (activated_balancing_price = A16 realised; _mfrr_scheduled = A60, _mfrr_direct = A61, _afrr_local = A68) — different prices for the same period, never to be merged. `related_series` names the siblings holding data for the zone; `stored_provenance` marks stored rows that still hold another process type's prices. Outage-family endpoints (different schema) stay on `get_outages()`.
  - after: Generic time-series query for ANY registered series endpoint. One tool covers every (non-outage) endpoint in the registry, so adding a new dataset (call `list_endpoints()` to see the current 14) gets an MCP surface automatically — no new tool to learn. Argument shape adapts to the endpoint: • single-zone (day_ahead_price, actual_load, generation_per_type, …) → pass `zone="DE_LU"` • cross-zone (crossborder_flow, scheduled_exchanges, net_transfer_capacity_dayahead) → pass `from_zone="DE_LU"` AND `to_zone="FR"` • psr-dependent (generation_per_type, wind_solar_forecast, installed_generation_capacity) → optionally filter via `psr_types=["solar","wind_onshore"]` `start`/`end`: UTC by default; `start` inclusive, `end` EXCLUSIVE (for "all of April 2026" use end=2026-05-01). Pass `tz="local"` or an IANA name to interpret as wall-clock in that timezone. `aggregation`: 'raw' (default — native PT15M/PT60M per endpoint), 'hourly' (AVG over quarters → one row per hour, useful for the growing list of PT15M-stored endpoints like DE_LU day_ahead_price), 'daily', or 'monthly'. For day-ahead prices specifically the auction still clears hourly even where stored at PT15M, so AVG=any-quarter; SUM would 4× over-count. Currency: price columns keep `_eur_` in their names for stability. An endpoint whose rows carry a `currency` column (day-ahead and intraday prices, the A81/A15 capacity prices, activated_balancing_price, imbalance_prices) labels each row (PLN, HUF, RON, CZK, GBP, …) and the response `unit` follows it; `unit` is null when the window mixes currencies (`mixed_currency`) or, for activated_balancing_price and imbalance_prices, holds rows of unknown currency (`currency_unknown_rows`). Never average a price across rows with different `currency`. Split periods: when a response carries `split_periods`, some periods hold more than one row — different published series told apart by the listed columns (activated_balancing_price: `product`, e.g. CZ/SK mFRR standard A01 vs specific A02; or `currency`). Never average across them. Keep one with `filters={"product": ["A01"]}` — `filters` takes any categorical column of the endpoint (`{column: [values]}`). Process types: activated balancing prices are one endpoint per ENTSO-E processType (activated_balancing_price = A16 realised; _mfrr_scheduled = A60, _mfrr_direct = A61, _afrr_local = A68) — different prices for the same period, never to be merged. `related_series` names the siblings holding data for the zone. Outage-family endpoints (different schema) stay on `get_outages()`.
- 2026-10-17 `get_series` changed (description)
  - before: Generic time-series query for ANY registered series endpoint. One tool covers every (non-outage) endpoint in the registry, so adding a new dataset (call `list_endpoints()` to see the current 14) gets an MCP surface automatically — no new tool to learn. Argument shape adapts to the endpoint: • single-zone (day_ahead_price, actual_load, generation_per_type, …) → pass `zone="DE_LU"` • cross-zone (crossborder_flow, scheduled_exchanges, net_transfer_capacity_dayahead) → pass `from_zone="DE_LU"` AND `to_zone="FR"` • psr-dependent (generation_per_type, wind_solar_forecast, installed_generation_capacity) → optionally filter via `psr_types=["solar","wind_onshore"]` `start`/`end`: UTC by default; `start` inclusive, `end` EXCLUSIVE (for "all of April 2026" use end=2026-05-01). Pass `tz="local"` or an IANA name to interpret as wall-clock in that timezone. `aggregation`: 'raw' (default — native PT15M/PT60M per endpoint), 'hourly' (AVG over quarters → one row per hour, useful for the growing list of PT15M-stored endpoints like DE_LU day_ahead_price), 'daily', or 'monthly'. For day-ahead prices specifically the auction still clears hourly even where stored at PT15M, so AVG=any-quarter; SUM would 4× over-count. Currency: price columns keep `_eur_` in their names for stability. An endpoint whose rows carry a `currency` column (day-ahead and intraday prices, the A81/A15 capacity prices, activated_balancing_price, imbalance_prices) labels each row (PLN, HUF, RON, CZK, GBP, …) and the response `unit` follows it; `unit` is null when the window mixes currencies (`mixed_currency`) or, for activated_balancing_price and imbalance_prices, holds rows of unknown currency (`currency_unknown_rows`). Never average a price across rows with different `currency`. Split periods: when a response carries `split_periods`, some periods hold more than one row — different published series told apart by the listed columns (activated_balancing_price: `product`, e.g. CZ/SK mFRR standard A01 vs specific A02; or `currency`). Never average across them. Keep one with `filters={"product": ["A01"]}` — `filters` takes any categorical column of the endpoint (`{column: [values]}`). Outage-family endpoints (different schema) stay on `get_outages()`.
  - after: Generic time-series query for ANY registered series endpoint. One tool covers every (non-outage) endpoint in the registry, so adding a new dataset (call `list_endpoints()` to see the current 14) gets an MCP surface automatically — no new tool to learn. Argument shape adapts to the endpoint: • single-zone (day_ahead_price, actual_load, generation_per_type, …) → pass `zone="DE_LU"` • cross-zone (crossborder_flow, scheduled_exchanges, net_transfer_capacity_dayahead) → pass `from_zone="DE_LU"` AND `to_zone="FR"` • psr-dependent (generation_per_type, wind_solar_forecast, installed_generation_capacity) → optionally filter via `psr_types=["solar","wind_onshore"]` `start`/`end`: UTC by default; `start` inclusive, `end` EXCLUSIVE (for "all of April 2026" use end=2026-05-01). Pass `tz="local"` or an IANA name to interpret as wall-clock in that timezone. `aggregation`: 'raw' (default — native PT15M/PT60M per endpoint), 'hourly' (AVG over quarters → one row per hour, useful for the growing list of PT15M-stored endpoints like DE_LU day_ahead_price), 'daily', or 'monthly'. For day-ahead prices specifically the auction still clears hourly even where stored at PT15M, so AVG=any-quarter; SUM would 4× over-count. Currency: price columns keep `_eur_` in their names for stability. An endpoint whose rows carry a `currency` column (day-ahead and intraday prices, the A81/A15 capacity prices, activated_balancing_price, imbalance_prices) labels each row (PLN, HUF, RON, CZK, GBP, …) and the response `unit` follows it; `unit` is null when the window mixes currencies (`mixed_currency`) or, for activated_balancing_price and imbalance_prices, holds rows of unknown currency (`currency_unknown_rows`). Never average a price across rows with different `currency`. Split periods: when a response carries `split_periods`, some periods hold more than one row — different published series told apart by the listed columns (activated_balancing_price: `product`, e.g. CZ/SK mFRR standard A01 vs specific A02; or `currency`). Never average across them. Keep one with `filters={"product": ["A01"]}` — `filters` takes any categorical column of the endpoint (`{column: [values]}`). Process types: activated balancing prices are one endpoint per ENTSO-E processType (activated_balancing_price = A16 realised; _mfrr_scheduled = A60, _mfrr_direct = A61, _afrr_local = A68) — different prices for the same period, never to be merged. `related_series` names the siblings holding data for the zone; `stored_provenance` marks stored rows that still hold another process type's prices. Outage-family endpoints (different schema) stay on `get_outages()`.
- 2026-10-15 `compare_zones` changed (description, inputSchema)
  - before: Compare one endpoint across multiple zones. `start`/`end` default to UTC; `start` inclusive, `end` EXCLUSIVE. `tz="local"` is rejected (zones may differ); pass an explicit IANA tz like "Europe/Berlin" if you need wall-clock alignment.
  - after: Compare one endpoint across multiple zones. `start`/`end` default to UTC; `start` inclusive, `end` EXCLUSIVE. `tz="local"` is rejected (zones may differ); pass an explicit IANA tz like "Europe/Berlin" if you need wall-clock alignment. `filters={column: [values]}` keeps only rows whose categorical column holds one of the values, in every zone (see get_series); zones whose periods hold several rows are listed under `split_periods_by_zone`.
- 2026-10-15 `get_series` changed (description, inputSchema)
  - before: Generic time-series query for ANY registered series endpoint. One tool covers every (non-outage) endpoint in the registry, so adding a new dataset (call `list_endpoints()` to see the current 14) gets an MCP surface automatically — no new tool to learn. Argument shape adapts to the endpoint: • single-zone (day_ahead_price, actual_load, generation_per_type, …) → pass `zone="DE_LU"` • cross-zone (crossborder_flow, scheduled_exchanges, net_transfer_capacity_dayahead) → pass `from_zone="DE_LU"` AND `to_zone="FR"` • psr-dependent (generation_per_type, wind_solar_forecast, installed_generation_capacity) → optionally filter via `psr_types=["solar","wind_onshore"]` `start`/`end`: UTC by default; `start` inclusive, `end` EXCLUSIVE (for "all of April 2026" use end=2026-05-01). Pass `tz="local"` or an IANA name to interpret as wall-clock in that timezone. `aggregation`: 'raw' (default — native PT15M/PT60M per endpoint), 'hourly' (AVG over quarters → one row per hour, useful for the growing list of PT15M-stored endpoints like DE_LU day_ahead_price), 'daily', or 'monthly'. For day-ahead prices specifically the auction still clears hourly even where stored at PT15M, so AVG=any-quarter; SUM would 4× over-count. Currency: price columns keep `_eur_` in their names for stability. An endpoint whose rows carry a `currency` column (day-ahead and intraday prices, the A81/A15 capacity prices, activated_balancing_price) labels each row (PLN, HUF, RON, CZK, GBP, …) and the response `unit` follows it; `unit` is null when the window mixes currencies (`mixed_currency`) or, for activated_balancing_price, holds rows of unknown currency (`currency_unknown_rows`). Never average a price across rows with different `currency`. An endpoint WITHOUT a `currency` column serves its registry unit as-is: imbalance_prices is published in the TSO's own currency for several zones (HU HUF, CZ CZK, PL PLN, RO RON) and does not label it yet. Outage-family endpoints (different schema) stay on `get_outages()`.
  - after: Generic time-series query for ANY registered series endpoint. One tool covers every (non-outage) endpoint in the registry, so adding a new dataset (call `list_endpoints()` to see the current 14) gets an MCP surface automatically — no new tool to learn. Argument shape adapts to the endpoint: • single-zone (day_ahead_price, actual_load, generation_per_type, …) → pass `zone="DE_LU"` • cross-zone (crossborder_flow, scheduled_exchanges, net_transfer_capacity_dayahead) → pass `from_zone="DE_LU"` AND `to_zone="FR"` • psr-dependent (generation_per_type, wind_solar_forecast, installed_generation_capacity) → optionally filter via `psr_types=["solar","wind_onshore"]` `start`/`end`: UTC by default; `start` inclusive, `end` EXCLUSIVE (for "all of April 2026" use end=2026-05-01). Pass `tz="local"` or an IANA name to interpret as wall-clock in that timezone. `aggregation`: 'raw' (default — native PT15M/PT60M per endpoint), 'hourly' (AVG over quarters → one row per hour, useful for the growing list of PT15M-stored endpoints like DE_LU day_ahead_price), 'daily', or 'monthly'. For day-ahead prices specifically the auction still clears hourly even where stored at PT15M, so AVG=any-quarter; SUM would 4× over-count. Currency: price columns keep `_eur_` in their names for stability. An endpoint whose rows carry a `currency` column (day-ahead and intraday prices, the A81/A15 capacity prices, activated_balancing_price, imbalance_prices) labels each row (PLN, HUF, RON, CZK, GBP, …) and the response `unit` follows it; `unit` is null when the window mixes currencies (`mixed_currency`) or, for activated_balancing_price and imbalance_prices, holds rows of unknown currency (`currency_unknown_rows`). Never average a price across rows with different `currency`. Split periods: when a response carries `split_periods`, some periods hold more than one row — different published series told apart by the listed columns (activated_balancing_price: `product`, e.g. CZ/SK mFRR standard A01 vs specific A02; or `currency`). Never average across them. Keep one with `filters={"product": ["A01"]}` — `filters` takes any categorical column of the endpoint (`{column: [values]}`). Outage-family endpoints (different schema) stay on `get_outages()`.
- 2026-10-13 `get_series` changed (description)
  - before: Generic time-series query for ANY registered series endpoint. One tool covers every (non-outage) endpoint in the registry, so adding a new dataset (call `list_endpoints()` to see the current 14) gets an MCP surface automatically — no new tool to learn. Argument shape adapts to the endpoint: • single-zone (day_ahead_price, actual_load, generation_per_type, …) → pass `zone="DE_LU"` • cross-zone (crossborder_flow, scheduled_exchanges, net_transfer_capacity_dayahead) → pass `from_zone="DE_LU"` AND `to_zone="FR"` • psr-dependent (generation_per_type, wind_solar_forecast, installed_generation_capacity) → optionally filter via `psr_types=["solar","wind_onshore"]` `start`/`end`: UTC by default; `start` inclusive, `end` EXCLUSIVE (for "all of April 2026" use end=2026-05-01). Pass `tz="local"` or an IANA name to interpret as wall-clock in that timezone. `aggregation`: 'raw' (default — native PT15M/PT60M per endpoint), 'hourly' (AVG over quarters → one row per hour, useful for the growing list of PT15M-stored endpoints like DE_LU day_ahead_price), 'daily', or 'monthly'. For day-ahead prices specifically the auction still clears hourly even where stored at PT15M, so AVG=any-quarter; SUM would 4× over-count. Outage-family endpoints (different schema) stay on `get_outages()`.
  - after: Generic time-series query for ANY registered series endpoint. One tool covers every (non-outage) endpoint in the registry, so adding a new dataset (call `list_endpoints()` to see the current 14) gets an MCP surface automatically — no new tool to learn. Argument shape adapts to the endpoint: • single-zone (day_ahead_price, actual_load, generation_per_type, …) → pass `zone="DE_LU"` • cross-zone (crossborder_flow, scheduled_exchanges, net_transfer_capacity_dayahead) → pass `from_zone="DE_LU"` AND `to_zone="FR"` • psr-dependent (generation_per_type, wind_solar_forecast, installed_generation_capacity) → optionally filter via `psr_types=["solar","wind_onshore"]` `start`/`end`: UTC by default; `start` inclusive, `end` EXCLUSIVE (for "all of April 2026" use end=2026-05-01). Pass `tz="local"` or an IANA name to interpret as wall-clock in that timezone. `aggregation`: 'raw' (default — native PT15M/PT60M per endpoint), 'hourly' (AVG over quarters → one row per hour, useful for the growing list of PT15M-stored endpoints like DE_LU day_ahead_price), 'daily', or 'monthly'. For day-ahead prices specifically the auction still clears hourly even where stored at PT15M, so AVG=any-quarter; SUM would 4× over-count. Currency: price columns keep `_eur_` in their names for stability. An endpoint whose rows carry a `currency` column (day-ahead and intraday prices, the A81/A15 capacity prices, activated_balancing_price) labels each row (PLN, HUF, RON, CZK, GBP, …) and the response `unit` follows it; `unit` is null when the window mixes currencies (`mixed_currency`) or, for activated_balancing_price, holds rows of unknown currency (`currency_unknown_rows`). Never average a price across rows with different `currency`. An endpoint WITHOUT a `currency` column serves its registry unit as-is: imbalance_prices is published in the TSO's own currency for several zones (HU HUF, CZ CZK, PL PLN, RO RON) and does not label it yet. Outage-family endpoints (different schema) stay on `get_outages()`.

Full history: https://api.protogrid.dev/v1/servers/com.entsoemcp%2Fentsoe-mcp/changes

## Remotes

- https://entsoemcp.com/mcp (streamable-http, auth none, reachable true, uptime30d 0.99159664)

## Packages

_none_

## Tools (14)

- `compare_zones`: Compare one endpoint across multiple zones.

`start`/`end` default to UTC; `start` inclusive, `end` EXCLUSIVE.
`tz="local"` is rejected (zones may differ); pass an explicit IANA
tz like "Europe/Berlin" if you need wall-clock alignment.
`filters={column: [values]}` keeps only rows whose categorical column
holds one of the values, in every zone (see get_series); zones whose
periods hold several rows are listed under `split_periods_by_zone`.
- `data_coverage`: Show ingest coverage and lag for (endpoint × zone) — call before queries
if you're not sure whether the data is landed yet.
- `get_crossborder_flow`: Cross-border physical flow (MW) between two adjacent zones.

`start`/`end` default to UTC; `start` inclusive, `end` EXCLUSIVE.
`tz="local"` uses the FROM-zone's timezone; or pass an IANA name.
- `get_day_ahead_prices`: Day-ahead clearing price for a bidding zone, in the zone's trading
currency (EUR for euro zones; the per-row `currency` column and the
response `unit` say which — GB=GBP, PL/RO/BG carry local-currency eras).

`start`/`end` default to UTC; `start` inclusive, `end` EXCLUSIVE.
For 'all of April 2026' use start=2026-04-01, end=2026-05-01
(end=2026-04-30 silently drops the final UTC day — and 1–2 local-time
hours of April for European zones in CET/CEST). The response's
`period` block shows the resolved window so you can verify
(a 30-day month is 720 hours).

`tz`: pass "local" to interpret start/end as wall-clock in the zone's
timezone, or an explicit IANA name like "Europe/Berlin". The server
converts to UTC at the boundary.

If you're computing a **generation-weighted** price metric — capture
price, capture rate, value factor, merchant-PPA achieved price — use
`get_derivation(slug="capture_price", …)` instead. It runs server-side
over the full window and returns monthly rows; no row cap, no pagination.
- `get_derivation`: Compute **capture price**, **capture rate** (a.k.a. value factor /
quality factor / Marktwertfaktor), **TBx battery-arbitrage spreads**,
and other generation-weighted market metrics server-side from landed
Parquet. Use this instead of fetching hourly prices + hourly generation
yourself and weighting them client-side — server-side aggregation has
no row cap and no pagination.

`slug`: a key from list_derivations() — today: `"capture_price"`,
`"negative_price_hours"`, `"residual_load"`, `"res_share"`,
`"emissions"`, `"tb_spread"`.

`tb_spread` returns monthly (default) or annual (aggregation='annual')
Top-Bottom spreads TB1/TB2/TB4/TB6 in <currency>/MW per period — the sum
of daily (top-x minus bottom-x hourly prices) over SDAC market days. The
only slug accepting `aggregation`, and the only one accepting
multi-zone `zone` ('all', a list, or CSV). Example — annual TB2 across
every European market in ONE call:
  get_derivation("tb_spread", "2025-01-01", "2026-01-01",
                 zone="all", aggregation="annual")
For a single day's top/bottom hour TIMESTAMPS use get_tb_spread.

`capture_price` returns monthly rows per (zone, psr_type, currency) with
columns: currency, capture_price_eur_per_mwh, baseload_price_eur_per_mwh,
quality_factor (the capture rate = capture/baseload), total_gen_mwh,
n_hours. Months are bucketed by local time using the zone's IANA
timezone.

CURRENCY (capture_price and tb_spread alike): the `*_eur_per_mwh` /
`tb*_eur_per_mw` key names are FIXED for API stability and do NOT track
the actual unit — read the row's `currency` column, which is
authoritative (EUR for euro zones, GBP for GB, PLN/RON/BGN for the
PL/RO/BG local-currency eras). The response echoes it top-level as
`currency`; a window spanning two currencies instead sets `unit` to null
with `mixed_currency: true` and a `currencies` list. A month (or period)
spanning a redenomination splits into one row PER CURRENCY, each computed
only from that currency's hours — so never average or sum a price column
across rows with different `currency` values. Summing `total_gen_mwh`
across them IS correct: the split rows partition the month's hours rather
than duplicating them. `quality_factor` is a ratio and stays comparable
across currencies.

`emissions` returns monthly rows per (zone, psr_type) with columns:
generation_mwh, n_hours, emission_factor_kg_per_mwh, emissions_t_co2.
Production-based; IPCC AR5 lifecycle factors. Zero-emission rows
(nuclear, wind, solar, hydro, geothermal, marine) appear with
emissions_t_co2 = 0 — useful for stacked charts.

Filter via `psr_types=["solar","wind_onshore","wind_offshore"]` (or
raw B-codes like "B16") to get only the technologies you care about.
Defaults to all psr_types that have generation data.

Example — Spain solar capture price, last 12 months:
  get_derivation("capture_price", "2025-05-01", "2026-05-01",
                 zone="ES", psr_types=["B16"], tz="Europe/Madrid")
Example — Germany 2024 emissions by fuel:
  get_derivation("emissions", "2024-01-01", "2025-01-01",
                 zone="DE_LU", tz="Europe/Berlin")
Returns 12 monthly rows in one call; no pagination.
- `get_generation`: Aggregated generation (MW) per production type.

`start`/`end` default to UTC; `start` inclusive, `end` EXCLUSIVE.
For a full calendar month set `end` to the first day of the next
month. Pass `tz="local"` or an IANA name to interpret start/end as
wall-clock in that timezone.

If you're computing a **generation-weighted** price metric — capture
price, capture rate, value factor, merchant-PPA achieved price — use
`get_derivation(slug="capture_price", …)` instead. It runs server-side
over the full window and returns monthly rows; no row cap, no pagination.
- `get_load`: Actual or forecast load (MW). kind = actual | forecast | both.

`start`/`end` default to UTC; `start` inclusive, `end` EXCLUSIVE.
For a full calendar month set `end` to the first day of the next
month. Pass `tz="local"` or an IANA name to interpret start/end as
wall-clock in that timezone.
- `get_outages`: Generation-unit unavailability events (UMM).

`start`/`end` default to UTC; `start` inclusive, `end` EXCLUSIVE. An
outage is returned if it overlaps the window. Pass `tz="local"` or an
IANA name to interpret start/end as wall-clock in that timezone.
- `get_series`: Generic time-series query for ANY registered series endpoint.

One tool covers every (non-outage) endpoint in the registry, so adding
a new dataset (call `list_endpoints()` to see the current 14) gets an
MCP surface automatically — no new tool to learn.

Argument shape adapts to the endpoint:
  • single-zone (day_ahead_price, actual_load, generation_per_type, …)
    → pass `zone="DE_LU"`
  • cross-zone  (crossborder_flow, scheduled_exchanges,
                 net_transfer_capacity_dayahead)
    → pass `from_zone="DE_LU"` AND `to_zone="FR"`
  • psr-dependent (generation_per_type, wind_solar_forecast,
                   installed_generation_capacity)
    → optionally filter via `psr_types=["solar","wind_onshore"]`

`start`/`end`: UTC by default; `start` inclusive, `end` EXCLUSIVE
(for "all of April 2026" use end=2026-05-01). Pass `tz="local"` or an
IANA name to interpret as wall-clock in that timezone.

`aggregation`: 'raw' (default — native PT15M/PT60M per endpoint),
'hourly' (AVG over quarters → one row per hour, useful for the
growing list of PT15M-stored endpoints like DE_LU day_ahead_price),
'daily', or 'monthly'. For day-ahead prices specifically the auction
still clears hourly even where stored at PT15M, so AVG=any-quarter;
SUM would 4× over-count.

Currency: price columns keep `_eur_` in their names for stability. An
endpoint whose rows carry a `currency` column (day-ahead and intraday
prices, the A81/A15 capacity prices, activated_balancing_price,
imbalance_prices) labels each row (PLN, HUF, RON, CZK, GBP, …) and the
response `unit` follows it; `unit` is null when the window mixes
currencies (`mixed_currency`) or, for activated_balancing_price and
imbalance_prices, holds rows of unknown currency
(`currency_unknown_rows`). Never average a price across rows with
different `currency`.

Split periods: when a response carries `split_periods`, some periods hold
more than one row — different published series told apart by the listed
columns (activated_balancing_price: `product`, e.g. CZ/SK mFRR standard
A01 vs specific A02; or `currency`). Never average across them. Keep one
with `filters={"product": ["A01"]}` — `filters` takes any categorical
column of the endpoint (`{column: [values]}`).

Process types: activated balancing prices are one endpoint per ENTSO-E
processType (activated_balancing_price = A16 realised; _mfrr_scheduled =
A60, _mfrr_direct = A61, _afrr_local = A68) — different prices for the
same period, never to be merged. `related_series` names the siblings
holding data for the zone.

Outage-family endpoints (different schema) stay on `get_outages()`.
- `get_tb_spread`: Top-Bottom (TBx) spread — daily battery-arbitrage benchmark.
TBx = sum(top X priced hours) − sum(bottom X priced hours) over the
day-ahead clearing prices for `zone` on `date`. The day is the SDAC
market day (23/25 hours on DST-transition days). `date` must be a
bare YYYY-MM-DD — time-bearing strings are rejected.
Returns both `spread` (<currency>/MW/day) and `mean_spread`
(<currency>/MWh = spread/X) in the zone's trading currency — see the
response `currency`/`unit` (EUR for euro zones; GB=GBP).
Common X: 1, 2, 4.
- `list_derivations`: List computed-metric derivations available via get_derivation().

Each derivation is a metric computed on-the-fly from one or more
landed endpoints (Tier-2 Parquet). Today: capture_price (monthly
VWAP capture price + baseload + capture rate per technology;
"capture rate" is the industry-standard name for what the JSON
response calls `quality_factor`), negative_price_hours, residual_load,
res_share, emissions (monthly CO2 emissions per fuel using IPCC
AR5 lifecycle factors, production-based), and tb_spread (monthly or
annual Top-Bottom battery-arbitrage spread TB1/TB2/TB4/TB6 per zone
on SDAC market days; accepts zone='all' for every zone in one call).
- `list_endpoints`: List supported ENTSO-E endpoint families.
- `list_psr_types`: List production-type (psr_type) codes. Pass zone= to scope the answer.

Most codes are ENTSO-E's B01..B25 and mean the same thing in every
ENTSO-E zone. A few are source-native (source != "entsoe") and exist only
where that source publishes — they express concepts the B-codes cannot,
so they are NOT interchangeable with a similar-looking B-code. Check
`source` and read `description` before comparing a code across zones.

Passing zone= also returns `taxonomy_note` for zones that mix taxonomies
(e.g. GB), and per-code `endpoints` showing where each code comes from.

Each code carries `counts_as_generation`: when False the figure is a net
flow or net storage number, not production — do not sum it into a
generation total.
- `list_zones`: List registered ENTSO-E bidding zones.
