eia-energy-mcp-server
An MCP server that allows agents to browse, search, and query the U.S. Energy Information Administration (EIA) API v2. It provides tools to explore dataset taxonomies, retrieve metadata for specific routes, and fetch time-series data for electricity, petroleum, natural gas, coal, and forecasts. It also supports staging results for SQL queries via DataCanvas.
Endpoint and access
No sign-in to connect and list tools. Individual tools may still need a vendor account, credentials, or permissions; check the documented prerequisites.
https://eia-energy.caseyjhand.com/mcpWhat its tools do
6 tools, grouped by the hints the server advertises. Hints are self-declared, not verified behavior.
Checks and signals
Each line is separate evidence, not a trust score.
- Passed Endpoint answered the live check ·
- Availability 1 of 1 checks passed in 30 days
What you can do with it
- Browse EIA dataset taxonomy to find relevant energy data categories
- Search for specific energy routes and facets using fuzzy text search
- Query time-series data with facet filters and date ranges
- Run SQL queries against staged energy dataframes
Source and maintenance
- Maintenance checked
Protocol and capabilities
- Server
- eia-energy-mcp-server 0.5.1
- MCP protocol negotiated in check
- 2025-11-25
- Tools
- 6 advertised
- Resources
- 1 advertised
- Resource templates
- 1 advertised
- Prompts
- 0 advertised
{
"tools": {
"listChanged": true
},
"logging": {},
"prompts": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
},
"extensions": {
"io.modelcontextprotocol/skills": {
"directoryRead": true
}
}
}Server-provided instructions
Untrusted server output shown for inspection. It is not Koskamo guidance and is excluded from knowledge search.
Tools
6 advertised
eia_browse_routes — Browse EIA Routes
Lists child routes under a given path in the EIA dataset taxonomy. Start with no path to get the 14 top-level categories (electricity, petroleum, natural-gas, steo, aeo, ieo, seds, etc.), then drill into subcategories. Each result includes an isLeaf flag — leaf routes are queryable endpoints; non-leaf routes have children to browse. When isLeaf is true on the browsed path itself, switch to eia_describe_route. The first call after server start waits up to 20s for the route tree to warm, then answers from EIA directly.
{
"type": "object",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"path": {
"type": "string",
"description": "Route path to browse (e.g. \"electricity\", \"petroleum/pri\"). Omit for root. Whitespace and slashes are normalized, then one complete leading v2 segment is removed: \"/v2/electricity/retail-sales/\" resolves to \"electricity/retail-sales\", and \"/v2/\" to root. Full URLs and /data suffixes are not normalized."
}
},
"additionalProperties": false
}{
"type": "object",
"anyOf": [
{
"not": {
"required": [
"error"
]
},
"required": [
"path",
"children",
"isLeaf"
]
},
{
"required": [
"error"
]
}
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"path": {
"type": "string",
"description": "The path that was browsed (empty string for root)."
},
"error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "JSON-RPC error code for this failure."
},
"data": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"examples": [
"upstream_invalid_response",
"upstream_unavailable",
"upstream_timeout",
"route_not_found",
"rate_limited",
"upstream_unreachable"
],
"description": "Machine-readable failure mode. Declared by this tool: `upstream_invalid_response`: EIA returned an unusable response body or a missing route or data envelope. `upstream_unavailable`: EIA answered with a retryable HTTP service failure. `upstream_timeout`: EIA did not answer in time: one attempt passed 45 s, or the call passed its 56 s deadline across retries. `route_not_found`: Path does not exist in the EIA taxonomy. `rate_limited`: EIA rate limit hit (OVER_RATE_LIMIT) while building the route tree on the first call after server start, on the live fetch of the browsed path a cold call falls back to, or while re-fetching a path the warm could not fetch. `upstream_unreachable`: EIA could not be reached (connection refused or reset, DNS failure) while building the route tree on the first call after server start, on the live fetch of the browsed path a cold call falls back to, or while re-fetching a path the warm could not fetch. Other values are possible when a failure originates below the handler."
},
"recovery": {
"type": "object",
"required": [
"hint"
],
"properties": {
"hint": {
"type": "string"
}
},
"description": "Actionable next step for the caller.",
"additionalProperties": {}
},
"retryable": {
"type": "boolean",
"description": "Whether retrying may succeed."
}
},
"additionalProperties": {}
},
"message": {
"type": "string",
"description": "Human-readable description of what went wrong."
}
},
"description": "Present when the call failed. Absent on success.",
"additionalProperties": {}
},
"isLeaf": {
"type": "boolean",
"description": "True when the browsed path itself is a leaf route — no children to drill into; use eia_describe_route instead."
},
"children": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"name",
"description",
"route",
"isLeaf"
],
"properties": {
"id": {
"type": "string",
"description": "Route segment ID."
},
"name": {
"type": "string",
"description": "Human-readable name."
},
"route": {
"type": "string",
"description": "Full route path usable in eia_describe_route or eia_query_route."
},
"isLeaf": {
"type": "boolean",
"description": "True when this child is a queryable leaf route with data."
},
"description": {
"type": "string",
"description": "Route description."
}
},
"description": "A child route entry.",
"additionalProperties": false
},
"description": "Child entries under the browsed path."
}
},
"additionalProperties": false
}{
"readOnlyHint": true,
"openWorldHint": false
}eia_describe_route — Describe EIA Route
Returns metadata for a leaf route: available facets with their valid values, data column names and units, frequency options, and date range. Call this before eia_query_route to discover valid facet IDs, facet values, column IDs, and frequency codes. Each facet returns a capped window of its values with value_count and values_truncated alongside; pass facet and values_offset to page through the rest of one facet. To find a value by name in a large facet (plant, entity, and mine IDs run to thousands), pass values_query: each facet keeps only the values whose id, name, or alias contains it — on a balancing-authority facet, also a value whose ISO/RTO acronym does, so "ERCOT" finds ERCO — values_matched counts them, and values_offset pages the matches. A values_offset past the last value of a facet returns an empty window for it and a notice naming the count to page against. Facet values are fetched from separate EIA endpoints, merged, and cached per route for 24 hours, after which the next call fetches the route again; a facet whose values EIA did not return is left out of facets, named in unavailable_facets, and fetched again by the next call. When a refresh fails, the cached values are served instead, with a notice naming when they were fetched.
{
"type": "object",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": [
"route"
],
"properties": {
"facet": {
"type": "string",
"minLength": 1,
"description": "Restrict the response to one facet by ID (e.g. \"stateid\"). Use with values_offset to page a facet whose values were truncated, or with values_query to find a value in it. Omit to get every facet."
},
"route": {
"type": "string",
"minLength": 1,
"description": "Leaf route path (e.g. \"electricity/retail-sales\", \"steo\"). Discoverable via eia_browse_routes or eia_search_routes. Whitespace and slashes are normalized, then one complete leading v2 segment is removed: \"/v2/electricity/retail-sales/\" resolves to \"electricity/retail-sales\". Full URLs and /data suffixes are not normalized."
},
"values_query": {
"type": "string",
"minLength": 1,
"description": "Keep only the facet values whose id, name, or alias contains this text, ignoring case (e.g. \"palo verde\" on plantCode). On the balancing-authority facets of electricity routes (respondent, fromba, toba, parent, balancing_authority_code), a value also matches when its ISO/RTO acronym contains the text: \"ERCOT\" finds ERCO, \"CAISO\" finds CISO, \"SPP\" finds SWPP and SWPW. A plain substring match, not a pattern or a ranked search. Applied to every facet in the response, before values_offset and the cap. Leading and trailing whitespace is ignored. Omit to list every value."
},
"values_offset": {
"type": "integer",
"default": 0,
"maximum": 9007199254740991,
"minimum": 0,
"description": "Index of the first facet value to return, applied to every facet in the response. Use the value named in a truncation hint to continue past the cap. With values_query, indexes the matching values."
}
},
"additionalProperties": false
}{
"type": "object",
"anyOf": [
{
"not": {
"required": [
"error"
]
},
"required": [
"route",
"description",
"facets",
"values_offset",
"data_columns",
"frequencies",
"date_range",
"default_frequency",
"default_date_format"
]
},
{
"required": [
"error"
]
}
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "JSON-RPC error code for this failure."
},
"data": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"examples": [
"upstream_invalid_response",
"upstream_unavailable",
"upstream_timeout",
"route_not_found",
"route_not_queryable",
"facet_not_found",
"facet_unavailable",
"rate_limited",
"upstream_unreachable"
],
"description": "Machine-readable failure mode. Declared by this tool: `upstream_invalid_response`: EIA returned an unusable response body or a missing route or data envelope. `upstream_unavailable`: EIA answered with a retryable HTTP service failure. `upstream_timeout`: EIA did not answer the route metadata request or the route-tree build in time: one attempt passed 45 s, or the call passed its 56 s deadline across retries. A value list that times out is named in unavailable_facets instead. `route_not_found`: Route does not exist in the EIA taxonomy. `route_not_queryable`: Route is a category node with sub-routes, not a queryable leaf. `facet_not_found`: The facet input names an ID the route does not expose. `facet_unavailable`: The facet input names a facet whose value list EIA did not return after retries. `rate_limited`: EIA rate limit hit on the route metadata request, or on the value list the facet input names. `upstream_unreachable`: EIA could not be reached (connection refused or reset, DNS failure) on the route metadata request or the route-tree build. A value list the facet input names fails as facet_unavailable instead. Other values are possible when a failure originates below the handler."
},
"recovery": {
"type": "object",
"required": [
"hint"
],
"properties": {
"hint": {
"type": "string"
}
},
"description": "Actionable next step for the caller.",
"additionalProperties": {}
},
"retryable": {
"type": "boolean",
"description": "Whether retrying may succeed."
}
},
"additionalProperties": {}
},
"message": {
"type": "string",
"description": "Human-readable description of what went wrong."
}
},
"description": "Present when the call failed. Absent on success.",
"additionalProperties": {}
},
"route": {
"type": "string",
"description": "The route path described."
},
"facets": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"description",
"values",
"value_count",
"values_truncated"
],
"properties": {
"id": {
"type": "string",
"description": "Facet ID — use as key in the filters parameter of eia_query_route (e.g. filters: { \"stateid\": \"TX\" })."
},
"values": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "string",
"description": "Facet value ID — use as filter value."
},
"name": {
"type": "string",
"description": "Human-readable name. Falls back to the alias, then to the id, on the values EIA sends without one."
},
"alias": {
"type": "string",
"description": "Short alias, when provided by EIA."
}
},
"description": "A valid facet value.",
"additionalProperties": false
},
"description": "Valid values for this facet dimension, starting at values_offset and capped at the window size this server sets. With values_query, only the matching values, in upstream order."
},
"description": {
"type": "string",
"description": "Facet description."
},
"value_count": {
"type": "number",
"description": "Total values this facet has upstream, independent of the returned window and of values_query."
},
"values_matched": {
"type": "number",
"description": "Values of this facet whose id, name, or alias contains values_query — or, on a balancing-authority facet, whose ISO/RTO acronym does — what values_offset indexes and values_truncated is measured against. One ID listed under two historical names counts twice. Present only when values_query is set."
},
"values_truncated": {
"type": "boolean",
"description": "True when values stops short of values_matched when values_query is set, otherwise of value_count. Call eia_describe_route again with this facet ID, the same values_query, and values_offset set to values_offset + values.length for the next page."
}
},
"description": "A filterable dimension for this route.",
"additionalProperties": false
},
"description": "Filterable dimensions. Each facet has an ID and a window of its valid values. Restricted to one entry when the facet input is set. A facet named in unavailable_facets is left out."
},
"notice": {
"type": "string",
"description": "Default-frequency date-range context, composed with the row field carrying the unit of each data column the route metadata gives no unit, the facet value lists EIA publishes partial, and guidance for exhausted facet windows, a values_query no value matched, unavailable facet values, or cached values served because their refresh failed."
},
"date_range": {
"type": "object",
"required": [
"start",
"end"
],
"properties": {
"end": {
"type": "string",
"description": "Metadata end period for the default frequency; another frequency may extend later."
},
"start": {
"type": "string",
"description": "Metadata start period for the default frequency; another frequency may extend earlier."
}
},
"description": "Route metadata date range in the default-frequency context, not a bound across all frequencies.",
"additionalProperties": false
},
"description": {
"type": "string",
"description": "Human-readable description of the dataset."
},
"frequencies": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"description",
"query",
"format"
],
"properties": {
"id": {
"type": "string",
"description": "Frequency ID (e.g. \"monthly\", \"annual\")."
},
"query": {
"type": "string",
"description": "API query value for this frequency."
},
"format": {
"type": "string",
"description": "Period format string (e.g. \"YYYY-MM\", \"YYYY\")."
},
"description": {
"type": "string",
"description": "Human-readable description."
}
},
"description": "A frequency option for eia_query_route.",
"additionalProperties": false
},
"description": "Valid frequency options for eia_query_route."
},
"data_columns": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"alias",
"units"
],
"properties": {
"id": {
"type": "string",
"description": "Column ID — use in the columns parameter of eia_query_route."
},
"alias": {
"type": "string",
"description": "Human-readable column alias."
},
"units": {
"type": "string",
"description": "Measurement units (e.g. \"cents per kilowatt-hour\"). An empty string means the route metadata names no unit for this column; the notice then names the eia_query_route row field that holds its unit when a row has one, and a column whose rows have no such field has no unit."
}
},
"description": "A data column available for this route.",
"additionalProperties": false
},
"description": "Data columns available for this route."
},
"values_query": {
"type": "string",
"description": "The values_query each facet was narrowed by, as matched after trimming. Absent when no values_query was sent."
},
"values_offset": {
"type": "number",
"description": "Index of the first facet value returned, echoing the requested offset."
},
"default_frequency": {
"type": "string",
"description": "Default frequency ID used when none is specified."
},
"unavailable_facets": {
"type": "array",
"items": {
"type": "string",
"description": "Facet ID whose value list EIA did not return."
},
"description": "Facet IDs whose value lists EIA did not return after retries, left out of facets. Each ID still filters eia_query_route — only its values are missing. Call eia_describe_route again to re-fetch these lists alone. Absent when every facet is in facets."
},
"default_date_format": {
"type": "string",
"description": "Period format for the default frequency (e.g. \"YYYY-MM\")."
}
},
"additionalProperties": false
}{
"readOnlyHint": true,
"openWorldHint": false
}eia_search_routes — Search EIA Routes
Fuzzy text search across route names, descriptions, category labels, and the names of the data columns a route publishes. Resolves natural-language queries like "electricity retail sales by state" or "natural gas imports" to matching route paths, and a measure named only by its column — "nameplate capacity", "gross generation" — to the route that publishes it. Multi-term queries are also matched term by term, so combining a commodity, a metric, and a sector — "electricity price residential", "coal generation industrial sector" — reaches the route carrying that data even when no single entry reads like the whole phrase. A query spelling the end of a route path in words — "retail sales" for electricity/retail-sales — ranks that route first. STEO, SEDS, and Monthly Energy Review (total-energy) series names are indexed so queries like "crude oil production forecast", "motor gasoline total consumption", or "primary energy consumption total" also resolve — a Monthly Energy Review series also by its ID, such as "TETCBUS" — and so are facet values, so a fuel type or sector term like "wind" or "anthracite coal" resolves to the route that exposes it, with filter_hint carrying the filter to pass on. That includes the series and product names of the daily and weekly petroleum and natural-gas routes and the RTO balancing authorities, so benchmarks like "Henry Hub", "WTI crude spot price", "Cushing", or "Electric Reliability Council of Texas" resolve before any describe; a balancing authority also answers to its EIA code or its ISO/RTO acronym ("ERCO", "ERCOT", "CAISO", "SPP"). A legacy APIv1 series ID as the EIA website and Excel add-in write them — "PET.RWTC.W", "ELEC.PRICE.TX-RES.M" — is resolved through the EIA series lookup into one result with its route, filter_hint, frequency, and columns, ready for eia_query_route, without waiting on the index. Results include isLeaf so you know whether to browse further or query directly. Results with score > 0.72 are weak matches — try a more specific query or use eia_browse_routes to explore the taxonomy. The first call after server start waits 20-40s while the index warms, and at most 45s; later calls answer from the built index. Check indexComplete before reading anything into a short or empty result set.
{
"type": "object",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": [
"query"
],
"properties": {
"limit": {
"type": "number",
"default": 10,
"minimum": 1,
"multipleOf": 1,
"description": "Maximum results to return. Default 10; a larger value returns at most 30 and says so. truncated says whether more matches exist."
},
"query": {
"type": "string",
"minLength": 1,
"description": "Free-text search terms to match against route names and descriptions, or a legacy APIv1 series ID (e.g. \"PET.RWTC.W\": three or more dot-separated segments ending in a frequency code A, Q, M, W, D, H, HL, or 4; any case). Leading and trailing whitespace is ignored, and only the first 256 characters are searched."
}
},
"additionalProperties": false
}{
"type": "object",
"anyOf": [
{
"not": {
"required": [
"error"
]
},
"required": [
"results",
"effectiveQuery",
"totalIndexed",
"indexComplete",
"truncated",
"shown",
"cap"
]
},
{
"required": [
"error"
]
}
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"cap": {
"type": "number",
"description": "Most results this call could return — limit, or 30 when limit was larger."
},
"error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "JSON-RPC error code for this failure."
},
"data": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"examples": [
"upstream_invalid_response",
"upstream_unavailable",
"upstream_timeout",
"rate_limited",
"upstream_unreachable"
],
"description": "Machine-readable failure mode. Declared by this tool: `upstream_invalid_response`: EIA returned an unusable response body or a missing route or data envelope. `upstream_unavailable`: EIA answered with a retryable HTTP service failure. `upstream_timeout`: EIA did not answer in time while the first call after server start built the route tree. An index pass that times out does not fail the search; it is reported in indexGaps. `rate_limited`: EIA rate limit hit (OVER_RATE_LIMIT) while building the route tree on the first call after server start. `upstream_unreachable`: EIA could not be reached (connection refused or reset, DNS failure) while building the route tree on the first call after server start. Other values are possible when a failure originates below the handler."
},
"recovery": {
"type": "object",
"required": [
"hint"
],
"properties": {
"hint": {
"type": "string"
}
},
"description": "Actionable next step for the caller.",
"additionalProperties": {}
},
"retryable": {
"type": "boolean",
"description": "Whether retrying may succeed."
}
},
"additionalProperties": {}
},
"message": {
"type": "string",
"description": "Human-readable description of what went wrong."
}
},
"description": "Present when the call failed. Absent on success.",
"additionalProperties": {}
},
"shown": {
"type": "number",
"description": "Number of results returned."
},
"notice": {
"type": "string",
"description": "Discloses requested/effective limits when clamped and says when only the start of the query was searched; names a resolved series ID (saying so when EIA reports no rows for it), or says the lookup of a query shaped like one found nothing or failed (the results are then the text search); and gives guidance when no routes matched or more matches exist."
},
"results": {
"type": "array",
"items": {
"type": "object",
"required": [
"route",
"name",
"description",
"score",
"isLeaf"
],
"properties": {
"name": {
"type": "string",
"description": "Human-readable route name."
},
"route": {
"type": "string",
"description": "Route path — usable directly in eia_describe_route or eia_query_route."
},
"score": {
"type": "number",
"description": "Match score: 0 = exact, 1 = no match. Lower is better; above 0.72 the match is unreliable. On a multi-term query it is the better of the whole-phrase score and a per-term score that penalizes each query term the entry does not carry. A resolved series ID scores 0."
},
"isLeaf": {
"type": "boolean",
"description": "True when the route is a queryable leaf; false when it has sub-routes to browse."
},
"columns": {
"type": "array",
"items": {
"type": "string"
},
"description": "Data column IDs the series reads (e.g. [\"price\"]). Present only on a resolved series ID; pass as eia_query_route columns."
},
"frequency": {
"type": "string",
"description": "Frequency ID the series is published at (e.g. \"weekly\", \"local-hourly\"). Present only on a resolved series ID; pass as eia_query_route frequency."
},
"description": {
"type": "string",
"description": "Route description."
},
"filter_hint": {
"type": "object",
"description": "Pre-built filter for eia_query_route when a specific facet value is required. Present on STEO and SEDS series results ({\"seriesId\": ...}), Monthly Energy Review series results ({\"msn\": \"TETCBUS\"} on total-energy), and facet-value results — fuel, sector, and RTO balancing-authority values (found by name, EIA code, or ISO/RTO acronym: \"ERCOT\" gives {\"respondent\": \"ERCO\"} on electricity/rto/region-data), and the series and product values of daily and weekly petroleum and natural-gas routes (e.g. {\"series\": \"RNGWHHD\"} on natural-gas/pri/fut) — and on a resolved series ID, where it holds every facet EIA maps the ID to, values as EIA writes them (e.g. {\"sectorid\": \"RES\", \"stateid\": \"TX\"}). Pass directly as filters (e.g. eia_query_route(route=\"seds\", filters=filter_hint)).",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
}
},
"description": "A search result entry.",
"additionalProperties": false
},
"description": "Ranked matches, best first."
},
"indexGaps": {
"type": "array",
"items": {
"type": "string"
},
"description": "Present only when indexComplete is false: route paths whose metadata could not be fetched (call eia_browse_routes on one to re-fetch it) and index passes that did not land (\"steo_series\", \"seds_series\", \"total_energy_series\", \"facet_values\"), which the next search retries."
},
"truncated": {
"type": "boolean",
"description": "True when more matches exist past the ones returned; false when these are all."
},
"totalIndexed": {
"type": "number",
"description": "Total entries in the search index: routes, STEO, SEDS, and Monthly Energy Review series names, and the facet values indexed at warm time, plus the facet values of every route described since the server started — so the count grows with describe calls."
},
"indexComplete": {
"type": "boolean",
"description": "True when this answer was ranked against the complete warm-time corpus. False means part of it is missing (see indexGaps) — results may be short, and a better match may exist that was never scored. Facet values folded in by eia_describe_route are additional and never count toward completeness. A resolved series ID is answered by EIA, not ranked, so it does not depend on this."
},
"effectiveQuery": {
"type": "string",
"description": "Query as searched — the submitted text with surrounding whitespace trimmed, cut to its first 256 characters."
}
},
"additionalProperties": false
}{
"readOnlyHint": true,
"openWorldHint": false
}eia_query_route — Query EIA Route Data
Fetches data from a leaf route with optional facet filters, date range, frequency, and column selection. Use eia_describe_route first to discover valid facet IDs, facet values, column IDs, and frequency codes. Data values are strings in the response (EIA API returns all numeric values as strings, e.g. "9.13"); cast to DOUBLE in SQL when arithmetic is needed. Returns a preview inline and stages nothing by default — one upstream request, whatever total says. Pass stage: true to also page past the preview and stage the accumulated set as a DataCanvas table, then pass the returned dataset name to eia_dataframe_describe for its column names and eia_dataframe_query for SQL. Every dataset a tenant stages lands in the same canvas, so tables from different routes cross-join by name with nothing to thread between calls.
{
"type": "object",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": [
"route"
],
"properties": {
"end": {
"type": "string",
"description": "Period end, read the same way as start: a coarser end selects only its first period, so \"2020\" ends at 2020-01 at monthly — use \"2020-12\" to include the whole year. At hourly a date-only end covers its whole day."
},
"sort": {
"type": "array",
"items": {
"type": "object",
"required": [
"column",
"direction"
],
"properties": {
"column": {
"type": "string",
"description": "Column ID to sort by."
},
"direction": {
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Sort direction."
}
},
"description": "A sort criterion."
},
"description": "Result ordering."
},
"route": {
"type": "string",
"minLength": 1,
"description": "Leaf route path (e.g. \"electricity/retail-sales\", \"steo\"). Discoverable via eia_browse_routes or eia_search_routes. Whitespace and slashes are normalized, then one complete leading v2 segment is removed: \"/v2/electricity/retail-sales/\" resolves to \"electricity/retail-sales\". Full URLs and /data suffixes are not normalized."
},
"stage": {
"type": "boolean",
"default": false,
"description": "Stage the matching rows as a DataCanvas table for SQL (default false). Off, the call makes one upstream request and returns the preview alone. On, the service pages past the preview up to the staging row cap this server sets and registers the accumulated rows, starting at offset, returning the handle in dataset — several extra upstream requests and seconds of latency on a large route, so turn it on when moving to analysis, not while exploring. Requires the server to have a canvas, which it does unless its operator turned it off; without one nothing is staged whatever this is set to."
},
"start": {
"type": "string",
"description": "Period start in the route date format (e.g. \"2020-01\" for monthly, \"2020\" for annual). Format from eia_describe_route. EIA reads a bound as the period of the effective frequency containing its first day, so a coarser bound selects that one period (\"2020\" is 2020-01 at monthly, \"2020-Q4\" is 2020-10). At local-hourly every bound needs its hour and UTC offset (YYYY-MM-DDTHH±HH, e.g. \"2024-02-01T02-06\")."
},
"length": {
"type": "number",
"default": 25,
"minimum": 1,
"multipleOf": 1,
"description": "Rows per page. Default 25; a larger value returns at most 5,000 and says so. With stage: true, staging is not bounded by this — it pages past the page on its own."
},
"offset": {
"type": "integer",
"default": 0,
"maximum": 9007199254740991,
"minimum": 0,
"description": "Row offset into the matching set (default 0). An offset at or beyond total returns zero rows."
},
"columns": {
"type": "array",
"items": {
"type": "string"
},
"description": "Data column IDs to return (reduces payload). Defaults to all. IDs discoverable via eia_describe_route data_columns[].id. Every row also carries period and each facet ID whatever this lists, so naming those here is unnecessary and they are dropped from the request."
},
"filters": {
"type": "object",
"description": "Facet filters keyed by facet ID (e.g. { \"stateid\": \"TX\", \"sectorid\": [\"RES\", \"COM\"] }). Use the facets[].id values returned by eia_describe_route as keys here. Every value travels in the request URI, which EIA caps at about 8 KB — a few hundred values across all filters; split a longer list across calls.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
},
"frequency": {
"type": "string",
"description": "Aggregation frequency ID (e.g. \"monthly\", \"annual\"). Defaults to route default. Valid IDs from eia_describe_route."
}
},
"additionalProperties": false
}{
"type": "object",
"anyOf": [
{
"not": {
"required": [
"error"
]
},
"required": [
"route",
"data",
"total",
"returned_count",
"frequency",
"date_format",
"effectiveRoute",
"totalCount",
"returnedCount",
"appliedOffset",
"appliedLength"
]
},
{
"required": [
"error"
]
}
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {},
"description": "A single data row with dynamic column keys.",
"additionalProperties": {}
},
"description": "Inline rows: the length rows from offset, fewer only at the end of the matching set. All numeric values are strings per the EIA API (e.g. \"9.13\"). Cast to DOUBLE in SQL for arithmetic: CAST(value AS DOUBLE). Per-column units appear as {col}-units fields inline in each row. Keys are dynamic column IDs from the EIA route."
},
"error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "JSON-RPC error code for this failure."
},
"data": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"examples": [
"upstream_invalid_response",
"upstream_unavailable",
"upstream_timeout",
"route_not_found",
"route_not_queryable",
"invalid_facet",
"invalid_column",
"invalid_frequency",
"invalid_sort",
"invalid_period",
"uri_too_long",
"no_data",
"rate_limited",
"upstream_unreachable"
],
"description": "Machine-readable failure mode. Declared by this tool: `upstream_invalid_response`: EIA returned an unusable response body or a missing route or data envelope. `upstream_unavailable`: EIA answered with a retryable HTTP service failure. `upstream_timeout`: EIA did not answer in time: one attempt passed 45 s, or the call passed its 56 s deadline across retries. Staging that runs out of time ends early instead, keeping the rows already staged. `route_not_found`: Route does not exist in the EIA taxonomy. `route_not_queryable`: Route is a category node with sub-routes, not a queryable leaf. `invalid_facet`: An unknown facet key was used in filters. `invalid_column`: An unknown data column ID was passed in columns. `invalid_frequency`: An unknown frequency code was passed. `invalid_sort`: A sort entry named a column the route does not sort by. `invalid_period`: start or end was not in a period format the route accepts; named a month outside 01–12 or an hour that is not two digits; named a day its month lacks or an hour 24–99 that rolls the bound into another period at the chosen frequency (any such day or hour at daily, hourly, and local-hourly, and any such day at weekly and four-week-average); or lacked its UTC offset at local-hourly frequency. `uri_too_long`: The request URI passed the EIA limit of about 8 KB (HTTP 414) — typically a filter list of several hundred values. `no_data`: EIA returned no rows and start selects a later period than end at the effective frequency (the range is inverted). `rate_limited`: EIA rate limit hit (OVER_RATE_LIMIT). `upstream_unreachable`: EIA could not be reached (connection refused or reset, DNS failure) on the data request, the route metadata request, or the route-tree build. Other values are possible when a failure originates below the handler."
},
"recovery": {
"type": "object",
"required": [
"hint"
],
"properties": {
"hint": {
"type": "string"
}
},
"description": "Actionable next step for the caller.",
"additionalProperties": {}
},
"retryable": {
"type": "boolean",
"description": "Whether retrying may succeed."
}
},
"additionalProperties": {}
},
"message": {
"type": "string",
"description": "Human-readable description of what went wrong."
}
},
"description": "Present when the call failed. Absent on success.",
"additionalProperties": {}
},
"route": {
"type": "string",
"description": "The canonical route path queried: whitespace/slashes normalized and one complete leading v2 segment removed. Reusable verbatim in a follow-up call."
},
"total": {
"type": "number",
"description": "Total matching rows in the EIA dataset for this query (may exceed returned rows when pagination or spillover applies)."
},
"notice": {
"type": "string",
"description": "Composed guidance for empty results or an offset past total, unmatched filter values in complete cached vocabularies, year-only bounds on effective monthly YYYY-MM data, and requested/effective length when clamped. Submitted filters and date bounds are preserved."
},
"dataset": {
"type": "string",
"description": "df_<id> handle for the staged rows — eia_dataframe_describe lists its column names; eia_dataframe_query runs SQL over it (SELECT … FROM df_<id>). Present only when a stage: true call registered a table on a deployment with a canvas configured; absent otherwise, since nothing was staged. Every dataset a tenant stages shares one canvas, so handles from different routes join directly."
},
"frequency": {
"type": "string",
"description": "Frequency of the returned data."
},
"appliedEnd": {
"type": "string",
"description": "Echo of the end period as applied, when an end was provided."
},
"totalCount": {
"type": "number",
"description": "Total matching rows in the EIA dataset."
},
"appliedSort": {
"type": "array",
"items": {
"type": "object",
"required": [
"column",
"direction"
],
"properties": {
"column": {
"type": "string",
"description": "Column ID sorted by."
},
"direction": {
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Sort direction."
}
},
"description": "A sort criterion as applied.",
"additionalProperties": false
},
"description": "Echo of the result ordering as applied, when a sort was provided — the ordering that decided which rows a capped stage holds."
},
"date_format": {
"type": "string",
"description": "Period format for the returned data (e.g. \"YYYY-MM\")."
},
"next_offset": {
"type": "number",
"description": "Offset that continues right after the rows in data (offset + returned_count) — pass it as offset for the next page, with no gap or overlap. Present whenever rows remain past this page; absent on the last page."
},
"appliedStart": {
"type": "string",
"description": "Echo of the start period as applied, when a start was provided."
},
"appliedLength": {
"type": "number",
"description": "Rows per page applied to this call — length, after any clamp."
},
"appliedOffset": {
"type": "number",
"description": "Row offset applied to the query — the cause when a page comes back empty."
},
"returnedCount": {
"type": "number",
"description": "Rows in this response. When returnedCount < totalCount, continue at next_offset or stage the rest."
},
"appliedColumns": {
"type": "array",
"items": {
"type": "string"
},
"description": "Echo of the column projection as sent to EIA, when columns named at least one data column — period and facet IDs, which every row carries anyway, are not part of it."
},
"appliedFilters": {
"type": "object",
"description": "Facet filters applied to the query, when provided.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
},
"effectiveRoute": {
"type": "string",
"description": "The route path that was queried."
},
"returned_count": {
"type": "number",
"description": "Number of rows in data. When returned_count < total, continue at next_offset or stage the rest for SQL."
},
"appliedFrequency": {
"type": "string",
"description": "Echo of the frequency as applied, when a frequency was provided."
},
"truncation_warning": {
"type": "string",
"description": "Upstream advisories forwarded verbatim from EIA's warnings[], joined with '; ' when more than one applies. These describe the inline page — EIA's \"incomplete return\" entry fires whenever the requested length is under total, at any size — not a 5,000-row ceiling on this response. Absent when the response already accounts for the gap the advisory names (the staged table reaches the last row, notice explains the empty page, or canvas_preview_note places the inline page against total because nothing was staged)."
},
"canvas_preview_note": {
"type": "string",
"description": "Human-readable note, present whenever dataset is set or staging failed, and otherwise when total exceeds the inline rows. On a stage: true call it opens with the stage outcome: the staged row count and dataset name, naming eia_dataframe_describe for the staged column names (quoting one rename when staging renamed a column) and then eia_dataframe_query for SQL — or, when the canvas failed, that no dataframe was created (dataset is then absent), with the retry — from offset 0 past the first page — and, while rows remain, offset paging. Next, where in the matching set the staged rows sit whenever the stage does not start at row 1, and when staging stopped short of total (the staging row cap this server sets, the time limit of the call, or an upstream page that did not return) the offset to resume from; last, the row range of the inline page (e.g. rows 21–30 of 55), whether it is the last page, and next_offset — and, after a registered stage, that the inline rows page with stage left off, since another stage: true call stages a new table. Where staging was not requested it names the row range of the inline page, says when it is the last page, and names stage: true and eia_dataframe_query as the way to get SQL access to the matching rows — with offset 0 past the first page, since a stage starts at offset — and offset paging while rows remain; where this server has no canvas, it says staging is unavailable on this deployment and, while rows remain, names offset paging instead."
}
},
"additionalProperties": false
}{
"readOnlyHint": true,
"openWorldHint": false
}eia_dataframe_describe — Describe EIA Dataframes
List canvas dataframes (df_<id>) materialized by eia_query_route calls that passed stage: true, with provenance, expiry, row count, and column schema. Nothing is staged until such a call runs, so an empty list on a fresh session means no query has staged yet, not that staging failed. Drops entries for dataframes the canvas no longer holds before responding, so the list is always current. Pass a specific name to inspect one dataframe; omit to list all active dataframes for this tenant. A name that is not staged comes back as found=false alongside the handles that are, never as an empty list. Listing is not use: only an eia_dataframe_query statement naming a dataframe extends its expiry, so a dataframe polled with this tool and never queried still lapses on schedule.
{
"type": "object",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"name": {
"type": "string",
"description": "df_<id> handle to describe a single dataframe. Omit to list all active dataframes."
}
},
"additionalProperties": false
}{
"type": "object",
"anyOf": [
{
"not": {
"required": [
"error"
]
},
"required": [
"active_names",
"dataframes"
]
},
{
"required": [
"error"
]
}
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "JSON-RPC error code for this failure."
},
"data": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"examples": [
"canvas_unavailable"
],
"description": "Machine-readable failure mode. Declared by this tool: `canvas_unavailable`: DataCanvas service is not configured for this deployment. Other values are possible when a failure originates below the handler."
},
"recovery": {
"type": "object",
"required": [
"hint"
],
"properties": {
"hint": {
"type": "string"
}
},
"description": "Actionable next step for the caller.",
"additionalProperties": {}
},
"retryable": {
"type": "boolean",
"description": "Whether retrying may succeed."
}
},
"additionalProperties": {}
},
"message": {
"type": "string",
"description": "Human-readable description of what went wrong."
}
},
"description": "Present when the call failed. Absent on success.",
"additionalProperties": {}
},
"found": {
"type": "boolean",
"description": "True when the requested name is staged, false when it is not. Absent when no name was supplied — an unscoped list has nothing to resolve."
},
"dataframes": {
"type": "array",
"items": {
"type": "object",
"required": [
"name",
"source_tool",
"query_params",
"created_at",
"row_count",
"truncated",
"column_schema"
],
"properties": {
"name": {
"type": "string",
"description": "Canvas table name (df_<id>)."
},
"max_rows": {
"type": "number",
"description": "Materialization cap that produced truncated, when applicable."
},
"row_count": {
"type": "number",
"description": "Rows materialized in the dataframe."
},
"truncated": {
"type": "boolean",
"description": "True when the EIA upstream had more rows than were registered."
},
"created_at": {
"type": "string",
"description": "ISO 8601 creation timestamp."
},
"expires_at": {
"type": "string",
"description": "ISO 8601 expiry, extended each time an eia_dataframe_query statement references this dataframe. Reading it here does not extend it. Absent when the dataframe carries no expiry of its own and follows the canvas lifecycle."
},
"source_tool": {
"type": "string",
"description": "Tool that produced this dataframe."
},
"query_params": {
"type": "object",
"description": "Input parameters the source tool was called with.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"column_schema": {
"type": "array",
"items": {
"type": "object",
"required": [
"name",
"type",
"nullable"
],
"properties": {
"name": {
"type": "string",
"description": "Column name."
},
"type": {
"type": "string",
"description": "DuckDB column type (VARCHAR for EIA data values)."
},
"nullable": {
"type": "boolean",
"description": "Whether the column permits NULL."
}
},
"description": "A column in the dataframe schema.",
"additionalProperties": false
},
"description": "Column schema (all EIA data columns are VARCHAR and nullable)."
}
},
"description": "A canvas dataframe entry.",
"additionalProperties": false
},
"description": "Dataframes matching the requested scope, newest first. Empty when nothing is staged, or when a supplied name does not resolve — read found and active_names to tell those apart."
},
"active_names": {
"type": "array",
"items": {
"type": "string"
},
"description": "Every df_<id> handle staged for this tenant, regardless of the requested scope. On a miss these are the handles that are still usable."
},
"requested_name": {
"type": "string",
"description": "Echo of the name input. Absent when no name was supplied."
}
},
"additionalProperties": false
}{
"readOnlyHint": true,
"openWorldHint": false,
"idempotentHint": true
}eia_dataframe_query — Query EIA Dataframes
Run a single-statement SELECT against canvas dataframes registered by eia_query_route calls that passed stage: true — a query that staged nothing leaves no table to select from. Standard DuckDB SQL — joins, aggregates, window functions, CTEs all supported. Reference dataframes by the df_<id> handles returned by eia_query_route or listed by eia_dataframe_describe. Read-only: writes, DDL, DROP, COPY, PRAGMA, ATTACH, file reads (read_* functions or a quoted file path), engine-setting functions (current_setting), and SQL built from a string (query_table) are rejected. System catalogs (information_schema, pg_catalog, sqlite_master, duckdb_*) are denied. EIA data values are VARCHAR — use CAST(col AS DOUBLE) for arithmetic and aggregation. Optional register_as chains results as a new dataframe with a fresh expiry. Every dataframe named in the statement has its expiry extended by the query. Inline rows stop at preview; a result cut short says how to reach the rest, including paging a register_as table with LIMIT/OFFSET and no ORDER BY, which keeps the order the preview showed.
{
"type": "object",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": [
"sql"
],
"properties": {
"sql": {
"type": "string",
"minLength": 1,
"description": "Single-statement SELECT against df_<id> tables. EIA data columns are VARCHAR — use CAST(col AS DOUBLE) for arithmetic. Example: SELECT period, CAST(value AS DOUBLE) AS val FROM df_XXXXX ORDER BY period"
},
"preview": {
"type": "number",
"minimum": 0,
"multipleOf": 1,
"description": "Rows to include in the immediate response. Default 25; a larger value returns at most row_limit and says so. 0 returns none, useful when chaining via register_as."
},
"row_limit": {
"type": "number",
"default": 1000,
"minimum": 1,
"multipleOf": 1,
"description": "Hard cap on rows materialized. Default 1,000; a larger value returns at most 10,000 and says so. Rows past the cap are dropped without being counted — the response then carries truncated: true and a totalRows equal to the cap rather than a true total. Pass register_as to materialize the whole result instead and get an exact count."
},
"register_as": {
"type": "string",
"minLength": 1,
"description": "When set, persist the result as a new dataframe with a fresh expiry. Use to chain analyses without re-running upstream queries. The name must be unused — reusing a staged name is rejected, and the fix is a different name, not dropping the existing dataframe. eia_dataframe_describe lists the names already taken."
}
},
"additionalProperties": false
}{
"type": "object",
"anyOf": [
{
"not": {
"required": [
"error"
]
},
"required": [
"columns",
"rows",
"totalRows",
"returnedRows",
"truncated",
"executedSql"
]
},
{
"required": [
"error"
]
}
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"rows": {
"type": "array",
"items": {
"type": "object",
"properties": {},
"description": "A result row with dynamic keys matching the SQL projection columns.",
"additionalProperties": {}
},
"description": "Inline rows, bounded by preview and row_limit."
},
"error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "JSON-RPC error code for this failure."
},
"data": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"examples": [
"denied_function",
"multi_statement",
"sql_read_only",
"canvas_unavailable",
"system_catalog_access",
"missing_table",
"non_select_statement",
"invalid_sql",
"register_as_clash"
],
"description": "Machine-readable failure mode. Declared by this tool: `denied_function`: SQL calls a denied function, including engine settings, file reads, or SQL built from strings. `multi_statement`: SQL contains more than one statement. `sql_read_only`: SQL attempts a write, external file access, or another operation refused by the read-only gate. `canvas_unavailable`: DataCanvas service is not configured for this deployment. `system_catalog_access`: SQL references a denied system catalog (information_schema, pg_catalog, sqlite_master, duckdb_*). `missing_table`: SQL references a df_<id> table that is not staged — mistyped, already dropped, or past its expiry. `non_select_statement`: The statement is not a single read-only SELECT — writes, DDL, DROP, COPY, PRAGMA, and ATTACH are rejected. `invalid_sql`: DuckDB could not parse or bind the statement — a syntax error, or a column or alias that does not exist on the referenced dataframe. `register_as_clash`: register_as names a dataframe that is already staged for this tenant. Other values are possible when a failure originates below the handler."
},
"recovery": {
"type": "object",
"required": [
"hint"
],
"properties": {
"hint": {
"type": "string"
}
},
"description": "Actionable next step for the caller.",
"additionalProperties": {}
},
"retryable": {
"type": "boolean",
"description": "Whether retrying may succeed."
}
},
"additionalProperties": {}
},
"message": {
"type": "string",
"description": "Human-readable description of what went wrong."
}
},
"description": "Present when the call failed. Absent on success.",
"additionalProperties": {}
},
"notice": {
"type": "string",
"description": "Discloses requested/effective row_limit and preview when clamped, including on short or empty results. When row_limit or preview ended the inline rows, names each bound and how to reach the rows it withheld (raise a bound below its cap, or page a register_as table with LIMIT/OFFSET and no ORDER BY)."
},
"columns": {
"type": "array",
"items": {
"type": "string"
},
"description": "Column names in projection order."
},
"totalRows": {
"type": "number",
"description": "Rows the query materialized. Exact when truncated is false — including on the register_as path, which stages and counts the whole result past row_limit. Equal to row_limit when truncated is true: a floor on the real match count, not a total."
},
"truncated": {
"type": "boolean",
"description": "True when row_limit cut the result: more rows matched than the cap and the remainder was dropped without being counted. False when every matching row was materialized, including on the register_as path, which counts the new dataframe exactly."
},
"expires_at": {
"type": "string",
"description": "ISO 8601 expiry for the newly registered dataframe, when applicable. Extended each time a later query references it."
},
"executedSql": {
"type": "string",
"description": "Echo of the SQL statement that was executed — confirms the exact query that ran."
},
"returnedRows": {
"type": "number",
"description": "Rows included in this response. Below totalRows when preview ended the inline rows; the notice names how to reach the rest."
},
"registered_as": {
"type": "string",
"description": "Set when register_as was supplied and the new dataframe was materialized."
}
},
"additionalProperties": false
}{
"readOnlyHint": true,
"openWorldHint": false,
"idempotentHint": true
}Prompts
0 advertised
Resources
1 advertised
eia-energy-guide
Gates for answering U.S. energy questions with the eia_* tools (EIA API v2) — per-route geography codes, total rows that sit beside their components, unit columns whose names vary by route, nulls and zeros, projections mixed with history, and SQL over staged tables. Use before ranking, summing, comparing, or joining EIA data across states, sectors, fuels, or routes.
skill://eia-energy-guide/SKILL.mdio.modelcontextprotocol/skills
skill://{+path}90-day uptime
A lightweight connection check about every six hours, sampled on hosts with many listings. It shows the server answered, not that its tools work.
- 2026-11-07: 1 of 1 checks answered
30-day availability
Aggregated from read-only remote checks; this is not a tool-execution or security guarantee.
- Successful checks
- 1 of 1 (100%)
- Average latency
- 582 ms
- 95th percentile latency
- 582 ms
Maintenance history
Public check and source-refresh events. Editorial and ownership records remain private.
- · probe · passed · 582 ms · metadata changed · 6 tool(s), 1 resource(s), 1 resource template(s), 0 prompt(s)
Connect from your client
Each snippet registers the same endpoint; pick the client you use.
Claude Code
claude mcp add --transport http eia-energy-mcp-server-caseyjhand https://eia-energy.caseyjhand.com/mcpCodex
codex mcp add eia-energy-mcp-server-caseyjhand --url https://eia-energy.caseyjhand.com/mcpCursor
{
"mcpServers": {
"eia-energy-mcp-server-caseyjhand": {
"url": "https://eia-energy.caseyjhand.com/mcp"
}
}
}Cline
{
"mcpServers": {
"eia-energy-mcp-server-caseyjhand": {
"type": "streamableHttp",
"url": "https://eia-energy.caseyjhand.com/mcp",
"disabled": false
}
}
}VS Code
{
"servers": {
"eia-energy-mcp-server-caseyjhand": {
"type": "http",
"url": "https://eia-energy.caseyjhand.com/mcp"
}
}
}Gemini CLI
gemini mcp add --transport http eia-energy-mcp-server-caseyjhand https://eia-energy.caseyjhand.com/mcpClaude Desktop
https://eia-energy.caseyjhand.com/mcpWindsurf
{
"mcpServers": {
"eia-energy-mcp-server-caseyjhand": {
"serverUrl": "https://eia-energy.caseyjhand.com/mcp"
}
}
}Let your agent find its tools
Find servers like eia-energy-mcp-server from your agent. Add Koskamo once to search the index for endpoints, access requirements, and client setup. Public discovery needs no key.