Skip to main content
Glama
cyanheads

eurostat-mcp-server

by cyanheads

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework

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

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

  • query 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 limit

  • Results carry code, 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 unreachable

  • If the Comext dataflow list cannot be loaded, results carry comextListMissing and a notice; a catalogue that could not be refreshed carries catalogueLoadedAt


eurostat_browse_themes tool

  • Omit theme_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)

  • Items carry 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 unreachable

  • If the Comext dataflow list cannot be loaded, ext_go and icts carry comextListMissing and a notice; a catalogue that could not be refreshed carries catalogueLoadedAt


eurostat_get_dataset_info tool

  • One dataset_code; returns every dimension with valuesCount and up to 10 sampleValues, plus timeRange, obsCount, lastUpdated, and the ESMS metadataUrl

  • Fails as not_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 tool

  • dataset_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 matchCount

  • Long lists come back in pages of up to 64,000 bytes (or an optional limit), 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_found

  • canvas_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 own


eurostat_query_dataset tool

  • dataset_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 DE

  • Each observation carries code/label pairs per dimension, a nullable value, 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 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-retryable async_response for a query too large to serve, canvas_not_found, or a retryable upstream_fault when the Statistics API is down


eurostat_download_dataset tool

  • dataset_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 notice

  • Returns rowCount (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_found

  • The EUROSTAT_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 tool

  • canvas_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_disabled

  • Columns depend on the staging tool: eurostat_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 label


eurostat_dataframe_query tool

  • One read-only SELECT per call; chained statements, other verbs, and functions that read files or external data are rejected

  • Returns columns, 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_disabled

  • A cut sets truncated: 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 paging


eurostat_dataframe_drop tool

  • canvas_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 canvas

  • Returns canvasId, 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_disabled


eurostat://dataset/{dataset_code} resource

  • Same payload as eurostat_get_dataset_info, returned as application/json; a failed read carries the same reason, and retryable on an outage

  • dataset_code comes from eurostat_search_datasets or eurostat_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, 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_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, or async_queued errors that name the dimensions left to filter

  • A query match above 5,000 observations, or any bulk download, is staged whole as a SQL table (canvasId / 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 install

Agent-friendly output:

  • Typed error contracts: every declared failure carries a reason and a recovery.hint naming the next tool to call

  • Next-step hints: eurostat_search_datasets and eurostat_browse_themes return a nextStep pointing 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: appliedFilters on eurostat_query_dataset and appliedQuery (with the SDMX url) on eurostat_download_dataset, plus a notice when the preview omits rows

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/mcp

Prerequisites

  • Bun v1.4.0 or higher (or Node.js v24+).

  • No API key: Eurostat's dissemination API and its Comext host are public.

Installation

  1. Clone the repository:

git clone https://github.com/cyanheads/eurostat-mcp-server.git
  1. Navigate into the directory:

cd eurostat-mcp-server
  1. Install dependencies:

bun install
  1. Configure environment (optional, every setting has a default):

cp .env.example .env

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 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.

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: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 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.

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.

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/catch in tool logic

  • Use ctx.log for logging, ctx.state for storage

  • Register new tools and resources in the createApp() arrays in src/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.

Related MCP Connectors

Related MCP Servers