Skip to main content
Glama

adsb-mcp

Live local ADS-B flight data as MCP tools, over your own receiver's aircraft.json (readsb / tar1090 / dump1090) — keyless, live, your own data. Answers "what's flying overhead right now?" and, with the optional recorder on, "have we seen this one before?" / "what flies here most?".

Tools

Tool

What it does

aircraft_overhead

Aircraft in range of the receiver, nearest first. Distances from the site (ADSB_SITE_LAT/LON), else from the airport (approximate); without either, no distances.

aircraft_near

Aircraft within a radius of a point you pass (no receiver location needed).

find_aircraft

Match by callsign / ICAO hex / registration (partial, case-insensitive).

traffic_stats

Recorded passes over a window, grouped by airframe, callsign, type, outcome or local hour; most or least common.

aircraft_history

Past passes of an airframe / callsign / registration: "seen before?", when, how low, landed or went around.

Data is structured (hex, callsign, registration, type, altitude_ft, ground_speed_kt, track_deg, lat/lon, distance_km + bearing_deg from the reference point, seen_s). The site's coordinates are never echoed. Every result has a status: ok / refused (caller-fixable, with advice) / error (receiver unreachable). ADS-B carries no route; the optional OpenSky enrichment below adds one.

Related MCP server: Aviation MCP Server

Run

uv sync                      # (--system-certs behind a TLS-inspecting proxy)
# stdio:
uv run adsb-mcp serve
# streamable-http (behind mcpo / on the LAN):
ADSB_MCP_TOKEN=... ADSB_URL=http://<feeder-host>:8080/data/aircraft.json \
  ADSB_SITE_LAT=... ADSB_SITE_LON=... ADSB_POLL_SECONDS=15 \
  uv run adsb-mcp serve --transport http --host 0.0.0.0 --port 8000
uv run pytest                # no network: the feeder is faked

Config (env)

Var

Default

Notes

ADSB_URL

http://localhost:8080/data/aircraft.json

Your feeder's live snapshot (readsb / tar1090 path).

ADSB_SITE_LAT / ADSB_SITE_LON

—

Receiver location: the exact reference point. Home-location PII — from a secret store, never committed, never echoed in results.

ADSB_EXACT_LOCATION

false

Off = approximate location: when distances are from the site, aircraft_overhead rounds positions (~1 km), distance (whole km) and bearing (10°). true = exact values.

ADSB_AIRPORT_ICAO / ADSB_AIRPORT_LAT / ADSB_AIRPORT_LON

—

The nearby airport (e.g. EGLL). An approximate, public reference point when the site isn't set.

ADSB_AIRPORT_ELEV_FT

—

Field elevation. With the airport position, turns on arrival / go-around / departure tracking.

ADSB_AIRPORT_ALIASES

—

Nearby airfields OpenSky reports instead of your airport (e.g. SEAS,SEHI), counted as the airport when reading routes. The store keeps OpenSky's codes as received.

ADSB_MILITARY_PATTERNS

—

Callsign / registration patterns marking military or state aircraft (e.g. FAE*,AEE*,HIERRO*,ARPIA*), on top of readsb's own military flag. Catches unregistered airframes on tactical callsigns. Applied when reading, so it covers history too.

ADSB_AIRPORT_NAME

—

Display name for the board header (e.g. Heathrow).

ADSB_BOARD

open

The flight board at /board: open (no bearer: it carries no site data), token (behind the bearer), off.

ADSB_MCP_TOKEN

—

Bearer required on streamable-http when set.

ADSB_HTTP_TIMEOUT

10

Seconds.

ADSB_POLL_SECONDS

0

>0 turns on the recorder: poll the feeder this often and record passes. 0 = live tools only. 10–15 is a good value.

ADSB_DB

data/adsb.db

SQLite store of passes (/data/adsb.db in the image).

ADSB_PASS_RADIUS_KM

50

A pass = an airborne aircraft within this radius of the reference point, or on approach to the airport.

ADSB_PASS_GRACE_SECONDS

90

Close a pass after the aircraft has been gone, or out of the zone, this long.

ADSB_PASS_MERGE_SECONDS

600

An aircraft back within this long of its pass closing continues that pass, so a coverage gap doesn't split one visit in two.

ADSB_ARRIVAL_GRACE_SECONDS

900

An aircraft lost on approach stays open this long (it's presumably landing below coverage).

ADSB_APPROACH_KM

30

How far from the airport a descent counts as an approach.

ADSB_OPENSKY_CLIENT_ID / ADSB_OPENSKY_CLIENT_SECRET

—

OpenSky API client (OAuth2). Both set + the recorder on = route enrichment. Secret: from the environment only.

ADSB_OPENSKY_DAILY_CREDITS

3000

Most OpenSky credits the server spends per UTC day (a past-day lookup costs ~30).

ADSB_OPENSKY_RESERVE

1000

Stop for the day when OpenSky reports fewer credits left than this.

ADSB_TZ

UTC

IANA zone for local times and hour-of-day stats (e.g. Europe/London).

History: passes

With ADSB_POLL_SECONDS>0 and a reference point (the site, else the airport), the server polls the feeder in the background, whether or not anyone is asking, and records one row per pass: a visit, not a poll. A ~6-minute overflight is 1 row, not ~24. Each pass keeps first / last seen, min / max altitude, closest approach, max RSSI and the number of snapshots. The live tools reuse the recorder's latest snapshot while it's fresh, so the feeder isn't hit twice.

With the airport elevation set, each pass also gets an outcome: arrival (seen landing), arrival_presumed (lost on approach: coverage usually ends before the runway, so the pass is held open ADSB_ARRIVAL_GRACE_SECONDS), go_around, departure or overflight, plus a go_arounds count. The heuristics are simple and documented in passes.py.

A pass that circles — heading changes of 360° or more while staying within about 15 km — is recorded as loitering (patrols, training, survey flights), and its climbs and descents don't count as go-arounds. Every pass also carries its ADS-B category (rotorcraft: true for helicopters) and a military flag (readsb's database flag, or a match in ADSB_MILITARY_PATTERNS). traffic_stats can group by category and narrow to only="military" or only="rotorcraft".

A failed poll never closes a pass, and open passes are written through, so a restart resumes them. An aircraft that drops out of coverage briefly (a hold, a turn behind terrain) continues its pass when it comes back within ADSB_PASS_MERGE_SECONDS; a confirmed landing always ends the pass. With neither a site nor an airport, passes can't be tracked and the recorder stays off.

Claude Desktop / Claude Code (stdio)

{
  "mcpServers": {
    "adsb": {
      "command": "uv",
      "args": ["--directory", "/path/to/adsb-mcp", "run", "adsb-mcp", "serve"],
      "env": { "ADSB_URL": "http://<feeder-host>:8080/data/aircraft.json" }
    }
  }
}

Routes from OpenSky (optional)

ADS-B doesn't say where a flight is going. If you feed the OpenSky Network, your account gets API access (an active feeder gets 8,000 credits a day). Create an API client in your OpenSky account and set ADSB_OPENSKY_CLIENT_ID / ADSB_OPENSKY_CLIENT_SECRET. A background job then adds origin and destination airports to recorded passes.

It's built to be a good citizen with access you earned by feeding:

  • It only looks up aircraft your own receiver saw.

  • OpenSky processes flights overnight, so it enriches yesterday's passes, after 04:00 UTC.

  • At most one call per airframe per day, however many times it passed.

  • It stops for the day at ADSB_OPENSKY_DAILY_CREDITS, when OpenSky reports fewer than ADSB_OPENSKY_RESERVE credits left, or when OpenSky says to slow down (HTTP 429).

What you get: past passes show origin / destination, traffic_stats can group by route, and live aircraft carry a usual_route — where that callsign has usually flown (e.g. 4 of its last 5 passes to SPJC), so the assistant can say "usually flies to Lima". A past pass that broke from its usual route is flagged unusual_route (a diversion, an unscheduled landing). The board shows usual routes too.

OpenSky estimates each airport from the last position its network saw, so when coverage ends before the runway it can name a nearby airfield instead of yours. List those in ADSB_AIRPORT_ALIASES. Each pass's route_lookup says whether it was matched, looked up with no_match (OpenSky had no overlapping flight), or is still pending; recorder.enrichment.last_7_days counts each.

With enrichment on, the server contacts OpenSky; the MCP tools themselves still only read the local store. Using OpenSky data is subject to OpenSky's terms.

Flight board

An airport-style arrivals board (FIDS) for an old phone or tablet on the wall, or a Home Assistant card. With the airport set and streamable-HTTP running, open http://<server>:8000/board.

  • Arrivals: aircraft on approach (with ETA), ones lost on approach ("Landing"), and ones seen landing.

  • Departures: aircraft climbing out.

  • Overhead: everything within ADSB_PASS_RADIUS_KM of the airport, nearest first; emergency squawks (7500 / 7600 / 7700) flash red.

  • Recent: passes that ended in the last 3 hours, with their outcome (needs the recorder).

The page is one self-contained file: no build, no dependencies, no external fonts. It polls /board.json every 8 s and renders in the browser, so many displays cost the server almost nothing (they share one snapshot for at least 5 s). It reflows for landscape, portrait and small embeds, dropping less important columns as space shrinks. Options: ?lang=es, ?interval=10, ?sections=arr,ovh.

Everything on the board is measured from the airport, a public place, and aircraft positions are left out, so it carries nothing about the receiver's location. That's why it's served without the bearer by default (a wall panel or an iframe can't send one); set ADSB_BOARD=token or off to change that.

Full screen on a phone or tablet: tap the Full screen button (Android Chrome, iPad), or use Add to Home Screen. The board ships a manifest, so it then opens without browser bars; on iPhone that's the only way. Android installs it as an app only over https, and keeping the screen awake (Wake Lock) also needs https; over plain http, use the device's stay-awake setting or a kiosk browser app.

In Home Assistant, add a Webpage card with the /board URL. If the dashboard is served over https, serve the board over https too, or the browser blocks the frame.

Docker

docker build -t adsb-mcp .
docker run --rm -p 8000:8000 -e ADSB_MCP_TOKEN=... \
  -e ADSB_URL=http://<feeder-host>:8080/data/aircraft.json \
  -e ADSB_SITE_LAT=... -e ADSB_SITE_LON=... adsb-mcp

It needs to reach your feeder over the LAN. Inside the container localhost is the container itself, so set ADSB_URL to the feeder's address. With the recorder on, mount /data on local disk (SQLite WAL and network filesystems don't mix) and back it up: the history accumulates and can't be re-fetched.

docker run -d -p 8000:8000 -v /srv/adsb-mcp-data:/data -e ADSB_POLL_SECONDS=15 \
  -e ADSB_AIRPORT_ICAO=... -e ADSB_AIRPORT_LAT=... -e ADSB_AIRPORT_LON=... -e ADSB_AIRPORT_ELEV_FT=... \
  -e ADSB_URL=http://<feeder-host>:8080/data/aircraft.json -e ADSB_MCP_TOKEN=... adsb-mcp

Auth and exposure

Auth is deliberately simple: stdio, or streamable-HTTP with a single bearer token (ADSB_MCP_TOKEN) on a private network, for example behind mcpo. /healthz stays open, and so does the flight board (/board, /board.json) unless ADSB_BOARD says otherwise. There is no OAuth, no users and no audit log, by design. To publish it more widely, put a management layer in front of it: an MCP gateway or auth proxy that handles OAuth, users and audit for all your servers at once. ADSB_SITE_LAT/LON is your home location. It is never echoed in a result, and the store holds distances, not coordinates. An exact aircraft position next to an exact distance and bearing would give the site away in a single result, so by default aircraft_overhead gives an approximate location, like choosing "approximate" in a phone's location permission: still good for "about 12 km to the south". Set ADSB_EXACT_LOCATION=true to get exact values. This makes the site hard to find, not impossible: someone who collects many results can still narrow it down. If that matters, set only the airport, so results are measured from a public place.

AI assistance

adsb-mcp is developed openly with the help of Claude (Anthropic). Commits Claude helped write carry a Co-Authored-By: Claude trailer. The code and design are open source so the work can be inspected, reused, and given back.

License

MPL-2.0 © Daniel Núñez.

Available Tools

3 tools
aircraft_nearAircraft near a pointA
Read-onlyIdempotent

Aircraft currently within radius_km of a given point (lat/lon you supply — e.g. a city), nearest first. Unlike aircraft_overhead this needs no receiver location: distance is measured from the point you pass.

    Args:
        latitude/longitude: the centre point to search around.
        radius_km: search radius in kilometres (default 50).
        max_results: cap on aircraft returned (default 10).
        include_grounded: include aircraft on the ground (default False).
    

Inputs: latitude (required), longitude (required), radius_km, max_results, +1 more.

aircraft_overheadAircraft overheadA
Read-onlyIdempotent

Aircraft currently in range of the receiver, nearest first. The "what's that plane overhead right now" query. With the receiver location configured, each result carries distance_km + bearing; without it, results are listed newest-position-first and distance is null (set ADSB_SITE_LAT/LON).

    Args:
        max_results: cap on aircraft returned (default 10).
        include_grounded: include aircraft on the ground (default False — only airborne).
    

Inputs: max_results, include_grounded.

find_aircraftFind aircraftA
Read-onlyIdempotent

Find aircraft currently in range by a partial match on callsign, ICAO hex, or registration (case-insensitive). Returns matches even if they have no position yet.

    Args:
        query: text to match against callsign / hex / registration (e.g. "LAN", "e80", "CC-B").
        max_results: cap on matches returned (default 10).
    

Inputs: query (required), max_results.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.7/5.0

    Scored across 3 tools

    Disambiguation4/5

    aircraft_overhead and aircraft_near are both 'aircraft in range, nearest first' spatial queries, but the descriptions clearly distinguish them (implicit receiver location vs. an explicit lat/lon you supply). find_aircraft is a clearly separate identifier-lookup path, so an agent can pick correctly.

    Naming Consistency3/5

    Two tools follow an aircraft_<modifier> pattern while the third is a verb_noun form (find_aircraft), and none use a consistent verb prefix. It is still readable and unprefixed-domain-consistent, but the convention is mixed.

    Tool Count3/5

    Three tools is on the thin side even for a narrow, read-only ADS-B query domain. It is defensible since each tool covers a distinct query mode, but the surface feels minimal.

    Completeness3/5

    Core live queries (overhead, radius, identifier lookup) are covered, but there is no tool for detailed single-aircraft data, historical/track positions, or receiver/feeder status. Agents can work around these gaps but cannot fully answer common questions like a plane's route or recent track.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers