google-flights-mcp-server 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google-flights-mcp-server",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "MCP server for searching Google Flights — flight search, date grid pricing, and airport lookup",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -244,51 +244,87 @@ function parseFareBrand(raw) {
244
244
  }
245
245
  }
246
246
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
247
+ function parseRawOffer(raw, currency) {
248
+ const details = raw[0];
249
+ const priceData = raw[1];
250
+ const rankData = raw[5]; // [is_best (1/0), ?, ?]
251
+ if (!details || !priceData)
252
+ return null;
253
+ // Parse segments
254
+ const segments = [];
255
+ const legs = details[2];
256
+ if (Array.isArray(legs)) {
257
+ for (const leg of legs) {
258
+ const segment = parseSegment(leg);
259
+ if (segment)
260
+ segments.push(segment);
261
+ }
262
+ }
263
+ const price = priceData[0]?.[1];
264
+ if (price === undefined || price === null)
265
+ return null;
266
+ return {
267
+ price,
268
+ currency,
269
+ airline: details[1]?.[0] || '',
270
+ airline_code: details[0] || '',
271
+ is_best: rankData?.[0] === 1,
272
+ fare_brand: parseFareBrand(raw),
273
+ departure: formatTime(details[5]),
274
+ arrival: formatTime(details[8]),
275
+ departure_date: formatDate(details[4]),
276
+ arrival_date: formatDate(details[7]),
277
+ duration_minutes: details[9] || 0,
278
+ stops: segments.length > 0 ? segments.length - 1 : 0,
279
+ segments,
280
+ extensions: parseExtensions(raw),
281
+ booking_token: priceData[1] || '',
282
+ };
283
+ }
284
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
247
285
  function parseFlightOffers(ds1, currency) {
248
286
  const offers = [];
249
- if (!ds1?.[3]?.[0] || !Array.isArray(ds1[3][0]))
250
- return offers;
251
- const rawOffers = ds1[3][0];
252
- for (const raw of rawOffers) {
253
- try {
254
- const details = raw[0];
255
- const priceData = raw[1];
256
- const rankData = raw[5]; // [is_best (1/0), ?, ?]
257
- if (!details || !priceData)
258
- continue;
259
- // Parse segments
260
- const segments = [];
261
- const legs = details[2];
262
- if (Array.isArray(legs)) {
263
- for (const leg of legs) {
264
- const segment = parseSegment(leg);
265
- if (segment)
266
- segments.push(segment);
287
+ const seenTokens = new Set();
288
+ // Google Flights returns results in two sections:
289
+ // - ds1[2][0]: "Best flights" (featured/highlighted flights, typically 3)
290
+ // - ds1[3][0]: "Other flights" (the main results list)
291
+ // Both sections use the same offer structure. In practice flights are not
292
+ // duplicated between them, but we deduplicate by booking_token defensively
293
+ // since this is an undocumented scraped API that could change.
294
+ //
295
+ // The is_best flag comes from raw[5][0] (per-offer rankData), not from which
296
+ // section the offer appears in. Google sets this flag on all ds1[2][0] offers
297
+ // and sometimes on ds1[3][0] offers too.
298
+ // Parse "best flights" from ds1[2][0]
299
+ const bestFlights = ds1?.[2]?.[0];
300
+ if (Array.isArray(bestFlights)) {
301
+ for (const raw of bestFlights) {
302
+ try {
303
+ const offer = parseRawOffer(raw, currency);
304
+ if (offer && !seenTokens.has(offer.booking_token)) {
305
+ seenTokens.add(offer.booking_token);
306
+ offers.push(offer);
267
307
  }
268
308
  }
269
- const price = priceData[0]?.[1];
270
- if (price === undefined || price === null)
271
- continue;
272
- offers.push({
273
- price,
274
- currency,
275
- airline: details[1]?.[0] || '',
276
- airline_code: details[0] || '',
277
- is_best: rankData?.[0] === 1,
278
- fare_brand: parseFareBrand(raw),
279
- departure: formatTime(details[5]),
280
- arrival: formatTime(details[8]),
281
- departure_date: formatDate(details[4]),
282
- arrival_date: formatDate(details[7]),
283
- duration_minutes: details[9] || 0,
284
- stops: segments.length > 0 ? segments.length - 1 : 0,
285
- segments,
286
- extensions: parseExtensions(raw),
287
- booking_token: priceData[1] || '',
288
- });
309
+ catch (e) {
310
+ logDebug('parseFlightOffers', `Skipping malformed best offer: ${e.message}`);
311
+ }
289
312
  }
290
- catch (e) {
291
- logDebug('parseFlightOffers', `Skipping malformed offer: ${e.message}`);
313
+ }
314
+ // Parse "other flights" from ds1[3][0]
315
+ const otherFlights = ds1?.[3]?.[0];
316
+ if (Array.isArray(otherFlights)) {
317
+ for (const raw of otherFlights) {
318
+ try {
319
+ const offer = parseRawOffer(raw, currency);
320
+ if (offer && !seenTokens.has(offer.booking_token)) {
321
+ seenTokens.add(offer.booking_token);
322
+ offers.push(offer);
323
+ }
324
+ }
325
+ catch (e) {
326
+ logDebug('parseFlightOffers', `Skipping malformed offer: ${e.message}`);
327
+ }
292
328
  }
293
329
  }
294
330
  return offers;
@@ -361,6 +397,10 @@ export async function searchFlights(options) {
361
397
  let allOffers = parseFlightOffers(ds1, options.currency);
362
398
  // Apply client-side stop filter (supplements the protobuf filter)
363
399
  allOffers = filterByStops(allOffers, options.max_stops);
400
+ // Filter out basic economy fares (fare_brand "Economy" = tier 1, the lowest/basic tier)
401
+ if (options.exclude_basic_economy) {
402
+ allOffers = allOffers.filter((o) => o.fare_brand !== 'Economy');
403
+ }
364
404
  // Sort
365
405
  allOffers = sortOffers(allOffers, options.sort_by);
366
406
  const totalResults = allOffers.length;
@@ -67,6 +67,7 @@ export interface SearchFlightsOptions {
67
67
  max_results: number;
68
68
  offset: number;
69
69
  currency: string;
70
+ exclude_basic_economy: boolean;
70
71
  }
71
72
  export interface SearchFlightsResult {
72
73
  query: {
@@ -17,10 +17,11 @@ export declare const SearchFlightsSchema: z.ZodObject<{
17
17
  max_results: z.ZodDefault<z.ZodNumber>;
18
18
  offset: z.ZodDefault<z.ZodNumber>;
19
19
  currency: z.ZodDefault<z.ZodString>;
20
+ exclude_basic_economy: z.ZodDefault<z.ZodBoolean>;
20
21
  }, "strip", z.ZodTypeAny, {
22
+ currency: string;
21
23
  sort_by: "best" | "price" | "duration" | "departure" | "arrival";
22
24
  max_stops: "any" | "nonstop" | "1" | "2";
23
- currency: string;
24
25
  origin: string;
25
26
  destination: string;
26
27
  departure_date: string;
@@ -32,14 +33,15 @@ export declare const SearchFlightsSchema: z.ZodObject<{
32
33
  infants_on_lap: number;
33
34
  max_results: number;
34
35
  offset: number;
36
+ exclude_basic_economy: boolean;
35
37
  return_date?: string | undefined;
36
38
  }, {
37
39
  origin: string;
38
40
  destination: string;
39
41
  departure_date: string;
42
+ currency?: string | undefined;
40
43
  sort_by?: "best" | "price" | "duration" | "departure" | "arrival" | undefined;
41
44
  max_stops?: "any" | "nonstop" | "1" | "2" | undefined;
42
- currency?: string | undefined;
43
45
  return_date?: string | undefined;
44
46
  trip_type?: "one_way" | "round_trip" | undefined;
45
47
  seat_class?: "economy" | "premium_economy" | "business" | "first" | undefined;
@@ -49,6 +51,7 @@ export declare const SearchFlightsSchema: z.ZodObject<{
49
51
  infants_on_lap?: number | undefined;
50
52
  max_results?: number | undefined;
51
53
  offset?: number | undefined;
54
+ exclude_basic_economy?: boolean | undefined;
52
55
  }>;
53
56
  export declare function searchFlightsTool(_server: Server, clientFactory: FlightsClientFactory): {
54
57
  name: string;
@@ -120,6 +123,10 @@ export declare function searchFlightsTool(_server: Server, clientFactory: Flight
120
123
  type: string;
121
124
  description: string;
122
125
  };
126
+ exclude_basic_economy: {
127
+ type: string;
128
+ description: string;
129
+ };
123
130
  };
124
131
  required: string[];
125
132
  };
@@ -57,6 +57,10 @@ export const SearchFlightsSchema = z.object({
57
57
  .max(3)
58
58
  .default('USD')
59
59
  .describe('Currency code for prices (e.g., "USD", "EUR", "GBP")'),
60
+ exclude_basic_economy: z
61
+ .boolean()
62
+ .default(true)
63
+ .describe('Exclude basic economy fares from results (default: true). Basic economy fares typically have restrictions like no carry-on, no seat selection, and no changes. Set to false to include all fare tiers.'),
60
64
  });
61
65
  export function searchFlightsTool(_server, clientFactory) {
62
66
  return {
@@ -67,6 +71,8 @@ Returns structured flight data including prices, airlines, times, durations, sto
67
71
 
68
72
  The fare_brand field indicates the fare tier: "Economy" (basic/lowest tier), "Economy+" (mid-tier with extras), or "Economy Flex" (higher tier with more flexibility). This is derived from Google's numeric fare tier data and may be null if unavailable. Use the extensions field (carry_on_included, checked_bags_included) for concrete amenity details.
69
73
 
74
+ By default, basic economy fares (fare_brand "Economy") are excluded from results since they typically have significant restrictions (no carry-on, no seat selection, non-refundable). Set exclude_basic_economy to false to include all fare tiers.
75
+
70
76
  IMPORTANT — Handling large result sets: Popular routes often return 50-150+ flights. If total_results is high, recommend narrowing with filters (max_stops, sort_by, seat_class) rather than paginating through everything. For example, set max_stops to "nonstop" or sort_by to "price" to surface the most relevant options quickly.
71
77
 
72
78
  Pagination: The response includes has_more (boolean) and next_offset (number or null). To get the next page, call search_flights again with the same parameters but set offset to next_offset. Keep paginating while has_more is true. Each page returns up to max_results flights.
@@ -137,6 +143,10 @@ Use get_date_grid to find the cheapest dates before searching.`,
137
143
  type: 'string',
138
144
  description: 'Currency code for prices (default: USD)',
139
145
  },
146
+ exclude_basic_economy: {
147
+ type: 'boolean',
148
+ description: 'Exclude basic economy fares (default: true). Set to false to include all fare tiers.',
149
+ },
140
150
  },
141
151
  required: ['origin', 'destination', 'departure_date'],
142
152
  },