unhcr-refugees-mcp-server
Server Details
Query UNHCR refugee, IDP, and stateless populations, asylum decisions, returns, and resettlement.
- Status
- Healthy
- Last Tested
- Transport
- Streamable HTTP · MCP 2025-11-25
- URL
- Repository
- cyanheads/unhcr-refugees-mcp-server
- GitHub Stars
- 1
- Server Listing
- @cyanheads/unhcr-refugees-mcp-server
TDQS
Scored across 8 tools
The get_* tools are largely distinct resources (applications, decisions, demographics, population, solutions), and the dataframe_describe/query pair is clearly metadata vs execution. There is mild potential overlap between asylum_applications and asylum_decisions, but the descriptions explicitly differentiate stages vs outcomes and cross-reference each other, so selection is generally unambiguous.
Every tool follows the same pattern: the unhcr_ prefix plus a verb_noun structure (unhcr_get_*, unhcr_dataframe_describe, unhcr_dataframe_query, unhcr_list_reference). All snake_case, no mixing of conventions, fully predictable.
Eight tools is well-scoped for a multi-dataset read-only server: five dataset getters, two dataframe helpers, and one reference decoder. Each tool earns its place with no redundancy or padding.
The surface covers the major UNHCR datasets, reference decoding, and a staging/inspection/query workflow for derived analysis, which is strong lifecycle coverage for a read-only data server. Minor gaps exist (no explicit export or convenience aggregation beyond SQL), but agents can work around these via unhcr_dataframe_query.
Available Tools
8 toolsunhcr_dataframe_describeDescribe staged dataframesARead-onlyIdempotentInspect
Describe a dataframe (df_XXXXX_XXXXX) staged by the unhcr_get_* tools — any response carrying a dataset handle staged its full result here — or list them all where this deployment allows listing. Each entry gives the source tool, query parameters, creation and expiry time, row count, whether the upstream fetch was complete, and the column schema. Read the columns here before writing SQL for unhcr_dataframe_query.
| Name | Required | Description | Default |
|---|---|---|---|
| name | No | One dataframe to describe, as df_XXXXX_XXXXX (uppercase letters and digits): the dataset.name a unhcr_get_* result returned, or a register_as name. Omit to list every staged dataframe; a deployment that serves unauthenticated callers over HTTP turns listing off, and there the name is required. |
Output Schema
| Name | Required | Description |
|---|---|---|
| error | No | Present when the call failed. Absent on success. |
| notice | No | Guidance when nothing is staged or the named dataframe is gone. |
| dataframes | No | Staged dataframes, newest first. Empty when none are staged. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover read-only, idempotent, closed-world behavior, but the description adds real context: listing may be turned off for unauthenticated HTTP deployments, and entries carry creation/expiry time and a fetch-completeness flag. That is meaningful beyond the annotations, though permissions/error behavior is not discussed.
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?
Front-loads what is described and what is returned in two dense sentences with no filler. Slightly long, but every clause (source, listing constraint, entry fields, query follow-up) 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?
With an output schema present, return values need not be explained, and the description still covers the listing caveat, entry contents, and the follow-up workflow. Nothing needed to invoke it correctly is missing.
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 single param already documents the pattern, the register_as/dataset.name source, and the omit-to-list semantics. The description mostly restates this, so the baseline 3 applies.
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?
Names a specific verb (describe) and resource (dataframe staged by unhcr_get_* tools), and distinguishes the list-all mode. It also names the sibling unhcr_dataframe_query, so an agent can tell it apart from the query tool without opening schemas.
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 instructs 'Read the columns here before writing SQL for unhcr_dataframe_query', giving a clear when-to-use and the alternative. It also states the deployment condition under which listing is unavailable and name becomes required.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
unhcr_dataframe_queryQuery staged dataframesARead-onlyIdempotentInspect
Run a single-statement SELECT against the dataframes staged by the unhcr_get_* tools. Check a dataframe’s columns with unhcr_dataframe_describe first. Read-only: writes, DDL, DROP, COPY, PRAGMA, ATTACH, external-file functions, and system catalogs (information_schema, pg_catalog, sqlite_master, duckdb_*) are rejected. Optional register_as saves the result as a new dataframe with a fresh TTL. Recompute rates from summed counts rather than averaging rate columns.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | One DuckDB SELECT against df_XXXXX_XXXXX tables, at most 20,000 characters — joins, aggregates, window functions, and CTEs work. SUM and COUNT results come back as JSON strings (BIGINT); CAST(… AS DOUBLE) for inline arithmetic. Staged tables add origin/asylum UNHCR and UN region columns for regional GROUP BY. | |
| preview | No | Rows to return inline when that should be fewer than the query materializes, e.g. a small sample while register_as keeps the whole result; a value above row_limit is treated as row_limit. Omit to return every row up to row_limit. | |
| row_limit | No | Most rows the query materializes (1–10000, default 1000). When more match, row_count_capped is true; use register_as to keep the full result. | |
| register_as | No | Save the result as a new dataframe under this name (df_ plus two groups of 5 uppercase letters or digits, e.g. df_ABCDE_12345) with a fresh TTL, to chain analyses. The name must not already be staged. The saved rows count toward the 1,000,000-row staging budget: the oldest other dataframes are evicted to make room, and a result larger than the budget is not saved. |
Output Schema
| Name | Required | Description |
|---|---|---|
| cap | No | The cap that bound: preview when lower than row_limit, otherwise row_limit. |
| rows | No | Result rows, one object per row keyed by column name, bounded by preview and row_limit. BIGINT values (SUM, COUNT) arrive as strings. |
| error | No | Present when the call failed. Absent on success. |
| shown | No | Rows returned inline. |
| notice | No | Guidance when the query returned no rows or rows were withheld. |
| columns | No | Column names in projection order. |
| evicted | No | Older dataframes dropped, oldest first, to keep the staged total within the 1,000,000-row budget once register_as saved this result; they can no longer be queried. Present only when any were evicted. |
| row_count | No | Rows the query produced, up to row_limit. When row_count_capped is true this is the cap, not the full size. |
| truncated | No | True when rows were withheld by a cap. |
| expires_at | No | ISO 8601 expiry of the new dataframe, when register_as was set. |
| attribution | No | Attribution UNHCR requires when these figures are reused. |
| registered_as | No | The new dataframe, when register_as was set. |
| row_count_capped | No | True when more rows matched than row_limit allowed through. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/openWorld, but the description goes well beyond them by enumerating exactly what is rejected (writes, DDL, DROP, COPY, PRAGMA, ATTACH, external-file functions, system catalogs) and by disclosing the eviction/TTL/staging-budget side effects of register_as. That is the kind of behavioral context an agent cannot infer from readOnlyHint=true.
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?
Front-loaded with the core action, then constraints, then the optional register_as behavior and the rate-column caveat. Every sentence carries information, though the denial list and budget details make it denser than strictly needed for scanning.
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?
An output schema exists, so return-shape explanation is unnecessary; the description covers the remaining gaps an agent needs — read-only enforcement, staging budget, eviction, TTL, and the recommended sequencing with unhcr_dataframe_describe.
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 description coverage is 100%, so the baseline is 3; the sql/preview/row_limit semantics are already fully spelled out in the schema. The description restates register_as's TTL behavior but adds no format, unit, or edge-case detail 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?
States a precise verb and resource ('Run a single-statement SELECT against the dataframes staged by the unhcr_get_* tools'), and distinguishes itself from the sibling fetchers and from unhcr_dataframe_describe by naming both roles. An agent can tell this is the query layer over staged data without opening a schema.
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?
Gives explicit prerequisites and routing: check a dataframe's columns with unhcr_dataframe_describe first, and use register_as to chain analyses. It also names the correct analytical practice for rate columns ('recompute rates from summed counts rather than averaging rate columns'), which is a when-to-do-what instruction, not just a description.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
unhcr_get_asylum_applicationsUNHCR asylum applicationsARead-onlyIdempotentInspect
Get asylum applications lodged per year (2000 to the latest year) by country of origin and/or asylum, split by default into application stage — new, repeat, appeal, and the combined stages some countries report — so new claims are not added to appeals of old ones. Counts given as cases are never added to counts of persons; each row states its unit. Decode stage, authority, and decision-level codes with unhcr_list_reference (topic asylum_codes).
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Rows returned inline, 1–500 (default 100). Sorting runs over the full result first; a larger result is staged as a dataframe where dataframes are enabled. | |
| stage | No | Stage the full result as a dataframe even when it fits inline, e.g. to join it with another unhcr_get_* result in unhcr_dataframe_query. Where dataframes are unavailable it is ignored, and the notice says so. | |
| asylum | No | Country of asylum — where people sought or hold protection; for returns, the country they returned from — as ISO3 codes (DEU, TUR), case-insensitive, same rules as origin. At most 50 codes. Omit to sum every asylum country into one row, or set expand to list each. | |
| expand | No | List every country, one row each, for a dimension that origin or asylum leaves unfiltered: origin, asylum, or both. Default none, where an unfiltered dimension is summed into one row. Expanding a dimension that origin or asylum already filters is rejected. | none |
| origin | No | Country of origin — where people fled from — as ISO3 codes (SYR, AFG), case-insensitive. ISO 3166 alpha-2 codes (SY) are rewritten to ISO3 and echoed in applied_scope. UNHCR's own codes (GFR) are rejected with the ISO3 to pass instead, and country names are rejected: resolve a name with unhcr_list_reference (topic countries, name_contains). Each listed code returns its own rows; codes are never summed together. At most 50 codes. Omit to sum every origin into one row, or set expand to list each. | |
| stages | No | Keep only these application stages before summing, case-insensitive: N new, R repeat, A appeal, NA new and appeal together, NR new and repeat together, FA first and appeal, J judiciary, BL backlog, SP subsidiary protection; V and RA appear in the data without a published definition. ["N"] gives new applications only, the basis of UNHCR's headline figure. Omit for every stage. | |
| sort_by | No | Order of the full result before the inline cut. year (default) orders by year, then origin ISO3, then asylum ISO3; applied orders largest first, nulls last. | year |
| year_to | No | Last year of the window. When omitted, the window runs to the latest published year (latest_year in the result). A year past the latest published year is clamped to it and the clamp is reported. | |
| split_by | No | Procedure dimensions kept as separate rows: authority (government, UNHCR, or joint), stage (new, repeat, appeal, …), decision_level. Every dimension left out is summed. Default ["stage"]; [] gives one total per year and scope for each unit. Unit is always kept separate. | |
| year_from | No | First year of the window. When omitted, the window starts at the dataset's first year. A year before the dataset's first year is clamped to it and the clamp is reported; a window entirely outside the dataset's years is rejected. |
Output Schema
| Name | Required | Description |
|---|---|---|
| cap | No | The limit that was applied. |
| rows | No | Inline rows, sorted, up to limit. |
| error | No | Present when the call failed. Absent on success. |
| shown | No | Rows returned inline. |
| notice | No | Guidance for this result: empty-result hints, year clamps, where the full set was staged, or how to narrow. |
| dataset | No | Present when the full result was staged as a dataframe; read its columns with unhcr_dataframe_describe, then query it with unhcr_dataframe_query. |
| measure | No | stock: people in a situation on 31 December; flow: events during the year. |
| complete | No | False when the server row cap stopped the upstream fetch; the notice says how many upstream rows were fetched and how to narrow. |
| truncated | No | True when rows were cut at limit. |
| data_notes | No | How to read the counts: stock or flow, rounding, the meaning of null, caveats. |
| total_rows | No | Rows in the full result before the inline cut. When complete is false, the result is built from only the upstream rows fetched before the cap, so rows and summed counts can fall short of the complete result. |
| attribution | No | Attribution UNHCR requires when these figures are reused. |
| latest_year | No | Newest year this dataset publishes. |
| applied_scope | No | The scope actually sent upstream, including clamped years and code rewrites. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, open-world, but the description goes further by explaining that counts of cases are never added to counts of persons, that each row states its unit, and that stage splitting by default prevents aggregation errors. It also mentions decoding with unhcr_list_reference. 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?
Two sentences, front-loaded with purpose and key behavioral caveats. No redundant wording, though slightly dense. Each sentence carries substantive information.
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 10 parameters (0 required), rich schema descriptions, and an output schema, the description is mostly complete. It covers the core operation, default behavior, unit distinction, and decoding. It could mention the split_by default more explicitly, but the schema covers that. No missing critical info for invocation.
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 description coverage is 100%, so the schema already documents all parameters thoroughly. The description adds a high-level rationale for default stage splitting and references the decode tool, but does not provide additional syntax or format details beyond 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?
States a specific verb and resource ('lodged per year ... by country of origin and/or asylum') with explicit temporal scope (2000 to latest year). It also distinguishes itself from unhcr_get_asylum_decisions by being about applications, not decisions.
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 implies usage (to get application counts by origin/asylum, to decode codes use unhcr_list_reference) and names a sibling for decoding, but it does not explicitly state when to use this tool over unhcr_get_asylum_decisions or other unhcr_get_* siblings. The 'so new claims are not added to appeals of old ones' hint is useful but not a direct usage guideline.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
unhcr_get_asylum_decisionsUNHCR asylum decisionsARead-onlyIdempotentInspect
Get asylum decisions per year (2000 to the latest year) by country of origin and/or asylum: recognized as refugees, complementary protection, rejected, and otherwise closed, with UNHCR's Refugee Recognition Rate and Total Protection Rate computed over substantive decisions (otherwise-closed cases excluded). By default all decision levels are summed and each row lists the levels it includes; appeal-stage decisions can concern people already decided at first instance, so split_by decision_level or filter decision_levels to FI for first-instance rates. Cases and persons are never added together.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Rows returned inline, 1–500 (default 100). Sorting runs over the full result first; a larger result is staged as a dataframe where dataframes are enabled. | |
| stage | No | Stage the full result as a dataframe even when it fits inline, e.g. to join it with another unhcr_get_* result in unhcr_dataframe_query. Where dataframes are unavailable it is ignored, and the notice says so. | |
| asylum | No | Country of asylum — where people sought or hold protection; for returns, the country they returned from — as ISO3 codes (DEU, TUR), case-insensitive, same rules as origin. At most 50 codes. Omit to sum every asylum country into one row, or set expand to list each. | |
| expand | No | List every country, one row each, for a dimension that origin or asylum leaves unfiltered: origin, asylum, or both. Default none, where an unfiltered dimension is summed into one row. Expanding a dimension that origin or asylum already filters is rejected. | none |
| origin | No | Country of origin — where people fled from — as ISO3 codes (SYR, AFG), case-insensitive. ISO 3166 alpha-2 codes (SY) are rewritten to ISO3 and echoed in applied_scope. UNHCR's own codes (GFR) are rejected with the ISO3 to pass instead, and country names are rejected: resolve a name with unhcr_list_reference (topic countries, name_contains). Each listed code returns its own rows; codes are never summed together. At most 50 codes. Omit to sum every origin into one row, or set expand to list each. | |
| sort_by | No | Order of the full result before the inline cut. year (default) orders by year, then origin ISO3, then asylum ISO3; a count field orders largest first, nulls last. Rates are not sortable, since rates on small rounded counts would crowd the top. | year |
| year_to | No | Last year of the window. When omitted, the window runs to the latest published year (latest_year in the result). A year past the latest published year is clamped to it and the clamp is reported. | |
| split_by | No | Procedure dimensions kept as separate rows: authority (government, UNHCR, or joint) and decision_level (first instance, administrative review, …). Every dimension left out is summed. Default [] sums all authorities and levels, as UNHCR does for its rates. Unit is always kept separate. | |
| year_from | No | First year of the window. When omitted, the window starts at the dataset's first year. A year before the dataset's first year is clamped to it and the clamp is reported; a window entirely outside the dataset's years is rejected. | |
| decision_levels | No | Keep only these decision levels before summing, case-insensitive: NA new applications, FI first instance, AR administrative review, RA repeat/reopened, IN US Citizenship and Immigration Services, EO US Executive Office for Immigration Review, JR judicial review, SP subsidiary protection, FA first instance and appeal, TP temporary protection, TA temporary asylum, BL backlog, TR temporary leave to remain, CA cantonal regulations (Switzerland). ["FI"] gives first-instance decisions. Omit for every level. |
Output Schema
| Name | Required | Description |
|---|---|---|
| cap | No | The limit that was applied. |
| rows | No | Inline rows, sorted, up to limit. |
| error | No | Present when the call failed. Absent on success. |
| shown | No | Rows returned inline. |
| notice | No | Guidance for this result: empty-result hints, year clamps, where the full set was staged, or how to narrow. |
| dataset | No | Present when the full result was staged as a dataframe; read its columns with unhcr_dataframe_describe, then query it with unhcr_dataframe_query. |
| measure | No | stock: people in a situation on 31 December; flow: events during the year. |
| complete | No | False when the server row cap stopped the upstream fetch; the notice says how many upstream rows were fetched and how to narrow. |
| truncated | No | True when rows were cut at limit. |
| data_notes | No | How to read the counts: stock or flow, rounding, the meaning of null, caveats. |
| total_rows | No | Rows in the full result before the inline cut. When complete is false, the result is built from only the upstream rows fetched before the cap, so rows and summed counts can fall short of the complete result. |
| attribution | No | Attribution UNHCR requires when these figures are reused. |
| latest_year | No | Newest year this dataset publishes. |
| applied_scope | No | The scope actually sent upstream, including clamped years and code rewrites. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover read-only/idempotent/openWorld, so the bar is lower, yet the description adds real behavioral context: rates are computed over substantive decisions with otherwise-closed cases excluded, defaults sum all decision levels, and 'cases and persons are never added together.' It does not touch staging/pagination behavior, which the schema handles.
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?
Front-loaded with what is retrieved before the caveats, and every clause carries information (rate definitions, appeal-stage caveat, cases-vs-persons rule). It is dense and reads as one long paragraph, but nothing is redundant with structured fields.
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 10-parameter analytical query tool with an output schema and full schema coverage, the description supplies the conceptual layer an agent needs: metric definition, exclusion rule, default aggregation, and the decision-level caveat. Nothing essential is missing.
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 description coverage is 100%, so the baseline is 3, but the description lifts key parameters out of the schema by explaining the consequence of defaults ('all decision levels are summed and each row lists the levels it includes') and linking split_by/decision_levels to first-instance rates. That is meaning beyond raw field documentation.
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?
States a specific verb and resource (asylum decisions per year by origin and/or asylum country) and enumerates the outcome categories returned, so an agent can distinguish it from unhcr_get_asylum_applications without opening the schema. The scope of what is retrieved is unambiguous.
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?
Gives clear operational guidance: default summing of decision levels, and the instruction to 'split_by decision_level or filter decision_levels to FI for first-instance rates' when appeal-stage cases would contaminate the result. It stops short of naming sibling tools (e.g., unhcr_get_asylum_applications) as alternatives or stating 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.
unhcr_get_demographicsUNHCR demographicsARead-onlyIdempotentInspect
Get UNHCR year-end stocks (2001 to the latest year) broken down by population type, sex, and age band (0–4, 5–11, 12–17, 18–59, 60+, unknown age), by country of origin and/or asylum. Coverage is partial: each row gives the share of its total that UNHCR could disaggregate by sex, and age bands are null where no breakdown exists. These totals come from a separate collection and need not match unhcr_get_population.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Rows returned inline, 1–500 (default 100). Sorting runs over the full result first; a larger result is staged as a dataframe where dataframes are enabled. | |
| stage | No | Stage the full result as a dataframe even when it fits inline, e.g. to join it with another unhcr_get_* result in unhcr_dataframe_query. Where dataframes are unavailable it is ignored, and the notice says so. | |
| asylum | No | Country of asylum — where people sought or hold protection; for returns, the country they returned from — as ISO3 codes (DEU, TUR), case-insensitive, same rules as origin. At most 50 codes. Omit to sum every asylum country into one row, or set expand to list each. | |
| expand | No | List every country, one row each, for a dimension that origin or asylum leaves unfiltered: origin, asylum, or both. Default none, where an unfiltered dimension is summed into one row. Expanding a dimension that origin or asylum already filters is rejected. | none |
| origin | No | Country of origin — where people fled from — as ISO3 codes (SYR, AFG), case-insensitive. ISO 3166 alpha-2 codes (SY) are rewritten to ISO3 and echoed in applied_scope. UNHCR's own codes (GFR) are rejected with the ISO3 to pass instead, and country names are rejected: resolve a name with unhcr_list_reference (topic countries, name_contains). Each listed code returns its own rows; codes are never summed together. At most 50 codes. Omit to sum every origin into one row, or set expand to list each. | |
| sort_by | No | Order of the full result before the inline cut. year (default) orders by year, then origin ISO3, then asylum ISO3, then population type; total orders largest first, nulls last. | year |
| year_to | No | Last year of the window. When omitted, the window runs to the latest published year (latest_year in the result). A year past the latest published year is clamped to it and the clamp is reported. | |
| year_from | No | First year of the window. When omitted, the window starts at the dataset's first year. A year before the dataset's first year is clamped to it and the clamp is reported; a window entirely outside the dataset's years is rejected. | |
| population_types | No | Keep only these population types, case-insensitive: REF refugees, ASY asylum-seekers, OIP other people in need of international protection, IDP internally displaced, STA stateless, OOC others of concern, HST host community, RET returned refugees, RDP returned IDPs. Decode them with unhcr_list_reference (topic population_types). Omit for every type. |
Output Schema
| Name | Required | Description |
|---|---|---|
| cap | No | The limit that was applied. |
| rows | No | Inline rows, sorted, up to limit. |
| error | No | Present when the call failed. Absent on success. |
| shown | No | Rows returned inline. |
| notice | No | Guidance for this result: empty-result hints, year clamps, where the full set was staged, or how to narrow. |
| dataset | No | Present when the full result was staged as a dataframe; read its columns with unhcr_dataframe_describe, then query it with unhcr_dataframe_query. |
| measure | No | stock: people in a situation on 31 December; flow: events during the year. |
| complete | No | False when the server row cap stopped the upstream fetch; the notice says how many upstream rows were fetched and how to narrow. |
| footnotes | No | UNHCR caveats matching rows of the full result, country-specific first; at most 20. |
| truncated | No | True when rows were cut at limit. |
| data_notes | No | How to read the counts: stock or flow, rounding, the meaning of null, caveats. |
| total_rows | No | Rows in the full result before the inline cut. When complete is false, the result is built from only the upstream rows fetched before the cap, so rows and summed counts can fall short of the complete result. |
| attribution | No | Attribution UNHCR requires when these figures are reused. |
| latest_year | No | Newest year this dataset publishes. |
| applied_scope | No | The scope actually sent upstream, including clamped years and code rewrites. |
| footnotes_total | No | Caveats matching the full result, before the cap. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover safety (readOnly, idempotent, openWorld), and the description adds genuinely non-schema behavior: partial coverage, per-row disaggregation share, null age bands, and divergence from unhcr_get_population. It stops short of describing output shape or staging behavior in narrative form, but the caveats delivered are substantive.
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, all front-loaded with the operation and its scope before the coverage caveats. Dense but every clause carries information; only marginal tightening is possible.
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?
An output schema exists, so return values need not be explained, and the description supplies the coverage caveat and sibling distinction an agent needs to interpret results correctly. It could still note the timeframe/limit interaction, but it is essentially complete for this complexity.
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 description coverage is 100%, so the schema already documents all nine parameters including code formats, clamping, and expand rules. The description adds essentially no parameter meaning beyond what the schema states, so the baseline 3 applies.
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 names a specific verb and resource (year-end stocks by population type, sex, age band) plus the dimensions of disaggregation and the source collection, and it explicitly contrasts itself with unhcr_get_population. An agent can distinguish it from siblings without opening schemas.
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?
It routes the agent to unhcr_get_population for consistent totals and notes that these numbers need not match, implying when to prefer each. However, it never states a positive when-to-use condition (e.g. 'use this when you need sex/age breakdowns'), so the guidance is contextual rather than explicit.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
unhcr_get_populationUNHCR displacement stocksARead-onlyIdempotentInspect
Get UNHCR year-end displacement stocks (1951 to the latest year) by country of origin and/or asylum: refugees, asylum-seekers, other people in need of international protection, IDPs, stateless people, others of concern, and host communities, plus refugees and IDPs who returned during the year. Stocks count people in a situation on 31 December, not arrivals. Palestine refugees under UNRWA's mandate and IDMC's conflict-IDP estimate are separate series shown beside each row. Set include_nowcast for UNHCR's current-year estimate by asylum country; for sex and age breakdowns, use unhcr_get_demographics.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Rows returned inline, 1–500 (default 100). Sorting runs over the full result first; a larger result is staged as a dataframe where dataframes are enabled. | |
| stage | No | Stage the full result as a dataframe even when it fits inline, e.g. to join it with another unhcr_get_* result in unhcr_dataframe_query. Where dataframes are unavailable it is ignored, and the notice says so. | |
| asylum | No | Country of asylum — where people sought or hold protection; for returns, the country they returned from — as ISO3 codes (DEU, TUR), case-insensitive, same rules as origin. At most 50 codes. Omit to sum every asylum country into one row, or set expand to list each. | |
| expand | No | List every country, one row each, for a dimension that origin or asylum leaves unfiltered: origin, asylum, or both. Default none, where an unfiltered dimension is summed into one row. Expanding a dimension that origin or asylum already filters is rejected. | none |
| origin | No | Country of origin — where people fled from — as ISO3 codes (SYR, AFG), case-insensitive. ISO 3166 alpha-2 codes (SY) are rewritten to ISO3 and echoed in applied_scope. UNHCR's own codes (GFR) are rejected with the ISO3 to pass instead, and country names are rejected: resolve a name with unhcr_list_reference (topic countries, name_contains). Each listed code returns its own rows; codes are never summed together. At most 50 codes. Omit to sum every origin into one row, or set expand to list each. | |
| sort_by | No | Order of the full result before the inline cut. year (default) orders by year, then origin ISO3, then asylum ISO3; a count field orders largest first, nulls last. | year |
| year_to | No | Last year of the window. When omitted, the window runs to the latest published year (latest_year in the result). A year past the latest published year is clamped to it and the clamp is reported. | |
| year_from | No | First year of the window. When omitted, the window starts at the dataset's first year. A year before the dataset's first year is clamped to it and the clamp is reported; a window entirely outside the dataset's years is rejected. | |
| include_nowcast | No | Append UNHCR's latest monthly estimate of refugees and asylum-seekers by asylum country: one current-year snapshot, independent of the year window. The nowcast has no origin dimension, so it is skipped when origin lists codes. When origin lists no codes, a window entirely after the latest published year returns the nowcast alone instead of failing. |
Output Schema
| Name | Required | Description |
|---|---|---|
| cap | No | The limit that was applied. |
| rows | No | Inline rows, sorted, up to limit. |
| error | No | Present when the call failed. Absent on success. |
| shown | No | Rows returned inline. |
| notice | No | Guidance for this result: empty-result hints, year clamps, where the full set was staged, or how to narrow. |
| dataset | No | Present when the full result was staged as a dataframe; read its columns with unhcr_dataframe_describe, then query it with unhcr_dataframe_query. |
| measure | No | stock: people in a situation on 31 December; flow: events during the year. |
| nowcast | No | Present when include_nowcast was set and origin lists no codes. |
| complete | No | False when the server row cap stopped the upstream fetch; the notice says how many upstream rows were fetched and how to narrow. |
| footnotes | No | UNHCR caveats matching rows of the full result, country-specific first; at most 20. |
| truncated | No | True when rows were cut at limit. |
| data_notes | No | How to read the counts: stock or flow, rounding, the meaning of null, caveats. |
| total_rows | No | Rows in the full result before the inline cut. When complete is false, the result is built from only the upstream rows fetched before the cap, so rows and summed counts can fall short of the complete result. |
| attribution | No | Attribution UNHCR requires when these figures are reused. |
| latest_year | No | Newest year this dataset publishes. |
| applied_scope | No | The scope actually sent upstream, including clamped years and code rewrites. |
| footnotes_total | No | Caveats matching the full result, before the cap. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish read-only, idempotent, open-world behavior, so the description is free to add substantive context: stock-vs-arrival semantics, the December 31 snapshot, treating UNRWA and IDMC figures as separate side-by-side series, and nowcast's lack of an origin dimension. It stops short of describing pagination/staging behavior or output shape, but the annotations and output schema cover much of that.
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?
Purpose and scope are front-loaded in the first sentence, with caveats (stock definition, side-by-side series, nowcast, demographics hand-off) following in a logical order. It is dense and somewhat long, but each sentence carries distinct information rather than padding.
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?
An output schema exists, so return values need no explanation. The description covers dimensions (origin/asylum), the stock concept, the special-cased series, the nowcast edge case, and the sibling to use for demographic breakdowns — enough for an agent to call it correctly without opening the schema.
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 schema already documents all nine parameters including enums, formats and clamping rules. The description largely restates the include_nowcast flag rather than adding syntax or format detail the schema lacks; baseline 3 applies when the schema does the heavy lifting.
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?
States a specific verb and resource (year-end displacement stocks by country of origin/asylum) plus the exact series included, which lets an agent separate it from siblings like unhcr_get_solutions and unhcr_get_demographics. It also disambiguates stocks from flows ('count people in a situation on 31 December, not arrivals').
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?
Gives explicit conditional guidance: set include_nowcast for the current-year estimate and use unhcr_get_demographics for sex/age breakdowns. It does not, however, state when to prefer this tool over unhcr_get_solutions or the asylum-applications/decisions tools, so routing across the remaining siblings is left to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
unhcr_get_solutionsUNHCR durable solutionsARead-onlyIdempotentInspect
Get durable solutions per year (1959 to the latest year) by country of origin and/or asylum: refugees who returned home, refugees resettled to a third country, refugees naturalised, and IDPs who returned. The asylum country means something different per column: the country refugees returned from, the country they were resettled to, the country that naturalised them; IDP returns sit on the origin country itself. Null means the figure was not collected for that country and year, not zero.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Rows returned inline, 1–500 (default 100). Sorting runs over the full result first; a larger result is staged as a dataframe where dataframes are enabled. | |
| stage | No | Stage the full result as a dataframe even when it fits inline, e.g. to join it with another unhcr_get_* result in unhcr_dataframe_query. Where dataframes are unavailable it is ignored, and the notice says so. | |
| asylum | No | Country of asylum — where people sought or hold protection; for returns, the country they returned from — as ISO3 codes (DEU, TUR), case-insensitive, same rules as origin. At most 50 codes. Omit to sum every asylum country into one row, or set expand to list each. | |
| expand | No | List every country, one row each, for a dimension that origin or asylum leaves unfiltered: origin, asylum, or both. Default none, where an unfiltered dimension is summed into one row. Expanding a dimension that origin or asylum already filters is rejected. | none |
| origin | No | Country of origin — where people fled from — as ISO3 codes (SYR, AFG), case-insensitive. ISO 3166 alpha-2 codes (SY) are rewritten to ISO3 and echoed in applied_scope. UNHCR's own codes (GFR) are rejected with the ISO3 to pass instead, and country names are rejected: resolve a name with unhcr_list_reference (topic countries, name_contains). Each listed code returns its own rows; codes are never summed together. At most 50 codes. Omit to sum every origin into one row, or set expand to list each. | |
| sort_by | No | Order of the full result before the inline cut. year (default) orders by year, then origin ISO3, then asylum ISO3; a count field orders largest first, nulls last. | year |
| year_to | No | Last year of the window. When omitted, the window runs to the latest published year (latest_year in the result). A year past the latest published year is clamped to it and the clamp is reported. | |
| year_from | No | First year of the window. When omitted, the window starts at the dataset's first year. A year before the dataset's first year is clamped to it and the clamp is reported; a window entirely outside the dataset's years is rejected. |
Output Schema
| Name | Required | Description |
|---|---|---|
| cap | No | The limit that was applied. |
| rows | No | Inline rows, sorted, up to limit. |
| error | No | Present when the call failed. Absent on success. |
| shown | No | Rows returned inline. |
| notice | No | Guidance for this result: empty-result hints, year clamps, where the full set was staged, or how to narrow. |
| dataset | No | Present when the full result was staged as a dataframe; read its columns with unhcr_dataframe_describe, then query it with unhcr_dataframe_query. |
| measure | No | stock: people in a situation on 31 December; flow: events during the year. |
| complete | No | False when the server row cap stopped the upstream fetch; the notice says how many upstream rows were fetched and how to narrow. |
| footnotes | No | UNHCR caveats matching rows of the full result, country-specific first; at most 20. |
| truncated | No | True when rows were cut at limit. |
| data_notes | No | How to read the counts: stock or flow, rounding, the meaning of null, caveats. |
| total_rows | No | Rows in the full result before the inline cut. When complete is false, the result is built from only the upstream rows fetched before the cap, so rows and summed counts can fall short of the complete result. |
| attribution | No | Attribution UNHCR requires when these figures are reused. |
| latest_year | No | Newest year this dataset publishes. |
| applied_scope | No | The scope actually sent upstream, including clamped years and code rewrites. |
| footnotes_total | No | Caveats matching the full result, before the cap. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the safety profile (readOnlyHint, idempotentHint, openWorldHint), so credit goes to the extra interpretive context the description supplies: that 'asylum' denotes a different country per measure (returned-from, resettled-to, naturalising country) while IDP returns sit on the origin country, and that null means 'not collected', not zero. These are non-obvious data behaviours that prevent real misinterpretation.
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?
Purpose leads, and all three sentences carry information that is not restated structurally — nothing is padding. The middle sentence about asylum meaning per column is dense and would read better as a short list, but the ordering is logical and front-loaded.
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?
With an output schema present, a fully documented 8-parameter schema and annotations covering read-only/idempotent behaviour, the description only needed to supply purpose, dimension semantics and null handling — all of which it does. An agent has everything required to call and interpret this tool 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?
Schema description coverage is 100%, so the baseline is 3; the description adds meaning beyond the schema by explaining the conceptual asymmetry of the origin/asylum dimensions and how they relate to the returned measures, which the schema's per-parameter text does not convey. It adds no detail on limit/stage/sort_by beyond what the schema already documents.
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?
States a specific verb and resource ('Get durable solutions per year') and immediately enumerates the four measures it returns (returned refugees, resettlement, naturalisation, IDP returns), which cleanly separates it from siblings like unhcr_get_population, unhcr_get_demographics and unhcr_get_asylum_applications. The year coverage (1959 to latest) and the origin/asylum slicing dimensions are explicit.
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?
Usage is only implied through content — the agent can infer this is the tool for durable-solutions figures, but there is no explicit when-to-use/when-not statement and no sibling is named as an alternative for population, demographics or asylum-applications data. The only routing hint (resolve names via unhcr_list_reference) lives in the schema, not the description.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
unhcr_list_referenceUNHCR reference vocabularyARead-onlyIdempotentInspect
Decode the vocabulary the unhcr_* tools take as input: countries (ISO3, ISO2, UNHCR code, names, UNHCR and UN regions), UNHCR’s regional bureaus, each dataset’s first and latest year, population-type definitions, and the asylum authority, stage, decision-level, and unit codes. Filter countries with name_contains to turn a country name into the ISO3 code that origin and asylum take.
| Name | Required | Description | Default |
|---|---|---|---|
| topic | Yes | countries: every queryable country with its codes, names, and regions. regions: UNHCR's regional bureaus. coverage: first and latest year of each dataset, plus the current nowcast month. population_types: what each population type counts and which output column carries it. asylum_codes: asylum authority, application stage, decision level, and unit codes. | |
| name_contains | No | For topic countries only: keep countries whose names, in UNHCR’s English spelling, contain every word given, ignoring case, accents, and punctuation, or whose ISO3, ISO2, or UNHCR code equals a word ("syria", "turkiye", "britain", "deu"). Words match every name variant UNHCR records, including short and formal names the output does not list. No fuzzy matching. |
Output Schema
| Name | Required | Description |
|---|---|---|
| error | No | Present when the call failed. Absent on success. |
| topic | No | The topic returned. |
| notice | No | Guidance when name_contains matched nothing or does not apply to the topic. |
| nowcast | No | Present for topic coverage: the single current-year estimate unhcr_get_population's include_nowcast returns. |
| regions | No | Present for topic regions. |
| coverage | No | Present for topic coverage. |
| countries | No | Present for topic countries, sorted by name. |
| totalCount | No | Countries returned, after any name_contains filter. |
| asylum_codes | No | Present for topic asylum_codes. |
| population_types | No | Present for topic population_types. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, and openWorldHint, so the safety profile is covered. The description adds content-scope context (what vocabulary is decoded) but no behavioral detail such as caching, latency, or result completeness beyond the structured fields.
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, front-loaded with the purpose and followed by the practical name_contains use case. Dense but every clause maps to a covered topic or usage instruction; no filler.
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?
An output schema exists so return-value detail is unnecessary, and the description covers the topic space and the code-conversion use case. Adequate for a reference-lookup tool, though it could note whether results are static or refreshed.
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 description coverage is 100%, and the nested enum descriptions already document each topic exhaustively. The description echoes the topic list and mentions name_contains' role, adding only a light usage hint over what the schema provides, so baseline 3 applies.
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?
States a specific verb ('Decode') and resource ('the vocabulary the unhcr_* tools take as input'), then enumerates exactly what it returns: countries/codes, regional bureaus, dataset year coverage, population types, and asylum codes. This clearly distinguishes it from the data-returning siblings like unhcr_get_population or unhcr_get_asylum_applications.
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?
Gives a concrete usage path: 'Filter countries with name_contains to turn a country name into the ISO3 code that origin and asylum take,' which tells the agent why it would call this before other tools. It stops short of explicitly stating when NOT to use it or naming a sibling as an alternative.
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.
8 tool updates
- First observed
unhcr_dataframe_describe - First observed
unhcr_dataframe_query - First observed
unhcr_get_asylum_applications - First observed
unhcr_get_asylum_decisions - First observed
unhcr_get_demographics - First observed
unhcr_get_population - First observed
unhcr_get_solutions - First observed
unhcr_list_reference
Related MCP Connectors
Query UN population data: fertility, mortality, migration, life expectancy for 298 countries.
Query US Census Bureau data: demographics, economics, and housing statistics.
Query IMF SDMX 3.0 macroeconomic dataflows — WEO, BOP, CPI, exchange rates, 190 countries.
Related MCP Servers
- AlicenseAqualityDmaintenanceProvides access to UNHCR refugee statistics through a standardized interface, allowing AI agents to query data by country of origin, country of asylum, and year.5MIT
- AlicenseNot gradedqualityBmaintenanceProvides a unified interface to access UNHCR's open data across statistics, RDF, and IATI MCP servers, enabling aggregated queries, cross-domain analytics, and dataset discovery.MIT
- AlicenseAqualityBmaintenanceEnables querying and analyzing humanitarian data, such as refugee statistics, through semantic tools like country comparisons, trend analysis, and report generation, using the UNHCR API.2154 npm1MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to query keyless UNHCR statistics on forcibly displaced and stateless persons, including refugees, asylum seekers, IDPs, and returnees, plus asylum applications and decisions, filtered by year, country of origin, or country of asylum, with a country-code lookup for resolving UNHCR codes. It can be reached through the hosted Pipeworx gateway or run locally over stdio.84 npmMIT
Glama MCP Gateway
Add one secure layer between your agents and this server.