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, and external-file table functions 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.

Input Schema

TableJSON 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. Defaults to row_limit. Set lower when chaining via register_as and only a sample is needed inline.
row_limitNoHard 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.
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

TableJSON Schema
NameRequiredDescriptionDefault
rowsNoMaterialized rows, bounded by preview / row_limit.
errorNoPresent when the call failed. Absent on success.
noticeNoGuidance when either cap bound the response — names the cap that applied and how to reach the rows it withheld.
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.
registered_asNoSet when register_as was supplied and the new dataframe was materialized.

Schema Changelog

Changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 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"
      -]
  2. 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"
      +]
  3. 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."
  4. 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"
      +]
  5. 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"
      +]
  6. First observed

TDQS

A4.6/5.0
Behavior4/5

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

Annotations already declare readOnlyHint and idempotentHint, and the description reinforces these by explicitly listing rejected operations. It adds behavior not in annotations: VARCHAR casting requirements, expiry extension for referenced dataframes, and the semantics of register_as (fresh expiry, name uniqueness). This goes beyond the annotation baseline, though it does not cover every edge case (e.g., performance or transaction behavior).

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?

The description is dense but efficient. It leads with the core purpose, then covers constraints, data type caveats, and optional chaining in a logical order. Every sentence conveys essential information without filler. It is longer than average, but the complexity of the tool justifies it — no word is wasted.

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?

For a tool of this complexity, the description covers all critical aspects: prerequisites, supported and rejected operations, data type handling, output truncation behavior, expiry semantics, and register_as usage. The output schema likely captures return format, so nothing critical is missing. This is a model of a complete tool description.

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?

The schema already provides detailed descriptions for all four parameters (100% coverage), so the baseline is 3. The description adds value by explaining how the sql parameter must reference df_<id> handles, giving an example, and clarifying the interplay between preview and register_as (lower preview when chaining). It also deepens register_as's meaning with reuse rules. This is more than the schema alone offers.

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?

The description opens with a specific verb and resource: 'Run a single-statement SELECT against canvas dataframes'. It clearly distinguishes this from sibling tools by referencing dataframes registered by eia_query_route and listing them via eia_dataframe_describe, and implies it is the querying counterpart to those route-management tools.

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

Usage Guidelines5/5

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

Explicitly states the prerequisite: dataframes must have been staged (stage: true) or there is nothing to select. It enumerates supported SQL features and lists rejected operations (writes, DDL, DROP, etc.), provides a concrete example, and explains when register_as should be used for chaining. This leaves an agent with no ambiguity about when and how to use the tool.

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.

TDQS

A4.7/5.0
Disambiguation5/5

Each tool serves a clearly distinct function: taxonomy browsing, fuzzy search, route metadata discovery, data retrieval, dataframe inspection, and SQL querying. There is no overlap between the route-focused and dataframe-focused tools, and even the two describe/query pairs are separated by target (route vs. dataframe).

Naming Consistency4/5

All tools share the eia_ prefix and use descriptive verbs, but there's a minor inconsistency: route tools mix plural (browse_routes, search_routes) and singular (describe_route, query_route) forms, while dataframe tools place 'dataframe' before the verb (eia_dataframe_describe, eia_dataframe_query). This is readable and predictable overall, with only slight deviation from a uniform pattern.

Tool Count5/5

With 6 tools, the server is well-scoped for its purpose. It covers discovery, metadata, data access, and SQL analysis without unnecessary bloat or missing essentials. The number fits comfortably within the ideal range for a domain-specific data server.

Completeness5/5

The tool chain is complete for the domain: browse/search to locate routes, describe to get facet/column metadata, query to fetch data, and optionally stage to query with SQL. Dataframe management includes listing and querying, with automatic expiry, so there are no dead ends in the typical workflow. It's a read-only API, but that's appropriate for the energy data use case.