Offline global aviation reference: airports, runways, navaids, frequencies from OurAirports.
One address for any MCP client. No sign-in needed on this server.
6 read
Grouped by tool name. At most: only reads or looks up information.
Odel charges nothing per call. The publisher may charge for its own service.
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
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.
| 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. |
| 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.
ourairports_search_airports toolcountry (two letters), region (two letters followed by one or two hyphen-separated segments of 1–3 letters/digits), and type. 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 raise unknown_country / unknown_region with discovery guidance. Valid codes with no matching airports remain empty successesinclude_closedourairports_get_airportlimit accepts 1–100 rows and defaults to OURAIRPORTS_DEFAULT_SEARCH_LIMIT (20); truncation reports the total matched count, cap, and recovery guidanceourairports_search_runways toolcountry, region, type) narrow the airports first; runway facets (surface, min_length_ft, min_width_ft, lighted) then filter their runwaysourairports_search_airports; valid empty selections remain successfulsurface is a case-insensitive substring match against the raw upstream surface string (no controlled vocabulary — a shorter fragment like asp matches ASP, ASPH, and Asphalt), not an exact code{ airport, runway } row per matching runway — an airport with three matching runways contributes three rowslengthFt and min_length_ft are the full runway surface as OurAirports records it, including displaced thresholds and overruns — not usable takeoff or landing distance; each runway also carries leDisplacedThresholdFt / heDisplacedThresholdFt (null when none is recorded)min_*_ft filter is set — never assumed to meet a threshold the data can't confirminclude_closed_airports / include_closed_runways is setlimit accepts 1–100 rows and defaults to OURAIRPORTS_DEFAULT_SEARCH_LIMIT (20); truncation reports the total matched count, cap, and recovery guidanceourairports_get_airport toolcode case-insensitively across all five identifier spaces (priority: ident → ICAO → IATA → GPS → local); surrounding whitespace is ignoredinclude trims the response to a subset, and the output's included field distinguishes a relation omitted by include from one that genuinely has no recordsresolvedVia / 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 identlengthFt plus per-end displaced thresholds (leDisplacedThresholdFt / heDisplacedThresholdFt)null; closed airports always resolveunknown_code error with a recovery hint when no identifier space matchesourairports_find_airports tooldistanceKm and bearingDeg (degrees true) from the query pointradius_km (1–500, default 100); limit (1–50) defaults to OURAIRPORTS_DEFAULT_SEARCH_LIMIT, clamped to this tool's 50-row max; optional type filter; include_closed opt-intotalCount is the filtered in-radius count before limit; truncation reports the cap and recovery guidance (narrow radius_km, add type, or raise limit)radius_kmourairports_find_navaids toollatitude + longitude (+ optional radius_km, 1–500, default 100) ranks navaids nearest-first with distance and bearing; limit (1–50) defaults to OURAIRPORTS_DEFAULT_SEARCH_LIMIT, clamped to 50airport_code returns the navaids serving that airportmode_conflict; incomplete-coordinate messages name the missing counterpartfrequencyKhz is the stored value for every type (a VOR on 114.5 MHz reads 114500); frequencyMhz carries the VHF frequency of a VOR or VOR-DME and the paired VHF frequency of a DME, TACAN, or VORTAC, and is null for NDB and NDB-DME, which tune in kHz; an upstream -1 placeholder reads as nullusageType (HI, LO, BOTH, TERMINAL, RNAV), power, magneticVariationDeg (actual, positive east), slavedVariationDeg (the variation built into the radials), and dmeFrequencyKhz (the DME's paired VHF frequency) — null where the dataset has no valuetotalCount is the match count before limit in both modes; truncation reports the cap and recovery guidanceunknown_code error) from "airport found but has no associated navaids" (empty list with a note)resolutionNote when airport_code is also a code of other airports, naming them by ident — the same note ourairports_get_airport returnsourairports_list_countries toolcountry and continent (AF, AN, AS, EU, NA, OC, SA) filters combine as AND; optional include_regions nests regions by their recorded country association (so KS-U-A belongs to XK){ "country": "US", "include_regions": true } for one country's regions. Unscoped include_regions returns the entire region table; unscoped and continent-only requests remain acceptedcountry uses the search tools' shape, normalization, and membership rules; "" is unset, whitespace-only is invalid, and an absent code raises unknown_countrycountry / region filters used by ourairports_search_airports and ourairports_search_runwaystotalCount counts returned countriesairport://{code} resourceourairports_get_airport, including the same resolutionNote — runways and radio frequencies always inline, so there is no include / includedcode resolves case-insensitively across all five identifier spaces (priority: ident → ICAO → IATA → GPS → local)unknown_code error with a recovery hint when no identifier space matchesBuilt 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:
Float64Array of coordinates, country/region maps, and a tokenized text-search indexlimit ranked candidates; runway search retains its first limit rows while both count every matchAgent-friendly output:
null, never fabricatedresolvedVia / resolutionNote that names every other airport sharing the code, by identcontent only, so names like GRASS&GRAVEL display as recordedA 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"
}
}
}
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
git clone https://github.com/cyanheads/ourairports-mcp-server.git
cd ourairports-mcp-server
bun install
data/):bun run build: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.
| 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.
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 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.
| 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/. |
See CLAUDE.md/AGENTS.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagesrc/mcp-server/*/definitions/index.tsnull, never fabricate missing valuesAirport, 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.
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.