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.

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
executefunction 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.
Recommended starter tool set by template
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.
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.modelContextaddress. - 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_scriptscoped to product/cart templates or your theme footer. See the dedicated walkthrough in WebMCP for WordPress.
- Add the script via
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- Results too big
- Symptom: Agent call times out or truncates results.
- Fix: Return concise JSON. For lists, include pagination params and return summaries.
- Wrong template
- Symptom: Agent calls
add_to_carton 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.
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.modelContextor onlynavigator.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.

