@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 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
+ }