Search and query the Eurostat catalogue — EU economy, demography, trade, and NUTS regional data.
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 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.
Two of the eight are listed only when the dataframe canvas is enabled (CANVAS_PROVIDER_TYPE=duckdb).
| 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, with 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 — canvas only |
eurostat_dataframe_query | Run a read-only SQL SELECT across staged tables — canvas only |
| Resource | Description |
|---|---|
eurostat://dataset/{dataset_code} | Dataset metadata (dimensions, time range, observation count, last-updated) by URI, for cache-injectable context |
eurostat_search_datasets toolcode, label, type (dataset/table), period coverage, observation count, and theme breadcrumb per resulttotalMatches and page slots count unique targetslimit (1–100, default 20) sets page size, totalMatches reports the full count, and passing nextCursor back as cursor pages 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, returns invalid_cursor instead of a silently shifted pagenextStep on each result names the next tool to callEUROSTAT_TOC_CACHE_TTL_MS), refreshed on the next call past that ageeurostat_browse_themes tooltheme_code: returns the top-level theme folders (Economy, Population, Transport, etc.)theme_code: returns immediate children — subtheme folders and datasets in that branchcode, label, type (folder/dataset/table), data period, and observation count where availableparentPath from root to the current node, plus a nextStep hint suited to the levelotherPlacements names those so the ambiguity is visibleeurostat_get_dataset_info tooltime), not just what appears in populated observationseurostat_get_dimension_values for the full listmetadataUrl links to the ESMS metadata page when Eurostat provides oneeurostat_get_dimension_values tooleurostat_get_dataset_info usesgeo, NUTS hierarchy filtering via geo_level: aggregate, country (default), nuts1, nuts2, nuts3 — an empty level reports no_results rather than implying the dataset lacks data, and pairing geo_level with any other dimension is rejectedeurostat_query_dataset or eurostat_download_dataset — an invalid dimension value returns no data silently from the former and a rejected fault from the lattereurostat_query_dataset tool{dimension_code: [values]}; a geo filter and geo_level (NUTS: aggregate/country/nuts1/nuts2/nuts3) are mutually exclusive, as are since_period/until_period and last_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 changes obsCount, 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 itselfvalue, an optional OBS_FLAG status (e.g. p=provisional, e=estimated), and a separate optional CONF_STATUS confStatus marker — usually why a value is nulltruncated is true only when the match exceeds the 5,000-observation staging threshold, independent of preview_limit. With the dataframe canvas enabled, a match above that threshold is staged whole as a SQL table (canvasId / tableName / stagedRowCount) — call eurostat_dataframe_describe before eurostat_dataframe_query; without a canvas those fields are absent and narrowing the query is the only way to reach the restcanvas_id reuses 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 outeurostat_download_dataset reads the SDMX bulk endpoint instead, at roughly half the byteseurostat_download_dataset tooleurostat_query_dataset reads for the same data — measured across four datasets from 1.1M to 12.8M observations{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 malformedlast_n_periods here — only since_period / until_period actually shrink the response, since the TSV layout keeps a column per period regardless of selectorEUROSTAT_BULK_MAX_BYTES, default 50 MiB) is enforced while streaming — Eurostat sends no Content-Length, so a transfer stopped mid-flight returns its rows with budgetExceeded: true instead of an errornot_found (100), filter_arity (140), and invalid_dimension (150 — also covers an out-of-coverage period range), each with a recovery hint naming the next toolcanvasId / tableName / stagedRowCount), streamed row by row — call eurostat_dataframe_describe before eurostat_dataframe_query. Without a canvas, only preview_limit rows (default 50, max 500) survive the call; rowCount / missingCount / periodRange still describe the whole downloadeurostat_dataframe_describe toolcanvas_id, returned by eurostat_query_dataset or eurostat_download_dataset) with row counts, column names, types, and nullability — call before writing SQL, since the two stagers write different dimension columnsexpiresAt; every call against a canvas slides its lifetime forward (CANVAS_TTL_MS, default 24h)canvas_disabled when this deployment runs without a canvas, canvas_not_found when the ID is unknown or expiredeurostat_dataframe_query toolSELECT against staged tables; statement chaining, non-SELECT verbs, and functions that read files or external data are rejected with a typed erroreurostat_query_dataset tables carry a code column per dimension plus a _label companion; eurostat_download_dataset tables carry code columns only (no labels) plus a time column — 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 and timeobs_flag = NULL with conf_status = 'C' on either table — JSON-stat folds the two into one string (|C) that eurostat_query_dataset splits before stagingCANVAS_DEFAULT_ROW_LIMIT (default 10,000); truncated: true means add a LIMIT, an aggregate, or a narrower WHERE. 64-bit integer results — COUNT(*) included — arrive as strings so values outside the JSON number range survive intactCANVAS_PROVIDER_TYPE=duckdb is the only switch — except the one-click .mcpb bundle, which strips native bindings to stay portable; use the npm, Docker, or from-source install for SQL analyticseurostat://dataset/{dataset_code} resourceeurostat_get_dataset_info, addressable as a resource URI for cache-injectable contextdataset_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:
aggregate / country / nuts1 / nuts2 / nuts3) across eurostat_query_dataset and eurostat_get_dimension_valuesAgent-friendly output:
reason and a recovery.hint naming the exact next tool to call, not just an error stringeurostat_search_datasets and eurostat_browse_themes responses carry a nextStep field pointing at the right follow-up callobsCount, timeRange.start/end, lastUpdated) are omitted from the response rather than defaulted to zero or blankeurostat_dataframe_describe → eurostat_dataframe_query follow-upA 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
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
| Variable | Description | Default |
|---|---|---|
MCP_TRANSPORT_TYPE | Transport: stdio or http | stdio |
MCP_HTTP_PORT | HTTP server port | 3010 |
MCP_HTTP_ENDPOINT_PATH | HTTP endpoint path | /mcp |
MCP_PUBLIC_URL | Public origin override for TLS-terminating reverse-proxy deployments | none |
MCP_SESSION_MODE | HTTP session posture: auto, stateful, or stateless (auto resolves to stateful). The server declares stateless in createApp(); an explicitly set value overrides that default. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.) | info |
MCP_GC_PRESSURE_INTERVAL_MS | Opt-in Bun-only forced-GC pressure loop (ms). Recommended starting point if heap growth is observed: 60000. | 0 (disabled) |
LOGS_DIR | Directory for log files (Node.js only) | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1 | in-memory |
EUROSTAT_BASE_URL | Eurostat API base URL | https://ec.europa.eu/eurostat/api/dissemination |
EUROSTAT_REQUEST_TIMEOUT_MS | HTTP request timeout in ms | 30000 |
EUROSTAT_TOC_CACHE_TTL_MS | Catalogue TOC cache lifetime in ms — the first search or browse call past this age refreshes it | 43200000 (12 hours) |
EUROSTAT_BULK_TIMEOUT_MS | HTTP timeout for one eurostat_download_dataset transfer in ms — held separate because a bulk body streams for minutes | 120000 (2 minutes) |
EUROSTAT_BULK_MAX_BYTES | Byte budget for one bulk download, counted on the decoded TSV and enforced while streaming | 52428800 (50 MiB) |
CANVAS_PROVIDER_TYPE | duckdb enables the dataframe canvas: lists the two dataframe tools, lets eurostat_query_dataset stage a match above 5,000 observations, and lets eurostat_download_dataset retain a bulk download | none |
CANVAS_TEMP_PATH | Directory DuckDB writes canvas spill files to. Must be writable by the server process | <os tmpdir>/mcp-canvas |
CANVAS_TTL_MS | Sliding lifetime of a staged canvas in ms; every call against it extends the window | 86400000 (24 hours) |
CANVAS_DEFAULT_ROW_LIMIT | Max rows one eurostat_dataframe_query returns before reporting truncated | 10000 |
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 resources and inits services. |
src/mcp-server/tools | Tool definitions (*.tool.ts). Six tools for discovery and data access, plus two canvas-gated dataframe tools. |
src/mcp-server/resources | Resource definitions. Dataset metadata resource. |
src/services/eurostat-catalogue | Catalogue service — fetches and parses the Eurostat TOC TXT file; TTL-bounded in-memory cache. |
src/services/eurostat-data | Data service — dataset-scoped SDMX metadata parser plus Statistics API querying, JSON-stat 2.0 decoding, async-response detection, and dataframe row source. |
src/services/canvas-accessor.ts | Module-level accessor for the optional DataCanvas, plus the acquire helper that names the misconfigured path on a permission failure. |
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() arraysIssues 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.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/eurostat-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-eurostat-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/eurostat-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/eurostat-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.