Skip to main content
Glama

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

geocode(query)

Forward geocode a place name.

lat/lon decimal degrees

reverse_geocode(lat, lon)

Coordinate → address string.

decimal degrees → string

isochrone(lat, lon, mode, minutes)

Reachable-area polygon within a time budget.

minutes; polygon [lon,lat]; area m²

catchment_pois(lat, lon, minutes, mode, categories)

POIs in the catchment, grouped by enum category.

counts

site_reach_score(lat, lon, minutes, mode, weights?)

Composite reach score [0,1] + breakdown. ★

unitless [0,1]

compare_sites(sites, minutes, mode, weights?)

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_mcp

Claude 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-missing

Methodology & 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) where d is its distance from the site and D a 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-Agent header (configurable; required by Nominatim).

  • transit mode falls back to the OSRM foot profile — OSRM has no transit router. For real transit isochrones, self-host a transit router (e.g. RAPTOR/OTP) and point OSRM_BASE_URL at 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_URL env 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

NOMINATIM_BASE_URL

https://nominatim.openstreetmap.org

Geocoding upstream

OVERPASS_BASE_URL

https://overpass-api.de

POI upstream

OSRM_BASE_URL

https://router.project-osrm.org

Routing upstream

USER_AGENT

open-reach-mcp/0.1.0 (...)

Required by Nominatim policy

CACHE_MAXSIZE / CACHE_TTL_SECONDS

2048 / 86400

TTL cache sizing

RATE_LIMIT_MIN_INTERVAL_SECONDS

1.0

Per-host request spacing

HTTP_TIMEOUT_SECONDS

15.0

Upstream call timeout


Publishing

v1 ships stdio transport. Publishing steps (run manually after build):

  1. PyPI: python -m build && twine upload dist/*.

  2. 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.