About eia-mcp-server
eia-mcp-server exposes U.S. Energy Information Administration API v2 data for energy analysts and researchers. It combines route discovery and filtering with optional SQL queries over staged results.
Key features
- 01Browse and describe EIA dataset routes
- 02Search series, routes, and facet values
- 03Query energy data with filters and date ranges
- 04Stage results for read-only SQL analysis
Use cases
- 01Find a dataset for a specific energy indicator
- 02Retrieve electricity or fuel time series
- 03Analyze large query results with SQL
MCP support
Exposes EIA API v2 discovery, search, and query tools to MCP clients, with optional DataCanvas tools for SQL over staged data.
Configuration
Local server config is the same for every client. Copy it into your client’s MCP config file.
npx -y @cyanheads/eia-mcp-serverEIA_API_KEYSecretFree API key from api.eia.gov. Required — all requests use this as the api_key query parameter.EIA_DATASET_TTL_SECONDSPer-table TTL for DataCanvas dataframes in seconds.EIA_DATAFRAME_DROP_ENABLEDSet to 'true' to expose eia_dataframe_drop for manual canvas cleanup.CANVAS_PROVIDER_TYPESet to 'duckdb' to enable DataCanvas spillover for large query result sets (Node.js only).MCP_LOG_LEVELSets the minimum log level for output (e.g., 'debug', 'info', 'warn').MCP_HTTP_HOSTThe hostname for the HTTP server.MCP_HTTP_PORTThe port to run the HTTP server on.MCP_HTTP_ENDPOINT_PATHThe endpoint path for the MCP server.MCP_PUBLIC_URLPublic origin override for deployments behind a TLS-terminating reverse proxy (e.g. https://mcp.example.com).MCP_AUTH_MODESecretAuthentication mode to use: 'none', 'jwt', or 'oauth'.Local servers need a runtime on your machine (e.g. Node.js 18+). Config file locations differ by client; see your client’s docs.
Tools
Local servers expose tools only once running on your machine, and the maintainer has not provided a tool list.
Details
From the project README@cyanheads/eia-energy-mcp-server
Browse and query the U.S. Energy Information Administration API v2 — electricity, petroleum, natural gas, coal, forecasts, and more via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://eia-energy.caseyjhand.com/mcp
Overview
US energy data from the Energy Information Administration (EIA): electricity, petroleum, natural gas, coal, and forecasts. Find datasets in plain language, query time series with facet filters, and run SQL over large results.
Tools
| Tool | Description |
|---|---|
eia_browse_routes | Lists child routes under a path in the EIA dataset taxonomy; omit path for the 14 top-level categories. |
eia_describe_route | Returns a leaf route's facets, valid values, data columns, frequencies, and date range. |
eia_search_routes | Fuzzy text search across route names, descriptions, STEO and SEDS series names, and facet values — including benchmark series such as Henry Hub, WTI, and Cushing. Resolves legacy APIv1 series IDs (PET.RWTC.W) to their route and filters. |
eia_query_route | Fetches data from a leaf route with facet filters, date range, and column selection; optionally stages results for SQL. |
eia_dataframe_describe | Lists active DataCanvas dataframes staged by eia_query_route, with schema and provenance. |
eia_dataframe_query | Runs a read-only SQL SELECT against staged DataCanvas dataframes. |
eia_dataframe_drop | Drops a DataCanvas dataframe, freeing its memory. |
The three eia_dataframe_* tools need DataCanvas, which is on by default; eia_dataframe_drop additionally requires EIA_DATAFRAME_DROP_ENABLED=true. With CANVAS_PROVIDER_TYPE=none, which the Claude Desktop bundle sets, they are neither callable nor listed by MCP. Disabled definitions remain in the server manifest with their enable hints. A default deployment lists the first six tools.
Capability reference
Browse accepts route as an alias for path; describe and query accept path as an alias for route. Supplying both spellings is rejected. Whitespace and slashes are normalized, then exactly one complete leading v2 segment is removed: /v2/electricity/retail-sales/ resolves to electricity/retail-sales, and /v2/ browses root. Full URLs, /data suffixes, interior v2, and case remain unchanged.
All four route tools distinguish retryable upstream failures: rate_limited for HTTP 429 or OVER_RATE_LIMIT, upstream_unreachable for transport failures, upstream_unavailable for HTTP service outages, upstream_invalid_response for unusable bodies or missing envelopes, and upstream_timeout when EIA does not answer in time — each attempt is cut at 45 s, and a call's attempts, backoffs, and chained requests share one 56 s deadline, inside the 60 s timeout MCP clients apply. Recovery hints reach both response surfaces. HTTP 501 remains definitive, 504 remains a timeout, and caller cancellation stays cancellation.
eia_browse_routes tool
- Omit
pathfor the 14 top-level categories; pass a path to browse its children. - Each child carries
isLeaf: describe and query leaves, or browse categories further. - A missing path returns
route_not_found; EIA rate-limiting, being unreachable, or not answering in time returns retryablerate_limited,upstream_unreachable, orupstream_timeout.
eia_describe_route tool
- Pass a leaf
routefor facets, data columns, frequencies, and date range. Each facet window is capped atEIA_FACET_VALUE_CAP(default 50); page withfacetandvalues_offset. - To find a value by name in a large facet (plant, entity, and mine IDs run to thousands), pass
values_query: each facet keeps the values whoseid,name, oraliascontains it, ignoring case, andvalues_offsetpages the matches.values_query: "palo verde"onelectricity/facility-fuel'splantCodereturns6008and57773in one call. date_rangeis metadata for the default frequency; another frequency may extend outside it. Both response surfaces carry this caveat beside the unchanged start/end values.value_count,values_truncated, and, withvalues_query,values_matcheddescribe each window. A facet whose values EIA did not return is listed inunavailable_facets; the next call retries it.- Errors:
route_not_found,route_not_queryable,facet_not_found, and retryablefacet_unavailable,rate_limited,upstream_unreachable,upstream_timeout.
eia_search_routes tool
- Search route names, descriptions, STEO's 1,469 and SEDS's 968 series names, and facet values. Before any describe, the facet values cover fuel, sector, technology, and coal-rank vocabularies, RTO balancing authorities (
Electric Reliability Council of Texas→respondent: ERCO), and the series and product names of the daily and weekly petroleum and natural-gas routes (Henry Hub→natural-gas/pri/fut,series: RNGWHHD); describing a route adds its own.limitdefaults to 10; positive integers above 30 are clamped to 30 with requested/effective values disclosed, even on short or empty results.truncatedis true when more matches exist. Only the first 256 characters ofqueryare searched, with a notice when it is cut. - Matches carry
score(0 exact, 1 no match; above 0.72 is weak) and may carry afilter_hintforeia_query_route. CheckindexCompleteandindexGapsbefore trusting a short result set. - A legacy APIv1 series ID, as EIA's website and Excel add-in write them, is resolved through EIA's
/v2/seriesidlookup before any text search, in any case:ELEC.PRICE.TX-RES.Mreturns one result,electricity/retail-saleswithfilter_hint{sectorid: "RES", stateid: "TX"},frequency: "monthly", andcolumns: ["price"], ready foreia_query_route. A hit needs no index, so it answers in under a second even on a cold server. When EIA's own lookup reports no rows for the mapping it returns (some AEO IDs), the notice says the query will come back empty. An ID EIA does not recognize (PET.ZZZZZZ.W, orU.S.A, which only looks like one) returns the text search with a notice saying so; a lookup that times out, is rate-limited, or hits an outage does the same with a retry notice, never an error. - The first search builds the index and normally takes 20–40 s (at most 45 s); later searches answer from the built index. If EIA rate-limits, cannot be reached, or does not answer in time, the call fails with retryable
rate_limited,upstream_unreachable, orupstream_timeout.
eia_query_route tool
- Pass a leaf
route, with optional facet-IDfilters,columns,frequency,start/end, andsort. Page withoffsetandlength(default 100; positive integers above 5000 are clamped to 5000 with a notice). - Responses stay within 64,000 bytes: a page may end early at the last whole row that fits, and
next_offsetgives where to continue. A single row too large to fit is withheld, and the note gives the offset that skips it. - After describe has cached a complete facet vocabulary, query notices name unmatched filter values and suggest case-sensitive IDs when possible. Filters and their echoes stay unchanged; diagnostics never fetch metadata or facet values.
- With effective monthly
YYYY-MMdata, a year-only bound means January:end: "2021"stops at2021-01; use2021-12for the full year. Query notices disclose this for either bound without expanding it. - EIA reads each bound as the period of the effective frequency containing its first day, so
start: "2023-01", end: "2023"returns January at monthly. A range is reported as inverted (no_data) only when EIA returns no rows andstartselects a later period thanend; the message names both periods. Atlocal-hourlyeach bound needs its hour and UTC offset (2024-02-01T02-06); one without fails asinvalid_periodbefore any request. So does a bound naming a date or hour that does not exist in the shape its frequency reads exactly —2024-02-30atdailyorweekly,2024-02-01T24athourly— since EIA answers those with an HTTP 500 rather than rows. totalreports the full match count; values remain strings and units appear as{col}-units. Errors cover invalid route, facet, column, frequency, sort, or period, inverted ranges (no_data), a filter list too long for EIA's ~8 KB request URI (uri_too_long; split the values across calls), and retryablerate_limited,upstream_unreachable, andupstream_timeout.stage: truestages up toEIA_CANVAS_MAX_ROWS(default 25000) as adatasethandle for SQL viaeia_dataframe_describeandeia_dataframe_query(hyphens in column names become underscores). If staging fails,datasetis absent and the note says so. Staging that would run past the call deadline stops before the next page, stages the rows already fetched, and the note says time ran out and gives the offset to resume from.
eia_dataframe_describe tool
- Omit
nameto list active staged dataframes; pass a handle to inspect one. A miss returnsfound: falsewithactive_names. - Entries carry
source_tool,query_params,created_at,expires_at,row_count,truncated/max_rows, andcolumn_schema. Listing does not extend expiry.
eia_dataframe_query tool
- Run one read-only SQL SELECT against staged handles.
row_limitdefaults to 1000;previewnarrows the inline slice and accepts zero. Integers above 10000 on either input are clamped to 10000 with requested/effective values disclosed; the 64,000-byte per-surface budget can end rows sooner.register_asstill stages the complete result. truncated: truemeanstotalRowsis a cap, not a full count.register_asstages the complete result under an unused name with an exact count; the notice gives theORDER BY … LIMIT/OFFSETquery that pages the rest.- Cast EIA VARCHAR values for arithmetic. Writes, file reads, and system catalogs are rejected; declared failures include
missing_table,invalid_sql,non_select_statement,denied_function,multi_statement,sql_read_only,system_catalog_access, andregister_as_clash.
eia_dataframe_drop tool
- Pass
nameto remove a staged dataframe; requiresEIA_DATAFRAME_DROP_ENABLED=true. - Returns
dropped: falsewhen nothing matched; a failed drop returns retryablecanvas_drop_failedand the dataframe stays staged. Cleanup is optional: dataframes expire after 24 h unused.
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:
- Covers all 14 top-level dataset categories of EIA API v2, with the route tree cached in process
- Fuzzy search (Fuse.js) over route names/descriptions, all 1,469 STEO and 968 SEDS series names, and facet values — 1,753 benchmark petroleum and natural-gas series names and the RTO balancing authorities among them, indexed at warm time
- Facet values are fetched and cached per route by
eia_describe_route; a list EIA does not return is reported inunavailable_facetsand retried on the next call - Routes whose metadata could not be fetched are reported through
eia_search_routesinstead of dropped - DataCanvas (DuckDB) is on by default and staging is opt-in per call; with the canvas off the three dataframe tools are not callable
Agent-friendly output:
- Provenance —
eia_query_routeechoes the canonical route, and staged dataframes record theirsource_toolandquery_params - Capped-window disclosure — truncated responses report the count against the cap and a
noticenaming the next call - Typed failures — error
reasonvalues (e.g.route_not_queryable,invalid_facet,missing_table) carry arecoveryhint
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/mcp
Prerequisites
- 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.git
- Navigate into the directory:
cd eia-energy-mcp-server
- Install dependencies:
bun install
- Configure 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 |
|---|---|---|
EIA_API_KEY | Required. Free API key from api.eia.gov — appended as api_key on every request. | — |
EIA_BASE_URL | EIA API base URL. | https://api.eia.gov/v2 |
EIA_DATASET_TTL_SECONDS | Dataframe lifetime in seconds; each eia_dataframe_query that references a dataframe extends it. | 86400 (24 h) |
EIA_DATAFRAME_DROP_ENABLED | Set to true to expose eia_dataframe_drop (no effect with CANVAS_PROVIDER_TYPE=none). Off by default to avoid accidental canvas cleanup. | false |
EIA_CANVAS_MAX_ROWS | Row ceiling for eia_query_route staging. Reaching it costs five more requests: about 8.5 s on narrow routes such as electricity/retail-sales, 30–46 s on wide ones such as electricity/facility-fuel. Staging that would run past the call deadline stops early and names the offset to resume from. | 25000 |
EIA_FACET_VALUE_CAP | Facet values eia_describe_route returns per facet before truncating; page with facet and values_offset, or narrow with values_query. | 50 |
CANVAS_PROVIDER_TYPE | DataCanvas provider. duckdb enables eia_dataframe_describe, eia_dataframe_query, and stage: true; drop also needs EIA_DATAFRAME_DROP_ENABLED=true. none disables DataCanvas. The Claude Desktop bundle sets none. | duckdb |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_HTTP_ENDPOINT_PATH | HTTP endpoint path. | /mcp |
MCP_SESSION_MODE | HTTP sessions: stateless, stateful, or auto. | stateless |
MCP_PUBLIC_URL | Public origin override for TLS-terminating reverse-proxy deployments. | — |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
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:http -
Run 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-server
The 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 |
|---|---|
src/index.ts | createApp() entry point — registers tools and inits services. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts) — browse, describe, search, query, and three DataCanvas dataframe tools. |
src/services/eia | EIA API v2 service — route tree cache, Fuse.js index, facet fan-out, HTTP client. |
src/services/canvas-bridge | DataCanvas bridge — registers EIA query results as DuckDB dataframes, routes SQL queries. |
tests/ | Unit and integration tests mirroring src/. |
docs/ | Design documents (design.md, idea.md). |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Always call
eia_describe_routebeforeeia_query_route— facet values require a separate API fan-out and are not embedded in route metadata - Wrap 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 test
License
Apache-2.0 — see LICENSE for details.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.