Skip to content

Commit d01469b

Browse files
authored
feat(aviation): add SearchGoogleFlights and SearchGoogleDates RPCs (koala73#2446)
* feat(aviation): add SearchGoogleFlights and SearchGoogleDates RPCs Port Google Flights internal API from fli Python library as two new aviation service RPCs routed through the Railway relay. - Proto: SearchGoogleFlights and SearchGoogleDates messages and RPCs - Relay: handleGoogleFlightsSearch and handleGoogleFlightsDates handlers with JSONP parsing, 61-day chunking for date ranges, cabin/stops/sort mappers - Server handlers: forward params to relay google-flights endpoints - gateway.ts: no-store for flights, medium cache for dates * feat(mcp): expose search_flights and search_flight_prices_by_date tools * test(mcp): update tool count to 24 after adding search_flights and search_flight_prices_by_date * fix(aviation): address PR review issues in Google Flights RPCs P1: airline filtering — use gfParseAirlines() in relay (handles comma-joined string from codegen) and parseStringArray() in server handlers P1: partial chunk failure now sets degraded: true instead of silently returning incomplete data as success; relay includes partial: true flag P2: round-trip date search validates trip_duration > 0 before proceeding; returns 400 when is_round_trip=true and duration is absent/zero P2: relay mappers accept user-friendly aliases ('0'/'1' for max_stops, 'price'/'departure' for sort_by) alongside symbolic enum values; MCP tool docs updated to match * fix(aviation): use cachedFetchJson in searchGoogleDates for stampede protection Medium cache tier (10 min) requires Redis-level coalescing to prevent concurrent requests from all hitting the relay before cache warms. Cache key includes all request params (sorted airlines for stable keys). * fix(aviation): always use getAll() for airlines in relay; add multi-airline tests The OR short-circuit (get() || getAll()) meant get() returned the first airline value (truthy), so getAll() never ran and only one airline was forwarded to Google. Fix: unconditionally use getAll(). Tests cover: multi-airline repeated params, single airline, empty array, comma-joined string from codegen, partial degraded flag propagation.
1 parent d9c7cdd commit d01469b

15 files changed

Lines changed: 1604 additions & 3 deletions

File tree

api/mcp.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,80 @@ const TOOL_REGISTRY: ToolDef[] = [
362362
return res.json();
363363
},
364364
},
365+
{
366+
name: 'search_flights',
367+
description: 'Search Google Flights for real-time flight options between two airports on a specific date. Returns available flights with prices, stops, airline, and segment details. Use IATA airport codes (e.g. "JFK", "LHR", "DXB").',
368+
inputSchema: {
369+
type: 'object',
370+
properties: {
371+
origin: { type: 'string', description: 'IATA code for the departure airport, e.g. "JFK"' },
372+
destination: { type: 'string', description: 'IATA code for the arrival airport, e.g. "LHR"' },
373+
departure_date: { type: 'string', description: 'Departure date in YYYY-MM-DD format' },
374+
return_date: { type: 'string', description: 'Return date in YYYY-MM-DD format for round trips (optional)' },
375+
cabin_class: { type: 'string', description: 'Cabin class: "economy", "premium_economy", "business", or "first" (optional, default economy)' },
376+
max_stops: { type: 'string', description: 'Max stops: "0" or "non_stop" for nonstop, "1" or "one_stop" for max one stop, or omit for any (optional)' },
377+
passengers: { type: 'number', description: 'Number of passengers (1-9, default 1)' },
378+
sort_by: { type: 'string', description: 'Sort order: "price" (cheapest), "duration", "departure", or "arrival" (optional)' },
379+
},
380+
required: ['origin', 'destination', 'departure_date'],
381+
},
382+
_execute: async (params, base, apiKey) => {
383+
const qs = new URLSearchParams({
384+
origin: String(params.origin ?? ''),
385+
destination: String(params.destination ?? ''),
386+
departure_date: String(params.departure_date ?? ''),
387+
...(params.return_date ? { return_date: String(params.return_date) } : {}),
388+
...(params.cabin_class ? { cabin_class: String(params.cabin_class) } : {}),
389+
...(params.max_stops ? { max_stops: String(params.max_stops) } : {}),
390+
...(params.sort_by ? { sort_by: String(params.sort_by) } : {}),
391+
passengers: String(Math.max(1, Math.min(Number(params.passengers ?? 1), 9))),
392+
});
393+
const res = await fetch(`${base}/api/aviation/v1/search-google-flights?${qs}`, {
394+
headers: { 'X-WorldMonitor-Key': apiKey, 'User-Agent': 'worldmonitor-mcp-edge/1.0' },
395+
signal: AbortSignal.timeout(25_000),
396+
});
397+
if (!res.ok) throw new Error(`search-google-flights HTTP ${res.status}`);
398+
return res.json();
399+
},
400+
},
401+
{
402+
name: 'search_flight_prices_by_date',
403+
description: 'Search Google Flights date-grid pricing across a date range. Returns cheapest prices for each departure date between two airports. Useful for finding the cheapest day to fly. Use IATA airport codes.',
404+
inputSchema: {
405+
type: 'object',
406+
properties: {
407+
origin: { type: 'string', description: 'IATA code for the departure airport, e.g. "JFK"' },
408+
destination: { type: 'string', description: 'IATA code for the arrival airport, e.g. "LHR"' },
409+
start_date: { type: 'string', description: 'Start of the date range in YYYY-MM-DD format' },
410+
end_date: { type: 'string', description: 'End of the date range in YYYY-MM-DD format' },
411+
is_round_trip: { type: 'boolean', description: 'Whether to search round-trip prices (default false). Requires trip_duration when true.' },
412+
trip_duration: { type: 'number', description: 'Trip duration in days — required when is_round_trip is true (e.g. 7 for a one-week trip)' },
413+
cabin_class: { type: 'string', description: 'Cabin class: "economy", "premium_economy", "business", or "first" (optional)' },
414+
passengers: { type: 'number', description: 'Number of passengers (1-9, default 1)' },
415+
sort_by_price: { type: 'boolean', description: 'Sort results by price ascending (default false, sorts by date)' },
416+
},
417+
required: ['origin', 'destination', 'start_date', 'end_date'],
418+
},
419+
_execute: async (params, base, apiKey) => {
420+
const qs = new URLSearchParams({
421+
origin: String(params.origin ?? ''),
422+
destination: String(params.destination ?? ''),
423+
start_date: String(params.start_date ?? ''),
424+
end_date: String(params.end_date ?? ''),
425+
is_round_trip: String(params.is_round_trip ?? false),
426+
...(params.trip_duration ? { trip_duration: String(params.trip_duration) } : {}),
427+
...(params.cabin_class ? { cabin_class: String(params.cabin_class) } : {}),
428+
sort_by_price: String(params.sort_by_price ?? false),
429+
passengers: String(Math.max(1, Math.min(Number(params.passengers ?? 1), 9))),
430+
});
431+
const res = await fetch(`${base}/api/aviation/v1/search-google-dates?${qs}`, {
432+
headers: { 'X-WorldMonitor-Key': apiKey, 'User-Agent': 'worldmonitor-mcp-edge/1.0' },
433+
signal: AbortSignal.timeout(25_000),
434+
});
435+
if (!res.ok) throw new Error(`search-google-dates HTTP ${res.status}`);
436+
return res.json();
437+
},
438+
},
365439
];
366440

367441
// Public shape for tools/list (strip internal _-prefixed fields, add MCP annotations)

docs/api/AviationService.openapi.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)