Skip to main content
Glama
README.md
# GeoAVM MCP

Geospatial automated valuation model (AVM) server over the
[Model Context Protocol](https://modelcontextprotocol.io). Four MCP tools
(`health`, `distance_km`, `select_comps`, `value_parcel`) and a matching CLI,
with JSON in/out for scripting.

## Status

Stages 1–8 complete, documentation in progress (v0.1.0).

- **56 tests, 99% statement coverage** of `src/`, enforced at a 95% floor in CI
  on Python 3.10–3.14, plus a wheel build and installed-wheel smoke test.
- **No live data providers.** Comps come from a JSON file you supply, with
  `distance_km` already measured. The `.env.example` geocoder keys are reserved,
  not read — see [Reality check](#reality-check).
- **Valuation is transparent but narrow**: inverse-distance weighting with no
  time or attribute adjustments. Read
  [docs/AVM_METHOD.md](docs/AVM_METHOD.md) before trusting a number.

## Install

```bash
uv venv && uv pip install -e ".[dev]"      # or: python -m venv .venv && .venv\Scripts\activate
```

Two entry points, same CLI: `geoavm` and `geoavm-mcp`.

## MCP client setup

stdio is the default and works everywhere. Install the package into an
environment your client can reach, then point the client at the console script.

Claude Desktop (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "geoavm": { "command": "geoavm-mcp", "args": ["serve"] }
  }
}
```

opencode (`opencode.json`):

```json
{
  "mcp": {
    "geoavm": { "type": "local", "command": ["geoavm-mcp", "serve"], "enabled": true }
  }
}
```

If the script is not on the client's `PATH`, use the interpreter instead:

```json
{ "command": "/path/to/.venv/bin/python", "args": ["-m", "geoavm_mcp", "serve"] }
```

HTTP instead of stdio:

```bash
geoavm serve --transport streamable-http --host 127.0.0.1 --port 8000
# endpoint: http://127.0.0.1:8000/mcp
```

`--transport sse` serves `/sse` for older clients. Both bind `127.0.0.1:8000`
by default (`GEOAVM_HOST` / `GEOAVM_PORT` override). There is no auth layer —
do not expose the HTTP transports to an untrusted network.

## CLI

```bash
geoavm serve                      # stdio MCP server (default)
geoavm info                       # version + defaults
geoavm distance "30.2504,-97.7601" "30.2672,-97.7431"
```

```
2.4811 km
```

Comps: `query.json` is a `CompsQuery`, `candidates.json` a `ComparableSale[]`.

```bash
geoavm comps query.json candidates.json            # table
geoavm comps query.json candidates.json --json     # machine-readable
```

```
                         Comps (3 of 4)
+--------------------------------------------------------------+
| apn | sale_date  | distance_km | sale_price | adjusted_price |
|-----+------------+-------------+------------+----------------|
| A-1 | 2025-11-04 | 0.420       | 715,000    | -              |
| A-2 | 2026-02-19 | 1.150       | 830,000    | -              |
| A-3 | 2025-08-27 | 1.620       | 655,000    | -              |
+--------------------------------------------------------------+
```

Valuation: `parcel.json` is a `Parcel`.

```bash
geoavm value parcel.json candidates.json --currency USD
geoavm value parcel.json candidates.json --json
```

```
         AVM 0205-0004-0031
+----------------------------------+
| Key          | Value             |
|--------------+-------------------|
| estimate     | 829,959 USD       |
| 90% interval | 662,717 - 997,202 |
| confidence   | 0.70              |
| method       | idw_ppsf          |
| comps used   | 4                 |
+----------------------------------+
```

Either file argument may be `-` to read from stdin, so a pipeline works:

```bash
curl -s https://example.com/comps.json | geoavm comps query.json -
```

Invalid input exits non-zero with the pydantic message; missing files and bad
JSON are reported by path. `python -m geoavm_mcp` is equivalent to `geoavm`.

Example payloads for both commands are in
[docs/TOOLS.md](docs/TOOLS.md).

## Tools

| Tool | Purpose |
| --- | --- |
| `health` | server name, version, timestamp |
| `distance_km` | great-circle distance between two WGS84 points |
| `select_comps` | filter + rank comps for a `CompsQuery` (nearest first, capped) |
| `value_parcel` | AVM estimate for a `Parcel` from comps: point value, 90% interval, confidence, warnings |

Full signatures, field tables, JSON examples, and gotchas:
[docs/TOOLS.md](docs/TOOLS.md).

## How it values

`select_comps` filters by radius/type/date/area and sorts by distance (newest
breaks ties); `value_parcel` weights the comps by `1 / (distance_km + 0.25)`,
scales $/sqft to the subject when areas are known, and reports a weighted 90%
interval plus data-quality warnings. No time adjustment, no attribute
adjustment, no outlier rejection — the details and the caveats are in
[docs/AVM_METHOD.md](docs/AVM_METHOD.md).

## Reality check

Honest limits, in the order they bite:

1. **You supply the comps and their `distance_km`.** Nothing geocodes, fetches
   MLS data, or measures subject-to-comp distance for you. `select_comps` trusts
   the field.
2. **No market-time adjustment.** Recent and stale sales weigh the same.
3. **The interval is a normal approximation**, not a calibrated confidence
   interval. Treat `confidence` as a heuristic.
4. **Coordinates are metadata.** A parcel without `location` still values.

## Layout

```
src/geoavm_mcp/
  __init__.py      # public surface + __version__
  types.py         # pydantic models (Coordinates, BoundingBox, Parcel, ComparableSale, ...)
  geo.py           # pure geospatial math
  comps.py         # comp filtering + ranking (select_comps)
  valuation.py     # weighted valuation -> ValuationEstimate
  server.py        # create_mcp_server() factory + MCP tools
  cli.py           # click CLI (serve / info / distance / comps / value)
docs/
  TOOLS.md         # MCP tool reference
  AVM_METHOD.md    # valuation maths and limits
tests/             # types, comps, valuation, server tools, CLI, scaffold
```

## Development

```bash
python -m pytest -q
python -m pytest -q --cov=geoavm_mcp --cov-report=term-missing --cov-fail-under=95
ruff check .
python -m build        # wheel + sdist
```

CI (`.github/workflows/ci.yml`) runs lint, the matrix above, and a wheel smoke
test that imports the package, checks `py.typed` and entry points, then runs
`geoavm-mcp info` and `geoavm-mcp distance`.

## Config

Copy `.env.example` to `.env`. Secrets stay in the environment, never in the
repo. Only `GEOAVM_HOST` and `GEOAVM_PORT` are read today; the rest is reserved
for future provider stages.

## License

MIT — see [LICENSE](LICENSE). Changes: [CHANGELOG.md](CHANGELOG.md).