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.

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.
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
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
- Inventory funnels by template: product, cart, checkout steps, account, search.
- For each, draft one tool name, a two‑sentence description, a JSON Schema, and a 3–10 line execute.
- Add data-test attributes to the UI where needed. Prefer stable names.
- Add explicit confirmation to sensitive steps.
- 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.

