eurostat-mcp-server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@eurostat-mcp-serversearch for datasets on unemployment"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Public Hosted Server: https://eurostat.caseyjhand.com/mcp
Overview
EU statistics from the Eurostat catalogue: economy, demography, trade, health, and NUTS regional data. Search and browse the catalogue by keyword or theme, inspect dataset dimensions, then query a slice or bulk-download a whole dataset. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
The catalogue spans two Eurostat hosts. Most datasets come from the dissemination API; the DS-* collections — detailed trade by CN8, HS, SITC, BEC and CPA (Comext), and PRODCOM industrial production — come from the Comext dissemination host. Every tool routes a DS-* code there by its prefix, in any case, so the same calls work on both.
Tools
Two of the eight are listed only when the dataframe canvas is enabled (CANVAS_PROVIDER_TYPE=duckdb).
Tool | Description |
| Search the Eurostat catalogue by keyword — returns codes, descriptions, period coverage, and theme breadcrumbs |
| Navigate the Eurostat theme hierarchy — list root themes or drill into subthemes and datasets |
| Fetch dataset metadata: dimensions with sample values, time range, observation count, and last-update date |
| List all valid codes for one dataset dimension, with NUTS hierarchy filtering for |
| Fetch a bounded preview of decoded observations with dimension filters, NUTS geo-level, and time-range controls |
| Download a whole dataset via the SDMX bulk endpoint and stage every observation on the dataframe canvas |
| List the tables staged on a dataframe canvas, with row counts and column types |
| Run a read-only SQL SELECT across staged tables |
Resources
Resource | Description |
| Dataset metadata (dimensions, time range, observation count, last-updated) by URI |
The same metadata is available through eurostat_get_dataset_info for tool-only clients.
Related MCP server: eurostat-mcp
Capability reference
eurostat_search_datasets tool
querytokens are ANDed case-insensitively across each dataset's label, theme breadcrumb, and code;limit1–100 (default 20), paged by passingnextCursorback ascursorResults carry
code,label,type(dataset/table), period coverage,obsCount,lastUpdated, andthemePath;totalMatchescounts every matchSearches the dissemination table of contents plus the Comext host's dataflow list, whose
DS-*entries carrylastUpdatedbut no period coverage orobsCount. A collection on neither, such as the legacy PRODCOMDS-056120, is not disseminated and cannot be reachedFails as
no_match— for a query naming such aDS-*code, saying the collection is not disseminated rather than suggesting a broader search — orinvalid_cursorwhen a cursor is reused with a different query or after the catalogue refreshes
eurostat_browse_themes tool
Omit
theme_codefor the top-level theme folders; pass a folder code for its immediate childrenThe Comext collections sit in two folders the table of contents lacks:
ext_go_detail(detailed trade) underext_go, andprom(PRODCOM) underictsItems carry
code,label,type(folder/dataset/table), andhasChildren, with aparentPathbreadcrumb;otherPlacementsnames other branches that file the same folder codeFails as
not_foundfor an unknown code,not_a_folderfor a dataset or table code
eurostat_get_dataset_info tool
One
dataset_code; returns every dimension withvaluesCountand up to 10sampleValues, plustimeRange,obsCount,lastUpdated, and the ESMSmetadataUrlThe Comext host reports no
timeRangeorobsCountfor aDS-*collection, so both read as unreported. Its structure carries a label for every code, 23 MB forDS-045409: parsed metadata is cached per dataset for an hour, within a 64 MiB memory budget, so only the first call pays for itFails as
not_foundfor an unknown dataset, orupstream_faultwhen Eurostat returns a structure or content constraint the server cannot read; a failed read is not cached, so a later call downloads it afresh
eurostat_get_dimension_values tool
dataset_codeplusdimension; returns the dataset-available code/label pairs invalues, withtotalCountAt most 2,000 values come back inline, in Eurostat's order. A longer list — the 37,069 CN8 codes of
DS-045409, a dailytimedimension, the largest airport-pair lists — is cut there, withtruncated,shown,cap, and anoticesaying where the rest isFor
geo,geo_levelpicks a NUTS level (aggregate,countryby default,nuts1,nuts2,nuts3)canvas_idalso stages every value, past the inline cap too, on that canvas as acode/labeltable (canvasId/tableName/stagedRowCount), to search with SQL or to label a download's code-only columns through a join; this tool never starts a canvas of its own, and ignorescanvas_idon a deployment without oneFails as
not_foundfor an unknown dataset or dimension,no_resultsfor a NUTS level with no values,conflicting_paramsforgeo_levelon any other dimension,upstream_faultfor a structure or content constraint the server cannot read, orcanvas_not_foundfor an unknown or expiredcanvas_id
eurostat_query_dataset tool
dataset_codeplusfilters({dimension_code: [values]}, codes in any case),geo_level, and eithersince_period/until_period(YYYY,YYYY-MM,YYYY-MM-DD,YYYY-Qn,YYYY-Sn,YYYY-Tn,YYYY-Mnn,YYYY-Wnn,YYYY-Dnnn; extra leading zeros after the letter are dropped, so2020-Q01is sent as2020-Q1, andYYYY-A1is sent asYYYY) orlast_n_periods, which counts back from the dataset's latest period rather than the slice's;preview_limit1–500 (default 50) sets the inline row count;langisEN,FR, orDEEach observation carries code/label pairs per dimension, a nullable
value, an OBS_FLAGstatus, and a CONF_STATUSconfStatus;obsCount,missingObsCount, andtimeRangecover the whole match,truncatedis true above 5,000 observations, andunmatchedValuesnames filter values that matched nothingPRODCOM publishes its flag and unit indicators as text in the value itself:
:Ccomes back as a nullvaluewithconfStatusC, and anything else, such as the unitKG, verbatim invalueTextwith a nullvalueFails as
not_found,no_results(naming the filter values that matched nothing and, up to the newest 24 with a full count, the selected periods that carry no value),invalid_dimension,invalid_period(a malformed or non-existent period, or asince_periodthat starts afteruntil_periodends, rejected before any request),conflicting_params(geofilter withgeo_level, or a period range withlast_n_periods), a non-retryableasync_responsefor a query too large to serve inline, orcanvas_not_foundfor an unknown or expiredcanvas_id
eurostat_download_dataset tool
dataset_codeplus the samefiltersmap andsince_period/until_periodin the same period forms (nolast_n_periods: the TSV keeps a column per period unless a range drops it);preview_limit1–500 (default 50)Returns
rowCount(echoed astotalCount),missingCount,periodRange, andbytesReadfor the whole download; theEUROSTAT_BULK_MAX_BYTESbudget (default 50 MiB) stops a transfer mid-stream and returns what arrived withbudgetExceeded: trueOn a
DS-*table,obs_value_textholds a value published as text (PRODCOM'sKG), and a PRODCOM:Clands inconf_statusFails as
not_found,invalid_dimension(including a range wholly outside the dataset's coverage),invalid_period(a malformed or non-existent period, or asince_periodthat starts afteruntil_periodends, rejected before any request),filter_arity, a non-retryableasync_queuedwhen Eurostat queues the extraction, a non-retryableextraction_too_bigwhen Eurostat refuses it as too large (past its 5,000,000-row limit, or an unfiltered download of a large Comext collection) with the dimensions left to filter,no_results(including a range inside the dataset's coverage that misses the filtered series),upstream_fault, orcanvas_not_foundfor an unknown or expiredcanvas_id
eurostat_dataframe_describe tool
canvas_idfrom a staging response; lists each table'sname,rowCount, and typed columns, plus canvas and tableexpiresAt(slidingCANVAS_TTL_MS, default 24h); fails ascanvas_not_foundfor an unknown or expired ID, orcanvas_disabledon a deployment without a canvaseurostat_query_datasettables carry a code and a_labelcolumn per dimension;eurostat_download_datasettables carry codes only, plustime. Both shareobs_value,obs_flag,obs_flag_label,conf_status, andconf_status_label, so they join on dimension codes andtime; aDS-*table from either addsobs_value_texteurostat_get_dimension_valuestables carrycodeandlabel: join one to a download on its dimension column (d.geo = g.code) to label it
eurostat_dataframe_query tool
One read-only
SELECTper call; chained statements, other verbs, and functions that read files or external data are rejectedReturns
columns,rows,rowCount, andtruncated, which is true pastCANVAS_DEFAULT_ROW_LIMIT(default 10,000) rows; 64-bit integers,COUNT(*)included, arrive as strings. Fails asmissing_table,canvas_not_found, orcanvas_disabled
eurostat://dataset/{dataset_code} resource
Same payload as
eurostat_get_dataset_info, returned asapplication/jsondataset_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 through the same tools: detailed trade by CN8, HS, SITC, BEC and CPA, and PRODCOM. The trade flows mix annual and monthly series in one dataset, so filterfreq;product,reporterandpartnercarry aggregates (TOTAL,EU27_2020,EXT_EU27_2020) that double-count when summed with their members. The Comext host serves no large collection unfilteredNUTS geo-level filtering (
aggregate/country/nuts1/nuts2/nuts3) oneurostat_query_datasetandeurostat_get_dimension_valuesOBS_FLAG (provisional, estimated, and so on) and CONF_STATUS (confidentiality) decoded into separate fields, with the same codes from JSON-stat and the bulk TSV
Eurostat's too-large-to-serve responses (HTTP-200 async warnings, HTTP 413, SOAP fault 413, SOAP queue tickets) come back as non-retryable
async_response,extraction_too_big, orasync_queuederrors that name the dataset's own dimensions left to filter, not timeoutsInline rows are capped by
preview_limit(at most 500). WithCANVAS_PROVIDER_TYPE=duckdb, a query match above 5,000 observations, or any bulk download, is staged whole as a SQL table (canvasId/tableName/stagedRowCount) for the dataframe tools, andcanvas_idstages the next result beside earlier ones for joins — including a dimension's code/label list fromeurostat_get_dimension_values, which labels a bulk table's codes. An empty download fails asno_resultswithout creating or touching a canvas. The.mcpbbundle strips the native DuckDB binding, 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 callNext-step hints:
eurostat_search_datasetsandeurostat_browse_themesreturn anextSteppointing at the follow-up callUnknown stays unknown: counts and period bounds Eurostat doesn't report (
obsCount,timeRange.start/end,lastUpdated) are omitted rather than zeroedEffective-query echo:
appliedFiltersoneurostat_query_datasetandappliedQuery(with the SDMXurl) oneurostat_download_dataset, plus anoticewhen the inline preview omits rows, naming the staged table when there is one;eurostat_query_dataset'snoticealso names any filter value that matched nothing
Getting started
Public Hosted Instance
A public instance is available at https://eurostat.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"eurostat-mcp-server": {
"type": "streamable-http",
"url": "https://eurostat.caseyjhand.com/mcp"
}
}
}Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"eurostat-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/eurostat-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}Or with npx (no Bun required):
{
"mcpServers": {
"eurostat-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/eurostat-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}Or with Docker:
{
"mcpServers": {
"eurostat-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/eurostat-mcp-server:latest"]
}
}
}For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcpPrerequisites
Bun v1.4.0 or higher (or Node.js v24+).
No API key: Eurostat's dissemination API and its Comext host are public.
Installation
Clone the repository:
git clone https://github.com/cyanheads/eurostat-mcp-server.gitNavigate into the directory:
cd eurostat-mcp-serverInstall dependencies:
bun installConfigure environment (optional, every setting has a default):
cp .env.example .envConfiguration
Variable | Description | Default |
| Eurostat API base URL. |
|
| Comext dissemination host, which serves every |
|
| HTTP request timeout, in ms. |
|
| Timeout for one dataset's SDMX dataflow structure, in ms — 23 MB, uncompressed, for |
|
| Catalogue TOC cache lifetime, in ms; the first search or browse call past it refreshes the TOC. |
|
| Timeout for one |
|
| Byte budget for one bulk download, counted on the decoded TSV and enforced while streaming. |
|
|
|
|
| Sliding lifetime of a staged canvas, in ms. |
|
| Max rows one |
|
| Transport: |
|
| HTTP server port. |
|
| HTTP session mode: |
|
| Authentication: |
|
| Log level ( |
|
| Directory for log files (Node.js only). |
|
| Storage backend: |
|
| Enable OpenTelemetry. |
|
See .env.example for the full list of optional overrides.
Running the server
Local development
Build and run the production version:
# One-time build bun run rebuild # Run the built server bun run start:http # or bun run start:stdioRun 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 build -t eurostat-mcp-server .
docker run --rm -p 3010:3010 eurostat-mcp-serverThe Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/eurostat-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Directory | Purpose |
|
|
| Tool definitions ( |
| Resource definitions. Dataset metadata resource. |
| Catalogue service — fetches and parses the Eurostat TOC, merges the Comext dataflow list into it, with a TTL-bounded in-memory cache. |
| Statistics API service — SDMX metadata parsing with a per-dataset cache, JSON-stat 2.0 decoding, async-response detection, canvas row source. |
| SDMX 2.1 TSV bulk service — streaming download, byte budget, SOAP fault mapping, canvas row source. |
| OBS_FLAG and CONF_STATUS codelists, the measure column names both stagers write, and the text-value decoder. |
| Which host serves a dataset code: |
| Accessor for the optional DataCanvas and the acquire helper canvas-touching tools share. |
| Lets concurrent callers await one shared download, so one caller's cancellation releases only that caller. |
| Server-specific environment variable parsing and validation with Zod. |
| Unit and integration tests, mirroring the |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor logging,ctx.statefor storageRegister new tools and resources in the
createApp()arrays insrc/index.tsWrap 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 testLicense
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
This server cannot be deployed
Maintenance
Related MCP Connectors
Eurostat MCP — wraps Eurostat Statistical Data API (no auth required)
Access European statistics: GDP, unemployment, inflation, population data.
Query official statistics of Catalonia (Idescat): tables, metadata and JSON-stat data via MCP.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceExposes the Eurostat Statistics API, enabling LLMs to discover, explore, and retrieve official EU statistical data through search, dimension inspection, and data retrieval tools.3-
- AlicenseNot gradedqualityDmaintenanceA Model Context Protocol (MCP) server that provides tools to query Eurostat APIs for European statistics data.10MIT
- AlicenseNot gradedqualityBmaintenanceEnables querying Eurostat statistical data through natural language or direct MCP tools, wrapping the Eurostat API without authentication.267 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables searching and accessing EU open datasets from data.europa.eu, including metadata discovery and dataset retrieval.265 npmMIT