Open Reach
Open Reach
An MCP server for out-of-home (OOH) site-reachability tooling.
Open Reach exposes geospatial reachability and catchment-scoring tools for billboard / ad-site evaluation — isochrone catchments, POI-density scoring, and multi-site comparison — to any MCP client (Claude Desktop, Cursor, etc.), built entirely on open geospatial data and public urban-analytics methodology.
The differentiator is server-side reach scoring and multi-site ranking (
site_reach_score,compare_sites). There are several public OSM-wrapper MCP servers already; none of them do catchment scoring. That gap — plus honest, public methodology — is the point of this project.
The hook (demo)
You (in Claude Desktop):
"I'm scouting 3 candidate billboard sites near KLCC, Bukit Bintang, and
Mont Kiara. Which has the best pedestrian catchment and highest POI
density within a 10-minute walk?"
Claude:
→ geocode("KLCC"), geocode("Bukit Bintang"), geocode("Mont Kiara")
→ isochrone(..., mode="walk", minutes=10) [x3]
→ catchment_pois(..., categories=["cafe","retail","transit"]) [x3]
→ site_reach_score(...) [x3]
→ compare_sites(sites=[...], minutes=10, mode="walk")
← ranked: 1. Bukit Bintang 2. KLCC 3. Mont Kiara (with per-site breakdown)The ranking arithmetic runs server-side in compare_sites — the model's job
is orchestration and narration, not the math.
How it works
┌─────────────────────┐ MCP (JSON-RPC over stdio)
│ MCP Client │ ──────────────────────────────┐
│ (Claude Desktop / │ │
│ Cursor / Code) │ ◄── tool schemas advertised │
└─────────────────────┘ ▼
┌────────────────────────────────────┐
│ Open Reach MCP Server │
│ (FastMCP process) │
│ │
│ @mcp.tool: geocode │
│ @mcp.tool: reverse_geocode │
│ @mcp.tool: isochrone │
│ @mcp.tool: catchment_pois │
│ @mcp.tool: site_reach_score ★ │
│ @mcp.tool: compare_sites ★ │
│ │
│ TTL cache + per-host rate limiting │
└──────────────────┬───────────────────┘
│ https GET/POST
┌──────────────────────────────────┼───────────────────────┐
▼ ▼ ▼
┌───────────────────┐ ┌──────────────────────┐ ┌───────────────────┐
│ OSM Nominatim │ │ Overpass API │ │ OSRM │
│ (geocoding) │ │ (POIs by category) │ │ (isochrones) │
└───────────────────┘ └──────────────────────┘ └───────────────────┘Each scoring tool chains isochrone → POIs/junctions → a pure, unit-tested arithmetic module, then returns a typed result plus an auditable breakdown.
Tools
Tool | Description | Units |
| Forward geocode a place name. | lat/lon decimal degrees |
| Coordinate → address string. | decimal degrees → string |
| Reachable-area polygon within a time budget. | minutes; polygon |
| POIs in the catchment, grouped by enum category. | counts |
| Composite reach score | unitless |
| Deterministic multi-site ranking. ★ | ranks (1 = best) |
mode ∈ {walk, drive, transit}. categories are enum-constrained — callers
never supply raw Overpass QL; all queries are built server-side.
Quick start
git clone https://github.com/kasturi/open-reach-mcp.git
cd open-reach-mcp
python -m venv .venv && .venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS/Linux
pip install -e ".[dev]"Run standalone (stdio):
open-reach-mcp # or: python -m open_reach_mcpClaude Desktop config
Add to claude_desktop_config.json (macOS:
~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"open-reach": {
"command": "C:\\path\\to\\open-reach-mcp\\.venv\\Scripts\\open-reach-mcp.exe",
"args": []
}
}
}Set
USER_AGENT(see.env.example) to a descriptive value — Nominatim usage policy requires it.
Tests
pytest --cov=open_reach_mcp --cov-report=term-missingMethodology & IP boundary (important)
The reach-scoring formula in site_reach_score is derived only from public
urban-analytics literature, not from any proprietary methodology or real
campaign/device data:
Gravity-model catchment accessibility (Hansen, 1959; classic spatial interaction) — each POI contributes
exp(-d / D)wheredis its distance from the site andDa pedestrian decay constant.Walkability-style density indices — POI density and road-junction density per km², normalized against public reference benchmarks to
[0, 1].Category-mix diversity via normalized Shannon entropy.
This project stands for the engineering pattern (geospatial proxy scoring under rate-limited free APIs, packaged as an MCP server). It is not a claim of parity with any production reach/audience system, and it uses no proprietary formulas or real client data.
Upstream dependencies & limits
All upstream APIs are free-tier and shared/public, so they are rate-limited and occasionally fragile. Open Reach mitigates this with:
TTL cache (
CACHE_TTL_SECONDS, default 24h) + per-host rate limiting (RATE_LIMIT_MIN_INTERVAL_SECONDS, default 1s — Nominatim's policy ceiling).A compliant
User-Agentheader (configurable; required by Nominatim).transitmode falls back to the OSRMfootprofile — OSRM has no transit router. For real transit isochrones, self-host a transit router (e.g. RAPTOR/OTP) and pointOSRM_BASE_URLat it. This is a known limitation, stated openly rather than hidden.For production throughput, self-host Nominatim / Overpass / OSRM (Docker images exist for all three) and set the
*_BASE_URLenv vars. Turning upstream fragility into a documented self-host path is part of the point.
Configuration
All settings are environment-driven (see .env.example):
Variable | Default | Purpose |
|
| Geocoding upstream |
|
| POI upstream |
|
| Routing upstream |
|
| Required by Nominatim policy |
|
| TTL cache sizing |
|
| Per-host request spacing |
|
| Upstream call timeout |
Publishing
v1 ships stdio transport. Publishing steps (run manually after build):
PyPI:
python -m build && twine upload dist/*.Official MCP registry: submit
server.json(verified GitHub ownership) at registry.modelcontextprotocol.io. PyPI alone no longer supports a "published to the registry" claim; the official registry (launched Sept 2025) requires the manifest + verified ownership.
SSE-only transports are deprecated since MCP spec 2025-03-26. A Streamable HTTP transport is the planned v2 stretch (no SSE).
License
MIT — see LICENSE.