Back
View source
AI Engineering··12 min

Building an AI-Powered Travel Planner with MCP and Azure OpenAI

Learn how to build a conversational AI travel assistant that understands natural language, extracts travel parameters, and returns comprehensive trip plans with flights, hotels, weather, and budget estimates.

Building an AI-Powered Travel Planner with MCP and Azure OpenAI#

Imagine telling an AI "I want to travel from San Francisco to Japan next month" and getting back a complete travel plan with flights, hotels, attractions, weather forecasts, and budget estimates. That's exactly what we'll build in this tutorial.

In this guide, I'll walk you through building a conversational AI travel assistant that:

  • 🗣️ Understands natural language travel requests
  • 🔄 Asks intelligent follow-up questions to gather trip details
  • ✈️ Searches real flight data via SerpAPI
  • 🏨 Finds hotels with prices and amenities
  • 🌤️ Provides weather forecasts for your travel dates
  • 💰 Calculates detailed budget breakdowns

By the end, you'll have a production-ready travel planning application deployed on Azure.


What We're Building#

The AI Travel Assistant Architecture#

User: "I want to go to Japan from San Francisco"
                    ↓
            [Conversational AI Agent]
                    ↓
         Extracts: destination=Japan, departure=SF
                    ↓
         Asks: "When would you like to travel?"
                    ↓
User: "March 15 to March 22"
                    ↓
         Asks: "How many travelers?"
                    ↓
User: "2 people"
                    ↓
            [All params collected!]
                    ↓
    ┌───────────┬───────────┬───────────┬───────────┐
    ↓           ↓           ↓           ↓           ↓
 Flights     Hotels     Weather   Attractions   Budget
    ↓           ↓           ↓           ↓           ↓
    └───────────┴───────────┴───────────┴───────────┘
                    ↓
            Complete Trip Plan

Live Demo#

Travel Planner Home The Travel Planner landing page with modern glass-morphism design


Key Technologies#

ComponentTechnologyPurpose
FrontendReact 19 + Tailwind CSS v4Modern, responsive UI
BackendFastAPI + PythonREST API + async processing
AIAzure OpenAI (GPT-4o-mini)Natural language understanding
Flight DataSerpAPI Google FlightsReal flight prices
Hotel DataSerpAPI Google HotelsHotel listings & prices
WeatherOpen-Meteo APIWeather forecasts
PlacesGoogle Places APIAttractions & ratings
InfrastructureAzure Container AppsServerless deployment
ProtocolMCP (Model Context Protocol)Tool orchestration

The Conversational AI Agent#

The heart of this application is a conversational AI that intelligently extracts travel parameters through natural dialogue.

How It Works#

Instead of a traditional form where users fill in every field, users simply describe their trip:

AI Agent Interface The AI Agent interface with large input area and suggestion chips

Multi-Turn Conversation Flow#

The agent maintains context across multiple messages:

Conversation Flow Multi-turn conversation showing how the AI gathers trip details

Key Features:

  • Progressive extraction: Parameters are extracted and displayed in real-time
  • Smart follow-ups: Only asks for missing information
  • Natural date parsing: Understands "next month", "February 10 to 15", "5 days"
  • Context awareness: Remembers what was already said

Step 1: Backend Setup#

Project Structure#

backend/
├── src/travel_mcp/
│   ├── api.py          # FastAPI endpoints
│   ├── server.py       # MCP server definition
│   ├── tools/          # Individual tool implementations
│   │   ├── flights.py
│   │   ├── hotels.py
│   │   ├── weather.py
│   │   └── places.py
│   └── config.py       # Settings & API keys
├── pyproject.toml
└── Dockerfile

Install Dependencies#

# Using uv (recommended)
uv venv --python 3.11
uv sync

# Or pip
pip install fastapi uvicorn httpx pydantic openai redis

Core Dependencies (pyproject.toml)#

[project]
name = "travel-mcp-server"
version = "0.1.0"
requires-python = ">=3.11"

dependencies = [
    "fastmcp>=2.0.0",
    "fastapi>=0.115.0",
    "uvicorn[standard]>=0.34.0",
    "httpx>=0.28.0",
    "pydantic>=2.10.0",
    "azure-identity>=1.19.0",
    "openai>=1.0.0",
    "redis>=5.2.0",
]

Step 2: Building the Conversational AI Endpoint#

The AI Agent Endpoint#

File: api.py

from fastapi import FastAPI
from pydantic import BaseModel
from openai import AzureOpenAI

app = FastAPI()

class AIAgentRequest(BaseModel):
    query: str
    conversation_history: list[dict]
    collected_params: dict

class AIAgentResponse(BaseModel):
    type: str  # 'conversation' or 'complete'
    message: str
    collected_params: dict
    missing_fields: list[str]
    is_complete: bool
    trip_plan: dict | None

@app.post("/api/ai-agent")
async def process_ai_agent(request: AIAgentRequest) -> AIAgentResponse:
    """
    Process conversational AI agent requests.

    1. Use Azure OpenAI to extract travel parameters
    2. Ask follow-up questions for missing info
    3. When complete, fetch full trip plan
    """
    result = await conversational_ai_agent(
        request.query,
        request.conversation_history,
        request.collected_params
    )

    return AIAgentResponse(**result)

The Conversational Agent Logic#

async def conversational_ai_agent(
    user_message: str,
    conversation_history: list[dict],
    collected_params: dict
) -> dict:
    """
    Multi-turn conversational agent for travel planning.
    """
    client = AzureOpenAI(
        azure_endpoint=settings.AZURE_OPENAI_ENDPOINT,
        api_version="2024-02-15-preview",
        azure_ad_token_provider=get_token_provider()
    )

    # Build conversation context
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        *conversation_history,
        {"role": "user", "content": user_message}
    ]

    # Get LLM response
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        temperature=0.3
    )

    # Parse extracted parameters
    extracted = parse_llm_response(response.choices[0].message.content)

    # Merge with existing params
    updated_params = merge_params(collected_params, extracted)

    # Check what's missing
    missing = get_missing_fields(updated_params)

    if not missing:
        # All params collected - fetch trip plan!
        trip_plan = await fetch_complete_trip_plan(updated_params)
        return {
            "type": "complete",
            "message": "I have all the details! Here's your trip plan.",
            "collected_params": updated_params,
            "missing_fields": [],
            "is_complete": True,
            "trip_plan": trip_plan
        }

    # Ask for next missing field
    follow_up = generate_follow_up_question(missing[0])
    return {
        "type": "conversation",
        "message": follow_up,
        "collected_params": updated_params,
        "missing_fields": missing,
        "is_complete": False,
        "trip_plan": None
    }

System Prompt for Parameter Extraction#

SYSTEM_PROMPT = """You are a friendly travel assistant.
Extract travel parameters from conversations and respond naturally.

Required parameters:
- destination: Where they want to go
- departure_city: Where they're traveling from
- start_date: Trip start date (YYYY-MM-DD)
- end_date: Trip end date (YYYY-MM-DD)
- travelers: Number of people (default: 2)

Respond with JSON:
{
  "destination": "extracted value or null",
  "departure_city": "extracted value or null",
  "start_date": "YYYY-MM-DD or null",
  "end_date": "YYYY-MM-DD or null",
  "travelers": number or null,
  "message": "Your friendly response"
}

Parse natural language dates:
- "next month" → first week of next month
- "February 10-15" → 2026-02-10 to 2026-02-15
- "5 days" → calculate end date from start date
"""

Smart Date Parsing with Regex Fallback#

When LLM fails to parse dates correctly, use regex fallback:

import re
from datetime import datetime, timedelta

def extract_dates_with_regex(text: str) -> tuple[str | None, str | None]:
    """
    Extract dates from natural language using regex patterns.
    """
    today = datetime.now()

    # Pattern: "February 10 to February 15"
    month_day_range = re.search(
        r'(\w+)\s+(\d{1,2})\s+(?:to|through|-)\s+(\w+)\s+(\d{1,2})',
        text, re.IGNORECASE
    )
    if month_day_range:
        start_month, start_day, end_month, end_day = month_day_range.groups()
        start = parse_month_day(start_month, int(start_day), today.year)
        end = parse_month_day(end_month, int(end_day), today.year)
        return start.strftime('%Y-%m-%d'), end.strftime('%Y-%m-%d')

    # Pattern: "for 5 days" or "5 days"
    duration_match = re.search(r'(\d+)\s*days?', text, re.IGNORECASE)
    if duration_match:
        days = int(duration_match.group(1))
        # Look for start date elsewhere in text
        start_date = extract_start_date(text) or today
        end_date = start_date + timedelta(days=days)
        return start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d')

    return None, None

Step 3: Building MCP Tools#

What is MCP?#

Model Context Protocol (MCP) is a standardized way to define tools that AI models can use. Each tool has:

  • A clear description the LLM reads
  • Parameters with types and validation
  • An async implementation that fetches real data

Flight Search Tool#

from fastmcp import FastMCP

mcp = FastMCP("travel-planner")

@mcp.tool()
async def search_flights(
    departure_city: str,
    destination: str,
    departure_date: str,
    return_date: str,
    passengers: int = 1
) -> dict:
    """
    Search for flights between two cities.

    Args:
        departure_city: IATA code or city name
        destination: IATA code or city name
        departure_date: YYYY-MM-DD format
        return_date: YYYY-MM-DD format
        passengers: Number of passengers

    Returns:
        List of flight options with prices
    """
    params = {
        "engine": "google_flights",
        "departure_id": get_airport_code(departure_city),
        "arrival_id": get_airport_code(destination),
        "outbound_date": departure_date,
        "return_date": return_date,
        "adults": passengers,
        "api_key": settings.SERPAPI_KEY
    }

    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://serpapi.com/search",
            params=params
        )
        data = response.json()

    return format_flight_results(data)

Hotel Search Tool#

@mcp.tool()
async def search_hotels(
    location: str,
    check_in_date: str,
    check_out_date: str,
    guests: int = 2
) -> dict:
    """
    Search for hotels in a location.
    """
    params = {
        "engine": "google_hotels",
        "q": f"hotels in {location}",
        "check_in_date": check_in_date,
        "check_out_date": check_out_date,
        "adults": guests,
        "api_key": settings.SERPAPI_KEY
    }

    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://serpapi.com/search",
            params=params
        )
        data = response.json()

    hotels = []
    for property in data.get("properties", [])[:6]:
        hotels.append({
            "name": property.get("name"),
            "type": property.get("type"),
            "price_per_night": property.get("rate_per_night", {}).get("lowest"),
            "rating": property.get("overall_rating"),
            "reviews_count": property.get("reviews"),
            "amenities": property.get("amenities", []),
            "images": property.get("images", []),
        })

    return {"hotels": hotels, "total_results": len(hotels)}

Weather Forecast Tool#

@mcp.tool()
async def get_weather_forecast(
    location: str,
    start_date: str,
    end_date: str
) -> dict:
    """
    Get weather forecast for travel dates.
    Uses Open-Meteo API (free, no key required).
    """
    # Get coordinates for location
    coords = await geocode_location(location)

    params = {
        "latitude": coords["lat"],
        "longitude": coords["lon"],
        "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum",
        "start_date": start_date,
        "end_date": end_date,
        "timezone": "auto"
    }

    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://api.open-meteo.com/v1/forecast",
            params=params
        )
        data = response.json()

    return format_weather_data(data)

Step 4: Frontend Implementation#

React Component Structure#

frontend/src/
├── pages/
│   ├── AIAgent.tsx      # Conversational AI page
│   ├── TripPlanner.tsx  # Form-based planner
│   └── Results.tsx      # Trip results display
├── components/
│   ├── layout/
│   │   └── Header.tsx
│   └── results/
│       ├── FlightCard.tsx
│       ├── HotelCard.tsx
│       └── BudgetCard.tsx
└── services/
    └── api.ts           # API client

AI Agent Page State Management#

// AIAgent.tsx
const [messages, setMessages] = useState<Message[]>([
  {
    role: 'assistant',
    content: "Hi! I'm your AI travel assistant. Where would you like to go?"
  }
])

const [collectedParams, setCollectedParams] = useState<CollectedParams>({
  destination: null,
  departure_city: null,
  start_date: null,
  end_date: null,
  travelers: null,
})

const [results, setResults] = useState<TripPlanResponse | null>(null)
const [isLoading, setIsLoading] = useState(false)

async function handleSendMessage(userMessage: string) {
  // Add user message to chat
  setMessages(prev => [...prev, { role: 'user', content: userMessage }])
  setIsLoading(true)

  try {
    const response = await processAIAgentQuery({
      query: userMessage,
      conversation_history: messages.map(m => ({
        role: m.role,
        content: m.content
      })),
      collected_params: collectedParams
    })

    // Update collected params
    setCollectedParams(response.collected_params)

    // Add assistant response
    setMessages(prev => [...prev, {
      role: 'assistant',
      content: response.message
    }])

    // If complete, show results
    if (response.is_complete && response.trip_plan) {
      setResults(response.trip_plan)
    }
  } catch (error) {
    console.error('AI Agent error:', error)
  } finally {
    setIsLoading(false)
  }
}

Trip Details Sidebar#

function TripDetailsSidebar({ params }: { params: CollectedParams }) {
  return (
    <div className="glass-card p-6 space-y-4">
      <h3 className="text-lg font-semibold flex items-center gap-2">
        <MapPin className="w-5 h-5 text-accent" />
        Trip Details
      </h3>

      <div className="space-y-3">
        <DetailRow
          icon={<Globe />}
          label="Destination"
          value={params.destination || 'Not set'}
        />
        <DetailRow
          icon={<Plane />}
          label="From"
          value={params.departure_city || 'Not set'}
        />
        <DetailRow
          icon={<Calendar />}
          label="Dates"
          value={params.start_date
            ? `${params.start_date}${params.end_date}`
            : 'Not set'
          }
        />
        <DetailRow
          icon={<Users />}
          label="Travelers"
          value={params.travelers
            ? `${params.travelers} people`
            : 'Not set'
          }
        />
      </div>
    </div>
  )
}

Step 5: Results Display#

When all parameters are collected, the AI fetches and displays comprehensive results:

Trip Results Complete trip plan with flights, hotels, and attractions

Budget Breakdown#

Budget Estimate Detailed budget breakdown with itemized costs

Results Component Structure#

function ResultsOverlay({ results, onClose }: ResultsProps) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      className="fixed inset-0 z-50 bg-background/95 overflow-y-auto"
    >
      <div className="max-w-4xl mx-auto px-4 py-8">
        <header className="flex items-center justify-between mb-8">
          <h2 className="text-2xl font-bold">
            Your Trip to <span className="text-gradient">{results.destination}</span>
          </h2>
          <button onClick={onClose}>Back to Chat</button>
        </header>

        <div className="space-y-6">
          <FlightsSection flights={results.flights} />
          <HotelsSection hotels={results.hotels} />
          <WeatherSection weather={results.weather} />
          <AttractionsSection attractions={results.attractions} />
          <BudgetSection budget={results.budget} />
        </div>
      </div>
    </motion.div>
  )
}

Budget Breakdown Card#

function BudgetSection({ budget }: { budget: BudgetData }) {
  const items = [
    { label: 'Flights', amount: budget.flights, color: 'text-cyan-400' },
    { label: 'Accommodation', amount: budget.accommodation, color: 'text-violet-400' },
    { label: 'Food', amount: budget.food, color: 'text-amber-400' },
    { label: 'Activities', amount: budget.activities, color: 'text-emerald-400' },
    { label: 'Local Transport', amount: budget.transport, color: 'text-rose-400' },
  ]

  return (
    <div className="glass-card p-6">
      <h3 className="text-lg font-semibold mb-4">Budget Estimate</h3>

      <div className="space-y-3">
        {items.map(item => (
          <div key={item.label} className="flex justify-between">
            <span className="text-muted">{item.label}</span>
            <span className={item.color}>${item.amount}</span>
          </div>
        ))}
      </div>

      <div className="border-t border-border mt-4 pt-4">
        <div className="flex justify-between text-lg font-semibold">
          <span>Total Estimate</span>
          <span className="text-gradient">${budget.total}</span>
        </div>
        <p className="text-sm text-muted mt-1">
          for {budget.travelers} travelers
        </p>
      </div>
    </div>
  )
}

Step 6: Deployment to Azure#

Docker Configuration#

Backend Dockerfile:

FROM python:3.11-slim

WORKDIR /app

# Install uv for fast dependency management
RUN pip install uv

# Copy and install dependencies
COPY pyproject.toml .
RUN uv pip install --system -e .

# Copy source code
COPY src/ src/

EXPOSE 8000
CMD ["uvicorn", "travel_mcp.api:app", "--host", "0.0.0.0", "--port", "8000"]

Frontend Dockerfile:

FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

Azure Container Apps Deployment#

# Build and push images
az acr build --registry $ACR_NAME --image travel-backend:latest ./backend
az acr build --registry $ACR_NAME --image travel-frontend:latest ./frontend

# Deploy backend
az containerapp create \
  --name travel-backend \
  --resource-group $RG \
  --environment $ENV \
  --image $ACR_NAME.azurecr.io/travel-backend:latest \
  --target-port 8000 \
  --ingress external \
  --env-vars \
    AZURE_OPENAI_ENDPOINT=$OPENAI_ENDPOINT \
    SERPAPI_KEY=$SERPAPI_KEY

# Deploy frontend
az containerapp create \
  --name travel-frontend \
  --resource-group $RG \
  --environment $ENV \
  --image $ACR_NAME.azurecr.io/travel-frontend:latest \
  --target-port 80 \
  --ingress external \
  --env-vars \
    VITE_API_URL=$BACKEND_URL

The Form-Based Planner#

For users who prefer traditional forms, we also have a structured planner:

Form Planner Form-based trip planner for structured input

This provides:

  • Dropdown selectors for destinations
  • Date pickers
  • Travel style buttons (Budget/Moderate/Luxury)
  • Interest tags for personalization

Performance Optimizations#

1. Parallel API Calls#

async def fetch_complete_trip_plan(params: dict) -> dict:
    """Fetch all travel data in parallel."""
    async with asyncio.TaskGroup() as tg:
        flights_task = tg.create_task(search_flights(...))
        hotels_task = tg.create_task(search_hotels(...))
        weather_task = tg.create_task(get_weather(...))
        attractions_task = tg.create_task(get_attractions(...))

    return {
        "flights": flights_task.result(),
        "hotels": hotels_task.result(),
        "weather": weather_task.result(),
        "attractions": attractions_task.result(),
        "budget": calculate_budget(...)
    }

2. Redis Caching#

from redis import asyncio as aioredis

redis = aioredis.from_url(settings.REDIS_URL)

async def cached_search_hotels(location: str, dates: str) -> dict:
    cache_key = f"hotels:{location}:{dates}"

    # Check cache
    cached = await redis.get(cache_key)
    if cached:
        return json.loads(cached)

    # Fetch fresh data
    result = await search_hotels(location, dates)

    # Cache for 1 hour
    await redis.setex(cache_key, 3600, json.dumps(result))

    return result

3. Smart LLM Fallback#

When Azure OpenAI returns invalid JSON, use regex extraction:

async def conversational_ai_agent(...) -> dict:
    try:
        response = await get_llm_response(messages)
        extracted = parse_json_response(response)
    except json.JSONDecodeError:
        # Fallback: extract based on what we asked
        extracted = smart_fallback_extraction(
            user_message,
            missing_fields[0] if missing_fields else None
        )

    return extracted

Lessons Learned#

1. LLM Output is Unpredictable#

Always have fallback logic when parsing LLM responses. JSON mode helps but isn't 100% reliable.

2. Date Parsing is Hard#

Natural language dates ("next month", "the 10th to 15th") need multiple parsing strategies:

  • LLM extraction first
  • Regex patterns as fallback
  • Relative date calculation

3. Conversation Context Matters#

Sending full conversation history to the LLM enables:

  • Better context understanding
  • Correction of previous misunderstandings
  • More natural dialogue flow

4. Progressive Parameter Display#

Showing extracted parameters in real-time:

  • Builds user confidence
  • Allows quick corrections
  • Makes the AI feel responsive

Next Steps#

Enhancements to Try#

  1. Add itinerary generation: Day-by-day travel schedules
  2. Price alerts: Notify when flight prices drop
  3. Multi-city trips: Support complex itineraries
  4. User preferences: Remember past travel styles
  5. Booking integration: Direct booking links

Advanced Features#

  1. Voice input: "Hey, plan me a trip to Japan"
  2. Map integration: Visual route planning
  3. Group planning: Collaborative trip planning
  4. Local recommendations: Restaurant and activity suggestions

Conclusion#

You've learned how to build a conversational AI travel planner that:

  • Understands natural language travel requests
  • Extracts parameters through intelligent conversation
  • Fetches real data from multiple APIs
  • Presents comprehensive results with modern UI
  • Deploys to Azure for production use

The combination of MCP tools, Azure OpenAI, and modern web technologies creates a powerful foundation for any AI-powered application.


Resources#

Happy travels!