# 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_URL` or `PHOTON_URL` or `VROOM_URL`, so a caller can fix it rather than guess.
- **Advisory tools never block.** `check_style_contrast` audits 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.ts`** is generated, not written. It carries `MCP_TOOL_NAMES` in 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.mjs`** has three subcommands: `list` prints the names read from `server.rs`, `generate` writes the module, and `check` verifies every surface agrees. `check` is not only a headline-number comparison. It asserts table completeness in both READMEs and in `/docs/mcp`, checks the `llms.txt` tool list name by name, reads `agent-terms.json` as 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.yml`** runs `check` on every pull request, deliberately without a path filter, because a path filter is one more list to forget to extend. **`.github/workflows/mcp-smoke.yml`** closes the other half nightly at 05:17 UTC: it derives the expected count from `server.rs` and compares it exactly against `tools/list` on 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](/news/mcp-server) and [the agent skills post](/news/agent-skills) 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:

```sh
claude mcp add --transport http mapmap https://mcp.mapmap.ai/mcp
```

To count it yourself, the same session our nightly smoke test runs:

```sh
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 own `STUDIO_API_KEY`, or call `POST /styles` with your `snk_` key.
- **The `sn-mcp` binary is not on a package registry.** You build it with `cargo build --release -p sn-mcp` from 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.rs` registers. 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/list` against `https://mcp.mapmap.ai/mcp` on 14 September 2026 and counted 43, the same 43 names `server.rs` registers. 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, when `server.rs` registered 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](/docs/mcp) has the per-client configuration, the full tool reference and the environment-variable table, and [the agents page](/agents) collects what we host for machine callers.

Routing and map data derive from OpenStreetMap. Credit "© OpenStreetMap contributors" when you render or republish it.
