Skip to main content
Glama
README.md
# Skywise — Weather-Prediction MCP Server + Agent Bricks Agent

A **weather-forecast MCP server** (FastMCP) plus a **Databricks Agent Bricks
agent** that uses it to answer natural-language weather questions and make simple
predictions ("Will it rain in Chicago tomorrow?", "Should I bring a jacket to
Austin this weekend?"). An optional **dashboard** app shows the history of
predictions the agent has made.

Built on the Day-3 reference pattern (an MCP-server app + a separate dashboard
app, thin `@mcp.tool` functions over a separate broker/adapter module), and
reusing the proven NWS + Lakebase code from an earlier weather app. As in the
reference repos, the dashboard Flask app sits at the **repo root** and the MCP
server is the **`mcp_server/`** subfolder.

---

## Weather API + auth used

| Provider | Used for | Auth | Notes |
|---|---|---|---|
| **Open-Meteo** | geocoding, current conditions, multi-day forecast | **none (keyless)** | worldwide; ~10k calls/day non-commercial |
| **NWS (weather.gov)** | active severe-weather alerts | **none (keyless)** | US-only; requires a descriptive `User-Agent` header |

**There is no weather API key anywhere in this project** — both providers are
free and keyless. The *only* secret is the **Lakebase connection URL**, and it is
only used to log predictions for the dashboard (see [Secrets](#secrets)). No
secrets are committed to git; `.env` is gitignored.

---

## Architecture

```
Natural-language question
        │
        ▼
┌─────────────────────┐        registers as external MCP tool
│  Agent Bricks agent  │◄───────────────────────────────────────┐
│  (system_prompt.md)  │                                         │
└─────────┬───────────┘                                          │
          │ calls MCP tools (streamable-HTTP)                    │
          ▼                                                      │
┌──────────────────────────── Databricks App #1 ───────────────┐│
│  mcp_server/weather_mcp_server.py   (FastMCP, thin @mcp.tool) ││
│      ├─ weather_broker.py   → Open-Meteo (current/historical/  ││
│      │                        forecast/air-quality + geocode)  ││
│      ├─ recommendation.py   → threshold logic (the judgment)  ││
│      ├─ nws_broker.py       → NWS active alerts (US)          ││
│      └─ query_log.py ──────────────┐ (best-effort logging)    ││
└────────────────────────────────────┼─────────────────────────┘│
                                      ▼                          │
                              ┌──────────────┐                   │
                              │   Lakebase    │  reads history ──┘
                              │ weather_queries│
                              └──────▲────────┘
                                     │
┌───────────────────── Databricks App #2 (repo root) ──────────┐
│  app.py  (Flask, read-only)                                   │
│      recent predictions history + live conditions lookup       │
└───────────────────────────────────────────────────────────────┘
```

**Design rule (from the assignment):** tools are thin, brokers are fat. No raw
`requests` calls live inside any `@mcp.tool` function — all HTTP + parsing is in
`weather_broker.py` / `nws_broker.py`.

---

## MCP tools

| Tool | Kind | What it does |
|---|---|---|
| `get_current_weather(location, units)` | **required** | Current temp, feels-like, humidity, wind, conditions. |
| `get_historical_weather(location, date, units)` | **required** | **Actual observed** weather for a past date (Open-Meteo ERA5 archive) — not a forecast. Rejects future/malformed dates. |
| `get_travel_recommendation(location, date, units)` | **required** | **Derived judgment** — applies documented thresholds (umbrella if rain ≥ 40%, jacket/coat by low temp, sunscreen if UV ≥ 6, heat/wind cautions, air-quality caution) and returns the reasoning. Logs to Lakebase. |
| `get_forecast(location, days, units)` | additional | N-day (1–16) daily highs/lows, precip chance, UV, wind. Also satisfies the assignment's named forecast capability. |
| `get_severe_weather_alerts(location)` | stretch | Active NWS watches/warnings for a US location. |
| `get_air_quality(location)` | stretch | Current US AQI + PM2.5/PM10/ozone (Open-Meteo Air Quality API). |
| `compare_cities_weather(locations, units)` | stretch | Current conditions for several cities side by side. |
| `get_current_user()` | utility | End-user identity from the App's `X-Forwarded-User` header (mirrors the tutorial MCP server). |

The "prediction" tool does **more than echo the API**: the threshold logic lives
in [`mcp_server/recommendation.py`](mcp_server/recommendation.py) and each rule
that fires is returned in a `reasons` list so the agent can explain *why*. It
also folds in **air quality** (limit strenuous outdoor activity when US AQI > 100)
as a best-effort signal that never breaks the core forecast advice.

A curated **seed cache** of common US cities (`_SEED_COORDS` in
[`weather_broker.py`](mcp_server/weather_broker.py)) resolves the demo cities
(Chicago, Austin, Oklahoma City, …) with zero network calls, and gracefully
falls through to live geocoding for anything else.

**Error handling:** brokers raise a `WeatherError` with a short message on a bad
location or provider outage; every tool catches it and returns
`{"status": "error", "message": ...}` — a clean sentence the agent can react to,
never a stack trace.

---

## Repository layout

The **dashboard app lives at the repo root** (its `app.py`/`app.yaml` are the
root's), so it deploys by pointing a Databricks App at the repo root — the same
shape as the reference repos (root Flask app + an `mcp_server/` subfolder). The
**MCP server** is the self-contained `mcp_server/` subfolder.

```
skywise-mcp-agent/            ← ROOT = the read-only dashboard app (Databricks App #2)
├── app.py                    ← Flask: predictions history + live conditions
├── app.yaml                  ← dashboard App config (command + env)
├── requirements.txt          ← dashboard deps (also what setup_secrets.py needs)
├── weather_display.py        ← FE-support helpers (geocode/daily/AQI), ported
├── query_log.py              ← (copy) reads recent predictions
├── lakebase.py               ← (copy) Lakebase connection helper
├── templates/index.html      ← "Nordic Cool" UI (ported from weather-intel app)
├── setup_secrets.py          ← one-time: store LAKEBASE_URL secret
├── databricks.yml            ← optional Asset Bundle for CLI deploys
├── .env.example              ← local dev template (no real secrets)
├── .gitignore
├── README.md                 ← you are here
├── agent/
│   ├── system_prompt.md      ← the agent's system prompt (a deliverable)
│   └── demo_transcripts.md   ← fill in with 3+ demo Q&As after deploy
└── mcp_server/               ← Databricks App #1 (the MCP server) — self-contained
    ├── weather_mcp_server.py ← FastMCP entrypoint, thin @mcp.tool funcs
    ├── weather_broker.py     ← Open-Meteo: geocode / current / historical / forecast
    ├── recommendation.py     ← derived judgment (thresholds + reasoning)
    ├── nws_broker.py         ← NWS active alerts (reused, User-Agent-safe)
    ├── query_log.py          ← best-effort prediction logging to Lakebase
    ├── lakebase.py           ← Lakebase connection helper (reused)
    ├── schema_weather_queries.sql ← DDL + app-role grants for the prediction-log table
    ├── test_weather.py       ← runnable live smoke test (✓/✗ per capability)
    ├── app.yaml              ← App config (env only; no key secret)
    └── requirements.txt
```

> The root (dashboard) app and the `mcp_server/` app each carry their own copies
> of the shared Lakebase modules (`lakebase.py`, `query_log.py`) so each deploys
> as a **self-contained** Databricks App — the same convention as the reference
> repos, whose root app is likewise a Flask app beside an `mcp_server/` folder.

### Dashboard UI (ported "Nordic Cool" design)

The dashboard's [`templates/index.html`](templates/index.html) reuses the
polished front-end from the earlier **weather-intelligence** app — its
self-hosted fonts (Fraunces + Hanken Grotesk), scandi palette, **geocode
typeahead**, and **live-conditions panel** (yesterday/today/tomorrow cards + an
AQI badge and EPA scale). The RAG "sync + semantic search" half of that original
page (irrelevant to this assignment) is **replaced** with the **Recent agent
predictions** history read from Lakebase. Two small endpoints in
[`app.py`](app.py) — `/weather/geocode` and `/weather/conditions` —
feed the reused JS, backed by [`weather_display.py`](weather_display.py)
(a trimmed port of that app's `weather_client.py` display functions).

Two Skywise touches layered on top of the ported design (palette-native, no new
colors): a **°F/°C segmented toggle** that re-fetches the current city's
conditions, and a **data-derived insight line** ("📈 4°F warmer than yesterday ·
rain rising to 47% tomorrow") computed only from the three days already on screen
— so it never states anything the panel doesn't show.

---

## Setup

### 1. (Optional) Lakebase for prediction logging

Only needed if you want the dashboard / prediction history. Skip if you just want
the required MCP tools + agent.

1. Create a Lakebase instance with a **native-password role** and copy its
   connection URL (`postgresql://role:password@host:5432/databricks_postgres?sslmode=require`).
2. Create the table **and grant the app role access**:
   ```bash
   psql "$LAKEBASE_URL" -f mcp_server/schema_weather_queries.sql
   ```
   (or paste the file into a Databricks SQL editor connected to the instance).
   This creates `weather.weather_queries` in the same `weather` schema the
   weather-intel app uses, and grants schema/table/sequence privileges to the
   Lakebase role **`student`** — the same role the day-1 tutorial and weather-intel
   app grant to. **Review placeholder:** if your Lakebase app role isn't
   `student`, edit the `GRANT ... TO student` lines at the bottom of that file
   first (otherwise the app hits "permission denied" on the first insert).

### 2. Secrets

The only secret is the Lakebase URL. Store it once by running the root script
(install its one dependency first — the root `requirements.txt` covers it):

```bash
pip install -r requirements.txt
python setup_secrets.py
```

This uses scope `database-day2` and key `lakebase-url` — the **same Lakebase
secret you already created for the weather-intel app** — so no new secret is
needed (matching the `LAKEBASE_SECRET_SCOPE` / `LAKEBASE_SECRET_KEY` in both
`app.yaml` files). If that secret already exists, you can skip this step.

### 3. NWS User-Agent

NWS returns **HTTP 403** without a descriptive `User-Agent`. Set your own contact
in `mcp_server/app.yaml` (`WEATHER_USER_AGENT`, e.g.
`"(skywise-mcp-agent, you@example.com)"`). It is a courtesy contact, not a
secret.

### 4. Run locally (optional)

```bash
cd mcp_server
pip install -r requirements.txt
python weather_mcp_server.py       # serves MCP over HTTP on :8000
```

```bash
# dashboard app — run from the repo root (this is where app.py lives)
pip install -r requirements.txt
python app.py                      # serves the dashboard on :8001
```

### 5. Deploy the two Databricks Apps — mind the source path

The two apps deploy from **different directories**. Pointing an App at the wrong
directory is the #1 deploy failure: the repo root has an `app.yaml` (the
dashboard), but the `mcp_server/` code has its own `app.yaml`, so **each App must
point at its own directory**.

| App | Point the Databricks App source at | Notes |
|---|---|---|
| Dashboard (Databricks App #2) | **the repo root** | root `app.py` + `app.yaml` |
| MCP server (Databricks App #1) | **`mcp_server/` subfolder** | name it **`mcp-skywise-weather`** |

Via the workspace UI (no CLI required):

1. Create a **Git folder** pointing at this repo.
2. **Compute → Apps → Create app** for the **MCP server**: set its source code
   path to the **`mcp_server/` subfolder** (not the repo root).
   - **⚠️ Name it with an `mcp-` prefix** — e.g. `mcp-skywise-weather`. Databricks
     only recognizes an App as an MCP server (for AI Playground / Agent Bricks
     discovery) when its name starts with `mcp-` (parallel to the tutorial's
     `mcp-trading-server`).
3. Create a **second** app for the **dashboard**: set its source code path to
   **the repo root** (any name — the dashboard is not an MCP server). Because the
   root has its own `app.yaml`, the container runs the Flask app, not
   `setup_secrets.py`. Open each app's URL and confirm it's serving.

---

## Register the MCP server as an external MCP + build the agent

> Written from Databricks' current MCP docs
> ([host a custom MCP](https://docs.databricks.com/aws/en/agents/mcp-tools/custom-mcp),
> [MCPs and agent tools](https://docs.databricks.com/aws/en/agents/mcp-tools)).
> UI labels move around — confirm against your workspace.

**What to swap vs. the tutorial's `mcp-trading-server`:** a new app name
(`mcp-skywise-weather`), the skywise `mcp_server/` source folder, and **no API-key
secret** (Open-Meteo + NWS are keyless). The Lakebase secret is the *same*
`database-day2/lakebase-url` you already created — nothing new to store.

1. **Get the MCP endpoint.** After the `mcp-skywise-weather` app deploys, its MCP
   URL is **`https://<app-url>/mcp`** (FastMCP's streamable-HTTP endpoint — the
   server already runs `transport="http"`, so no code change is needed).
2. **Grant access.** On the app → **Permissions**, grant yourself (and the
   agent's principal) access. Databricks governs MCP access through **Apps
   permissions**, not per-tool grants. (Governance also appears under
   **AI Gateway → MCPs**.)
3. **Attach it to an agent in AI Playground.** Open **AI Playground**, pick an
   LLM endpoint, open **Tools**, and add the `mcp-skywise-weather` MCP server.
   Its `@mcp.tool` functions appear for selection: `get_current_weather`,
   `get_historical_weather`, `get_travel_recommendation`, `get_forecast`,
   `get_severe_weather_alerts`, `get_air_quality`, `compare_cities_weather`,
   `get_current_user`.
4. **Set the instructions.** Paste [`agent/system_prompt.md`](agent/system_prompt.md)
   as the agent's system prompt.
5. **Test, then export.** Run the prompts in
   [`agent/demo_transcripts.md`](agent/demo_transcripts.md), capture the tool
   calls + answers for your submission, then **Export / Create agent** from the
   Playground.

---

## Demonstrating the agent (required)

Fill in [`agent/demo_transcripts.md`](agent/demo_transcripts.md) with at least 3
natural-language questions, the tool calls the agent made, and its final answers
(screenshots encouraged). Suggested prompts:

1. "Will it rain in Chicago tomorrow — should I take an umbrella?"
2. "What's the weather looking like in Austin this weekend?"
3. "Are there any severe weather warnings for Oklahoma City right now?"

---

## Assignment Requirements Checklist

### Required MCP Tools (minimum 3) ✅
- ✅ `get_current_weather(location, units)` - current conditions
- ✅ `get_forecast(location, days, units)` - multi-day forecast
- ✅ `get_travel_recommendation(location, date, units)` - **derived judgment with documented thresholds** (not a passthrough)

### Stretch Tools (5 bonus tools) ✅
- ✅ `get_historical_weather` - past weather lookup
- ✅ `get_severe_weather_alerts` - NWS alerts
- ✅ `get_air_quality` - AQI data
- ✅ `compare_cities_weather` - multi-city comparison
- ✅ `get_current_user` - identity utility

### Technical Requirements ✅
- ✅ **FastMCP** with `@mcp.tool` decorators
- ✅ **Separate adapter modules** - `weather_broker.py`, `recommendation.py`, `nws_broker.py` (no HTTP in tool functions)
- ✅ **Secrets properly stored** - Lakebase URL in Databricks secrets, no API keys needed (Open-Meteo & NWS are keyless)
- ✅ **requirements.txt & app.yaml** - present for both MCP server and dashboard apps
- ✅ **Deployed as Databricks App** - `mcp-skywise-weather` running with `mcp-` prefix
- ✅ **Clear docstrings** - Args/Returns documented for all tools
- ✅ **Error handling** - returns clean error messages, not stack traces
- ✅ **Prediction tool logic** - applies explicit thresholds in `recommendation.py` with reasoning

### Agent Requirements ✅
- ✅ **Agent Bricks agent configured** - registered in AI Playground with MCP server
- ✅ **System prompt** - [`agent/system_prompt.md`](agent/system_prompt.md) with tool selection rules and anti-hallucination guardrails
- ✅ **Demo transcripts** - [`agent/demo_transcripts.md`](agent/demo_transcripts.md) with 4+ examples and screenshots

### Documentation ✅
- ✅ **README.md** - architecture diagram, tool list, setup steps, API documentation
- ✅ **Weather API documented** - Open-Meteo (keyless) + NWS (keyless with User-Agent)
- ✅ **No secrets in git** - `.gitignore` configured, `.env.example` provided

### Bonus Features ✨
- ✅ **Dashboard app** - real-time prediction logging to Lakebase, "Nordic Cool" UI
- ✅ **8 tools total** - 3 required + 5 stretch
- ✅ **Comprehensive error handling** - graceful degradation for API outages
- ✅ **Air quality integration** - enriches recommendations with AQI data

---

## Notes / reuse

- **NWS handling** (`nws_broker.py`) reuses the mandatory-`User-Agent` +
  4-decimal-coordinate-rounding pattern proven in an earlier weather app, so the
  common NWS 403/redirect gotchas are handled up front.
- **Air quality** (`get_air_quality`) reuses the Open-Meteo AQI adapter +
  EPA-category logic from that same weather app.
- **Seed cache** (`_SEED_COORDS`) reuses its curated city→coordinate map.
- **Dashboard UI** reuses that app's "Nordic Cool" front-end (fonts, palette,
  geocode typeahead, live-conditions + AQI panel); its display helpers are
  ported into `weather_display.py` (at the repo root).
- **Lakebase helper** (`lakebase.py`) is the standard single-`LAKEBASE_URL`
  pattern from the reference apps — no token refresh needed.
- **Test script** (`test_weather.py`) mirrors the reference's `test_watchlist.py`
  style — a runnable ✓/✗ smoke test over the live free APIs, no secrets needed.
- Prediction logging is **best-effort**: if Lakebase is down or unconfigured, the
  weather answer is still returned and only the dashboard history is affected.
```