Browse and query the EIA API v2 — electricity, petroleum, natural gas, coal, forecasts via MCP.
One address for any MCP client. No sign-in needed on this server.
6 read
Grouped by tool name. At most: only reads or looks up information.
Odel charges nothing per call. The publisher may charge for its own service.
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
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.
| 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.
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 toolpath for the 14 top-level categories; pass a path to browse its children.isLeaf: describe and query leaves, or browse categories further.route_not_found; EIA rate-limiting, being unreachable, or not answering in time returns retryable rate_limited, upstream_unreachable, or upstream_timeout.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.values_query: each facet keeps the values whose id, name, or alias contains it, ignoring case, and values_offset pages the matches. values_query: "palo verde" on electricity/facility-fuel's plantCode returns 6008 and 57773 in one call.date_range is 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, with values_query, values_matched describe each window. A facet whose values EIA did not return is listed in unavailable_facets; the next call retries it.route_not_found, route_not_queryable, facet_not_found, and retryable facet_unavailable, rate_limited, upstream_unreachable, upstream_timeout.eia_search_routes toolElectric 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. limit defaults to 10; positive integers above 30 are clamped to 30 with requested/effective values disclosed, even on short or empty results. truncated is true when more matches exist. Only the first 256 characters of query are searched, with a notice when it is cut.score (0 exact, 1 no match; above 0.72 is weak) and may carry a filter_hint for eia_query_route. Check indexComplete and indexGaps before trusting a short result set./v2/seriesid lookup before any text search, in any case: ELEC.PRICE.TX-RES.M returns one result, electricity/retail-sales with filter_hint {sectorid: "RES", stateid: "TX"}, frequency: "monthly", and columns: ["price"], ready for eia_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, or U.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.rate_limited, upstream_unreachable, or upstream_timeout.eia_query_route toolroute, with optional facet-ID filters, columns, frequency, start/end, and sort. Page with offset and length (default 100; positive integers above 5000 are clamped to 5000 with a notice).next_offset gives where to continue. A single row too large to fit is withheld, and the note gives the offset that skips it.YYYY-MM data, a year-only bound means January: end: "2021" stops at 2021-01; use 2021-12 for the full year. Query notices disclose this for either bound without expanding it.start: "2023-01", end: "2023" returns January at monthly. A range is reported as inverted (no_data) only when EIA returns no rows and start selects a later period than end; the message names both periods. At local-hourly each bound needs its hour and UTC offset (2024-02-01T02-06); one without fails as invalid_period before any request. So does a bound naming a date or hour that does not exist in the shape its frequency reads exactly — 2024-02-30 at daily or weekly, 2024-02-01T24 at hourly — since EIA answers those with an HTTP 500 rather than rows.total reports 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 retryable rate_limited, upstream_unreachable, and upstream_timeout.stage: true stages up to EIA_CANVAS_MAX_ROWS (default 25000) as a dataset handle for SQL via eia_dataframe_describe and eia_dataframe_query (hyphens in column names become underscores). If staging fails, dataset is 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 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; preview narrows 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_as still stages the complete result.truncated: true means totalRows is a cap, not a full count. register_as stages the complete result under an unused name with an exact count; the notice gives the ORDER BY … LIMIT/OFFSET query that pages the rest.missing_table, invalid_sql, non_select_statement, denied_function, multi_statement, sql_read_only, 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 failed drop returns retryable canvas_drop_failed and the dataframe stays staged. Cleanup is optional: dataframes expire after 24 h unused.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; a list EIA does not return is reported in unavailable_facets and retried on the next calleia_search_routes instead of droppedAgent-friendly output:
eia_query_route echoes the canonical route, and staged dataframes record their source_tool and query_paramsnotice naming the next callreason values (e.g. route_not_queryable, invalid_facet, missing_table) carry a recovery hintA 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 | 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.
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.