Skip to main content
Glama
skandru

Waterboards MCP Server

by skandru
README.md
# Waterboards MCP Server

An [MCP](https://modelcontextprotocol.io) server that answers California **drinking water** questions — public water systems and their SAFER risk/violation status, source-water chemistry, and school lead results, by ZIP code or water system ID — from curated **Databricks Gold views** over State Water Board open data (data.ca.gov). Built with [FastMCP](https://github.com/jlowin/fastmcp) and [uv](https://docs.astral.sh/uv/); deployed as a **Databricks App**.

Design decisions and the review of the original plan: [req/mcp_design_review.md](req/mcp_design_review.md) (local, git-ignored).

## Architecture

```
data.ca.gov drinking water files (+ Census ZCTA centroids)
   └─ sql/drinking_water/  Bronze → Silver → Gold   (run in a Databricks SQL warehouse)
        └─ users.suresh_kandru.v_* views  (the contract in this README)
             └─ Databricks SQL warehouse
                  └─ this server (FastMCP, Databricks Apps, /mcp over HTTP)
                       └─ MCP client / agent
```

The server is read-only and queries **only** `<catalog>.<schema>.v_*` Gold views (plus `information_schema` for dataset discovery). Every caller-supplied value is bound as a named SQL parameter.

## Data pipeline

`scripts/download_drinking_water_data.py` downloads the source files. Upload them (plus the Census ZCTA gazetteer as `zcta_centroids.txt`) to `/Volumes/users/suresh_kandru/raw/drinking_water/current/`, then run in order:

| Script | Layer | Output |
|---|---|---|
| `sql/drinking_water/00_setup.sql` | setup | volume, `silver_clean_str` helper |
| `sql/drinking_water/01_bronze.sql` | Bronze | raw all-string tables `bronze_dw_*`, `bronze_zcta_centroids` |
| `sql/drinking_water/02_silver.sql` | Silver | typed `dw_systems`, `dw_safer`, `dw_lead_schools`, `dw_source_chemistry(_latest)`, `dw_wells`, and 50 km ZIP pair tables |
| `sql/drinking_water/03_gold.sql` | Gold | the `v_*` views below |
| `sql/drinking_water/04_tap_water.sql` | Bronze/Silver/Gold | DDW SDWIS tap-water results and ZIP-to-service-area lookup → `v_tap_water_for_zip`. Inputs `tap_water_results.csv.gz` and `zip_service_areas.csv` are built by `scripts/build_tap_water_results.py` and `scripts/build_zip_service_areas.py` |

Silver turns `NULL`/`NaN`/`N/A` into SQL NULL, parses dates, keeps non-detects explicit (`is_non_detect`, `result_numeric` NULL, `reporting_limit`), and drops street addresses. Systems are located by SAFER coordinates, wells by their own coordinates, and schools by the ZIP parsed from their address.

## Tools

| Tool | Gold view | Purpose |
|---|---|---|
| `waterboards_list_datasets` | `information_schema.tables` | What datasets are available |
| `waterboards_find_water_systems_near_zip` | `v_water_systems_near_zip` | Water systems near a ZIP, optionally only failing/at-risk/violating |
| `waterboards_get_water_system` | `v_water_systems` | Profile, SAFER status, violations for one `pws_id` |
| `waterboards_get_source_chemistry_near_zip` | `v_source_chemistry_near_zip` | Latest well chemistry near a ZIP, filter by chemical |
| `waterboards_get_system_source_chemistry` | `v_source_chemistry_latest` | Latest well chemistry for one `pws_id` |
| `waterboards_get_school_lead_near_zip` | `v_school_lead_near_zip` | School lead sampling near a ZIP |
| `waterboards_get_tap_water_for_zip` | `v_tap_water_for_zip` | Latest tap/treated-water sample results for the systems serving a ZIP |

All return `{results, count, offset, has_more, next_offset, source, retrieved_at}`, or `{error, hint}`. Empty results include an actionable `hint`.

### Gold view contract

The views must exist with at least these columns (the server does `SELECT *` then filters/orders on them):

| View | Required columns |
|---|---|
| `v_water_systems` | `pws_id, system_name, safer_status, is_failing, has_primary_mcl_violation, has_ecoli_violation, has_treatment_technique_violation, data_as_of, source_url` + profile/risk columns |
| `v_water_systems_near_zip` | `zip, distance_km` + all `v_water_systems` columns |
| `v_source_chemistry_latest` | `pws_id, well_id, chemical_name, is_non_detect, result_numeric, units, reporting_limit, last_observed, source_url` |
| `v_source_chemistry_near_zip` | `zip, distance_km` + all `v_source_chemistry_latest` columns |
| `v_tap_water_for_zip` | `zip, pct_of_zip_covered, pws_id, system_name, water_stage, analyte_name, result_numeric, units, is_non_detect, mcl, exceeds_mcl, last_observed, source_url` |
| `v_school_lead_near_zip` | `zip, distance_km, school_name, action_level_exceeded, result_numeric, last_observed, source_url` + school/system columns |

`zip` + `distance_km` are precomputed (pairs up to 50 km), so the server filters `distance_km <= :radius_km` rather than doing geospatial math per request. Keep PII (street addresses, phone numbers) out of Gold.
## Run locally

```bash
cp .env.example .env          # set DATABRICKS_SQL_WAREHOUSE_ID
databricks auth login --host https://<workspace>.cloud.databricks.com
uv sync --group dev
uv run python main.py         # stdio
```

Local auth uses the Databricks SDK's unified auth (CLI profile via `DATABRICKS_CONFIG_PROFILE`, or `DATABRICKS_HOST` + `DATABRICKS_TOKEN`). Connect a client with an entry like:

```json
{
  "mcpServers": {
    "waterboards": {
      "command": "uv",
      "args": ["run", "waterboards-mcp"],
      "cwd": "/absolute/path/to/this/repo"
    }
  }
}
```

HTTP mode (what Databricks Apps runs): `MCP_TRANSPORT=streamable-http uv run python main.py`, then `curl localhost:8000/health`.

## Verify

```bash
uv run pytest tests/ -v
uv run ruff check . && uv run ruff format --check .
```

Tests mock the Databricks SDK; no workspace is needed.

## Add a tool

One tool per file in `src/waterboards_mcp/tools/`, exposing `register(mcp)`; add it to `register_tools` in `tools/__init__.py`. Use `ZipCode`/`RadiusKm`/`PaginationParams` from `models.py` for validated inputs and call `warehouse.query_gold(...)` with static `where`/`order_by` SQL and `Param`s for values. Document source, refresh cadence, and result shape in the docstring — the model reads it.

## Deploy

See [deploy/databricks/README.md](deploy/databricks/README.md).

## Evaluate

See [eval/README.md](eval/README.md) and the `mcp-eval` skill.

## License

See [LICENSE](LICENSE).

TDQS

A4.4/5.0

Scored across 7 tools

Disambiguation4/5

Tools are largely distinct by resource and input key (ZIP vs pws_id), so most selections are clear. The two source-chemistry tools could be briefly confused because they share the same output schema, but the descriptions and name qualifiers ('near_zip' vs 'system') clarify scope.

Naming Consistency5/5

All names use a consistent waterboards_ prefix and snake_case verb_noun pattern. Verb choices (list/find/get) are appropriate and no mixed conventions appear.

Tool Count5/5

7 tools is well-scoped for a read-only water-data server. Each tool covers a clear data domain or lookup mode, with no redundant or filler tools.

Completeness4/5

The surface covers dataset discovery, water-system lookup, source chemistry by geography and system, school lead, and ZIP tap water. Minor gaps remain, such as direct tap-water or school-lead lookup by pws_id/system and broader search by county or system name, but core workflows are supported.

Maintenance

ActivityMaintained
ResponsivenessNo issues