Skip to content

50,000 free calls a month, card-free. Get an API key →

Documentation menu
docs / mcp · raw .md
Wire an agent to MapMap in 2 minutes · 2:20 · all videos

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:

arduino
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

sh
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):

json
{
  "mcpServers": {
    "mapmap": { "type": "http", "url": "https://mcp.mapmap.ai/mcp" }
  }
}

Cursor: .cursor/mcp.json in your project (or ~/.cursor/mcp.json globally):

json
{
  "mcpServers": {
    "mapmap": { "url": "https://mcp.mapmap.ai/mcp" }
  }
}

Codex: ~/.codex/config.toml:

toml
[mcp_servers.mapmap]
url = "https://mcp.mapmap.ai/mcp"

VS Code (Copilot agent mode): .vscode/mcp.json:

json
{
  "servers": {
    "mapmap": { "type": "http", "url": "https://mcp.mapmap.ai/mcp" }
  }
}

Windsurf: ~/.codeium/windsurf/mcp_config.json:

json
{
  "mcpServers": {
    "mapmap": { "serverUrl": "https://mcp.mapmap.ai/mcp" }
  }
}

From code: the Claude API's MCP connector takes the URL directly:

json
{
  "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:

sh
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

ToolWhat it does
routeTurn-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_routePlaces 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_routeCheapest 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_dayTurn 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_areaReachability 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_tunnelPure 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_routeMeasured 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.
geocodeForward geocoding: {query, limit?, focus?} returns candidates with name, label, lat/lon, type and address parts.
reverse_geocodeThe 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_placesWhat 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_placesCheck 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.
matrixMany-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.
elevationTerrain 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_routesMulti-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_stopsPut 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_geodataCheck 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_issueQueue 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_retroSend 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_layersList 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_styleFetch a hosted style's latest theme document and compiled style URL. Public read, unmetered.
create_styleCreate a hosted style from a theme document (metered publish).
set_paletteRecolour a style by setting palette slots, published as a new immutable version (metered publish).
set_layer_paintOverride one paint property on one style layer, e.g. road width or opacity, published as a new immutable version (metered publish).
check_style_contrastAudit 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.

ToolWhat it does
geo_distanceDistance 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_bearingInitial 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_destinationThe coordinate reached by travelling a given distance on a given bearing. The inverse of geo_distance + geo_bearing.
geo_point_in_polygonWhether 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_bboxThe 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_centroidThe centroid of a set of coordinates, e.g. to pick a depot location or centre a map.
geo_lengthTotal length in metres of a polyline. Measures the line you supply, not a driven route.
geo_areaArea 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_simplifyDouglas-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_lineThe 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.

VariablePowersExample
VALHALLA_URLroute, 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_URLoptimise_routes, and plan_day with optimise: truehttp://localhost:3000
PHOTON_URLgeocode, verify_places, search_along_route, and plan_day name lookupshttp://localhost:2322
STUDIO_URLget_style, create_style, set_palette, set_layer_paint (the gateway hosting the style API)http://localhost:8080
STUDIO_API_KEYstyle publishes only: create_style, set_palette, set_layer_paint (a gateway API key, snk_…); style reads work without itsnk_...
GATEWAY_URLsearch_along_route (it searches the key's own /places dataset) and submit_integration_retro delivery; falls back to STUDIO_URL; both name the same gatewayhttp://localhost:8080
GATEWAY_API_KEYauthenticates those same two; falls back to STUDIO_API_KEYsnk_...
SN_MAP_ISSUES_DIRreport_map_issue: the NDJSON review queue it appends to/feedback/map-issues
SN_RETROS_DIRsubmit_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_DIRoptional 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):

sh
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):

sh
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):

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:

sh
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):

sh
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:

json
{
  "mcpServers": {
    "mapmap": {
      "type": "http",
      "url": "http://your-host:8200/mcp"
    }
  }
}

Or with Claude Code:

sh
claude mcp add --transport http mapmap http://your-host:8200/mcp

Example: a dimensioned truck route

A route call with a hazmat lorry profile:

json
{
  "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):

json
{
  "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_TOKEN is a self-hosted server's door key. Set it and every HTTP request must carry Authorization: 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:

makefile
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:

sh
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:

json
{
  "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 in verify_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 own snk_ 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, 429 with Retry-After over 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 public GET /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 /mcp endpoint is unauthenticated unless you set SN_MCP_AUTH_TOKEN, in which case requests must carry Authorization: Bearer <token> or get a 401 (a caller may also carry their own snk_ billing key; the two do not conflict). It binds 127.0.0.1:8200 by default; pass --listen to 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 errors
  • VROOM_URL is not set: optimise_routes (and plan_day optimisation) will answer with errors
  • PHOTON_URL is not set: geocode, verify_places and search_along_route will answer with errors
  • STUDIO_URL is not set: the hosted style tools (get_style, create_style, set_palette, set_layer_paint) will answer with errors
  • STUDIO_API_KEY is not set: style publishes (create_style, set_palette, set_layer_paint) will answer with errors; style reads still work
  • SN_MAP_ISSUES_DIR is not set: report_map_issue will answer with errors
  • neither GATEWAY_URL+GATEWAY_API_KEY nor SN_RETROS_DIR is set: submit_integration_retro will answer with errors
  • SN_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