PlaceScout
by joyanujoy
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
```
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues