Skip to main content
Glama
PhilippeSch

google-flights-mcp

by PhilippeSch
README.md
# google-flights-mcp

Remote MCP server (Streamable HTTP, stateless) for Claude that searches Google Flights.
It is a thin MCP layer on top of [fast-flights](https://github.com/AWeirdDev/flights) by
AWeirdDev and contributors, which does the heavy lifting of talking to Google (see [Credits](#credits)).
Runs as a Docker container (here on a Synology NAS) and is reachable from anywhere over HTTPS.

Unofficial: not affiliated with or endorsed by Google. It reads the public Google Flights page,
which may conflict with Google's terms of service; use it for personal purposes and at low volume.

## Tools

| Tool | Purpose |
|---|---|
| `search_flights` | One-way or round-trip search with filters: cabin, passengers, max stops, airlines/alliances, departure hour windows, max duration, connecting airports, layover min/max, max price, bags, basic economy, self-transfer, lower emissions. Returns price, legs with flight number, codeshares, operator, local times and aircraft, layovers, CO2 and the Google Flights link. For round trips the cheapest outbound options come with their actual return flights and the whole-trip price of each pair (`return_options`, one extra Google request each, `return_flights_for_top`). |
| `compare_dates` | Cheapest itinerary per departure date over up to 10 days, optionally as round-trip with fixed trip length. |
| `compare_round_trip_vs_separate` | Round-trip ticket vs outbound and return as two one-way tickets: three searches, verdict and saving. |
| `search_open_jaw` | Open-jaw trip (Gabelflug): back from or to a different airport, priced as two one-ways. |
| `search_multi_city` | 2 to 6 one-way legs, cheapest option per leg and the total. |

Claude picks the tool by itself: the server `instructions` and the tool descriptions say when to
use them (any question about flights, fares, cheapest day, round trip vs separate, open-jaw), in
English and German. If Claude still answers from memory, ask it to "check Google Flights".

All search tools accept `airline_group`:

- `star_alliance`: Star Alliance airlines only.
- `mm_qualifying`: only itineraries where every leg has a flight number that earns Miles & More
  Qualifying Points (Lufthansa Group: LH, LX, OS, SN, EN, 4Y, EW, VL, AZ, plus LOT, Croatia,
  Luxair). Each leg then shows `mm_qp_flight_number`. The list is `MM_QP_CARRIERS` in `server.py`.
  QP also need a booking class that earns miles, which Google does not show.

## Deploy

The image is built by GitHub Actions on every push to `main` and published to `ghcr.io/philippesch/google-flights-mcp:latest`
(`linux/amd64` and `linux/arm64`). Make the package public once (GitHub → Packages →
google-flights-mcp → Package settings → Change visibility), or log the host in to ghcr.io.

```bash
cp .env.example .env          # set MCP_SECRET (openssl rand -hex 24)
docker compose up -d          # pulls the image
curl http://localhost:8000/health   # {"status":"ok"}
python smoke_test.py --url http://localhost:8000/<MCP_SECRET>/mcp
```

Build from local source instead: `docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build`.

### Update

After a push, wait for the GitHub Action to finish, then pull the new image:

- **Synology Container Manager**: Project → `flights-mcp` → Action → *Build* (pulls `latest` and
  recreates the container). Or Image → pull `ghcr.io/philippesch/google-flights-mcp:latest`,
  then restart the project.
- **Plain Docker**: `docker compose pull && docker compose up -d`.
- **Automatic**: run [Watchtower](https://containrrr.dev/watchtower/) for the `flights-mcp` container.

## Synology setup (as deployed)

1. DSM 7.2 or newer with Container Manager (DS218+: DSM 7.3 works).
2. Copy `docker-compose.yml` and a filled `.env` to a share, e.g. `/volume1/docker/flights-mcp`.
3. Container Manager → Project → Create, path `/docker/flights-mcp`, use the existing compose file.
4. DNS: CNAME `flights.<your-domain>` to the host that points at your public IP. Ports 80 and 443
   must be forwarded to the NAS.
5. Control Panel → Security → Certificate → Add → Let's Encrypt for `flights.<your-domain>`.
6. Control Panel → Login Portal → Advanced → Reverse Proxy → Create: source HTTPS
   `flights.<your-domain>` port 443, destination HTTP `localhost` port 8000.
7. Control Panel → Security → Certificate → Settings: assign the new certificate to the
   `flights.<your-domain>` service.

## Add to Claude

Access protection is the secret path, there is no OAuth.

- **claude.ai**: Settings → Connectors → *Add custom connector* → `https://flights.<your-domain>/<MCP_SECRET>/mcp`.
  Enable it per chat via **+ → Connectors**.
- **Claude Code**: `claude mcp add --transport http flights https://flights.<your-domain>/<MCP_SECRET>/mcp --scope user`

Treat the URL like a password: anyone who has it can use the server. Rotate it by changing
`MCP_SECRET` in `.env` and restarting the container.

## Configuration (`.env`)

| Variable | Default | Meaning |
|---|---|---|
| `MCP_SECRET` | none | Path prefix of the endpoint. Always set it when public. |
| `DEFAULT_CURRENCY` / `DEFAULT_LANGUAGE` | `CHF` / `en-GB` | Used when a call does not set them. |
| `CACHE_TTL_SECONDS` | `600` | In-memory result cache. |
| `FLIGHTS_PROXY` | none | HTTP(S) proxy for Google requests. |
| `GOOGLE_COOKIES` | built-in `SOCS` | Override the consent cookie, `NAME=value; NAME2=value2`. |
| `CF_TUNNEL_TOKEN` | none | Only for the optional Cloudflare Tunnel profile. |

## Limitations

- No booking classes or fare brands. Flight numbers are read from Google's raw payload by `server.py`.
- Scraping can break when Google changes its page, and Google may rate-limit the IP
  (set `FLIGHTS_PROXY` if needed). Requests originate from the server's IP, so prices
  reflect that point of sale. Keep request volume low.
- A built-in `SOCS` cookie ("reject all") skips Google's consent page for EU/CH IPs. If Google still
  shows a consent page, the tool says so; then set `GOOGLE_COOKIES` to override it.
- Google lists only outbound flights for a round trip. The server therefore repeats the search with the chosen outbound flight (Google's own URL format, not part of fast-flights) to get the matching returns; only the cheapest few outbound options get this lookup.
- Open-jaw, multi-city and separate-ticket combinations are sums of independent one-way searches:
  no protected connections, and bags and rebooking rules differ per ticket.
- `compare_dates` makes one Google request per date and takes 20 to 40 seconds for 10 days.
- Max 2 parallel Google requests.

## Credits

This project would not exist without other people's work:

- **[fast-flights](https://github.com/AWeirdDev/flights)** by AWeirdDev and contributors (MIT).
  It builds the Google Flights query and parses the result page. `server.py` imports it as a
  dependency (`create_query`, `parse_js`) and its HTTP fetch follows the approach of
  fast-flights' own fetcher (browser impersonation with `primp`). fast-flights credits
  @kftang for discovering the data format Google embeds in the page.
- **[MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)** (MIT) for the server.
- [primp](https://github.com/deedy5/primp), [selectolax](https://github.com/rushter/selectolax),
  [uvicorn](https://www.uvicorn.org/), [Starlette](https://www.starlette.io/) and
  [pydantic](https://docs.pydantic.dev/).
- Flight data comes from **Google Flights**; all rights to that data belong to Google and the airlines.

What this repository adds: the MCP server and tools, flight numbers/codeshares/operator per leg,
merging Google's "Top flights" with "Other flights", the Star Alliance and Miles & More
Qualifying Points filters, round trip vs separate tickets, open-jaw and multi-city pricing,
the consent-cookie handling, caching and the Docker/GitHub Actions/Synology setup.

Third-party license texts: see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).

## License

[MIT](LICENSE). Dependencies keep their own licenses, see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).