Documentation menu
MCP server
sn-mcp makes the platform agent-consumable: any MCP client (Claude Code,
Claude Desktop, Cursor, Codex, VS Code, or your own agent) gets routing,
dangerous-goods compliance, geocoding, matrix, elevation, optimisation,
along-route search, day planning, reachability, place verification, map
correction, integration feedback, local geometry and map-styling tools
(thirty-four in total) with full JSON Schemas and structured outputs.
There are two ways in: connect to the hosted endpoint (nothing to install), or run the server yourself against a self-hosted deployment. Both are covered below.
Connect your agent (hosted, no install)
The hosted MCP server speaks streamable HTTP at:
https://mcp.mapmap.ai/mcp
Status, honestly: the endpoint is live and open for evaluation: no key
needed to connect, fair use applies (rate limited per client IP; over the
limit you get 429 with Retry-After). Every tool below is registered on
the hosted endpoint, and the routing, geocoding, optimisation, place and
local-geometry tools all answer there today.
You can also bill metered calls to your own gateway key: present your
snk_ key as Authorization: Bearer snk_… on this endpoint and every
metered call you make meters and bills to that key rather than to ours. That
is how an agent pays its own way over MCP; connecting without a key still
works for evaluation under fair use. See
Authentication and billing below for exactly
what is billed and what is free.
Presenting your own snk_ key also unlocks style publishes
(create_style, set_palette, set_layer_paint) on this endpoint, because
you are spending your own credential rather than ours. Without a key,
publishes are still refused here: publishing has to be signed with a gateway
key, and an open endpoint would otherwise have to sign it with ours. You can
also publish by running the server yourself with your own STUDIO_API_KEY
(below), or by calling POST /styles on the gateway with your snk_ key.
Style reads stay open here, exactly as the gateway serves them. For the same
capabilities over plain REST, call the REST API with
your snk_ key.
Pick your client:
Claude Code
claude mcp add --transport http mapmap https://mcp.mapmap.ai/mcp
Claude Desktop: claude_desktop_config.json
(macOS: ~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"mapmap": { "type": "http", "url": "https://mcp.mapmap.ai/mcp" }
}
}
Cursor: .cursor/mcp.json in your project (or ~/.cursor/mcp.json
globally):
{
"mcpServers": {
"mapmap": { "url": "https://mcp.mapmap.ai/mcp" }
}
}
Codex: ~/.codex/config.toml:
[mcp_servers.mapmap]
url = "https://mcp.mapmap.ai/mcp"
VS Code (Copilot agent mode): .vscode/mcp.json:
{
"servers": {
"mapmap": { "type": "http", "url": "https://mcp.mapmap.ai/mcp" }
}
}
Windsurf: ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"mapmap": { "serverUrl": "https://mcp.mapmap.ai/mcp" }
}
}
From code: the Claude API's MCP connector takes the URL directly:
{
"mcp_servers": [
{ "type": "url", "url": "https://mcp.mapmap.ai/mcp", "name": "mapmap" }
]
}
Any other MCP client that supports streamable HTTP works the same way: give it the URL, no command, no environment variables.
Agent skills (knowledge, not tools)
The MCP server gives an agent live tools; agent skills teach it how to build with MapMap: truck and ADR routing patterns, web SDK integration, migrating from Mapbox, self-host operations. They install into Claude Code, Cursor, Codex and compatible agents:
npx skills add Mapmapai/mapmap-agent-skills # all skills
npx skills add Mapmapai/mapmap-agent-skills --list # see what's inside
Each of the ten skills is listed and explained on the Agent skills page.
Resources and prompts
Beyond tools, the server publishes MCP resources (the deployment's own
llms.txt and openapi.json, the ADR tunnel-code reference, the
measured-clearance reference and the agent-skills index) and two prompts:
plan_truck_route (geocode → route with ADR costing → explain what the
profile changed) and restyle_map_to_brand (the restrained
brand-to-palette method, wired to the style tools). Connecting the server
therefore delivers the knowledge layer as well as the tools: a client can
read the orientation and the skills index without a single web request.
Two of those resources are static arguments rather than reference tables, and
they are worth reading before an agent reports either kind of answer to a
person. mapmap://guide/adr-tunnel-codes is what an ADR-costed route does
and does not prove. mapmap://guide/clearance is the same job for
check_clearance_on_route: why an unchanged truck route is not a clearance,
why a surveyed measurement is not a signed height, and why ground the survey
did not cover is reported as not surveyed rather than as clear. They share a
sentence on purpose, and it is the sentence both products turn on. Full
prose: measured clearance on a route.
Status, honestly: resources and prompts ship with the next server release; they are in the source now and appear on the hosted endpoint at its next deploy. Tools are unaffected either way.
Run it yourself
Everything below is for running sn-mcp against your own deployment: it is
a thin front that needs the routing engine and (optionally) the geocoder, the
VROOM optimisation solver and the gateway's style API reachable over HTTP. If
you don't have one yet, start with Self-hosting.
Tools
| Tool | What it does |
|---|---|
route | Turn-by-turn route in any costing: auto (car), truck (lorry), bicycle, pedestrian (walking) or motor_scooter. The truck profile {height_m, width_m, length_m, gross_weight_t, hazmat, tunnel_code} merges dimensional and ADR costing (ADR is the European agreement on carriage of dangerous goods by road; tunnel codes B–E restrict which tunnels a hazmat load may use; full tunnel-code table in Conventions); pedestrian {use_lit, type, max_hiking_difficulty} and bicycle {bicycle_type, use_roads, use_living_streets, avoid_bad_surfaces, use_hills} tune their own models. Truck, auto, bicycle, pedestrian and motor_scooter routes may set rationale: true for an opt-in avoided[] explanation of which declared constraints or avoidance-side preferences (hills, surfaces, tolls, unlit streets, hiking caps, access profiles) changed the route (route-divergence basis; it does not identify the physical restriction or feature, and never attributes live traffic; see Why this route). Returns {distance_m, duration_s, summary, maneuvers[], geometry_polyline6, applied_adr}; geometry is a polyline with six decimal digits of precision (polyline6). Set landmarks: true (needs the MapMap gateway) and every manoeuvre that passes a recognisable place gains a landmark_instruction ("Turn right just after the Shell garage") beside the engine's own street-name instruction, which is never replaced. Read it aloud in preference: it is how a passenger gives directions. Nothing is named unless it is recognisable from the road, within 40 m of the junction and not tagged as closed, so plenty of routes return none. |
search_along_route | Places along a route with the REAL extra travel time of stopping at each, never a straight-line guess. {origin+destination or geometry_polyline6, query, category?, max_detour_minutes?, costing, source?} prices candidates through the routing engine (detour = origin→place + place→destination − origin→destination); at most 25 candidates priced per call. Returns results sorted by detour with detour_minutes, detour_km, along_route_position (0–1) and off_route_m. Pass category when you mean a kind of place, query alone when you mean a name or brand. category takes the same vocabulary as nearby_places (the map's lowercased OSM tag values: fuel, cafe, charging_station, parking, pharmacy, …), with colloquial phrases normalised for you ("petrol station" and "gas station" → fuel, "EV charger" → charging_station). A query that is nothing but a category phrase is promoted to the same browse, so query: "fuel" now returns fuel stations instead of every place whose name happens to start "Ful"; anything else ("starbucks", "coffee soho") keeps matching on names, unchanged. When a browse ran the response adds matched_categories (e.g. ["fuel"]), so you can see how your words were read rather than infer it from the results; no such field means name matching answered the call. With source: "places", category filters your own dataset's taxonomy exactly as it always has, and the two vocabularies never cross-filter. An explicit category on a deployment whose geocoding is the Photon proxy rather than the first-party index returns a clear tool error (501 reverse-filtering-not-enabled, as on GET /geocode/reverse); a promoted query falls back to name matching there instead of failing. |
cheapest_fuel_along_route | Cheapest fuel along a route, with the REAL extra travel time of stopping at each station, never a straight-line guess. {origin+destination or geometry_polyline6, fuel? (diesel default, petrol_95, petrol_98, premium_diesel, e85, lpg), max_detour_minutes? (default 10), costing, fill_litres?}. Stations come from the live open-data price feeds (statutory UK Fuel Finder, FR prix-carburants, DE Tankerkoenig), are priced through the routing engine with your costing (a truck profile makes detours respect dimensional and ADR restrictions) and are ranked cheapest first. Each result carries price {value, currency, updated_at, stale} (stale = not verifiably fresher than 24 h), detour_minutes/detour_km and saving_per_litre against the cheapest on-route baseline (fill_litres adds saving_total). Needs the MapMap gateway, and answers a clear error when the deployment carries no fuel-price dataset. Display the returned fuel_attribution with the prices. |
plan_day | Turn an itinerary into one navigable multi-stop route. {start, stops[] (each a location or a free-text name to geocode, plus optional dwell_minutes), depart_at?, optimise?, return_to_start?, costing}. With optimise: true a VROOM solver reorders stops for the shortest day. Returns stops in visit order with per-leg duration/distance, arrival/departure times, totals and the full route geometry (polyline6); geocoded names carry a resolution/alternatives to catch ambiguity. |
reachable_area | Reachability contours (isochrones) from an origin within one or more travel-time budgets: walkability/cyclability rings. {origin, contours_minutes[] (1–10 values, each ≤120), costing, polygons?} returns a GeoJSON FeatureCollection, one feature per contour. |
check_adr_tunnel | Pure ADR 8.6.4 tunnel-entry decision: {tunnel_code?, hazmat, tunnel_category} returns decision, reason, explanation and the worst-case forbidden category list. No network; answers instantly. |
check_clearance_on_route | Measured overhead clearance along a route, judged against surveyed point cloud geometry. {origin, destination, height_m, width_m?, margin_m?}. Routes with truck costing, so the search avoids the height restrictions the map has tagged, then walks that corridor over the survey's own heights, station by station. Returns pass, fail, indeterminate or no_verdict with the limiting point, its measured headroom_m, the bound on it (safe_headroom_m, sigma_m, sampling_gap_m) and a deep link to that exact view. Optional width_m asks the corridor-width axis too, reported separately and never as a headroom; without a width that axis answers not_assessed, and with one over ground the survey covers too thinly at the corridor edges it runs the same verdict ladder and comes back indeterminate or no_verdict there while the height axis can still answer. Optional margin_m adds your operating margin to the vehicle before the verdict. An indeterminate carries indeterminate_reasons as codes to branch on, with the same reasons written out in English inside explanation: read out the English, never the code. A pass may carry no limiting point at all, which means the survey found nothing above that corridor. The honesty matters here: this measures physical geometry from a dated survey, it is not a signed or posted height, clearance_enforcement.route_certified is always false, and ground the survey did not cover comes back as not_surveyed_m and is never judged, so a pass is possible over complete coverage and nowhere else. Sparse or stale coverage is reported separately again as insufficient_data_m. Needs the MapMap gateway, which holds the surveys, and answers a clear error when the deployment carries no clearance dataset. Read the mapmap://guide/clearance resource for why an unchanged truck route is not a clearance. |
geocode | Forward geocoding: {query, limit?, focus?} returns candidates with name, label, lat/lon, type and address parts. |
reverse_geocode | The inverse of geocode: coordinates to the nearest places, nearest first. {lat, lon, limit?, heading_deg?, fov_deg?} (default 5, max 10) returns name, one-line label, lat/lon, type, address parts and distance_m, plus categories and a details object of display tags (opening_hours, website, phone, wikipedia, …) on POI hits where the index carries them. Every hit also carries bearing_deg and a spoken direction; see nearby_places below for how heading_deg and fov_deg work. |
nearby_places | What is NEAR a point, nearest first with distance_m: by category (cafe, fuel, charging_station, parking, pharmacy, supermarket, …, matched against the map's lowercased OSM tag values, with colloquial names normalised: coffee → cafe, petrol → fuel), by name for a brand ("the nearest Lloyds"), or both to disambiguate one from the other (Lloyds + bank excludes Lloyds Pharmacy). Use it instead of geocode for proximity questions: geocode ranks a brand's branches worldwide and only biases by proximity, so it will return a branch in another city over the one 100 m away. {lat, lon, category?, name?, radius_m? (default 2500, max 100000), limit?, heading_deg?, fov_deg?}; a category or name the map does not carry returns an empty list, never an error. Every result carries bearing_deg (degrees clockwise from true north) and a direction phrased for speech. Pass heading_deg (which way the user is facing, same convention) and results are described egocentrically ("ahead and slightly to your right, about 80 metres") with a signed relative_bearing_deg, negative left, positive right; without it the phrasing falls back to cardinals ("to the north-east"), so voice clients work with or without a compass. fov_deg sets the full width of a cone around the heading and drops what falls outside it, counting the casualties in out_of_view: a non-zero count means there ARE matching places nearby, just not in front of the user, which is a different answer from "nothing nearby". Needs the MapMap gateway. |
verify_places | Check whether places (and itineraries) an AI mentioned are real, findable and physically possible: the check to run before acting on a model's suggested venues. Pass structured claims[{name, locality?, id?, sequence?, claimed_time?}] (up to 20 per call) or free text (up to 8,000 characters); the two are mutually exclusive. Every claim gets exactly one of three verdicts, never a boolean: verified (matched a real place, with its stable id and the source and date of the evidence), contradicted (a dated, sourced fact rules it out) or unverified (no evidence either way). A missing match is always unverified: the tool never asserts that a named real business does not exist or has closed. Claims with increasing sequence values that both carry claimed_time form itinerary legs checked for feasibility through matrix, catching "breakfast in Bath, 10am meeting in Edinburgh". Pass a locality with every claim. A bare name is searched against the whole world, so a short or generic name can come back verified against a same-named place in another country, which is a real match to a real place but not the one that was claimed. Free text is best-effort extraction (quoted phrases and Title Case runs after "at", "in", "near", "to"), attaches no locality and runs no feasibility pass, so use it to triage prose and re-check anything that matters as a structured claim. Full contract in API reference. |
matrix | Many-to-many travel matrix: {origins[], destinations[], costing, truck?} returns durations_s[i][j] (seconds) and distances_m[i][j] (metres); null cells are unreachable pairs. |
elevation | Terrain elevation: {points[]} for a bare list of coordinates, or {encoded_polyline, resample_distance_m?} for an along-route profile, not both. Returns one sample per point/resampled point, elevation_m null wherever the engine's terrain-tile set has no coverage (never a guess); the encoded_polyline form also returns each sample's resampled lat/lon and cumulative range_km. |
optimise_routes | Multi-vehicle, multi-stop route optimisation (VRP) via a VROOM solver sidecar, over a travel-time matrix computed by our routing engine, so with costing truck every optimised route respects dimensional and ADR restrictions. Returns a summary, unassigned tasks and per-vehicle routes with ordered steps. Size cap: problems with more than 200 unique locations return a tool error asking you to split the problem, as does one spread wider than the engine's 400 km matrix span. |
order_stops | Put a single run's stops in the best visiting order ("order my errands"): a thin wrapper over the same optimiser for the one-vehicle case. Takes start {lat, lon} and stops (1–100 entries of {location, label?, service_s?}); optionally an end destination or round_trip: true to return to the start. Costing auto = car, truck = lorry (pass truck as in route for a restriction-aware stop order). Returns the stops in visit order with arrival offsets in seconds, plus total duration and distance. |
validate_geodata | Check whether a dataset's declared coordinate reference system actually describes its own coordinates, before you draw it on a map. Pass declared_crs (e.g. "EPSG:4326") and a sample of the raw coordinates as {x, y} in the dataset's own units, deliberately not named lon/lat, because whether they are degrees is exactly the question. Returns a verdict of consistent, suspect or impossible, the problems in plain language, and suggestions saying where the numbers actually point when read another way. It catches the failures that are otherwise silent: swapped lat/lon axes, degrees labelled as metres, and Web Mercator or another projection mislabelled with a UTM or national-grid code, the kind of header that lands a survey in the wrong hemisphere. A sanity check, not a reprojection: it never transforms coordinates and carries no projection database, so it can be silent about an exotic CRS but is never wrong about a UTM easting of 6.1 million. Local check, no network. |
report_map_issue | Queue a first-party map correction for human or agent review, never an automatic OSM edit. {location {lat, lon}, category, description?, way_id?, evidence_url?}; category is one of road_closed, wrong_restriction, wrong_speed_limit, access_changed (and related). Appends the observation as NDJSON to the deployment's review queue (SN_MAP_ISSUES_DIR) and returns an acknowledgement. It stops there: the queue is local to the MCP server, it is not sent to the gateway, and it does not appear on the public Map Health board. To get a report onto that board, and an agent diagnosis with it, call the gateway's POST /map-issues instead (different category vocabulary: road-missing, restriction-wrong and so on). |
submit_integration_retro | Send MapMap a structured end-of-integration retro so the friction you hit gets fixed. {what_built (required, ≤500 bytes), problems[{area, description ≤1000 bytes, workaround_found}], gotchas[], wins[], docs_gaps[] (≤20 entries each, ≤500 bytes per entry), agent_name?, sdk_version?}; area is one of sdk, api, mcp, docs, billing, self-host, other. Only call it if the developer has approved sending feedback to MapMap, and at most once, after the integration works or you stop trying. Only the structured fields above are ever sent: never your conversation, prompts or code. With GATEWAY_URL + GATEWAY_API_KEY set it POSTs to the gateway's /v1/feedback, and falls back to the local queue at SN_RETROS_DIR whenever that delivery cannot happen (no key set, gateway unreachable, or any non-2xx answer) so a retro is never lost. Terms, retention and opt-out: Agent Feedback Programme. |
list_style_layers | List everything a map style theme can restyle: palette slots (with light/dark defaults), skeleton layer ids and source-layers. Local lookup, no network, no configuration needed. |
get_style | Fetch a hosted style's latest theme document and compiled style URL. Public read, unmetered. |
create_style | Create a hosted style from a theme document (metered publish). |
set_palette | Recolour a style by setting palette slots, published as a new immutable version (metered publish). |
set_layer_paint | Override one paint property on one style layer, e.g. road width or opacity, published as a new immutable version (metered publish). |
check_style_contrast | Audit a map style's colour contrast against WCAG 2.1 (4.5:1 for label text, 3:1 for graphics like the route line), across both the light and dark palette variants. Pass a hosted style_id or an inline theme document (as accepted by create_style). Advisory: failing pairs list the palette slots to adjust with set_palette, and publishing is never blocked on contrast. Local check, no network. |
Local geometry (no network, no quota)
Every tool above reaches a backend. These do not: they are pure computation over the coordinates you supply, so they work offline, cost nothing to serve, are never rate limited or metered, and keep answering when a backend is unavailable. They exist so an agent reasoning about space can stay in one tool surface instead of leaving it to do arithmetic.
All measurement is geodesic on the WGS84 ellipsoid, not a spherical approximation, so results agree with the datum the rest of the platform uses.
| Tool | What it does |
|---|---|
geo_distance | Distance in metres between two coordinates. Straight-line (as the crow flies), not a driving distance: use route or matrix for travel distance and time. |
geo_bearing | Initial bearing from one coordinate to another, degrees clockwise from true north (0–360). It is the bearing at the start of the geodesic; over long distances the bearing changes en route. |
geo_destination | The coordinate reached by travelling a given distance on a given bearing. The inverse of geo_distance + geo_bearing. |
geo_point_in_polygon | Whether a coordinate lies inside a polygon: delivery zones, catchments, clean-air or congestion zones, site boundaries. The ring is closed automatically if the last point does not repeat the first. Points exactly on the boundary count as outside. |
geo_bbox | The axis-aligned bounding box enclosing a set of coordinates, as {min_lat, min_lon, max_lat, max_lon}. Useful for fitting a map view to a set of stops. |
geo_centroid | The centroid of a set of coordinates, e.g. to pick a depot location or centre a map. |
geo_length | Total length in metres of a polyline. Measures the line you supply, not a driven route. |
geo_area | Area in square metres enclosed by a ring of 3+ coordinates. Always positive: the answer does not depend on winding direction. Intended for zones and boundaries, not polygons covering more than half the globe. |
geo_simplify | Douglas-Peucker simplification of a polyline. tolerance_deg is in degrees, not metres: about 0.0001 drops detail finer than roughly 10 m at the equator. Endpoints are always kept. |
geo_nearest_point_on_line | The closest position on a polyline to a coordinate, plus the geodesic distance to it. The answer may lie between vertices, not only on them: "how far is this address from the route?" |
Upstream failures and invalid inputs come back as MCP tool errors with
actionable messages, so agents can self-correct. In the tool inputs,
coordinates are named {lat, lon} objects (unambiguous, unlike positional
arrays); distances are metres and durations seconds throughout; see
Conventions.
Environment variables
Each upstream is configured by one variable. All are optional at startup; a tool whose upstream is missing answers with an error naming the variable to set.
| Variable | Powers | Example |
|---|---|---|
VALHALLA_URL | route, matrix, elevation, optimise_routes, search_along_route, plan_day, reachable_area, and the itinerary feasibility pass in verify_places (without it the existence checks still run, the legs are simply not checked) | http://localhost:8002 |
VROOM_URL | optimise_routes, and plan_day with optimise: true | http://localhost:3000 |
PHOTON_URL | geocode, verify_places, search_along_route, and plan_day name lookups | http://localhost:2322 |
STUDIO_URL | get_style, create_style, set_palette, set_layer_paint (the gateway hosting the style API) | http://localhost:8080 |
STUDIO_API_KEY | style publishes only: create_style, set_palette, set_layer_paint (a gateway API key, snk_…); style reads work without it | snk_... |
GATEWAY_URL | search_along_route (it searches the key's own /places dataset) and submit_integration_retro delivery; falls back to STUDIO_URL; both name the same gateway | http://localhost:8080 |
GATEWAY_API_KEY | authenticates those same two; falls back to STUDIO_API_KEY | snk_... |
SN_MAP_ISSUES_DIR | report_map_issue: the NDJSON review queue it appends to | /feedback/map-issues |
SN_RETROS_DIR | submit_integration_retro safety net: every retro the gateway cannot take (no key, unreachable, rate-limited) lands here instead of being lost. Set it even when the gateway path is configured | /feedback/retros |
SN_MCP_USAGE_DIR | optional per-tool-call log: one NDJSON line of {ts, tool, ok, duration_ms, error_code?} per call; never arguments or payloads. Unset = tracing only | /feedback/mcp-usage |
check_adr_tunnel and list_style_layers are fully local and need nothing.
Install
Status, honestly: the sn-mcp binary is not on a package registry yet.
You build it from the source distribution (source access on request) or run
it from the self-host distro's Docker image.
From source (Rust toolchain required):
cargo build --release -p sn-mcp
# binary at target/release/sn-mcp; copy it to /usr/local/bin/sn-mcp
As a container (from the distro):
docker build -f distro/Dockerfile --target mcp -t mapmap/mcp .
# stdio, for a desktop MCP client; -i keeps stdin open for the protocol
docker run -i --rm \
-e VALHALLA_URL=http://valhalla:8002 \
-e PHOTON_URL=http://photon:2322 \
mapmap/mcp --transport stdio
(Point the container at your services' network; with the self-host distro, attach it to the compose network.)
Connect over stdio (local process)
Claude Desktop, add to claude_desktop_config.json
(macOS: ~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"mapmap": {
"command": "/usr/local/bin/sn-mcp",
"args": ["--transport", "stdio"],
"env": {
"VALHALLA_URL": "http://localhost:8002",
"VROOM_URL": "http://localhost:3000",
"PHOTON_URL": "http://localhost:2322",
"STUDIO_URL": "http://localhost:8080",
"STUDIO_API_KEY": "snk_..."
}
}
}
}
Claude Code:
claude mcp add mapmap \
--env VALHALLA_URL=http://localhost:8002 \
--env VROOM_URL=http://localhost:3000 \
--env PHOTON_URL=http://localhost:2322 \
--env STUDIO_URL=http://localhost:8080 \
--env STUDIO_API_KEY=snk_... \
-- /usr/local/bin/sn-mcp --transport stdio
(The server key, mapmap here, is your choice.) Set only the variables for
the tools you want; the server starts either way and logs a stderr warning for
each missing upstream.
Connect over HTTP (remote agents)
The self-host distro runs the server with the mcp compose profile (default
port 8200, endpoint path /mcp):
cd distro
docker compose --profile mcp up -d
Out of the box the compose service sets only VALHALLA_URL and PHOTON_URL.
To enable optimise_routes, uncomment the vroom sidecar service and the
service's VROOM_URL line; to enable the style tools, uncomment STUDIO_URL
and STUDIO_API_KEY (comments in the compose file mark all three).
Then point your client at it:
{
"mcpServers": {
"mapmap": {
"type": "http",
"url": "http://your-host:8200/mcp"
}
}
}
Or with Claude Code:
claude mcp add --transport http mapmap http://your-host:8200/mcp
Example: a dimensioned truck route
A route call with a hazmat lorry profile:
{
"origin": { "lat": 51.5074, "lon": -0.1278 },
"destination": { "lat": 53.4808, "lon": -2.2426 },
"costing": "truck",
"truck": {
"height_m": 4.0,
"gross_weight_t": 40,
"hazmat": true,
"tunnel_code": "C"
}
}
Structured result (truncated):
{
"distance_m": 335412.0,
"duration_s": 15102.0,
"summary": "M1, M6",
"maneuvers": [
{
"instruction": "Drive northwest on Whitehall.",
"distance_m": 254.0,
"duration_s": 41.0
}
],
"geometry_polyline6": "}vd~zAxwl@...",
"applied_adr": {
"hazmat": true,
"tunnel_code": "C",
"forbidden_tunnel_categories": ["C", "D", "E"],
"costing_options": { "truck": { "height": 4.0, "...": "..." } }
}
}
applied_adr shows exactly which ADR costing was merged into the request, so
an agent can explain why a route avoids a tunnel.
Authentication and billing
Two different keys do two different jobs, and it helps to keep them apart:
SN_MCP_AUTH_TOKENis a self-hosted server's door key. Set it and every HTTP request must carryAuthorization: Bearer <token>or get a 401. It gates access to the server; it is not a billing credential and never reaches the gateway.- A gateway key (
snk_…) is a caller's billing credential. Present it and every metered call you make is metered and billed to that key.
Bill metered calls to your own key
On the streamable-HTTP transport, present your gateway key as an ordinary bearer header:
Authorization: Bearer snk_your_key
The server reads that header per request and routes every metered call in the tool invocation through the MapMap gateway on your key, so the usage meters and bills to you rather than to the operator's shared key. This is what lets an agent pay per call on the hosted endpoint. It also works on a self-hosted HTTP server that is pointed at a gateway.
Only a bearer token with the snk_ prefix is treated as a billing key. An
operator's SN_MCP_AUTH_TOKEN, or any other token, is not mistaken for
one: the call then runs as an anonymous caller on the operator's key instead.
So if you expect to be billed, check that the token you send actually begins
snk_.
Set it in your client the same way you set the URL. Claude Code:
claude mcp add --transport http mapmap https://mcp.mapmap.ai/mcp \
--header "Authorization: Bearer snk_your_key"
Any streamable-HTTP client that lets you set request headers works the same way:
{
"mcpServers": {
"mapmap": {
"type": "http",
"url": "https://mcp.mapmap.ai/mcp",
"headers": { "Authorization": "Bearer snk_your_key" }
}
}
}
What is billed, and what is free
- Metered to your key are the tools that reach the MapMap gateway:
route,geocode,reverse_geocode,matrix,reachable_area,optimise_routes,order_stops,search_along_route,cheapest_fuel_along_route,nearby_places,plan_day,elevation, and the itinerary feasibility pass inverify_places, plus the style publishes below. Each draws down that key's allowance and is priced exactly as the same capability over the REST API; see Pricing, and x402 payments for what happens once a key is past its allowance (the 402-versus-429 distinction is the same as on the gateway). - Style publishes (
create_style,set_palette,set_layer_paint) are metered publishes. A caller presenting their ownsnk_key may publish with it on any transport, including an untrusted HTTP one, because they are spending their own credential rather than the operator's. - Never metered, no key needed: the local-geometry tools (
geo_*),validate_geodata,check_adr_tunnel,list_style_layers,check_style_contrast, and style reads (get_style). They are pure local computation or public reads, so they answer on any transport, with or without a key.
One nuance for self-hosting: the routing, matrix, geocoding and optimisation
tools are metered only when the deployment reaches them through the
gateway (as the hosted endpoint does, and as a self-host does once
GATEWAY_URL and a key are set). A self-host wired directly to the
engines (VALHALLA_URL, VROOM_URL, PHOTON_URL) with no gateway key calls
them straight, and those calls are not API-key metered.
With no caller key
- Anonymous over HTTP dispatches unchanged on the operator's server. On
the hosted endpoint that is the fair-use evaluation lane (rate limited per
client IP,
429withRetry-Afterover the limit), and style publishes are refused, because the server will not spend its own operator credential on an anonymous caller's behalf: hosted style ids are enumerable through the gateway's publicGET /styles. - Over stdio there are no per-request headers, so there is no caller key to
read. You own the process, and calls use whatever key it was started with
(
GATEWAY_API_KEY,STUDIO_API_KEY), exactly as before.
Self-hosted HTTP server hardening
- The HTTP
/mcpendpoint is unauthenticated unless you setSN_MCP_AUTH_TOKEN, in which case requests must carryAuthorization: Bearer <token>or get a 401 (a caller may also carry their ownsnk_billing key; the two do not conflict). It binds127.0.0.1:8200by default; pass--listento widen that deliberately (the container image does, since a published port cannot reach a loopback bind). Anyone who can reach the port can call every keyless tool, so still run it inside your trust boundary, restrict it by network policy or add auth at your reverse proxy, and terminate TLS there exactly as for the gateway. - In production pass
--allowed-host(repeatable) so Host-header validation defends against DNS rebinding; when omitted, validation is off and the server warns at startup.
Troubleshooting
The server starts even with no upstreams configured, and tells you on stderr what won't work (stdout stays clean for the MCP protocol):
VALHALLA_URL is not set: route, matrix, elevation, optimise_routes, search_along_route, plan_day and reachable_area will answer with errorsVROOM_URL is not set: optimise_routes (and plan_day optimisation) will answer with errorsPHOTON_URL is not set: geocode, verify_places and search_along_route will answer with errorsSTUDIO_URL is not set: the hosted style tools (get_style, create_style, set_palette, set_layer_paint) will answer with errorsSTUDIO_API_KEY is not set: style publishes (create_style, set_palette, set_layer_paint) will answer with errors; style reads still workSN_MAP_ISSUES_DIR is not set: report_map_issue will answer with errorsneither GATEWAY_URL+GATEWAY_API_KEY nor SN_RETROS_DIR is set: submit_integration_retro will answer with errorsSN_RETROS_DIR is not set: a retro the gateway cannot take (unreachable, rate-limited, key rejected) has nowhere to fall back to and is lost
The same guidance surfaces at call time: an unconfigured tool returns an MCP tool error naming the exact variable to set.
Next steps
- Self-hosting: run the deployment these tools talk to
- Route optimisation: the VRP problem model behind
optimise_routes - Conventions: units, coordinate order, ADR tunnel codes, error envelope
- API reference: the same capabilities over REST