Skip to content

Query governed data with MCP

5 min read · Updated 2 Nov 2026

Before you start: the hosted server has no default layer. A data-only client (data:query without sl:read) must pass a semanticLayerId to astrato_data_prepare_query, and it cannot list layers, so get the ID from someone with structural access.

Clients holding sl:read call astrato_get_context before using the other structural tools. It returns permissions, the configured default semantic layer, revision rules, and the supported authoring workflows. A data-only client holding data:query cannot call astrato_get_context: use tools/list and the server-provided instructions, then start the data flow with astrato_data_prepare_query. For complete editing sequences, see Semantic layer workflows.

A data question resolves in three tiers, and a client stops at the first that answers. First, a stored business question: list them and execute a match by reference, because it carries the layer’s own definition of the concept. Second, the governed items: astrato_data_prepare_query lists the stored dimensions and calculations by default (scope: "semantic"), and reports how many raw view fields it withheld. Third, only when no stored question or item expresses what is needed, astrato_data_prepare_query with scope: "all" adds the raw view fields and the client assembles the query itself. A cursor is bound to its scope, so a semantic page cannot be continued under all.

Preparation analyzes up to 12 stored candidates per page against the supplied query context. Other stored candidates remain listed with candidateAnalysis.queryAnalysisUnavailable explaining the limit. Narrow the search for candidate previews, or prepare the exact query selection to check it.

A semantic page reports no aggregations in its capability blocks, because every item on it is a stored one and a stored item carries its own. What the tier behind it accepts is named under withheld.aggregations instead. Read that list before concluding a layer cannot express a concept: a question about spread, variance or a median almost never has a stored calculation to find, so the answer is usually to widen to scope: "all" and define the aggregation in query.local.measures over a raw field.

The list is pooled across the data types of the withheld fields, so it establishes that an aggregation exists in this layer — not that it applies to the field you go on to select. LISTAGG alongside SUM means one of the withheld fields is a string and another is numeric, not that either accepts both. The per-type capability block on the scope: "all" page remains the contract: check the aggregation there against the chosen field’s data type before defining the measure, and report the concept unavailable only once that block rules it out. Which aggregations appear at all depends on the data connection — the list is what this layer’s driver advertises for a single field, so one needing a second argument, such as CORR, is never among them.

A local measure follows the stored measure expression rules. One of them refuses, before anything runs, a row expression whose fields no join brings onto one row, such as supply cost × quantity across two views with no many-to-one join between them: the refusal names the view the others cannot reach and what to add to the layer instead.

Every page carries tiers — how many stored questions, dimensions, calculations and raw fields the layer holds — so a client skips a tier that holds nothing instead of paying a call to be told so. A layer with no queryable dimensions or calculations — none stored, or none published yet, since an unpublished item is listed under unavailable rather than counted — lists its raw fields directly under the default scope and says why in fallback; the summary tool reports the same counts, including businessQuestions.

A custom group is listed once, as its dimension. Publishing stores it as a generated column on its table, but that column is not offered as a raw field, so it can’t be queried around the dimension or picked up under an old name after the group is renamed.

Not every question is an aggregate. One that asks for a list — “the customers in California” — selects dimensions and omits measures entirely; a query needs at least one dimension or measure, not necessarily a measure. Attaching a measure to satisfy the shape is not a neutral act: the measure’s own filtering decides which rows survive, so a contact who bought nothing this period drops out of a list that never asked about buying.

A stored calculation may declare parameters — an operand of its formula, or a bound on its own measure filter — each with a default. astrato_data_prepare_query lists them on the entry under parameters with the name, engine data type, default and where each is used, and astrato_data_execute_query takes scoped values in parameters.measures[selectionKey][parameterName]. The same overrides work for stored BQ execution. A calculation runs with its declared defaults for every parameter not given. A value for a name no selected calculation declares, or of another type than declared, is refused. A filter on a field is not a substitute: it narrows the rows around the calculation rather than changing what the calculation computes.

A stored dimension’s entry says what kind it is, under dimension.kind:

  • A drillthrough lists its levels in order, each with a reference, label and data type. A query groups by the first level unless parameters.drillLevels[selectionKey] names another.
  • A bucket gives its field, minValue, maxValue and buckets. value says whether each row reads as the bucket number or the value its bucket starts at. outOfRange says whether values outside the range get their own bucket or null.
  • A custom group (mapping) lists the value each group returns, in rule order, and otherwise, the value for rows that match no group. Filter on those values. A long list is cut at 50 and marked groupsTruncated.
  • field and periodAxis carry the kind alone.

A calculation that resolves to another one — the conditional and derived kinds — lists that calculation’s parameters too, each marked with the declaredBy name of the calculation declaring it. Binding one of those works: the engine carries the override through the reference to the calculation underneath, so a dated question can be asked of the outer calculation rather than only of the one it resolves to.

Every page also carries capabilities, keyed by data type: the aggregations a raw field of that type can be given, and the filter operators that type accepts. A stored dimension or calculation supplies its own aggregation, so its block lists none — but it still reports the operators, which are published nowhere else. Where a page holds both a raw field and a stored item of one data type, the raw field’s block stands, because only it names the aggregations a caller has to choose between.