iflow-mcp_sap156-zillow-mcp-server 1.0.0__tar.gz → 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iflow-mcp_sap156-zillow-mcp-server
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Zillow MCP Server for real estate data access via the Model Context Protocol
5
5
  Author-email: Sai Abhinav Parvathaneni <neni.abhinav@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iflow-mcp_sap156-zillow-mcp-server
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Zillow MCP Server for real estate data access via the Model Context Protocol
5
5
  Author-email: Sai Abhinav Parvathaneni <neni.abhinav@gmail.com>
6
6
  License: MIT
@@ -1,5 +1,6 @@
1
1
  README.md
2
2
  pyproject.toml
3
+ zillow_mcp_server.py
3
4
  iflow_mcp_sap156_zillow_mcp_server.egg-info/PKG-INFO
4
5
  iflow_mcp_sap156_zillow_mcp_server.egg-info/SOURCES.txt
5
6
  iflow_mcp_sap156_zillow_mcp_server.egg-info/dependency_links.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "iflow-mcp_sap156-zillow-mcp-server"
7
- version = "1.0.0"
7
+ version = "1.0.2"
8
8
  description = "Zillow MCP Server for real estate data access via the Model Context Protocol"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -48,9 +48,8 @@ Homepage = "https://github.com/sap156/zillow-mcp-server"
48
48
  Repository = "https://github.com/sap156/zillow-mcp-server"
49
49
  Issues = "https://github.com/sap156/zillow-mcp-server/issues"
50
50
 
51
- [tool.setuptools.packages.find]
52
- where = ["."]
53
- include = ["*"]
51
+ [tool.setuptools]
52
+ py-modules = ["zillow_mcp_server"]
54
53
 
55
54
  [tool.setuptools.package-data]
56
- "*" = ["README.md", "LICENSE"]
55
+ "*" = ["README.md", "LICENSE"]
@@ -0,0 +1,844 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Zillow MCP Server
4
+
5
+ This server provides access to Zillow real estate data through the Model Context Protocol (MCP).
6
+ It connects to Zillow's Bridge API to retrieve property information, market trends, and more.
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import logging
12
+ import time
13
+ import backoff
14
+ from typing import Dict, List, Optional, Any, Union
15
+ from datetime import datetime
16
+ import requests
17
+ from requests.adapters import HTTPAdapter
18
+ from urllib3.util.retry import Retry
19
+ from fastmcp import FastMCP, Context
20
+ from dotenv import load_dotenv
21
+
22
+ # Configure logging
23
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
24
+ logger = logging.getLogger("zillow-mcp")
25
+
26
+ # Load environment variables
27
+ load_dotenv()
28
+
29
+ # Initialize the MCP server
30
+ mcp = FastMCP("Zillow-Data-Server")
31
+
32
+ # Get API key from environment variables
33
+ ZILLOW_API_KEY = os.getenv("ZILLOW_API_KEY")
34
+ ZILLOW_API_BASE_URL = "https://api.bridgeinteractive.com/v1"
35
+
36
+ # Set up a session with retries and timeouts
37
+ session = requests.Session()
38
+ retry_strategy = Retry(
39
+ total=3,
40
+ backoff_factor=0.5,
41
+ status_forcelist=[429, 500, 502, 503, 504],
42
+ allowed_methods=["GET", "POST"]
43
+ )
44
+ adapter = HTTPAdapter(max_retries=retry_strategy)
45
+ session.mount("http://", adapter)
46
+ session.mount("https://", adapter)
47
+
48
+ # Helper function to make API requests to Zillow Bridge API
49
+ @backoff.on_exception(backoff.expo, (requests.exceptions.RequestException, ValueError), max_tries=5)
50
+ def zillow_api_request(endpoint: str, params: Dict = None, method: str = "GET") -> Dict:
51
+ """Make a request to the Zillow Bridge API with retries and error handling."""
52
+ if not ZILLOW_API_KEY:
53
+ raise ValueError("Zillow API key not found. Please set the ZILLOW_API_KEY environment variable.")
54
+
55
+ headers = {
56
+ "Authorization": f"Bearer {ZILLOW_API_KEY}",
57
+ "Content-Type": "application/json",
58
+ "Accept": "application/json",
59
+ "User-Agent": "Zillow-MCP-Server/1.0"
60
+ }
61
+
62
+ url = f"{ZILLOW_API_BASE_URL}/{endpoint}"
63
+
64
+ try:
65
+ logger.info(f"Making {method} request to {endpoint}")
66
+
67
+ if method.upper() == "GET":
68
+ response = session.get(url, headers=headers, params=params, timeout=30)
69
+ elif method.upper() == "POST":
70
+ response = session.post(url, headers=headers, json=params, timeout=30)
71
+ else:
72
+ raise ValueError(f"Unsupported HTTP method: {method}")
73
+
74
+ # Log response status
75
+ logger.info(f"Response status code: {response.status_code}")
76
+
77
+ # Handle rate limiting
78
+ if response.status_code == 429:
79
+ retry_after = int(response.headers.get('Retry-After', 60))
80
+ logger.warning(f"Rate limited. Retrying after {retry_after} seconds.")
81
+ time.sleep(retry_after)
82
+ return zillow_api_request(endpoint, params, method)
83
+
84
+ response.raise_for_status()
85
+
86
+ # Parse response
87
+ response_data = response.json()
88
+
89
+ # Basic validation
90
+ if not response_data:
91
+ raise ValueError("Empty response from Zillow API")
92
+
93
+ # Log success
94
+ logger.info(f"Successfully received data from {endpoint}")
95
+
96
+ return response_data
97
+ except requests.exceptions.Timeout:
98
+ logger.error(f"Request to {endpoint} timed out")
99
+ raise ValueError("Zillow API request timed out. Please try again later.")
100
+ except requests.exceptions.HTTPError as e:
101
+ logger.error(f"HTTP error: {e}")
102
+ error_msg = f"Zillow API HTTP error: {e}"
103
+ # Try to get more error details from response
104
+ try:
105
+ error_data = response.json()
106
+ if 'error' in error_data:
107
+ error_msg += f" - {error_data['error']}"
108
+ except:
109
+ pass
110
+ raise ValueError(error_msg)
111
+ except requests.exceptions.RequestException as e:
112
+ logger.error(f"API request failed: {e}")
113
+ raise ValueError(f"Zillow API request failed: {str(e)}")
114
+
115
+ # Define MCP tools for Zillow data access
116
+
117
+ @mcp.tool()
118
+ def search_properties(
119
+ location: str,
120
+ type: str = "forSale",
121
+ min_price: Optional[int] = None,
122
+ max_price: Optional[int] = None,
123
+ beds_min: Optional[int] = None,
124
+ beds_max: Optional[int] = None,
125
+ baths_min: Optional[float] = None,
126
+ baths_max: Optional[float] = None,
127
+ home_types: Optional[List[str]] = None,
128
+ ctx: Context = None
129
+ ) -> Dict:
130
+ """
131
+ Search for properties on Zillow based on criteria.
132
+
133
+ Args:
134
+ location: Address, city, ZIP code, or neighborhood
135
+ type: Property listing type - "forSale", "forRent", or "sold"
136
+ min_price: Minimum price in dollars
137
+ max_price: Maximum price in dollars
138
+ beds_min: Minimum number of bedrooms
139
+ beds_max: Maximum number of bedrooms
140
+ baths_min: Minimum number of bathrooms
141
+ baths_max: Maximum number of bathrooms
142
+ home_types: List of home types (e.g., ["house", "condo", "apartment"])
143
+
144
+ Returns:
145
+ Dictionary with property listings matching the criteria
146
+ """
147
+ if ctx:
148
+ ctx.info(f"Searching for {type} properties in {location}")
149
+
150
+ params = {
151
+ "location": location,
152
+ "type": type,
153
+ }
154
+
155
+ # Add optional parameters if provided
156
+ if min_price is not None:
157
+ params["price_min"] = min_price
158
+ if max_price is not None:
159
+ params["price_max"] = max_price
160
+ if beds_min is not None:
161
+ params["beds_min"] = beds_min
162
+ if beds_max is not None:
163
+ params["beds_max"] = beds_max
164
+ if baths_min is not None:
165
+ params["baths_min"] = baths_min
166
+ if baths_max is not None:
167
+ params["baths_max"] = baths_max
168
+ if home_types is not None:
169
+ params["home_types"] = home_types
170
+
171
+ try:
172
+ # Make the actual API call to Zillow Bridge API
173
+ response = zillow_api_request("properties/search", params)
174
+
175
+ # Process the response from the API
176
+ if not response or 'properties' not in response:
177
+ raise ValueError("No properties found or invalid API response")
178
+
179
+ properties = response.get('properties', [])
180
+
181
+ # Apply any additional filtering if needed
182
+ if min_price is not None:
183
+ properties = [p for p in properties if p.get('price', 0) >= min_price]
184
+ if max_price is not None:
185
+ properties = [p for p in properties if p.get('price', 0) <= max_price]
186
+ if beds_min is not None:
187
+ properties = [p for p in properties if p.get('bedrooms', 0) >= beds_min]
188
+ if beds_max is not None:
189
+ properties = [p for p in properties if p.get('bedrooms', 0) <= beds_max]
190
+ if baths_min is not None:
191
+ properties = [p for p in properties if p.get('bathrooms', 0) >= baths_min]
192
+ if baths_max is not None:
193
+ properties = [p for p in properties if p.get('bathrooms', 0) <= baths_max]
194
+ if home_types is not None:
195
+ properties = [p for p in properties if p.get('home_type', '').lower() in [t.lower() for t in home_types]]
196
+
197
+ return {
198
+ "success": True,
199
+ "count": len(properties),
200
+ "properties": properties,
201
+ "searchCriteria": params,
202
+ "metadata": {
203
+ "timestamp": datetime.now().isoformat(),
204
+ "source": "Zillow Data Server"
205
+ }
206
+ }
207
+ except Exception as e:
208
+ logger.error(f"Property search failed: {e}")
209
+ return {
210
+ "success": False,
211
+ "error": str(e),
212
+ "searchCriteria": params
213
+ }
214
+
215
+ @mcp.tool()
216
+ def get_property_details(property_id: str = None, address: str = None) -> Dict:
217
+ """
218
+ Get detailed information about a property by ID or address.
219
+
220
+ Args:
221
+ property_id: Zillow property ID (zpid)
222
+ address: Full property address
223
+
224
+ Returns:
225
+ Detailed property information
226
+ """
227
+ if not property_id and not address:
228
+ raise ValueError("Either property_id or address must be provided")
229
+
230
+ params = {}
231
+ if property_id:
232
+ params["zpid"] = property_id
233
+ else:
234
+ params["address"] = address
235
+
236
+ try:
237
+ # Make the actual API call to Zillow Bridge API
238
+ response = zillow_api_request("properties/details", params)
239
+
240
+ # Process the response from the API
241
+ if not response or 'property' not in response:
242
+ raise ValueError("Property not found or invalid API response")
243
+
244
+ property_data = response.get('property', {})
245
+
246
+ return {
247
+ "success": True,
248
+ "property": property_data,
249
+ "metadata": {
250
+ "timestamp": datetime.now().isoformat(),
251
+ "source": "Zillow Data Server"
252
+ }
253
+ }
254
+ except Exception as e:
255
+ logger.error(f"Property details lookup failed: {e}")
256
+ return {
257
+ "success": False,
258
+ "error": str(e),
259
+ "searchCriteria": params
260
+ }
261
+
262
+ @mcp.tool()
263
+ def get_zestimate(property_id: str = None, address: str = None) -> Dict:
264
+ """
265
+ Get Zillow's estimated value (Zestimate) for a property.
266
+
267
+ Args:
268
+ property_id: Zillow property ID (zpid)
269
+ address: Full property address
270
+
271
+ Returns:
272
+ Zestimate information including current value and historical data
273
+ """
274
+ if not property_id and not address:
275
+ raise ValueError("Either property_id or address must be provided")
276
+
277
+ params = {}
278
+ if property_id:
279
+ params["zpid"] = property_id
280
+ else:
281
+ params["address"] = address
282
+
283
+ try:
284
+ # Make the actual API call to Zillow Bridge API
285
+ response = zillow_api_request("zestimates", params)
286
+
287
+ # Process the response from the API
288
+ if not response or 'zestimate' not in response:
289
+ raise ValueError("Zestimate not found or invalid API response")
290
+
291
+ zestimate_data = response.get('zestimate', {})
292
+
293
+ return {
294
+ "success": True,
295
+ "zestimate": zestimate_data,
296
+ "metadata": {
297
+ "timestamp": datetime.now().isoformat(),
298
+ "source": "Zillow Data Server"
299
+ }
300
+ }
301
+ except Exception as e:
302
+ logger.error(f"Zestimate lookup failed: {e}")
303
+ return {
304
+ "success": False,
305
+ "error": str(e),
306
+ "searchCriteria": params
307
+ }
308
+
309
+ @mcp.tool()
310
+ def get_market_trends(
311
+ location: str,
312
+ metrics: List[str] = ["median_list_price", "median_sale_price", "median_days_on_market"],
313
+ time_period: str = "1year" # Options: "1month", "3months", "6months", "1year", "5years", "10years", "all"
314
+ ) -> Dict:
315
+ """
316
+ Get real estate market trends for a specific location.
317
+
318
+ Args:
319
+ location: City, ZIP code, or neighborhood
320
+ metrics: List of metrics to retrieve
321
+ time_period: Time period for historical data
322
+
323
+ Returns:
324
+ Market trend data for the specified location and metrics
325
+ """
326
+ params = {
327
+ "location": location,
328
+ "metrics": metrics,
329
+ "time_period": time_period
330
+ }
331
+
332
+ try:
333
+ # Make the actual API call to Zillow Bridge API
334
+ api_params = {
335
+ "location": location,
336
+ "metrics": metrics,
337
+ "time_period": time_period
338
+ }
339
+ response = zillow_api_request("market/trends", api_params)
340
+
341
+ # Process the response from the API
342
+ if not response or 'trends' not in response:
343
+ raise ValueError("Market trends not found or invalid API response")
344
+
345
+ trend_data = response.get('trends', {})
346
+
347
+ return {
348
+ "success": True,
349
+ "location": location,
350
+ "trends": trend_data,
351
+ "time_period": time_period,
352
+ "metadata": {
353
+ "timestamp": datetime.now().isoformat(),
354
+ "source": "Zillow Data Server"
355
+ }
356
+ }
357
+ except Exception as e:
358
+ logger.error(f"Market trends lookup failed: {e}")
359
+ return {
360
+ "success": False,
361
+ "error": str(e),
362
+ "searchCriteria": params
363
+ }
364
+
365
+ @mcp.tool()
366
+ def calculate_mortgage(
367
+ home_price: int,
368
+ down_payment: int = None,
369
+ down_payment_percent: float = None,
370
+ loan_term: int = 30, # Years
371
+ interest_rate: float = 6.5, # Percentage
372
+ annual_property_tax: int = None,
373
+ annual_homeowners_insurance: int = None,
374
+ monthly_hoa: int = 0,
375
+ include_pmi: bool = True
376
+ ) -> Dict:
377
+ """
378
+ Calculate mortgage payments and related costs.
379
+
380
+ Args:
381
+ home_price: Price of the home in dollars
382
+ down_payment: Down payment amount in dollars
383
+ down_payment_percent: Down payment as a percentage of home price
384
+ loan_term: Loan term in years
385
+ interest_rate: Annual interest rate as a percentage
386
+ annual_property_tax: Annual property tax in dollars
387
+ annual_homeowners_insurance: Annual homeowners insurance in dollars
388
+ monthly_hoa: Monthly HOA fees in dollars
389
+ include_pmi: Whether to include PMI for down payments less than 20%
390
+
391
+ Returns:
392
+ Dictionary with mortgage payment details
393
+ """
394
+ # Calculate down payment if not provided
395
+ if down_payment is None and down_payment_percent is None:
396
+ down_payment_percent = 20.0
397
+
398
+ if down_payment is None:
399
+ down_payment = int(home_price * (down_payment_percent / 100))
400
+ else:
401
+ down_payment_percent = (down_payment / home_price) * 100
402
+
403
+ # Calculate loan amount
404
+ loan_amount = home_price - down_payment
405
+
406
+ # Calculate monthly interest rate
407
+ monthly_interest_rate = (interest_rate / 100) / 12
408
+
409
+ # Calculate total number of payments
410
+ total_payments = loan_term * 12
411
+
412
+ # Calculate principal and interest payment
413
+ if monthly_interest_rate > 0:
414
+ monthly_principal_interest = loan_amount * (monthly_interest_rate * (1 + monthly_interest_rate) ** total_payments) / ((1 + monthly_interest_rate) ** total_payments - 1)
415
+ else:
416
+ monthly_principal_interest = loan_amount / total_payments
417
+
418
+ # Calculate PMI (Private Mortgage Insurance)
419
+ monthly_pmi = 0
420
+ if include_pmi and down_payment_percent < 20:
421
+ # Typical PMI is around 0.5% to 1% of the loan amount annually
422
+ monthly_pmi = (loan_amount * 0.007) / 12
423
+
424
+ # Calculate property tax payment if provided
425
+ monthly_property_tax = 0
426
+ if annual_property_tax is not None:
427
+ monthly_property_tax = annual_property_tax / 12
428
+ else:
429
+ # Estimate property tax if not provided (varies by location, using 1.1% as average)
430
+ annual_property_tax = int(home_price * 0.011)
431
+ monthly_property_tax = annual_property_tax / 12
432
+
433
+ # Calculate homeowners insurance payment if provided
434
+ monthly_homeowners_insurance = 0
435
+ if annual_homeowners_insurance is not None:
436
+ monthly_homeowners_insurance = annual_homeowners_insurance / 12
437
+ else:
438
+ # Estimate homeowners insurance if not provided
439
+ annual_homeowners_insurance = int(home_price * 0.0035)
440
+ monthly_homeowners_insurance = annual_homeowners_insurance / 12
441
+
442
+ # Calculate total monthly payment
443
+ monthly_payment = monthly_principal_interest + monthly_pmi + monthly_property_tax + monthly_homeowners_insurance + monthly_hoa
444
+
445
+ # Calculate total cost over the life of the loan
446
+ total_cost = (monthly_payment * total_payments) + down_payment
447
+
448
+ return {
449
+ "success": True,
450
+ "mortgage_details": {
451
+ "home_price": home_price,
452
+ "down_payment": down_payment,
453
+ "down_payment_percent": round(down_payment_percent, 2),
454
+ "loan_amount": loan_amount,
455
+ "loan_term_years": loan_term,
456
+ "interest_rate": interest_rate,
457
+ "monthly_payment": round(monthly_payment, 2),
458
+ "monthly_principal_interest": round(monthly_principal_interest, 2),
459
+ "monthly_property_tax": round(monthly_property_tax, 2),
460
+ "monthly_homeowners_insurance": round(monthly_homeowners_insurance, 2),
461
+ "monthly_hoa": monthly_hoa,
462
+ "monthly_pmi": round(monthly_pmi, 2),
463
+ "total_interest_paid": round((monthly_principal_interest * total_payments) - loan_amount, 2),
464
+ "total_payments": round(monthly_payment * total_payments, 2),
465
+ "total_cost": round(total_cost, 2)
466
+ },
467
+ "metadata": {
468
+ "timestamp": datetime.now().isoformat(),
469
+ "source": "Zillow Mortgage Calculator"
470
+ }
471
+ }
472
+
473
+ @mcp.tool()
474
+ def get_server_tools() -> Dict:
475
+ """
476
+ Get a list of all available tools on this server.
477
+
478
+ Returns:
479
+ Dictionary with information about all available tools
480
+ """
481
+ tools = [
482
+ {
483
+ "name": "search_properties",
484
+ "description": "Search for properties on Zillow based on criteria",
485
+ "parameters": {
486
+ "location": "Address, city, ZIP code, or neighborhood",
487
+ "type": "Property listing type - 'forSale', 'forRent', or 'sold'",
488
+ "min_price": "Minimum price in dollars",
489
+ "max_price": "Maximum price in dollars",
490
+ "beds_min": "Minimum number of bedrooms",
491
+ "beds_max": "Maximum number of bedrooms",
492
+ "baths_min": "Minimum number of bathrooms",
493
+ "baths_max": "Maximum number of bathrooms",
494
+ "home_types": "List of home types (e.g., ['house', 'condo', 'apartment'])"
495
+ }
496
+ },
497
+ {
498
+ "name": "get_property_details",
499
+ "description": "Get detailed information about a property by ID or address",
500
+ "parameters": {
501
+ "property_id": "Zillow property ID (zpid)",
502
+ "address": "Full property address"
503
+ }
504
+ },
505
+ {
506
+ "name": "get_zestimate",
507
+ "description": "Get Zillow's estimated value (Zestimate) for a property",
508
+ "parameters": {
509
+ "property_id": "Zillow property ID (zpid)",
510
+ "address": "Full property address"
511
+ }
512
+ },
513
+ {
514
+ "name": "get_market_trends",
515
+ "description": "Get real estate market trends for a specific location",
516
+ "parameters": {
517
+ "location": "City, ZIP code, or neighborhood",
518
+ "metrics": "List of metrics to retrieve",
519
+ "time_period": "Time period for historical data"
520
+ }
521
+ },
522
+ {
523
+ "name": "calculate_mortgage",
524
+ "description": "Calculate mortgage payments and related costs",
525
+ "parameters": {
526
+ "home_price": "Price of the home in dollars",
527
+ "down_payment": "Down payment amount in dollars",
528
+ "down_payment_percent": "Down payment as a percentage of home price",
529
+ "loan_term": "Loan term in years",
530
+ "interest_rate": "Annual interest rate as a percentage",
531
+ "annual_property_tax": "Annual property tax in dollars",
532
+ "annual_homeowners_insurance": "Annual homeowners insurance in dollars",
533
+ "monthly_hoa": "Monthly HOA fees in dollars",
534
+ "include_pmi": "Whether to include PMI for down payments less than 20%"
535
+ }
536
+ },
537
+ {
538
+ "name": "check_health",
539
+ "description": "Check the health and status of the Zillow API connection",
540
+ "parameters": {}
541
+ },
542
+ {
543
+ "name": "get_server_tools",
544
+ "description": "Get a list of all available tools on this server",
545
+ "parameters": {}
546
+ }
547
+ ]
548
+
549
+ resources = [
550
+ {
551
+ "name": "zillow://property/{property_id}",
552
+ "description": "Get property information as a formatted text resource",
553
+ "parameters": {
554
+ "property_id": "Zillow property ID (zpid)"
555
+ }
556
+ },
557
+ {
558
+ "name": "zillow://market-trends/{location}",
559
+ "description": "Get market trends information as a formatted text resource",
560
+ "parameters": {
561
+ "location": "City, ZIP code, or neighborhood"
562
+ }
563
+ }
564
+ ]
565
+
566
+ @mcp.tool()
567
+ def check_health() -> Dict:
568
+ """
569
+ Check the health and status of the Zillow API connection.
570
+
571
+ Returns:
572
+ Dictionary with health status information
573
+ """
574
+ start_time = datetime.now()
575
+ status = {
576
+ "success": False,
577
+ "api_available": False,
578
+ "response_time_ms": 0,
579
+ "timestamp": start_time.isoformat(),
580
+ "version": "1.0.0"
581
+ }
582
+
583
+ try:
584
+ # Perform a lightweight API call to check connection
585
+ response = zillow_api_request("health", method="GET")
586
+
587
+ # Calculate response time
588
+ end_time = datetime.now()
589
+ response_time = (end_time - start_time).total_seconds() * 1000 # ms
590
+
591
+ # Update status
592
+ status.update({
593
+ "success": True,
594
+ "api_available": True,
595
+ "response_time_ms": round(response_time, 2),
596
+ "zillow_api_status": response.get("status", "OK"),
597
+ "api_version": response.get("version", "unknown")
598
+ })
599
+ except Exception as e:
600
+ # API call failed
601
+ end_time = datetime.now()
602
+ response_time = (end_time - start_time).total_seconds() * 1000 # ms
603
+
604
+ # Update status with error info
605
+ status.update({
606
+ "success": False,
607
+ "api_available": False,
608
+ "response_time_ms": round(response_time, 2),
609
+ "error": str(e)
610
+ })
611
+
612
+ return status
613
+
614
+ @mcp.resource("zillow://property/{property_id}")
615
+ def get_property_resource(property_id: str) -> str:
616
+ """
617
+ Get property information as a resource.
618
+
619
+ Args:
620
+ property_id: Zillow property ID (zpid)
621
+
622
+ Returns:
623
+ Property information as a formatted string
624
+ """
625
+ try:
626
+ # Get property details directly from API
627
+ params = {"zpid": property_id} if property_id else {"address": property_id}
628
+ response = zillow_api_request("properties/details", params)
629
+
630
+ if not response or not response.get('success', False):
631
+ error = response.get('error', 'Unknown error')
632
+ return f"Error retrieving property information: {error}"
633
+
634
+ property_info = response.get('property', {})
635
+
636
+ # Format property information as a string - handle potentially missing fields
637
+ info = [f"# Property Details for {property_info.get('address', 'Unknown Address')}"]
638
+
639
+ # Add basic property details with safety checks
640
+ if 'price' in property_info:
641
+ info.append(f"- **Price**: ${property_info['price']:,}")
642
+ if 'zestimate' in property_info:
643
+ info.append(f"- **Zestimate**: ${property_info['zestimate']:,}")
644
+ if 'bedrooms' in property_info:
645
+ info.append(f"- **Bedrooms**: {property_info['bedrooms']}")
646
+ if 'bathrooms' in property_info:
647
+ info.append(f"- **Bathrooms**: {property_info['bathrooms']}")
648
+ if 'sqft' in property_info:
649
+ info.append(f"- **Square Feet**: {property_info['sqft']:,}")
650
+ if 'year_built' in property_info:
651
+ info.append(f"- **Year Built**: {property_info['year_built']}")
652
+ if 'lot_size' in property_info:
653
+ info.append(f"- **Lot Size**: {property_info['lot_size']} acres")
654
+ if 'home_type' in property_info:
655
+ info.append(f"- **Home Type**: {property_info['home_type']}")
656
+ if 'last_sold_date' in property_info and 'last_sold_price' in property_info:
657
+ info.append(f"- **Last Sold**: {property_info['last_sold_date']} for ${property_info['last_sold_price']:,}")
658
+
659
+ # Add features if available
660
+ if 'features' in property_info and property_info['features']:
661
+ info.extend(["", "## Features"])
662
+ features_list = property_info.get('features', [])
663
+ if features_list:
664
+ info.append("- " + "\n- ".join(features_list))
665
+
666
+ # Add schools if available
667
+ if 'schools' in property_info and property_info['schools']:
668
+ info.extend(["", "## Schools"])
669
+ for school in property_info.get('schools', []):
670
+ name = school.get('name', 'Unknown School')
671
+ level = school.get('level', 'Unknown Level')
672
+ rating = school.get('rating', 'N/A')
673
+ distance = school.get('distance', 'Unknown')
674
+ info.append(f"- **{name}** ({level}): Rating {rating}/10, {distance} miles away")
675
+
676
+ # Add neighborhood info if available
677
+ neighborhood_info = []
678
+ if 'neighborhood' in property_info:
679
+ neighborhood_info.append(f"- **Neighborhood**: {property_info['neighborhood']}")
680
+ if 'walk_score' in property_info:
681
+ neighborhood_info.append(f"- **Walk Score**: {property_info['walk_score']}/100")
682
+ if 'transit_score' in property_info:
683
+ neighborhood_info.append(f"- **Transit Score**: {property_info['transit_score']}/100")
684
+
685
+ if neighborhood_info:
686
+ info.extend(["", "## Neighborhood"])
687
+ info.extend(neighborhood_info)
688
+
689
+ # Add Zillow link if available
690
+ if 'url' in property_info:
691
+ info.extend(["", f"View on Zillow: {property_info['url']}"])
692
+
693
+ return "\n".join(info)
694
+ except Exception as e:
695
+ logger.error(f"Property resource lookup failed: {e}")
696
+ return f"Error: {str(e)}"
697
+
698
+ @mcp.resource("zillow://market-trends/{location}")
699
+ def get_market_trends_resource(location: str) -> str:
700
+ """
701
+ Get market trends information as a resource.
702
+
703
+ Args:
704
+ location: City, ZIP code, or neighborhood
705
+
706
+ Returns:
707
+ Market trends information as a formatted string
708
+ """
709
+ try:
710
+ # Make direct API call to get market trends
711
+ api_params = {
712
+ "location": location,
713
+ "metrics": ["median_list_price", "median_sale_price", "median_days_on_market"],
714
+ "time_period": "1year"
715
+ }
716
+ response = zillow_api_request("market/trends", api_params)
717
+
718
+ if not response or not response.get('success', False):
719
+ error = response.get('error', 'Unknown error')
720
+ return f"Error retrieving market trends: {error}"
721
+
722
+ trends = response.get('trends', {})
723
+
724
+ # Format trends information as a string - handle potentially missing data
725
+ info = [
726
+ f"# Real Estate Market Trends for {location}",
727
+ "",
728
+ "## Current Market Overview"
729
+ ]
730
+
731
+ # Add current metrics with safety checks
732
+ if 'median_list_price' in trends and 'current' in trends['median_list_price'] and 'change_1year' in trends['median_list_price']:
733
+ info.append(f"- **Median Listing Price**: ${trends['median_list_price']['current']:,} ({trends['median_list_price']['change_1year']:+.1f}% year-over-year)")
734
+
735
+ if 'median_sale_price' in trends and 'current' in trends['median_sale_price'] and 'change_1year' in trends['median_sale_price']:
736
+ info.append(f"- **Median Sale Price**: ${trends['median_sale_price']['current']:,} ({trends['median_sale_price']['change_1year']:+.1f}% year-over-year)")
737
+
738
+ if 'median_days_on_market' in trends and 'current' in trends['median_days_on_market'] and 'change_1year' in trends['median_days_on_market']:
739
+ info.append(f"- **Median Days on Market**: {trends['median_days_on_market']['current']} days ({trends['median_days_on_market']['change_1year']:+.0f} days year-over-year)")
740
+
741
+ # Add historical section header if we have historical data
742
+ has_historical = any('historical' in metric_data for metric_data in trends.values() if isinstance(metric_data, dict))
743
+ if has_historical:
744
+ info.append("")
745
+ info.append("## Historical Trends (Last 12 Months)")
746
+
747
+ # Add historical data for each metric
748
+ for metric_name, metric_data in trends.items():
749
+ if not isinstance(metric_data, dict) or 'historical' not in metric_data:
750
+ continue
751
+
752
+ if metric_name == "median_list_price":
753
+ display_name = "Median Listing Price"
754
+ prefix = "$"
755
+ suffix = ""
756
+ elif metric_name == "median_sale_price":
757
+ display_name = "Median Sale Price"
758
+ prefix = "$"
759
+ suffix = ""
760
+ else: # median_days_on_market
761
+ display_name = "Median Days on Market"
762
+ prefix = ""
763
+ suffix = " days"
764
+
765
+ info.append(f"\n### {display_name}")
766
+ for point in metric_data.get("historical", []):
767
+ if 'date' in point and 'value' in point:
768
+ info.append(f"- {point['date']}: {prefix}{point['value']:,}{suffix}")
769
+
770
+ return "\n".join(info)
771
+ except Exception as e:
772
+ logger.error(f"Market trends resource lookup failed: {e}")
773
+ return f"Error: {str(e)}"
774
+
775
+ if __name__ == "__main__":
776
+ import argparse
777
+
778
+ # Set up command line arguments
779
+ parser = argparse.ArgumentParser(description="Zillow MCP Server")
780
+ parser.add_argument("--http", action="store_true", help="Run as HTTP server instead of stdio")
781
+ parser.add_argument("--host", type=str, default="127.0.0.1", help="Host for HTTP server")
782
+ parser.add_argument("--port", type=int, default=8000, help="Port for HTTP server")
783
+ parser.add_argument("--debug", action="store_true", help="Enable debug logging")
784
+ args = parser.parse_args()
785
+
786
+ # Configure logging based on arguments
787
+ if args.debug:
788
+ logging.getLogger().setLevel(logging.DEBUG)
789
+
790
+ # Print some info
791
+ print("Starting Zillow MCP Server...")
792
+ print(f"API Key Present: {'Yes' if ZILLOW_API_KEY else 'No - Please set ZILLOW_API_KEY environment variable'}")
793
+
794
+ # Check API connectivity
795
+ if ZILLOW_API_KEY:
796
+ try:
797
+ # Try a simple API call to verify connectivity
798
+ zillow_api_request("health", method="GET")
799
+ print("✅ Successfully connected to Zillow API")
800
+ except Exception as e:
801
+ print(f"⚠️ Warning: Could not connect to Zillow API: {e}")
802
+
803
+ # Run the server with appropriate transport
804
+ if args.http:
805
+ print(f"Running as HTTP server on {args.host}:{args.port}")
806
+ mcp.run(transport="streamable_http", host=args.host, port=args.port)
807
+ else:
808
+ print("Running as stdio server")
809
+ mcp.run()
810
+
811
+ def main():
812
+ """Main entry point for the MCP server"""
813
+ import argparse
814
+
815
+ parser = argparse.ArgumentParser(description="Zillow MCP Server")
816
+ parser.add_argument("--http", action="store_true", help="Run as HTTP server instead of stdio")
817
+ parser.add_argument("--host", type=str, default="127.0.0.1", help="Host for HTTP server")
818
+ parser.add_argument("--port", type=int, default=8000, help="Port for HTTP server")
819
+ parser.add_argument("--debug", action="store_true", help="Enable debug logging")
820
+ args = parser.parse_args()
821
+
822
+ # Configure logging based on arguments
823
+ if args.debug:
824
+ logging.getLogger().setLevel(logging.DEBUG)
825
+
826
+ # Print some info
827
+ print("Starting Zillow MCP Server...")
828
+ print(f"API Key Present: {'Yes' if ZILLOW_API_KEY else 'No - Please set ZILLOW_API_KEY environment variable'}")
829
+
830
+ # Check API connectivity
831
+ if ZILLOW_API_KEY:
832
+ try:
833
+ zillow_api_request("health", method="GET")
834
+ print("✅ Successfully connected to Zillow API")
835
+ except Exception as e:
836
+ print(f"⚠️ Warning: Could not connect to Zillow API: {e}")
837
+
838
+ # Run the server
839
+ if args.http:
840
+ print(f"Running as HTTP server on {args.host}:{args.port}")
841
+ mcp.run(transport="streamable_http", host=args.host, port=args.port)
842
+ else:
843
+ print("Running as stdio server")
844
+ mcp.run()