Version License Docker MCP SDK npm TypeScript Bun
Install in Claude Desktop Install in Cursor Install in VS Code
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.