unesco-heritage-mcp-server
by cyanheads
README.md
<div align="center">
<h1>@cyanheads/unesco-heritage-mcp-server</h1>
<p><b>Search UNESCO World Heritage sites, intangible heritage, biosphere reserves, and Global Geoparks via MCP. STDIO or Streamable HTTP.</b>
<div>9 Tools • 4 Resources</div>
</p>
</div>
<div align="center">
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/unesco-heritage-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/unesco-heritage-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
</div>
<div align="center">
[](https://github.com/cyanheads/unesco-heritage-mcp-server/releases/latest/download/unesco-heritage-mcp-server.mcpb) [](https://cursor.com/en/install-mcp?name=unesco-heritage-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvdW5lc2NvLWhlcml0YWdlLW1jcC1zZXJ2ZXIiXX0=) [](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22unesco-heritage-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Funesco-heritage-mcp-server%22%5D%7D)
[](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
</div>
<div align="center">
**Public Hosted Server:** [https://unesco-heritage.caseyjhand.com/mcp](https://unesco-heritage.caseyjhand.com/mcp)
</div>
---
## Overview
UNESCO's World Heritage List, Intangible Cultural Heritage lists, Biosphere Reserves, and Global Geoparks. Search and read full records, check the List of World Heritage in Danger, and find sites, reserves, or geoparks near any point. No API key required.
### Tools
| Tool | Description |
|:---|:---|
| `unesco_search_sites` | Search World Heritage sites by keyword, country, category, region, criteria, inscription years, Danger-list or transboundary status, or distance from a point |
| `unesco_get_site` | Fetch a site's full record: statement of Outstanding Universal Value, criteria with meanings, component parts, coordinates, and image credit |
| `unesco_search_intangible_heritage` | Search the three intangible heritage lists by keyword, country, list, inscription years, multinational status, or linked World Heritage site |
| `unesco_get_intangible_heritage_element` | Fetch an element's full record: description, list, countries, concept terms, linked sites, and image credit |
| `unesco_search_biosphere_reserves` | Search biosphere reserves by keyword, country, region, MAB regional network, designation years, transboundary or SIDS status, or distance from a point |
| `unesco_get_biosphere_reserve` | Fetch a reserve's full record: ecological and socio-economic profile, zoned areas and population, review years, and coordinates |
| `unesco_search_geoparks` | Search UNESCO Global Geoparks by keyword, country, designation years, transnational status, or distance from a point |
| `unesco_get_geopark` | Fetch a geopark's full record: introduction, description, account of sustaining local communities, recorded area and population, and coordinates |
| `unesco_list_reference` | Decode criteria, countries (name to ISO code), regions, intangible heritage lists, and MAB networks; report dataset coverage and data dates |
### Resources
| Resource | Description |
|:---|:---|
| `unesco://site/{id_no}` | One World Heritage site record |
| `unesco://intangible-heritage/{ich_ref}` | One intangible heritage element record |
| `unesco://biosphere-reserve/{mab_id}` | One biosphere reserve record |
| `unesco://geopark/{ugg_id}` | One UNESCO Global Geopark record |
Each resource mirrors a get tool. The server also serves its `unesco-heritage-guide` skill at `skill://unesco-heritage-guide/SKILL.md`: gates and chains for landmark lookups, empty results, `near` searches, and partial answers.
## Capability reference
### `unesco_search_sites` <sub>tool</sub>
- Filters: `query` (site names in six languages, component names, description, and statement of Outstanding Universal Value), `country` (ISO 3166-1 alpha-2 or alpha-3), `category`, `region`, `criteria` (every listed criterion required), `in_danger`, `transboundary`, `inscribed_from` / `inscribed_to`, and `near` (`latitude`, `longitude`, `radius_km` up to 5000, default 100), which matches a site through its representative point or any of its components
- Up to 50 sites per page (default 20; a larger `limit`, up to 10,000, is served as 50 and says so in `notice`), continued with `next_cursor`; `sort` takes `relevance`, `name`, `inscribed_newest`, `inscribed_oldest`, `area_largest`, `danger_listed_newest`, or `distance`; `include_description: false` leaves row descriptions out
- `in_danger: true` is the List of World Heritage in Danger; `totalCount` and `facets` (category, region, Danger status, criteria, top 10 countries) cover the whole match; rows carry `matched_in` when `query` is set (`name`, `component`, `description`, or `justification`, in rank order) and, when a query word the site's own names lack matched component names, up to 10 `matched_components` with `matched_components_total`; and with `near`, `distance_km` to the nearest of the site's points (with `nearest_component` when a component is nearer)
---
### `unesco_get_site` <sub>tool</sub>
- One site by `id_no`: a number, a digit string, or the site's whc.unesco.org page URL; `max_components` lists up to 1000 component parts (default 20; a larger value, up to 10,000, is served as 1000 and says so in `notice`)
- Description, statement of Outstanding Universal Value, `criteria[]` with meanings and `source: "recorded" | "inferred"`, States Parties with ISO codes, coordinates, area, `secondary_years`, Danger-list year, names in six languages, and the main `image` with its credit
- `description_language` (`en`, `fr`, `es`, `ru`, `ar`, `zh`) returns UNESCO's description in that language and names it in `description_language`; where UNESCO publishes none, the English one comes back as `en` with a `notice`. The statement of Outstanding Universal Value is English only
- `components_total` is UNESCO's count and `components_unparsed` the entries that could not be read; an unknown id fails as `site_not_found`
---
### `unesco_search_intangible_heritage` <sub>tool</sub>
- Filters: `query`, `country`, `list` (Representative List, Urgent Safeguarding List, Register of Good Safeguarding Practices; `RL` / `USL` / `Art18` accepted), `multinational`, `world_heritage_site` (an `id_no`), and `inscribed_from` / `inscribed_to`
- Up to 50 elements per page (default 20; a larger `limit`, up to 10,000, is served as 50 and says so in `notice`), continued with `next_cursor`; rows omit the description and carry primary `concepts` and linked `world_heritage_sites`
- `facets` cover list, multinational status, the top 10 countries, and the top 10 concept terms
---
### `unesco_get_intangible_heritage_element` <sub>tool</sub>
- One element by `ich_ref`: a number, a digit string, or the element's ich.unesco.org page URL
- Description, list, countries, inscription year, primary and secondary `concepts`, linked `world_heritage_sites`, UNESCO page, and the main `image` with its caption and credit; an unknown ref fails as `element_not_found`
- `description_language: "fr"` returns UNESCO's French description (English and French are the only languages UNESCO publishes), with the same English fallback and `notice` as `unesco_get_site`
---
### `unesco_search_biosphere_reserves` <sub>tool</sub>
- Filters: `query` (name, introduction, and ecological or socio-economic text; there is no biome field, so search habitat words), `country`, `region`, `regional_network` (name or acronym), `transboundary`, `sids`, `designated_from` / `designated_to`, and `near`
- Up to 50 reserves per page (default 20; a larger `limit`, up to 10,000, is served as 50 and says so in `notice`), continued with `next_cursor`; `sort` takes `relevance`, `name`, `designated_newest`, `designated_oldest`, `area_largest`, or `distance`; `include_description: false` leaves row introductions out
- A transboundary reserve appears once per participating country, each with its own `mab_id`; `facets` cover region, regional network, transboundary and SIDS status, and the top 10 countries
---
### `unesco_get_biosphere_reserve` <sub>tool</sub>
- One reserve by `mab_id`, matched without regard to case or accents
- Introduction, ecological and socio-economic characteristics, terrestrial and marine area by zone, population by zone, designation, extension, renaming, and periodic-review years, coordinates, website, and UNESCO page
- Areas (hectares) and populations pass through as recorded: zone sums can differ from totals, and a population of `0` can mean none or unreported; an unknown id fails as `biosphere_reserve_not_found`
---
### `unesco_search_geoparks` <sub>tool</sub>
- Filters: `query` (name, introduction, description, and community account; search landform words such as volcanic or karst), `country`, `transnational`, `designated_from` / `designated_to`, and `near`, measured to each geopark's one point
- Up to 50 geoparks per page (default 20; a larger `limit`, up to 10,000, is served as 50 and says so in `notice`), continued with `next_cursor`; `sort` takes `relevance`, `name`, `designated_newest`, `designated_oldest`, `area_largest`, or `distance`; `include_description: false` leaves row introductions out
- A transnational geopark is one row listing each of its countries; `facets` cover transnational status and the top 10 countries; a year bound whose range starts at or before 2015 adds a `notice` that geoparks recognized before the designation existed are dated 2015
---
### `unesco_get_geopark` <sub>tool</sub>
- One geopark by `ugg_id` (such as `EUFR10`), in any case
- Countries, designation year, transnational status, area and population as recorded (`population` is absent when UNESCO records none), coordinates, introduction, description, the account of sustaining local communities, website, and UNESCO page; an unknown id fails as `geopark_not_found`
---
### `unesco_list_reference` <sub>tool</sub>
- `topic`: `criteria`, `countries`, `regions`, `intangible_lists`, `biosphere_networks`, or `datasets`
- `filter` keeps matching rows; on `countries`, a country name, an ISO code, or a common former name returns the codes every `country` input accepts, with the country's count of sites, intangible elements, reserves, and geoparks
- `datasets` reports each dataset's record count, `data_as_of`, license, attribution line, and coverage notes
- When a dataset cannot load, `countries`, `regions`, and `datasets` answer from the others: its counts are absent (never `0`), its `datasets` row carries `available: false` and `retryable`, and the notice names it
---
### `unesco://site/{id_no}` <sub>resource</sub>
- The `unesco_get_site` record with up to 20 components (`components_total` carries the full count) and the English description, plus `sources`, as `application/json`; `id_no` comes from `unesco_search_sites`
---
### `unesco://intangible-heritage/{ich_ref}` <sub>resource</sub>
- The `unesco_get_intangible_heritage_element` record with the English description, plus `sources`, as `application/json`; `ich_ref` comes from `unesco_search_intangible_heritage`
---
### `unesco://biosphere-reserve/{mab_id}` <sub>resource</sub>
- The `unesco_get_biosphere_reserve` record plus `sources`, as `application/json`; `mab_id` comes from `unesco_search_biosphere_reserves`, percent-encoded when it holds non-ASCII letters
---
### `unesco://geopark/{ugg_id}` <sub>resource</sub>
- The `unesco_get_geopark` record plus `sources`, as `application/json`; `ugg_id` comes from `unesco_search_geoparks`
## Features
Built on [`@cyanheads/mcp-ts-core`](https://github.com/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.
UNESCO-specific:
- Datasets `whc001` (World Heritage List), `ich001` (Intangible Heritage List), `mab001` (Man and the Biosphere Programme), and `eg0001` (UNESCO Global Geoparks) each load on first use as an in-memory snapshot (two upstream requests) and refresh every 24 hours; searching, facets, and distance run locally, and a failed refresh keeps serving the previous snapshot
- Upstream traffic is paced at two concurrent requests and at most 200 a day, with a cooldown after a 429 that honors `Retry-After`
- Criterion (vi), which UNESCO's criteria fields omit, is inferred from each site's statement of Outstanding Universal Value and marked as inferred wherever it appears
- Country inputs take ISO 3166-1 alpha-2 or alpha-3 codes in any case and match every transboundary site, multinational element, or transnational geopark a country takes part in; site and element ids also accept their UNESCO page URLs
Agent-friendly output:
- Attribution on every response: `sources` names each dataset with its `data_as_of` date, license, and credit line
- Search results report the whole match: `totalCount`, `facets`, and an `applied_filters` echo of the filters and sort the server ran
- Keyword matching is word-prefix with every word required (up to 16 distinct words), and `matched_in` says which field tier matched; site searches also match component names, so a landmark that is part of a serial site finds that site and the components that matched; a zero-hit `notice` names the filter whose removal would match the most records
- Typed errors (`unknown_country`, `invalid_year_range`, `sort_needs_input`, `cursor_mismatch`, `invalid_cursor`, `*_not_found`, `snapshot_unavailable`) carry a recovery hint naming the next call; `snapshot_unavailable` is `retryable` with `retryAfter` when the Data Hub is unreachable, and not retryable when it rejects the request or serves data the server cannot use
## Data and licensing
All four datasets come from the [UNESCO Data Hub](https://data.unesco.org) and are licensed [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). Credit UNESCO when you reuse the data; every response's `sources` block carries a ready-made credit line. Under ShareAlike, adapted data must be shared under the same license.
Images are not covered by that license. Each World Heritage and intangible heritage image keeps its own copyright, and its holder and photographer travel with the image record. The server returns image links but never fetches or proxies them.
This server is an independent project and is not affiliated with or endorsed by UNESCO.
## Getting started
### Public Hosted Instance
A public instance is available at `https://unesco-heritage.caseyjhand.com/mcp` — no installation required. Point any MCP client at it via Streamable HTTP:
```json
{
"mcpServers": {
"unesco-heritage-mcp-server": {
"type": "streamable-http",
"url": "https://unesco-heritage.caseyjhand.com/mcp"
}
}
}
```
### Self-Hosted / Local
Add the following to your MCP client configuration file.
```json
{
"mcpServers": {
"unesco-heritage-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/unesco-heritage-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
```
Or with npx (no Bun required):
```json
{
"mcpServers": {
"unesco-heritage-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/unesco-heritage-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
```
Or with Docker:
```json
{
"mcpServers": {
"unesco-heritage-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/unesco-heritage-mcp-server:latest"]
}
}
}
```
For Streamable HTTP, set the transport and start the server:
```sh
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
```
### Prerequisites
- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
- No API key or account: the UNESCO Data Hub is open.
### Installation
1. **Clone the repository:**
```sh
git clone https://github.com/cyanheads/unesco-heritage-mcp-server.git
```
2. **Navigate into the directory:**
```sh
cd unesco-heritage-mcp-server
```
3. **Install dependencies:**
```sh
bun install
```
4. **Configure environment (optional):**
```sh
cp .env.example .env
# edit .env to change the transport, port, or log level
```
## Configuration
The server has no settings of its own; these framework variables apply.
| Variable | Description | Default |
|:---|:---|:---|
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
| `MCP_HTTP_PORT` | HTTP server port. | `3010` |
| `MCP_HTTP_HOST` | HTTP server host. | `127.0.0.1` |
| `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto`. `.env.example` and the Docker image set `stateless`. | `auto` |
| `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` |
| `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |
See [`.env.example`](./.env.example) for the common framework overrides.
## Running the server
### Local development
- **Build and run the production version**:
```sh
# One-time build
bun run rebuild
# Run the built server
bun run start:http
# or
bun run start:stdio
```
- **Run checks and tests**:
```sh
bun run devcheck # Lints, formats, type-checks, and more
bun run test # Runs the test suite
```
## Project structure
| Directory | Purpose |
|:---|:---|
| `src/index.ts` | `createApp()` entry point: registers the tools and resources, serves the server instructions from `src/mcp-server/instructions.ts`, and starts and stops the service. |
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). |
| `src/mcp-server/resources` | One record resource per dataset. |
| `skills/` | The published `unesco-heritage-guide` skill, served over MCP and shipped in the npm package, the Docker image, plugin installs, and the `.mcpb` bundle. |
| `src/mcp-server/shared` | Input schemas and normalizers, enrichment fields, and markdown helpers shared by the tools and resources. |
| `src/services/unesco-datahub` | UNESCO Data Hub service: snapshot loading and refresh, row validation and repair, search and facets, the ISO 3166 table, and vocabularies. |
| `tests/` | Unit tests over synthetic fixtures, mirroring the `src/` structure. |
## Development guide
See [`CLAUDE.md`](./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 and `ctx.enrich` for attribution, totals, and notices
- Register new tools and resources in the `createApp()` arrays in `src/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:
```sh
bun run devcheck
bun run test
```
## License
This project is licensed under the Apache 2.0 License. See the [LICENSE](./LICENSE) file for details.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues