@cyanheads/faa-airmen-registry-mcp-server
Search FAA airmen certificates, ratings, and informational medical records via MCP. STDIO or Streamable HTTP.
6 Tools
Overview
FAA airmen certification records from the Releasable Airmen Database, served from a local snapshot. Resolve names to separate identity candidates, read pilot and non-pilot certificates with their ordered ratings, find flight and ground instructors and remote pilots, and read informational medical fields.
Runs as a stdio process, a loopback Streamable HTTP server (directly or in Docker), or the public hosted endpoint above. A self-hosted operator builds the snapshot with a separate CLI; the server never downloads data, and no API key is needed.
Tools
Capability reference
faa_airmen_verify_certificate tool
mode: "name" requires lastName; firstName, state, city, and zipCode narrow it. nameMatch applies to each supplied name field: words (default) matches whole leading words, so JOHN finds the FIRST NAME JOHN ROBERT (FAA first names usually include middle names) and SMITH finds the LAST NAME SMITH JR; only a space separates words, so SMITH does not find SMITH-JONES. exact compares the whole field, and prefix matches from its beginning, mid-word included
mode: "id" takes one uniqueId, an opaque A or C plus seven digits that is not a certificate number, and rejects name and location fields
certificateType (a type letter, its reference topic certificate_types description such as remote pilot or flight_instructor in any case, or CFI) selects people but returns all their certificates; people sharing a name stay separate candidates
certificateLevel (one letter from reference topic certificate_levels; requires certificateType), ratingCodes (1–11 codes from topic ratings), and typeRatings (1–11 designators from topic type_ratings, such as B-737) narrow candidates further. Every requested credential must sit on one certificate, and matches still return all their certificates. Repeats collapse. A bare code or designator matches any level, and level/code only that slot's level: A/B-737 is a type rating held at ATP level, while certificateLevel: "A" with B-737 asks for an ATP certificate carrying it at any level, C/B-737 included
ratingCodes carries the same CFII and MEI mapping as faa_airmen_search_instructors; CFII or MEI sent as a code matches nothing. When a zero result routes to topic ratings and the codes sent include CFII, MEI, or one of their codes, the notice names the alternatives and, unless the call sent certificateType: "F", says to send each with it, since a bare INSTA also matches a pilot certificate's instrument rating
- A zero result with credential filters says how many people the name search matches without them, so a name miss reads apart from a credential miss
- With
FAA_AIRMEN_REDACT_PII on (the default), name mode needs both lastName and firstName and refuses nameMatch: "prefix", city, and zipCode with pii_redacted; city and ZIP are omitted from results
faa_airmen_get_type_ratings tool
- One
uniqueId from a faa_airmen_verify_certificate, faa_airmen_search_instructors, or faa_airmen_verify_remote_pilot result; returns every certificate from the FAA pilot files (pilot, flight instructor, authorized aircraft instructor, remote pilot, flight engineer) with ordered ratings and typeRatings slots, each keeping its raw value, ordinal, and code
- Ground instructor, mechanic, and other non-pilot certificates are not included;
faa_airmen_verify_certificate in mode: "id" returns every certificate
found: false for an unknown ID; a known person with only non-pilot certificates gets an empty certificates array
faa_airmen_check_medical_status tool
- One
uniqueId from a faa_airmen_verify_certificate, faa_airmen_search_instructors, or faa_airmen_verify_remote_pilot result; returns each distinct reported medical record: medicalClass, medicalDateRaw and medicalExpireDateRaw (normally MMYYYY), and the BasicMed course and checklist dates (normally YYYYMMDD)
found separates an unknown ID from a known person, and hasReportedMedicalFields: false means nothing was reported, never an adverse finding. Every result carries a disclaimer: the fields are informational, not evidence of fitness or eligibility
- With
FAA_AIRMEN_REDACT_PII on (the default), every call is refused with pii_redacted
faa_airmen_search_instructors tool
- Needs at least one of
lastName, firstName, state, city, zipCode, or ratingCodes; instructorType is flight (F), ground (G), or both (default)
ratingCodes takes 1–11 codes from reference topic ratings, all required on the same certificate; repeats collapse. A bare code matches any source level, and level/code (the raw form results return, such as F/INSTA; the level is one letter) only that level. CFII is INSTA, INSTH, INSTI, or INSTP on F and MEI is AME or ASME; INST, ADV, and BASIC occur only on G
- A zero result that sent
ratingCodes with a name or location filter counts the instructors of the requested type those filters match without the codes: a positive count points to the rating codes, zero to the name and location filters. A rating-codes route after CFII, MEI, or one of their codes also names the alternatives; after an instructorType: "ground" search it says to send them with flight or both, since all of them sit on F
- With
FAA_AIRMEN_REDACT_PII on (the default), a name search needs both lastName and firstName, and nameMatch: "prefix", city, and zipCode are refused with pii_redacted; city and ZIP are omitted from results
- Matches carry their complete qualifying
instructorCertificates. Legacy expiry fields don't establish currency, and CFI certificates no longer expire under the December 2024 rule
faa_airmen_verify_remote_pilot tool
- Same
mode, uniqueId, and name selectors as faa_airmen_verify_certificate; returns only people holding U certificates, with all of them in remotePilotCertificates
- With
FAA_AIRMEN_REDACT_PII on (the default), the same name restrictions apply: both names required, and nameMatch: "prefix", city, and zipCode refused with pii_redacted
- Certificate presence doesn't establish Part 107 recurrent-training currency or operating authority
faa_airmen_list_reference tool
topic is certificate_types, certificate_levels, ratings, type_ratings, identifiers, medical_guidance, or mirror_status; the four vocabulary topics take a case-insensitive filter of up to 80 characters
- Static topics and
mirror_status answer before a snapshot exists; type_ratings lists the designators observed in the imported snapshot
mirror_status returns mirrorStatus.state (missing, ready, unavailable), refreshState (idle, running, failed, or unreadable when REFRESH.json can't be read or isn't a valid refresh record; removing it is safe, and the next refresh rewrites it), lastRefreshFailure (the stage of a refresh that didn't complete, such as acquisition, validation, disk, cancelled, timeout, or interrupted for one whose process ended without recording a result), and the latest generation's provenance, without filesystem paths
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.
FAA-specific:
- Offline SQLite lookups over the four monthly source files (
PILOT_BASIC, NONPILOT_BASIC, PILOT_CERT, NONPILOT_CERT), joined by FAA Unique ID, keeping every distinct certificate and differing identity variant
- Street addresses are discarded before anything is stored. City and ZIP import, output, and filtering need
FAA_AIRMEN_INCLUDE_LOCATION=true; state is always available
- Serving-time redaction is on by default (
FAA_AIRMEN_REDACT_PII, only false turns it off): name searches need first and last name, prefix matching and city/ZIP filters are refused, city and ZIP are omitted from output, and faa_airmen_check_medical_status refuses every call. Refusals carry the reason pii_redacted, every successful response carries piiRedacted, and the mirror import is unaffected
- The
faa-airmen-registry-mirror CLI downloads and validates each snapshot into an immutable generation; a failed refresh leaves the last complete one serving
- The FAA omits airmen who withhold address release and publishes no certificate numbers or recurrent-training dates. A miss is not evidence that someone is uncertificated, and a match is not a current-privilege determination; use the FAA's official services for authoritative answers
Agent-friendly output:
- Provenance on every snapshot-backed response: source name, snapshot month, source age with a
stale flag, generation ID, and the generation's location import policy, plus the normalized appliedFilters; faa_airmen_list_reference topic mirror_status returns the full record, including hashes, row counts, and source-quality warning counts
- Whole-record paging: list tools return complete people within a ~24 KiB response budget, so
limit (default 10, at most 50; a larger limit returns 50 with a notice) is an upper bound. totalCount, returnedCount, and nextOffset describe the actual page, and a page the budget cut short of limit says so in its notice with the offset to continue from; offsets stop at 10,000, where atPagingCap asks for narrower filters
- A published Agent Skill,
faa-airmen-registry-guide, served over MCP as skill://faa-airmen-registry-guide/SKILL.md and shipped in the plugin, npm package, .mcpb, and image: mapping CFII, MEI, ATP, and type-rating shorthand to codes, one call per alternative code, name matches that miss silently, reading each rating at its own level, and the stop rule before reporting a credential absent
- Typed failures that name the next call:
mirror_missing, location_access_disabled, pii_redacted, invalid_filter, and the other declared reasons each carry a recovery step. A validation failure names the field and its expected form (state must be a two-letter USPS state, territory, or military code.), never the value, and empty pages still return totals and guidance
Getting started
Connect to the hosted instance, no install or snapshot:
The hosted instance runs with redaction on: name searches need both first and last names, nameMatch: "prefix" and city/ZIP searches are refused, and medical lookups are unavailable. Whole leading words remain supported by the default words matching. For the full surface, self-host with FAA_AIRMEN_REDACT_PII=false.
Or run it locally. A local install serves its own snapshot, so build it before connecting a client:
init downloads the FAA archive (about 57 MB) and builds a ~1.75 GB snapshot in about 12 minutes; allow about 3 GB of disk. Point the MCP client at the same FAA_AIRMEN_MIRROR_DIR, and set FAA_AIRMEN_REDACT_PII=false on a personal install that wants the full surface. Refreshing, pruning old generations, and keeping city and ZIP are covered in docs/snapshot.md.
Then add it to Claude Code with the same snapshot directory:
For other clients, add this to the MCP config file:
For Docker, Streamable HTTP, or a source build, see Running the server.
Configuration
See .env.example for the common framework overrides. Framework storage settings don't relocate the FAA snapshot.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+) and about 3 GB of persistent local storage for the snapshot; network access is needed only for init and refresh. Under Node, better-sqlite3 loads prebuilt bindings for macOS, Linux, and Windows on x64 and arm64 with no compiler step. Execution is tested on macOS under Node and Bun.
-
Clone and install:
-
Build, initialize the snapshot, and run:
The CLI doesn't load .env itself; pass --env-file=.env or export the variables.
-
Run checks and tests:
Docker
The image keeps its snapshot in /data/airmen, so initialize a named volume first; run the same command with refresh, verify, or status for maintenance, and with prune once every server container using the volume is stopped:
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/faa-airmen-registry-mcp-server; the last command publishes it on loopback only, at http://127.0.0.1:3010/mcp. To run it over stdio from an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-v", "faa-airmen-data:/data/airmen", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/faa-airmen-registry-mcp-server:latest"]. To build locally, docker build -t faa-airmen-registry-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catch in tool logic
- The snapshot is operator-owned and read-only; keep it in the service layer and use
ctx.state only for tenant state
- Register new tools in the
createApp() arrays in src/index.ts
- Validate source rows → normalize to domain types → return the output schema; never fabricate missing fields, never persist street addresses, and use synthetic identities in tests and examples
Contributing
Issues are welcome. Run checks and tests before submitting:
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.