ilostat-mcp-server

v0.1.1 pre-1.0

Search ILOSTAT labour indicators, query and compare series, build country profiles, run SQL via MCP. STDIO or Streamable HTTP.

ilostat.caseyjhand.com/mcp
claude mcp add --transport http ilostat-mcp-server https://ilostat.caseyjhand.com/mcp
codex mcp add ilostat-mcp-server --url https://ilostat.caseyjhand.com/mcp
{
  "mcpServers": {
    "ilostat-mcp-server": {
      "url": "https://ilostat.caseyjhand.com/mcp"
    }
  }
}
gemini mcp add --transport http ilostat-mcp-server https://ilostat.caseyjhand.com/mcp
{
  "mcpServers": {
    "ilostat-mcp-server": {
      "command": "bunx",
      "args": [
        "mcp-remote",
        "https://ilostat.caseyjhand.com/mcp"
      ]
    }
  }
}
{
  "mcpServers": {
    "ilostat-mcp-server": {
      "type": "http",
      "url": "https://ilostat.caseyjhand.com/mcp"
    }
  }
}
curl -X POST https://ilostat.caseyjhand.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

Tools

9

read 8

ilostat_search_indicators

Search ILOSTAT's catalog of labour-statistics indicators by plain-language terms and filters. Each hit is one indicator with its datasets — one per available frequency (annual, quarterly, monthly) — plus breakdowns, coverage years, number of reference areas, source database, and last update; pass a dataset ID to ilostat_describe_indicator for its units and breakdown codes, then to ilostat_query_indicator or ilostat_compare_geographies for values. Every search term must match a word or word prefix in the indicator's label, subject, database, breakdown names, code, or definition; British and American spellings (labour/labor) match alike. Facet counts reflect all applied filters.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ilostat_search_indicators",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "query": {
      "description": "Plain-language terms, e.g. \"youth unemployment\" or \"informal employment rate\". Case, accents, and punctuation are ignored, labor matches labour, and a trailing plural s is tolerated. Omit to browse by filters (results then order by indicator code).",
      "type": "string",
      "maxLength": 500
    },
    "frequency": {
      "description": "Keep only datasets of this frequency: A annual, Q quarterly, M monthly. Narrows each hit's datasets; an indicator with none left drops out.",
      "type": "string",
      "enum": [
        "A",
        "Q",
        "M"
      ]
    },
    "database": {
      "description": "Source database code, e.g. LFS or ILOEST (the ILO modelled estimates); case-insensitive. ilostat_list_reference topic databases lists them.",
      "type": "string",
      "maxLength": 32
    },
    "subject": {
      "description": "Subject code, e.g. LUU (unemployment and labour underutilization); case-insensitive. ilostat_list_reference topic subjects lists them.",
      "type": "string",
      "maxLength": 32
    },
    "breakdown": {
      "description": "Classification type the indicator is broken down by, e.g. AGE, ECO, GEO, SEX; case-insensitive. ilostat_list_reference topic classification_types lists them.",
      "type": "string",
      "maxLength": 32
    },
    "aggregates_only": {
      "default": false,
      "description": "Keep only datasets carrying World, regional, or income-group rows, dropping indicators with none.",
      "type": "boolean"
    },
    "limit": {
      "default": 10,
      "description": "Hits per page (1–50).",
      "type": "integer",
      "minimum": 1,
      "maximum": 50
    },
    "cursor": {
      "description": "Opaque continuation token: the previous page's next_cursor, passed unchanged.",
      "type": "string",
      "maxLength": 256
    }
  },
  "required": [
    "aggregates_only",
    "limit"
  ],
  "additionalProperties": false
}
view source ↗

ilostat_describe_indicator

open-world

Explain one ILOSTAT dataset before comparing numbers: its definition, unit and multiplier, frequency variants and coverage, the sex and breakdown codes in use across its frequencies (the total code of each breakdown marked), the reference areas it covers, whether it carries World/regional/income-group aggregates, and how its observations are classed as reported, modelled, or projected. Accepts a dataset ID (UNE_DEAP_SEX_AGE_RT_A) or a bare indicator code (UNE_DEAP_SEX_AGE_RT), which describes every frequency. An unknown code returns found: false with guidance.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ilostat_describe_indicator",
    "arguments": {
      "dataset_id": "<dataset_id>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "dataset_id": {
      "description": "One dataset ID (indicator code plus _A, _Q, or _M, e.g. UNE_DEAP_SEX_AGE_RT_A) or a bare indicator code (UNE_DEAP_SEX_AGE_RT), as ilostat_search_indicators returns them. Case-insensitive; a DF_ prefix (the SDMX dataflow form) is stripped.",
      "type": "string",
      "maxLength": 200,
      "pattern": "^[A-Z0-9_]+( *[+,] *[A-Z0-9_]+)*$"
    }
  },
  "required": [
    "dataset_id"
  ],
  "additionalProperties": false
}
view source ↗

ilostat_query_indicator

open-world

Fetch observations for up to 3 ILOSTAT datasets, filtered by reference area or area group, sex, breakdown codes (classif1, classif2), source, and period. Rows keep their source, observation status, decoded notes, and a basis — reported, modelled_estimate, or projection — and the response echoes every filter applied, including the best-source default. Codes are checked against ILOSTAT's dictionaries before the request is sent: ilostat_list_reference lists valid codes and ilostat_describe_indicator lists the codes a dataset actually uses. A result larger than the inline preview is staged in full as a df_<id> dataframe for SQL through ilostat_dataframe_describe and ilostat_dataframe_query when this deployment enables dataframes. A request with no filters at all is refused when the dataset exceeds the row ceiling, and a filtered request that still exceeds it is refused with guidance to narrow it.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ilostat_query_indicator",
    "arguments": {
      "dataset_ids": "<dataset_ids>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "dataset_ids": {
      "description": "One to three dataset IDs — an indicator code plus _A, _Q, or _M (UNE_DEAP_SEX_AGE_RT_A), as ilostat_search_indicators returns them. Case-insensitive; a DF_ prefix (the SDMX dataflow form) is stripped, a bare indicator code resolves when it has one frequency, and an element holding + or , joined IDs is split.",
      "minItems": 1,
      "maxItems": 3,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 64,
        "pattern": "^[A-Z0-9_]+$"
      }
    },
    "ref_areas": {
      "description": "Reference areas (up to 300): ISO3 country codes (USA) or X-coded aggregates (X01 World); case-insensitive, ILO_GEO_ forms accepted. Aggregates need a dataset with has_aggregates true. Omit for every area.",
      "maxItems": 300,
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^[A-Z0-9]{3}$"
      }
    },
    "area_group": {
      "description": "X01 for every country, an ILO region or subregion, or a World Bank income group (X06, X56, X02, …); expands to its member countries, unioned with ref_areas. ilostat_list_reference topic area_groups lists the codes.",
      "type": "string",
      "pattern": "^[A-Z0-9]{3}$"
    },
    "sex": {
      "description": "Sex codes SEX_T, SEX_M, SEX_F, SEX_O; T/M/F/O and total/both/male/female/other are accepted.",
      "maxItems": 4,
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "SEX_T",
          "SEX_M",
          "SEX_F",
          "SEX_O"
        ]
      }
    },
    "classif1": {
      "description": "Codes of the first breakdown (e.g. AGE_YTHADULT_YGE15); case-insensitive. ilostat_describe_indicator lists the codes a dataset uses.",
      "maxItems": 100,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 64
      }
    },
    "classif2": {
      "description": "Codes of the second breakdown, for datasets that have one; case-insensitive. ilostat_describe_indicator lists them.",
      "maxItems": 100,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 64
      }
    },
    "sources": {
      "description": "Source codes (e.g. BA:453); ilostat_list_reference topic sources with ref_area lists an area's sources. Without source_selection, setting sources switches it to all, since a secondary source matches nothing under best.",
      "maxItems": 100,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 64
      }
    },
    "time": {
      "description": "One exact period: YYYY (on a quarterly or monthly dataset, every period of that year), YYYYQn, or YYYYMmm; 2024-Q2, 2024 Q2, and 2025-03 are normalized. Not combinable with time_from, time_to, or latest_only.",
      "type": "string",
      "pattern": "^\\d{4}(Q[1-4]|M(0[1-9]|1[0-2]))?$"
    },
    "time_from": {
      "description": "First year (YYYY); upstream filters by year only.",
      "type": "string",
      "pattern": "^\\d{4}$"
    },
    "time_to": {
      "description": "Last year (YYYY), not before time_from.",
      "type": "string",
      "pattern": "^\\d{4}$"
    },
    "latest_only": {
      "default": false,
      "description": "Only the latest period per reference area and dataset (the latest quarter or month on sub-annual datasets); combines with time_from/time_to.",
      "type": "boolean"
    },
    "source_selection": {
      "description": "best (default): the preferred source per area and period; all: secondary sources too, each row flagged best_source; secondary: secondary sources only. Defaults to all when sources is set.",
      "type": "string",
      "enum": [
        "best",
        "all",
        "secondary"
      ]
    }
  },
  "required": [
    "dataset_ids",
    "latest_only"
  ],
  "additionalProperties": false
}
view source ↗

ilostat_get_country_profile

open-world

Build a headline labour-market profile for one reference area: labour force participation, employment-to-population ratio, unemployment and youth unemployment rates, youth NEET rate, informal employment rate, employment level, labour income share, and working poverty rate. Each indicator shows its latest reported value (from a national or institutional source, with its source and notes) and, separately, its latest ILO modelled estimate that is not a projection, each with its dataset, period, and status — a missing reported value stays missing and is never filled from the model. Accepts a country (ISO3, e.g. KEN) or an X-coded aggregate (X01 World, regions, income groups), for which only modelled values exist.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ilostat_get_country_profile",
    "arguments": {
      "ref_area": "<ref_area>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "ref_area": {
      "description": "One reference area with annual data: an ISO3 country code (KEN) or an X-coded aggregate (X01 World, X06, X02); ILO_GEO_ forms accepted, case-insensitive. ilostat_list_reference topic ref_areas lists them.",
      "type": "string",
      "pattern": "^[A-Z0-9]{3}$"
    },
    "sex": {
      "description": "SEX_T (default), SEX_M, or SEX_F; T/M/F and total/both/male/female are accepted. Indicators without a sex breakdown (labour income share) are unaffected.",
      "default": "SEX_T",
      "type": "string",
      "enum": [
        "SEX_T",
        "SEX_M",
        "SEX_F"
      ]
    }
  },
  "required": [
    "ref_area",
    "sex"
  ],
  "additionalProperties": false
}
view source ↗

ilostat_compare_geographies

open-world

Compare reference areas on one ILOSTAT dataset and one slice — a sex code plus breakdown codes, defaulting to the dataset's totals — giving each area's value at a common period or at its latest non-projected period, optional change over N years, and a rank, with each value's period, source, status, and basis (reported, modelled_estimate, or projection). Areas without a value are listed separately with the reason, and the response flags mixed periods and mixed bases rather than hiding them. Select areas by code list, by group (X01 for every country, an ILO region or subregion, or a World Bank income group), or both; X-coded aggregates require a dataset with aggregates.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ilostat_compare_geographies",
    "arguments": {
      "dataset_id": "<dataset_id>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "dataset_id": {
      "description": "One dataset ID (UNE_DEAP_SEX_AGE_RT_A), as ilostat_search_indicators returns it; case-insensitive, a DF_ prefix (the SDMX dataflow form) stripped, a bare indicator code resolved when it has one frequency.",
      "type": "string",
      "maxLength": 64,
      "pattern": "^[A-Z0-9_]+$"
    },
    "ref_areas": {
      "description": "Reference areas (up to 300): ISO3 codes (USA) or X-coded aggregates (X01 World); case-insensitive, ILO_GEO_ forms accepted. At least one of ref_areas or area_group is required.",
      "maxItems": 300,
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^[A-Z0-9]{3}$"
      }
    },
    "area_group": {
      "description": "X01 for every country, an ILO region or subregion, or a World Bank income group (X06, X56, X02, …); expands to its member countries. The group's own aggregate is compared only when listed in ref_areas. ilostat_list_reference topic area_groups lists the codes.",
      "type": "string",
      "pattern": "^[A-Z0-9]{3}$"
    },
    "sex": {
      "description": "Sex code SEX_T, SEX_M, SEX_F, or SEX_O; T/M/F/O and total/both/male/female/other are accepted. Defaults to SEX_T on a dataset with a sex breakdown; refused on one without.",
      "type": "string",
      "enum": [
        "SEX_T",
        "SEX_M",
        "SEX_F",
        "SEX_O"
      ]
    },
    "classif1": {
      "description": "First breakdown code, case-insensitive. Defaults to the dataset's total code; required when the breakdown has no total (deciles); refused on a dataset without the breakdown. ilostat_describe_indicator lists the dataset's codes and marks its totals.",
      "type": "string",
      "maxLength": 64
    },
    "classif2": {
      "description": "Second breakdown code; same defaults and rules as classif1.",
      "type": "string",
      "maxLength": 64
    },
    "period": {
      "description": "A common period, YYYY, YYYYQn, or YYYYMmm matching the dataset frequency (2024-Q2 and 2025-03 are normalized). Omit to compare each area at its latest period.",
      "type": "string",
      "pattern": "^\\d{4}(Q[1-4]|M(0[1-9]|1[0-2]))?$"
    },
    "lookback_years": {
      "default": 10,
      "description": "Latest mode: an area's latest value must fall within this many years of the current year.",
      "type": "integer",
      "minimum": 1,
      "maximum": 50
    },
    "change_years": {
      "description": "Adds each value's change from the same sub-period this many years earlier, in the dataset unit.",
      "type": "integer",
      "minimum": 1,
      "maximum": 30
    },
    "include_projections": {
      "default": false,
      "description": "Latest mode: let projections (ILO modelled values after the cutoff) be an area's latest value.",
      "type": "boolean"
    },
    "sort": {
      "description": "Row order: value_desc (default), value_asc, or ref_area. rank is always by value, highest first.",
      "default": "value_desc",
      "type": "string",
      "enum": [
        "value_desc",
        "value_asc",
        "ref_area"
      ]
    }
  },
  "required": [
    "dataset_id",
    "lookback_years",
    "include_projections",
    "sort"
  ],
  "additionalProperties": false
}
view source ↗

ilostat_list_reference

Decode ILOSTAT's code vocabulary: reference areas (ISO3 countries and X-coded aggregates, with World Bank income group and ILO region), the groups that area_group accepts (X01 for every country, ILO regions and subregions, World Bank income groups; exact-code lookups list their member countries), databases, subjects, sex codes, breakdown codes for classif1/classif2 with their classification types, per-country data sources, observation status flags, note codes, and frequencies. Filter by text or look up exact codes; long topics page with a cursor. These are ILOSTAT-wide vocabularies; ilostat_describe_indicator lists the sex and breakdown codes one dataset actually uses.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ilostat_list_reference",
    "arguments": {
      "topic": "<topic>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "topic": {
      "type": "string",
      "enum": [
        "ref_areas",
        "area_groups",
        "databases",
        "subjects",
        "sexes",
        "classifications",
        "classification_types",
        "sources",
        "obs_status",
        "notes",
        "frequencies"
      ],
      "description": "Vocabulary to list: ref_areas, area_groups (the X codes area_group accepts), databases, subjects, sexes, classifications (classif1/classif2 codes), classification_types, sources, obs_status, notes, or frequencies."
    },
    "filter": {
      "description": "Text filter: every word must match a word or word prefix of the code or label (case, accents, and punctuation ignored; labor matches labour). Omit to list the whole topic.",
      "type": "string",
      "maxLength": 200
    },
    "codes": {
      "description": "Exact codes to look up (up to 100; case-insensitive, and ILO_GEO_ forms accepted for ref_areas and area_groups). Codes the topic lacks are listed in not_found. With topic area_groups, each group found also lists its member countries.",
      "maxItems": 100,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 64
      }
    },
    "ref_area": {
      "description": "Topic sources only: list the sources of this reference area (ISO3 or X code; case-insensitive, ILO_GEO_ forms accepted).",
      "type": "string",
      "pattern": "^[A-Z0-9]{3}$"
    },
    "classification_type": {
      "description": "Topic classifications only: keep codes of this classification type, the code prefix (AGE, ECO, EDU, …).",
      "type": "string",
      "maxLength": 32
    },
    "limit": {
      "default": 50,
      "description": "Entries per page (1–500).",
      "type": "integer",
      "minimum": 1,
      "maximum": 500
    },
    "cursor": {
      "description": "Opaque continuation token: the previous page's next_cursor, passed unchanged.",
      "type": "string",
      "maxLength": 256
    }
  },
  "required": [
    "topic",
    "limit"
  ],
  "additionalProperties": false
}
view source ↗

ilostat_dataframe_query

Run a single-statement SELECT against the df_<id> dataframes staged by ilostat_query_indicator and ilostat_compare_geographies or stored by an earlier register_as. Inspect a dataframe with ilostat_dataframe_describe first; its column schema is what the SQL has to match. Read-only: writes, DDL, DROP, COPY, PRAGMA, ATTACH, and file-reading table functions are rejected, and system catalogs (information_schema, pg_catalog, sqlite_master, duckdb_*) are denied. Breakdown versions overlap (AGE_YTHADULT_*, AGE_AGGREGATE_*, AGE_10YRBANDS_*), so filter to one version before summing. Optional register_as stores the result as a new dataframe with a fresh TTL.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ilostat_dataframe_query",
    "arguments": {
      "sql": "<sql>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "sql": {
      "type": "string",
      "minLength": 1,
      "maxLength": 20000,
      "description": "One SELECT over df_<id> tables (DuckDB SQL: joins, aggregates, window functions, CTEs), at most 20,000 characters. BIGINT results such as COUNT or SUM of integers serialize as strings; CAST to DOUBLE for inline arithmetic."
    },
    "register_as": {
      "description": "Store the result as a new dataframe under this name (df_XXXXX_XXXXX: letters and digits, five in each part; stored uppercased after df_) with a fresh TTL, to chain analyses. A result over 1,000,000 rows is refused, and storing one can evict the oldest dataframes, which evicted names.",
      "type": "string",
      "pattern": "^df_[A-Z0-9]{5}_[A-Z0-9]{5}$"
    },
    "preview": {
      "description": "Rows returned inline; defaults to row_limit. Set lower when register_as keeps the full result.",
      "type": "integer",
      "minimum": 0,
      "maximum": 10000
    },
    "row_limit": {
      "default": 1000,
      "description": "Hard cap on rows materialized (1–10,000). A query matching more stops at the cap and row_count_capped is true; register_as keeps the full result.",
      "type": "integer",
      "minimum": 1,
      "maximum": 10000
    }
  },
  "required": [
    "sql",
    "row_limit"
  ],
  "additionalProperties": false
}
view source ↗

ilostat_dataframe_describe

Describe the df_<id> dataframes staged by ilostat_query_indicator and ilostat_compare_geographies or stored by ilostat_dataframe_query register_as: the tool and parameters that produced each, the datasets it holds (label, unit, last update), coverage, basis counts, attribution, creation and expiry times, row count, and column schema. Pass name for one dataframe. Without it, every staged dataframe is listed, except on a deployment whose callers share one canvas, where listing is off and only the exact name works. Read the schema here before writing SQL for ilostat_dataframe_query.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ilostat_dataframe_describe",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": {
      "description": "One dataframe name as the producing tool returned it (df_XXXXX_XXXXX: letters and digits, five in each part; case-insensitive, as in SQL). Omit to list every staged dataframe; a deployment whose callers share one canvas refuses the listing and needs the name.",
      "type": "string",
      "pattern": "^df_[A-Z0-9]{5}_[A-Z0-9]{5}$"
    }
  },
  "additionalProperties": false
}
view source ↗

disabled 1

ilostat_dataframe_drop

Drop a staged df_<id> dataframe by name before its TTL expires: once an analysis with it is finished, to free the table, or to reuse its name as an ilostat_dataframe_query register_as target. Idempotent: dropped is false when nothing matched.

disabledwould be destructive

Disabled. Dropping dataframes is turned off in this deployment; staged tables expire on their own TTL.

ILOSTAT_DATAFRAME_DROP_ENABLED=true
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": {
      "description": "Dataframe name to drop (df_XXXXX_XXXXX: letters and digits, five in each part; case-insensitive, as in SQL), as the producing tool returned it.",
      "type": "string",
      "pattern": "^df_[A-Z0-9]{5}_[A-Z0-9]{5}$"
    }
  },
  "required": [
    "name"
  ],
  "additionalProperties": false
}
view source ↗