Sitemap

TripMate: Building an AI Travel Agent

12 min readAug 25, 2026

--

With lastminute.com MCP, Fetch.ai & ASI:One

Press enter or click to view image in full size

The problem

Planning a trip shouldn’t feel like a second job. But for most people, it does.You open Skyscanner for flights. Booking.com for hotels. Maybe a third site for packages. You copy airport codes into spreadsheets. You lose the thread halfway through. By the time you find a decent flight, you’ve forgotten which hotel you liked.

And if you try to use a regular chatbot?

→ It 𝘩𝘢𝘭𝘭𝘶𝘤𝘪𝘯𝘢𝘵𝘦𝘴 prices that don't exist
→ It can't actually search live inventory
→ It gives you walls of text instead of something you can tap and book
→ It has no memory of what you searched 30 seconds ago

Travel needs real data. Not made-up answers.

That’s the problem we set out to solve with TripMate.

What we wanted to build

A personal AI travel agent where you:

→ Ask in plain language - "Find me flights from Milan to London on December 15th"
→ Get 𝘳𝘦𝘢𝘭 results from a live travel API - not hallucinated prices
→ Browse options in interactive UI cards - carousel, review, booking
→ Tap to book on lastminute.com - secure deeplink, you stay in control
→ Never leave the chat - no tab switching, no copy-paste

Three technologies made this possible:

1. ASI:One — the chat platform with interactive card UI

2. Fetch.ai uAgents — the agent runtime on Agentverse

3. lastminute.com MCP — real-time flight, hotel & package data

Let’s break each one down.

What is ASI:One?

ASI:One is a personal AI platform — think of it as the place where users talk to their agents.

But it’s not just a chat window. ASI:One supports:
→ Multi-turn memory — your agent remembers the conversation
→ @mentions — talk directly to a specific agent like @TripMate
→ Interactive Cards — rich drawer-based UI (carousel, form, review) rendered inside chat
→ Multi-user chat — friends and their AIs in one conversation
→ Connected Services — Gmail, Calendar, Spotify and more
→ AI Payments — your agent can act on your behalf

For TripMate, ASI:One is the front door. Users discover the agent, ask questions, tap cards, and book — all without leaving the platform.

Try it: https://asi1.ai/chat

What are Fetch.ai uAgents?

uAgents is Fetch.ai’s Python framework for building autonomous agents.An agent is not just an API endpoint. It’s a persistent identity on the network that can:
→ Receive messages via the **Agent Chat Protocol (ACP)**
→ Store session state per user
→ Run on Agentverse with a mailbox (no public URL needed)
→ Publish a manifest so ASI:One and other platforms can discover it
→ Communicate with other agents in the ecosystem

In TripMate, the uAgent is the backbone. It listens for chat messages, routes them, calls MCP tools, builds cards, and sends responses back.

Key config in our agent:

agent = Agent(
name="TripMate",
seed="TripMate" # AGENT_SEED seed is used with Fetch.ai's Identity class to generate a unique cryptographic identity for agents.
port=8000,
mailbox=True, # ASI:One reaches us via Agentverse
publish_agent_details=True,
)

Agent Seed : seed is used with Fetch.ai’s Identity class to generate a unique cryptographic identity for agents.

Why mailbox matters: Without mailbox, you’d need a public server URL. With mailbox=True, Agentverse relays messages to your agent — works on localhost, Render, anywhere.

What is MCP (Model Context Protocol)?

MCP is an open standard for connecting AI models and agents to external tools and data sources.Instead of hardcoding API calls, you expose tools through an MCP server. Any MCP-compatible client (Claude, Cursor, LangChain, uAgents) can discover and call them.

lastminute.com MCP Server exposes travel search as standard tools:

→ search_flights - real-time flight search across global airlines
→ search_only_hotel - hotel stays with star & facility filters
→ search_flight_and_hotel_package - bundled flight + hotel offers
→ select_hotel_options - room types for a selected hotel
→ generate_booking_link - secure deeplink to lastminute.co

Endpoint: https://mcp.lastminute.com/mcp

Search modes:

→ Default - cheapest first, no filters
→ Filter - time windows, price caps, max stops, airline include/exclude
→ Ranking - smart recommendations by price, duration & convenience

Supported languages: English, Italian, Spanish, French, German, Dutch, Polish, Swedish, Danish, Norwegian, Finnish

For TripMate, MCP is the data layer. Every price, every flight, every hotel comes from lastminute.com’s live network — never invented by the LLM.

Meet TripMate

Press enter or click to view image in full size

TripMate combines all three:

ASI:One (chat + cards) + uAgents (runtime) + lastminute.com MCP (travel data) + ASI1 LLM (understanding)

Ask in plain language. Get real results. Tap to book.

“Find me flight and hotel packages from Rome to Madrid for 3 nights, checking in on May 20.”

TripMate understands. Searches live data. Shows interactive cards. You pick. You book on lastminute.com.

What TripMate can do

✈️ Flights — one-way & round-trip, global airlines, real-time pricing

🏨 Hotels — star ratings, amenities, location filters

🧳 Flight + Hotel packages — bundled deals in a single request

🎴 Interactive cards — carousel, review, form & booking redirect

🔍 Smart filters — times, airports, stops, airlines, price, stars

⭐ Recommendations — best options ranked by price, duration & convenience

🔗 Instant booking Link — secure deeplinks to lastminute.com

🌍 11 languages — English, Italian, Spanish, French, German & more

Example queries

Flights

→ "Find flights from Milan to London on December 15th"
→ "Show me direct morning flights from Rome to Paris under 150 EUR"
→ "Find round-trip flights from Berlin to Tokyo, Jan 10 returning Jan 20"
→ "What are the best flights from Barcelona to New York next week?"

Hotels

→ "Search for a 4-star hotel in London with a pool, May 20-23"
→ "Find hotels in Barcelona with Wi-Fi and parking, 2 adults, 3 nights"

Packages

→ "Find me flight and hotel packages from Rome to Madrid for 3 nights, May 20"
→ "Search for a flight + hotel package to Malaga with spa and parking"

The full flow (end-to-end)

Here’s what happens when a user asks TripMate to find a flight:

Step 1 — User sends message on ASI:One

User types: “Find flights from Milan to London on December 15th”

ASI:One routes the message to TripMate via Agentverse mailbox.

Step 2 — uAgent receives ChatMessage

agent.py receives an ACP ChatMessage. handler.py extracts the text and checks: is this natural language or a card tap?

Step 3 — LangGraph + ASI1 understands intent

graph.py runs the ReAct agent. ASI1 reads the message, decides to call search_flights with:

departure=”MXP”, arrival=”LHR”, start_date=”2026–12–15"

Step 4 — Arguments get normalized

mcp_args.py converts “Milan” → “MXP”, validates the date is in the future, sets lang=”en”.

Step 5 — MCP tool call

mcp_client.py spawns npx mcp-remote, sends JSON-RPC tools/call to lastminute.com MCP. Live flight data comes back as structured JSON.

Step 6 — Results become a carousel card

results.py parses the JSON, builds carousel items with airline, price, duration, stops.

cards.py wraps it in MetadataContent with card_kind=”carousel”.

Step 7 — ASI:One renders the card

User sees a horizontal scrollable list of flights inside the chat drawer. Each has a “Select” button.

Step 8 — User taps a flight

ASI:One sends the selection back: {“action”: “pick_flight”, “offer_id”: “off_123”}

handler.py looks up the flight from session cache, builds a review card.

Step 9 — User confirms

Review card shows summary rows: airline, dates, price. User taps “Approve”.

Step 10 — Booking redirect

custom card with “Book on lastminute.com →” button. action.redirect opens the deeplink in a new tab. User completes payment on lastminute.com.

Your agent finds. You book. You stay in control.

Press enter or click to view image in full size

Card flow (the UI layer)

TripMate doesn’t reply with plain text for search results. It uses ASI:One Interactive Cards — rich UI rendered inside the chat drawer.

Press enter or click to view image in full size

Card flow diagram:

form → (user submits) → MCP search → carousel → (user picks) → review → (user approves) → custom redirect → lastminute.com

How cards work in code:

Each card is a MetadataContent block attached to a ChatMessage:

card_protocol_version: "1"
requires_card_interaction: "true"
card_kind: "carousel" | "review" | "form" | "custom"
card_payload: JSON-stringified UI data (max 64 KB)

When the user taps a button, ASI:One forwards the CTA’s selection payload back as a new ChatMessage. The handler parses it and chains the next card.

Press enter or click to view image in full size

MCP bridge (how we connect to lastminute.com)

TripMate doesn’t call lastminute.com’s API directly. It uses the MCP protocol via mcp-remote:

MCP_COMMAND = "npx"
MCP_ARGS = ["mcp-remote", "https://mcp.lastminute.com/mcp"]

On startup, LastMinuteMCP class:

Get GautamManak’s stories in your inbox

Join Medium for free to get updates from this writer.

1. Spawns npx mcp-remote as a subprocess

2. Sends initialize JSON-RPC message

3. Calls tools/list to discover available tools

4. Ready to accept tools/call requests

Each call is thread-safe (threading.Lock) so multiple users don’t corrupt the subprocess.

def call_tool(self, name, arguments):
with self._lock:
self._ensure_session_unlocked()
result = self._rpc("tools/call", {"name": name, "arguments": arguments})
return parsed_text_result
Press enter or click to view image in full size

Code walkthrough

The code is split into 𝟳 layers. Each does one job.

𝟭. agent.py — Entry point

The thinnest file. Registers the uAgent, attaches the chat protocol, and runs.

agent = Agent(name="TripMate", seed=AGENT_SEED, port=8000, mailbox=True)
chat_proto = Protocol(spec=chat_protocol_spec)

@chat_proto.on_message(ChatMessage)
async def on_message(ctx, sender, msg):
await on_chat_message(ctx, sender, msg)

agent.include(chat_proto, publish_manifest=True)
agent.run()

What it does: Creates the agent identity, enables mailbox for Agentverse, listens for ACP messages, forwards everything to handler.py.

𝟮. mcp_client.py — MCP bridge

Manages the npx mcp-remote subprocess. All travel data flows through here.

class LastMinuteMCP:
def _start_unlocked(self):
self._proc = subprocess.Popen([self._command, *self._args], ...)
self._rpc("initialize", {...})
tools = self._rpc("tools/list", {})
self._tool_names = {t["name"] for t in tools["tools"]}

def call_tool(self, name, arguments):
with self._lock:
result = self._rpc("tools/call", {"name": name, "arguments": arguments})

What it does: Spawns mcp-remote, discovers tools, calls them via JSON-RPC, returns structured text. Handles reconnection if subprocess dies.

𝟯. graph.py — ASI𝟭 brain (LangGraph)

When user sends natural language, LangGraph ReAct agent decides which tool to call.

_llm = ChatOpenAI(model="asi1", base_url="https://api.asi1.ai/v1", temperature=0)
_graph = create_react_agent(_llm, MCP_LANGCHAIN_TOOLS, checkpointer=MemorySaver())

async def run_graph(user_text, thread_id):
result = await _graph.ainvoke(
{"messages": [("user", user_text)]},
config={"configurable": {"thread_id": thread_id}},
)

What it does: ASI1 reads user message → picks search_flights / search_only_hotel / search_flight_and_hotel_package → calls MCP → returns short narration. System prompt injects today’s date so LLM never suggests past dates.

𝟰. tools.py — LangChain tools

Three @tool functions that LangGraph can call:

async def search_flights(departure, arrival, start_date, end_date="", adults=1):
"""Search flights. Use IATA codes. Dates YYYY-MM-DD."""
return await _call_mcp("search_flights", args, thread_id)

async def search_only_hotel(destination, date_from, date_to, adults="2"):
"""Search hotel-only stays."""
return await _call_mcp("search_only_hotel", args, thread_id)

async def search_flight_and_hotel_package(origin, destination, date_from, date_to, adults="2"):
"""Search flight+hotel packages."""
return await _call_mcp("search_flight_and_hotel_package", args, thread_id)

What it does: Bridges LangGraph tool calling to MCP. Each call runs in asyncio.to_thread() so it doesn’t block the event loop. Results stored in session for card building.

𝟱. mcp_args.py — Argument normalization

Users say “Rome” not “FCO”. This file fixes that before every MCP call.

def prepare_mcp_args(tool_name, args):
args["origin"] = to_iata(args["origin"]) # "Rome" → "FCO"
args["date_from"] = normalize_future_date(...) # never past dates
if not args.get("date_to"):
args["date_to"] = add_days(args["date_from"], 3) # default 3 nights
args.setdefault("lang", "en")

What it does: City → IATA conversion via CITY_IATA map. Date validation. Default check-out calculation. Prevents MCP validation errors from messy user input.

𝟲. handler.py — The router

The brain of TripMate. Every message flows through here.

async def on_chat_message(ctx, sender, msg):
session = SessionStore(ctx.storage, sender)
selection = extract_selection(user_text)

# Path B: Card tap
if is_card_action(selection):
await handle_card_action(ctx, sender, session, selection)
return

# Path A: Natural language
reply = await run_graph(clean_text, thread_id=sender)
# If tool was called, build carousel from stored result
tool_result = pop_tool_result(sender)
if tool_result:
await search_and_reply(ctx, sender, session, tool_name, args)

Path A — Natural language:

User text → run_graph() → ASI1 calls MCP tool → search_and_reply() → carousel card

Path B — Card tap:

User taps CTA → extract_selection() → handle_card_action():

submit_search → build MCP args from form → search_and_reply → carousel
pick_flight → lookup from session cache → review card
pick_package → lookup + select_hotel_options → review card
pick_hotel → lookup + select_hotel_options → review card
approve → booking_redirect_card → lastminute.com
cancel → clear session → search form
back → re-show last carousel

search_and_reply:

async def search_and_reply(ctx, sender, session, tool_name, args):
mcp_text = await run_mcp(tool_name, args)
index = index_search_results(mcp_text)
session.remember_search(tool_name, args, mcp_text, index)
kind, payload = results_to_carousel(tool_name, mcp_text, subtitle)
await ctx.send(sender, card_message(kind, payload))

𝟳. results.py + cards.py — MCP → UI

results.py turns raw MCP JSON into carousel items:

def index_search_results(mcp_text):
for f in data["flights"]:
index[f"flight_{f['flight_number']}"] = f
for p in data["products_summary"]:
index[f"pkg_{p['index']}"] = p

def results_to_carousel(tool_name, mcp_text, subtitle):
# builds items with image, title, price, primary_cta.selection

cards.py — ASI:One card messages:

def card_message(card_kind, card_payload, narration=""):
return ChatMessage(content=[
TextContent(text=narration),
MetadataContent(metadata={
"card_protocol_version": "1",
"requires_card_interaction": "true",
"card_kind": card_kind,
"card_payload": json.dumps(card_payload),
}),
])

def booking_redirect_card(title, book_url):
# custom card with redirect button → opens lastminute.com in new tab

What it does: MCP JSON → indexed cache → carousel / review / form / custom cards → ACP ChatMessage with MetadataContent.

session.py stores per-user state in uAgent KeyValueStore:

class SessionStore:
def remember_search(self, tool, args, raw_result, index):
self._data["last_search"] = {"tool": tool, "args": args, "search_id": ...}
self._data["last_results"] = raw_result
self._data["results_index"] = index

def lookup_item(self, item_id):
return self._data["results_index"].get(item_id, {})
Stored per sender (user):

last_search — tool name, args, search_id
last_results — raw MCP JSON text
results_index — flights/packages indexed by offer_id / package_id
selected — item user picked
book_url — deeplink from MCP
history — last 12 messages for context

When user taps carousel item, handler looks up by ID from cache — no second MCP search needed.

ASI1 system prompt

prompt.md defines TripMate's personality and tool-calling rules:

→ Name: TripMate
→ Tone: Warm, concise, confident — like a smart friend who travels often
→ Length: 1–2 short sentences per reply (results show as cards below)
→ Tool rules: Call tool as soon as origin + destination + dates are known
→ Never invent prices — only real MCP data
→ After tool call, app shows carousel — don't list options in text

Dynamic context injected at runtime:
→ Today's date (so LLM never suggests past dates)
→ Current year
→ Platform: ASI:One with interactive cards

Interactive Cards protocol

TripMate uses ASI:One Interactive Cards spec (card_protocol_version: “1”).
Required metadata keys:

→ card_protocol_version: "1"
→ requires_card_interaction: "true"
→ card_kind: carousel | detail | form | review | custom
→ card_payload: JSON-stringified (max 64 KB)

Selection response:

When user taps, ASI:One sends selection back as ChatMessage text:

→ Direct @mention: JSON object {"action": "pick_flight", "offer_id": "off_123"}
→ Planner-mediated: natural language prose mentioning the selection

Handler tries json.loads() first, falls back to prose parsing.

Error handling

TripMate handles failures gracefully:

→ MCP timeout → error_card("Search failed: timeout")
→ No results → error_card with formatted MCP error message
→ Missing booking link → error_card("Could not get a booking link")
→ Invalid card action → re-show search form

No crashes. No silent failures. User always sees a card with next steps.

Run locally

Prerequisites:

→ Python 3.10+
→ Node.js + npx (for mcp-remote bridge)
→ ASI_ONE_API_KEY
git clone https://github.com/gautammanak1/TripMate
cd TripMate
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env

# Set ASI_ONE_API_KEY in .env
python agent.py
Agent runs on http://0.0.0.0:8000 with mailbox enabled.

After this flow this docs : https://innovationlab.fetch.ai/resources/docs/agent-creation/uagent-creation#mailbox-agents

Built with

🤖 Fetch.ai uAgents — agent runtime, mailbox, Agentverse

💬 ASI:One — chat platform, interactive cards, @mentions

🧠 LangGraph — ReAct agent, tool calling, memory

✈️ lastminute.com MCP — real-time flights, hotels, packages

Press enter or click to view image in full size

Try it

👉 ASI:One: https://asi1.ai/ai/tripmate

👉 GitHub: https://github.com/gautammanak1/TripMate

👉 MCP Docs: https://mcp.lastminute.com/mcp

Ask about your next trip. TripMate handles the rest.

What’s next

→ More filter modes - business class, specific airlines, max duration
→ Multi-city trip planning
→ Price alerts via ASI:One recurring tasks
→ More MCP integrations - restaurants, activities, car rentals
→ Seat map cards via ASI:One custom element tree

Final thoughts

Travel planning doesn’t have to be fragmented.

TripMate connects what belongs together:

→ A **personal AI** on ASI:One that understands what you want
→ A **real data source** via lastminute.com MCP that never makes up prices
→ An **interactive UI** with cards that feels like an app, not a chatbot
→ An **autonomous agent** on Fetch.ai uAgents that runs 24/7 on Agentverse

We didn’t build another chatbot that talks about travel.

We built an agent that searches, shows, and helps you book — with real data, real UI, and real deeplinks.

Build agents that 𝘢ct. 𝘕ot agents that just talk.

📦 Full source code: https://github.com/gautammanak1/TripMate

Built with ❤️ using Fetch.ai · ASI:One · lastminute.com

--

--

GautamManak
GautamManak

Written by GautamManak

Developer Advocate & AI Agent Developer@Fetch.ai Public Speaker, Community Builder, Full Stack Engineer & AI Agent Developer