Patricio Martins Logo
Back to articles

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.

  1. Install uv
    We will use uv, 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
    
  2. Create the project folder
    Create a directory for our project and enter it.
    uv init weather
    cd weather
    
  3. Create and activate the virtual environment
    This isolates our project dependencies—an essential Python practice.
    uv venv
    source .venv/bin/activate
    
  4. Install dependencies
    We need three packages: the MCP SDK, httpx for async HTTP requests, and python-dotenv to manage our API key.
    uv add "mcp[cli]" httpx python-dotenv
    
  5. Create the project files
    We will create our main server file and a .env file to store our API key securely.
    touch weather.py
    echo "OPENWEATHER_API_KEY=YOUR_KEY_HERE" > .env
    

    Important: Open the .env file and replace YOUR_KEY_HERE with 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?

I hope this practical guide helped demystify building MCP servers. Now it’s your turn! Experiment, build, and integrate.