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, and query a slice or bulk-download a whole dataset from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
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 — canvas only |
| Run a read-only SQL SELECT across staged tables — canvas only |
Resources
Resource | Description |
| Dataset metadata (dimensions, time range, observation count, last-updated) by URI, for cache-injectable context |
Related MCP server: eurostat-mcp
Capability reference
eurostat_search_datasets tool
Tokenized keyword match — whitespace-separated tokens are ANDed case-insensitively across each dataset's label, theme breadcrumb, and code, so word order and theme-named queries resolve without a verbatim label match
Returns
code,label,type(dataset/table), period coverage, observation count, and theme breadcrumb per resultOne row per dataset code — Eurostat files some datasets under multiple theme branches; matches are deduplicated so
totalMatchesand page slots count unique targetsCursor pagination:
limit(1–100, default 20) sets page size,totalMatchesreports the full count, and passingnextCursorback ascursorpages through every match. A cursor is bound to its originating query and catalogue snapshot — reusing one with a different query, or after the catalogue refreshes, returnsinvalid_cursorinstead of a silently shifted pagenextStepon each result names the next tool to callCatalogue TOC is cached in memory for 12 hours (
EUROSTAT_TOC_CACHE_TTL_MS), refreshed on the next call past that age
eurostat_browse_themes tool
Without
theme_code: returns the top-level theme folders (Economy, Population, Transport, etc.)With
theme_code: returns immediate children — subtheme folders and datasets in that branchEach entry carries
code,label,type(folder/dataset/table), data period, and observation count where availableReturns a breadcrumb
parentPathfrom root to the current node, plus anextStephint suited to the levelOne branch per folder code — Eurostat files a few folder codes under several branches; a code resolves to the branch listed first in the catalogue (never fewer children than the ones it shadows), and
otherPlacementsnames those so the ambiguity is visible
eurostat_get_dataset_info tool
Returns all dimensions with their codes, labels, and up to 10 sample values each
Dimension values reflect the full dataset-available set (including every period for
time), not just what appears in populated observationsReports overall time range and total observation count, each omitted — not zeroed — when Eurostat does not report it
For a dimension with more than 10 values, call
eurostat_get_dimension_valuesfor the full listmetadataUrllinks to the ESMS metadata page when Eurostat provides one
eurostat_get_dimension_values tool
Returns the complete dataset-available set of codes and labels for any dimension, from the same content constraint
eurostat_get_dataset_infousesFor
geo, NUTS hierarchy filtering viageo_level:aggregate,country(default),nuts1,nuts2,nuts3— an empty level reportsno_resultsrather than implying the dataset lacks data, and pairinggeo_levelwith any other dimension is rejectedVerify codes here before
eurostat_query_datasetoreurostat_download_dataset— an invalid dimension value returns no data silently from the former and a rejected fault from the latter
eurostat_query_dataset tool
Dimension filters as
{dimension_code: [values]}; ageofilter andgeo_level(NUTS:aggregate/country/nuts1/nuts2/nuts3) are mutually exclusive, as aresince_period/until_periodandlast_n_periods; an empty filter array is dropped rather than appliedpreview_limit(1–500, default 50) bounds only the inline prefix of decoded observations — it never changesobsCount,missingObsCount,timeRange, or what gets staged. There is deliberately no cursor or offset; filters and period controls are the only way to shrink the match itselfEach observation carries dimension code/label pairs, a nullable
value, an optional OBS_FLAGstatus(e.g.p=provisional,e=estimated), and a separate optionalCONF_STATUSconfStatusmarker — usually why a value is nulltruncatedis true only when the match exceeds the 5,000-observation staging threshold, independent ofpreview_limit. With the dataframe canvas enabled, a match above that threshold is staged whole as a SQL table (canvasId/tableName/stagedRowCount) — calleurostat_dataframe_describebeforeeurostat_dataframe_query; without a canvas those fields are absent and narrowing the query is the only way to reach the restcanvas_idreuses an existing canvas so a result can be joined against earlier ones; an oversized unfiltered query is caught by async-response detection and returned as an actionable, non-retryable error instead of timing outFetches a slice — for a whole dataset,
eurostat_download_datasetreads the SDMX bulk endpoint instead, at roughly half the bytes
eurostat_download_dataset tool
TSV bulk body runs 48–63% of the JSON-stat bytes
eurostat_query_datasetreads for the same data — measured across four datasets from 1.1M to 12.8M observationsFilters take the same
{dimension_code: [values]}map, applied server-side; the positional key needs every dimension in the dataset's own order, so a filter naming one the dataset lacks is rejected with the real dimension list rather than sent malformedNo
last_n_periodshere — onlysince_period/until_periodactually shrink the response, since the TSV layout keeps a column per period regardless of selectorByte budget (
EUROSTAT_BULK_MAX_BYTES, default 50 MiB) is enforced while streaming — Eurostat sends noContent-Length, so a transfer stopped mid-flight returns its rows withbudgetExceeded: trueinstead of an errorFailure modes are typed: an async queue ticket (Eurostat's too-costly-to-serve-inline response) is a non-retryable error; XML SOAP faults map to
not_found(100),filter_arity(140), andinvalid_dimension(150 — also covers an out-of-coverage period range), each with a recovery hint naming the next toolWith the dataframe canvas enabled, every observation is staged as a SQL table (
canvasId/tableName/stagedRowCount), streamed row by row — calleurostat_dataframe_describebeforeeurostat_dataframe_query. Without a canvas, onlypreview_limitrows (default 50, max 500) survive the call;rowCount/missingCount/periodRangestill describe the whole download
eurostat_dataframe_describe tool
Lists tables staged on the canvas (from
canvas_id, returned byeurostat_query_datasetoreurostat_download_dataset) with row counts, column names, types, and nullability — call before writing SQL, since the two stagers write different dimension columnsAlso reports the canvas's and each table's
expiresAt; every call against a canvas slides its lifetime forward (CANVAS_TTL_MS, default 24h)Errors
canvas_disabledwhen this deployment runs without a canvas,canvas_not_foundwhen the ID is unknown or expired
eurostat_dataframe_query tool
Runs a single read-only
SELECTagainst staged tables; statement chaining, non-SELECTverbs, and functions that read files or external data are rejected with a typed erroreurostat_query_datasettables carry a code column per dimension plus a_labelcompanion;eurostat_download_datasettables carry code columns only (no labels) plus atimecolumn — both write the same five measure columns (obs_value,obs_flag,obs_flag_label,conf_status,conf_status_label) with matching codes, so tables from either stager join on dimension codes andtimeA confidential cell reads
obs_flag = NULLwithconf_status = 'C'on either table — JSON-stat folds the two into one string (|C) thateurostat_query_datasetsplits before stagingResults are bounded by
CANVAS_DEFAULT_ROW_LIMIT(default 10,000);truncated: truemeans add aLIMIT, an aggregate, or a narrowerWHERE. 64-bit integer results —COUNT(*)included — arrive as strings so values outside the JSON number range survive intactThe DuckDB binding ships with the server —
CANVAS_PROVIDER_TYPE=duckdbis the only switch — except the one-click.mcpbbundle, which strips native bindings to stay portable; use the npm, Docker, or from-source install for SQL analytics
eurostat://dataset/{dataset_code} resource
Same payload as
eurostat_get_dataset_info, addressable as a resource URI for cache-injectable contextdataset_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:
NUTS hierarchy geo-level filtering (
aggregate/country/nuts1/nuts2/nuts3) acrosseurostat_query_datasetandeurostat_get_dimension_valuesOBS_FLAG (provisional, estimated, etc.) and CONF_STATUS (confidentiality) decoded into separate fields from Eurostat's combined encoding, consistent whether the observation came from JSON-stat or the bulk TSV
Async-response detection across every data-fetching tool — Eurostat's over-limit HTTP-200 warnings and HTTP-413/SOAP faults are classified into one typed, non-retryable error with filter guidance instead of surfacing as a timeout
SDMX 2.1 TSV bulk downloads at roughly half the JSON-stat byte cost, with a mid-transfer byte budget
Optional DuckDB dataframe canvas stages a query match above 5,000 observations, or a whole bulk download, as a queryable SQL table
Agent-friendly output:
Structured error contracts — every declared failure carries a typed
reasonand arecovery.hintnaming the exact next tool to call, not just an error stringNext-step hints —
eurostat_search_datasetsandeurostat_browse_themesresponses carry anextStepfield pointing at the right follow-up callOmitted-vs-unknown fields — counts and period bounds Eurostat doesn't report (
obsCount,timeRange.start/end,lastUpdated) are omitted from the response rather than defaulted to zero or blankTruncation and staging notices — a response that exceeds an inline cap carries an enrichment notice naming the exact
eurostat_dataframe_describe→eurostat_dataframe_queryfollow-up
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. No API key required — Eurostat's dissemination API is public.
Installation
Clone the repository:
git clone https://github.com/cyanheads/eurostat-mcp-server.gitNavigate into the directory:
cd eurostat-mcp-serverInstall dependencies:
bun installConfiguration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
Variable | Description | Default |
| Transport: |
|
| HTTP server port |
|
| HTTP endpoint path |
|
| Public origin override for TLS-terminating reverse-proxy deployments | none |
| Authentication: |
|
| Log level ( |
|
| Opt-in Bun-only forced-GC pressure loop (ms). Recommended starting point if heap growth is observed: |
|
| Directory for log files (Node.js only) |
|
| Storage backend: |
|
| Eurostat API base URL |
|
| HTTP request timeout in ms |
|
| Catalogue TOC cache lifetime in ms — the first search or browse call past this age refreshes it |
|
| HTTP timeout for one |
|
| Byte budget for one bulk download, counted on the decoded TSV and enforced while streaming |
|
|
|
|
| Directory DuckDB writes canvas spill files to. Must be writable by the server process |
|
| Sliding lifetime of a staged canvas in ms; every call against it extends the window |
|
| Max rows one |
|
| 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 TXT file; TTL-bounded in-memory cache. |
| Data service — dataset-scoped SDMX metadata parser plus Statistics API querying, JSON-stat 2.0 decoding, async-response detection, and dataframe row source. |
| Module-level accessor for the optional DataCanvas, plus the acquire helper that names the misconfigured path on a permission failure. |
| 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()arraysWrap 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.
Statistics Canada (StatCan) WDS MCP — Canadian official statistics (no auth)
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 gradedqualityCmaintenanceEnables querying Eurostat statistical data through natural language or direct MCP tools, wrapping the Eurostat API without authentication.2 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables searching and accessing EU open datasets from data.europa.eu, including metadata discovery and dataset retrieval.2 npmMIT