Google Flights MCP: real-time fares your agent can search across a whole date range
An MCP server for live flight prices and hotel rates: Claude, Claude Code, Cursor, Cline and any other MCP client get
real-time Google Flights fares and Booking.com room rates as tools. One call searches a date range and a list of destination
airports, every fare carries Google's own low / typical / high verdict and price range, and a round trip is priced as one
request (both legs, one total, one booking link). Hosted at https://flights.flightpowers.com/mcp (hotels at
https://hotels.flightpowers.com/mcp), nothing to install. It finds and prices, it does not book.
Built by FlightPowers, a travel data API for developers and AI agents: REST, MCP servers and an n8n node on one RapidAPI key. Sign in with Google and your first 10 searches each day are free.
Where to get a key: Google Flights Live API and Booking Live API on RapidAPI. A free pack of 10 searches a month, then PRO is $10 for 2,500 flight searches, about a sixth of SerpApi's price per search (their cheapest plan is $25 for 1,000).
One URL, either way in:
claude mcp add --transport http google-flights https://flights.flightpowers.com/mcp
In a client that supports MCP authorization, a Sign in button appears: you sign in with
Google, and your first 10 searches each day are free on our key, with nothing to
paste. Paste your own RapidAPI key once on the /connect page when you want that cap gone, and
nothing goes in your client config either way. In a client that shows no Sign in button, bring
the key yourself:
claude mcp add --transport http google-flights https://flights.flightpowers.com/mcp --header "x-rapidapi-key: YOUR_RAPIDAPI_KEY"
Same URL, both times. A request carrying a credential of any kind is served; a request carrying
nothing at all is answered 401 with the OAuth metadata, which is what makes the Sign in button
appear. https://flights.flightpowers.com/mcp/oauth is still live and demands the sign-in on the
first request, for clients whose auth mode is fixed when a server is added.
Hosted. Nothing to clone, nothing to build. Listed in the official MCP Registry as
com.flightpowers/google-flights-mcp. Health check:
/health.
No key yet? You do not need one to start. Add the URL above, sign in with Google, and the first 10 searches each UTC day run on our key, with nothing to paste. The exact unit counted is in Try it with no key.
Need a key? Subscribe to the Google Flights Live API on RapidAPI, free tier available,
and copy your x-rapidapi-key: https://rapidapi.com/mtnrabi/api/google-flights-live-api
Want more free searches than that, and do not mind ads? There is a separate free,
ad-supported server:
claude mcp add --transport http google-flights-free https://free-trial.flightpowers.com/mcp
(50 searches a day and 250 a month per signed-in account, one disclosed sponsored card per
result, fan-out capped at 15, and clients that cannot render the sponsored card may be capped
further.) Come back here when the ads, the 15-search cap, or those client restrictions get in
your way.
What your agent gets
Two tools that answer a fare question, not a date lookup.
- Ask open-ended questions. "Cheapest one-way to Sri Lanka anywhere in October", "5 to 7
nights in Rome sometime in May, from Tel Aviv or Larnaca": each is one tool call. Both
tools take a departure date range, a list of destination airports, and (round-trip) a
nightsvalue instead of a fixed return date, and expand them internally. - Say whether a price is actually good. Every result carries Google's own historical range
for that route and period:
price_insights_low,price_insights_high, and aprice_range_in_relation_to_other_periodsverdict oflow/typical/high. That is what lets an agent answer "$209 is typical here, don't rush" instead of just quoting a number. - Book, not just browse. Every result includes a
buy_linkto Google Flights. - Know what it spent. Every response carries
api_usage: requests used by this call, and what is left on the caller's plan. See Spend reporting. - Know what it searched. Every response carries
search_coverage, so the model can say honestly which dates and destinations the answer is based on.
Results are live fares. They go stale within minutes: when you need a current price, search again rather than reusing an earlier result, and state when the data was fetched.
Try it with no key: sign in with Google
A signed-in account gets 10 free searches per UTC day on our key, with nothing to
paste into your client. Sign in the way your client offers it, or at
https://flights.flightpowers.com/connect, and ask for a fare. Ten is what the hosted
deployment runs today; /health reports the live values as trial_enabled and trial_day_cap
and is the number to trust. (The allowance is off by default in this code and needs both
PAID_TRIAL_DAY_CAP and PAID_TRIAL_RAPIDAPI_KEY set on purpose, so a deployment you run
yourself has none until you configure one.)
A search is one date x destination combination, so one call with a three-day range spends three.
The allowance is held for the whole plan before the first request goes out and whatever the
fan-out did not send is given straight back, so two calls at once cannot both spend it. Every
result carries a trial block with the count and the cap. Past the cap the tools answer with
search_status: "trial_exhausted" and no results -- never an error, and retrying does not help;
the allowance renews at 00:00 UTC.
A free search is capped by what is left of the day: a range or grid larger than that (a whole
month at three trip lengths is 93 searches) is refused before anything runs, with the counts and a
smaller question to ask instead. With your own key the cap rises by itself to cover a month-sized
request. Comparing several sources in one call (compare_hotel_rates, on a deployment that
enables it) is not part of the allowance and asks for your own key.
Connecting your own RapidAPI key removes the cap entirely. That is the next section.
Get a key (free tier available)
The server holds no upstream credential of its own. Every search beyond the free allowance is billed to your RapidAPI subscription, which is why the key travels with the request.
- Subscribe to the Google Flights Live API: https://rapidapi.com/mtnrabi/api/google-flights-live-api
- Copy your
x-rapidapi-key. - Pass it to the server in any one of the three ways below.
If a key is missing and the allowance is not available to you, the tools do not fail silently and
do not spend anything. They return needs_api_key: true with the signup URL and these
instructions, phrased for the model to read back to you.
Three ways to pass your key
| Way | How | When to use it |
|---|---|---|
| Header (preferred) | --header "x-rapidapi-key: YOUR_RAPIDAPI_KEY" |
Anything that lets you set headers. Keys stay out of URLs, and therefore out of proxy and access logs. |
| Query parameter | https://google-flights-mcp.flightpowers.com/mcp?rapidapi_key=YOUR_RAPIDAPI_KEY |
Hosts that only let you paste a URL: claude.ai's custom-connector dialog is the case that matters. |
| Client API-key field | Paste the key into the client's own "API key" box | Hosts that send authorization: Bearer <key> or x-api-key. Smithery's saved-config form (config.rapidApiKey=) is also accepted. |
First non-empty source wins, in that order. The key is never logged, never echoed into an error message, and never returned in a tool response.
A fourth way: sign in once at /connect
This is the page the sign-in URL at the top of this README sends you to. A client that speaks MCP authorization walks you through it on its own; the steps below are the same thing done by hand.
Where a deployment has it enabled (check connect_enabled on /health), there is a page at
/connect that replaces all of the above with a sign-in:
- Open https://google-flights-mcp.flightpowers.com/connect (hotels: **https://hotels.flightpowers.com/connect**) and sign in with Google.
- Paste your RapidAPI key once, into a form, over TLS.
- Press Reveal the URL and copy the connect URL,
…/mcp?fp_token=fpk_…, and use that as the server URL in your MCP client. Clients that let you set headers can send the same token asAuthorization: Bearer fpk_…instead. (It is hidden until you ask for it: that URL is a 90-day bearer credential for your plan, and a page that prints one by default puts it in every screenshot and screen share.)
If your client signed you in itself -- Claude, Cursor, ChatGPT and anything else that speaks
MCP authorization -- there is no URL to copy. /connect says so: it shows the key you
connected and tells you to go back to your assistant. There is a link on it for the case where a
second client cannot sign in and does need a connect URL.
What that buys you: your RapidAPI key is not in your client config, not in a URL, and not in
whatever logs that URL passes through. What it costs: the server stores your key, encrypted, and
knows your Google account id and email address. Disconnect on the same page deletes the record
and kills every connect token for your account, immediately. The full description is
section 2a of the privacy policy.
Some details worth knowing:
- Saving runs one check. The key is validated against the listing before it is stored, so a typo fails on the page rather than in your client an hour later. That check costs at most one request from your own plan: on the free BASIC plan (10 a month), one of ten. A key that RapidAPI rejects at the gateway costs nothing.
- A key on the request always wins. If you send an
x-rapidapi-keyheader (or any of the other channels above) and carry a connect token, the request's own key is used. Nothing you already have set up changes behaviour because you signed in. - The token is not your key and cannot be turned back into it. It is valid for 90 days, and
it stops resolving the moment you disconnect. A call carrying a token whose key has been
disconnected gets a
needs_api_keyreply telling you to reconnect. It never falls back to somebody else's subscription and never spends anything. - BASIC is free. Google Flights Live API · Booking Live API. One RapidAPI key covers whichever of the two you have subscribed to; you connect it once.
Running /connect on your own deployment
Off unless all four of these are set. A half-configured deployment registers none of the
routes and serves keyed callers exactly as before; /health reports connect_enabled so that is
visible rather than guessed.
| Variable | What it is |
|---|---|
GOOGLE_OAUTH_CLIENT_ID |
Google Cloud Console → Credentials → OAuth client ID, type Web application. Ends .apps.googleusercontent.com. |
GOOGLE_OAUTH_CLIENT_SECRET |
The same client's secret (GOCSPX-…). |
MCP_KEY_MASTER |
32 bytes, base64: openssl rand -base64 32. Encrypts stored keys (AES-256-GCM) and derives the cookie and token signing keys. |
DATABASE_URL |
Neon Postgres, pooled endpoint (…-pooler…). Schema: migrations/001_mcp_user_keys.sql. |
Optional: MCP_CONNECT_VALIDATE=0 stores a pasted key without checking it first.
Authorised redirect URIs to register on the Google client, one per product origin, exactly:
https://google-flights-mcp.flightpowers.com/connect/callback
https://hotels.flightpowers.com/connect/callback
flights.flightpowers.com needs no entry. /connect and /connect/start bounce an alias to
the canonical origin before the sign-in starts, because cookies are per-host and Google compares
redirect_uri literally: an alias that started its own sign-in would come back to a host with no
state cookie and fail with a message that reads like a Google misconfiguration.
Also on the OAuth consent screen: scopes openid and .../auth/userinfo.email, and nothing else.
Rotating MCP_KEY_MASTER logs everybody out and invalidates every stored key. That is
deliberate: after a rotation nothing is left holding a token that resolves to a key nobody can
read. Users see "connect again", not a failed search. key_version on the table is there so a
staged rotation is possible later without a flag day.
Verifying the flow end to end
Migration first, once per database:
psql "$DATABASE_URL" -f migrations/001_mcp_user_keys.sql
Then, after deploying:
# 1. The feature is actually on.
curl -s https://google-flights-mcp.flightpowers.com/health | grep connect_enabled
# 2. The page renders for an anonymous visitor.
curl -sI https://google-flights-mcp.flightpowers.com/connect # 200
curl -sI https://google-flights-mcp.flightpowers.com/connect/start # 302 to accounts.google.com
# 3. Sign in in a browser, paste a key, press Reveal and copy the connect URL.
# 4. MCP Inspector against that URL -- list the tools, then run one real search.
npx @modelcontextprotocol/inspector
# Transport: Streamable HTTP
# URL: https://google-flights-mcp.flightpowers.com/mcp?fp_token=fpk_...
# 5. Claude Code, the same URL.
claude mcp add --transport http flightpowers \
"https://google-flights-mcp.flightpowers.com/mcp?fp_token=fpk_..."
claude mcp list # shows it connected
# then, in a session: ask for a fare and check the result is real
# 6. Cursor: Settings -> MCP -> Add, same URL. Or in ~/.cursor/mcp.json:
# { "mcpServers": { "flightpowers": {
# "url": "https://google-flights-mcp.flightpowers.com/mcp?fp_token=fpk_..." } } }
# 7. Hotels, the other hostname, with the same token.
# https://hotels.flightpowers.com/mcp?fp_token=fpk_...
# 8. Press Disconnect on /connect, then re-run step 4. The tool must answer
# needs_api_key with a "connect again" message -- not a search, and not a
# generic "get a key" reply.
A tools/list that succeeds proves nothing about any of this: a token is only consulted when a
tool actually runs. Step 4 has to be a real search.
A fifth way: sign in from inside your MCP client
An MCP client only starts a sign-in when a request comes back 401 with a
WWW-Authenticate: Bearer resource_metadata=… header. Nothing else on the wire tells it one is
available. Until 2026-09-09 /mcp never sent that header, so a caller with no key got a 200
whose body said needs_api_key: correct JSON, invisible to every client's auth machinery, and
the user saw "the tool failed" with nowhere to click.
Now /mcp challenges, but only a caller who brought nothing, and only on a call that would spend something:
| URL | Behaviour |
|---|---|
https://flights.flightpowers.com/mcp |
The one to use. A RapidAPI key in a header, on the query string or in a Smithery config blob, an fpk_ connect token or an fpo_ access token: all served exactly as before. Nothing at all: initialize, tools/list and the rest of the read-only handshake are still answered, and tools/call gets 401 + the challenge, so your client offers a Sign in button. |
https://hotels.flightpowers.com/mcp |
The same, for hotels. |
…/mcp/oauth |
The same server with the sign-in demanded on request one. For clients whose auth mode is fixed when a server is added, and for connectors saved on that URL before the change. |
Same tools, same product-per-hostname routing, same everything else. The alias is the identical tool registry behind a token check, not a second copy of the server.
The property that protects paying integrations: a request carrying a credential is never
challenged, and the credential order is unchanged (header, query, config blob, then OAuth
identity, then a connect token, then the RAPIDAPI_KEY env fallback). A wrong key is not
"nothing" either: it reaches the tools and comes back as the precise error RapidAPI gave,
because replacing that with a sign-in prompt would be a worse answer. A deployment with
RAPIDAPI_KEY set serves keyless callers off its own plan on purpose and is never challenged.
MCP_REQUIRE_AUTH=off turns the challenge off entirely, with no deploy.
Discovery stays open, and that was learned the hard way. For a few hours on 2026-09-09 the
challenge covered initialize and tools/list too. Glama re-checks every connector hourly by
opening an MCP connection and listing its tools, with no credentials; both paid listings were
marked unhealthy and ranked down the same evening, and Smithery's release scan, mcpservers.org
and M8ven probe the same way. So the line is drawn at spending, not at connecting: initialize,
notifications/initialized, ping, tools/list, prompts/list and resources/list are served
to anybody (src/discovery.py), and everything else needs a key or a sign-in. A batch with a
tools/call in it, an oversized body and an unparseable one are all challenged — the allowlist
fails closed. /mcp/oauth still challenges everything, which is the URL to give a directory
that wants a server always requiring auth, and the public, unauthenticated
/.well-known/mcp/server-card.json is still there for a scanner that reads a card instead.
What it is like to use. Paste the /mcp/oauth URL into your client. It registers itself,
opens a browser, you sign in with Google, and you approve that client by name on a page that
says exactly what it will be able to do: run searches billed to your own RapidAPI plan, nothing
else. The client never sees your RapidAPI key. If you have not connected one yet, the approval
still works and the first search comes back telling you to paste a key at /connect, with the
URL.
What you can revoke, and how. Press Disconnect on /connect: the stored key is deleted
and every OAuth token for that Google account is dropped in the same action. A client that was
connected stops working immediately. Individually, a client can call /oauth/revoke (RFC 7009).
Client setup
# Claude Code
claude mcp add --transport http flightpowers \
"https://google-flights-mcp.flightpowers.com/mcp/oauth"
claude mcp list # shows "needs authentication" until you sign in
/mcp # in a session: pick the server, follow the sign-in
# Cursor -- Settings -> MCP -> Add, URL above. Or ~/.cursor/mcp.json:
# { "mcpServers": { "flightpowers": {
# "url": "https://google-flights-mcp.flightpowers.com/mcp/oauth" } } }
# Cursor discovers the 401, registers itself and opens the browser.
# ChatGPT -- Settings -> Connectors -> Create. It asks for:
# MCP server URL: https://google-flights-mcp.flightpowers.com/mcp/oauth
# Authentication: OAuth
# Leave client id and secret EMPTY: this server supports dynamic client
# registration, so ChatGPT registers itself. Nothing else has to be filled in.
# MCP Inspector -- the quickest way to watch the whole handshake.
npx @modelcontextprotocol/inspector
# Transport: Streamable HTTP
# URL: https://google-flights-mcp.flightpowers.com/mcp/oauth
# Auth: OAuth 2.0 -> "Guided OAuth Flow" walks metadata -> DCR ->
# authorize -> token, and shows each response. Then run ONE real search.
The protocol surface
| Route | Spec |
|---|---|
GET /.well-known/oauth-protected-resource and …/mcp/oauth |
RFC 9728 |
GET /.well-known/oauth-authorization-server and …/mcp/oauth |
RFC 8414 |
POST /oauth/register |
RFC 7591, dynamic client registration, open |