We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/marc-shade/threat-intel-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Military aviation tracking source for world-intel-mcp.
Provides real-time military aircraft tracking via OpenSky Network and
aircraft detail lookups via hexdb.io (free, no API key).
"""
import asyncio
import base64
import logging
import os
from datetime import datetime, timezone
from ..fetcher import Fetcher
logger = logging.getLogger("world-intel-mcp.sources.military")
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_OPENSKY_STATES_URL = "https://opensky-network.org/api/states/all"
_ADSBLOL_MIL_URL = "https://api.adsb.lol/v2/mil"
_HEXDB_BASE_URL = "https://hexdb.io/api/v1/aircraft"
# ICAO hex prefix ranges known to be allocated to military operators.
MILITARY_ICAO_PREFIXES = [
"AE", # US Military
"A8", "A9", "AA", "AB", "AC", "AD", # US Military extended
"43C", "43D", "43E", "43F", # UK Military
"3F", # Germany Military
"3A8", "3A9", "3AA", "3AB", # France Military
"500", "501", "502", # Israel Military
"70", # Pakistan Military
"C0", # Canada Military
]
# Callsign prefixes commonly used by military flights.
MILITARY_CALLSIGN_PREFIXES = [
"RCH", "DUKE", "REACH", "EVAC", "JAKE", "TOPCAT", "BOXER",
"IRON", "CASA", "NATO", "LAGR", "TEAL", "SAM", "EXEC",
"SPAR", "VALOR", "BLADE",
]
# ICAO hex prefix → country mapping for military aircraft.
_ICAO_COUNTRY: list[tuple[str, str]] = [
("AE", "United States"), ("A8", "United States"), ("A9", "United States"),
("AA", "United States"), ("AB", "United States"), ("AC", "United States"),
("AD", "United States"),
("43C", "United Kingdom"), ("43D", "United Kingdom"),
("43E", "United Kingdom"), ("43F", "United Kingdom"),
("3F", "Germany"), ("3A8", "France"), ("3A9", "France"),
("3AA", "France"), ("3AB", "France"),
("500", "Israel"), ("501", "Israel"), ("502", "Israel"),
("70", "Pakistan"), ("C0", "Canada"),
("34", "Italy"), ("3C", "Germany"),
("E4", "Brazil"), ("71", "Turkey"),
("50", "Israel"), ("48", "Netherlands"),
("44", "Austria"), ("45", "Belgium"),
("46", "Bulgaria"), ("49", "Denmark"),
("4A", "Finland"), ("39", "France"),
("4B", "Greece"), ("4D", "Hungary"),
("4C", "Ireland"), ("30", "Italy"),
("73", "Japan"), ("78", "China"),
("7C", "Australia"), ("C8", "Australia"),
]
def _icao_to_country(icao24: str) -> str:
"""Derive country from ICAO24 hex address prefix."""
upper = icao24.upper()
# Try longest prefixes first for specificity
for prefix, country in sorted(_ICAO_COUNTRY, key=lambda x: -len(x[0])):
if upper.startswith(prefix):
return country
return ""
# Theater bounding boxes for global military posture assessment.
THEATERS = {
"european": {"bbox": "35,-25,72,45", "desc": "NATO/Russia theater"},
"indo_pacific": {"bbox": "-10,95,55,155", "desc": "China/Taiwan/SCS"},
"middle_east": {"bbox": "10,25,45,65", "desc": "Persian Gulf/Red Sea"},
"arctic": {"bbox": "65,-180,90,180", "desc": "Arctic region"},
"korean_peninsula": {"bbox": "33,124,43,132", "desc": "Korean DMZ"},
}
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _is_military_icao(icao24: str) -> bool:
"""Check whether an ICAO24 hex address falls within known military ranges."""
upper = icao24.upper()
for prefix in MILITARY_ICAO_PREFIXES:
if upper.startswith(prefix):
return True
return False
def _is_military_callsign(callsign: str | None) -> bool:
"""Check whether a callsign matches known military callsign patterns."""
if not callsign:
return False
cs = callsign.strip().upper()
for prefix in MILITARY_CALLSIGN_PREFIXES:
if cs.startswith(prefix):
return True
return False
def _build_opensky_auth_headers() -> dict[str, str] | None:
"""Build HTTP Basic auth header from OpenSky env vars, or None if unset."""
client_id = os.environ.get("OPENSKY_CLIENT_ID")
client_secret = os.environ.get("OPENSKY_CLIENT_SECRET")
if client_id and client_secret:
credentials = base64.b64encode(
f"{client_id}:{client_secret}".encode()
).decode()
return {"Authorization": f"Basic {credentials}"}
return None
def _extract_aircraft(state: list) -> dict:
"""Extract a structured aircraft dict from an OpenSky state vector."""
return {
"icao24": state[0],
"callsign": state[1].strip() if state[1] else None,
"origin_country": state[2],
"latitude": state[6],
"longitude": state[5],
"altitude_m": state[7],
"velocity_ms": state[9],
"heading": state[10],
"on_ground": state[8],
"squawk": state[14],
}
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
async def _fetch_adsblol_military(fetcher: Fetcher) -> list[dict] | None:
"""Fetch military aircraft from adsb.lol free API (pre-filtered)."""
data = await fetcher.get_json(
url=_ADSBLOL_MIL_URL,
source="adsblol",
cache_key="military:adsblol:mil",
cache_ttl=300,
)
if data is None:
return None
aircraft: list[dict] = []
for ac in data.get("ac") or []:
lat = ac.get("lat")
lon = ac.get("lon")
if lat is None or lon is None:
continue
icao24 = ac.get("hex", "")
aircraft.append({
"icao24": icao24,
"callsign": (ac.get("flight") or "").strip() or None,
"origin_country": _icao_to_country(icao24),
"latitude": lat,
"longitude": lon,
"altitude_m": ac.get("alt_baro"),
"velocity_ms": ac.get("gs"),
"heading": ac.get("track"),
"on_ground": ac.get("alt_baro") == "ground",
"squawk": ac.get("squawk"),
"aircraft_type": ac.get("t"),
"registration": ac.get("r"),
})
return aircraft
async def _fetch_opensky_military(
fetcher: Fetcher, bbox: str | None = None,
) -> list[dict] | None:
"""Fetch military aircraft from OpenSky (requires filtering)."""
params: dict[str, str] = {}
if bbox is not None:
parts = bbox.split(",")
if len(parts) == 4:
params["lamin"] = parts[0]
params["lomin"] = parts[1]
params["lamax"] = parts[2]
params["lomax"] = parts[3]
headers = _build_opensky_auth_headers()
cache_label = bbox or "global"
data = await fetcher.get_json(
url=_OPENSKY_STATES_URL,
source="opensky",
cache_key=f"military:flights:{cache_label}",
cache_ttl=300,
headers=headers,
params=params if params else None,
)
if data is None:
return None
states = data.get("states") or []
aircraft: list[dict] = []
for state in states:
if not state or len(state) < 17:
continue
icao24 = state[0] or ""
callsign = state[1] or ""
if _is_military_icao(icao24) or _is_military_callsign(callsign):
aircraft.append(_extract_aircraft(state))
return aircraft
async def fetch_military_flights(
fetcher: Fetcher,
bbox: str | None = None,
) -> dict:
"""Fetch current military aircraft positions.
Tries adsb.lol (free, pre-filtered military endpoint) first,
falls back to OpenSky Network if unavailable.
Args:
fetcher: Shared HTTP fetcher with caching and circuit breaking.
bbox: Optional bounding box as "lamin,lomin,lamax,lomax" (OpenSky only).
Returns:
Dict with aircraft list, count, filter description, source, and timestamp.
"""
# Primary: adsb.lol (free, dedicated military endpoint, no filtering needed)
aircraft = await _fetch_adsblol_military(fetcher)
if aircraft is not None and len(aircraft) > 0:
# Apply bbox filter client-side if requested
if bbox is not None:
parts = bbox.split(",")
if len(parts) == 4:
la_min, lo_min, la_max, lo_max = map(float, parts)
aircraft = [
a for a in aircraft
if a["latitude"] is not None and a["longitude"] is not None
and la_min <= a["latitude"] <= la_max
and lo_min <= a["longitude"] <= lo_max
]
return {
"aircraft": aircraft,
"count": len(aircraft),
"military_filter": "adsblol_mil_endpoint",
"source": "adsb.lol",
"timestamp": _utc_now_iso(),
}
# Fallback: OpenSky Network
logger.debug("adsb.lol unavailable, trying OpenSky")
aircraft = await _fetch_opensky_military(fetcher, bbox)
if aircraft is not None:
return {
"aircraft": aircraft,
"count": len(aircraft),
"military_filter": "icao_prefix+callsign",
"source": "opensky",
"timestamp": _utc_now_iso(),
}
return {
"aircraft": [],
"count": 0,
"military_filter": "icao_prefix+callsign",
"source": "none",
"timestamp": _utc_now_iso(),
}
async def fetch_theater_posture(fetcher: Fetcher) -> dict:
"""Assess global military air posture across 5 theater regions.
Calls fetch_military_flights for each theater in parallel and aggregates
the results into a per-theater summary.
Args:
fetcher: Shared HTTP fetcher with caching and circuit breaking.
Returns:
Dict with per-theater summaries, total count, source, and timestamp.
"""
# Check cache first to avoid redundant parallel fetches.
cached = fetcher.cache.get("military:posture")
if cached is not None:
return cached
async def _fetch_theater(name: str, info: dict) -> tuple[str, dict]:
result = await fetch_military_flights(fetcher, bbox=info["bbox"])
aircraft_list = result.get("aircraft", [])
countries: set[str] = set()
callsigns: list[str] = []
for ac in aircraft_list:
if ac.get("origin_country"):
countries.add(ac["origin_country"])
if ac.get("callsign"):
callsigns.append(ac["callsign"])
return (name, {
"count": len(aircraft_list),
"countries": sorted(countries),
"sample_callsigns": callsigns[:5],
"bbox": info["bbox"],
"description": info["desc"],
})
tasks = [
_fetch_theater(name, info) for name, info in THEATERS.items()
]
results = await asyncio.gather(*tasks)
theaters: dict[str, dict] = {}
total = 0
for name, summary in results:
theaters[name] = summary
total += summary["count"]
response = {
"theaters": theaters,
"total_military_aircraft": total,
"source": "opensky",
"timestamp": _utc_now_iso(),
}
fetcher.cache.set("military:posture", response, 300)
return response
async def fetch_aircraft_details_batch(fetcher: Fetcher, icao24_list: list[str]) -> dict:
"""Look up multiple aircraft by ICAO24 hex codes in parallel.
Args:
fetcher: Shared HTTP fetcher.
icao24_list: List of ICAO24 hex addresses (max 20).
Returns:
Dict with aircraft[] details, count, source, timestamp.
"""
icao24_list = icao24_list[:20] # cap at 20
async def _lookup(icao: str) -> dict | None:
result = await fetch_aircraft_details(fetcher, icao)
ac = result.get("aircraft", {})
if not ac:
return None
ac["icao24"] = icao
return ac
tasks = [_lookup(icao) for icao in icao24_list]
results = await asyncio.gather(*tasks)
aircraft = [a for a in results if a is not None]
return {
"aircraft": aircraft,
"count": len(aircraft),
"requested": len(icao24_list),
"source": "hexdb",
"timestamp": _utc_now_iso(),
}
async def fetch_aircraft_details(fetcher: Fetcher, icao24: str) -> dict:
"""Look up detailed aircraft information from hexdb.io (free, no API key).
Args:
fetcher: Shared HTTP fetcher with caching and circuit breaking.
icao24: The ICAO 24-bit hex address of the aircraft.
Returns:
Dict with aircraft detail payload, source, and timestamp.
"""
url = f"{_HEXDB_BASE_URL}/{icao24}"
data = await fetcher.get_json(
url=url,
source="hexdb",
cache_key=f"military:aircraft:{icao24}",
cache_ttl=3600,
)
if data is None:
logger.warning("hexdb.io returned no data for icao24=%s", icao24)
return {
"aircraft": {},
"source": "hexdb",
"timestamp": _utc_now_iso(),
}
return {
"aircraft": data,
"source": "hexdb",
"timestamp": _utc_now_iso(),
}