Skip to main content
Glama
README.md
# PlaceScout

PlaceScout turns open places data into AI-assisted local market intelligence. It exposes a set of MCP tools that let an AI agent search 4.4 million UK places by category, location, and proximity — and save promising results to a prospecting pipeline with a live dashboard.

---

## Architecture

```mermaid
graph TD
    Client["MCP Client\n(Claude / ChatGPT)"]

    subgraph PlaceScout Service
        MCP["FastMCP Server\nstreamable-http · /mcp"]
        API["REST API\n/api/v2/leads"]
        UI["React Dashboard\n/dashboard"]

        subgraph Search
            Embed["Jina Embedder\njina-embeddings-v5-text-nano · 768D"]
            DB["DuckDB\nVSS · FTS · Spatial\n4.4M places · 1278 categories"]
        end

        subgraph Generative UI
            Prefab["Prefab UI\nPyodide WASM sandbox"]
        end
    end

    Browser["Browser"]

    Client -->|"tool calls over HTTP"| MCP
    MCP --> Embed
    MCP --> DB
    Embed --> DB
    MCP --> API
    MCP -->|"dashboard tool"| UI
    MCP -->|"execute_ui tool"| Prefab
    API --> DB
    UI -->|"fetch"| API
    Client -->|"opens"| Browser
    Browser --> UI
```

---

## MCP Tools

All tools are served at `/mcp` over the [MCP streamable-http transport](https://modelcontextprotocol.io).

### Search

| Tool | Description |
|------|-------------|
| `search_places` | Hybrid semantic + keyword search against 1278 categories, filtered by location. Returns name, address, website, contact info, and social handles. |
| `nearby_places` | Finds places within a radius of one or more seed place IDs, sorted by distance. Optionally filtered by category. |
| `categories_by_location` | Returns the most common place categories in an area — useful for understanding a local market before searching. |
| `sample_places` | Random or criteria-based sampling: `random`, `recent`, `with_website`, `with_social_media`, `with_email`, `with_phone`. |

Search uses a two-stage hybrid pipeline:
1. **Category matching** — vector similarity (Jina v5, 60%) + BM25 full-text (40%) against the category taxonomy
2. **Place filtering** — DuckDB spatial + FTS query against matched category IDs and location fields

### Leads / Prospecting

| Tool | Description |
|------|-------------|
| `save_lead` | Saves a place to the prospecting pipeline with an initial status (`new`, `contacted`, `qualified`, `disqualified`, `converted`) and optional notes. |
| `dashboard` | Opens the leads dashboard as a full-screen embed in the MCP client. |

### Generative UI

| Tool | Description |
|------|-------------|
| `execute_ui` | Runs Prefab Python code in a Pyodide WASM sandbox and renders the result inline. Accepts search result JSON as injected variables. Supports charts, tables, sliders, and reactive controls. |
| `search_prefab_components` | Searches the Prefab component library for import paths and field signatures. |

---

## Dashboard

The React dashboard lives at `/dashboard`. It shows all saved leads alongside their place details on an interactive Leaflet map, with filtering by status and free-text search.

Leads can have their status and notes updated directly in the UI. Updates are pushed to the server via `PATCH /api/v2/leads/{id}` and streamed back to any connected MCP clients via server-sent events at `/api/v2/events`.

---

## Data

- **Places** — 4.4 million UK points of interest sourced from Foursquare/OpenStreetMap. Fields include name, coordinates, address, locality, region, postcode, website, email, phone, and social handles.
- **Categories** — 1278 taxonomy categories, each pre-embedded with Jina v5 (768-dimensional vectors) and indexed for BM25 full-text search.
- **Database** — DuckDB with the VSS (HNSW vector index), FTS, and Spatial extensions.

The `data/` directory is not included in this repository. Follow the steps below to build it locally.

### Data setup

**1. Download the Foursquare OS Places dataset**

Download the `places_os` and `categories_os` datasets from the [Foursquare Open Source Places](https://opensource.foursquare.com/os-places/) programme. Convert both to Parquet format and place the files inside the `data/` directory:

```
data/
├── places_os.parquet
└── categories_os.parquet
```

**2. Populate the database**

Run the two seed scripts in order. The first populates the categories table (including generating Jina embeddings); the second loads all places.

```bash
uv run populate_categories.py
uv run populate_places.py
```

Both scripts can take a while to complete on first run due to the volume of data and the embedding generation step.

---

## Running locally

```bash
# Install dependencies
uv sync

# Start the MCP + API + dashboard server (default port 8000)
./start_mcp.sh

# Or explicitly:
uv run -m place_scout.mcp --transport streamable-http --host 0.0.0.0 --port 8000
```

Set `PLACE_SCOUT_PUBLIC_URL` in `.env` to your public HTTPS URL (e.g. an ngrok tunnel) so the dashboard iframe loads correctly when embedded in an MCP client.

```bash
cp .env.example .env
# edit .env
```

Runtime plugin config (embedder provider, DB path) lives in `place_scout.toml`.

---

## Deployment

The service is deployed to [Sprites](https://sprites.dev) — a Fly.io-backed ephemeral environment that auto-pauses when idle and wakes on the first request.

```bash
# First deploy (builds frontend, pushes DB in chunks, creates service)
./scripts/deploy_to_sprite.sh

# Re-deploy code only (skip frontend rebuild and DB transfer)
./scripts/deploy_to_sprite.sh --skip-build --skip-db

# Check service status
sprite exec -s place-scout -- sprite-env services list
```

The deployed service runs:
```
uv run -m place_scout.mcp --transport streamable-http --host 0.0.0.0 --port 8000
```