Practical Guide: Building Your First MCP Server in Python
Hello! If you want to learn in practice how to connect AI applications with the real world, this guide is for you. We will build, step by step, a server for the Model Context Protocol (MCP) using Python.
The idea is to show, in a practical way, how you can expose features from your code so that any MCP-compatible application, like an LLM, can use them.
What are we going to build?
Our goal is to create a weather forecasting server that offers four simple yet powerful tools:
get_current_weather_city: Fetch the current weather in a Brazilian city.get_forecast_city: Get the weather forecast for the next few days.get_weather_coordinates: For precision needs, fetch weather by latitude and longitude.get_weather_state_capital: A handy tool to check the weather in a Brazilian state capital.
By the end, you will have a working server that can be connected to a host like Claude for Desktop, allowing the AI to query real-time weather.
Before we start: what you need
To follow this guide smoothly, it helps to have a few things handy:
- Python 3.10 or higher installed on your machine.
- Basic familiarity with how LLMs work.
- A free account on OpenWeatherMap. We will need an API key from there to fetch weather data.
- Claude for Desktop (or another MCP-compatible host) installed so we can test our server at the end.
Preparing our development environment
Let’s organize our workspace. Each step here has a clear purpose to keep our project clean and functional.
- Install
uv
We will useuv, a super-fast environment and package manager for Python. If you don’t have it yet, install it with:curl -LsSf https://astral.sh/uv/install.sh | sh - Create the project folder
Create a directory for our project and enter it.uv init weather cd weather - Create and activate the virtual environment
This isolates our project dependencies—an essential Python practice.uv venv source .venv/bin/activate - Install dependencies
We need three packages: the MCP SDK,httpxfor async HTTP requests, andpython-dotenvto manage our API key.uv add "mcp[cli]" httpx python-dotenv - Create the project files
We will create our main server file and a.envfile to store our API key securely.touch weather.py echo "OPENWEATHER_API_KEY=YOUR_KEY_HERE" > .env
Important: Open the.envfile and replaceYOUR_KEY_HEREwith your real OpenWeatherMap API key.
Building the server: step by step
Now comes the fun part: writing the code. We will build the weather.py file in stages, explaining each block.
1. Imports and initial setup
Let’s start by importing what we need and setting initial variables. This lays the foundation for our server.
# weather.py
import os
from typing import Any, Optional
import httpx
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
# Initialize the server with a descriptive name
mcp = FastMCP("weather-brazil-server")
# Base URLs and a User-Agent as a good practice
OPENWEATHER_API_BASE = "https://api.openweathermap.org/data/2.5"
USER_AGENT = "weather-brazil-mcp-server/1.0"
# Load environment variables (our API key)
load_dotenv()
OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY")
2. A function to fetch data from the web
To avoid repeating code, we’ll create a helper function that makes OpenWeatherMap API requests. It’s asynchronous (async) so the server doesn’t block while waiting for responses.
# weather.py (continued)
async def make_request(url: str, params: dict) -> Optional[dict[str, Any]]:
"""Perform an asynchronous GET request and return JSON or None on error."""
headers = {"User-Agent": USER_AGENT}
async with httpx.AsyncClient() as client:
try:
response = await client.get(url, params=params, headers=headers, timeout=30.0)
response.raise_for_status() # Raises on 4xx/5xx
return response.json()
except httpx.HTTPStatusError as e:
print(f"HTTP error: {e.response.status_code}")
return None
except httpx.RequestError as e:
print(f"Request error: {e}")
return None
3. Formatting data for humans
The API response is a JSON with many details. Let’s add functions to format it into readable, friendly text.
# weather.py (continued)
def format_current_weather(data: Optional[dict]) -> str:
"""Format current weather data into a friendly string."""
if not data:
return "Unable to fetch weather data."
weather = data.get("weather", [{}])[0]
main = data.get("main", {})
wind = data.get("wind", {})
return (
f'Current Conditions - {data.get("name", "Unknown Location")}\n\n'
f'Temperature: {main.get("temp", "N/A")}°C (Feels like: {main.get("feels_like", "N/A")}°C)\n'
f'Condition: {weather.get("description", "N/A").title()}\n'
f'Humidity: {main.get("humidity", "N/A")}%'
)
def format_forecast(data: Optional[dict]) -> str:
"""Format weather forecast into a friendly string."""
if not data or "list" not in data:
return "Unable to fetch weather forecast."
city_name = data.get("city", {}).get("name", "Unknown Location")
forecasts = [f"Forecast for {city_name}\n"]
for item in data["list"][:5]: # Next 5 periods
dt_txt = item.get("dt_txt", "")
main = item.get("main", {})
weather = item.get("weather", [{}])[0]
forecasts.append(
f'\n---\n{dt_txt}\n'
f'Temperature: {main.get("temp", "N/A")}°C\n'
f'Condition: {weather.get("description", "N/A").title()}'
)
return "".join(forecasts)
4. Creating the tools
This is the core part. Using the @mcp.tool() decorator, we turn Python functions into tools exposed by MCP.
# weather.py (continued)
@mcp.tool()
async def get_current_weather_city(city: str, state: str = "") -> str:
"""Get current weather conditions for a Brazilian city."""
query = f"{city},{state},BR" if state else f"{city},BR"
params = {"q": query, "appid": OPENWEATHER_API_KEY, "units": "metric", "lang": "pt_br"}
data = await make_request(f"{OPENWEATHER_API_BASE}/weather", params)
return format_current_weather(data)
@mcp.tool()
async def get_forecast_city(city: str, state: str = "") -> str:
"""Get the weather forecast for a Brazilian city."""
query = f"{city},{state},BR" if state else f"{city},BR"
params = {"q": query, "appid": OPENWEATHER_API_KEY, "units": "metric", "lang": "pt_br"}
data = await make_request(f"{OPENWEATHER_API_BASE}/forecast", params)
return format_forecast(data)
@mcp.tool()
async def get_weather_coordinates(latitude: float, longitude: float) -> str:
"""Get weather conditions for geographic coordinates."""
params = {"lat": latitude, "lon": longitude, "appid": OPENWEATHER_API_KEY, "units": "metric", "lang": "pt_br"}
data = await make_request(f"{OPENWEATHER_API_BASE}/weather", params)
return format_current_weather(data)
5. An extra tool using a dictionary
To make our server smarter, we’ll create a tool that gets the weather for a state capital using a dictionary.
# weather.py (continued)
CAPITAIS_BRASIL = {
"acre": ("Rio Branco", "AC"), "alagoas": ("Maceió", "AL"),
"amapá": ("Macapá", "AP"), "amazonas": ("Manaus", "AM"),
"bahia": ("Salvador", "BA"), "ceará": ("Fortaleza", "CE"),
"distrito federal": ("Brasília", "DF"), "espírito santo": ("Vitória", "ES"),
"goiás": ("Goiânia", "GO"), "maranhão": ("São Luís", "MA"),
"mato grosso": ("Cuiabá", "MT"), "mato grosso do sul": ("Campo Grande", "MS"),
"minas gerais": ("Belo Horizonte", "MG"), "pará": ("Belém", "PA"),
"paraíba": ("João Pessoa", "PB"), "paraná": ("Curitiba", "PR"),
"pernambuco": ("Recife", "PE"), "piauí": ("Teresina", "PI"),
"rio de janeiro": ("Rio de Janeiro", "RJ"), "rio grande do norte": ("Natal", "RN"),
"rio grande do sul": ("Porto Alegre", "RS"), "rondônia": ("Porto Velho", "RO"),
"roraima": ("Boa Vista", "RR"), "santa catarina": ("Florianópolis", "SC"),
"são paulo": ("São Paulo", "SP"), "sergipe": ("Aracaju", "SE"),
"tocantins": ("Palmas", "TO"),
}
@mcp.tool()
async def get_weather_state_capital(state: str) -> str:
"""Get weather conditions for a Brazilian state capital."""
state_lower = state.lower()
capital_info = None
for state_name, info in CAPITAIS_BRASIL.items():
if state_lower == state_name or state_lower == info[1].lower():
capital_info = info
break
if not capital_info:
return f"State '{state}' not found. Try the full name or the abbreviation."
return await get_current_weather_city(capital_info[0], capital_info[1])
6. Running the server
Finally, add the code that starts the server when the script runs. We include a check to ensure the API key is set.
# weather.py (continued)
if __name__ == "__main__":
if not OPENWEATHER_API_KEY or OPENWEATHER_API_KEY == "YOUR_KEY_HERE":
print("❌ Error: OpenWeatherMap API key is not configured.")
print(" Please edit the .env file and add your key.")
exit(1)
print("🌦️ Weather MCP Server started.")
print(" Waiting for a host like Claude for Desktop to connect...")
mcp.run()
Connecting with Claude for Desktop
For Claude to find our server, we need to tell it where it is. Edit Claude’s config file (paths below are the most common):
- macOS/Linux:
~/.config/claude_desktop/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add the following entry, adjusting the paths to where you saved the project and where your uv is installed:
{
"mcpServers": {
"weather": {
"command": "/path/to/your/.local/bin/uv",
"args": [
"run",
"--cwd",
"/path/to/your/project/weather",
"python",
"weather.py"
]
}
}
}
Save the file and restart Claude for Desktop.
Testing our work
With Claude open, go to the “Search and tools” section. If everything is correct, you will see our four tools listed! You can now ask questions like:
- “What’s the weather like in Salvador, Bahia?”
- “What’s the forecast for Curitiba?”
- “What’s the weather in the capital of Goiás?”
Continue your MCP journey
Now that you’ve built your first server, how about going deeper?
- MCP Servers in Production: Best Practices with FastMCP
- Model Context Protocol: what it is and why it will change AI
I hope this practical guide helped demystify building MCP servers. Now it’s your turn! Experiment, build, and integrate.