Skip to main content
Glama
flights.py19 kB
"""Flight search and booking tools.""" from datetime import datetime from ..server import mcp from ..models import ResponseFormat, FlightSearchInput, GetOfferInput, CreateOrderInput, GetOrderInput from ..api import make_api_request from ..formatters import format_json_response, format_markdown_flight_offer, format_currency, truncate_text @mcp.tool( name="duffel_search_flights", annotations={ "title": "Search Flights", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True } ) async def search_flights(params: FlightSearchInput) -> str: """ Search for available flights based on journey requirements. This tool creates an offer request and returns available flight options with pricing, schedules, and airline information. Each offer includes: - Total price and currency - Flight segments with timings - Airline and aircraft details - Cabin class and baggage allowance - Offer ID for booking Use this when users want to: - Find flights between destinations - Compare prices and schedules - Check availability for specific dates - Get flight options before booking Important notes: - Offers expire after 15-30 minutes (check expires_at) - Use passenger age instead of type for better accuracy - Round trips require 2 slices (outbound + return) - Direct flights: set max_connections=0 Returns flight offers in specified format (JSON or Markdown summary). """ request_data = { "data": { "slices": [ { "origin": slice_data.origin.upper(), "destination": slice_data.destination.upper(), "departure_date": slice_data.departure_date } for slice_data in params.slices ], "passengers": [] } } for passenger in params.passengers: if passenger.age is not None: request_data["data"]["passengers"].append({"age": passenger.age}) else: request_data["data"]["passengers"].append({"type": passenger.type.value}) if params.cabin_class: request_data["data"]["cabin_class"] = params.cabin_class.value if params.max_connections is not None: request_data["data"]["max_connections"] = params.max_connections try: response = await make_api_request( method="POST", endpoint="/air/offer_requests", data=request_data ) offers = response["data"].get("offers", []) if not offers: return "No flights found for the specified criteria. Try adjusting your search parameters (dates, cabin class, or connections)." if params.response_format == ResponseFormat.JSON: result = { "offer_request_id": response["data"]["id"], "total_offers": len(offers), "offers": offers[:10] # Limit to 10 offers for readability } return truncate_text(format_json_response(result)) else: # Markdown format lines = [ f"# Flight Search Results", f"**Offer Request ID**: `{response['data']['id']}`", f"**Total Offers Found**: {len(offers)}", "", f"Showing top {min(5, len(offers))} offers:", "" ] for offer in offers[:5]: lines.append(format_markdown_flight_offer(offer)) lines.append("---") lines.append("") if len(offers) > 5: lines.append(f"\n*{len(offers) - 5} more offers available. Use duffel_get_offer with specific offer ID for details.*") return truncate_text("\n".join(lines)) except Exception as e: return f"Error searching flights: {str(e)}\n\nTroubleshooting:\n- Verify airport/city codes are valid 3-letter IATA codes\n- Check dates are in future and in YYYY-MM-DD format\n- Ensure passenger count is reasonable (1-9)\n- Try broader search criteria if no results" @mcp.tool( name="duffel_get_offer", annotations={ "title": "Get Flight Offer Details", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def get_offer(params: GetOfferInput) -> str: """ Retrieve detailed information and current pricing for a specific flight offer. This tool fetches the latest version of an offer, including: - Up-to-date pricing and availability - Complete flight schedule and routing - Passenger requirements and restrictions - Baggage allowance and cabin details - Cancellation and change policies Use this when: - User selects a flight from search results - Before booking to confirm current price - To check if an offer is still available - To get passenger IDs needed for booking Important: Always retrieve the offer immediately before booking to ensure pricing is current, as offers expire after 15-30 minutes. Returns offer details in specified format (JSON or Markdown). """ try: response = await make_api_request( method="GET", endpoint=f"/air/offers/{params.offer_id}" ) offer = response["data"] expires_at = datetime.fromisoformat(offer["expires_at"].replace("Z", "+00:00")) if expires_at < datetime.now().astimezone(): return ( f"⚠️ This offer has expired at {offer['expires_at']}.\n\n" "Offers typically expire 15-30 minutes after creation. " "Please perform a new flight search to get current offers." ) if params.response_format == ResponseFormat.JSON: return truncate_text(format_json_response(offer)) else: # Markdown format result = format_markdown_flight_offer(offer) passengers = offer.get("passengers", []) if passengers: result += "\n### Passengers\n" for i, pax in enumerate(passengers, 1): result += f"{i}. **ID**: `{pax['id']}` - Type: {pax.get('type', 'N/A')}\n" conditions = offer.get("conditions", {}) if conditions: result += "\n### Booking Conditions\n" if conditions.get("change_before_departure"): result += f"- Change allowed: {conditions['change_before_departure'].get('allowed', False)}\n" if conditions.get("refund_before_departure"): result += f"- Refund allowed: {conditions['refund_before_departure'].get('allowed', False)}\n" return truncate_text(result) except Exception as e: return f"Error retrieving offer: {str(e)}\n\nTroubleshooting:\n- Verify the offer ID is correct (starts with 'off_')\n- Check if the offer has expired\n- Offer might have been booked by someone else" @mcp.tool( name="duffel_create_order", annotations={ "title": "Create Flight Order (Book Flight)", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True } ) async def create_order(params: CreateOrderInput) -> str: """ Create a flight booking (order) for the specified offer and passengers. ⚠️ IMPORTANT: This creates a real booking and may involve payment. This action: - Creates a confirmed airline reservation - May charge the payment method (if not using test mode) - Issues a booking reference from the airline - Is typically non-refundable or has cancellation fees Before calling this tool: 1. Verify the offer is current using duffel_get_offer 2. Confirm all passenger details are accurate (names match IDs) 3. Check payment amount matches offer total 4. Ensure user understands booking terms Required data: - Offer ID from search results - Complete passenger details (names as on ID, date of birth, contact info) - Payment information matching offer total The tool returns: - Order ID for reference - Airline booking reference - Confirmation details - Ticket information Response codes: - 201: Order created successfully (immediate confirmation) - 200: Booking confirmed, details arriving soon (webhook notification will follow) - 202: Booking processing (check webhooks or email for confirmation) Use test mode tokens for testing to avoid actual charges. Returns order details in specified format (JSON or Markdown). """ order_data = { "data": { "selected_offers": [params.offer_id], "passengers": [ { "id": pax.id, "given_name": pax.given_name, "family_name": pax.family_name, "born_on": pax.born_on, "email": pax.email, "phone_number": pax.phone_number, "gender": pax.gender, **({"title": pax.title} if pax.title else {}) } for pax in params.passengers ], "payments": [ { "type": payment.type.value, "amount": payment.amount, "currency": payment.currency.upper() } for payment in params.payments ] } } try: response = await make_api_request( method="POST", endpoint="/air/orders", data=order_data ) status_code = response.get("meta", {}).get("status", 201) if status_code == 200: message = response.get("data", {}).get("message", "") return ( f"✅ Booking Confirmed!\n\n" f"{message}\n\n" f"The complete booking details will be available shortly. " f"You will receive:\n" f"- A webhook notification when the order is created\n" f"- An email to your support contact address\n\n" f"**Offer ID**: `{params.offer_id}`\n" f"Please check your webhooks or email for the order ID and booking reference." ) elif status_code == 202: return ( f"⏳ Booking Processing\n\n" f"Your booking request has been accepted and is being processed. " f"You will receive confirmation via:\n" f"- Webhook notification (order.created or order.creation_failed)\n" f"- Email to your support contact address\n\n" f"**Offer ID**: `{params.offer_id}`\n" f"This typically takes a few minutes. Please do not retry this request." ) else: order = response["data"] if params.response_format == ResponseFormat.JSON: return truncate_text(format_json_response(order)) else: # Markdown format lines = [ "# ✅ Booking Confirmed!", "", f"**Order ID**: `{order['id']}`", f"**Booking Reference**: {order.get('booking_reference', 'N/A')}", f"**Total**: {format_currency(order['total_amount'], order['total_currency'])}", "", "## Passengers" ] for i, pax in enumerate(order.get("passengers", []), 1): lines.append(f"{i}. {pax.get('given_name')} {pax.get('family_name')}") lines.append("") lines.append("## Flight Details") for i, slice_info in enumerate(order.get("slices", []), 1): origin = slice_info["origin"]["iata_code"] destination = slice_info["destination"]["iata_code"] lines.append(f"**Slice {i}**: {origin} → {destination}") lines.append(f"- Departure: {slice_info['departure_at']}") lines.append(f"- Arrival: {slice_info['arrival_at']}") lines.append("") lines.append("---") lines.append("*Save the Order ID and Booking Reference for future reference.*") return truncate_text("\n".join(lines)) except Exception as e: error_msg = str(e) troubleshooting = "\n\nTroubleshooting:\n" if "offer_expired" in error_msg: troubleshooting += "- The offer has expired. Perform a new flight search to get current offers.\n" elif "offer_no_longer_available" in error_msg: troubleshooting += "- The offer is no longer available. Search again or select a different offer.\n" elif "price_changed" in error_msg: troubleshooting += "- The price has changed. Retrieve the offer again to get updated pricing.\n" elif "validation" in error_msg.lower(): troubleshooting += "- Check passenger details are complete and accurate\n" troubleshooting += "- Verify names match government ID exactly\n" troubleshooting += "- Ensure date of birth is in YYYY-MM-DD format\n" troubleshooting += "- Confirm email and phone number are valid\n" elif "payment" in error_msg.lower(): troubleshooting += "- Verify payment amount matches offer total_amount exactly\n" troubleshooting += "- Confirm currency matches offer total_currency\n" troubleshooting += "- Check payment type is appropriate for your account\n" else: troubleshooting += "- Verify all passenger details are accurate\n" troubleshooting += "- Ensure payment amount and currency match the offer\n" troubleshooting += "- Check if using test mode token for testing\n" return f"Error creating order: {error_msg}{troubleshooting}" @mcp.tool( name="duffel_get_order", annotations={ "title": "Get Order Details", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def get_order(params: GetOrderInput) -> str: """ Retrieve complete details for an existing flight order. This tool fetches: - Order status and booking reference - Flight itinerary and schedule - Passenger information - Payment and pricing details - Documents and tickets - Change and cancellation options Use this when: - User needs to review their booking - Checking order status - Before making changes or cancellations - Retrieving booking reference for airline website Returns order details in specified format (JSON or Markdown). """ try: response = await make_api_request( method="GET", endpoint=f"/air/orders/{params.order_id}" ) order = response["data"] if params.response_format == ResponseFormat.JSON: return truncate_text(format_json_response(order)) else: # Markdown format lines = [ "# Flight Order Details", "", f"**Order ID**: `{order['id']}`", f"**Booking Reference**: {order.get('booking_reference', 'N/A')}", f"**Status**: {order.get('status', 'N/A')}", f"**Total**: {format_currency(order['total_amount'], order['total_currency'])}", f"**Booked**: {order.get('created_at', 'N/A')}", "", "## Passengers" ] for i, pax in enumerate(order.get("passengers", []), 1): lines.append(f"{i}. {pax.get('given_name')} {pax.get('family_name')}") if pax.get('born_on'): lines.append(f" - DOB: {pax['born_on']}") lines.append("") lines.append("## Flight Itinerary") for i, slice_info in enumerate(order.get("slices", []), 1): origin = slice_info["origin"] destination = slice_info["destination"] lines.append(f"\n### Slice {i}: {origin['city_name']} → {destination['city_name']}") lines.append(f"**Departure**: {slice_info['departure_at']} ({origin['iata_code']})") lines.append(f"**Arrival**: {slice_info['arrival_at']} ({destination['iata_code']})") lines.append(f"**Duration**: {format_duration(slice_info.get('duration', ''))}") for j, segment in enumerate(slice_info.get("segments", []), 1): carrier = segment.get("marketing_carrier", {}) lines.append(f"\n**Flight {j}**: {carrier.get('name', 'N/A')} {segment.get('marketing_carrier_flight_number', '')}") lines.append(f"- Aircraft: {segment.get('aircraft', {}).get('name', 'N/A')}") conditions = order.get("conditions", {}) if conditions: lines.append("\n## Booking Conditions") change_before = conditions.get("change_before_departure", {}) if change_before: allowed = "✅ Yes" if change_before.get("allowed") else "❌ No" lines.append(f"- Changes before departure: {allowed}") if change_before.get("penalty_amount"): penalty = format_currency( change_before["penalty_amount"], change_before.get("penalty_currency", "") ) lines.append(f" - Change fee: {penalty}") refund_before = conditions.get("refund_before_departure", {}) if refund_before: allowed = "✅ Yes" if refund_before.get("allowed") else "❌ No" lines.append(f"- Refund before departure: {allowed}") return truncate_text("\n".join(lines)) except Exception as e: return f"Error retrieving order: {str(e)}\n\nTroubleshooting:\n- Verify the order ID is correct (starts with 'ord_')\n- Check if you have access to this order\n- Order might not exist or might have been cancelled"

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/FortripEngineering/duffel-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server