overpass-mcp

overpass-mcp

MCP server providing structured access to OpenStreetMap data through Overpass and Nominatim APIs, enabling geocoding, nearby place searches, element queries, and tag counting without API keys.

Category
访问服务器

README

overpass-mcp

An MCP (Model Context Protocol) server that gives an AI agent typed, structured access to OpenStreetMap data through two public, key-free APIs: Overpass (feature queries) and Nominatim (geocoding). No API keys, no paid tier — just the public OSM infrastructure, used the way its operators ask it to be used.

What this is

Seven tools, each returning a Pydantic-validated, JSON-serializable result:

  • geocoding a place name to coordinates and a bounding box
  • finding tagged elements (amenity=cafe, shop=bakery, ...) near a point or inside a bounding box
  • fetching a single OSM element by type and id
  • counting matches cheaply, without pulling full geometry
  • listing common OSM tag keys/values as a static, offline reference
  • running a raw Overpass QL query as an escape hatch

Every tool returns either a valid result or a structured error object — never an exception. See Design notes below for why that distinction matters for an MCP server specifically.

Installation

git clone https://github.com/Rusty0508/overpass-mcp.git
cd overpass-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

Requires Python 3.11+.

Configuration for an MCP client

Add the server to your MCP client's config (for Claude Desktop, this is claude_desktop_config.json; for the Claude Code CLI, .mcp.json or via claude mcp add):

{
  "mcpServers": {
    "overpass": {
      "command": "/absolute/path/to/overpass-mcp/.venv/bin/overpass-mcp"
    }
  }
}

The overpass-mcp console script is installed by pip install -e . (see [project.scripts] in pyproject.toml) and talks over stdio, which is what most MCP clients expect by default. Alternatively, run it directly:

python -m overpass_mcp.server

No environment variables or API keys are needed — both upstream APIs are public and unauthenticated.

Tools

Tool Parameters Returns
geocode_place query: str, limit: int = 1 (1-10) List of matches: name, coordinates, bounding box, OSM type/id
find_places_nearby lat: float, lon: float, radius_m: int (1-50000), tag_key: str, tag_value: str | None, limit: int = 50 (1-200) List of Place objects + count
find_places_in_area south, west, north, east: float, tag_key: str, tag_value: str | None, limit: int = 50 List of Place objects + count
get_element element_type: "node" | "way" | "relation", element_id: int A single Place object
count_places south, west, north, east: float, tag_key: str, tag_value: str | None {total, nodes, ways, relations}
list_common_tags none Static dict of tag key -> popular values (no network call)
raw_overpass_query ql: str (max 8000 chars) Raw parsed Overpass JSON response

A Place is {osm_type, osm_id, name, coordinates: {lat, lon} | null, tags}. Every tool response is wrapped as either {"ok": true, "data": {...}} or {"ok": false, "error": {"code": ..., "message": ..., "hint": ...}}.

Example calls

Geocode a place:

{"tool": "geocode_place", "arguments": {"query": "Alexanderplatz, Berlin"}}
{"ok": true, "data": {"results": [{"name": "Alexanderplatz, Mitte, Berlin, Germany",
  "coordinates": {"lat": 52.521, "lon": 13.413},
  "bounding_box": {"south": 52.520, "west": 13.410, "north": 52.522, "east": 13.416},
  "osm_type": "way", "osm_id": 123456}]}}

Find cafes within 500m of a point:

{"tool": "find_places_nearby",
 "arguments": {"lat": 52.521, "lon": 13.413, "radius_m": 500, "tag_key": "amenity", "tag_value": "cafe"}}

Count fuel stations in a bounding box without fetching their geometry:

{"tool": "count_places",
 "arguments": {"south": 52.3, "west": 13.0, "north": 52.7, "east": 13.7, "tag_key": "amenity", "tag_value": "fuel"}}

A failure looks like this (never a stack trace, never a raised exception):

{"ok": false, "error": {"code": "TIMEOUT",
  "message": "timeout calling https://overpass-api.de/api/interpreter",
  "hint": "upstream did not respond in time; retry, or reduce the search radius/area"}}

Design notes

Why errors are returned, not raised

Every tool in this server catches its own failures and returns a structured {"ok": false, "error": {"code", "message", "hint"}} object instead of letting an exception propagate out of the tool call. This is a deliberate choice, not an oversight of Python idiom.

An MCP tool call happens inside an agent's reasoning loop. If the tool raises, the exception surfaces as a protocol-level failure the agent cannot reason about the way it can reason about data — depending on the client, it can look like the tool doesn't exist, or it can terminate the turn outright. Either way, the agent loses the chance to notice what kind of failure happened and decide what to do next: retry a timeout, back off on a 429, or tell the user a bounding box was invalid and to please review it. A structured error is just another shape of successful tool output — the agent reads error.code, decides on a strategy, and keeps going. The distinction that matters here is not "exception vs. return value" as a Python style preference; it is "does the protocol layer see a broken tool, or does the agent see actionable information." An MCP server is a service boundary, and prompted agents behave better with predictable failure data than with the interruption of an exception.

Idempotency-aware retry

client._request_with_retry retries on timeout and 5xx responses, with exponential backoff, for both the Overpass POST call and the Nominatim GET call. The common heuristic — "retry GET, never retry POST" — uses the HTTP method as a proxy for whether a retry is safe. That heuristic is the right default when the method is unknown, but here the actual property that matters is checked directly: neither upstream API has a write endpoint at all, and both calls used by this server are pure reads. Overpass happens to use POST only because a QL query body doesn't fit comfortably into a query string — semantically it is a GET. Retrying is therefore safe for both calls: repeating the same request cannot create a duplicate side effect, because there is no side effect to duplicate. A 429 is handled separately from timeouts/5xx: if the response carries a Retry-After header, the retry waits exactly that long instead of using its own backoff schedule, because the upstream server is telling us precisely how long to wait.

Respecting public infrastructure

Both APIs are free, key-free, and run by volunteers/small teams on donated infrastructure — nothing about them requires payment, but that also means nothing stops a careless client from taking them down for everyone else. This server takes their published usage policies as hard constraints, not suggestions:

  • Nominatim's documented limit of one request per second is enforced in code (asyncio.Lock + a monotonic timestamp), not left to the caller's discipline — the lock ensures it holds even under concurrent tool calls from the same process.
  • Every request sends a descriptive User-Agent identifying the project, because Nominatim blocks generic/default user agents outright.
  • Every request has an explicit connect/read/write/pool timeout — nothing waits forever, and the server does not hold a connection open speculatively.
  • raw_overpass_query has a hard length cap (8000 characters) so a single agent-generated query cannot balloon into something that hurts a shared public endpoint.

out center for way/relation

Overpass elements come in three kinds — node, way, relation — and only node carries coordinates directly. A way is a sequence of node references; a relation is a set of member references; neither has a lat/lon of its own. Every query built by this server appends out center;, which asks Overpass to compute and attach a centroid to way/relation elements. Forgetting this is a common, easy-to-miss bug: the query still succeeds, still returns elements, and roughly half the results (every non-node) simply come back with no usable position — a silent hole in the data rather than a visible error. element_to_place reads lat/lon directly for nodes and falls back to center.lat/center.lon for ways/relations, and its coordinates field is only None in the rare case where Overpass itself could not resolve a center.

Testing

source .venv/bin/activate
python -m pytest tests/ -v
ruff check .

All network access in tests is mocked with respx at the httpx transport layer — the test suite never contacts overpass-api.de or nominatim.openstreetmap.org.

License

MIT — see LICENSE.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
mcp-server-qdrant

mcp-server-qdrant

这个仓库展示了如何为向量搜索引擎 Qdrant 创建一个 MCP (Managed Control Plane) 服务器的示例。

官方
精选
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选