Skip to main content
Glama
Kerry1020

Geo MCP Worker

by Kerry1020

geo-mcp-worker

CI License: GPL v3 Cloudflare Workers MCP

English | 简体中文

A stateless MCP server on Cloudflare Workers for geocoding, reverse geocoding, nearby POI search and routing, powered by free OpenStreetMap services (Nominatim / Overpass / OSRM). No API key needed.

Features

  • Four tools: geo_geocode, geo_reverse, geo_find_poi, geo_route

  • Free upstreams, no API key, no KV, no sessions

  • MCP over Streamable HTTP (JSON responses), protocol versions 2025-06-18, 2025-03-26, 2024-11-05, JSON-RPC batches

  • Real walking and cycling profiles (FOSSGIS routers), with car routing via the OSRM demo server

  • Strict input validation; tool failures come back as structured { ok: false, reason } payloads instead of protocol errors

  • Optional bearer auth (MCP_AUTH_TOKEN), CORS origin allow-list, per-request upstream timeouts

  • Every upstream URL can be pointed at a self-hosted instance

  • No runtime dependencies; node:test suite with mocked upstreams

Related MCP server: OpenStreetMap MCP Server

Quick start

git clone https://github.com/Kerry1020/geo-mcp-worker.git
cd geo-mcp-worker
npm run dev                                   # http://localhost:8787
curl -s http://localhost:8787/health

Deploy to your own account:

npx wrangler login
npx wrangler secret put MCP_AUTH_TOKEN        # recommended for public deployments
npm run deploy

Then add https://<your-worker>.workers.dev/mcp to your MCP client (see MCP client config).

Tools

Tool

Description

Arguments

Source

geo_geocode

Address text → lat/lon

address (string, required, ≤500 chars), limit (1–10, default 1)

Nominatim (OSM)

geo_reverse

Lat/lon → address

lat (−90..90, required), lon (−180..180, required)

Nominatim (OSM)

geo_find_poi

Nearby POIs, sorted by distance

lat, lon (required), radius_m (1–5000, default 1000), category (default restaurant), limit (1–50, default 20)

Overpass API (OSM)

geo_route

Point-to-point distance & duration

from {lat, lon}, to {lat, lon} (required), mode driving | walking | cycling (default driving)

OSRM

POI categories: restaurant, cafe, school, hospital, clinic, pharmacy, bank, atm, supermarket, convenience, subway, bus_stop, park, gym, cinema, library, kindergarten, police, fire_station, post_office, parking, fuel, marketplace. Any other lowercase [a-z0-9_] value is queried as amenity=<value>.

Coordinates may be numbers or numeric strings. Out-of-range or non-numeric coordinates are rejected with reason: "invalid_coords" (they are not silently clamped).

Endpoints & protocol

  • POST /mcp — MCP endpoint (JSON-RPC 2.0 over Streamable HTTP, JSON responses only, no SSE)

  • GET /health (also /, /healthz) — health check, always public

  • OPTIONS * — CORS preflight

Supported methods: initialize, ping, tools/list, tools/call. Notifications (messages without id, e.g. notifications/initialized) are accepted with 202 Accepted and no body. JSON-RPC batches are supported.

Protocol versions: 2025-06-18, 2025-03-26, 2024-11-05 (the client's version is echoed if supported, otherwise 2025-06-18).

Errors

Protocol errors use standard JSON-RPC codes:

Code

When

-32700

Body is not valid JSON

-32600

Invalid request (missing jsonrpc: "2.0"/method, empty batch, unauthorized, forbidden origin, body > 1 MB)

-32601

Unknown method

-32602

Unknown tool name, or arguments is not an object

-32603

Unexpected internal error (no stack trace is returned)

Tool-level failures are not JSON-RPC errors. They come back as a normal result with isError: true and a semantic payload in structuredContent (and as JSON text in content[0].text):

{ "ok": false, "reason": "invalid_coords", "message": "..." }

reason values: missing_address, invalid_address, missing_coords, invalid_coords, missing_points, invalid_category, invalid_mode, not_found, upstream_timeout, geocode_error, reverse_error, poi_error, route_error, internal_error.

An empty POI search is a success: { "ok": true, "count": 0, "results": [], "reason": "poi_not_found", ... }, so an agent can retry with a larger radius.

Configuration

All configuration comes from environment variables. Defaults live in wrangler.toml / src/config.js. Nothing is required.

Name

Required

Secret

Default

Description

MCP_AUTH_TOKEN

No (recommended)

Yes

unset

If set, every request to /mcp requires Authorization: Bearer <token>. /health and CORS preflight stay public.

ALLOWED_ORIGINS

No

No

*

* or a comma-separated list of origins. With a list, requests to /mcp carrying another Origin get 403.

UPSTREAM_TIMEOUT_MS

No

No

10000

Per-request timeout for upstream APIs, in ms (capped at 60000).

USER_AGENT

No

No

geo-mcp-worker/1.1 (+https://github.com/Kerry1020/geo-mcp-worker)

Sent upstream. The Nominatim policy asks for an identifying UA, so put your own contact here.

ACCEPT_LANGUAGE

No

No

zh

accept-language sent to Nominatim.

NOMINATIM_URL

No

No

https://nominatim.openstreetmap.org

Nominatim base URL.

OVERPASS_URL

No

No

https://overpass-api.de/api/interpreter

Overpass interpreter URL.

OSRM_URL_DRIVING

No

No

https://router.project-osrm.org

OSRM base for driving.

OSRM_URL_WALKING

No

No

https://routing.openstreetmap.de/routed-foot

OSRM base for walking.

OSRM_URL_CYCLING

No

No

https://routing.openstreetmap.de/routed-bike

OSRM base for cycling.

BUILD_SHA, BUILD_TIME

No

No

unknown

Shown in /health; usually injected at deploy time.

The public OSRM demo server (router.project-osrm.org) only serves the car profile: it gives car routes for foot/bike requests too. That is why walking and cycling go to the FOSSGIS routers by default.

For local development, put secrets in .dev.vars (it is git-ignored):

MCP_AUTH_TOKEN=dev-token

MCP client config

Claude Code (native Streamable HTTP):

claude mcp add --transport http geo https://<your-worker>.workers.dev/mcp

# with MCP_AUTH_TOKEN set on the worker
claude mcp add --transport http geo https://<your-worker>.workers.dev/mcp \
  --header "Authorization: Bearer <token>"

Claude Desktop, Cursor and other stdio-only clients via mcp-remote:

{
  "mcpServers": {
    "geo": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://<your-worker>.workers.dev/mcp",
        "--header",
        "Authorization: Bearer ${AUTH_TOKEN}"
      ],
      "env": {
        "AUTH_TOKEN": "<token>"
      }
    }
  }
}

Drop the --header arguments and env block if MCP_AUTH_TOKEN is not set.

curl examples

URL=https://<your-worker>.workers.dev   # or http://localhost:8787 with `npm run dev`
AUTH="Authorization: Bearer $GEO_MCP_TOKEN"   # only needed when MCP_AUTH_TOKEN is set

curl -s $URL/health

curl -s $URL/mcp -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'

curl -s $URL/mcp -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

curl -s $URL/mcp -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"geo_geocode","arguments":{"address":"上海市徐汇区云锦路"}}}'

curl -s $URL/mcp -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"geo_find_poi","arguments":{"lat":31.169501,"lon":121.453866,"category":"subway","radius_m":1000,"limit":5}}}'

curl -s $URL/mcp -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"geo_route","arguments":{"from":{"lat":31.169501,"lon":121.453866},"to":{"lat":31.240168,"lon":121.497945},"mode":"driving"}}}'

Example results

These were captured from a live deployment. Exact values change as OSM data changes.

geo_geocode

{ "address": "上海市徐汇区云锦路", "limit": 1 }
{
  "ok": true,
  "query": "上海市徐汇区云锦路",
  "results": [
    {
      "lat": 31.169501,
      "lon": 121.453866,
      "display_name": "云锦路, 龙华, 龙华街道, 徐汇区, 上海市, 200232, 中国",
      "type": "residential",
      "importance": 0.42
    }
  ]
}

School names work too: { "address": "向明中学浦江校区" } → lat=31.075575, lon=121.496283.

⚠ Company and brand names (e.g. "中电金信") are not accepted. For those, use search-mcp first to get the street address, then pass the address to geo_geocode.

geo_reverse

{ "lat": 31.169501, "lon": 121.453866 }
{
  "ok": true,
  "lat": 31.169501,
  "lon": 121.453866,
  "display_name": "云锦路, 龙华, 龙华街道, 徐汇区, 上海市, 200232, 中国",
  "address": { "road": "云锦路", "suburb": "龙华街道", "city": "徐汇区", "state": "上海市", "postcode": "200232", "country": "中国" }
}

geo_find_poi

{ "lat": 31.169501, "lon": 121.453866, "category": "subway", "radius_m": 1000, "limit": 5 }
{
  "ok": true,
  "center": { "lat": 31.169501, "lon": 121.453866 },
  "count": 4,
  "results": [
    { "name": "云锦路", "distance_m": 0,   "category": "subway" },
    { "name": "龙华",   "distance_m": 772, "category": "subway" },
    { "name": "龙耀路", "distance_m": 897, "category": "subway" },
    { "name": "龙华",   "distance_m": 962, "category": "subway" }
  ]
}

Each result also carries lat, lon and tags (cuisine, opening_hours, phone, website, operator, when available).

geo_route

{ "from": { "lat": 31.169501, "lon": 121.453866 }, "to": { "lat": 31.240168, "lon": 121.497945 }, "mode": "driving" }
{ "ok": true, "from": { ... }, "to": { ... }, "distance_m": 12609, "duration_min": 14.7, "mode": "driving", "confidence": "high" }

For walking routes longer than 2 km, duration_min is recalculated at 80 m/min (~4.8 km/h) and flagged confidence: "low".

Health

{
  "ok": true,
  "name": "geo-mcp-worker",
  "version": "1.1.0",
  "build": { "sha": "<git sha>", "time": "<build time>" },
  "tools": ["geo_geocode", "geo_reverse", "geo_find_poi", "geo_route"],
  "data_sources": ["nominatim", "overpass", "osrm"]
}

Search MCP integration

Geo MCP does spatial computation only. For semantic search, pair it with search-mcp-worker:

User: "What subway stations are near 中电金信 Shanghai HQ?"

1. search_mcp("中电金信上海总部地址") → "上海市徐汇区云锦路XXX号"
2. geo_geocode("上海市徐汇区云锦路") → { lat: 31.17, lon: 121.45 }
3. geo_find_poi(lat, lon, category="subway", radius_m=1000) → 云锦路(0m), 龙华(772m)
4. geo_route(from=office, to=云锦路, mode="walking") → 3 min walk

Security notes

  • Without MCP_AUTH_TOKEN, /mcp is open to anyone who knows the URL, and they can spend your Workers quota and the fair-use budget of the public upstreams. For any public deployment, set a token: npx wrangler secret put MCP_AUTH_TOKEN. The token is compared in constant time.

  • /, /health, /healthz and OPTIONS preflight are always unauthenticated. They return only name, version, build info and the tool list.

  • ALLOWED_ORIGINS limits browser origins (requests with a non-listed Origin get 403). It is not a substitute for the token: non-browser clients can omit Origin.

  • Request bodies over 1 MB are rejected, and internal errors never return stack traces.

  • Tool inputs (addresses, coordinates) are sent to the configured upstreams (by default OSM community services). Do not send data you would not share with them.

Development

Requires Node.js ≥ 20. There are no runtime dependencies.

npm test          # node:test suite; upstream APIs are mocked, no network needed
npm run check     # syntax / import check
npm run dev       # local server via wrangler (http://localhost:8787)

Layout:

src/index.js      Worker entry: routing, CORS, auth, body parsing
src/protocol.js   MCP JSON-RPC handling
src/tools.js      Tool schemas + handlers (input validation)
src/providers.js  Nominatim / Overpass / OSRM clients (timeouts, errors)
src/config.js     Env var parsing and defaults
src/http.js       Response, CORS and auth helpers
src/util.js       Validation and geometry helpers
test/             node:test suites with a mocked global fetch

Deploy

npx wrangler login
npx wrangler secret put MCP_AUTH_TOKEN      # optional, enables bearer auth
npx wrangler deploy \
  --var BUILD_SHA:$(git rev-parse --short HEAD) \
  --var BUILD_TIME:$(date -u +%FT%TZ)

Use a scoped Cloudflare API token (CLOUDFLARE_API_TOKEN, "Edit Cloudflare Workers" template) for CI or non-interactive deploys. Don't use the Global API Key.

Usage limits of the public upstreams

The default upstreams are free community services with fair-use policies: Nominatim allows at most 1 request/second, and Overpass and the OSRM/FOSSGIS routers throttle heavy users. For production traffic, set MCP_AUTH_TOKEN so the endpoint isn't open to everyone, set an identifying USER_AGENT, and consider self-hosted instances (the *_URL vars).

Expansion roadmap

See docs/GEO_TRANSIT_RESEARCH_REPORT.md for the full survey of 28 projects.

Layer

Solution

Coverage

Status

Layer 0

OSRM + Nominatim + Overpass

Global driving/walking/POI

✓ Deployed

Layer 1

Transitous

International public transit

Researched, not deployed

Layer 2

Amap (高德) API

China public transit

Researched, not deployed

License

Licensed under the GNU General Public License v3.0.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    F
    maintenance
    A geospatial MCP server that provides tools for geocoding, routing, elevation profiles, and spatial analysis. It enables AI agents to process GIS file formats like GeoJSON and Shapefiles while performing complex coordinate transformations and distance calculations.
    4
    MIT
  • A
    license
    B
    quality
    D
    maintenance
    A comprehensive MCP server providing 30 tools for geocoding, routing, and OpenStreetMap data analysis. It enables AI assistants to search for locations, calculate travel routes, and perform quality assurance checks on map data.
    30
    251 npm
    5
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    MCP server for geocoding, reverse geocoding, place/POI search, and distance calculation using OpenStreetMap Nominatim, with no API key required.
    50 npm
    1
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    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.
    7
    MIT