Search Price Paid Transactions by Postcode
land_title_searchSearch HM Land Registry Price Paid Index by postcode or address and retrieve up to 10 recent sale transactions with price, date, property type, and tenure.
Instructions
Search HM Land Registry Price Paid Index by postcode or address.
Returns up to 10 recent sale transactions for the postcode: price, date, address, property type, and tenure (Freehold/Leasehold). Covers England and Wales only. Postcode gives the most reliable results — a full address is also accepted and the postcode is extracted automatically.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| address_or_postcode | Yes | UK property address or postcode. Postcode is most reliable: e.g. 'NG1 1AB'. Full address also accepted. |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| postcode | Yes | Normalised UK postcode extracted from the input. | |
| total | Yes | Number of Price Paid transactions returned. Capped at 10 by the upstream SPARQL query. | |
| transactions | No | Recent Price Paid transactions for the postcode, sorted newest first. |
Implementation Reference
- land_registry.py:80-156 (handler)The main handler function for the land_title_search tool. Extracts a postcode from the input, queries the HM Land Registry SPARQL endpoint for Price Paid Index data, and returns up to 10 recent sale transactions.
async def land_title_search( address_or_postcode: Annotated[str, Field(description="UK property address or postcode. Postcode is most reliable: e.g. 'NG1 1AB'. Full address also accepted.", min_length=4, max_length=200)], ) -> LandTitleSearchResult: """Search HM Land Registry Price Paid Index by postcode or address. Returns up to 10 recent sale transactions for the postcode: price, date, address, property type, and tenure (Freehold/Leasehold). Covers England and Wales only. Postcode gives the most reliable results — a full address is also accepted and the postcode is extracted automatically. """ # Extract or use postcode text = address_or_postcode.strip().upper() postcode = _extract_postcode(text) if not postcode: raise ValueError( "Could not extract a valid UK postcode from the input. " "Please include a postcode, e.g. 'NG1 1AB' or " "'1 High Street, Nottingham, NG1 1AB'." ) import httpx as _httpx # Price Paid query via SPARQL — POST with form-encoded body # (the endpoint requires POST). sparql_query = PPI_QUERY_TEMPLATE.format(postcode=postcode) body = urllib.parse.urlencode({"query": sparql_query}).encode() async with _httpx.AsyncClient(timeout=20.0) as client: ppi_resp = await client.post( SPARQL_ENDPOINT, content=body, headers={ "Accept": "application/sparql-results+json", "Content-Type": "application/x-www-form-urlencoded", }, ) ppi_resp.raise_for_status() ppi_data = ppi_resp.json() def _val(b: dict, key: str) -> str: return b.get(key, {}).get("value", "") or "" def _uri_label(uri: str) -> str: """Extract readable label from HMLR URI, e.g. .../propertyType/terraced → Terraced.""" return uri.rstrip("/").split("/")[-1].replace("-", " ").title() if uri else "" bindings = ppi_data.get("results", {}).get("bindings", []) transactions: list[LandTitleTransaction] = [] for b in bindings: price_raw = _val(b, "pricePaid") price_int: int | None try: price_int = int(float(price_raw)) if price_raw else None except ValueError: price_int = None transactions.append( LandTitleTransaction( price_paid=price_int, transaction_date=_val(b, "transactionDate")[:10] or None, postcode=_val(b, "postcode") or None, paon=_val(b, "paon") or None, saon=_val(b, "saon") or None, street=_val(b, "street") or None, town=_val(b, "town") or None, county=_val(b, "county") or None, property_type=_uri_label(_val(b, "propertyType")) or None, estate_type=_uri_label(_val(b, "estateType")) or None, ) ) return LandTitleSearchResult( postcode=postcode, total=len(transactions), transactions=transactions, ) - models.py:654-676 (schema)Pydantic response model for the land_title_search tool. Contains the normalized postcode, total count, and a list of LandTitleTransaction items.
class LandTitleSearchResult(BaseModel): """HMLR Price Paid Index search result for a given postcode.""" model_config = BASE_CFG postcode: str = Field( ..., description="Normalised UK postcode extracted from the input.", ) total: int = Field( ..., description=( "Number of Price Paid transactions returned. Capped at 10 by the " "upstream SPARQL query." ), ) transactions: list[LandTitleTransaction] = Field( default_factory=list, description=( "Recent Price Paid transactions for the postcode, sorted newest first." ), ) - models.py:618-652 (schema)Pydantic model representing a single Price Paid transaction returned by the land_title_search tool.
class LandTitleTransaction(BaseModel): """A single Price Paid transaction for a property.""" model_config = BASE_CFG price_paid: int | None = Field( None, description="Sale price in GBP (integer pounds)." ) transaction_date: str | None = Field( None, description="Transaction date (ISO YYYY-MM-DD)." ) postcode: str | None = Field(None, description="Property postcode.") paon: str | None = Field( None, description="Primary addressable object name (house number or name).", ) saon: str | None = Field( None, description="Secondary addressable object name (flat/unit identifier).", ) street: str | None = Field(None, description="Street name.") town: str | None = Field(None, description="Town/city.") county: str | None = Field(None, description="County.") property_type: str | None = Field( None, description=( "Property type label extracted from the HMLR URI " "(e.g. 'Terraced', 'Semi Detached', 'Flat')." ), ) estate_type: str | None = Field( None, description="Tenure / estate type label (e.g. 'Freehold', 'Leasehold').", ) - land_registry.py:70-79 (registration)Registration of the land_title_search tool via the @mcp.tool decorator with name='land_title_search' and annotations declaring it read-only, idempotent, and safe.
@mcp.tool( name="land_title_search", annotations={ "title": "Search Price Paid Transactions by Postcode", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True, }, ) - land_registry.py:55-62 (helper)Helper function that extracts a UK postcode from free-text input using a regex pattern. Used by land_title_search to parse the address_or_postcode parameter.
def _extract_postcode(text: str) -> str | None: """Try to extract a postcode from a free-text address.""" import re # UK postcode regex pattern = r"\b[A-Z]{1,2}\d{1,2}[A-Z]?\s*\d[A-Z]{2}\b" match = re.search(pattern, text.upper()) return match.group(0).replace(" ", " ").strip() if match else None