Add: Decisive appraiser (שמאי מכריע) search via gov.il public API
Adds a new MCP tool `search_decisive_appraisals` that queries the Ministry of Justice public registry (~30K published decisions) by block (גוש), plot (חלקה), appraiser name, committee, decision/publicity date ranges, or free text — and returns metadata + direct PDF URLs. Implementation notes: - New `nadlan_mcp/govil/` package, parallel to `nadlan_mcp/govmap/`, for gov.il APIs that are not Govmap. Pydantic v2 models match the upstream PascalCase response via aliases. - Upstream sits behind an F5 WAF that rejects standard `requests`; uses `curl_cffi` with Chrome 120 impersonation to traverse it. - Static `x-client-id` header (issued to the gov.il SPA, public, visible in any DevTools session) is required by the gateway — without it every call returns a generic 500. - 14 unit tests cover model parsing, body shape, pagination, and the 500-is-fatal contract (configuration error, not retryable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ from mcp.server.fastmcp import FastMCP
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from nadlan_mcp.config import get_config
|
||||
from nadlan_mcp.govil import DecisiveAppraiserClient
|
||||
from nadlan_mcp.govmap import GovmapClient
|
||||
from nadlan_mcp.govmap.models import Deal
|
||||
from nadlan_mcp.govmap.outlier_detection import filter_deals_for_analysis
|
||||
@@ -28,6 +29,9 @@ mcp = FastMCP("nadlan-mcp")
|
||||
# Initialize the Govmap client
|
||||
client = GovmapClient()
|
||||
|
||||
# Initialize the Decisive Appraiser (gov.il / Ministry of Justice) client
|
||||
decisive_appraiser_client = DecisiveAppraiserClient()
|
||||
|
||||
|
||||
def conditional_tool(config_flag: str):
|
||||
"""
|
||||
@@ -1310,6 +1314,112 @@ def get_market_activity_metrics(address: str, years_back: int = 2, radius_meters
|
||||
return f"Error analyzing market activity: {str(e)}"
|
||||
|
||||
|
||||
@conditional_tool("tool_search_decisive_appraisals_enabled")
|
||||
def search_decisive_appraisals(
|
||||
block: Optional[str] = None,
|
||||
plot: Optional[str] = None,
|
||||
decisive_appraiser: Optional[str] = None,
|
||||
committee: Optional[str] = None,
|
||||
decision_date_from: Optional[str] = None,
|
||||
decision_date_to: Optional[str] = None,
|
||||
publicity_date_from: Optional[str] = None,
|
||||
publicity_date_to: Optional[str] = None,
|
||||
search_text: Optional[str] = None,
|
||||
appraisal_header: Optional[str] = None,
|
||||
max_results: int = 30,
|
||||
) -> str:
|
||||
"""Search published "decisive appraiser" (שמאי מכריע) decisions from the Israeli
|
||||
Ministry of Justice public registry. The registry holds 30,000+ written
|
||||
decisions on land betterment levy, expropriation, real-estate disputes etc.
|
||||
|
||||
All filters are optional. Use block + plot for property-level lookups
|
||||
(גוש + חלקה), or `decisive_appraiser` to find every decision by a given
|
||||
appraiser. Page size is fixed by the upstream at 10; this tool pages
|
||||
automatically up to `max_results`.
|
||||
|
||||
Args:
|
||||
block: Land block number, גוש (e.g. "6212"). Exact match.
|
||||
plot: Land plot number, חלקה (e.g. "894"). Use with block.
|
||||
decisive_appraiser: Appraiser name in Hebrew (e.g. "דדון דוד").
|
||||
committee: Local planning committee (e.g. "תל אביב-יפו").
|
||||
decision_date_from / decision_date_to: Date range for the decision date.
|
||||
Format: dd-MM-yyyy (e.g. "01-01-2024"). Either bound is optional.
|
||||
publicity_date_from / publicity_date_to: Date range for publication.
|
||||
search_text: Free-text search inside the decision document.
|
||||
appraisal_header: Search within the decision title.
|
||||
max_results: Maximum number of decisions to return (default 30).
|
||||
|
||||
Returns:
|
||||
JSON string with `total_results` (database-wide), `returned`, the
|
||||
applied `filters`, and a `decisions` array. Each decision contains
|
||||
the appraiser name, block/plot, committee, dates, the decision title
|
||||
and a `pdf_url` to download the original PDF.
|
||||
"""
|
||||
log_mcp_call(
|
||||
"search_decisive_appraisals",
|
||||
block=block,
|
||||
plot=plot,
|
||||
decisive_appraiser=decisive_appraiser,
|
||||
committee=committee,
|
||||
decision_date_from=decision_date_from,
|
||||
decision_date_to=decision_date_to,
|
||||
publicity_date_from=publicity_date_from,
|
||||
publicity_date_to=publicity_date_to,
|
||||
search_text=search_text,
|
||||
appraisal_header=appraisal_header,
|
||||
max_results=max_results,
|
||||
)
|
||||
try:
|
||||
response = decisive_appraiser_client.search_decisions_paged(
|
||||
max_results=max_results,
|
||||
block=block,
|
||||
plot=plot,
|
||||
decisive_appraiser=decisive_appraiser,
|
||||
committee=committee,
|
||||
decision_date_from=decision_date_from,
|
||||
decision_date_to=decision_date_to,
|
||||
publicity_date_from=publicity_date_from,
|
||||
publicity_date_to=publicity_date_to,
|
||||
search_text=search_text,
|
||||
appraisal_header=appraisal_header,
|
||||
)
|
||||
|
||||
decisions = []
|
||||
for idx, decision in enumerate(response.results, start=1):
|
||||
data = decision.model_dump(mode="json", exclude_none=True)
|
||||
documents = data.pop("documents", []) or []
|
||||
primary_pdf = documents[0]["file_url"] if documents else None
|
||||
data["id"] = idx
|
||||
data["pdf_url"] = primary_pdf
|
||||
data["all_documents"] = documents
|
||||
decisions.append(data)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"total_results": response.total_results,
|
||||
"returned": len(decisions),
|
||||
"filters": {
|
||||
"block": block,
|
||||
"plot": plot,
|
||||
"decisive_appraiser": decisive_appraiser,
|
||||
"committee": committee,
|
||||
"decision_date_from": decision_date_from,
|
||||
"decision_date_to": decision_date_to,
|
||||
"publicity_date_from": publicity_date_from,
|
||||
"publicity_date_to": publicity_date_to,
|
||||
"search_text": search_text,
|
||||
"appraisal_header": appraisal_header,
|
||||
},
|
||||
"decisions": decisions,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in search_decisive_appraisals: {e}", exc_info=True)
|
||||
return f"Error searching decisive appraisals: {str(e)}"
|
||||
|
||||
|
||||
# Health check endpoint for HTTP deployments
|
||||
@mcp.custom_route("/health", methods=["GET"])
|
||||
async def health_check(request):
|
||||
|
||||
Reference in New Issue
Block a user