#
tokens: 6230/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .python-version
├── main.py
├── pyproject.toml
├── README.md
└── src
    ├── flights-mcp-server.py
    └── google_flights_mcp
        └── __init__.py
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.11
2 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Flight Planner MCP Server
 2 | 
 3 | A Model Context Protocol server that creates travel agent-level flight plans using the fast-flights API.
 4 | 
 5 | ## Features
 6 | 
 7 | - Search for one-way and round-trip flights
 8 | - Create comprehensive travel plans based on trip parameters
 9 | - Get airport code information
10 | - Use templates for common travel queries
11 | 
12 | ## Installation
13 | 
14 | 1. Make sure you have Python 3.10 or higher installed
15 | 2. Install the required packages:
16 | 
17 | ```bash
18 | pip install mcp fast-flights
19 | ```
20 | 
21 | ## Usage
22 | 
23 | ### Running the Server
24 | 
25 | You can run the server directly:
26 | 
27 | ```bash
28 | python flight_planner_server.py
29 | ```
30 | 
31 | ### Integrating with Claude Desktop
32 | 
33 | 1. Install [Claude Desktop](https://claude.ai/download)
34 | 2. Create or edit your Claude Desktop configuration file:
35 |    - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
36 |    - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
37 | 
38 | 3. Add the flight-planner server configuration:
39 | 
40 | ```json
41 | {
42 |   "mcpServers": {
43 |     "flight-planner": {
44 |       "command": "python",
45 |       "args": [
46 |         "/PATH/TO/flight_planner_server.py"
47 |       ],
48 |       "env": {
49 |         "PYTHONPATH": "/PATH/TO/PROJECT"
50 |       }
51 |     }
52 |   }
53 | }
54 | ```
55 | 
56 | 4. Replace `/PATH/TO/` with the actual path to your server file
57 | 5. Restart Claude Desktop
58 | 
59 | ### Using the MCP Inspector
60 | 
61 | For testing and development, you can use the MCP Inspector:
62 | 
63 | ```bash
64 | # Install the inspector
65 | npm install -g @modelcontextprotocol/inspector
66 | 
67 | # Run the inspector with your server
68 | npx @modelcontextprotocol/inspector python flight_planner_server.py
69 | ```
70 | 
71 | ## Available Tools
72 | 
73 | - `search_one_way_flights`: Search for one-way flights between airports
74 | - `search_round_trip_flights`: Search for round-trip flights between airports  
75 | - `create_travel_plan`: Generate a comprehensive travel plan
76 | 
77 | ## Available Resources
78 | 
79 | - `airport_codes://{query}`: Get airport code information based on a search query
80 | 
81 | ## Available Prompts
82 | 
83 | - `flight_search_prompt`: Template for searching flights
84 | - `travel_plan_prompt`: Template for creating a comprehensive travel plan
85 | 
86 | ## Example Queries for Claude
87 | 
88 | Once integrated with Claude Desktop, you can ask things like:
89 | 
90 | - "What flights are available from NYC to SFO on 2025-04-15?"
91 | - "Can you create a travel plan for my business trip from LAX to TPE from 2025-05-01 to 2025-05-08?"
92 | - "Help me find airport codes for Tokyo."
93 | - "What's the best time to book flights from Boston to London for a summer vacation?"
94 | 
95 | ## License
96 | 
97 | MIT
98 | 
```

--------------------------------------------------------------------------------
/src/google_flights_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
1 | def main() -> None:
2 |     print("Hello from google-flights-mcp!")
3 | 
```

--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------

```python
1 | def main():
2 |     print("Hello from google-flights-mcp!")
3 | 
4 | 
5 | if __name__ == "__main__":
6 |     main()
7 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "google-flights-mcp"
 3 | version = "0.1.0"
 4 | description = "Add your description here"
 5 | readme = "README.md"
 6 | requires-python = ">=3.11"
 7 | dependencies = [
 8 |     "fast-flights>=2.1",
 9 |     "mcp[cli]",
10 | ]
11 | 
```

--------------------------------------------------------------------------------
/src/flights-mcp-server.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Flight Planner Server using FastMCP
  4 | 
  5 | An MCP server that uses the fast-flights API to search for flight information,
  6 | with comprehensive airport data from a public CSV source.
  7 | """
  8 | 
  9 | import sys
 10 | import os
 11 | import json
 12 | import csv
 13 | import io
 14 | import asyncio
 15 | from datetime import datetime, timedelta
 16 | from typing import Optional, List, Dict, Any, Tuple
 17 | from pathlib import Path
 18 | 
 19 | # Print debug info to stderr (will be captured in Claude logs)
 20 | print("Starting Flight Planner server...", file=sys.stderr)
 21 | 
 22 | try:
 23 |     from fastmcp import FastMCP, Context
 24 |     print("Successfully imported FastMCP", file=sys.stderr)
 25 | except ImportError as e:
 26 |     print(f"Error importing FastMCP: {e}", file=sys.stderr)
 27 |     print("Please install FastMCP with: uv pip install fastmcp", file=sys.stderr)
 28 |     sys.exit(1)
 29 | 
 30 | # Constants
 31 | CSV_URL = "https://raw.githubusercontent.com/mborsetti/airportsdata/refs/heads/main/airportsdata/airports.csv"
 32 | DEFAULT_CONFIG = {
 33 |     "max_results": 10,
 34 |     "default_trip_days": 7,
 35 |     "default_advance_days": 30,
 36 |     "seat_classes": ["economy", "premium_economy", "business", "first"]
 37 | }
 38 | AIRPORTS_CACHE_FILE = Path(__file__).parent / "airports_cache.json"
 39 | 
 40 | # Global variables
 41 | airports = {}
 42 | 
 43 | # Fetch airport data from CSV
 44 | async def fetch_airports_csv(url: str = CSV_URL) -> Dict[str, str]:
 45 |     """Fetch airport data from a CSV URL."""
 46 |     print(f"Fetching airports from {url}", file=sys.stderr)
 47 |     
 48 |     try:
 49 |         import aiohttp
 50 |         
 51 |         airports_data = {}
 52 |         async with aiohttp.ClientSession() as session:
 53 |             async with session.get(url) as response:
 54 |                 if response.status != 200:
 55 |                     print(f"Error fetching CSV: HTTP {response.status}", file=sys.stderr)
 56 |                     return {}
 57 |                 
 58 |                 csv_text = await response.text()
 59 |                 csv_reader = csv.DictReader(io.StringIO(csv_text))
 60 |                 
 61 |                 for row in csv_reader:
 62 |                     iata = row.get('iata', '')
 63 |                     name = row.get('name', '')
 64 |                     city = row.get('city', '')
 65 |                     country = row.get('country', '')
 66 |                     
 67 |                     # Only store entries with a valid IATA code (3 uppercase letters)
 68 |                     if iata and len(iata) == 3 and iata.isalpha() and iata.isupper():
 69 |                         # Include city and country in the name for better context
 70 |                         full_name = f"{name}, {city}, {country}" if city else f"{name}, {country}"
 71 |                         airports_data[iata] = full_name
 72 |                 
 73 |                 print(f"Loaded {len(airports_data)} airports from CSV", file=sys.stderr)
 74 |                 
 75 |                 # Save to cache file
 76 |                 try:
 77 |                     with open(AIRPORTS_CACHE_FILE, 'w') as f:
 78 |                         json.dump(airports_data, f)
 79 |                     print(f"Saved airports to cache file: {AIRPORTS_CACHE_FILE}", file=sys.stderr)
 80 |                 except Exception as cache_e:
 81 |                     print(f"Warning: Could not save airports cache: {cache_e}", file=sys.stderr)
 82 |                 
 83 |                 return airports_data
 84 |     except ImportError:
 85 |         print("aiohttp not installed. Cannot fetch airports CSV.", file=sys.stderr)
 86 |         print("Please install with: uv pip install aiohttp", file=sys.stderr)
 87 |         return {}
 88 |     except Exception as e:
 89 |         print(f"Error fetching airports: {e}", file=sys.stderr)
 90 |         return {}
 91 | 
 92 | # Load airports from cache if available
 93 | def load_airports_cache() -> Dict[str, str]:
 94 |     """Load airports from cache file if available."""
 95 |     if AIRPORTS_CACHE_FILE.exists():
 96 |         try:
 97 |             with open(AIRPORTS_CACHE_FILE, 'r') as f:
 98 |                 cache = json.load(f)
 99 |                 print(f"Loaded {len(cache)} airports from cache", file=sys.stderr)
100 |                 return cache
101 |         except Exception as e:
102 |             print(f"Error loading airports cache: {e}", file=sys.stderr)
103 |     return {}
104 | 
105 | # Initialize the FastMCP server with dependencies
106 | mcp = FastMCP(
107 |     "Flight Planner", 
108 |     dependencies=["fast-flights", "aiohttp"]
109 | )
110 | 
111 | @mcp.tool()
112 | def search_flights(
113 |     from_airport: str, 
114 |     to_airport: str,
115 |     departure_date: str,
116 |     return_date: Optional[str] = None,
117 |     adults: int = 1,
118 |     children: int = 0,
119 |     infants_in_seat: int = 0,
120 |     infants_on_lap: int = 0,
121 |     seat_class: str = "economy",
122 |     ctx: Context = None
123 | ) -> str:
124 |     """
125 |     Search for flights between two airports.
126 | 
127 |     Args:
128 |         from_airport: Departure airport code (3-letter IATA code, e.g., 'LAX')
129 |         to_airport: Arrival airport code (3-letter IATA code, e.g., 'JFK')
130 |         departure_date: Departure date in YYYY-MM-DD format
131 |         return_date: Return date in YYYY-MM-DD format (optional, for round trips)
132 |         adults: Number of adult passengers (default: 1)
133 |         children: Number of children (default: 0)
134 |         infants_in_seat: Number of infants in seat (default: 0)
135 |         infants_on_lap: Number of infants on lap (default: 0)
136 |         seat_class: Seat class (economy, premium_economy, business, first) (default: economy)
137 | 
138 |     Returns:
139 |         Flight search results in a formatted string
140 |     """
141 |     if ctx:
142 |         ctx.info(f"Searching flights from {from_airport} to {to_airport}")
143 |     
144 |     # Validate inputs
145 |     try:
146 |         # Validate dates
147 |         departure_datetime = datetime.strptime(departure_date, "%Y-%m-%d")
148 |         return_datetime = None
149 |         if return_date:
150 |             return_datetime = datetime.strptime(return_date, "%Y-%m-%d")
151 |             if return_datetime < departure_datetime:
152 |                 return "Error: Return date cannot be before departure date."
153 |             
154 |         # Validate airport codes
155 |         if len(from_airport) != 3 or len(to_airport) != 3:
156 |             return "Error: Airport codes must be 3-letter IATA codes."
157 |         
158 |         # Check if airports exist in our database
159 |         from_airport = from_airport.upper()
160 |         to_airport = to_airport.upper()
161 |         
162 |         if from_airport not in airports:
163 |             return f"Error: Departure airport code '{from_airport}' not found in our database."
164 |         if to_airport not in airports:
165 |             return f"Error: Arrival airport code '{to_airport}' not found in our database."
166 |             
167 |         # Validate passenger numbers
168 |         if adults < 1:
169 |             return "Error: At least one adult passenger is required."
170 |         if any(num < 0 for num in [adults, children, infants_in_seat, infants_on_lap]):
171 |             return "Error: Passenger numbers cannot be negative."
172 |             
173 |         # Validate seat class
174 |         valid_classes = DEFAULT_CONFIG["seat_classes"]
175 |         if seat_class.lower() not in valid_classes:
176 |             return f"Error: Seat class must be one of {', '.join(valid_classes)}."
177 |     
178 |     except ValueError:
179 |         return "Error: Invalid date format. Please use YYYY-MM-DD format."
180 | 
181 |     # Import fast_flights here to avoid startup issues
182 |     try:
183 |         if ctx:
184 |             ctx.info("Importing fast_flights module")
185 |         from fast_flights import FlightData, Passengers, Result, get_flights
186 |     except ImportError as e:
187 |         error_msg = f"Error importing fast_flights: {str(e)}"
188 |         if ctx:
189 |             ctx.error(error_msg)
190 |         return f"Error: Unable to import fast_flights library. Please make sure it's installed correctly. Error details: {error_msg}"
191 |     
192 |     # Create flight data
193 |     try:
194 |         if ctx:
195 |             ctx.info("Creating flight data objects")
196 |         
197 |         flight_data = [FlightData(date=departure_date, from_airport=from_airport, to_airport=to_airport)]
198 |         
199 |         # Add return flight if return_date is provided
200 |         if return_date:
201 |             flight_data.append(FlightData(date=return_date, from_airport=to_airport, to_airport=from_airport))
202 |         
203 |         # Set trip type based on whether a return date was provided
204 |         trip_type = "round-trip" if return_date else "one-way"
205 |         
206 |         # Create passengers object
207 |         passengers = Passengers(
208 |             adults=adults,
209 |             children=children,
210 |             infants_in_seat=infants_in_seat,
211 |             infants_on_lap=infants_on_lap
212 |         )
213 |         
214 |         if ctx:
215 |             ctx.info("Calling get_flights API")
216 |             ctx.report_progress(0.5, 1.0)
217 |             
218 |         # Get flight results
219 |         result: Result = get_flights(
220 |             flight_data=flight_data,
221 |             trip=trip_type,
222 |             seat=seat_class,
223 |             passengers=passengers,
224 |             fetch_mode="fallback",  # Use fallback mode for more reliable results
225 |         )
226 |         
227 |         if ctx:
228 |             ctx.info("Processing flight results")
229 |             ctx.report_progress(1.0, 1.0)
230 |             
231 |         # Format results
232 |         return format_flight_results(result, trip_type, DEFAULT_CONFIG["max_results"])
233 |         
234 |     except Exception as e:
235 |         error_msg = f"Error searching for flights: {str(e)}"
236 |         if ctx:
237 |             ctx.error(error_msg)
238 |         return error_msg
239 | 
240 | def format_flight_results(result, trip_type: str, max_results: int) -> str:
241 |     """Format flight results into a readable string."""
242 |     if not result or not hasattr(result, 'flights') or not result.flights:
243 |         return "No flights found matching your criteria."
244 |     
245 |     output = []
246 |     output.append(f"Found {len(result.flights)} flight options.")
247 |     
248 |     if hasattr(result, 'current_price'):
249 |         output.append(f"Price assessment: {result.current_price}")
250 |     
251 |     output.append("\n")
252 |     
253 |     for i, flight in enumerate(result.flights[:max_results], 1):  # Limit to max results
254 |         best_tag = " [BEST OPTION]" if hasattr(flight, 'is_best') and flight.is_best else ""
255 |         output.append(f"Option {i}{best_tag}:")
256 |         
257 |         if hasattr(flight, 'name'):
258 |             output.append(f"  Airline: {flight.name}")
259 |         
260 |         if hasattr(flight, 'departure'):
261 |             output.append(f"  Departure: {flight.departure}")
262 |         
263 |         if hasattr(flight, 'arrival'):
264 |             output.append(f"  Arrival: {flight.arrival}")
265 |         
266 |         if hasattr(flight, 'arrival_time_ahead') and flight.arrival_time_ahead:
267 |             output.append(f"  Arrives: {flight.arrival_time_ahead}")
268 |             
269 |         if hasattr(flight, 'duration'):
270 |             output.append(f"  Duration: {flight.duration}")
271 |         
272 |         if hasattr(flight, 'stops'):
273 |             output.append(f"  Stops: {flight.stops}")
274 |         
275 |         if hasattr(flight, 'delay') and flight.delay:
276 |             output.append(f"  Delay: {flight.delay}")
277 |             
278 |         if hasattr(flight, 'price'):
279 |             output.append(f"  Price: {flight.price}")
280 |         
281 |         output.append("")
282 |     
283 |     if len(result.flights) > max_results:
284 |         output.append(f"... and {len(result.flights) - max_results} more flight options available.")
285 |     
286 |     if trip_type == "round-trip":
287 |         output.append("Note: Price shown is for the entire round trip.")
288 |     
289 |     return "\n".join(output)
290 | 
291 | @mcp.tool()
292 | def airport_search(query: str, ctx: Context = None) -> str:
293 |     """
294 |     Search for airport codes by name or city.
295 | 
296 |     Args:
297 |         query: The search term (city name, airport name, or partial code)
298 | 
299 |     Returns:
300 |         List of matching airports with their codes
301 |     """
302 |     if ctx:
303 |         ctx.info(f"Searching for airports matching: {query}")
304 |     
305 |     if not query or len(query.strip()) < 2:
306 |         return "Please provide at least 2 characters to search for airports."
307 |     
308 |     query = query.strip().upper()
309 |     matches = []
310 |     
311 |     # Search by airport code or name
312 |     for code, name in airports.items():
313 |         if query in code or query.upper() in name.upper():
314 |             matches.append(f"{name} ({code})")
315 |     
316 |     if not matches:
317 |         return f"No airports found matching '{query}'."
318 |     
319 |     # Sort matches and format the output
320 |     matches.sort()
321 |     result = [f"Found {len(matches)} airports matching '{query}':"]
322 |     result.extend(matches[:20])  # Limit to 20 results
323 |     
324 |     if len(matches) > 20:
325 |         result.append(f"...and {len(matches) - 20} more. Please refine your search to see more specific results.")
326 |         
327 |     return "\n".join(result)
328 | 
329 | @mcp.tool()
330 | def get_travel_dates(days_from_now: Optional[int] = None, trip_length: Optional[int] = None) -> str:
331 |     """
332 |     Get suggested travel dates based on days from now and trip length.
333 | 
334 |     Args:
335 |         days_from_now: Number of days from today for departure 
336 |                       (default: configured default_advance_days)
337 |         trip_length: Length of the trip in days
338 |                      (default: configured default_trip_days)
339 | 
340 |     Returns:
341 |         Suggested travel dates in YYYY-MM-DD format
342 |     """
343 |     # Use configured defaults if not provided
344 |     if days_from_now is None:
345 |         days_from_now = DEFAULT_CONFIG["default_advance_days"]
346 |     if trip_length is None:
347 |         trip_length = DEFAULT_CONFIG["default_trip_days"]
348 |     
349 |     if days_from_now < 1:
350 |         return "Error: Days from now must be at least 1."
351 |     if trip_length < 1:
352 |         return "Error: Trip length must be at least 1 day."
353 |     
354 |     today = datetime.now()
355 |     departure_date = today + timedelta(days=days_from_now)
356 |     return_date = departure_date + timedelta(days=trip_length)
357 |     
358 |     departure_str = departure_date.strftime("%Y-%m-%d")
359 |     return_str = return_date.strftime("%Y-%m-%d")
360 |     
361 |     return f"Departure date: {departure_str}\nReturn date: {return_str}"
362 | 
363 | @mcp.tool()
364 | async def update_airports_database(ctx: Context = None) -> str:
365 |     """
366 |     Update the airports database from the configured CSV source.
367 | 
368 |     Returns:
369 |         Status message with the number of airports loaded
370 |     """
371 |     if ctx:
372 |         ctx.info("Starting airport database update")
373 |         ctx.report_progress(0.1, 1.0)
374 |     
375 |     try:
376 |         if ctx:
377 |             ctx.info(f"Fetching airports from {CSV_URL}")
378 |             ctx.report_progress(0.3, 1.0)
379 |         
380 |         global airports
381 |         fresh_airports = await fetch_airports_csv()
382 |         
383 |         if not fresh_airports:
384 |             return "Error: Failed to fetch airports or no valid airports found"
385 |         
386 |         # Update the global airports dictionary
387 |         airports = fresh_airports
388 |         
389 |         if ctx:
390 |             ctx.report_progress(1.0, 1.0)
391 |         
392 |         return f"Successfully updated airports database with {len(airports)} airports"
393 |     
394 |     except Exception as e:
395 |         error_msg = f"Error updating airports: {str(e)}"
396 |         if ctx:
397 |             ctx.error(error_msg)
398 |         return error_msg
399 | 
400 | @mcp.resource("airports://all")
401 | def get_all_airports() -> str:
402 |     """Get a list of all available airports."""
403 |     result = [f"Available Airports ({len(airports)} total):"]
404 |     for code, name in sorted(airports.items())[:100]:  # Limit to first 100 to avoid overwhelming
405 |         result.append(f"{code}: {name}")
406 |     
407 |     if len(airports) > 100:
408 |         result.append(f"... and {len(airports) - 100} more airports. Use airport_search tool to find specific airports.")
409 |     
410 |     return "\n".join(result)
411 | 
412 | @mcp.resource("airports://{code}")
413 | def get_airport_info(code: str) -> str:
414 |     """Get information about a specific airport by its code."""
415 |     code = code.upper()
416 |     if code in airports:
417 |         return f"{code}: {airports[code]}"
418 |     return f"Airport code '{code}' not found"
419 | 
420 | @mcp.prompt()
421 | def plan_trip(destination: str) -> str:
422 |     """Create a prompt for trip planning to a specific destination."""
423 |     return f"""I'd like to plan a trip to {destination}. Can you help me with the following:
424 | 
425 | 1. What's the best time of year to visit {destination}?
426 | 2. How long should I plan to stay to see the major attractions?
427 | 3. What are the must-see places in {destination}?
428 | 4. What's the typical cost range for accommodations?
429 | 5. Are there any travel advisories or cultural considerations I should be aware of?
430 | 
431 | After you answer these questions, could you help me find flights to {destination} using the flight search tool?"""
432 | 
433 | @mcp.prompt()
434 | def compare_destinations(destination1: str, destination2: str) -> str:
435 |     """Create a prompt for comparing two travel destinations."""
436 |     return f"""I'm trying to decide between traveling to {destination1} and {destination2}. 
437 | Can you help me compare these destinations on the following factors:
438 | 
439 | 1. Weather and best time to visit each location
440 | 2. Cost of travel and accommodations
441 | 3. Popular attractions and activities
442 | 4. Food and cultural experiences
443 | 5. Safety and travel considerations
444 | 
445 | Based on these factors, which would you recommend and why?
446 | After your recommendation, could you show me flight options for both destinations?"""
447 | 
448 | # Initialize airports on startup - this is crucial
449 | async def initialize_airports():
450 |     """Initialize airport data at startup."""
451 |     global airports
452 |     
453 |     # First try to load from cache
454 |     cache = load_airports_cache()
455 |     if cache:
456 |         airports = cache
457 |     
458 |     # If cache is empty, fetch from CSV
459 |     if not airports:
460 |         fresh_airports = await fetch_airports_csv()
461 |         if fresh_airports:
462 |             airports = fresh_airports
463 |     
464 |     print(f"Initialized with {len(airports)} airports", file=sys.stderr)
465 | 
466 | # Run the server
467 | if __name__ == "__main__":
468 |     print("Initializing airports database...", file=sys.stderr)
469 |     # Run the initialization in an event loop
470 |     loop = asyncio.get_event_loop()
471 |     loop.run_until_complete(initialize_airports())
472 |     
473 |     print("Starting server - waiting for connections...", file=sys.stderr)
474 |     try:
475 |         # This will keep the server running until interrupted
476 |         mcp.run()
477 |     except Exception as e:
478 |         print(f"Error running server: {e}", file=sys.stderr)
479 |         import traceback
480 |         traceback.print_exc(file=sys.stderr)
481 |         sys.exit(1)
```