index.tsâĸ15.3 kB
#!/usr/bin/env node
import { config } from "dotenv";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
config();
interface AircraftData {
hex: string;
reg?: string;
type?: string;
t?: string;
squawk?: string;
callsign?: string;
lat?: number;
lon?: number;
alt_baro?: number | "ground";
gs?: number;
track?: number;
seen?: number;
}
interface ADSBResponse {
ac?: AircraftData[];
[key: string]: any;
}
class FlightMCPServer {
private server: Server;
private rapidApiKey: string;
private baseUrl = "https://adsbexchange-com1.p.rapidapi.com/v2";
constructor() {
this.rapidApiKey = process.env.RAPIDAPI_KEY || "";
if (!this.rapidApiKey) {
throw new Error("RAPIDAPI_KEY environment variable is required");
}
this.server = new Server(
{
name: "flight-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private async makeApiRequest(endpoint: string): Promise<ADSBResponse> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
"x-rapidapi-host": "adsbexchange-com1.p.rapidapi.com",
"x-rapidapi-key": this.rapidApiKey,
},
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
private filterAircraftByRegion(aircraft: AircraftData[], region: string): AircraftData[] {
if (region === "all") return aircraft;
return aircraft.filter(ac => {
if (!ac.lat || !ac.lon) return false;
switch (region) {
case "us":
return ac.lat >= 25 && ac.lat <= 70 && ac.lon >= -170 && ac.lon <= -65;
case "europe":
return ac.lat >= 35 && ac.lat <= 75 && ac.lon >= -15 && ac.lon <= 40;
case "asia":
return ac.lat >= 10 && ac.lat <= 60 && ac.lon >= 70 && ac.lon <= 180;
default:
return true;
}
});
}
private formatMilitaryAircraft(aircraft: AircraftData): any {
return {
hex: aircraft.hex,
callsign: aircraft.callsign?.trim() || null,
registration: aircraft.reg || null,
type: aircraft.t || null,
position: aircraft.lat && aircraft.lon ? {
lat: Math.round(aircraft.lat * 1000) / 1000,
lon: Math.round(aircraft.lon * 1000) / 1000
} : null,
altitude: aircraft.alt_baro === "ground" ? "ground" : aircraft.alt_baro || null,
speed: aircraft.gs || null,
heading: aircraft.track || null,
squawk: aircraft.squawk || null,
seen_seconds: aircraft.seen || null
};
}
private setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "search_aircraft",
description: "Return live ADS-B targets within a given radius of a point.",
inputSchema: {
type: "object",
properties: {
lat: {
type: "number",
minimum: -90,
maximum: 90,
description: "Latitude in decimal degrees"
},
lon: {
type: "number",
minimum: -180,
maximum: 180,
description: "Longitude in decimal degrees"
},
radius_nm: {
type: "number",
minimum: 0,
maximum: 250,
description: "Search radius in nautical miles (typical buckets 1,5,10,25,50,100,250; any positive value accepted)"
}
},
required: ["lat", "lon", "radius_nm"]
}
},
{
name: "get_aircraft",
description: "Fetch the latest position & metadata for one aircraft by 6-digit ICAO hex.",
inputSchema: {
type: "object",
properties: {
hex: {
type: "string",
pattern: "^[0-9A-Fa-f]{6}$",
description: "6-character ICAO hex code"
}
},
required: ["hex"]
}
},
{
name: "list_military_aircraft",
description: "Return a filtered list of military aircraft with essential information only.",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
minimum: 1,
maximum: 50,
default: 20,
description: "Maximum number of aircraft to return per page (default: 20)"
},
page: {
type: "number",
minimum: 1,
default: 1,
description: "Page number for pagination (1-based, default: 1)"
},
min_altitude: {
type: "number",
minimum: 0,
description: "Minimum altitude in feet (filters out ground aircraft)"
},
region: {
type: "string",
enum: ["us", "europe", "asia", "all"],
default: "all",
description: "Geographic region filter"
},
aircraft_type: {
type: "string",
description: "Filter by aircraft type. Use 'types_only' to see available types, or partial matches like: H60 (helicopters), C17/C130/C27J (cargo), K35R/KC135 (tankers), F16/F35 (fighters), B52 (bombers), V22 (tiltrotor)"
},
show_available_types: {
type: "boolean",
default: false,
description: "Set to true to return only a list of available aircraft types in the current dataset"
}
}
}
},
{
name: "get_military_aircraft_types",
description: "Get a list of all military aircraft types currently active to help with filtering.",
inputSchema: {
type: "object",
properties: {
region: {
type: "string",
enum: ["us", "europe", "asia", "all"],
default: "all",
description: "Geographic region to check for aircraft types"
}
}
}
}
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "search_aircraft": {
const searchSchema = z.object({
lat: z.number().min(-90).max(90),
lon: z.number().min(-180).max(180),
radius_nm: z.number().min(0).max(250),
});
const { lat, lon, radius_nm } = searchSchema.parse(args);
const endpoint = `/lat/${lat}/lon/${lon}/dist/${Math.round(radius_nm)}`;
const data = await this.makeApiRequest(endpoint);
return {
content: [
{
type: "text",
text: JSON.stringify({
query: { lat, lon, radius_nm },
aircraft_count: data.ac?.length || 0,
aircraft: data.ac || []
}, null, 2),
},
],
};
}
case "get_aircraft": {
const aircraftSchema = z.object({
hex: z.string().regex(/^[0-9A-Fa-f]{6}$/),
});
const { hex } = aircraftSchema.parse(args);
const endpoint = `/icao/${hex}`;
const data = await this.makeApiRequest(endpoint);
return {
content: [
{
type: "text",
text: JSON.stringify({
query: { hex },
aircraft: data.ac?.[0] || null
}, null, 2),
},
],
};
}
case "list_military_aircraft": {
const schema = z.object({
limit: z.number().min(1).max(50).default(20),
page: z.number().min(1).default(1),
min_altitude: z.number().min(0).optional(),
region: z.enum(["us", "europe", "asia", "all"]).default("all"),
aircraft_type: z.string().optional(),
show_available_types: z.boolean().default(false)
});
const filters = schema.parse(args);
const endpoint = "/mil/";
const data = await this.makeApiRequest(endpoint);
let filtered = data.ac || [];
// Apply regional filter
filtered = this.filterAircraftByRegion(filtered, filters.region);
// If only showing types, return unique aircraft types
if (filters.show_available_types) {
const types = [...new Set(filtered.map(ac => ac.t).filter(Boolean))].sort();
const typeCounts = types.map(type => ({
type,
count: filtered.filter(ac => ac.t === type).length,
examples: filtered.filter(ac => ac.t === type && ac.callsign).slice(0, 3).map(ac => ac.callsign?.trim()).filter(Boolean)
}));
return {
content: [
{
type: "text",
text: JSON.stringify({
region: filters.region,
total_aircraft: filtered.length,
available_types: typeCounts
}, null, 2),
},
],
};
}
// Filter by minimum altitude
if (filters.min_altitude !== undefined) {
filtered = filtered.filter(ac =>
ac.alt_baro !== "ground" &&
ac.alt_baro !== undefined &&
ac.alt_baro >= filters.min_altitude!
);
}
// Filter by aircraft type (support partial matches)
if (filters.aircraft_type) {
const typeFilter = filters.aircraft_type.toUpperCase();
filtered = filtered.filter(ac =>
ac.t?.toUpperCase().includes(typeFilter)
);
}
// Sort by most recent activity (lowest 'seen' value)
filtered.sort((a, b) => (a.seen || 999) - (b.seen || 999));
// Calculate pagination
const totalFiltered = filtered.length;
const totalPages = Math.ceil(totalFiltered / filters.limit);
const startIndex = (filters.page - 1) * filters.limit;
const endIndex = startIndex + filters.limit;
// Apply pagination
const paginatedResults = filtered.slice(startIndex, endIndex);
// Format for cleaner output
const formattedAircraft = paginatedResults.map(ac => this.formatMilitaryAircraft(ac));
return {
content: [
{
type: "text",
text: JSON.stringify({
filters_applied: filters,
pagination: {
current_page: filters.page,
total_pages: totalPages,
per_page: filters.limit,
total_filtered: totalFiltered,
showing_range: `${startIndex + 1}-${Math.min(endIndex, totalFiltered)}`,
has_next_page: filters.page < totalPages,
has_previous_page: filters.page > 1
},
total_military_aircraft: data.ac?.length || 0,
aircraft: formattedAircraft
}, null, 2),
},
],
};
}
case "get_military_aircraft_types": {
const schema = z.object({
region: z.enum(["us", "europe", "asia", "all"]).default("all")
});
const filters = schema.parse(args);
const endpoint = "/mil/";
const data = await this.makeApiRequest(endpoint);
let filtered = data.ac || [];
// Apply regional filter
filtered = this.filterAircraftByRegion(filtered, filters.region);
// Get aircraft type statistics
const typeStats = new Map<string, {count: number, examples: string[]}>();
filtered.forEach(ac => {
if (ac.t) {
const existing = typeStats.get(ac.t) || {count: 0, examples: []};
existing.count++;
if (ac.callsign && existing.examples.length < 3) {
existing.examples.push(ac.callsign.trim());
}
typeStats.set(ac.t, existing);
}
});
const sortedTypes = Array.from(typeStats.entries())
.map(([type, stats]) => ({type, ...stats}))
.sort((a, b) => b.count - a.count);
return {
content: [
{
type: "text",
text: JSON.stringify({
region: filters.region,
total_military_aircraft: filtered.length,
aircraft_types: sortedTypes,
usage_examples: {
"Helicopters": "aircraft_type: 'H60' or 'H47'",
"Cargo planes": "aircraft_type: 'C17' or 'C130'",
"Tankers": "aircraft_type: 'K35' or 'KC135'",
"Fighters": "aircraft_type: 'F16' or 'F35'",
"Transport": "aircraft_type: 'C27J' or 'V22'"
}
}, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Flight MCP server running on stdio");
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
const server = new FlightMCPServer();
server.run().catch(console.error);
}