@cyanheads/openstreetmap-mcp-server
Geocode, reverse geocode, and run Overpass spatial queries on OpenStreetMap data via MCP. STDIO or Streamable HTTP.
6 Tools
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
Capability reference
The openstreetmap-guide 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, naming any countrycodes, layer, featureType, or bounded viewbox that was sent
- Every page with results returns
nextExcludeIds to pass back as exclude_place_ids, since a page shorter than limit can still have more; once results run out you get zero results and a notice, not an error, and a walk too long for the instance's URL limit fails as request_too_long
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)
element_types searches nodes, ways, and relations unless set; ["node", "way"] leaves relations out
- 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
- With ways or relations searched, an area containing the point, such as the park or city it lies in, matches at
distance_meters 0 whatever the radius, and areasTimestamp says how old the area data is. An endpoint that cannot run the area lookup is asked again without it, and the notice says areas containing the point were not checked
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
element_types searches nodes, ways, and relations unless set; ["node", "way"] leaves relations out
effectiveArea names the boundary within reached by its name, boundary, and admin_level tags, then the Overpass area it mapped to, so a mistyped ref shows; areasTimestamp says 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: 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 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 and effectiveTag echo what was sent, and effectiveArea names the boundary a within ref reached; 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:
Or run it locally:
For other clients, add this to the MCP config file:
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.
Configuration
See .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:
- 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 instance (the FOSSGIS servers, maps.mail.ru, overpass.private.coffee)
- 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, 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 or higher (or Node.js v24+).
Docker
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
Development guide
See 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:
License
Apache-2.0 — see LICENSE for details.
Map data from OpenStreetMap contributors, available under the Open Database License (ODbL).