Shopify Builds>Three Ships>Eight WebMCP tools that make a product page answerable — and buyable — by a browser agent

Eight WebMCP tools that make a product page answerable — and buyable — by a browser agent

A feature-detected block in the theme layout registers eight navigator.modelContext tools on one trial-kit product page — seven an agent can read from, one that adds a Liquid-fixed variant to the cart.

Browser-based AI assistants have no reliable way to read a product page; they scrape tabs, accordions and review widgets and guess. On one Three Ships trial-kit page we register eight navigator.modelContext tools — seven that answer questions about the kit, one that adds a server-fixed variant to the cart. It is feature-detected, idempotent, and leaves no trace where WebMCP is absent.

Fact Strip

  • Client: Three Ships — clean skincare DTC, Shopify
  • Surface: product detail page
  • Scope: two product listings of the same trial kit, one per market
  • Complexity: High
  • Attribution: Deploi-authored. The tool layer, the data assembly and the cart call are ours. They sit inside theme.liquid on Palo Alto 5.8.0 by Presidio Creative, a paid premium theme we did not write and do not claim.
  • Status: Live, verified 2026-09-06
  • Scale: roughly 330 lines in the theme layout
  • Evidence: Strong — read from the live theme, with one client planning record on the same surface

The Problem

A product page is written for eyes. The facts an assistant needs in order to answer a shopper — what is actually in the kit, which ingredients do the work, how the four pieces are meant to be used together, what it costs in this market, whether it suits this particular person — are spread across tabs, accordions, an ingredients drawer and a third-party review widget, and several of them only exist after JavaScript has run. An agent operating inside the browser has to scrape that surface and infer the rest. Nothing on the page tells it what it may ask or what it is allowed to do. For a low-cost starter kit — the product Three Ships uses to acquire first-time skincare customers — that inference happens at exactly the moment a first purchase is decided.

The Constraint

WebMCP is not a shipped web platform feature. navigator.modelContext exists in a small number of browsers and build channels, so every line of this has to be feature-detected and leave nothing behind when the API is absent: no thrown error, no injected node, no console noise a support agent would later have to explain. A Shopify theme also has no server of its own. There is nowhere to host a tool endpoint, so the tool definitions and every value they return have to be composed by Liquid at render time and shipped inside the document that the agent is already looking at. And registration has to be idempotent, because the theme editor and section rendering both re-execute inline scripts against a document that has already run them once.

What We Built

The whole block lives in the theme layout, gated in Liquid on request.page_type == 'product' and on two product handles — the US and Canadian listings of the same kit. Nothing parses on any other page.

Inside the gate, an IIFE assembles a productData object from values Liquid resolves at render time: product.title, a price from product.price | divided_by: 100.0, shop.currency, localization.country.iso_code as the market, an absolute URL from request.origin plus product.url, and product.selected_or_first_available_variant.id. Around that sit hand-curated constants describing the four kit components, the hero ingredients, and a recommendationFit object with explicit bestFor and notIdealFor arrays.

The script then feature-detects with ('modelContext' in navigator) && !!navigator.modelContext?.registerTool, sets a window.__webMcpRegistered guard, and registers eight async tools through a registerToolSafe() wrapper that try/catches each registration on its own. Seven of them read and one writes. Among the reads:

  • getProductSummary — what the kit is, in one structured object rather than a paragraph an agent has to parse.
  • getKitProducts — the four components, named individually.
  • getIngredientHighlights — the hero ingredients and what each is there to do.
  • getMarketContext — the country code and currency the page was rendered for.
  • isGoodFitForCustomer — the bestFor and notIdealFor arrays, so the answer can be no.

And the one that writes:

  • addBestSellersKitToCart — the only tool that changes anything.

That last one is a real mutation. It POSTs { id: variantId, quantity } as JSON to /cart/add.js, clamps the quantity with Math.max(1, Number(input.quantity || 1)), and returns a { success, message, item } envelope. A non-ok response is mapped to { success: false, message: data.description } rather than thrown, so the agent gets Shopify's own explanation of the failure instead of a rejected promise it has to describe generically.

The variant id is not a parameter. It is written into the script by Liquid, from the product the page was rendered for.

Why This Way

Seven read tools and exactly one write tool is a boundary, drawn on purpose. An agent can ask this page anything about the product, and the only state it can change is adding this one variant to this cart. Because the variant id comes from Liquid rather than from tool input, a tool call cannot be steered onto a different SKU by an instruction the agent picked up somewhere else. Returning a structured success or failure envelope instead of throwing keeps the agent able to explain what happened in Shopify's own words rather than inventing a reason. Handling both market handles in one guard means the same agent surface exists on both storefront listings, with each market's currency and country code baked into what getMarketContext returns.

The cost is duplication. The kit contents and the ingredient highlights are curated constants sitting in the layout — a second copy of what the product page already says, which someone has to keep in step with the first. And this is deliberately scoped to two product handles: it is one product's experiment, not a storefront-wide layer.

Why Not an App

There is no app to buy here. The App Store has no category for "expose this page's facts as callable tools", because the browser API those tools register against is still emerging. This is a bet, placed directly on the platform primitive rather than on a vendor's interpretation of it.

Even if the category existed, the shape of the problem argues against it. Tools have to be registered on the document the agent is already inside, which means the registration code has to run in the page, in the same tick as everything else — not injected from a third-party script after paint, when an agent may already have read the page. The values the tools return have to be the same values the page was rendered with, which in a Shopify theme means Liquid. An app sitting between the two would have to reproduce that render context from an API, and would be describing a page it did not render.

Implementation Notes

  • Feature detection is ('modelContext' in navigator) && !!navigator.modelContext?.registerTool. The optional-chained method check matters, because the property can plausibly exist before the method does. On a browser without it, the block logs once and returns.
  • The Liquid gate tests request.page_type == 'product' alongside the two kit handles, so on every other product page — and every other page type — the block is not rendered into the document at all.
  • addBestSellersKitToCart posts JSON with Content-Type: application/json to /cart/add.js rather than a FormData body, and takes its variant id from product.selected_or_first_available_variant.id in Liquid, never from the tool's own input object.
  • getMarketContext returns localization.country.iso_code and shop.currency exactly as the page was rendered, so an agent reasoning about price is reasoning in the market the shopper is actually browsing.
  • recommendationFit carries a notIdealFor array beside bestFor — customers wanting full-size products, customers seeking a single targeted treatment — so an agent has explicit grounds to talk a shopper out of the purchase rather than only toward it.
  • window.__webMcpRegistered guards the whole registration pass, and every registerTool call goes through a registerToolSafe() wrapper that catches individually, so a re-run registers nothing twice and one rejected tool cannot take the other seven down with it.
  • Quantity arriving from the agent is coerced and floored with Math.max(1, Number(input.quantity || 1)), so a missing, negative or non-numeric value becomes one unit instead of an error.

Edge Cases

  • No WebMCP: nothing registers, nothing throws, no node is added or changed. The page behaves for every other browser exactly as it did before this shipped.
  • Script re-execution — theme editor, section re-render — hits the window.__webMcpRegistered guard and returns before touching the tool registry a second time.
  • One tool failing to register does not block the rest. Each registration is caught on its own and the remaining tools still come up.
  • A cart failure resolves rather than rejects: the tool returns { success: false, message } built from Shopify's own description text, so the agent can explain the refusal in the shopper's terms.
  • The read tools answer from the object Liquid assembled when the page was rendered. They describe this product in this market, and nothing about an order in flight.

Platform Primitives Used

  • cart-ajax-api — the single write tool POSTs to /cart/add.js and hands Shopify's own response back to the agent.
  • shopify-marketslocalization.country.iso_code and shop.currency are read at render, so the tools speak the market's currency.
  • liquid-schema-settings — the tool definitions and their data are composed by Liquid in the theme, not fetched from an endpoint.

Where It Runs

On the product detail page, in the theme layout rather than a section — because the block has to exist before any section renders and does not need theme-editor configuration. It is gated to the two product handles that list the same trial kit in two markets, so the agent surface exists on both and nowhere else on the storefront.

What This Demonstrates

Structured data for AI answers and rich results — the same instinct as the storefront's JSON-LD work, taken further: not describing the page for a crawler, but handing a running agent a typed set of things it may ask and one thing it may do.

It is also the corpus's only agentic-commerce evidence: one build, one product page.

How We Know

One theme file, read directly from the live storefront, plus one client planning record that places this trial-kit surface in scope. We will be precise about that link: the planning record establishes the surface, not the feature. Nothing in the client documents mentions agent tooling at all. The eight tool names, the feature-detection expression, the registration guard and the cart call are documented from the code itself, which is why the confidence here is strong on what was built and silent on who first asked for it.

Related Builds

The Buy-vs-Build Question

Machine-readable product facts are one of the few areas where the buy option is genuinely thin: SEO apps emit a generic entity graph, and nothing on the market registers agent tools. Our reasoning about when hand-authored structured data is worth its maintenance cost — and when an app's output is good enough — is set out in structured data and schema markup: buy or build?.

Ready to Make Your Product Pages Answerable?

If shoppers are starting to arrive through an assistant rather than a search box, the question is what your product pages can hand that assistant — and what you would let it do. Contact us today to talk through what yours would need to expose.

More builds