
Connects Claude to the U.S. Energy Information Administration's API v2, covering electricity, petroleum, natural gas, coal, and forecasts from STEO, AEO, and other major datasets. You browse the EIA's route taxonomy from root categories down to leaf datasets, describe facets and valid filter values, then pull data with facet filters, date ranges, and frequency selection. Large result sets spill to DuckDB-backed DataCanvas tables for SQL analysis. Includes fuzzy search across 1,469 STEO series names and all route metadata, making it easy to turn natural language queries like "Texas residential electricity prices" into the right API route. Requires a free EIA API key. Built on the author's mcp-ts-core framework with pluggable transports, storage backends, and OpenTelemetry tracing.
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
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.
| 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. |
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 are callable and listed by MCP only when CANVAS_PROVIDER_TYPE=duckdb is set; eia_dataframe_drop additionally requires EIA_DATAFRAME_DROP_ENABLED=true. Disabled definitions remain in the server manifest with their enable hints. A default deployment lists the first four tools.
Browse accepts route as an alias for path; describe and query accept path as an alias for route. Supplying both spellings is rejected. Leading, trailing, and repeated slashes are normalized.
eia_browse_routes toolpath for the 14 top-level categories; pass a path to browse its children.isLeaf: describe and query leaves, or browse categories further. A missing path returns route_not_found; EIA rate-limiting or unreachable while the route tree builds returns retryable rate_limited or upstream_unreachable.eia_describe_route toolroute for facets, data columns, frequencies, and date range. Each facet window is capped at EIA_FACET_VALUE_CAP (default 50); page with facet and values_offset.value_count and values_truncated describe each window. A facet whose value list EIA did not return is named in unavailable_facets instead, and the next call re-fetches only that list. Errors distinguish route_not_found, route_not_queryable, facet_not_found, and retryable facet_unavailable, rate_limited, and upstream_unreachable (EIA could not be reached).eia_search_routes toollimit defaults to 10; one call returns at most 30, and a larger limit (up to 100) is served at 30. truncated is true only when more matches exist than were returned.retail sales for electricity/retail-sales) ranks that route first.score (0 exact, 1 no match) and may carry a filter_hint for eia_query_route. Scores above 0.72 are weak matches; check indexComplete and indexGaps before trusting a short result set. The next search retries an index pass listed in indexGaps.rate_limited or upstream_unreachable.eia_query_route toolroute, with optional facet-ID filters, columns, frequency, start/end, and sort. Page with offset and length (default 100, max 5000).length rows are fetched, but the page ends early at the last whole row that fits, and next_offset names where the next page starts. A row too large to fit on its own is withheld, and the note gives the offset that skips it.total reports the full match count; values remain strings and units appear as {col}-units. Errors distinguish 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 retryable rate_limited and upstream_unreachable (EIA could not be reached).stage: true accumulates up to EIA_CANVAS_MAX_ROWS (default 25000) as a dataset handle; its canvas_preview_note names eia_dataframe_describe for the staged column names (hyphens become underscores) and then eia_dataframe_query for SQL. Omitted, the call fetches one page. A stage the canvas could not register leaves dataset absent and says so in the note.eia_dataframe_describe toolname to list active staged dataframes; pass a handle to inspect one. A miss returns found: false with active_names.source_tool, query_params, created_at, expires_at, row_count, truncated/max_rows, and column_schema. Listing does not extend expiry.eia_dataframe_query toolrow_limit defaults to 1000 and caps at 10000; preview narrows the inline slice, and the 64,000-byte per-surface budget can end it sooner.truncated: true means totalRows is the materialization cap, not a full count. register_as stages the complete result under an unused name with an exact count and fresh expiry; when the budget ends the inline rows, the notice gives the ORDER BY … LIMIT/OFFSET query that pages the rest from that table.missing_table, invalid_sql, non_select_statement, system_catalog_access, and register_as_clash.eia_dataframe_drop toolname to remove a staged dataframe; requires EIA_DATAFRAME_DROP_ENABLED=true.dropped: false when nothing matched. A drop the canvas cannot perform fails as retryable canvas_drop_failed, leaving the dataframe staged and listed. Manual cleanup is optional: the default 24 h expiry extends whenever a query references the dataframe.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:
eia_describe_route fetches each facet's value list by fan-out and caches them per route; a list EIA does not return is named in unavailable_facets and re-fetched alone on the next describe, never cached as an empty facet. eia_query_route sends filters to EIA as given and fetches no facet valueseia_search_routes rather than silently dropped — and re-fetched on the next eia_browse_routes call that reaches itAgent-friendly output:
eia_query_route echoes the canonical, slash-normalized route rather than the caller's spelling, and every staged dataframe records its source_tool and query_paramsnotice naming the exact next call to page past itreason values (e.g. route_not_queryable, invalid_facet, missing_table) each carry a recovery hint naming the next tool callA 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"
}
}
}
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
DEMO_KEY hits rate limits quickly; a real key is required for sustained use.git clone https://github.com/cyanheads/eia-energy-mcp-server.git
cd eia-energy-mcp-server
bun install
cp .env.example .env
# edit .env and set required vars (at minimum, EIA_API_KEY)
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 | Sliding per-dataframe TTL in seconds. The window is extended every time an eia_dataframe_query statement references the dataframe, so a dataframe stays alive through a long analysis and lapses only once it goes unused for the full interval. Listing it with eia_dataframe_describe is not use and does not extend it. | 86400 (24 h) |
EIA_DATAFRAME_DROP_ENABLED | Set to true to expose eia_dataframe_drop, which also requires CANVAS_PROVIDER_TYPE=duckdb. Off by default to avoid accidental canvas cleanup. | false |
EIA_CANVAS_MAX_ROWS | Cumulative row ceiling for eia_query_route canvas staging — five requests at EIA's 5,000-row-per-request ceiling, adding ~8.5 s to a call when it binds. Lower it for snappier exploration, raise it for wider staged analyses. | 25000 |
EIA_FACET_VALUE_CAP | Facet values eia_describe_route returns per facet before truncating. Bounds the response on high-cardinality facets — STEO's seriesId alone has 1,469 values. Page past it with the tool's facet and values_offset inputs. | 50 |
CANVAS_PROVIDER_TYPE | Set to duckdb to enable DataCanvas (Node only). Adds the three eia_dataframe_* tools to the surface and lets eia_query_route stage rows when called with stage: true. | — |
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 (framework schema default, resolves to stateful). This server defaults to stateless; an explicit environment value overrides it. | 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.
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 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.
| 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). |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storageeia_describe_route before eia_query_route — facet values require a separate API fan-out and are not embedded in route metadataIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
EIA_API_KEY*Free API key from api.eia.gov. Required — all requests use this as the api_key query parameter.
EIA_DATASET_TTL_SECONDSdefault: 86400Per-table TTL for DataCanvas dataframes in seconds.
EIA_DATAFRAME_DROP_ENABLEDdefault: falseSet 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_LEVELdefault: infoSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
MCP_HTTP_HOSTdefault: 127.0.0.1The hostname for the HTTP server.
MCP_HTTP_PORTdefault: 3010The port to run the HTTP server on.
MCP_HTTP_ENDPOINT_PATHdefault: /mcpThe 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_MODEdefault: noneAuthentication mode to use: 'none', 'jwt', or 'oauth'.