6882d7ab71
The MCP now includes detailed outlier filtering information in the response,
allowing LLMs and bots to inform users about data quality improvements.
Changes to nadlan_mcp/fastmcp_server.py:
- Updated get_valuation_comparables to include outlier filtering metadata
- deal_breakdown now includes when filtering is applied:
- total_deals: Count after filtering (final result)
- total_deals_before_filtering: Original count before filtering
- outliers_removed: Number of deals filtered out
- filtering_method: Method used ("iqr", "percent", or "none")
- iqr_multiplier: IQR multiplier when using IQR method (e.g., 1.5)
- Updated docstring to clarify that outlier filtering is automatic
and metadata is included in response
Changes to tests/e2e/test_mcp_tools_comprehensive.py:
- Added assertions to verify outlier filtering metadata fields
- Test now validates the structure and types of filtering information
Example response:
{
"market_statistics": {
"deal_breakdown": {
"total_deals": 15,
"total_deals_before_filtering": 17,
"outliers_removed": 2,
"filtering_method": "iqr",
"iqr_multiplier": 1.5
}
}
}
This allows bots like nadlan-bot to properly inform users:
"Found 17 comparable deals, filtered 2 outliers using IQR method (k=1.5),
showing 15 deals."
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
205 lines
7.7 KiB
Python
205 lines
7.7 KiB
Python
"""
|
|
End-to-end tests for all MCP tools.
|
|
|
|
These tests make real API calls to verify the complete functionality
|
|
of each MCP tool from end to end.
|
|
|
|
Mark as integration tests since they hit real APIs.
|
|
"""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from nadlan_mcp.fastmcp_server import (
|
|
analyze_market_trends,
|
|
autocomplete_address,
|
|
compare_addresses,
|
|
find_recent_deals_for_address,
|
|
get_deal_statistics,
|
|
get_deals_by_radius,
|
|
get_market_activity_metrics,
|
|
get_neighborhood_deals,
|
|
get_street_deals,
|
|
get_valuation_comparables,
|
|
)
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestMCPToolsE2E:
|
|
"""End-to-end tests for all 10 MCP tools."""
|
|
|
|
# Test addresses
|
|
TEST_ADDRESS_1 = "סוקולוב 38 חולון"
|
|
TEST_ADDRESS_2 = "דיזנגוף 50 תל אביב"
|
|
TEST_POLYGON_ID = "52385050" # Known polygon with data
|
|
# ITM coordinates for Holon (lat, lon format for MCP tool)
|
|
TEST_LAT = 3766290.19
|
|
TEST_LON = 3870928.84
|
|
|
|
def test_autocomplete_address(self):
|
|
"""Test address autocomplete returns results."""
|
|
result = autocomplete_address("חולון סוקולוב")
|
|
data = json.loads(result)
|
|
|
|
assert isinstance(data, list)
|
|
assert len(data) > 0
|
|
|
|
# Check first result has expected structure
|
|
first = data[0]
|
|
assert "text" in first
|
|
assert "coordinates" in first or "id" in first
|
|
|
|
def test_find_recent_deals_for_address(self):
|
|
"""Test finding recent deals for an address."""
|
|
result = find_recent_deals_for_address(self.TEST_ADDRESS_1, max_deals=100)
|
|
data = json.loads(result)
|
|
|
|
# Check response structure
|
|
assert "search_parameters" in data
|
|
assert "market_statistics" in data
|
|
assert "deals" in data
|
|
|
|
# Verify some deals were found
|
|
assert isinstance(data["deals"], list)
|
|
assert len(data["deals"]) > 0
|
|
|
|
# Check deal structure
|
|
deal = data["deals"][0]
|
|
assert "deal_amount" in deal
|
|
assert "deal_date" in deal
|
|
assert "settlement_name_heb" in deal
|
|
|
|
def test_analyze_market_trends(self):
|
|
"""Test market trend analysis."""
|
|
result = analyze_market_trends(self.TEST_ADDRESS_1, years_back=3, radius_meters=100)
|
|
data = json.loads(result)
|
|
|
|
# Check response structure (normalized in MCP_NORMALIZATION_FIX)
|
|
assert "market_statistics" in data
|
|
assert "deal_breakdown" in data["market_statistics"]
|
|
assert "total_deals" in data["market_statistics"]["deal_breakdown"]
|
|
assert isinstance(data["market_statistics"]["deal_breakdown"]["total_deals"], int)
|
|
assert data["market_statistics"]["deal_breakdown"]["total_deals"] >= 0
|
|
|
|
def test_get_valuation_comparables(self):
|
|
"""Test getting valuation comparables."""
|
|
result = get_valuation_comparables(
|
|
self.TEST_ADDRESS_1, years_back=3, min_rooms=3.0, max_rooms=5.0
|
|
)
|
|
data = json.loads(result)
|
|
|
|
# Normalized structure: total_comparables -> market_statistics.deal_breakdown.total_deals
|
|
assert "market_statistics" in data
|
|
assert "deal_breakdown" in data["market_statistics"]
|
|
deal_breakdown = data["market_statistics"]["deal_breakdown"]
|
|
assert "total_deals" in deal_breakdown
|
|
assert isinstance(deal_breakdown["total_deals"], int)
|
|
assert deal_breakdown["total_deals"] >= 0
|
|
|
|
# Verify outlier filtering metadata is included when filtering is applied
|
|
if "outliers_removed" in deal_breakdown:
|
|
assert isinstance(deal_breakdown["outliers_removed"], int)
|
|
assert deal_breakdown["outliers_removed"] >= 0
|
|
assert "total_deals_before_filtering" in deal_breakdown
|
|
assert "filtering_method" in deal_breakdown
|
|
assert deal_breakdown["filtering_method"] in ["iqr", "percent", "none"]
|
|
if deal_breakdown["filtering_method"] == "iqr":
|
|
assert "iqr_multiplier" in deal_breakdown
|
|
assert isinstance(deal_breakdown["iqr_multiplier"], (int, float))
|
|
|
|
# Normalized structure: comparables -> deals
|
|
if deal_breakdown["total_deals"] > 0:
|
|
assert "deals" in data
|
|
comp = data["deals"][0]
|
|
assert "deal_amount" in comp
|
|
# rooms field is optional and excluded when None
|
|
# Just verify the comparable has basic required fields
|
|
assert "deal_date" in comp
|
|
|
|
def test_get_deal_statistics(self):
|
|
"""Test deal statistics calculation."""
|
|
result = get_deal_statistics(self.TEST_ADDRESS_1, years_back=3)
|
|
data = json.loads(result)
|
|
|
|
# Check response structure (normalized: statistics -> market_statistics)
|
|
assert "market_statistics" in data
|
|
# Check for total_deals in normalized location
|
|
if "deal_breakdown" in data["market_statistics"]:
|
|
assert "total_deals" in data["market_statistics"]["deal_breakdown"]
|
|
assert isinstance(data["market_statistics"]["deal_breakdown"]["total_deals"], int)
|
|
assert data["market_statistics"]["deal_breakdown"]["total_deals"] >= 0
|
|
|
|
def test_get_market_activity_metrics(self):
|
|
"""Test market activity metrics."""
|
|
result = get_market_activity_metrics(self.TEST_ADDRESS_1, years_back=3)
|
|
data = json.loads(result)
|
|
|
|
# Check response structure
|
|
assert "market_activity" in data
|
|
if data["market_activity"] is not None:
|
|
assert "activity_score" in data["market_activity"]
|
|
|
|
def test_compare_addresses(self):
|
|
"""Test comparing multiple addresses."""
|
|
result = compare_addresses([self.TEST_ADDRESS_1, self.TEST_ADDRESS_2])
|
|
data = json.loads(result)
|
|
|
|
assert isinstance(data, dict)
|
|
assert "addresses_compared" in data
|
|
assert "all_results" in data
|
|
|
|
def test_get_street_deals(self):
|
|
"""Test getting street-level deals."""
|
|
result = get_street_deals(self.TEST_POLYGON_ID, limit=100, deal_type=2)
|
|
data = json.loads(result)
|
|
|
|
assert "total_deals" in data
|
|
assert isinstance(data["total_deals"], int)
|
|
assert data["total_deals"] > 0 # Known polygon should have deals
|
|
|
|
assert "deals" in data
|
|
assert len(data["deals"]) > 0
|
|
|
|
# Check deal structure
|
|
deal = data["deals"][0]
|
|
assert "deal_amount" in deal
|
|
assert "deal_date" in deal
|
|
|
|
def test_get_neighborhood_deals(self):
|
|
"""Test getting neighborhood-level deals."""
|
|
result = get_neighborhood_deals(self.TEST_POLYGON_ID, limit=100, deal_type=2)
|
|
data = json.loads(result)
|
|
|
|
assert "total_deals" in data
|
|
assert isinstance(data["total_deals"], int)
|
|
assert data["total_deals"] > 0 # Known polygon should have deals
|
|
|
|
assert "deals" in data
|
|
assert len(data["deals"]) > 0
|
|
|
|
def test_get_deals_by_radius(self):
|
|
"""Test getting polygon metadata by radius."""
|
|
result = get_deals_by_radius(self.TEST_LAT, self.TEST_LON, radius_meters=500)
|
|
data = json.loads(result)
|
|
|
|
assert "total_polygons" in data
|
|
assert isinstance(data["total_polygons"], int)
|
|
assert data["total_polygons"] > 0 # Known coords should have polygons
|
|
|
|
assert "polygons" in data
|
|
assert len(data["polygons"]) > 0
|
|
|
|
# Check polygon metadata structure
|
|
polygon = data["polygons"][0]
|
|
assert "polygon_id" in polygon or "objectid" in polygon
|
|
|
|
def test_get_deals_by_radius_no_results(self):
|
|
"""Test get_deals_by_radius with coords that have no data."""
|
|
# Use coordinates unlikely to have data
|
|
result = get_deals_by_radius(37.66, 38.71, radius_meters=10)
|
|
|
|
# Should return a message string, not JSON
|
|
assert isinstance(result, str)
|
|
assert "No polygons found" in result or "total_polygons" in result
|