temp1 / weather_mcp_server.py
dbenham2's picture
Add beginner-friendly documentation to api.py and weather_mcp_server.py
7f46631
Raw
History Blame Contribute Delete
8.33 kB
"""
weather_mcp_server.py — The weather tool server for this app.
WHAT THIS FILE DOES:
This file is a "tool server" — it doesn't talk to users directly. Instead,
it waits for api.py to ask it to do things, like "fetch the current
temperature in Tokyo." It does the work and sends the result back.
WHAT IS MCP (Model Context Protocol)?
MCP is a standard developed by Anthropic that defines how AI models can use
external tools. The problem it solves: AI models only know what they were
trained on — they can't look things up in real time. MCP lets you plug in
tools that the AI can call to get live data.
There are two sides to MCP:
- MCP Server (this file): defines and runs the tools
- MCP Client (api.py): connects to this server and calls the tools
on behalf of the AI
Think of it like a power outlet and a plug — MCP is the standard that
makes them compatible.
WHAT IS FastMCP?
FastMCP is a Python library that makes it easy to build MCP servers. Without
it, you'd have to implement the MCP protocol by hand. With it, you just write
normal Python functions and add a @mcp.tool() decorator, and FastMCP handles
all the communication details automatically.
HOW THE WEATHER DATA WORKS:
This app uses Open-Meteo (open-meteo.com), a free weather API that doesn't
require an account or API key. Getting weather for a city takes two steps:
1. Geocoding: convert the city name ("Tokyo") into coordinates (lat/lon)
2. Weather fetch: use those coordinates to get current conditions
HOW THIS SERVER COMMUNICATES:
This server uses SSE (Server-Sent Events) — a way for one server to talk to
another over HTTP. It runs on port 8000 and api.py connects to it there.
SSE is used instead of stdio (standard input/output) because both servers
run as separate processes inside Docker and need to communicate over the
network, not through a shared terminal.
"""
import httpx
from mcp.server.fastmcp import FastMCP
# Create the MCP server instance.
# "weather-server" is just a name that identifies this server.
# host/port tell it to listen on all network interfaces on port 8000.
mcp = FastMCP("weather-server", host="0.0.0.0", port=8000)
def _geocode(location: str) -> tuple[float, float, str]:
"""
Convert a city name into GPS coordinates (latitude and longitude).
AI models work with place names like "Tokyo" or "Paris", but weather APIs
need exact coordinates. This function bridges that gap by calling Open-Meteo's
free geocoding API.
Returns a tuple of (latitude, longitude, full_place_name).
For example: "Tokyo" → (35.6895, 139.6917, "Tokyo, Tokyo, Japan")
Raises ValueError if the location isn't found.
"""
resp = httpx.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={"name": location, "count": 1, "language": "en", "format": "json"},
timeout=10,
)
resp.raise_for_status() # Raises an error if the HTTP request failed
data = resp.json()
results = data.get("results")
if not results:
raise ValueError(f"Location not found: {location!r}")
r = results[0]
# Build a full place name like "Chicago, Illinois, United States"
name = f"{r['name']}, {r.get('admin1', '')}, {r.get('country', '')}".strip(", ")
return r["latitude"], r["longitude"], name
def _fetch_temperature(lat: float, lon: float, unit: str) -> dict:
"""
Fetch current weather conditions from Open-Meteo using GPS coordinates.
Requests three pieces of data:
- temperature_2m: air temperature measured 2 meters above ground
- weathercode: a WMO standard code representing current conditions
(e.g. 0 = clear sky, 61 = slight rain)
- wind_speed_10m: wind speed measured 10 meters above ground
Returns the raw JSON response from Open-Meteo.
"""
unit_param = "fahrenheit" if unit == "fahrenheit" else "celsius"
resp = httpx.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,weathercode,wind_speed_10m",
"temperature_unit": unit_param,
"wind_speed_unit": "mph",
"timezone": "auto", # Automatically detect the timezone for this location
},
timeout=10,
)
resp.raise_for_status()
return resp.json()
# WMO (World Meteorological Organization) weather codes.
# Open-Meteo returns a numeric code for current conditions — this table
# translates those numbers into human-readable descriptions.
# This is a subset of the full WMO code table (codes go up to 99).
WMO_CODES = {
0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
45: "Fog", 48: "Icy fog",
51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle",
61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain",
71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow",
80: "Slight showers", 81: "Moderate showers", 82: "Violent showers",
95: "Thunderstorm", 96: "Thunderstorm with hail",
}
# ---------------------------------------------------------------------------
# The MCP tool — this is what the AI model can call
# ---------------------------------------------------------------------------
# @mcp.tool() is a "decorator" — it registers this function with the MCP server
# so that api.py can discover it and offer it to the AI model. Without this
# decorator, the function would just be a regular Python function that nothing
# outside this file knows about.
@mcp.tool()
def get_current_temperature(location: str, unit: str = "fahrenheit") -> str:
"""
Get the current temperature for any city or location.
This is the function the AI model calls when it needs weather data.
The AI passes in the location name and preferred unit, and gets back
a plain-text summary it can include in its response to the user.
Args:
location: City name or place (e.g. "Chicago", "Paris", "Tokyo").
unit: Temperature unit — "fahrenheit" or "celsius". Defaults to fahrenheit.
Returns:
A plain-text summary of current conditions, for example:
Current conditions in Tokyo, Tokyo, Japan:
Temperature: 68°F
Conditions: Partly cloudy
Wind speed: 12 mph
"""
unit = unit.lower()
if unit not in ("fahrenheit", "celsius"):
return f"Invalid unit {unit!r}. Use 'fahrenheit' or 'celsius'."
# Step 1: Convert the city name to GPS coordinates
try:
lat, lon, resolved = _geocode(location)
except ValueError as e:
return str(e)
except httpx.HTTPError as e:
return f"Geocoding request failed: {e}"
# Step 2: Fetch the current weather for those coordinates
try:
data = _fetch_temperature(lat, lon, unit)
except httpx.HTTPError as e:
return f"Weather request failed: {e}"
# Step 3: Extract the relevant fields from the response and format them
current = data["current"]
temp = current["temperature_2m"]
wind = current["wind_speed_10m"]
code = current.get("weathercode", 0)
condition = WMO_CODES.get(code, f"Weather code {code}") # Fall back to the raw code if unknown
symbol = "°F" if unit == "fahrenheit" else "°C"
return (
f"Current conditions in {resolved}:\n"
f" Temperature: {temp}{symbol}\n"
f" Conditions: {condition}\n"
f" Wind speed: {wind} mph"
)
# ---------------------------------------------------------------------------
# Entry point — how this server starts up
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import os
# MCP_TRANSPORT can be set to "stdio" for local testing (communicates via
# terminal input/output). In Docker/HuggingFace Spaces it defaults to "sse"
# so api.py can connect to it over the network.
transport = os.environ.get("MCP_TRANSPORT", "sse")
if transport == "stdio":
mcp.run(transport="stdio")
else:
# SSE (Server-Sent Events) — runs as a persistent HTTP server on port 8000
mcp.run(transport="sse")