@striderlabs/mcp-zillow 1.0.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/README.md +188 -0
- package/package.json +29 -0
- package/src/index.ts +651 -0
- package/tsconfig.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# @striderlabs/mcp-zillow
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server for Zillow real estate search and listings. Search for homes, get property details, retrieve Zestimates, and search rental listings — all from within Claude or any MCP-compatible client.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### 1. Install the package
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @striderlabs/mcp-zillow
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 2. Install Playwright Chromium browser
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx playwright install chromium
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## MCP Configuration
|
|
20
|
+
|
|
21
|
+
### Claude Desktop
|
|
22
|
+
|
|
23
|
+
Add to your `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"zillow": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["mcp-zillow"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or if installed globally:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"zillow": {
|
|
42
|
+
"command": "mcp-zillow"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Tools
|
|
49
|
+
|
|
50
|
+
### `search_homes`
|
|
51
|
+
|
|
52
|
+
Search Zillow for homes for sale by location with optional filters.
|
|
53
|
+
|
|
54
|
+
**Parameters:**
|
|
55
|
+
| Parameter | Type | Required | Description |
|
|
56
|
+
|-----------|------|----------|-------------|
|
|
57
|
+
| `location` | string | Yes | City, state, zip code, or neighborhood (e.g. "Seattle, WA" or "90210") |
|
|
58
|
+
| `min_price` | number | No | Minimum listing price in dollars |
|
|
59
|
+
| `max_price` | number | No | Maximum listing price in dollars |
|
|
60
|
+
| `min_beds` | number | No | Minimum number of bedrooms |
|
|
61
|
+
| `max_beds` | number | No | Maximum number of bedrooms |
|
|
62
|
+
| `min_baths` | number | No | Minimum number of bathrooms |
|
|
63
|
+
| `home_type` | string | No | One of: `single_family`, `condo`, `townhouse`, `multi_family`, `land`, `mobile` |
|
|
64
|
+
| `max_results` | number | No | Maximum results to return (default: 10) |
|
|
65
|
+
|
|
66
|
+
**Example:**
|
|
67
|
+
```
|
|
68
|
+
Search for 3-bedroom homes in Austin, TX under $500,000
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### `get_listing`
|
|
74
|
+
|
|
75
|
+
Get detailed information about a specific Zillow listing including price history, school ratings, tax history, and full property details.
|
|
76
|
+
|
|
77
|
+
**Parameters:**
|
|
78
|
+
| Parameter | Type | Required | Description |
|
|
79
|
+
|-----------|------|----------|-------------|
|
|
80
|
+
| `url` | string | No* | Full Zillow listing URL |
|
|
81
|
+
| `zpid` | string | No* | Zillow property ID |
|
|
82
|
+
|
|
83
|
+
*At least one of `url` or `zpid` is required.
|
|
84
|
+
|
|
85
|
+
**Example:**
|
|
86
|
+
```
|
|
87
|
+
Get details for Zillow property zpid 12345678
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### `get_estimates`
|
|
93
|
+
|
|
94
|
+
Get the Zestimate (Zillow's estimated market value) and rent estimate for a specific address.
|
|
95
|
+
|
|
96
|
+
**Parameters:**
|
|
97
|
+
| Parameter | Type | Required | Description |
|
|
98
|
+
|-----------|------|----------|-------------|
|
|
99
|
+
| `address` | string | Yes | Full property address (e.g. "123 Main St, Seattle, WA 98101") |
|
|
100
|
+
|
|
101
|
+
**Example:**
|
|
102
|
+
```
|
|
103
|
+
What is the Zestimate for 1600 Pennsylvania Ave NW, Washington, DC 20500?
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### `save_home`
|
|
109
|
+
|
|
110
|
+
Save a home to Zillow favorites. Note: This requires Zillow authentication and currently returns instructions for manual saving.
|
|
111
|
+
|
|
112
|
+
**Parameters:**
|
|
113
|
+
| Parameter | Type | Required | Description |
|
|
114
|
+
|-----------|------|----------|-------------|
|
|
115
|
+
| `zpid` | string | Yes | Zillow property ID to save |
|
|
116
|
+
| `note` | string | No | Optional note to attach |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### `search_rentals`
|
|
121
|
+
|
|
122
|
+
Search Zillow for rental properties by location with optional filters.
|
|
123
|
+
|
|
124
|
+
**Parameters:**
|
|
125
|
+
| Parameter | Type | Required | Description |
|
|
126
|
+
|-----------|------|----------|-------------|
|
|
127
|
+
| `location` | string | Yes | City, state, zip code, or neighborhood (e.g. "Austin, TX") |
|
|
128
|
+
| `min_price` | number | No | Minimum monthly rent in dollars |
|
|
129
|
+
| `max_price` | number | No | Maximum monthly rent in dollars |
|
|
130
|
+
| `min_beds` | number | No | Minimum number of bedrooms |
|
|
131
|
+
| `max_beds` | number | No | Maximum number of bedrooms |
|
|
132
|
+
| `max_results` | number | No | Maximum results to return (default: 10) |
|
|
133
|
+
|
|
134
|
+
**Example:**
|
|
135
|
+
```
|
|
136
|
+
Find 2-bedroom apartments for rent in Brooklyn, NY under $3000/month
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Example Usage
|
|
140
|
+
|
|
141
|
+
Once configured in Claude Desktop, you can ask questions like:
|
|
142
|
+
|
|
143
|
+
- "Find homes for sale in Denver, CO with at least 3 bedrooms under $600,000"
|
|
144
|
+
- "What are rental prices like in San Francisco for 1-bedroom apartments?"
|
|
145
|
+
- "Get the Zestimate and details for 123 Main St, Portland, OR 97201"
|
|
146
|
+
- "Show me condos for sale in Miami Beach under $400,000"
|
|
147
|
+
- "Search for townhouses in Chicago with 2+ baths between $300,000 and $500,000"
|
|
148
|
+
|
|
149
|
+
## How It Works
|
|
150
|
+
|
|
151
|
+
This MCP server uses Playwright to automate a headless Chromium browser to access Zillow. It:
|
|
152
|
+
|
|
153
|
+
1. Launches a stealth-configured browser (mimics real user behavior)
|
|
154
|
+
2. Navigates to relevant Zillow pages
|
|
155
|
+
3. Extracts data from Zillow's embedded `__NEXT_DATA__` JSON
|
|
156
|
+
4. Falls back to CSS selector parsing if JSON is unavailable
|
|
157
|
+
5. Returns structured data to the MCP client
|
|
158
|
+
|
|
159
|
+
## Notes
|
|
160
|
+
|
|
161
|
+
- **Rate limiting**: Zillow may rate-limit or block automated requests. The server uses stealth techniques to minimize this.
|
|
162
|
+
- **Authentication**: Some features (saving homes) require a Zillow account. The server operates in anonymous mode for search and data retrieval.
|
|
163
|
+
- **Data freshness**: Data is fetched live from Zillow on each request.
|
|
164
|
+
- **Playwright**: The server requires Chromium to be installed via `npx playwright install chromium`.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Clone the repo
|
|
170
|
+
git clone https://github.com/markswendsen-code/mcp-zillow.git
|
|
171
|
+
cd mcp-zillow
|
|
172
|
+
|
|
173
|
+
# Install dependencies
|
|
174
|
+
npm install
|
|
175
|
+
|
|
176
|
+
# Install Playwright browser
|
|
177
|
+
npx playwright install chromium
|
|
178
|
+
|
|
179
|
+
# Build
|
|
180
|
+
npm run build
|
|
181
|
+
|
|
182
|
+
# Run in development mode
|
|
183
|
+
npm run dev
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@striderlabs/mcp-zillow",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Zillow real estate search and listings",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-zillow": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"mcpName": "io.github.markswendsen-code/mcp-zillow",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --external:playwright",
|
|
12
|
+
"dev": "ts-node src/index.ts",
|
|
13
|
+
"pack": "npm run build && npm pack"
|
|
14
|
+
},
|
|
15
|
+
"keywords": ["mcp", "zillow", "real-estate", "playwright"],
|
|
16
|
+
"author": "StriiderLabs",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
20
|
+
"playwright": "^1.40.0",
|
|
21
|
+
"playwright-extra": "^4.3.6",
|
|
22
|
+
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.0.0",
|
|
26
|
+
"esbuild": "^0.19.0",
|
|
27
|
+
"typescript": "^5.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { chromium, Browser, Page } from "playwright";
|
|
9
|
+
|
|
10
|
+
// Server setup
|
|
11
|
+
const server = new Server(
|
|
12
|
+
{
|
|
13
|
+
name: "io.github.markswendsen-code/mcp-zillow",
|
|
14
|
+
version: "1.0.0",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
capabilities: {
|
|
18
|
+
tools: {},
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
let browser: Browser | null = null;
|
|
24
|
+
|
|
25
|
+
async function getBrowser(): Promise<Browser> {
|
|
26
|
+
if (!browser) {
|
|
27
|
+
browser = await chromium.launch({
|
|
28
|
+
headless: true,
|
|
29
|
+
args: [
|
|
30
|
+
"--no-sandbox",
|
|
31
|
+
"--disable-setuid-sandbox",
|
|
32
|
+
"--disable-dev-shm-usage",
|
|
33
|
+
"--disable-accelerated-2d-canvas",
|
|
34
|
+
"--no-first-run",
|
|
35
|
+
"--no-zygote",
|
|
36
|
+
"--disable-gpu",
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return browser;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function getStealthPage(browser: Browser): Promise<Page> {
|
|
44
|
+
const context = await browser.newContext({
|
|
45
|
+
userAgent:
|
|
46
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
47
|
+
viewport: { width: 1280, height: 800 },
|
|
48
|
+
extraHTTPHeaders: {
|
|
49
|
+
Accept:
|
|
50
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
51
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
52
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
53
|
+
Connection: "keep-alive",
|
|
54
|
+
"Upgrade-Insecure-Requests": "1",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const page = await context.newPage();
|
|
59
|
+
|
|
60
|
+
// Remove webdriver flag
|
|
61
|
+
await page.addInitScript(() => {
|
|
62
|
+
Object.defineProperty(navigator, "webdriver", {
|
|
63
|
+
get: () => undefined,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return page;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function dismissPopups(page: Page): Promise<void> {
|
|
71
|
+
// Try to dismiss cookie consent
|
|
72
|
+
const cookieSelectors = [
|
|
73
|
+
'button[data-test="cookie-consent-accept"]',
|
|
74
|
+
'button:has-text("Accept")',
|
|
75
|
+
'button:has-text("Accept All")',
|
|
76
|
+
"#onetrust-accept-btn-handler",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const selector of cookieSelectors) {
|
|
80
|
+
try {
|
|
81
|
+
const el = await page.$(selector);
|
|
82
|
+
if (el) {
|
|
83
|
+
await el.click();
|
|
84
|
+
await page.waitForTimeout(500);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Try to dismiss sign-in modal
|
|
91
|
+
const closeSelectors = [
|
|
92
|
+
'button[aria-label="Close"]',
|
|
93
|
+
'button[data-test="modal-close"]',
|
|
94
|
+
".modal-close",
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
for (const selector of closeSelectors) {
|
|
98
|
+
try {
|
|
99
|
+
const el = await page.$(selector);
|
|
100
|
+
if (el) {
|
|
101
|
+
await el.click();
|
|
102
|
+
await page.waitForTimeout(500);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function extractNextData(page: Page): Promise<any> {
|
|
110
|
+
try {
|
|
111
|
+
const data = await page.evaluate(() => {
|
|
112
|
+
const script = document.getElementById("__NEXT_DATA__");
|
|
113
|
+
if (script) {
|
|
114
|
+
return JSON.parse(script.textContent || "{}");
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
});
|
|
118
|
+
return data;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Tool: search_homes
|
|
125
|
+
async function searchHomes(params: {
|
|
126
|
+
location: string;
|
|
127
|
+
min_price?: number;
|
|
128
|
+
max_price?: number;
|
|
129
|
+
min_beds?: number;
|
|
130
|
+
max_beds?: number;
|
|
131
|
+
min_baths?: number;
|
|
132
|
+
home_type?: string;
|
|
133
|
+
max_results?: number;
|
|
134
|
+
}): Promise<any> {
|
|
135
|
+
const browser = await getBrowser();
|
|
136
|
+
const page = await getStealthPage(browser);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const location = params.location.replace(/\s+/g, "-").replace(/,/g, "");
|
|
140
|
+
let url = `https://www.zillow.com/homes/for_sale/${encodeURIComponent(location)}_rb/`;
|
|
141
|
+
|
|
142
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
143
|
+
await page.waitForTimeout(2000);
|
|
144
|
+
await dismissPopups(page);
|
|
145
|
+
|
|
146
|
+
const nextData = await extractNextData(page);
|
|
147
|
+
const results: any[] = [];
|
|
148
|
+
|
|
149
|
+
if (nextData?.props?.pageProps?.searchPageState?.cat1?.searchResults?.listResults) {
|
|
150
|
+
const listings =
|
|
151
|
+
nextData.props.pageProps.searchPageState.cat1.searchResults.listResults;
|
|
152
|
+
|
|
153
|
+
for (const listing of listings.slice(0, params.max_results || 10)) {
|
|
154
|
+
if (!listing.zpid) continue;
|
|
155
|
+
|
|
156
|
+
// Apply filters
|
|
157
|
+
if (params.min_price && listing.price < params.min_price) continue;
|
|
158
|
+
if (params.max_price && listing.price > params.max_price) continue;
|
|
159
|
+
if (params.min_beds && listing.beds < params.min_beds) continue;
|
|
160
|
+
if (params.max_beds && listing.beds > params.max_beds) continue;
|
|
161
|
+
if (params.min_baths && listing.baths < params.min_baths) continue;
|
|
162
|
+
|
|
163
|
+
results.push({
|
|
164
|
+
zpid: listing.zpid,
|
|
165
|
+
address: listing.address,
|
|
166
|
+
price: listing.price,
|
|
167
|
+
beds: listing.beds,
|
|
168
|
+
baths: listing.baths,
|
|
169
|
+
sqft: listing.area,
|
|
170
|
+
home_type: listing.homeType,
|
|
171
|
+
listing_url: listing.detailUrl
|
|
172
|
+
? `https://www.zillow.com${listing.detailUrl}`
|
|
173
|
+
: null,
|
|
174
|
+
days_on_zillow: listing.daysOnZillow,
|
|
175
|
+
broker: listing.brokerName,
|
|
176
|
+
latitude: listing.latLong?.latitude,
|
|
177
|
+
longitude: listing.latLong?.longitude,
|
|
178
|
+
zestimate: listing.zestimate,
|
|
179
|
+
img_url: listing.imgSrc,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (results.length === 0) {
|
|
185
|
+
// Try alternate data path
|
|
186
|
+
const altListings =
|
|
187
|
+
nextData?.props?.pageProps?.searchPageState?.cat1?.searchResults?.mapResults;
|
|
188
|
+
if (altListings) {
|
|
189
|
+
for (const listing of altListings.slice(0, params.max_results || 10)) {
|
|
190
|
+
if (!listing.zpid) continue;
|
|
191
|
+
results.push({
|
|
192
|
+
zpid: listing.zpid,
|
|
193
|
+
address: listing.address,
|
|
194
|
+
price: listing.price,
|
|
195
|
+
beds: listing.beds,
|
|
196
|
+
baths: listing.baths,
|
|
197
|
+
sqft: listing.area,
|
|
198
|
+
home_type: listing.homeType,
|
|
199
|
+
listing_url: listing.detailUrl
|
|
200
|
+
? `https://www.zillow.com${listing.detailUrl}`
|
|
201
|
+
: null,
|
|
202
|
+
zestimate: listing.zestimate,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
location: params.location,
|
|
210
|
+
total_found: results.length,
|
|
211
|
+
listings: results,
|
|
212
|
+
};
|
|
213
|
+
} finally {
|
|
214
|
+
await page.context().close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Tool: get_listing
|
|
219
|
+
async function getListing(params: {
|
|
220
|
+
url?: string;
|
|
221
|
+
zpid?: string;
|
|
222
|
+
}): Promise<any> {
|
|
223
|
+
const browser = await getBrowser();
|
|
224
|
+
const page = await getStealthPage(browser);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
let url = params.url;
|
|
228
|
+
if (!url && params.zpid) {
|
|
229
|
+
url = `https://www.zillow.com/homedetails/${params.zpid}_zpid/`;
|
|
230
|
+
}
|
|
231
|
+
if (!url) throw new Error("Either url or zpid is required");
|
|
232
|
+
|
|
233
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
234
|
+
await page.waitForTimeout(2000);
|
|
235
|
+
await dismissPopups(page);
|
|
236
|
+
|
|
237
|
+
const nextData = await extractNextData(page);
|
|
238
|
+
|
|
239
|
+
let listing: any = {};
|
|
240
|
+
|
|
241
|
+
if (nextData?.props?.pageProps) {
|
|
242
|
+
const props = nextData.props.pageProps;
|
|
243
|
+
const home = props.componentProps?.gdpClientCache
|
|
244
|
+
? Object.values(props.componentProps.gdpClientCache)[0]
|
|
245
|
+
: props.homeDetails;
|
|
246
|
+
|
|
247
|
+
if (home?.property) {
|
|
248
|
+
const p = home.property;
|
|
249
|
+
listing = {
|
|
250
|
+
zpid: p.zpid,
|
|
251
|
+
address: {
|
|
252
|
+
street: p.streetAddress,
|
|
253
|
+
city: p.city,
|
|
254
|
+
state: p.state,
|
|
255
|
+
zip: p.zipcode,
|
|
256
|
+
full: `${p.streetAddress}, ${p.city}, ${p.state} ${p.zipcode}`,
|
|
257
|
+
},
|
|
258
|
+
price: p.price,
|
|
259
|
+
zestimate: p.zestimate,
|
|
260
|
+
rent_zestimate: p.rentZestimate,
|
|
261
|
+
beds: p.bedrooms,
|
|
262
|
+
baths: p.bathrooms,
|
|
263
|
+
sqft: p.livingArea,
|
|
264
|
+
lot_size: p.lotAreaValue,
|
|
265
|
+
lot_size_unit: p.lotAreaUnit,
|
|
266
|
+
home_type: p.homeType,
|
|
267
|
+
year_built: p.yearBuilt,
|
|
268
|
+
heating: p.heating,
|
|
269
|
+
cooling: p.cooling,
|
|
270
|
+
parking: p.parkingFeatures,
|
|
271
|
+
description: p.description,
|
|
272
|
+
listing_status: p.homeStatus,
|
|
273
|
+
days_on_zillow: p.daysOnZillow,
|
|
274
|
+
views: p.pageViewCount,
|
|
275
|
+
saves: p.favoriteCount,
|
|
276
|
+
hoa_fee: p.monthlyHoaFee,
|
|
277
|
+
tax_history: p.taxHistory?.slice(0, 3),
|
|
278
|
+
price_history: p.priceHistory?.slice(0, 5),
|
|
279
|
+
school_ratings: p.schools?.slice(0, 3).map((s: any) => ({
|
|
280
|
+
name: s.name,
|
|
281
|
+
rating: s.rating,
|
|
282
|
+
level: s.level,
|
|
283
|
+
distance: s.distance,
|
|
284
|
+
})),
|
|
285
|
+
listing_url: url,
|
|
286
|
+
images: p.photos?.slice(0, 5).map((ph: any) => ph.url),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return listing;
|
|
292
|
+
} finally {
|
|
293
|
+
await page.context().close();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Tool: get_estimates
|
|
298
|
+
async function getEstimates(params: { address: string }): Promise<any> {
|
|
299
|
+
const browser = await getBrowser();
|
|
300
|
+
const page = await getStealthPage(browser);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Use page to make the request
|
|
304
|
+
await page.goto(
|
|
305
|
+
`https://www.zillow.com/homes/${encodeURIComponent(params.address)}/`,
|
|
306
|
+
{ waitUntil: "domcontentloaded", timeout: 30000 }
|
|
307
|
+
);
|
|
308
|
+
await page.waitForTimeout(2000);
|
|
309
|
+
await dismissPopups(page);
|
|
310
|
+
|
|
311
|
+
const nextData = await extractNextData(page);
|
|
312
|
+
|
|
313
|
+
let estimate: any = {
|
|
314
|
+
address: params.address,
|
|
315
|
+
zestimate: null,
|
|
316
|
+
rent_zestimate: null,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
if (nextData?.props?.pageProps) {
|
|
320
|
+
const props = nextData.props.pageProps;
|
|
321
|
+
// Try to find property data
|
|
322
|
+
if (props.componentProps?.gdpClientCache) {
|
|
323
|
+
const cacheData = Object.values(props.componentProps.gdpClientCache)[0] as any;
|
|
324
|
+
if (cacheData?.property) {
|
|
325
|
+
const p = cacheData.property;
|
|
326
|
+
estimate = {
|
|
327
|
+
address: params.address,
|
|
328
|
+
full_address: `${p.streetAddress}, ${p.city}, ${p.state} ${p.zipcode}`,
|
|
329
|
+
zestimate: p.zestimate,
|
|
330
|
+
zestimate_range: p.zestimateLowPercent && p.zestimateHighPercent ? {
|
|
331
|
+
low: Math.round(p.zestimate * (1 - p.zestimateLowPercent / 100)),
|
|
332
|
+
high: Math.round(p.zestimate * (1 + p.zestimateHighPercent / 100)),
|
|
333
|
+
} : null,
|
|
334
|
+
rent_zestimate: p.rentZestimate,
|
|
335
|
+
last_sold_price: p.lastSoldPrice,
|
|
336
|
+
last_sold_date: p.lastSoldDate,
|
|
337
|
+
price_per_sqft: p.resoFacts?.pricePerSquareFoot,
|
|
338
|
+
sqft: p.livingArea,
|
|
339
|
+
beds: p.bedrooms,
|
|
340
|
+
baths: p.bathrooms,
|
|
341
|
+
year_built: p.yearBuilt,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return estimate;
|
|
348
|
+
} finally {
|
|
349
|
+
await page.context().close();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Tool: save_home
|
|
354
|
+
async function saveHome(params: {
|
|
355
|
+
zpid: string;
|
|
356
|
+
note?: string;
|
|
357
|
+
}): Promise<any> {
|
|
358
|
+
// This requires authentication - return helpful message
|
|
359
|
+
return {
|
|
360
|
+
success: false,
|
|
361
|
+
message:
|
|
362
|
+
"Saving homes requires Zillow authentication. Please log in to Zillow at https://www.zillow.com and use the save feature directly. This MCP server operates in anonymous mode for search and data retrieval.",
|
|
363
|
+
zpid: params.zpid,
|
|
364
|
+
zillow_url: `https://www.zillow.com/homedetails/${params.zpid}_zpid/`,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Tool: search_rentals
|
|
369
|
+
async function searchRentals(params: {
|
|
370
|
+
location: string;
|
|
371
|
+
min_price?: number;
|
|
372
|
+
max_price?: number;
|
|
373
|
+
min_beds?: number;
|
|
374
|
+
max_beds?: number;
|
|
375
|
+
max_results?: number;
|
|
376
|
+
}): Promise<any> {
|
|
377
|
+
const browser = await getBrowser();
|
|
378
|
+
const page = await getStealthPage(browser);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const location = params.location.replace(/\s+/g, "-").replace(/,/g, "");
|
|
382
|
+
const url = `https://www.zillow.com/homes/for_rent/${encodeURIComponent(location)}_rb/`;
|
|
383
|
+
|
|
384
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
385
|
+
await page.waitForTimeout(2000);
|
|
386
|
+
await dismissPopups(page);
|
|
387
|
+
|
|
388
|
+
const nextData = await extractNextData(page);
|
|
389
|
+
const results: any[] = [];
|
|
390
|
+
|
|
391
|
+
const searchResults =
|
|
392
|
+
nextData?.props?.pageProps?.searchPageState?.cat1?.searchResults?.listResults ||
|
|
393
|
+
nextData?.props?.pageProps?.searchPageState?.cat1?.searchResults?.mapResults ||
|
|
394
|
+
[];
|
|
395
|
+
|
|
396
|
+
for (const listing of searchResults.slice(0, params.max_results || 10)) {
|
|
397
|
+
if (!listing.zpid) continue;
|
|
398
|
+
|
|
399
|
+
if (params.min_price && listing.price < params.min_price) continue;
|
|
400
|
+
if (params.max_price && listing.price > params.max_price) continue;
|
|
401
|
+
if (params.min_beds && listing.beds < params.min_beds) continue;
|
|
402
|
+
if (params.max_beds && listing.beds > params.max_beds) continue;
|
|
403
|
+
|
|
404
|
+
results.push({
|
|
405
|
+
zpid: listing.zpid,
|
|
406
|
+
address: listing.address,
|
|
407
|
+
monthly_rent: listing.price,
|
|
408
|
+
beds: listing.beds,
|
|
409
|
+
baths: listing.baths,
|
|
410
|
+
sqft: listing.area,
|
|
411
|
+
home_type: listing.homeType,
|
|
412
|
+
listing_url: listing.detailUrl
|
|
413
|
+
? `https://www.zillow.com${listing.detailUrl}`
|
|
414
|
+
: null,
|
|
415
|
+
days_on_zillow: listing.daysOnZillow,
|
|
416
|
+
broker: listing.brokerName,
|
|
417
|
+
img_url: listing.imgSrc,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
location: params.location,
|
|
423
|
+
total_found: results.length,
|
|
424
|
+
rentals: results,
|
|
425
|
+
};
|
|
426
|
+
} finally {
|
|
427
|
+
await page.context().close();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Register tools
|
|
432
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
433
|
+
return {
|
|
434
|
+
tools: [
|
|
435
|
+
{
|
|
436
|
+
name: "search_homes",
|
|
437
|
+
description:
|
|
438
|
+
"Search Zillow for homes for sale by location with optional filters for price, bedrooms, bathrooms, and home type",
|
|
439
|
+
inputSchema: {
|
|
440
|
+
type: "object",
|
|
441
|
+
properties: {
|
|
442
|
+
location: {
|
|
443
|
+
type: "string",
|
|
444
|
+
description:
|
|
445
|
+
'Location to search (city, state, zip code, or neighborhood). Example: "Seattle, WA" or "90210"',
|
|
446
|
+
},
|
|
447
|
+
min_price: {
|
|
448
|
+
type: "number",
|
|
449
|
+
description: "Minimum listing price in dollars",
|
|
450
|
+
},
|
|
451
|
+
max_price: {
|
|
452
|
+
type: "number",
|
|
453
|
+
description: "Maximum listing price in dollars",
|
|
454
|
+
},
|
|
455
|
+
min_beds: {
|
|
456
|
+
type: "number",
|
|
457
|
+
description: "Minimum number of bedrooms",
|
|
458
|
+
},
|
|
459
|
+
max_beds: {
|
|
460
|
+
type: "number",
|
|
461
|
+
description: "Maximum number of bedrooms",
|
|
462
|
+
},
|
|
463
|
+
min_baths: {
|
|
464
|
+
type: "number",
|
|
465
|
+
description: "Minimum number of bathrooms",
|
|
466
|
+
},
|
|
467
|
+
home_type: {
|
|
468
|
+
type: "string",
|
|
469
|
+
enum: [
|
|
470
|
+
"single_family",
|
|
471
|
+
"condo",
|
|
472
|
+
"townhouse",
|
|
473
|
+
"multi_family",
|
|
474
|
+
"land",
|
|
475
|
+
"mobile",
|
|
476
|
+
],
|
|
477
|
+
description: "Type of home",
|
|
478
|
+
},
|
|
479
|
+
max_results: {
|
|
480
|
+
type: "number",
|
|
481
|
+
description: "Maximum number of results to return (default: 10)",
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
required: ["location"],
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: "get_listing",
|
|
489
|
+
description:
|
|
490
|
+
"Get detailed information about a specific Zillow listing including price history, school ratings, and property details",
|
|
491
|
+
inputSchema: {
|
|
492
|
+
type: "object",
|
|
493
|
+
properties: {
|
|
494
|
+
url: {
|
|
495
|
+
type: "string",
|
|
496
|
+
description: "Full Zillow listing URL",
|
|
497
|
+
},
|
|
498
|
+
zpid: {
|
|
499
|
+
type: "string",
|
|
500
|
+
description: "Zillow property ID (zpid)",
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: "save_home",
|
|
507
|
+
description:
|
|
508
|
+
"Save a home to your Zillow favorites (requires Zillow account - returns instructions if not authenticated)",
|
|
509
|
+
inputSchema: {
|
|
510
|
+
type: "object",
|
|
511
|
+
properties: {
|
|
512
|
+
zpid: {
|
|
513
|
+
type: "string",
|
|
514
|
+
description: "Zillow property ID (zpid) to save",
|
|
515
|
+
},
|
|
516
|
+
note: {
|
|
517
|
+
type: "string",
|
|
518
|
+
description: "Optional note to attach to the saved home",
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
required: ["zpid"],
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: "get_estimates",
|
|
526
|
+
description:
|
|
527
|
+
"Get Zestimate (Zillow's estimated market value) and rent estimate for a specific address",
|
|
528
|
+
inputSchema: {
|
|
529
|
+
type: "object",
|
|
530
|
+
properties: {
|
|
531
|
+
address: {
|
|
532
|
+
type: "string",
|
|
533
|
+
description:
|
|
534
|
+
'Full property address. Example: "123 Main St, Seattle, WA 98101"',
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
required: ["address"],
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "search_rentals",
|
|
542
|
+
description:
|
|
543
|
+
"Search Zillow for rental properties by location with optional filters for price and bedrooms",
|
|
544
|
+
inputSchema: {
|
|
545
|
+
type: "object",
|
|
546
|
+
properties: {
|
|
547
|
+
location: {
|
|
548
|
+
type: "string",
|
|
549
|
+
description:
|
|
550
|
+
'Location to search (city, state, zip code, or neighborhood). Example: "Austin, TX"',
|
|
551
|
+
},
|
|
552
|
+
min_price: {
|
|
553
|
+
type: "number",
|
|
554
|
+
description: "Minimum monthly rent in dollars",
|
|
555
|
+
},
|
|
556
|
+
max_price: {
|
|
557
|
+
type: "number",
|
|
558
|
+
description: "Maximum monthly rent in dollars",
|
|
559
|
+
},
|
|
560
|
+
min_beds: {
|
|
561
|
+
type: "number",
|
|
562
|
+
description: "Minimum number of bedrooms",
|
|
563
|
+
},
|
|
564
|
+
max_beds: {
|
|
565
|
+
type: "number",
|
|
566
|
+
description: "Maximum number of bedrooms",
|
|
567
|
+
},
|
|
568
|
+
max_results: {
|
|
569
|
+
type: "number",
|
|
570
|
+
description: "Maximum number of results to return (default: 10)",
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
required: ["location"],
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
};
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
581
|
+
const { name, arguments: args } = request.params;
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
let result: any;
|
|
585
|
+
|
|
586
|
+
switch (name) {
|
|
587
|
+
case "search_homes":
|
|
588
|
+
result = await searchHomes(args as any);
|
|
589
|
+
break;
|
|
590
|
+
case "get_listing":
|
|
591
|
+
result = await getListing(args as any);
|
|
592
|
+
break;
|
|
593
|
+
case "save_home":
|
|
594
|
+
result = await saveHome(args as any);
|
|
595
|
+
break;
|
|
596
|
+
case "get_estimates":
|
|
597
|
+
result = await getEstimates(args as any);
|
|
598
|
+
break;
|
|
599
|
+
case "search_rentals":
|
|
600
|
+
result = await searchRentals(args as any);
|
|
601
|
+
break;
|
|
602
|
+
default:
|
|
603
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
content: [
|
|
608
|
+
{
|
|
609
|
+
type: "text",
|
|
610
|
+
text: JSON.stringify(result, null, 2),
|
|
611
|
+
},
|
|
612
|
+
],
|
|
613
|
+
};
|
|
614
|
+
} catch (error: any) {
|
|
615
|
+
return {
|
|
616
|
+
content: [
|
|
617
|
+
{
|
|
618
|
+
type: "text",
|
|
619
|
+
text: JSON.stringify({
|
|
620
|
+
error: error.message,
|
|
621
|
+
tool: name,
|
|
622
|
+
}),
|
|
623
|
+
},
|
|
624
|
+
],
|
|
625
|
+
isError: true,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Cleanup on exit
|
|
631
|
+
process.on("SIGINT", async () => {
|
|
632
|
+
if (browser) await browser.close();
|
|
633
|
+
process.exit(0);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
process.on("SIGTERM", async () => {
|
|
637
|
+
if (browser) await browser.close();
|
|
638
|
+
process.exit(0);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Start server
|
|
642
|
+
async function main() {
|
|
643
|
+
const transport = new StdioServerTransport();
|
|
644
|
+
await server.connect(transport);
|
|
645
|
+
console.error("Zillow MCP server running on stdio");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
main().catch((error) => {
|
|
649
|
+
console.error("Fatal error:", error);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|