π° πππππ πππ π΄πππππππ πππ πΎπππππππππ ππππππ.
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.
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.
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
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"
]
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
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¤t_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,
}
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)
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
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"
}
}
}
}
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
You can see the full source code here on github