eurostat-mcp-server

v0.6.3 pre-1.0

Search and query the Eurostat catalogue — EU economy, demography, trade, health, and NUTS regional data via MCP. STDIO or Streamable HTTP.

eurostat.caseyjhand.com/mcp
claude mcp add --transport http eurostat-mcp-server https://eurostat.caseyjhand.com/mcp
codex mcp add eurostat-mcp-server --url https://eurostat.caseyjhand.com/mcp
{
  "mcpServers": {
    "eurostat-mcp-server": {
      "url": "https://eurostat.caseyjhand.com/mcp"
    }
  }
}
gemini mcp add --transport http eurostat-mcp-server https://eurostat.caseyjhand.com/mcp
{
  "mcpServers": {
    "eurostat-mcp-server": {
      "command": "bunx",
      "args": [
        "mcp-remote",
        "https://eurostat.caseyjhand.com/mcp"
      ]
    }
  }
}
{
  "mcpServers": {
    "eurostat-mcp-server": {
      "type": "http",
      "url": "https://eurostat.caseyjhand.com/mcp"
    }
  }
}
curl -X POST https://eurostat.caseyjhand.com/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

Tools

8

eurostat_search_datasets

open-world

Search the Eurostat catalogue by keyword. Returns matching datasets with codes, descriptions, period coverage, and theme breadcrumbs. Use this to discover dataset codes before calling eurostat_get_dataset_info, then eurostat_query_dataset for a slice of a dataset or eurostat_download_dataset for the whole of one. Results are limited to datasets and predefined tables — folders are excluded.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "eurostat_search_datasets",
    "arguments": {
      "query": "<query>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "minLength": 1,
      "pattern": "\\S",
      "description": "Search terms — at least one non-whitespace token is required. Split on whitespace into tokens; every token must match (AND), case-insensitively, somewhere across the dataset label, theme breadcrumb, or code. Word order does not matter, so \"business demography NUTS 3\" or \"regional economic accounts\" resolve without naming a label verbatim."
    },
    "limit": {
      "default": 20,
      "description": "Page size — maximum datasets returned per page (1–100). Default is 20. To retrieve matches beyond one page, pass the returned nextCursor back as cursor; the page size is fixed by this first call.",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "cursor": {
      "description": "Opaque pagination cursor from a previous call's nextCursor. Omit for the first page; pass it back — with the same query — to fetch the next page of matches over a stable order. A cursor is bound to the query that produced it and to the catalogue snapshot in effect at that time, so reusing one with a different query, or after the catalogue refreshes, is rejected rather than silently paging a different result set.",
      "type": "string"
    }
  },
  "required": [
    "query",
    "limit"
  ],
  "additionalProperties": false
}
view source ↗

eurostat_browse_themes

open-world

Navigate the Eurostat theme tree. Without theme_code returns the top-level theme folders (Economy, Population, Transport, etc.) — the practical starting points. With a theme_code returns its immediate children: subtheme folders and datasets in that branch. Use this for structured discovery when you know the domain but not the dataset code, or to drill down from a broad topic to a specific dataset. Pair with eurostat_search_datasets for keyword-based discovery.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "eurostat_browse_themes",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "theme_code": {
      "description": "Folder code to expand (e.g., \"economy\", \"reg\"). Omit to list the top-level theme folders.",
      "type": "string"
    }
  },
  "additionalProperties": false
}
view source ↗

eurostat_get_dataset_info

open-world

Fetch metadata for a Eurostat dataset: dimensions with valid values, time range, observation count, and last-update date. Call this before eurostat_query_dataset or eurostat_download_dataset to discover what dimension codes are valid (unit, na_item, geo, etc.); eurostat_download_dataset builds its positional filter key from this dimension list, so a filter naming a dimension absent here is rejected outright. Returns up to 10 sample values per dimension for orientation; use eurostat_get_dimension_values to list the full set for large dimensions.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "eurostat_get_dataset_info",
    "arguments": {
      "dataset_code": "<dataset_code>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "dataset_code": {
      "type": "string",
      "minLength": 1,
      "description": "Dataset code (e.g., \"nama_10_gdp\"). Use eurostat_search_datasets or eurostat_browse_themes to find codes."
    }
  },
  "required": [
    "dataset_code"
  ],
  "additionalProperties": false
}
view source ↗

eurostat_get_dimension_values

open-world

List all valid values for a specific dimension in a Eurostat dataset (e.g., all unit codes for nama_10_gdp, all geo codes for a regional dataset). Use this when eurostat_get_dataset_info returns more values than the 10-item sample, or to confirm exact codes before querying. For the "geo" dimension, use geo_level to filter by NUTS hierarchy (country, nuts1, nuts2, nuts3). Invalid dimension_value codes silently return no data from eurostat_query_dataset, and are rejected by Eurostat as a fault on eurostat_download_dataset; use this tool to verify codes first.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "eurostat_get_dimension_values",
    "arguments": {
      "dataset_code": "<dataset_code>",
      "dimension": "<dimension>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "dataset_code": {
      "type": "string",
      "minLength": 1,
      "description": "Dataset code (e.g., \"nama_10_gdp\")."
    },
    "dimension": {
      "type": "string",
      "minLength": 1,
      "description": "Dimension code to retrieve values for (e.g., \"unit\", \"na_item\", \"geo\"). Use eurostat_get_dataset_info to see available dimensions."
    },
    "geo_level": {
      "description": "NUTS hierarchy level filter — applies only when dimension is \"geo\"; passing it with any other dimension is rejected. Options: \"aggregate\" (EU/EA codes), \"country\" (2-letter codes, default), \"nuts1\" (3-char), \"nuts2\" (4-char), \"nuts3\" (5-char).",
      "type": "string",
      "enum": [
        "aggregate",
        "country",
        "nuts1",
        "nuts2",
        "nuts3"
      ]
    }
  },
  "required": [
    "dataset_code",
    "dimension"
  ],
  "additionalProperties": false
}
view source ↗

eurostat_query_dataset

open-world

Fetch statistical data from a Eurostat dataset with dimension filters. Returns a deterministic inline prefix of decoded observations with dimension codes and labels, numeric values, an OBS_FLAG status (e.g., "p" = provisional, "e" = estimated) and a separate CONF_STATUS confidentiality marker (e.g., "C" = confidential, which is usually why a value is null). preview_limit controls only that prefix; filters and period controls reduce the matched result itself. Call eurostat_get_dataset_info first to discover valid dimension codes and values. Apply filters to keep the result set manageable — large unfiltered queries may trigger an async response error. Use filters.geo for specific country/region codes, or geo_level for NUTS hierarchy filtering (mutually exclusive). Use last_n_periods for the N most recent periods without knowing the end date. Matches above 5,000 observations are staged whole when this deployment runs a dataframe canvas: call eurostat_dataframe_describe first, then eurostat_dataframe_query. Matches at or below 5,000 are never staged. When the target is a whole dataset rather than a slice, eurostat_download_dataset reads the SDMX bulk endpoint instead and is the cheaper route.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "eurostat_query_dataset",
    "arguments": {
      "dataset_code": "<dataset_code>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "dataset_code": {
      "type": "string",
      "minLength": 1,
      "description": "Dataset code (e.g., \"nama_10_gdp\"). Required."
    },
    "filters": {
      "default": {},
      "description": "Dimension filters as a map of dimension code → array of valid values. Example: {\"unit\": [\"CP_MEUR\"], \"na_item\": [\"B1GQ\"], \"geo\": [\"DE\", \"FR\"]}. An empty array is treated as no filter for that dimension and is dropped from the request. Do not include \"geo\" here if using geo_level. Invalid dimension values silently return no data — verify with eurostat_get_dimension_values first.",
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {
        "type": "array",
        "items": {
          "type": "string"
        }
      }
    },
    "geo_level": {
      "description": "Filter by NUTS hierarchy level. Mutually exclusive with a \"geo\" key in filters. Options: \"aggregate\" (EU/EA totals), \"country\" (41 member/candidate states), \"nuts1\" (127 major regions), \"nuts2\" (309 basic regions), \"nuts3\" (1,343 small regions).",
      "type": "string",
      "enum": [
        "aggregate",
        "country",
        "nuts1",
        "nuts2",
        "nuts3"
      ]
    },
    "since_period": {
      "description": "Start of time range (e.g., \"2020\", \"2023-Q1\", \"2024-01\"). Mutually exclusive with last_n_periods.",
      "type": "string"
    },
    "until_period": {
      "description": "End of time range (e.g., \"2024\"). Omit for data through the latest available period. Mutually exclusive with last_n_periods.",
      "type": "string"
    },
    "last_n_periods": {
      "description": "Return only the N most recent periods. Mutually exclusive with since_period and until_period.",
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "preview_limit": {
      "default": 50,
      "description": "How many matched observations to return inline, from the deterministic start of the JSON-stat cell order. Default 50; maximum 500. This changes only the inline prefix: it does not reduce obsCount, missingObsCount, timeRange, the upstream response, or the rows staged when the match exceeds 5,000. Use filters or period controls to reduce the match itself.",
      "type": "integer",
      "minimum": 1,
      "maximum": 500
    },
    "lang": {
      "default": "EN",
      "description": "Language for labels in the response. Default is \"EN\". Options: \"EN\", \"FR\", \"DE\".",
      "type": "string",
      "enum": [
        "EN",
        "FR",
        "DE"
      ]
    },
    "canvas_id": {
      "description": "Reuse an existing dataframe canvas, so a result staged by this call lands beside earlier ones and can be joined against them. Pass a canvasId from a previous response; omit to start a fresh canvas. Ignored on deployments without a dataframe canvas and when the match is at or below 5,000 observations.",
      "type": "string"
    }
  },
  "required": [
    "dataset_code",
    "filters",
    "preview_limit",
    "lang"
  ],
  "additionalProperties": false
}
view source ↗

eurostat_download_dataset

open-world

Download a Eurostat dataset in bulk through the SDMX 2.1 TSV endpoint and stage every observation as a SQL table on the dataframe canvas — the route to a whole dataset, where eurostat_query_dataset is the route to a slice of one. The TSV wire format is roughly half the bytes of the JSON-stat body eurostat_query_dataset reads, so it reaches datasets that would otherwise time out, and it is expanded here into one row per observation. Filters take the same dimension-code map eurostat_query_dataset uses and are applied server-side by Eurostat; call eurostat_get_dataset_info first for the dimension codes and eurostat_get_dimension_values for their values. Narrow with since_period/until_period rather than asking for the most recent N periods — the TSV layout keeps a column for every period whichever is requested, so a period range is what actually shrinks the response. Transfers are bounded by a byte budget enforced while streaming: when it is spent the download stops and budgetExceeded is set, leaving a prefix of the dataset rather than an error. Only preview_limit rows come back inline. When a table is staged, call eurostat_dataframe_describe first to confirm its columns, then eurostat_dataframe_query; without a canvas, rows past the preview are not retained.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "eurostat_download_dataset",
    "arguments": {
      "dataset_code": "<dataset_code>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "dataset_code": {
      "type": "string",
      "minLength": 1,
      "description": "Dataset code (e.g., \"nama_10_gdp\"). Required."
    },
    "filters": {
      "default": {},
      "description": "Dimension filters as a map of dimension code → array of accepted values, applied by Eurostat before the body is sent. Example: {\"unit\": [\"CP_MEUR\"], \"na_item\": [\"B1G\"], \"geo\": [\"DE\", \"FR\"]}. Omit a dimension or pass an empty array to accept every value for it. Do not put \"time\" here — use since_period/until_period. Naming a dimension the dataset does not have is rejected with the dataset's dimension list rather than silently ignored.",
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {
        "type": "array",
        "items": {
          "type": "string"
        }
      }
    },
    "since_period": {
      "description": "Start of the period range (e.g., \"2020\", \"2023-Q1\", \"2024-01\"), sent as startPeriod. The most effective way to shrink a bulk response: it removes period columns from the TSV rather than blanking their cells.",
      "type": "string"
    },
    "until_period": {
      "description": "End of the period range (e.g., \"2024\"), sent as endPeriod. Omit for data through the latest available period.",
      "type": "string"
    },
    "preview_limit": {
      "default": 50,
      "description": "How many observations to echo inline, from the start of the download. Caps at 500. The full download is on the canvas table when one was staged; this is orientation, not the result set.",
      "type": "integer",
      "minimum": 1,
      "maximum": 500
    },
    "canvas_id": {
      "description": "Reuse an existing dataframe canvas so this download lands beside earlier results and can be joined against them. Pass a canvasId from a previous response; omit to start a fresh canvas. Ignored on deployments without a dataframe canvas.",
      "type": "string"
    }
  },
  "required": [
    "dataset_code",
    "filters",
    "preview_limit"
  ],
  "additionalProperties": false
}
view source ↗

eurostat_dataframe_describe

List the tables staged on a Eurostat dataframe canvas, with their row counts and column names and types. Call this before eurostat_dataframe_query to learn the table and column names to write SQL against. The canvas_id comes from a eurostat_query_dataset or eurostat_download_dataset response that reported a staged table. Every observation column is flat, but the two stagers write different dimension columns, so read the columns reported here rather than assuming: eurostat_query_dataset gives each dimension a code column named after the dimension (e.g. "geo") plus a label companion (e.g. "geo_label"); eurostat_download_dataset gives code columns only — the bulk endpoint carries 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 — carrying the same codes for the same observation, so tables from the two stagers join on dimension codes and time and compare like with like.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "eurostat_dataframe_describe",
    "arguments": {
      "canvas_id": "<canvas_id>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "type": "string",
      "minLength": 1,
      "description": "Canvas identifier returned as canvasId by eurostat_query_dataset or eurostat_download_dataset. Identifies the workspace holding the staged tables."
    }
  },
  "required": [
    "canvas_id"
  ],
  "additionalProperties": false
}
view source ↗

eurostat_dataframe_query

Run a read-only SQL SELECT against tables staged on a Eurostat dataframe canvas — the way to reach observations past the 5,000-row inline cap of eurostat_query_dataset and past the inline preview of a eurostat_download_dataset bulk download, and to aggregate, group, or join across staged tables without re-fetching from Eurostat. Call eurostat_dataframe_describe first for the table and column names, which differ between the two stagers. Only a single SELECT statement runs: statement chaining, non-SELECT verbs, and functions that read files or external data are rejected. Columns are flat — every dimension is a code column named after the dimension, the measure is obs_value, the observation flag is obs_flag / obs_flag_label and the confidentiality marker is conf_status / conf_status_label; a "_label" companion per dimension exists only on tables eurostat_query_dataset staged. Both stagers write the same five measure columns with the same codes, so join their tables on dimension codes and time and compare obs_flag or conf_status across them directly.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "eurostat_dataframe_query",
    "arguments": {
      "canvas_id": "<canvas_id>",
      "sql": "<sql>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "type": "string",
      "minLength": 1,
      "description": "Canvas identifier returned as canvasId by eurostat_query_dataset or eurostat_download_dataset. Identifies the workspace holding the staged tables."
    },
    "sql": {
      "type": "string",
      "minLength": 1,
      "description": "A single read-only SELECT statement. Reference tables by the names eurostat_dataframe_describe reports. Example: SELECT geo, geo_label, AVG(obs_value) AS mean FROM df_a1b2c3d4 WHERE time >= '2020' GROUP BY geo, geo_label ORDER BY mean DESC."
    }
  },
  "required": [
    "canvas_id",
    "sql"
  ],
  "additionalProperties": false
}
view source ↗

Resources

1

Dataset metadata for a Eurostat dataset — dimensions, time range, observation count, and last-updated date. Equivalent to eurostat_get_dataset_info but accessible as a resource URI for cache-injectable context.

uri eurostat://dataset/{dataset_code} mime application/json