GeoAVM MCP
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).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues