About eurostat-mcp-server
eurostat-mcp-server is for researchers and analysts working with EU statistics. It searches the catalogue, queries datasets across topics such as economy, demography, trade, health, and NUTS regions, and can stage observations for read-only SQL analysis.
Key features
- 01Search and browse the statistics catalogue
- 02Inspect dataset dimensions and metadata
- 03Filter data by dimensions and periods
- 04Bulk-download and stage observations
- 05Run read-only SQL on staged tables
Use cases
- 01Find EU economic or demographic statistics
- 02Extract data for selected regions and periods
- 03Compare Comext or PRODCOM data
- 04Analyze staged observations with SQL
MCP support
Exposes Eurostat catalogue search, dataset query, download, and canvas analysis tools to MCP clients, as well as dataset metadata resources and the eurostat-guide skill. Supports STDIO and Streamable HTTP, with a hosted endpoint.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://eurostat.caseyjhand.com/mcpclaude mcp add --transport http cyanheads-eurostat https://eurostat.caseyjhand.com/mcp{"mcpServers":{"cyanheads-eurostat":{"type":"http","url":"https://eurostat.caseyjhand.com/mcp"}}}Open in Cursor{"servers":{"cyanheads-eurostat":{"type":"http","url":"https://eurostat.caseyjhand.com/mcp"}}}Open in VS Code{"mcpServers":{"cyanheads-eurostat":{"type":"http","url":"https://eurostat.caseyjhand.com/mcp"}}}{"mcpServers":{"cyanheads-eurostat":{"serverUrl":"https://eurostat.caseyjhand.com/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
Tools
8| Parameter | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Page size — maximum datasets returned per page (1–100). Default is 20. To retrieve matches beyond one page, pass the returned nextCursor back as cursor; the page size is fixed by this first call. |
| query | string | Yes | Search terms — at least one non-whitespace token is required. Split on whitespace into tokens; every token must match (AND), case-insensitively, somewhere across the dataset label, theme breadcrumb, or code. Spacing beside punctuation in the catalogue is ignored, so "Rev.2" also finds "Rev. 2" and "2015=100" finds "2015 = 100"; a token keeps its own punctuation, as in "R&D", "non-EU", or "DS-045409". Word order does not matter, so "business demography NUTS 3" or "regional economic accounts" resolve without naming a label verbatim. When nothing matches, the error names each word whose removal alone would find matches. |
| cursor | string | No | Opaque pagination cursor from a previous call's nextCursor. Omit for the first page; pass it back — with the same query — to fetch the next page of matches over a stable order. A cursor is bound to the query that produced it and to the catalogue snapshot in effect at that time, so reusing one with a different query, or after the catalogue refreshes, is rejected rather than silently paging a different result set. |
Details
From the project README@cyanheads/eurostat-mcp-server
Search and query the Eurostat catalogue — EU economy, demography, trade, health, and NUTS regional data via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://eurostat.caseyjhand.com/mcp
Overview
EU statistics from Eurostat: economy, demography, trade, health, and NUTS regional data, including detailed Comext trade and PRODCOM industrial production. Search or browse the catalogue, inspect a dataset's dimensions, then query a slice or bulk-download the whole thing.
Tools
The three eurostat_dataframe_* tools need the dataframe canvas, which is on by default and off with CANVAS_PROVIDER_TYPE=none; eurostat_dataframe_drop also needs EUROSTAT_DATAFRAME_DROP_ENABLED=true.
| Tool | Description |
|---|---|
eurostat_search_datasets | Search the Eurostat catalogue by keyword — returns codes, descriptions, period coverage, and theme breadcrumbs |
eurostat_browse_themes | Navigate the Eurostat theme hierarchy — list root themes or drill into subthemes and datasets |
eurostat_get_dataset_info | Fetch dataset metadata: dimensions with sample values, time range, observation count, and last-update date |
eurostat_get_dimension_values | List all valid codes for one dataset dimension, paged, with a code/label word filter and NUTS hierarchy filtering for geo |
eurostat_query_dataset | Fetch a bounded preview of decoded observations with dimension filters, NUTS geo-level, and time-range controls |
eurostat_download_dataset | Download a whole dataset via the SDMX bulk endpoint and stage every observation on the dataframe canvas |
eurostat_dataframe_describe | List the tables staged on a dataframe canvas, with row counts and column types |
eurostat_dataframe_query | Run a read-only SQL SELECT across staged tables |
eurostat_dataframe_drop | Remove one staged table from a dataframe canvas, leaving its other tables in place (opt-in) |
Resources
| Resource | Description |
|---|---|
eurostat://dataset/{dataset_code} | Dataset metadata (dimensions, time range, observation count, last-updated) by URI |
skill://eurostat-guide/SKILL.md | Statistical comparison gates, identifier provenance, and SQL analysis workflows |
The same metadata is available through eurostat_get_dataset_info for tool-only clients.
Agent skill
eurostat-guide covers country and regional comparisons, Comext aggregation, missing-data interpretation, and SQL joins over staged observations. It ships with plugin, npm, Docker, and MCPB installs and is served over MCP at skill://eurostat-guide/SKILL.md; supporting SQL examples are included under the skill's references/ directory.
Capability reference
eurostat_search_datasets tool
querywords are ANDed case-insensitively across each dataset's label, theme breadcrumb, and code, ignoring spacing beside punctuation (Rev.2findsRev. 2);limitis a positive integer (default 20), clamped to 100 with a notice. Page by passingnextCursorback ascursor; cursor calls ignorelimit- Results carry
code,label,type(dataset/table), period coverage,obsCount,lastUpdated, andthemePath, withtotalMatchesacross every page; fails asno_match(naming the words whose removal finds matches),invalid_cursor, or a retryableupstream_faultwhen no catalogue is loaded and Eurostat's table of contents is unreachable - If the Comext dataflow list cannot be loaded, results carry
comextListMissingand anotice; a catalogue that could not be refreshed carriescatalogueLoadedAt
eurostat_browse_themes tool
- Omit
theme_codefor the top-level theme folders; pass a folder code for its immediate children. Codes resolve exact-first, then case-insensitively. The Comext collections sit underext_go_detail(detailed trade, inext_go) andprom(PRODCOM, inicts) - Items carry
code,label,type(folder/dataset/table), andhasChildren, with aparentPathbreadcrumb andotherPlacementsfor a folder filed in several branches; fails asnot_foundfor an unknown code,not_a_folderfor a dataset or table code, or a retryableupstream_faultwhen no catalogue is loaded and Eurostat's table of contents is unreachable - If the Comext dataflow list cannot be loaded,
ext_goandictscarrycomextListMissingand anotice; a catalogue that could not be refreshed carriescatalogueLoadedAt
eurostat_get_dataset_info tool
- One
dataset_code; returns every dimension withvaluesCountand up to 10sampleValues, plustimeRange,obsCount,lastUpdated, and the ESMSmetadataUrl - Fails as
not_foundfor an unknown dataset, orupstream_faultwhen Eurostat returns a structure the server cannot read (retryable when the endpoint is down)
eurostat_get_dimension_values tool
dataset_codeplusdimension, resolved exact-first then case-insensitively (TIME_PERIODaliasestime); returned value codes and labels stay unchanged. Forgeo,geo_levelpicks a NUTS level (aggregate,countryby default,nuts1,nuts2,nuts3).filterkeeps values whose code and label contain every word given, ignoring case and accents (palma frankfurtfindsDE_EDDF_ES_LEPA), counted inmatchCountlimitdefaults to 25 and clamps to 1,000 with a notice. Pages contain that many values, fewer at the end, withtotalCountfor the full list; a page with more after it carriestruncated,nextCursor(pass back ascursor), and anotice. Continuations retain the effective count and ignore a newlimit; older cursors above 1,000 normalize to 1,000 with disclosure. Fails asnot_found,no_results,conflicting_params,invalid_cursor,upstream_fault(retryable when the endpoint is down), orcanvas_not_foundcanvas_idstages every value on that canvas as acode/labeltable (canvasId/tableName/stagedRowCount) that labels a download's code columns through a join; it never starts a canvas of its own
eurostat_query_dataset tool
dataset_codeplusfilters({dimension_code: [values]}),geo_level, and eithersince_period/until_period(2020,2020-Q1,2020-01, down toYYYY-WnnandYYYY-Dnnn) orlast_n_periods;preview_limitis a positive integer (default 25), clamped to 500 with a notice;langisEN,FR, orDEstage: truestages the whole match on an available canvas, regardless of size; reusecanvas_idor omit it to create one. Without it, automatic staging starts above 5,000 observations.truncatedstill describes that threshold;tableNameidentifies a staged result. Canvas-disabled deployments disclose an explicit staging request as unavailablelast_n_periodscounts dataset time positions, which can include periods with no populated cells. A notice names gaps across the full match (up to the newest 24, with an omitted count); increase N or replace it withuntil_periodto reach earlier observations- Each observation carries code/label pairs per dimension, a nullable
value,status(OBS_FLAG), andconfStatus(CONF_STATUS);obsCount,missingObsCount, andtimeRangecover the whole match,truncatedis true above 5,000 observations, andunmatchedValuesnames filter values that matched nothing - Fails as
not_found,no_results(naming the unmatched values and the periods with no value),invalid_dimension,invalid_period,conflicting_params, a non-retryableasync_responsefor a query too large to serve,canvas_not_found, or a retryableupstream_faultwhen the Statistics API is down
eurostat_download_dataset tool
dataset_codeplus the samefiltersmap andsince_period/until_period(nolast_n_periods: the TSV keeps a column per period unless a range drops it);preview_limitis a positive integer (default 25), clamped to 500 with a notice- Returns
rowCount(echoed astotalCount),missingCount,periodRange, andbytesReadfor the whole download; fails asnot_found,invalid_dimension,invalid_period,filter_arity, a non-retryableasync_queuedorextraction_too_big,no_results,upstream_fault(retryable when the SDMX endpoint is down), orcanvas_not_found - The
EUROSTAT_BULK_MAX_BYTESbudget (default 50 MiB decoded) retains complete observations within the permitted prefix and returnsbudgetExceeded: true. A budget too small for a complete header and usable observation fails asbyte_budget_exceeded, with the configured limit and narrowing guidance.bytesReadcounts accepted decoded bytes, never more than the budget; incomplete trailing rows are discarded.
eurostat_dataframe_describe tool
canvas_idfrom a staging response; lists each table'sname,rowCount, typed columns, andexpiresAt(slidingCANVAS_TTL_MS, default 24h); fails ascanvas_not_foundorcanvas_disabled- Columns depend on the staging tool:
eurostat_query_datasetwrites a code and a_labelcolumn per dimension,eurostat_download_datasetcodes only plustime(both share theobs_*andconf_status*measures), andeurostat_get_dimension_valueswritescodeandlabel
eurostat_dataframe_query tool
- One read-only
SELECTper call; chained statements, other verbs, and functions that read files or external data are rejected - Returns
columns,rows,rowCount, and required booleantruncated. Every row supplied by the canvas is returned intact, up toCANVAS_DEFAULT_ROW_LIMIT(default 10,000). Cells render as lossless JSON, including nesting and quoted strings; 64-bit integers arrive as strings. Fails asmissing_table,canvas_not_found, orcanvas_disabled - A cut sets
truncated: trueand adds a notice. Page an unchanged deterministic SELECT with a totalORDER BY(tie-breakers required),LIMITandOFFSET; advance by actualrowCount. Restart an initially unordered result sorted at offset zero. Volatile SQL and changing or expired tables cannot provide stable paging
eurostat_dataframe_drop tool
canvas_idplustable_name, exactly aseurostat_dataframe_describelists it; removes that one table and leaves the canvas and its other tables in place. Listed only whenEUROSTAT_DATAFRAME_DROP_ENABLED=trueis set beside the canvas- Returns
canvasId,tableName,dropped, andexpiresAt;droppedisfalse, with anotice, when nothing by that name was staged, so repeating a drop is safe. Fails ascanvas_not_found,identifier_reserved, orcanvas_disabled
eurostat://dataset/{dataset_code} resource
- Same payload as
eurostat_get_dataset_info, returned asapplication/json; a failed read carries the samereason, andretryableon an outage dataset_codecomes fromeurostat_search_datasetsoreurostat_browse_themes
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
Eurostat-specific:
- The Statistics API (JSON-stat 2.0) for slices, the SDMX 2.1 TSV bulk endpoint for whole datasets at roughly half the bytes, and the catalogue TOC cached in memory for 12 hours
- The Comext host's
DS-*collections (detailed trade by CN8, HS, SITC, BEC and CPA, and PRODCOM) through the same tools. Filter every request,freqincluded, since trade flows mix annual and monthly series; aggregates such asTOTAL,EU27_2020, andEXT_EU27_2020double-count when summed with their members. PRODCOM text values arrive invalueText/obs_value_text - NUTS geo-level filtering (
aggregate/country/nuts1/nuts2/nuts3), and OBS_FLAG and CONF_STATUS decoded into separate fields - Too-large-to-serve responses come back as non-retryable
async_response,extraction_too_big, orasync_queuederrors that name the dimensions left to filter - A query with
stage: true, a match above 5,000 observations, or any bulk download is staged as a SQL table (canvasId/tableName/stagedRowCount) on the DuckDB dataframe canvas, which is on by default. The.mcpbbundle strips the native DuckDB binding and runs withCANVAS_PROVIDER_TYPE=none, so SQL analytics need the npm, Docker, or from-source install
Agent-friendly output:
- Typed error contracts: every declared failure carries a
reasonand arecovery.hintnaming the next tool to call - Next-step hints:
eurostat_search_datasetsandeurostat_browse_themesreturn anextSteppointing at the follow-up call - Unknown stays unknown: counts and period bounds Eurostat doesn't report (
obsCount,timeRange.start/end,lastUpdated) are omitted rather than zeroed - Effective-query echo:
appliedFiltersoneurostat_query_datasetandappliedQuery(with the SDMXurl) oneurostat_download_dataset, plus anoticewhen the preview omits rows
Getting started
Connect to the hosted instance:
claude mcp add --transport http eurostat-mcp-server https://eurostat.caseyjhand.com/mcp
Or run it locally:
claude mcp add eurostat-mcp-server -- npx -y @cyanheads/eurostat-mcp-server
For other clients, add this to the MCP config file:
{
"mcpServers": {
"eurostat-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/eurostat-mcp-server"]
}
}
}
No API key is required. For Docker or a source build, see Running the server.
Configuration
| Variable | Description | Default |
|---|---|---|
EUROSTAT_BASE_URL | Eurostat API base URL. | https://ec.europa.eu/eurostat/api/dissemination |
EUROSTAT_COMEXT_BASE_URL | Comext dissemination host, which serves every DS-* dataset code (detailed trade and PRODCOM). | https://ec.europa.eu/eurostat/api/comext/dissemination |
EUROSTAT_REQUEST_TIMEOUT_MS | Total allowance for one request, including retries, backoff and body reads, in ms. | 30000 |
EUROSTAT_METADATA_TIMEOUT_MS | Total allowance for one dataset's SDMX dataflow structure, including retries, backoff and body reads, in ms. | 120000 (2 min) |
EUROSTAT_TOC_CACHE_TTL_MS | Catalogue TOC cache lifetime, in ms. | 43200000 (12 h) |
EUROSTAT_BULK_TIMEOUT_MS | Total allowance for one bulk transfer, including request retries, backoff and streaming, in ms. | 120000 (2 min) |
EUROSTAT_BULK_MAX_BYTES | Byte budget for one bulk download. | 52428800 (50 MiB) |
CANVAS_PROVIDER_TYPE | Dataframe canvas backend; none disables the canvas and its tools. The .mcpb bundle sets none. | duckdb |
CANVAS_TTL_MS | Sliding lifetime of a staged canvas, in ms. | 86400000 (24 h) |
CANVAS_DEFAULT_ROW_LIMIT | Max complete rows one eurostat_dataframe_query returns before reporting truncated. | 10000 |
EUROSTAT_CLAMP_LIMITS | Clamp positive integer preview/search/dimension limits to 500/100/1,000 with a notice. false restores schemas rejecting over-cap inputs; zero, negatives, fractions and nonnumeric inputs are always rejected. Each clamp logs once and increments eurostat.limit_clamps, labelled by tool. | true |
EUROSTAT_DATAFRAME_DROP_ENABLED | true lists eurostat_dataframe_drop; no effect with CANVAS_PROVIDER_TYPE=none. | false |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto. The server declares stateless; a set value overrides it. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). The Docker image sets info. | debug |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
LOG_TOOL_FAILURE_PAYLOADS | Log each failed tool call's arguments and result, redacted by key name and capped at LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES (default 16384). A secret inside a free-form value is not redacted. | false |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry. | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+).
git clone https://github.com/cyanheads/eurostat-mcp-server.git
cd eurostat-mcp-server && bun install
cp .env.example .env # optional; every setting has a default
-
Build and run the production version:
# One-time build bun run rebuild # Run the built server bun run start:http # or bun run start:stdio -
Run checks and tests:
bun run devcheck # Lints, formats, type-checks, and more bun run test # Runs the test suite bun run lint:mcp # Validates MCP definitions against spec
Docker
docker run --rm -p 3010:3010 ghcr.io/cyanheads/eurostat-mcp-server:latest
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/eurostat-mcp-server. For stdio, use docker run -i --rm -e MCP_TRANSPORT_TYPE=stdio ghcr.io/cyanheads/eurostat-mcp-server:latest. Build locally with docker build -t eurostat-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools and the resource, inits services, gates the dataframe tools on the canvas and the drop tool on its own flag. |
src/mcp-server/tools | Tool definitions (*.tool.ts). Six discovery and data tools, plus three canvas-gated dataframe tools, one of them opt-in. |
src/mcp-server/resources | Resource definitions. Dataset metadata resource. |
src/services/eurostat-catalogue | Catalogue service — fetches and parses the Eurostat TOC, merges the Comext dataflow list into it, with a TTL-bounded in-memory cache. |
src/services/eurostat-data | Statistics API service — SDMX metadata parsing with a per-dataset cache, JSON-stat 2.0 decoding, async-response detection, canvas row source. |
src/services/eurostat-bulk | SDMX 2.1 TSV bulk service — streaming download, byte budget, SOAP fault mapping, canvas row source. |
src/services/eurostat-codelists.ts | OBS_FLAG and CONF_STATUS codelists, the measure column names both stagers write, and the text-value decoder. |
src/services/eurostat-hosts.ts | Which host serves a dataset code: DS-* to the Comext host, everything else to the main one. |
src/services/canvas-accessor.ts | Accessor for the optional DataCanvas and the acquire helper canvas-touching tools share. |
src/services/shared-load.ts | Lets concurrent callers await one shared download, so one caller's cancellation releases only that caller. |
src/config | Server-specific environment variable parsing and validation with Zod. |
skills/ | Published eurostat-guide and SQL reference, served over MCP and included in installation packages. |
tests/ | Unit and integration tests, mirroring the src/ structure. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor logging,ctx.statefor storage - Register new tools and resources in the
createApp()arrays insrc/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.