43 MCP tools, counted from the server, not typed by hand
Our MCP server registers 43 tools, and we no longer type that number into a page. A script reads the #[tool(name = "...")] registrations out of crates/sn-mcp/src/server.rs, writes them into a generated TypeScript module, and CI fails the build when a published surface disagrees. We built that because the count moved five times in a fortnight, and on the day we sat down to correct it four of our own published surfaces each claimed a different number. For a customer that is a language model, the tool list is not documentation about the product. It is the product surface: the only vocabulary the model has, and the thing it plans against before it has read a word of our prose.
Why is the tool list the product surface?
Because an agent plans against the list it was handed, not the documentation it could have read. A capability with an HTTP endpoint but no tool name is not reachable in practice: the model has no reason to suspect it exists, so it never goes looking. Every entry is therefore a product decision rather than plumbing. Adding a tool changes what agents attempt. Naming it badly changes what they attempt it for. Leaving one out means the capability may as well not exist, however well documented.
The current 43 fall into five groups. Twenty-one reach the routing and places backends, from route, matrix and geocode through plan_day, verify_places, cheapest_fuel_along_route, plan_ev_route and the fleet tools cluster, replan_routes, submit_optimise_job and get_job. Six drive the style engine, list_style_layers through set_layer_paint. Two point the other way, letting an agent tell us the map or the integration is wrong (report_map_issue and submit_integration_retro). One, get_usage, reports what a key has spent in weighted quota units. And 13 answer locally: the 10 geo_* primitives from geo_distance through geo_nearest_point_on_line, validate_geodata, which checks whether a dataset's declared coordinate reference system actually describes its own coordinates before you draw it on a map, plus check_adr_tunnel and list_place_categories.
What did the count actually do?
It climbed from 17 to 32 between 19 and 31 July 2026, reached 33 on 9 August when validate_geodata landed, and has since gone to 43. The git history is messier than any of our published corrections admitted. submit_integration_retro took it to 17 on 19 July and verify_places to 18 on 27 July. elevation made 19 and cheapest_fuel_along_route made 20, both on 28 July. reverse_geocode made 21 and nearby_places made 22, both on 30 July. Then one change on 31 July added 10 local geometry tools at once, taking the surface from 22 to 32.
It kept moving after that, in larger steps and for better reasons. check_clearance_on_route made 34 on 28 August. On 1 September match_trace, list_place_categories and get_usage took it to 37, cheapest_charging_along_route to 38 and plan_ev_route to 39. On 3 September the fleet build-out added cluster, replan_routes, submit_optimise_job and get_job in one change, and the surface reached 43, which is where it stands as this post goes out.
Two of those states, 18 and 21, existed only in the repository: no page we published ever claimed either, because a hand-typed copy cannot keep up with a list that moves twice in a day. That is not a documentation failure. The number had no single definition anywhere, so every mention of it was a guess with a timestamp.
What discipline does the list have to hold?
Four rules, applied identically to all 43, because inconsistency across a tool list costs an agent more than a missing feature.
- Coordinates are
{lat, lon}objects, never positional arrays. A positional pair is a coin flip every model eventually loses, and the failure is silent: you get a route in the wrong hemisphere rather than an error. - Units are fixed across the surface. Metres and seconds, in every tool. One tool returning kilometres would spoil the whole list, because the agent would then have to check each one.
- Errors name the fix. A tool whose upstream is not configured answers with an error naming the environment variable to set,
VALHALLA_URLorPHOTON_URLorVROOM_URL, so a caller can fix it rather than guess. - Advisory tools never block.
check_style_contrastaudits a style against WCAG 2.1 thresholds, 4.5:1 for label text and 3:1 for graphics such as the route line, and names the palette slots to change. It does not refuse the publish. A gate agents learn to route around is worth less than an audit they call.
Of the 43, 15 need no network: the 11 local-computation tools, plus check_adr_tunnel, which answers from the regulation's own table, list_place_categories, which returns a fixed taxonomy, list_style_layers, which reads local data, and check_style_contrast, which works on an inline theme document. Those answer instantly, are never metered or rate limited, and keep working when a backend is down. Geometry is measured geodesically on the WGS84 ellipsoid, not with a spherical approximation.
What does it look like when the tool count is wrong?
It looks like four published pages disagreeing while two open pull requests each propose a different wrong correction, which is what we found on 2 August 2026. The README said 17. llms.txt said nineteen and listed 19 names. The agents page claimed 19 hosted tools in one table and spelled out 22 in a heading, on the same page. /docs/mcp had the right number in prose and a reference table missing three rows. .well-known/agent-terms.json, which exists so machines do not have to parse our marketing, said 17.
All of those are corrected and now held there. Every surface moved with the code through each of the increases above and reads 43 today: the README names the geometry, fuel and fleet tools, llms.txt spells out all 43 names, /docs/mcp carries a complete reference table, and agent-terms.json reports "tools": 43. The agents page no longer states a number of its own: it imports one.
For a brand whose positioning is that its numbers are checkable, publishing four different counts of our own tools was the wrong thing to get wrong. Saying so plainly is cheaper than being caught.
How did we stop it drifting again?
By deleting the hand-typed number from every surface that could import it, and making CI read the rest. Three files do the work, and they are worth naming because the mechanism is the point:
website/lib/mcp-tools.generated.tsis generated, not written. It carriesMCP_TOOL_NAMESin declaration order,MCP_TOOL_COUNT, and the count spelled out for prose, under the header line "Source of truth: crates/sn-mcp/src/server.rs". The agents page and our internal CRM copy import it, so four claims that used to drift cannot.scripts/mcp-tool-inventory.mjshas three subcommands:listprints the names read fromserver.rs,generatewrites the module, andcheckverifies every surface agrees.checkis not only a headline-number comparison. It asserts table completeness in both READMEs and in/docs/mcp, checks thellms.txttool list name by name, readsagent-terms.jsonas JSON, and asserts the LangChain and CrewAI wrappers carry no count at all, because those ship to PyPI on their own cadence and a number in a released sdist goes stale where no CI can reach it..github/workflows/tool-count.ymlrunscheckon every pull request, deliberately without a path filter, because a path filter is one more list to forget to extend..github/workflows/mcp-smoke.ymlcloses the other half nightly at 05:17 UTC: it derives the expected count fromserver.rsand compares it exactly againsttools/liston the live endpoint. It used to compare against a floor of 10 or more, which passed happily throughout the drift.
Dated posts are excluded on purpose. Rewriting a published article so its number matches today's code would be the dishonest fix, so our original MCP server write-up and the agent skills post keep the counts they went out with and carry a dated note pointing at the current list. The script lists them by name, so the exclusion is a recorded decision rather than an oversight.
How does an agent get the list?
By asking the server, the only copy that cannot be stale. The hosted endpoint speaks streamable HTTP, needs no key to connect and runs on fair use, rate limited per client IP with a 429 and Retry-After over the limit:
claude mcp add --transport http mapmap https://mcp.mapmap.ai/mcp
To count it yourself, the same session our nightly smoke test runs:
MCP=https://mcp.mapmap.ai/mcp
H='Content-Type: application/json'
A='Accept: application/json, text/event-stream'
SID=$(curl -sS -i -X POST "$MCP" -H "$H" -H "$A" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"count","version":"1"}}}' \
| tr -d '\r' | awk 'tolower($1)=="mcp-session-id:"{print $2}')
curl -sS -X POST "$MCP" -H "$H" -H "$A" -H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}' > /dev/null
curl -sS -X POST "$MCP" -H "$H" -H "$A" -H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
tools/list answers with {"tools":[{"name":"route","description":"...","inputSchema":{...}}, ...]}, one entry per registration, each with a full JSON Schema. Count the names and you have today's number without trusting anyone's prose, ours included.
What this does not do
- The hosted endpoint is for evaluation, not production traffic. Style publishes (
create_style,set_palette,set_layer_paint) are refused there, because publishing is signed with a gateway key and an open endpoint would sign with ours. Run the server yourself with your ownSTUDIO_API_KEY, or callPOST /styleswith yoursnk_key. - The
sn-mcpbinary is not on a package registry. You build it withcargo build --release -p sn-mcpfrom the source distribution, or run it from the self-host distro image. There is no one-line install and we are not going to imply there is. - The check proves agreement, not correctness. CI verifies every surface says what
server.rsregisters. It cannot tell you a tool is well named, well scoped or worth having, and those are the decisions that matter. - 43 is today's number, and the deployed endpoint agrees. We checked
tools/listagainsthttps://mcp.mapmap.ai/mcpon 14 September 2026 and counted 43, the same 43 namesserver.rsregisters. The two can drift: the generated file and the CI check keep the source honest, but only a rebuild of the hosted box carries a new tool through to the runtime, and until one lands the endpoint can trail the code. That happened once already, whenserver.rsregistered 33 and the hosted server still listed 32. - The count moves faster than any document about it. It was 17 in July, and it passed through 18, 19, 20, 21, 22, 32, 33, 34, 37, 38 and 39 on the way to 43. Read it from the server you are actually calling, with
tools/list, rather than from a blog post, including this one.
Try it
Add the endpoint, then ask your agent for something that chains two tools: plan a day from place names, then profile that route for grade with elevation. The MCP connection guide has the per-client configuration, the full tool reference and the environment-variable table, and the agents page collects what we host for machine callers.
Routing and map data derive from OpenStreetMap. Credit "© OpenStreetMap contributors" when you render or republish it.
