Search and query the Eurostat catalogue — EU economy, demography, trade, and NUTS regional data.
One address for any MCP client. No sign-in needed on this server.
8 read
Grouped by tool name. At most: only reads or looks up information.
Odel charges nothing per call. The publisher may charge for its own service.
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
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.
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) |
| Resource | Description |
|---|---|
eurostat://dataset/{dataset_code} | 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.
eurostat_search_datasets toolquery words are ANDed case-insensitively across each dataset's label, theme breadcrumb, and code, ignoring spacing beside punctuation (Rev.2 finds Rev. 2); limit is a positive integer (default 20), clamped to 100 with a notice. Page by passing nextCursor back as cursor; cursor calls ignore limitcode, label, type (dataset / table), period coverage, obsCount, lastUpdated, and themePath, with totalMatches across every page; fails as no_match (naming the words whose removal finds matches), invalid_cursor, or a retryable upstream_fault when no catalogue is loaded and Eurostat's table of contents is unreachablecomextListMissing and a notice; a catalogue that could not be refreshed carries catalogueLoadedAteurostat_browse_themes tooltheme_code for the top-level theme folders; pass a folder code for its immediate children. The Comext collections sit under ext_go_detail (detailed trade, in ext_go) and prom (PRODCOM, in icts)code, label, type (folder / dataset / table), and hasChildren, with a parentPath breadcrumb and otherPlacements for a folder filed in several branches; fails as not_found for an unknown code, not_a_folder for a dataset or table code, or a retryable upstream_fault when no catalogue is loaded and Eurostat's table of contents is unreachableext_go and icts carry comextListMissing and a notice; a catalogue that could not be refreshed carries catalogueLoadedAteurostat_get_dataset_info tooldataset_code; returns every dimension with valuesCount and up to 10 sampleValues, plus timeRange, obsCount, lastUpdated, and the ESMS metadataUrlnot_found for an unknown dataset, or upstream_fault when Eurostat returns a structure the server cannot read (retryable when the endpoint is down)eurostat_get_dimension_values tooldataset_code plus dimension; for geo, geo_level picks a NUTS level (aggregate, country by default, nuts1, nuts2, nuts3). filter keeps values whose code and label contain every word given, ignoring case and accents (palma frankfurt finds DE_EDDF_ES_LEPA), counted in matchCountlimit), with totalCount for every value; a page with more after it carries truncated, nextCursor (pass back as cursor), and a notice. Fails as not_found, no_results, conflicting_params, invalid_cursor, upstream_fault (retryable when the endpoint is down), or canvas_not_foundcanvas_id stages every value on that canvas as a code / label table (canvasId / tableName / stagedRowCount) that labels a download's code columns through a join; it never starts a canvas of its owneurostat_query_dataset tooldataset_code plus filters ({dimension_code: [values]}), geo_level, and either since_period / until_period (2020, 2020-Q1, 2020-01, down to YYYY-Wnn and YYYY-Dnnn) or last_n_periods; preview_limit is a positive integer (default 50), clamped to 500 with a notice; lang is EN, FR, or DEvalue, status (OBS_FLAG), and confStatus (CONF_STATUS); obsCount, missingObsCount, and timeRange cover the whole match, truncated is true above 5,000 observations, and unmatchedValues names filter values that matched nothingnot_found, no_results (naming the unmatched values and the periods with no value), invalid_dimension, invalid_period, conflicting_params, a non-retryable async_response for a query too large to serve, canvas_not_found, or a retryable upstream_fault when the Statistics API is downeurostat_download_dataset tooldataset_code plus the same filters map and since_period / until_period (no last_n_periods: the TSV keeps a column per period unless a range drops it); preview_limit is a positive integer (default 50), clamped to 500 with a noticerowCount (echoed as totalCount), missingCount, periodRange, and bytesRead for the whole download; fails as not_found, invalid_dimension, invalid_period, filter_arity, a non-retryable async_queued or extraction_too_big, no_results, upstream_fault (retryable when the SDMX endpoint is down), or canvas_not_foundEUROSTAT_BULK_MAX_BYTES budget (default 50 MiB decoded) retains complete observations within the permitted prefix and returns budgetExceeded: true. A budget too small for a complete header and usable observation fails as byte_budget_exceeded, with the configured limit and narrowing guidance. bytesRead counts accepted decoded bytes, never more than the budget; incomplete trailing rows are discarded.eurostat_dataframe_describe toolcanvas_id from a staging response; lists each table's name, rowCount, typed columns, and expiresAt (sliding CANVAS_TTL_MS, default 24h); fails as canvas_not_found or canvas_disabledeurostat_query_dataset writes a code and a _label column per dimension, eurostat_download_dataset codes only plus time (both share the obs_* and conf_status* measures), and eurostat_get_dimension_values writes code and labeleurostat_dataframe_query toolSELECT per call; chained statements, other verbs, and functions that read files or external data are rejectedcolumns, rows, rowCount, and required boolean truncated. Whole rows fit within CANVAS_DEFAULT_ROW_LIMIT (default 10,000) and 64,000 bytes independently for JSON-serialized structuredContent and content[], including enrichment. At least one complete row is retained even if oversized. Cells render as lossless JSON, including nesting and quoted strings; 64-bit integers arrive as strings. Fails as missing_table, canvas_not_found, or canvas_disabledtruncated: true and adds a notice. Page an unchanged deterministic SELECT with a total ORDER BY (tie-breakers required), LIMIT and OFFSET; advance by actual rowCount. Restart an initially unordered result sorted at offset zero. Volatile SQL and changing or expired tables cannot provide stable pagingeurostat_dataframe_drop toolcanvas_id plus table_name, exactly as eurostat_dataframe_describe lists it; removes that one table and leaves the canvas and its other tables in place. Listed only when EUROSTAT_DATAFRAME_DROP_ENABLED=true is set beside the canvascanvasId, tableName, dropped, and expiresAt; dropped is false, with a notice, when nothing by that name was staged, so repeating a drop is safe. Fails as canvas_not_found, identifier_reserved, or canvas_disabledeurostat://dataset/{dataset_code} resourceeurostat_get_dataset_info, returned as application/json; a failed read carries the same reason, and retryable on an outagedataset_code comes from eurostat_search_datasets or eurostat_browse_themesBuilt 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:
DS-* collections (detailed trade by CN8, HS, SITC, BEC and CPA, and PRODCOM) through the same tools. Filter every request, freq included, since trade flows mix annual and monthly series; aggregates such as TOTAL, EU27_2020, and EXT_EU27_2020 double-count when summed with their members. PRODCOM text values arrive in valueText / obs_value_textaggregate / country / nuts1 / nuts2 / nuts3), and OBS_FLAG and CONF_STATUS decoded into separate fieldsasync_response, extraction_too_big, or async_queued errors that name the dimensions left to filtercanvasId / tableName / stagedRowCount) on the DuckDB dataframe canvas, which is on by default. The .mcpb bundle strips the native DuckDB binding and runs with CANVAS_PROVIDER_TYPE=none, so SQL analytics need the npm, Docker, or from-source installAgent-friendly output:
reason and a recovery.hint naming the next tool to calleurostat_search_datasets and eurostat_browse_themes return a nextStep pointing at the follow-up callobsCount, timeRange.start / end, lastUpdated) are omitted rather than zeroedappliedFilters on eurostat_query_dataset and appliedQuery (with the SDMX url) on eurostat_download_dataset, plus a notice when the preview omits rowsA 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"
}
}
}
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/mcp
git clone https://github.com/cyanheads/eurostat-mcp-server.git
cd eurostat-mcp-server
bun install
cp .env.example .env
| 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 rows one eurostat_dataframe_query returns before reporting truncated; a separate 64,000-byte budget applies to each serialized response surface, retaining at least one complete row. | 10000 |
EUROSTAT_CLAMP_LIMITS | Clamp positive integer preview/search limits to 500/100 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.
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 build -t eurostat-mcp-server .
docker run --rm -p 3010:3010 eurostat-mcp-server
The 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.
| 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. |
tests/ | Unit and integration tests, mirroring the src/ structure. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for logging, ctx.state for storagecreateApp() arrays in src/index.tsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.