# webmcpui — full documentation > Framework-agnostic, WebMCP-native web components: accessible form controls an agent can fill and interaction primitives an agent can drive, with Standard Schema validation and design tokens. Each element is a good HTML control first and, when a WebMCP host is present, exposes itself as a tool an agent can call — additive and feature-detected. Source: https://webmcpui.com --- # Introduction URL: https://webmcpui.com/docs Agent-aware web components for the WebMCP era — framework-agnostic form and interaction primitives with Standard Schema validation. # Introduction **webmcpui** is a framework-agnostic, WebMCP-native web component library. Every element is a proper, accessible HTML control first and, when you opt in, also registers an imperative [WebMCP](/docs/webmcp) tool an agent can call. Three families of primitives: - **Form controls** expose a *value* an agent can set — ``, ``, ``, ``, `` / ``. Shared behavior (form association, validation, WebMCP exposure, theming) lives in a `WmcpFormControl` base. - **Interaction primitives** expose an *action* an agent can trigger — ``, ``, ``, ``, ``, ``, `` — or, for ``, a *reading* an agent can perceive. They share a `WmcpAction` / `WmcpExposable` base. - **Presentational primitives** are accessible, themed controls with no agent surface — ``, ``, ``, ``, ``. Building in React or Vue? [`@webmcpui/react`](/docs/frameworks) and [`@webmcpui/vue`](/docs/frameworks) wrap these elements with typed, idiomatic components — same behavior, framework-native ergonomics. ## Why it exists The same control a person operates by hand, an agent should be able to operate by calling a tool — fill a field, click a button, open a dialog, switch a tab, or read a notification. webmcpui's elements are good, accessible controls first; agent-exposure is additive and opt-in. It's all feature-detected: with no agent present (the common case today), the WebMCP layer is a complete no-op, so your controls are always good HTML. ## One source of truth, two channels Vanilla custom elements built with [Lit](https://lit.dev), distributed two ways: - an **ESM package** for build tools (`@webmcpui/core`), and - a single-file **CDN bundle** for no-build environments (Webflow, WordPress, plain HTML). ## Next steps - [Installation](/docs/installation) — add it to your project, with or without a build step. - [Validation](/docs/validation) — bring any Standard Schema validator. - [WebMCP exposure](/docs/webmcp) — how elements become agent tools. - [Testing](/docs/testing) — exercise exposure with the fake agent. --- # Installation URL: https://webmcpui.com/docs/installation Add webmcpui to your project with a build tool, or drop a single script tag — no build step required. # Installation Two distribution channels from one source of truth. Use the ESM package with your bundler, or the single-file CDN bundle for no-build environments. ## With a build tool ```bash pnpm add @webmcpui/core @webmcpui/tokens ``` ```ts import { defineComponents } from '@webmcpui/core'; import '@webmcpui/tokens/css'; // theme tokens (CSS custom properties) defineComponents(); // registers and the other elements ``` ```html ``` Importing the package does **not** register the elements — you call `defineComponents()` so you control timing. (The CDN bundle below registers automatically.) > In an SSR framework, call `defineComponents()` on the client only — custom elements need the DOM. In Nuxt, for example, do it from a `.client.ts` plugin. ## No build? Drop a script tag For Webflow, WordPress, or hand-written HTML — one tag, every `` element auto-registers: ```html ``` ## Frameworks Because these are standard custom elements, they work anywhere HTML renders. The only framework-specific note is telling the framework's template compiler not to treat `` as its own components: - **Vue / Nuxt** — set `compilerOptions.isCustomElement = (tag) => tag.startsWith('wmcp-')`. - **React** (19+) — custom elements and their props/attributes work directly. - **Svelte / SolidJS / Angular** — supported with each framework's standard custom-element handling. --- # Validation URL: https://webmcpui.com/docs/validation Bring any Standard Schema validator — Zod, Valibot, ArkType — and wire it straight into form validation. # Validation Every control accepts a [Standard Schema](https://standardschema.dev) validator on its `schema` property. Bring Zod, Valibot, ArkType, or anything that implements the spec — there's no bespoke schema language to learn. ```ts import { z } from 'zod'; const input = document.querySelector('wmcp-input')!; input.schema = z.string().email('Enter a valid email'); ``` Validation runs on input and during native form validation. On failure the element: - sets `aria-invalid`, - renders the error message in a live region (announced to assistive tech), and - propagates the failure to the containing `
` via `ElementInternals`, so native form submission is blocked just like a built-in control. ## Required fields `required` is a real constraint, not just a visual marker — an empty required control fails validation and participates in native form validity. ```html ``` ## A note on tool schemas Standard Schema validates *values* but does not emit JSON Schema, so a control's WebMCP tool parameter schema is derived from the element (its `type`, or its enumerated options for `` / ``) rather than from the validator. Richer tool schemas are a future enhancement. --- # WebMCP exposure URL: https://webmcpui.com/docs/webmcp How a form control becomes an imperative WebMCP tool an agent can discover and call. # WebMCP exposure WebMCP is an imperative browser API — `document.modelContext.registerTool(...)` — that lets a page offer tools to an AI agent running in or alongside the browser. webmcpui controls can register themselves as tools. ## Opt in with `expose` ```html ``` On connect, the element registers an imperative WebMCP tool; on disconnect, it unregisters. It is **feature-detected** — a complete no-op when no agent/host is present (the common case today), so the control is always a good form control first. ## The generated tool For the example above, the registered tool is: | Field | Value | | --- | --- | | `name` | `fill_email` (`fill_` + the `name` attribute) | | `description` | `Set the value of the "Email" field.` | | `inputSchema` | `{ type: 'object', properties: { value: { type: 'string' } }, required: ['value'] }` | When the agent calls the tool, the element applies the value exactly as if a user typed it — updating state, running validation, announcing errors, and firing `input`/`change` events. ## Customizing the tool ```html ``` - `tool-name` overrides the generated `fill_`. - `tool-description` overrides the generated description. Controls with enumerated values (``, ``) generate an `enum`-typed schema so the agent knows the exact allowed values. > **Tool names are page-global.** Two `expose`d controls that resolve to the same tool name — say two fields that both become `fill_email` — collide, and the host rejects the duplicate, so the second control won't be agent-callable. webmcpui logs a console warning when it sees this; give one control a unique `name`, or override it with `tool-name`. ## Only on a secure context WebMCP is a secure-context feature, so `document.modelContext` exists only on **HTTPS** pages — it's `undefined` on plain HTTP (`localhost` counts as secure). webmcpui's feature detection reads that as "no host present" and quietly does nothing, so your controls stay perfectly good form controls. If you're testing exposure on an `http://` staging box and see no tools, that's why. ## Designed for a human in the loop WebMCP is explicitly a **human-in-the-loop** API — its own goals call out cooperative workflows where a person delegates to an agent while keeping visibility and control, and rule out fully autonomous, headless operation. There is, as yet, **no built-in confirmation or user-prompting primitive** in `registerTool` (it's an [open question](https://github.com/webmachinelearning/webmcp/issues/165)). So consent stays a UI decision you make: an agent can *set* a value or *open* a [dialog](/docs/elements/dialog), but the consequential step — submitting, confirming — remains a deliberate human action. That's the line webmcpui draws by default. ## Imperative and declarative The API webmcpui builds on is the **imperative** one (`registerTool`). A **declarative** companion is also being standardized: native attributes (`toolname`, `tooldescription`, `toolparamdescription`, `toolautosubmit`) that annotate a plain `` directly. The two describe the same kind of tool; webmcpui uses the imperative path under the hood because it's the lower-level primitive and the only one that covers interaction beyond forms — a [button](/docs/elements/button)'s click or a dialog's open aren't expressible as form annotations. ## Spec status As of mid-2026 WebMCP is early but moving: a **public origin trial in Chrome 149+**, with Gemini in Chrome as the first consumer. The surface still shifts month to month — it moved to `document.modelContext` (with `navigator.modelContext` deprecated in Chrome 150), and is `undefined` for almost everyone else. Everything here is additive and feature-detected, so adopting webmcpui costs nothing today and pays off as hosts ship. webmcpui detects both surfaces and prefers `document.modelContext`. To exercise exposure now, use the [fake agent](/docs/testing). --- # Testing URL: https://webmcpui.com/docs/testing Exercise WebMCP exposure end-to-end with the bundled fake agent. # Testing with the fake agent No mainstream agent calls WebMCP yet, so `@webmcpui/core/testing` ships a fake host that lets you exercise exposure end to end. It installs a stub `navigator.modelContext`, records the tools your elements register, and lets you invoke them exactly as an agent would. ```ts import { installFakeAgent } from '@webmcpui/core/testing'; const agent = installFakeAgent(); // ... connect a to the DOM ... const result = await agent.call('fill_email', { value: 'ada@webmcpui.com' }); // the live element is now filled, validated, and has fired input/change agent.restore(); // remove the stub ``` > **Order matters.** Install the fake agent *before* the element connects — controls register their tool in `connectedCallback`, so the host must already be present. ## The handle `installFakeAgent()` returns: | Member | Description | | --- | --- | | `tools` | All currently-registered tools, in registration order. | | `get(name)` | Look up a registered tool by name. | | `call(name, args?)` | Invoke a tool as an agent would; throws if unknown. | | `restore()` | Restore the previous `navigator.modelContext`. | This is exactly what powers the live demo on the [homepage](/) — the same fake host, driving real elements. --- # React & Vue URL: https://webmcpui.com/docs/frameworks Idiomatic, typed React and Vue components that wrap the core custom elements — with unstyled and SSR guidance. # React & Vue The core is framework-agnostic custom elements — they work in any framework as-is. But `@webmcpui/react` and `@webmcpui/vue` give you **idiomatic, typed** components: real props, `ref`s / `v-model`, and `on*` / `@event` handlers. They're thin wrappers — the WebMCP exposure, form association, and accessibility all live once in [`@webmcpui/core`](/docs), so there's no second implementation to drift. ## React ```bash pnpm add @webmcpui/react @webmcpui/core react react-dom ``` ```tsx import { Button, Input, Dialog } from '@webmcpui/react'; import '@webmcpui/tokens/css'; function Example() { const [email, setEmail] = React.useState(''); return ( <> setEmail(e.currentTarget.value)} expose /> ); } ``` Props mirror the [element attributes](/docs/elements/input), camelCased. Imperative handles come through the `ref` (e.g. `dialogRef.current.show()`). ## Vue ```bash pnpm add @webmcpui/vue @webmcpui/core vue ``` ```vue ``` `v-model` binds an `Input`'s value; `v-model:open` a `Dialog`; `v-model:active` a `Tabs`. **No `isCustomElement` config needed** — the `wmcp-*` tag only lives inside each component's render function, never your templates. ## Unstyled (bring your own styles) Every layer is headless underneath: the elements render with neutral inline fallbacks and only *look* shadcn-aligned when you load the theme. So: - **Themed** — `import '@webmcpui/tokens/css'` once at your app root. - **Unstyled** — don't import it. Then style the elements yourself via the CSS custom properties (e.g. `--button-primary-bg`) or the `::part()` selectors each element exposes: ```css wmcp-button::part(control) { border-radius: 9999px; } ``` ## Server rendering (Next.js, Nuxt, …) These are **client-rendered custom elements** — importing them evaluates `class extends HTMLElement`, which has no meaning on the server. Load them client-side: ```tsx // Next.js App Router 'use client'; import dynamic from 'next/dynamic'; const Button = dynamic(() => import('@webmcpui/react').then((m) => m.Button), { ssr: false, }); ``` `'use client'` alone isn't enough — Next still renders client components on the server, so use `ssr: false` (or add a DOM shim like [`@lit-labs/ssr-dom-shim`](https://www.npmjs.com/package/@lit-labs/ssr-dom-shim) to render them server-side). In **Nuxt**, register/use them in a `.client` plugin or wrap usage in ``. The elements upgrade and hydrate on the client normally. --- # URL: https://webmcpui.com/docs/elements/input A text input — the canonical form control, with type variants, validation, and WebMCP exposure. # `` A single-line text input. The reference implementation of `WmcpFormControl`. ```html ``` ## Element attributes | Attribute | Type | Description | | --- | --- | --- | | `type` | `string` | Native input type: `text`, `email`, `url`, `tel`, `password`, `number`, `search`. | ## Common attributes These are shared by every `` form control: | Attribute | Type | Description | | --- | --- | --- | | `label` | `string` | Visible label, associated with the control. | | `name` | `string` | Form field name; also the default tool name (`fill_`). | | `value` | `string` | Current value. | | `placeholder` | `string` | Placeholder text. | | `required` | `boolean` | Real constraint — empty fails validation. | | `disabled` | `boolean` | Disables the control. | | `helper-text` | `string` | Helper text below the control. | | `required-message` | `string` | Custom message when a required field is empty. | | `expose` | `boolean` | Register a [WebMCP tool](/docs/webmcp) for this control. | | `tool-name` | `string` | Override the generated tool name. | | `tool-description` | `string` | Override the generated tool description. | `schema` (a [Standard Schema](/docs/validation) validator) is set as a **property**, not an attribute: ```ts import { z } from 'zod'; document.querySelector('wmcp-input')!.schema = z.string().email(); ``` --- # URL: https://webmcpui.com/docs/elements/textarea A multi-line text control with the same validation and WebMCP exposure as the input. # `` A multi-line text control. Shares every [common attribute](/docs/elements/input#common-attributes) with ``. ```html ``` ## Element attributes | Attribute | Type | Default | Description | | --- | --- | --- | --- | | `rows` | `number` | `3` | Visible rows of text. | The exposed WebMCP tool is `fill_` with a single string `value` argument, identical to ``. --- # URL: https://webmcpui.com/docs/elements/select A native select with declarative options and an enum-typed WebMCP tool schema. # `` A dropdown built on the native `