flights-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@flights-mcpfind the cheapest flights from Zurich to Barcelona next week"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 |
| 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-mcpThe 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_mcpThe 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 |
| yes | IATA code or city name, 1–80 characters | |
| yes | same | |
| one of the date forms |
| |
| no |
| 0–3 days either side of |
| with | first departure, inclusive | |
| with | last departure, inclusive | |
| no | one-way | fixed return, on or after a searched departure |
| no | 1–30. Return becomes each departure plus this many days. Not with | |
| no |
| 1–9 |
| no |
|
|
| no |
| a currency Google Flights prices in ( |
| no |
| 1–20 |
| no |
| 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_numberswhen Google sent them. A leg without one hasflight_number: nulland a note that says solayovers: airport and minutes.
minutes_basisisgooglewhen the payload included the layover length,schedulewhen it was the gap between the leg times, andunknownwhen neither could be read. An emptylayoverslist means nonstopcarry_on,checked_bag, andfare_family. On this source those areunknown/null, andfare_notesays that nothing was assumedsearch_urlfor that date, andbooking_urlwhen 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 |
| yes | city, airport name, or IATA code | |
| no |
| 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_basisisgooglewhen the payload included the trip duration,elapsedwhen it was computed from airport timezones, andairwhen only airborne minutes were available.For a round trip,
priceis the total Google showed for that departure and return.legsare 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 pytestThe 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 -sLive 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:
Treat the tables as a snapshot from that run.
Layout
flights_mcp/server.py— MCP tools and stdio entrypointflights_mcp/search.py— date window, labels, timeout, cacheflights_mcp/google_parse.py— flight numbers, layovers, and prices from the pageflights_mcp/dates.py— the 7-date capflights_mcp/upstream.py— one bounded Google Flights fetchflights_mcp/airports.py— local IATA lookupflights_mcp/models.py— strict input and output schemas
Available Tools
2 toolsresolve_airportsResolve airportsARead-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.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum number of matches. | |
| query | Yes | City, airport name, or IATA code. Examples: Zurich, BCN, NYC. |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | Number of matches returned. |
| query | Yes | The query after whitespace was collapsed. |
| airports | Yes |
TDQS
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.
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.
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.
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.
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.
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 flightsARead-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.
| Name | Required | Description | Default |
|---|---|---|---|
| cabin | No | Seat cabin. | economy |
| adults | No | Adults, 1 to 9. Children are not supported. | |
| nights | No | Stay length in nights. The return date is each departure plus this many days. Do not combine with return_date. | |
| origin | Yes | IATA airport or city code (ZRH, BCN, NYC) or a city or airport name (Zurich, Barcelona). | |
| nonstop | No | When true, only nonstop itineraries. | |
| currency | No | ISO 4217 currency Google Flights can price in. Default CHF. | CHF |
| depart_to | No | Last 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_date | No | Departure date YYYY-MM-DD. Use this, optionally with flexibility_days. Do not combine it with depart_from and depart_to. | |
| depart_from | No | First departure date of a window, inclusive. Pair with depart_to. | |
| destination | Yes | IATA airport or city code (ZRH, BCN, NYC) or a city or airport name (Zurich, Barcelona). | |
| max_results | No | How many itineraries to return, at most 20. | |
| return_date | No | Fixed return date YYYY-MM-DD. Omit for one-way. Do not combine with nights. | |
| flexibility_days | No | Days before and after depart_date. 0 searches that day only. Not used with a from/to window. |
Output Schema
| Name | Required | Description |
|---|---|---|
| days | Yes | |
| cabin | Yes | |
| count | Yes | |
| notes | Yes | |
| picks | Yes | |
| adults | Yes | |
| cached | Yes | |
| nights | Yes | Length of stay applied to each departure, when requested. |
| origin | Yes | |
| source | Yes | |
| flights | Yes | |
| nonstop | Yes | |
| currency | Yes | |
| depart_to | Yes | |
| search_url | Yes | Google Flights search for the first date that was actually queried. |
| depart_date | Yes | The 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_from | Yes | |
| destination | Yes | |
| origin_name | Yes | |
| return_date | Yes | Fixed return date, when one was requested. Null for one-way or when nights varies the return. |
| dates_searched | Yes | |
| destination_name | Yes | |
| dates_not_searched | Yes | Dates inside the requested window that were not queried, because of the 7-date cap or the timeout. |
TDQS
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.
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.
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.
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.
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.
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.
2 tool updates
v1.1.0- First observed
resolve_airports - First observed
search_flights
TDQS
Scored across 2 tools
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.
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.
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.
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
Related MCP Connectors
Search and compare flight offers through a cache-aware Streamable HTTP MCP server for AI agents.
Flight search & booking for AI agents. 400+ airlines, $20-50 cheaper than OTAs.
Live flight prices and working booking links for AI agents and travel apps.
Real-time Google Flights fares for agents. Three things people do with this server. Scan for deals: one call takes a date range and a list of destination airports, expands every combination server side, and returns each fare with Google's own low, typical or high verdict. Put live search in your app: flat JSON with a bookable link on every result, and round trips priced as paired legs. Run a 24/7 AI travel agent: add the server, sign in with Google, and schedule it. No ads, no sponsored content. You bring your own RapidAPI key, so every search is billed to your plan and never to anyone else's. Add https://flights.flightpowers.com/mcp , click Sign in, sign in with Google, and paste your RapidAPI key once on the page that opens. Scripts and clients without a sign-in button send the key as x-rapidapi-key on the same URL.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceFlight search for AI agents: flexible-date cheapest round-trips with a good-price verdict from historical data. Hosted remote MCP, free, no key.3MIT
- AlicenseNot gradedqualityCmaintenanceFlexible 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
- AlicenseAqualityBmaintenanceEnables 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.1MIT
- AlicenseAqualityBmaintenanceEnables 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.41MIT