Blog / AI agents

AI Travel Planning Agent: Flights and Hotels via MCP

An AI travel planning agent needs two things: live prices and a way to call them. Connect an agent to the Perceptlayer MCP server and it can search flights and hotels with three tools, search_endpoints, get_endpoint and call_endpoint. One trip plan in this tutorial takes six paid calls, so six credits. The agent reads public prices and never books anything.

How does an agent plan a trip with MCP tools?

The agent follows a short dependency chain. It finds the flight and hotel endpoints in the catalogue, reads their parameters, then calls them in order: a date search narrows the departure day, flight searches price the legs, and hotel searches price the stay. Each step feeds the next, which is why tool calling suits this job better than one big request.

MCP is the protocol that lets an agent discover and call tools. This server speaks Streamable HTTP, so any client that supports remote MCP servers can connect. For the wider picture, see MCP vs function calling for live web data and how an MCP server for web data works. If you prefer a framework, a LangChain custom tool can wrap the same endpoints, and Claude MCP web search covers a single-tool agent.

Here is the chain this tutorial builds. The credits column counts only successful calls.

Step Tool Endpoint Credits
1 search_endpoints Catalogue lookup 0
2 get_endpoint Catalogue lookup 0
3 call_endpoint /v1/google-flights/date-range 1
4 call_endpoint /v1/google-flights/one-way (outbound) 1
5 call_endpoint /v1/google-flights/one-way (return) 1
6 call_endpoint /v1/google-hotels/search 1
7 call_endpoint /v1/booking/search 1
8 call_endpoint /v1/google-hotels/reviews 1
Total 6

What do you need before you start?

You need an API key and an MCP client. The key is free: verify an email address and you get 1,000 free credits every month, with no credit card. Keys start with sk_live_. Claude Code is the client used here because it adds a remote server in one command, but any MCP client that supports Streamable HTTP works the same way.

Step 1: get a key

Get a free API key, then export it. The authentication docs cover the X-API-Key header that the plain HTTP calls in step 5 use.

export API_KEY=sk_live_your_key_here

Step 2: add the MCP server

Run this once. It registers the server under the name perceptlayer and passes your key as a bearer token, as described in the Claude Code MCP documentation and the AI agents section of the docs.

claude mcp add --transport http perceptlayer https://api.perceptlayer.com/mcp \
  --header "Authorization: Bearer $API_KEY"

Which endpoints does the agent chain together?

The agent draws on five endpoints from three APIs, and a single plan makes six paid calls because the one-way search runs twice. Flights come from the Google Flights API, hotel prices and ratings from the Google Hotels API, and a second hotel price list from the Booking.com API. All of them cost 1 credit per successful call.

Job Endpoint Required parameters
Find a cheap departure day GET /v1/google-flights/date-range origin, destination, from_date, to_date
Price one leg GET /v1/google-flights/one-way origin, destination, departure_date
Search hotels GET /v1/google-hotels/search q, check_in_date, check_out_date
Cross-check hotel prices GET /v1/booking/search ss
Read reviews for one property GET /v1/google-hotels/reviews property_token

Two details make the chain work. The flight response carries a booking_token per fare, and the hotel response carries a property_token per property. The reviews call needs that property_token, so the hotel search has to come first. Every parameter above is documented on its endpoint page, for example One Way and Google Hotels Search. For each API on its own, see the articles on the Google Flights API and the Google Hotels API.

How do you prompt the agent so it stays inside budget?

Put the limits in the prompt: what to search, how many paid calls it may make, and what it must not do. A prompt is an instruction, not a guarantee, so treat the cap as guidance and the prepaid balance as the hard stop. When the balance reaches zero, calls return insufficient_credits (402) and nothing more is debited.

Step 3: write the prompt

Save this as trip-prompt.txt.

Plan a 3-night trip from JFK to Los Angeles, leaving around 15 December 2026 and returning 18 December 2026. One adult.

Use the perceptlayer MCP tools. Look up endpoints with search_endpoints and read their parameters with get_endpoint before you call anything.

Make at most 6 call_endpoint calls in total:
1. A date-range flight search to check the departure window.
2. A one-way search for the outbound leg and one for the return leg.
3. A Google Hotels search for Los Angeles for the same dates.
4. A Booking.com search for the same dates, to compare prices.
5. A Google Hotels reviews lookup for the single hotel you recommend.

Report the cheapest fares, the three best-value hotels and the credits used and remaining, as call_endpoint reports them after each call.

Do not book, pay for or reserve anything, and do not ask for personal details. Prices are a snapshot from the time of each call; say so.

Step 4: run it

Run the agent headless so the whole plan comes back in one reply. The -p flag prints the response and exits, as the CLI reference describes. The --allowedTools flag lists tools that run without a permission prompt, and per the permissions docs a bare server name such as mcp__perceptlayer matches every tool that server provides.

claude -p "$(cat trip-prompt.txt)" --allowedTools "mcp__perceptlayer"

Watch the credits used and remaining that call_endpoint reports after each call. If the agent exceeds six paid calls, tighten the prompt. For hard spending limits around an agent, see AI agent API cost control.

What do the tool calls return?

The flight search returns a flights list. Each entry has a price, currency, airline_name, stops, total_duration_minutes, a booking_token and a legs array with the flight number and times. This is an excerpt of a recorded response for GET /v1/google-flights/one-way with origin=JFK, destination=LAX and departure_date=2026-12-15.

{
  "flights": [
    {
      "price": 179,
      "currency": "USD",
      "total_duration_minutes": 384,
      "stops": 0,
      "airline_name": "JetBlue",
      …
      "legs": [
        {
          "airline": "B6",
          "flight_number": "2223",
          "departure_airport": "JFK",
          "arrival_airport": "LAX",
          "departure_time": "2026-12-15T08:01:00",
          "arrival_time": "2026-12-15T11:25:00",
          "duration_minutes": 384
        }
      ]
    },
    …
  ],
  …
}

The hotel search returns a properties list. This excerpt is from a separate recorded request, GET /v1/google-hotels/search for Paris, France with check_in_date=2026-12-15 and check_out_date=2026-12-18. It shows the shape of the data, not the Los Angeles results.

{
  "properties": [
    {
      "type": "hotel",
      "name": "Novotel Paris Porte de Versailles",
      …
      "property_token": "ChkI5rWisq2xuMNRGg0vZy8xMWhfa18xc2w4EAE",
      …
      "rate_per_night": {
        "lowest": "€136",
        "extracted_lowest": 136,
        …
      },
      "total_rate": {
        "lowest": "€408",
        "extracted_lowest": 408,
        …
      },
      …
      "overall_rating": 4.6,
      "reviews": 4337,
      …
      "hotel_class": "4-star hotel",
      "extracted_hotel_class": 4
    },
    …
  ],
  …
}

The Booking.com API search takes ss (the place), plus checkin, checkout, group_adults, dest_id and dest_type. It returns property results for those dates. We have no recorded response for it, so check its fields in the playground on the API page rather than assuming them.

How can you run the same plan without an LLM?

You can run the flight and hotel legs as plain HTTP calls. This is useful for testing, for checking what the agent should have found, and for a scheduled job that doesn't need reasoning. The script below makes three paid calls (3 credits): two one-way flight searches and one hotel search. It reads only fields shown in the recorded responses above.

Step 5: run the script

Save this as plan_trip.py and install requests first. The script retries 429 and 503 responses after the Retry-After delay, and stops with the error code on any other failure.

import os
import sys
import time
from datetime import date

import requests

BASE = "https://api.perceptlayer.com"
HEADERS = {"X-API-Key": os.environ["API_KEY"]}
spent = 0
remaining = None


def call(path, **params):
    global spent, remaining
    for _ in range(3):
        r = requests.get(f"{BASE}{path}", headers=HEADERS, params=params, timeout=60)
        if r.status_code in (429, 503):
            time.sleep(int(r.headers.get("Retry-After", 5)))
            continue
        break
    if not r.ok:
        err = r.json()["error"]
        sys.exit(f"{err['code']} ({r.status_code}): {err['message']} [{err['request_id']}]")
    spent += int(r.headers["X-Credits-Used"])
    remaining = r.headers["X-Credits-Remaining"]
    return r.json()


def cheapest_flight(origin, destination, day):
    data = call(
        "/v1/google-flights/one-way",
        origin=origin,
        destination=destination,
        departure_date=day,
        adults=1,
        sort_by="cheapest",
    )
    flights = data["flights"]
    if not flights:
        sys.exit(f"No flights {origin}-{destination} on {day}")
    best = min(flights, key=lambda f: f["price"])
    leg = best["legs"][0]
    print(
        f"{origin}->{destination} {day}: {best['airline_name']} "
        f"{leg['airline']}{leg['flight_number']} dep {leg['departure_time']} "
        f"{best['stops']} stops, {best['price']} {best['currency']}"
    )


def top_hotels(query, check_in, check_out):
    data = call(
        "/v1/google-hotels/search",
        q=query,
        check_in_date=check_in,
        check_out_date=check_out,
        adults=1,
        currency="USD",
        gl="us",
    )
    props = [
        p for p in data["properties"]
        if p.get("overall_rating") and p.get("total_rate")
    ]
    props = [p for p in props if p["overall_rating"] >= 4.0]
    props.sort(key=lambda p: p["total_rate"]["extracted_lowest"])
    for p in props[:3]:
        print(
            f"{p['name']}: {p['overall_rating']} stars from {p['reviews']} reviews, "
            f"{p['total_rate']['lowest']} total"
        )


origin, destination, hotel_query, check_in, check_out = sys.argv[1:6]
nights = (date.fromisoformat(check_out) - date.fromisoformat(check_in)).days
print(f"{nights} nights")
cheapest_flight(origin, destination, check_in)
cheapest_flight(destination, origin, check_out)
top_hotels(hotel_query, check_in, check_out)
print(f"Credits used: {spent}, remaining: {remaining}")
pip install requests
python plan_trip.py JFK LAX "Los Angeles, California" 2026-12-15 2026-12-18

The output is a list of the cheapest outbound and return fares, the three lowest-priced hotels rated 4.0 or higher, and a credits line. Three calls cost 3 credits. If a request times out on your side, don't assume it failed: requests have no idempotency key, so it may have succeeded and been billed. Retrying web API calls without paying twice covers that case.

What does one trip plan cost?

One trip plan costs 6 credits, because six call_endpoint calls each cost 1 credit and the catalogue lookups are free. Failed calls cost nothing and return X-Credits-Used: 0. From the credits docs, a successful call costs the credits shown on its endpoint, so the plan's cost in credits is the same on every pack.

Credit source Credits Trip plans (6 credits each)
Free tier, per month 1,000 166
Starter pack 10,000 1,666
Build pack 50,000 8,333
Growth pack 200,000 33,333
Pro pack 500,000 83,333
Scale pack 1,000,000 166,666
Volume pack 5,000,000 833,333

The arithmetic: 10,000 credits ÷ 6 = 1,666 plans on the Starter pack, and 500,000 credits ÷ 6 = 83,333 plans on the Pro pack. One plan is 6 calls × 1 credit = 6 credits, and the free tier's 1,000 credits ÷ 6 = 166 plans. Plans are rounded down to whole trips. Purchased credits last 12 months, and there is no subscription. For what a pack of credits costs, see pricing.

Each pack includes a request rate, and a faster rate can be chosen at checkout. See pricing for the packs and the rate options, and the rate limits docs for how limits are counted. An agent that fires several searches at once can hit the limit on a free account, which gets 1 request a second.

If the agent re-plans a trip with a different date, each new search is another paid call. Caching identical searches avoids paying twice; caching API responses in Python shows how.

Why does the agent read prices but never book?

The agent reads public prices and stops there. Booking means taking payment, holding personal details and acting for the traveller, and a language model that misreads one parameter can spend someone's money. Keeping the agent read-only means the worst outcome is a wrong summary, which a person can check before acting.

There is a second reason. Data is collected from publicly available sources and returned as found at the time of the call. A fare or room rate can change between your call and the traveller's click, so the agent should present results as a snapshot and send the traveller to the airline or hotel to confirm the price. That belongs in the prompt, as step 3 shows.

The Google Flights API does include a Booking Details endpoint, which takes a booking_token and the flight's details. This tutorial leaves it out on purpose: it adds a paid call per fare and moves the agent a step closer to checkout. Add it only if your product shows fare details and a person makes the decision.

What can go wrong, and how does the agent recover?

Most failures are parameter mistakes or rate limits, and each one has a code the agent can act on. Errors return an HTTP status and a JSON body with error.code, error.message and error.request_id. Because failed calls aren't billed, a wrong guess costs time, not credits.

Code Status What it means for the agent
invalid_parameters 422 A parameter is missing or malformed. Re-read the endpoint with get_endpoint and fix the call.
rate_limited 429 Wait for the Retry-After seconds, then retry.
temporarily_unavailable 503 Wait for the Retry-After seconds, then retry.
source_error 502 The source failed. Try again later or report that this search is unavailable.
insufficient_credits 402 The balance is empty. Stop and tell the user.

The full list is in the errors docs, and the limits are in the rate limits docs. The next step is to run the prompt: add the MCP server and plan your first trip with the free credits.

APIs in this post

Frequently asked questions

Does the agent book flights or hotels?

No. It reads public prices through search endpoints and returns a summary. Nothing in the tools reserves or pays for anything, and the prompt tells the agent not to try. A person confirms the price on the airline or hotel site before booking.

How many credits does one trip plan use?

Six in this tutorial: one date-range search, two one-way flight searches, two hotel searches and one reviews lookup, each 1 credit. The catalogue lookups, search_endpoints and get_endpoint, cost nothing, and failed calls aren't billed.

Can I use an MCP client other than Claude Code?

Yes. The server is at https://api.perceptlayer.com/mcp and uses Streamable HTTP, so any client that supports remote MCP servers can connect. Authenticate with your key as a bearer token, as in the Claude Code command in step 2.

What happens when I run out of credits mid-plan?

The next call returns insufficient_credits (402) and nothing more is debited. The service is prepaid with no automatic top-up, so the agent can't overspend your balance. The prompt should tell it to stop and report what it already found.

Are the prices live?

They are returned as found at the time of each call, from publicly available sources. A fare or room rate can change afterwards, so present the results as a snapshot with the time of the call, and have the traveller confirm before booking.

  1. MCP Server Authentication: API Keys vs OAuth

    The MCP spec makes OAuth 2.1 optional for remote servers. When a static API key in an Authorization header fits better, and how clients send one.

  2. MCP Server Too Many Tools: Keeping Agent Context Small

    One tool per endpoint for 1,659 endpoints is about 567,000 characters of schema. A 10-step checklist to keep an MCP server's tool list small.

  3. How to Add an MCP Server to Claude Desktop and Cursor

    Add the Perceptlayer MCP server to Claude Desktop or Cursor with an API key header, run a first Google search, and check only successful calls cost credits.

Start with 1,000 free credits

No password to set. We email you a sign-in link, and your key works on every API.

No credit card. 1,000 free credits a month.