The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Unhcr Refugees MCP Server listing page.
Query UNHCR refugee, IDP, and stateless populations, asylum decisions, returns, and resettlement via MCP. STDIO or Streamable HTTP.
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.
| 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.
unhcr_get_* call whose full result exceeds limit, or that sets stage: true, stages every row as a df_XXXXX_XXXXX table and returns its handle in dataset.unhcr_dataframe_describe lists the staged tables with their columns, source call, and expiry. Listing is off over HTTP with MCP_AUTH_MODE=none, so there describe each table by the name in dataset.unhcr_dataframe_query runs one read-only DuckDB SELECT across them; register_as saves the result as a new table for the next query.UNHCR_DATASET_TTL_SECONDS (24 hours by default). With UNHCR_DATAFRAME_DROP_ENABLED=true, unhcr_dataframe_drop removes 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 in evicted.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.
The five unhcr_get_* tools share one contract:
origin and asylum take 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 unless expand (origin, asylum, or both) lists every country. year_from / year_to default to the dataset's span and are clamped to it.sort_by, then cut to limit (1–500, default 100). Where dataframes are on, the full set of a larger result is staged, and stage: true stages a result that fits too.total_rows, complete (false when UNHCR_MAX_ROWS stopped the fetch), measure (stock or flow), applied_scope, latest_year, dataset when staged, data_notes, and attribution.unknown_country_code, invalid_year_window, year_out_of_coverage, conflicting_scope, and the retryable upstream_busy, which carries retryAfter.unhcr_list_reference tooltopic: countries, regions, coverage, population_types, or asylum_codes. With countries, name_contains keeps countries whose names contain every word given, or whose ISO3, ISO2, or UNHCR code equals one; there is no fuzzy matchingcoverage gives each dataset's first_year, latest_year, and measure, plus the month of the current nowcastdocumented: falseunhcr_get_population toolrefugees, asylum_seekers, oip, idps, stateless, ooc, and hst, plus returned_refugees and returned_idps, which are flows during the year; sort_by takes any of them or yearunrwa_refugees (Palestine refugees registered with UNRWA) and idmc_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 UNHCR footnotes come back too, up to 20, with footnotes_totalinclude_nowcast: true appends UNHCR's current-year monthly estimate of refugees and asylum-seekers by asylum country; it is skipped when origin lists codesunhcr_get_demographics toolpopulation_types filters to REF, ASY, OIP, IDP, STA, OOC, HST, RET, or RDP; sort_by takes year or totaltotal, fourteen sex × age bands (female_0_4 … female_60_plus, female_unknown_age, female_total, and the male_* twins), disaggregated, and sex_disaggregated_share (0–1). Bands are null where UNHCR has no breakdownunhcr_get_population; matching footnotes come back as thereunhcr_get_asylum_applications toolsplit_by names which of authority, stage, and decision_level stay separate rows (default ["stage"]; [] gives one total per year, scope, and unit). stages filters before summing, e.g. ["N"] for new applications; sort_by takes year or appliedauthorities, stages, and decision_levels codes summed into it, its unit (persons or cases), and applied. Cases are never added to personsunhcr_get_asylum_decisions toolsplit_by names which of authority and decision_level stay separate rows (default [], all summed, as UNHCR does for its rates). decision_levels filters before summing, e.g. ["FI"] for first instancerecognized, complementary_protection, rejected, otherwise_closed, total_decisions, substantive_decisions, and a unit, plus refugee_recognition_rate and total_protection_rate as percentages of substantive decisions. A rate is null when that denominator is 0 or nullsort_by takes year or a count (total_decisions, substantive_decisions, recognized, rejected); rates are not sortableunhcr_get_solutions toolreturned_refugees, resettlement, naturalisation, and returned_idps; sort_by takes any of them or year. Matching footnotes come back as with populationunhcr_dataframe_describe toolname (df_XXXXX_XXXXX); omit it to list every staged table, newest first. Over HTTP with MCP_AUTH_MODE=none listing is off and name is requiredsource_tool, query_params, created_at, expires_at, row_count, complete, providers, and column_schema. Staged tables add origin_unhcr_region, origin_unsd_region, asylum_unhcr_region, and asylum_unsd_region columns the inline rows omitunhcr_dataframe_query toolsql is one DuckDB SELECT of 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, and row_count_capped flags a capped result. preview returns fewer rows inline; register_as saves the result under a new df_XXXXX_XXXXX name with a fresh TTL. It counts toward the 1,000,000-row staging budget, and a result that alone exceeds it fails register_as_too_largeSUM and COUNT values come back as strings (BIGINT). attribution credits the third-party series of every table the SQL readsunhcr_dataframe_drop toolname (df_XXXXX_XXXXX); returns dropped: false when nothing matched, so repeating a call is safeUNHCR_DATAFRAME_DROP_ENABLED=true; otherwise the per-table TTL reclaims staged tablesBuilt 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:
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)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"-" returned as null rather than zero, cases never added to persons, recognition rates over UNHCR's substantive-decision denominatorattribution.providers credits them whenever they appear, since their own conditions may applyAgent-friendly output:
applied_scope echoes the codes, year window, and ISO2 rewrites actually sent, alongside latest_year, measure, complete, and attributiondata_notes say how to read each result (stock or flow, rounding, what null means), and matching UNHCR footnotes arrive with rows_matchedunknown_country_code suggests the ISO3 for a UNHCR code, year_out_of_coverage names the dataset's span, upstream_busy carries retryAfterAdd the following to your MCP client configuration file.
Or with npx (no Bun required):
Or with Docker:
For Streamable HTTP, set the transport and start the server:
| 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.
Build and run the production version:
Run checks and tests:
| 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/. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for logging, ctx.state for storagebuildToolDefinitions() in src/mcp-server/tools/definitions/index.tsIssues are welcome. Run checks and tests before submitting:
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.