From 9c78bad4a03e00be0b0992b2b4ce2007492a43a0 Mon Sep 17 00:00:00 2001 From: walidsi <42148514+walidsi@users.noreply.github.com> Date: Sun, 4 May 2025 23:33:19 +0300 Subject: [PATCH] v0.1.0 --- .gitignore | 4 + .python-version | 1 + LICENSE | 24 ++++ README.md | 37 +++++++ openmeteo_weather/__init__.py | 3 + openmeteo_weather/openmeteo_weather.py | 147 +++++++++++++++++++++++++ pyproject.toml | 26 +++++ 7 files changed, 242 insertions(+) create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 LICENSE create mode 100644 README.md create mode 100644 openmeteo_weather/__init__.py create mode 100644 openmeteo_weather/openmeteo_weather.py create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05795cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +.venv +uv.lock +.cache.sqlite diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0031c4 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# MCP Weather Server + +A simple MCP server that provides hourly weather forecasts using the AccuWeather API. + +## Setup + +1. Install dependencies using `uv`: +```bash +uv venv +uv sync +``` + + +## Running the Server + +```json +{ + "mcpServers": { + "open_meteo_weather": { + + } + } +} +``` + +## API Usage + +### Get 7-dy Weather Forecast + +Response: +```json +{ + +} +``` + +## The API provides: diff --git a/openmeteo_weather/__init__.py b/openmeteo_weather/__init__.py new file mode 100644 index 0000000..2d339ba --- /dev/null +++ b/openmeteo_weather/__init__.py @@ -0,0 +1,3 @@ +"""MCP Weather - Weather forecast tool for MCP.""" + +__version__ = "0.1.0" diff --git a/openmeteo_weather/openmeteo_weather.py b/openmeteo_weather/openmeteo_weather.py new file mode 100644 index 0000000..d3ebcb0 --- /dev/null +++ b/openmeteo_weather/openmeteo_weather.py @@ -0,0 +1,147 @@ +import os +import json +from pathlib import Path +from typing import Dict, Optional +from fastmcp import FastMCP +from geopy.geocoders import Nominatim +import openmeteo_requests + +import pandas as pd +import requests_cache +from retry_requests import retry + +# Initialize FastMCP +mcp = FastMCP("openmeteo-weather-mcp") + +# Setup the Open-Meteo API client with cache and retry on error +cache_session = requests_cache.CachedSession(".cache", expire_after=3600) +retry_session = retry(cache_session, retries=5, backoff_factor=0.2) +openmeteo = openmeteo_requests.Client(session=retry_session) + +geolocator = Nominatim(user_agent="openmeteo-weather-mcp") + + +def get_lat_long(location_string: str): + """ + Gets the latitude and longitude of a location string using Nominatim. + + Args: + location_string: The location string (e.g., "Paris, France"). + + Returns: + A tuple containing (latitude, longitude) or None if the location is not found. + """ + + try: + location = geolocator.geocode(location_string) + if location: + return (location.latitude, location.longitude) + else: + return None + except Exception as e: + print(f"Error geocoding location: {e}") + return None + + +@mcp.tool() +async def get_7day_weather(location: str) -> Dict: + """Get hourly weather forecast for a location.""" + + # Make sure all required weather variables are listed here + # The order of variables in hourly or daily is important to assign them correctly below + url = "https://api.open-meteo.com/v1/forecast" + lat, long = get_lat_long(location) + params = { + "latitude": lat, + "longitude": long, + "hourly": ["temperature_2m", "relative_humidity_2m", "precipitation"], + } + responses = openmeteo.weather_api(url, params=params) + + # Process first location. Add a for-loop for multiple locations or weather models + response = responses[0] + print(f"Coordinates {response.Latitude()}°N {response.Longitude()}°E") + print(f"Elevation {response.Elevation()} m asl") + print(f"Timezone {response.Timezone()}{response.TimezoneAbbreviation()}") + print(f"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s") + + # Process hourly data. The order of variables needs to be the same as requested. + hourly = response.Hourly() + hourly_temperature_2m = hourly.Variables(0).ValuesAsNumpy() + hourly_relative_humidity_2m = hourly.Variables(1).ValuesAsNumpy() + hourly_precipitation = hourly.Variables(2).ValuesAsNumpy() + + hourly_data = { + "date": pd.date_range( + start=pd.to_datetime(hourly.Time(), unit="s", utc=True), + end=pd.to_datetime(hourly.TimeEnd(), unit="s", utc=True), + freq=pd.Timedelta(seconds=hourly.Interval()), + inclusive="left", + ) + } + + hourly_data["temperature_2m"] = hourly_temperature_2m + hourly_data["relative_humidity_2m"] = hourly_relative_humidity_2m + hourly_data["precipitation"] = hourly_precipitation + + hourly_dataframe = pd.DataFrame(data=hourly_data) + return hourly_dataframe.to_dict(orient="records") + + +@mcp.tool() +async def get_current_weather(location: str) -> Dict: + """Get current weather forecast for a location.""" + + # Make sure all required weather variables are listed here + # The order of variables in hourly or daily is important to assign them correctly below + url = "https://api.open-meteo.com/v1/forecast" + lat, long = get_lat_long(location) + params = { + "latitude": lat, + "longitude": long, + "current": [ + "temperature_2m", + "relative_humidity_2m", + "apparent_temperature", + "precipitation", + "weather_code", + "wind_speed_10m", + "wind_direction_10m", + ], + } + responses = openmeteo.weather_api(url, params=params) + + # Process first location. Add a for-loop for multiple locations or weather models + response = responses[0] + print(f"Coordinates {response.Latitude()}°N {response.Longitude()}°E") + print(f"Elevation {response.Elevation()} m asl") + print(f"Timezone {response.Timezone()}{response.TimezoneAbbreviation()}") + print(f"Timezone difference to GMT+0 {response.UtcOffsetSeconds()} s") + + # Current values. The order of variables needs to be the same as requested. + current = response.Current() + current_temperature_2m = current.Variables(0).Value() + current_relative_humidity_2m = current.Variables(1).Value() + current_apparent_temperature = current.Variables(2).Value() + current_precipitation = current.Variables(3).Value() + current_weather_code = current.Variables(4).Value() + current_wind_speed_10m = current.Variables(5).Value() + current_wind_direction_10m = current.Variables(6).Value() + + current_dict = { + "temperature_2m": current_temperature_2m, + "relative_humidity_2m": current_relative_humidity_2m, + "apparent_temperature": current_apparent_temperature, + "precipitation": current_precipitation, + "weather_code": current_weather_code, + "wind_speed_10m": current_wind_speed_10m, + "wind_direction_10m": current_wind_direction_10m, + } + + to_json = json.dumps(current_dict) + return to_json + + +if __name__ == "__main__": + # Initialize and run the server + mcp.run(transport="stdio") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b52803e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "openmeteo-weather-mcp" +version = "0.1.0" +requires-python = ">=3.13" +description = "Weather forecast tool for MCP" +authors = [] +dependencies = [ + "fastmcp", + "python-dotenv", + "aiohttp", + "uvicorn", + "geopy", + "pandas", + "requests_cache", + "retry_requests", + "openmeteo_requests" +] +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project.scripts] +openmeteo-weather-mcp = "openmeteo_weather.openmeteo_weather:mcp.run" + +[tool.hatch.build.targets.wheel] +packages = ["openmeteo_weather.py"]