io.github.cyanheads/ilostat-mcp-server
Search ILOSTAT labour indicators, query and compare series, build country profiles, run SQL.
What can you do with io.github.cyanheads/ilostat-mcp-server?
- 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_busycarryingretryAfter. Data responses of up to 5,000 rows are cached forILOSTAT_CACHE_TTL_SECONDS - Dataframes on by default: a query or comparison larger than
ILOSTAT_PREVIEW_CHARSstages in full as a DuckDBdf_<id>table that lives forILOSTAT_DATASET_TTL_SECONDS, up to 1,000,000 rows in 100 dataframes per tenant (older ones are evicted and named ... - ILOSTAT data and metadata are published under the ILO Open Access policy as CC BY 4.0; every data response carries an
attributionto keep with the numbers - Basis on every value: each row is
reported,modelled_estimate, orprojection, and results carrybasis_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 alegendthat decodes them, and query and comparison responses echoapplied_filters, defaults included - Misses as data: an unknown dataset returns
found: falsewithguidance, and compared areas without a value land inmissingwith a typedreason
Install
Add this to your client's MCP configuration (Claude Desktop: claude_desktop_config.json; Cursor: .cursor/mcp.json; VS Code and others use the same "mcpServers" shape).
claude mcp add --transport http ilostat-mcp-server 'https://ilostat.caseyjhand.com/mcp'
{
"mcpServers": {
"ilostat-mcp-server": {
"type": "http",
"url": "https://ilostat.caseyjhand.com/mcp"
}
}
}
@cyanheads/ilostat-mcp-serverclaude mcp add ilostat-mcp-server -e 'ILOSTAT_CATALOG_REFRESH_HOURS=6' -e 'ILOSTAT_MAX_ROWS=500000' -e 'ILOSTAT_PREVIEW_CHARS=40000' -e 'ILOSTAT_CACHE_TTL_SECONDS=900' -e 'ILOSTAT_DATASET_TTL_SECONDS=86400' -e 'ILOSTAT_DATAFRAME_DROP_ENABLED=false' -e 'CANVAS_PROVIDER_TYPE=duckdb' -e 'MCP_LOG_LEVEL=info' -- npx -y @cyanheads/ilostat-mcp-server@0.2.0
{
"mcpServers": {
"ilostat-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/ilostat-mcp-server@0.2.0"
],
"env": {
"ILOSTAT_CATALOG_REFRESH_HOURS": "6",
"ILOSTAT_MAX_ROWS": "500000",
"ILOSTAT_PREVIEW_CHARS": "40000",
"ILOSTAT_CACHE_TTL_SECONDS": "900",
"ILOSTAT_DATASET_TTL_SECONDS": "86400",
"ILOSTAT_DATAFRAME_DROP_ENABLED": "false",
"CANVAS_PROVIDER_TYPE": "duckdb",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Environment variables
ILOSTAT_CATALOG_REFRESH_HOURSHours between checks of the ILOSTAT tables of contents (1–168).ILOSTAT_MAX_ROWSCeiling on the rows one query may return or stage (1,000–1,000,000).ILOSTAT_PREVIEW_CHARSInline preview budget in serialized characters; a larger query or comparison result is staged as a dataframe, and a dataframe SQL result returns only the leading rows that fit.ILOSTAT_CACHE_TTL_SECONDSSeconds a small upstream data response stays cached (0 disables the cache).ILOSTAT_DATASET_TTL_SECONDSPer-table TTL for staged dataframes, in seconds (at least 60).ILOSTAT_DATAFRAME_DROP_ENABLEDSet to true to expose ilostat_dataframe_drop; staged dataframes otherwise expire on their TTL.CANVAS_PROVIDER_TYPEDataCanvas engine for staged dataframes: duckdb (default) or none to turn dataframes off.MCP_LOG_LEVELSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
@cyanheads/ilostat-mcp-serverclaude mcp add ilostat-mcp-server -e 'MCP_TRANSPORT_TYPE=<MCP_TRANSPORT_TYPE>' -e 'ILOSTAT_CATALOG_REFRESH_HOURS=6' -e 'ILOSTAT_MAX_ROWS=500000' -e 'ILOSTAT_PREVIEW_CHARS=40000' -e 'ILOSTAT_CACHE_TTL_SECONDS=900' -e 'ILOSTAT_DATASET_TTL_SECONDS=86400' -e 'ILOSTAT_DATAFRAME_DROP_ENABLED=false' -e 'CANVAS_PROVIDER_TYPE=duckdb' -e 'MCP_HTTP_HOST=127.0.0.1' -e 'MCP_HTTP_PORT=3010' -e 'MCP_HTTP_ENDPOINT_PATH=/mcp' -e 'MCP_AUTH_MODE=none' -e 'MCP_LOG_LEVEL=info' -- npx -y @cyanheads/ilostat-mcp-server@0.2.0
{
"mcpServers": {
"ilostat-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/ilostat-mcp-server@0.2.0"
],
"env": {
"MCP_TRANSPORT_TYPE": "<MCP_TRANSPORT_TYPE>",
"ILOSTAT_CATALOG_REFRESH_HOURS": "6",
"ILOSTAT_MAX_ROWS": "500000",
"ILOSTAT_PREVIEW_CHARS": "40000",
"ILOSTAT_CACHE_TTL_SECONDS": "900",
"ILOSTAT_DATASET_TTL_SECONDS": "86400",
"ILOSTAT_DATAFRAME_DROP_ENABLED": "false",
"CANVAS_PROVIDER_TYPE": "duckdb",
"MCP_HTTP_HOST": "127.0.0.1",
"MCP_HTTP_PORT": "3010",
"MCP_HTTP_ENDPOINT_PATH": "/mcp",
"MCP_AUTH_MODE": "none",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Environment variables
MCP_TRANSPORT_TYPESelects the HTTP transport.ILOSTAT_CATALOG_REFRESH_HOURSHours between checks of the ILOSTAT tables of contents (1–168).ILOSTAT_MAX_ROWSCeiling on the rows one query may return or stage (1,000–1,000,000).ILOSTAT_PREVIEW_CHARSInline preview budget in serialized characters; a larger query or comparison result is staged as a dataframe, and a dataframe SQL result returns only the leading rows that fit.ILOSTAT_CACHE_TTL_SECONDSSeconds a small upstream data response stays cached (0 disables the cache).ILOSTAT_DATASET_TTL_SECONDSPer-table TTL for staged dataframes, in seconds (at least 60).ILOSTAT_DATAFRAME_DROP_ENABLEDSet to true to expose ilostat_dataframe_drop; staged dataframes otherwise expire on their TTL.CANVAS_PROVIDER_TYPEDataCanvas engine for staged dataframes: duckdb (default) or none to turn dataframes off.MCP_HTTP_HOSTThe hostname for the HTTP server.MCP_HTTP_PORTThe port to run the HTTP server on.MCP_HTTP_ENDPOINT_PATHThe endpoint path for the MCP server.MCP_AUTH_MODEAuthentication mode to use: 'none', 'jwt', or 'oauth'.MCP_LOG_LEVELSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
Documentation
From the project's README on GitHub, refreshed weekly. Links open on GitHub.
Contents
@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
Public Hosted Server: https://ilostat.caseyjhand.com/mcp
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
| Tool | Description |
|---|---|
ilostat_search_indicators | Search the indicator catalog by plain-language terms and filters; each hit lists a dataset ID per available frequency |
ilostat_describe_indicator | Explain one dataset: definition, unit and multiplier, breakdown codes in use, covered areas, and how its values are classed |
ilostat_query_indicator | Fetch observations for up to 3 datasets per call, filtered by area or area group, sex, breakdown codes, source, and period |
ilostat_get_country_profile | Headline labour-market figures for one area: latest reported value and latest ILO modelled estimate, side by side |
ilostat_compare_geographies | Rank areas on one slice of one dataset at a common or latest period, with optional change over N years |
ilostat_list_reference | Decode ILOSTAT's code vocabulary: areas, area groups, databases, subjects, sexes, breakdowns, sources, status flags, notes |
ilostat_dataframe_describe | Describe a staged df_<id> dataframe by name, or list them all (off under HTTP with auth none), with provenance, coverage, basis counts, and column schema |
ilostat_dataframe_query | Run one read-only SQL SELECT over staged dataframes, optionally storing the result as a new one |
ilostat_dataframe_drop | Drop a staged dataframe before its TTL |
ilostat_dataframe_drop is off unless ILOSTAT_DATAFRAME_DROP_ENABLED=true, and CANVAS_PROVIDER_TYPE=none turns off all three dataframe tools.
Capability reference
ilostat_search_indicators tool
- Plain-language
query(every term must match a word or word prefix; case, accents, and labour/labor folded) plusfrequency,database,subject,breakdown,measure, andaggregates_onlyfilters; omitqueryto browse by filters - Up to 50 hits per page (default 10; a larger
limitis applied as 50, with a notice), continued withnext_cursor; each hit is one indicator with adataset_idper frequency, coverage years,n_ref_area,last_update, andhas_aggregates, andfacetscount the whole match set - An unrecognized
database,subject,breakdown, ormeasurecode fails asunknown_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 returnsfound: falsewithguidance - Definition,
unitand itsmultiplier, frequency variants,breakdowns(sex codes, plusclassif1/classif2codes with totals markedis_total),default_slice, coveredref_areas,basis_rule, and the first 20related_datasetswithrelated_total - When the SDMX API can't answer,
structure_statusisunavailableand 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 asremaining_dataset_idsfor another call. Filter byref_areas(up to 300) and/or anarea_group,sex,classif1,classif2,sources, and period: one exacttime, atime_from/time_toyear window,latest_only, or both latest_onlykeeps 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, andapplied_filters.latest_modeand the notice say sosource_selectionisbestby default,all, orsecondary; rows carrysource,obs_status,notes, andbasis, decoded inlegend, andapplied_filtersechoes every parameter sent- A result past the inline preview stages in full as a
df_<id>dataframe; a request overILOSTAT_MAX_ROWSfails asrequest_too_broad(unfiltered) orresult_too_large(filtered)
ilostat_get_country_profile tool
- One
ref_areawith annual data, an ISO3 country (KEN) or an X-coded aggregate (X01World, regions, income groups), andsex(SEX_Tby default,SEX_M, orSEX_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
reportedvalue for that slice and, separately, its latest non-projectedmodelledestimate - A missing reported value stays missing and is listed in
reported_missing; aggregates carry modelled values only
ilostat_compare_geographies tool
- One
dataset_idand one slice (sex,classif1,classif2, defaulting to the dataset's totals); areas fromref_areas(up to 300), anarea_group, or both, and at least one is required periodputs every area at one period; without it, each area gets its latest value withinlookback_years(default 10), projections excluded unlessinclude_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, andbreak_in_seriesflags; the notice counts the changes that are not like-for-like - Rows carry
rank,value,period,basis, andsource; areas without a value land inmissingwith areason, andcomparabilityreportsmixed_periods,basis_counts, anddistinct_sources
ilostat_list_reference tool
topic:ref_areas,area_groups,databases,subjects,sexes,classifications,classification_types,sources,obs_status,notes, orfrequencies- A text
filteror up to 100 exactcodes(misses innot_found; exactarea_groupslookups list member countries);ref_areanarrowssources, andclassification_typenarrowsclassifications - Up to 500 entries per page (default 50; a larger
limitis applied as 500, with a notice), continued withnext_cursor
ilostat_dataframe_describe tool
- Pass one
df_XXXXX_XXXXX, or omitnameto list every staged dataframe, newest first (listing is off under HTTP with authnone; use the exact name) - Each entry carries the producing
source_tooland itsquery_params,datasets,coverage,basis_counts,attribution,created_at/expires_at,row_count, and thecolumn_schemato write SQL against - Each dataset keeps its full unit beside
unit:unit_measure,unit_type,unit_type_label,unit_multiplier, andunit_multiplier_label; values are never rescaled
ilostat_dataframe_query tool
- One DuckDB
SELECT(joins, aggregates, window functions, CTEs) overdf_<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_limitcaps the rows materialized (default 1,000; capped at 10,000 per call, withrow_count_cappedflagging a hit) andpreviewthe rows returned inline, which also stop atILOSTAT_PREVIEW_CHARS;applied_limitsechoes the values applied, and BIGINT values arrive as stringsregister_asstores the full result as a new dataframe with a fresh TTL; a result over 1,000,000 rows fails asregister_as_too_large, andevictednames any older dataframes dropped to make room
ilostat_dataframe_drop tool
- Drops one staged dataframe by
namebefore its TTL; idempotent, withdropped: falsewhen nothing matched - Listed only when
ILOSTAT_DATAFRAME_DROP_ENABLED=trueand 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_busycarryingretryAfter. Data responses of up to 5,000 rows are cached forILOSTAT_CACHE_TTL_SECONDS - Dataframes on by default: a query or comparison larger than
ILOSTAT_PREVIEW_CHARSstages in full as a DuckDBdf_<id>table that lives forILOSTAT_DATASET_TTL_SECONDS, up to 1,000,000 rows in 100 dataframes per tenant (older ones are evicted and named inevicted). WithCANVAS_PROVIDER_TYPE=none, or in the.mcpbbundle (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
attributionto keep with the numbers
Agent-friendly output:
- Basis on every value: each row is
reported,modelled_estimate, orprojection, and results carrybasis_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 alegendthat decodes them, and query and comparison responses echoapplied_filters, defaults included - Misses as data: an unknown dataset returns
found: falsewithguidance, and compared areas without a value land inmissingwith a typedreason - 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:
{
"mcpServers": {
"ilostat-mcp-server": {
"type": "streamable-http",
"url": "https://ilostat.caseyjhand.com/mcp"
}
}
}
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.
{
"mcpServers": {
"ilostat-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/ilostat-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"ilostat-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/ilostat-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"ilostat-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/ilostat-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 or account: both ILO APIs are open.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/ilostat-mcp-server.git
- Navigate into the directory:
cd ilostat-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# every variable is optional; see Configuration below
Configuration
| Variable | Description | Default |
|---|---|---|
ILOSTAT_CATALOG_REFRESH_HOURS | Hours between catalog refresh checks (1–168). | 6 |
ILOSTAT_MAX_ROWS | Ceiling on the rows one query may return or stage (1,000–1,000,000). | 500000 |
ILOSTAT_PREVIEW_CHARS | Inline preview budget in serialized characters (at least 1,000); larger results stage as a dataframe. | 40000 |
ILOSTAT_CACHE_TTL_SECONDS | Seconds an upstream data response of up to 5,000 rows stays cached; 0 disables the cache. | 900 |
ILOSTAT_DATASET_TTL_SECONDS | Per-table TTL for staged dataframes, in seconds (at least 60). | 86400 |
ILOSTAT_DATAFRAME_DROP_ENABLED | Set true to expose ilostat_dataframe_drop. | false |
CANVAS_PROVIDER_TYPE | DataCanvas engine for staged dataframes: duckdb, or none to turn dataframes and the three dataframe tools off. | duckdb |
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. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
LOGS_DIR | Directory for log files (Node.js only). | <app-root>/logs |
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
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point. Turns dataframes on by default, registers the tools, starts the catalog load. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts) and the input and rendering helpers they share. Nine tools. |
src/services/rplumber, src/services/sdmx | Clients for the ILOSTAT data API and the ILO SDMX API, on the paced, retried HTTP layer in src/services/upstream. |
src/services/catalog | In-memory catalog snapshot, indicator search, reference listings, paging. |
src/services/structure | Per-indicator SDMX structure and unit, cached. |
src/services/observations | Request validation, size preflight, row decoding, comparisons, response cache. |
src/services/profile | Headline country profile. |
src/services/basis | Rules classing each value as reported, modelled estimate, or projection. |
src/services/canvas-bridge | DataCanvas adapter: df_<id> staging, provenance, SQL, drop. |
tests/ | Vitest suite mirroring src/, with recorded upstream fixtures. |
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 in
buildToolDefinitions()insrc/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:
bun run devcheck
bun run test
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.
Frequently asked questions
What is io.github.cyanheads/ilostat-mcp-server?
Search ILOSTAT labour indicators, query and compare series, build country profiles, run SQL.
How do I install io.github.cyanheads/ilostat-mcp-server?
It runs as a remote server. In Claude Code: claude mcp add --transport http ilostat-mcp-server 'https://ilostat.caseyjhand.com/mcp'. For other clients, paste the JSON configuration from this page into their MCP settings.
Does io.github.cyanheads/ilostat-mcp-server need an API key?
Its registry entry lists no required key or secret. Some features may still need an account with the service it connects to.
Is io.github.cyanheads/ilostat-mcp-server open source?
Yes. Its code is on GitHub under the Apache-2.0 license.
Who maintains io.github.cyanheads/ilostat-mcp-server?
It is published by cyanheads. AI.tn lists it but does not maintain or endorse it.
From the official MCP Registry. Listing is not an endorsement: review a server's code and permissions before giving it access to your data.