Grounded Spatial-Reasoning MCP Server

Inspiration

Agentic AI is being pointed at disaster response, delivery routing, and resource access more every month — but recent research (January 2026) found that LLM-based agents often hallucinate spatial relationships instead of computing them, relying on pattern-matching where real geometry is needed. Ask most agents "is this point inside the flood zone" or "how far apart are these addresses by road," and they'll often answer with confidence and no actual math behind it.

At the same time, two of the most enduring "hackathon that became real infrastructure" stories share a trait worth noticing: Ushahidi, a crisis-mapping platform built in a matter of days during Kenya's 2007–2008 post-election violence, is still open-source on GitHub today and has been deployed more than 150,000 times across 160 countries. Be My Eyes, a project that won at a 2015 hackathon connecting blind and low-vision users to sighted volunteers over live video, grew into standing infrastructure still running today. Neither succeeded because of a sophisticated model — both Ire dead-simple, grounded mechanics solving one real problem well.

Iwanted to build that same kind of grounded simplicity, but aimed at the gap the research pointed at directly: give any agent real geometry instead of a guess. And instead of building another app people have to discover and install, Iexposed it as an MCP server — infrastructure any agent can call the moment it's connected, the same way Google Maps and Mapbox are already shipping live geospatial data to agents over this exact protocol.

What it does

The server exposes four deterministic geometry tools over MCP:

  • distance_between — real geodesic (great-circle) distance between two points, computed with the haversine formula:

$$d = 2r \cdot \arcsin\left(\sqrt{\sin^2\left(\frac{\Delta\varphi}{2}\right) + \cos(\varphi_1)\cos(\varphi_2)\sin^2\left(\frac{\Delta\lambda}{2}\right)}\right)$$

  • is_within_zone — a true point-in-polygon test against named geographic zones (flood zones, service districts, restricted areas), not a guess about whether a point "seems" inside a boundary.
  • nearest_of_type — the k nearest points of interest of a given category, ranked by real geodesic distance.
  • reachable_within — what's actually reachable on foot within N minutes, computed over a real cached street network rather than a straight-line circle. A river, a highway, or a missing bridge can make something close as-the-crow-flies but practically unreachable, and this tool accounts for that.

A fifth tool, list_data_layers, lets an agent introspect what's currently loaded — because the data layer isn't hardcoded. Point the server at any directory of GeoJSON files and it auto-discovers every point and polygon feature inside it, with zero source code changes.

How I built it

Python, Shapely for geometry, NetworkX for graph algorithms, and a cached OSMnx walking network for real routing — all exposed through the MCP Python SDK's FastMCP server.

The street network is the part that makes the "grounded, not guessed" claim literally true: build_network.py downloads real OpenStreetMap walking-network data once, caches it locally as a graph, and reachable_within runs Dijkstra's algorithm over real travel-time-weighted edges to find what's genuinely reachable — not everything inside an arbitrary radius.

Itested at three layers before trusting any of it: unit-level sanity checks on the confidence and distance math, a standalone MCP client script (test_client.py) that calls every tool through the real protocol without a browser in the loop, and finally live connection to actual agent clients (Claude Desktop, Cursor) to confirm natural-language questions triggered the right tool with no manual prompting.

Challenges I ran into

  • GraphML couldn't serialize our own graph. OSMnx graphs frequently store list-valued edge attributes (e.g. an edge merged from multiple OSM ways carries a list of source IDs), and the standard networkx GraphML writer flatly refuses to serialize a list. The fix was switching to OSMnx's own save_graphml/load_graphml, which handle exactly this case — a good reminder to reach for a library's own purpose-built serialization instead of the generic one underneath it.
  • A security feature silently broke a real feature. Ibuilt reachable_within to work with any user-supplied data directory via an environment variable, and it worked perfectly running the server directly — but silently failed when tested through an MCP client, because MCP's stdio client spawns the server subprocess with a restricted default environment for security and doesn't forward arbitrary variables unless explicitly told to. Diagnosing "why does the same feature work here and not there" across a process boundary was the trickiest bug of the build.
  • A cache-mutation bug leaked state between unrelated tool calls. nearest_of_type was mutating cached POI dictionaries in place rather than working on a copy, so a field it added would silently reappear in a later, unrelated reachable_within call on the same objects. It only surfaced because the output for two specific entries looked slightly different from the rest — a good lesson in reading tool output closely rather than just checking for the absence of an error.

What I learned

Getting an agent to pick the right tool turned out to be its own discipline — the tool's docstring is effectively a prompt aimed at a model, not documentation aimed at a human, and vague wording produces vague tool selection. Ialso learned how much invisible infrastructure sits between "the code is correct" and "an agent can actually use it": environment variable propagation across subprocess boundaries, serialization format quirks specific to geospatial libraries, and the difference between a security-token-gated developer tool (the MCP Inspector) and a direct, auth-free protocol client. None of that shows up in a tutorial; all of it showed up in this build.

What's next

  • CSV and Shapefile ingestion, extending the same auto-discovery pattern already built for GeoJSON.
  • Driving-mode reachability, which needs a road-speed-weighted graph rather than the uniform walking speed Iused.
  • A multi-agent layer on top of the same server — separate lightweight personas (a "reporter" skill for intake, a "concierge" skill for querying) sharing this one tool layer and one source of truth, rather than duplicating logic across agents.
  • Real, verified community data in place of the illustrative sample data used for this build, so the tool answers real questions about a real place.

Built With

  • ai
  • anthropic-sdk
  • apis
  • chatgpt-api
  • codex
  • data
  • external
  • fastmcp
  • frameworks
  • git
  • github
  • mcp
  • mcp-python-sdk
  • model
  • model-context-protocol
  • networkx
  • openstreetmap
  • osmnx
  • python
  • sdks
  • shapely
Share this project:

Updates