From f7b4e0a66409345ab295210c0bc5ed988e3f5fa4 Mon Sep 17 00:00:00 2001 From: furyhawk Date: Sat, 21 Jun 2025 09:41:03 +0800 Subject: [PATCH] feat: add weather tool with NWS API integration and update dependencies --- notebooks/pyproject.toml | 2 + notebooks/uv.lock | 4 ++ notebooks/weather/weather.py | 96 ++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 notebooks/weather/weather.py diff --git a/notebooks/pyproject.toml b/notebooks/pyproject.toml index 8808ad6..e71a590 100644 --- a/notebooks/pyproject.toml +++ b/notebooks/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "fastapi[standard]>=0.115.0", "filelock>=3.16.1", "gguf>=0.13.0", + "httpx>=0.28.1", "huggingface-hub[hf-xet]>=0.32.0", "importlib-metadata>=8.6.1 ; python_full_version < '3.10'", "ipykernel>=6.29.5", @@ -28,6 +29,7 @@ dependencies = [ "llama-cpp-python>=0.3.9", "llguidance>=0.7.11,<0.8.0 ; platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'x86_64'", "lm-format-enforcer>=0.10.11,<0.11", + "mcp[cli]>=1.9.4", "mistral-common[opencv]>=1.5.4", "msgspec>=0.19.0", "ninja>=1.11.1.4", diff --git a/notebooks/uv.lock b/notebooks/uv.lock index 2ff794f..2a58ca1 100644 --- a/notebooks/uv.lock +++ b/notebooks/uv.lock @@ -2067,6 +2067,7 @@ dependencies = [ { name = "fastapi", extra = ["standard"] }, { name = "filelock" }, { name = "gguf" }, + { name = "httpx" }, { name = "huggingface-hub", extra = ["hf-xet"] }, { name = "ipykernel" }, { name = "ipywidgets" }, @@ -2077,6 +2078,7 @@ dependencies = [ { name = "llama-cpp-python" }, { name = "llguidance", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, { name = "lm-format-enforcer" }, + { name = "mcp", extra = ["cli"] }, { name = "mistral-common", extra = ["opencv"] }, { name = "msgspec" }, { name = "ninja" }, @@ -2133,6 +2135,7 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" }, { name = "filelock", specifier = ">=3.16.1" }, { name = "gguf", specifier = ">=0.13.0" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "huggingface-hub", extras = ["hf-xet"], specifier = ">=0.32.0" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'", specifier = ">=8.6.1" }, { name = "ipykernel", specifier = ">=6.29.5" }, @@ -2144,6 +2147,7 @@ requires-dist = [ { name = "llama-cpp-python", specifier = ">=0.3.9" }, { name = "llguidance", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'x86_64'", specifier = ">=0.7.11,<0.8.0" }, { name = "lm-format-enforcer", specifier = ">=0.10.11,<0.11" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.9.4" }, { name = "mistral-common", extras = ["opencv"], specifier = ">=1.5.4" }, { name = "msgspec", specifier = ">=0.19.0" }, { name = "ninja", specifier = ">=1.11.1.4" }, diff --git a/notebooks/weather/weather.py b/notebooks/weather/weather.py new file mode 100644 index 0000000..93456bf --- /dev/null +++ b/notebooks/weather/weather.py @@ -0,0 +1,96 @@ +from typing import Any +import httpx +from mcp.server.fastmcp import FastMCP + +# Initialize FastMCP server +mcp = FastMCP("weather") + +# Constants +NWS_API_BASE = "https://api.weather.gov" +USER_AGENT = "weather-app/1.0" + + +async def make_nws_request(url: str) -> dict[str, Any] | None: + """Make a request to the NWS API with proper error handling.""" + headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"} + async with httpx.AsyncClient() as client: + try: + response = await client.get(url, headers=headers, timeout=30.0) + response.raise_for_status() + return response.json() + except Exception: + return None + + +def format_alert(feature: dict) -> str: + """Format an alert feature into a readable string.""" + props = feature["properties"] + return f""" + Event: {props.get('event', 'Unknown')} + Area: {props.get('areaDesc', 'Unknown')} + Severity: {props.get('severity', 'Unknown')} + Description: {props.get('description', 'No description available')} + Instructions: {props.get('instruction', 'No specific instructions provided')} + """ + + +@mcp.tool() +async def get_alerts(state: str) -> str: + """Get weather alerts for a US state. + + Args: + state: Two-letter US state code (e.g. CA, NY) + """ + url = f"{NWS_API_BASE}/alerts/active/area/{state}" + data = await make_nws_request(url) + + if not data or "features" not in data: + return "Unable to fetch alerts or no alerts found." + + if not data["features"]: + return "No active alerts for this state." + + alerts = [format_alert(feature) for feature in data["features"]] + return "\n---\n".join(alerts) + + +@mcp.tool() +async def get_forecast(latitude: float, longitude: float) -> str: + """Get weather forecast for a location. + + Args: + latitude: Latitude of the location + longitude: Longitude of the location + """ + # First get the forecast grid endpoint + points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}" + points_data = await make_nws_request(points_url) + + if not points_data: + return "Unable to fetch forecast data for this location." + + # Get the forecast URL from the points response + forecast_url = points_data["properties"]["forecast"] + forecast_data = await make_nws_request(forecast_url) + + if not forecast_data: + return "Unable to fetch detailed forecast." + + # Format the periods into a readable forecast + periods = forecast_data["properties"]["periods"] + forecasts = [] + for period in periods[:5]: # Only show next 5 periods + forecast = f""" + {period['name']}: + Temperature: {period['temperature']}°{period['temperatureUnit']} + Wind: {period['windSpeed']} {period['windDirection']} + Forecast: {period['detailedForecast']} + """ + forecasts.append(forecast) + + return "\n---\n".join(forecasts) + + +if __name__ == "__main__": + # Initialize and run the server + mcp.run(transport="stdio")