AI Integration · MCP

Survey Data (ESS)

The MCP server hosts the European Social Survey — respondent-level microdata on attitudes and behaviours across European countries — behind three read-only tools: survey__list_datasets, survey__lookup_variable, and survey__query. This page describes what the data contains, how querying works, and walks through worked examples.

What the data is

The ESS is a cross-national, repeated survey run in waves ("rounds") roughly every two years. One observation is one respondent in one round; the survey's own axes are cntry (country, ISO 3166-1 alpha-2), essround (the wave number), and a resolved NUTS region. Questions cover trust, politics, wellbeing, media and internet use, household composition, income, and rotating topical modules.

Two datasets are exposed:

DatasetContentsWhen to use
ess-integrated The pooled cross-round integrated file: rounds 2, 4, 6, 8, 9, 10, and 11 in one table, ~2,800 variables. The default for almost every question — comparisons across countries, rounds, or respondent groups.
ess-multilevel The round-11 multilevel edition: the same respondent microdata plus a contextual layer of ~3,300 macro indicators (c_*) — GDP, population, area, and other country/region statistics. Only when a question mixes individual responses with country- or region-level context.

Each dataset is versioned by edition — an immutable republished snapshot (e.g. ESS11MD_e01_2). Queries default to the latest edition; the concrete edition used is always echoed back in meta.edition. Rounds are values inside the data (filter on essround), not editions.

To see what's actually in the catalog, use the interactive catalog browser — every dataset, edition, and variable as a searchable tree, grouped by survey family and theme.

Discover, don't assume. Call survey__list_datasets first in a fresh session — it returns the exact dataset and edition identifiers and which rounds each edition carries.

Variables, value labels, and the codebook

Every column is a variable with a short name (ppltrst, netusoft, agea), a human label ("Most people can be trusted or you can't be too careful"), a set of value labels giving the meaning of each coded answer (e.g. 0 = "You can't be too careful"10 = "Most people can be trusted"), and per-variable missing-value codes. This codebook metadata is ingested into a searchable catalog — survey__lookup_variable is the interface to it.

Answers are stored as their raw numeric codes. Always decode a coded value through its value labels before presenting it — a bare netusoft = 5 means nothing to a reader ("every day" does).

The two layers

Correctness by construction

The tools do not accept SQL. Queries are expressed in a structured request the survey engine executes server-side, which lets two things be enforced on every query rather than left to the caller:

Provenance on every result. Each response carries a meta block — dataset, resolved edition, weight used, unweighted respondent count, sum of weights, rows dropped for a missing weight, and a truncation flag. A number without its meta is not a result.

The tools

survey__list_datasets read
Lists the datasets, their editions, and the rounds each carries. No parameters. Call first — it is the source of truth for valid dataset/edition values.
survey__lookup_variable read
Full-text search over variable names and labels. Returns each hit's name, label, value labels, missing-value codes, and which editions contain it. Use it both to find the variable name for a query and to decode coded values in a result.
q dataset limit
survey__query read
Subsets and aggregates the microdata. Aggregate mode (weighted mean / count / share, optionally grouped) is the primary path; rows mode returns bounded raw rows for inspection. Weighting and missing-value exclusion are applied by construction.
dataset edition filter aggregate select rowLimit weight joinContext includeMissing

of, by, select, and every filter.eq / filter.range key take variable names (ppltrst), never labels ("trust in people"). Passing a label or a country name where a variable name belongs is rejected up front with a suggestion.

Query semantics

Two modes

Filters (all conditions AND together)

FieldTakesNotes
filter.cntryISO 3166-1 alpha-2 codes["DK", "SE"] — country names are rejected.
filter.essroundInteger round numbers[8, 11] — a round is a value on the survey axis, not an edition.
filter.regionResolved NUTS region codesMatches the finest NUTS level the edition supplies.
filter.eq{ variable: [values] }Equality on any variable, e.g. { "gndr": [2] }.
filter.range{ variable: [min, max] }Inclusive numeric range, e.g. { "agea": [18, 29] }.

Weights

WeightUse for
anweightThe default. ESS's own guidance for almost everything; required for pooled cross-round or cross-country estimates.
pspwghtPost-stratification estimates within a single round and country.
pweight / dweightPopulation-size or design weights — only when specifically required.
noneExplicitly unweighted. Raw counts for data-quality checks — never a headline figure.

Rows whose chosen weight is NULL are dropped and reported in meta.droppedForMissingWeight — never silently discarded.

Missing values

All catalog missing codes for the variables being aggregated, grouped, or filtered are excluded by default, and system-missing (NULL) is always excluded with no override. To deliberately treat a missing code as a valid answer — say, to count refusals — pass includeMissing: { "variable": [77] } for that query only.

The meta block

FieldMeaning
dataset / editionWhat was actually queried. edition is the concrete resolved edition — never "latest".
weightThe weight applied. State it alongside any figure.
unweightedNDistinct respondents behind the estimate. A small n warrants caution.
sumWeightsSum of the applied weights (0 when weight: "none").
droppedForMissingWeightRows excluded because their weight was NULL.
truncatedtrue when a rows-mode result hit its cap — the view is partial.

Worked examples

The standard workflow is: discover the datasets, resolve the human question to a variable name, then aggregate — and read meta before presenting anything.

  1. 1survey__list_datasets — confirm the dataset, latest edition, and available rounds.
  2. 2survey__lookup_variable — turn "trust in people" into ppltrst, and note its value labels and missing codes.
  3. 3survey__query — aggregate mode with of, by, and filter; leave weight at its default.
  4. 4Present with provenance — decode coded values, state the weight and the unweighted n.

1 · Discover what's available

survey__list_datasets() → { "datasets": [ { "dataset": "ess-integrated", "title": "European Social Survey — respondent files", "editions": ["Complete_ESS"], "edition_titles": { "Complete_ESS": "All ESS rounds, pooled" }, "rounds": [2, 4, 6, 8, 9, 10, 11] }, { "dataset": "ess-multilevel", "title": "European Social Survey — respondents + context", "editions": ["ESS11MD_e01_2"], "edition_titles": { "ESS11MD_e01_2": "ESS round 11 + country/region context (2022)" }, "rounds": [11] } ] }

2 · Find a variable and its value labels

survey__lookup_variable({ "q": "trust in people", "dataset": "ess-integrated" }) → { "hits": [ { "variable": "ppltrst", "label": "Most people can be trusted or you can't be too careful", "dataset": "ess-integrated", "edition": "Complete_ESS", "editions": ["Complete_ESS"] } ] }

The full hit also carries the value labels (0 = "You can't be too careful"10 = "Most people can be trusted") and the missing codes (77, 88, 99) — keep them for decoding the result.

3 · Weighted mean, grouped by country

"How does trust in people compare across countries in the latest round?"

survey__query({ "dataset": "ess-integrated", "filter": { "essround": [11] }, "aggregate": { "op": "mean", "of": "ppltrst", "by": ["cntry"] } }) → { "rows": [ { "cntry": "DK", "mean": 6.9 }, { "cntry": "FI", "mean": 6.8 }, { "cntry": "HU", "mean": 5.2 }, ... ], "meta": { "dataset": "ess-integrated", "edition": "Complete_ESS", "weight": "anweight", "unweightedN": 39870, "sumWeights": 41210.3, "droppedForMissingWeight": 0, "truncated": false } }

Present it as: "In round 11, mean trust in people ranges from about 6.9 in Denmark to 5.2 in Hungary on the 0–10 scale (anweight, n ≈ 39,900)."

4 · Weighted shares of an answer distribution

"How often did Austrians use the internet in round 8?" — share returns the weighted proportion of each value of of:

survey__query({ "dataset": "ess-integrated", "filter": { "cntry": ["AT"], "essround": [8] }, "aggregate": { "op": "share", "of": "netusoft" } }) → { "rows": [ { "netusoft": 1, "share": 0.166 }, // Never { "netusoft": 2, "share": 0.055 }, // Only occasionally { "netusoft": 3, "share": 0.085 }, // A few times a week { "netusoft": 4, "share": 0.115 }, // Most days { "netusoft": 5, "share": 0.578 } // Every day ], "meta": { "edition": "Complete_ESS", "weight": "anweight", "unweightedN": 2010, ... } }

The value labels come from survey__lookup_variable — never present the bare codes. To compare distributions across groups, add "by": ["cntry"]; shares then sum to 1 within each group.

5 · Trend over rounds

Group by both country and round to get a time series per country:

survey__query({ "dataset": "ess-integrated", "filter": { "cntry": ["DK", "SE"] }, "aggregate": { "op": "mean", "of": "ppltrst", "by": ["cntry", "essround"] } }) → one row per (country, round) — e.g. { "cntry": "DK", "essround": 8, "mean": 7.0 }

6 · Subgroup filters on any variable

"Mean trust among women aged 18–29" — combine eq and range filters (here gndr = 2 is "Female", per its value labels):

survey__query({ "dataset": "ess-integrated", "filter": { "essround": [11], "eq": { "gndr": [2] }, "range": { "agea": [18, 29] } }, "aggregate": { "op": "mean", "of": "ppltrst", "by": ["cntry"] } })

7 · Rows mode: inspect raw records

For eyeballing individual observations — an explicit select is required and results are capped:

survey__query({ "dataset": "ess-integrated", "filter": { "cntry": ["DK"], "essround": [11] }, "select": ["idno", "agea", "gndr", "ppltrst", "netusoft"], "rowLimit": 20 }) → { "rows": [ { "idno": 10012, "agea": 34, "gndr": 2, "ppltrst": 7, "netusoft": 5 }, ... ], "meta": { ..., "truncated": false } }

If meta.truncated is true the view is partial — narrow the filter or switch to aggregate mode. Do not compute totals or averages from rows-mode output; that bypasses weighting.

8 · Joining the contextual layer (ess-multilevel)

"Does trust track regional GDP?" — request c_* indicators with joinContext; each attaches to a respondent via their resolved region and appears as a column:

survey__query({ "dataset": "ess-multilevel", "filter": { "cntry": ["DK", "PL"] }, "select": ["idno", "cntry", "ppltrst", "c_gdp_2022"], "joinContext": ["c_gdp_2022"], "rowLimit": 100 })

Discover the available indicators with survey__lookup_variable against ess-multilevel. Requesting joinContext on a dataset without a contextual layer is an error.

9 · Counting non-answers deliberately

"How many refused to state their income?" — re-include the refusal code as a valid value, and use an explicit unweighted count for the data-quality view:

survey__query({ "dataset": "ess-integrated", "filter": { "essround": [11] }, "aggregate": { "op": "count", "by": ["hinctnta"] }, "includeMissing": { "hinctnta": [77] }, "weight": "none" })

Common errors

ResponseWhat it means
unknown variable … did you mean …?A label or country name was passed where a variable name belongs. Resolve it with survey__lookup_variable and retry.
rows mode requires an explicit selectBoth aggregate and select were omitted. Add an aggregate (preferred) or a select list.
aggregate op 'mean'/'share' requires ofThe measured variable is missing — add it as of.
no editions found for datasetThe dataset/edition isn't available — use a value returned by survey__list_datasets.
joinContext requested but … has no contextual layerContextual indicators exist only on ess-multilevel. Switch dataset or drop joinContext.
Scope. The surface is read-only and query-shaped by design: no raw SQL, no mutation, no data outside the ingested ESS editions. If a round, country, or indicator doesn't appear in survey__list_datasets / survey__lookup_variable, it isn't available.

Updated 25 July 2026 at 10:50