Skip to main content
Glama

eia-energy-mcp-server

Query EIA Dataframes

eia_dataframe_query
Read-onlyIdempotent

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_ 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.

Input Schema

NameRequiredDescriptionDefault
sqlYesSingle-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
previewNoRows 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_limitNoHard 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_asNoWhen 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.

Output Schema

NameRequiredDescriptionDefault
rowsNoInline rows, bounded by preview and row_limit.
errorNoPresent when the call failed. Absent on success.
noticeNoDiscloses 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).
columnsNoColumn names in projection order.
totalRowsNoRows 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.
truncatedNoTrue 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_atNoISO 8601 expiry for the newly registered dataframe, when applicable. Extended each time a later query references it.
executedSqlNoEcho of the SQL statement that was executed — confirms the exact query that ran.
returnedRowsNoRows included in this response. Below totalRows when preview ended the inline rows; the notice names how to reach the rest.
registered_asNoSet when register_as was supplied and the new dataframe was materialized.

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed13 schema fields changed
    • changedInput schema / properties / preview / description
      Previous value: -"Rows to include in the immediate response. Defaults to row_limit; larger integers are clamped to 10000 with a notice. Set lower when chaining via register_as and only a sample is needed inline. The 64,000-byte response budget can end the inline rows sooner."New value: +"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."
    • removedInput schema / properties / preview / maximum
      Removed value: -9007199254740991
    • addedInput schema / properties / preview / multipleOf
      Added value: +1
    • changedInput schema / properties / preview / type
      Previous value: -"integer"New value: +"number"
    • changedInput schema / properties / row_limit / description
      Previous value: -"Hard cap on rows materialized (default 1000; larger integers are clamped to 10000 with a notice). 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. Of the rows materialized, only as many go inline as fit the 64,000-byte response budget."New value: +"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."
    • removedInput schema / properties / row_limit / maximum
      Removed value: -9007199254740991
    • addedInput schema / properties / row_limit / multipleOf
      Added value: +1
    • changedInput schema / properties / row_limit / type
      Previous value: -"integer"New value: +"number"
    • changedOutput schema / properties / columns / description
      Previous value: -"Column names in projection order. When no row is inline and the names pass 32,000 bytes, the list ends early with an entry counting the columns left out."New value: +"Column names in projection order."
    • changedOutput schema / properties / executedSql / description
      Previous value: -"Echo of the SQL statement that was executed — confirms the exact query that ran. A statement past 4,000 bytes is echoed cut, ending with a marker that gives its full size; the whole statement ran."New value: +"Echo of the SQL statement that was executed — confirms the exact query that ran."
    • changedOutput schema / properties / notice / description
      Previous value: -"Discloses requested/effective row_limit and preview when clamped, including on short or empty results. When row_limit, preview, or the 64,000-byte response budget ended the inline rows, names each bound and how to reach the rows it withheld (raise a bound below its ceiling, or page a register_as table with ORDER BY … LIMIT/OFFSET)."New value: +"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)."
    • changedOutput schema / properties / returnedRows / description
      Previous value: -"Rows included in this response. Below totalRows when preview or the response budget ended the inline rows; the notice names how to page the rest."New value: +"Rows included in this response. Below totalRows when preview ended the inline rows; the notice names how to reach the rest."
    • changedOutput schema / properties / rows / description
      Previous value: -"Inline rows, bounded by preview, row_limit, and a 64,000-byte budget on each response surface — whole rows only; a row too large for the budget alone is withheld."New value: +"Inline rows, bounded by preview and row_limit."
  2. Changed7 schema fields changed
    • changedInput schema / properties / preview / description
      Previous value: -"Rows to include in the immediate response. Defaults to row_limit. Set lower when chaining via register_as and only a sample is needed inline. The 64,000-byte response budget can end the inline rows sooner."New value: +"Rows to include in the immediate response. Defaults to row_limit; larger integers are clamped to 10000 with a notice. Set lower when chaining via register_as and only a sample is needed inline. The 64,000-byte response budget can end the inline rows sooner."
    • changedInput schema / properties / preview / maximum
      Previous value: -10000New value: +9007199254740991
    • changedInput schema / properties / row_limit / description
      Previous value: -"Hard cap on rows materialized (default 1000, max 10000). 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. Of the rows materialized, only as many go inline as fit the 64,000-byte response budget."New value: +"Hard cap on rows materialized (default 1000; larger integers are clamped to 10000 with a notice). 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. Of the rows materialized, only as many go inline as fit the 64,000-byte response budget."
    • changedInput schema / properties / row_limit / maximum
      Previous value: -10000New value: +9007199254740991
    • changedOutput schema / properties / error / properties / data / properties / reason / description
      Previous value: -"Machine-readable failure mode. Declared by this tool: `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."New value: +"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."
    • changedOutput schema / properties / error / properties / data / properties / reason / examples
      Previous value: -[
      -  "canvas_unavailable",
      -  "system_catalog_access",
      -  "missing_table",
      -  "non_select_statement",
      -  "invalid_sql",
      -  "register_as_clash"
      -]New value: +[
      +  "denied_function",
      +  "multi_statement",
      +  "sql_read_only",
      +  "canvas_unavailable",
      +  "system_catalog_access",
      +  "missing_table",
      +  "non_select_statement",
      +  "invalid_sql",
      +  "register_as_clash"
      +]
    • changedOutput schema / properties / notice / description
      Previous value: -"Guidance when row_limit, preview, or the 64,000-byte response budget ended the inline rows — names each bound that applied and how to reach the rows it withheld (raise the cap, or page a register_as table with ORDER BY … LIMIT/OFFSET)."New value: +"Discloses requested/effective row_limit and preview when clamped, including on short or empty results. When row_limit, preview, or the 64,000-byte response budget ended the inline rows, names each bound and how to reach the rows it withheld (raise a bound below its ceiling, or page a register_as table with ORDER BY … LIMIT/OFFSET)."
  3. Changed7 schema fields changed
    • changedInput schema / properties / preview / description
      Previous value: -"Rows to include in the immediate response. Defaults to row_limit. Set lower when chaining via register_as and only a sample is needed inline."New value: +"Rows to include in the immediate response. Defaults to row_limit. Set lower when chaining via register_as and only a sample is needed inline. The 64,000-byte response budget can end the inline rows sooner."
    • changedInput schema / properties / row_limit / description
      Previous value: -"Hard cap on rows materialized in the response (default 1000, max 10000). 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."New value: +"Hard cap on rows materialized (default 1000, max 10000). 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. Of the rows materialized, only as many go inline as fit the 64,000-byte response budget."
    • changedOutput schema / properties / columns / description
      Previous value: -"Column names in projection order."New value: +"Column names in projection order. When no row is inline and the names pass 32,000 bytes, the list ends early with an entry counting the columns left out."
    • changedOutput schema / properties / executedSql / description
      Previous value: -"Echo of the SQL statement that was executed — confirms the exact query that ran."New value: +"Echo of the SQL statement that was executed — confirms the exact query that ran. A statement past 4,000 bytes is echoed cut, ending with a marker that gives its full size; the whole statement ran."
    • changedOutput schema / properties / notice / description
      Previous value: -"Guidance when either cap bound the response — names the cap that applied and how to reach the rows it withheld."New value: +"Guidance when row_limit, preview, or the 64,000-byte response budget ended the inline rows — names each bound that applied and how to reach the rows it withheld (raise the cap, or page a register_as table with ORDER BY … LIMIT/OFFSET)."
    • changedOutput schema / properties / returnedRows / description
      Previous value: -"Rows included in this response."New value: +"Rows included in this response. Below totalRows when preview or the response budget ended the inline rows; the notice names how to page the rest."
    • changedOutput schema / properties / rows / description
      Previous value: -"Materialized rows, bounded by preview / row_limit."New value: +"Inline rows, bounded by preview, row_limit, and a 64,000-byte budget on each response surface — whole rows only; a row too large for the budget alone is withheld."
  4. Changed6 schema fields changed
    • changedInput schema / $schema
      Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • addedInput schema / additionalProperties
      Added value: +false
    • changedOutput schema / $schema
      Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • addedOutput schema / anyOf
      Added value: +[
      +  {
      +    "not": {
      +      "required": [
      +        "error"
      +      ]
      +    },
      +    "required": [
      +      "columns",
      +      "rows",
      +      "totalRows",
      +      "returnedRows",
      +      "truncated",
      +      "executedSql"
      +    ]
      +  },
      +  {
      +    "required": [
      +      "error"
      +    ]
      +  }
      +]
    • addedOutput schema / properties / error
      Added value: +{
      +  "additionalProperties": {},
      +  "description": "Present when the call failed. Absent on success.",
      +  "properties": {
      +    "code": {
      +      "description": "JSON-RPC error code for this failure.",
      +      "maximum": 9007199254740991,
      +      "minimum": -9007199254740991,
      +      "type": "integer"
      +    },
      +    "data": {
      +      "additionalProperties": {},
      +      "properties": {
      +        "reason": {
      +          "description": "Machine-readable failure mode. Declared by this tool: `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.",
      +          "examples": [
      +            "canvas_unavailable",
      +            "system_catalog_access",
      +            "missing_table",
      +            "non_select_statement",
      +            "invalid_sql",
      +            "register_as_clash"
      +          ],
      +          "type": "string"
      +        },
      +        "recovery": {
      +          "additionalProperties": {},
      +          "description": "Actionable next step for the caller.",
      +          "properties": {
      +            "hint": {
      +              "type": "string"
      +            }
      +          },
      +          "required": [
      +            "hint"
      +          ],
      +          "type": "object"
      +        },
      +        "retryable": {
      +          "description": "Whether retrying may succeed.",
      +          "type": "boolean"
      +        }
      +      },
      +      "type": "object"
      +    },
      +    "message": {
      +      "description": "Human-readable description of what went wrong.",
      +      "type": "string"
      +    }
      +  },
      +  "required": [
      +    "code",
      +    "message"
      +  ],
      +  "type": "object"
      +}
    • removedOutput schema / required
      Removed value: -[
      -  "columns",
      -  "rows",
      -  "totalRows",
      -  "returnedRows",
      -  "truncated",
      -  "executedSql"
      -]
  5. Changed5 schema fields changed
    • changedInput schema / properties / row_limit / description
      Previous value: -"Hard cap on rows materialized in the response (default 1000, max 10000)."New value: +"Hard cap on rows materialized in the response (default 1000, max 10000). 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."
    • changedOutput schema / properties / notice / description
      Previous value: -"Guidance when results are capped — shows how many rows were omitted."New value: +"Guidance when either cap bound the response — names the cap that applied and how to reach the rows it withheld."
    • changedOutput schema / properties / totalRows / description
      Previous value: -"Total rows the query produced (may exceed rows.length when capped by row_limit)."New value: +"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."
    • addedOutput schema / properties / truncated
      Added value: +{
      +  "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.",
      +  "type": "boolean"
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "columns",
      -  "rows",
      -  "totalRows",
      -  "returnedRows",
      -  "executedSql"
      -]New value: +[
      +  "columns",
      +  "rows",
      +  "totalRows",
      +  "returnedRows",
      +  "truncated",
      +  "executedSql"
      +]
  6. Changed3 schema fields changed
    • changedInput schema / properties / register_as / description
      Previous value: -"When set, persist the result as a new dataframe with a fresh TTL. Use to chain analyses without re-running upstream queries. Conflicts with an existing name throw Conflict."New value: +"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."
    • addedInput schema / properties / register_as / minLength
      Added value: +1
    • changedOutput schema / properties / expires_at / description
      Previous value: -"ISO 8601 expiry for the newly registered dataframe, when applicable."New value: +"ISO 8601 expiry for the newly registered dataframe, when applicable. Extended each time a later query references it."
  7. Changed2 schema fields changed
    • addedOutput schema / properties / executedSql
      Added value: +{
      +  "description": "Echo of the SQL statement that was executed — confirms the exact query that ran.",
      +  "type": "string"
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "columns",
      -  "rows",
      -  "totalRows",
      -  "returnedRows"
      -]New value: +[
      +  "columns",
      +  "rows",
      +  "totalRows",
      +  "returnedRows",
      +  "executedSql"
      +]
  8. Changed8 schema fields changed
    • addedOutput schema / properties / notice
      Added value: +{
      +  "description": "Guidance when results are capped — shows how many rows were omitted.",
      +  "type": "string"
      +}
    • addedOutput schema / properties / returnedRows
      Added value: +{
      +  "description": "Rows included in this response.",
      +  "type": "number"
      +}
    • removedOutput schema / properties / row_count
      Removed value: -{
      -  "description": "Total rows the query produced (may exceed rows.length when capped).",
      -  "type": "number"
      -}
    • addedOutput schema / properties / rows / items / description
      Added value: +"A result row with dynamic keys matching the SQL projection columns."
    • addedOutput schema / properties / rows / items / properties
      Added value: +{}
    • removedOutput schema / properties / rows / items / propertyNames
      Removed value: -{
      -  "type": "string"
      -}
    • addedOutput schema / properties / totalRows
      Added value: +{
      +  "description": "Total rows the query produced (may exceed rows.length when capped by row_limit).",
      +  "type": "number"
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "columns",
      -  "row_count",
      -  "rows"
      -]New value: +[
      +  "columns",
      +  "rows",
      +  "totalRows",
      +  "returnedRows"
      +]
  9. First observed

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations: enumerates the rejected surface (writes, DDL, DROP, COPY, PRAGMA, ATTACH, file reads, current_setting, query_table), the denied system catalogs, the VARCHAR typing of EIA values, the expiry-extension side effect on every referenced dataframe, and how truncation/preview behave. This is unusually rich behavioral disclosure for a read tool that already carries readOnlyHint/idempotentHint.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action and prerequisite, and nearly every sentence carries operational information. It is a dense block rather than a structured list, and the trailing paging sentence is slightly overloaded, so it is efficient but not maximally scannable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, return shape need not be explained, and the description still covers truncation behavior, totalRows semantics, and how to page to the remainder. For a 4-parameter, SQL-surface tool with broad rejection rules, nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% so the baseline is 3, but the description adds real meaning: register_as chains results as a new dataframe with a fresh expiry, preview 0 is useful when chaining, and paging a register_as table with LIMIT/OFFSET and no ORDER BY preserves preview order. Those are non-obvious semantics not spelled out in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (single-statement SELECT) and resource (canvas dataframes registered by eia_query_route with stage: true), and explicitly names the sibling tools that produce or list the df_<id> handles. An agent can distinguish this from eia_query_route, eia_dataframe_describe, and the browse/search siblings without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives a clear precondition ('dataframes registered by eia_query_route calls that passed stage: true — a query that staged nothing leaves no table to select from') and explains when to reach for register_as and where to find valid handles. It stops short of an explicit 'use X instead when…' routing statement against its siblings, so it falls just under the top score.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.