21 KiB
Nadlan-MCP Architecture
Overview
Nadlan-MCP is a Model Context Protocol (MCP) server that provides Israeli real estate data to AI agents (LLMs). The architecture follows a clean separation of concerns with three main layers:
- MCP Tools Layer - Exposes functions to LLM clients
- Business Logic Layer - Data analysis and processing
- API Client Layer - Communicates with Govmap API
Design Principles
1. MCP Provides Data, LLM Provides Intelligence
The server's role is to retrieve, structure, and optionally summarize data. Complex analysis, decision-making, and predictions are left to the LLM client.
Example:
- ✅ MCP: Provides comparable property deals with filters
- ✅ LLM: Analyzes comparables and estimates property value
- ❌ MCP: Calculates ML-based property valuation
2. Structured by Default
All tools return detailed, structured data by default. Optional summarized_response parameter provides condensed summaries when needed.
3. Reliability First
- Automatic retry with exponential backoff
- Rate limiting to respect API limits
- Comprehensive input validation
- Detailed error messages
4. Configuration Over Hard-coding
All settings (timeouts, retries, rate limits) are configurable via environment variables or code.
System Architecture
┌─────────────────────────────────────────────────────────────┐
│ LLM Client (AI Agent) │
│ (Claude, GPT, etc.) │
└────────────────────────────┬────────────────────────────────┘
│ MCP Protocol
↓
┌─────────────────────────────────────────────────────────────┐
│ MCP Tools Layer │
│ ┌───────────────┐ ┌─────────────────┐ ┌──────────────┐ │
│ │autocomplete │ │find_recent_deals│ │analyze_market│ │
│ │_address │ │_for_address │ │_trends │ │
│ └───────┬───────┘ └────────┬────────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌───────┴───────────────────┴────────────────────┴──────┐ │
│ │ fastmcp_server.py (Tool Definitions) │ │
│ └───────────────────────────┬───────────────────────────┘ │
└────────────────────────────┬─┴────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────┐
│ Business Logic Layer │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ govmap/ Package │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ client.py │ │ validators.py│ │ filters.py │ │ │
│ │ │ (API calls) │ │ (validation) │ │ (filtering) │ │ │
│ │ └─────────────┘ └──────────────┘ └───────────────┘ │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │statistics.py│ │market_ │ │ utils.py │ │ │
│ │ │ (stats) │ │analysis.py │ │ (helpers) │ │ │
│ │ └─────────────┘ └──────────────┘ └───────────────┘ │ │
│ │ • Retry Logic • Rate Limiting • Validation │ │
│ └─────────────────────────┬───────────────────────────────┘ │
└───────────────────────────┬┴──────────────────────────────┘
│ HTTP/JSON
↓
┌─────────────────────────────────────────────────────────────┐
│ Govmap API Layer │
│ (Israeli Government Real Estate API) │
│ ┌───────────────┐ ┌────────────┐ ┌──────────────────┐ │
│ │ Autocomplete │ │ Deals by │ │ Street/ │ │
│ │ Endpoint │ │ Radius │ │ Neighborhood │ │
│ │ │ │ Endpoint │ │ Deals Endpoints │ │
│ └───────────────┘ └────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Component Details
Configuration Layer (config.py)
Purpose: Centralized configuration management
Key Components:
GovmapConfig- Dataclass with all configuration settings- Environment variable support for all settings
- Validation on initialization
Configuration Options:
- API settings (base URL, user agent)
- Timeout settings (connect, read)
- Retry settings (max retries, backoff)
- Rate limiting (requests per second)
- Default search parameters
Usage:
from nadlan_mcp.config import get_config, set_config, GovmapConfig
# Use global config
config = get_config()
# Override config
custom_config = GovmapConfig(max_retries=5, requests_per_second=10.0)
set_config(custom_config)
API Client Layer (govmap/ package)
Purpose: Modular package for Govmap API interaction and data processing
Package Structure:
models.py- Pydantic v2 data models (9 models, type-safe, validated)client.py- GovmapClient class with API methods (returns models)validators.py- Input validation functionsfilters.py- Deal filtering logic (accepts/returns models)statistics.py- Statistical calculations (returns models)market_analysis.py- Market analysis functions (returns models)utils.py- Helper utilities__init__.py- Public API exports
Pydantic Models Layer (govmap/models.py) ✨ NEW in v2.0.0
Purpose: Type-safe, validated data models for all API responses and business logic
9 Comprehensive Models:
CoordinatePoint- ITM coordinates (frozen/immutable)Address- Israeli address with optional coordinatesAutocompleteResult&AutocompleteResponse- Search resultsDeal- Real estate transaction with computedprice_per_sqmfieldDealStatistics- Statistical aggregationsMarketActivityScore- Market activity metricsInvestmentAnalysis- Investment potential analysisLiquidityMetrics- Market liquidity metricsDealFilters- Filter criteria with validation
Key Features:
- Field Aliasing: API camelCase ↔ Python snake_case (e.g.,
dealAmount↔deal_amount) - Computed Fields: Auto-calculate price per sqm using
@computed_field - Validation: Automatic data validation with clear error messages
- Serialization: Easy conversion to/from JSON via
.model_dump() - Type Safety: Full IDE autocomplete and mypy support
Usage:
from nadlan_mcp.govmap import GovmapClient
from nadlan_mcp.govmap.models import Deal, AutocompleteResponse
client = GovmapClient()
# Returns AutocompleteResponse model
result = client.autocomplete_address("חולון")
coords = result.results[0].coordinates # Optional[CoordinatePoint]
# Returns List[Deal]
deals = client.get_street_deals("polygon123")
for deal in deals:
price = deal.deal_amount # float (snake_case)
price_per_sqm = deal.price_per_sqm # Computed field!
# Serialize to dict/JSON when needed
deal_dict = deal.model_dump()
deal_json = deal.model_dump_json()
GovmapClient Class (govmap/client.py)
Responsibilities:
- Make HTTP requests to Govmap API
- Parse JSON responses into Pydantic models
- Implement retry logic with exponential backoff
- Enforce rate limiting
- Delegate to specialized modules for validation, filtering, analysis
Core API Methods (all return Pydantic models):
autocomplete_address()→AutocompleteResponseget_gush_helka()→Dict(parcel metadata)get_deals_by_radius()→List[Deal]get_street_deals()→List[Deal]get_neighborhood_deals()→List[Deal]find_recent_deals_for_address()→List[Deal]
Business Logic Methods (delegate to modules, return models):
filter_deals_by_criteria()→List[Deal]calculate_deal_statistics()→DealStatisticscalculate_market_activity_score()→MarketActivityScoreanalyze_investment_potential()→InvestmentAnalysisget_market_liquidity()→LiquidityMetrics
Reliability Features:
- Retry Logic - Exponential backoff on failures
- Rate Limiting - Tracks request times, enforces delays
- Input Validation - Delegates to validators module
- Timeouts - Configurable connect and read timeouts
MCP Tools Layer (fastmcp_server.py)
Purpose: Expose functionality to LLM clients via MCP protocol
Key Components:
- FastMCP server instance
- Tool definitions (decorated functions)
- JSON response formatting
- Error handling for user-friendly messages
Tool Design Pattern:
@mcp.tool()
def tool_name(param: type, summarized_response: bool = False) -> str:
"""
Tool description for LLM.
Args:
param: Parameter description
summarized_response:
- False (default): Full structured data for LLM processing
- True: Condensed summary with key insights
Returns:
JSON string with data or summary
"""
# 1. Call business logic / API client
# 2. Format response
# 3. Return JSON string
Data Flow
Example: Find Recent Deals for Address
1. LLM Request
└─> "Find deals for Sokolov 38 Holon in last 2 years"
2. MCP Tool: find_recent_deals_for_address()
├─> Validate inputs
└─> Call GovmapClient
3. GovmapClient Workflow:
├─> autocomplete_address("סוקולוב 38 חולון")
│ ├─> Rate limit check
│ ├─> HTTP POST (with retry)
│ └─> Parse coordinates
│
├─> get_deals_by_radius(coordinates, 30m)
│ ├─> Rate limit check
│ ├─> HTTP GET (with retry)
│ └─> Extract polygon_ids
│
├─> For each polygon_id:
│ ├─> get_street_deals(polygon_id)
│ └─> get_neighborhood_deals(polygon_id)
│
├─> Filter & Prioritize:
│ ├─> Same building deals (priority 0)
│ ├─> Street deals (priority 1)
│ └─> Neighborhood deals (priority 2)
│
└─> Sort by priority then date
4. Format Response
└─> JSON with deals + metadata
5. Return to LLM
└─> LLM analyzes and responds to user
Error Handling Strategy
Validation Errors (ValueError)
- Invalid address format
- Negative/zero parameters
- Out-of-range values
- Action: Immediate failure with clear message
Network Errors (requests.RequestException)
- Connection failures
- Timeouts
- HTTP errors
- Action: Retry with exponential backoff (up to configured max_retries)
API Response Errors
- Invalid JSON
- Unexpected format
- Missing required fields
- Action: Raise ValueError with specific details
Rate Limiting
- Track last request time
- Sleep if necessary before request
- Action: Transparent to caller, automatic
Performance Considerations
Current Implementation
Optimizations:
- Connection pooling (requests.Session)
- Rate limiting prevents API throttling
- Early validation reduces unnecessary API calls
- Polygon deduplication in find_recent_deals_for_address()
Limitations:
- Synchronous (blocking) API calls
- No caching (MVP decision)
- Sequential polygon queries
Future Optimizations (Not in MVP)
Phase 1: In-Memory Caching
- Cache autocomplete results (1 hour TTL)
- Cache deal results (30 minute TTL)
- Reduce API load by 60-80%
Phase 2: Async/Parallel
- Convert to async/await
- Parallel polygon queries
- 3-5x faster for multi-polygon searches
Phase 3: Production Caching
- Redis for distributed caching
- Cache warming strategies
- Cross-instance consistency
Security & Rate Limiting
Rate Limiting
Current: 5 requests/second (configurable)
Rationale:
- Respects Govmap API (no published limits, being conservative)
- Prevents accidental DoS
- Balances responsiveness with safety
Implementation:
- Tracks last request timestamp
- Sleeps if interval too short
- Per-instance (not distributed)
Input Validation
All user inputs are validated before use:
- Address strings: length, type, non-empty
- Coordinates: numeric, reasonable bounds
- Integers: positive, max values
- Deal types: enum validation (1 or 2)
Prevents:
- Injection attacks
- API errors from malformed requests
- Expensive/dangerous operations
API Key Management
Current: No API keys required (public API)
Future: If API keys added:
- Store in environment variables only
- Never log or expose in responses
- Rotate regularly
Testing Strategy
Unit Tests
- Mock API responses
- Test validation logic
- Test error handling
- Test retry logic
Integration Tests
- Real API calls (marked
@pytest.mark.integration) - VCR.py for recording/replaying
- Test complete workflows
Validation Tests
- Invalid inputs
- Edge cases
- Boundary conditions
Configuration Options
Environment Variables
# API Settings
GOVMAP_BASE_URL=https://www.govmap.gov.il/api/
GOVMAP_USER_AGENT=NadlanMCP/1.0.0
# Timeouts (seconds)
GOVMAP_CONNECT_TIMEOUT=10
GOVMAP_READ_TIMEOUT=30
# Retry Settings
GOVMAP_MAX_RETRIES=3
GOVMAP_RETRY_MIN_WAIT=1
GOVMAP_RETRY_MAX_WAIT=10
# Rate Limiting
GOVMAP_REQUESTS_PER_SECOND=5.0
# Defaults
GOVMAP_DEFAULT_RADIUS=50
GOVMAP_DEFAULT_YEARS_BACK=2
GOVMAP_DEFAULT_DEAL_LIMIT=100
# Performance
GOVMAP_MAX_POLYGONS=10 # Max polygons to query per search (limits API calls)
Tuning Guidelines
High Reliability (Production):
MAX_RETRIES=5RETRY_MAX_WAIT=30REQUESTS_PER_SECOND=3.0
High Performance (Development):
MAX_RETRIES=2RETRY_MAX_WAIT=5REQUESTS_PER_SECOND=10.0MAX_POLYGONS=5(faster, fewer results)
Conservative (Shared API):
MAX_RETRIES=3RETRY_MAX_WAIT=10REQUESTS_PER_SECOND=2.0
Future Architecture Evolution
Phase 3: Package Refactoring (✅ COMPLETED)
✅ COMPLETED - See .cursor/plans/PHASE3-REFACTORING.md for detailed plan
The monolithic govmap.py (1,454 lines) has been refactored into a modular package:
nadlan_mcp/
├── __init__.py # Backward compatibility
├── config.py # ✅ Configuration
├── main.py # ✅ Entry point
├── fastmcp_server.py # ✅ MCP tools
└── govmap/ # 📦 NEW PACKAGE
├── __init__.py # Public API exports
├── client.py # Core API client (~300 lines)
├── validators.py # Input validation (~100 lines)
├── models.py # ✅ Pydantic v2 models (~340 lines)
├── filters.py # Deal filtering (~150 lines)
├── statistics.py # Statistical calculations (~150 lines)
├── market_analysis.py # Market analysis (~400 lines)
└── utils.py # Helper utilities (~100 lines)
Benefits:
- ✅ Single Responsibility Principle - each module has one job
- ✅ Easier testing - test modules in isolation
- ✅ Better maintainability - smaller files (100-400 lines)
- ✅ Reusability - import only what you need
- ✅ 100% backward compatible - existing code still works
Module Responsibilities:
-
client.py - Pure API interactions with Govmap
- HTTP requests, rate limiting, response parsing
- No business logic, just API calls
-
validators.py - Input validation
- Address, coordinate, integer, date validation
- Pure functions, clear error messages
-
filters.py - Deal filtering logic
- Property type, rooms, price, area, floor filters
- Composable filter functions
-
statistics.py - Statistical calculations
- Mean, median, percentiles, std dev
- Pure math functions, no I/O
-
market_analysis.py - Market analysis functions
- Activity scoring, investment analysis, liquidity
- Works with data, no API calls
-
utils.py - Shared utilities
- Address matching, text normalization, helpers
- Reusable across modules
-
models.py - Pydantic v2 data models ✅ IMPLEMENTED
- 9 models: Deal, Address, AutocompleteResponse, DealStatistics, etc.
- Type safety, validation, computed fields
- Field aliasing for API compatibility
Backward Compatibility:
# OLD CODE (still works)
from nadlan_mcp import GovmapClient
client = GovmapClient()
# NEW CODE (also works)
from nadlan_mcp.govmap import GovmapClient
from nadlan_mcp.govmap.filters import filter_deals_by_criteria
Phase 4: Pydantic Data Models ✅ IMPLEMENTED in v2.0.0
Comprehensive Pydantic v2 models with type safety and validation:
from nadlan_mcp.govmap.models import Deal, DealStatistics, AutocompleteResponse
# Deal model with computed fields
deal = Deal(
objectid=123,
deal_amount=1500000.0,
deal_date="2024-01-15",
asset_area=85.0
)
# price_per_sqm automatically computed!
assert deal.price_per_sqm == 17647.06
# Field aliases support both API and Python naming
deal = Deal(dealAmount=1500000, assetArea=85, ...) # API style
deal = Deal(deal_amount=1500000, asset_area=85, ...) # Python style
9 Models Implemented:
- CoordinatePoint, Address, AutocompleteResult, AutocompleteResponse
- Deal, DealStatistics, DealFilters
- MarketActivityScore, InvestmentAnalysis, LiquidityMetrics
See MIGRATION.md for v1.x → v2.0 upgrade guide.
Phase 5: Database Layer (Future - Optional)
For historical tracking and faster queries:
├── database/
│ ├── models.py # SQLAlchemy models
│ ├── crud.py # CRUD operations
│ └── migrations/ # Alembic migrations
Deployment
Development
python run_fastmcp_server.py
Production (MCP Client)
{
"servers": {
"nadlan-mcp": {
"command": "python",
"args": ["/path/to/nadlan-mcp/run_fastmcp_server.py"],
"env": {
"GOVMAP_REQUESTS_PER_SECOND": "3.0",
"GOVMAP_MAX_RETRIES": "5"
}
}
}
}
API Limitations
Govmap API Constraints
Known Limitations:
- No published rate limits (being conservative with 5 req/sec)
- No authentication required (public API)
- Data freshness: Updated periodically (not real-time)
- Coverage: Official Israeli government records only
Best Practices:
- Use appropriate time ranges (2-5 years recommended)
- Limit radius searches (under 1000m for performance)
- Cache results when possible (future feature)
- Respect rate limiting
MCP Protocol Constraints
Token Limits:
- Large deal lists can exceed LLM context windows
- Use
summarized_response=Truefor large datasets - Default limits (50-100 deals) are token-optimized
Monitoring & Debugging
Logging Levels
INFO: Normal operations
- Requests being made
- Successful responses
WARNING: Recoverable issues
- Retrying after failures
- Rate limiting delays
- Validation warnings
ERROR: Failures
- Exhausted retries
- Invalid responses
- Configuration errors
Debug Mode
Enable debug logging:
import logging
logging.basicConfig(level=logging.DEBUG)