| """ |
| 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 |
|
|
| |
| |
| |
| 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() |
| data = resp.json() |
| results = data.get("results") |
| if not results: |
| raise ValueError(f"Location not found: {location!r}") |
| r = results[0] |
| |
| 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", |
| }, |
| timeout=10, |
| ) |
| resp.raise_for_status() |
| return resp.json() |
|
|
|
|
| |
| |
| |
| |
| 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", |
| } |
|
|
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| @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'." |
|
|
| |
| try: |
| lat, lon, resolved = _geocode(location) |
| except ValueError as e: |
| return str(e) |
| except httpx.HTTPError as e: |
| return f"Geocoding request failed: {e}" |
|
|
| |
| try: |
| data = _fetch_temperature(lat, lon, unit) |
| except httpx.HTTPError as e: |
| return f"Weather request failed: {e}" |
|
|
| |
| 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}") |
| 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" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| import os |
| |
| |
| |
| transport = os.environ.get("MCP_TRANSPORT", "sse") |
| if transport == "stdio": |
| mcp.run(transport="stdio") |
| else: |
| |
| mcp.run(transport="sse") |
|
|