All posts
Best practicesEngineeringEcosystem

WebMCP forms: model your forms as agent‑callable tools

“WebMCP forms” aren’t an API. They’re your site’s forms modeled as WebMCP tools. Learn the patterns, JSON Schemas, guardrails, and code to make forms agent‑ready.

Agent LayerOctober 20, 202612 min read

Opening “WebMCP forms” is a common search, but there isn’t a WebMCP forms API. The standard exposes tools in the page; you model your forms as those tools. By the end, you’ll know how to wrap any form—search, sign‑up, cart, checkout—into well‑described, JSON‑validated WebMCP tools that agents can discover and execute reliably.

If you want a quick refresher on the standard itself, see how WebMCP differs from MCP in What is the Difference Between MCP and WebMCP? in the post What is the Difference Between MCP and WebMCP?. This post stays hands‑on: naming, input schemas, DOM bindings, guardrails, and what actually works in browsers and agents today.

TL;DR

  • There is no “WebMCP forms” API. You expose forms as WebMCP tools with clear descriptions, JSON Schemas, and small JSON results.
  • Register tools with feature detection: document.modelContext first, then navigator.modelContext for older implementations.
  • Map parameters to your UI: set inputs, click submit, wait for state, then return a concise result. Add in‑page user confirmation for sensitive actions.
  • Cover your primary funnels: search, add_to_cart, update_quantity, checkout_contact, checkout_address, apply_coupon, subscribe_newsletter, start_return.
  • Today’s support is real but uneven: ChatGPT desktop and several frameworks call tools; Chrome and Edge are in origin trials. Track status on the public ecosystem tracker.

What “WebMCP forms” actually means

WebMCP’s only surface in the page is tool registration. A tool is a named, JSON‑schema‑typed capability with an execute function that runs in the user’s tab and session. There’s no special form primitive. When people say “WebMCP forms”, they mean “forms exposed as tools that agents call instead of scraping the DOM.”

The core pattern:

  • One tool per meaningful action, not per click. Example: add_to_cart, not click_add_button.
  • Describe preconditions and side effects in the tool description. Agents choose from descriptions.
  • Validate with JSON Schema. Reject early with clear errors.
  • Execute by driving your existing UI or app state. Don’t pass secrets through tool params.
  • Return a small JSON result that states what happened (not raw HTML).

If you’re new to enabling the standard itself, start with How to Enable WebMCP on Your Website: A Practical Guide and the broader walkthrough in WebMCP Tutorial: Setup, Tools, and Deployment.

The minimum viable “form as a tool”

This example exposes a newsletter sign‑up form. It validates the email, sets the input value, clicks submit, and returns a short result.

// Feature-detect WebMCP where agents look today
const mc = (document as any).modelContext ?? (navigator as any).modelContext;

if (mc && typeof mc.registerTool === 'function') {
  mc.registerTool({
    name: "subscribe_newsletter",
    description: "Subscribe the current user to the email newsletter. Use when the user asks to get updates by email. Preconditions: email field is visible. Side effects: submits the form and shows a confirmation.",
    inputSchema: {
      type: "object",
      properties: {
        email: { type: "string", format: "email", description: "User email" },
        consent: { type: "boolean", description: "User consents to receive emails" }
      },
      required: ["email", "consent"],
      additionalProperties: false
    },
    execute: async ({ email, consent }) => {
      if (!consent) {
        return { ok: false, error: "Consent required" };
      }
      const input = document.querySelector<HTMLInputElement>("input[type=email][name=email]");
      const form = input?.closest("form");
      if (!input || !form) return { ok: false, error: "Form not found" };

      input.value = email;
      input.dispatchEvent(new Event("input", { bubbles: true }));
      form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));

      // Wait for a confirmation banner to appear (simple polling)
      const ok = await new Promise<boolean>((resolve) => {
        const start = Date.now();
        const iv = setInterval(() => {
          if (document.querySelector('[data-test="newsletter-success"]')) {
            clearInterval(iv); resolve(true);
          } else if (Date.now() - start > 4000) {
            clearInterval(iv); resolve(false);
          }
        }, 150);
      });

      return ok ? { ok: true, message: "Subscribed" } : { ok: false, error: "No confirmation" };
    }
  });
}

Notes:

  • The description tells the agent when to use it, preconditions, and side effects.
  • The schema enforces inputs; the code returns small JSON, not DOM.
  • We avoid passing secrets. For any sensitive action (payments, account changes), add an explicit in‑page confirmation.

Modeling typical forms as tools

Your site likely has the same high‑value forms agents will try first. Expose them as stable, well‑named tools.

Recommended coverage

  • Search: site_search
  • Product: add_to_cart, update_quantity, select_variant
  • Promotion: apply_coupon
  • Checkout: checkout_contact, checkout_address, select_shipping, select_payment_method, place_order (with confirmation)
  • Account: sign_in, create_account, change_password (with confirmation), delete_account (with confirmation)
  • Support: start_return, open_ticket

Example: add to cart with variant selection

This tool validates the variant combination, drives your existing selectors, and confirms the cart.

const mc = (document as any).modelContext ?? (navigator as any).modelContext;
if (mc?.registerTool) {
  mc.registerTool({
    name: "add_to_cart",
    description: "Add the displayed product to cart. Use only on a product page. Side effects: updates cart count.",
    inputSchema: {
      type: "object",
      properties: {
        quantity: { type: "integer", minimum: 1, default: 1 },
        variant: {
          type: "object",
          properties: {
            size: { type: "string" },
            color: { type: "string" }
          },
          additionalProperties: false
        }
      },
      required: [],
      additionalProperties: false
    },
    execute: async ({ quantity = 1, variant = {} }) => {
      // Set variant selectors if present
      if ((variant as any).size) {
        const sel = document.querySelector<HTMLSelectElement>("select[name=size]");
        if (sel) { sel.value = (variant as any).size; sel.dispatchEvent(new Event("change", { bubbles: true })); }
      }
      if ((variant as any).color) {
        const sel = document.querySelector<HTMLSelectElement>("select[name=color]");
        if (sel) { sel.value = (variant as any).color; sel.dispatchEvent(new Event("change", { bubbles: true })); }
      }

      // Quantity
      const qty = document.querySelector<HTMLInputElement>("input[name=quantity]");
      if (qty) { qty.value = String(quantity); qty.dispatchEvent(new Event("input", { bubbles: true })); }

      // Submit
      const btn = document.querySelector<HTMLButtonElement>("button[data-test=add-to-cart]");
      if (!btn) return { ok: false, error: "Add button not found" };
      btn.click();

      // Wait for cart count to increase or mini-cart open
      const ok = await new Promise<boolean>((resolve) => {
        const start = Date.now();
        const iv = setInterval(() => {
          const count = Number(document.querySelector("[data-test=cart-count]")?.textContent || 0);
          if (count >= quantity) { clearInterval(iv); resolve(true); }
          else if (Date.now() - start > 5000) { clearInterval(iv); resolve(false); }
        }, 150);
      });

      return ok ? { ok: true, items_added: quantity } : { ok: false, error: "Cart not updated" };
    }
  });
}

Example: checkout address with explicit confirmation

Payments and account changes need a human confirmation in the page. The tool should require the page to show and receive that confirmation before proceeding.

const mc = (document as any).modelContext ?? (navigator as any).modelContext;
if (mc?.registerTool) {
  mc.registerTool({
    name: "checkout_address",
    description: "Fill checkout shipping address. Requires on-page confirmation. Use after contact step.",
    inputSchema: {
      type: "object",
      properties: {
        full_name: { type: "string" },
        line1: { type: "string" },
        city: { type: "string" },
        postal_code: { type: "string" },
        country: { type: "string" },
        confirm: { type: "boolean", description: "User confirmed on page" }
      },
      required: ["full_name", "line1", "city", "postal_code", "country", "confirm"],
      additionalProperties: false
    },
    execute: async (p) => {
      if (!p.confirm) return { ok: false, error: "Confirmation required" };
      const map: Record<string, string> = {
        full_name: "input[name=shipping_full_name]",
        line1: "input[name=shipping_address1]",
        city: "input[name=shipping_city]",
        postal_code: "input[name=shipping_postal]",
        country: "select[name=shipping_country]"
      };
      for (const [k, sel] of Object.entries(map)) {
        const el = document.querySelector<HTMLInputElement | HTMLSelectElement>(sel);
        if (!el) return { ok: false, error: `Field not found: ${k}` };
        (el as any).value = (p as any)[k];
        el.dispatchEvent(new Event("input", { bubbles: true }));
        el.dispatchEvent(new Event("change", { bubbles: true }));
      }
      // Proceed
      document.querySelector<HTMLButtonElement>("button[data-test=continue-to-shipping]")?.click();
      return { ok: true, step: "address_submitted" };
    }
  });
}

Security guardrail: The tool refuses to proceed unless confirm is true, which should map to a visible confirm control in your UI (e.g., a checkbox the user must click). Do not accept or handle payment tokens, API keys, or stored card data directly through tool parameters. For more context on safe patterns, read Preventing Prompt Injection in Agentic Web Interfaces.

How agents discover and use your tools

Agents that support WebMCP enumerate tools from the page and choose based on descriptions. They pass JSON matching your schema, your execute runs in‑tab with the user’s cookies/session, and you return a short result.

The diagram shows the flow from agent intent to form submission via a WebMCP tool.

Agent selects a tool and the page executes the mapped form

If you want a practical end‑to‑end run with a real agent, see A practical WebMCP demo: register tools and run them with a real agent.

Writing strong tool descriptions for forms

Agents don’t see your DOM. They pick from text. Make descriptions do the routing work.

  • Name in snake_case and action‑oriented: add_to_cart, apply_coupon, start_return.
  • State when to use it: “Use on product pages where a variant is selected.”
  • State preconditions: “Requires items in cart.”
  • State side effects: “Updates cart and opens mini‑cart.”
  • Avoid UI trivia: don’t mention CSS classes unless it’s a precondition.

Example description for apply_coupon “Apply a discount code to the active cart. Use only when the user provides a code and the cart is not empty. Preconditions: cart page or drawer is open. Side effects: recalculates totals; displays success or error.”

JSON Schema patterns for forms

A few practical patterns help agents succeed:

  • Required vs optional fields: mark only the fields truly required to submit. Use minimum/maximum for numeric inputs.
  • Enums for known options: sizes, shipping methods, countries.
  • Nested objects for grouped inputs: address, variant.
  • Reject extra fields with additionalProperties: false so agents don’t over‑specify.
  • Provide helpful formats: email, uri.

Example: schema for a return request

const returnSchema = {
  type: "object",
  properties: {
    order_id: { type: "string" },
    items: {
      type: "array",
      minItems: 1,
      items: {
        type: "object",
        properties: {
          sku: { type: "string" },
          quantity: { type: "integer", minimum: 1 }
        },
        required: ["sku", "quantity"],
        additionalProperties: false
      }
    },
    reason: { type: "string", enum: ["damaged", "wrong_size", "changed_mind", "other"] }
  },
  required: ["order_id", "items"],
  additionalProperties: false
};

Guardrails for sensitive forms

Some forms can cost money or change accounts. Build explicit confirmation and narrow scopes.

  • Add a confirm boolean in the schema and require a visible user acknowledgement in the page. Reject when not set.
  • Split risky actions: select_payment_method separate from place_order, with place_order requiring confirm.
  • Never pass secrets in params: card numbers, API keys, tokens.
  • Return minimal results: order_number, total, currency. Never echo PII back out.

Sequence of a sensitive action with guardrail Sensitive action requires user confirmation before execute proceeds

For a reality‑check comparison between in‑page WebMCP and backend MCP, read WebMCP vs Playwright MCP: What Actually Exists and What Works.

What works today, honestly

WebMCP is early, but you can ship value now in specific environments. Based on verified status:

  • Agents: ChatGPT desktop app and ChatGPT Work call page tools today. Brave Leo and Codex need flags. Others have no public signal yet. See the ecosystem tracker for details.
  • Browsers: Chrome and Edge have origin trials; Brave has it behind a flag; Firefox and Safari don’t ship it today. Lightpanda exposes an older address.
  • Frameworks: Browser Use, Stagehand, and Vercel agent-browser work today; Puppeteer and Cloudflare Browser Run need flags; Playwright declined for now.

If your customers use ChatGPT desktop or you run agents via the working frameworks, adding tools pays off immediately. For broader support, keep an eye on the public WebMCP support tracker.

Testing your forms as tools

Don’t ship blind. Test the exact user intents you expect.

  • Local manual tests: open a page, call execute from the console with sample inputs to verify DOM bindings and results.
  • Agent tests: if you use a framework that supports WebMCP, run scripted flows against your staging site.
  • Health checks and drift: UI changes break selectors. Document fragile bindings in one place and prefer data‑test attributes.

If you’d like a structured path, try the step‑by‑step challenges in WebMCP Challenge: Test and Fix Your Site’s Agent Readiness and the practical checklist in WebMCP form coverage: a practical developer checklist.

Mapping tools to your app architecture

How you implement execute depends on your stack.

  • Traditional MPA: set input values, fire submit on the form, wait for server response indicators (flash message, URL change).
  • SPA/SSR: update controlled inputs and dispatch input/change to trigger state; call the same actions your UI uses (button clicks, component events). Do not bypass your app’s validations.
  • Server actions present? Keep them behind the UI. The agent runs in‑tab; no backend keys exposed.

A small table of options and trade‑offs

Approach How it works Pros Cons
Drive DOM inputs Set values, dispatch events, click buttons Matches real user flow; no new API Fragile if selectors change; add data-test hooks
Call app functions Invoke exported UI actions (if exposed intentionally) Stable; less DOM coupling Requires careful exposure and versioning
Hybrid DOM for fields, small app hook for submit Balances stability and speed Two surfaces to maintain

What breaks in production

  • Selector drift: A class or attribute rename breaks your execute. Mitigate with data-test attributes, short wait loops, and consistent result shapes. If you use Agent Layer, health checks detect drift and suggest new selectors.
  • Vague descriptions: Agents pick the wrong tool if the description doesn’t state preconditions and effects. Tighten the text.
  • Over‑large results: Returning HTML or full cart JSON can exceed model/tool limits. Return concise summaries.
  • Missing guardrails: Accidentally executing place_order when the user didn’t mean it. Require confirm and show an in‑page acknowledgement.
  • Form parity: Tool schema doesn’t match the real form (e.g., missing country). Keep schemas aligned with your templates.
  • Ecosystem mismatch: Testing only in one agent. Check current support and plan fallbacks for users who don’t have agent tooling.

Putting it together: a minimal coverage plan

  1. Inventory funnels by template: product, cart, checkout steps, account, search.
  2. For each, draft one tool name, a two‑sentence description, a JSON Schema, and a 3–10 line execute.
  3. Add data-test attributes to the UI where needed. Prefer stable names.
  4. Add explicit confirmation to sensitive steps.
  5. Test locally and with a working agent or framework. Log small JSON results and fix drift quickly.

If you prefer an end‑to‑end tutorial that starts from zero, see WebMCP Tutorial: Setup, Tools, and Deployment.

Try it

Run the free agent‑readiness audit. It flags missing WebMCP tools, unlabeled fields, invalid JSON‑LD, and more, and proposes page‑by‑page tools. Then check current browser and agent support in the public ecosystem tracker. If you want less maintenance, Agent Layer keeps tools versioned, detects drift, adds guardrails, and shows which agents actually called your tools.

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.