google-flights-mcp-server 0.2.0 → 0.2.2

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.2.0",
3
+ "version": "0.2.2",
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;
@@ -19,9 +19,9 @@ export declare const SearchFlightsSchema: z.ZodObject<{
19
19
  currency: z.ZodDefault<z.ZodString>;
20
20
  exclude_basic_economy: z.ZodDefault<z.ZodBoolean>;
21
21
  }, "strip", z.ZodTypeAny, {
22
+ currency: string;
22
23
  sort_by: "best" | "price" | "duration" | "departure" | "arrival";
23
24
  max_stops: "any" | "nonstop" | "1" | "2";
24
- currency: string;
25
25
  origin: string;
26
26
  destination: string;
27
27
  departure_date: string;
@@ -39,9 +39,9 @@ export declare const SearchFlightsSchema: z.ZodObject<{
39
39
  origin: string;
40
40
  destination: string;
41
41
  departure_date: string;
42
+ currency?: string | undefined;
42
43
  sort_by?: "best" | "price" | "duration" | "departure" | "arrival" | undefined;
43
44
  max_stops?: "any" | "nonstop" | "1" | "2" | undefined;
44
- currency?: string | undefined;
45
45
  return_date?: string | undefined;
46
46
  trip_type?: "one_way" | "round_trip" | undefined;
47
47
  seat_class?: "economy" | "premium_economy" | "business" | "first" | undefined;