Skip to content

Repository files navigation

QuePlan WebMCP Demo — Health Insurance Comparator

An agent-operable health insurance comparator built for the WebMCP Challenge. It exposes 13 WebMCP tools through document.modelContext so an AI agent (ChatGPT in-app browser, Chrome 149+ with the WebMCP flag, or any MCP client through the polyfill) can operate the real UI: set a risk profile, filter and sort plans, page through results, open plan details, compare side by side, get an explainable top-3 recommendation, save a shareable comparison, and prepare — but never send — a contact request.

Live demo: https://queplan.github.io/queplan-webmcp/ (GitHub Pages, auto-deployed from main by .github/workflows/pages.yml).

The page looks like the real queplan.cl comparator (same brand, layout, filters, cards, compare bar, comparison modal and detail sheet) and adds an "Asistente IA" activity panel: a live, plain-Spanish timeline of every tool the agent calls, with a short spotlight on the part of the page that just changed — so judges see the agent operate the comparator step by step, and see exactly where it stops ("✋ Tu decisión: presiona Contratar").

This is a standalone, open-source demo of the WebMCP layer built by QuePlan (a Chilean insurtech). The same layer is integrated into QuePlan's private production codebase behind an internal feature flag; public rollout is pending. The monorepo is private, so this repo ships the same tool names and input contracts (insuranceTypes, organizations, precioMax/precioMin, order, currency, edad/sexo/ingreso/cargas, limite, query, criterio, id, ids) against a fully synthetic catalog. Parameters whose concept doesn't exist in this catalog — prevision (health-system domain), categoria (navigable parent category), coverageOrganizations (provider network) — are omitted from the schemas, exactly the way production omits per-country unsupported filters; grouped option trees are simplified to flat lists.

All data is synthetic. Every insurer ("Andina Salud", "Cordillera Seguros", "Patagonia Vida", "Austral Care", "Altiplano Seguros"), plan, price and coverage figure is invented. No real insurer, quote, or personal data is involved, and nothing is ever submitted anywhere.

Product principle

The person explains what they need in plain language; the agent operates the real comparator; the page shows every step; the person keeps the final decision. The agent is an expert broker with the human in command — it never contracts, and it never sends data without a human click (preparar_solicitud only highlights the button; submitting is physically the human's).

Quick start

npm install
npm run dev        # http://localhost:5173
npm test           # contract + UI + activity tests (vitest, 63 tests)
npm run build      # static build in dist/ (relative asset paths: any static host or subpath)

Requires Node 20+. Every push to main runs the tests, builds and deploys dist/ to GitHub Pages (.github/workflows/pages.yml).

Testing instructions (for judges)

The page registers its tools on load — no toggle, no setup.

Option A — ChatGPT in-app browser: open the live demo URL inside ChatGPT's browser and ask it to operate the page (prompts below). Verified 2026-09-01: 13/13 tools registered and executed there, both on this public demo and on QuePlan's private integration (cross-checked by Claude and Codex agents).

Option B — Chrome 149+ (native WebMCP):

  1. Open chrome://flags/#enable-webmcp-testing, enable it, relaunch Chrome.

  2. Open the demo. The header badge should read WebMCP: 13 tools (API nativa).

  3. Use any WebMCP-capable agent/extension (e.g. DevTools' Model Context panel) to list and invoke the tools — or drive them from the DevTools console (verified on the native runtime; note the native executeTool takes the RegisteredTool object and the arguments as a JSON string):

    const mc = document.modelContext;
    const tool = (name) => mc.getTools().then((ts) => ts.find((t) => t.name === name));
    await mc.executeTool(await tool('setear_perfil'), JSON.stringify({ edad: 34, sexo: 'Femenino', cargas: [{ edad: 5, sexo: 'Femenino' }] }));
    await mc.executeTool(await tool('aplicar_filtros_comparador'), JSON.stringify({ precioMax: 100000 }));
    await mc.executeTool(await tool('recomendar_seguros'), '{}');

    Each call moves the page and adds a line to the "Asistente IA" panel.

Option C — any browser + any MCP client (dev relay): without the native API the page falls back to @mcp-b/webmcp-polyfill automatically, and in dev mode (npm run dev) it also loads the @mcp-b/webmcp-local-relay embed, which bridges the tab's tools to external MCP clients over a local WebSocket:

  1. npm run dev and open http://localhost:5173 (keep the tab open).

  2. Configure the relay as an MCP server in your client — e.g. for Claude Code, a .mcp.json:

    {
      "mcpServers": {
        "webmcp-local-relay": {
          "command": "npx",
          "args": ["-y", "@mcp-b/webmcp-local-relay@4.0.0"]
        }
      }
    }
  3. The client can now call webmcp_list_tools and invoke the 13 tools by name (the embed and the relay server must be the same version — both are pinned to 4.0.0).

The relay embed is dev-only; production builds ship no relay.

Judge demo (2:30)

A scripted pass through the full journey — paste these one by one into the agent (ChatGPT in-app browser or a Chrome 149+ WebMCP agent) with the demo open:

  1. Watch the panel — keep the "Asistente IA" panel (right side, or the floating pill on small screens) in view: every step below appears there in plain Spanish, and the page spotlights what changed.
  2. Profile — "I'm a 34-year-old woman with two kids: a 5-year-old girl and an 8-year-old boy. Set that up." (the sexes are explicit on purpose: if you only give ages, the agent should ask — setear_perfil never invents a dependent's sex; the profile card shows "—" for it)
  3. Budget + needs — "Keep only complementary health and catastrophic plans under $120,000 a month."
  4. Explainable top-3 — "Recommend the best three for us and tell me WHY each one." (watch the "Tu comparación 🥊" podium open on screen; the reasons come as structured data)
  5. Compare — "Now compare the two cheapest of those head to head. Which wins on deductible?"
  6. More options — "Anything else beyond this page?" (the visible list pages forward)
  7. Share — "Save this comparison so I can send it to my husband." (state-restoring URL)
  8. Human-only CTA — "I want to be contacted about the winner." (the agent can only scroll and highlight the Contratar button — the click, and everything after it, is yours)
  9. Adversarial — "Filter prices from $100,000 down to $50,000... I mean min 100000, max 50000." (typed input_invalido error, no impossible state left behind)

Example prompts

English:

  • "I'm a 34-year-old woman with two kids (5 and 8). Find me the three cheapest complementary health plans and compare them side by side."
  • "Filter to oncology coverage only, sort by lowest price, and open the details of the best one."
  • "Show prices in UF instead of pesos. What's the price range now?"
  • "Save this comparison so I can send the link to my partner."
  • "I want to be contacted about the second plan." (the agent can only highlight the button — you click it)

Español:

  • "Tengo 41 años, hombre, una carga de 3 años. ¿Qué plan catastrófico me conviene con tope alto y deducible bajo? Muéstrame la ficha."
  • "Compara los tres planes oncológicos y dime cuál gana en cada atributo."
  • "Filtra solo Andina Salud y Cordillera Seguros bajo $80.000 mensuales."
  • "Guárdame esta comparación para retomarla después."
  • "Prepárame la solicitud del plan que elegimos." (el agente resalta el botón; el click es tuyo)

The 13 tools

Tool Type What it does
estado_comparador read-only Full current context: profile, currency, order, active filters (with monedaFiltros), total, page, price range (UF).
listar_seguros read-only Plans on the current page. total is always the full filtered total; enPagina the page size; placeholders without an id are excluded.
ver_mas_resultados mutation (UI) Advances the visible list to the next page. Last-page guard (sin_cambios) + auto-recovery to page 1 if pagination desyncs.
obtener_opciones_filtro read-only Translates filter names to IDs. query + limite search options without dumping the catalog into context.
setear_perfil mutation PATCH the quoting profile (edad, sexo, ingreso, cargas); cargas: [] clears; each carga requires edad (no phantom dependents) and a carga's sexo is never invented — absent stays absent (UI shows "—", the share link encodes only the age). ingreso is write-only — confirmed with ingresoActualizado: true, never the amount.
aplicar_filtros_comparador mutation insuranceTypes / organizations / precioMax+precioMin (CLP). ID lists REPLACE; [] RESTORES that filter's default — all categories / all companies marked (a company filter is all-by-default, so [] never means "0 companies"; the price filter ignores []); unknown IDs and inverted ranges (min > max) are typed errors.
ordenar_resultados mutation recommended / lowerPrice / higherPrice.
cambiar_moneda mutation Display currency CLP ⇄ UF (display-only, no re-quote).
obtener_detalle_seguro read-only Opens the plan's detail on screen and returns the sheet incl. price breakdown (long lists capped + recortado flag).
comparar_seguros mutation (UI) Side-by-side view of 2–3 plans + compact difference matrix with per-attribute winner.
recomendar_seguros mutation (UI) Explainable top-3 for the CURRENT profile+filters (mejor_valor / menor_precio / mayor_cobertura) with structured reasons per rank, and opens the podium side by side. menor_precio only ranks within the dominant currency.
guardar_comparacion read-only Shareable URL that restores the exact state (profile, filters, order, currency, comparison).
preparar_solicitud human-in-the-loop Navigates to the plan and highlights its "Contratar" button. Returns requiereConfirmacionHumana: true. Never submits.

Contract principles (enforced by tests)

  1. Imperative, top-level document.modelContext.registerTool — snake_case ASCII names.
  2. Strict schemas: enum for every closed value, additionalProperties: false, per-parameter descriptions — plus own-schema validation in the handler, because the native browser engine does not validate (the polyfill does; the standard doesn't promise it).
  3. Every tool returns human text and structuredContent (+ declared outputSchema whose estado enum is always COMPLETE — a partial enum makes output validation reject legitimate responses).
  4. Shared result-state taxonomy in estado: ok / timeout / error_backend / input_invalido / sin_cambiosisError only on the middle three; sin_cambios is a readable no-op, never a fake success. Nothing is ever invented on the user's behalf: no phantom dependents, no defaulted sex, no "0 companies" from an empty selection.
  5. Honest annotations: readOnlyHint on reads, untrustedContentHint wherever output carries third-party catalog text. Never instructions inside descriptions or outputs.
  6. Totals are consistent across tools (listar_seguros.totalestado_comparador.resultado.total), and every re-quoting mutation resets pagination to page 1 — the UI, the state and the list can never tell three different stories.
  7. Every mutation moves the visible UI and is re-readable via estado_comparador (read-modify-write is always possible).
  8. Output budget: long lists are capped and flagged (recortado), options are searchable (query), and rankings never mix currencies.
  9. Consequential actions stay human: the agent proposes, the person confirms with a click.

Architecture

                    agent (ChatGPT in-app browser / Chrome WebMCP / MCP client)
                                          │
                                          ▼
                            document.modelContext.registerTool
                                          │
      ┌───────────────────────────────────┼─────────────────────────────────────┐
      │  src/webmcp/  (portable WebMCP layer)                                   │
      │                                                                         │
      │   tools/*.ts ── 13 tool contracts (schemas, taxonomy, annotations)      │
      │        │ wrapped by                                                     │
      │   validate-args.ts ── own-schema validation (works on native engine)    │
      │   native-boundary.ts ── native V8 promise + JSON-plain clone + in-band  │
      │        │                errors (survives zone.js-patched environments)  │
      │   activity.ts ── observer hook: one ActivityEvent per tool call (any    │
      │        │          engine, any outcome) — feeds the on-page activity panel │
      │   register.ts ── engine detection (native → polyfill) + per-tool        │
      │        │          registration with graceful degradation + diagnostics  │
      │        ▼                                                                │
      │   ComparatorDataSource  ◄── the ONLY seam the tools know about          │
      └───────────────────────────────────┬─────────────────────────────────────┘
                                          │ implemented by
      ┌───────────────────────────────────▼─────────────────────────────────────┐
      │  src/app/  (demo host — the private codebase binds the real Angular UI) │
      │   store.ts ── in-memory state + pricing/filter/sort + share-URL codec   │
      │   ui.ts ───── vanilla-TS view styled after queplan.cl: cards, filters,  │
      │               profile card, compare bar, "Tu comparación" modal, detail │
      │               sheet, highlight of the human-only "Contratar" button     │
      │   activity-panel.ts ── "Asistente IA" timeline + spotlight (observer)   │
      │   src/data/demo-products.json ── 25 synthetic plans, 5 fake insurers    │
      └─────────────────────────────────────────────────────────────────────────┘

The ComparatorDataSource interface is the portability seam: in QuePlan's private codebase the same tool layer binds to the live Angular comparator (real quoting backend, real filters); here it binds to an in-memory store. Tools, schemas, taxonomy and wrappers are identical by design.

Why the native-boundary wrapper matters

QuePlan's comparator runs Angular + zone.js, which patches window.Promise — so every tool result crosses the WebMCP boundary as a non-native thenable, and anything that reaches the engine is structured-cloned. native-boundary.ts removes that whole class of hazards by construction: it converts at the boundary (native V8 promise, JSON-plain descriptor and result, in-band errors), so the same tool code works on the polyfill and inside zone-patched apps regardless of how a given engine treats patched promises. This demo's layer is verified with the WebMCP polyfill, through a real MCP client via the local relay, on Chrome's native WebMCP runtime, and end-to-end in the ChatGPT in-app browser (2026-09-01: 13/13 tools registered and executed, on this public demo and on the private integration, cross-checked by Claude and Codex). This demo has no zone.js, but ships the wrapper — it is the piece most WebMCP adopters with a framework will need first.

The activity panel (what the human sees the agent do)

src/webmcp/activity.ts wraps every tool at registration time (withNativeBoundary(withActivity(withValidatedArgs(tool)))) and publishes one ActivityEvent per execution — whatever the engine and whatever the outcome (ok, sin_cambios, a typed input_invalido, or a thrown exception). It observes only: names, schemas and results are untouched (a test asserts the descriptors are identical before and after wrapping). src/app/activity-panel.ts subscribes and renders the "Asistente IA" panel: a timestamped timeline in plain Spanish ("👤 Perfil: 34 años, femenino, 2 cargas ✓ → 25 opciones", "🔎 Filtros: máx $100.000 · Salud Complementario + Catastrófico → 15 opciones", "🏆 Top 3 recomendado (mejor valor): …", "🔗 Link guardado…", "✋ Tu decisión: revisa … y presiona Contratar"), newest first, capped at 8 entries, with a 1.8 s spotlight on the zone that changed (results badge, cards, price slider, profile card, comparison modal, detail sheet, the human-only CTA). Errors show as "⚠️ … rechazado — <the tool's own message>" so an adversarial prompt is visibly caught, not hidden.

Tests

tests/tools.contract.test.ts (vitest, no browser needed) locks the 13 contracts: registered names, strict schemas with the complete estado enum, honest annotations, structured output everywhere, consistent totals, []-restores-filter semantics ("compañías restauradas (todas)"), inverted-range rejection, PATCH profile semantics with required carga edad and never-invented sexo (output and share link), write-only ingreso confirmation, option search (query/limite), pagination (advance, last-page guard, reset-on-mutation), explainable top-3 ranking, placeholder exclusion, share-URL round-trip, human-in-the-loop flag, and clone-safety of every result. tests/ui.smoke.test.ts (happy-dom) proves tool calls move the visible UI — paging, the recommendation podium, the queplan.cl anatomy (brand, banner, results badge, profile card, sidebar counts, card data row, compare bar, "Tu comparación", detail tabs) and the "—" shown for a dependent without sex. tests/activity.test.ts locks the activity hook: every executed tool emits exactly one event (validation rejections and thrown handlers included), a broken observer never breaks a tool call, descriptors are untouched by the wrapper, and the panel renders, narrates, caps and spotlights as described. npm test runs all 63.

License

MIT © 2026 QuePlan SpA

About

WebMCP tools for a real-world insurance comparator — open demo by QuePlan.cl for the OpenAI WebMCP Challenge

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages