llm-tools/mcps/open_meteo_mcp/open_meteo_mcp_server.py

243 lines
8.1 KiB
Python
Raw Normal View History

2026-04-08 11:11:04 +00:00
"""
open_meteo_mcp - MCP server for global weather data via the Open-Meteo API.
Provides tools for fetching current weather conditions and multi-day forecasts
for any location worldwide. No API key required.
"""
import logging
from typing import Optional
import httpx
from mcp.server.fastmcp import FastMCP
# ---------------------------------------------------------------------------
# Logging & Output Redirection
# ---------------------------------------------------------------------------
import os
import sys
LOG_FILE = os.environ.get("OPEN_METEO_MCP_LOG_FILE", "open_meteo_mcp.log")
def setup_redirection():
"""Reduces noise by suppressing stderr and logging to a file."""
# Suppress stderr
devnull = open(os.devnull, "w")
os.dup2(devnull.fileno(), sys.stderr.fileno())
# Configure logging to write to the log file directly
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
filename=LOG_FILE,
filemode="a"
)
setup_redirection()
logger = logging.getLogger("open_meteo_mcp")
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
OPEN_METEO_BASE = "https://api.open-meteo.com/v1/forecast"
# WMO Weather interpretation codes
WMO_CODES = {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Fog",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
56: "Light freezing drizzle",
57: "Dense freezing drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
66: "Light freezing rain",
67: "Heavy freezing rain",
71: "Slight snowfall",
73: "Moderate snowfall",
75: "Heavy snowfall",
77: "Snow grains",
80: "Slight rain showers",
81: "Moderate rain showers",
82: "Violent rain showers",
85: "Slight snow showers",
86: "Heavy snow showers",
95: "Thunderstorm",
96: "Thunderstorm with slight hail",
99: "Thunderstorm with heavy hail",
}
# ---------------------------------------------------------------------------
# MCP Server
# ---------------------------------------------------------------------------
mcp = FastMCP(
"open_meteo_mcp",
instructions="Global weather data via Open-Meteo API",
)
def _describe_weather_code(code: int) -> str:
return WMO_CODES.get(code, f"Unknown ({code})")
def _c_to_f(celsius: float) -> float:
return round(celsius * 9 / 5 + 32, 1)
def _temp_both(celsius: float) -> str:
return f"{celsius}°C / {_c_to_f(celsius)}°F"
def _kmh_to_mph(kmh: float) -> float:
return round(kmh * 0.621371, 1)
def _wind_both(kmh: float) -> str:
return f"{kmh}km/h / {_kmh_to_mph(kmh)}mph"
@mcp.tool()
async def get_current_weather(latitude: float, longitude: float) -> str:
"""Get current weather conditions for a location.
Args:
latitude: Latitude of the location (e.g. 51.752 for Oxford, UK)
longitude: Longitude of the location (e.g. -1.258 for Oxford, UK)
"""
try:
params = {
"latitude": latitude,
"longitude": longitude,
"current": ",".join([
"temperature_2m",
"relative_humidity_2m",
"apparent_temperature",
"weather_code",
"wind_speed_10m",
"wind_direction_10m",
"wind_gusts_10m",
"precipitation",
"cloud_cover",
"pressure_msl",
]),
"timezone": "auto",
}
async with httpx.AsyncClient() as client:
resp = await client.get(OPEN_METEO_BASE, params=params, timeout=30)
resp.raise_for_status()
data = resp.json()
current = data["current"]
units = data["current_units"]
tz = data.get("timezone", "Unknown")
lines = [
f"Current Weather (timezone: {tz})",
f" Time: {current['time']}",
f" Condition: {_describe_weather_code(current['weather_code'])}",
f" Temperature: {_temp_both(current['temperature_2m'])}",
f" Feels like: {_temp_both(current['apparent_temperature'])}",
f" Humidity: {current['relative_humidity_2m']}{units['relative_humidity_2m']}",
f" Wind: {_wind_both(current['wind_speed_10m'])} from {current['wind_direction_10m']}{units['wind_direction_10m']}",
f" Gusts: {_wind_both(current['wind_gusts_10m'])}",
f" Precipitation: {current['precipitation']}{units['precipitation']}",
f" Cloud cover: {current['cloud_cover']}{units['cloud_cover']}",
f" Pressure: {current['pressure_msl']}{units['pressure_msl']}",
]
return "\n".join(lines)
except Exception as e:
logger.error("Error in get_current_weather(%s, %s): %s", latitude, longitude, e, exc_info=True)
return f"Error fetching current weather: {type(e).__name__}: {e}"
@mcp.tool()
async def get_forecast(
latitude: float,
longitude: float,
days: int = 7,
) -> str:
"""Get a multi-day weather forecast for a location.
Args:
latitude: Latitude of the location
longitude: Longitude of the location
days: Number of forecast days (1-16, default 7)
"""
try:
days = max(1, min(16, days))
params = {
"latitude": latitude,
"longitude": longitude,
"daily": ",".join([
"weather_code",
"temperature_2m_max",
"temperature_2m_min",
"apparent_temperature_max",
"apparent_temperature_min",
"precipitation_sum",
"precipitation_probability_max",
"wind_speed_10m_max",
"wind_gusts_10m_max",
"wind_direction_10m_dominant",
"sunrise",
"sunset",
]),
"temperature_unit": "celsius",
"timezone": "auto",
"forecast_days": days,
}
async with httpx.AsyncClient() as client:
resp = await client.get(OPEN_METEO_BASE, params=params, timeout=30)
resp.raise_for_status()
data = resp.json()
daily = data["daily"]
units = data["daily_units"]
tz = data.get("timezone", "Unknown")
sections = [f"Forecast for {days} day(s) (timezone: {tz})"]
for i in range(len(daily["time"])):
section = [
f"\n--- {daily['time'][i]} ---",
f" Condition: {_describe_weather_code(daily['weather_code'][i])}",
f" High: {_temp_both(daily['temperature_2m_max'][i])} Low: {_temp_both(daily['temperature_2m_min'][i])}",
f" Feels like: {_temp_both(daily['apparent_temperature_max'][i])} / {_temp_both(daily['apparent_temperature_min'][i])}",
f" Precipitation: {daily['precipitation_sum'][i]}{units['precipitation_sum']} (chance: {daily['precipitation_probability_max'][i]}{units['precipitation_probability_max']})",
f" Wind: {_wind_both(daily['wind_speed_10m_max'][i])} gusts {_wind_both(daily['wind_gusts_10m_max'][i])} from {daily['wind_direction_10m_dominant'][i]}{units['wind_direction_10m_dominant']}",
f" Sunrise: {daily['sunrise'][i]} Sunset: {daily['sunset'][i]}",
]
sections.append("\n".join(section))
return "\n".join(sections)
except Exception as e:
logger.error("Error in get_forecast(%s, %s, days=%s): %s", latitude, longitude, days, e, exc_info=True)
return f"Error fetching forecast: {type(e).__name__}: {e}"
# ---------------------------------------------------------------------------
# Entrypoints
# ---------------------------------------------------------------------------
def main():
logger.info("Starting open_meteo_mcp on streamable-http")
mcp.run(transport="streamable-http")
def main_stdio():
logger.info("Starting open_meteo_mcp via stdio")
mcp.run(transport="stdio")
if __name__ == "__main__":
main()