Skip to main content
Glama
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