Skip to main content
Glama

data_aggregate

Aggregate rows SERVER-SIDE — stats over a whole (optionally filtered) table WITHOUT pulling the rows into context (feedback #191). Use this for totals and distributions over a large vivier (e.g. total kWc, average score, count per department) instead of reading 300+ rows and summing them yourself.

metrics = list of {op, field?}; op ∈ count|count_rows|sum|avg|min|max (default [{"op":"count"}]). count without field = total rows; sum/avg/min/max require a numeric field and ignore non-numeric values. group_by = a column to group on (omit = one global row). Results are sorted by the first metric descending when grouped (so group_by gives you the TOP groups first).

A metric may carry its OWN condition — where, same clauses as filters — so the total and a subset are counted in the SAME query. That is how you get a RATE without crossing two calls whose scopes can silently differ. Give such a metric a label, and it comes back under that name.

group_by also accepts a LIST of columns: their values are POOLED, one row contributing one occurrence per filled column ("all ranks together"). Under a pooled group, count counts OCCURRENCES and count_rows counts ROWS — two different questions, so ask for the one you mean.

A list column (e.g. contacts) is aggregated ACROSS ITS ITEMS with contacts[].<attribute>. group_by: "contacts[].fonction" makes every item of every matching row one occurrence — same vocabulary: count counts the contacts, count_rows the rows; a contact without that attribute falls in the null group. filters still select ROWS (a row kept by contacts[].fonction eq DRH contributes ALL its contacts). sum/avg/min/max with field: "contacts[].<numeric attribute>" aggregate over all items (read on the current item when grouping by the same list). A list path cannot be pooled with other columns, nor sorted; contacts[0].x targets one rank. A list of plain VALUES (e.g. tags) is read element by element with tags[]: group_by: "tags[]" counts each tag (count) and the rows carrying it (count_rows); filters on tags[] keep the rows holding the value.

⚠️ Pooling is NOT a two-dimensional group-by, and group_by: "a,b" is not one either — it is REFUSED (oto#50). A comma-separated string used to be read as a single column name containing a comma: no row carries it, so you got 200 with ONE group of key null holding every row, and no way to tell that from empty data. Group on one column per call, or pass the list if you meant to pool. Crossing two dimensions is not served yet.

Returns {results: [...]} — each entry carries the group_by value (when set) plus one key per metric (count, sum_<field>, avg_<field>, or label).

count_by = a list of columns to profile in ONE call (not with group_by or metrics): returns {counts: {column: {values: [{value, count}], empty, distinct, total, truncated}}} — most frequent values first, at most values_limit (default 50, max 500) per column; truncated says more exist.

Examples — - total rows matching a filter: metrics omitted, filter={"statut":"qualified"} - MWc by department: group_by="departement", metrics=[{"op":"sum","field":"kwc_estime"}, {"op":"count"}] - share of companies with an HR contact on ANY rank, by headcount band — one call, no rows pulled: group_by="tranche_effectif", metrics=[{"op":"count","label":"fiches"}, {"op":"count","label":"avec_rh","where":[ {"fields":["contact1_fonction","contact2_fonction", "contact3_fonction"], "op":"in","value":["DRH","DAF"]}]}] - which roles appear across all contact ranks: group_by=["contact1_fonction","contact2_fonction","contact3_fonction"] - the same on a list column, contacts and companies counted apart: group_by="contacts[].fonction", metrics=[{"op":"count"}, {"op":"count_rows"}]

Tool call template

Replace each placeholder with a value matching the input schema. Only required parameters are included; add optional parameters as needed.

{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "arguments": {
      "datastore": "<datastore>"
    },
    "name": "data_aggregate"
  }
}

Input Schema

NameRequiredDescriptionDefault
qNofree-text search over the row. Every WORD of `q` must be found, in any order, either by meaning (French stemming, accents and case folded: `cahiers` finds "Le Cahier", `corp acme` finds "ACME Corp") or as a literal FRAGMENT (a piece of a SIREN, an URL or a reference pasted as is). Without an explicit sort, best matches come first (rows matched by meaning, then rows matched only by fragment); an explicit sort stays yours. Where to search is `q_scope`. Aggregates the same set a `data_rows` search shows (ranking aside).
_orgNoOrg (id) de CET APPEL. `oto_list_orgs()`.
_groupNoÉquipe (id) de CET APPEL, sous son org parente.
filterNodict `{column: value}` exact match to scope the aggregate.
_run_idNoOBLIGATOIRE à chaque appel dès `run_start` ou `data_claim_next` : sans lui, l'écriture sur une ligne réservée est refusée. Hérité seulement si le serveur tient un run actif — n'y compte pas.
filtersNolist of clauses, incl. multi-column ones — same grammar as `data_rows.filters`. Combines with `filter` (AND).
metricsNolist of `{op, field?, where?, label?}` aggregations (default = count of rows).
q_scopeNowhere `q` searches: `values` = only the values a reader is served (no column names, no layers — `origine`/`comment`/`link` — no JSON quoting); `all` = the whole stored row, names and layers included (finds a row by its `comment`). Default `all`. Any other value is refused.
_projectNoProjet (id) de CET APPEL — jeton PRIMAIRE. `oto_project(op='list')`.
count_byNocolumns to count values and empties of, in one call.
group_byNocolumn to group by, `list[].attribute` to group the items of a list column, or a LIST of columns whose values are pooled (omit = global aggregate, single row).
datastoreYestarget datastore, or `slot:<name>` (active project).
values_limitNowith `count_by`, values kept per column (1-500).

Schema Changelog

Changes observed during successful MCP inspections.

No schema history has been recorded yet.

TDQS

A4.9/5.0
Behavior5/5

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

With no annotations, the description carries the full burden and does so: it discloses sorting behavior (first metric descending when grouped), default metric, how non-numeric values are ignored by sum/avg/min/max, null-group fallback for missing list attributes, values_limit truncation, and an explicit refusal case for comma-separated group_by with the historical failure mode described.

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 value statement and then organized by parameter and by worked example, so it is navigable despite its length. It is however very long and somewhat repetitive around list-path pooling (restated across two paragraphs plus two examples), so a few sentences could be tightened without losing meaning.

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 13-parameter, no-output-schema tool that is genuinely mode-switching (metrics vs count_by vs bare filters), the description documents both return shapes ({results: [...]} with key naming rules, and {counts: {...}} with values/empty/distinct/total/truncated) plus four worked examples, which is enough for correct invocation without a output schema.

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

Parameters5/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 goes well past the schema: it defines the metrics object grammar (op set, field requirement, label, per-metric where), the three distinct meanings of group_by (single column, list-of-columns pooling, list-column path with [] and [0]), and the count vs count_rows distinction under pooling — semantics the schema text does not convey.

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?

Names a specific verb and resource (aggregate rows server-side) and states the defining value proposition immediately: stats over a filtered table without pulling rows into context. It also implicitly differentiates from the data_rows sibling ("instead of reading 300+ rows and summing them yourself"), so an agent can pick between them without opening schemas.

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 prescribes when to use this tool (totals, distributions, rates over large viviers) and when not to (don't read rows and sum yourself). It also routes between the tool's own modes: count_by is stated as "not with group_by or metrics", and metrics-with-where is recommended over two calls "whose scopes can silently differ".

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.

Resources