Browse and query the EIA API v2 — electricity, petroleum, natural gas, coal, forecasts via MCP.
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 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 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.
eia_browse_routes toolpath for 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 subcategoriesisLeaf — leaf routes are queryable via eia_describe_route / eia_query_route; non-leaf routes have further children to browsesteo is a flat leaf with 1,469 named series and no sub-routesroute as an alias for path; supplying both is rejected. Leading, trailing, and doubled slashes are stripped before resolvingroute_not_found when the path does not exist in the taxonomyeia_describe_route toolroute; accepts path as an alias, but not alongside routeEIA_FACET_VALUE_CAP values (default 50), with value_count and values_truncated; page one facet with facet + values_offsetvalues_offset past a facet's last value returns an empty window plus a notice naming the facet and its value_count, rather than reading as an exhausted enumerationroute_not_found, route_not_queryable (category node, not a leaf), facet_not_found, rate_limited (retryable)eia_search_routes toollimit caps results (default 10, max 30)score runs 0 (exact) to 1 (no match); above 0.72 is a weak match — narrow the query or use eia_browse_routesfilter_hint, a ready-to-use filter object for eia_query_routeindexComplete / indexGaps report whether the corpus was complete when scored — check before trusting a short result seteia_query_route toolroute (or alias path, never both), facet filters keyed by facet ID (from eia_describe_route), plus optional columns, frequency, start/end, and sortoffset/length page the inline preview (length default 100, max 5000 per EIA's per-request ceiling); total reports the full match count{col}-units fieldsstage: true pages past the preview and stages the accumulated rows as a DataCanvas df_<id> table (bounded by EIA_CANVAS_MAX_ROWS, default 25000) for eia_dataframe_query; omitted, the call costs one upstream request regardless of totalroute_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 tooleia_query_route calls with stage: true; only registered when CANVAS_PROVIDER_TYPE=duckdbname to list every active dataframe for the tenant; pass name to check one — a miss comes back as found: false alongside active_names, never as an empty listsource_tool, query_params, created_at, expires_at, row_count, truncated / max_rows, and column_schemaeia_dataframe_query statement referencing it doescanvas_unavailable when no canvas is configuredeia_dataframe_query tooldf_<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, so totalRows becomes the cap, not a true total; preview separately narrows the inline slice without affecting the countregister_as persists the result as a new dataframe with a fresh expiry; the name must be unusedCAST(col AS DOUBLE) for arithmeticcanvas_unavailable, system_catalog_access, missing_table, non_select_statement, invalid_sql, register_as_clasheia_dataframe_drop toolname; idempotent — returns dropped: false when nothing matchedEIA_DATAFRAME_DROP_ENABLED=true and CANVAS_PROVIDER_TYPE=duckdbcanvas_unavailable when no canvas is configuredBuilt 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:
Promise.all) and cached, so eia_query_route filters are validated without re-fetchingeia_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.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/eia-energy-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-eia-energy-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/eia-energy-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/eia-energy-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.