MCP server: One MCP server.

ourairports-mcp-server

@cyanheadsMCP server · External

Offline global aviation reference: airports, runways, navaids, frequencies from OurAirports.

Aviationv0.2.6Apache-2.0TypeScript
Freeno charge per call

One address for any MCP client. No sign-in needed on this server.

What it does

6 read

Grouped by tool name. At most: only reads or looks up information.

What it costs

Free

Odel charges nothing per call. The publisher may charge for its own service.

Tools

6 · grouped by tool name · checked 25 Oct 2026

Read · 6Look things up

  • ourairports_search_airports
  • ourairports_search_runways
  • ourairports_get_airport
  • ourairports_find_airports
  • ourairports_find_navaids
  • ourairports_list_countries
Try it in the Inspector
README12 min read

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

6 Tools • 1 Resource

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework


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

ToolDescription
ourairports_search_airportsFull-text and faceted search over the airport corpus by name, municipality, country, region, or type. Ranked summaries, closed airports excluded by default.
ourairports_search_runwaysSearch 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_airportFull record for one airport resolved by any code (IATA/ICAO/GPS/local/ident), with its runways and radio frequencies inline.
ourairports_find_airportsAirports within a radius of a coordinate, ranked nearest-first by great-circle distance, with distance and bearing.
ourairports_find_navaidsNavigation aids (VOR, VOR-DME, DME, NDB, NDB-DME, TACAN, VORTAC) near a coordinate or serving a specific airport.
ourairports_list_countriesCountries 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

ResourceDescription
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), 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 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
  • limit accepts 1–100 rows and defaults to OURAIRPORTS_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
  • surface 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
  • Returns one flat { airport, runway } row per matching runway — an airport with three matching runways contributes three rows
  • lengthFt 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)
  • A runway whose length or width is unknown is excluded when the matching min_*_ft filter 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_runways is set
  • limit accepts 1–100 rows and defaults to OURAIRPORTS_DEFAULT_SEARCH_LIMIT (20); truncation reports the total matched count, cap, and recovery guidance

ourairports_get_airport tool

  • Resolves a single code case-insensitively across all five identifier spaces (priority: ident → ICAO → IATA → GPS → local); surrounding whitespace is ignored
  • Runways and radio frequencies inline; include trims the response to a subset, and the output's included field distinguishes a relation omitted by include from 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 lengthFt plus per-end displaced thresholds (leDisplacedThresholdFt / heDisplacedThresholdFt)
  • Absent codes reported as null; closed airports always resolve
  • unknown_code error with a recovery hint when no identifier space matches

ourairports_find_airports tool

  • Great-circle (haversine) ranking, nearest-first, each result with distanceKm and bearingDeg (degrees true) from the query point
  • radius_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-in
  • Coordinate in, ranked airports out — no geocoding; resolve place names to lat/lon upstream first
  • totalCount is the filtered in-radius count before limit; truncation reports the cap and recovery guidance (narrow radius_km, add type, or raise limit)
  • Empty-radius guidance suggesting a wider radius_km

ourairports_find_navaids tool

  • Coordinate mode: latitude + 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 50
  • Airport mode: airport_code returns 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
  • frequencyKhz 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 null
  • 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), and dmeFrequencyKhz (the DME's paired VHF frequency) — null where the dataset has no value
  • totalCount is the match count before limit in both modes; truncation reports the cap and recovery guidance
  • Airport mode distinguishes "airport not found" (unknown_code error) from "airport found but has no associated navaids" (empty list with a note)
  • Airport mode adds a resolutionNote when airport_code is also a code of other airports, naming them by ident — the same note ourairports_get_airport returns

ourairports_list_countries tool

  • Optional country 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)
  • Use { "country": "US", "include_regions": true } for one country's regions. Unscoped include_regions returns the entire region table; unscoped and continent-only requests remain accepted
  • country uses the search tools' shape, normalization, and membership rules; "" is unset, whitespace-only is invalid, and an absent code raises unknown_country
  • Airport counts exclude closed airports
  • Countries sorted by name; the lookup table for valid country / region filters used by ourairports_search_airports and ourairports_search_runways
  • A country/continent mismatch returns an empty success with a notice naming both filters; totalCount counts returned countries

airport://{code} resource

  • Same record as ourairports_get_airport, including the same resolutionNote — runways and radio frequencies always inline, so there is no include / included
  • code 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 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 Float64Array of 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 limit ranked candidates; runway search retains its first limit rows 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 / resolutionNote that 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 content only, so names like GRASS&GRAVEL display 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:

json
{
  "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.

json
{
  "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):

json
{
  "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:

json
{
  "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:

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 or higher (or Node.js v24+).
  • No API key, account, or external service — all data is bundled.

Installation

  1. Clone the repository:
sh
git clone https://github.com/cyanheads/ourairports-mcp-server.git
  1. Navigate into the directory:
sh
cd ourairports-mcp-server
  1. Install dependencies:
sh
bun install
  1. Fetch and bundle the dataset (writes the six CSVs into data/):
sh
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

VariableDescriptionDefault
OURAIRPORTS_DATA_DIRDirectory holding the six OurAirports CSV files. Overridable to point at a fresher local data drop.Bundled data/
OURAIRPORTS_DEFAULT_SEARCH_LIMITDefault result cap for the search/find tools when the caller omits limit (1–100).20
MCP_TRANSPORT_TYPETransport: stdio or http.stdio
MCP_HTTP_PORTPort for the HTTP server.3010
MCP_HTTP_ENDPOINT_PATHHTTP endpoint path where the server is mounted./mcp
MCP_AUTH_MODEAuth mode: none, jwt, or oauth.none
MCP_SESSION_MODEHTTP session mode: stateful, stateless, or auto. This server uses stateless mode because no tool requests follow-up input.stateless
MCP_LOG_LEVELLog level (RFC 5424).info
LOGS_DIRDirectory for log files (Node.js only).<project-root>/logs
STORAGE_PROVIDER_TYPEStorage backend (unused on the data path — the index is in-memory).in-memory
OTEL_ENABLEDEnable OpenTelemetry instrumentation.false

See .env.example for the full list of optional overrides.

Running the server

Local development

  • Build and run:

    sh
    # 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:

    sh
    bun run devcheck   # Lint, format, typecheck, security
    bun run test       # Vitest test suite
    bun run lint:mcp   # Validate MCP definitions against spec
    

Docker

sh
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

DirectoryPurpose
src/index.tscreateApp() entry point — registers tools/resources and loads the bundled index at setup().
src/configServer-specific environment variable parsing and validation with Zod.
src/mcp-server/toolsTool definitions (*.tool.ts). Six read-only airport/runway/navaid tools.
src/mcp-server/resourcesResource definitions. The airport://{code} record.
src/services/airport-dataThe bundled-data service — CSV parsing, in-memory indices, code resolution, search, and the haversine geo scan.
scripts/build-data.tsBuild-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/catch in tool logic
  • Use ctx.log for request-scoped logging, ctx.state for 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:

sh
bun run devcheck
bun run test

License

Apache-2.0 — see LICENSE for details.

Similar modules

Browse Aviation