unicefstats-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@unicefstats-mcpWhat is the under-5 mortality rate in Kenya?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
unicefstats-mcp
Experimental — not an official UNICEF product. Verify retrieved values against the UNICEF Data Warehouse before citing in publications. See Limitations.
MCP server for UNICEF child development statistics. Query 790+ child-focused indicators across 200+ countries with disaggregations by sex, age, wealth quintile, and residence. No API key required.
Indicators cover child mortality, nutrition, education, child protection, WASH (water/sanitation/hygiene), HIV/AIDS, immunization, early childhood development, and more. Many align with SDG targets, but the dataset is broader than SDGs alone.
Data source: UNICEF SDMX API
Identity
Property | Value |
MCP identity |
|
PyPI package | |
Canonical source | |
Data source | |
Maintainer | Joao Pedro Azevedo ( |
Status | Experimental — not endorsed by UNICEF |
Third-party aggregator listings (LobeHub, Smithery, mcp.so, Glama) are not controlled by the maintainer. Verify against the canonical source above.
Contents
Key documents
Document | Description |
Data origin, ownership, distribution pipeline, verification steps | |
Version history (v0.1.0–v0.4.0) with sources cited | |
Release process checklist and version management | |
Development setup, code style, PR guidelines | |
Contributor Covenant v2.1 | |
Full 300-query benchmark analysis with EQA decomposition | |
Literature review: MCP servers for official statistics — ecosystem, patterns, evaluation, 15 papers | |
20 official statistics MCP servers compared — timeline, feature matrix, strengths/weaknesses | |
Annotated bibliography — 15 papers on tool-augmented hallucination | |
Wilcoxon, bootstrap CI, McNemar tests on benchmark results | |
Comprehensive directory of all official statistics MCP servers | |
Cross-model smoke test (Anthropic / OpenAI / Google / OpenRouter) for the v0.7.3 cross-provider generalisation question — design, rubric, ~$1 default run, path to full mini-EQA |
Related MCP server: @nor-data/statfin-mcp
How it relates to the unicefdata packages
unicefstats-mcp is not a replacement for the unicefdata packages in Python, R, or Stata. They serve different audiences:
unicefstats-mcp | unicefdata (Python/R/Stata) | |
Audience | AI assistants (Claude, Cursor, Copilot) | Data scientists, researchers, analysts |
Interface | MCP protocol (tool calls via JSON) | Native language API ( |
Use case | Conversational data exploration, quick lookups, AI-assisted analysis | Reproducible research, ETL pipelines, statistical analysis |
Output | JSON (compact or full) optimized for LLM context | DataFrames, tibbles, Stata matrices |
Scripting | No — single queries via AI chat | Yes — full programmatic control, loops, joins, transforms |
Caching | Delegates to unicefdata | Built-in SDMX response caching |
Bulk download | Limited (max 500 rows per call) | Unlimited — designed for full dataset pulls |
Under the hood, unicefstats-mcp wraps the unicefdata Python package. Every tool call ultimately calls unicefdata.unicefData() or its metadata functions. Think of the MCP as a thin AI-friendly interface on top of the same data layer.
When to use which:
Use unicefstats-mcp when you're chatting with an AI and want to quickly explore indicators, check values, or compare countries
Use unicefdata (Python/R/Stata) when you're writing scripts, building dashboards, running regressions, or doing any reproducible analytical work
How it compares to other data MCPs
Feature | unicefstats-mcp | FRED MCP | World Bank MCP |
Tools | 9 (search → metadata → data → code → identity + strict canonical lookup) | 3 (browse → search → get) | 1 (get only) |
Indicators | 790+ child-focused indicators | 800,000+ economic series | ~1,600 indicators |
Countries | 200+ (ISO3) | US-focused (some intl) | 200+ (ISO2) |
Disaggregations | Sex, age, wealth quintile, residence | Frequency, seasonal adjustment | None |
MCP Prompt |
| None | None |
Output modes | Compact (5 cols) / Full (all cols) | JSON | CSV |
Data summary | Value range, year range, country count | None | None |
Pagination metadata |
|
| None (hardcoded 20K) |
Input validation | ISO3, sex, wealth, residence validated | Zod schemas | None |
Error guidance |
| HTTP status text | Raw exception |
API key | Not required | FRED_API_KEY required | Not required |
Truncation handling |
| None | None |
Landscape: MCP servers for official statistics
This project is part of a growing ecosystem of MCP servers for international and official statistics. As of March 2026:
UN Agencies
Server | Data Source | Tools | SDMX | Published |
unicefstats-mcp (this repo) | UNICEF Data Warehouse | 7 | Yes | PyPI |
Any SDMX registry | 23 | Yes | No | |
UNICEF Data Warehouse | 3 | Yes | No | |
UNHCR refugee data | 5 | No | No | |
WHO GHO / FDA / PubMed | 18 | No | npm |
International Organizations
Server | Data Source | Tools | SDMX | Published |
FRED (800K+ series) | 3 | No | npm | |
World Bank Open Data | 1 | No | No | |
IMF (IFS, BOP, WEO) | 10 | Yes | PyPI | |
OECD (5,000+ datasets) | 9 | Yes | npm | |
Eurostat EU statistics | 7 | Yes | No |
National Statistics Offices
Server | Data Source | Tools | Published |
US Census Bureau (official) | 5 | No | |
40+ US Gov APIs | 300+ | npm | |
Brazil IBGE (227 tests) | 22 | npm | |
Ukraine SDMX v3 | 8 | npm | |
Italy ISTAT SDMX | 7 | No |
Known gaps
No MCP server exists for: FAO/FAOSTAT, UNESCO/UIS (4,000+ education indicators), ILO/ILOSTAT, UNSD SDG API, UN DESA Population, UNDP/HDI.
Full directory with install commands: MCP-DIRECTORY-STATS.md
Relationship to sdmx-mcp
UNICEF also maintains sdmx-mcp, a generic SDMX protocol MCP server. The two servers are complementary, not competing:
unicefstats-mcp (this repo) | ||
Scope | UNICEF child development data only | Any SDMX registry (UNICEF, Eurostat, OECD, ...) |
Tools | 7 (analyst-friendly, 4-step workflow) | 23 (SDMX power-user, structural queries) |
Data layer | Wraps | Direct SDMX REST API calls via |
Output | Formatted for LLMs (compact tables, summaries, tips) | Raw SDMX-JSON/CSV |
Accuracy (EQA) | 0.891 (v0.7.3 + fixes) | 0.074 |
Hallucination | hall_b 1.00% (mcp060) / 2.25% (mcp073) — below the no-tools hall_a baseline (2.50%) | 0% T1 / 0% T2 |
Cost per query | ~$0.04 | $0.087 |
Latency | ~10s avg | 60s avg |
Key tradeoff: unicefstats-mcp is dramatically more accurate (EQA 0.891 vs 0.074) because its formatted output is optimized for LLM parsing. sdmx-mcp achieves zero hallucination on absent-data queries through aggressive assistant_guidance fields and a validate_query_scope pattern; its accuracy floor is too low to be useful, but the refusal discipline is exemplary. unicefstats-mcp v0.7.3 + fixes is the first version where MCP demonstrably makes the model safer than the no-tools baseline on absent-data queries (hall_b < hall_a), achieved without sdmx-mcp's accuracy cost.
When to use which:
Use unicefstats-mcp for UNICEF child development analysis — it's simpler, faster, and far more accurate
Use sdmx-mcp when you need to query non-UNICEF SDMX registries, explore dataflow structures, or work with hierarchical codelists
Full 3-way benchmark (LLM alone vs unicefstats-mcp vs sdmx-mcp): examples/results/
Quick Start
pip install unicefstats-mcpClaude Code
Add to ~/.claude/.mcp.json:
{
"mcpServers": {
"unicefstats": {
"command": "unicefstats-mcp"
}
}
}Cursor / VS Code
Add to your MCP settings:
{
"unicefstats": {
"command": "unicefstats-mcp"
}
}What v1.1.0 adds
v1.1.0 is additive: v1.0.0's ambiguity_flag, candidates, and abstain_instruction still fire unchanged on ambiguous queries. v1.1.0 layers four new advisory envelope fields on top:
requires_confirmation— true when the server wants the assistant to pause for user inputrecommended— the server's preferred next action (code, country, year, or tool)assistant_guidance— short natural-language hint the LLM can paraphrasenext_step— a structured tool/parameter suggestion the LLM can chain into
Decision order (4 stages): strict canonical lookup → ambiguity check (v1.0.0 fields) → confirmation gate (requires_confirmation) → advisory hints (recommended / assistant_guidance / next_step).
Verdict: ALLOW (no behavioural regression; advisory fields fire on 23/30 Sonnet and 26/30 Haiku paired stuck queries; see internal/v1.1.0_design/ab_results.md).
Tools
Tool | Purpose | API call? |
| Find indicators by keyword | No |
| Browse thematic groups (CME, NUTRITION, EDUCATION, ...) | No |
| List countries with ISO3 codes | No |
| Full metadata, SDMX details, available disaggregations | No |
| v1.0.0 Strict canonical-code lookup (rejects natural-language input with abstain instruction) | No |
| Available year range and country count | Yes (lightweight) |
| Fetch observations with optional disaggregation filters | Yes |
| unicefdata package API reference (Python/R/Stata) | No |
| Server identity, version, provenance, data source | No |
Workflow
1. search_indicators("child mortality") → find indicator codes
2. get_indicator_info("CME_MRY0T4") → check disaggregations & SDMX details
3. get_temporal_coverage("CME_MRY0T4") → check year range
4. get_data("CME_MRY0T4", ["BRA", "IND"]) → fetch data
5. get_api_reference("python", "unicefData") → get code template to continue in a scriptResources
The server exposes six MCP resources clients can load for guidance and reference data:
URI | Purpose |
| Recommended system prompt — operating loop + temporal-frontier check + anti-extrapolation directive (load at session start) |
| Full DO/DON'T rules, common mistakes, and anti-fabrication guidance |
| Runtime context — |
| All indicator categories with counts |
| ISO3 codes and country names |
| Disaggregation codes and indicator-prefix legend |
The system-prompt and context resources address the T2 hallucination failure mode (model fabricating values for years beyond the data frontier). Pattern adopted from the World Bank data360-mcp server. See CHANGELOG entry for v0.5.0.
Scope: UNICEF DW indicators only
unicefstats-mcp searches and serves the UNICEF Data Warehouse SDMX catalog — approximately 790 child-focused indicators across mortality, nutrition, education, child protection, WASH, HIV/AIDS, immunization, and early childhood development. Indicator codes follow UNICEF conventions (e.g. CME_MRY0T4, NT_ANT_HAZ_NE2, ED_ANAR_L1).
Codes from other organisations are out of scope. The MCP cannot find, resolve, or fetch them because they do not exist in the UNICEF SDMX catalog. Examples of out-of-scope code families:
Source organisation | Prefix examples | Typical content |
World Bank WDI |
| Poverty, GDP, broader education |
World Bank ASPIRE |
| Social protection coverage |
ILO / ILOSTAT |
| Labour, employment, NEET |
UNESCO UIS |
| Education finance, learning outcomes |
These codes will return no hits from search_indicators and a not-found error from get_data. The refusal is correct behaviour, not a bug.
For cross-organisation queries, use a sister MCP:
data360-mcp— World Bank's multi-source aggregator covering WDI, ILO, UN, and other official providers under one tool surface.worldbank-mcp— World Bank Open Data only.
For SDMX power-user queries against arbitrary registries (Eurostat, OECD, national NSOs), see sdmx-mcp — the generic SDMX protocol server discussed under Relationship to sdmx-mcp.
The scope boundary was sharpened in v1.1.1 after the v9 edge-test sample surfaced three prompts whose ground-truth codes (SI.POV.DDAY, NY.GDP.PCAP.PP.KD, per_allsp.cov_pop_tot) sit in the World Bank universe rather than the UNICEF Data Warehouse. Full write-up: internal/v1.1.0_design/ambiguity_forensic.md sections C-4 to C-6 (dev repo only).
Understanding UNICEF indicator codes
Why this matters — the MCP's semantic layer
UNICEF SDMX indicator codes look cryptic on first contact — PT_F_20-24_MRD_U18,
NT_ANT_HAZ_NE2, TRGT_2030_IM_DTP3 — and most of an LLM's failure modes on this
data start with picking the wrong code. The single biggest piece of added value
this MCP offers over a raw SDMX endpoint is exposing the semantic structure
inside those codes so the assistant can disambiguate without guessing. The
implementation lives in src/unicefstats_mcp/differentiator.py (segment-level
suffix meanings and base/variant explanation) and
src/unicefstats_mcp/indicator_resolver.py (natural-language synonyms and known
ambiguous tokens). v1.1.1 added a query-aware CURATED_PREFERRED dimension_hint
that tells the resolver which code from a family to surface for a given phrasing
(e.g. "target" → TRGT_*, "modelled" → *_MOD, "child" → _T total rather
than a sex-disaggregated variant).
This section documents the conventions the MCP relies on so users and downstream agents can read codes directly, and so reviewers can audit the resolver's choices.
Anatomy of a UNICEF code
UNICEF DW codes are concatenations of dot-free, underscore-delimited segments, read left-to-right from broadest to most specific. Take a layered protection indicator from the catalog:
PT_F_20-24_MRD_U18
│ │ │ │ │
│ │ │ │ └── Marriage cutoff: U18 = first union before age 18 (SDG 5.3.1)
│ │ │ └────── Indicator class: MRD = ever-married / in-union
│ │ └──────────── Age band (cohort): 20-24 = women 20-24 (retrospective denominator)
│ └────────────── Population restriction: F = female-only indicator (women/girls)
└───────────────── Family prefix: PT = Child ProtectionRead out loud: "Child Protection / female respondents / cohort aged 20-24 /
ever-married / before age 18" — i.e. the share of women aged 20-24 who were first
married before 18. Note that the F here is not a sex-disaggregation suffix
appended to a sex-neutral parent; it is a population-restriction marker built
into a family of indicators that only exist for female respondents (FGM, child
marriage, anaemia in women, antenatal care). See "Population restrictions vs sex
disaggregation" below.
The same left-to-right reading applies to the nutrition anthropometric pattern:
NT_ANT_HAZ_NE2
│ │ │ │
│ │ │ └── Threshold: NE2 = below -2 SD (NE = "negative end", 2 = SD count)
│ │ └────── Metric: HAZ = Height-for-Age Z-score
│ └────────── Sub-family: ANT = Anthropometry
└───────────── Family: NT = NutritionNT_ANT_HAZ_NE2 is the stunting prevalence indicator for children under 5. To
get the sex-stratified value, query the indicator with the SEX dimension
filter (get_data(indicator='NT_ANT_HAZ_NE2', sex='F')), not by appending _F
to the code — see the next subsection.
And the derived-metric pattern, which the v1.1.1 query-aware scoring is specifically tuned for:
TRGT_2030_IM_DTP3
│ │ │ │
│ │ │ └──── Antigen / dose: DTP3 = third dose of DTP
│ │ └─────── Family: IM = Immunization
│ └──────────── Target year: 2030 = SDG horizon
└───────────────── Derived metric: TRGT = country-set target valueThe combinatorial reading — family → sub-family → metric → threshold/age band → disaggregation token — is the unwritten contract behind almost every code in the catalog. The tables below enumerate the parts.
Topic prefixes (families)
The first segment is the topical family. The top 10–12 prefixes in the catalog cover the vast majority of child-focused indicators:
Prefix | Meaning | Example code |
| Child Mortality Estimates — mortality rates by age bracket (neonatal, infant, under-5, childhood, stillbirth) |
|
| Nutrition — anthropometry, anaemia, birthweight, micronutrients |
|
| Education — completion rates, literacy, attendance by ISCED level |
|
| WASH — water, sanitation, hygiene; population-level access |
|
| Immunization — vaccine coverage by antigen / dose |
|
| Maternal, Newborn & Child Health — antenatal care, skilled birth attendance, early childbearing |
|
| Early Childhood Development — learning materials, parental stimulation, attendance |
|
| Child Protection — FGM, child marriage, violent discipline |
|
| HIV/AIDS — ART coverage, prevalence, adolescent indicators |
|
| Child poverty — monetary and multidimensional |
|
| Causes of death — disease- and condition-specific mortality / morbidity |
|
| Demography — household composition, population structure |
|
| Migration — child migrants, displacement |
|
| Gender / adolescent girls (often cross-cuts NT) |
|
| Country-set targets (see "Derived metrics" below) |
|
| Hazard exposure (climate, conflict, disaster) |
|
The full mapping of natural-language phrases to these prefixes lives in
indicator_resolver._SYNONYMS — e.g. "stunting" → NT_ANT_HAZ_NE2, "child
marriage" → PT_F_20-24_MRD_U18, "BCG" → IM_BCG.
Methodology and provenance suffixes
A trailing segment often signals how the value was produced, not what was measured. These suffixes are central to the v1.1.1 query-aware scoring because users almost always want either "survey" or "modelled" but rarely both.
Suffix | Meaning | Example code |
| Modelled estimate (joint-estimation group output, e.g. UIS for education, IGME for mortality) |
|
| Merged across multiple source surveys / years into a single comparable series |
|
| Proxy indicator — a related variable used in lieu of the conceptually exact one |
|
| New / revised methodology series (often runs alongside a legacy series for one cycle) |
|
| Numerator / threshold variant — alternate cut used by some agencies |
|
| Aggregate (regional or income-group rollup rather than country observation) |
|
| UNESCO Institute for Statistics source / methodology |
|
The MCP's differentiator.py:_SUFFIX_MEANINGS table is the canonical source.
When two codes differ only in this trailing segment, explain_difference()
reports them as base vs. variant (e.g. ECD_CHLD_LMPSL as base, _MERGE
and _PRXY as derivation variants).
Population restrictions vs sex disaggregation
This distinction is easy to miss and routinely trips up downstream agents.
Population restriction (built into the code). Some UNICEF indicators only
exist for one sex because the underlying measurement only applies to one sex:
FGM prevalence, child-marriage cohorts, anaemia in women of reproductive age,
antenatal care coverage. In those families, an F segment near the start of
the code (PT_F_*, NT_ANE_WOM_*, MNCH_BIRTH18, antenatal-care codes) is a
population-restriction marker that is part of the indicator's identity.
There is no PT_M_20-24_MRD_U18 counterpart for child marriage; the indicator
is defined on female respondents. Treat the F here as part of the indicator
name, not as a disaggregation switch.
Sex disaggregation (SEX dimension filter, at query time). For indicators
that are defined on both sexes (under-5 mortality, primary completion, stunting,
literacy), sex is not encoded in the code. It is a separate SDMX dimension —
the SEX dimension — with values F, M, _T (total). You select a slice
at query time:
get_data(indicator='CME_MRY0T4', sex='F') # under-5 mortality, girls
get_data(indicator='CME_MRY0T4', sex='M') # under-5 mortality, boys
get_data(indicator='CME_MRY0T4', sex='_T') # under-5 mortality, totalThe differentiator.py:_SUFFIX_MEANINGS table lists F, M, _T, MF as
sex-token meanings; those entries describe the values of the SEX
dimension, not a suffix you append to an arbitrary code. Appending _F to a
code that does not already carry it (e.g. inventing NT_ANT_HAZ_NE2_F) will
not resolve — the catalog does not contain such codes.
Age / wealth / residence disaggregation tokens
Disaggregation tokens appear either embedded in the code (when they are part of
the canonical definition, e.g. PT_F_20-24_MRD_U18) or applied at query time
via get_data() filters. The tokens below are the same in both places.
Age bands
Age is encoded as LOW-HIGH (inclusive) in the embedded form, or as a bound
token at the end of the code.
Token | Meaning |
| Inclusive age band in years |
| Under 1 year (neonatal / infant variants) |
| 0 through 4 years inclusive (under-5) |
| 1 through 4 years inclusive (childhood, post-infant) |
| "Under" threshold — strictly below the named age |
| Adolescent (10–19, occasionally 10–24) |
Wealth quintile (query-time filter)
Token | Meaning |
| Lowest quintile (poorest 20%) |
| Middle quintiles |
| Highest quintile (richest 20%) |
| Bottom 20% / bottom 40% |
| Top 20% |
Residence (query-time filter)
Token | Meaning |
| Total |
| Urban |
| Rural |
Anthropometric Z-score thresholds
These appear in the NT_ANT_* family and need their own table because the
convention is non-obvious:
Token | Meaning |
| Below -2 SD ("negative end, 2 SD") — moderate-or-severe form |
| Below -3 SD — severe form |
| Between -3 SD and -2 SD — moderate-only form |
| Above +2 SD — overweight side |
| Height-for-age / Weight-for-age / Weight-for-height / BMI-for-age Z-scores |
Derived metrics: TRGT_ / _ARR_ / _PRJ
Three special grammars flag values that are not observations but
transformations of them. These are the cases where v1.1.1's query-aware
CURATED_PREFERRED scoring matters most. Note that ARR is a middle-segment
token (e.g. CME_ARR_U5MR, CME_ARR_SBR), not a trailing suffix.
Pattern | Meaning | Example |
| Country-set target value for the named indicator at the named horizon year. |
|
| Annual Rate of Reduction — annualised percentage change derived from the underlying series; appears as a middle-segment token |
|
| Projected / forecast value rather than observed |
|
v1.1.1 query-aware scoring (commit 7112e1d): the MCP only surfaces
TRGT_* codes when your natural-language query mentions target, goal,
objective, or a horizon year. A bare "DTP3 coverage in Brazil" question will
resolve to the observation series; "Brazil's 2030 DTP3 target" will resolve to
TRGT_2030_IM_DTP3. This is implemented as a hint field on CURATED_PREFERRED
entries in differentiator.py, not as a hard exclusion — the target code is
still discoverable via direct lookup, just demoted in resolver ranking unless
the query signals intent.
The same query-aware demotion applies to _MOD (modelled) variants: queries
mentioning survey, raw, or observed prefer the un-suffixed code; queries
mentioning modelled, estimate, joint estimation prefer _MOD.
Education levels
In the ED_* family, ISCED levels are abbreviated L1 / L2 / L3:
Token | ISCED level | Conventional name |
| ISCED 1 | Primary education |
| ISCED 2 | Lower-secondary education |
| ISCED 3 | Upper-secondary education |
Examples: ED_ANAR_L1 (adjusted net attendance rate, primary), ED_ANAR_L2
(lower-secondary), ED_CR_L1 (completion rate, primary). Where a code stops at
L1 it is primary-only; where two levels are reported jointly the codes are
listed separately rather than concatenated.
Scope caveat. The L<n> = ISCED <n> mapping holds only inside the
ED_* family. Outside ED_*, L<n> may encode a non-ISCED level — for
example PV_CHLD_MPI_L1 and PV_CHLD_MPI_L2 use L1 / L2 to mean severe
and moderate multidimensional-poverty deprivation respectively. Some ED_*
codes also use the two-digit form L01 / L02 to encode early-childhood or
pre-primary levels that sit below ISCED 1. Always check the indicator's name
before assuming L<n> means primary / lower-secondary / upper-secondary.
Where this is encoded in the MCP
The conventions documented above are not folklore — they are encoded in the MCP source and can be audited directly:
src/unicefstats_mcp/differentiator.pyis the canonical reference for segment-level meaning. The_SUFFIX_MEANINGStable maps every recognised trailing token to a human-readable gloss;explain_difference()walks two codes side-by-side and labels each diverging segment; theCURATED_PREFERREDtable carries per-indicatordimension_hintstrings that drive the v1.1.1 query-aware scoring.src/unicefstats_mcp/indicator_resolver.pymaps natural-language phrases to canonical codes._SYNONYMScovers the routine cases ("stunting", "under-5 mortality", "child marriage");_AMBIGUOUSflags phrases that legitimately map to more than one code and require user disambiguation;_DISAMBIGUATION_TIPScarries the short hints surfaced in theassistant_guidanceenvelope field added in v1.1.0.unicef://glossaryMCP resource — clients that load resources at session start get the disaggregation-code and indicator-prefix legend without having to parse this README.
When the resolver picks a code, the chain is: natural-language query →
indicator_resolver lookup → if ambiguous, _AMBIGUOUS hit fires the v1.0.0
ambiguity_flag + candidate list → if a CURATED_PREFERRED entry matches, the
dimension_hint re-ranks candidates → the chosen code is annotated by
differentiator.py for the assistant_guidance field. Every step is
inspectable in the source; nothing in this section is heuristic on the LLM side.
Demo
Step 1: Search for indicators
>>> search_indicators("stunting", limit=3){
"query": "stunting",
"total_matches": 11,
"showing": 3,
"results": [
{"code": "FD_STUNTING", "name": "Moderate and severe stunting (Functional difficulties)"},
{"code": "NT_ANT_HAZ_NE2", "name": "Height-for-age <-2 SD (stunting)"},
{"code": "NT_ANT_HAZ_NE3", "name": "Height-for-age <-3 SD (severe stunting)"}
],
"tip": "Use get_indicator_info('FD_STUNTING') for full details including available disaggregations."
}Step 2: Get indicator metadata
>>> get_indicator_info("CME_MRY0T4"){
"code": "CME_MRY0T4",
"name": "Under-five mortality rate",
"description": "Probability of dying between birth and exactly 5 years of age, expressed per 1,000 live births",
"dataflow": "GLOBAL_DATAFLOW",
"sdmx_api": "https://sdmx.data.unicef.org/ws/public/sdmxapi/rest/data/UNICEF,GLOBAL_DATAFLOW,1.0/.CME_MRY0T4?format=csv",
"disaggregation_filters": {
"sex": ["_T (Total)", "M (Male)", "F (Female)"],
"wealth_quintile": ["Q1 (Lowest)", "Q2", "Q3", "Q4", "Q5 (Highest)"],
"residence": ["_T (Total)", "U (Urban)", "R (Rural)"]
}
}Step 3: Check temporal coverage
>>> get_temporal_coverage("CME_MRY0T4"){
"code": "CME_MRY0T4",
"start_year": 1931,
"end_year": 2024,
"latest_year": 2024,
"countries_with_data": 249,
"note": "Not all countries have data for all years. Coverage varies by country."
}Step 4: Fetch data
>>> get_data("CME_MRY0T4", ["BRA", "IND", "NGA"], start_year=2018, end_year=2023){
"indicator": "CME_MRY0T4",
"countries_requested": ["BRA", "IND", "NGA"],
"total_rows_available": 18,
"rows_returned": 18,
"rows_truncated": false,
"format": "compact",
"summary": {
"value_range": {"min": 14.42, "max": 117.56, "mean": 54.78},
"year_range": {"earliest": 2018, "latest": 2023},
"countries_in_result": 3
},
"data": [
{"iso3": "BRA", "country": "Brazil", "period": 2018, "indicator": "CME_MRY0T4", "value": 15.22},
{"iso3": "BRA", "country": "Brazil", "period": 2019, "indicator": "CME_MRY0T4", "value": 15.03},
{"iso3": "BRA", "country": "Brazil", "period": 2020, "indicator": "CME_MRY0T4", "value": 14.87},
{"iso3": "BRA", "country": "Brazil", "period": 2021, "indicator": "CME_MRY0T4", "value": 14.72},
{"iso3": "BRA", "country": "Brazil", "period": 2022, "indicator": "CME_MRY0T4", "value": 14.59},
{"iso3": "BRA", "country": "Brazil", "period": 2023, "indicator": "CME_MRY0T4", "value": 14.42},
{"iso3": "IND", "country": "India", "period": 2018, "indicator": "CME_MRY0T4", "value": 36.87},
{"iso3": "IND", "country": "India", "period": 2019, "indicator": "CME_MRY0T4", "value": 34.86},
{"iso3": "IND", "country": "India", "period": 2020, "indicator": "CME_MRY0T4", "value": 32.98},
{"iso3": "IND", "country": "India", "period": 2021, "indicator": "CME_MRY0T4", "value": 31.19},
{"iso3": "IND", "country": "India", "period": 2022, "indicator": "CME_MRY0T4", "value": 29.53},
{"iso3": "IND", "country": "India", "period": 2023, "indicator": "CME_MRY0T4", "value": 27.99},
{"iso3": "NGA", "country": "Nigeria", "period": 2018, "indicator": "CME_MRY0T4", "value": 117.19},
{"iso3": "NGA", "country": "Nigeria", "period": 2019, "indicator": "CME_MRY0T4", "value": 117.37},
{"iso3": "NGA", "country": "Nigeria", "period": 2020, "indicator": "CME_MRY0T4", "value": 117.42},
{"iso3": "NGA", "country": "Nigeria", "period": 2021, "indicator": "CME_MRY0T4", "value": 117.56},
{"iso3": "NGA", "country": "Nigeria", "period": 2022, "indicator": "CME_MRY0T4", "value": 117.46},
{"iso3": "NGA", "country": "Nigeria", "period": 2023, "indicator": "CME_MRY0T4", "value": 116.82}
]
}Key insights an AI assistant would extract from this:
Brazil: 14.4 per 1,000 — steadily declining, on track for SDG 3.2 target (≤25)
India: 28.0 per 1,000 — rapid improvement (37→28 in 5 years), recently crossed SDG target
Nigeria: 117 per 1,000 — essentially flat, 4.7× the SDG target, highest burden
Step 5: Get code template to continue in a script
>>> get_api_reference("r", "unicefData"){
"language": "r",
"install": "install.packages(\"unicefdata\")",
"import": "library(unicefdata)",
"function": "unicefData",
"signature": "unicefData(\n indicator = NULL, # character — indicator code(s)\n countries = NULL, # character vector — ISO3 codes, NULL = all\n year = NULL, # numeric, character (\"2015:2023\"), or vector\n sex = \"_T\", # character — \"_T\", \"M\", \"F\"\n totals = FALSE, # logical — only return aggregate totals\n tidy = TRUE, # logical — standardize column names\n country_names = TRUE, # logical — add country name column\n format = \"long\", # character — \"long\", \"wide\", \"wide_indicators\"\n latest = FALSE, # logical — most recent value per country\n circa = FALSE, # logical — closest available year\n add_metadata = NULL, # character vector — e.g. c('region', 'income_group')\n dropna = FALSE, # logical — drop rows with missing values\n simplify = FALSE, # logical — minimal columns\n mrv = NULL, # integer — most recent N values per country\n raw = FALSE, # logical — all disaggregations, no filtering\n)",
"returns": "tibble with columns: indicator_code, iso3, country, period, value, sex, age, wealth_quintile, residence, ...",
"examples": [
{"description": "Under-5 mortality for Brazil, India, Nigeria (2015–2023)", "code": "df <- unicefData(\"CME_MRY0T4\", countries = c(\"BRA\", \"IND\", \"NGA\"), year = \"2015:2023\")"},
{"description": "Latest stunting data for all countries", "code": "df <- unicefData(\"NT_ANT_HAZ_NE2\", latest = TRUE)"},
{"description": "Wide format with region metadata", "code": "df <- unicefData(\"CME_MRY0T4\", format = \"wide\", add_metadata = c(\"region\", \"income_group\"))"}
]
}This lets the AI generate correct R/Python/Stata code using the exact parameter names and syntax — no guessing from training data.
get_data parameters
Parameter | Type | Default | Description |
| str | required | Indicator code |
| list[str] | required | ISO3 codes (max 30) |
| int | None | Start of year range |
| int | None | End of year range |
| str | "_T" | "_T" (total), "M" (male), "F" (female) |
| str | None | "Q1"–"Q5", "B20", "B40", "T20" |
| str | None | "U" (urban), "R" (rural), "_T" (total) |
| str | "compact" | "compact" (5 cols) or "full" (all cols) |
| int | 200 | Max rows (1–500) |
Response features
summary: Value range (min/max/mean), year range, country countdisaggregations_in_data: Which dimensions have non-trivial variationtotal_rows_availablevsrows_returned: Pagination metadatatip: Contextual guidance for next steps or narrowing results
Prompts
compare_indicators
Pre-built analysis workflow: fetches indicator metadata and data, then produces a structured comparison.
compare_indicators(indicator="CME_MRY0T4", countries="BRA,IND,NGA", start_year="2015", end_year="2023")write_unicefdata_code
Generate runnable Python, R, or Stata code using the unicefdata package. The AI will call get_api_reference() to get the exact function signatures, then write code matching the user's task.
write_unicefdata_code(
task="Compare under-5 mortality for Brazil and India, 2015-2023, then plot the trends",
language="r"
)This bridges the gap between conversational exploration (via MCP tools) and reproducible analysis scripts (via unicefdata packages).
Benchmark Results
We benchmarked the MCP against a bare LLM (Claude Sonnet 4, no tools) using the EQA metric from Azevedo (2025). 300 queries across 10 indicators, 20 countries, 2 prompt types, and 2 hallucination test categories.
Current canonical numbers (v0.7.3 + fixes, May 2026)
The numbers below are the current canonical scoreboard. They reflect (a) four engineering fixes in the v0.7.3 cycle (see CHANGELOG) and (b) a scoring correction from the v1.4 extractor that respects refusal language. Both samples (mcp060: 40 countries; mcp073: disjoint 20-country validation sample) score under the same v1.4 rules.
Metric | LLM alone (no tools) | LLM + MCP (v0.7.3 + fixes) | Sample |
POS EQA mean | 0.121 | 0.891 | mcp060 (40 ctry) |
POS EQA mean | 0.121 | 0.909 | mcp073 (20 disjoint ctry) |
hall_b combined (T1+T2) | 2.50% (hall_a) | 1.00% | mcp060 |
hall_b combined (T1+T2) | 2.50% (hall_a) | 2.25% | mcp073 |
MCP makes model safer (hall_b < hall_a) | — | Yes — both samples | both |
v0.7.3 + fixes is the first version where MCP demonstrably makes the model safer than the no-tools baseline on absent-data queries. Through v0.7.2, hall_b ≥ hall_a — the safety layer was reducing magnitude but not direction. The four fixes that flipped the property:
server.py:_seed_data_frontier_cache— monotonic max instead of unconditional overwrite (a probe-induced regression of the cached frontier was telling the LLM that current data was unavailable).server.py:get_dataexception handler —unicefdatacascade exhaustion reclassified asno_data(noterror), so the LLM treats it as authoritative absence rather than tool failure.benchmark_eqa_batch.py— persist toolresult_stronstate.tool_callsso the v1.4 extractor can see refusals.benchmark_eqa_batch.py— refusal-respect parity with the sync runner.
Canonical scoreboard: see the [Unreleased] §Fixed block at the top of CHANGELOG.md for the full cross-version table, including the four post-fix corrections and the mcp073 second-sample validation. Full per-run write-ups live in internal/v0_7_3_validation.md and internal/v0_7_3_second_sample_validation.md in the dev repo (jpazvd/unicefstats-mcp-dev, dev-only and not synced to this public mirror).
Historical: v0.3.0 (n=600, 2025) and v0.7.2 (n=500 same-day, 2026-05-08)
The original v0.3.0 benchmark reported POS EQA 0.147 → 0.990 (6.7×) and T2 hallucination 11% → 37%. Two corrections since:
The "37%" headline was substantially a v1.3 extractor scoring artefact: the extractor counted any numeric value mentioned in the response as a "claim," including values quoted from
no_datatool results inside an explicit refusal. The v1.4 extractor (_detect_refusal) reclassifies those as appropriate refusals. We did not rescore the v0.3.0 parquets under v1.4 (they're archived); the v1.4-equivalent of "37%" is unknown but substantially lower.The v0.7.2 reproduction (n=500, 2026-05-08) under v1.3 scoring reported POS EQA 0.897 and combined T1+T2 hallucination 13%. Under v1.4 scoring the same parquets report POS EQA 0.793 and hall_b 3.75%.
The accuracy headline (~7× lift) has held up across every rescoring. The hallucination headline required both a scoring fix (v1.4 extractor) and a server fix (v0.7.3 cache + cascade fixes) before MCP actually reduced hallucination below the no-tools baseline.
EQA decomposition (baseline_latest prompt)
Component | LLM alone | LLM + MCP | Gain |
ER (extraction rate) | 0.50 | 1.00 | +0.50 |
YA (year accuracy) | 0.24 | 0.99 | +0.75 |
VA (value accuracy) | 0.37 | 1.00 | +0.63 |
EQA = ER × YA × VA | 0.147 | 0.990 | +0.843 |
Key findings
All 10 indicators at EQA >= 0.95 with MCP, replicated across 40 countries (R1 + R2 with zero overlap). 7 of 10 achieve perfect EQA = 1.000.
Year accuracy is the bare LLM's biggest weakness (YA = 0.24). It cites 2021-2022 as "latest" when IGME 2024 estimates exist. The MCP queries the API and returns the actual latest year.
The direct prompt shows larger MCP gain (+0.722 vs +0.613) because it eliminates YA and isolates pure retrieval accuracy.
T2 hallucination (~37%) is inflated by ground truth misclassification: the SDMX API has IGME mortality data for micro-states that the ground truth pipeline missed. After correction: MCP ~10%, LLM alone ~5%. The remaining hallucination is driven by the confidence effect — Claude overrides tool errors when it has strong domain priors.
The confidence effect: When the MCP tool returns "no data" but the LLM has strong domain priors (e.g., child mortality for well-known countries), it overrides the tool and fabricates anyway. This is a fundamental LLM behavior, not MCP-specific.
3-way comparison (vs sdmx-mcp)
Metric | LLM alone | unicefstats-mcp (v0.7.3 + fixes) | sdmx-mcp |
EQA (POS) | 0.121 | 0.891 | 0.074 |
hall_b combined (T1+T2) | hall_a 2.50% | 1.00% (mcp060) / 2.25% (mcp073) | 0% |
MCP safer than no-tools? | — | Yes | Yes (zero by construction) |
Cost per query | ~$0.003 | ~$0.04 | $0.087 |
Avg latency | ~5s | ~10s | 60s |
sdmx-mcp's raw SDMX-JSON output is hard for LLMs to parse (VA ≈ 0.11), but its anti-hallucination guardrails are highly effective (0% fabrication). See Relationship to sdmx-mcp for details.
Full analysis, per-indicator decomposition, and methodology: examples/RESULTS.md
Benchmark data (parquet with full LLM responses): examples/results/
Benchmark design rationale: examples/DESIGN_ISSUES.md
Reproducing the benchmark
# Build ground truth from UNICEF SDMX API
python examples/00_build_ground_truth.py
# Run 200-query benchmark (requires ANTHROPIC_API_KEY, ~$6)
python examples/benchmark_eqa.py
# Add 100 direct-prompt queries to existing run (~$3)
python examples/01_run_direct_supplement.pyCitation
This benchmark uses the EQA metric from:
Azevedo, J.P. (2025). "AI Reliability for Official Statistics: Benchmarking Large Language Models with the UNICEF Data Warehouse." UNICEF Chief Statistician Office. github.com/jpazvd/unicef-sdg-llm-benchmark-dev
Deployment
Local (stdio)
unicefstats-mcpRemote (SSE)
unicefstats-mcp --transport sse --port 8000Docker
docker build -t unicefstats-mcp .
docker run -p 8000:8000 unicefstats-mcpDevelopment
pip install -e ".[dev]"
pytest tests/ -v
ruff check src/ tests/
mypy src/unicefstats_mcp/Contributing
Contributions are welcome.
Ways to contribute
Bug reports: Open an issue with steps to reproduce
Feature requests: Suggest new tools, indicators, or output formats via issues
Code: Fork, branch, submit a PR — see development setup below
Benchmark: Run the EQA benchmark on different models and share results
Documentation: Improve examples, fix typos, add use cases
Development setup
git clone https://github.com/jpazvd/unicefstats-mcp.git
cd unicefstats-mcp
pip install -e ".[dev,benchmark]"
pytest tests/ -v
ruff check src/ tests/
mypy src/unicefstats_mcp/Pull request guidelines
One concern per PR — keep changes focused and reviewable
Include tests for new tools or bug fixes
Run the linter (
ruff check) and type checker (mypy) before submittingUpdate the README if you change tool signatures or add new features
Do not commit API keys or benchmark result parquets larger than 500KB
Priority areas
See the audit findings for known issues. High-impact areas:
MNCH dataflow bug:
MNCH_CSECandMNCH_BIRTH18return 0 EQA due to a dataflow resolution issue in theunicefdatapackageT2 hallucination reduction: Further reduce fabrication when API returns no results (currently ~10%; see Limitations)
Limitations and Hallucination Risks
Data limitations
Coverage is uneven across indicators, countries, and years. Survey-based indicators (nutrition, education, protection) have 3-5 year gaps between data points by design.
Mortality indicators (CME_*) are modeled estimates from the UN Inter-agency Group (IGME), with uncertainty intervals not surfaced in compact output.
Not all indicators support all disaggregation dimensions;
get_indicator_info()lists what's available per indicator.get_data()caps at 500 rows per call.
Hallucination risks
Benchmark testing across multiple country samples (v0.7.3 + fixes, May 2026):
Type | Description | Rate (LLM alone, hall_a) | Rate (LLM + MCP, hall_b) | Notes |
T1 (gap-year) | LLM cites a value for a year when the indicator has data but not for that specific year | ~1.0% | 0.00% (mcp060) / 0.50% (mcp073) | Below the no-tools rate |
T2 (forward-of-frontier) | LLM fabricates a value for a year beyond the data frontier | ~3.5% | 2.00% (mcp060) / 4.00% (mcp073) | Below the no-tools rate |
Combined | T1 + T2 | hall_a 2.50% | 1.00% (mcp060) / 2.25% (mcp073) | hall_b < hall_a — MCP makes safer |
v0.7.3 + fixes is the first release where MCP makes the model safer than the no-tools baseline. Through v0.7.2 (v1.4 scoring), hall_b ≥ hall_a — the safety layer was reducing magnitude relative to a no-safety-layer baseline but never enough to put MCP under the no-tools floor.
What changed:
Cache contamination in
_seed_data_frontier_cache— a probe routine was overwriting the cached max-year for an indicator with whatever year happened to come back, sometimes lower than the cached value. The LLM was being told frontiers were 2018 for indicators whose real frontier was 2023, then answering about 2023 from parametric memory. Fix: monotonicmax()only.unicefdatacascade asno_data, noterror— the underlying wrapper's fallback-dataflow exhaustion was leaking as a tool exception, which the LLM read as "tool failed, fall back to my own knowledge." Now classified as a cleanno_datasignal that the safety layer converts into "do not estimate." Filedunicefdata-dev#74for an upstream fix.v1.4 extractor (
_detect_refusal) — corrected the long-standing scoring artefact in which any numeric value in a response was counted as a "claim," including values the LLM was quoting fromno_datatool results inside an explicit refusal. This dropped hall_b in the v0.7.3 PRE-FIX rescoring from ~37% to 2.25% — most of the historical "MCP-makes-it-worse" finding was extractor, not behaviour.Batch runner parity —
result_strpersistence and refusal-respect in the async batch runner.
This finding still leaves room for the broader tool-augmented LLM literature on the structural cost of giving models an answer-producing pathway:
The Reasoning Trap: How Enhancing LLM Reasoning Amplifies Tool Hallucination (ICLR 2025) — shows the relationship is causal: as models get better at tool use, tool hallucination rises proportionally with capability.
Reducing Tool Hallucination via Reliability Alignment (Cao et al., 2024, arXiv:2412.04141) — formalises the failure as tool-selection errors (wrong tool, failed refusal) and tool-usage errors (fabricated parameters).
ReDeEP: Detecting Hallucination in Retrieval-Augmented Generation via Mechanistic Interpretability (Sun et al., 2024) — shows mechanistically that an LLM's parametric knowledge can override retrieved context inside the residual stream.
These results show the structural tendency is real; the v0.7.3 + fixes result shows it can be reversed for a specific data domain through (a) safety-layer architecture, (b) server-side discipline (no stale frontier cache, no leaking of no-data as tool errors), and (c) honest scoring.
The takeaway for users:
Load the
unicef://system-promptandunicef://contextresources at session start (handles forward-of-frontier fabrication).Treat MCP results as best-effort retrieval, not infallible truth — verify load-bearing values against the UNICEF Data Warehouse before citing.
Prefer queries with explicit years ("under-five mortality in Nigeria in 2023") over open-ended ones ("the latest under-five mortality in Nigeria") — the former triggers refusal more reliably when data is absent.
The 1.00% / 2.25% residuals were measured on Sonnet 4 only. Cross-model generalisation is the next benchmark.
Full benchmark methodology: examples/RESULTS.md
Provenance and Ownership
All data served by this MCP originates from the UNICEF Data Warehouse, accessed live via the public SDMX REST API. No observation data is stored or cached — every get_data() call results in a live SDMX request. The indicator and country registries are cached in memory at first access for performance; these are catalogue metadata, not statistical values. The MCP reformats output for LLM consumption but does not alter values.
All releases are published from GitHub Actions using PyPI Trusted Publishing (OIDC). No long-lived API tokens exist. Release provenance is verifiable via PyPI attestations.
For full details on data origin, ownership, distribution pipeline, and interpretation caveats, see PROVENANCE.md.
How to Verify This MCP
Check | How |
Source | Repository is |
Package |
|
Version |
|
Provenance | PyPI attestations link each release to a GitHub Actions workflow |
Runtime | Call |
License
MIT
Available Tools
9 toolsget_api_referenceGet unicefdata API Reference (Python / R / Stata)ARead-onlyIdempotent
Get the unicefdata package API reference for Python, R, or Stata.
Returns function signatures, parameter descriptions, and usage examples. Use this when you need to write code that uses the unicefdata package, or when the user wants to move from conversational exploration to reproducible scripts.
| Name | Required | Description | Default |
|---|---|---|---|
| function | No | Optional function name to filter the reference to a single entry (e.g. 'unicefData', 'list_indicators'). If omitted, returns the full reference. | |
| language | No | Target language for the unicefdata code template. One of 'python', 'r', or 'stata' (case-insensitive — the server normalises before lookup). Returns the language-specific API surface for users who want to reproduce the MCP's data fetches in their own scripts. | python |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the tool is known to be safe and idempotent. The description adds value by detailing the output content (function signatures, parameter descriptions, usage examples), which is beyond the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is three concise sentences: first states purpose, second lists content, third gives usage guidance. No wasteful text, well-structured.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the presence of an output schema (not shown but confirmed), the description does not need to explain return values. It covers purpose, content, and usage completely for a tool with two optional parameters.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the input schema already documents both parameters with descriptions. The tool description does not add additional meaning for the parameters beyond what is in the schema. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description specifies 'Get the unicefdata package API reference for Python, R, or Stata' with clear verb and resource. It distinguishes from siblings like get_data or search_indicators by focusing on code reference rather than data retrieval or search.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit usage guidance: 'Use this when you need to write code that uses the unicefdata package, or when the user wants to move from conversational exploration to reproducible scripts.' It implies when to use but does not explicitly mention when not to use or list alternatives, which prevents a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_dataFetch UNICEF Indicator DataARead-onlyIdempotent
Fetch UNICEF data for an indicator and one or more countries.
Returns annual observations from the UNICEF SDMX API. Use format="compact" (default) for a clean 5-column table; use format="full" for all columns including disaggregation details and confidence bounds.
indicator accepts BOTH codes AND human-readable names (v0.7.0). Pass
whichever you have from the user's question — the server resolves names
to canonical codes for you. Examples:
indicator='CME_MRM0' → code passthrough
indicator='neonatal mortality' → resolves to 'CME_MRM0'
indicator='Under-five mortality rate' → resolves to 'CME_MRY0T4'
indicator='U5MR' → resolves to 'CME_MRY0T4'
indicator='stunting' → resolves to 'NT_ANT_HAZ_NE2'
indicator='LBW' / 'low birth weight' → resolves to 'NT_BW_LBW'
Acronyms accepted: NMR, IMR, U5MR, SBR, LBW, ANC1, SAB, BCG, DTP1/3 …
Genuinely ambiguous queries are refused with a disambiguation list. Examples: indicator='child mortality' → refused: pick CME_MRM0 / CME_MRY0 / CME_MRY0T4 / CME_MRY1T4 indicator='vaccination' → refused: pick IM_BCG / IM_DTP1/3 / IM_MCV1/2
Prefer passing the user's phrasing verbatim over guessing the code from
memory — that's where the v0.6.x had a documented failure mode (model
recalls CME_MRY0T4 thinking it's neonatal mortality; it's under-five).
The response echoes the resolution under indicator_resolution so you
can confirm the canonical code+name match the user's intent.
countries accepts BOTH ISO3 codes AND country names (v0.6.2). Pass
whichever you have from the user's question — the server resolves country
names to canonical ISO3 codes for you. Examples:
countries=['Burundi'] → resolves to ['BDI']
countries=['BDI', 'Belgium'] → resolves to ['BDI', 'BEL']
countries=['Cote d'Ivoire'] → resolves to ['CIV']
countries=['USA', 'UK'] → resolves to ['USA', 'GBR']
Common synonyms accepted: 'USA'/'United States', 'UK'/'Great Britain',
'Ivory Coast'/'Cote d'Ivoire', 'South Korea', 'DRC', 'Czech Republic',
'Burma', 'Vatican', etc. The response echoes the resolution under
country_resolutions and the canonical name+code pairs under
countries_returned_with_names so you can confirm intent matched.
Prefer passing the user's country name verbatim over guessing the ISO3 code from memory — that's where v0.6.0 had a documented failure mode.
v0.6.0 server-side hardening: this tool also performs a pre-flight
year-frontier check. If start_year or end_year exceeds the indicator's
data frontier (max year observed), the call is refused server-side without
issuing the SDMX request — preventing the silent-truncation pattern where
a range like 2020-2027 returns 2020-2024 and the model extrapolates the
missing years. Successful responses include a data_frontier field
naming the max year and an explicit no-extrapolation directive.
Disaggregation filters (v1.2.0):
sex: "_T" (total, default), "M" (male), "F" (female).
age: SDMX AGE code such as "Y0T4", "Y15T19", "Y15T24". The age codelist
depends on the indicator's dataflow — call get_indicator_info(code)
to see what's supported.
filters: dict[str, str | None] for every other dimension. Examples:
filters={"WEALTH_QUINTILE": "Q1"} → poorest quintile
filters={"RESIDENCE": "U"} → urban
filters={"EDUCATION_LEVEL": "ISCED11_2"} → secondary
filters={"WEALTH_QUINTILE": "Q1", "RESIDENCE": "U"} → both
A None value is treated as "use the dim's total" (typically "_T").
When any non-_T filter is present (typed age= or filters dict
non-empty), the response carries mode: "raw_filtered" to signal that
the underlying call switched to raw=True + post-filter — same call,
different number versus a totals-only response.
Validation: filters are checked against the indicator's actual primary
dataflow before the SDMX call. Unsupported (dim, value) pairs are
refused with a failed_validation envelope listing available
dimensions and codelist values, so the LLM can recover in a single
wave instead of retrying blindly.
BREAKING CHANGE FROM v1.1.x: the typed wealth_quintile= and
residence= kwargs no longer route to the SDMX call — they are present
in the signature only as deprecation trip-wires. Pass them and the call
returns a structured migration error pointing at
filters={"WEALTH_QUINTILE": ...}. v1.1.x silently dropped these
kwargs at the SDMX call and returned the totals slice — that hazard
is gone.
Limit defaults to 200 rows — narrow your country/year filters or
increase limit (max 500) if you need more data. In raw_filtered mode,
rows_truncated: true also means the pre-filter raw pull was larger
than limit, so post-filter rows you wanted may be missing.
| Name | Required | Description | Default |
|---|---|---|---|
| age | No | SDMX age slice (e.g. 'Y15T19' for 15-19 years). Routed through raw=True + post-filter — engages mode='raw_filtered'. | |
| sex | No | SDMX sex code: '_T' (total, default), 'F' (female), 'M' (male). First-class via the unicefData() call. | _T |
| limit | No | Maximum row count. Carries `truncated: true` in envelope if hit. | |
| format | No | 'compact' = 5-column table (default); 'full' = all SDMX columns including disaggregation details and confidence bounds. | compact |
| filters | No | Free-form filter dict for any dim not first-class via unicefData() — WEALTH_QUINTILE, RESIDENCE, EDUCATION_LEVEL, DISABILITY_STATUS, etc. Engages mode='raw_filtered'. Use get_indicator_info to discover valid dims and codelists per indicator. | |
| end_year | No | Inclusive end year (e.g. 2023). Refused if > frontier. | |
| countries | Yes | List of ISO3 country codes (e.g. ['BRA', 'IND', 'NGA']) OR country names (e.g. ['Brazil', 'India']). Names are resolved server-side. Hard cap of 30 entries per request. | |
| indicator | Yes | UNICEF SDMX indicator code (e.g. 'CME_MRY0T4') OR human-readable name (e.g. 'under-5 mortality'). Names are resolved server-side. | |
| residence | No | REMOVED in v1.2.0. Use filters={'RESIDENCE': 'U'}. | |
| start_year | No | Inclusive start year (e.g. 2010). Refused if > frontier. | |
| wealth_quintile | No | REMOVED in v1.2.0. Use filters={'WEALTH_QUINTILE': 'Q1'}. | |
| cascade_on_empty | No | Opt into upstream `unicefdata`'s multi-dataflow cascade. When True AND the indicator has ≥2 dataflows in metadata AND a year or country filter is set, an empty primary-dataflow response causes upstream to walk fallback dataflows (typically GLOBAL_DATAFLOW) for the same code. COST: +1 to +N SDMX round-trips on the unhappy path. BENEFIT: recovers data on multi-dataflow indicators where the topic-specific dataflow doesn't carry the requested country/year. CAVEAT: the walked dataflow may use different methodology / aggregation than the primary (e.g. GLOBAL_DATAFLOW values can be modelled estimates where the primary is observed survey data); inspect `dataflow_used` in the response to confirm. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, openWorldHint, idempotentHint, and destructiveHint. The description adds significant detail: resolution of codes/names, ambiguous query refusal, year-frontier check, raw_filtered mode, validation, deprecation migration, and cascade behavior. No contradictions with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured with headings and bullet points, front-loaded with the core purpose. Each section earns its place by conveying necessary details efficiently, despite its length.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity (12 parameters, 2 required, 100% schema coverage, output schema exists), the description is exhaustive: it covers all parameters, error cases, deprecations, best practices, and breaking changes. The output schema exists, so return values are not needed here.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, providing a baseline of 3. The description dramatically enhances parameter understanding with real examples, resolution logic, disambiguation lists, filter usage patterns, and deprecation notes, adding substantial value beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states 'Fetch UNICEF data for an indicator and one or more countries' with a specific verb and resource. It distinguishes itself from siblings like get_indicator_info and search_indicators by focusing on data retrieval rather than metadata or search.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides extensive usage guidelines including how to pass parameters verbatim, handle disambiguation, use filters, and avoid deprecated features. However, it does not explicitly contrast with sibling tools or state when not to use this tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_indicator_infoGet UNICEF Indicator MetadataARead-onlyIdempotent
Get full metadata for a UNICEF indicator.
Returns description, category, dataflow, and SDMX API details. Use this before calling get_data() to understand what the indicator measures and which disaggregation filters (sex, age, wealth_quintile, residence) apply.
| Name | Required | Description | Default |
|---|---|---|---|
| code | Yes | Exact UNICEF SDMX indicator code (e.g. 'CME_MRY0T4' for under-5 mortality, 'NT_ANT_HAZ_NE2' for stunting). Case-sensitive. Use search_indicators if you don't already know the code. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, and destructiveHint. Description adds context about the tool being a metadata lookup with no side effects, naming the returned fields and their use in filtering. This adds moderate value beyond annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences: first states broad purpose, second lists return content, third gives usage context. Front-loaded and every sentence is informative with no redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter tool with an output schema and clear annotations, the description fully covers what the tool does, how to use it, and its relationship to siblings. No gaps remain.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Input schema coverage is 100% with a detailed description for the 'code' parameter including examples and hints. The tool description does not add additional parameter semantics beyond what the schema provides, so baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool retrieves full metadata for a UNICEF indicator, listing specific fields (description, category, dataflow, SDMX API details). It distinguishes from sibling tools like search_indicators and get_data by explaining when to use each.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly advises using this before get_data() to understand indicator and disaggregation filters. Also implies using search_indicators if code is unknown, providing clear when-to and when-not-to guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_server_metadataGet unicefstats-mcp Server MetadataARead-onlyIdempotent
Return machine-readable identity, provenance, and version information for this MCP server.
Use this to verify you are connected to the authentic unicefstats-mcp server and to inspect its canonical identity, data source, and publisher information. No API call — returns local metadata only.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Description adds useful context beyond annotations: 'No API call — returns local metadata only.' Annotations already provide readOnlyHint, destructiveHint, etc., so description enhances transparency without contradiction.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three concise sentences, no fluff. Front-loaded with return type, then usage, then behavior. Every sentence adds value.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given no parameters, annotations covering safety, and existence of output schema, the description sufficiently explains purpose, usage, and behavior. No gaps for this simple tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
No parameters exist (schema coverage 100%). Description adds no parameter details, but that's acceptable; baseline 4 for zero-parameter tool.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Description clearly states the tool returns machine-readable identity, provenance, and version information. It distinguishes from sibling data tools by specifying it's for server metadata and is local.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly says 'Use this to verify you are connected to the authentic unicefstats-mcp server' and describes what it inspects. Does not explicitly mention when not to use, but context is clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_temporal_coverageGet UNICEF Indicator Temporal CoverageARead-onlyIdempotent
Check what years of data are available for a UNICEF indicator.
Fetches a small sample to determine the time range. Lightweight — does not fetch all observations. Use before get_data() to pick a year range.
| Name | Required | Description | Default |
|---|---|---|---|
| code | Yes | Exact UNICEF SDMX indicator code. Returns the min/max observed year range (the 'data frontier') across all countries. The MCP uses this internally to refuse get_data calls whose year(s) exceed the frontier — callers can also query it directly to display the data window. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, and destructiveHint as false. The description adds behavioral traits: 'fetches a small sample', 'lightweight — does not fetch all observations', which aligns with annotations and provides extra context. No contradictions.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences, no redundant words. Efficiently communicates purpose, behavior, and usage guidance. Front-loaded with key action, then lightweight hint, then usage advice.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's simplicity (single parameter, output schema exists), the description is complete. It explains what the tool does, how it behaves, and when to use it relative to siblings. No missing information for effective selection.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, and the schema provides detailed description for the single parameter 'code'. The tool description does not add parameter-specific detail beyond what the schema offers, but baseline 3 is appropriate given high schema coverage.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states it checks what years of data are available for a UNICEF indicator, with specific verbs ('Check', 'Fetches') and resource ('temporal coverage'). It distinguishes itself from sibling 'get_data' by advising to use it before get_data to pick a year range.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit usage context: 'Use before get_data() to pick a year range.' Also notes it is lightweight and does not fetch all observations, implying when to prefer this tool over get_data. Could be improved by mentioning when not to use it, but current guidance is clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_categoriesList UNICEF Indicator CategoriesARead-onlyIdempotent
List all UNICEF indicator categories (thematic groups).
Categories correspond to SDMX dataflows: CME (child mortality), NUTRITION, EDUCATION, CHILD_PROTECTION, WASH, HIV_AIDS, etc. Use this to browse available topics before searching for specific indicators.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum number of categories to return per page. | |
| offset | No | Pagination offset — skip this many categories before returning. The envelope carries `has_more` and `next_offset` so the LLM can page through. UNICEF has ~25-40 categories so a single page is almost always enough. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, which the description does not contradict. The description adds that categories correspond to SDMX dataflows, but this is more about content than behavioral traits. The description carries minimal additional behavioral disclosure beyond annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is very concise: two sentences in the first paragraph and two in the second. It is front-loaded with the core purpose, and every sentence adds value. No wasted words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the simplicity of the tool (list with pagination), the combination of annotations, schema, and description provides complete context. The description explains the purpose and usage context, while schema covers parameters and annotations cover safety.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With 100% schema coverage, the parameter descriptions already provide full meaning. The description does not add new parameter information; it only mentions categories correspond to dataflows, which is not directly about parameters. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the action (list) and resource (UNICEF indicator categories). It distinguishes from siblings by explaining that categories are thematic groups and that this tool is for browsing topics before searching for specific indicators (e.g., search_indicators).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly recommends using this tool to browse available topics before searching for indicators, providing clear context. It does not explicitly state when not to use it, but the purpose is well-understood in relation to siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_countriesList UNICEF Country CodesARead-onlyIdempotent
List countries available in the UNICEF database with ISO3 codes.
Optionally filter by region name (case-insensitive partial match). Use the iso3 values in get_data().
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum number of countries to return per page. | |
| offset | No | Pagination offset — skip this many countries before returning. Use with `limit` to page through. UNICEF's country list has ~450 entries (member states + dependent territories + regional aggregates), so the default limit returns everything in one page. | |
| region | No | Optional region filter applied as a case-insensitive substring match against each country's name (e.g. 'asia' matches 'South Asia'). Not a UNICEF region code lookup. When omitted, returns all 200+ countries. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, destructiveHint=false. Description adds the optional filter behavior and hints at pagination through schema. No contradictions.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, no fluff. The core purpose and key usage hint are front-loaded. Every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the output schema exists and annotations cover safety, the description adequately explains the tool's role. Mentions ISO3 codes and linkage to get_data(). Could optionally reference pagination limits, but schema handles that.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema has 100% coverage with detailed descriptions for all three parameters. The description's mention of 'case-insensitive partial match' for region is already in the schema, so minimal added value beyond what the schema provides.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Clearly states the verb 'List', the resource 'countries', and the outcome 'with ISO3 codes'. Differentiates from siblings like get_data and search_indicators by being a straightforward enumeration tool.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly tells when to use ('List countries...') and how to connect output to another tool ('Use the iso3 values in get_data()'). Does not explicitly state when not to use, but context is strong.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lookup_by_codeLookup UNICEF Indicator by Exact CodeARead-onlyIdempotent
Strict canonical lookup of a UNICEF indicator by its exact code.
Use this INSTEAD of search_indicators when you already have a UNICEF indicator code (e.g. CME_MRY0T4, IM_DTP3, ED_15-24_LR). Do NOT pass natural-language descriptions, synonyms, or partial names — this tool is canonical-only.
Two-tool separation introduced in v0.9.0 to give the LLM a self-describing choice at tool-selection time:
have a CODE? → lookup_by_code(code)
have WORDS? → search_indicators(query)
Returns the same canonical metadata shape as get_indicator_info on success. On unknown code, returns an error with an explicit abstain_instruction directing the model to stop — NOT to fall back to search_indicators (which would re-enter the loop that 96% of v9 Arm B stuck queries hit).
Returns:
on success: {status: "ok", code, name, description, dataflow, sdmx_api, disaggregation_filters, ambiguity_flag: false, ...}
on unknown code: {status: "error", error, ambiguity_flag: false, abstain_instruction, tip}
on natural-language input mistakenly passed: {status: "error", error, abstain_instruction directing to search_indicators}
| Name | Required | Description | Default |
|---|---|---|---|
| code | Yes | Exact UNICEF SDMX indicator code (e.g. 'CME_MRY0T4'). Same envelope as get_indicator_info — both route through a shared _build_indicator_envelope helper, so the disaggregation_filters block is literally identical across both tools. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Discloses behavior on success and error, including abstain instructions to prevent fallback loops. Explains the rationale for two-tool separation and how the tool handles unknown codes vs. natural-language input. Annotations already confirm read-only and idempotent, so description adds valuable context without contradiction.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured with clear sections, but is somewhat lengthy. However, every sentence adds value, and the front-loading of purpose and usage makes it efficient for an agent to parse.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has one parameter, clear annotations, and an output schema (not shown but indicated), the description covers all necessary aspects: purpose, usage, behavior, error handling, and rationale. It is fully complete for an agent to decide and invoke correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The single parameter `code` is fully described in the schema with constraints and example. The description enhances this by providing exact code examples and specifying the canonical nature, adding meaning beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states 'Strict canonical lookup of a UNICEF indicator by its exact code,' specifying the verb, resource, and method. It differentiates from sibling tool `search_indicators` by emphasizing exact code vs. words.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly advises to use this tool instead of `search_indicators` when having a exact code, and warns against passing natural-language descriptions. Provides a clear decision rule: 'have a CODE? → lookup_by_code; have WORDS? → search_indicators'.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_indicatorsSearch UNICEF IndicatorsARead-onlyIdempotent
Search UNICEF child development indicators by keyword.
Returns indicator codes, names, and categories. Use the returned code
values with get_indicator_info() or get_data().
Always start here if you don't know the indicator code.
Examples: "mortality", "breastfeeding", "education", "child labour", "stunting"
v1.1.0 advisory layer (additive to v0.9.0/v1.0.0 ambiguity_flag):
If response carries requires_confirmation=True, STOP and ask the user to disambiguate before calling get_data.
If response carries recommended + next_step, the model SHOULD call next_step verbatim (e.g. get_indicator_info(code='...')).
assistant_guidance is a plain-English directive (<200 chars).
Decision order: curated-ambiguous -> curated-preferred -> confident match -> none (wire-equivalent to v1.0.0). See internal/v1.1.0_design/ for rationale.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum number of indicator candidates to return. | |
| query | Yes | Natural-language search query (e.g. 'under-5 mortality', 'stunting', 'IMR', 'modelled estimates'). Accepts acronyms, common synonyms, and methodology phrases. | |
| offset | No | Pagination offset — skip this many top-ranked matches before returning. Use with `limit` to page through long match lists (e.g. limit=20, offset=0 for page 1; offset=20 for page 2). The response envelope carries `has_more` and `next_offset` so the LLM can decide whether to keep paging. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations indicate read-only, idempotent, non-destructive. Description adds substantial behavioral details about the advisory layer, including requires_confirmation and next_step, which are crucial for correct agent behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Well-structured with main purpose, usage, examples, and an advisory section. The advisory layer is detailed but necessary. Could be slightly more concise, but effective.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given output schema and annotations, the description fully covers purpose, usage, pagination, and integration with sibling tools. No gaps for an agent to fail understanding.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema covers 100% of parameters with descriptions. Description adds context for the query parameter (natural-language, acronyms) and explains pagination logic beyond schema fields.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Clearly states it searches UNICEF indicators by keyword and returns codes, names, and categories. Distinguishes from siblings by explaining how to use returned codes with get_indicator_info() or get_data().
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly says 'Always start here if you don't know the indicator code.' Provides examples and integration guidance. Does not explicitly state when not to use, but the context is clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
9 tool updates
v1.5.5- First observed
get_api_reference - First observed
get_data - First observed
get_indicator_info - First observed
get_server_metadata - First observed
get_temporal_coverage - First observed
list_categories - First observed
list_countries - First observed
lookup_by_code - First observed
search_indicators
TDQS
Scored across 9 tools
Each tool has a clear, distinct purpose: reference, data fetching, metadata, server info, temporal coverage, category listing, country listing, code lookup, and keyword search. No overlap in functionality.
All tool names follow a consistent verb_noun pattern (e.g., get_data, list_countries, search_indicators), making them predictable and easy to navigate.
With 9 tools, the set is well-scoped for a UNICEF data server, covering browsing, searching, fetching data, metadata, and reference without bloat.
The tool surface is comprehensive for a read-only data API: search, browse categories/countries, fetch data, check temporal coverage, get metadata, and API reference. No obvious gaps.
Maintenance
Related MCP Connectors
UNICEF Data (UN Children's Fund) MCP — global statistics on child health,
WHO GHO MCP — World Health Organization Global Health Observatory (free, no auth)
MCP server for Statistics Sweden (SCB) - 1200+ tables with population, economy, environment data
This MCP server provides seamless access to Malaysia's government open data, including datasets, w…
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceMCP server that enables AI agents to assess child growth, plot growth curves, and interpret z-scores using WHO and China NHC standards.1MIT
- AlicenseAqualityDmaintenanceMCP server for official statistics from Statistics Finland (Tilastokeskus) — the StatFin database, exposed through the PxWeb API. Search 3000+ tables, inspect their dimensions, and pull data as JSON-stat2.435 npmMIT
- FlicenseAqualityDmaintenanceAn MCP server that provides official statistical data for development indicators in Asia-Pacific. It wraps the ESCAP Data Explorer API, letting any MCP-compatible host query SDG and thematic data through natural language.5-
- AlicenseNot gradedqualityAmaintenanceQuery WHO Global Health Observatory data — 3,059 indicators across 194 member states with country, region, year, and sex filters via MCP.69 npm2Apache 2.0