Clivern

𝙰 πš‚πš˜πšπšπš πšŠπš›πšŽ π™΄πš—πšπš’πš—πšŽπšŽπš› πšŠπš—πš π™ΎπšŒπšŒπšŠπšœπš’πš˜πš—πšŠπš• πš†πš›πš’πšπšŽπš›.

Building a Secure MCP Server for Weather

17 July 2025

Ever wanted to build your own weather API that AI models can actually use? Today we’re going to create a secure MCP (Model Context Protocol) server that gives AI assistants the power to check the weather anywhere in the world. We’ll add proper authentication, geocoding (so you can just say β€œLondon” instead of coordinates), and make it production-ready with Docker.

What’s MCP Anyway?

Think of MCP as a translator between AI models and the real world. It’s a standard that lets AI assistants call functions, grab data, and interact with external systems in a secure way. Instead of AI models being stuck with their training data, they can now reach out and touch the real world - like checking the weather, reading files, or calling APIs.

Let’s Get Started

First, let’s create our project folder and get organized:

mkdir secure-weather-mcp-server
cd secure-weather-mcp-server

Create the project structure:

secure-weather-mcp-server/
β”œβ”€β”€ core/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ middleware/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   └── auth.py
β”‚   └── tools/
β”‚       β”œβ”€β”€ __init__.py
β”‚       └── weather.py
β”œβ”€β”€ app.py
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ Makefile
└── README.md

Setting Up Dependencies

Let’s create our pyproject.toml file to manage our Python packages:

[project]
name = "secure-weather-mcp-server"
version = "0.1.0"
description = "Secure MCP Server For Weather"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "fastmcp",
    "uvicorn[standard]",
    "fastapi",
    "requests"
]

Adding Security

We don’t want just anyone calling our weather API! Let’s create our authentication middleware in core/middleware/auth.py. Also this can be used to get the client API key and maybe use it to call third party service like their digitalocean account.

import logging
from fastapi import Request, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware

logger = logging.getLogger(__name__)

# Global variable to store the current request context
_current_request = None

def set_current_request(request: Request):
    """Set the current request for tools to access"""
    global _current_request
    _current_request = request

def get_api_key():
    """Get the current user's API key"""
    global _current_request

    if _current_request and hasattr(_current_request.state, "api_key"):
        return _current_request.state.api_key

    return None

class AuthMiddleware(BaseHTTPMiddleware):
    """Middleware for Handling Authentication and Authorization"""

    async def dispatch(self, request: Request, call_next):
        # Set the current request context for tools to access
        set_current_request(request)

        # Only require authentication for actual MCP tool calls
        if (
            request.url.path == "/mcp"
            and request.method == "POST"
            and "application/json" in request.headers.get("content-type", "")
        ):
            # Extract and store the API key from request headers
            api_key = request.headers.get("X-API-KEY")

            if api_key:
                # Store API key in request state for tools to access
                request.state.api_key = api_key
                logger.info(f"API key stored: {api_key}")
            else:
                request.state.api_key = None
                logger.info("No valid X-API-KEY header found for MCP tool call")
                raise HTTPException(
                    status_code=401,
                    detail="X-API-KEY header required for MCP tool calls",
                )
        else:
            # For non-tool-call requests, just log but don't require authentication
            logger.info(
                f"Non-tool-call request to {request.url.path} ({request.method}) - no authentication required"
            )
            request.state.api_key = None

        # Continue with the request
        response = await call_next(request)
        return response

Building the Weather Magic

Let’s create our weather tools in core/tools/weather.py. This is where the real magic happens. We will turns β€œLondon” into actual coordinates using Open-Meteo’s free geocoding API and then Grab current weather info using those coordinates.

$ curl -s "https://geocoding-api.open-meteo.com/v1/search?name=london&count=1&language=en&format=json" | jq .
{
  "results": [
    {
      "id": 2643743,
      "name": "London",
      "latitude": 51.50853,
      "longitude": -0.12574,
      "elevation": 25.0,
      "feature_code": "PPLC",
      "country_code": "GB",
      "admin1_id": 6269131,
      "admin2_id": 2648110,
      "timezone": "Europe/London",
      "population": 8961989,
      "country_id": 2635167,
      "country": "United Kingdom",
      "admin1": "England",
      "admin2": "Greater London"
    }
  ],
  "generationtime_ms": 0.6403923
}

$ curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.50853&longitude=-0.12574&current_weather=true" | jq .
{
  "latitude": 51.5,
  "longitude": -0.120000124,
  "generationtime_ms": 0.053763389587402344,
  "utc_offset_seconds": 0,
  "timezone": "GMT",
  "timezone_abbreviation": "GMT",
  "elevation": 23.0,
  "current_weather_units": {
    "time": "iso8601",
    "interval": "seconds",
    "temperature": "Β°C",
    "windspeed": "km/h",
    "winddirection": "Β°",
    "is_day": "",
    "weathercode": "wmo code"
  },
  "current_weather": {
    "time": "2025-08-26T11:30",
    "interval": 900,
    "temperature": 21.9,
    "windspeed": 16.4,
    "winddirection": 241,
    "is_day": 1,
    "weathercode": 3
  }
}

Here is the tool I created using the above requests to get the weather from location.

import logging
import requests
from typing import Dict, Optional, Tuple
from mcp.server.fastmcp import FastMCP
from core.middleware.auth import get_api_key

logger = logging.getLogger(__name__)

def get_coordinates_from_location(location: str) -> Optional[Tuple[float, float]]:
    """
    Get latitude and longitude coordinates from a location name using Open-Meteo's geocoding API.
    """
    try:
        # Use Open-Meteo's geocoding API
        geocoding_url = "https://geocoding-api.open-meteo.com/v1/search"
        params = {"name": location, "count": 1, "language": "en", "format": "json"}

        response = requests.get(geocoding_url, params=params, timeout=10)
        response.raise_for_status()

        data = response.json()

        if "results" in data and len(data["results"]) > 0:
            result = data["results"][0]
            latitude = result["latitude"]
            longitude = result["longitude"]
            logger.info(f"Found coordinates for '{location}': {latitude}, {longitude}")
            return latitude, longitude
        else:
            logger.info(f"No coordinates found for location: {location}")
            return None

    except Exception as e:
        logger.error(f"Error parsing geocoding response for '{location}': {e}")
        return None

def get_weather_by_coordinates(latitude: float, longitude: float) -> Optional[Dict]:
    """
    Get current weather data using latitude and longitude coordinates.
    """
    try:
        weather_url = "https://api.open-meteo.com/v1/forecast"
        params = {
            "latitude": latitude,
            "longitude": longitude,
            "current_weather": "true",
        }

        response = requests.get(weather_url, params=params, timeout=10)
        response.raise_for_status()

        data = response.json()
        logger.info(f"Weather data retrieved for coordinates {latitude}, {longitude}")
        return data

    except Exception as e:
        logger.error(f"Error fetching weather data: {e}")
        return None

def register_weather_tools(mcp: FastMCP):
    """Register weather-related tools with the MCP server"""

    @mcp.tool()
    def get_weather(location: str) -> Dict:
        """
        Get current weather for a location.

        Args:
            location: The location name (e.g., "London", "New York", "Tokyo")

        Returns:
            Dictionary containing weather information
        """
        # Get the current user's token
        api_key = get_api_key()
        logger.info(f"Tool 'get_weather' called for location: {location}")

        # Get coordinates from location name
        coordinates = get_coordinates_from_location(location)
        if not coordinates:
            return {
                "error": f"Could not find coordinates for location: {location}",
                "success": False,
            }

        latitude, longitude = coordinates

        # Get weather data using coordinates
        weather_data = get_weather_by_coordinates(latitude, longitude)
        if not weather_data:
            return {
                "error": f"Could not fetch weather data for {location}",
                "success": False,
            }

        # Extract current weather information
        current_weather = weather_data.get("current_weather", {})
        weather_units = weather_data.get("current_weather_units", {})

        return {
            "location": location,
            "coordinates": {"latitude": latitude, "longitude": longitude},
            "temperature": f"{current_weather.get('temperature')} {weather_units.get('temperature', 'Β°C')}",
            "windspeed": f"{current_weather.get('windspeed')} {weather_units.get('windspeed', 'km/h')}",
            "winddirection": f"{current_weather.get('winddirection')} {weather_units.get('winddirection', 'Β°')}",
            "weathercode": current_weather.get("weathercode"),
            "time": current_weather.get("time"),
            "success": True,
        }

Tying It All Together

Now let’s create our main application file app.py that brings everything together:

import argparse
import logging
from mcp.server.fastmcp import FastMCP
import uvicorn

# Import core modules
from core.middleware.auth import AuthMiddleware
from core.tools.weather import register_weather_tools

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Create an MCP server
mcp = FastMCP("Weather MCP Server")

def register_all_components(mcp: FastMCP):
    """Register all MCP components"""
    register_weather_tools(mcp)
    logger.info("All MCP components registered successfully")

# Register components
register_all_components(mcp)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Run MCP server")
    parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
    parser.add_argument("--port", type=int, default=8000, help="Port to bind to")

    args = parser.parse_args()

    # Create the HTTP app and add middleware
    app = mcp.streamable_http_app()
    app.add_middleware(AuthMiddleware)

    # Run as HTTP server
    print(f"Starting MCP HTTP server on {args.host}:{args.port}")
    print("Authentication middleware enabled - Authorization header required")
    uvicorn.run(app, host=args.host, port=args.port)

Making Development Easier

Let’s create a Makefile so we don’t have to remember all these commands. You need to install uv

.PHONY: help install run run-custom clean

help: ## Show this help message
	@echo "Available commands:"
	@echo "  install     - Install dependencies using uv sync"
	@echo "  run         - Run the server on localhost:8000"
	@echo "  run-custom  - Run the server with custom host and port"
	@echo "  clean       - Clean up any temporary files"
	@echo ""
	@echo "Examples:"
	@echo "  make install"
	@echo "  make run"
	@echo "  make run-custom HOST=0.0.0.0 PORT=9000"

install: ## Install dependencies
	uv sync

run: ## Run the server on localhost:8000
	uv run python app.py

run-custom: ## Run the server with custom host and port (e.g., make run-custom HOST=0.0.0.0 PORT=9000)
	uv run python app.py --host $(HOST) --port $(PORT)

clean: ## Clean up temporary files
	find . -type f -name "*.pyc" -delete
	find . -type d -name "__pycache__" -delete
	find . -type f -name "*.log" -delete

Now let’s start up our server:

make install
make run

Testing the Server

Test the server with Cursor AI IDE or other AI IDEs like Github Copilot, Trae AI … etc:

{
  "mcpServers": {
    "luchian": {
      "url": "http://localhost:8000/mcp",
      "headers": {
        "X-API-KEY": "315bbc9c-7441-415e-a68a-070b93ff2c0e"
      }
    }
  }
}

Running with Docker

Let’s create a Dockerfile so we can deploy this thing anywhere:

FROM python:3.11-slim

# Set environment variables
ENV DEBIAN_FRONTEND=noninteractive
ENV DEBCONF_NONINTERACTIVE_SEEN=true

# Set working directory
WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# Copy dependency files
COPY pyproject.toml ./

# Install Python dependencies
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -e .

# Copy application code
COPY . .

# Expose the port
EXPOSE 8000

# Set environment variables
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1

# Run the application
CMD ["python", "app.py", "--host", "0.0.0.0", "--port", "8000"]

Now let’s build and run our container:

docker build -t weather-mcp-server .
docker run -p 8000:8000 weather-mcp-server

Resources

You can see the full source code here on github