@cyanheads/eia-energy-mcp-server
Enables use of Cloudflare KV, R2, or D1 as a storage backend for persistent state.
Enables tabular data spillover and SQL querying via DuckDB-powered DataCanvas for large result sets.
Provides optional OpenTelemetry tracing for observability.
Enables use of Supabase as a storage backend for persistent state.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@@cyanheads/eia-energy-mcp-serverFind electricity net generation data for California in 2022"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Public Hosted Server: https://eia-energy.caseyjhand.com/mcp
Overview
Energy data from the U.S. Energy Information Administration (EIA) API v2 — electricity, petroleum, natural gas, coal, and forecasts. Browse the dataset taxonomy, search it by natural language, and query time-series data with facet filters, then stage large result sets as a SQL-queryable DataCanvas table. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
Tools
Tool | Description |
| Lists child routes under a path in the EIA dataset taxonomy; omit |
| Returns a leaf route's facets, valid values, data columns, frequencies, and date range. |
| Fuzzy text search across route names, descriptions, STEO series names, and facet values. |
| Fetches data from a leaf route with facet filters, date range, and column selection; optionally stages results for SQL. |
| Lists active DataCanvas dataframes staged by |
| Runs a read-only SQL SELECT against staged DataCanvas dataframes. |
| Drops a DataCanvas dataframe, freeing its memory. |
The three eia_dataframe_* tools are registered only when CANVAS_PROVIDER_TYPE=duckdb is set; eia_dataframe_drop additionally requires EIA_DATAFRAME_DROP_ENABLED=true. A default deployment lists the first four tools.
Related MCP server: @cyanheads/federal-reserve-mcp-server
Capability reference
eia_browse_routes tool
Omit
pathfor the 14 top-level categories (electricity, petroleum, natural-gas, coal, international, total-energy, steo, aeo, ieo, seds, crude-oil-imports, nuclear-outages, densified-biomass, co2-emissions); pass a path to drill into subcategoriesEach child carries
isLeaf— leaf routes are queryable viaeia_describe_route/eia_query_route; non-leaf routes have further children to browsesteois a flat leaf with 1,469 named series and no sub-routesAccepts
routeas an alias forpath; supplying both is rejected. Leading, trailing, and doubled slashes are stripped before resolvingroute_not_foundwhen the path does not exist in the taxonomy
eia_describe_route tool
Returns facets (with valid values), data column names/units, frequency options, and date range for a leaf
route; acceptspathas an alias, but not alongsiderouteEach facet is capped at
EIA_FACET_VALUE_CAPvalues (default 50), withvalue_countandvalues_truncated; page one facet withfacet+values_offsetA
values_offsetpast a facet's last value returns an empty window plus anoticenaming the facet and itsvalue_count, rather than reading as an exhausted enumerationErrors:
route_not_found,route_not_queryable(category node, not a leaf),facet_not_found,rate_limited(retryable)
eia_search_routes tool
Fuzzy match over route names/descriptions, STEO's 1,469 series names, and facet values;
limitcaps results (default 10, max 30)scoreruns 0 (exact) to 1 (no match); above 0.72 is a weak match — narrow the query or useeia_browse_routesMatching facet-value or STEO results carry
filter_hint, a ready-to-use filter object foreia_query_routeThe first call after server start waits 24–30 s (never more than 45 s) for the index to warm; every later call is served from the in-process index in milliseconds
indexComplete/indexGapsreport whether the corpus was complete when scored — check before trusting a short result set
eia_query_route tool
Takes
route(or aliaspath, never both), facet filters keyed by facet ID (fromeia_describe_route), plus optionalcolumns,frequency,start/end, andsortoffset/lengthpage the inline preview (lengthdefault 100, max 5000 per EIA's per-request ceiling);totalreports the full match countData values arrive as strings; per-column units appear as inline
{col}-unitsfieldsstage: truepages past the preview and stages the accumulated rows as a DataCanvasdf_<id>table (bounded byEIA_CANVAS_MAX_ROWS, default 25000) foreia_dataframe_query; omitted, the call costs one upstream request regardless oftotalErrors:
route_not_found,route_not_queryable,invalid_facet/invalid_column/invalid_frequency/invalid_sort/invalid_period,no_data(inverted date range),rate_limited(retryable)
eia_dataframe_describe tool
Lists DataCanvas dataframes staged by prior
eia_query_routecalls withstage: true; only registered whenCANVAS_PROVIDER_TYPE=duckdbOmit
nameto list every active dataframe for the tenant; passnameto check one — a miss comes back asfound: falsealongsideactive_names, never as an empty listEach entry reports
source_tool,query_params,created_at,expires_at,row_count,truncated/max_rows, andcolumn_schemaListing does not extend a dataframe's expiry — only an
eia_dataframe_querystatement referencing it doescanvas_unavailablewhen no canvas is configured
eia_dataframe_query tool
Runs one read-only SQL SELECT against
df_<id>tables; writes, DDL,DROP,COPY,PRAGMA,ATTACH, and system catalogs (information_schema,pg_catalog,sqlite_master,duckdb_*) are rejectedrow_limit(default 1000, max 10000) hard-caps materialized rows — rows past it are dropped uncounted, sototalRowsbecomes the cap, not a true total;previewseparately narrows the inline slice without affecting the countregister_aspersists the result as a new dataframe with a fresh expiry; the name must be unusedEIA data columns are VARCHAR — cast with
CAST(col AS DOUBLE)for arithmeticErrors:
canvas_unavailable,system_catalog_access,missing_table,non_select_statement,invalid_sql,register_as_clash
eia_dataframe_drop tool
Drops a dataframe by
name; idempotent — returnsdropped: falsewhen nothing matchedOnly registered when
EIA_DATAFRAME_DROP_ENABLED=trueandCANVAS_PROVIDER_TYPE=duckdbManual cleanup only — the per-dataframe expiry (default 24 h, extended by every referencing query) handles cleanup in normal operation
canvas_unavailablewhen no canvas is configured
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
EIA-specific:
Full coverage of EIA API v2's 14 top-level dataset categories, via an in-process route tree cache built once on first use
Fuzzy search index (Fuse.js) covers route names/descriptions, all 1,469 STEO series names, and facet values for natural-language discovery
Per-route facet metadata is fetched by fan-out (
Promise.all) and cached, soeia_query_routefilters are validated without re-fetchingA route whose metadata could not be fetched is held as an incomplete stub — reported through
eia_search_routesrather than silently dropped — and re-fetched on the nexteia_browse_routescall that reaches itDataCanvas (DuckDB) staging is opt-in per call; the three dataframe tools are gated at registration so a canvas-less deployment lists no tool it cannot serve
Agent-friendly output:
Provenance —
eia_query_routeechoes the canonical, slash-normalized route rather than the caller's spelling, and every staged dataframe records itssource_toolandquery_paramsCapped-window disclosure — every truncatable response (facet values, row previews, SQL row limits) reports the count against its cap and a
noticenaming the exact next call to page past itDiscriminated failure — typed error
reasonvalues (e.g.route_not_queryable,invalid_facet,missing_table) each carry arecoveryhint naming the next tool call
Getting started
Public Hosted Instance
A public instance is available at https://eia-energy.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"eia-energy-mcp-server": {
"type": "streamable-http",
"url": "https://eia-energy.caseyjhand.com/mcp"
}
}
}Self-Hosted / Local
Get a free API key at api.eia.gov, then add the following to your MCP client configuration file.
{
"mcpServers": {
"eia-energy-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/eia-energy-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"EIA_API_KEY": "your-api-key"
}
}
}
}Or with npx (no Bun required):
{
"mcpServers": {
"eia-energy-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/eia-energy-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"EIA_API_KEY": "your-api-key"
}
}
}
}Or with Docker:
{
"mcpServers": {
"eia-energy-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"-e", "EIA_API_KEY=your-api-key",
"ghcr.io/cyanheads/eia-energy-mcp-server:latest"
]
}
}
}For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 EIA_API_KEY=your-key bun run start:http
# Server listens at http://localhost:3010/mcpPrerequisites
Bun v1.4.0 or higher (or Node.js v24+).
A free EIA API key from api.eia.gov. The
DEMO_KEYhits rate limits quickly; a real key is required for sustained use.
Installation
Clone the repository:
git clone https://github.com/cyanheads/eia-energy-mcp-server.gitNavigate into the directory:
cd eia-energy-mcp-serverInstall dependencies:
bun installConfigure environment:
cp .env.example .env
# edit .env and set required vars (at minimum, EIA_API_KEY)Configuration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
Variable | Description | Default |
| Required. Free API key from api.eia.gov — appended as | — |
| EIA API base URL. |
|
| Sliding per-dataframe TTL in seconds. The window is extended every time an |
|
| Set to |
|
| Cumulative row ceiling for |
|
| Facet values |
|
| Set to | — |
| Transport: |
|
| HTTP server port. |
|
| HTTP endpoint path. |
|
| HTTP sessions: |
|
| Public origin override for TLS-terminating reverse-proxy deployments. | — |
| Auth mode: |
|
| Log level (RFC 5424). |
|
| Directory for log files (Node.js only). |
|
| Storage backend: |
|
| Enable OpenTelemetry instrumentation. |
|
See .env.example for the full list of optional overrides.
Running the server
Local development
Build and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:httpRun checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t eia-energy-mcp-server .
docker run --rm -e EIA_API_KEY=your-key -p 3010:3010 eia-energy-mcp-serverThe Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/eia-energy-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Directory | Purpose |
|
|
| Server-specific environment variable parsing and validation with Zod. |
| Tool definitions ( |
| EIA API v2 service — route tree cache, Fuse.js index, facet fan-out, HTTP client. |
| DataCanvas bridge — registers EIA query results as DuckDB dataframes, routes SQL queries. |
| Unit and integration tests mirroring |
| Design documents ( |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storageAlways call
eia_describe_routebeforeeia_query_route— facet values require a separate API fan-out and are not embedded in route metadataWrap EIA responses: validate raw → normalize to domain type → return output schema; data values are strings — never coerce silently
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run testLicense
Apache-2.0 — see LICENSE for details.
This server cannot be deployed
Maintenance
Related MCP Connectors
Query SEC EDGAR filings, XBRL financials, and company data through MCP. STDIO & Streamable HTTP.
EIA MCP — US Energy Information Administration API v2
Econdata MCP — wraps BLS (Bureau of Labor Statistics) public API v2
NREL MCP — wraps the US National Renewable Energy Laboratory developer API
Related MCP Servers
- AlicenseNot gradedqualityNot gradedmaintenanceProvides access to comprehensive U.S. and international energy data from the EIA API, including electricity, natural gas, petroleum, coal, renewables, CO2 emissions, and energy forecasts.MIT
- AlicenseNot gradedqualityAmaintenanceSearch and fetch ~800K Federal Reserve economic time-series from the FRED API via MCP, with STDIO or Streamable HTTP transport.297 npm1Apache 2.0
- AlicenseNot gradedqualityAmaintenanceExposes the FBI Crime Data Explorer API — crime estimates, agency offense rates, and LEOKA officer safety data via MCP. Supports STDIO or Streamable HTTP transport.93 npm1Apache 2.0
- FlicenseAqualityCmaintenanceAn MCP server that exposes the U.S. Energy Information Administration (EIA) Open Data API, enabling LLMs to browse and query energy data across 17 datasets with generic, composable tools.4-