> For the complete documentation index, see [llms.txt](https://docs.maker.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.maker.co/maker-greatstore/developer-reference/webmcp.md).

# WebMCP

Code examples for publishing page tools the assistant can call: add to cart, switch variants, filter listings, and more.

WebMCP lets your page publish **tools**: small JavaScript functions the assistant can call to act on the page the shopper is looking at. This page is the code companion to [WebMCP](/maker-greatstore/connectors/webmcp.md), which covers turning page tools on and choosing which ones the assistant may use.

{% hint style="info" %}
WebMCP is a web standard. Chrome's documentation is the reference for the API itself:

* [WebMCP overview](https://developer.chrome.com/docs/ai/webmcp)
* [The imperative API](https://developer.chrome.com/docs/ai/webmcp/imperative-api)
  {% endhint %}

## On this page

* [Registering a tool](#registering-a-tool)
* [Examples](#examples)
* [Which group a tool lands in](#which-group-a-tool-lands-in)
* [Writing good tools](#writing-good-tools)

## Registering a tool

[Install GreatStore](/maker-greatstore/get-started/installation.md) first. Once it has loaded, `document.modelContext` is available on the page, including in browsers that don't support WebMCP on their own. Register tools after that point, by listening for the `greatstore:ready` event. Tools registered before GreatStore is ready aren't available to the assistant.

{% hint style="warning" %}
**Add your tools script to the `<head>` of your site, on every page.** This gives the best performance and an uninterrupted experience: your tools are ready as soon as the assistant is, so it can use them from a shopper's first message on every page they visit. It doesn't matter whether the script comes before or after the GreatStore script tag.

A script placed further down the page, or added after the page has loaded (for example through a tag manager), can miss the moment GreatStore becomes ready. Its tools are then never available to the assistant on that page.
{% endhint %}

```html
<head>
  <!-- ... -->
  <script>
    window.addEventListener('greatstore:ready', () => {
      document.modelContext.registerTool({
        name: 'get_cart',
        description:
          "Read the shopper's cart on this site: items, quantities and totals. " +
          'Use before answering any question about the cart.',
        inputSchema: { type: 'object', properties: {} },
        async execute() {
          const cart = await (await fetch('/cart.js')).json();
          return { content: [{ type: 'text', text: JSON.stringify(cart) }] };
        },
      });
    }, { once: true });
  </script>
</head>
```

Every tool has four parts:

| Field         | What it's for                                                                                         |
| ------------- | ----------------------------------------------------------------------------------------------------- |
| `name`        | A unique name on the page, such as `add_to_cart`. Registering the same name twice throws.             |
| `description` | How the assistant decides **when** to call the tool. Write it for the assistant, and be specific.     |
| `inputSchema` | A JSON Schema describing the arguments. Leave it out for a tool that takes none.                      |
| `execute`     | The function that does the work. Return `{ content: [{ type: 'text', text }] }`, or a Promise of one. |

## Examples

Each example below is complete: it registers its tool inside a `greatstore:ready` listener, so you can drop it into your tools script as it stands. They use a Shopify-style cart endpoint — swap in your own platform's calls; the shape of each tool stays the same.

{% tabs %}
{% tab title="Add to cart" %}

```javascript
window.addEventListener('greatstore:ready', () => {
  const reply = (value) => ({
    content: [{ type: 'text', text: JSON.stringify(value) }],
  });

  document.modelContext.registerTool({
    name: 'add_to_cart',
    description:
      "Add a product variant to the shopper's cart. Use when they ask to add " +
      'or buy something. Confirm the size or colour first if it is unclear.',
    inputSchema: {
      type: 'object',
      properties: {
        variantId: { type: 'string' },
        quantity: { type: 'integer', minimum: 1, maximum: 10 },
      },
      required: ['variantId'],
    },
    async execute({ variantId, quantity = 1 }) {
      const res = await fetch('/cart/add.js', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id: variantId, quantity }),
      });
      if (!res.ok) throw new Error(`Could not add to cart (${res.status})`);

      // Let your own cart drawer or badge refresh.
      document.dispatchEvent(new CustomEvent('cart:refresh'));
      return reply(await (await fetch('/cart.js')).json());
    },
  });
}, { once: true });
```

{% endtab %}

{% tab title="Update the cart" %}

```javascript
window.addEventListener('greatstore:ready', () => {
  const reply = (value) => ({
    content: [{ type: 'text', text: JSON.stringify(value) }],
  });

  document.modelContext.registerTool({
    name: 'update_cart_quantity',
    description:
      'Change the quantity of an item already in the cart. Set quantity to 0 ' +
      'to remove it.',
    inputSchema: {
      type: 'object',
      properties: {
        lineKey: { type: 'string', description: 'The cart line to change.' },
        quantity: { type: 'integer', minimum: 0, maximum: 10 },
      },
      required: ['lineKey', 'quantity'],
    },
    async execute({ lineKey, quantity }) {
      const res = await fetch('/cart/change.js', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id: lineKey, quantity }),
      });
      if (!res.ok) throw new Error(`Could not update the cart (${res.status})`);
      document.dispatchEvent(new CustomEvent('cart:refresh'));
      return reply(await res.json());
    },
  });
}, { once: true });
```

{% endtab %}

{% tab title="Switch a variant" %}

```javascript
window.addEventListener('greatstore:ready', () => {
  const reply = (value) => ({
    content: [{ type: 'text', text: JSON.stringify(value) }],
  });

  document.modelContext.registerTool({
    name: 'select_variant',
    description:
      'Switch the product on this page to a given size and colour, updating ' +
      'the image and price. Use when the shopper asks to see another option.',
    inputSchema: {
      type: 'object',
      properties: {
        size: { type: 'string', enum: ['S', 'M', 'L', 'XL'] },
        colour: { type: 'string' },
      },
    },
    execute({ size, colour }) {
      const variant = window.product.variants.find(
        (v) => (!size || v.size === size) && (!colour || v.colour === colour),
      );
      if (!variant) throw new Error('That combination is not available.');

      selectVariant(variant.id); // your theme's own variant switcher
      return reply({ id: variant.id, price: variant.price, inStock: variant.available });
    },
  });
}, { once: true });
```

{% endtab %}

{% tab title="Filter a listing" %}

```javascript
window.addEventListener('greatstore:ready', () => {
  const reply = (value) => ({
    content: [{ type: 'text', text: JSON.stringify(value) }],
  });

  document.modelContext.registerTool({
    name: 'apply_filters',
    description:
      'Filter the products on this category page, for example by maximum ' +
      'price or availability.',
    inputSchema: {
      type: 'object',
      properties: {
        maxPrice: { type: 'number', minimum: 0 },
        inStockOnly: { type: 'boolean' },
        sort: { type: 'string', enum: ['price-asc', 'price-desc', 'newest'] },
      },
    },
    execute({ maxPrice, inStockOnly, sort }) {
      const url = new URL(location.href);
      if (maxPrice !== undefined) url.searchParams.set('price_max', maxPrice);
      if (inStockOnly) url.searchParams.set('available', '1');
      if (sort) url.searchParams.set('sort', sort);

      history.pushState({}, '', url);
      refreshProductGrid(); // your listing's own re-render
      return reply({ applied: { maxPrice, inStockOnly, sort } });
    },
  });
}, { once: true });
```

{% endtab %}

{% tab title="Navigate" %}

```javascript
window.addEventListener('greatstore:ready', () => {
  const reply = (value) => ({
    content: [{ type: 'text', text: JSON.stringify(value) }],
  });

  const sections = {
    reviews: '#reviews',
    size_guide: '#size-guide',
    shipping: '#shipping-info',
  };

  document.modelContext.registerTool({
    name: 'show_section',
    description:
      'Scroll the page to a section so the shopper can see it alongside your ' +
      'answer.',
    inputSchema: {
      type: 'object',
      properties: { section: { type: 'string', enum: Object.keys(sections) } },
      required: ['section'],
    },
    execute({ section }) {
      const el = document.querySelector(sections[section]);
      if (!el) throw new Error(`There is no ${section} section on this page.`);
      el.scrollIntoView({ behavior: 'smooth' });
      return reply({ shown: section });
    },
  });
}, { once: true });
```

{% endtab %}

{% tab title="Submit a form" %}

```javascript
window.addEventListener('greatstore:ready', () => {
  const reply = (value) => ({
    content: [{ type: 'text', text: JSON.stringify(value) }],
  });

  document.modelContext.registerTool({
    name: 'subscribe_restock_alert',
    description:
      'Sign the shopper up for a back-in-stock email for the product on this ' +
      'page. Ask for their email address first.',
    inputSchema: {
      type: 'object',
      properties: { email: { type: 'string', format: 'email' } },
      required: ['email'],
    },
    async execute({ email }) {
      const form = document.querySelector('#restock-form');
      form.elements.email.value = email;

      const res = await fetch(form.action, { method: 'POST', body: new FormData(form) });
      if (!res.ok) throw new Error('Sign-up failed. Please try again.');
      return reply({ subscribed: email });
    },
  });
}, { once: true });
```

{% endtab %}
{% endtabs %}

These are starting points, not a complete list. Anything a shopper can do by clicking around your page can be a tool: applying a promo code, starting checkout, booking an appointment, starting a return, opening their order history.

## Which group a tool lands in

The connectors screen sorts page tools into **Write**, **Read-only** and **Other**, and you allow or disallow each group; see [Controlling tool access](/maker-greatstore/connectors/tool-access.md). The group follows from the tool's **name**:

| Group         | Names containing a word like                                                                  | Example                                                |
| ------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| **Write**     | `add`, `update`, `remove`, `delete`, `set`, `submit`, `apply`, `checkout`, `buy`, `subscribe` | `add_to_cart`, `apply_filters`, `update_cart_quantity` |
| **Read-only** | `get`, `search`, `list`, `show`, `find`, `view`, `filter`, `compare`                          | `get_cart`, `show_section`                             |
| **Other**     | Anything else                                                                                 | `select_variant`                                       |

Words are separated by `_` or `-`, so write names in `snake_case` or `kebab-case`: `addToCart` is read as a single word and lands in **Other**. A name with words from both lists, like `apply_filters`, counts as **Write**.

Name tools for what they actually do. A tool that changes the cart but is named `cart_helper` lands in **Other**, so disallowing **Write** won't stop it.

## Writing good tools

* **Treat arguments as untrusted.** They are written by the AI, not by your code. Validate them before passing them to your own APIs, cap quantities in the schema, and never `eval` anything from them.
* **Throw when something goes wrong.** The assistant is told the tool failed, along with your error message, and can explain or try something else. Errors never break your page.
* **Return the new state after a change.** Sending back the updated cart after `add_to_cart` lets the assistant confirm exactly what happened.
* **Keep the page in sync.** Refresh your own cart badge, drawer or listing, so the shopper sees the change the assistant made.
* **Spend time on the description.** It is the only thing the assistant has to decide when a tool is the right one. If a tool is never used, improve its description first.

{% hint style="info" %}
Page tools pair well with page context. [JavaScript API](/maker-greatstore/developer-reference/javascript-api.md#passing-page-context) tells the assistant what the shopper is looking at; tools let it do something about it.
{% endhint %}

## What's next?

{% content-ref url="/pages/38nczSS63XmuzC86kFHo" %}
[WebMCP](/maker-greatstore/connectors/webmcp.md)
{% endcontent-ref %}

{% content-ref url="/pages/Mf90Eudy8wqnmesbEaFu" %}
[JavaScript API](/maker-greatstore/developer-reference/javascript-api.md)
{% endcontent-ref %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.maker.co/maker-greatstore/developer-reference/webmcp.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
