Skip to main content
Glama
cyanheads

@cyanheads/unhcr-refugees-mcp-server

by cyanheads

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework


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

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

  1. A 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.

  2. 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.

  3. unhcr_dataframe_query runs one read-only DuckDB SELECT across them; register_as saves the result as a new table for the next query.

  4. Tables expire after 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.

Related MCP server: @cyanheads/federal-reserve-mcp-server

Capability reference

The five unhcr_get_* tools share one contract:

  • Scope: 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.

  • Rows: the full result is sorted by 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.

  • Output: 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.

  • Errors: unknown_country_code, invalid_year_window, year_out_of_coverage, conflicting_scope, and the retryable upstream_busy, which carries retryAfter.


unhcr_list_reference tool

  • topic: 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 matching

  • coverage gives each dataset's first_year, latest_year, and measure, plus the month of the current nowcast

  • 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, and hst, plus returned_refugees and returned_idps, which are flows during the year; sort_by takes any of them or year

  • unrwa_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_total

  • include_nowcast: true appends UNHCR's current-year monthly estimate of refugees and asylum-seekers by asylum country; it is skipped when origin lists codes


unhcr_get_demographics tool

  • population_types filters to REF, ASY, OIP, IDP, STA, OOC, HST, RET, or RDP; sort_by takes year or total

  • Each row is one population type: total, 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 breakdown

  • Totals come from a separate collection and need not match unhcr_get_population; matching footnotes come back as there


unhcr_get_asylum_applications tool

  • split_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 applied

  • Each row lists the authorities, stages, and decision_levels codes summed into it, its unit (persons or cases), and applied. Cases are never added to persons


unhcr_get_asylum_decisions tool

  • split_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 instance

  • Each row carries recognized, 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 null

  • sort_by takes year or a count (total_decisions, substantive_decisions, recognized, rejected); rates are not sortable


unhcr_get_solutions tool

  • Flows returned_refugees, resettlement, naturalisation, and returned_idps; sort_by takes any of them or year. Matching footnotes come 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 with MCP_AUTH_MODE=none listing is off and name is required

  • Each entry gives source_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 omit


unhcr_dataframe_query tool

  • sql 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 reason

  • row_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_large

  • SUM and COUNT values come back as strings (BIGINT). attribution credits the third-party series of every table the SQL reads


unhcr_dataframe_drop tool

  • name (df_XXXXX_XXXXX); returns dropped: false when 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.providers credits them whenever they appear, since their own conditions may apply

Agent-friendly output:

  • Provenance on every data result: applied_scope echoes the codes, year window, and ISO2 rewrites actually sent, alongside latest_year, measure, complete, and attribution

  • Caveats as data: data_notes say how to read each result (stock or flow, rounding, what null means), and matching UNHCR footnotes arrive with rows_matched

  • Typed failures with recovery hints: unknown_country_code suggests the ISO3 for a UNHCR code, year_out_of_coverage names the dataset's span, upstream_busy carries retryAfter

  • 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

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

  1. Clone the repository:

git clone https://github.com/cyanheads/unhcr-refugees-mcp-server.git
  1. Navigate into the directory:

cd unhcr-refugees-mcp-server
  1. Install dependencies:

bun install
  1. 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/catch in tool logic

  • Use ctx.log for logging, ctx.state for storage

  • Register new tools in buildToolDefinitions() in src/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.

Related MCP Connectors

Related MCP Servers