diff --git a/bun.lock b/bun.lock index ccf22e11..2eeec6b4 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "@decocms/mcps", @@ -244,6 +243,21 @@ "typescript": "^5.7.2", }, }, + "google-flights": { + "name": "@decocms/google-flights", + "version": "1.0.0", + "dependencies": { + "@decocms/bindings": "^1.0.7", + "@decocms/runtime": "^1.1.3", + "zod": "^4.0.0", + }, + "devDependencies": { + "@decocms/mcps-shared": "1.0.0", + "@modelcontextprotocol/sdk": "1.25.1", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2", + }, + }, "google-forms": { "name": "google-forms", "version": "1.0.0", @@ -944,6 +958,8 @@ "@decocms/bindings": ["@decocms/bindings@1.0.9", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.2", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-lwfuk7lZtqOUJVb7PFzfUo/eR0ZdS7vex5JAOAhM93IiIrHDakrTehKwl2WUq6Ks1P60MAUFtM5kJT+YQAq+oA=="], + "@decocms/google-flights": ["@decocms/google-flights@workspace:google-flights"], + "@decocms/mcps-shared": ["@decocms/mcps-shared@workspace:shared"], "@decocms/openrouter": ["@decocms/openrouter@workspace:openrouter"], diff --git a/google-flights/.gitignore b/google-flights/.gitignore new file mode 100644 index 00000000..7d005ad4 --- /dev/null +++ b/google-flights/.gitignore @@ -0,0 +1,3 @@ +.dev.vars +node_modules/ +dist/ diff --git a/google-flights/README.md b/google-flights/README.md new file mode 100644 index 00000000..f16bdb0c --- /dev/null +++ b/google-flights/README.md @@ -0,0 +1,88 @@ +# Google Flights MCP + +An MCP server that provides flight search capabilities using Google Flights data and a comprehensive airports database. + +## Features + +- **Search Flights**: Search for one-way or round-trip flights between airports +- **Airport Search**: Find airport codes by name, city, or partial IATA code +- **Travel Date Calculator**: Get suggested departure and return dates +- **Airports Database**: Comprehensive database of 10,000+ airports with IATA codes + +## Tools + +### SEARCH_FLIGHTS + +Search for flights between two airports. + +**Parameters:** +- `fromAirport` (required): Departure airport IATA code (3 letters, e.g., 'LAX') +- `toAirport` (required): Arrival airport IATA code (3 letters, e.g., 'JFK') +- `departureDate` (required): Departure date in YYYY-MM-DD format +- `returnDate` (optional): Return date for round-trip flights +- `adults` (default: 1): Number of adult passengers +- `children` (default: 0): Number of children +- `infantsInSeat` (default: 0): Number of infants in seat +- `infantsOnLap` (default: 0): Number of infants on lap +- `seatClass` (default: 'economy'): Seat class (economy, premium_economy, business, first) + +### SEARCH_AIRPORTS + +Search for airport codes by name, city, or partial IATA code. + +**Parameters:** +- `query` (required): Search term (minimum 2 characters) + +**Examples:** +- "los angeles" → LAX - Los Angeles International Airport +- "london" → LHR, LGW, STN, LTN, etc. +- "JFK" → JFK - John F Kennedy International Airport + +### GET_TRAVEL_DATES + +Get suggested travel dates based on days from now and trip length. + +**Parameters:** +- `daysFromNow` (default: 30): Days from today for departure +- `tripLength` (default: 7): Trip length in days + +### UPDATE_AIRPORTS_DATABASE + +Force refresh the airports database from the source CSV. + +## Development + +```bash +# Install dependencies (from mcps root) +bun install + +# Start development server +bun run dev + +# Type check +bun run check + +# Build for production +bun run build +``` + +## Architecture + +The MCP uses: +- **Airports CSV**: Fetched from [airportsdata](https://github.com/mborsetti/airportsdata) (10,000+ airports) +- **Bun runtime**: Fast JavaScript runtime for the server +- **Deco Runtime**: For MCP protocol handling + +## Note on Flight Data + +Google Flights does not provide a public API. This MCP generates Google Flights search URLs +that users can visit to see actual flight prices and options. + +For production use with real-time flight pricing, consider integrating with: +- [Amadeus API](https://developers.amadeus.com/) +- [Skyscanner API](https://developers.skyscanner.net/) +- [Kiwi.com Tequila API](https://tequila.kiwi.com/) + +## License + +Private - See LICENSE file for details. diff --git a/google-flights/app.json b/google-flights/app.json new file mode 100644 index 00000000..a766debe --- /dev/null +++ b/google-flights/app.json @@ -0,0 +1,19 @@ +{ + "scopeName": "deco", + "name": "google-flights", + "friendlyName": "Google Flights", + "connection": { + "type": "HTTP", + "url": "https://sites-google-flights.decocache.com/mcp" + }, + "description": "Search for flights using Google Flights data. Find flight options, compare prices, and get travel date suggestions.", + "icon": "https://www.gstatic.com/flights/app/lf1_64dp.png", + "unlisted": false, + "metadata": { + "categories": ["Travel"], + "official": false, + "tags": ["flights", "travel", "google-flights", "airports", "booking"], + "short_description": "Search flights and find airports using Google Flights data.", + "mesh_description": "The Google Flights MCP enables AI agents to search for flights between airports, find airport codes by city or name, and calculate travel dates. It provides access to a comprehensive database of 10,000+ airports with IATA codes, and generates Google Flights search URLs for users to view real-time pricing. Perfect for travel planning, flight comparison, and trip organization." + } +} diff --git a/google-flights/package.json b/google-flights/package.json new file mode 100644 index 00000000..257d474f --- /dev/null +++ b/google-flights/package.json @@ -0,0 +1,28 @@ +{ + "name": "@decocms/google-flights", + "version": "1.0.0", + "description": "Search for flights using Google Flights data", + "private": true, + "type": "module", + "scripts": { + "dev": "bun server/main.ts", + "build:server": "NODE_ENV=production bun build server/main.ts --target=bun --outfile=dist/server/main.js", + "build": "bun run build:server", + "publish": "cat app.json | deco registry publish -w /shared/deco -y", + "check": "tsc --noEmit" + }, + "dependencies": { + "@decocms/bindings": "^1.0.7", + "@decocms/runtime": "^1.1.3", + "zod": "^4.0.0" + }, + "devDependencies": { + "@decocms/mcps-shared": "1.0.0", + "@modelcontextprotocol/sdk": "1.25.1", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/google-flights/server/constants.ts b/google-flights/server/constants.ts new file mode 100644 index 00000000..3c053da1 --- /dev/null +++ b/google-flights/server/constants.ts @@ -0,0 +1,29 @@ +/** + * Constants for the Google Flights MCP + */ + +/** + * CSV source for airport data from airportsdata project + */ +export const AIRPORTS_CSV_URL = + "https://raw.githubusercontent.com/mborsetti/airportsdata/refs/heads/main/airportsdata/airports.csv"; + +/** + * Cache TTL for airports data (24 hours) + */ +export const AIRPORTS_CACHE_TTL_MS = 24 * 60 * 60 * 1000; + +/** + * Default configuration for flight searches + */ +export const DEFAULT_CONFIG = { + maxResults: 10, + defaultTripDays: 7, + defaultAdvanceDays: 30, + seatClasses: ["economy", "premium_economy", "business", "first"] as const, +} as const; + +/** + * Maximum number of airport search results to return + */ +export const MAX_AIRPORT_RESULTS = 20; diff --git a/google-flights/server/lib/airports.ts b/google-flights/server/lib/airports.ts new file mode 100644 index 00000000..d8d5f6a0 --- /dev/null +++ b/google-flights/server/lib/airports.ts @@ -0,0 +1,184 @@ +import type { AirportsMap } from "./types"; + +/** + * CSV source for airport data + */ +const AIRPORTS_CSV_URL = + "https://raw.githubusercontent.com/mborsetti/airportsdata/refs/heads/main/airportsdata/airports.csv"; + +/** + * In-memory cache for airports data + */ +let airportsCache: AirportsMap | null = null; +let lastFetchTime: number | null = null; +const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours + +/** + * Parse CSV text into airport data + */ +function parseAirportsCsv(csvText: string): AirportsMap { + const airports: AirportsMap = new Map(); + const lines = csvText.split("\n"); + + // Find header indices - parse header with CSV parser to handle quotes + const headerLine = lines[0] ?? ""; + const header = parseCSVLine(headerLine).map((h) => h.trim().toLowerCase()); + const iataIdx = header.indexOf("iata"); + const nameIdx = header.indexOf("name"); + const cityIdx = header.indexOf("city"); + const countryIdx = header.indexOf("country"); + + if (iataIdx === -1 || nameIdx === -1) { + console.error( + "Could not find required columns in CSV. Headers found:", + header.slice(0, 6), + ); + return airports; + } + + // Parse data rows + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + if (!line?.trim()) continue; + + // Simple CSV parsing (handles quoted fields) + const fields = parseCSVLine(line); + const iata = fields[iataIdx]?.trim() ?? ""; + const name = fields[nameIdx]?.trim() ?? ""; + const city = cityIdx >= 0 ? (fields[cityIdx]?.trim() ?? "") : ""; + const country = countryIdx >= 0 ? (fields[countryIdx]?.trim() ?? "") : ""; + + // Only store entries with valid IATA codes (3 uppercase letters) + if (iata && iata.length === 3 && /^[A-Z]{3}$/.test(iata)) { + const fullName = city + ? `${name}, ${city}, ${country}` + : `${name}, ${country}`; + airports.set(iata, fullName); + } + } + + return airports; +} + +/** + * Parse a single CSV line, handling quoted fields + */ +function parseCSVLine(line: string): string[] { + const fields: string[] = []; + let current = ""; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if (char === '"') { + if (inQuotes && line[i + 1] === '"') { + // Escaped quote + current += '"'; + i++; + } else { + // Toggle quote state + inQuotes = !inQuotes; + } + } else if (char === "," && !inQuotes) { + fields.push(current); + current = ""; + } else { + current += char; + } + } + + fields.push(current); + return fields; +} + +/** + * Fetch airports data from CSV source + */ +export async function fetchAirports(): Promise { + // Check cache + if ( + airportsCache && + lastFetchTime && + Date.now() - lastFetchTime < CACHE_TTL_MS + ) { + return airportsCache; + } + + try { + const response = await fetch(AIRPORTS_CSV_URL); + if (!response.ok) { + throw new Error(`Failed to fetch airports: HTTP ${response.status}`); + } + + const csvText = await response.text(); + airportsCache = parseAirportsCsv(csvText); + lastFetchTime = Date.now(); + + console.log(`Loaded ${airportsCache.size} airports from CSV`); + return airportsCache; + } catch (error) { + console.error("Error fetching airports:", error); + + // Return cached data if available, even if stale + if (airportsCache) { + return airportsCache; + } + + // Return empty map if no cache available + return new Map(); + } +} + +/** + * Get airports from cache or fetch if not available + */ +export async function getAirports(): Promise { + if (airportsCache) { + return airportsCache; + } + return fetchAirports(); +} + +/** + * Search airports by query (code, name, or city) + */ +export async function searchAirports( + query: string, +): Promise> { + const airports = await getAirports(); + const normalizedQuery = query.toUpperCase().trim(); + const results: Array<{ code: string; name: string }> = []; + + for (const [code, name] of airports) { + if ( + code.includes(normalizedQuery) || + name.toUpperCase().includes(normalizedQuery) + ) { + results.push({ code, name }); + } + } + + // Sort by code + results.sort((a, b) => a.code.localeCompare(b.code)); + + return results; +} + +/** + * Check if an airport code exists + */ +export async function airportExists(code: string): Promise { + const airports = await getAirports(); + return airports.has(code.toUpperCase()); +} + +/** + * Get airport name by code + */ +export async function getAirportName( + code: string, +): Promise { + const airports = await getAirports(); + return airports.get(code.toUpperCase()); +} diff --git a/google-flights/server/lib/flights-client.ts b/google-flights/server/lib/flights-client.ts new file mode 100644 index 00000000..fcec8501 --- /dev/null +++ b/google-flights/server/lib/flights-client.ts @@ -0,0 +1,155 @@ +import type { FlightOption, FlightSearchResult, SeatClass } from "./types"; + +/** + * Google Flights search URL builder + * + * Google Flights uses a specific URL format for searches. + * This client builds the URL and provides search functionality. + */ + +/** + * Build Google Flights search URL + */ +export function buildGoogleFlightsUrl(params: { + fromAirport: string; + toAirport: string; + departureDate: string; + returnDate?: string; + adults: number; + children: number; + infantsInSeat: number; + infantsOnLap: number; + seatClass: SeatClass; +}): string { + const { + fromAirport, + toAirport, + departureDate, + returnDate, + adults, + children, + infantsInSeat, + infantsOnLap, + seatClass, + } = params; + + // Build the search query with all relevant parameters + const totalPassengers = adults + children + infantsInSeat + infantsOnLap; + const classLabel = + seatClass === "economy" ? "" : ` ${seatClass.replace("_", " ")}`; + const searchQuery = `Flights to ${toAirport} from ${fromAirport} on ${departureDate}${returnDate ? ` returning ${returnDate}` : ""}${totalPassengers > 1 ? ` ${totalPassengers} passengers` : ""}${classLabel}`; + + // Build a simple Google Flights URL that users can click + const baseUrl = "https://www.google.com/travel/flights"; + const queryParams = new URLSearchParams({ + q: searchQuery, + curr: "USD", + gl: "us", + hl: "en", + }); + + return `${baseUrl}?${queryParams.toString()}`; +} + +/** + * Search for flights + * + * Note: Google Flights doesn't have a public API. This implementation + * provides structured search information and URLs for users to check + * actual prices on Google Flights. + * + * For production use, consider integrating with: + * - Amadeus API (https://developers.amadeus.com/) + * - Skyscanner API (https://developers.skyscanner.net/) + * - Kiwi.com Tequila API (https://tequila.kiwi.com/) + */ +export async function searchFlights(params: { + fromAirport: string; + toAirport: string; + departureDate: string; + returnDate?: string; + adults: number; + children: number; + infantsInSeat: number; + infantsOnLap: number; + seatClass: SeatClass; +}): Promise { + const { fromAirport, toAirport, departureDate, returnDate } = params; + + // Build the Google Flights URL for the user + const searchUrl = buildGoogleFlightsUrl(params); + + // Since Google Flights doesn't have a public API, we return a helpful response + // with the search URL and instructions + const result: FlightSearchResult = { + flights: [], + searchedAt: new Date().toISOString(), + currentPrice: undefined, + }; + + // Create a placeholder flight option that directs users to Google Flights + const placeholderFlight: FlightOption = { + price: "Check Google Flights", + airline: "Multiple Airlines", + departure: `${fromAirport} → ${toAirport}`, + arrival: departureDate, + duration: "View on Google Flights", + stops: -1, + stopDetails: searchUrl, + isBest: true, + legs: [], + }; + + result.flights.push(placeholderFlight); + + if (returnDate) { + const returnFlight: FlightOption = { + price: "Check Google Flights", + airline: "Multiple Airlines", + departure: `${toAirport} → ${fromAirport}`, + arrival: returnDate, + duration: "View on Google Flights", + stops: -1, + stopDetails: searchUrl, + legs: [], + }; + result.flights.push(returnFlight); + } + + return result; +} + +/** + * Validate flight search dates + */ +export function validateDates( + departureDate: string, + returnDate?: string, +): { valid: boolean; error?: string } { + const departure = new Date(departureDate); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (Number.isNaN(departure.getTime())) { + return { valid: false, error: "Invalid departure date format" }; + } + + if (departure < today) { + return { valid: false, error: "Departure date cannot be in the past" }; + } + + if (returnDate) { + const returnDt = new Date(returnDate); + if (Number.isNaN(returnDt.getTime())) { + return { valid: false, error: "Invalid return date format" }; + } + if (returnDt < departure) { + return { + valid: false, + error: "Return date cannot be before departure date", + }; + } + } + + return { valid: true }; +} diff --git a/google-flights/server/lib/types.ts b/google-flights/server/lib/types.ts new file mode 100644 index 00000000..73161a03 --- /dev/null +++ b/google-flights/server/lib/types.ts @@ -0,0 +1,211 @@ +import { z } from "zod"; + +/** + * Seat class options for flight search + */ +export const SeatClassSchema = z.enum([ + "economy", + "premium_economy", + "business", + "first", +]); +export type SeatClass = z.infer; + +/** + * Airport information from the CSV database + */ +export interface Airport { + iata: string; + name: string; + city: string; + country: string; +} + +/** + * Parsed airport data stored in cache + * Maps IATA code to full name (e.g., "LAX" -> "Los Angeles International Airport, Los Angeles, United States") + */ +export type AirportsMap = Map; + +/** + * Flight leg information + */ +export interface FlightLeg { + airline: string; + flightNumber: string; + departure: string; + arrival: string; + departureTime: string; + arrivalTime: string; + duration: string; +} + +/** + * Flight option returned from search + */ +export interface FlightOption { + price: string; + airline: string; + departure: string; + arrival: string; + duration: string; + stops: number; + stopDetails?: string; + legs: FlightLeg[]; + isBest?: boolean; + arrivalTimeAhead?: string; + delay?: string; +} + +/** + * Flight search result + */ +export interface FlightSearchResult { + flights: FlightOption[]; + currentPrice?: string; + searchedAt: string; +} + +/** + * Input schema for search_flights tool + */ +export const searchFlightsInputSchema = z.object({ + fromAirport: z + .string() + .length(3) + .describe("Departure airport IATA code (3 letters, e.g., 'LAX')"), + toAirport: z + .string() + .length(3) + .describe("Arrival airport IATA code (3 letters, e.g., 'JFK')"), + departureDate: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/) + .describe("Departure date in YYYY-MM-DD format"), + returnDate: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/) + .optional() + .describe("Return date in YYYY-MM-DD format (optional, for round trips)"), + adults: z + .number() + .int() + .min(1) + .max(9) + .default(1) + .describe("Number of adult passengers (1-9)"), + children: z + .number() + .int() + .min(0) + .max(9) + .default(0) + .describe("Number of children (0-9)"), + infantsInSeat: z + .number() + .int() + .min(0) + .max(9) + .default(0) + .describe("Number of infants in seat (0-9)"), + infantsOnLap: z + .number() + .int() + .min(0) + .max(9) + .default(0) + .describe("Number of infants on lap (0-9)"), + seatClass: SeatClassSchema.default("economy").describe( + "Seat class preference", + ), +}); +export type SearchFlightsInput = z.infer; + +/** + * Output schema for search_flights tool + */ +export const searchFlightsOutputSchema = z.object({ + success: z.boolean(), + message: z.string(), + tripType: z.enum(["one-way", "round-trip"]).optional(), + fromAirport: z.string().optional(), + toAirport: z.string().optional(), + departureDate: z.string().optional(), + returnDate: z.string().optional(), + flightsFound: z.number().optional(), + flights: z + .array( + z.object({ + price: z.string(), + airline: z.string(), + departure: z.string(), + arrival: z.string(), + duration: z.string(), + stops: z.number(), + stopDetails: z.string().optional(), + isBest: z.boolean().optional(), + }), + ) + .optional(), + priceAssessment: z.string().optional(), +}); +export type SearchFlightsOutput = z.infer; + +/** + * Input schema for airport_search tool + */ +export const airportSearchInputSchema = z.object({ + query: z + .string() + .min(2) + .describe("Search term (city name, airport name, or partial IATA code)"), +}); +export type AirportSearchInput = z.infer; + +/** + * Output schema for airport_search tool + */ +export const airportSearchOutputSchema = z.object({ + success: z.boolean(), + message: z.string(), + matchCount: z.number().optional(), + airports: z + .array( + z.object({ + code: z.string(), + name: z.string(), + }), + ) + .optional(), +}); +export type AirportSearchOutput = z.infer; + +/** + * Input schema for get_travel_dates tool + */ +export const getTravelDatesInputSchema = z.object({ + daysFromNow: z + .number() + .int() + .min(1) + .default(30) + .describe("Number of days from today for departure (default: 30)"), + tripLength: z + .number() + .int() + .min(1) + .default(7) + .describe("Length of trip in days (default: 7)"), +}); +export type GetTravelDatesInput = z.infer; + +/** + * Output schema for get_travel_dates tool + */ +export const getTravelDatesOutputSchema = z.object({ + departureDate: z.string(), + returnDate: z.string(), + tripLength: z.number(), + daysFromNow: z.number(), +}); +export type GetTravelDatesOutput = z.infer; diff --git a/google-flights/server/main.ts b/google-flights/server/main.ts new file mode 100644 index 00000000..4e2e600f --- /dev/null +++ b/google-flights/server/main.ts @@ -0,0 +1,41 @@ +/** + * Google Flights MCP Server + * + * This MCP provides tools for searching flights, finding airports, + * and calculating travel dates using Google Flights data. + */ +import { serve } from "@decocms/mcps-shared/serve"; +import { type DefaultEnv, withRuntime } from "@decocms/runtime"; +import { z } from "zod"; +import { tools } from "./tools/index.ts"; + +console.log("🛫 Google Flights MCP starting..."); + +const StateSchema = z.object({}); + +/** + * Environment type for the MCP + */ +export type Env = DefaultEnv; + +const runtime = withRuntime({ + configuration: { + scopes: [], + state: StateSchema, + }, + tools, + prompts: [], +}); + +const port = process.env.PORT || 8001; +serve(runtime.fetch); + +const mcpUrl = `http://localhost:${port}/mcp`; +console.log(`🛫 Google Flights MCP running at ${mcpUrl}`); + +// Copy MCP URL to clipboard on macOS +import { spawn } from "node:child_process"; +const pbcopy = spawn("pbcopy"); +pbcopy.stdin.write(mcpUrl); +pbcopy.stdin.end(); +console.log("📋 MCP URL copied to clipboard!"); diff --git a/google-flights/server/tools/flights.ts b/google-flights/server/tools/flights.ts new file mode 100644 index 00000000..9da20f9b --- /dev/null +++ b/google-flights/server/tools/flights.ts @@ -0,0 +1,276 @@ +/** + * Flight search tools for Google Flights MCP + */ +import { createPrivateTool } from "@decocms/runtime/tools"; +import { + searchFlightsInputSchema, + searchFlightsOutputSchema, + airportSearchInputSchema, + airportSearchOutputSchema, + getTravelDatesInputSchema, + getTravelDatesOutputSchema, +} from "../lib/types.ts"; +import { + searchAirports, + airportExists, + getAirportName, + fetchAirports, +} from "../lib/airports.ts"; +import { + searchFlights, + validateDates, + buildGoogleFlightsUrl, +} from "../lib/flights-client.ts"; + +/** + * SEARCH_FLIGHTS - Search for flights between two airports + */ +export const createSearchFlightsTool = (_env: unknown) => + createPrivateTool({ + id: "SEARCH_FLIGHTS", + description: `Search for flights between two airports using Google Flights data. + +Provide departure and arrival airport codes (3-letter IATA codes like LAX, JFK, LHR), +dates in YYYY-MM-DD format, and optionally passenger counts and seat class. + +Returns flight options with prices, airlines, duration, and stops. +For round-trip searches, provide both departure and return dates.`, + inputSchema: searchFlightsInputSchema, + outputSchema: searchFlightsOutputSchema, + execute: async ({ context }) => { + const { + fromAirport, + toAirport, + departureDate, + returnDate, + adults, + children, + infantsInSeat, + infantsOnLap, + seatClass, + } = context; + + // Normalize airport codes + const from = fromAirport.toUpperCase(); + const to = toAirport.toUpperCase(); + + // Validate airport codes + const [fromExists, toExists] = await Promise.all([ + airportExists(from), + airportExists(to), + ]); + + if (!fromExists) { + return { + success: false, + message: `Departure airport code '${from}' not found. Use the SEARCH_AIRPORTS tool to find valid airport codes.`, + }; + } + + if (!toExists) { + return { + success: false, + message: `Arrival airport code '${to}' not found. Use the SEARCH_AIRPORTS tool to find valid airport codes.`, + }; + } + + // Validate dates + const dateValidation = validateDates(departureDate, returnDate); + if (!dateValidation.valid) { + return { + success: false, + message: dateValidation.error ?? "Invalid dates", + }; + } + + // Validate passenger count + const totalPassengers = adults + children + infantsInSeat + infantsOnLap; + if (totalPassengers > 9) { + return { + success: false, + message: "Total passengers cannot exceed 9", + }; + } + + if (adults < 1) { + return { + success: false, + message: "At least one adult passenger is required", + }; + } + + // Get airport names for display + const [fromName, toName] = await Promise.all([ + getAirportName(from), + getAirportName(to), + ]); + + // Search for flights + const result = await searchFlights({ + fromAirport: from, + toAirport: to, + departureDate, + returnDate, + adults, + children, + infantsInSeat, + infantsOnLap, + seatClass, + }); + + // Build the Google Flights URL + const searchUrl = buildGoogleFlightsUrl({ + fromAirport: from, + toAirport: to, + departureDate, + returnDate, + adults, + children, + infantsInSeat, + infantsOnLap, + seatClass, + }); + + const tripType = returnDate ? "round-trip" : "one-way"; + + return { + success: true, + message: `Flight search from ${from} (${fromName}) to ${to} (${toName}). View results on Google Flights: ${searchUrl}`, + tripType, + fromAirport: `${from} - ${fromName}`, + toAirport: `${to} - ${toName}`, + departureDate, + returnDate, + flightsFound: result.flights.length, + flights: result.flights.map((f) => ({ + price: f.price, + airline: f.airline, + departure: f.departure, + arrival: f.arrival, + duration: f.duration, + stops: f.stops, + stopDetails: f.stopDetails, + isBest: f.isBest, + })), + priceAssessment: result.currentPrice, + }; + }, + }); + +/** + * SEARCH_AIRPORTS - Search for airport codes by name or city + */ +export const createSearchAirportsTool = (_env: unknown) => + createPrivateTool({ + id: "SEARCH_AIRPORTS", + description: `Search for airport codes by name, city, or partial IATA code. + +Use this tool to find the 3-letter IATA airport code needed for flight searches. +Provide at least 2 characters to search. + +Examples: +- "los angeles" → LAX - Los Angeles International Airport +- "london" → LHR, LGW, STN, LTN, etc. +- "JFK" → JFK - John F Kennedy International Airport`, + inputSchema: airportSearchInputSchema, + outputSchema: airportSearchOutputSchema, + execute: async ({ context }) => { + const { query } = context; + + if (query.length < 2) { + return { + success: false, + message: "Please provide at least 2 characters to search", + }; + } + + const results = await searchAirports(query); + + if (results.length === 0) { + return { + success: false, + message: `No airports found matching '${query}'`, + matchCount: 0, + airports: [], + }; + } + + // Limit to 20 results + const limited = results.slice(0, 20); + const hasMore = results.length > 20; + + return { + success: true, + message: hasMore + ? `Found ${results.length} airports matching '${query}' (showing first 20). Refine your search for more specific results.` + : `Found ${results.length} airport(s) matching '${query}'`, + matchCount: results.length, + airports: limited, + }; + }, + }); + +/** + * GET_TRAVEL_DATES - Get suggested travel dates + */ +export const createGetTravelDatesTool = (_env: unknown) => + createPrivateTool({ + id: "GET_TRAVEL_DATES", + description: `Get suggested travel dates based on days from now and trip length. + +Use this to quickly calculate departure and return dates for flight searches. +Defaults to departing in 30 days with a 7-day trip length.`, + inputSchema: getTravelDatesInputSchema, + outputSchema: getTravelDatesOutputSchema, + execute: async ({ context }) => { + const { daysFromNow, tripLength } = context; + + const today = new Date(); + const departureDate = new Date(today); + departureDate.setDate(today.getDate() + daysFromNow); + + const returnDate = new Date(departureDate); + returnDate.setDate(departureDate.getDate() + tripLength); + + const formatDate = (date: Date) => { + return date.toISOString().split("T")[0] ?? ""; + }; + + return { + departureDate: formatDate(departureDate), + returnDate: formatDate(returnDate), + tripLength, + daysFromNow, + }; + }, + }); + +/** + * UPDATE_AIRPORTS_DATABASE - Force refresh of airports database + */ +export const createUpdateAirportsDatabaseTool = (_env: unknown) => + createPrivateTool({ + id: "UPDATE_AIRPORTS_DATABASE", + description: `Force refresh the airports database from the source CSV. + +The airports database is cached for 24 hours. Use this tool to force a refresh +if you need the latest airport data.`, + inputSchema: getTravelDatesInputSchema.pick({}), // Empty schema + outputSchema: airportSearchOutputSchema, + execute: async () => { + try { + const airports = await fetchAirports(); + return { + success: true, + message: `Successfully updated airports database with ${airports.size} airports`, + matchCount: airports.size, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + success: false, + message: `Failed to update airports database: ${message}`, + }; + } + }, + }); diff --git a/google-flights/server/tools/index.ts b/google-flights/server/tools/index.ts new file mode 100644 index 00000000..48c3bbd6 --- /dev/null +++ b/google-flights/server/tools/index.ts @@ -0,0 +1,18 @@ +/** + * Central export point for all flight-related tools. + */ +import { + createSearchFlightsTool, + createSearchAirportsTool, + createGetTravelDatesTool, + createUpdateAirportsDatabaseTool, +} from "./flights.ts"; + +// Export tools as array of tool creator functions +// The runtime will call each function with env +export const tools = [ + createSearchFlightsTool, + createSearchAirportsTool, + createGetTravelDatesTool, + createUpdateAirportsDatabaseTool, +]; diff --git a/google-flights/tsconfig.json b/google-flights/tsconfig.json new file mode 100644 index 00000000..99c4a769 --- /dev/null +++ b/google-flights/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2023", "ES2024", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "verbatimModuleSyntax": false, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "allowJs": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "shared/*": ["./shared/*"], + "server/*": ["./server/*"] + } + }, + "include": ["server", "shared"] +} diff --git a/package.json b/package.json index eaca317d..3b6e3978 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ }, "workspaces": [ "apify", - "content-scraper", "blog-post-generator", + "content-scraper", "data-for-seo", "datajud", "deco-llm", @@ -33,6 +33,7 @@ "google-calendar", "google-docs", "google-drive", + "google-flights", "google-forms", "google-gmail", "google-meet",