Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,20 @@ jobs:
with:
node-version: "20"
- name: ESLint (map app JS)
run: npx --yes eslint@9 scripts/_lib/assets/app.js
run: npx --yes eslint@9 scripts/_lib/assets/app.js scripts/_lib/assets/query_core.mjs

mcp:
# The remote MCP server (mcp/, a Bunny Edge Script): the bundle must build
# and the tests pass. Tests that need a stage-04 build (wiki/) skip here.
runs-on: ubuntu-latest
defaults:
run:
working-directory: mcp
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- run: mkdir -p dist && npm run build
- run: npm test
72 changes: 59 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6245,19 +6245,65 @@ expected storage zone. Full runbook, the .env layout and the reason beta is
blocked at robots.txt rather than with a noindex header (it would leak into
production through the cross-host canonical): [docs/deploy.md](docs/deploy.md).

## WebMCP tools

The end of [scripts/_lib/assets/app.js](scripts/_lib/assets/app.js) registers
four read-mostly WebMCP tools (`search_appellations`, `filter_appellations`,
`get_appellation`, `show_appellation`) through `document.modelContext`, only
where the browser exposes the API. Chrome ships it as an origin trial
(Chrome 149–162, ends 2027-03-30): the token lives in the repo-root `.env` as
`WEBMCP_ORIGIN_TRIAL_TOKEN` (registered for https://openwinemap.com with
subdomain matching, so www and beta share it) and stage 04 emits it as
`<meta http-equiv="origin-trial">` straight after `<meta charset>` on every map
page (`_origin_trial_meta` in map_template.py). Chrome reissues the token on
renewal — swap the `.env` value and rebuild. Unset ⇒ no tag. Calls are counted
as the `WebMCP Tool` Plausible event (see docs/analytics.md).
## WebMCP tools and the MCP server

The map answers agents through two surfaces built on **one query core**,
[scripts/_lib/assets/query_core.mjs](scripts/_lib/assets/query_core.mjs): a
dependency-free ES module (search normalisation and scoring, grape / style /
region / scheme resolution, `search`, `filter`, `full`, `facets`, and the shared
`TOOL_DEFS` schemas). Stage 04 inlines it into app.js at the `__OWM_query_core__`
token (`export ` stripped; `_load_query_core_inline` refuses imports or build
tokens), and the app's omnisearch, tree filter and label helpers (`grapeName`,
`regionLabel`, `countryLabel`, `expandGrapeSet`, …) are aliases of the `QC`
instance — change search semantics in the core, never in app.js.

- **WebMCP** (end of app.js): `search_appellations`, `filter_appellations`,
`get_appellation`, `list_facets` (adapters over `QC`) plus `show_appellation`
(opens the panel), registered through `document.modelContext` only where the
browser exposes it. Chrome ships it as an origin trial (Chrome 149–162, ends
2027-03-30): the token lives in the repo-root `.env` as
`WEBMCP_ORIGIN_TRIAL_TOKEN` (registered for https://openwinemap.com with
subdomain matching, so www and beta share it) and stage 04 emits it as
`<meta http-equiv="origin-trial">` straight after `<meta charset>` on every
map page (`_origin_trial_meta`). Chrome reissues the token on renewal — swap
the `.env` value and rebuild. Counted as the `WebMCP Tool` Plausible event.
- **Remote MCP server** ([mcp/](mcp/), https://mcp.openwinemap.com/mcp): a
Bunny standalone Edge Script built on `@modelcontextprotocol/server`
(`createMcpHandler`, stateless; serves protocol 2026-07-28 natively and
2025-era clients through the SDK's stateless fallback). Same four read tools
plus a `locale` argument (en / fr / es / nl). Data: stage 04 writes
`wiki/data/mcp/<locale>.json` — the startup records plus the core's lookup
tables (`QUERY_CONTEXT_KEYS` / `QUERY_CONTEXT_FORMAT` in map_template.py,
pinned against `mcp/src/context.js` by `tests/test_mcp_query_context.py`);
the script caches it per isolate and revalidates by ETag every 5 min, and
`get_appellation` fetches the panel JSON `/data/d/<locale>/<slug>.json`.
Counted server-side as the `MCP Tool` Plausible event. Build / deploy:
`.venv/bin/python scripts/deploy_mcp.py deploy` (esbuild bundle → Bunny
compute API → publish with the git SHA → smoke test in all three protocol
modes); the site deploy (`deploy.sh`) must go first when the context format
changes. Tests: `npm test` in mcp/ (core anchors, protocol conformance per
mode, context store), `.venv/bin/python mcp/test/parity_webmcp.py` (the page's
WebMCP tools and the server answer the same calls identically, headless
Chromium). Plan, decisions and spike results:
[docs/plan-mcp-server.md](docs/plan-mcp-server.md).
- **The map inside the chat** (MCP Apps): a fifth tool, `show_on_map`
{slugs}, carries `_meta.ui.resourceUri` → `ui://open-wine-map/map`, a
self-contained MapLibre view ([mcp/view/](mcp/view/), built into
`dist/view.html` by `npm run build:view`) that draws the requested
appellations straight from the site's pmtiles (`TILE_ORIGIN`, default the
site; Bunny already answers them cross-origin, `Range` preflight included)
over the CARTO basemap (`CARTO_KEY` script variable). The tile URLs carry the
site's own `?v=` content fingerprint, handed over in the query context's
`tiles` key and the tool result — the files are browser-cached for 30 days,
so a deploy must change the URL. With ≤ 10
appellations each has its own colour and a legend list: hover a row or a
polygon to light it, click a row to zoom to it (the site button then opens
its page), the colour square hides it. Its text result and the view's site
button link the set as **`/?aocs=a,b,c`** (any locale home, ≤ 200 slugs):
app.js opens that stack in the panel, highlights all of it and frames the
map on it; the query stays in the URL until the reader opens something else.
Host check: `.venv/bin/python mcp/test/apps_host_check.py [--shots DIR]
[--live MCP_URL]`. Plan and phases: [docs/plan-mcp-apps.md](docs/plan-mcp-apps.md).

## Analytics

Expand Down
4 changes: 3 additions & 1 deletion docs/analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ option is inert, and a build with no `localhost` entry loads nothing there.
| `Grape Scope Toggled` | `scope` (`main` / `all`), `locale` | the "main grape only" checkbox that appears as a chip next to the grape chips once a grape is picked in the search box (2026-09-23; before that, a permanent checkbox in the Grapes facet). Not persisted: it ends with the last grape chip. |
| `Omnisearch Used` | `result_count`, `had_match`, `groups` (a/g/r/s/c — appellations / grapes / regions / styles / classifications; `c` and the classification count in `result_count` since the 2026-09-25 build, so a query that matched only a classification — an aging / Prädikat / selection tier — no longer logs `had_match=false`), `query_len`, `locale` | 1 s after typing stops in the omnisearch |
| `Omnisearch Result Picked` | `type` (`grape` / `region` / `style` / `classification`), `locale` | a non-appellation suggestion is picked (appellation picks fire `Appellation Opened`) |
| `WebMCP Tool` | `tool` (`search_appellations` / `filter_appellations` / `get_appellation` / `show_appellation`), `locale` | a browser agent called one of the page's WebMCP tools (`document.modelContext`, end of app.js; registered only where the browser exposes the API). Arguments are never sent — the query text stays private, as for the search box. |
| `WebMCP Tool` | `tool` (`search_appellations` / `filter_appellations` / `get_appellation` / `list_facets` / `show_appellation`), `locale` | a browser agent called one of the page's WebMCP tools (`document.modelContext`, end of app.js; registered only where the browser exposes the API). Arguments are never sent — the query text stays private, as for the search box. |
| `MCP Address Copied` | `locale` | the copy button next to the MCP server address in the About dialog. |
| `MCP Tool` | `tool` (`search_appellations` / `filter_appellations` / `get_appellation` / `list_facets`), `locale` | a client of the remote MCP server (`mcp.openwinemap.com/mcp`, mcp/) called a tool. Sent server-side by the Bunny Edge Script through Plausible's events API (`PLAUSIBLE_HOST` / `PLAUSIBLE_DOMAIN` script variables; unset → nothing sent), with the client's User-Agent and forwarded IP so visitor counting works as for page events. Arguments are never sent. Its page URL is `https://mcp.openwinemap.com/mcp`. |
| `Search Used` | `result_count`, `had_match`, `query_len`, `locale` | the legacy sidebar search (superseded by the omnisearch in June 2026) |
| `Theme Changed` | `theme`, `locale` | light / dark toggle |
| `Feedback Flagged` | `slug`, `aspect` (`boundary` / `grapes` / `facts` / `name` / `sources` / `other`), `country`, `kind`, `geom_source`, `view_mode` (`simple` / `advanced` — since the 2026-09-22 build; `geom_source` is the record's *advanced* provenance, so without the mode a boundary flag on a French appellation cannot be attributed to the commune outline or the parcellaire), `lod` (`footprint` / `parcels` / `zone` — what was on screen for this record: below the detail zoom a French parcel-level record draws its generalised vineyard footprint, from z12 its parcels, every other record one polygon at every zoom), `zoom` (the map zoom at the tap, one decimal), `via` (`card` / `stub-help`), `locale` | one tap on an aspect chip in the **"Report a mistake" section at the bottom of every appellation card** (2026-09). No account, no form. Pressed chips are remembered per browser (localStorage) so a revisit does not re-fire. A `slug × aspect` breakdown is the curator queue; flags ÷ `Appellation Viewed` is a per-record trust score. |
Expand Down
Loading
Loading