Skip to main content
Glama
manas-katyal

EnergiMCP

by manas-katyal
README.md
# EnergiMCP

Read-only MCP server for Danish energy data: electricity prices, carbon intensity, consumption, production, grid capacity, balancing markets and gas — the 100 datasets Energinet publishes through [Energi Data Service](https://www.energidataservice.dk/).

No API key, no account, no registration. It is open public data.

```
You   When should I run the dishwasher today?
      → get_electricity_prices · DK2 · today
AI    Wait until the afternoon. The cheapest quarter-hour is 14:15 at 0.91 kr/kWh;
      the morning peak at 07:15 is 2.96 kr/kWh. Average across the day is 1.43 kr/kWh.
```

## Install

Needs Node 24 or newer. Add it to your MCP client:

```json
{
  "mcpServers": {
    "energi": { "command": "npx", "args": ["-y", "energimcp"] }
  }
}
```

That is the whole setup.

From a clone instead:

```bash
npm install
npm test          # stubs fetch; never touches the API
npm run stdio     # the server an MCP client launches
npm run check     # config + a live call
npm run datasets  # the catalogue, one line per dataset
```

## Tools

| Tool | What it does |
| --- | --- |
| `list_datasets` | Search the catalogue by text or publisher. Hides the 22 discontinued datasets unless asked. |
| `describe_dataset` | Every column with type and unit, the time column `start`/`end` filter on, resolution, update frequency, and the first and last timestamp that exist. |
| `query_dataset` | Any dataset, with `start`, `end`, `columns`, `filter`, `sort`, `offset`, `limit`. `summary: true` returns per-column statistics instead of rows. |
| `download_url` | A CSV, JSON or Excel link for extracts too large to read into a conversation. Builds the URL without fetching it. |
| `get_electricity_prices` | Day-ahead prices in 15-minute resolution with the cheapest and dearest periods worked out. |
| `get_carbon_intensity` | g CO₂/kWh in 5-minute resolution, with the forecast and the greenest upcoming window. |
| `get_power_system_now` | One-minute snapshot: production by source, wind and solar share, carbon intensity, every interconnector. |

Three prompts ship with it: `when-to-run`, `find-dataset` and `grid-snapshot`.

## What this API does that the code works around

Four things about Energi Data Service shaped the implementation, and all four are easy to get wrong:

**Rate limits are per dataset and tight.** The platform expects roughly one request per dataset update interval and answers `429 Rate limit is exceeded. Try again in N seconds.` otherwise — in testing, the third request inside two minutes was refused. So every response is cached for that dataset's own update interval, identical in-flight calls are shared, outbound calls are spaced, and a 429 is reported to the model as something to wait out rather than retry.

**22 of the 100 datasets are discontinued and still answer `200`.** `Elspotprices` stopped updating on 2025-09-30 and happily returns year-old rows. Retirement is recorded only in the title, and the replacement only as a markdown link in the description. `query_dataset` detects both and returns a warning naming the successor, so stale prices are not presented as current.

**The metadata endpoint emits invalid JSON.** Descriptions contain raw unescaped newlines, which `JSON.parse` rejects. `parseLenientJson` repairs the control characters inside strings.

**Errors are plain text.** `404 dataset not found X`, `400 Invalid column X` — and `/meta/dataset/{unknown}` answers `204` with an empty body rather than 404. A 400 is enriched with the dataset's actual column list, since the model cannot guess them.

One correctness note worth stating: in `PowerSystemRightNow`, **positive exchange values are imports into Denmark**, not exports. The metadata does not say so. It is verifiable in `ProductionConsumptionSettlement`, where production plus exchange equals gross consumption exactly.

## Hosting it

`npm start` serves stateless streamable HTTP on `/mcp`, with `/healthz` for a probe. There is no auth, because there is nothing private behind it — do not put anything private behind it. The `Dockerfile` builds a container; `CACHE_DIR` is the only volume worth mounting, and only to keep the catalogue warm across restarts.

Hosting it is optional. The stdio server above is enough for a desktop client; deploy only if you want the tools on claude.ai or your phone, where a client cannot launch a local process.

### Railway

`.railway/railway.ts` declares the service and health-checks `/healthz`; the build comes from the `Dockerfile` at the repository root, which Railway picks up automatically. From a clone:

```bash
npm i -g @railway/cli
railway login
railway init            # creates the project
railway up              # builds and deploys
railway domain          # gives it a public URL
```

Then add `https://<your-domain>/mcp` as a custom connector in your assistant. Nothing else to configure: there are no secrets, and `BASE_URL` is detected from `RAILWAY_PUBLIC_DOMAIN`.

The same container runs anywhere — Fly, Render, a VPS. It is stateless, so scale it to as many replicas as you like; the only cost of losing the cache is one extra catalogue fetch per instance.

## Layout

```
src/eds.ts       API client: caching, request spacing, lenient JSON, typed errors
src/catalog.ts   the 100 datasets: search, name resolution, retirement, successors
src/data.ts      response shaping: dataset cards, freshness, per-column summaries
src/tools.ts     the seven tools
src/mcp.ts       server factory and the instructions the model reads first
src/stdio.ts     local entry point
src/server.ts    hosted entry point
data/catalog.json  catalogue snapshot, so discovery works on a cold start
docs/            the landing page (GitHub Pages)
```

Refresh the snapshot with `npm run catalog`.

## Not affiliated

An independent open-source client for a public API. Not affiliated with Energinet, Energi Data Service or Anthropic. Data is published by Energinet under their own terms. Spot prices exclude tariffs, taxes and VAT, which roughly double a Danish household bill — `DatahubPricelist` has the tariffs if you want the real number. MIT licence.

TDQS

A4.3/5.0

Scored across 7 tools

Disambiguation4/5

The catalogue workflow tools (list_datasets, describe_dataset, query_dataset) and the three convenience getters are largely distinct, but query_dataset can in principle fetch data overlapping with get_electricity_prices, get_carbon_intensity, and get_power_system_now. That boundary is slightly ambiguous, though the specialized getters make the intended choice clear in most cases.

Naming Consistency4/5

Most tool names follow a predictable verb_noun snake_case pattern: list_datasets, describe_dataset, query_dataset, get_electricity_prices. Minor inconsistencies include list_datasets being plural while describe_dataset/query_dataset are singular, and download_url being less verb-like than the other retrieval helpers.

Tool Count5/5

Seven tools is a well-scoped set for this domain: three catalogue lifecycle tools, one export helper, and three high-value convenience queries. Each tool earns its place without redundancy or bloat.

Completeness5/5

The set covers the full read-only workflow: discover datasets, inspect metadata, query rows, and generate download URLs for large exports. It also anticipates common user questions with electricity prices, carbon intensity, and power system snapshots, while query_dataset covers anything else in the catalogue.