@cyanheads/openstreetmap-mcp-server
Geocode, reverse geocode, and run Overpass spatial queries on OpenStreetMap data via MCP. STDIO or Streamable HTTP.
6 Tools
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/openstreetmap-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/openstreetmap-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
[](https://github.com/cyanheads/openstreetmap-mcp-server/releases/latest/download/openstreetmap-mcp-server.mcpb) [](https://cursor.com/en/install-mcp?name=openstreetmap-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvb3BlbnN0cmVldG1hcC1tY3Atc2VydmVyIl19) [](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22openstreetmap-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Fopenstreetmap-mcp-server%22%5D%7D)
[](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
**Public Hosted Server:** [https://openstreetmap.caseyjhand.com/mcp](https://openstreetmap.caseyjhand.com/mcp)
---
## Overview
OpenStreetMap data through Nominatim and the Overpass API. Geocode places and addresses, resolve coordinates to the nearest address, look up known OSM objects, and query features by tag around a point, inside a bounding box, or inside an OSM boundary.
### Tools
| Tool | Description |
|:---|:---|
| `openstreetmap_search_places` | Convert a place name or address to coordinates and structured place data |
| `openstreetmap_reverse_geocode` | Convert latitude/longitude to the nearest address or place name |
| `openstreetmap_lookup_objects` | Fetch address records for known OSM objects by ID |
| `openstreetmap_query_nearby` | Find OSM features within a radius of a point |
| `openstreetmap_query_bbox` | Find OSM features inside a bounding box or an OSM boundary such as a city, park, or region |
| `openstreetmap_query_raw` | Run a raw Overpass QL query for anything the other tools cannot express |
## Capability reference
The [`openstreetmap-guide`](./skills/openstreetmap-guide/SKILL.md) skill covers place resolution, spatial queries, paging, and interpreting coverage and tag results.
Argument aliases are accepted before canonical validation: `latitude` / `longitude` for `lat` / `lon` on nearby and reverse queries, `radius_m` for nearby's `radius_meters`, and `q` for search's `query`. Send either spelling, never both. `radius`, `tag`, and `tags` remain invalid; advertised schemas use canonical names.
### `openstreetmap_search_places` tool
- Free-form `query` or the structured fields `street`, `city`, `county`, `state`, `country`, `postalcode`, never both; up to 40 results (`limit`, default 5; a larger `limit` is served as 40 with a `limitNotice`), narrowed by `countrycodes`, `layer`, `featureType`, and a `viewbox` (restricting with `bounded: true`)
- Results carry coordinates, structured `address`, `boundingbox`, `importance`, and `osm_type` + `osm_id`; `extratags` adds contact and physical-attribute tags; an empty first page fails as `no_results`
- A full page returns `nextExcludeIds` to pass back as `exclude_place_ids`; once results run out you get zero results and a notice, not an error
---
### `openstreetmap_reverse_geocode` tool
- `lat` / `lon` plus `zoom` from 3 (country) to 18 (building, the default); `layer` defaults to `address,poi`
- Returns the nearest indexed object with structured `address`, `osm_type` + `osm_id`, and `boundingbox`; coordinates with no OSM data fail as `no_coverage`
---
### `openstreetmap_lookup_objects` tool
- Up to 500 `osm_ids` entries, each prefixed `N`, `W`, or `R` (`["N240109189"]`); a malformed ID fails as `invalid_id_format`
- The first 50 unique IDs are looked up; the rest come back as `deferred_ids` for the next call
- Returns address records for the objects found and lists the rest under `not_found` (including objects Nominatim does not index, such as street lamps); a `notice` carries an `openstreetmap_query_raw` query to check which exist
---
### `openstreetmap_query_nearby` tool
- `lat` / `lon` with `radius_meters` up to 50,000 (default 1,000); one primary tag, `amenity` or `tag_key` with an optional `tag_value`, plus up to five ANDed `filters`; up to 500 features per page (a larger `limit` is served as 500 with a `limitNotice`)
- A value matches exactly by default; `list_item` mode (`tag_value_match`, or `match` on a filter) matches one item of a semicolon list, so `cuisine` = `pizza` also finds `cuisine=pizza;italian`
- An `amenity` value that OpenStreetMap tags under another key (`amenity: "museum"`; museums are `tourism=museum`) gets a `notice` naming the key to retry with
- Features carry `osm_type` + `osm_id`, coordinates, `distance_meters` (to the feature's nearest point), and the full tag set, sorted nearest-first; `effectiveTag` echoes the filter chain
---
### `openstreetmap_query_bbox` tool
- One scope per call: the corners `south`, `west`, `north`, `east`, or `within` and one `R`/`W` boundary ref (`R237385`) built from a geocoding result's `osm_type` + `osm_id`; the same tag filters, list-item matching, stray-`amenity` notice, and 500-per-page cap as `openstreetmap_query_nearby`
- `effectiveArea` echoes how `within` resolved and `areasTimestamp` how old the area data is; a ref that maps to no area returns an empty page with a notice
- A `west` greater than `east` is a box crossing the antimeridian; `south` greater than `north` fails as `invalid_bbox`
---
### `openstreetmap_query_raw` tool
- Any Overpass QL containing `[out:json]`; `[timeout:N]` comes from `timeout_seconds` (up to 180, default 30) and `[maxsize:268435456]` (256 MiB) is added when the query sets none; up to 500 elements per page (a larger `limit` is served as 500 with a `limitNotice`)
- Returns raw elements (ways carry `nodes[]`, relations `members[]`), plus `total_elements` and the `effectiveQuery` as sent; malformed QL fails as `query_error` with Overpass's parse error
- `max_element_bytes` (default 20,000, minimum 1,000) withholds an over-budget element's `members`, `nodes`, or `geometry` arrays, listed under `withheld_keys`; `withheldElements` and `withheldNotice` say how to fetch them
## Features
Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
OpenStreetMap-specific:
- Nominatim requests are spaced 1,050 ms apart to respect its one-request-per-second policy; `OSM_USER_AGENT` identifies the server to both upstreams
- Overpass queries are limited per endpoint by `OSM_OVERPASS_MAX_CONCURRENCY`, with opt-in failover across `OSM_OVERPASS_ENDPOINTS` (see [Overpass endpoint failover](#overpass-endpoint-failover))
- Overpass tools page with `limit` and `offset` and report `totalFound`, `truncated`, and `nextOffset`; results are cached for 10 minutes (Nominatim for 60), so re-paging makes no new upstream request
- An Overpass response over 64 MiB fails as `result_too_large`
- `OSM_NOMINATIM_BASE_URL` and `OSM_OVERPASS_BASE_URL` point the server at a self-hosted or mirror instance
Agent-friendly output:
- `effectiveQuery`, `effectiveTag`, and `effectiveArea` echo what was sent; Overpass responses add `data_timestamp` and `servingEndpoint`, and every response carries the ODbL `attribution`
- `osm_type` + `osm_id` from any tool feed `openstreetmap_lookup_objects`, and a geocoded relation or way becomes a `within` ref for `openstreetmap_query_bbox`
- The Nominatim tools never select by tag, and `tagSelectionCaveat` says so; regex, alternation, and negation go through `openstreetmap_query_raw`
- Names and tag values are escaped in the Markdown output; `structuredContent` keeps the raw text
## Getting started
Connect to the hosted instance, no install:
```sh
claude mcp add --transport http openstreetmap-mcp-server https://openstreetmap.caseyjhand.com/mcp
```
Or run it locally:
```sh
claude mcp add openstreetmap-mcp-server -- npx -y @cyanheads/openstreetmap-mcp-server
```
For other clients, add this to the MCP config file:
```json
{
"mcpServers": {
"openstreetmap-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/openstreetmap-mcp-server"]
}
}
}
```
No API key is required. For heavy use, point `OSM_NOMINATIM_BASE_URL` and `OSM_OVERPASS_BASE_URL` at your own instances. For Docker or a source build, see [Running the server](#running-the-server).
## Configuration
| Variable | Description | Default |
|:---|:---|:---|
| `OSM_NOMINATIM_BASE_URL` | Nominatim base URL. A path prefix such as `https://maps.example.com/nominatim` is supported. | `https://nominatim.openstreetmap.org` |
| `OSM_OVERPASS_BASE_URL` | Pins every query to one Overpass endpoint and disables failover. | unset |
| `OSM_OVERPASS_ENDPOINTS` | Comma-separated, ordered Overpass endpoints; more than one enables failover. Ignored when `OSM_OVERPASS_BASE_URL` is set. | `https://overpass-api.de/api/interpreter` |
| `OSM_OVERPASS_MAX_CONCURRENCY` | Max Overpass queries in flight per endpoint. Public instances allow one per user. | `1` |
| `OSM_USER_AGENT` | User-Agent sent to Nominatim and Overpass. Required by usage policy. | `openstreetmap-mcp-server/` |
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
| `MCP_HTTP_PORT` | HTTP server port. | `3010` |
| `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto`. The server declares `stateless` in code; an explicit value overrides it. | `stateless` |
| `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
| `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). | `info` |
| `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1`. | `in-memory` |
| `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |
See [`.env.example`](./.env.example) for the full list of optional overrides. An invalid value for any `OSM_*` variable stops the server at startup with a message naming the variable.
### Overpass endpoint failover
By default every Overpass query goes to one endpoint, the FOSSGIS-operated main instance. List more in `OSM_OVERPASS_ENDPOINTS` and a failed call moves to the next entry in order:
```sh
OSM_OVERPASS_ENDPOINTS="https://overpass-api.de/api/interpreter,https://overpass.private.coffee/api/interpreter"
```
- Endpoints that fail repeatedly are skipped for a while (30 seconds, doubling up to 10 minutes)
- When every endpoint fails, the call fails with `rate_limited`, `upstream_error`, `overpass_unavailable`, `endpoints_rejected`, `endpoints_exhausted`, or `endpoints_unavailable`, with each endpoint's outcome in the message
- Endpoints are reported by origin only (`https://overpass-api.de`) in `servingEndpoint`, error messages, and logs
- Extra entries are failover only and never add concurrency. For an instance that allows parallel queries, append `|N` to its entry (`https://mirror.example/api/interpreter|4`) for N slots; write a literal `|` in a URL as `%7C`. Never size a public FOSSGIS server
- Calls waiting for a slot fail with `pacer_shed` and a `retryAfter` of at least 30 seconds after 30 seconds in line
Adding an endpoint sends your queries to a third party. Check its usage policy on the [OSM wiki instance list](https://wiki.openstreetmap.org/wiki/Overpass_API#Public_Overpass_API_instances), confirm it holds global data (a regional instance answers out-of-extract queries with an empty HTTP 200), and expect mirrors to lag the main instance; `servingEndpoint` and `data_timestamp` show which host answered and how fresh its data was.
## Running the server
### Local development
Requires [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
```sh
git clone https://github.com/cyanheads/openstreetmap-mcp-server.git
cd openstreetmap-mcp-server && bun install
cp .env.example .env
# edit .env to override endpoints or the User-Agent as needed
```
- **Build and run the production version**:
```sh
# One-time build
bun run rebuild
# Run the built server
bun run start:http
# or
bun run start:stdio
```
- **Run checks and tests**:
```sh
bun run devcheck # Lints, formats, type-checks, and more
bun run test # Runs the test suite
```
### Docker
```sh
docker run --rm -p 3010:3010 ghcr.io/cyanheads/openstreetmap-mcp-server:latest
# or build locally
docker build -t openstreetmap-mcp-server .
docker run --rm -p 3010:3010 openstreetmap-mcp-server
```
The image serves HTTP at `http://localhost:3010/mcp` in stateless session mode and logs to `/var/log/openstreetmap-mcp-server`. OpenTelemetry peer dependencies are installed by default; build with `--build-arg OTEL_ENABLED=false` to omit them. Runtime telemetry stays off until you pass `-e OTEL_ENABLED=true`.
## Project structure
| Directory | Purpose |
|:---|:---|
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`) and shared input, formatting, and escaping helpers. Six tools across Nominatim and Overpass. |
| `src/services/nominatim` | Nominatim service: paced API client for search, reverse, and lookup, plus error parsing. |
| `src/services/overpass` | Overpass service: query builder, endpoint failover and executor, element normalizer, error parsing. |
| `src/config` | Server-specific environment variable parsing and validation with Zod. |
| `tests/` | Unit and integration tests for tools, services, and security. |
## Development guide
See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no `try/catch` in tool logic
- Use `ctx.log` for logging, `ctx.state` for storage
- Register new tools in the `createApp()` arrays in `src/index.ts`
- Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
## Contributing
Issues are welcome. Run checks and tests before submitting:
```sh
bun run devcheck
bun run test
```
## License
Apache-2.0 — see [LICENSE](./LICENSE) for details.
Map data from [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors, available under the [Open Database License (ODbL)](https://opendatacommons.org/licenses/odbl/).