Back to Browse

Google Flights MCP Server

Developer ToolsUse Caution4.2MCP RegistryLocalRemote
Free

Server data from the Official MCP Registry

Live Booking.com hotel prices, plus per-country pricing for rate-parity monitoring.

About

Live Booking.com hotel prices, plus per-country pricing for rate-parity monitoring.

Remote endpoints: streamable-http: https://hotels.flightpowers.com/mcp

Security Report

4.2
Use Caution4.2High Risk

This MCP server implements a Google Flights API wrapper with generally sound security architecture. Authentication and authorization are well-designed with OAuth 2.0, token rotation, and proper credential handling. However, there are several code quality concerns: broad exception handling, potential information leakage in error messages, and a dependency constraint workaround that masks an underlying fragility. The server appropriately handles sensitive credentials (RapidAPI keys) without hardcoding or logging them, and permissions align with its stated purpose of making authenticated API calls. Supply chain analysis found 9 known vulnerabilities in dependencies (1 critical, 4 high severity).

3 files analyzed · 15 issues found

Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.

Permissions Required

This plugin requests these system permissions. Most are normal for its category.

HTTP Network Access

Connects to external APIs or services over the internet.

env_vars

Check that this permission is expected for this type of plugin.

File System Read

Reads files on your machine. Normal for tools that analyze or process local data.

How to Install & Connect

Available as Local & Remote

This plugin can run on your machine or connect to a hosted endpoint. during install.

Documentation

View on GitHub

From the project's GitHub README.

Google Flights MCP: real-time fares your agent can search across a whole date range, ad-free

FlightPowers is a travel data API for developers and AI agents: live Google Flights fares with Google's own low / typical / high price band and a round trip priced as one request, plus live Booking.com hotel rates, over REST, MCP servers and an n8n node on one RapidAPI key. Free tier of 10 searches; 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). Best for price tracking, date scans and AI agents; it does not book. Ad-free on your own RapidAPI key; a date range and a destination list in one call.

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 and ad-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, ad-free, 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 nights value 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 a price_range_in_relation_to_other_periods verdict of low / 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_link to 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: never cache a fare or reuse an earlier result; search again 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, ad-free, 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.

Comparing several sources in one call (compare_hotel_rates, or search_hotels with more than one providers entry) 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.

  1. Subscribe to the Google Flights Live API: https://rapidapi.com/mtnrabi/api/google-flights-live-api
  2. Copy your x-rapidapi-key.
  3. 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

WayHowWhen 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 parameterhttps://google-flights-mcp.flightpowers.com/mcp?rapidapi_key=YOUR_RAPIDAPI_KEYHosts that only let you paste a URL: claude.ai's custom-connector dialog is the case that matters.
Client API-key fieldPaste the key into the client's own "API key" boxHosts 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:

  1. Open https://google-flights-mcp.flightpowers.com/connect (hotels: https://hotels.flightpowers.com/connect) and sign in with Google.
  2. Paste your RapidAPI key once, into a form, over TLS.
  3. 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 as Authorization: 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-key header (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_key reply 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.

VariableWhat it is
GOOGLE_OAUTH_CLIENT_IDGoogle Cloud Console → Credentials → OAuth client ID, type Web application. Ends .apps.googleusercontent.com.
GOOGLE_OAUTH_CLIENT_SECRETThe same client's secret (GOCSPX-…).
MCP_KEY_MASTER32 bytes, base64: openssl rand -base64 32. Encrypts stored keys (AES-256-GCM) and derives the cookie and token signing keys.
DATABASE_URLNeon 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:

URLBehaviour
https://flights.flightpowers.com/mcpThe 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/mcpThe same, for hotels.
…/mcp/oauthThe 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

RouteSpec
GET /.well-known/oauth-protected-resource and …/mcp/oauthRFC 9728
GET /.well-known/oauth-authorization-server and …/mcp/oauthRFC 8414
POST /oauth/registerRFC 7591, dynamic client registration, open
GET /connect/authorize · POST /connect/authorizeRFC 6749 §4.1, PKCE S256 required
POST /oauth/tokenauthorization_code and refresh_token
POST /oauth/revokeRFC 7009

The authorization endpoint is under /connect on purpose: the sign-in session cookie is scoped Path=/connect so it can never be attached to a /mcp request, and putting authorize anywhere else would mean either widening that cookie or making the user sign in twice.

Codes live 10 minutes and are single-use (DELETE … RETURNING, so two concurrent exchanges race on one row and exactly one wins). Access tokens live 1 hour, refresh tokens 30 days with rotation. Everything is opaque and stored as a SHA-256 hash, so a dump of the database contains nothing that can be replayed. Tokens are not JWTs, deliberately: a signed token stays valid until it expires whatever we decide afterwards, and Disconnect has to mean disconnect.

An access token is checked against the resource it was approved for before it is accepted, not only when it is issued. Both products are the same deployment, the same database and the same stored RapidAPI key per user, so without that check a token approved on the flights consent page, which says "search live flight fares" and nothing else, would be accepted on the hotels hostname and spend the user's hotels plan. The check is strict about the host and forgiving about the path, because clients in the wild send the origin, /mcp and /mcp/oauth for the same server. tests/test_oauth.py::TestATokenIsBoundToTheResourceItWasApprovedFor pins both halves.

Running it on your own deployment

No new environment variable. It comes on wherever /connect is configured, because it reuses that Google sign-in and that key store. It needs one more table in the same database:

psql "$DATABASE_URL" -f migrations/002_mcp_oauth.sql

/health then reports oauth_enabled: true and oauth_mcp_endpoint; that URL is what goes in a directory listing. MCP_OAUTH=off disables it while leaving /connect running; that is the rollback that needs no code change.

Nothing has to change on the Google OAuth client. The redirect URI is still …/connect/callback, because the MCP client's OAuth flow ends at our authorize page, and only that page talks to Google.

Verifying it end to end

BASE=https://google-flights-mcp.flightpowers.com

# 1. On, and advertising itself.
curl -s $BASE/health | python3 -m json.tool | grep oauth_

# 2. The challenge. This is the whole feature in one response.
curl -si $BASE/mcp/oauth | head -20
#   HTTP/2 401
#   www-authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource/mcp/oauth"

# 3. Discovery, at both the scoped and the bare path.
curl -s $BASE/.well-known/oauth-protected-resource/mcp/oauth | python3 -m json.tool
curl -s $BASE/.well-known/oauth-authorization-server | python3 -m json.tool

# 4. Dynamic registration answers.
curl -s -X POST $BASE/oauth/register -H 'content-type: application/json' \
  -d '{"client_name":"probe","redirect_uris":["http://127.0.0.1:9999/cb"],
       "token_endpoint_auth_method":"none"}' | python3 -m json.tool

# 5. The real test: MCP Inspector, Guided OAuth Flow, then ONE real search.
#    A tools/list proves nothing -- the token is only consulted when a tool runs.

# 6. Hotels, the other hostname, same walk: https://hotels.flightpowers.com/mcp/oauth

# 7. /mcp is untouched. This must still work, with no 401 anywhere:
curl -s -X POST $BASE/mcp -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H "x-rapidapi-key: $REAL_KEY" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}'

# 8. Disconnect on /connect, then re-run step 5's search: it must answer
#    needs_api_key, and the client must be logged out.

Keeping the registration table honest

/oauth/register is open, because the MCP spec requires it and because a client_id on its own authorises nothing: every flow through one still ends at a consent page a signed-in human has to press a button on. Open is not the same as unlimited, so three things stand behind it.

MechanismWhere it livesWhat it stops
Rate limitin memory, per instancea burst: 10 registrations per address per 10 minutes, 60 token requests per minute, 10 /connect/save per hour. Over the limit is 429 with Retry-After.
Daily capsPostgres, so every instance agreesa slow drip: 30 registrations per address per day. The global cap is 5,000 a day -- a backstop against unbounded rows, not a defence: a global number set near real traffic is a lever an attacker pulls to refuse every new Claude or Cursor user for a day. Crossing 500 in a day logs and refuses nothing. MCP_OAUTH_DCR_MAX_PER_IP_PER_DAY, MCP_OAUTH_DCR_MAX_PER_DAY and MCP_OAUTH_DCR_WARN_PER_DAY move them without a deploy.
Sweepon /oauth/register, at most once every 15 minutes per instancethe litter: expired codes and tokens, and registrations that never became an authorization within 7 days. A client with a live token, an outstanding code, or a consent page that has been rendered for it is never swept.

The rate limit is per INSTANCE. On Vercel that means N warm instances allow up to N times those numbers between them, and a cold start starts the counters at zero. That is why the durable caps exist as well: they are counted in the database, where the number is the same everywhere.

The address every one of these is keyed on comes from x-real-ip first -- Vercel sets it to the peer it accepted -- and otherwise from the LAST usable entry of x-forwarded-for, skipping hops that can only be internal. Never entry 0: proxies append on the right, so the left-most entry is whatever the caller wrote, and reading it would make every limit here one header away from being bypassed. A request with neither header shares one bucket named unknown, which is rate limited as a single caller and is not subject to the durable per-address cap (one missing header on the edge must not lock the whole server out for a day).

A registration is stamped as in-use when its consent page is RENDERED, not only when the human presses Approve. MCP clients commonly register when they are installed and authorize days later, and the sweep must not delete a row while its consent page is on screen -- there is no foreign key from codes or tokens back to the client, so the exchange that followed would fail invalid_client with nothing naming the cause.

Requires one migration:

psql "$DATABASE_URL" -f migrations/003_mcp_oauth_hygiene.sql

Refresh tokens rotate, and a replay revokes the family

A refresh token is single-use: exchanging it issues a new pair and retires the one presented. The retired row is kept and stamped, not deleted, because a deleted row and a token that was never issued look identical -- and telling those apart is the point. Presenting an already-rotated refresh token means either a client that lost the response or a copy in somebody else's hands, and OAuth 2.1 §4.14.2 says to assume the second: the answer is invalid_grant, and every token descended from that authorization is deleted. The honest client signs in again; the thief's access token stops working at the same moment.

With one deliberate exception, for the case that is almost always the innocent one: the FIRST replay of the token we just rotated, from the same client, within 10 seconds, is answered with the pair that rotation already issued. It is an idempotent retry -- nothing new is created -- and it means a client whose response was lost to a dropped connection is not silently signed out. A second replay, or one after the window, is the real thing and still kills the family. The window is per instance and in memory, so a miss simply falls through to the conservative answer.

Revoking a refresh token through /oauth/revoke takes its access tokens with it, for the same reason (RFC 7009 §2.1) -- and only if the token was issued to the client asking, which is the other half of that section. A client presenting somebody else's token still gets 200 (§2.2) and nothing is revoked.

A client_id can be a URL

client_id_metadata_document_supported: true is advertised in /.well-known/oauth-authorization-server. A client may use an https URL as its client_id; the document at that URL lists its redirect_uris, and we fetch and check it per flow instead of writing a registration row. Smithery asks for this before it will proxy a remote OAuth server.

What is checked, every time: https only, a public hostname (no IP literals, no localhost, no credentials in the URL), the hostname resolved and every address it answers with required to be public unicast (a name is not a control: 127.0.0.1.nip.io has a dot in it and points at loopback), no redirect followed, a 5-second timeout, a 64 KB cap enforced while the body is read rather than after it is buffered, a client_id inside the document that matches the URL if it is present, and -- the one that matters -- the redirect_uri in the request must be listed in the document. The lookup is rate limited on its own (60 per address per 10 minutes), because it is the only outbound fetch in this server that a caller can trigger before signing in. Dynamic registration is unchanged and still the default: a client_id that is not an https URL is looked up in the table exactly as before.

Tools

ToolWhat it does
search_oneway_flightsReal-time one-way fares. Input: origin IATA, destination IATA or a list, and either one departure date or a date range. Returns price, airline, duration, stops, buy_link, and Google's historical price range so you can judge the fare. Use for any one-way question, including open-ended ones: one call with a range, never one call per date.
search_roundtrip_flightsReal-time round-trip fares priced as paired legs, not two one-ways. Input: origin, destination(s), a departure date or range, and either a return_date or a trip length in nights (a number or a list like [5,6,7]). Returns total price, per-leg airline/stops/duration, and one buy_link for the trip.

The hotels deployment serves search_hotels, find_hotel_by_name and compare_hotel_rates instead; see Hotels: providers and compare_hotel_rates.

search_oneway_flights

search_oneway_flights(
    from_airport: str,                     # origin IATA, e.g. "TLV"
    to_airport: str | list[str],           # destination IATA, or a list to compare
    departure_date: str | None = None,     # "YYYY-MM-DD"
    departure_date_from: str | None = None,# first date of a range
    departure_date_to: str | None = None,  # last date of a range
    max_stops: int | None = None,          # 0 = non-stop only
    airline_codes: list[str] | None = None,
    exclude_airline_codes: list[str] | None = None,
    departure_time_min: int | None = None, # hour, 0-23
    departure_time_max: int | None = None,
    arrival_time_min: int | None = None,
    arrival_time_max: int | None = None,
    currency: str = "usd",
    max_price: int | None = None,
    seat_type: int | None = None,          # 1 economy, 2 premium economy, 3 business, 4 first
    passengers: list[int] | None = None,   # one code per traveller: 1 adult, 2 child, 3 infant on lap, 4 infant in seat
    sort_by: str = "best",                 # "best" | "price" | "duration"
    limit: int = 10,                       # results returned after merge + sort
    max_searches: int | None = None,       # cap the billed requests this call may make
    use_fallback: bool | None = None,      # leave unset: accepted upstream, currently inert
)

search_roundtrip_flights

search_roundtrip_flights(
    from_airport: str,
    to_airport: str | list[str],
    departure_date: str | None = None,
    departure_date_from: str | None = None,
    departure_date_to: str | None = None,
    return_date: str | None = None,        # use this OR nights, not both
    nights: int | list[int] | None = None, # e.g. 7, or [5, 6, 7]
    max_departure_stops: int | None = None,
    max_return_stops: int | None = None,
    departure_airline_codes: list[str] | None = None,
    return_airline_codes: list[str] | None = None,
    currency: str = "usd",
    max_price: int | None = None,
    seat_type: int | None = None,
    passengers: list[int] | None = None,
    sort_by: str = "best",
    limit: int = 10,
    max_searches: int | None = None,
    use_fallback: bool | None = None,
)

sort_by is applied by this server across the merged result set from every search it ran, so it is predictable regardless of how many combinations were expanded.

A worked example

User: "I'm in Tel Aviv. Cheapest week-long trip to Rome or Athens, leaving any day in the first half of May."

One call:

{
  "name": "search_roundtrip_flights",
  "arguments": {
    "from_airport": "TLV",
    "to_airport": ["FCO", "ATH"],
    "departure_date_from": "2026-05-01",
    "departure_date_to": "2026-05-15",
    "nights": 7,
    "sort_by": "price",
    "limit": 5
  }
}

That expands to 15 dates × 2 destinations = 30 combinations, which is exactly the per-call cap. The response shape (field names are real; the values below are illustrative, not a quote, run the call to get live fares):

{
  "results": [
    {
      "from_airport": "Tel Aviv (TLV)",
      "to_airport": "Rome (FCO)",
      "departure_date": "2026-05-05",
      "return_date": "2026-05-12",
      "total_price": "$XXX",
      "total_price_as_number": 0,
      "total_duration_seconds": 0,
      "total_stops": 0,
      "price_range_in_relation_to_other_periods": "low",
      "price_insights_low": 0,
      "price_insights_high": 0,
      "departure_flight_airline": "...",
      "departure_flight_departure_description": "...",
      "departure_flight_arrival_description": "...",
      "departure_flight_duration": "...",
      "departure_flight_stops": 0,
      "departure_stops_info": [],
      "return_flight_airline": "...",
      "return_flight_departure_description": "...",
      "return_flight_arrival_description": "...",
      "return_flight_duration": "...",
      "return_flight_stops": 0,
      "return_stops_info": [],
      "buy_link": "https://www.google.com/travel/flights?tfs=..."
    }
  ],
  "result_count": 5,
  "search_coverage": {
    "requested_combinations": 30,
    "searched_combinations": 30,
    "truncated": false,
    "max_searches_per_request": 30,
    "departure_dates_searched": ["2026-05-01", "..."],
    "destinations_searched": ["ATH", "FCO"]
  },
  "api_usage": {
    "requests_used_by_this_call": 30,
    "plan_requests_remaining": 0,
    "plan_requests_limit": 0,
    "note": "This search used 30 of your RapidAPI plan's requests; ... remain in the current period. Each date and destination combination is one billed request."
  }
}

Other response shapes to expect, all of them normal:

  • No flights on those dates. results: [] with a message: Google Flights genuinely returns nothing for some route/date combinations. Not an error. Try nearby dates or a nearby airport. use_fallback will not change this and is left unset by default: the backend accepts the field, but the second flight-data source it selects is gated behind USE_FALLBACK_FLI (fallback_available()), which is not switched on for this API, so none of its three values has any observable effect on a search today. The automatic retries the backend does on an unreadable page are unconditional and are not affected by it.
  • Some searches failed. A partial field says how many of the executed searches failed, and the results cover the rest.
  • Range too wide. search_coverage.truncated: true plus a note. The range is sampled evenly across the whole window (first and last kept), not cut short, so the sample is representative, not the first N days. Raise max_searches or narrow the range for fuller coverage.
  • No key / rejected key. needs_api_key: true, zero spend, with the fix. A valid RapidAPI key that is not subscribed to this API is the most common cause.
  • Plan exhausted. quota_exhausted: true with api_usage, plus a reminder that narrowing the range makes remaining quota go further.

Hotels: a check-in range

POST /search prices exactly one stay, so "cheapest three nights in Rome in May" used to be 31 tool calls -- or, in practice, one call on a date the model picked and an answer presented as the cheapest. Both hotel search tools now take the flights shape instead:

search_hotels(
    destination: str,
    checkin_date: str | None = None,        # one stay: this plus checkout_date
    checkout_date: str | None = None,
    checkin_date_from: str | None = None,   # or a range: this, checkin_date_to and nights
    checkin_date_to: str | None = None,
    nights: int | list[int] | None = None,  # 3, or [2, 3, 7] to price several lengths
    max_searches: int | None = None,        # cap the billed requests this call may make
    ...
)

Same machinery as the flights fan-out (src/fanout.py): one backend call per stay, capped at max_searches_per_tool_call (30, hard max 60), sampled evenly across the range when it does not fit, and reported in search_coverage. nights derives each check-out date, so it replaces checkout_date rather than joining it. A fixed checkout_date against a range of check-in dates is allowed and means "out on the 4th, whenever I arrive"; the impossible pairs are dropped.

The response is bounded on purpose. Every property of every stay is ~25 KB per stay (measured: 25,892 bytes for 25 properties, 18,989 of them URLs), so each stay reports its cheapest property, its per-night rate and its median, and the full property list comes back for the cheapest stay only:

{
  "results": [ /* every property of the CHEAPEST stay, upstream rows untouched */ ],
  "result_count": 18,
  "results_for_stay": {"checkin_date": "2026-05-12", "checkout_date": "2026-05-15", "nights": 3},
  "stays": [
    {
      "checkin_date": "2026-05-01", "checkout_date": "2026-05-04", "nights": 3,
      "search_status": "ok", "reason": "ok",
      "property_count": 22, "priced_count": 19,
      "cheapest_total": 411.0, "price_per_night": 137.0, "median_total": 690.0,
      "currency": "USD",
      "cheapest": { /* the row, minus its image URL */ }
    },
    {"checkin_date": "2026-05-02", "search_status": "degraded", "reason": "search_failed",
     "property_count": null, "priced_count": null, "cheapest": null},
    {"checkin_date": "2026-05-03", "search_status": "not_searched", "reason": "not_searched",
     "property_count": null, "cheapest": null}
  ],
  "cheapest_overall": {"checkin_date": "2026-05-12", "total": 305.0, "price_per_night": 101.67,
                       "currency": "USD", "property": { /* ... */ }},
  "search_status": "partial",
  "search_coverage": {
    "requested_combinations": 31,
    "searched_combinations": 15,
    "truncated": true,
    "max_searches_per_request": 30,
    "stays_searched": [{"checkin_date": "2026-05-01", "checkout_date": "2026-05-04"}, "..."],
    "checkin_dates_searched": ["2026-05-01", "..."],
    "note": "This request expanded to 31 stays, above the ..."
  },
  "api_usage": {"requests_used_by_this_call": 15, "note": "... Each stay -- one check-in date paired with one length -- is one billed request."}
}

reason on a stay is a fact about our pipeline, never a guess about the hotel:

reasonsearch_statusMeans
okokpriced
no_availabilityemptysearched, answered, nothing came back
no_priceemptyproperties came back, none carried a price (available: false lands here)
search_faileddegradedthe search errored, so nothing is known -- not "no rooms"
not_searchednot_searchedthe cap sampled it away

Counts are null rather than 0 on the last two: zero reads as "nothing there", and neither case knows that. Top-level search_status is ok / partial / empty / degraded over the stays; every stay failing raises instead of answering with an empty list.

Two deliberate refusals: a check-in range with providers naming more than one source (a fan-out times a per-source fan-out, billed to two subscriptions, that neither search_coverage nor api_usage can describe honestly today), and max_searches on a single stay, which would silently do nothing.

Documentation truncated — see the full README on GitHub.

Reviews

No reviews yet

Be the first to review this server!