
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
Four route tools cover the two-phase EIA workflow — find the right dataset route, then pull the data. Three DataCanvas tools add SQL over staged results and are listed only where a canvas is configured (CANVAS_PROVIDER_TYPE=duckdb); eia_dataframe_drop needs its own opt-in on top. A default deployment therefore advertises four tools, all of them working:
| Tool | Description |
|---|---|
eia_browse_routes | Lists child routes under a given path in the EIA dataset taxonomy. Start at root to see top-level categories, then drill into subcategories and leaf routes. |
eia_describe_route | Returns metadata for a leaf route: available facets with valid values, data column names, frequency options, units, and date range. Call before eia_query_route to understand filter options. Facet values come back capped, with facet and values_offset to page one facet. |
eia_search_routes | Fuzzy text search across route names, descriptions, category labels, STEO series names, and facet values. Resolves natural-language queries like "electricity retail sales by state" or a fuel type like "wind" to matching route paths. |
eia_query_route | Fetches data from a leaf route with optional facet filters, date range, frequency, and column selection. Returns a preview; pass stage: true to also page past it and stage the matching rows as a DataCanvas table for SQL analysis. |
eia_dataframe_describe | Lists active DataCanvas dataframes created by prior eia_query_route calls that passed stage: true. Only exposed when a canvas is configured. Shows table name, column names and types, row count, expiry, and the query that produced it. A handle that is not staged comes back as a miss alongside the handles that are. |
eia_dataframe_query | Runs a read-only SQL SELECT across DataCanvas dataframes, referenced by their df_<id> table names. Only exposed when a canvas is configured. |
eia_dataframe_drop | Drops a DataCanvas dataframe, freeing its memory. Only exposed when a canvas is configured and EIA_DATAFRAME_DROP_ENABLED=true. |
eia_browse_routesWalk the EIA dataset taxonomy from root to leaf.
eia_describe_routeSTEO (Short-Term Energy Outlook) is a flat leaf with 1,469 named series — no sub-routeseia_describe_routeFull schema for a leaf route. Required before constructing facet filters.
EIA_FACET_VALUE_CAP values, alongside value_count and values_truncated. Pass facet with values_offset to page one facet past the cap — the cap shapes this tool's response only, and the in-process cache keeps every valuecontent[] renders every value structuredContent carries, so both name the same next callvalues_offset applies to every facet in the response. One past a facet's last value empties that facet's window and returns a notice naming the facet and its value_count, so an overshoot never reads like a fully enumerated facetcontent[] a value reads as id=name (alias), with the alias left off when it only restates the pair — EIA supplies (IN) Indiana beside IN=Indiana on most values. An alias that adds something, such as Region: (MAT) Middle Atlantic, still prints, and the alias field itself is unchangedname is labelled from its alias, then from its id — the id is what filters, so the value is kept. A value EIA sends without an id is dropped, having nothing to filter witheia_search_routes and eia_browse_routes resolve the route path; this tool provides the filter vocabularyeia_search_routesFuzzy search across the in-memory route index.
filter_hint to pass straight to eia_query_routescore runs 0 (exact) to 1 (no match), lower is better; above 0.72 the match is unreliable and the query is worth narrowing. bun run eval:search scores a labelled query battery against a live corpus, which is where that number comes fromindexComplete reports whether the answer was ranked against the whole corpus; when it is false, indexGaps names the routes and index passes that are missing, so a short result set is never mistaken for a settled oneeia_query_routePull data from a leaf route.
{ "stateid": "TX", "sectorid": ["RES", "COM"] })eia_describe_routeoffset/length (max 5,000 rows per page); total row count in response{col}-units fields per row/electricity/retail-sales/ resolves to the same route, and the response echoes the canonical form backstage: true: further pages are fetched and the accumulated rows are staged as a dataset (df_<id>) for SQL, bounded by EIA_CANVAS_MAX_ROWS. The response note names how many rows actually reached the table. Left off (the default), a query costs one upstream request however large the match is, and the note names stage: true as the way to reach the rest.Built on @cyanheads/mcp-ts-core:
none, jwt, oauthin-memory, filesystem, Supabase, Cloudflare KV/R2/D1EIA-specific:
eia_search_routes, and re-fetched by the next eia_browse_routes call that reaches itPromise.all fan-out — valid filter values available without re-fetchingGet 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_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 |
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
| 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 and pull requests 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'.