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 you can ask
“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.”
“How much did Belgium spend on redispatching each month in 2025?”
Prerequisites
| 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. |
Quick start
Step 1 — Get an ENTSO-E API token
- 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
Step 2 — Pull the Docker image using CLI
Or build from source (see Building from source below).
Step 3 — Configure MCP server settings of Claude Desktop
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", "-v", "entsoeapi-cache:/home/mcp/.cache", "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 -
-v entsoeapi-cache:/home/mcp/.cache— optional; keeps the downloaded EIC code tables in a named volume for a day, so the EIC lookup tools answer instantly in later sessions instead of re-downloading them
Step 4 — Restart Claude Desktop and verify
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.
Available tools (30)
The 22 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.
EIC lookup
| 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 |
Load
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 |
Generation
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 | Wind and solar forecast; process_type A01 day-ahead (default), A40 intraday, A18 current |
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 |
Generation — installed capacity
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 |
Market
| 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 |
Transmission
| 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 |
Congestion management
One tool (congestion_management) with a type parameter:
type value |
ENTSO-E dataset | Description |
|---|---|---|
redispatching_internal (default)
|
13.1.A | Internal redispatching in a control area (eic) |
redispatching_cross_border |
13.1.A | Cross-border redispatching (eic_in, eic_out; direction matters) |
countertrading |
13.1.B | Countertrading on a border (eic_in, eic_out) |
costs |
13.1.C | Monthly costs of redispatching, countertrading and other congestion management (eic, optional event_nature) |
Outages
| 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 |
Balancing
| 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 |
Session DuckDB cache
| 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 several TimeSeries are stacked together in one cached table. This is the real envelope for Austrian day-ahead prices on 12 October 2026: one day of 15-minute prices, but 288 rows, because the document holds classification sequence 2, sequence 1, and sequence 1 again under a third ts_mrid:
# table: energy_prices_2c603b
# rows: 288
# columns: ts_in_domain_name VARCHAR, ts_out_domain_name VARCHAR, type VARCHAR, type_def VARCHAR, market_agreement_type VARCHAR, market_agreement_type_def VARCHAR, ts_auction_type VARCHAR, ts_auction_type_def VARCHAR, ts_business_type VARCHAR, ts_business_type_def VARCHAR, ts_time_interval_start TIMESTAMP, ts_time_interval_end TIMESTAMP, ts_mrid DOUBLE, ts_point_dt_start TIMESTAMP, ts_point_dt_start_cet TIMESTAMP, ts_point_price_amount DOUBLE, ts_currency_unit_name VARCHAR, ts_price_measure_unit_name VARCHAR, ts_classification_sequence_position DOUBLE
# ------------------------------------------------------------
# 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.
# ------------------------------------------------------------
# time: ts_point_dt_start is UTC; ts_point_dt_start_cet is the same
# instant in Central European wall-clock time (CET/CEST, DST
# handled). Group by it for local days and hours; do not shift
# by a fixed INTERVAL.
# WARNING: multiple series present in this table:
# ts_classification_sequence_position: 1, 2 (filter explicitly; do not assume one)
# ts_mrid: 1, 2, 3 (filter explicitly; do not assume one)
# schema sample (5 of 288 rows; do not use for analysis):
"ts_in_domain_name","ts_out_domain_name","type","type_def","market_agreement_type","market_agreement_type_def","ts_auction_type","ts_auction_type_def","ts_business_type","ts_business_type_def","ts_time_interval_start","ts_time_interval_end","ts_mrid","ts_point_dt_start","ts_point_dt_start_cet","ts_point_price_amount","ts_currency_unit_name","ts_price_measure_unit_name","ts_classification_sequence_position"
"Austria","Austria","A44","Price Document","A01","Daily contract","A01","Implicit","A62","Spot price",2026-10-11 22:00:00,2026-10-12 22:00:00,1,2026-10-11 22:00:00,2026-10-12 00:00:00,195.36,"EUR","MWH",2
"Austria","Austria","A44","Price Document","A01","Daily contract","A01","Implicit","A62","Spot price",2026-10-11 22:00:00,2026-10-12 22:00:00,1,2026-10-11 22:15:00,2026-10-12 00:15:00,190.12,"EUR","MWH",2
"Austria","Austria","A44","Price Document","A01","Daily contract","A01","Implicit","A62","Spot price",2026-10-11 22:00:00,2026-10-12 22:00:00,1,2026-10-11 22:30:00,2026-10-12 00:30:00,178.49,"EUR","MWH",2
"Austria","Austria","A44","Price Document","A01","Daily contract","A01","Implicit","A62","Spot price",2026-10-11 22:00:00,2026-10-12 22:00:00,1,2026-10-11 22:45:00,2026-10-12 00:45:00,178.24,"EUR","MWH",2
"Austria","Austria","A44","Price Document","A01","Daily contract","A01","Implicit","A62","Spot price",2026-10-11 22:00:00,2026-10-12 22:00:00,1,2026-10-11 23:00:00,2026-10-12 01:00:00,190,"EUR","MWH",2
# suggested query: SELECT date_trunc('week', ts_point_dt_start_cet) AS wk, AVG(ts_point_price_amount) FROM energy_prices_2c603b GROUP BY 1 ORDER BY 1Note that ts_point_dt_start is UTC (22:00 UTC is midnight in Vienna in summer), while ts_point_dt_start_cet holds the same instant in Central European wall-clock time, with daylight saving applied. The server computes it in R because the sandboxed DuckDB cannot load its time-zone extension, and a fixed + INTERVAL 2 HOUR shift is wrong on changeover days. The sample rows show only sequence 2, so Claude would filter first, e.g. WHERE ts_classification_sequence_position = 1 AND ts_mrid = 2, then group by ts_point_dt_start_cet.
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.
The database is sandboxed: SQL cannot read or write files, reach the network, install extensions, or change settings, and its memory is capped at 1 GB (override with -e ENTSOEAPI_MCP_DB_MEMORY=2GB).
Example workflow Claude would run for “Weekly average German load for January 2024”:
-
area_eic(query = "Germany")— resolve EIC code -
load(eic = "10Y1001A1001A83F", period_start = "2024-01-01", period_end = "2024-02-01")— envelope withtable = load_actual_<hash> -
sql_query(sql = "SELECT date_trunc('week', ts_point_dt_start_cet) AS wk, AVG(ts_point_quantity)::INT AS avg_mw FROM load_actual_<hash> GROUP BY 1 ORDER BY 1")— 5 weekly rows instead of 2,976 quarter-hourly rows
Tips for good results
-
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.
The server also sends these rules to the client as its MCP instructions when a session starts, so the model sees the workflow (EIC first, fetch once, aggregate with sql_query) before its first tool call.
Agent skill
inst/skills/entsoe-market-data/SKILL.md is an Agent Skill that teaches an agent when and how to use this server: picking the right EIC per dataset, reading the cache envelope, writing the sql_query step, and the common pitfalls (several series in one table, UTC versus local timestamps). It needs the MCP server above to be connected. To use it with Claude Code, copy the folder into ~/.claude/skills/ (or a project’s .claude/skills/); in Claude Desktop, upload it as a zip under Settings → Customize → Skills. In an R installation it is at system.file("skills", "entsoe-market-data", package = "entsoeapi.mcp").
Data notes
-
Timezone: All dates are interpreted as CET (Central European Time; UTC+1 / UTC+2 in summer) regardless of your local timezone. Cached tables keep point timestamps in UTC (
ts_point_dt_start) and add a Central European wall-clock copy (ts_point_dt_start_cet) for grouping by local day or hour. - 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
Alternative: native R (no Docker)
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"
}
}
}
}Building from source
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 built in two stages: rocker/r-ver installs the pre-built Linux binaries from Posit Public Package Manager, then R and the packages are copied onto a plain ubuntu:noble together with only the shared libraries they link against (~230 MB instead of ~1.8 GB). It runs as a non-root mcp user. Override the R version with --build-arg R_VERSION=4.6.1.
Testing
The package ships with 133 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.
Repository structure
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()), server instructions
| ├── tools.R # All 30 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 (16 tests)
| ├── test-tools.R # Unit tests of tools.R (62 tests)
| ├── test-utils.R # Unit tests of utils.R (20 tests)
| └── test-db.R # Unit tests of db.R (35 tests)
├── inst
| └── skills
| └── entsoe-market-data
| └── SKILL.md # Agent Skill: how an agent should use this server
├── man
| └── run.Rd # Documentation of functions in R/run.R
└── .github/
├── pull_request_template.md
└── ISSUE_TEMPLATE
├── bug_report.md
└── feature_request.mdToken budget design
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.
Requirements
- R ≥ 4.2.0
-
entsoeapi(GitHub) -
mcptools(GitHub) -
ellmer,lubridate,jsonlite,duckdb,DBI,rlang(CRAN)
License
MIT © Sándor Budai. See LICENSE.
The underlying data is provided by ENTSO-E under their terms of use.