Files
hope-voice-api/agent/weather_mcp.py
T
Shane c620867854 fix: disable preemptive_generation for tool call compatibility; add get_time tool
- preemptive_generation was incompatible with MCP tool calls: it starts
  generating before the turn finalizes, breaking the tool execution loop
  (agent said 'let me check' then hung forever)
- Add get_time(location?) to weather_mcp.py for date/time queries
- Update system prompt with Weather & Time section
2026-08-22 16:13:06 -04:00

128 lines
4.3 KiB
Python

"""Weather MCP server — exposes a get_weather tool backed by wttr.in.
Runs over stdio inside the voice container. The agent attaches it via
MCPServerStdio, so the LLM can call get_weather during a conversation to
report current conditions and today's forecast for any location.
Backed by wttr.in (no API key needed):
- GET https://wttr.in/{location}?format=j1 -> full JSON weather report
The tool returns a short, conversational summary suitable for speech
(1-2 sentences), not raw data.
"""
from __future__ import annotations
import os
from datetime import datetime, timezone
import httpx
from mcp.server.fastmcp import FastMCP
WTTR_BASE = os.environ.get("WTTR_BASE", "https://wttr.in").rstrip("/")
mcp = FastMCP("weather")
def _summarize(data: dict, location: str) -> str:
"""Build a short spoken-style weather summary from wttr.in j1 JSON."""
current = data.get("current_condition", [{}])[0] or {}
area = data.get("nearest_area", [{}])[0] or {}
city = (area.get("areaName", [{}])[0].get("value") or location).strip()
temp_c = current.get("temp_C")
feels_c = current.get("FeelsLikeC")
desc = (current.get("weatherDesc", [{}])[0].get("value") or "").strip().lower()
astronomy = data.get("astronomy", [{}])[0] or {}
sunrise = astronomy.get("sunrise", "")
sunset = astronomy.get("sunset", "")
today = (data.get("weather") or [None])[0] or {}
high_c = today.get("maxtempC")
min_c = today.get("mintempC")
parts: list[str] = []
if temp_c is not None:
lead = f"It's {temp_c} degrees and {desc or 'clear'} in {city}"
if feels_c is not None and feels_c != temp_c:
lead += f", feels like {feels_c}"
parts.append(lead + ".")
if high_c is not None and min_c is not None:
parts.append(f"Today's high is {high_c} and the low is {min_c}.")
if sunrise and sunset:
parts.append(f"Sunrise was at {sunrise} and sunset is at {sunset}.")
return " ".join(parts) or f"I have no detailed weather for {city} right now."
@mcp.tool()
async def get_weather(location: str) -> str:
"""Get the current weather and today's forecast for a location.
Use this when the user asks about current conditions, temperature,
or the forecast for a place. Returns a short conversational summary
suitable for reading aloud (1-2 sentences).
"""
try:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.get(
f"{WTTR_BASE}/{location}?format=j1",
headers={"User-Agent": "curl/8.0"},
)
resp.raise_for_status()
data = resp.json()
except Exception: # noqa: BLE001
return "I couldn't get the weather for that location"
try:
summary = _summarize(data, location)
except Exception: # noqa: BLE001
return "I couldn't get the weather for that location"
return summary
@mcp.tool()
def get_time(location: str = "") -> str:
"""Get the current date and time.
If a location is provided, returns the local time there (approximated
via wttr.in's timezone data). Otherwise returns UTC time.
Use this when the user asks what time or day it is.
"""
if not location:
now = datetime.now(timezone.utc)
return f"It's {now.strftime('%I:%M %p')} on {now.strftime('%A, %B %d')}, UTC."
try:
async def _fetch():
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(
f"{WTTR_BASE}/{location}?format=j1",
headers={"User-Agent": "curl/8.0"},
)
resp.raise_for_status()
return resp.json()
import asyncio
data = asyncio.get_event_loop().run_until_complete(_fetch())
current = data.get("current_condition", [{}])[0] or {}
tz_offset = current.get("localObsDateTime", "")
area = data.get("nearest_area", [{}])[0] or {}
city = (area.get("areaName", [{}])[0].get("value") or location).strip()
if tz_offset:
return f"It's {tz_offset.split(' ')[1][:5]} on {tz_offset.split(' ')[0]} in {city}."
except Exception: # noqa: BLE001
pass
now = datetime.now(timezone.utc)
return f"It's {now.strftime('%I:%M %p')} on {now.strftime('%A, %B %d')}, UTC."
if __name__ == "__main__":
mcp.run(transport="stdio")