Inspiration
ShowMeOnMap is a live product: you type a geographic question in plain language and get a professionally rendered map built from real, fetched data — never invented values — with a legend, a grounded caption and a shareable page. It already had a server-side MCP endpoint and a REST API for headless agents. What it did not have was any way for the agent in your browser to work with you on the page you are both looking at.
That gap is sharper for a map than for almost any other kind of site. A WebGL canvas is the
most illegible thing a browser agent can meet: pixels it cannot read, data it cannot touch,
a camera it cannot move. Screen-reading it is hopeless; the DOM contains a <canvas> and a
legend. WebMCP flips the problem: instead of the agent guessing what the page shows, the page
tells the agent what it can do — and, crucially for us, what it currently shows.
What it does
showmeonmap.com registers 8 tools on document.modelContext. In an agent-capable browser
(ChatGPT's built-in browser, or Chrome 149+ with WebMCP enabled) you can now say:
"We just moved to Vancouver — show me the elementary schools in Kerrisdale and their catchment areas. How many are actually inside Kerrisdale? Zoom to them. Export a PNG."
and watch it happen on the canvas: build_map → query_features → zoom_to_layer →
export_image. Then "show me the secondary schools instead" (mutate_map) and "undo that"
(undo).
| Tool | What it does | Cost |
|---|---|---|
build_map(query) |
natural-language query → fetched real data → rendered map | 1 free daily map (anonymous) / 1 credit |
mutate_map(instruction) |
refine the current map's data or style | 1 credit, auto-refunded on failure |
get_map_state() |
read-back: layers, exact feature counts, camera, filters, analysis headline | free |
query_features(layerId, predicate) |
exact answers from the rendered data via a typed predicate grammar | free |
set_camera(center?, zoom?) |
move the live MapLibre camera | free |
zoom_to_layer(layerId) |
fit the camera to a layer's real extent | free |
export_image(format?) |
render a legend-complete PNG/JPEG/PDF and hand the human the Save button | free |
undo() |
rewind the last map change | free |
Why WebMCP fits this use case. Half of these tools write to the page; the other half
read from it. The read-back channel is the part a screenshot or a DOM scrape can never give
an agent: query_features answers "how many schools are inside Kerrisdale?" with the exact
count from the rendered layer, not a model's guess. The map stops being a picture the agent
describes and becomes a grounded data source for the agent's next sentence.
What people and agents can now do together. The person keeps the canvas, the legend,
the undo history and the Save button. The agent gets typed verbs and honest read-back. Neither
side has to trust the other's description of the map, because both are looking at the same
post-apply state. "Agents propose, people click" is literal here: browsers block downloads
that a human did not initiate, so export_image renders the full-size image and puts a real
Save button on the page for the person.
How we built it
The integration is deliberately one thin boundary, three touch points:
- Mount. A
WebMCPBridgecomponent (renders nothing) runsregisterWebMcpTools()once on the client. For the ~99% of visitors without an agent runtime that is a single property probe (document.modelContext, with the legacynavigator.modelContextname as fallback — the entry point has been renamed twice already). Teardown aborts the registration'sAbortController. - The chat bridge seam. The chat panel owns
sendMessage— the same submission pipeline behind the composer (mutate-first ladder, SSE streaming, store application, billing refresh). It registers that callable with the WebMCP layer, sobuild_map/mutate_mapare processed exactly as if the human had typed: same quota, same 1-credit billing with auto-refund, same anonymous-visitor rules (anonymous sessions get a typed sign-in prompt frommutate_map, never a silent no-op). - Store reads. The free tools read and write the app's existing map store directly — no
server calls. Every result is summarized from post-apply state: layer and feature counts
come from what actually landed, never from "the request was sent". If nothing rendered, the
result says
delivered: false — do not claim a map exists.
Two safety pieces sit on top: every paid tool states its cost in its description (that is what runtimes use to classify a call as consequential — a silently-costed tool is the prompt-injection amplifier), and a gesture-reset budget refuses paid calls after 8 of them with no human interaction until the person touches the page.
Tests: a unit suite pinning the budget semantics, the grounding rule and the cost-honest
descriptions; a Playwright spec in the main app's CI where a stubbed agent runtime drives the
real page; and, in the public repo, a live-site contract test that drives
showmeonmap.com's registered tools through document.modelContext with no access to app
internals — npm test, about 7 seconds, all 8 tools.
Stack: TypeScript, React / Next.js on Cloudflare Workers (OpenNext), MapLibre GL + deck.gl, Zustand stores, Playwright. The tool schemas are plain JSON Schema.
Challenges we ran into
- The spec moved under us.
window.agent→navigator.modelContext→document.modelContext, and Chrome's docs showexecutereturning a string while ChatGPT's show an object. We probe both entry points and return JSON text, which both runtimes accept. - A tool that says "ok" while the canvas didn't change is lying. Our first camera tool
wrote to a legacy config channel the renderer ignores while a plan is active — it reported
success and nothing moved. Caught in the first live ChatGPT-browser test, fixed by driving
the live MapLibre instance and reading the camera back through
get_map_state. - Downloads. Agent-initiated tool calls carry no user activation, so
export_imagecannot save a file. It renders the image and hands the human a real Save button instead. - Reading a map back honestly at neighbourhood scale. "Schools in Kerrisdale" means the area people call Kerrisdale, not the cadastral polygon; strict containment cut schools a block outside the line. Solid dots are inside, faded ones sit within 800 m outside, and the read-back says which is which.
- Cost. An agent in a loop can spend a person's credits. Cost in the description plus the gesture-reset budget is the answer we shipped; it is also why paid tools go through the same billing path as the composer, with auto-refund.
What we learned
WebMCP is turly the future way for AI and web apps
Built With
- deck.gl
- duckdb-wasm
- gl
- javascript
- maplibre
- next.js
- opennext
- openstreetmap
- playwright
- react
- stripe
- typescript
- webmcp
- zustand
Log in or sign up for Devpost to join the conversation.