@cyanheads/unhcr-refugees-mcp-server
MCP serverDev toolsQuery UNHCR refugee, IDP, and stateless populations, asylum decisions, returns, and resettlement.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the unhcr list reference tool from @cyanheads/unhcr-refugees-mcp-server
Install @cyanheads/unhcr-refugees-mcp-server
The server’s own address, for the clients that take one directly. Or connect ahel once and every client you use reads it from one address, with the account kept on ahel rather than in each client’s config.
Claude Code
claude mcp add --transport http --scope user cyanheads-unhcr-refugees-mcp-ser 'https://unhcr-refugees.caseyjhand.com/mcp'Run it once in your project, then open /mcp to approve any sign-in the server asks for.
Claude Desktop
https://unhcr-refugees.caseyjhand.com/mcpAdd a custom connector in Settings, paste this address, and approve the sign-in.
Cursor
cursor://anysphere.cursor-deeplink/mcp/install?name=cyanheads-unhcr-refugees-mcp-ser&config=eyJ1cmwiOiJodHRwczovL3VuaGNyLXJlZnVnZWVzLmNhc2V5amhhbmQuY29tL21jcCJ9Open the link and Cursor adds the server at that address.
ChatGPT
https://unhcr-refugees.caseyjhand.com/mcpIn Settings, enable Developer mode, create an MCP app, and paste this address. Your plan and workspace must allow custom apps.
Codex
codex mcp add cyanheads-unhcr-refugees-mcp-ser --url 'https://unhcr-refugees.caseyjhand.com/mcp'Run it once, then sign in with codex mcp login cyanheads-unhcr-refugees-mcp-ser if the server asks for an account.
From the project's README
As published by cyanheads/unhcr-refugees-mcp-server in README.md.
Public Hosted Server: https://unhcr-refugees.caseyjhand.com/mcp
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, a local Streamable HTTP server, or the public hosted endpoint above.
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(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 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,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 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.
Tools it offers (8)
What this server listed when ahel dialed its public endpoint in Oct 2026, with no key and no account of yours. The names are the server’s own.
unhcr_list_referenceunhcr_get_populationunhcr_get_demographicsunhcr_get_asylum_applicationsunhcr_get_asylum_decisionsunhcr_get_solutionsunhcr_dataframe_describeunhcr_dataframe_query
Advanced
- Delivery
- unhcr-refugees-mcp-server MCP server → your ahel connector (mcp.ahel.ai) → your AI.
- Item type
- mcp-server
- Key
io-github-cyanheads-unhcr-refugees-mcp-server- Source
- github.com/cyanheads/unhcr-refugees-mcp-server
- Hosted endpoint
https://unhcr-refugees.caseyjhand.com/mcp
github.com/cyanheads/unhcr-refugees-mcp-server
More in Dev tools
MCP server · adarsh4052
More in Dev tools0nMCP
MCP server · 0nork
More in Dev toolsairtreks-mcp
MCP server · airtreks
More in Dev toolssportwizzard
MCP server · adamruehle
More in Dev toolsdraw.io
MCP server · jgraph
More in Dev toolsbilig-workpaper
MCP server · proompteng
More in Dev tools