mcp second iteration
This commit is contained in:
@@ -66,7 +66,17 @@ This project provides a comprehensive Python interface to the Israeli government
|
|||||||
|
|
||||||
The project includes an MCP (Model Context Protocol) server that allows AI agents to access Israeli real estate data. The server provides multiple deployment options:
|
The project includes an MCP (Model Context Protocol) server that allows AI agents to access Israeli real estate data. The server provides multiple deployment options:
|
||||||
|
|
||||||
#### 1. Interactive Demo Server
|
#### 1. FastMCP Server (Recommended)
|
||||||
|
|
||||||
|
**NEW: Using FastMCP for better compatibility and reliability**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python run_fastmcp_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the FastMCP server which resolves compatibility issues with the standard MCP library.
|
||||||
|
|
||||||
|
#### 2. Interactive Demo Server
|
||||||
|
|
||||||
For testing and demonstration purposes:
|
For testing and demonstration purposes:
|
||||||
|
|
||||||
@@ -76,7 +86,7 @@ python simple_mcp_server.py
|
|||||||
|
|
||||||
This runs an interactive demo where you can test the tools directly in the terminal.
|
This runs an interactive demo where you can test the tools directly in the terminal.
|
||||||
|
|
||||||
#### 2. Full MCP Server
|
#### 3. Full MCP Server (Legacy)
|
||||||
|
|
||||||
For production use with MCP clients:
|
For production use with MCP clients:
|
||||||
|
|
||||||
@@ -86,22 +96,36 @@ python run_mcp_server.py
|
|||||||
|
|
||||||
This starts the full MCP server that can be connected to by MCP-compatible clients.
|
This starts the full MCP server that can be connected to by MCP-compatible clients.
|
||||||
|
|
||||||
#### 3. Direct Server Module
|
#### 4. Direct Server Module
|
||||||
|
|
||||||
You can also run the server directly:
|
You can also run the server directly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m nadlan_mcp.mcp_server
|
python -m nadlan_mcp.simple_fastmcp_server
|
||||||
```
|
```
|
||||||
|
|
||||||
#### MCP Client Configuration
|
#### MCP Client Configuration
|
||||||
|
|
||||||
To connect to the server from MCP clients, use the following configuration:
|
To connect to the server from MCP clients, use the following configuration:
|
||||||
|
|
||||||
**For Claude Desktop or other MCP clients:**
|
**For Claude Desktop or other MCP clients (FastMCP - Recommended):**
|
||||||
|
|
||||||
Add to your MCP client configuration:
|
Add to your MCP client configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"nadlan-mcp": {
|
||||||
|
"command": "python",
|
||||||
|
"args": ["/path/to/nadlan-mcp/run_fastmcp_server.py"],
|
||||||
|
"env": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative (Legacy MCP Server):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"servers": {
|
"servers": {
|
||||||
@@ -114,14 +138,14 @@ Add to your MCP client configuration:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**For development with stdio transport:**
|
**For development with stdio transport (FastMCP):**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
from mcp.client.stdio import stdio_client
|
from mcp.client.stdio import stdio_client
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
async with stdio_client(["python", "run_mcp_server.py"]) as client:
|
async with stdio_client(["python", "run_fastmcp_server.py"]) as client:
|
||||||
# List available tools
|
# List available tools
|
||||||
result = await client.list_tools()
|
result = await client.list_tools()
|
||||||
print("Available tools:", result.tools)
|
print("Available tools:", result.tools)
|
||||||
@@ -138,7 +162,14 @@ asyncio.run(main())
|
|||||||
|
|
||||||
#### Available MCP Tools
|
#### Available MCP Tools
|
||||||
|
|
||||||
The server provides these tools for AI agents:
|
**FastMCP Server provides these 5 tools:**
|
||||||
|
- 🏠 `find_recent_deals_for_address` - Main comprehensive analysis tool
|
||||||
|
- 📊 `get_deals_by_radius` - Find deals within a radius of coordinates
|
||||||
|
- 🏘️ `get_street_deals` - Get deals for a specific street polygon
|
||||||
|
- 🔍 `autocomplete_address` - Address search and validation
|
||||||
|
- 📈 `compare_addresses` - Compare multiple addresses
|
||||||
|
|
||||||
|
**Legacy MCP Server provides these 4 tools:**
|
||||||
|
|
||||||
##### 🏠 `find_recent_deals_for_address`
|
##### 🏠 `find_recent_deals_for_address`
|
||||||
**Main comprehensive analysis tool**
|
**Main comprehensive analysis tool**
|
||||||
|
|||||||
@@ -0,0 +1,417 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
FastMCP Server for Israeli Real Estate Data (Nadlan)
|
||||||
|
|
||||||
|
This server provides access to Israeli government real estate data through the Govmap API
|
||||||
|
using the FastMCP library for better compatibility and reliability.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
from .main import GovmapClient # type: ignore
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Initialize FastMCP server
|
||||||
|
mcp = FastMCP("nadlan-mcp")
|
||||||
|
|
||||||
|
# Initialize the Govmap client
|
||||||
|
client = GovmapClient()
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def autocomplete_address(search_text: str) -> str:
|
||||||
|
"""Search and autocomplete Israeli addresses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
search_text: The partial address to search for (in Hebrew or English)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing matching addresses with their coordinates
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = client.autocomplete_address(search_text)
|
||||||
|
|
||||||
|
if not response or 'results' not in response:
|
||||||
|
return f"No addresses found for '{search_text}'"
|
||||||
|
|
||||||
|
# Format results for better readability
|
||||||
|
formatted_results = []
|
||||||
|
for result in response['results']:
|
||||||
|
formatted_results.append({
|
||||||
|
"address": result.get("addressLabel", ""),
|
||||||
|
"settlement": result.get("settlementNameHeb", ""),
|
||||||
|
"coordinates": result.get("coordinates", {}),
|
||||||
|
"polygon_id": result.get("polygon_id")
|
||||||
|
})
|
||||||
|
|
||||||
|
import json
|
||||||
|
return json.dumps(formatted_results, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in autocomplete_address: {e}")
|
||||||
|
return f"Error searching for address: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_deals_by_radius(
|
||||||
|
latitude: float,
|
||||||
|
longitude: float,
|
||||||
|
radius_meters: int = 500,
|
||||||
|
limit: int = 100
|
||||||
|
) -> str:
|
||||||
|
"""Get real estate deals within a radius of coordinates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
latitude: Latitude coordinate
|
||||||
|
longitude: Longitude coordinate
|
||||||
|
radius_meters: Search radius in meters (default: 500)
|
||||||
|
limit: Maximum number of deals to return (default: 100)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing recent real estate deals in the area
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
deals = client.get_deals_by_radius((longitude, latitude), radius_meters)
|
||||||
|
|
||||||
|
if not deals:
|
||||||
|
return f"No deals found within {radius_meters}m of coordinates ({latitude}, {longitude})"
|
||||||
|
|
||||||
|
# Format deals for better readability
|
||||||
|
formatted_deals = []
|
||||||
|
for deal in deals:
|
||||||
|
formatted_deals.append({
|
||||||
|
"address": deal.get("addressLabel", ""),
|
||||||
|
"settlement": deal.get("settlementNameHeb", ""),
|
||||||
|
"deal_amount": deal.get("dealAmount"),
|
||||||
|
"deal_date": deal.get("dealDate", ""),
|
||||||
|
"asset_area": deal.get("assetArea"),
|
||||||
|
"asset_type": deal.get("assetTypeHeb", ""),
|
||||||
|
"coordinates": deal.get("coordinates", {})
|
||||||
|
})
|
||||||
|
|
||||||
|
import json
|
||||||
|
return json.dumps({
|
||||||
|
"total_deals": len(formatted_deals),
|
||||||
|
"search_radius_meters": radius_meters,
|
||||||
|
"center_coordinates": {"latitude": latitude, "longitude": longitude},
|
||||||
|
"deals": formatted_deals
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in get_deals_by_radius: {e}")
|
||||||
|
return f"Error fetching deals by radius: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_street_deals(street_name: str, settlement_name: str, limit: int = 100) -> str:
|
||||||
|
"""Get real estate deals for a specific street.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
street_name: Name of the street (in Hebrew)
|
||||||
|
settlement_name: Name of the city/settlement (in Hebrew)
|
||||||
|
limit: Maximum number of deals to return (default: 100)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing recent real estate deals on the specified street
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
deals = await client.get_street_deals(street_name, settlement_name, limit)
|
||||||
|
|
||||||
|
if not deals:
|
||||||
|
return f"No deals found for {street_name}, {settlement_name}"
|
||||||
|
|
||||||
|
# Format deals for better readability
|
||||||
|
formatted_deals = []
|
||||||
|
for deal in deals:
|
||||||
|
formatted_deals.append({
|
||||||
|
"address": deal.get("addressLabel", ""),
|
||||||
|
"settlement": deal.get("settlementNameHeb", ""),
|
||||||
|
"deal_amount": deal.get("dealAmount"),
|
||||||
|
"deal_date": deal.get("dealDate", ""),
|
||||||
|
"asset_area": deal.get("assetArea"),
|
||||||
|
"asset_type": deal.get("assetTypeHeb", ""),
|
||||||
|
"coordinates": deal.get("coordinates", {})
|
||||||
|
})
|
||||||
|
|
||||||
|
import json
|
||||||
|
return json.dumps({
|
||||||
|
"total_deals": len(formatted_deals),
|
||||||
|
"street": street_name,
|
||||||
|
"settlement": settlement_name,
|
||||||
|
"deals": formatted_deals
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in get_street_deals: {e}")
|
||||||
|
return f"Error fetching street deals: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_neighborhood_deals(polygon_id: str, limit: int = 100) -> str:
|
||||||
|
"""Get real estate deals for a specific neighborhood polygon.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
polygon_id: The polygon ID of the neighborhood
|
||||||
|
limit: Maximum number of deals to return (default: 100)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing recent real estate deals in the specified neighborhood
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
deals = await client.get_neighborhood_deals(polygon_id, limit)
|
||||||
|
|
||||||
|
if not deals:
|
||||||
|
return f"No deals found for polygon ID {polygon_id}"
|
||||||
|
|
||||||
|
# Format deals for better readability
|
||||||
|
formatted_deals = []
|
||||||
|
for deal in deals:
|
||||||
|
formatted_deals.append({
|
||||||
|
"address": deal.get("addressLabel", ""),
|
||||||
|
"settlement": deal.get("settlementNameHeb", ""),
|
||||||
|
"deal_amount": deal.get("dealAmount"),
|
||||||
|
"deal_date": deal.get("dealDate", ""),
|
||||||
|
"asset_area": deal.get("assetArea"),
|
||||||
|
"asset_type": deal.get("assetTypeHeb", ""),
|
||||||
|
"coordinates": deal.get("coordinates", {})
|
||||||
|
})
|
||||||
|
|
||||||
|
import json
|
||||||
|
return json.dumps({
|
||||||
|
"total_deals": len(formatted_deals),
|
||||||
|
"polygon_id": polygon_id,
|
||||||
|
"deals": formatted_deals
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in get_neighborhood_deals: {e}")
|
||||||
|
return f"Error fetching neighborhood deals: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def find_recent_deals_for_address(
|
||||||
|
address: str,
|
||||||
|
radius_meters: int = 500,
|
||||||
|
limit: int = 100
|
||||||
|
) -> str:
|
||||||
|
"""Comprehensive analysis of recent real estate deals for an address.
|
||||||
|
|
||||||
|
This is the main tool for getting detailed market analysis around a specific address.
|
||||||
|
It finds the coordinates for the address and then searches for deals in the area.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
address: The address to analyze (in Hebrew or English)
|
||||||
|
radius_meters: Search radius in meters (default: 500)
|
||||||
|
limit: Maximum number of deals to return (default: 100)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing comprehensive real estate analysis including:
|
||||||
|
- Address details and coordinates
|
||||||
|
- Recent deals in the area
|
||||||
|
- Market statistics and trends
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
deals = await client.find_recent_deals_for_address(address, radius_meters, limit)
|
||||||
|
|
||||||
|
if not deals:
|
||||||
|
return f"No deals found for address '{address}'"
|
||||||
|
|
||||||
|
# Calculate market statistics
|
||||||
|
prices = [deal.get("dealAmount", 0) for deal in deals if deal.get("dealAmount")]
|
||||||
|
areas = [deal.get("assetArea", 0) for deal in deals if deal.get("assetArea")]
|
||||||
|
|
||||||
|
stats = {}
|
||||||
|
if prices:
|
||||||
|
stats["price_stats"] = {
|
||||||
|
"average_price": sum(prices) / len(prices),
|
||||||
|
"min_price": min(prices),
|
||||||
|
"max_price": max(prices),
|
||||||
|
"total_deals": len(prices)
|
||||||
|
}
|
||||||
|
|
||||||
|
if areas:
|
||||||
|
stats["area_stats"] = {
|
||||||
|
"average_area": sum(areas) / len(areas),
|
||||||
|
"min_area": min(areas),
|
||||||
|
"max_area": max(areas)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Format deals for better readability
|
||||||
|
formatted_deals = []
|
||||||
|
for deal in deals:
|
||||||
|
formatted_deals.append({
|
||||||
|
"address": deal.get("addressLabel", ""),
|
||||||
|
"settlement": deal.get("settlementNameHeb", ""),
|
||||||
|
"deal_amount": deal.get("dealAmount"),
|
||||||
|
"deal_date": deal.get("dealDate", ""),
|
||||||
|
"asset_area": deal.get("assetArea"),
|
||||||
|
"asset_type": deal.get("assetTypeHeb", ""),
|
||||||
|
"coordinates": deal.get("coordinates", {})
|
||||||
|
})
|
||||||
|
|
||||||
|
import json
|
||||||
|
return json.dumps({
|
||||||
|
"search_address": address,
|
||||||
|
"search_radius_meters": radius_meters,
|
||||||
|
"total_deals": len(formatted_deals),
|
||||||
|
"market_statistics": stats,
|
||||||
|
"deals": formatted_deals
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in find_recent_deals_for_address: {e}")
|
||||||
|
return f"Error analyzing address: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def analyze_market_trends(
|
||||||
|
address: str,
|
||||||
|
radius_meters: int = 1000,
|
||||||
|
limit: int = 200
|
||||||
|
) -> str:
|
||||||
|
"""Analyze market trends and price patterns for an area.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
address: The address to analyze trends around
|
||||||
|
radius_meters: Search radius in meters (default: 1000)
|
||||||
|
limit: Maximum number of deals to analyze (default: 200)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing market trend analysis including:
|
||||||
|
- Price trends over time
|
||||||
|
- Average prices by property type
|
||||||
|
- Market activity levels
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get deals for the address
|
||||||
|
deals = await client.find_recent_deals_for_address(address, radius_meters, limit)
|
||||||
|
|
||||||
|
if not deals:
|
||||||
|
return f"No deals found for market analysis near '{address}'"
|
||||||
|
|
||||||
|
# Analyze trends by year
|
||||||
|
from collections import defaultdict
|
||||||
|
import json
|
||||||
|
|
||||||
|
trends_by_year = defaultdict(list)
|
||||||
|
trends_by_type = defaultdict(list)
|
||||||
|
|
||||||
|
for deal in deals:
|
||||||
|
deal_date = deal.get("dealDate", "")
|
||||||
|
deal_amount = deal.get("dealAmount", 0)
|
||||||
|
asset_type = deal.get("assetTypeHeb", "Unknown")
|
||||||
|
|
||||||
|
if deal_date and deal_amount:
|
||||||
|
year = deal_date.split('-')[0] if '-' in deal_date else deal_date[:4]
|
||||||
|
trends_by_year[year].append(deal_amount)
|
||||||
|
trends_by_type[asset_type].append(deal_amount)
|
||||||
|
|
||||||
|
# Calculate yearly trends
|
||||||
|
yearly_trends = {}
|
||||||
|
for year, prices in trends_by_year.items():
|
||||||
|
yearly_trends[year] = {
|
||||||
|
"average_price": sum(prices) / len(prices),
|
||||||
|
"min_price": min(prices),
|
||||||
|
"max_price": max(prices),
|
||||||
|
"deal_count": len(prices)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate type trends
|
||||||
|
type_trends = {}
|
||||||
|
for asset_type, prices in trends_by_type.items():
|
||||||
|
type_trends[asset_type] = {
|
||||||
|
"average_price": sum(prices) / len(prices),
|
||||||
|
"min_price": min(prices),
|
||||||
|
"max_price": max(prices),
|
||||||
|
"deal_count": len(prices)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"analysis_address": address,
|
||||||
|
"analysis_radius_meters": radius_meters,
|
||||||
|
"total_deals_analyzed": len(deals),
|
||||||
|
"yearly_trends": yearly_trends,
|
||||||
|
"property_type_trends": type_trends
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in analyze_market_trends: {e}")
|
||||||
|
return f"Error analyzing market trends: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def compare_neighborhoods(addresses: List[str], radius_meters: int = 500) -> str:
|
||||||
|
"""Compare real estate markets between multiple neighborhoods.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
addresses: List of addresses to compare (in Hebrew or English)
|
||||||
|
radius_meters: Search radius for each address (default: 500)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing comparative analysis of multiple neighborhoods
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
|
||||||
|
comparisons = []
|
||||||
|
|
||||||
|
for address in addresses:
|
||||||
|
try:
|
||||||
|
deals = await client.find_recent_deals_for_address(address, radius_meters, 100)
|
||||||
|
|
||||||
|
if deals:
|
||||||
|
prices = []
|
||||||
|
areas = []
|
||||||
|
for deal in deals:
|
||||||
|
if isinstance(deal, dict):
|
||||||
|
if deal.get("dealAmount"):
|
||||||
|
prices.append(deal.get("dealAmount", 0))
|
||||||
|
if deal.get("assetArea"):
|
||||||
|
areas.append(deal.get("assetArea", 0))
|
||||||
|
|
||||||
|
comparison = {
|
||||||
|
"address": address,
|
||||||
|
"total_deals": len(deals),
|
||||||
|
"price_stats": {
|
||||||
|
"average_price": sum(prices) / len(prices) if prices else 0,
|
||||||
|
"min_price": min(prices) if prices else 0,
|
||||||
|
"max_price": max(prices) if prices else 0
|
||||||
|
},
|
||||||
|
"area_stats": {
|
||||||
|
"average_area": sum(areas) / len(areas) if areas else 0,
|
||||||
|
"min_area": min(areas) if areas else 0,
|
||||||
|
"max_area": max(areas) if areas else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
comparison = {
|
||||||
|
"address": address,
|
||||||
|
"total_deals": 0,
|
||||||
|
"price_stats": {},
|
||||||
|
"area_stats": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
comparisons.append(comparison)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error comparing {address}: {e}")
|
||||||
|
comparisons.append({
|
||||||
|
"address": address,
|
||||||
|
"error": str(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Rank neighborhoods by average price
|
||||||
|
valid_comparisons = [c for c in comparisons if c.get("price_stats", {}).get("average_price", 0) > 0]
|
||||||
|
valid_comparisons.sort(key=lambda x: x["price_stats"]["average_price"], reverse=True)
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"comparison_radius_meters": radius_meters,
|
||||||
|
"neighborhoods_compared": len(addresses),
|
||||||
|
"ranking_by_average_price": valid_comparisons,
|
||||||
|
"all_results": comparisons
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in compare_neighborhoods: {e}")
|
||||||
|
return f"Error comparing neighborhoods: {str(e)}"
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mcp.run()
|
||||||
@@ -542,7 +542,7 @@ async def main():
|
|||||||
read_stream,
|
read_stream,
|
||||||
write_stream,
|
write_stream,
|
||||||
InitializationOptions(
|
InitializationOptions(
|
||||||
server_name="israel-real-estate-mcp",
|
server_name="nadlan-mcp",
|
||||||
server_version="1.0.0",
|
server_version="1.0.0",
|
||||||
capabilities=server.get_capabilities(
|
capabilities=server.get_capabilities(
|
||||||
notification_options=NotificationOptions(),
|
notification_options=NotificationOptions(),
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple FastMCP Server for Israeli Real Estate Data
|
||||||
|
|
||||||
|
This server provides access to Israeli government real estate data through the Govmap API
|
||||||
|
using the FastMCP library with simplified, working functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
from .main import GovmapClient
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Initialize FastMCP server
|
||||||
|
mcp = FastMCP("nadlan-mcp")
|
||||||
|
|
||||||
|
# Initialize the Govmap client
|
||||||
|
client = GovmapClient()
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def autocomplete_address(search_text: str) -> str:
|
||||||
|
"""Search and autocomplete Israeli addresses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
search_text: The partial address to search for (in Hebrew or English)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing matching addresses with their coordinates
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = client.autocomplete_address(search_text)
|
||||||
|
|
||||||
|
if not response or 'results' not in response:
|
||||||
|
return f"No addresses found for '{search_text}'"
|
||||||
|
|
||||||
|
# Format results for better readability
|
||||||
|
formatted_results = []
|
||||||
|
for result in response['results']:
|
||||||
|
formatted_results.append({
|
||||||
|
"address": result.get("addressLabel", ""),
|
||||||
|
"settlement": result.get("settlementNameHeb", ""),
|
||||||
|
"coordinates": result.get("coordinates", {}),
|
||||||
|
"polygon_id": result.get("polygon_id")
|
||||||
|
})
|
||||||
|
|
||||||
|
return json.dumps(formatted_results, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in autocomplete_address: {e}")
|
||||||
|
return f"Error searching for address: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_deals_by_radius(latitude: float, longitude: float, radius_meters: int = 500) -> str:
|
||||||
|
"""Get real estate deals within a radius of coordinates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
latitude: Latitude coordinate
|
||||||
|
longitude: Longitude coordinate
|
||||||
|
radius_meters: Search radius in meters (default: 500)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing recent real estate deals in the area
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Note: GovmapClient expects (longitude, latitude) tuple
|
||||||
|
deals = client.get_deals_by_radius((longitude, latitude), radius_meters)
|
||||||
|
|
||||||
|
if not deals:
|
||||||
|
return f"No deals found within {radius_meters}m of coordinates ({latitude}, {longitude})"
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"total_deals": len(deals),
|
||||||
|
"search_radius_meters": radius_meters,
|
||||||
|
"center_coordinates": {"latitude": latitude, "longitude": longitude},
|
||||||
|
"deals": deals
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in get_deals_by_radius: {e}")
|
||||||
|
return f"Error fetching deals by radius: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_street_deals(polygon_id: str, limit: int = 100) -> str:
|
||||||
|
"""Get real estate deals for a specific street polygon.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
polygon_id: The polygon ID of the street/area
|
||||||
|
limit: Maximum number of deals to return (default: 100)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing recent real estate deals for the street
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
deals = client.get_street_deals(polygon_id, limit)
|
||||||
|
|
||||||
|
if not deals:
|
||||||
|
return f"No deals found for polygon ID {polygon_id}"
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"total_deals": len(deals),
|
||||||
|
"polygon_id": polygon_id,
|
||||||
|
"deals": deals
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in get_street_deals: {e}")
|
||||||
|
return f"Error fetching street deals: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def find_recent_deals_for_address(address: str, years_back: int = 2) -> str:
|
||||||
|
"""Find recent real estate deals for a specific address.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
address: The address to search for (in Hebrew or English)
|
||||||
|
years_back: How many years back to search (default: 2)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing recent real estate deals for the address
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
deals = client.find_recent_deals_for_address(address, years_back)
|
||||||
|
|
||||||
|
if not deals:
|
||||||
|
return f"No deals found for address '{address}'"
|
||||||
|
|
||||||
|
# Calculate basic statistics
|
||||||
|
prices = [deal.get("dealAmount", 0) for deal in deals if deal.get("dealAmount")]
|
||||||
|
areas = [deal.get("assetArea", 0) for deal in deals if deal.get("assetArea")]
|
||||||
|
|
||||||
|
stats = {}
|
||||||
|
if prices:
|
||||||
|
stats["price_stats"] = {
|
||||||
|
"average_price": sum(prices) / len(prices),
|
||||||
|
"min_price": min(prices),
|
||||||
|
"max_price": max(prices),
|
||||||
|
"total_deals": len(prices)
|
||||||
|
}
|
||||||
|
|
||||||
|
if areas:
|
||||||
|
stats["area_stats"] = {
|
||||||
|
"average_area": sum(areas) / len(areas),
|
||||||
|
"min_area": min(areas),
|
||||||
|
"max_area": max(areas)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"search_address": address,
|
||||||
|
"years_back": years_back,
|
||||||
|
"total_deals": len(deals),
|
||||||
|
"market_statistics": stats,
|
||||||
|
"deals": deals
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in find_recent_deals_for_address: {e}")
|
||||||
|
return f"Error analyzing address: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def compare_addresses(addresses: List[str]) -> str:
|
||||||
|
"""Compare real estate markets between multiple addresses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
addresses: List of addresses to compare (in Hebrew or English)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string containing comparative analysis of multiple addresses
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
comparisons = []
|
||||||
|
|
||||||
|
for address in addresses:
|
||||||
|
try:
|
||||||
|
deals = client.find_recent_deals_for_address(address, 2)
|
||||||
|
|
||||||
|
if deals:
|
||||||
|
prices = [deal.get("dealAmount", 0) for deal in deals if deal.get("dealAmount")]
|
||||||
|
areas = [deal.get("assetArea", 0) for deal in deals if deal.get("assetArea")]
|
||||||
|
|
||||||
|
comparison = {
|
||||||
|
"address": address,
|
||||||
|
"total_deals": len(deals),
|
||||||
|
"price_stats": {
|
||||||
|
"average_price": sum(prices) / len(prices) if prices else 0,
|
||||||
|
"min_price": min(prices) if prices else 0,
|
||||||
|
"max_price": max(prices) if prices else 0
|
||||||
|
},
|
||||||
|
"area_stats": {
|
||||||
|
"average_area": sum(areas) / len(areas) if areas else 0,
|
||||||
|
"min_area": min(areas) if areas else 0,
|
||||||
|
"max_area": max(areas) if areas else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
comparison = {
|
||||||
|
"address": address,
|
||||||
|
"total_deals": 0,
|
||||||
|
"price_stats": {},
|
||||||
|
"area_stats": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
comparisons.append(comparison)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error comparing {address}: {e}")
|
||||||
|
comparisons.append({
|
||||||
|
"address": address,
|
||||||
|
"error": str(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Rank addresses by average price
|
||||||
|
valid_comparisons = [c for c in comparisons if c.get("price_stats", {}).get("average_price", 0) > 0]
|
||||||
|
valid_comparisons.sort(key=lambda x: x["price_stats"]["average_price"], reverse=True)
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"addresses_compared": len(addresses),
|
||||||
|
"ranking_by_average_price": valid_comparisons,
|
||||||
|
"all_results": comparisons
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in compare_addresses: {e}")
|
||||||
|
return f"Error comparing addresses: {str(e)}"
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mcp.run()
|
||||||
@@ -2,4 +2,5 @@ requests>=2.31.0
|
|||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
pytest>=7.0.0
|
pytest>=7.0.0
|
||||||
mcp>=1.0.0
|
mcp>=1.0.0
|
||||||
|
fastmcp>=0.1.0
|
||||||
types-requests>=2.31.0
|
types-requests>=2.31.0
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Run script for FastMCP Israeli Real Estate Server
|
||||||
|
|
||||||
|
This script runs the FastMCP server for accessing Israeli government real estate data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from nadlan_mcp.simple_fastmcp_server import mcp
|
||||||
|
mcp.run()
|
||||||
Reference in New Issue
Block a user