@cyanheads/unhcr-refugees-mcp-server
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., "@@cyanheads/unhcr-refugees-mcp-serverWhat was the refugee population in Syria in 2022?"
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.
Overview
Displacement statistics from the keyless UNHCR Refugee Data Finder API: refugee, asylum-seeker, IDP, and stateless populations by country of origin and asylum from 1951, asylum applications and decisions from 2000, and returns, resettlement, and naturalisation from 1959. Resolve country names to ISO3 codes, pull annual figures with notes on how to read them, and run SQL over results too large to return inline. Runs as a stdio process or a local Streamable HTTP server.
Tools
Tool | Description |
| Decode countries (ISO3, ISO2, and UNHCR codes, names, regions), regional bureaus, dataset coverage years, population types, and asylum codes |
| Year-end displacement stocks by origin and/or asylum country from 1951, with UNRWA and IDMC series alongside and an optional current-year nowcast |
| Year-end stocks by population type, sex, and age band from 2001, with the share UNHCR could disaggregate |
| Asylum applications lodged per year from 2000, split by application stage by default |
| Asylum decisions per year from 2000 by outcome, with the Refugee Recognition Rate and Total Protection Rate |
| Refugee returns, resettlement, naturalisation, and IDP returns per year from 1959 |
| Describe a staged dataframe by name, or list them all where listing is on, with provenance, expiry, completeness, and column schema |
| Run one read-only SQL |
| Drop a staged dataframe before its TTL — opt-in, absent from |
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.
Related MCP server: @cyanheads/federal-reserve-mcp-server
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(1–500, default 100). 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 currentnowcastCountries, 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 oryearunrwa_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_bytakesyearortotalEach 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 breakdownTotals 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_bytakesyearorappliedEach 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 instanceEach 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 nullsort_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 populationThe 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 requiredEach entry gives
source_tool,query_params,created_at,expires_at,row_count,complete,providers, andcolumn_schema. 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-reading functions, and system catalogs are rejected with a typed reasonrow_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 reads
unhcr_dataframe_drop tool
name(df_XXXXX_XXXXX); returnsdropped: falsewhen nothing matched, so repeating a call is safeRegistered 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 callUNHCR'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 denominatorUNHCR 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, andattributionCaveats as data:
data_notessay how to read each result (stock or flow, rounding, what null means), and matching UNHCRfootnotesarrive withrows_matchedTyped failures with recovery hints:
unknown_country_codesuggests the ISO3 for a UNHCR code,year_out_of_coveragenames the dataset's span,upstream_busycarriesretryAfterNothing 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
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/mcpPrerequisites
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.gitNavigate into the directory:
cd unhcr-refugees-mcp-serverInstall dependencies:
bun installConfigure environment:
cp .env.example .env
# optional: every variable has a working defaultConfiguration
Variable | Description | Default |
| Upstream request starts per second for the whole process (1–10). Every caller of an HTTP instance shares it. |
|
| Most upstream rows one tool call fetches (10,000–500,000). A larger result stops at the cap and reports |
|
| Response-cache budget in MB; |
|
| Lifetime of a staged dataframe, in seconds (minimum 60). |
|
| Set |
|
| Dataframe engine: |
|
| Transport: |
|
| HTTP server port. |
|
| HTTP session mode: |
|
| Authentication: |
|
| Log level ( |
|
| Directory for log files (Node.js only). |
|
| Storage backend: |
|
| Enable OpenTelemetry. |
|
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:stdioRun 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 |
|
|
| Server-specific environment variable parsing and validation with Zod. |
| Tool definitions ( |
| Scope inputs, output schemas, result shaping, and markdown rendering shared by the data tools. |
| UNHCR API client — request builder, paced fetch, response cache, reference data, normalization, asylum aggregation, footnote matching. |
| Adapter to the framework DataCanvas — dataframe naming, provenance, expiry, and the read-only SQL gate. |
| Unit tests mirroring the |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor logging,ctx.statefor storageRegister new tools in
buildToolDefinitions()insrc/mcp-server/tools/definitions/index.tsWrap 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 testLicense
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.
This server cannot be deployed
Maintenance
Related MCP Connectors
Query SEC EDGAR filings, XBRL financials, and company data through MCP. STDIO & Streamable HTTP.
UN FAOSTAT global food & agriculture statistics over a local SQLite mirror, via MCP.
Query, join, profile, clean and convert CSV/JSON/Parquet with server-side DuckDB over MCP.
Query U.S. Census Bureau data, variables, and geography via MCP.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceAccess FEC campaign finance data through MCP. Query data about candidates, money trails, and election filings. STDIO & Streamable HTTP.552 npm2Apache 2.0
- AlicenseNot gradedqualityAmaintenanceSearch and fetch ~800K Federal Reserve economic time-series from the FRED API via MCP, with STDIO or Streamable HTTP transport.297 npm1Apache 2.0
- AlicenseNot gradedqualityAmaintenanceEnables querying stops, routes, real-time arrivals, vehicle positions, and schedules from OneBusAway transit APIs via MCP, supporting STDIO or Streamable HTTP.265 npm1Apache 2.0
- AlicenseNot gradedqualityAmaintenanceExposes the FBI Crime Data Explorer API — crime estimates, agency offense rates, and LEOKA officer safety data via MCP. Supports STDIO or Streamable HTTP transport.93 npm1Apache 2.0