faa-airmen-registry-mcp-server
RemoteAbout faa-airmen-registry-mcp-server
An MCP server for searching FAA airmen records from a local snapshot of the Releasable Airmen Database. It supports certificate, instructor, remote-pilot, and informational medical lookups, with configurable PII redaction and a public hosted endpoint.
Key features
- 01Search certificate records by name or FAA ID
- 02Inspect ratings and instructor certificates
- 03Look up remote pilot records
- 04Read informational medical fields
- 05Browse reference codes and snapshot status
Use cases
- 01Check a pilot’s listed certificates and ratings
- 02Find flight or ground instructors by location or rating
- 03Review remote-pilot certificate records
- 04Inspect source coverage and snapshot freshness
MCP support
Exposes six FAA-record lookup tools to MCP clients over STDIO or Streamable HTTP, including a public hosted endpoint. Self-hosted deployments serve a locally built snapshot.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://faa-airmen-registry.caseyjhand.com/mcpclaude mcp add --transport http cyanheads-faa-airmen-registry https://faa-airmen-registry.caseyjhand.com/mcp{"mcpServers":{"cyanheads-faa-airmen-registry":{"type":"http","url":"https://faa-airmen-registry.caseyjhand.com/mcp"}}}Open in Cursor{"servers":{"cyanheads-faa-airmen-registry":{"type":"http","url":"https://faa-airmen-registry.caseyjhand.com/mcp"}}}Open in VS Code{"mcpServers":{"cyanheads-faa-airmen-registry":{"type":"http","url":"https://faa-airmen-registry.caseyjhand.com/mcp"}}}{"mcpServers":{"cyanheads-faa-airmen-registry":{"serverUrl":"https://faa-airmen-registry.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 |
|---|---|---|---|
| city | string | No | Exact city, 1–60 visible characters; requires operator-enabled location access and is rejected while personal-data redaction is on. Blank means unset. |
| mode | enum | Yes | id requires uniqueId and forbids nonblank name/location fields and explicit nameMatch; name requires lastName, plus firstName while personal-data redaction is on, and forbids nonblank uniqueId.id | name |
| limit | integer | No | Maximum complete people or reference entries per page; default 10, maximum 50. A larger limit returns at most 50, with a notice. |
| state | string | No | Two-letter state or territory; trimmed and uppercased. Blank means unset. |
| offset | integer | No | Zero-based offset; default 0, maximum 10000. Follow nextOffset or narrow filters at the cap. |
| zipCode | string | No | Five-digit ZIP or ZIP+4, preserving leading zeros; requires operator-enabled location access and is rejected while personal-data redaction is on. Blank means unset. |
| lastName | string | No | LAST NAME, which may include a suffix; the default words match finds SMITH JR from SMITH. 1–60 visible characters after normalization. Blank means unset. |
| uniqueId | string | No | Opaque FAA Unique ID returned by faa_airmen_verify_certificate in name mode, A or C plus seven digits, for example A9999999; not a certificate number. Trimmed and uppercased. |
| firstName | string | No | FIRST NAME, which usually includes middle names; the default words match finds JOHN ROBERT from JOHN. 1–60 visible characters after normalization. Blank means unset. |
| nameMatch | enum | No | How each supplied name field matches; default words for name searches. words: the field equals the term or continues with a space after it; only a space separates words, so a hyphenated or apostrophe name is one word. exact: the whole field. prefix: the field begins with the term, also mid-word; rejected while personal-data redaction is on.words | exact | prefix |
| ratingCodes | array | No | One to eleven rating codes, repeats collapsed; every code must occur on the same certificate as every other requested credential, never across certificates. |
| typeRatings | array | No | One to eleven type-rating designators, repeats collapsed; every designator must occur on the same certificate as every other requested credential. |
| certificateType | string | No | One FAA certificate type letter, its description from faa_airmen_list_reference topic certificate_types (case-insensitive; underscore or space, such as remote pilot or flight_instructor), or CFI; selects people while retaining all their certificates. |
| certificateLevel | string | No | One level letter listed for the certificateType in faa_airmen_list_reference topic certificate_levels, such as A (airline transport pilot) with certificateType P; requires certificateType and matches the certificate's own level. Trimmed and uppercased; blank means unset. |
Details
From the project README@cyanheads/faa-airmen-registry-mcp-server
Search FAA airmen certificates, ratings, and informational medical records via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://faa-airmen-registry.caseyjhand.com/mcp
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
| Tool | Description |
|---|---|
faa_airmen_verify_certificate | Find identity candidates by name or FAA Unique ID, optionally narrowed by certificate type, level, and ratings, and read their complete certificate records |
faa_airmen_get_type_ratings | Read all pilot-family certificates and their aircraft type, category/class, and instrument ratings |
faa_airmen_check_medical_status | Read informational medical and BasicMed fields without assessing fitness or eligibility |
faa_airmen_search_instructors | Find flight and ground instructor records by name, state, and ratings |
faa_airmen_verify_remote_pilot | Find releasable remote pilot certificate records by name or Unique ID |
faa_airmen_list_reference | Read codes, interpretation guidance, observed type designators, and mirror status |
Capability reference
faa_airmen_verify_certificate tool
mode: "name"requireslastName;firstName,state,city, andzipCodenarrow it.nameMatchapplies to each supplied name field:words(default) matches whole leading words, soJOHNfinds the FIRST NAMEJOHN ROBERT(FAA first names usually include middle names) andSMITHfinds the LAST NAMESMITH JR; only a space separates words, soSMITHdoes not findSMITH-JONES.exactcompares the whole field, andprefixmatches from its beginning, mid-word includedmode: "id"takes oneuniqueId, an opaque A or C plus seven digits that is not a certificate number, and rejects name and location fieldscertificateType(a type letter, its reference topiccertificate_typesdescription such asremote pilotorflight_instructorin any case, orCFI) selects people but returns all their certificates; people sharing a name stay separate candidatescertificateLevel(one letter from reference topiccertificate_levels; requirescertificateType),ratingCodes(1–11 codes from topicratings), andtypeRatings(1–11 designators from topictype_ratings, such asB-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, andlevel/codeonly that slot's level:A/B-737is a type rating held at ATP level, whilecertificateLevel: "A"withB-737asks for an ATP certificate carrying it at any level,C/B-737included- 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_PIIon (the default), name mode needs bothlastNameandfirstNameand refusesnameMatch: "prefix",city, andzipCodewithpii_redacted; city and ZIP are omitted from results
faa_airmen_get_type_ratings tool
- One
uniqueIdfromfaa_airmen_verify_certificate; returns every certificate from the FAA pilot files with orderedratingsandtypeRatingsslots, each keeping itsrawvalue,ordinal, andcode found: falsefor an unknown ID; a known person with only non-pilot certificates gets an emptycertificatesarray
faa_airmen_check_medical_status tool
- One
uniqueId; returns each distinct reported medical record:medicalClass,medicalDateRawandmedicalExpireDateRaw(normally MMYYYY), and the BasicMed course and checklist dates (normally YYYYMMDD) foundseparates an unknown ID from a known person, andhasReportedMedicalFields: falsemeans nothing was reported, never an adverse finding. Every result carries adisclaimer: the fields are informational, not evidence of fitness or eligibility- With
FAA_AIRMEN_REDACT_PIIon (the default), every call is refused withpii_redacted
faa_airmen_search_instructors tool
- Needs at least one of
lastName,firstName,state,city,zipCode, orratingCodes;instructorTypeisflight(F),ground(G), orboth(default) ratingCodestakes 1–11 codes from reference topicratings, all required on the same certificate; repeats collapse. A bare code matches any source level, andlevel/code(therawform results return, such asF/INSTA; the level is one letter) only that level. CFII isINSTA,INSTH,INSTI, orINSTPon F and MEI isAMEorASME;INST,ADV, andBASICoccur only on G- With
FAA_AIRMEN_REDACT_PIIon (the default), a name search needs bothlastNameandfirstName, andnameMatch: "prefix",city, andzipCodeare refused withpii_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 asfaa_airmen_verify_certificate; returns only people holding U certificates, with all of them inremotePilotCertificates - With
FAA_AIRMEN_REDACT_PIIon (the default), the same name restrictions apply: both names required, andnameMatch: "prefix",city, andzipCoderefused withpii_redacted - Certificate presence doesn't establish Part 107 recurrent-training currency or operating authority
faa_airmen_list_reference tool
topiciscertificate_types,certificate_levels,ratings,type_ratings,identifiers,medical_guidance, ormirror_status; the four vocabulary topics take a case-insensitivefilterof up to 80 characters- Static topics and
mirror_statusanswer before a snapshot exists;type_ratingslists the designators observed in the imported snapshot mirror_statusreturnsmirrorStatus.state(missing,ready,unavailable),refreshState(idle,running,failed),lastRefreshFailure(the stage of a refresh that didn't complete, such asacquisition,validation,disk,cancelled,timeout, orinterruptedfor 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, onlyfalseturns it off): name searches need first and last name,prefixmatching and city/ZIP filters are refused, city and ZIP are omitted from output, andfaa_airmen_check_medical_statusrefuses every call. Refusals carry the reasonpii_redacted, every successful response carriespiiRedacted, and the mirror import is unaffected - The
faa-airmen-registry-mirrorCLI 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
staleflag, generation ID, and the generation's location import policy, plus the normalizedappliedFilters;faa_airmen_list_referencetopicmirror_statusreturns 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, andnextOffsetdescribe the actual page; offsets stop at 10,000, whereatPagingCapasks for narrower filters - 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 state or territory code.), never the value, and empty pages still return totals and guidance
Getting started
Public Hosted Instance
A public instance is available at https://faa-airmen-registry.caseyjhand.com/mcp — no installation or snapshot required. It serves Streamable HTTP; each client takes its own config shape.
For Claude Code:
claude mcp add --transport http faa-airmen-registry-mcp-server https://faa-airmen-registry.caseyjhand.com/mcp
For Cline (cline_mcp_settings.json):
{
"mcpServers": {
"faa-airmen-registry-mcp-server": {
"type": "streamableHttp",
"url": "https://faa-airmen-registry.caseyjhand.com/mcp"
}
}
}
For Cursor (mcp.json):
{
"mcpServers": {
"faa-airmen-registry-mcp-server": {
"url": "https://faa-airmen-registry.caseyjhand.com/mcp"
}
}
}
Claude Desktop adds remote servers under Settings → Connectors.
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.
Self-Hosted / Local
A local install serves its own snapshot, so build it before connecting a client:
export FAA_AIRMEN_MIRROR_DIR="$HOME/.local/share/faa-airmen-registry-mcp-server"
npx -y --package @cyanheads/faa-airmen-registry-mcp-server faa-airmen-registry-mirror init
init downloads the current FAA archive (about 57 MB) and builds a SQLite generation of about 1.75 GB; it took about 12 minutes in testing. The importer also keeps a 1 GiB free-space reserve, so allow about 3 GB. The same CLI runs verify, status, refresh, and prune. refresh builds the next generation beside the current one (another ~1.75 GB) and never deletes old ones, since a running server may still be reading one. status reports how many superseded generations there are and their size. Stop every server process that uses the mirror, then run prune: it takes the writer lock, refuses unless the published snapshot is ready, deletes every generation CURRENT doesn't name, and prints what it removed and the bytes freed. Deleting a generation a server still has open can fail that server's lookups (Bun on macOS) or the deletion itself (Windows).
Point the MCP client at the same FAA_AIRMEN_MIRROR_DIR; left unset, both use the platform user-data directory. To keep city and ZIP, set FAA_AIRMEN_INCLUDE_LOCATION=true for both import and serving; a snapshot imported without it needs a refresh after enabling. Output redaction is on by default and applies at serving time only; a personal local install that wants the full surface (medical lookups, partial-name and city/ZIP searches, city and ZIP in output) sets FAA_AIRMEN_REDACT_PII=false on the server.
MCP client configuration
Add the following to your MCP client configuration file.
{
"mcpServers": {
"faa-airmen-registry-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/faa-airmen-registry-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"FAA_AIRMEN_MIRROR_DIR": "/absolute/path/to/airmen-mirror"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"faa-airmen-registry-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/faa-airmen-registry-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"FAA_AIRMEN_MIRROR_DIR": "/absolute/path/to/airmen-mirror"
}
}
}
}
Or with 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:
docker volume create faa-airmen-data
docker run --rm -v faa-airmen-data:/data/airmen ghcr.io/cyanheads/faa-airmen-registry-mcp-server:latest bun dist/cli/mirror.js init
{
"mcpServers": {
"faa-airmen-registry-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-v", "faa-airmen-data:/data/airmen", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/faa-airmen-registry-mcp-server:latest"]
}
}
}
For Streamable HTTP, start the server on loopback:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Or from the image, published on loopback only:
docker run --rm -p 127.0.0.1:3010:3010 -v faa-airmen-data:/data/airmen ghcr.io/cyanheads/faa-airmen-registry-mcp-server:latest
# Server listens at http://127.0.0.1:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- About 3 GB of persistent local storage for the snapshot; network access is needed only for
initandrefresh. - Under Node,
better-sqlite3loads prebuilt bindings for macOS, Linux, and Windows on x64 and arm64 with no compiler step. Execution is tested on macOS under Node and Bun.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/faa-airmen-registry-mcp-server.git
- Navigate into the directory:
cd faa-airmen-registry-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# set FAA_AIRMEN_MIRROR_DIR, and FAA_AIRMEN_INCLUDE_LOCATION if you want city and ZIP
# FAA_AIRMEN_REDACT_PII=false lifts serving-time redaction on a personal local install
- Build and initialize the snapshot:
bun run rebuild
node --env-file=.env dist/cli/mirror.js init
The CLI doesn't load .env itself; pass --env-file=.env or export the variables.
Configuration
| Variable | Description | Default |
|---|---|---|
FAA_AIRMEN_MIRROR_DIR | Persistent snapshot directory, shared by the CLI and the server. | Platform user-data directory plus faa-airmen-registry-mcp-server |
FAA_AIRMEN_INCLUDE_LOCATION | true opts in to city and ZIP import, and to output and filtering when FAA_AIRMEN_REDACT_PII=false; false disables them. | false |
FAA_AIRMEN_REDACT_PII | Serving-time redaction, on by default; only false (any case) turns it off, and any other value leaves it on. Never changes what the mirror imports. | true |
FAA_AIRMEN_STALE_AFTER_DAYS | Source-age warning threshold, integer 1–366; never a credential verdict. | 45 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_HOST | HTTP bind address. | 127.0.0.1 |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto; a meaningful value overrides the server's stateless declaration. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
OTEL_ENABLED | Enable OpenTelemetry. | false |
See .env.example for the common framework overrides. Framework storage settings don't relocate the FAA snapshot.
Running the server
Local development
-
Build and run the production version:
# One-time build and snapshot bun run rebuild node --env-file=.env dist/cli/mirror.js init # Run the built server bun run start:stdio # or bun run start:http -
Run checks and tests:
bun run devcheck # Lints, formats, type-checks, and more bun run test # Runs the test suite
Project structure
| Directory | Purpose |
|---|---|
src/mcp-server/tools | Tool definitions (*.tool.ts), shared schemas, and lossless rendering. Six read-only tools. |
src/services/airmen | FAA acquisition, streaming import, immutable snapshot lifecycle, queries, and reference tables. |
src/cli | The faa-airmen-registry-mirror operator CLI. |
src/config | Server-specific environment variable parsing and validation with Zod. |
tests/ | Synthetic-source lifecycle, query, and MCP contract tests. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - The snapshot is operator-owned and read-only; keep it in the service layer and use
ctx.stateonly for tenant state - Register new tools in the
createApp()arrays insrc/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:
bun run devcheck
bun run test
License
This project is licensed under the Apache 2.0 License. See the LICENSE file 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.