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.
@@ -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
@@ -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