Skip to main content
Glama
pranavyadav11

location-risk-mcp

README.md
# location-risk-mcp

> **Independent prototype, not a Precisely product and not affiliated with Precisely.**
> Built by Pranav Yadav as a personal project. It calls Precisely's hosted MCP server with the
> user's own trial credentials. Decision support only: it does not make or recommend underwriting
> decisions.

An MCP server that gives an AI agent what it needs to pre-screen a property for a homeowners policy.
It provides three tools, each shaped around a step in the agent's task:

```
standardise_address  ──►  enrich_location  ──►  summarise_risk_context
  messy text in            flood, wildfire,        overall level, flags ranked
  one location_id out      quake, crime, fire      by severity, evidence for
  (or candidates/none)     station, building       each flag, follow-ups, gaps
```

In live mode it is a **task layer on top of Precisely's hosted MCP server**. Precisely exposes its data services as hundreds of actions behind three generic tools (`precisely_actions_search` / `_describe` / `_execute`). That's ideal for discovery, but an agent running an underwriting pre-screen would have to find, sequence and interpret seven actions every time. This server does that once, deterministically:

| Step | Precisely actions used |
|---|---|
| `standardise_address` | `geo_addressing.geocode`: standardised address, building-level point, PreciselyID |
| `enrich_location` | `risk.flood`, `risk.wildfire`, `risk.earthquake`, `demographics.crime_index`, `risk.fire_station`, `property.attributes`. Called in parallel by PreciselyID, so every block describes the same matched property. |
| `summarise_risk_context` | none: versioned rules over the enrichment (`risk.py`) |

## Why it is shaped this way

- **Task-shaped tools, not endpoint wrappers.** The agent passes a `location_id` from step to step. It never carries coordinates or IDs between calls, and never has to know which of Precisely's actions holds which field.
- **Every value is citable.** Outputs are typed (each tool publishes an MCP `outputSchema`). Each data block carries its own `source`: `data_source` (`fixture` or `live`), the Precisely action it came from, `retrieved_at` and the dataset `vintage`. Each risk flag lists the exact fields it rests on.
- **Gaps are reported, never filled.** Provider placeholders (`-9999`, empty `{value: null}` objects, a crime index of `0` with no category) become gaps, not numbers. One failing action becomes a gap for that block only. If a core peril (flood or wildfire) can't be assessed, the summary says `insufficient_data` instead of `low`.
- **Corrections are surfaced.** If the geocoder changed something the user typed (e.g. a different ZIP), `standardise_address` returns a warning to confirm with the applicant.
- **Ambiguity goes back to the user.** Close-scoring candidates return `status: ambiguous`, and the server instructions tell the agent to ask, not pick.
- **Data minimisation.** `property.attributes` also returns owner names, tax mailing addresses and sale history. None of that is passed to the agent; only building attributes are.
- **The risk rules are deterministic and versioned.** The same input always gives the same summary, so it can be tested and audited.
- **Read-only by declaration.** All tools set `readOnlyHint` and `idempotentHint`. `openWorldHint` is true only in live mode.

## Quick start (fixture mode, no account needed)

```bash
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest -q          # 24 tests, no network
.venv/bin/python scripts/demo.py       # full flow through a real MCP client over stdio
```

Fixture mode serves bundled **synthetic** data (`src/location_risk_mcp/fixtures/locations.json`). Every address and value in it is invented, and every response is stamped `data_source: "fixture"`.

## Live mode (your own Precisely trial)

1. Create `.env` from the template, then add the API key and secret from your Precisely workspace. `.env` is git-ignored.
   ```bash
   cp .env.example .env
   ```
2. Set `PRECISELY_MODE=live`. The server authenticates to `https://api.cloud.precisely.com/mcp` (us-east-1; set `PRECISELY_MCP_URL` for other regions) with the header Precisely documents: `Authorization: Apikey base64(key:secret)`.
3. Check the connection and run one address end to end:
   ```bash
   .venv/bin/python scripts/doctor.py --probe "1700 District Ave, Burlington, MA 01803"
   ```
4. Record a live transcript:
   ```bash
   .venv/bin/python scripts/demo.py "<test address>" ...
   ```

Use test or sample addresses only, never real customer data.

### Web UI (for demos)

```bash
.venv/bin/python -m location_risk_mcp.web      # then open http://127.0.0.1:8765
```

A single page: pick a country, enter an address, watch the three tool calls run with timings, and see the verdict, flags with their evidence chips, warnings, gaps and the raw MCP outputs. The page is **just another MCP client**: it calls the same three tools an agent would, so it shows exactly what an agent gets. It follows `PRECISELY_MODE` from `.env`, shows a FIXTURE or LIVE badge, and listens on localhost only.

### Connect it to an MCP client

Claude Code:

```bash
claude mcp add location-risk -- /ABSOLUTE/PATH/location-risk-mcp/.venv/bin/python -m location_risk_mcp.server
```

Claude Desktop (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "location-risk": {
      "command": "/ABSOLUTE/PATH/location-risk-mcp/.venv/bin/python",
      "args": ["-m", "location_risk_mcp.server"]
    }
  }
}
```

The server reads `.env` from the project folder. It also exposes an `underwriting_prescreen` prompt and, in fixture mode, a `fixtures://sample-addresses` resource. A matching Claude Skill is in [`skills/underwriting-prescreen`](skills/underwriting-prescreen/SKILL.md).

## Demo transcript (live mode)

Recorded on 2026-11-01 against Precisely's hosted MCP server, using addresses from Precisely's own documentation examples. Full transcript: [`demo/transcript.live.md`](demo/transcript.live.md). A synthetic-data run is in [`demo/transcript.fixture.md`](demo/transcript.fixture.md).

**`1700 District Ave, Burlington, MA 01803`** → 1700 DISTRICT AVE, BURLINGTON, MA 01803 (building-level point, score 100)
overall **moderate**
- **MODERATE** flood: zone X, but 204 ft from a 100-year flood zone. Evidence: `flood.zone`='X', `flood.distance_to_100yr_zone_ft`=204. Next: check site elevation relative to the nearby flood zone. *A zone-only check would have called this low.*
- wildfire (Minimal), earthquake (site class C), fire response (1.55 road miles, about 10.6 min) and building age (1979): low.
- Gaps (reported, not filled): `crime.composite_index` (not populated for this block), `earthquake.nearest_fault_miles`.

**`2755 Milwaukee St, Denver, CO 80238`** → 2755 N MILWAUKEE ST, DENVER, CO 80205 (score 86)
> ⚠ ZIP corrected: input 80238, matched 80205

overall **elevated**
- **ELEVATED** theft_and_crime: crime index 174. **MODERATE** earthquake: nearest fault 12.75 mi. **MODERATE** building_age: built 1927.

**`Near the old mill`** → **no_match**: the agent stops and says so.

## Layout

```
src/location_risk_mcp/
  server.py                  MCP server: tools, prompt, resource, agent instructions
  web.py, static/index.html  local demo UI (an MCP client of server.py)
  risk.py                    deterministic, versioned risk rules
  models.py                  typed outputs (become each tool's outputSchema)
  config.py                  settings from env/.env; never exposes secrets
  providers/precisely_mcp.py client of the hosted Precisely MCP server + response mapping
  providers/fixture.py       synthetic data, no network
  fixtures/locations.json
skills/underwriting-prescreen/SKILL.md
scripts/demo.py              end-to-end run through a stdio MCP client
scripts/doctor.py            config + connection check, live probe, action search
tests/                       server (via MCP client), rules, live mapping (canned responses)
```

## Limits

- Risk thresholds are illustrative, not any insurer's underwriting guidelines.
- Risk context is US-only: every Precisely risk and property action used here covers US addresses only. Other countries (e.g. `IND`) are standardised and geocoded live, and the risk blocks are skipped and reported as gaps, so the verdict is `insufficient_data`, never a guess.
- One MCP session per tool call; a hosted version would keep a pooled session, a shared cache keyed by `location_id`, and per-tenant auth on its own endpoint.
- Precisely's hosted MCP server is in Preview, so action names and response shapes may change. The mapping turns anything unexpected into gaps, and `doctor.py --probe` shows them.

## License

MIT. See [LICENSE](LICENSE).