Compare two submarkets
A common pattern: the user knows submarkets by name (“Buckhead” vs
“Sandy Springs”) but the comparison tool needs IDs. This example chains
search_estaite_submarkets to resolve each
name, then calls
compare_estaite_submarkets with the two
IDs.
This is the search → use pattern you’ll write a lot of. Note the
short asyncio.sleep between tool calls — the Free tier caps tool
calls at 2/sec, and this workflow makes three back-to-back tool calls,
so we pace them.
The code
Save as compare-submarkets.py:
"""Compare two submarkets side by side.
Setup: pip install mcp export ESTAITE_API_KEY="mk_..."
Usage: python compare-submarkets.py "Buckhead" "Sandy Springs""""
import argparseimport asyncioimport jsonimport osimport sys
from mcp import ClientSessionfrom mcp.client.streamable_http import streamablehttp_client
MCP_URL = "https://mcp.estaite.com"
async def resolve_id(session, name: str) -> tuple[int, str]: """Search for a submarket by name and return (id, full_name) for the top match.""" result = await session.call_tool( "search_estaite_submarkets", {"query": name}, ) if not result.content: raise RuntimeError(f"Empty response searching for '{name}'") data = json.loads(result.content[0].text) matches = data.get("results") or [] if not matches: raise RuntimeError(f"No submarkets matched '{name}'") top = matches[0] return top["id"], top.get("name") or name
async def compare(api_key: str, name_a: str, name_b: str) -> dict: headers = {"x-api-key": api_key} async with streamablehttp_client(MCP_URL, headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize()
# Free tier allows 2 tools/sec. We make 3 tool calls back-to-back # (two searches + one compare), so we pace ~0.6s between them to # stay under the limit. Production code should also catch 429 and # honor the Retry-After response header. id_a, full_a = await resolve_id(session, name_a) await asyncio.sleep(0.6) id_b, full_b = await resolve_id(session, name_b) await asyncio.sleep(0.6) print(f" Resolved: '{name_a}' → {full_a} (id {id_a})") print(f" Resolved: '{name_b}' → {full_b} (id {id_b})") print()
result = await session.call_tool( "compare_estaite_submarkets", {"submarkets": [id_a, id_b]}, ) return json.loads(result.content[0].text)
def fmt_money(v): return f"${v:,.0f}" if v is not None else "—"def fmt_pct(v): return f"{v:+.1f}%" if v is not None else "—"def fmt_vac(v): return f"{v:.1f}%" if v is not None else "—"
def print_comparison(data: dict, name_a: str, name_b: str) -> None: subs = data.get("data") or [] if len(subs) < 2: print(" Comparison failed — fewer than two submarkets returned.") return
a, b = subs[0], subs[1] # Summary mode (no property_type/bedrooms specified) returns: # { segments: { apartments: { br1, br2, br3, br4 }, ... }, market_health: { vacancy, saturation }, ... } apt2 = lambda s: ((s.get("segments") or {}).get("apartments") or {}).get("br2") or {} mh = lambda s: s.get("market_health") or {}
rows = [ ("Median rent (apt 2BR)", fmt_money(apt2(a).get("median_price")), fmt_money(apt2(b).get("median_price"))), ("YoY change", fmt_pct(apt2(a).get("yoy_change")), fmt_pct(apt2(b).get("yoy_change"))), ("Listings (apt 2BR)", str(apt2(a).get("listings") or "—"), str(apt2(b).get("listings") or "—")), ("Vacancy", fmt_vac(mh(a).get("vacancy")), fmt_vac(mh(b).get("vacancy"))), ("Saturation", fmt_vac(mh(a).get("saturation")), fmt_vac(mh(b).get("saturation"))), ]
label_a = a.get("submarket_name") or name_a label_b = b.get("submarket_name") or name_b print(f" {'Metric':28s} {label_a[:18]:>18s} {label_b[:18]:>18s}") print(f" {'-' * 28} {'-' * 18} {'-' * 18}") for label, va, vb in rows: print(f" {label:28s} {va:>18s} {vb:>18s}") print() if data.get("attribution"): print(f" {data['attribution']}") print()
def main(): parser = argparse.ArgumentParser(description="Compare two submarkets by name") parser.add_argument("name_a", help='First submarket (e.g. "Buckhead")') parser.add_argument("name_b", help='Second submarket (e.g. "Sandy Springs")') args = parser.parse_args()
api_key = os.environ.get("ESTAITE_API_KEY") if not api_key: print("ERROR: ESTAITE_API_KEY env var not set.") sys.exit(1)
print(f"\nComparing {args.name_a} vs {args.name_b}...\n") data = asyncio.run(compare(api_key, args.name_a, args.name_b)) print_comparison(data, args.name_a, args.name_b)
if __name__ == "__main__": main()Run it
python compare-submarkets.py "Buckhead" "Sandy Springs"Sample output
Comparing Buckhead vs Sandy Springs...
Resolved: 'Buckhead' → Buckhead (id 758) Resolved: 'Sandy Springs' → Roswell / Sandy Springs (id 43)
Metric Buckhead Roswell / Sandy Sp ---------------------------- ------------------ ------------------ Median rent (apt 2BR) $2,348 $1,675 YoY change +5.0% -4.3% Listings (apt 2BR) 276 686 Vacancy 5.5% 5.5% Saturation 17.6% 10.0%
Data provided by Estaite Solutions (estaite.com)(Numbers shift each month as the data refreshes — yours may differ.)
What’s happening
- Two
search_estaite_submarketscalls resolve the user’s friendly names to canonical IDs. Take the top match (results[0]) — that’s almost always what the user meant. Note that partial matches are returned, so “Sandy Springs” can resolve to “Roswell / Sandy Springs” since that’s the most specific submarket containing the name. - One
compare_estaite_submarketscall with the IDs returns both submarkets underdatain the response, with parallel metrics so the rows line up. - Response shape (summary mode — no
property_type/bedrooms): each item hassegments.apartments.br2.median_price,.yoy_change,.listings, plusmarket_health.vacancyand.saturationat the top level. Vacancy and YoY are whole-percent (e.g. 5.5 means 5.5%). - We print a simple text table. In a real app you’d render it as HTML or hand it to an LLM for narration.
Next steps
- Add a third submarket.
compare_estaite_submarketsaccepts an array of IDs — pass three or four for a wider view. - Filter by property type and bedrooms. Pass
property_type: "sfr"andbedrooms: 3to get the SFR 3BR slice instead of the multi-segment summary. The response shape changes — in slice mode each item has a singlemetricsobject withmedian_price,yoy_change,listingsdirectly. - Disambiguate ambiguous names. Cities have repeated submarket names (every metro has a “Downtown”). When
resultsreturns more than one, you can either show the user the list or filter bymsaon the search call.