Skip to main content
Glama

flights

A local MCP server for planning a trip from live public fares. It speaks stdio, needs no API key, and reads Google Flights pages through the unofficial fast-flights query builder. Airport names are resolved offline with airportsdata.

Defaults are 1 adult, economy, CHF.

Why this source

The constraint is a planner an agent can run with no paid account and no API key, and with no invented prices. The fields that matter are flight numbers, layovers, bags or fare family, a date window, and a fare that was actually returned.

Source

What it would take

What a planner gets

Google Flights page, parsed here

No key. Unofficial, so it can break or be blocked.

Live CHF prices. Flight numbers and layover minutes are in the page payload. Bags and fare family are not attached to an itinerary. No date-price calendar, so a window is several bounded searches.

Kiwi Tequila

api.tequila.kiwi.com returns HTTP 403 without a key.

Not usable.

Amadeus, Duffel, Skyscanner partner APIs

A developer account and a key.

Out of scope.

A single airline API

No key for some public pages, but only that airline.

Misses the rest of a ZRH–BCN comparison.

Hosted date-grid sites (for example whentofly)

No key, but the fares are a hosted cache, not this process reading a live public page.

Not a local source, and not something to treat as a bookable fare.

Google is the one that still lets an agent compare real itineraries. The server says so when a field is missing. It does not fill bags in from legroom inches or from unnamed numeric flags on the payload.

Related MCP server: rihla

Run

From this directory, with uv:

uv sync
uv run flights-mcp

The same process is uv run python -m flights_mcp. A host that does not use uv can launch the module with whatever Python has the package installed:

python -m flights_mcp

The process is silent on stdout. MCP messages are the stdout stream. Logs go to stderr.

Example host config (Cursor, Claude Desktop, and anything else that launches a local stdio server):

{
  "mcpServers": {
    "flights": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/this/repo", "flights-mcp"]
    }
  }
}

No environment variables are required.

Tools

search_flights

Search live public fares across a short set of departure dates.

Field

Required

Default

Bounds

origin

yes

IATA code or city name, 1–80 characters

destination

yes

same

depart_date

one of the date forms

YYYY-MM-DD, today through 330 days, Europe/Zurich

flexibility_days

no

0

0–3 days either side of depart_date

depart_from

with depart_to

first departure, inclusive

depart_to

with depart_from

last departure, inclusive

return_date

no

one-way

fixed return, on or after a searched departure

nights

no

1–30. Return becomes each departure plus this many days. Not with return_date

adults

no

1

1–9

cabin

no

economy

economy, premium-economy, business, first

currency

no

CHF

a currency Google Flights prices in (EUR, USD, GBP, …)

max_results

no

8

1–20

nonstop

no

false

direct flights only

Pass depart_date, or depart_date with flexibility_days, or depart_from and depart_to. Not both an anchor and a from/to window. "Zurich to Barcelona, sometime in a week" is depart_from and depart_to seven days apart.

At most 7 departure dates are queried, one after another, inside 25 seconds. A longer window is sampled, including the first and last day, and the dates that were skipped are listed in dates_not_searched. A day that runs out of time is recorded on days and is not given an invented fare. nights does not search a grid of return dates. Each departure is one query.

Each itinerary includes:

  • price and currency, as the integer Google returned

  • airline names, local times, duration, stops, route, and each leg

  • flight_numbers when Google sent them. A leg without one has flight_number: null and a note that says so

  • layovers: airport and minutes. minutes_basis is google when the payload included the layover length, schedule when it was the gap between the leg times, and unknown when neither could be read. An empty layovers list means nonstop

  • carry_on, checked_bag, and fare_family. On this source those are unknown / null, and fare_note says that nothing was assumed

  • search_url for that date, and booking_url when Google attached a booking token. The booking link opens the itinerary on Google Flights. It is not a ticket

picks.cheapest, picks.fastest, and picks.compromise are itinerary ids. compromise is the option with the best even balance of price and duration among the flights that are not already the cheapest or the fastest. When the pool is too small for a distinct third option, it matches one of the other picks. The same id can carry more than one label. flights is sorted by price and always includes the three picks, up to max_results.

A city that matches several airports (Springfield) is an error. Pass an IATA code, or a metro code such as NYC. London and New York resolve to the metro code so the search covers the city's airports.

Unknown arguments are rejected. A failure is a tool error whose text contains JSON {"code","message",...}. Codes include unknown_place, ambiguous_place, invalid_input, no_results, unpriced, timeout, and upstream. An error never carries an invented price. A single day inside a window can also be skipped when its return would fall before departure.

resolve_airports

Offline IATA lookup.

Field

Required

Default

Bounds

query

yes

city, airport name, or IATA code

limit

no

10

1–20

Zurich and Zürich resolve to ZRH. Barcelona resolves to BCN (Barcelona in Venezuela is BLA; the airport named for the city is the one a search uses). NYC returns the metro code plus JFK and LGA. Newark is EWR.

Limits

  • Fares are snapshots. They can change before anyone books.

  • Bags and fare family are not on the itinerary Google returns to this parser. The fields are present so a planner can see the absence. They are never guessed.

  • Times are local to each airport and have no offset. duration_basis is google when the payload included the trip duration, elapsed when it was computed from airport timezones, and air when only airborne minutes were available.

  • For a round trip, price is the total Google showed for that departure and return. legs are the segments included in that result.

  • The price is the integer Google returned, in the currency you asked for, in major units. It is not converted or rounded. Itineraries with no usable price are omitted. If none remain, the tool errors with unpriced.

  • Identical searches in one process are reused for 2 minutes. The cache is memory only and includes the date window.

  • Each day contributes at most 4 itineraries to the comparison: the cheapest ones, plus that day's fastest if it was not already among them. The overall list is still capped by max_results, and the labeled options are kept inside that cap.

  • The whole search, including every date in the window, gets 25 seconds.

  • At most two searches run at once.

Read SECURITY.md before pointing anything automated at this. fast-flights can break when Google changes the page, it can be rate limited, and automated access may violate Google's terms.

Tests

Offline tests cover schemas, validation, airport resolution, the page parser, date-window sampling, and a search that never leaves the machine:

uv run pytest

The live check is opt-in. It searches ZRH to BCN across seven days, one adult, economy, CHF, a few weeks out:

FLIGHTS_LIVE=1 uv run pytest tests/test_live.py -s

Live check

FLIGHTS_LIVE=1 uv run pytest tests/test_live.py on 2026-09-30. One-way ZRH to BCN, departures 2026-10-26 through 2026-11-01 (seven days, all searched), 1 adult, economy, CHF. The call wrote nothing to stdout. Every priced itinerary had carry_on and checked_bag set to unknown and fare_family null. Nothing was filled in.

Cheapest fare Google returned for each day:

Departure

Cheapest

2026-10-26

63 CHF

2026-10-27

48 CHF

2026-10-28

57 CHF

2026-10-29

63 CHF

2026-10-30

69 CHF

2026-10-31

57 CHF

2026-11-01

54 CHF

The eight itineraries the tool returned (max_results 8), sorted by price. picks were the 48 CHF Vueling, the 54 CHF Vueling, and the 75 CHF SWISS.

Price

Date

Flight

Local times

Duration

Stops

Codeshare

48 CHF

27 Oct

VY1229

21:15 → 23:10

1h 55m

0

IB5489

54 CHF

1 Nov

VY1227

09:40 → 11:30

1h 50m

0

IB5487

57 CHF

27 Oct

VY1227

09:40 → 11:30

1h 50m

0

IB5487

57 CHF

28 Oct

VY1227

09:40 → 11:30

1h 50m

0

IB5487

57 CHF

28 Oct

VY1229

21:35 → 23:30

1h 55m

0

IB5489

57 CHF

31 Oct

VY1229

21:15 → 23:10

1h 55m

0

IB5489

63 CHF

26 Oct

VY1227

09:40 → 11:30

1h 50m

0

IB5487

75 CHF

1 Nov

LX1954

12:20 → 14:05

1h 45m

0

none

Duration basis on these was google. The cheapest Vueling was an Airbus A320, with the carbon figure Google sent (83000 grams) and a booking URL. Fetching that booking URL from this environment returned HTTP 200 and the Google Flights page. It is not a held ticket. The booking token is not copied here.

The same parser, on the 2026-10-28 page alone, also saw connections. They did not make the shortlist: the nonstops above were cheaper, and the SWISS flight was faster, so the per-day keep (cheapest plus fastest) never held onto them. They are recorded because the layover fields were present:

Price

Flights

Duration

Layover

77 CHF

JU331, JU580

23h 40m

Belgrade (BEG), 1155 minutes, as Google reported

101 CHF

AF1815, AF1448

4h 5m

Paris (CDG), 45 minutes

138 CHF

SN2732, SN3703

4h 25m

Brussels (BRU), 70 minutes

Bags and fare family were absent on those connections too.

Search URL for the first day of the window:

https://www.google.com/travel/flights/search?tfs=GhoSCjIwMjYtMTAtMjZqBRIDWlJIcgUSA0JDTkIBAUgBmAEC&hl=en-US&curr=CHF

Treat the tables as a snapshot from that run.

Layout

  • flights_mcp/server.py — MCP tools and stdio entrypoint

  • flights_mcp/search.py — date window, labels, timeout, cache

  • flights_mcp/google_parse.py — flight numbers, layovers, and prices from the page

  • flights_mcp/dates.py — the 7-date cap

  • flights_mcp/upstream.py — one bounded Google Flights fetch

  • flights_mcp/airports.py — local IATA lookup

  • flights_mcp/models.py — strict input and output schemas

Available Tools

2 tools
resolve_airportsResolve airportsA
Read-onlyIdempotent

Resolve a city or airport name to IATA codes, offline.

Zurich resolves to ZRH. Barcelona resolves to BCN. A metro code such as NYC returns that city code and its airports. The lookup does not use the network.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMaximum number of matches.
queryYesCity, airport name, or IATA code. Examples: Zurich, BCN, NYC.

Output Schema

ParametersJSON Schema
NameRequiredDescription
countYesNumber of matches returned.
queryYesThe query after whitespace was collapsed.
airportsYes

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover read-only, idempotent, non-destructive, and closed-world behavior. The description adds useful lookup semantics beyond the annotations: it explicitly says the lookup is offline/no-network and explains that a metro code like NYC returns the city code plus its airports.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The definition is front-loaded and appropriately sized. The final sentence repeats the earlier 'offline' claim slightly, but the examples earn their place and the structure is easy to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The input schema is fully documented, an output schema exists, and annotations carry the safety profile. The description supplies the remaining important context about offline resolution and metro-code behavior, so nothing essential is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description goes further by showing what the query parameter resolves to in practice, including metro-code expansion, which adds meaning beyond the schema's short examples.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: resolving city or airport names to IATA codes. The examples (Zurich → ZRH, Barcelona → BCN, NYC metro behavior) make the function concrete and clearly distinct from the sibling search_flights tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explains what the tool does but gives no explicit when-to-use or when-not-to-use guidance. It does not mention the sibling search_flights or any alternative, leaving selection to inference from the tool name and purpose alone.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_flightsSearch flightsA
Read-onlyIdempotent

Search live public Google Flights fares across a short date window.

origin and destination take an IATA code or a city name. Defaults are 1 adult, economy, CHF. A planner can ask for one day, a few days around that day, or a from/to window such as a week. The price is the integer Google returned. This tool does not invent a fare, a flight number, or a bag rule.

ParametersJSON Schema
NameRequiredDescriptionDefault
cabinNoSeat cabin.economy
adultsNoAdults, 1 to 9. Children are not supported.
nightsNoStay length in nights. The return date is each departure plus this many days. Do not combine with return_date.
originYesIATA airport or city code (ZRH, BCN, NYC) or a city or airport name (Zurich, Barcelona).
nonstopNoWhen true, only nonstop itineraries.
currencyNoISO 4217 currency Google Flights can price in. Default CHF.CHF
depart_toNoLast departure date of a window, inclusive. At most 7 dates are searched; a longer window is sampled and the rest are listed as not searched.
depart_dateNoDeparture date YYYY-MM-DD. Use this, optionally with flexibility_days. Do not combine it with depart_from and depart_to.
depart_fromNoFirst departure date of a window, inclusive. Pair with depart_to.
destinationYesIATA airport or city code (ZRH, BCN, NYC) or a city or airport name (Zurich, Barcelona).
max_resultsNoHow many itineraries to return, at most 20.
return_dateNoFixed return date YYYY-MM-DD. Omit for one-way. Do not combine with nights.
flexibility_daysNoDays before and after depart_date. 0 searches that day only. Not used with a from/to window.

Output Schema

ParametersJSON Schema
NameRequiredDescription
daysYes
cabinYes
countYes
notesYes
picksYes
adultsYes
cachedYes
nightsYesLength of stay applied to each departure, when requested.
originYes
sourceYes
flightsYes
nonstopYes
currencyYes
depart_toYes
search_urlYesGoogle Flights search for the first date that was actually queried.
depart_dateYesThe depart_date argument when the search was anchored on one day, with or without flexibility. Null when the search used depart_from and depart_to.
depart_fromYes
destinationYes
origin_nameYes
return_dateYesFixed return date, when one was requested. Null for one-way or when nights varies the return.
dates_searchedYes
destination_nameYes
dates_not_searchedYesDates inside the requested window that were not queried, because of the 7-date cap or the timeout.

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnly/idempotent/openWorld/destructive, so the bar is lower. The description adds real behavioral context: the price is exactly 'the integer Google returned' and the tool 'does not invent a fare, a flight number, or a bag rule' — a useful data-fidelity guarantee. It omits rate limits or latency expectations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose in the first sentence, followed by short, scannable points. Slight redundancy with the schema (defaults, IATA/city input) costs it the top score, but nothing is padded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 13-parameter tool with an output schema and rich annotations, the description covers the key ambiguities: date-window modes, defaults, and fare provenance. Mutual-exclusion rules (nights vs return_date, depart_date vs from/to) are left entirely to the schema, but the schema handles them well.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all 13 parameters including date formats, mutual exclusions and the 7-date search cap. The description restates defaults (1 adult, economy, CHF) and the accepted origin/destination forms, adding little beyond what the schema provides; baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: 'Search live public Google Flights fares across a short date window.' It also implicitly separates itself from the resolve_airports sibling by noting origin/destination accept city names directly, so an agent knows it need not resolve codes first.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It sketches the planning patterns supported ('one day, a few days around that day, or a from/to window'), which implies usage, but never states when to use this over resolve_airports or when the tool is inappropriate. No explicit exclusions or alternative-routing guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv1.1.0
    • First observedresolve_airports
    • First observedsearch_flights

TDQS

A4.1/5.0

Scored across 2 tools

Disambiguation5/5

search_flights and resolve_airports have entirely distinct purposes: one performs live fare lookups, the other does offline name-to-IATA resolution. There is no overlap in intent, so an agent can select correctly without hesitation.

Naming Consistency5/5

Both names follow a clean verb_noun snake_case pattern (search_flights, resolve_airports). The convention is predictable and consistent throughout the two-tool surface.

Tool Count3/5

Two tools is thin for a flight-search domain; resolve_airports is really a supporting lookup rather than a peer operation. It is not wrong, but the surface feels minimal and could reasonably include one or two more operations.

Completeness4/5

The stated purpose is read-only fare search, and the pair covers lookup plus resolution with no dead ends for that scope. Minor gaps exist (e.g. no explicit round-trip or multi-passenger handling surfaced as separate operations), but agents can work around them.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Flight search for AI agents: flexible-date cheapest round-trips with a good-price verdict from historical data. Hosted remote MCP, free, no key.
    3
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Flexible multi-leg, multi-airport flight search that finds the cheapest route across an entire itinerary — available as a CLI and as an MCP server for AI agents.
    Apache 2.0
  • A
    license
    A
    quality
    B
    maintenance
    Enables users to find the cheapest dates to fly a route via Google Flights, supporting one-way and round-trip searches through multiple backends. It provides a tool that can search date ranges, filter by nonstop, seat, currency, and force a particular backend.
    1
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables real-time flight fare searches across date ranges and multiple destinations, with historical price insights and booking links. Provides one-way and round-trip search tools through MCP.
    4
    1
    MIT