from mcp.server.fastmcp import FastMCP import random import urllib.parse from datetime import datetime, timedelta mcp = FastMCP("RecommendationsAgent") # Comprehensive destination database with seasonal info, prices, and highlights DESTINATIONS = { # Beach & Relaxation "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, }, # City Breaks "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, }, # Adventure "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, }, # Hidden Gems "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 and promotions (simulated) 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) # Estimate flight price based on distance 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 # Round trip 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 [] # Score each destination scored_destinations = [] for dest_key, dest in DESTINATIONS.items(): score = 0 reasons = [] # Check if month is good for this destination 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 # Flexible dates bonus # Check interests match 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)") # Budget alignment 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 # Check for deals 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']}") # Base rating score += int(dest["rating"] * 10) # Calculate price 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 }) # Sort by score scored_destinations.sort(key=lambda x: x["score"], reverse=True) # Build results 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("---") # Top 5 recommendations for i, rec in enumerate(scored_destinations[:5], 1): dest = rec["data"] prices = rec["prices"] deal = rec["deal"] # Build booking URLs dest_encoded = urllib.parse.quote(dest["name"]) results.append("") # Deal badge 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("") # Highlights results.append(f"✨ **Highlights:** {' • '.join(dest['highlights'][:4])}") results.append("") # Why this destination if rec["reasons"]: results.append(f"💡 **Why we recommend:** {' | '.join(rec['reasons'][:3])}") results.append("") # Price estimate 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("") # Booking links 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("---") # Current deals section 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(",", "") # Try to find the destination 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!" # Check for deals 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 # Filter by budget if specified 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 # Filter by type if specified 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()