Skip to main content
Glama
README.md
# Open MCP Data Server

**An MCP server that gives Claude (or Cursor, or Claude Code) live geospatial data — geocoding, POI search, isochrones, and area density — over open data.**

[![CI](https://github.com/abangbroy/osm-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/abangbroy/osm-mcp/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/osm-mcp.svg)](https://pypi.org/project/osm-mcp/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

> One-line pitch: turn real open data sources into tools any LLM client can call
> directly. The model orchestrates the calls; this server does the fetching,
> caching, rate-limiting, and typed validation.

---

## The hook (demo)

Ask Claude Desktop a real geography question and it calls your tools directly:

```
You (in Claude Desktop):
  "I'm opening a coffee shop. Find all existing cafés within a 15-minute walk
   of Bukit Bintang MRT, and tell me the postcode centroid so I can cross-check
   rent data."

Claude:
  → isochrone(lat=3.1498, lon=101.7149, mode="walk", minutes=15)
  → pois(lat=3.1499, lon=101.7144, radius_m=1200, categories=["cafe"])
  → reverse_geocode(lat=3.1499, lon=101.7144)
  ← cafe list (12 matches), postcode "55100", centroid coords

Claude:
  "There are 12 cafés within a 15-min walk. The postcode centroid is 55100
   (Bukit Bintang). Here's the list, sorted by distance…"
```

The user never sees an API key or an HTTP call — the model orchestrates the
tools. That orchestration, made possible by the server's tool design, is the
point of this project.

> 📸 _GIF of a live Claude Desktop session goes here on first publish._

---

## How it works

```
  ┌─────────────────────┐         MCP (JSON-RPC over stdio)
  │   LLM Client        │  ─────────────────────────────────────┐
  │  (Claude Desktop /  │                                        │
  │   Cursor / Code)    │  ◄──── tool schemas advertised         │
  └─────────────────────┘                                        ▼
                                ┌─────────────────────────────┐
                                │   Open MCP Data Server      │
                                │  (FastMCP Python process)   │
                                │                             │
                                │  @mcp.tool: geocode         │
                                │  @mcp.tool: reverse_geocode │
                                │  @mcp.tool: pois            │
                                │  @mcp.tool: isochrone       │
                                │  @mcp.tool: bbox_summary    │
                                │                             │
                                │  TTLCache + rate limiting   │
                                └──────────────┬──────────────┘
                                               │  https GET/POST
                    ┌──────────────────────────┼──────────────────────┐
                    ▼                          ▼                      ▼
          ┌─────────────────┐    ┌────────────────────┐    ┌─────────────────┐
          │ OSM Nominatim   │    │ Overpass API       │    │ OSRM            │
          │ (geocoding)     │    │ (POIs by amenity)  │    │ (isochrones)    │
          └─────────────────┘    └────────────────────┘    └─────────────────┘
```

Each tool is a thin async function that fetches upstream data through a shared
cache + per-host rate limiter, validates it with Pydantic, and returns a typed
result. Inputs are **enum-constrained** — callers never supply raw Overpass QL.

---

## Tools

| Tool | Description | Units |
|---|---|---|
| `geocode(query)` | Forward geocode a place name → coordinate. | lat/lon decimal degrees |
| `reverse_geocode(lat, lon)` | Coordinate → human-readable address. | decimal degrees → string |
| `pois(lat, lon, radius_m, categories)` | Points of interest within a radius, by category. | metres; counts |
| `isochrone(lat, lon, mode, minutes)` | Reachable-area polygon within a time budget. | minutes; polygon `[lon,lat]`; area m² |
| `bbox_summary(min_lat, min_lon, max_lat, max_lon, categories?)` | Counts of key amenities inside a bounding box (density helper). | counts |

`mode` ∈ `{walk, drive, transit}`. `categories` are **enum-constrained**
(`cafe`, `restaurant`, `retail`, `transit`, `school`, `attraction`,
`accommodation`, `bank`, `healthcare`) — all Overpass queries are built
server-side.

---

## Quick start

```bash
git clone https://github.com/abangbroy/osm-mcp.git
cd osm-mcp
python -m venv .venv && .venv\Scripts\activate     # Windows
# source .venv/bin/activate                        # macOS/Linux
pip install -e ".[dev]"
```

Run standalone over stdio:

```bash
osm-mcp            # or: python -m osm_mcp
```

Or install the published package directly:

```bash
uvx osm-mcp        # or: pip install osm-mcp
```

> Set `USER_AGENT` (see `.env.example`) to a descriptive value — Nominatim usage
> policy requires it.

### Claude Desktop config

Add to `claude_desktop_config.json` (macOS:
`~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "osm-mcp": {
      "command": "C:\\path\\to\\osm-mcp\\.venv\\Scripts\\osm-mcp.exe",
      "args": []
    }
  }
}
```

With the published package:

```json
{
  "mcpServers": {
    "osm-mcp": {
      "command": "uvx",
      "args": ["osm-mcp"]
    }
  }
}
```

### Cursor config

Point Cursor at the same command via **Settings → MCP → Add Server**, using
`osm-mcp` (local venv) or `uvx osm-mcp` (published).

### Tests

```bash
pytest --cov=osm_mcp --cov-report=term-missing
```

---

## Upstream dependencies & rate-limit policy

All upstream APIs are free-tier and shared/public, so they are rate-limited.
This server respects their usage terms:

- **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).
- **Bounded retry with exponential backoff** for transient errors (429/502/503/
  504, timeouts), honoring `Retry-After`.
- **`transit` mode falls back to the OSRM `foot` profile** — OSRM has no transit
  router. For real transit isochrones, self-host a transit router and point
  `OSRM_BASE_URL` at it. This is a documented limitation, stated openly.
- For production throughput, **self-host** Nominatim / Overpass / OSRM and set
  the `*_BASE_URL` env vars.

**Attribution:** data © OpenStreetMap contributors
([ODbL](https://www.openstreetmap.org/copyright)). Code is MIT; data attribution
must accompany any reuse.

---

## 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` | `osm-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. Releases are automated:

1. **PyPI** — pushing a `v*` tag runs
   [`publish.yml`](.github/workflows/publish.yml), which re-runs the tests and
   lint, verifies the tag matches the version in `pyproject.toml`, builds, and
   uploads via [Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
   (OIDC — no API token is stored in the repo).

   ```bash
   git tag v0.1.0 && git push origin v0.1.0
   ```

   Requires a one-time pending publisher on PyPI — see the header comment in
   `publish.yml` for the exact field values.

2. **Official MCP registry** — submit `server.json` at
   [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io)
   once the PyPI release is live. The server is registered as
   `io.github.abangbroy/osm-mcp`; the `io.github.<user>/` namespace is what
   proves GitHub ownership.

> SSE-only transports are deprecated since MCP spec 2025-03-26. A **Streamable
> HTTP** transport is the planned v2 stretch (no SSE).

---

## Learned in public

This project is a portfolio piece. A few things I learned openly while building
it, rather than claiming prior mastery:

- **FastMCP packaging** — wiring `@mcp.tool` decorators to Pydantic-typed
  signatures and exposing the bounds in the generated JSON schema (so the model
  *sees* the limits, not just gets rejected by them).
- **OSRM as an isochrone source** — OSRM has no native isochrone endpoint; the
  radial-sampling + `/table` approach is a public-methodology workaround.
- **Overpass `(poly:)` coordinate order** — it expects latitude-then-longitude,
  the opposite of GeoJSON; getting this wrong returns HTTP 400 live.

---

## License

MIT — see [LICENSE](LICENSE).

TDQS

A4.1/5.0

Scored across 6 tools

Disambiguation5/5

Each tool targets a distinct spatial operation: forward and reverse geocoding are clearly opposed, while isochrone, pois, and bbox_summary are differentiated by query shape (travel budget, radius, bounding box) and return type. There is no real overlap that would cause an agent to select the wrong tool confidently.

Naming Consistency3/5

All names are lowercase snake_case, which helps, but the convention is mixed: geocode/reverse_geocode/ping are verb-like, while isochrone, pois, and bbox_summary are noun-like descriptors. The pattern is readable but not consistently verb_noun.

Tool Count5/5

Six tools is a well-scoped size for an OSM-focused server. Each tool covers a distinct geospatial need without redundancy, and the set does not feel either thin or bloated.

Completeness4/5

The server covers core geospatial workflows: geocoding, reverse geocoding, reachability, POI lookup, and area density summaries. It lacks advanced routing or direct OSM element retrieval, but these are not core to the apparent purpose and can be worked around.

Maintenance

ActivityMaintained
ResponsivenessNo issues