Weather-Prediction MCP Server
README.md
# Weather-Prediction MCP Server + Agent Bricks Agent
A weather MCP server built with [FastMCP](https://gofastmcp.com/) that exposes weather-forecast
tools to a Databricks **Agent Bricks** agent, so the agent can answer natural-language weather
questions and make simple predictions/recommendations (e.g. *"Will it rain in Chicago tomorrow?"*,
*"Should I bring a jacket to Austin this weekend?"*).
Built as a homework for **Day 3 (Agent Bricks + Alpaca paper-trading MCP server)**, using that
repo's `mcp_server/` split as the reference pattern: thin `@mcp.tool` functions on top of a separate
adapter module that owns all the HTTP/parsing.
## Live deployment
Two Databricks Apps (mirroring Day 3's server + agent split):
| App | Role | URL |
|-----|------|-----|
| `mcp-weather-server` | MCP server (the 5 tools) | `https://mcp-weather-server-7474650707148987.aws.databricksapps.com` |
| `agent-weather-app` | The agent (chat UI, calls the MCP tools) | `https://agent-weather-app-7474650707148987.aws.databricksapps.com` |
- **MCP endpoint (what the agent connects to):**
`https://mcp-weather-server-7474650707148987.aws.databricksapps.com/mcp`
- Confirmed running from the server logs: `Starting MCP server 'weather-prediction' with transport
'http' on http://0.0.0.0:8000/mcp`. (Opening `/mcp` in a browser returns
`Not Acceptable: Client must accept text/event-stream` — that's expected; only an MCP client can
speak to it.)
- `agent-weather-app` declares `mcp-weather-server` as an app **resource**, and its service
principal has `CAN_USE` on the server, so the agent can call all 5 tools.
## Weather API + auth
**[Open-Meteo](https://open-meteo.com)** — chosen because it needs **no signup, no API key, and no
credit card** (free for non-commercial use, ~10k calls/day). Two key-less endpoints are used:
- **Geocoding API** (`geocoding-api.open-meteo.com`) — turns a city name into latitude/longitude.
- **Forecast API** (`api.open-meteo.com`) — current conditions + daily forecast.
Because there are **no credentials**, there is **no Databricks secret to manage** for this project.
(If you swap in a keyed provider like WeatherAPI.com, fetch the key in `weather_adapter.py` via
`WorkspaceClient().secrets.get_secret()` — the same pattern as Day 3's `alpaca_broker.py` — and
nothing else changes.)
## Architecture
```
Agent Bricks agent --(MCP tool calls, streamable HTTP)--> weather_mcp_server.py
| |
| natural-language weather Q&A | (thin @mcp.tool funcs)
v v
final answer <---------------------------------------- weather_adapter.py
|
| (all HTTP + parsing)
v
Open-Meteo geocoding + forecast APIs
```
- **`weather_mcp_server.py`** — FastMCP server; each tool is a thin wrapper that delegates to the
adapter and returns a clean dict (or `{"status": "error", ...}` on failure).
- **`weather_adapter.py`** — the adapter/broker module (like `alpaca_broker.py`): **all** `requests`
calls and response parsing live here, plus the WMO weather-code → text mapping. No raw `requests`
calls exist inside any `@mcp.tool` function.
## Tools
Minimum three capabilities (current / forecast / prediction), plus two stretch tools:
| Tool | Kind | What it does |
|------|------|--------------|
| `get_current_weather(location)` | current | Temperature, feels-like, humidity, precipitation, wind, conditions for now. |
| `get_forecast(location, days=3)` | forecast | Daily high/low, precip chance & amount, max wind, conditions for the next N days (1–16). |
| `predict_umbrella_needed(location, day_offset=0)` | **prediction** | Derived yes/no: umbrella if precip chance ≥ 40% **or** ≥ 0.1 in expected. Returns the reasoning + thresholds. |
| `get_travel_recommendation(location, day_offset=0)` | **prediction** | Multi-factor packing advice (umbrella / jacket / sunscreen / wind), each with its threshold. |
| `compare_cities_weather(locations, day_offset=0)` | stretch | Side-by-side forecast for several cities + picks the "nicest" via a simple score. |
`location` accepts a city name (`"Chicago"`, `"Austin, TX"`, `"London"`) or a `"lat,lon"` pair
(`"41.8781,-87.6298"`). `day_offset` is `0` = today, `1` = tomorrow, etc.
### Why the prediction tools aren't just passthroughs
`predict_umbrella_needed` and `get_travel_recommendation` apply explicit thresholds (configurable via
env vars in `app.yaml`) to the raw forecast and return the *decision plus the reason*, rather than
echoing the API. Defaults:
| Threshold | Default | Meaning |
|-----------|---------|---------|
| `UMBRELLA_PRECIP_CHANCE_PCT` | 40 | Umbrella if max precip probability ≥ this |
| `UMBRELLA_PRECIP_AMOUNT` | 0.1 in | …or total precip ≥ this |
| `JACKET_TEMP_F` | 55 | Jacket if the day's high ≤ this |
| `SUNSCREEN_TEMP_F` | 75 | Sunscreen if sunny **and** high ≥ this |
| `WIND_ADVISORY_MPH` | 25 | Wind warning if max wind ≥ this |
## Error handling & edge cases (with examples)
Every tool catches errors and returns a structured `{"status": "error", "message": ...}` dict — the
MCP-appropriate analog of an HTTP 4xx — so a bad input or an API outage never reaches the agent as a
stack trace. Concrete cases:
| Input | Result | Where |
|-------|--------|-------|
| Unknown city — `get_current_weather("Xyzzyville")` | `{"status":"error","message":"Could not find a location matching 'Xyzzyville'. Try a 'City, ST' form or a 'lat,lon' pair."}` | adapter raises `ValueError` (`weather_adapter.py:130`), tool catches (`weather_mcp_server.py:86`) |
| Empty location — `get_current_weather("")` | `ValueError("Location is required …")` → clean error dict | `weather_adapter.py:110` |
| Out-of-range `days` — `get_forecast("Chicago", 999)` | clamped to 1..16 | `weather_adapter.py:213` |
| Out-of-range `day_offset` — `predict_umbrella_needed("Chicago", 99)` | `{"status":"error","message":"day_offset 99 is out of range - only N day(s) … (0 = today)."}` | `weather_mcp_server.py:60` |
| Too few cities — `compare_cities_weather(["Austin"])` | `{"status":"error","message":"Provide at least two locations to compare."}` | `weather_mcp_server.py:272` |
| One bad city in a compare | that city goes into an `errors[]` array; the rest still return | `weather_mcp_server.py:302` |
| API / network outage | generic `Exception` caught + `logger.exception(...)`, returns `{"status":"error","message":"Could not fetch …: <e>"}` | e.g. `weather_mcp_server.py:88` |
| `lat,lon` input — `get_current_weather("41.88,-87.63")` | resolved directly, no geocoding call | `weather_adapter.py` (`_LATLON_RE`) |
**Agent behavior on error:** the system prompt tells the agent to relay the error and ask the user
to clarify rather than guess — see the guardrail screenshot (`will it rain?` with no location → the
agent asks for a location and invents nothing).
## Code map (nothing left to inference)
| Requirement | Exact location |
|-------------|----------------|
| FastMCP + streamable-HTTP transport | `weather_mcp_server.py:324` — `mcp.run(transport="http", host="0.0.0.0", port=port)` |
| Tools via `@mcp.tool` (thin wrappers) | `weather_mcp_server.py` lines 69, 93, 117, 186, 252 |
| No raw `requests` in tools — all HTTP in the adapter | `weather_adapter.py`: `_get():95`, `geocode():100`, `get_current():151`, `get_forecast():201`. The server imports `weather_adapter` and never calls `requests`. |
| Docstrings (Args/Returns) | every tool function in `weather_mcp_server.py` |
| Prediction = threshold logic, not a passthrough | constants at `weather_mcp_server.py:47-51`; applied in `predict_umbrella_needed:117` and `get_travel_recommendation:186`; overridable via `app.yaml` |
| Clean error handling | try/except in every tool; adapter raises typed `ValueError` with messages |
| System prompt + guardrails | `agent/system_prompt.md` |
| No secrets | Open-Meteo is key-less; `.env` git-ignored; no `WorkspaceClient().secrets` used |
> This MCP server is **stateless** — no model or DB loaded per request. The only external calls are
> two key-less Open-Meteo endpoints, all confined to `weather_adapter.py`.
## Files
- `weather_mcp_server.py` — FastMCP server exposing the 5 tools
- `weather_adapter.py` — adapter: all Open-Meteo HTTP calls + parsing + WMO code mapping
- `requirements.txt` / `app.yaml` — Databricks App config for the MCP server
- `.env.example` — local dev env template (all optional; no key required)
- `agent/system_prompt.md` — the Agent Bricks system prompt
- `agent/agent_setup.md` — how to register the MCP server + build the agent, and the tool list
## Setup
### 1. Run locally
```bash
pip install -r requirements.txt
python weather_mcp_server.py # serves MCP over streamable HTTP on :8000
```
Sanity-check the adapter without MCP:
```bash
python -c "import json, weather_adapter as w; print(json.dumps(w.get_current('Chicago'), indent=2))"
```
Or point an [MCP Inspector](https://docs.databricks.com/aws/en/agents/mcp-tools/connect-clients) at
`http://localhost:8000` to list and call the tools.
### 2. Deploy as a Databricks App
Same flow as Day 3's `mcp_server/`:
1. Put this folder in a Databricks **Git folder**.
2. **Compute > Apps > Create app > Custom**, name it e.g. `weather-mcp`, and point its source at this
folder (so it picks up `app.yaml`).
3. Deploy, then copy the app URL — you'll register it as an external MCP server next.
No secrets step is needed (Open-Meteo is key-less).
### 3. Register the MCP server + build the agent
See [`agent/agent_setup.md`](agent/agent_setup.md) for the full steps (mirrors Day 3's README):
register the app URL as an external MCP, create an Agent Bricks agent, add the 5 tools, and paste the
system prompt from [`agent/system_prompt.md`](agent/system_prompt.md).
## Demonstration (agent Q&A)
Real tool outputs from the live Open-Meteo API (captured 2026-08-09; your numbers will differ):
**Q1 — "What's the weather like in Chicago right now?"**
> Agent calls `get_current_weather("Chicago")` →
> ```json
> {"location": "Chicago, Illinois, United States", "temperature": 83.9, "feels_like": 90.6,
> "humidity_pct": 68, "wind_speed": 11.2, "conditions": "Overcast", "as_of": "2026-08-09T12:15"}
> ```
> **Answer:** "Right now in Chicago it's about 84°F (feels like 91°F because of the humidity — 68%),
> overcast, with an 11 mph wind."
**Q2 — "Should I bring an umbrella to Seattle tomorrow?"**
> Agent calls `predict_umbrella_needed("Seattle", day_offset=1)` →
> ```json
> {"location": "Seattle, Washington, United States", "date": "2026-08-10", "umbrella_needed": false,
> "precip_chance_pct": 2, "conditions": "Overcast",
> "reason": "No umbrella needed. 2026-08-10 in Seattle... only a 2% chance of precipitation... below the 40% / 0.1 in threshold."}
> ```
> **Answer:** "No need — tomorrow in Seattle is overcast but only a 2% chance of rain, well below the
> 40% umbrella threshold."
**Q3 — "Should I pack a jacket for Seattle this weekend?"**
> Agent calls `get_travel_recommendation("Seattle", day_offset=1)` →
> ```json
> {"location": "Seattle, Washington, United States", "date": "2026-08-10",
> "recommendations": ["No special gear needed - mild conditions expected."],
> "forecast": {"temp_high": 74.7, "temp_low": 55.2, "conditions": "Overcast"}}
> ```
> **Answer:** "You should be fine without a jacket during the day — highs around 75°F. Evenings dip to
> the mid-50s, so a light layer wouldn't hurt after sunset."
**Q4 (stretch) — "Which has nicer weather this weekend: Austin, Seattle, or Denver?"**
> Agent calls `compare_cities_weather(["Austin","Seattle","Denver"], day_offset=1)` → ranks by score
> and reports the `best` city with its high/low, conditions, and precip chance.
## Notes / guardrails
- **Clean errors, not stack traces.** A bad location (`get_current_weather("Xyzzy")`) returns
`{"status": "error", "message": "Could not find a location matching 'Xyzzy'. Try a 'City, ST' form
or a 'lat,lon' pair."}`, and the system prompt tells the agent to ask the user to clarify rather
than guess.
- **No hallucinated weather.** The system prompt requires the agent to base every weather claim on a
tool result and to say so if a tool fails.
- **No secrets committed.** Open-Meteo needs none; `.env` is git-ignored.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues