About ourairports-mcp-server
An MCP server that provides offline access to the public-domain OurAirports dataset. It serves aviation reference data for travelers, researchers, and other users who need airport, runway, navaid, or frequency information.
Key features
- 01Resolve airports across five code types
- 02Search airports and filter runway records
- 03Find nearby airports and navaids by coordinates
- 04Retrieve airport records with runways and frequencies
- 05Browse country and region reference data
Use cases
- 01Identify an airport from an IATA, ICAO, GPS, or local code
- 02Find nearby airports or navigation aids for a location
- 03Filter runways by surface, dimensions, lighting, or airport area
MCP support
Exposes six aviation data tools and an `airport://{code}` resource to MCP clients. The bundled dataset enables offline use; stdio and Streamable HTTP transports are supported.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://ourairports.caseyjhand.com/mcpclaude mcp add --transport http cyanheads-ourairports https://ourairports.caseyjhand.com/mcp{"mcpServers":{"cyanheads-ourairports":{"type":"http","url":"https://ourairports.caseyjhand.com/mcp"}}}Open in Cursor{"servers":{"cyanheads-ourairports":{"type":"http","url":"https://ourairports.caseyjhand.com/mcp"}}}Open in VS Code{"mcpServers":{"cyanheads-ourairports":{"type":"http","url":"https://ourairports.caseyjhand.com/mcp"}}}{"mcpServers":{"cyanheads-ourairports":{"serverUrl":"https://ourairports.caseyjhand.com/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
Tools
6| Parameter | Type | Required | Description |
|---|---|---|---|
| type | enum | No | Restrict to one airport type: large_airport, medium_airport, small_airport, heliport, seaplane_base, balloonport, or closed.large_airport | medium_airport | small_airport | heliport | seaplane_base | balloonport | closed |
| limit | integer | No | Maximum airports to return (1–100). Defaults to OURAIRPORTS_DEFAULT_SEARCH_LIMIT (20). |
| query | string | No | Free-text search over airport name, municipality, and keywords. Tokens are AND-matched. Omit to browse purely by facets. |
| region | string | No | ISO 3166-2 region code filter (e.g. US-WA). Exact match, case-insensitive; surrounding whitespace is ignored. |
| country | string | No | ISO 3166-1 alpha-2 country code filter (e.g. US). Exact match, case-insensitive; surrounding whitespace is ignored. Discover codes with ourairports_list_countries. |
| include_closed | boolean | No | Include airports of type "closed". Off by default — closed airports pollute the live-flight grounding use case. |
Details
From the project README@cyanheads/ourairports-mcp-server
Resolve airport codes (IATA/ICAO/GPS/local), search airports, find the nearest by coordinate, and look up runways, navaids, and radio frequencies from the bundled public-domain OurAirports dataset via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://ourairports.caseyjhand.com/mcp
Overview
Airports, runways, navaids, and radio frequencies worldwide from the public-domain OurAirports dataset, bundled for offline use. Resolve an airport by any code, search by name or facets, and find the airports and navaids nearest a coordinate.
Tools
| Tool | Description |
|---|---|
ourairports_search_airports | Full-text and faceted search over the airport corpus by name, municipality, country, region, or type. Ranked summaries, closed airports excluded by default. |
ourairports_search_runways | Search runways across all airports by surface, length, width, and lighting, joined back to their airports and filtered by country, region, or airport type. One flat { airport, runway } row per matching runway. |
ourairports_get_airport | Full record for one airport resolved by any code (IATA/ICAO/GPS/local/ident), with its runways and radio frequencies inline. |
ourairports_find_airports | Airports within a radius of a coordinate, ranked nearest-first by great-circle distance, with distance and bearing. |
ourairports_find_navaids | Navigation aids (VOR, VOR-DME, DME, NDB, NDB-DME, TACAN, VORTAC) near a coordinate or serving a specific airport. |
ourairports_list_countries | Countries with airport counts and optional nested regions, filtered by country and/or continent. The lookup table for valid country/region filters on both search tools. |
Resources
| Resource | Description |
|---|---|
airport://{code} | Single airport record by any code (IATA/ICAO/GPS/local/ident), with runways and frequencies inline. |
All data is also reachable via ourairports_get_airport — tool-only clients lose nothing. Not exposed as a resource list (enumerating 85k airports is a dump, not a discovery aid); discovery is ourairports_search_airports.
Capability reference
ourairports_search_airports tool
- Free-text search over name, municipality, and keywords; tokens are AND-matched (word order and partial words handled)
- Faceted filters:
country(two letters),region(two letters followed by one or two hyphen-separated segments of 1–3 letters/digits), andtype. Codes are case-insensitive with surrounding whitespace ignored; literal""is unset, whitespace-only is invalid. Malformed codes fail argument validation; codes absent from the bundled tables raiseunknown_country/unknown_regionwith discovery guidance. Valid codes with no matching airports remain empty successes - Closed airports excluded by default; opt in with
include_closed - Results ranked operational/larger-airports-first, each with its full code set and coordinates for chaining into
ourairports_get_airport limitaccepts 1–100 rows and defaults toOURAIRPORTS_DEFAULT_SEARCH_LIMIT(20); truncation reports the total matched count, cap, and recovery guidance
ourairports_search_runways tool
- Airport facets (
country,region,type) narrow the airports first; runway facets (surface,min_length_ft,min_width_ft,lighted) then filter their runways - Country/region facets follow the same shape, empty-string, normalization, and bundled-membership rules as
ourairports_search_airports; valid empty selections remain successful surfaceis a case-insensitive substring match against the raw upstream surface string (no controlled vocabulary — a shorter fragment likeaspmatches ASP, ASPH, and Asphalt), not an exact code- Returns one flat
{ airport, runway }row per matching runway — an airport with three matching runways contributes three rows lengthFtandmin_length_ftare the full runway surface as OurAirports records it, including displaced thresholds and overruns — not usable takeoff or landing distance; each runway also carriesleDisplacedThresholdFt/heDisplacedThresholdFt(nullwhen none is recorded)- A runway whose length or width is unknown is excluded when the matching
min_*_ftfilter is set — never assumed to meet a threshold the data can't confirm - Closed airports and closed runways are both excluded unless
include_closed_airports/include_closed_runwaysis set limitaccepts 1–100 rows and defaults toOURAIRPORTS_DEFAULT_SEARCH_LIMIT(20); truncation reports the total matched count, cap, and recovery guidance
ourairports_get_airport tool
- Resolves a single
codecase-insensitively across all five identifier spaces (priority: ident → ICAO → IATA → GPS → local); surrounding whitespace is ignored - Runways and radio frequencies inline;
includetrims the response to a subset, and the output'sincludedfield distinguishes a relation omitted byincludefrom one that genuinely has no records - Echoes the airport's complete code set plus a
resolvedVia/resolutionNote; when other airports carry the same code, the note names each one (ident, country, code space) and says whether resolution priority or dataset row order picked this airport, so a wrong resolution is self-correcting — pass the other airport's ident - Runways carry full-surface
lengthFtplus per-end displaced thresholds (leDisplacedThresholdFt/heDisplacedThresholdFt) - Absent codes reported as
null; closed airports always resolve unknown_codeerror with a recovery hint when no identifier space matches
ourairports_find_airports tool
- Great-circle (haversine) ranking, nearest-first, each result with
distanceKmandbearingDeg(degrees true) from the query point radius_km(1–500, default 100);limit(1–50) defaults toOURAIRPORTS_DEFAULT_SEARCH_LIMIT, clamped to this tool's 50-row max; optionaltypefilter;include_closedopt-in- Coordinate in, ranked airports out — no geocoding; resolve place names to lat/lon upstream first
totalCountis the filtered in-radius count beforelimit; truncation reports the cap and recovery guidance (narrowradius_km, addtype, or raiselimit)- Empty-radius guidance suggesting a wider
radius_km
ourairports_find_navaids tool
- Coordinate mode:
latitude+longitude(+ optionalradius_km, 1–500, default 100) ranks navaids nearest-first with distance and bearing;limit(1–50) defaults toOURAIRPORTS_DEFAULT_SEARCH_LIMIT, clamped to 50 - Airport mode:
airport_codereturns the navaids serving that airport - Exactly one complete mode required — both modes, neither mode, or incomplete coordinates raise
mode_conflict; incomplete-coordinate messages name the missing counterpart frequencyKhzis the stored value for every type (a VOR on 114.5 MHz reads 114500);frequencyMhzcarries the VHF frequency of a VOR or VOR-DME and the paired VHF frequency of a DME, TACAN, or VORTAC, and isnullfor NDB and NDB-DME, which tune in kHz; an upstream-1placeholder reads asnull- Each navaid carries its service-volume metadata as recorded upstream:
usageType(HI, LO, BOTH, TERMINAL, RNAV),power,magneticVariationDeg(actual, positive east),slavedVariationDeg(the variation built into the radials), anddmeFrequencyKhz(the DME's paired VHF frequency) —nullwhere the dataset has no value totalCountis the match count beforelimitin both modes; truncation reports the cap and recovery guidance- Airport mode distinguishes "airport not found" (
unknown_codeerror) from "airport found but has no associated navaids" (empty list with a note) - Airport mode adds a
resolutionNotewhenairport_codeis also a code of other airports, naming them by ident — the same noteourairports_get_airportreturns
ourairports_list_countries tool
- Optional
countryandcontinent(AF, AN, AS, EU, NA, OC, SA) filters combine as AND; optionalinclude_regionsnests regions by their recorded country association (soKS-U-Abelongs toXK) - Use
{ "country": "US", "include_regions": true }for one country's regions. Unscopedinclude_regionsreturns the entire region table; unscoped and continent-only requests remain accepted countryuses the search tools' shape, normalization, and membership rules;""is unset, whitespace-only is invalid, and an absent code raisesunknown_country- Airport counts exclude closed airports
- Countries sorted by name; the lookup table for valid
country/regionfilters used byourairports_search_airportsandourairports_search_runways - A country/continent mismatch returns an empty success with a notice naming both filters;
totalCountcounts returned countries
airport://{code} resource
- Same record as
ourairports_get_airport, including the sameresolutionNote— runways and radio frequencies always inline, so there is noinclude/included coderesolves case-insensitively across all five identifier spaces (priority: ident → ICAO → IATA → GPS → local)unknown_codeerror with a recovery hint when no identifier space matches
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.
OurAirports-specific:
- Bundled, public-domain dataset baked into the package and Docker image — zero runtime API, no key, no rate limit, no upstream outage
- In-memory indices built once at startup: id maps, a priority-ordered unified code index, airport-ref joins for runways and frequencies, an ident-keyed navaid join, a flat
Float64Arrayof coordinates, country/region maps, and a tokenized text-search index - Brute-force haversine nearest-neighbour over the coordinate array — a few milliseconds across 85k airports, no spatial-index dependency
- CSVs parsed by header name, not column position, so an upstream column reorder can't silently misalign fields
- Airport search retains at most
limitranked candidates; runway search retains its firstlimitrows while both count every match
Agent-friendly output:
- Honest sparsity — absent upstream fields (no IATA, no elevation, null runway dimensions) surface as
null, never fabricated - Self-correcting resolution — every airport record echoes its full code set and a
resolvedVia/resolutionNotethat names every other airport sharing the code, by ident - Text output that matches the structured output — coordinates render at full precision, and upstream text is Markdown-escaped in
contentonly, so names likeGRASS&GRAVELdisplay as recorded - Truncation and empty-result disclosure — total counts, applied caps, and recovery guidance so callers can broaden, narrow, or re-query without parsing prose
Getting started
Public Hosted Instance
A public instance is available at https://ourairports.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP, with this client config:
{
"mcpServers": {
"ourairports-mcp-server": {
"type": "streamable-http",
"url": "https://ourairports.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"ourairports-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/ourairports-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"ourairports-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/ourairports-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"ourairports-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"ghcr.io/cyanheads/ourairports-mcp-server:latest"
]
}
}
}
No API key is required — the dataset ships with the package and the image.
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, account, or external service — all data is bundled.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/ourairports-mcp-server.git
- Navigate into the directory:
cd ourairports-mcp-server
- Install dependencies:
bun install
- Fetch and bundle the dataset (writes the six CSVs into
data/):
bun run build:data
Refreshing the data
The bundled snapshot is as fresh as the last build:data run (or, for the Docker image, the last build). To pull the latest daily drop from the OurAirports mirror, re-run bun run build:data and rebuild. The build validates country/region code shapes and airport references against the lookup tables before writing the fetched bundle; bun run build:data --check validates an existing bundle. To point at an existing local data drop without rebuilding, set OURAIRPORTS_DATA_DIR.
Configuration
| Variable | Description | Default |
|---|---|---|
OURAIRPORTS_DATA_DIR | Directory holding the six OurAirports CSV files. Overridable to point at a fresher local data drop. | Bundled data/ |
OURAIRPORTS_DEFAULT_SEARCH_LIMIT | Default result cap for the search/find tools when the caller omits limit (1–100). | 20 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for the HTTP server. | 3010 |
MCP_HTTP_ENDPOINT_PATH | HTTP endpoint path where the server is mounted. | /mcp |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_SESSION_MODE | HTTP session mode: stateful, stateless, or auto. This server uses stateless mode because no tool requests follow-up input. | stateless |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend (unused on the data path — the index is in-memory). | in-memory |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
-
Build and run:
# One-time data fetch + build bun run build:data bun run rebuild # Run the built server bun run start:stdio # or bun run start:http -
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t ourairports-mcp-server .
docker run --rm -i -e MCP_TRANSPORT_TYPE=stdio ourairports-mcp-server
The build stage runs bun run build:data so the dataset is fetched and baked into the image — the resulting container is fully self-contained and makes no network calls at runtime. The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/ourairports-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools/resources and loads the bundled index at setup(). |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts). Six read-only airport/runway/navaid tools. |
src/mcp-server/resources | Resource definitions. The airport://{code} record. |
src/services/airport-data | The bundled-data service — CSV parsing, in-memory indices, code resolution, search, and the haversine geo scan. |
scripts/build-data.ts | Build-time fetcher that bundles the six OurAirports CSVs into data/. |
framework-skills/ | Project development skills synced from @cyanheads/mcp-ts-core. |
tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md/AGENTS.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools and resources via the barrels in
src/mcp-server/*/definitions/index.ts - Surface upstream data as-is: report absent fields as
null, never fabricate missing values
Attribution
Airport, runway, navaid, and frequency data from OurAirports, dedicated to the public domain. Attribution is a courtesy, not a requirement. Source CSVs are published daily at davidmegginson.github.io/ourairports-data.
OurAirports is community-edited; the data is surfaced as-is and is not authoritative for real flight operations — treat it the way you would any crowd-sourced reference.
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.