Skip to content

MCP server, WebMCP tools and the map inside the chat - #28

Merged
bdevloed merged 14 commits into
mainfrom
feat/mcp-server
Oct 4, 2026
Merged

bdevloed merged 14 commits into
mainfrom
feat/mcp-server

Conversation

@bdevloed

@bdevloed bdevloed commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Agents can query the map through two surfaces built on one query core (scripts/_lib/assets/query_core.mjs, inlined into app.js):

  • WebMCP — search_appellations, filter_appellations, get_appellation, list_facets, show_appellation, registered via document.modelContext (Chrome origin trial; token from .env).
  • Remote MCP server at https://mcp.openwinemap.com/mcp — a Bunny Edge Script (mcp/) serving the same four read tools plus show_on_map, an MCP Apps view that draws the requested appellations from the site's pmtiles, with a legend and a link to the same set.
  • /?aocs=a,b,c opens a set of up to 200 appellations in the panel, the link show_on_map gives.
  • Stage 04 writes wiki/data/mcp/<locale>.json (startup records + lookup tables) for the server; deploy_mcp.py builds, publishes and smoke-tests it.
  • About dialog: the server address with a copy button, and a WebMCP note.

Already live in production. Plans and spike results: docs/plan-mcp-server.md, docs/plan-mcp-apps.md.

Deploy order when the context format changes: deploy.sh first, then deploy_mcp.py deploy.

🤖 Generated with Claude Code

bdevloed and others added 14 commits October 4, 2026 15:44
Search, filter and record shaping move out of app.js into
scripts/_lib/assets/query_core.mjs, a dependency-free ES module that
stage 04 inlines into app.js (export keywords stripped) and the coming
MCP server bundles unchanged, so both surfaces answer identically. The
omnisearch, tree filter and label helpers are aliases of the core
instance; the WebMCP tools become thin adapters over it.

- new WebMCP tool list_facets (countries, regions, styles, schemes with
  counts, values that round-trip into filter_appellations)
- tool schemas hardened (limit bounds, country pattern, slug pattern,
  maxLength) and shared as TOOL_DEFS; results carry structuredContent
- fix: a country-qualified scheme ("it:docg", "es:vino-de-pago") never
  matched — the ':' was normalised away; "Vino de Pago" now matches too
- stage 04 writes wiki/data/mcp/<locale>.json, the query context the
  server reads (QUERY_CONTEXT_KEYS / QUERY_CONTEXT_FORMAT)
- About dialog (4 locales) and llms.txt advertise
  https://mcp.openwinemap.com/mcp

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mcp/ is a stateless MCP server (official @modelcontextprotocol/server
2.3.0, createMcpHandler) deployed as a Bunny standalone Edge Script at
https://mcp.openwinemap.com/mcp. It serves protocol 2026-07-28 natively
and 2025-era clients through the SDK's stateless fallback, with the
query core's tools (search_appellations, filter_appellations,
get_appellation, list_facets) plus a locale argument.

- data: /data/mcp/<locale>.json from the site, cached per isolate and
  revalidated by ETag; get_appellation merges the panel JSON
- MCP Tool Plausible event sent server-side (tool + locale only)
- scripts/deploy_mcp.py: esbuild bundle -> Bunny compute API -> publish
  with the git SHA -> smoke test in all three protocol modes
- tests: node --test (core anchors, protocol conformance per mode,
  context store), parity_webmcp.py (page WebMCP == server, headless),
  tests/test_mcp_query_context.py (stage 04 <-> context.js contract)
- CI job builds the bundle and runs the tests
- docs/plan-mcp-server.md: plan, decisions, spike results

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- deploy_mcp.py hostname HOST: attach the hostname to the script's pull
  zone, add the CNAME in the Bunny DNS zone, issue the free certificate,
  force SSL (idempotent; refuses to overwrite a different record)
- deploy_mcp.py vars NAME=VALUE: set script environment variables
- the smoke client sends X-OWM-Smoke; the edge script skips the
  `MCP Tool` Plausible event for it, so deploys don't inflate the counts

Production: script owm-mcp (id 94571), https://mcp.openwinemap.com/mcp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A smoke test right after publishing was answered by isolates still
running the previous release (seen as test calls counted under the old,
untracked code). The bundle now carries its git SHA as OWM_BUILD, the
server reports it as version 1.0.0+<sha>, and deploy_mcp.py's smoke test
polls until the endpoint reports that version before testing; a failed
mode now fails the deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tor"

The About dialog shows https://mcp.openwinemap.com/mcp in a code box
with a Copy button (clipboard API, falls back to selecting the address);
copies are counted as the `MCP Address Copied` Plausible event. Dutch
wording "aangepaste connector" (a calque of "custom connector") becomes
"eigen connector".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `show_on_map` tool with an MCP Apps view (ui://open-wine-map/map): a
MapLibre 4.7.1 map of the requested appellations drawn from the site's
own pmtiles over the CARTO basemap, framed and highlighted, click for
name + "Open on Open Wine Map", fullscreen, host light/dark theme. The
text result stays Markdown map links for hosts without MCP Apps.

- view/ → scripts/build-view.mjs → dist/view.html (one self-contained
  document, 1.46 MB; MapLibre's UMD build starts its worker from blob:),
  imported as text by the edge bundle (1.8 MB)
- the resource declares its CSP (tile origin + *.basemaps.cartocdn.com);
  tile origin and CARTO key are injected at resources/read time, so one
  bundle serves the spike and production
- optional TILE_PROXY: the script re-serves /map-data/*.pmtiles with CORS
  (no-store), for the spike, leaving the production site untouched
- server instructions and the tool description ask the model to call
  show_on_map whenever it names appellations
- test/apps_host_check.py: a local MCP Apps host (the SDK's AppBridge),
  the view on its own origin under the spec's reference CSP: draws with a
  Claude-like `worker-src blob:`, and — as expected — not under the
  literal spec CSP (blob: worker refused by script-src)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e 0 results

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Does your browser already speak WebMCP? Every page of the map offers it
the same tools. If that means nothing to you, never mind: the MCP server
above is the one your AI assistant knows how to use." — in the four
locales, WebMCP linked to Chrome's documentation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the same set

Up to 10 appellations each get their own colour; the title opens a legend
list (open by default for several). Hover a row or a polygon to light it,
click a row to zoom to it (the site button becomes "<name> ↗"), the colour
square hides it, "Show all" resets. A multi-appellation result links the set
as /?aocs=a,b,c (≤ 50) in the text and the view; a hidden appellation drops
out of that link. MapLibre's controls get dark styling. apps_host_check.py
exercises the list, hover, hide, pin and the links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…everal appellations

The landing IIFE opens the set as one panel stack, highlights all of it and
frames the map on the union of its boxes (unless the link carries a camera
hash). The query stays in the URL until the reader opens something else.
A set's header is the plain count ("2 appellations"), not "at this point",
and keeps it through a card pick. Docs: CLAUDE.md (map inside the chat),
mcp/README (show_on_map, view/, host check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The site's pull zone already answers /map-data/*.pmtiles cross-origin (ranged
GET and a Range preflight), so the view reads the tiles straight from the
site. They are browser-cached for 30 days, so stage 04 writes the map's own
fingerprinted URLs (?v=<sha8>) into the query context as `tiles`;
show_on_map passes them on and the view uses them (bare paths only while the
site predates the key; additive, format stays 1). llms.txt names show_on_map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 51 Alsace grands crus fell back to the homepage. 200 is show_on_map's own
maximum; the worst case is under 10 KB of URL and the CDN takes 16 KB, so the
homepage fallback goes. The site's ?aocs= landing reads up to 200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bdevloed
bdevloed merged commit 7d45f49 into main Oct 4, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant