# 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)
```