An MCP server that exposes the ENTSO-E Transparency Platform as tools for LLM-enabled applications such as Claude Desktop. Query European electricity market, load, generation, transmission, outages, and balancing data through natural language.
Built in pure R on top of entsoeapi and mcptools. Distributed as a Docker image — no R installation required for end users.
Large time-series results are cached in an in-process DuckDB inside the container, so the LLM can aggregate, filter, and join via SQL without flooding its context window.
“What was the actual electricity load in Germany on 3 January 2025?”
“Show me day-ahead prices for France and Germany last Monday.”
“Which generation units in Belgium had unplanned outages this week?”
“What is the installed solar capacity in the Netherlands in 2024?”
“Get the net transfer capacity from Germany to Poland for today.”
| Requirement | Purpose | Notes |
|---|---|---|
| Docker | Runs the server container | Any version that supports docker run. |
| Claude Desktop | MCP client | or any MCP-compatible client |
| ENTSO-E API token | Access to the Transparency Platform | Free registration; UUID v4 format. |
- Register at transparency.entsoe.eu
- After logging in, go to My Account Settings → Web API Security Token
- Generate a token — it looks like
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
docker pull sbudai/entsoeapi.mcp:latestOr build from source (see Building from source below).
Open (or create) your Claude Desktop config file:
- macOS / Linux:
~/.config/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add the entsoe entry under "mcpServers":
{
"mcpServers": {
"entsoe": {
"command": "docker",
"args": ["run", "--rm", "-i", "-e", "ENTSOE_PAT", "sbudai/entsoeapi.mcp:latest"],
"env": {
"ENTSOE_PAT": "your-entsoe-api-token-here"
}
}
}
}Key flags:
--rm— removes the container when the session ends (no container accumulation)-i— keeps stdin open, required for MCP stdio transport-e ENTSOE_PAT— forwards the token from Claude Desktop into the container
Restart Claude Desktop to load the new MCP server configuration. You should see entsoe listed under the tools panel.
Test with a simple query:
“What is the EIC code for Germany?”
Claude should call the area_eic tool and return a list of German bidding zones and control areas.
The 21 time-series tools return a compact envelope pointing at a cached DuckDB table ({table, rows, schema, preview, SQL hint}). Use sql_query to aggregate or join. EIC lookup tools and get_news return CSV directly. All date parameters use CET timezone and YYYY-MM-DD format.
| Tool | Description |
|---|---|
area_eic |
Bidding zones, control areas, member states — look up EIC codes by country name |
party_eic |
Market participants (TSOs, traders, etc.) |
all_approved_eic |
All approved EIC codes across all types |
resource_object_eic |
Generation units and other resource objects |
get_news |
Latest news and maintenance notices from ENTSO-E |
One tool with a type parameter:
type value |
ENTSO-E dataset | Description |
|---|---|---|
actual (default) |
6.1.A | Actual total load |
day_ahead |
6.1.B | Day-ahead total load forecast |
week_ahead |
6.1.C | Week-ahead total load forecast |
month_ahead |
6.1.D | Month-ahead total load forecast |
year_ahead |
6.1.E | Year-ahead total load forecast |
margin |
8.1 | Year-ahead generation forecast margin |
One tool (gen_time_series) with a type parameter:
type value |
ENTSO-E dataset | Description |
|---|---|---|
actual (default) |
16.1.B&C | Actual generation per production type |
wind_solar |
14.1.D | Day-ahead wind and solar forecast |
day_ahead |
14.1.C | Day-ahead aggregated generation forecast |
per_unit |
16.1.A | Actual generation per generation unit (slow) |
storage |
16.1.D | Hydro reservoir / water storage filling rate |
One tool (gen_capacity) with a per_unit parameter:
per_unit value |
ENTSO-E dataset | Description |
|---|---|---|
FALSE (default) |
14.1.A | aggregate by production type |
TRUE |
14.1.C | individual production unit level |
| Tool | ENTSO-E dataset | Description |
|---|---|---|
energy_prices |
12.1.D | Day-ahead / intraday market clearing prices |
intraday_prices |
12.1.D | intraday market prices |
net_transfer_capacities |
11.1 | NTC between two bidding zones |
day_ahead_commercial_sched |
12.1.F | Day-ahead commercial schedules |
explicit_offered_transfer_capacities |
12.1.A | Explicit offered transfer capacities |
flow_based_allocations |
12.1.B | Flow-based allocations for CWE/Core region |
congestion_income |
12.1.G | Congestion income |
allocated_transfer_capacities_3rd_countries |
12.1.C | Allocated transfer capacities on third-country borders |
| Tool | ENTSO-E dataset | Description |
|---|---|---|
cross_border_physical_flows |
12.1.G | Physical cross-border flows |
total_commercial_sched |
12.1.F | Total commercial schedules |
net_positions |
12.1.H | Net positions |
forecasted_transfer_capacities |
11.1 | Forecasted transfer capacities |
| Tool | ENTSO-E dataset | Description |
|---|---|---|
outages_gen_units |
15.1.A&B | Unavailability of generation units |
outages_prod_units |
15.1.C&D | Unavailability of production units |
outages_transmission_grid |
15.1.A&B | Unavailability in the transmission grid |
| Tool | ENTSO-E dataset | Description |
|---|---|---|
imbalance_prices |
17.1.D | Imbalance prices |
imbalance_volumes |
17.1.C | Imbalance volumes |
contracted_reserves |
17.1.B | Contracted balancing reserves |
| Tool | Description |
|---|---|
sql_query |
Run a SQL query (DuckDB dialect) against cached tables. Returns CSV, capped at max_rows (default 100). Raise the cap for plotting (e.g. 800 for monthly hourly data); avoid values above 5000. |
list_tables |
List every cached table in this session with row counts and column schemas. |
describe_table |
Schema + row count + N sample rows for one named table. |
Every time-series tool inserts its full result into an in-memory DuckDB table and returns an envelope. The envelope tells Claude — in every call — that statistics must be computed via sql_query, never from the schema sample, and warns when multiple TimeSeries (e.g. day-ahead Sequence 1 vs. Sequence 2) are stacked together in one cached table:
# table: energy_prices_a3f7d2
# rows: 336
# columns: dt_start TIMESTAMP, price DOUBLE, sequence INTEGER
# ────────────────────────────────────────────────────────────
# IMPORTANT — read before using this result:
# This is a CACHED TABLE, not a result set. The rows below
# are a SCHEMA SAMPLE only — they are NOT representative of
# the data, may omit other series, and MUST NOT be used for
# any analysis. NEVER compute averages, totals, min/max,
# counts, or any statistic from this sample.
# ALWAYS call sql_query(...) against the table named above
# for ANY aggregation, filter, statistic, or join.
# ────────────────────────────────────────────────────────────
# WARNING: multiple series present in this table:
# sequence: 1, 2 (filter explicitly; do not assume one)
# schema sample (5 of 336 rows; do not use for analysis):
dt_start,price,sequence
2024-01-01 00:00:00,42.5,1
2024-01-01 01:00:00,41.2,1
2024-01-01 02:00:00,40.8,1
2024-01-01 03:00:00,40.1,1
2024-01-01 04:00:00,39.7,1
# suggested query: SELECT date_trunc('week', dt_start) AS wk, AVG(price) FROM energy_prices_a3f7d2 GROUP BY 1 ORDER BY 1
The cache lives for the lifetime of one Claude Desktop session (the docker run --rm -i container). Identical queries within a session reuse the same table — no second API hit.
Example workflow Claude would run for “Weekly average German load for January 2024”:
area_eic(query = "Germany")— resolve EIC codeload(eic = "10Y...", period_start = "2024-01-01", period_end = "2024-01-31")— envelope withtable = load_actual_a3f7d2sql_query(sql = "SELECT date_trunc('week', dt_start) AS wk, AVG(value)::INT AS avg_mw FROM load_actual_a3f7d2 GROUP BY 1 ORDER BY 1")— 5 weekly rows instead of 744 hourly rows
- Always call
area_eicfirst to resolve a country name to its EIC code before querying data. Many countries have multiple bidding zones (e.g. Norway has NO1–NO5). - Fetch generously, aggregate via SQL. The 100-row cap only applies to
sql_queryoutput — the cached DuckDB table holds the full result. Fetch a month or year of data once, then aggregate down to weekly / daily values viasql_query(...). - For generation time-series, omit
gen_typeto get all production types, or filter with codes likeB16(Solar) orB18(Wind offshore). - Never accept synthetic values. Every data-fetching tool explicitly instructs the LLM not to generate synthetic data when a live ENTSO-E tool is available. If Claude produces values without calling a tool, ask it to retry using the appropriate tool.
- Timezone: All dates are interpreted as CET (Central European Time; UTC+1 / UTC+2 in summer) regardless of your local timezone.
- Resolution: Load and generation data is typically at 15-minute or 1-hour intervals
- Row cap: The 21 time-series tools store their full result in DuckDB — no row cap at the fetch stage. The cap applies to
sql_queryoutput (default 100 rows) to keep aggregated results compact; raisemax_rowsexplicitly when you need raw rows for plotting (e.g. 800 for a month of hourly data). A truncation notice is appended when output is cut. - Max range: Most time-series tools accept up to 1 year per query
- EIC codes: Use
area_eicto resolve country or zone names to EIC codes before querying data
If you already have R ≥ 4.2.0 installed:
# Install dependencies
Rscript -e "install.packages('pak'); pak::pkg_install(c('krose/entsoeapi', 'posit-dev/mcptools', 'ellmer', 'lubridate', 'jsonlite'))"
# Install this package
R CMD INSTALL /path/to/entsoeapi.mcpThen in claude_desktop_config.json:
{
"mcpServers": {
"entsoe": {
"command": "Rscript",
"args": ["-e", "entsoeapi.mcp::run()"],
"env": {
"ENTSOE_PAT": "your-entsoe-api-token-here"
}
}
}
}git clone https://github.com/sbudai/entsoeapi.mcp.git
cd entsoeapi.mcp
# Build the Docker image
docker build -t sbudai/entsoeapi.mcp:latest .
# Verify the server starts and responds to MCP initialize
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0"}}}' \
| docker run --rm -i -e ENTSOE_PAT=test sbudai/entsoeapi.mcp:latestThe image is based on rocker/r-ver:4.4.3, uses Posit Public Package Manager for pre-built Linux binaries, and runs as a non-root mcp user.
The package ships with 94 unit tests that exercise every tool wrapper, every dispatch branch, and every utility — yielding 99% line coverage across R/run.R, R/tools.R, and R/utils.R.
Run the suite locally:
# Plain run (fast)
pkgload::load_all()
testthat::test_dir("tests/testthat")
# With coverage report
covr::package_coverage() |> covr::report()Tests are network-free: every entsoeapi::* call is mocked at the package’s imports environment via a small .with_imp_bindings() helper, and safe_to_csv() is mocked in the namespace via testthat::with_mocked_bindings(.package = "entsoeapi.mcp"). This lets the suite run in CI without an ENTSOE_PAT token and without hitting the Transparency Platform.
entsoeapi.mcp/
├── Dockerfile # Self-contained runtime image
├── DESCRIPTION # Package metadata and dependencies
├── NAMESPACE # Exported functions (generated by roxygen2)
├── LICENSE # MIT
├── NEWS.md # What has changed in each version
├── README.md # GitHub standard version of README.Rmd
├── README.Rmd # Comprehensive introduction of the project
├── RELEASE_CHECKLIST # What to check during launching a new version
├── R/
| ├── run.R # Entry point: run() → mcp_server(tools = all_tools())
| ├── tools.R # All 29 ellmer::tool() definitions
| ├── utils.R # parse_date(), slim_ts(), safe_to_csv(), safe_to_cache()
| └── db.R # In-memory DuckDB singleton + sql_query / list / describe helpers
├── tests
| ├── testthat.R # Test framework helper
| └── testthat
| ├── test-run.R # Unit tests of run.R (8 tests)
| ├── test-tools.R # Unit tests of tools.R (47 tests)
| ├── test-utils.R # Unit tests of utils.R (14 tests)
| └── test-db.R # Unit tests of db.R (25 tests)
├── man
| └── run.Rd # Documentation of functions in R/run.R
└── .github/
├── pull_request_template.md
└── ISSUE_TEMPLATE
├── bug_report.md
└── feature_request.md
Tool results from the 21 time-series tools are inserted into an in-process DuckDB (in-memory, per session) and the LLM gets a compact envelope back — table name, row count, column schema, a 5-row preview, and an SQL hint. The LLM then issues SQL via sql_query to aggregate, filter, or join inside the container, so only the aggregated result ever reaches the prompt context. slim_ts() still drops constant metadata columns before insertion to save memory and preview tokens. The 5 EIC lookup tools and get_news keep their direct-CSV path because they’re small and interactive.
- R ≥ 4.2.0
entsoeapi(GitHub)mcptools(GitHub)ellmer,lubridate,jsonlite,duckdb,DBI,digest(CRAN)
MIT © Sándor Budai. See LICENSE.
The underlying data is provided by ENTSO-E under their terms of use.