About unhcr-refugees-mcp-server
unhcr-refugees-mcp-server gives researchers and policy analysts access to UNHCR Refugee Data Finder statistics on displacement, asylum applications and decisions, returns, and resettlement. It supports country- and year-based queries, with staging and read-only SQL for large results.
Key features
- 01Query annual population and displacement data
- 02Retrieve asylum applications and decisions
- 03Query returns, resettlement, and naturalisation
- 04Explore demographics by sex and age
- 05Stage results and run read-only SQL
Use cases
- 01Compare refugee and IDP populations across countries
- 02Analyze asylum application volumes and decision outcomes
- 03Aggregate large statistical results with SQL
MCP support
As an MCP server, it exposes UNHCR statistics queries and dataset analysis to clients through tools and resources. It supports STDIO and Streamable HTTP; staged data can be queried with read-only DuckDB SQL.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://unhcr-refugees.caseyjhand.com/mcpclaude mcp add --transport http cyanheads-unhcr-refugees https://unhcr-refugees.caseyjhand.com/mcp{"mcpServers":{"cyanheads-unhcr-refugees":{"type":"http","url":"https://unhcr-refugees.caseyjhand.com/mcp"}}}Open in Cursor{"servers":{"cyanheads-unhcr-refugees":{"type":"http","url":"https://unhcr-refugees.caseyjhand.com/mcp"}}}Open in VS Code{"mcpServers":{"cyanheads-unhcr-refugees":{"type":"http","url":"https://unhcr-refugees.caseyjhand.com/mcp"}}}{"mcpServers":{"cyanheads-unhcr-refugees":{"serverUrl":"https://unhcr-refugees.caseyjhand.com/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
Tools
8| Parameter | Type | Required | Description |
|---|---|---|---|
| topic | enum | 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.countries | regions | coverage | population_types | asylum_codes |
| name_contains | string | 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. |
Details
From the project README@cyanheads/unhcr-refugees-mcp-server
Query UNHCR refugee, IDP, and stateless populations, asylum decisions, returns, and resettlement via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://unhcr-refugees.caseyjhand.com/mcp
Overview
Refugee and displacement statistics from the UNHCR Refugee Data Finder: refugee, asylum-seeker, IDP, and stateless populations since 1951, asylum applications and decisions since 2000, and returns and resettlement since 1959. Pull annual figures by country of origin and asylum, and run SQL over large results. No API key required.
Tools
| Tool | Description |
|---|---|
unhcr_list_reference | Decode countries (ISO3, ISO2, and UNHCR codes, names, regions), regional bureaus, dataset coverage years, population types, and asylum codes |
unhcr_get_population | Year-end displacement stocks by origin and/or asylum country from 1951, with UNRWA and IDMC series alongside and an optional current-year nowcast |
unhcr_get_demographics | Year-end stocks by population type, sex, and age band from 2001, with the share UNHCR could disaggregate |
unhcr_get_asylum_applications | Asylum applications lodged per year from 2000, split by application stage by default |
unhcr_get_asylum_decisions | Asylum decisions per year from 2000 by outcome, with the Refugee Recognition Rate and Total Protection Rate |
unhcr_get_solutions | Refugee returns, resettlement, naturalisation, and IDP returns per year from 1959 |
unhcr_dataframe_describe | Describe a staged dataframe by name, or list them all where listing is on, with provenance, expiry, completeness, and column schema |
unhcr_dataframe_query | Run one read-only SQL SELECT across staged dataframes, optionally saving the result as a new one |
unhcr_dataframe_drop | Drop a staged dataframe before its TTL — opt-in, absent from tools/list by default |
unhcr_dataframe_drop is registered only when UNHCR_DATAFRAME_DROP_ENABLED=true; the other eight tools are always advertised.
Dataframe workflow
- A
unhcr_get_*call whose full result exceedslimit, or that setsstage: true, stages every row as adf_XXXXX_XXXXXtable and returns its handle indataset. unhcr_dataframe_describelists the staged tables with their columns, source call, and expiry. Listing is off over HTTP withMCP_AUTH_MODE=none, so there describe each table by the name indataset.unhcr_dataframe_queryruns one read-only DuckDBSELECTacross them;register_assaves the result as a new table for the next query.- Tables expire after
UNHCR_DATASET_TTL_SECONDS(24 hours by default). WithUNHCR_DATAFRAME_DROP_ENABLED=true,unhcr_dataframe_dropremoves one sooner. Once a tenant's staged rows pass 1,000,000, the oldest tables are evicted first, and the call that pushed the total over names them inevicted.
Dataframes run on DuckDB's native binding, which the npm and Docker installs carry. The Claude Desktop .mcpb bundle ships without it, so there the data tools answer inline only. Set CANVAS_PROVIDER_TYPE=none to turn dataframes off anywhere.
Capability reference
The five unhcr_get_* tools share one contract:
- Scope:
originandasylumtake ISO3 codes, case-insensitive, as a string or a list, up to 50 each. ISO2 codes are rewritten to ISO3; UNHCR's own codes and country names are rejected. Each listed code returns its own rows, and an omitted dimension is summed into one row unlessexpand(origin,asylum, orboth) lists every country.year_from/year_todefault to the dataset's span and are clamped to it. - Rows: the full result is sorted by
sort_by, then cut tolimit(default 100). Requests from 1 to 1,000,000 are accepted; values above 500 clamp to 500, with requested and effective limits disclosed even for an empty or small result. Where dataframes are on, the full set of a larger result is staged, andstage: truestages a result that fits too. - Output:
total_rows,complete(false whenUNHCR_MAX_ROWSstopped the fetch),measure(stockorflow),applied_scope,latest_year,datasetwhen staged,data_notes, andattribution. - Errors:
unknown_country_code,invalid_year_window,year_out_of_coverage,conflicting_scope, and the retryableupstream_busy, which carriesretryAfter.
unhcr_list_reference tool
topic:countries,regions,coverage,population_types, orasylum_codes. Withcountries,name_containskeeps countries whose names contain every word given, or whose ISO3, ISO2, or UNHCR code equals one; there is no fuzzy matchingcoveragegives each dataset'sfirst_year,latest_year, andmeasure, plus the month of the currentnowcast- Countries, regions, and coverage come from the live API and are cached for 24 hours; population types and asylum codes are static, and asylum codes UNHCR's methodology does not define carry
documented: false
unhcr_get_population tool
- Year-end stocks
refugees,asylum_seekers,oip,idps,stateless,ooc, andhst, plusreturned_refugeesandreturned_idps, which are flows during the year;sort_bytakes any of them oryear unrwa_refugees(Palestine refugees registered with UNRWA) andidmc_conflict_idps(IDMC's conflict-IDP estimate) sit beside a row when that series has one and are never added into UNHCR's counts. Matching UNHCRfootnotescome back too, up to 20, withfootnotes_totalinclude_nowcast: trueappends UNHCR's current-year monthly estimate of refugees and asylum-seekers by asylum country; it is skipped whenoriginlists codes
unhcr_get_demographics tool
population_typesfilters toREF,ASY,OIP,IDP,STA,OOC,HST,RET, orRDP;sort_bytakesyearortotal- Each row is one population type:
total, fourteen sex × age bands (female_0_4…female_60_plus,female_unknown_age,female_total, and themale_*twins),disaggregated, andsex_disaggregated_share(0–1). Bands are null where UNHCR has no breakdown - Totals come from a separate collection and need not match
unhcr_get_population; matchingfootnotescome back as there
unhcr_get_asylum_applications tool
split_bynames which ofauthority,stage, anddecision_levelstay separate rows (default["stage"];[]gives one total per year, scope, and unit).stagesfilters before summing, e.g.["N"]for new applications;sort_bytakesyearorapplied- Each row lists the
authorities,stages, anddecision_levelscodes summed into it, itsunit(personsorcases), andapplied. Cases are never added to persons
unhcr_get_asylum_decisions tool
split_bynames which ofauthorityanddecision_levelstay separate rows (default[], all summed, as UNHCR does for its rates).decision_levelsfilters before summing, e.g.["FI"]for first instance- Each row carries
recognized,complementary_protection,rejected,otherwise_closed,total_decisions,substantive_decisions, and aunit, plusrefugee_recognition_rateandtotal_protection_rateas percentages of substantive decisions. A rate is null when that denominator is 0 or null sort_bytakesyearor a count (total_decisions,substantive_decisions,recognized,rejected); rates are not sortable
unhcr_get_solutions tool
- Flows
returned_refugees,resettlement,naturalisation, andreturned_idps;sort_bytakes any of them oryear. Matchingfootnotescome back as with population - 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
unhcr_dataframe_describe tool
- Optional
name(df_XXXXX_XXXXX); omit it to list every staged table, newest first. Over HTTP withMCP_AUTH_MODE=nonelisting is off andnameis required - Each entry gives
source_tool,query_params,created_at,expires_at,row_count,complete,incomplete_sources,providers, andcolumn_schema. Saved descendants retain original incomplete-source handles after their ancestors expire or are evicted. Staged tables addorigin_unhcr_region,origin_unsd_region,asylum_unhcr_region, andasylum_unsd_regioncolumns the inline rows omit
unhcr_dataframe_query tool
sqlis one DuckDBSELECTof at most 20,000 characters; joins, aggregates, window functions, and CTEs work. Writes, DDL,PRAGMA,ATTACH, file references, external-data functions, engine introspection (current_setting,current_schema,current_schemas,in_search_path), dynamic table lookup (query_table), and system catalogs are rejected with a typed reason.query()accepts only a single SQL string literal, checked by the same gaterow_limit(1–10,000, default 1,000) caps the rows the query materializes, androw_count_cappedflags a capped result.previewreturns fewer rows inline;register_assaves the result under a newdf_XXXXX_XXXXXname with a fresh TTL. It counts toward the 1,000,000-row staging budget, and a result that alone exceeds it failsregister_as_too_largeSUMandCOUNTvalues come back as strings (BIGINT).attributioncredits the third-party series of every table the SQL readspreviewaccepts 0–1,000,000 and clamps torow_limit, disclosing requested and effective values. Zero still returns no preview rows.completeandincomplete_sourcesdescribe upstream coverage independently of display truncation: aggregates, joins and saved descendants remain partial if an original source was partial- The serialized tool result, including both response surfaces and notices, stays within 65,536 UTF-8 bytes. Whole rows or oversized metadata are withheld identically; values are never clipped.
result_chunksidentifies a tenant-scoped snapshot of the materialized columns, rows and result metadata. It has the usual TTL and shares the staging budget;result_storage_unavailablemeans that snapshot could not be retained. A savedregister_astable remains available
For a result_chunks.name such as df_ABCDE_12345, read one chunk per call:
SELECT chunk_index,chunk FROM df_ABCDE_12345 WHERE chunk_index=0
Increment the index through result_chunks.chunk_count - 1. Base64-decode each chunk separately (at most 6,144 source bytes), concatenate the decoded bytes in index order, then decode UTF-8 and parse JSON. A byte boundary may split a Unicode character, so decode UTF-8 only after joining the bytes. The snapshot contains rows materialized up to row_limit, including rows hidden by preview; row_count_capped still means additional matching rows were not materialized. For the full registered result, query registered_as. Small scalar results use Markdown tables; values needing exact whitespace, control characters, nested structures or Markdown syntax use fenced JSON.
unhcr_dataframe_drop tool
name(df_XXXXX_XXXXX); returnsdropped: falsewhen nothing matched, so repeating a call is safe- Registered only when
UNHCR_DATAFRAME_DROP_ENABLED=true; otherwise the per-table TTL reclaims staged tables
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
UNHCR-specific:
- Keyless client for the API behind UNHCR's Refugee Data Finder (
api.unhcr.org/population/v1). The API ignores unknown parameters and lone year bounds rather than rejecting them, so the server sends only allowlisted parameters, always pairs the year bounds, and pins ISO3 country matching (cf_type=ISO) - Shared request queue: paced request starts (
UNHCR_REQUESTS_PER_SECOND), at most two in flight, a cooldown after a 429, identical requests collapsed into one, a response cache, and a 45-second budget per call - UNHCR's reading of the numbers: stocks and flows labelled as such, UNHCR's
"-"returned as null rather than zero, cases never added to persons, recognition rates over UNHCR's substantive-decision denominator - UNHCR rounds counts below 5 (below 10 for asylum decisions) to the nearest multiple of 5, so small values are approximate
- Every result carrying UNHCR figures cites the "UNHCR Refugee Population Statistics Database" (CC BY 4.0) with a link to UNHCR's Terms of Use for Datasets. UNRWA and IDMC figures are those providers' own series, carried through UNHCR's API;
attribution.providerscredits them whenever they appear, since their own conditions may apply
Agent-friendly output:
- Provenance on every data result:
applied_scopeechoes the codes, year window, and ISO2 rewrites actually sent, alongsidelatest_year,measure,complete, andattribution - Caveats as data:
data_notessay how to read each result (stock or flow, rounding, what null means), and matching UNHCRfootnotesarrive withrows_matched - Typed failures with recovery hints:
unknown_country_codesuggests the ISO3 for a UNHCR code,year_out_of_coveragenames the dataset's span,upstream_busycarriesretryAfter - Nothing trimmed silently: year clamps, empty-result hints, truncation, and where the full set was staged all arrive as a notice in the response
Getting started
Public Hosted Instance
A public instance is available at https://unhcr-refugees.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"unhcr-refugees-mcp-server": {
"type": "streamable-http",
"url": "https://unhcr-refugees.caseyjhand.com/mcp"
}
}
}
Every caller of the hosted instance shares one UNHCR request queue and one dataframe workspace, so unhcr_dataframe_describe takes a table's exact name there; listing is off.
Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"unhcr-refugees-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/unhcr-refugees-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"unhcr-refugees-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/unhcr-refugees-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"unhcr-refugees-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/unhcr-refugees-mcp-server:latest"]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- No API key or account: UNHCR's API is keyless, and every variable below is optional.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/unhcr-refugees-mcp-server.git
- Navigate into the directory:
cd unhcr-refugees-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# optional: every variable has a working default
Configuration
| Variable | Description | Default |
|---|---|---|
UNHCR_REQUESTS_PER_SECOND | Upstream request starts per second for the whole process (1–10). Every caller of an HTTP instance shares it. | 4 |
UNHCR_MAX_ROWS | Most upstream rows one tool call fetches (10,000–500,000). A larger result stops at the cap and reports complete: false. | 150000 |
UNHCR_CACHE_MAX_MB | Response-cache budget in MB; 0 disables it. Reference data (countries, regions, coverage, footnotes) has its own cache, always on. | 64 |
UNHCR_DATASET_TTL_SECONDS | Lifetime of a staged dataframe, in seconds (minimum 60). | 86400 |
UNHCR_DATAFRAME_DROP_ENABLED | Set true to register unhcr_dataframe_drop. | false |
CANVAS_PROVIDER_TYPE | Dataframe engine: duckdb, or none to turn dataframes off. | duckdb |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
LOGS_DIR | Directory for log files (Node.js only). | <app-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry. | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
-
Build and run the production version:
# One-time build bun run rebuild # Run the built server bun run start:http # or bun run start:stdio -
Run checks and tests:
bun run devcheck # Lints, formats, type-checks, and more bun run test # Runs the test suite bun run test:live # Opt-in: re-checks the upstream behaviors the client relies on against api.unhcr.org
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point: server instructions, tool registration, service setup. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools/definitions | Tool definitions (*.tool.ts) and the registration list that gates unhcr_dataframe_drop. |
src/mcp-server/tools/shared | Scope inputs, output schemas, result shaping, and markdown rendering shared by the data tools. |
src/services/unhcr | UNHCR API client — request builder, paced fetch, response cache, reference data, normalization, asylum aggregation, footnote matching. |
src/services/canvas-bridge | Adapter to the framework DataCanvas — dataframe naming, provenance, expiry, and the read-only SQL gate. |
tests/ | Unit tests mirroring the src/ structure, plus the opt-in live suite in tests/live/. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor logging,ctx.statefor storage - Register new tools in
buildToolDefinitions()insrc/mcp-server/tools/definitions/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
The data belongs to UNHCR and is published under CC BY 4.0 and UNHCR's Terms of Use for Datasets. This project is independent of UNHCR and not endorsed by it.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.