zillow-mcp 0.2.1 → 0.3.0
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +20 -0
- package/dist/bundle.js +443 -91
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Zillow — search listings, fetch property details, Zestimate history, saved searches & homes, market reports. Routes through the user's signed-in zillow.com tab via the fetchproxy browser extension to dodge bot detection.",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.3.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Zillow",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "MCP server for Zillow — search listings, get property details, Zestimate history, saved searches & homes, market reports",
|
|
18
|
-
"version": "0.
|
|
18
|
+
"version": "0.3.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zillow-mcp",
|
|
3
3
|
"displayName": "Zillow",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Zillow real-estate access for Claude — search listings, get property details, Zestimate history, your saved searches & homes, market reports",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/README.md
CHANGED
|
@@ -25,6 +25,26 @@ None of them can see what *you* have saved, favorited, or recently viewed — be
|
|
|
25
25
|
| `zillow_get_market_report` | Median sale/list/rent, days on market, inventory, ZHVI for a region | |
|
|
26
26
|
| `zillow_calculate_mortgage` | Local PITI calculator — principal+interest, taxes, insurance, HOA, PMI (no network) | |
|
|
27
27
|
|
|
28
|
+
## Acknowledgement of Terms
|
|
29
|
+
|
|
30
|
+
By using this MCP server, you acknowledge and agree to the following:
|
|
31
|
+
|
|
32
|
+
**1. This server accesses your own Zillow session.** Every request is dispatched through your own browser tab (logged in or not) via the fetchproxy extension. It does not — and cannot — access anyone else's account.
|
|
33
|
+
|
|
34
|
+
**2. [Zillow's Terms of Use](https://www.zillow.com/z/corp/terms/) govern your use of this server**, just as they govern your direct use of zillow.com. The clauses most relevant here:
|
|
35
|
+
|
|
36
|
+
> You may not use any robot, spider, scraper or other automated means to access the Services for any purpose without our express written permission… nor may you conduct automated queries (including screen and database scraping, spiders, robots, crawlers, bypassing CAPTCHAs or similar precautions).
|
|
37
|
+
|
|
38
|
+
You are agreeing to those terms — read by the maintainer 2026-05-23 — every time you invoke a tool in this server. Zillow's terms broadly prohibit automated access without written permission; this is an unofficial tool and Zillow has not granted it permission.
|
|
39
|
+
|
|
40
|
+
**3. Personal, non-commercial use only.** This project is not affiliated with, endorsed by, sponsored by, or in partnership with Zillow Group. It is a personal automation tool that drives the same Zillow website you would drive by hand — one search at a time, your own saved homes, your own market reports. Do not use it to bulk-extract listings, train models, populate a competing real-estate product, or for any commercial purpose.
|
|
41
|
+
|
|
42
|
+
**4. Stability is not guaranteed.** This server reads private internal endpoints (`/async-create-search-page-state/`, `__NEXT_DATA__` blobs, `/myzillow/...`) that Zillow may change without notice. It may break. It may stop working. That's by design — the surface is not theirs to maintain on our behalf.
|
|
43
|
+
|
|
44
|
+
**5. You accept full responsibility** for any consequences of using this server in connection with your Zillow access — rate limiting, account warnings, suspension, IP blocks, captcha walls, or any enforcement action Zillow Group takes. If Zillow objects to your use, stop using this server.
|
|
45
|
+
|
|
46
|
+
This section is the maintainer's good-faith summary of the terms — it is not legal advice and does not modify or supersede Zillow's actual ToS.
|
|
47
|
+
|
|
28
48
|
## Install
|
|
29
49
|
|
|
30
50
|
### Option A — npx (after publishing)
|
package/dist/bundle.js
CHANGED
|
@@ -36668,6 +36668,40 @@ function textResult(data) {
|
|
|
36668
36668
|
};
|
|
36669
36669
|
}
|
|
36670
36670
|
|
|
36671
|
+
// src/next-data.ts
|
|
36672
|
+
var ParseError = class extends Error {
|
|
36673
|
+
constructor(message) {
|
|
36674
|
+
super(message);
|
|
36675
|
+
this.name = "ParseError";
|
|
36676
|
+
}
|
|
36677
|
+
};
|
|
36678
|
+
var OPEN_TAG_RE = /<script[^>]*id=["']__NEXT_DATA__["'][^>]*>/i;
|
|
36679
|
+
var CLOSE_TAG = "</script>";
|
|
36680
|
+
function extractNextData(html) {
|
|
36681
|
+
const openMatch = OPEN_TAG_RE.exec(html);
|
|
36682
|
+
if (!openMatch) {
|
|
36683
|
+
throw new ParseError("__NEXT_DATA__ script tag not found in HTML");
|
|
36684
|
+
}
|
|
36685
|
+
const start = openMatch.index + openMatch[0].length;
|
|
36686
|
+
const end = html.indexOf(CLOSE_TAG, start);
|
|
36687
|
+
if (end < 0) {
|
|
36688
|
+
throw new ParseError("__NEXT_DATA__ script tag has no closing </script>");
|
|
36689
|
+
}
|
|
36690
|
+
const json2 = html.slice(start, end).trim();
|
|
36691
|
+
try {
|
|
36692
|
+
return JSON.parse(json2);
|
|
36693
|
+
} catch (err) {
|
|
36694
|
+
throw new ParseError(
|
|
36695
|
+
`Failed to parse __NEXT_DATA__ JSON: ${err.message}`
|
|
36696
|
+
);
|
|
36697
|
+
}
|
|
36698
|
+
}
|
|
36699
|
+
function getPageProps(nextData) {
|
|
36700
|
+
const props = nextData.props;
|
|
36701
|
+
const pageProps = props?.pageProps;
|
|
36702
|
+
return pageProps ?? {};
|
|
36703
|
+
}
|
|
36704
|
+
|
|
36671
36705
|
// src/tools/search.ts
|
|
36672
36706
|
var HOME_TYPE_FILTERS = {
|
|
36673
36707
|
house: "isSingleFamily",
|
|
@@ -36703,7 +36737,7 @@ function formatListing(raw) {
|
|
|
36703
36737
|
url: url2
|
|
36704
36738
|
};
|
|
36705
36739
|
}
|
|
36706
|
-
function
|
|
36740
|
+
function buildSearchQueryState(input) {
|
|
36707
36741
|
const filterState = {};
|
|
36708
36742
|
switch (input.status ?? "for_sale") {
|
|
36709
36743
|
case "for_rent":
|
|
@@ -36745,17 +36779,18 @@ function buildSearchBody(input) {
|
|
|
36745
36779
|
}
|
|
36746
36780
|
}
|
|
36747
36781
|
return {
|
|
36748
|
-
|
|
36749
|
-
|
|
36750
|
-
|
|
36751
|
-
|
|
36752
|
-
isListVisible: true
|
|
36753
|
-
},
|
|
36754
|
-
wants: { cat1: ["listResults"] },
|
|
36755
|
-
requestId: 1,
|
|
36756
|
-
isDebugRequest: false
|
|
36782
|
+
usersSearchTerm: input.location,
|
|
36783
|
+
filterState,
|
|
36784
|
+
isListVisible: true,
|
|
36785
|
+
isMapVisible: false
|
|
36757
36786
|
};
|
|
36758
36787
|
}
|
|
36788
|
+
function buildSearchPath(input) {
|
|
36789
|
+
const sqs = buildSearchQueryState(input);
|
|
36790
|
+
const slug = encodeURIComponent(input.location.trim());
|
|
36791
|
+
const qs = encodeURIComponent(JSON.stringify(sqs));
|
|
36792
|
+
return `/homes/${slug}_rb/?searchQueryState=${qs}`;
|
|
36793
|
+
}
|
|
36759
36794
|
function registerSearchTools(server2, client2) {
|
|
36760
36795
|
server2.registerTool(
|
|
36761
36796
|
"zillow_search_properties",
|
|
@@ -36792,9 +36827,12 @@ function registerSearchTools(server2, client2) {
|
|
|
36792
36827
|
}
|
|
36793
36828
|
},
|
|
36794
36829
|
async (input) => {
|
|
36795
|
-
const
|
|
36796
|
-
const
|
|
36797
|
-
const
|
|
36830
|
+
const path = buildSearchPath(input);
|
|
36831
|
+
const html = await client2.fetchHtml(path);
|
|
36832
|
+
const nextData = extractNextData(html);
|
|
36833
|
+
const pageProps = getPageProps(nextData);
|
|
36834
|
+
const sps = pageProps.searchPageState;
|
|
36835
|
+
const raw = sps?.cat1?.searchResults?.listResults ?? [];
|
|
36798
36836
|
const limit = input.limit ?? 40;
|
|
36799
36837
|
const formatted = raw.map(formatListing).filter((x) => x !== null).slice(0, limit);
|
|
36800
36838
|
return textResult(formatted);
|
|
@@ -36802,40 +36840,6 @@ function registerSearchTools(server2, client2) {
|
|
|
36802
36840
|
);
|
|
36803
36841
|
}
|
|
36804
36842
|
|
|
36805
|
-
// src/next-data.ts
|
|
36806
|
-
var ParseError = class extends Error {
|
|
36807
|
-
constructor(message) {
|
|
36808
|
-
super(message);
|
|
36809
|
-
this.name = "ParseError";
|
|
36810
|
-
}
|
|
36811
|
-
};
|
|
36812
|
-
var OPEN_TAG_RE = /<script[^>]*id=["']__NEXT_DATA__["'][^>]*>/i;
|
|
36813
|
-
var CLOSE_TAG = "</script>";
|
|
36814
|
-
function extractNextData(html) {
|
|
36815
|
-
const openMatch = OPEN_TAG_RE.exec(html);
|
|
36816
|
-
if (!openMatch) {
|
|
36817
|
-
throw new ParseError("__NEXT_DATA__ script tag not found in HTML");
|
|
36818
|
-
}
|
|
36819
|
-
const start = openMatch.index + openMatch[0].length;
|
|
36820
|
-
const end = html.indexOf(CLOSE_TAG, start);
|
|
36821
|
-
if (end < 0) {
|
|
36822
|
-
throw new ParseError("__NEXT_DATA__ script tag has no closing </script>");
|
|
36823
|
-
}
|
|
36824
|
-
const json2 = html.slice(start, end).trim();
|
|
36825
|
-
try {
|
|
36826
|
-
return JSON.parse(json2);
|
|
36827
|
-
} catch (err) {
|
|
36828
|
-
throw new ParseError(
|
|
36829
|
-
`Failed to parse __NEXT_DATA__ JSON: ${err.message}`
|
|
36830
|
-
);
|
|
36831
|
-
}
|
|
36832
|
-
}
|
|
36833
|
-
function getPageProps(nextData) {
|
|
36834
|
-
const props = nextData.props;
|
|
36835
|
-
const pageProps = props?.pageProps;
|
|
36836
|
-
return pageProps ?? {};
|
|
36837
|
-
}
|
|
36838
|
-
|
|
36839
36843
|
// src/url.ts
|
|
36840
36844
|
function urlToPath(input) {
|
|
36841
36845
|
try {
|
|
@@ -36869,7 +36873,20 @@ function findPropertyInPageProps(pageProps) {
|
|
|
36869
36873
|
function buildPath(args) {
|
|
36870
36874
|
if (args.url) return urlToPath(args.url);
|
|
36871
36875
|
if (args.zpid !== void 0) return `/homedetails/${args.zpid}_zpid/`;
|
|
36872
|
-
throw new Error("
|
|
36876
|
+
throw new Error("zillow property tool: must provide either zpid or url");
|
|
36877
|
+
}
|
|
36878
|
+
async function fetchPropertyRecord(client2, args) {
|
|
36879
|
+
const path = buildPath(args);
|
|
36880
|
+
const html = await client2.fetchHtml(path);
|
|
36881
|
+
const nextData = extractNextData(html);
|
|
36882
|
+
const pageProps = getPageProps(nextData);
|
|
36883
|
+
const property = findPropertyInPageProps(pageProps);
|
|
36884
|
+
if (!property) {
|
|
36885
|
+
throw new Error(
|
|
36886
|
+
`Could not locate property data in __NEXT_DATA__ at ${path}. Zillow may have changed their page structure.`
|
|
36887
|
+
);
|
|
36888
|
+
}
|
|
36889
|
+
return { raw: property, path };
|
|
36873
36890
|
}
|
|
36874
36891
|
function format(raw) {
|
|
36875
36892
|
const zpid = String(raw.zpid ?? "");
|
|
@@ -36919,17 +36936,8 @@ function registerPropertyTools(server2, client2) {
|
|
|
36919
36936
|
}
|
|
36920
36937
|
},
|
|
36921
36938
|
async ({ zpid, url: url2 }) => {
|
|
36922
|
-
const
|
|
36923
|
-
|
|
36924
|
-
const nextData = extractNextData(html);
|
|
36925
|
-
const pageProps = getPageProps(nextData);
|
|
36926
|
-
const property = findPropertyInPageProps(pageProps);
|
|
36927
|
-
if (!property) {
|
|
36928
|
-
throw new Error(
|
|
36929
|
-
`Could not locate property data in __NEXT_DATA__ at ${path}. Zillow may have changed their page structure.`
|
|
36930
|
-
);
|
|
36931
|
-
}
|
|
36932
|
-
return textResult(format(property));
|
|
36939
|
+
const { raw } = await fetchPropertyRecord(client2, { zpid, url: url2 });
|
|
36940
|
+
return textResult(format(raw));
|
|
36933
36941
|
}
|
|
36934
36942
|
);
|
|
36935
36943
|
}
|
|
@@ -37030,6 +37038,17 @@ function findSavedSearches(pageProps) {
|
|
|
37030
37038
|
);
|
|
37031
37039
|
}
|
|
37032
37040
|
function findSavedHomes(pageProps) {
|
|
37041
|
+
const collections = pageProps.collectionsResponse;
|
|
37042
|
+
if (Array.isArray(collections)) {
|
|
37043
|
+
const flat = [];
|
|
37044
|
+
for (const c of collections) {
|
|
37045
|
+
if (Array.isArray(c.homes)) flat.push(...c.homes);
|
|
37046
|
+
else if (Array.isArray(c.properties)) flat.push(...c.properties);
|
|
37047
|
+
else if (Array.isArray(c.items)) flat.push(...c.items);
|
|
37048
|
+
}
|
|
37049
|
+
if (flat.length > 0) return flat;
|
|
37050
|
+
if (collections.length > 0) return [];
|
|
37051
|
+
}
|
|
37033
37052
|
return findArrayByShape(
|
|
37034
37053
|
pageProps,
|
|
37035
37054
|
SAVED_HOME_KEYS,
|
|
@@ -37080,7 +37099,7 @@ function registerSavedTools(server2, client2) {
|
|
|
37080
37099
|
inputSchema: {}
|
|
37081
37100
|
},
|
|
37082
37101
|
async () => {
|
|
37083
|
-
const html = await client2.fetchHtml("/
|
|
37102
|
+
const html = await client2.fetchHtml("/myzillow/SavedSearches");
|
|
37084
37103
|
const nextData = extractNextData(html);
|
|
37085
37104
|
const pageProps = getPageProps(nextData);
|
|
37086
37105
|
const searches = findSavedSearches(pageProps);
|
|
@@ -37091,7 +37110,7 @@ function registerSavedTools(server2, client2) {
|
|
|
37091
37110
|
"zillow_get_saved_homes",
|
|
37092
37111
|
{
|
|
37093
37112
|
title: "Get my saved (favorited) Zillow homes",
|
|
37094
|
-
description: "The signed-in user's saved (favorited) homes on zillow.com. Returns address, price, Zestimate, status, and when each home was saved. Requires the user to be signed in. Read-only; safe to call repeatedly.",
|
|
37113
|
+
description: "The signed-in user's saved (favorited) homes on zillow.com, flattened across all of the user's collections. Returns address, price, Zestimate, status, and when each home was saved. Requires the user to be signed in. Read-only; safe to call repeatedly.",
|
|
37095
37114
|
annotations: {
|
|
37096
37115
|
title: "Get my saved (favorited) Zillow homes",
|
|
37097
37116
|
readOnlyHint: true,
|
|
@@ -37101,7 +37120,7 @@ function registerSavedTools(server2, client2) {
|
|
|
37101
37120
|
inputSchema: {}
|
|
37102
37121
|
},
|
|
37103
37122
|
async () => {
|
|
37104
|
-
const html = await client2.fetchHtml("/myzillow/favorites
|
|
37123
|
+
const html = await client2.fetchHtml("/myzillow/favorites");
|
|
37105
37124
|
const nextData = extractNextData(html);
|
|
37106
37125
|
const pageProps = getPageProps(nextData);
|
|
37107
37126
|
const homes = findSavedHomes(pageProps);
|
|
@@ -37111,35 +37130,34 @@ function registerSavedTools(server2, client2) {
|
|
|
37111
37130
|
}
|
|
37112
37131
|
|
|
37113
37132
|
// src/tools/market.ts
|
|
37114
|
-
function
|
|
37115
|
-
const candidates = [
|
|
37116
|
-
pageProps.marketInfo,
|
|
37117
|
-
pageProps.marketReport,
|
|
37118
|
-
pageProps.regionInfo,
|
|
37119
|
-
pageProps.componentProps?.marketInfo
|
|
37120
|
-
];
|
|
37133
|
+
function pickRegion(pageProps) {
|
|
37134
|
+
const candidates = [pageProps.zhviRegion, pageProps.requestedRegion];
|
|
37121
37135
|
for (const c of candidates) {
|
|
37122
37136
|
if (c && typeof c === "object") return c;
|
|
37123
37137
|
}
|
|
37124
37138
|
return null;
|
|
37125
37139
|
}
|
|
37126
|
-
function
|
|
37140
|
+
function pickAnalytics(pageProps) {
|
|
37141
|
+
const a = pageProps.odpMarketAnalytics;
|
|
37142
|
+
if (a && typeof a === "object") return a;
|
|
37143
|
+
return null;
|
|
37144
|
+
}
|
|
37145
|
+
function format2(region, analytics) {
|
|
37146
|
+
const yoy = typeof analytics?.zhviLatest?.zhviYoY === "number" ? Math.round(analytics.zhviLatest.zhviYoY * 1e3) / 10 : void 0;
|
|
37127
37147
|
return {
|
|
37128
|
-
|
|
37129
|
-
|
|
37130
|
-
|
|
37131
|
-
|
|
37132
|
-
|
|
37133
|
-
|
|
37134
|
-
median_days_on_market:
|
|
37135
|
-
|
|
37136
|
-
new_listings:
|
|
37137
|
-
|
|
37138
|
-
zhvi:
|
|
37139
|
-
zhvi_yoy_percent:
|
|
37140
|
-
|
|
37141
|
-
buyer_seller_index: raw.buyerSellerIndex,
|
|
37142
|
-
as_of_date: raw.asOfDate
|
|
37148
|
+
region_name: region?.name,
|
|
37149
|
+
region_type: region?.regionTypeName,
|
|
37150
|
+
parent_county: region?.parentCounty?.name,
|
|
37151
|
+
parent_state: region?.parentState?.name,
|
|
37152
|
+
median_sale_price: analytics?.mrktSaleLatest?.medianSalePrice,
|
|
37153
|
+
median_list_price: analytics?.mrktListingLatest?.medianListPrice,
|
|
37154
|
+
median_days_on_market: analytics?.mrktListingLatest?.medianDaysOnMarket,
|
|
37155
|
+
days_to_pending: analytics?.mrktSaleLatest?.daysToPending,
|
|
37156
|
+
new_listings: analytics?.mrktListingLatest?.newListings,
|
|
37157
|
+
for_sale_inventory: analytics?.mrktListingLatest?.forSaleInventory,
|
|
37158
|
+
zhvi: analytics?.zhviLatest?.zhvi,
|
|
37159
|
+
zhvi_yoy_percent: yoy,
|
|
37160
|
+
as_of_date: analytics?.zhviLatest?.asOfDate
|
|
37143
37161
|
};
|
|
37144
37162
|
}
|
|
37145
37163
|
function pathFromInput(args) {
|
|
@@ -37157,7 +37175,7 @@ function registerMarketTools(server2, client2) {
|
|
|
37157
37175
|
"zillow_get_market_report",
|
|
37158
37176
|
{
|
|
37159
37177
|
title: "Get Zillow market report for a region",
|
|
37160
|
-
description: 'Market report for a Zillow region: median sale/list
|
|
37178
|
+
description: 'Market report for a Zillow region: median sale/list prices, days on market, for-sale inventory, new listings, Zillow Home Value Index (ZHVI), and year-over-year ZHVI change. Provide either a `region_path` (e.g. "/home-values/6181/brooklyn-ny/") or a full Zillow home-values URL. Read-only; safe to call repeatedly.',
|
|
37161
37179
|
annotations: {
|
|
37162
37180
|
title: "Get Zillow market report for a region",
|
|
37163
37181
|
readOnlyHint: true,
|
|
@@ -37176,13 +37194,14 @@ function registerMarketTools(server2, client2) {
|
|
|
37176
37194
|
const html = await client2.fetchHtml(path);
|
|
37177
37195
|
const nextData = extractNextData(html);
|
|
37178
37196
|
const pageProps = getPageProps(nextData);
|
|
37179
|
-
const
|
|
37180
|
-
|
|
37197
|
+
const region = pickRegion(pageProps);
|
|
37198
|
+
const analytics = pickAnalytics(pageProps);
|
|
37199
|
+
if (!region && !analytics) {
|
|
37181
37200
|
throw new Error(
|
|
37182
|
-
`Could not locate
|
|
37201
|
+
`Could not locate market data (zhviRegion + odpMarketAnalytics) in __NEXT_DATA__ at ${path}.`
|
|
37183
37202
|
);
|
|
37184
37203
|
}
|
|
37185
|
-
return textResult(format2(
|
|
37204
|
+
return textResult(format2(region, analytics));
|
|
37186
37205
|
}
|
|
37187
37206
|
);
|
|
37188
37207
|
}
|
|
@@ -37265,8 +37284,338 @@ function registerMortgageTools(server2) {
|
|
|
37265
37284
|
);
|
|
37266
37285
|
}
|
|
37267
37286
|
|
|
37287
|
+
// src/tools/history.ts
|
|
37288
|
+
function toPercent(rate) {
|
|
37289
|
+
if (typeof rate !== "number") return void 0;
|
|
37290
|
+
return Math.round(rate * 1e3) / 10;
|
|
37291
|
+
}
|
|
37292
|
+
function formatPriceEvent(raw) {
|
|
37293
|
+
const date5 = raw.date ?? (typeof raw.time === "number" ? new Date(raw.time).toISOString().slice(0, 10) : void 0);
|
|
37294
|
+
return {
|
|
37295
|
+
date: date5,
|
|
37296
|
+
event: raw.event,
|
|
37297
|
+
price: raw.price,
|
|
37298
|
+
price_change_percent: toPercent(raw.priceChangeRate),
|
|
37299
|
+
price_per_sqft: raw.pricePerSquareFoot,
|
|
37300
|
+
source: raw.source,
|
|
37301
|
+
mls_number: raw.attributeSource?.infoString1
|
|
37302
|
+
};
|
|
37303
|
+
}
|
|
37304
|
+
function formatTaxEvent(raw) {
|
|
37305
|
+
const year = typeof raw.time === "number" ? new Date(raw.time).getUTCFullYear() : void 0;
|
|
37306
|
+
return {
|
|
37307
|
+
year,
|
|
37308
|
+
tax_paid: raw.taxPaid,
|
|
37309
|
+
tax_increase_percent: toPercent(raw.taxIncreaseRate),
|
|
37310
|
+
assessed_value: raw.value,
|
|
37311
|
+
assessed_value_increase_percent: toPercent(raw.valueIncreaseRate)
|
|
37312
|
+
};
|
|
37313
|
+
}
|
|
37314
|
+
function registerHistoryTools(server2, client2) {
|
|
37315
|
+
server2.registerTool(
|
|
37316
|
+
"zillow_get_price_history",
|
|
37317
|
+
{
|
|
37318
|
+
title: "Get Zillow price history for a property",
|
|
37319
|
+
description: "Listing-price events for a property \u2014 listings, price changes, pending, sold, etc. \u2014 by zpid or homedetails URL. Each entry has a date, event type, price, percent price-change, price/sqft, and MLS attribution. Sourced from the same homedetails page as zillow_get_property, but returns just the price-history series for easier downstream reasoning.",
|
|
37320
|
+
annotations: {
|
|
37321
|
+
title: "Get Zillow price history for a property",
|
|
37322
|
+
readOnlyHint: true,
|
|
37323
|
+
idempotentHint: true,
|
|
37324
|
+
openWorldHint: true
|
|
37325
|
+
},
|
|
37326
|
+
inputSchema: {
|
|
37327
|
+
zpid: external_exports.union([external_exports.number().int().positive(), external_exports.string()]).optional().describe("Zillow Property ID"),
|
|
37328
|
+
url: external_exports.string().optional().describe("Zillow homedetails URL or path")
|
|
37329
|
+
}
|
|
37330
|
+
},
|
|
37331
|
+
async ({ zpid, url: url2 }) => {
|
|
37332
|
+
const { raw } = await fetchPropertyRecord(client2, { zpid, url: url2 });
|
|
37333
|
+
const events = (raw.priceHistory ?? []).map(formatPriceEvent);
|
|
37334
|
+
return textResult({ zpid: String(raw.zpid ?? zpid ?? ""), events });
|
|
37335
|
+
}
|
|
37336
|
+
);
|
|
37337
|
+
server2.registerTool(
|
|
37338
|
+
"zillow_get_tax_history",
|
|
37339
|
+
{
|
|
37340
|
+
title: "Get Zillow tax history for a property",
|
|
37341
|
+
description: "Year-by-year property-tax record for a property: tax paid, assessed value, and the year-over-year change rates. Sourced from the homedetails page. Useful for spotting reassessment jumps or comparing tax burdens across properties.",
|
|
37342
|
+
annotations: {
|
|
37343
|
+
title: "Get Zillow tax history for a property",
|
|
37344
|
+
readOnlyHint: true,
|
|
37345
|
+
idempotentHint: true,
|
|
37346
|
+
openWorldHint: true
|
|
37347
|
+
},
|
|
37348
|
+
inputSchema: {
|
|
37349
|
+
zpid: external_exports.union([external_exports.number().int().positive(), external_exports.string()]).optional().describe("Zillow Property ID"),
|
|
37350
|
+
url: external_exports.string().optional().describe("Zillow homedetails URL or path")
|
|
37351
|
+
}
|
|
37352
|
+
},
|
|
37353
|
+
async ({ zpid, url: url2 }) => {
|
|
37354
|
+
const { raw } = await fetchPropertyRecord(client2, { zpid, url: url2 });
|
|
37355
|
+
const events = (raw.taxHistory ?? []).map(formatTaxEvent);
|
|
37356
|
+
return textResult({ zpid: String(raw.zpid ?? zpid ?? ""), events });
|
|
37357
|
+
}
|
|
37358
|
+
);
|
|
37359
|
+
}
|
|
37360
|
+
|
|
37361
|
+
// src/tools/compare.ts
|
|
37362
|
+
function buildSummary(rows) {
|
|
37363
|
+
const pick2 = (label, fn) => ({
|
|
37364
|
+
field: label,
|
|
37365
|
+
values: rows.map((r) => r.property ? fn(r.property) ?? null : null)
|
|
37366
|
+
});
|
|
37367
|
+
return [
|
|
37368
|
+
pick2("price", (p) => p.price),
|
|
37369
|
+
pick2("zestimate", (p) => p.zestimate),
|
|
37370
|
+
pick2("rent_zestimate", (p) => p.rent_zestimate),
|
|
37371
|
+
pick2("beds", (p) => p.beds),
|
|
37372
|
+
pick2("baths", (p) => p.baths),
|
|
37373
|
+
pick2("living_area_sqft", (p) => p.living_area),
|
|
37374
|
+
pick2("lot_size_sqft", (p) => p.lot_size),
|
|
37375
|
+
pick2("year_built", (p) => p.year_built),
|
|
37376
|
+
pick2("home_type", (p) => p.home_type),
|
|
37377
|
+
pick2("status", (p) => p.status),
|
|
37378
|
+
pick2("days_on_zillow", (p) => p.days_on_zillow),
|
|
37379
|
+
pick2("tax_assessed_value", (p) => p.tax_assessed_value),
|
|
37380
|
+
pick2("neighborhood", (p) => p.neighborhood)
|
|
37381
|
+
];
|
|
37382
|
+
}
|
|
37383
|
+
function registerCompareTools(server2, client2) {
|
|
37384
|
+
server2.registerTool(
|
|
37385
|
+
"zillow_compare_properties",
|
|
37386
|
+
{
|
|
37387
|
+
title: "Compare multiple Zillow properties side-by-side",
|
|
37388
|
+
description: "Fetch and compare 2 or more Zillow properties side-by-side. Provide an array of zpids (or homedetails URLs). Returns a compact summary table aligned by field (price, beds/baths, sqft, year built, Zestimate, etc.) plus the full per-property record. Errors for individual properties are captured per-row \u2014 one bad zpid won't fail the whole call. Calls are concurrent.",
|
|
37389
|
+
annotations: {
|
|
37390
|
+
title: "Compare multiple Zillow properties side-by-side",
|
|
37391
|
+
readOnlyHint: true,
|
|
37392
|
+
idempotentHint: true,
|
|
37393
|
+
openWorldHint: true
|
|
37394
|
+
},
|
|
37395
|
+
inputSchema: {
|
|
37396
|
+
zpids: external_exports.array(external_exports.union([external_exports.number().int().positive(), external_exports.string()])).min(2).max(8).optional().describe(
|
|
37397
|
+
"Array of 2\u20138 zpids to compare. Provide either zpids or urls."
|
|
37398
|
+
),
|
|
37399
|
+
urls: external_exports.array(external_exports.string()).min(2).max(8).optional().describe(
|
|
37400
|
+
"Array of 2\u20138 Zillow homedetails URLs/paths to compare. Provide either zpids or urls."
|
|
37401
|
+
)
|
|
37402
|
+
}
|
|
37403
|
+
},
|
|
37404
|
+
async ({ zpids, urls }) => {
|
|
37405
|
+
const targets = zpids && zpids.length > 0 ? zpids.map((zpid) => ({ zpid })) : urls && urls.length > 0 ? urls.map((url2) => ({ url: url2 })) : null;
|
|
37406
|
+
if (!targets || targets.length < 2) {
|
|
37407
|
+
throw new Error(
|
|
37408
|
+
"zillow_compare_properties: provide an array of at least 2 zpids or urls."
|
|
37409
|
+
);
|
|
37410
|
+
}
|
|
37411
|
+
const results = await Promise.all(
|
|
37412
|
+
targets.map(async (t) => {
|
|
37413
|
+
const fallbackZpid = "zpid" in t ? String(t.zpid) : "";
|
|
37414
|
+
try {
|
|
37415
|
+
const { raw } = await fetchPropertyRecord(client2, t);
|
|
37416
|
+
return {
|
|
37417
|
+
zpid: String(raw.zpid ?? fallbackZpid),
|
|
37418
|
+
property: format(raw)
|
|
37419
|
+
};
|
|
37420
|
+
} catch (e) {
|
|
37421
|
+
return { zpid: fallbackZpid, error: e.message };
|
|
37422
|
+
}
|
|
37423
|
+
})
|
|
37424
|
+
);
|
|
37425
|
+
return textResult({
|
|
37426
|
+
count: results.length,
|
|
37427
|
+
summary: buildSummary(results),
|
|
37428
|
+
results
|
|
37429
|
+
});
|
|
37430
|
+
}
|
|
37431
|
+
);
|
|
37432
|
+
}
|
|
37433
|
+
|
|
37434
|
+
// src/tools/affordability.ts
|
|
37435
|
+
function computeAffordability(input) {
|
|
37436
|
+
if (input.monthly_income <= 0)
|
|
37437
|
+
throw new Error("monthly_income must be positive");
|
|
37438
|
+
if (input.down_payment < 0) throw new Error("down_payment must be >= 0");
|
|
37439
|
+
if (input.interest_rate < 0)
|
|
37440
|
+
throw new Error("interest_rate must be >= 0");
|
|
37441
|
+
const term_years = input.loan_term_years ?? 30;
|
|
37442
|
+
const monthly_debts = input.monthly_debts ?? 0;
|
|
37443
|
+
const front_dti = input.front_end_dti ?? 0.28;
|
|
37444
|
+
const back_dti = input.back_end_dti ?? 0.36;
|
|
37445
|
+
const tax_rate = input.property_tax_rate ?? 1.1;
|
|
37446
|
+
const insurance_annual = input.insurance_annual ?? 0;
|
|
37447
|
+
const hoa_monthly = input.hoa_monthly ?? 0;
|
|
37448
|
+
const front_max = input.monthly_income * front_dti;
|
|
37449
|
+
const back_max = input.monthly_income * back_dti - monthly_debts;
|
|
37450
|
+
const max_piti = Math.max(0, Math.min(front_max, back_max));
|
|
37451
|
+
const binding = front_max <= back_max ? "front_end" : "back_end";
|
|
37452
|
+
const monthly_ins = insurance_annual / 12;
|
|
37453
|
+
const monthly_tax_per_dollar = tax_rate / 100 / 12;
|
|
37454
|
+
const monthly_pi_budget = Math.max(
|
|
37455
|
+
0,
|
|
37456
|
+
max_piti - monthly_ins - hoa_monthly
|
|
37457
|
+
);
|
|
37458
|
+
const r = input.interest_rate / 100 / 12;
|
|
37459
|
+
const n = term_years * 12;
|
|
37460
|
+
const factor = r === 0 ? 1 / n : r * Math.pow(1 + r, n) / (Math.pow(1 + r, n) - 1);
|
|
37461
|
+
const coeff = monthly_tax_per_dollar + factor;
|
|
37462
|
+
const max_home_price = coeff === 0 ? input.down_payment : (monthly_pi_budget + input.down_payment * factor) / coeff;
|
|
37463
|
+
const loan_amount = Math.max(0, max_home_price - input.down_payment);
|
|
37464
|
+
const monthly_pi = r === 0 ? loan_amount / n : loan_amount * factor;
|
|
37465
|
+
const monthly_tax = max_home_price * monthly_tax_per_dollar;
|
|
37466
|
+
return {
|
|
37467
|
+
max_home_price: round22(max_home_price),
|
|
37468
|
+
max_monthly_piti: round22(max_piti),
|
|
37469
|
+
binding_constraint: binding,
|
|
37470
|
+
monthly_principal_interest: round22(monthly_pi),
|
|
37471
|
+
monthly_property_tax: round22(monthly_tax),
|
|
37472
|
+
monthly_insurance: round22(monthly_ins),
|
|
37473
|
+
monthly_hoa: round22(hoa_monthly),
|
|
37474
|
+
loan_amount: round22(loan_amount),
|
|
37475
|
+
down_payment: round22(input.down_payment),
|
|
37476
|
+
front_end_dti_used: front_dti,
|
|
37477
|
+
back_end_dti_used: back_dti
|
|
37478
|
+
};
|
|
37479
|
+
}
|
|
37480
|
+
function computeRentVsBuy(input) {
|
|
37481
|
+
if (input.home_price <= 0) throw new Error("home_price must be positive");
|
|
37482
|
+
if (input.monthly_rent <= 0)
|
|
37483
|
+
throw new Error("monthly_rent must be positive");
|
|
37484
|
+
const horizon = input.horizon_years ?? 7;
|
|
37485
|
+
if (horizon <= 0) throw new Error("horizon_years must be positive");
|
|
37486
|
+
const term_years = input.loan_term_years ?? 30;
|
|
37487
|
+
const r = input.interest_rate / 100 / 12;
|
|
37488
|
+
const n = term_years * 12;
|
|
37489
|
+
const loan = Math.max(0, input.home_price - input.down_payment);
|
|
37490
|
+
const monthly_pi = r === 0 ? loan / n : loan * (r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);
|
|
37491
|
+
const closing = input.home_price * ((input.closing_cost_rate ?? 2.5) / 100);
|
|
37492
|
+
const selling_rate = (input.selling_cost_rate ?? 6) / 100;
|
|
37493
|
+
const tax_rate = (input.property_tax_rate ?? 1.1) / 100;
|
|
37494
|
+
const insurance = input.insurance_annual ?? 0;
|
|
37495
|
+
const hoa = (input.hoa_monthly ?? 0) * 12;
|
|
37496
|
+
const maint_rate = (input.maintenance_rate ?? 1) / 100;
|
|
37497
|
+
const appreciation = (input.appreciation_rate ?? 3) / 100;
|
|
37498
|
+
const rent_growth = (input.rent_growth_rate ?? 3) / 100;
|
|
37499
|
+
const invest_return = (input.investment_return_rate ?? 6) / 100;
|
|
37500
|
+
let cum_buy = input.down_payment + closing;
|
|
37501
|
+
let cum_rent = 0;
|
|
37502
|
+
let invested = input.down_payment;
|
|
37503
|
+
let home_value = input.home_price;
|
|
37504
|
+
let principal_remaining = loan;
|
|
37505
|
+
const yearly = [];
|
|
37506
|
+
let break_even = null;
|
|
37507
|
+
let final_net_after_sale = 0;
|
|
37508
|
+
for (let y = 1; y <= horizon; y++) {
|
|
37509
|
+
const year_pi = monthly_pi * 12;
|
|
37510
|
+
const year_tax = home_value * tax_rate;
|
|
37511
|
+
const year_maint = home_value * maint_rate;
|
|
37512
|
+
cum_buy += year_pi + year_tax + insurance + hoa + year_maint;
|
|
37513
|
+
let interest_paid = 0;
|
|
37514
|
+
let principal_paid = 0;
|
|
37515
|
+
for (let m = 0; m < 12; m++) {
|
|
37516
|
+
const int_m = principal_remaining * r;
|
|
37517
|
+
const pi_m = Math.min(monthly_pi, principal_remaining + int_m);
|
|
37518
|
+
const prin_m = pi_m - int_m;
|
|
37519
|
+
principal_remaining = Math.max(0, principal_remaining - prin_m);
|
|
37520
|
+
interest_paid += int_m;
|
|
37521
|
+
principal_paid += prin_m;
|
|
37522
|
+
}
|
|
37523
|
+
const year_rent = input.monthly_rent * 12 * Math.pow(1 + rent_growth, y - 1);
|
|
37524
|
+
cum_rent += year_rent;
|
|
37525
|
+
invested = invested * (1 + invest_return);
|
|
37526
|
+
home_value = home_value * (1 + appreciation);
|
|
37527
|
+
const sale_proceeds = home_value * (1 - selling_rate) - principal_remaining;
|
|
37528
|
+
const buy_net_if_sold = cum_buy - sale_proceeds;
|
|
37529
|
+
const renter_invest_growth = invested - input.down_payment;
|
|
37530
|
+
const rent_net = cum_rent - renter_invest_growth;
|
|
37531
|
+
if (break_even === null && buy_net_if_sold <= rent_net) break_even = y;
|
|
37532
|
+
yearly.push({
|
|
37533
|
+
year: y,
|
|
37534
|
+
cumulative_buy_cost: round22(cum_buy),
|
|
37535
|
+
cumulative_rent_cost: round22(cum_rent),
|
|
37536
|
+
home_value: round22(home_value),
|
|
37537
|
+
remaining_mortgage: round22(principal_remaining),
|
|
37538
|
+
equity_if_sold_now: round22(home_value * (1 - selling_rate) - principal_remaining)
|
|
37539
|
+
});
|
|
37540
|
+
if (y === horizon) final_net_after_sale = buy_net_if_sold - rent_net;
|
|
37541
|
+
}
|
|
37542
|
+
return {
|
|
37543
|
+
horizon_years: horizon,
|
|
37544
|
+
buy_total_cost_after_sale: round22(
|
|
37545
|
+
cum_buy - (home_value * (1 - selling_rate) - principal_remaining)
|
|
37546
|
+
),
|
|
37547
|
+
rent_total_cost: round22(cum_rent - (invested - input.down_payment)),
|
|
37548
|
+
net_difference: round22(final_net_after_sale),
|
|
37549
|
+
buy_wins: final_net_after_sale < 0,
|
|
37550
|
+
break_even_year: break_even,
|
|
37551
|
+
yearly
|
|
37552
|
+
};
|
|
37553
|
+
}
|
|
37554
|
+
function round22(n) {
|
|
37555
|
+
return Math.round(n * 100) / 100;
|
|
37556
|
+
}
|
|
37557
|
+
function registerAffordabilityTools(server2) {
|
|
37558
|
+
server2.registerTool(
|
|
37559
|
+
"zillow_calculate_affordability",
|
|
37560
|
+
{
|
|
37561
|
+
title: "Calculate max affordable home price",
|
|
37562
|
+
description: "Solve for the maximum home price you can afford under the standard 28/36 DTI rule. Inputs: monthly income, monthly recurring debts (car loans, student loans, etc.), down payment, interest rate, and optional property-tax rate / insurance / HOA / loan term. Output: max home price, the binding constraint (front-end vs back-end), and the full PITI breakdown at that price. No network \u2014 pure local math.",
|
|
37563
|
+
annotations: {
|
|
37564
|
+
title: "Calculate max affordable home price",
|
|
37565
|
+
readOnlyHint: true,
|
|
37566
|
+
idempotentHint: true,
|
|
37567
|
+
openWorldHint: false
|
|
37568
|
+
},
|
|
37569
|
+
inputSchema: {
|
|
37570
|
+
monthly_income: external_exports.number().positive(),
|
|
37571
|
+
monthly_debts: external_exports.number().nonnegative().optional().describe("Sum of monthly debt payments (car, student loans, etc.)"),
|
|
37572
|
+
down_payment: external_exports.number().nonnegative(),
|
|
37573
|
+
interest_rate: external_exports.number().nonnegative().describe("Annual %, e.g. 6.5"),
|
|
37574
|
+
loan_term_years: external_exports.number().int().positive().optional().describe("Default 30"),
|
|
37575
|
+
property_tax_rate: external_exports.number().nonnegative().optional().describe("Annual % of home price, default 1.1"),
|
|
37576
|
+
insurance_annual: external_exports.number().nonnegative().optional(),
|
|
37577
|
+
hoa_monthly: external_exports.number().nonnegative().optional(),
|
|
37578
|
+
front_end_dti: external_exports.number().positive().max(1).optional().describe("Front-end DTI cap as decimal, default 0.28"),
|
|
37579
|
+
back_end_dti: external_exports.number().positive().max(1).optional().describe("Back-end DTI cap as decimal, default 0.36")
|
|
37580
|
+
}
|
|
37581
|
+
},
|
|
37582
|
+
async (input) => textResult(computeAffordability(input))
|
|
37583
|
+
);
|
|
37584
|
+
server2.registerTool(
|
|
37585
|
+
"zillow_estimate_rent_vs_buy",
|
|
37586
|
+
{
|
|
37587
|
+
title: "Estimate rent-vs-buy break-even over a horizon",
|
|
37588
|
+
description: "Project the cumulative cost of buying a home versus renting a comparable place over N years. Accounts for down payment, closing costs, monthly PITI, maintenance (~1%/yr default), property appreciation (~3%/yr default), rent growth (~3%/yr default), and the opportunity cost of the down payment (renter invests it at the investment_return_rate, default 6%/yr). Returns the year-by-year cumulative costs, the break-even year, and the net difference at the horizon. No network \u2014 pure local math.",
|
|
37589
|
+
annotations: {
|
|
37590
|
+
title: "Estimate rent-vs-buy break-even over a horizon",
|
|
37591
|
+
readOnlyHint: true,
|
|
37592
|
+
idempotentHint: true,
|
|
37593
|
+
openWorldHint: false
|
|
37594
|
+
},
|
|
37595
|
+
inputSchema: {
|
|
37596
|
+
home_price: external_exports.number().positive(),
|
|
37597
|
+
down_payment: external_exports.number().nonnegative(),
|
|
37598
|
+
interest_rate: external_exports.number().nonnegative(),
|
|
37599
|
+
loan_term_years: external_exports.number().int().positive().optional(),
|
|
37600
|
+
property_tax_rate: external_exports.number().nonnegative().optional(),
|
|
37601
|
+
insurance_annual: external_exports.number().nonnegative().optional(),
|
|
37602
|
+
hoa_monthly: external_exports.number().nonnegative().optional(),
|
|
37603
|
+
maintenance_rate: external_exports.number().nonnegative().optional().describe("Annual % of home value, default 1.0"),
|
|
37604
|
+
closing_cost_rate: external_exports.number().nonnegative().optional().describe("% of home price, default 2.5"),
|
|
37605
|
+
selling_cost_rate: external_exports.number().nonnegative().optional().describe("% of sale price, default 6.0"),
|
|
37606
|
+
appreciation_rate: external_exports.number().optional().describe("Annual %, default 3.0"),
|
|
37607
|
+
monthly_rent: external_exports.number().positive(),
|
|
37608
|
+
rent_growth_rate: external_exports.number().optional().describe("Annual %, default 3.0"),
|
|
37609
|
+
investment_return_rate: external_exports.number().optional().describe("Annual return on the renter's parallel-invested down payment, default 6.0"),
|
|
37610
|
+
horizon_years: external_exports.number().int().positive().optional().describe("Default 7")
|
|
37611
|
+
}
|
|
37612
|
+
},
|
|
37613
|
+
async (input) => textResult(computeRentVsBuy(input))
|
|
37614
|
+
);
|
|
37615
|
+
}
|
|
37616
|
+
|
|
37268
37617
|
// src/index.ts
|
|
37269
|
-
var VERSION = "0.
|
|
37618
|
+
var VERSION = "0.3.0";
|
|
37270
37619
|
var port = process.env.ZILLOW_WS_PORT ? Number(process.env.ZILLOW_WS_PORT) : void 0;
|
|
37271
37620
|
var transport = new FetchproxyTransport({ port, version: VERSION });
|
|
37272
37621
|
var client = new ZillowClient({ transport });
|
|
@@ -37278,6 +37627,9 @@ registerZestimateTools(server, client);
|
|
|
37278
37627
|
registerSavedTools(server, client);
|
|
37279
37628
|
registerMarketTools(server, client);
|
|
37280
37629
|
registerMortgageTools(server);
|
|
37630
|
+
registerHistoryTools(server, client);
|
|
37631
|
+
registerCompareTools(server, client);
|
|
37632
|
+
registerAffordabilityTools(server);
|
|
37281
37633
|
console.error(
|
|
37282
37634
|
`[zillow-mcp] v${VERSION} \u2014 WebSocket bridge via @fetchproxy/server on 127.0.0.1:${port ?? 37149}. Install the fetchproxy extension (see https://github.com/chrischall/fetchproxy) and sign into zillow.com. This project was developed and is maintained by AI (Claude). Use at your own discretion.`
|
|
37283
37635
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zillow-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/zillow-mcp",
|
|
5
5
|
"description": "Zillow MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/zillow-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.3.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "zillow-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.3.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|