MCPAI agentsClaudeCursorGeospatial intelligenceOpenStreetMap

Geospatial MCP Server: Give AI Agents Live OpenStreetMap Data

Ask an agent for the nearest pharmacy and it invents a street. Give it live OpenStreetMap and it looks the place up, checks hours, and walks the real network. How the MapLark MCP server does that.

See the MCP serverJoel Sten · Sep 14, 2026Geospatial MCP Server: Give AI Agents Live OpenStreetMap Data

People already talk to Claude, Cursor, and Copilot as if those models can see a city. A guest lands in Stockholm and asks for dinner they can walk to. A parent needs a pharmacy still open at 21:00. A field tech wants EV chargers on this side of the river, not a pin invented from a training run. Those are map questions. The model answers them from memory anyway.

OpenStreetMap already holds the facts those jobs need: where places are, what they are tagged as, when they are open, and how streets actually connect. Plug that planet into the agent and the job changes from sounding plausible to looking it up. That is what the MapLark MCP server does. This post is the why and the how, not the install. Setup, the full tool list, and per-tier limits live on that page. The code is open source at github.com/MapLark/osmfeatures-py.

Why put OpenStreetMap in an AI agent

An agent without a map is a concierge with a two-year-old guidebook. It will name a cafe that closed last summer, put the pharmacy on the wrong bank of the harbour, and call a 35-minute detour a 12-minute stroll. The model is not being lazy. It was never given live coordinates, live hours, or a street graph.

OSM is the dataset that already answers those questions for humans, at planet scale, with a tag language that models already know. You want it in the agent whenever the prompt is about the physical world:

  • Travel. Italian restaurants open at 19:30 within a 15-minute walk of the hotel, not a training-set favourite on the other side of town.
  • Night. A pharmacy still open at 21:00, with hours evaluated in that venue's own timezone, not parsed in the model's head.
  • Commute. Whether the office is really a 20-minute walk, on the OSM street network, not as the crow flies across water or rail cuttings.
  • Field work. EV chargers, ATMs, or a supermarket near a job site, ranked by real distance.
  • A walking night out. A loop of bars that are actually open, ordered on the pedestrian network, then drawn on a map.

The solution is the same in every case. The model chooses what to look for. Code geocodes, searches OSM tags, walks the network, and evaluates opening hours. The rest of this post is that split, and a worked dinner prompt that uses it.

What goes wrong without a map

Three failures show up the moment you trust an ungrounded agent with a city.

Models invent coordinates, because a lat/lng looks like text and text is what they generate. Models estimate distance, and a straight line across a harbour is not a walk. Models try to parse OSM opening_hours strings like Mo-Th 11:00-23:00; Fr-Sa 11:00-01:00; Su off in their head, which burns tokens and is not consistent across runs.

Tools fix all three. The model decides what to ask for. Code computes the answer.

The plug: how MCP solves the glue problem

Before the Model Context Protocol, every agent host invented its own tool-calling glue. An integration written for one chat app had to be rewritten for the next. MCP is a common plug. Any compliant host can load any compliant server.

There are three roles:

  • Host. The application the user talks to: Claude Desktop, Cursor, Copilot, or your own agent loop. It owns the model and decides which tool calls to make.
  • Client. The connector inside the host. One client per server, managing the session.
  • Server. The process that exposes capabilities. MapLark ships one of these.

Servers expose three kinds of capability: tools (functions the model can call), resources (data the host can read), and prompts (reusable templates). The MapLark server is tool-focused. Messages are JSON-RPC 2.0, carried either over stdio, where the host spawns the server as a local subprocess and talks over stdin and stdout, or over HTTP for remote servers. The MapLark server is stdio, so it runs on your machine.

The handshake is why a good MCP server needs almost no prompt engineering from you. On connect, the client sends initialize. The server replies with its protocol version, its capabilities, and an instructions string. The host then calls tools/list and receives every tool with a JSON Schema for its arguments. Those schemas go straight into the model's tool-calling context. The model picks a tool and emits arguments, the client sends tools/call, the server runs real code and returns a structured result, and the model reads that result as ground truth rather than inventing one.

That instructions field is where the MapLark server does its most useful work. It ships the planner rules described below, so the model knows to call geocode before searching a named neighbourhood, and knows never to compute a distance itself.

How the MCP server talks to MapLark

You do not want the model talking to PostGIS, and you do not want to stand up Overpass for every agent. The server is a thin stdio wrapper over the osmfeatures SDK, which is itself a client for the MapLark HTTP API. The chain is short:

  • Your host (Claude, Cursor, or your own loop) spawns osmfeatures mcp as a subprocess.
  • The model calls a tool such as places_search.
  • The server maps that to an HTTP request against https://api.maplark.com, authenticated with the MAPLARK_API_KEY from your MCP config.
  • MapLark runs the query against managed PostGIS holding the full OSM planet and returns GeoJSON.
  • The server summarises the result for the model. Coordinate arrays are deliberately left out of the summary so a 200-feature answer does not blow the context window. When the host needs the real geometry to draw it, it calls export_geojson(collection_id).

Some tools never leave your machine. nearest_within, filter_open, point_in_polygon, and points_in_polygon run locally on FeatureCollections the agent already fetched. They cost nothing and add no latency.

This is the same data and the same endpoints as the Places API, Routing API, and OSM Features API. MCP is a second doorway into them, not a separate product.

A note on privacy, since this is a common question about agent tooling. The server runs locally over stdio. Your prompts and your conversation never reach MapLark. We see the HTTP calls the tools make: a bounding box, a set of OSM tags, a timestamp. Results come from OpenStreetMap, not a proprietary place graph built on end-user profiles. See the privacy policy.

Why the tools speak OSM tags

The other design problem is vocabulary. A proprietary category tree means the model must learn your taxonomy. OSM tags are already in the training data. amenity=restaurant, cuisine=italian, shop=supermarket, tourism=hotel, amenity=charging_station, amenity=pharmacy. The model can produce those strings on the first try.

The server exposes fifteen tools in six groups: geocode, places, routes, generic OSM queries, local joins that never touch the network, and draw or export. The MCP server page lists every one of them. This post only covers the part that changes how you design an agent: the model names tags and a place, code does the rest.

Worked example: dinner you can walk to

Same city, three jobs. The Dinner tab is the walkthrough below. Pharmacy and Commute use the same tools with different tags and a different question.

Dinner Pharmacy Commute
Find Italian restaurants in Sodermalm, Stockholm that are open at 19:30 tonight
and within a 15 minute walk of Mariatorget. Show them on a map.

Typed into Claude or Cursor with the MapLark MCP server connected, the dinner prompt becomes this chain. Nothing here is hand-written glue:

  1. geocode("Mariatorget, Stockholm") returns a point and a bounding box. The model named a place, so it must resolve it rather than guess coordinates.
  2. routes_isochrone from that point with duration_s=900 and travel_mode=WALK returns the real 15-minute walk polygon. Not a circle. Water, rail cuttings, and missing crossings all shape it.
  3. places_search over a covering bbox with tags=["amenity=restaurant", "cuisine=italian"] and as_of="2026-09-11T19:30:00". A naive timestamp like that is 19:30 local to the search area. Passing as_of also filters out POIs with no opening_hours tag, so the page is not padded with venues whose hours nobody has mapped.
  4. points_in_polygon keeps only the restaurants inside the isochrone. A local call, no HTTP, no cost.
  5. filter_open keeps the ones where openNow is true at 19:30. The server evaluated each opening_hours string in that venue's own IANA timezone. The model did not parse a single one.
  6. preview_map draws the survivors so you can see them before committing.

One server, one conversation, one prompt. The model is the planner. Code geocodes, walks the street network, evaluates opening hours, and draws the map.

The same pattern, other jobs

Swap the tags and the question. The planner still must not invent metres.

  • Pharmacy at 21:00. Same isochrone from the hotel, then places_search with amenity=pharmacy and as_of at 21:00. filter_open drops anything closed. The model never reads an hours string.
  • Is the office a 20-minute walk?geocode both ends, routes_isochrone from the hotel with duration_s=1200, then point_in_polygon for the office. Inside the polygon means yes. A straight-line guess would have said yes across a body of water.
  • EV chargers on this side of the river.places_search with amenity=charging_station, then points_in_polygon against a walk or bike isochrone so you do not send the van over a bridge it cannot use.
  • A walking bar crawl.places_search for amenity=bar, filter_open, then routes_optimized_path with loop=true so the walk is a tour on the pedestrian network, not a list sorted by name.

More prompt-to-tool mappings, costs, and spatial caps are on the MCP server page. The restaurant guide example is the same POI pattern as plain Python.

What this server will not book

The leftover problem after dinner discovery is a table. MapLark answers where things are, what they are tagged as, whether they are open, and how far they are on foot or by bike. It does not hold restaurant inventory, it does not know how many two-tops are free at 19:30, and it cannot write a reservation. Table availability lives in the restaurant's booking platform. OpenStreetMap has no idea about it.

A well-tagged restaurant often carries website or reservation:website, plus a phone. That is the honest handoff from this server: give the diner the booking page or the number. Live slots need official OpenTable, TheFork, or similar partner APIs, and those companies require a partnership before you get real inventory access. Unofficial scrapers and third-party wrappers exist. They are a workaround to that gate, not something we ship or walk through.

MCP composition is still the right pattern once you have a real partner API: specialised servers, each authoritative over its own domain, combined by the model at runtime. Discovery is a map problem. Availability is an inventory problem. We will not show a booking server, config, or payloads here.

Build the agent yourself

You do not need Claude or Cursor. If the problem is "my product has to answer map questions," MCP is a protocol, so anything that speaks it can drive these tools. The first tab connects to the server with the official Python MCP SDK, reads the shipped instructions, lists the tools, and calls two of them. Feed list_tools() output into your model's tool-calling API and you have a geospatial agent in about thirty lines.

The second tab skips MCP entirely. If you already know the calls, write deterministic code against the osmfeatures SDK. Same data, same endpoints, no protocol overhead. MCP earns its keep when a model is choosing the calls.

Custom agent (MCP client) No MCP (direct SDK)
import asyncio, os
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

server = StdioServerParameters(
    command="uvx",
    args=["--from", "osmfeatures[mcp]", "osmfeatures", "mcp"],
    env={"MAPLARK_API_KEY": os.environ["MAPLARK_API_KEY"]},
)

async def main() -> None:
    async with stdio_client(server) as (read, write):
        async with ClientSession(read, write) as session:
            init = await session.initialize()
            # Ship these to your model as the system prompt. They carry the
            # planner-vs-code rules, so the model stops inventing distances.
            print(init.instructions)

            tools = await session.list_tools()
            print([t.name for t in tools.tools])

            area = await session.call_tool("geocode", {"q": "Sodermalm, Stockholm"})
            hits = await session.call_tool(
                "places_search",
                {
                    "bbox": "18.03,59.30,18.12,59.33",
                    "tags": ["amenity=restaurant", "cuisine=italian"],
                    "as_of": "2026-09-11T19:30:00",
                    "limit": 60,
                },
            )
            print(area.content, hits.content)

asyncio.run(main())

There is a TypeScript SDK too (npm install osmfeatures), and the raw HTTP API is documented in OpenAPI if you would rather work in Go, Rust, or anything else.

Planner versus code: the rule that keeps agents honest

Every failure in the opening of this post is the model doing arithmetic. The fix is one rule, and the server ships it to your model as part of the MCP handshake. The planner chooses. Code computes.

The model may chooseCode must compute
Place name, OSM tags, orTagsCoordinates, from geocode or OSM
Viewport bbox, or location plus radiusRanked distances
Distance budget, duration, travel modeNetwork path and isochrone polygon
openNow or asOfWhether a venue is actually open then
Which tool to call nextJoin, filter, sort, slice

The model must never compute a haversine distance, decide by eye which restaurant is nearest a station, parse an opening_hours string, or invent a walk time. Those are the four places agents quietly produce confident nonsense, and they are the four places these tools take over.

Under that rule most local-search problems come out as two or three calls. The MCP server page maps a dozen example prompts to their tool chains.

Common questions

Is this a Google Places alternative?

For discovery, yes, and usually a much cheaper one. You get OSM tags rather than a proprietary category tree, and no per-session licensing rules about what you may cache. What you do not get is Google's review corpus or business-submitted hours. Data quality tracks OpenStreetMap coverage, which is excellent in cities and patchier in rural areas. If the agent must not invent a pharmacy, OSM is the cheaper honest source. If the agent must rank by star rating, you still need another dataset.

Can the agent book anything on its own?

No. MapLark finds the restaurant, checks OSM hours, and can surface the OSM booking URL or phone number. Live table inventory lives behind official marketplace APIs. OpenTable and TheFork require partnership signup for real access. Apply there if you need slots.

How fresh is the data?

It comes from the OpenStreetMap planet data, refreshed weekly, so an edit a mapper makes today is not available immediately. For POI attributes and hours, that is rarely the limiting factor. Mapper coverage is. An agent that looks empty in a rural area is usually telling the truth about the map, not failing the query.

Connect your agent

Grab a free API key, paste it into your MCP config, and ask your first map question.

MCP server docs Get your API key