# Integrate — patterns per stack
> Prefer the live official guides when online:
> `npx -y modern-web-guidance@latest retrieve "webmcp,agentic-forms,agentic-javascript-tools"`.
> The patterns below follow Google's reference implementations
> (GoogleChromeLabs/webmcp-tools) and the W3C CG draft.
## Declarative — standard HTML forms
Applies to plain HTML, SSG-emitted, server-rendered, and framework-rendered
(uncontrolled) forms — anywhere a real `
```
Rules:
- The browser derives the JSON Schema from the controls — every control needs
`name`, a resolvable description (`toolparamdescription` → `label[for]` text →
`aria-description`), and correct HTML constraints. Radio groups: description on
the enclosing ``.
- `toolautosubmit` **only** on pure read forms (search/filter/availability).
Never on contact/checkout/settings/messaging forms.
- Fetch-submitted forms (`preventDefault()`) MUST route the result back to the
agent — the most common integration bug is a swallowed submit:
```js
form.addEventListener('submit', (e) => {
e.preventDefault();
const result = doSubmit(new FormData(e.target))
.then(() => 'Request received. Reply within one business day.');
if (e.agentInvoked) e.respondWith(result); // pass the PROMISE, not a value
});
```
- Optional UX (verbatim from Chrome docs): style agent activity with
`form:tool-form-active` / `:tool-submit-active` CSS pseudo-classes.
- Forms that navigate to a thank-you page: `executeTool` returns `null` on
navigation (expected). A JSON-LD `{"@type":"Message","text":"…"}` block on the
target page is best-effort garnish — the mechanism is still under spec debate;
never make behavior depend on it.
### Framework notes — React
- Vendor `templates/webmcp-jsx.d.ts` alongside the ambient types so strict TSX
accepts `toolname`/`tooldescription`/`toolparamdescription` (it augments the
React attribute interfaces; it is a MODULE file — keep it separate from
`webmcp.d.ts`).
- The typings are string-valued (boolean-attribute style): write
`toolautosubmit=""` — and only on pure read forms (ground rule 5).
- In `onSubmit`, the WebMCP fields live on the NATIVE event:
```tsx
const native = e.nativeEvent as SubmitEvent;
if (native.agentInvoked) native.respondWith?.(doSubmit(new FormData(e.currentTarget))
.then(() => 'Request received. Reply within one business day.'));
```
Pass the PROMISE of the result string, not an already-resolved value.
## Imperative — SPAs and dynamic apps
Use the vendored runtime (`runtime.md`). Tools live in a dedicated module per app
(e.g. `src/webmcp/tools.ts`), decoupled from components:
```ts
import { createToolScope, dispatchAndWait } from './webmcpify';
export const searchTicketsTool = {
name: 'search_tickets',
description: 'Searches tickets in the currently open project and shows results on screen.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search terms, exactly as the user phrased them.' },
},
required: ['query'],
},
annotations: { readOnlyHint: true, untrustedContentHint: true },
async execute(input: Record) {
const q = String(input.query ?? '').trim();
if (!q) return 'ERROR: `query` must be a non-empty string.';
return dispatchAndWait('webmcp:search_tickets', { query: q });
},
};
```
Key rules:
- **`execute()` wraps the existing UI code path** — dispatch the same event / call
the same store action / hit the same API the button does. Never a parallel
implementation.
- **Return only after the interface state is settled**: the component listener
awaits the real work, then fires the completion event with the outcome payload
(`{ ok, message | error }`) — full contract and component example in
`runtime.md`. A canned success before the work finishes is a false green.
- Return short strings; errors as `"ERROR: "` so the model can
self-correct. Cap outputs ~1.5k chars.
- Validate strictly in code, loosely in schema — and keep **parity with the
form's native HTML constraints**: when a tool wraps a form, probe the real
constraints on a detached clone instead of re-implementing them —
`const probe = emailInput.cloneNode() as HTMLInputElement; probe.value = value;`
then reject when `!probe.checkValidity()`. `execute()` must refuse exactly what
the form itself would refuse.
### Registration & lifecycle
- **Static registration is the default**: register app-wide tools once at bootstrap.
- **Per-view registration only** for tools meaningless outside their view — via
`createToolScope` in the view's mount/unmount (React `useEffect` cleanup, Vue
`onUnmounted`, Angular `DestroyRef`). Over-scoping makes the toolset flicker and
strands agents mid-plan.
- Registration failures roll back the scope and surface via `onError` — check the
console during integration; a silently missing toolset usually means a duplicate
name or invalid schema rejected the batch.
### Auth / roles (SaaS)
Never register a tool the current session couldn't use through the UI. On
login/logout/role change/tenant switch: dispose the scope and re-register the
correct set (`runtime.md` §Wiring). The server still re-checks everything (ground
rule 3) — role-scoped registration is UX hygiene, not security.
## Origin trial / flags note
WebMCP is a Chrome origin trial (149→, stable milestone still an estimate). For
production exposure the origin needs a token:
` ` or an `Origin-Trial` response
header — registered at the Chrome Origin Trials console. For local work,
`chrome://flags/#enable-webmcp-testing`. Chrome **silently ignores** expired
tokens, so nothing may depend on WebMCP being present (ground rule 4). Add a short
note about this to the target repo's README as part of setup, and record the
touched file path in `pipeline.setup.originTrialNoted` (e.g. `["README.md"]`).