Tutorial: Build a Simple MCP Server With Claude Desktop
In the previous part of this series, I introduced the basic concepts of Model Context Protocol (MCP) — an open standard designed to streamline how AI models interact with external tools, data sources and resources. In this installment, I’ll walk you through the step-by-step process of building an MCP server and integrating it with Claude Desktop.
We’ll use Python and FastMCP 2.0 to build an MCP server that utilizes the FlightAware API. For more information on FlightAware, refer to the previous tutorial, where I integrated it with large language model (LLM) tools. Ensure that you have a valid FlightAware API key before proceeding.
In subsequent parts of this series, I’ll demonstrate how to extend this to work with AI agents built with OpenAI Agents SDK and Google Agent Development Kit (ADK).
Step 1: Setting Up the Environment
Let’s start by creating a virtual environment and installing the required Python modules:
|
1 2 |
python -m venv venv source venv/bin/activate |
|
1 |
pip install requests fastmcp pytz |
Step 2: Building the MCP Server
First, import the required Python modules in the code:
|
1 2 3 4 5 6 7 |
import json import os import requests import pytz from datetime import datetime, timedelta from typing import Any, Callable, Set, Dict, List, Optional from mcp.server.fastmcp import FastMCP |
The next step is to create the MCP object from FastMCP:
|
1 |
mcp = FastMCP("Flight Server") |
We’ll then create a function and decorate it with the @mcp.tool() annotation to explicitly identify it as a tool:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
@mcp.tool() def get_flight_status(flight): """Returns Flight Information""" AEROAPI_BASE_URL = "https://aeroapi.flightaware.com/aeroapi" AEROAPI_KEY="YOUR_API_KEY" def get_api_session(): session = requests.Session() session.headers.update({"x-apikey": AEROAPI_KEY}) return session def fetch_flight_data(flight_id, session): if "flight_id=" in flight_id: flight_id = flight_id.split("flight_id=")[1] start_date = datetime.now().date().strftime('%Y-%m-%d') end_date = (datetime.now().date() + timedelta(days=1)).strftime('%Y-%m-%d') api_resource = f"/flights/{flight_id}?start={start_date}&end={end_date}" # Fixed & to & try: response = session.get(f"{AEROAPI_BASE_URL}{api_resource}") response.raise_for_status() json_data = response.json() # Print the response for debugging print("API Response:", json.dumps(json_data, indent=2)) if 'flights' not in json_data or len(json_data['flights']) == 0: return None return json_data['flights'][0] except Exception as e: print(f"Error fetching flight data: {e}") return None def utc_to_local(utc_date_str, local_timezone_str): utc_datetime = datetime.strptime(utc_date_str, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=pytz.utc) local_timezone = pytz.timezone(local_timezone_str) local_datetime = utc_datetime.astimezone(local_timezone) return local_datetime.strftime('%Y-%m-%d %H:%M:%S') session = get_api_session() flight_data = fetch_flight_data(flight, session) if flight_data is None: return json.dumps({ 'error': 'Failed to retrieve flight data. Check the API key and flight ID.', 'flight': flight }) try: dep_key = 'estimated_out' if 'estimated_out' in flight_data and flight_data['estimated_out'] else \ 'actual_out' if 'actual_out' in flight_data and flight_data['actual_out'] else \ 'scheduled_out' arr_key = 'estimated_in' if 'estimated_in' in flight_data and flight_data['estimated_in'] else \ 'actual_in' if 'actual_in' in flight_data and flight_data['actual_in'] else \ 'scheduled_in' flight_details = f""" Flight: {flight} Source: {flight_data['origin']['city']} Destination: {flight_data['destination']['city']} Departure Time: {utc_to_local(flight_data[dep_key], flight_data['origin']['timezone'])} Arrival Time: {utc_to_local(flight_data[arr_key], flight_data['destination']['timezone'])} Status: {flight_data['status']} """ return flight_details except Exception as e: print(f"Error processing flight data: {e}") return json.dumps({ 'error': f'Error processing flight data: {str(e)}', 'flight': flight }) |
The last step is to create the main function to run the server if it’s executed directly:
|
1 2 |
if __name__ == "__main__": mcp.run() |
That’s it! We’re done building our first MCP server. It’s time to put it to the test by integrating it with Claude Desktop.
Step 3: Integrating With Claude Desktop
Ensure you have the latest version of Claude Desktop. Open the Settings menu to access the Developer tab:

Click on the Edit Config button to access the MCP settings file named claude_desktop_config.json.
Open the file in your favorite editor and add the following content:
|
1 2 3 4 5 6 7 8 9 10 |
{ "mcpServers": { "flight": { "command": "/Users/janakiramm/Demo/venv/bin/python", "args": [ "/Users/janakiramm/Demo/flight_server.py" ] } } } |
Replace the path with the absolute path to your Python environment and MCP server and save the file.
Restart Claude, and you should now find your MCP server within the settings option.

Make sure it’s turned on. Now we’re all set to use our MCP server from Claude.
It’s time to test the MCP server by asking Claude about a flight schedule. Claude will ask for your permission to access a third-party service.

Once you allow it, Claude will invoke the MCP server and fetch the data, which acts as the context to the original prompt.

As we can see from the output, Claude invoked the MCP server to fetch the real-time status. Note that we are using the STDIO transport.
You can find the entire code in the Gist below:
In upcoming articles in this series, we’ll explore how to use other transport protocols, including streamable HTTP transport. Stay tuned!