osm-mcp
by a-tsitanov
README.md
# osm-mcp
An MCP (Model Context Protocol) server that exposes read-only query tools over
`osm2pgsql`-imported OpenStreetMap data in PostGIS (`planet_osm_*` tables).
## Quick start
```bash
uv sync --extra dev # creates .venv + uv.lock
cp .env.example .env # then set OSM_MCP_DSN
docker compose up -d # optional: demo PostGIS with seed data on localhost:55432
```
`.env.example` documents the settings:
- `OSM_MCP_DSN` — required libpq connection string. Use a **read-only** DB role
(see the comment in `.env.example` for the recommended `GRANT`s).
- `OSM_MCP_SRID` — SRID of the `way` geometry column (default `3857`).
- `OSM_MCP_STATEMENT_TIMEOUT_MS` — per-query statement timeout (default `5000`).
- `OSM_MCP_MAX_ROWS` — row cap applied to result sets (default `200`).
## Running the server
```bash
uv run osm-mcp
```
Runs over **stdio** by default (how local MCP clients launch it).
### HTTP streaming transport
Set `OSM_MCP_TRANSPORT=streamable-http` (alias `http`) to serve over HTTP
instead of stdio; `sse` is also supported. Host/port come from `OSM_MCP_HOST`
/ `OSM_MCP_PORT` (default `127.0.0.1:8000`); the endpoint path is `/mcp`.
```bash
OSM_MCP_TRANSPORT=http OSM_MCP_HOST=127.0.0.1 OSM_MCP_PORT=8000 \
OSM_MCP_DSN=postgresql://osm_readonly:secret@localhost:5432/osm \
uv run osm-mcp
# -> serves on http://127.0.0.1:8000/mcp
```
An HTTP-capable MCP client then connects with:
```json
{ "mcpServers": { "osm": { "type": "http", "url": "http://127.0.0.1:8000/mcp" } } }
```
Unlike stdio, the HTTP/SSE transports print a startup line to stderr showing
the URL. (stdio stays silent — its stdout is the protocol channel.)
### MCP client config (stdio)
```json
{
"mcpServers": {
"osm": {
"command": "osm-mcp",
"env": {
"OSM_MCP_DSN": "postgresql://osm_readonly:secret@localhost:5432/osm"
}
}
}
}
```
## Tools
| Tool | Purpose |
| --- | --- |
| `describe_schema` | List available `planet_osm_*` tables, their tag columns, and geometry SRID. |
| `list_categories` | Top values (with counts) of key OSM tags, to discover filters. |
| `search_features` | Search features by name (ILIKE) and/or tag filters, optional bbox. |
| `find_nearby` | Features within N meters of a lat/lon, ordered by distance (points + POI polygons; excludes admin/boundary polygons). |
| `features_in_area` | Features inside a bbox or a named/osm_id polygon. |
| `count_by_category` | Counts of features per tag value, optionally scoped to an area. |
| `run_sql` | Escape hatch: run a single read-only `SELECT`/`WITH` query against the OSM tables. |
**Full reference** — parameters, return shapes, and worked examples for every
tool: **[docs/TOOLS.md](docs/TOOLS.md)**.
### Performance notes
`list_categories` and area-less `count_by_category` do full-table scans over
unindexed tag columns; prefer scoping them with an `area`/`table`, or use them
only on modest-size datasets.
## Coordinates, SRID, and read-only access
- Public tool coordinates (lat/lon in arguments and results) are always WGS84
(`EPSG:4326`). The DB geometry column (`way`) is stored in `OSM_MCP_SRID`
(default `3857`) and reprojected at the query boundary.
- Distance calculations (`find_nearby`) use PostGIS `geography`, so
`distance_m` is a true geodesic distance in meters.
- Read-only access is enforced at two layers: the DB role used for
`OSM_MCP_DSN` should itself be read-only, and every query additionally runs
inside a `READ ONLY` transaction. `run_sql` is further restricted to a
single `SELECT`/`WITH` statement.
## Testing
```bash
uv run pytest # unit + integration
uv run pytest -m "not integration" # unit only, no DB required
uv run pytest tests/unit/test_config.py::test_defaults_applied -v # single test
```
Integration tests need a live PostGIS (`docker compose up -d`); they skip
automatically if the DB at `OSM_MCP_TEST_DSN` (defaults to
`postgresql://osm:osm@localhost:55432/osm`) is unreachable.
TDQS
B3.4/5.0
Scored across 7 tools
Disambiguation5/5
Each tool has a clearly distinct purpose: counting, schema description, spatial querying, nearby search, category listing, SQL execution, and general search. No overlap in functionality.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern in snake_case (e.g., count_by_category, search_features). No mixing of styles or unconventional names.
Tool Count5/5
With 7 tools, the set is well-scoped for OSM data querying. Each tool serves a distinct operation, and the count is neither too small nor overwhelming for the domain.
Completeness4/5
Covers key OSM query patterns: spatial filters, text search, counts, schema introspection, and custom SQL. A minor gap is the lack of a dedicated tool to fetch a single feature by ID, but run_sql can handle it.
Maintenance
ActivitySlowing
ResponsivenessNo issues