- 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
128 lines
4.3 KiB
Python
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")
|