|
|
from mcp.server.fastmcp import FastMCP |
|
|
import random |
|
|
import urllib.parse |
|
|
from datetime import datetime, timedelta |
|
|
|
|
|
mcp = FastMCP("RecommendationsAgent") |
|
|
|
|
|
|
|
|
DESTINATIONS = { |
|
|
|
|
|
"maldives": { |
|
|
"name": "Maldives", |
|
|
"country": "Maldives", |
|
|
"type": ["beach", "luxury", "honeymoon", "relaxation"], |
|
|
"best_months": [1, 2, 3, 4, 11, 12], |
|
|
"avg_price": {"budget": 150, "moderate": 350, "luxury": 800}, |
|
|
"flight_hours": {"europe": 10, "asia": 5, "americas": 18}, |
|
|
"highlights": ["Overwater villas", "Crystal clear waters", "World-class diving", "Private islands"], |
|
|
"weather": "Tropical, 27-30°C year-round", |
|
|
"visa": "Visa on arrival for most nationalities", |
|
|
"currency": "MVR (USD widely accepted)", |
|
|
"rating": 4.8, |
|
|
}, |
|
|
"bali": { |
|
|
"name": "Bali", |
|
|
"country": "Indonesia", |
|
|
"type": ["beach", "culture", "adventure", "budget-friendly"], |
|
|
"best_months": [4, 5, 6, 7, 8, 9, 10], |
|
|
"avg_price": {"budget": 50, "moderate": 120, "luxury": 350}, |
|
|
"flight_hours": {"europe": 14, "asia": 5, "americas": 20}, |
|
|
"highlights": ["Rice terraces", "Hindu temples", "Surf beaches", "Yoga retreats"], |
|
|
"weather": "Tropical, 24-30°C, dry season Apr-Oct", |
|
|
"visa": "Visa on arrival (30 days)", |
|
|
"currency": "IDR", |
|
|
"rating": 4.7, |
|
|
}, |
|
|
"phuket": { |
|
|
"name": "Phuket", |
|
|
"country": "Thailand", |
|
|
"type": ["beach", "nightlife", "budget-friendly", "family"], |
|
|
"best_months": [11, 12, 1, 2, 3, 4], |
|
|
"avg_price": {"budget": 40, "moderate": 100, "luxury": 300}, |
|
|
"flight_hours": {"europe": 11, "asia": 3, "americas": 18}, |
|
|
"highlights": ["Patong Beach", "Island hopping", "Thai cuisine", "Elephant sanctuaries"], |
|
|
"weather": "Tropical, 25-32°C, dry Nov-Apr", |
|
|
"visa": "Visa-free for many (30-60 days)", |
|
|
"currency": "THB", |
|
|
"rating": 4.5, |
|
|
}, |
|
|
"cancun": { |
|
|
"name": "Cancún", |
|
|
"country": "Mexico", |
|
|
"type": ["beach", "party", "all-inclusive", "adventure"], |
|
|
"best_months": [12, 1, 2, 3, 4], |
|
|
"avg_price": {"budget": 80, "moderate": 180, "luxury": 450}, |
|
|
"flight_hours": {"europe": 10, "asia": 18, "americas": 4}, |
|
|
"highlights": ["Mayan ruins", "Cenotes", "Caribbean beaches", "Nightlife"], |
|
|
"weather": "Tropical, 24-33°C, dry Nov-Apr", |
|
|
"visa": "Visa-free for most Western countries", |
|
|
"currency": "MXN (USD accepted)", |
|
|
"rating": 4.4, |
|
|
}, |
|
|
"santorini": { |
|
|
"name": "Santorini", |
|
|
"country": "Greece", |
|
|
"type": ["beach", "romantic", "honeymoon", "culture"], |
|
|
"best_months": [5, 6, 7, 8, 9, 10], |
|
|
"avg_price": {"budget": 100, "moderate": 250, "luxury": 600}, |
|
|
"flight_hours": {"europe": 3, "asia": 10, "americas": 12}, |
|
|
"highlights": ["Sunset in Oia", "Blue domed churches", "Volcanic beaches", "Wine tasting"], |
|
|
"weather": "Mediterranean, 20-30°C, dry May-Oct", |
|
|
"visa": "Schengen visa / visa-free for many", |
|
|
"currency": "EUR", |
|
|
"rating": 4.8, |
|
|
}, |
|
|
|
|
|
|
|
|
"dubai": { |
|
|
"name": "Dubai", |
|
|
"country": "UAE", |
|
|
"type": ["luxury", "shopping", "modern", "family"], |
|
|
"best_months": [11, 12, 1, 2, 3], |
|
|
"avg_price": {"budget": 100, "moderate": 250, "luxury": 600}, |
|
|
"flight_hours": {"europe": 6, "asia": 4, "americas": 14}, |
|
|
"highlights": ["Burj Khalifa", "Desert safari", "Shopping malls", "Palm Jumeirah"], |
|
|
"weather": "Desert, 20-25°C winter, 35-45°C summer", |
|
|
"visa": "Visa on arrival for most nationalities", |
|
|
"currency": "AED", |
|
|
"rating": 4.6, |
|
|
}, |
|
|
"tokyo": { |
|
|
"name": "Tokyo", |
|
|
"country": "Japan", |
|
|
"type": ["culture", "food", "modern", "shopping"], |
|
|
"best_months": [3, 4, 5, 10, 11], |
|
|
"avg_price": {"budget": 80, "moderate": 180, "luxury": 450}, |
|
|
"flight_hours": {"europe": 12, "asia": 3, "americas": 12}, |
|
|
"highlights": ["Cherry blossoms", "Shibuya crossing", "Temples", "Food scene"], |
|
|
"weather": "Temperate, cherry blossoms Mar-Apr", |
|
|
"visa": "Visa-free for most (90 days)", |
|
|
"currency": "JPY", |
|
|
"rating": 4.8, |
|
|
}, |
|
|
"paris": { |
|
|
"name": "Paris", |
|
|
"country": "France", |
|
|
"type": ["romantic", "culture", "food", "art"], |
|
|
"best_months": [4, 5, 6, 9, 10], |
|
|
"avg_price": {"budget": 100, "moderate": 220, "luxury": 500}, |
|
|
"flight_hours": {"europe": 1, "asia": 12, "americas": 8}, |
|
|
"highlights": ["Eiffel Tower", "Louvre", "Café culture", "Fashion"], |
|
|
"weather": "Temperate, 5-25°C depending on season", |
|
|
"visa": "Schengen visa / visa-free for many", |
|
|
"currency": "EUR", |
|
|
"rating": 4.7, |
|
|
}, |
|
|
"new_york": { |
|
|
"name": "New York", |
|
|
"country": "USA", |
|
|
"type": ["culture", "shopping", "food", "entertainment"], |
|
|
"best_months": [4, 5, 6, 9, 10, 12], |
|
|
"avg_price": {"budget": 150, "moderate": 300, "luxury": 700}, |
|
|
"flight_hours": {"europe": 8, "asia": 14, "americas": 3}, |
|
|
"highlights": ["Broadway", "Central Park", "Statue of Liberty", "Museums"], |
|
|
"weather": "Continental, hot summers, cold winters", |
|
|
"visa": "ESTA for visa waiver countries", |
|
|
"currency": "USD", |
|
|
"rating": 4.6, |
|
|
}, |
|
|
"singapore": { |
|
|
"name": "Singapore", |
|
|
"country": "Singapore", |
|
|
"type": ["modern", "food", "family", "shopping"], |
|
|
"best_months": [2, 3, 4, 5, 6, 7, 8, 9], |
|
|
"avg_price": {"budget": 100, "moderate": 200, "luxury": 500}, |
|
|
"flight_hours": {"europe": 12, "asia": 2, "americas": 18}, |
|
|
"highlights": ["Marina Bay Sands", "Gardens by the Bay", "Hawker centers", "Sentosa"], |
|
|
"weather": "Tropical, 25-32°C year-round", |
|
|
"visa": "Visa-free for most (30-90 days)", |
|
|
"currency": "SGD", |
|
|
"rating": 4.7, |
|
|
}, |
|
|
"barcelona": { |
|
|
"name": "Barcelona", |
|
|
"country": "Spain", |
|
|
"type": ["beach", "culture", "food", "nightlife"], |
|
|
"best_months": [5, 6, 9, 10], |
|
|
"avg_price": {"budget": 70, "moderate": 150, "luxury": 350}, |
|
|
"flight_hours": {"europe": 2, "asia": 12, "americas": 9}, |
|
|
"highlights": ["Sagrada Familia", "La Rambla", "Beaches", "Tapas"], |
|
|
"weather": "Mediterranean, 15-30°C", |
|
|
"visa": "Schengen visa / visa-free for many", |
|
|
"currency": "EUR", |
|
|
"rating": 4.6, |
|
|
}, |
|
|
"rome": { |
|
|
"name": "Rome", |
|
|
"country": "Italy", |
|
|
"type": ["culture", "history", "food", "romantic"], |
|
|
"best_months": [4, 5, 6, 9, 10], |
|
|
"avg_price": {"budget": 80, "moderate": 170, "luxury": 400}, |
|
|
"flight_hours": {"europe": 2, "asia": 11, "americas": 10}, |
|
|
"highlights": ["Colosseum", "Vatican", "Pasta & pizza", "Ancient ruins"], |
|
|
"weather": "Mediterranean, hot summers, mild winters", |
|
|
"visa": "Schengen visa / visa-free for many", |
|
|
"currency": "EUR", |
|
|
"rating": 4.7, |
|
|
}, |
|
|
"istanbul": { |
|
|
"name": "Istanbul", |
|
|
"country": "Turkey", |
|
|
"type": ["culture", "history", "food", "shopping"], |
|
|
"best_months": [4, 5, 9, 10], |
|
|
"avg_price": {"budget": 50, "moderate": 120, "luxury": 300}, |
|
|
"flight_hours": {"europe": 3, "asia": 5, "americas": 11}, |
|
|
"highlights": ["Hagia Sophia", "Grand Bazaar", "Bosphorus cruise", "Turkish cuisine"], |
|
|
"weather": "Mediterranean, 5-30°C", |
|
|
"visa": "e-Visa for most nationalities", |
|
|
"currency": "TRY", |
|
|
"rating": 4.6, |
|
|
}, |
|
|
|
|
|
|
|
|
"cape_town": { |
|
|
"name": "Cape Town", |
|
|
"country": "South Africa", |
|
|
"type": ["adventure", "nature", "wine", "beach"], |
|
|
"best_months": [11, 12, 1, 2, 3], |
|
|
"avg_price": {"budget": 60, "moderate": 140, "luxury": 350}, |
|
|
"flight_hours": {"europe": 11, "asia": 13, "americas": 17}, |
|
|
"highlights": ["Table Mountain", "Wine regions", "Safaris nearby", "Cape Peninsula"], |
|
|
"weather": "Mediterranean, summer Nov-Mar", |
|
|
"visa": "Visa-free for many (90 days)", |
|
|
"currency": "ZAR", |
|
|
"rating": 4.7, |
|
|
}, |
|
|
"iceland": { |
|
|
"name": "Reykjavik", |
|
|
"country": "Iceland", |
|
|
"type": ["adventure", "nature", "unique"], |
|
|
"best_months": [6, 7, 8, 9, 12, 1, 2, 3], |
|
|
"avg_price": {"budget": 150, "moderate": 300, "luxury": 600}, |
|
|
"flight_hours": {"europe": 3, "asia": 14, "americas": 6}, |
|
|
"highlights": ["Northern Lights", "Geysers", "Glaciers", "Blue Lagoon"], |
|
|
"weather": "Cool, 0-15°C, northern lights Sep-Mar", |
|
|
"visa": "Schengen visa / visa-free for many", |
|
|
"currency": "ISK", |
|
|
"rating": 4.8, |
|
|
}, |
|
|
"marrakech": { |
|
|
"name": "Marrakech", |
|
|
"country": "Morocco", |
|
|
"type": ["culture", "adventure", "budget-friendly", "unique"], |
|
|
"best_months": [3, 4, 5, 10, 11], |
|
|
"avg_price": {"budget": 40, "moderate": 100, "luxury": 300}, |
|
|
"flight_hours": {"europe": 3, "asia": 10, "americas": 10}, |
|
|
"highlights": ["Medina souks", "Jardin Majorelle", "Atlas Mountains", "Riads"], |
|
|
"weather": "Semi-arid, 10-35°C", |
|
|
"visa": "Visa-free for many (90 days)", |
|
|
"currency": "MAD", |
|
|
"rating": 4.5, |
|
|
}, |
|
|
|
|
|
|
|
|
"lisbon": { |
|
|
"name": "Lisbon", |
|
|
"country": "Portugal", |
|
|
"type": ["culture", "food", "budget-friendly", "beach"], |
|
|
"best_months": [4, 5, 6, 9, 10], |
|
|
"avg_price": {"budget": 60, "moderate": 130, "luxury": 300}, |
|
|
"flight_hours": {"europe": 2, "asia": 14, "americas": 8}, |
|
|
"highlights": ["Trams", "Pastéis de nata", "Sintra", "Alfama"], |
|
|
"weather": "Mediterranean, mild year-round", |
|
|
"visa": "Schengen visa / visa-free for many", |
|
|
"currency": "EUR", |
|
|
"rating": 4.7, |
|
|
}, |
|
|
"vietnam": { |
|
|
"name": "Ho Chi Minh City", |
|
|
"country": "Vietnam", |
|
|
"type": ["culture", "food", "budget-friendly", "adventure"], |
|
|
"best_months": [12, 1, 2, 3, 4], |
|
|
"avg_price": {"budget": 30, "moderate": 80, "luxury": 200}, |
|
|
"flight_hours": {"europe": 12, "asia": 3, "americas": 18}, |
|
|
"highlights": ["Street food", "Cu Chi Tunnels", "Mekong Delta", "War history"], |
|
|
"weather": "Tropical, dry season Dec-Apr", |
|
|
"visa": "e-Visa available", |
|
|
"currency": "VND", |
|
|
"rating": 4.5, |
|
|
}, |
|
|
"budapest": { |
|
|
"name": "Budapest", |
|
|
"country": "Hungary", |
|
|
"type": ["culture", "budget-friendly", "nightlife", "relaxation"], |
|
|
"best_months": [4, 5, 6, 9, 10], |
|
|
"avg_price": {"budget": 50, "moderate": 100, "luxury": 250}, |
|
|
"flight_hours": {"europe": 2, "asia": 10, "americas": 10}, |
|
|
"highlights": ["Thermal baths", "Ruin bars", "Parliament", "Danube views"], |
|
|
"weather": "Continental, -5 to 30°C", |
|
|
"visa": "Schengen visa / visa-free for many", |
|
|
"currency": "HUF", |
|
|
"rating": 4.6, |
|
|
}, |
|
|
"bucharest": { |
|
|
"name": "Bucharest", |
|
|
"country": "Romania", |
|
|
"type": ["culture", "budget-friendly", "history", "nightlife"], |
|
|
"best_months": [5, 6, 9, 10], |
|
|
"avg_price": {"budget": 40, "moderate": 80, "luxury": 200}, |
|
|
"flight_hours": {"europe": 2, "asia": 9, "americas": 11}, |
|
|
"highlights": ["Palace of Parliament", "Old Town", "Day trips to Transylvania", "Vibrant nightlife"], |
|
|
"weather": "Continental, -5 to 30°C", |
|
|
"visa": "Visa-free for EU/US/many others", |
|
|
"currency": "RON", |
|
|
"rating": 4.4, |
|
|
}, |
|
|
} |
|
|
|
|
|
|
|
|
CURRENT_DEALS = [ |
|
|
{"destination": "dubai", "discount": 25, "reason": "Winter Sun Sale", "valid_until": "2025-12-31"}, |
|
|
{"destination": "bali", "discount": 30, "reason": "Early Bird 2026", "valid_until": "2026-01-15"}, |
|
|
{"destination": "maldives", "discount": 20, "reason": "Honeymoon Special", "valid_until": "2026-02-28"}, |
|
|
{"destination": "tokyo", "discount": 15, "reason": "Cherry Blossom Preview", "valid_until": "2026-01-31"}, |
|
|
{"destination": "iceland", "discount": 20, "reason": "Northern Lights Season", "valid_until": "2026-03-31"}, |
|
|
{"destination": "marrakech", "discount": 35, "reason": "Last Minute Deal", "valid_until": "2025-12-15"}, |
|
|
{"destination": "lisbon", "discount": 25, "reason": "Spring Escape", "valid_until": "2026-04-30"}, |
|
|
{"destination": "cancun", "discount": 20, "reason": "All-Inclusive Special", "valid_until": "2026-01-31"}, |
|
|
] |
|
|
|
|
|
|
|
|
def get_region_from_origin(origin: str) -> str: |
|
|
"""Determine user's region based on origin city.""" |
|
|
origin_lower = origin.lower() |
|
|
european_cities = ["london", "paris", "berlin", "madrid", "rome", "amsterdam", "frankfurt", "munich", "vienna", "zurich", "brussels", "dublin", "copenhagen", "stockholm", "oslo", "lisbon", "barcelona", "milan", "prague", "warsaw", "budapest", "bucharest"] |
|
|
asian_cities = ["tokyo", "singapore", "hong kong", "bangkok", "kuala lumpur", "seoul", "shanghai", "beijing", "mumbai", "delhi", "dubai", "doha", "abu dhabi"] |
|
|
|
|
|
if any(city in origin_lower for city in european_cities): |
|
|
return "europe" |
|
|
elif any(city in origin_lower for city in asian_cities): |
|
|
return "asia" |
|
|
else: |
|
|
return "americas" |
|
|
|
|
|
|
|
|
def calculate_trip_price(dest_data: dict, budget: str, nights: int, travelers: int, region: str) -> dict: |
|
|
"""Calculate estimated trip price.""" |
|
|
hotel_per_night = dest_data["avg_price"].get(budget, dest_data["avg_price"]["moderate"]) |
|
|
flight_hours = dest_data["flight_hours"].get(region, 10) |
|
|
|
|
|
|
|
|
flight_price = int(flight_hours * 50 * (1.5 if budget == "luxury" else 1.2 if budget == "moderate" else 1)) |
|
|
|
|
|
hotel_total = hotel_per_night * nights |
|
|
flights_total = flight_price * travelers * 2 |
|
|
activities_est = int(50 * nights * travelers * (2 if budget == "luxury" else 1.5 if budget == "moderate" else 1)) |
|
|
|
|
|
total = hotel_total + flights_total + activities_est |
|
|
|
|
|
return { |
|
|
"hotel_per_night": hotel_per_night, |
|
|
"hotel_total": hotel_total, |
|
|
"flights_total": flights_total, |
|
|
"activities_est": activities_est, |
|
|
"total": total, |
|
|
"per_person": total // travelers if travelers > 0 else total |
|
|
} |
|
|
|
|
|
|
|
|
@mcp.tool() |
|
|
def get_destination_recommendations( |
|
|
origin: str = "", |
|
|
budget: str = "moderate", |
|
|
travel_month: int = 0, |
|
|
interests: str = "", |
|
|
travelers: int = 2, |
|
|
trip_days: int = 7 |
|
|
) -> str: |
|
|
""" |
|
|
Get smart destination recommendations when user doesn't know where to go. |
|
|
Only needs: origin city, number of travelers, and trip duration. |
|
|
Everything else is optional and will be used to refine recommendations. |
|
|
|
|
|
Args: |
|
|
origin: Departure city (required for flight estimates) |
|
|
budget: "budget", "moderate", or "luxury" (default: moderate) |
|
|
travel_month: Month number (1-12), 0 for flexible |
|
|
interests: Comma-separated interests like "beach,culture,food" |
|
|
travelers: Number of travelers (default: 2) |
|
|
trip_days: Trip duration in days (default: 7) |
|
|
""" |
|
|
|
|
|
region = get_region_from_origin(origin) if origin else "europe" |
|
|
interest_list = [i.strip().lower() for i in interests.split(",") if i.strip()] if interests else [] |
|
|
|
|
|
|
|
|
scored_destinations = [] |
|
|
|
|
|
for dest_key, dest in DESTINATIONS.items(): |
|
|
score = 0 |
|
|
reasons = [] |
|
|
|
|
|
|
|
|
if travel_month > 0: |
|
|
if travel_month in dest["best_months"]: |
|
|
score += 30 |
|
|
reasons.append("✅ Perfect weather") |
|
|
else: |
|
|
score -= 10 |
|
|
reasons.append("⚠️ Off-season") |
|
|
else: |
|
|
score += 15 |
|
|
|
|
|
|
|
|
if interest_list: |
|
|
matches = sum(1 for interest in interest_list if any(interest in t for t in dest["type"])) |
|
|
if matches > 0: |
|
|
score += matches * 20 |
|
|
reasons.append(f"🎯 Matches {matches} interest(s)") |
|
|
|
|
|
|
|
|
dest_budget_level = "luxury" if dest["avg_price"]["moderate"] > 200 else "moderate" if dest["avg_price"]["moderate"] > 100 else "budget" |
|
|
if budget == dest_budget_level: |
|
|
score += 20 |
|
|
reasons.append("💰 Perfect for your budget") |
|
|
elif (budget == "moderate" and dest_budget_level == "budget") or (budget == "luxury" and dest_budget_level != "budget"): |
|
|
score += 10 |
|
|
|
|
|
|
|
|
deal = next((d for d in CURRENT_DEALS if d["destination"] == dest_key), None) |
|
|
if deal: |
|
|
score += deal["discount"] |
|
|
reasons.append(f"🔥 {deal['discount']}% OFF - {deal['reason']}") |
|
|
|
|
|
|
|
|
score += int(dest["rating"] * 10) |
|
|
|
|
|
|
|
|
prices = calculate_trip_price(dest, budget, trip_days, travelers, region) |
|
|
|
|
|
scored_destinations.append({ |
|
|
"key": dest_key, |
|
|
"data": dest, |
|
|
"score": score, |
|
|
"reasons": reasons, |
|
|
"prices": prices, |
|
|
"deal": deal |
|
|
}) |
|
|
|
|
|
|
|
|
scored_destinations.sort(key=lambda x: x["score"], reverse=True) |
|
|
|
|
|
|
|
|
results = [] |
|
|
results.append("# 🌍 **Personalized Destination Recommendations**") |
|
|
results.append("") |
|
|
if origin: |
|
|
results.append(f"📍 From: **{origin}** | 👥 {travelers} travelers | 📅 {trip_days} days | 💰 {budget.title()} budget") |
|
|
if travel_month > 0: |
|
|
month_name = datetime(2025, travel_month, 1).strftime("%B") |
|
|
results.append(f"🗓️ Travel month: **{month_name}**") |
|
|
if interest_list: |
|
|
results.append(f"❤️ Interests: **{', '.join(interest_list)}**") |
|
|
results.append("") |
|
|
results.append("---") |
|
|
|
|
|
|
|
|
for i, rec in enumerate(scored_destinations[:5], 1): |
|
|
dest = rec["data"] |
|
|
prices = rec["prices"] |
|
|
deal = rec["deal"] |
|
|
|
|
|
|
|
|
dest_encoded = urllib.parse.quote(dest["name"]) |
|
|
|
|
|
results.append("") |
|
|
|
|
|
|
|
|
if deal: |
|
|
results.append(f"### {'🥇' if i == 1 else '🥈' if i == 2 else '🥉' if i == 3 else '🏅'} #{i} {dest['name']}, {dest['country']} 🔥 **{deal['discount']}% OFF**") |
|
|
else: |
|
|
results.append(f"### {'🥇' if i == 1 else '🥈' if i == 2 else '🥉' if i == 3 else '🏅'} #{i} {dest['name']}, {dest['country']}") |
|
|
|
|
|
results.append(f"⭐ **{dest['rating']}/5** | {dest['weather']}") |
|
|
results.append("") |
|
|
|
|
|
|
|
|
results.append(f"✨ **Highlights:** {' • '.join(dest['highlights'][:4])}") |
|
|
results.append("") |
|
|
|
|
|
|
|
|
if rec["reasons"]: |
|
|
results.append(f"💡 **Why we recommend:** {' | '.join(rec['reasons'][:3])}") |
|
|
results.append("") |
|
|
|
|
|
|
|
|
original_total = prices["total"] |
|
|
if deal: |
|
|
discounted = int(original_total * (1 - deal["discount"] / 100)) |
|
|
results.append(f"💰 **Estimated total:** ~~${original_total:,}~~ → **${discounted:,}** ({travelers} travelers, {trip_days} nights)") |
|
|
results.append(f" 📦 Flights ~${prices['flights_total']:,} | Hotels ~${prices['hotel_total']:,} | Activities ~${prices['activities_est']:,}") |
|
|
else: |
|
|
results.append(f"💰 **Estimated total:** **${original_total:,}** ({travelers} travelers, {trip_days} nights)") |
|
|
results.append(f" 📦 Flights ~${prices['flights_total']:,} | Hotels ~${prices['hotel_total']:,} | Activities ~${prices['activities_est']:,}") |
|
|
|
|
|
results.append("") |
|
|
|
|
|
|
|
|
google_flights = f"https://www.google.com/travel/flights?q={urllib.parse.quote(origin)}%20to%20{dest_encoded}" if origin else f"https://www.google.com/travel/flights?q=flights%20to%20{dest_encoded}" |
|
|
booking_url = f"https://www.booking.com/searchresults.html?ss={dest_encoded}" |
|
|
viator_url = f"https://www.viator.com/searchResults/all?text={dest_encoded}" |
|
|
|
|
|
results.append(f"🔗 [Search Flights]({google_flights}) | [Find Hotels]({booking_url}) | [Book Activities]({viator_url})") |
|
|
results.append("") |
|
|
results.append("---") |
|
|
|
|
|
|
|
|
results.append("") |
|
|
results.append("## 🔥 **Current Hot Deals**") |
|
|
results.append("") |
|
|
|
|
|
for deal in CURRENT_DEALS[:4]: |
|
|
dest = DESTINATIONS.get(deal["destination"], {}) |
|
|
if dest: |
|
|
dest_encoded = urllib.parse.quote(dest["name"]) |
|
|
results.append(f"• **{dest['name']}** - {deal['discount']}% off ({deal['reason']}) - Valid until {deal['valid_until']}") |
|
|
|
|
|
results.append("") |
|
|
results.append("---") |
|
|
results.append("") |
|
|
results.append("💬 **Tell me more about your preferences and I'll refine these recommendations!**") |
|
|
results.append("") |
|
|
results.append("*Examples: 'I want beach and relaxation' or 'Looking for adventure on a budget' or 'Romantic getaway in spring'*") |
|
|
|
|
|
return "\n".join(results) |
|
|
|
|
|
|
|
|
@mcp.tool() |
|
|
def get_destination_details(destination: str) -> str: |
|
|
"""Get detailed information about a specific destination.""" |
|
|
|
|
|
dest_lower = destination.lower().replace(" ", "_").replace(",", "") |
|
|
|
|
|
|
|
|
dest = None |
|
|
dest_key = None |
|
|
for key, data in DESTINATIONS.items(): |
|
|
if key == dest_lower or data["name"].lower() == destination.lower() or destination.lower() in data["name"].lower(): |
|
|
dest = data |
|
|
dest_key = key |
|
|
break |
|
|
|
|
|
if not dest: |
|
|
return f"Sorry, I don't have detailed information about {destination}. Try one of our recommended destinations!" |
|
|
|
|
|
|
|
|
deal = next((d for d in CURRENT_DEALS if d["destination"] == dest_key), None) |
|
|
|
|
|
dest_encoded = urllib.parse.quote(dest["name"]) |
|
|
|
|
|
results = [] |
|
|
results.append(f"# 🌍 {dest['name']}, {dest['country']}") |
|
|
results.append("") |
|
|
|
|
|
if deal: |
|
|
results.append(f"## 🔥 **SPECIAL OFFER: {deal['discount']}% OFF** - {deal['reason']}") |
|
|
results.append(f"*Valid until {deal['valid_until']}*") |
|
|
results.append("") |
|
|
|
|
|
results.append(f"⭐ **Rating:** {dest['rating']}/5") |
|
|
results.append(f"🌡️ **Weather:** {dest['weather']}") |
|
|
results.append(f"📅 **Best time to visit:** {', '.join([datetime(2025, m, 1).strftime('%B') for m in dest['best_months'][:4]])}") |
|
|
results.append(f"🛂 **Visa:** {dest['visa']}") |
|
|
results.append(f"💱 **Currency:** {dest['currency']}") |
|
|
results.append("") |
|
|
|
|
|
results.append("## ✨ Highlights") |
|
|
for highlight in dest["highlights"]: |
|
|
results.append(f"• {highlight}") |
|
|
results.append("") |
|
|
|
|
|
results.append("## 💰 Price Guide (per night)") |
|
|
results.append(f"• 🎒 Budget: ~${dest['avg_price']['budget']}") |
|
|
results.append(f"• 🏨 Moderate: ~${dest['avg_price']['moderate']}") |
|
|
results.append(f"• 👑 Luxury: ~${dest['avg_price']['luxury']}") |
|
|
results.append("") |
|
|
|
|
|
results.append("## 🔗 Start Planning") |
|
|
results.append(f"• [Search Flights](https://www.google.com/travel/flights?q=flights%20to%20{dest_encoded})") |
|
|
results.append(f"• [Find Hotels](https://www.booking.com/searchresults.html?ss={dest_encoded})") |
|
|
results.append(f"• [Book Activities](https://www.viator.com/searchResults/all?text={dest_encoded})") |
|
|
results.append(f"• [Read Reviews](https://www.tripadvisor.com/Search?q={dest_encoded})") |
|
|
|
|
|
return "\n".join(results) |
|
|
|
|
|
|
|
|
@mcp.tool() |
|
|
def search_deals(budget: str = "any", travel_type: str = "any") -> str: |
|
|
"""Search for current travel deals and promotions.""" |
|
|
|
|
|
results = [] |
|
|
results.append("# 🔥 **Current Travel Deals & Offers**") |
|
|
results.append("") |
|
|
results.append("*Limited time offers from our partners*") |
|
|
results.append("") |
|
|
results.append("---") |
|
|
|
|
|
for deal in CURRENT_DEALS: |
|
|
dest = DESTINATIONS.get(deal["destination"], {}) |
|
|
if not dest: |
|
|
continue |
|
|
|
|
|
|
|
|
if budget != "any": |
|
|
dest_budget = "luxury" if dest["avg_price"]["moderate"] > 200 else "moderate" if dest["avg_price"]["moderate"] > 100 else "budget" |
|
|
if budget != dest_budget: |
|
|
continue |
|
|
|
|
|
|
|
|
if travel_type != "any" and travel_type.lower() not in dest["type"]: |
|
|
continue |
|
|
|
|
|
dest_encoded = urllib.parse.quote(dest["name"]) |
|
|
|
|
|
results.append("") |
|
|
results.append(f"### 🏷️ {dest['name']}, {dest['country']} - **{deal['discount']}% OFF**") |
|
|
results.append(f"📣 *{deal['reason']}*") |
|
|
results.append(f"⏰ Valid until: **{deal['valid_until']}**") |
|
|
results.append("") |
|
|
results.append(f"✨ {' • '.join(dest['highlights'][:3])}") |
|
|
results.append(f"💰 From **${int(dest['avg_price']['budget'] * (1 - deal['discount']/100))}/night** (budget) to **${int(dest['avg_price']['luxury'] * (1 - deal['discount']/100))}/night** (luxury)") |
|
|
results.append("") |
|
|
results.append(f"🔗 [Book Now](https://www.booking.com/searchresults.html?ss={dest_encoded}) | [See Flights](https://www.google.com/travel/flights?q=flights%20to%20{dest_encoded})") |
|
|
results.append("") |
|
|
results.append("---") |
|
|
|
|
|
if len(results) <= 5: |
|
|
results.append("") |
|
|
results.append("No deals match your filters. Try broadening your search!") |
|
|
|
|
|
results.append("") |
|
|
results.append("💡 **Tip:** Deals are updated daily. Check back often for new offers!") |
|
|
|
|
|
return "\n".join(results) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
mcp.run() |
|
|
|