@cyanheads/ilostat-mcp-server
Search ILOSTAT labour indicators, query and compare series, build country profiles, run SQL via MCP. STDIO or Streamable HTTP.
9 Tools • 1 Skill
Overview
Labour statistics from ILOSTAT, the International Labour Organization's database. Find an indicator, pull observations for countries, regions, and income groups, compare areas, and build a labour-market profile, with every value marked as reported, modelled, or projected. No API key required.
Tools
ilostat_dataframe_drop is off unless ILOSTAT_DATAFRAME_DROP_ENABLED=true, and CANVAS_PROVIDER_TYPE=none turns off all three dataframe tools.
Skill
Ships in skills/ for plugin installs, the npm package, the Docker image, and the .mcpb bundle, and is served over MCP as skill://ilostat-guide/SKILL.md.
Capability reference
ilostat_search_indicators tool
- Plain-language
query (every term must match a word or word prefix; a term no catalog word starts with matches by its stem, so informality finds informal and disabled finds disability; female, women, male, and men match every indicator broken down by sex, and the notice names the sex filter for ilostat_query_indicator; case, accents, and labour/labor folded) plus frequency, database, subject, breakdown, measure, and aggregates_only filters; omit query to browse by filters
- Up to 50 hits per page (default 10; a larger
limit is applied as 50, with a notice), continued with next_cursor; each hit is one indicator with a dataset_id per frequency, coverage years, n_ref_area, last_update, and has_aggregates, and facets count the whole match set
- An unrecognized
database, subject, breakdown, or measure code fails as unknown_filter_code
ilostat_describe_indicator tool
- One
dataset_id (UNE_DEAP_SEX_AGE_RT_A) or a bare indicator code, which describes every frequency; an unknown code returns found: false with guidance
- Definition,
unit and its multiplier, frequency variants, breakdowns (sex codes, plus classif1 / classif2 codes with totals marked is_total), default_slice, covered ref_areas, basis_rule, and the first 20 related_datasets with related_total
- When the SDMX API can't answer,
structure_status is unavailable and breakdowns, unit, and areas are absent rather than the call failing
ilostat_query_indicator tool
- Up to 100
dataset_ids; the first 3 distinct datasets are queried and the rest come back as remaining_dataset_ids for another call. Filter by ref_areas (up to 300) and/or an area_group, sex, classif1, classif2, sources, and period: one exact time, a time_from / time_to year window, latest_only, or both
latest_only keeps each series' own latest period; a request whose full history is too large to read in one call falls back to the latest period per area and dataset, and applied_filters.latest_mode and the notice say so
source_selection is best by default, all, or secondary; rows carry source, obs_status, notes, and basis, decoded in legend, and applied_filters echoes every parameter sent
- A result past the inline preview stages in full as a
df_<id> dataframe; a request over ILOSTAT_MAX_ROWS fails as request_too_broad (unfiltered) or result_too_large (filtered)
ilostat_get_country_profile tool
- One
ref_area with annual data, an ISO3 country (KEN) or an X-coded aggregate (X01 World, regions, income groups), and sex (SEX_T by default, SEX_M, or SEX_F)
- Nine headline indicators (labour force participation, employment-to-population ratio, unemployment, youth unemployment, youth NEET, informal employment, employment, labour income share, working poverty), each with its latest
reported value for that slice and, separately, its latest non-projected modelled estimate
- A missing reported value stays missing and is listed in
reported_missing; aggregates carry modelled values only
ilostat_compare_geographies tool
- One
dataset_id and one slice (sex, classif1, classif2, defaulting to the dataset's totals); areas from ref_areas (up to 300), an area_group, or both, and at least one is required
period puts every area at one period; without it, each area gets its latest value within lookback_years (default 10), projections excluded unless include_projections; change_years (1–30) adds each value's change over that span
- A change carries the earlier value's source, basis, status, and notes, with
source_changed, basis_changed, and break_in_series flags; the notice counts the changes that are not like-for-like
- Rows carry
rank, value, period, basis, and source; areas without a value land in missing with a reason, and comparability reports mixed_periods, basis_counts, and distinct_sources
ilostat_list_reference tool
topic: ref_areas, area_groups, databases, subjects, sexes, classifications, classification_types, sources, obs_status, notes, or frequencies
- A text
filter or up to 100 exact codes (misses in not_found; exact area_groups lookups list member countries); ref_area narrows sources, and classification_type narrows classifications
- Up to 500 entries per page (default 50; a larger
limit is applied as 500, with a notice), continued with next_cursor
ilostat_dataframe_describe tool
- Pass one
df_XXXXX_XXXXX, or omit name to list every staged dataframe, newest first (listing is off under HTTP with auth none; use the exact name)
- Each entry carries the producing
source_tool and its query_params, datasets, coverage, basis_counts, attribution, created_at / expires_at, row_count, and the column_schema to write SQL against
- Each dataset keeps its full unit beside
unit: unit_measure, unit_type, unit_type_label, unit_multiplier, and unit_multiplier_label; values are never rescaled
ilostat_dataframe_query tool
- One DuckDB
SELECT (joins, aggregates, window functions, CTEs) over df_<id> tables, up to 20,000 characters; writes, DDL, multi-statement SQL, file references and file-reading functions, engine-setting reads, and system catalogs are rejected
row_limit caps the rows materialized (default 1,000; capped at 10,000 per call, with row_count_capped flagging a hit) and preview the rows returned inline, which also stop at ILOSTAT_PREVIEW_CHARS; applied_limits echoes the values applied, and BIGINT values arrive as strings
register_as stores the full result as a new dataframe with a fresh TTL; a result over 1,000,000 rows fails as register_as_too_large, and evicted names any older dataframes dropped to make room
ilostat_dataframe_drop tool
- Drops one staged dataframe by
name before its TTL; idempotent, with dropped: false when nothing matched
- Listed only when
ILOSTAT_DATAFRAME_DROP_ENABLED=true and dataframes are on; it is the only destructive tool
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.
ILOSTAT-specific:
- Two keyless ILO APIs: the ILOSTAT data API serves the catalog, the code dictionaries, and observations; the ILO SDMX API serves each dataset's breakdown codes, default slice, and unit
- The catalog and code dictionaries are held in memory and rechecked every
ILOSTAT_CATALOG_REFRESH_HOURS; every code a tool takes is validated against them before a request goes upstream
- A request pacer per upstream host; a 429 or challenge page reaches the caller as a retryable
upstream_busy carrying retryAfter. Data responses of up to 5,000 rows are cached for ILOSTAT_CACHE_TTL_SECONDS
- Dataframes on by default: a query or comparison larger than
ILOSTAT_PREVIEW_CHARS stages in full as a DuckDB df_<id> table that lives for ILOSTAT_DATASET_TTL_SECONDS, up to 1,000,000 rows in 100 dataframes per tenant (older ones are evicted and named in evicted). With CANVAS_PROVIDER_TYPE=none, or in the .mcpb bundle (no DuckDB binding), results stop at the inline preview and say so
- ILOSTAT data and metadata are published under the ILO Open Access policy as CC BY 4.0; every data response carries an
attribution to keep with the numbers
Agent-friendly output:
- Basis on every value: each row is
reported, modelled_estimate, or projection, and results carry basis_counts. The country profile returns reported and modelled values in separate fields and never fills one from the other
- Provenance and decoding: rows keep
source, obs_status, and note codes with a legend that decodes them, and query and comparison responses echo applied_filters, defaults included
- Misses as data: an unknown dataset returns
found: false with guidance, and compared areas without a value land in missing with a typed reason
- Typed errors: failures carry a reason (
unknown_code, request_too_broad, upstream_busy, …) and a recovery hint
Getting started
Public Hosted Instance
A public instance is available at https://ilostat.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
Every caller of the hosted instance shares one request pacer per ILO API and one dataframe workspace, so ilostat_dataframe_describe takes a table's exact name there; listing is off.
Self-Hosted / Local
Add the following to your MCP client configuration file.
Or with npx (no Bun required):
Or with Docker:
For Streamable HTTP, set the transport and start the server:
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- No API key or account: both ILO APIs are open.
Installation
- Clone the repository:
- Navigate into the directory:
- Install dependencies:
- Configure environment:
Configuration
See .env.example for the full list of optional overrides.
Running the server
Local development
Project 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 in
buildToolDefinitions() in src/mcp-server/tools/definitions/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:
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details. The ILOSTAT data the tools return belongs to the International Labour Organization and is published under CC BY 4.0; cite ILOSTAT and the dataset ID.