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:

  1. Mount. A WebMCPBridge component (renders nothing) runs registerWebMcpTools() once on the client. For the ~99% of visitors without an agent runtime that is a single property probe (document.modelContext, with the legacy navigator.modelContext name as fallback — the entry point has been renamed twice already). Teardown aborts the registration's AbortController.
  2. 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, so build_map / mutate_map are 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 from mutate_map, never a silent no-op).
  3. 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 show execute returning 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_image cannot 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

Share this project:

Updates

Submission history