Skip to content

WebMCP

Every page of unmap.dev registers a small set of tools with the browser's WebMCP model context. An agent working inside the tab (a browser extension, or an assistant driving the page a person is looking at) can use the site rather than scrape it.

This page is the reference for those four tools: their arguments, what they return, and what they refuse. If you want the wider picture of how unmap presents itself to agents, Agents covers the discovery documents and the A2A endpoint, and MCP server covers the server-side MCP endpoint that does call the API.

What these tools are not

The distinction matters more here than anywhere else in these docs, because the two surfaces look alike and behave completely differently.

Nothing on this page touches the unmap API. No key, no request to api.unmap.dev, no metered call, no rate limit. Search runs against a static index already in the browser; the other three read pages this site serves anyway. You can use them logged out, and they cost nothing.

Anything that would cost a call (a geocode, a route, an isochrone, a tile) is deliberately absent. Those live on the MCP server and the REST API, both of which need a key. An agent that finds list_unmap_map_styles here and expects to geocode with it has misread the surface.

search_unmap_docs

Full-text search across these docs, returning ranked sections. This is the tool to reach for first when the question is "how do I do X with unmap".

{
  "query": "isochrone",   // required, non-empty
  "limit": 5              // optional integer, 1–10, default 5
}

Returns JSON: the query as run, and a results array of { url, title, section?, excerpt }. The URL is absolute, so it can be handed to someone else or passed straight to read_unmap_page. section appears when the hit landed on a heading within a page rather than the page itself.

Excerpts arrive as prose. The underlying index returns them as HTML with the matched words wrapped in <mark>; the markup is stripped and the entities decoded before the text reaches the model, because a model wants the sentence, not the tags.

An out-of-range limit is clamped rather than refused: 0 and 999 both give you a working search. A missing or empty query is an error.

read_unmap_page

Any page of unmap.dev as Markdown, given a root-relative path. Use it to read a page's full prose after a search returns an excerpt.

{ "path": "/docs/start/quickstart" }   // required

Returns the page's Markdown as text, the same bytes you get from the site with an Accept header:

curl -H "Accept: text/markdown" https://unmap.dev/docs/start/quickstart

This tool reads; it does not move the person. If someone asks what a page says, read it and leave them where they are. navigate_unmap_site is for when they want to look at the page themselves.

The path is narrowed before anything is fetched, and refused rather than repaired. An absolute URL on unmap.dev's own origin is accepted, because that is what a search result gives you. Everything else is rejected: another origin, a protocol-relative URL, a traversal, and anything with a file extension, which is an asset with no Markdown beside it.

That check leans on a URL parser rather than on string inspection, and the reason is worth knowing if you are writing something similar. The WHATWG parser folds a backslash into a slash and strips tabs and newlines outright, so /\evil.com and a path with an embedded tab both resolve to //evil.com, a different host, from input that reads like a root-relative path. Backslashes and embedded whitespace are therefore refused outright, and the parsed origin gets the final word.

One failure is expected and says so plainly: on a local next dev there is no Worker in front of the site to serve Markdown, so the request comes back as HTML and the tool reports that rather than handing a model a page of markup as if it were prose.

Opens a page of unmap.dev in the current tab.

{ "path": "/docs/apis/geocoding" }   // required; an enum, not free text

The path argument is a JSON Schema enum of every page on the site, not a string. That does two things: it documents the site to the model, so it can see where it may go without guessing; and it makes an off-site or invented destination unrepresentable rather than merely rejected.

The list is built from the filesystem at build time, so a new documentation page joins it without anyone remembering to, and a page that does not exist can never be in it. In French it drops anything with no French mirror, so an agent is never offered a /fr/… path that would 404.

list_unmap_map_styles

Takes no arguments. Returns the twelve basemap styles, each in light and dark, with the flavor value that requests it, plus the style URL template and a worked example, because a list of names alone is not enough to point a map at one.

{
  "modes": ["light", "dark"],
  "styles": [{ "name": "base", "flavors": ["base-light", "base-dark"] }],
  "usage": {
    "styleUrl": "https://api.unmap.dev/styles/{style}.json?key={YOUR_KEY}&flavor={style}-{mode}",
    "example": "https://api.unmap.dev/styles/base.json?key=um_live_…&flavor=base-dark",
    "docs": "https://unmap.dev/docs/apis/maps"
  }
}

Note the {YOUR_KEY} placeholder: this tool tells an agent how a style URL is shaped, and the agent still needs a key of its own to fetch one. Maps API is the full reference.

How registration works

WebMCP is a draft, and the API has lived in two places. Chrome's early preview exposes it as navigator.modelContext; the specification now hangs it off Document. unmap looks in both, deduplicates by identity, and registers on whatever it finds.

A browser with neither is left completely alone. Nothing is polyfilled and no global is patched. Registration is the entire integration, and the page behaves identically without it.

Registration is also per-tool rather than all-or-nothing. If an implementation already holds one of these names, that one registration is refused and the other three still land: a tool surface that half-appears is more useful than one that vanishes because a page happened to render twice.

Errors

A tool that fails returns its message as an ordinary result marked as an error, rather than throwing across the boundary. A model can read a returned message and try something else; a rejection is far less legible to it.

So read_unmap_page on a bad path answers with a sentence naming the path and what a good one looks like, and navigate_unmap_site on an unknown path lists the pages that do exist. Neither is an exception.

Checking it yourself

The tools register on page load, so the quickest check is the browser console on any page of unmap.dev:

(navigator.modelContext ?? document.modelContext) !== undefined

A true there means the browser has a model context for the tools to register with. What is registered is not enumerable from page script (the model context is the agent's view, not the page's), so the observable check is whether an agent connected to the tab can see the four names.

Next steps

  • MCP server for the same tools outside the browser, with a key.
  • Agents for the A2A card that lets an agent discover the API.
  • API reference for the endpoints behind the tools.