All posts
EngineeringBest practicesEcosystem

How to Enable WebMCP on Your Website: A Practical Guide

A step‑by‑step guide on how to enable WebMCP, add your first tools, and debug why an agent isn’t calling them—plus what works in the ecosystem today.

Agent LayerOctober 2, 202613 min read

You searched for how to enable WebMCP. The short answer: add feature-detected tooling on your pages, register a few high‑value actions with clear JSON Schemas, and test in an agent that actually calls page tools. By the end, you’ll have working code, a basic tool set, and a checklist for debugging when nothing fires.

WebMCP is early but usable. ChatGPT desktop and Work call in‑page tools today; Chrome and Edge have origin trials. If you need a primer on the standard itself, see the backgrounder on WebMCP explained.

TL;DR

  • Feature-detect both addresses, then register tools: const mc = document.modelContext ?? navigator.modelContext; if (mc) mc.registerTool({ ... }).
  • Start with 3 tools per template: product info, add to cart, and checkout or apply coupon. Use precise JSON Schemas and plain-English descriptions.
  • Test in an agent that supports page tools (ChatGPT desktop/Work). Check the ecosystem tracker for current support.
  • Guard sensitive actions with explicit user confirmation in the page (e.g., window.confirm). Do not pass secrets via tool params.
  • If you don’t want to wire selectors by hand or chase drift, Agent Layer publishes tools as configs, adds guardrails, analytics, tests, and health checks.

What “enable WebMCP” actually means

WebMCP lets your page declare machine‑readable tools that an in‑browser agent can invoke instead of scraping or clicking. You:

  • Detect whether the page exposes a model context.
  • Register tools with name, description, JSON Schema, and an async execute function that runs in the user’s session.
  • Keep results small and JSON‑serialisable (e.g., { status: "ok", cart_count: 3 }).

You do not need a new backend or API keys for the agent. Tools run in‑page with the user’s existing auth.

If you’re choosing between MCP and WebMCP, they’re different layers: MCP runs server‑side over stdio/HTTP; WebMCP runs in a browser tab. See how WebMCP differs from MCP.

Minimal setup: one page, one tool

The smallest working example below exposes a “get_product” tool on a product page. It feature‑detects both the current and older addresses (document.modelContext and navigator.modelContext).

<!-- Place near the end of <body> so the DOM exists -->
<script type="module">
  // Feature-detect WebMCP
  const mc = /** @type {any} */ (document).modelContext ?? /** @type {any} */ (navigator).modelContext;

  // Optional: surface state if not present
  if (!mc) {
    console.debug("WebMCP not available in this client. Page still works for humans.");
  } else {
    // Register a read-only tool with clear preconditions and side effects
    mc.registerTool({
      name: "get_product",
      description: "Use on a product page to read the visible product's id, name, price, and availability. Read-only; no side effects.",
      inputSchema: {
        type: "object",
        properties: {
          // Allow agents to request specific fields; optional
          fields: {
            type: "array",
            items: { type: "string", enum: ["id", "name", "price", "currency", "in_stock"] },
          }
        },
        additionalProperties: false
      },
      execute: async (params) => {
        // Defensive DOM lookups; tolerate small layout changes
        const el = document.querySelector('[data-product]');
        if (!el) return { status: "error", error: "not_on_product_page" };

        const id = el.getAttribute('data-product-id');
        const name = el.querySelector('[itemprop="name"], .js-product-name')?.textContent?.trim() ?? null;
        const priceText = el.querySelector('[data-price], [itemprop="price"]')?.getAttribute('content')
          ?? el.querySelector('[data-price], [itemprop="price"]')?.textContent?.replace(/[^0-9.]/g, '');
        const currency = el.querySelector('[itemprop="priceCurrency"]')?.getAttribute('content') ?? 'USD';
        const inStock = !el.querySelector('.sold-out, [data-oos="true"]');

        const payload = { id, name, price: priceText ? Number(priceText) : null, currency, in_stock: !!inStock };
        // If specific fields were requested, filter
        const { fields } = params ?? {};
        const result = Array.isArray(fields) && fields.length ? Object.fromEntries(Object.entries(payload).filter(([k]) => fields.includes(k))) : payload;
        return { status: "ok", product: result };
      }
    });
  }
</script>

Key points:

  • Good descriptions teach agents when to call the tool and warn about side effects.
  • JSON Schemas should be strict (additionalProperties: false) to reduce agent error.
  • Handle “wrong page” gracefully with a small structured error string.

Add write tools with guardrails

When you mutate state (cart, account, payment), ask the user to confirm in‑page. A simple pattern uses window.confirm. Avoid piping secrets through parameters.

<script type="module">
  const mc = /** @type {any} */ (document).modelContext ?? /** @type {any} */ (navigator).modelContext;
  if (!mc) return;

  mc.registerTool({
    name: "add_to_cart",
    description: "Use on a product page to add the current product to the cart. Prompts the user to confirm. Side effect: cart count increases.",
    inputSchema: {
      type: "object",
      properties: {
        quantity: { type: "integer", minimum: 1, default: 1 },
        variant_id: { type: "string" }
      },
      required: ["quantity"],
      additionalProperties: false
    },
    execute: async ({ quantity = 1, variant_id } = {}) => {
      const el = document.querySelector('[data-product]');
      if (!el) return { status: "error", error: "not_on_product_page" };

      // User confirmation guardrail
      const ok = window.confirm(`Add ${quantity} to cart?`);
      if (!ok) return { status: "cancelled" };

      // Reuse your site’s existing add-to-cart JS or form submit
      const form = el.querySelector('form[action*="/cart"]');
      if (form) {
        const fd = new FormData(form);
        if (variant_id) fd.set('variant_id', variant_id);
        fd.set('quantity', String(quantity));
        await fetch(form.action, { method: form.method || 'POST', body: fd, credentials: 'include' });
      } else {
        // Fallback: click existing button if safe
        el.querySelector('[data-action="add-to-cart"], button[name="add"]')?.dispatchEvent(new Event('click', { bubbles: true }));
      }

      const cartCount = Number(document.querySelector('[data-cart-count]')?.textContent?.trim() ?? 0);
      return { status: "ok", cart_count: cartCount };
    }
  });
</script>

This is enough to power simple agent tasks today. For concrete patterns that work across verticals, see WebMCP example use cases.

You’ll get better agent behavior if each page template exposes the obvious intents.

  • Product page
    • get_product (read‑only)
    • add_to_cart (guarded)
    • apply_coupon (guarded)
  • Cart page
    • get_cart (read‑only summary)
    • update_quantity (guarded)
    • begin_checkout (guarded)
  • Search/listing page
    • search_products (read‑only, returns hits)
  • Account pages
    • get_orders (read‑only)
    • update_profile (guarded)

For a deeper list, see Essential WebMCP Tools for AI‑Ready Websites.

Architecture at a glance

The sequence below shows an agent discovering and calling your tool inside the user’s tab.

Agent discovers and calls a page tool via WebMCP, which runs in the tab

Known ecosystem support

WebMCP is progressing, but not universal. What works today, verified by hand:

  • Agents: ChatGPT desktop app and ChatGPT Work call page tools. Others vary or are behind flags. Some agents still only look at the older navigator.modelContext address.
  • Browsers: Chrome and Edge have origin trials; Brave has a Nightly flag; Firefox has no support; Safari is opposed. Some fringe browsers expose only the old address.
  • Frameworks: Vercel agent-browser, Stagehand, and Browser Use support it today; several others require flags.

Before you debug your code, confirm current status on the public WebMCP ecosystem tracker.

Testing checklist

  • Use an agent that supports page tools. Today, ChatGPT desktop/Work is the fastest route.
  • Verify feature detection runs and registers tools without throwing.
  • Open DevTools and log results from execute. Keep them small and human‑verifiable.
  • Provide clear descriptions so the agent chooses the right tool. Include preconditions like “Use on a product page”.
  • Prefer read‑only tools first to validate discovery. Then add guarded write tools.

A simple end‑to‑end flow

This snippet wires up three tools on a storefront: get_product, add_to_cart, and begin_checkout, with minimal coupling to your DOM.

<script type="module">
  const mc = /** @type {any} */ (document).modelContext ?? /** @type {any} */ (navigator).modelContext;
  if (!mc) { console.debug("No WebMCP context"); }
  else {
    const onProduct = () => !!document.querySelector('[data-product]');

    mc.registerTool({
      name: "get_product",
      description: "Use on a product page to read id, name, price, currency, and stock. Read-only.",
      inputSchema: { type: "object", properties: {}, additionalProperties: false },
      execute: async () => {
        if (!onProduct()) return { status: "error", error: "not_on_product_page" };
        const root = document.querySelector('[data-product]');
        return {
          status: "ok",
          product: {
            id: root?.getAttribute('data-product-id') ?? null,
            name: root?.querySelector('[itemprop="name"]')?.textContent?.trim() ?? null,
            price: Number(root?.querySelector('[itemprop="price"]')?.getAttribute('content') ?? 0),
            currency: root?.querySelector('[itemprop="priceCurrency"]')?.getAttribute('content') ?? 'USD',
            in_stock: !root?.querySelector('.sold-out')
          }
        };
      }
    });

    mc.registerTool({
      name: "add_to_cart",
      description: "Use on a product page to add the current product. Prompts the user to confirm.",
      inputSchema: { type: "object", properties: { quantity: { type: "integer", minimum: 1, default: 1 } }, additionalProperties: false },
      execute: async ({ quantity = 1 } = {}) => {
        if (!onProduct()) return { status: "error", error: "not_on_product_page" };
        if (!window.confirm(`Add ${quantity} to cart?`)) return { status: "cancelled" };
        document.querySelector('[data-action="add-to-cart"], button[name="add"]')?.dispatchEvent(new Event('click', { bubbles: true }));
        await new Promise(r => setTimeout(r, 500));
        return { status: "ok", cart_count: Number(document.querySelector('[data-cart-count]')?.textContent ?? 0) };
      }
    });

    mc.registerTool({
      name: "begin_checkout",
      description: "Use on the cart page to start checkout. Prompts the user first. Side effect: navigates to checkout.",
      inputSchema: { type: "object", properties: {}, additionalProperties: false },
      execute: async () => {
        const onCart = !!document.querySelector('[data-cart]');
        if (!onCart) return { status: "error", error: "not_on_cart_page" };
        if (!window.confirm('Proceed to checkout?')) return { status: "cancelled" };
        document.querySelector('[data-action="checkout"], a[href*="checkout"]')?.dispatchEvent(new Event('click', { bubbles: true }));
        return { status: "ok", navigated: true };
      }
    });
  }
</script>

Integration notes by platform

  • SPA frameworks (React/Vue/Svelte)
    • Register tools after route change when the relevant template is mounted. Re‑registering the same tool is safe if your implementation de‑duplicates; if not, gate with a boolean.
    • Use stable data attributes (data-product, data-cart) so small class/name changes don’t break selectors.
  • SSR/static sites
    • Inline the script at the bottom of each template or include a small module that guards by template detection.
  • WordPress
    • Add the script via wp_enqueue_script scoped to product/cart templates or your theme footer. See the dedicated walkthrough in WebMCP for WordPress.
  • Headless commerce
    • Keep tools in the storefront (not the admin). The agent runs in the shopper’s session, so no API keys are required.

Tool design that agents actually pick

Agents choose by description + schema. Help them succeed:

  • Name tools in snake_case and be literal: get_product, add_to_cart, apply_coupon.
  • In the description, state when to use, preconditions, and side effects.
  • Keep inputs minimal and typed. Avoid optional strings that change meaning.
  • Return small, structured results that confirm what happened.

A short example of a coupon tool with clear guardrails:

<script type="module">
  const mc = /** @type {any} */ (document).modelContext ?? /** @type {any} */ (navigator).modelContext;
  if (!mc) return;
  mc.registerTool({
    name: "apply_coupon",
    description: "Use on cart page to apply a coupon code. Prompts the user. Side effect: cart total may decrease.",
    inputSchema: { type: "object", properties: { code: { type: "string", minLength: 3 } }, required: ["code"], additionalProperties: false },
    execute: async ({ code }) => {
      const cart = document.querySelector('[data-cart]');
      if (!cart) return { status: "error", error: "not_on_cart_page" };
      if (!window.confirm(`Apply coupon ${code}?`)) return { status: "cancelled" };
      const input = cart.querySelector('input[name="coupon"], input[data-coupon]');
      const btn = cart.querySelector('[data-action="apply-coupon"], button[name="apply_coupon"]');
      if (!input || !btn) return { status: "error", error: "coupon_ui_missing" };
      input.value = code;
      btn.dispatchEvent(new Event('click', { bubbles: true }));
      await new Promise(r => setTimeout(r, 600));
      const total = cart.querySelector('[data-cart-total]')?.textContent?.trim() ?? null;
      return { status: "ok", total_text: total };
    }
  });
</script>

Troubleshooting: when your tool never fires

Work down this list before refactoring code.

  1. The agent doesn’t support WebMCP
  • Symptom: No discovery or calls; your console shows tools registered.
  • Fix: Test in ChatGPT desktop or Work. Cross‑check the ecosystem status. Some agents only check navigator.modelContext.
  1. Description doesn’t match the user intent
  • Symptom: The agent ignores your tool and scrapes or does nothing.
  • Fix: Make descriptions explicit: “Use on the cart page to … Side effect: …”. Keep inputs small and obvious.
  1. Schema too loose or too strict
  • Symptom: Agent sends unexpected parameters or refuses to call due to validation in their runtime.
  • Fix: Set additionalProperties: false. Prefer enums and integers over free‑text strings when you can.
  1. Tool throws due to DOM drift
  • Symptom: Works in staging, breaks after a deploy.
  • Fix: Use stable data attributes. Return structured errors like "coupon_ui_missing" instead of throwing.
  1. Sensitive actions auto‑execute
  • Symptom: Agent completes checkout or changes account data without user confirmation.
  • Fix: Guard with an in‑page confirm dialog and state it in the description. Never accept secrets via params.
  1. Results too big
  • Symptom: Agent call times out or truncates results.
  • Fix: Return concise JSON. For lists, include pagination params and return summaries.
  1. Wrong template
  • Symptom: Agent calls add_to_cart on a listing page.
  • Fix: Early‑exit with not_on_product_page. Mention preconditions in the description so the agent learns when to call it.

What breaks in production (and how to prevent it)

  • UI drift breaks selectors
    • Prevent: Stable data attributes; smoke tests; consider Agent Layer health checks that detect selector drift and suggest fixes.
  • Silent tool misuse
    • Prevent: Analytics per call. Agent Layer logs which agent called what, success/error, and duration.
  • Fragile rollouts
    • Prevent: Version configs and rollback. Agent Layer publishes tools per template as versioned configs.
  • Unknown agent coverage
    • Prevent: Test scenarios. In Agent Layer you write intents (“apply a coupon on cart”) and assert the tool used.
  • Missed guardrails on sensitive paths
    • Prevent: In‑page confirmations and Guardrails in Agent Layer for payments and account changes.

A simple enablement flow

The flow below shows a pragmatic rollout: start read‑only, then guarded writes, then productionize.

Rollout flow from read-only tools to guarded writes to production ops

Without Agent Layer vs with Agent Layer

You can do today without Agent Layer What Agent Layer adds when you care about ops
Feature-detect, register tools, write selectors, add confirm() for sensitive actions Detect templates, bind tools per template, guardrails for payments/account changes
Manual testing in one agent Test scenarios mapped to intents; see which agents call which tools
Console logs and guesswork Analytics per call: agent identity, success/error, duration
Fix drift by hand after deploys Health checks detect broken selectors and suggest replacements
Inline scripts, copy-paste between pages Versioned configs with rollback

If you’re just evaluating, hand‑rolled scripts are fine. If you expect UI churn or multiple teams shipping changes, consider letting Agent Layer keep the plumbing healthy.

Security notes

  • Do not pass secrets, API keys, payment tokens, or card data via tool params. Tools run in a shared page context.
  • For destructive or costly actions (checkout, cancellations, profile changes), require explicit user confirmation and state that in the description.
  • Keep results small and free of PII unless strictly necessary for the task.

Where WebMCP helps and where it doesn’t

  • Helps: repeatable flows where scraping is brittle (product add to cart, apply coupon, read current plan).
  • Doesn’t help: agents that ignore tools; locked-down pages behind CAPTCHAs; browser engines with no WebMCP support.
  • For a practical tour of outcomes you can expect right now, see WebMCP example: concrete uses that work today.

Common pitfalls

  • Only using document.modelContext or only navigator.modelContext. Use both.
  • Vague tool names like do_action. Use literal, narrow names.
  • Missing additionalProperties: false, causing agents to invent parameters.
  • Returning raw DOM or giant arrays. Return compact JSON.
  • Not stating preconditions in descriptions, leading to misuse.
  • No user confirmation for sensitive actions.
  • Assuming all agents or browsers support WebMCP. Always check the support tracker.

Try it

Start by running the free agent‑readiness audit on your site. It flags missing WebMCP tools, invalid JSON‑LD, unlabeled fields, CAPTCHAs, and more, and suggests tools per page. If you want to see where support stands or which agents to test first, check the ecosystem tracker. When you outgrow inline scripts, Agent Layer keeps tools working across UI changes with guardrails, analytics, tests, and versioned configs.

Get started

Don't Be Invisible to Tomorrow's Buyers.

Make your platform executable for every AI assistant your customers already use. No engineering, no lock-in, no breakage.