arv-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arv_mcp-0.1.0/.github/workflows/ci.yml +22 -0
- arv_mcp-0.1.0/.github/workflows/release.yml +137 -0
- arv_mcp-0.1.0/.gitignore +30 -0
- arv_mcp-0.1.0/LICENSE +21 -0
- arv_mcp-0.1.0/PKG-INFO +290 -0
- arv_mcp-0.1.0/README.md +260 -0
- arv_mcp-0.1.0/pyproject.toml +69 -0
- arv_mcp-0.1.0/server.json +42 -0
- arv_mcp-0.1.0/src/arv_mcp/__init__.py +3 -0
- arv_mcp-0.1.0/src/arv_mcp/arv.py +361 -0
- arv_mcp-0.1.0/src/arv_mcp/comps.py +419 -0
- arv_mcp-0.1.0/src/arv_mcp/data/__init__.py +1 -0
- arv_mcp-0.1.0/src/arv_mcp/data/client.py +172 -0
- arv_mcp-0.1.0/src/arv_mcp/data/geocode.py +59 -0
- arv_mcp-0.1.0/src/arv_mcp/data/homeharvest.py +499 -0
- arv_mcp-0.1.0/src/arv_mcp/ratelimit.py +81 -0
- arv_mcp-0.1.0/src/arv_mcp/scoring.py +457 -0
- arv_mcp-0.1.0/src/arv_mcp/server.py +172 -0
- arv_mcp-0.1.0/src/arv_mcp/service.py +602 -0
- arv_mcp-0.1.0/tests/conftest.py +25 -0
- arv_mcp-0.1.0/tests/test_arv.py +373 -0
- arv_mcp-0.1.0/tests/test_client.py +146 -0
- arv_mcp-0.1.0/tests/test_comps.py +391 -0
- arv_mcp-0.1.0/tests/test_geocode.py +68 -0
- arv_mcp-0.1.0/tests/test_homeharvest.py +412 -0
- arv_mcp-0.1.0/tests/test_live.py +86 -0
- arv_mcp-0.1.0/tests/test_ratelimit.py +81 -0
- arv_mcp-0.1.0/tests/test_release_metadata.py +46 -0
- arv_mcp-0.1.0/tests/test_scoring.py +294 -0
- arv_mcp-0.1.0/tests/test_server.py +73 -0
- arv_mcp-0.1.0/tests/test_service.py +368 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: pip install -e ".[dev]"
|
|
21
|
+
- run: ruff check src tests
|
|
22
|
+
- run: pytest
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes a tagged version to PyPI and the MCP Registry.
|
|
4
|
+
# Release by bumping __version__ in src/arv_mcp/__init__.py, then pushing a
|
|
5
|
+
# matching tag: git tag v0.2.0 && git push origin v0.2.0
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags: ["v*"]
|
|
10
|
+
|
|
11
|
+
permissions: {}
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: release
|
|
15
|
+
cancel-in-progress: false
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
name: Check, test and build
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
outputs:
|
|
24
|
+
version: ${{ steps.version.outputs.version }}
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.12"
|
|
31
|
+
|
|
32
|
+
- name: Tag must match __version__
|
|
33
|
+
id: version
|
|
34
|
+
run: |
|
|
35
|
+
set -euo pipefail
|
|
36
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
37
|
+
pkg="$(python -c 'import re; print(re.search(r"__version__ = \"([^\"]+)\"", open("src/arv_mcp/__init__.py").read()).group(1))')"
|
|
38
|
+
if [ "$tag" != "$pkg" ]; then
|
|
39
|
+
echo "::error::Tag $GITHUB_REF_NAME does not match __version__ $pkg in src/arv_mcp/__init__.py"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
echo "version=$pkg" >> "$GITHUB_OUTPUT"
|
|
43
|
+
|
|
44
|
+
- name: README carries the MCP Registry ownership marker
|
|
45
|
+
run: |
|
|
46
|
+
name="$(jq -r .name server.json)"
|
|
47
|
+
grep -q "mcp-name: $name" README.md || { echo "::error::README.md is missing 'mcp-name: $name'"; exit 1; }
|
|
48
|
+
|
|
49
|
+
- name: Lint and test
|
|
50
|
+
run: |
|
|
51
|
+
pip install -e ".[dev]"
|
|
52
|
+
ruff check src tests
|
|
53
|
+
pytest
|
|
54
|
+
|
|
55
|
+
- name: Build and check distributions
|
|
56
|
+
run: |
|
|
57
|
+
pip install build twine
|
|
58
|
+
python -m build
|
|
59
|
+
twine check --strict dist/*
|
|
60
|
+
|
|
61
|
+
- uses: actions/upload-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
name: dist
|
|
64
|
+
path: dist/
|
|
65
|
+
|
|
66
|
+
pypi:
|
|
67
|
+
name: Publish to PyPI
|
|
68
|
+
needs: build
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
environment:
|
|
71
|
+
name: pypi
|
|
72
|
+
url: https://pypi.org/project/arv-mcp/${{ needs.build.outputs.version }}/
|
|
73
|
+
permissions:
|
|
74
|
+
id-token: write # PyPI Trusted Publishing
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/download-artifact@v4
|
|
77
|
+
with:
|
|
78
|
+
name: dist
|
|
79
|
+
path: dist/
|
|
80
|
+
|
|
81
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
82
|
+
|
|
83
|
+
registry:
|
|
84
|
+
name: Publish to MCP Registry
|
|
85
|
+
needs: [build, pypi]
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
permissions:
|
|
88
|
+
id-token: write # MCP Registry GitHub OIDC login
|
|
89
|
+
contents: read
|
|
90
|
+
env:
|
|
91
|
+
VERSION: ${{ needs.build.outputs.version }}
|
|
92
|
+
# Pinned and signature-verified below. Bump deliberately; if publishing
|
|
93
|
+
# fails with "invalid audience", this release is too old for the registry.
|
|
94
|
+
MCP_PUBLISHER_VERSION: v1.8.1
|
|
95
|
+
steps:
|
|
96
|
+
- uses: actions/checkout@v4
|
|
97
|
+
|
|
98
|
+
- name: Stamp version into server.json
|
|
99
|
+
run: |
|
|
100
|
+
jq --arg v "$VERSION" '.version = $v | .packages[0].version = $v' server.json > server.tmp
|
|
101
|
+
mv server.tmp server.json
|
|
102
|
+
cat server.json
|
|
103
|
+
|
|
104
|
+
- name: Wait for the release to appear on PyPI
|
|
105
|
+
run: |
|
|
106
|
+
for i in $(seq 1 30); do
|
|
107
|
+
if curl -fsS "https://pypi.org/pypi/arv-mcp/$VERSION/json" > /dev/null; then
|
|
108
|
+
echo "arv-mcp $VERSION is live on PyPI"; exit 0
|
|
109
|
+
fi
|
|
110
|
+
sleep 10
|
|
111
|
+
done
|
|
112
|
+
echo "::error::arv-mcp $VERSION did not appear on PyPI within 5 minutes"
|
|
113
|
+
exit 1
|
|
114
|
+
|
|
115
|
+
- name: Install cosign
|
|
116
|
+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
|
117
|
+
|
|
118
|
+
- name: Download and verify mcp-publisher
|
|
119
|
+
run: |
|
|
120
|
+
set -euo pipefail
|
|
121
|
+
archive="mcp-publisher_linux_amd64.tar.gz"
|
|
122
|
+
base_url="https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}"
|
|
123
|
+
curl --fail --location --silent --show-error --output "$archive" "$base_url/$archive"
|
|
124
|
+
curl --fail --location --silent --show-error --output "$archive.sigstore.json" "$base_url/$archive.sigstore.json"
|
|
125
|
+
cosign verify-blob \
|
|
126
|
+
--bundle "$archive.sigstore.json" \
|
|
127
|
+
--certificate-identity "https://github.com/modelcontextprotocol/registry/.github/workflows/release.yml@refs/tags/${MCP_PUBLISHER_VERSION}" \
|
|
128
|
+
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
|
129
|
+
"$archive"
|
|
130
|
+
tar xzf "$archive" mcp-publisher
|
|
131
|
+
rm -f "$archive" "$archive.sigstore.json"
|
|
132
|
+
|
|
133
|
+
- name: Log in to the MCP Registry
|
|
134
|
+
run: ./mcp-publisher login github-oidc
|
|
135
|
+
|
|
136
|
+
- name: Publish to the MCP Registry
|
|
137
|
+
run: ./mcp-publisher publish
|
arv_mcp-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Secrets — never commit
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
|
|
5
|
+
# Local planning notes
|
|
6
|
+
SCOPE.md
|
|
7
|
+
|
|
8
|
+
# Local data (never source code)
|
|
9
|
+
*.db
|
|
10
|
+
*.sqlite
|
|
11
|
+
*.gpkg
|
|
12
|
+
/data/
|
|
13
|
+
*.log
|
|
14
|
+
*.log.*
|
|
15
|
+
|
|
16
|
+
# Python
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.py[cod]
|
|
19
|
+
*.egg-info/
|
|
20
|
+
dist/
|
|
21
|
+
build/
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
|
|
25
|
+
# Tooling
|
|
26
|
+
.claude/
|
|
27
|
+
.coverage
|
|
28
|
+
htmlcov/
|
|
29
|
+
.pytest_cache/
|
|
30
|
+
.ruff_cache/
|
arv_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mario Verdugo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
arv_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: arv-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server that estimates after-repair value (ARV) from scored, gated sold comps.
|
|
5
|
+
Project-URL: Repository, https://github.com/mariov7757/arv-mcp
|
|
6
|
+
Project-URL: Issues, https://github.com/mariov7757/arv-mcp/issues
|
|
7
|
+
Author: Mario Verdugo
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: appraisal,arv,comps,mcp,real-estate
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: anyio>=4
|
|
21
|
+
Requires-Dist: homeharvest<0.9,>=0.8.18
|
|
22
|
+
Requires-Dist: mcp<3,>=2.3
|
|
23
|
+
Requires-Dist: pydantic>=2.0
|
|
24
|
+
Requires-Dist: requests>=2.31
|
|
25
|
+
Requires-Dist: tenacity>=8
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# arv-mcp
|
|
32
|
+
|
|
33
|
+
<!-- mcp-name: io.github.mariov7757/arv-mcp -->
|
|
34
|
+
|
|
35
|
+
An [MCP](https://modelcontextprotocol.io) server that estimates a home's
|
|
36
|
+
**after-repair value (ARV)** from nearby sold comps, and hands the model the
|
|
37
|
+
scored evidence behind the number so it can explain, check and challenge it.
|
|
38
|
+
|
|
39
|
+
Ask Claude *"What's the ARV of 1234 Main St?"* and it gets back a typical
|
|
40
|
+
value, a renovated range, a confidence tier, the comps that support it (and
|
|
41
|
+
the ones that were thrown out, with reasons), what's listed nearby right
|
|
42
|
+
now, and warnings about anything that should make it cautious.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
You: What's the ARV of 1234 Main Street?
|
|
46
|
+
|
|
47
|
+
Claude: Typical value is about $516k ($263/sqft on 1,961 sqft), based on
|
|
48
|
+
20 eligible sales, 5+ of them within a quarter mile (confidence 5/5).
|
|
49
|
+
Renovated comps support $541k–$586k. The closest match, 1260 Main
|
|
50
|
+
Street, sold 23 days ago for $465k ($272/sqft): same subdivision,
|
|
51
|
+
a bit smaller, no pool.
|
|
52
|
+
|
|
53
|
+
Worth knowing: this house itself sold this week for $432,500
|
|
54
|
+
($220/sqft), well under the comp-based value. That usually means it
|
|
55
|
+
sold in rough condition, which fits a flip at the renovated range.
|
|
56
|
+
Two of the eligible comps were distressed sales; without them the
|
|
57
|
+
median barely moves.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
> **Not an appraisal.** Data comes from public listings on Realtor.com via
|
|
61
|
+
> [HomeHarvest](https://github.com/ZacharyHampton/HomeHarvest) and can be
|
|
62
|
+
> wrong or missing. Use it for research and as a starting point, not for
|
|
63
|
+
> lending or legal decisions.
|
|
64
|
+
|
|
65
|
+
## How it works
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
address ──► verify the property ──► sold comps ──► score & gate ──► estimate
|
|
69
|
+
│ (any status) within 1.5 mi each comp + evidence
|
|
70
|
+
└─ not verified? ──► locate it + ask for its facts
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
1. **Find the subject.** The address is resolved to Realtor.com's record
|
|
74
|
+
for exactly that street and unit, whether it's for sale, pending, sold
|
|
75
|
+
or off market. Realtor.com's geocoder will happily return the wrong
|
|
76
|
+
house for a bad address, so every result is checked against the street
|
|
77
|
+
and unit typed. If nothing verifies, the address is located (Census
|
|
78
|
+
geocoder, or caller-supplied lat/lon) and the tool asks for the facts it
|
|
79
|
+
needs (`sqft`, `property_type`) instead of guessing.
|
|
80
|
+
2. **Pull comps around the subject's coordinates:** sold in the last 180
|
|
81
|
+
days within 1.5 miles (both adjustable per call), ±40% of the subject's
|
|
82
|
+
size, same property type. The subject's own sale is pulled out and
|
|
83
|
+
reported separately, so a just-sold house can't comp itself.
|
|
84
|
+
3. **Score every comp** on 13 factors: size, year built, lot, beds/baths,
|
|
85
|
+
property type, recency, condition signals, distance, subdivision,
|
|
86
|
+
garage, stories, pool and basement. Each known factor also has a
|
|
87
|
+
**floor**. A comp that fails any one (a different property type, a much
|
|
88
|
+
bigger house, one built decades apart) is excluded outright, however good
|
|
89
|
+
it looks elsewhere. Size, age and lot floors loosen for comps right next
|
|
90
|
+
door and tighten with distance.
|
|
91
|
+
4. **Check the micro-market.** Each comp's subdivision median (or its own
|
|
92
|
+
$/sqft) must sit within ±20% of the subject's local price level, so a
|
|
93
|
+
great physical match in a pricier pocket doesn't inflate the estimate.
|
|
94
|
+
Subdivisions come from the MLS "Subdivision" field, with phases like
|
|
95
|
+
"Unit 2" or "Ph 04 Sec 01" merged.
|
|
96
|
+
5. **Estimate:**
|
|
97
|
+
- **Typical value:** similarity-weighted median $/sqft × subject sqft.
|
|
98
|
+
- **Renovated range:** weighted 75th–90th percentile, what the top of
|
|
99
|
+
the pool supports.
|
|
100
|
+
- **Confidence 1–5:** how close the supporting comps are.
|
|
101
|
+
|
|
102
|
+
If no comp survives, the tool falls back to the area's going rate and
|
|
103
|
+
says plainly that the number isn't comp-backed.
|
|
104
|
+
|
|
105
|
+
## Tools
|
|
106
|
+
|
|
107
|
+
### `get_arv`
|
|
108
|
+
|
|
109
|
+
| Parameter | Description |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `address` (required) | Full street address, including any unit number. |
|
|
112
|
+
| `sqft`, `property_type` | Override the record. Required when the address can't be verified. |
|
|
113
|
+
| `beds`, `baths`, `year_built`, `lot_sqft`, `stories`, `garage`, `has_pool` | Override or supply facts; also how what-ifs work ("what if it had a pool?"). |
|
|
114
|
+
| `latitude`, `longitude` | Locate a property no geocoder can find. |
|
|
115
|
+
| `radius_mi`, `days_back` | Comp search window: 0.25–5 mi (default 1.5), 30–730 days (default 180). |
|
|
116
|
+
| `include_market_context` | Also fetch nearby active/pending listings (default true). |
|
|
117
|
+
| `max_comps` | Eligible comps to return, 1–25 (default 10). |
|
|
118
|
+
|
|
119
|
+
Returns `subject`, `estimate` (`arv`, `renovated_range`, `arv_fallback`,
|
|
120
|
+
window), `confidence`, `pool_stats`, `eligible_comps` (each with the factors
|
|
121
|
+
that cost it points and any distress flags), `excluded_comps` (with
|
|
122
|
+
reasons), `market_context` and `guidance`. Unknown facts are `null`, never
|
|
123
|
+
`0`.
|
|
124
|
+
|
|
125
|
+
### `lookup_property`
|
|
126
|
+
|
|
127
|
+
The verified record for an address: size, beds/baths, year, lot, stories,
|
|
128
|
+
garage, pool, subdivision, status, list price (only while it's on the
|
|
129
|
+
market) and last sale.
|
|
130
|
+
|
|
131
|
+
### Prompt: `analyze_arv`
|
|
132
|
+
|
|
133
|
+
An appraiser-style walkthrough (subject → comps → adjustments → range).
|
|
134
|
+
Shows up as a slash command in Claude Code.
|
|
135
|
+
|
|
136
|
+
### Errors
|
|
137
|
+
|
|
138
|
+
Failures come back as `{"error": {"code", "message", ...}}` so the model
|
|
139
|
+
can act on them:
|
|
140
|
+
|
|
141
|
+
| Code | Meaning |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `ADDRESS_NOT_VERIFIED` | Located, but no record matches; `needs` lists the facts to ask for. |
|
|
144
|
+
| `ADDRESS_NOT_FOUND` | Couldn't find or locate the address. |
|
|
145
|
+
| `MISSING_FACTS` | The record lacks something the estimate needs (e.g. sqft). |
|
|
146
|
+
| `UNSUPPORTED_PROPERTY_TYPE` | Vacant land: no living area to price per sqft. |
|
|
147
|
+
| `NO_COMPS` | No sales in the window; try a larger `radius_mi` / `days_back`. |
|
|
148
|
+
| `RATE_LIMITED` | Local rate limit; `retry_after_s` says when to retry. |
|
|
149
|
+
| `UPSTREAM_ERROR` | Realtor.com refused or failed the request. |
|
|
150
|
+
|
|
151
|
+
## Install
|
|
152
|
+
|
|
153
|
+
Requires Python 3.11+. Published on [PyPI](https://pypi.org/project/arv-mcp/)
|
|
154
|
+
and listed in the [MCP Registry](https://registry.modelcontextprotocol.io) as
|
|
155
|
+
`io.github.mariov7757/arv-mcp`.
|
|
156
|
+
|
|
157
|
+
**Claude Code**
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
claude mcp add arv-mcp -- uvx arv-mcp
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Claude Desktop** (`claude_desktop_config.json`)
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"mcpServers": {
|
|
168
|
+
"arv-mcp": {
|
|
169
|
+
"command": "uvx",
|
|
170
|
+
"args": ["arv-mcp"]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**pip**
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pip install arv-mcp
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
To run the unreleased `main` branch instead, use
|
|
183
|
+
`uvx --from git+https://github.com/mariov7757/arv-mcp arv-mcp`.
|
|
184
|
+
|
|
185
|
+
**From a clone**
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
git clone https://github.com/mariov7757/arv-mcp && cd arv-mcp
|
|
189
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
190
|
+
claude mcp add arv-mcp -- "$PWD/.venv/bin/arv-mcp"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Windows: if `uvx` fails installing `pywin32`** (`os error 32` /
|
|
194
|
+
"being used by another process", usually antivirus scanning uv's temporary
|
|
195
|
+
build folder), install into a regular virtualenv instead and point the
|
|
196
|
+
client at the installed executable:
|
|
197
|
+
|
|
198
|
+
```powershell
|
|
199
|
+
uv venv $HOME\.arv-mcp
|
|
200
|
+
uv pip install --python $HOME\.arv-mcp\Scripts\python.exe arv-mcp
|
|
201
|
+
claude mcp add arv-mcp -- $HOME\.arv-mcp\Scripts\arv-mcp.exe
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Configuration
|
|
205
|
+
|
|
206
|
+
All optional, via environment variables:
|
|
207
|
+
|
|
208
|
+
| Variable | Default | |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| `ARV_MCP_REQUESTS_PER_MIN` | `30` | Realtor.com requests per minute (token bucket)… |
|
|
211
|
+
| `ARV_MCP_REQUEST_BURST` | `10` | …and how many can go at once. |
|
|
212
|
+
| `ARV_MCP_GEOCODER_PER_SEC` | `1` | Census geocoder requests per second. |
|
|
213
|
+
| `ARV_MCP_RATE_WAIT_S` | `15` | Longest a call waits for a rate-limit slot before returning `RATE_LIMITED`. |
|
|
214
|
+
| `ARV_MCP_MAX_CONCURRENT` | `2` | Tool calls processed at once. |
|
|
215
|
+
| `ARV_MCP_LOG_LEVEL` | `WARNING` | Logs go to stderr. |
|
|
216
|
+
|
|
217
|
+
A full `get_arv` makes 5 requests to Realtor.com (2 to look up the subject,
|
|
218
|
+
1 for sold comps, 2 for active/pending) and takes ~4–6 s. Results are cached
|
|
219
|
+
for 15 minutes, so follow-ups and what-ifs on the same address return
|
|
220
|
+
instantly.
|
|
221
|
+
|
|
222
|
+
## Limitations
|
|
223
|
+
|
|
224
|
+
- **Tuned on Phoenix metro.** Gate bands and confidence tiers were
|
|
225
|
+
calibrated on ~8,000 Phoenix-area valuations. The engine runs anywhere in
|
|
226
|
+
the US, but in other markets (especially rural or low-volume ones) lean
|
|
227
|
+
on the comps and confidence tier, and widen `radius_mi` / `days_back`.
|
|
228
|
+
- **Typical value isn't condition-aware.** Distressed sales are flagged
|
|
229
|
+
(MLS REO/short-sale conditions, "fixer"/"as-is" language) and their
|
|
230
|
+
effect on the median is reported, but they aren't excluded.
|
|
231
|
+
- **$/sqft is the wrong tool for some properties.** Land is refused; farms,
|
|
232
|
+
multi-family and mobile homes get a warning.
|
|
233
|
+
- **Unofficial data source.** HomeHarvest scrapes Realtor.com's public
|
|
234
|
+
site. Requests run from your own machine and are rate-limited, but the
|
|
235
|
+
site can change or block access at any time. `homeharvest` is pinned to
|
|
236
|
+
a tested range because the client relies on its internals.
|
|
237
|
+
|
|
238
|
+
## Development
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pip install -e ".[dev]"
|
|
242
|
+
pytest # offline suite; real network calls are blocked
|
|
243
|
+
ARV_MCP_LIVE_ADDRESS="<a real address>" pytest -m live # smoke tests against Realtor.com + Census
|
|
244
|
+
ruff check src tests
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
src/arv_mcp/
|
|
249
|
+
server.py MCP tools, instructions and prompt
|
|
250
|
+
service.py address in, estimate + evidence out
|
|
251
|
+
arv.py eligibility, weighted median, renovated range, confidence
|
|
252
|
+
scoring.py per-comp gated scoring
|
|
253
|
+
comps.py comp normalization and bad-data filters
|
|
254
|
+
ratelimit.py token buckets
|
|
255
|
+
data/client.py Realtor.com transport over HomeHarvest
|
|
256
|
+
data/homeharvest.py subject lookup, comp searches, field extraction
|
|
257
|
+
data/geocode.py Census geocoder
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The scoring engine was extracted from a private wholesale-real-estate
|
|
261
|
+
pipeline where it priced thousands of Phoenix-area listings.
|
|
262
|
+
|
|
263
|
+
### Releasing
|
|
264
|
+
|
|
265
|
+
The version lives in one place: `__version__` in `src/arv_mcp/__init__.py`.
|
|
266
|
+
|
|
267
|
+
1. Bump `__version__` (patch for fixes, minor for new tools, parameters or
|
|
268
|
+
fields) and merge to `main` with CI green.
|
|
269
|
+
2. Tag the release and push the tag:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
git tag v0.2.0
|
|
273
|
+
git push origin v0.2.0
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The [Release workflow](.github/workflows/release.yml) then:
|
|
277
|
+
|
|
278
|
+
- checks that the tag matches `__version__`
|
|
279
|
+
- runs lint and tests, and builds the package
|
|
280
|
+
- publishes to PyPI with Trusted Publishing (no API token)
|
|
281
|
+
- stamps the version into `server.json` and publishes it to the
|
|
282
|
+
[MCP Registry](https://registry.modelcontextprotocol.io) as
|
|
283
|
+
`io.github.mariov7757/arv-mcp`, authenticated by GitHub OIDC
|
|
284
|
+
|
|
285
|
+
PyPI versions are permanent. A broken release gets yanked on PyPI and
|
|
286
|
+
replaced by the next patch version; a version number is never reused.
|
|
287
|
+
|
|
288
|
+
## License
|
|
289
|
+
|
|
290
|
+
MIT
|