Search FAA airmen certificates, ratings, and informational medical records.
Search FAA airmen certificates, ratings, and informational medical records via MCP. STDIO or Streamable HTTP.
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 or a loopback Streamable HTTP server, directly or in Docker. The operator builds the snapshot with a separate CLI; the server never downloads data, and no API key is needed.
| 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 |
faa_airmen_verify_certificate toolmode: "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 includedmode: "id" takes one uniqueId, 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 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 candidatescertificateLevel (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 includedFAA_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 resultsfaa_airmen_get_type_ratings tooluniqueId from faa_airmen_verify_certificate; returns every certificate from the FAA pilot files with ordered ratings and typeRatings slots, each keeping its raw value, ordinal, and codefound: false for an unknown ID; a known person with only non-pilot certificates gets an empty certificates arrayfaa_airmen_check_medical_status tooluniqueId; 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 eligibilityFAA_AIRMEN_REDACT_PII on (the default), every call is refused with pii_redactedfaa_airmen_search_instructors toollastName, 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 GFAA_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 resultsinstructorCertificates. Legacy expiry fields don't establish currency, and CFI certificates no longer expire under the December 2024 rulefaa_airmen_verify_remote_pilot toolmode, uniqueId, and name selectors as faa_airmen_verify_certificate; returns only people holding U certificates, with all of them in remotePilotCertificatesFAA_AIRMEN_REDACT_PII on (the default), the same name restrictions apply: both names required, and nameMatch: "prefix", city, and zipCode refused with pii_redactedfaa_airmen_list_reference tooltopic 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 charactersmirror_status answer before a snapshot exists; type_ratings lists the designators observed in the imported snapshotmirror_status returns mirrorStatus.state (missing, ready, unavailable), refreshState (idle, running, failed), 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 pathsBuilt 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:
PILOT_BASIC, NONPILOT_BASIC, PILOT_CERT, NONPILOT_CERT), joined by FAA Unique ID, keeping every distinct certificate and differing identity variantFAA_AIRMEN_INCLUDE_LOCATION=true; state is always availableFAA_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 unaffectedfaa-airmen-registry-mirror CLI downloads and validates each snapshot into an immutable generation; a failed refresh leaves the last complete one servingAgent-friendly output:
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 countslimit (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; offsets stop at 10,000, where atPagingCap asks for narrower filtersmirror_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 guidanceBuild the local snapshot 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.
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
init and refresh.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.git clone https://github.com/cyanheads/faa-airmen-registry-mcp-server.git
cd faa-airmen-registry-mcp-server
bun install
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
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.
| 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; .env.example and the Docker image set stateless. | auto |
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.
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
| 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. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.state only for tenant statecreateApp() arrays in src/index.tsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/faa-airmen-registry-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-faa-airmen-registry-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/faa-airmen-registry-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/faa-airmen-registry-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.