WebMCP makes websites agent-ready. Instead of teaching agents how to click your UI, you teach them what your application can do.
1. What is WebMCP?
WebMCP (Web Model Context Protocol) is a proposed web standard developed under the W3C Web Machine Learning Working Group and spearheaded by Google Chrome. It enables web applications to expose structured tools to AI agents running within the browser.
Historically, when an AI agent wanted to interact with a website, it had to:
- Analyze the DOM to find buttons
- Detect and fill form fields
- Simulate page navigation
- Interpret screenshots
This approach — called actuation — is fragile. When the UI changes, the agent breaks. A button moves, a CSS class updates, and the agent's entire behavior falls apart.
WebMCP inverts this paradigm: Instead of teaching agents how to use your UI, you tell them what your application can do.
Core Concepts
| Concept | Description |
|---|---|
| Tool | A named function an agent can invoke. e.g. searchProducts, checkout
|
| JSON Schema | Defines the parameters a tool accepts, their types, and requirements |
| Actuation | An agent simulating human clicks and text input to interact with a site |
| Discovery | An agent querying what tools are registered on a page |
| Execution | An agent invoking a registered tool with parameters |
WebMCP solves three core problems:
-
Discovery: A standard way for pages to register tools with agents (
checkout,filter_results, etc.). - JSON Schemas: Explicit definitions of inputs and outputs, reducing hallucination and misunderstanding.
- State: A shared understanding of the current page context, so the agent knows what resources are available.
2. Why WebMCP?
Problems with Actuation
When agents rely on UI interaction (actuation), you face:
- Fragility: Any UI change (CSS class, DOM structure, button position) breaks the agent.
- Multi-step ambiguity: Every step is open to interpretation. A misunderstood dropdown derails the entire workflow.
- Slowness: DOM analysis, element discovery, click simulation — all take time.
- Accessibility gaps: Agents can't understand elements without aria-label and semantic HTML.
What WebMCP Brings
Actuation: Agent → "What does this button do?" → DOM analysis → guess → click
WebMCP: Agent → "What tools are available?" → getTools() → checkout() call
Speed & Reliability: WebMCP uses the browser's internal systems, so communication between client and tool is nearly instant. No round-trip to a remote server needed.
UI-Independent: WebMCP tools connect to application logic, not design. You can redesign your site without breaking the agent's ability to interact with it.
You're in Control: You define how agents interact with your site. Instead of hoping the agent finds the right button, you tell it exactly what to do.
Trust & Brand: Tools execute visibly on your page. Users see tasks completed as expected, and your brand experience remains intact.
Progressive Enhancement: WebMCP is an additive layer. In unsupported browsers, your application works normally.
3. WebMCP vs MCP
The most common question: "Will WebMCP replace MCP?"
Answer: No. As the Chrome team states, WebMCP and MCP solve different problems. They are designed to work together.
Comparison Table
| Dimension | MCP (Model Context Protocol) | WebMCP |
|---|---|---|
| Layer | Server-side (backend) | Client-side (browser) |
| Transport | stdio, SSE, JSON-RPC |
document.modelContext (Browser API) |
| Context | Can run headless | Requires a browser tab |
| Lifecycle | Persistent (server/daemon lifetime) | Ephemeral (tab lifetime) |
| UI Ownership | Rendered within the agent's UI | Works on your existing site |
| Discovery | Agent-specific registration flows | Tools registered during page visit |
| Access | Desktop, mobile, cloud, everywhere | Browser only |
| Target Audience | Claude Desktop, Cursor, VS Code agents | Chrome built-in agent, browser extensions |
| DOM Access | None | Yes (live session, cookies, DOM) |
| SDKs | Rust, Python, TypeScript | JavaScript, HTML attributes |
How They Work Together
The Chrome team's analogy:
MCP is like a company's 24/7 call center. Accessible anywhere, handles core tasks anytime.
WebMCP is like the same company's in-store expert. Only available when you're at the store (site open), but provides context-specific, fast, accurate service.
The most effective agentic applications use both:
- MCP handles background API operations, data fetching, and batch jobs. It's platform-agnostic and always available.
- WebMCP kicks in when a user visits your site. It provides instant, reliable interaction within the live tab context.
┌──────────────────────────────────────┐
│ AI Agent (Browser) │
├──────────────────────────────────────┤
│ MCP (Persistent) WebMCP (Tab) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Backend API │ │ Web Tools │ │
│ │ • Fetch data│ │ • search() │ │
│ │ • Batch ops │ │ • checkout()│ │
│ │ • Auth │ │ • filter() │ │
│ └─────────────┘ └─────────────┘ │
└──────────────────────────────────────┘
4. WebMCP Architecture
WebMCP exposes two API surfaces:
Imperative API (JavaScript)
// document.modelContext.registerTool() — tool registration
// document.modelContext.getTools() — tool discovery
// document.modelContext.executeTool() — manual tool invocation
// 'toolchange' event — listen for tool list changes
Note:
navigator.modelContextis deprecated as of Chrome 150. Usedocument.modelContextinstead.
Declarative API (HTML)
<form
toolname="search_cars"
tooldescription="Search for cars based on criteria"
>
<!-- Form elements are automatically converted to tool parameters -->
</form>
Origin Isolation & Permission Model
WebMCP APIs are protected by a two-layer security model:
Origin Isolation: WebMCP only works in origin-isolated documents. It is disabled when
Origin-Agent-Cluster: ?0header is set ordocument.domainis used.Permissions Policy: Both APIs are gated by the
toolspermissions policy. Default isself— only top-level and same-origin contexts can register tools. For cross-origin iframes, addallow="tools".
<iframe src="https://example.com/widget" allow="tools"></iframe>
Tool Lifecycle
Register ──► Discover ──► Execute ──► Unregister
│ │
│ └── Cancellable via AbortSignal
└── Cross-origin shareable via exposedTo
5. Browser + Agent + React Application Flow
The interaction flow between a user, their AI agent, and a WebMCP-enabled React application:
1. USER OPENS THE SITE
React app mounts
> useWebMcp hooks execute
> document.modelContext.registerTool() registers tools
(searchProducts, checkout, getMetrics, ...)
2. USER GIVES AGENT A COMMAND
"Find this product and add it to cart"
> Agent discovers tools via document.modelContext.getTools()
3. AGENT SELECTS AND INVOKES TOOLS
Agent > searchProducts({ query: "black jacket" })
Agent > addToCart({ productId: "JKT-42" })
> execute() callbacks run
> UI updates, user sees results
4. USER LEAVES THE PAGE
Component unmounts
> Tools are automatically unregistered (or via AbortSignal)
6. Chrome Origin Trial & Requirements
Origin Trial
WebMCP is currently in origin trial phase for Chrome 149+.
Local Development Chrome Flag
For local development before production:
- Navigate to
chrome://flags/#enable-webmcp-testing - Set the flag to Enabled
- Relaunch Chrome
Browser Support Check
const isWebMCPSupported = (): boolean => {
return typeof document !== "undefined" && "modelContext" in document;
};
Model Context Tool Inspector Extension
Install the Inspector Extension to:
- View registered tools on any page
- Manually invoke tools
- Verify JSON Schema definitions
- Test agent responses to natural language prompts
7. Imperative API
The Imperative API allows you to define tools with JavaScript. Expose any function — form input, site navigation, state management — as a tool.
Core API
// Register a tool
await document.modelContext.registerTool({
name: "get_order_status",
description: "Search orders in a given timeframe.",
inputSchema: {
type: "object",
properties: {
timeframe: {
type: "string",
enum: [
"today",
"yesterday",
"last_7_days",
"last_30_days",
"last_6_months",
],
description: "Timeframe for the order lookup.",
},
},
required: ["timeframe"],
},
execute: async ({ timeframe }) => {
const orders = await api.getOrders({ timeframe });
return JSON.stringify(orders);
},
annotations: {
readOnlyHint: true,
untrustedContentHint: false,
},
});
AbortSignal for Tool Removal
const controller = new AbortController();
await document.modelContext.registerTool(
{
name: "addTodo",
description: "Add a new item to the to-do list",
inputSchema: {
type: "object",
properties: { text: { type: "string" } },
},
execute: async ({ text }) => {
const todo = await todoService.add(text);
return `Added to-do: ${todo.title}`;
},
},
{ signal: controller.signal },
);
// Later:
controller.abort(); // Tool is unregistered
Tool Discovery & Manual Execution
// Same-origin tools
const tools = await document.modelContext.getTools();
// Including cross-origin tools
const allTools = await document.modelContext.getTools({
fromOrigins: ["https://partner.org"],
});
// Manual tool invocation
const result = await document.modelContext.executeTool(
tool,
JSON.stringify({ text: "Buy milk" }),
);
// Listen for tool changes
document.modelContext.addEventListener("toolchange", () => {
console.log("Tools changed");
});
Cross-Origin Tool Sharing
// On https://partner.org
await document.modelContext.registerTool(
{
name: "my_shared_tool",
description: "Shared across origins",
inputSchema: { type: "object", properties: {} },
execute: async () => "Hello from partner",
},
{ exposedTo: ["https://example.com"] },
);
// On https://example.com
const tools = await document.modelContext.getTools({
fromOrigins: ["https://partner.org"],
});
8. Declarative API
The Declarative API lets you define tools by adding attributes to HTML form elements. Turn existing forms into agent-usable tools without writing JavaScript.
HTML Attributes
| Attribute | Location | Description |
|---|---|---|
toolname |
<form> |
The tool's name |
tooldescription |
<form> |
What the tool does |
toolautosubmit |
<form> |
Auto-submit when agent invokes the tool |
toolparamdescription |
Form elements | Parameter description (when label is insufficient) |
Example: Support Request Form
<form
toolname="createSupportRequest"
tooldescription="Submits a request for customer support."
action="/submit"
>
<label for="firstName">First Name</label>
<input type="text" name="firstName" id="firstName" />
<label for="lastName">Last Name</label>
<input type="text" name="lastName" id="lastName" />
<select
name="team"
required
toolparamdescription="Determines what team this request is routed to."
>
<option value="Customer happiness team">Return my purchase.</option>
<option value="Distribution team">Check where my package is.</option>
<option value="Website support team">Get help on the website.</option>
</select>
<button type="submit">Submit</button>
</form>
The browser automatically converts this form into a JSON Schema.
SubmitEvent.agentInvoked & CSS Pseudo-Classes
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
if (e.agentInvoked) {
// Triggered by an agent
e.respondWith(Promise.resolve("Search is done!"));
return;
}
performTraditionalSubmit();
});
// Agent activation events
window.addEventListener("toolactivated", ({ toolName }) => {
console.log(`Tool "${toolName}" activated.`);
});
window.addEventListener("toolcancel", ({ toolName }) => {
console.log(`Tool "${toolName}" cancelled.`);
});
/* When an agent activates the form */
form:tool-form-active {
outline: light-dark(blue, cyan) dashed 1px;
}
/* When an agent targets the submit button */
input:tool-submit-active {
outline: light-dark(red, pink) dashed 1px;
}
9. Using the Imperative API with React
Type Definitions
// types/webmcp.ts
export interface WebMCPToolDefinition<
TInput extends Record<string, unknown> = Record<string, unknown>,
TOutput = unknown,
> {
name: string;
description: string;
inputSchema: Record<string, unknown>;
execute: (args: TInput) => Promise<TOutput>;
annotations?: {
readOnlyHint?: boolean;
untrustedContentHint?: boolean;
};
}
export interface RegisterToolOptions {
signal?: AbortSignal;
exposedTo?: string[];
}
declare global {
interface Document {
readonly modelContext?: {
registerTool(
tool: WebMCPToolDefinition,
options?: RegisterToolOptions,
): Promise<void>;
getTools(options?: { fromOrigins?: string[] }): Promise<WebMCPTool[]>;
executeTool(
tool: WebMCPTool,
args: string,
options?: { signal?: AbortSignal },
): Promise<string | null>;
addEventListener(
type: "toolchange",
listener: (event: Event) => void,
): void;
removeEventListener(
type: "toolchange",
listener: (event: Event) => void,
): void;
};
}
}
export interface WebMCPTool extends WebMCPToolDefinition {
origin: string;
window: Window;
inputSchema: string;
}
Basic Usage
// components/ProductSearch.tsx
import { useEffect } from "react";
export function ProductSearch() {
useEffect(() => {
if (!document.modelContext) return;
const controller = new AbortController();
document.modelContext.registerTool(
{
name: "searchProducts",
description:
"Search the product catalog by query string. Returns matching products with name, price, and SKU.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description:
"The search term to find products by name or category",
},
maxPrice: {
type: "number",
description: "Optional maximum price filter in USD",
},
},
required: ["query"],
},
execute: async ({ query, maxPrice }) => {
const products = await searchProducts({ query, maxPrice });
return JSON.stringify(products);
},
annotations: {
readOnlyHint: true,
untrustedContentHint: false,
},
},
{ signal: controller.signal },
);
return () => {
controller.abort();
};
}, []);
return <div>{/* Product search UI */}</div>;
}
10. React Hooks: useWebMcp
Basic Hook
// hooks/useWebMcp.ts
import { useEffect, useRef } from "react";
import type { WebMCPToolDefinition, RegisterToolOptions } from "@/types/webmcp";
interface UseWebMcpOptions<
TInput extends Record<string, unknown> = Record<string, unknown>,
TOutput = unknown,
> {
name: string;
description: string;
inputSchema: Record<string, unknown>;
execute: (args: TInput) => Promise<TOutput>;
annotations?: { readOnlyHint?: boolean; untrustedContentHint?: boolean };
enabled?: boolean;
registerOptions?: RegisterToolOptions;
}
/**
* Registers a WebMCP tool and automatically cleans up on unmount.
*/
export function useWebMcp<
TInput extends Record<string, unknown> = Record<string, unknown>,
TOutput = unknown,
>({
name,
description,
inputSchema,
execute,
annotations,
enabled = true,
registerOptions,
}: UseWebMcpOptions<TInput, TOutput>) {
const executeRef = useRef(execute);
executeRef.current = execute;
useEffect(() => {
if (!document.modelContext || !enabled) return;
const controller = new AbortController();
document.modelContext.registerTool(
{
name,
description,
inputSchema,
execute: async (args: TInput) => executeRef.current(args),
annotations,
},
{ ...registerOptions, signal: controller.signal },
);
return () => {
controller.abort();
};
}, [name, description, inputSchema, enabled]);
}
State-Aware Version
// hooks/useWebMcp.ts (continued)
interface UseWebMcpStateAwareOptions<
TInput extends Record<string, unknown> = Record<string, unknown>,
TOutput = unknown,
> {
name: string;
description: string;
inputSchema: Record<string, unknown>;
execute: (args: TInput) => Promise<TOutput>;
annotations?: { readOnlyHint?: boolean; untrustedContentHint?: boolean };
dependencies?: unknown[];
}
export function useWebMcpStateAware<
TInput extends Record<string, unknown> = Record<string, unknown>,
TOutput = unknown,
>({
name,
description,
inputSchema,
execute,
annotations,
dependencies = [],
}: UseWebMcpStateAwareOptions<TInput, TOutput>) {
const executeRef = useRef(execute);
const toolRef = useRef<AbortController | null>(null);
useEffect(() => {
executeRef.current = execute;
if (!document.modelContext) return;
const controller = new AbortController();
if (toolRef.current) {
toolRef.current.abort();
}
document.modelContext.registerTool(
{
name,
description,
inputSchema,
execute: async (args: TInput) => executeRef.current(args),
annotations,
},
{ signal: controller.signal },
);
toolRef.current = controller;
return () => {
controller.abort();
};
}, [name, description, inputSchema, ...dependencies]);
}
Usage Examples
// features/cart/Checkout.tsx
export function Checkout() {
const { items, totalPrice } = useCartStore();
useWebMcp({
name: "checkout",
description: "Complete the purchase of all items currently in the cart.",
inputSchema: {
type: "object",
properties: {
paymentMethodId: {
type: "string",
description: "The saved payment method ID to use",
},
shippingAddressId: {
type: "string",
description: "The saved shipping address ID",
},
},
required: ["paymentMethodId", "shippingAddressId"],
},
execute: async ({ paymentMethodId, shippingAddressId }) => {
const order = await checkoutApi.createOrder({
items,
paymentMethodId,
shippingAddressId,
totalPrice,
});
return JSON.stringify({ orderId: order.id, status: order.status });
},
annotations: { readOnlyHint: false },
enabled: items.length > 0,
});
return <div>{/* Checkout UI */}</div>;
}
// features/admin/UserManagement.tsx
export function UserManagement() {
const { role } = useUserRole();
useWebMcp({
name: "createUser",
description: "Create a new user account with email, role, and department.",
inputSchema: {
type: "object",
properties: {
email: { type: "string", format: "email" },
role: { type: "string", enum: ["admin", "editor", "viewer"] },
department: { type: "string" },
},
required: ["email", "role"],
},
execute: async ({ email, role: userRole, department }) => {
const user = await adminApi.createUser({
email,
role: userRole,
department,
});
return JSON.stringify({ userId: user.id, email: user.email });
},
annotations: { readOnlyHint: false },
enabled: role === "admin",
});
return <div>{/* User management UI */}</div>;
}
11. Tool Registry Pattern
For production apps with dozens of tools, use a centralized registry pattern:
// features/registry/tools.ts
export interface ToolFactory<TInput, TOutput> {
definition: Omit<WebMCPToolDefinition<TInput, TOutput>, "execute">;
createExecute: (deps: ToolDependencies) => (args: TInput) => Promise<TOutput>;
}
export interface ToolDependencies {
api: typeof import("@/lib/api");
analytics: typeof import("@/lib/analytics");
}
export const searchProductsTool = {
definition: {
name: "searchProducts" as const,
description:
"Search the product catalog by keywords. Returns matching products.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search keywords" },
limit: {
type: "number",
description: "Max results (default 10)",
default: 10,
},
},
required: ["query"],
},
annotations: { readOnlyHint: true, untrustedContentHint: false },
} as const,
createExecute:
({ api }: ToolDependencies) =>
async ({ query, limit = 10 }) => {
const products = await api.searchProducts({ query, limit });
return JSON.stringify(products);
},
} satisfies ToolFactory<{ query: string; limit?: number }, string>;
export const checkoutTool = {
definition: {
name: "checkout" as const,
description: "Complete the purchase of all items in the cart.",
inputSchema: {
type: "object",
properties: {
paymentMethodId: {
type: "string",
description: "Saved payment method ID",
},
shippingAddressId: {
type: "string",
description: "Saved shipping address ID",
},
},
required: ["paymentMethodId", "shippingAddressId"],
},
annotations: { readOnlyHint: false },
} as const,
createExecute:
({ api, analytics }: ToolDependencies) =>
async ({ paymentMethodId, shippingAddressId }) => {
const order = await api.createOrder({
paymentMethodId,
shippingAddressId,
});
analytics.track("checkout_completed", { orderId: order.id });
return JSON.stringify({ orderId: order.id, status: order.status });
},
} satisfies ToolFactory<
{ paymentMethodId: string; shippingAddressId: string },
string
>;
export const ALL_TOOLS = [searchProductsTool, checkoutTool] as const;
Registry Provider
// features/registry/WebMCPProvider.tsx
export function WebMCPProvider({
children,
dependencies,
toolFilter = () => true,
}: {
children: ReactNode;
dependencies: ToolDependencies;
toolFilter?: (toolName: string) => boolean;
}) {
useEffect(() => {
if (!document.modelContext) return;
const controllers: AbortController[] = [];
for (const tool of ALL_TOOLS) {
if (!toolFilter(tool.definition.name)) continue;
const controller = new AbortController();
const execute = tool.createExecute(dependencies);
document.modelContext.registerTool(
{ ...tool.definition, execute },
{ signal: controller.signal },
);
controllers.push(controller);
}
return () => controllers.forEach((c) => c.abort());
}, [dependencies]);
return <>{children}</>;
}
// App.tsx
export function App() {
const { role } = useAuth();
return (
<WebMCPProvider
dependencies={{ api, analytics }}
toolFilter={(name) => {
if (["createUser", "generateReport"].includes(name)) {
return role === "admin";
}
return true;
}}
>
<Router />
</WebMCPProvider>
);
}
12. State Management
Zustand Integration
// stores/cart-store.ts
export const useCartStore = create<CartState>((set, get) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (productId) =>
set((state) => ({
items: state.items.filter((i) => i.productId !== productId),
})),
clearCart: () => set({ items: [] }),
getTotalPrice: () =>
get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}));
// features/cart/CartTools.tsx
export function useCartTools() {
useWebMcp({
name: "addToCart",
description: "Add a product to the shopping cart.",
inputSchema: {
type: "object",
properties: {
productId: { type: "string" },
quantity: { type: "number", minimum: 1, maximum: 99 },
},
required: ["productId"],
},
execute: async ({ productId, quantity = 1 }) => {
const product = await catalogApi.getProduct(productId);
useCartStore.getState().addItem({
productId,
quantity,
price: product.price,
name: product.name,
});
return JSON.stringify({ added: product.name, quantity });
},
annotations: { readOnlyHint: false },
});
useWebMcp({
name: "checkout",
description: "Complete the purchase of all items in the cart.",
inputSchema: {
type: "object",
properties: {
paymentMethodId: { type: "string" },
shippingAddressId: { type: "string" },
},
required: ["paymentMethodId", "shippingAddressId"],
},
execute: async ({ paymentMethodId, shippingAddressId }) => {
const { items, getTotalPrice, clearCart } = useCartStore.getState();
if (items.length === 0) {
return JSON.stringify({ error: "Cart is empty" });
}
const order = await checkoutApi.createOrder({
items,
paymentMethodId,
shippingAddressId,
totalPrice: getTotalPrice(),
});
clearCart();
return JSON.stringify({ orderId: order.id, status: order.status });
},
annotations: { readOnlyHint: false },
enabled: items.length > 0,
});
}
Legend-State Integration
// stores/cart-legend.ts
export const cart$ = observable({
items: [] as CartItem[],
addItem(item: CartItem) {
this.items.push(item);
},
getTotalPrice() {
return this.items.get().reduce((sum, i) => sum + i.price * i.quantity, 0);
},
});
// Legend-State — always reads the latest state from the observable
useWebMcp({
name: "checkout",
execute: async ({ paymentMethodId, shippingAddressId }) => {
const currentItems = cart$.items.get();
if (currentItems.length === 0) {
return JSON.stringify({ error: "Cart is empty" });
}
const order = await checkoutApi.createOrder({
items: currentItems,
paymentMethodId,
shippingAddressId,
totalPrice: cart$.getTotalPrice(),
});
cart$.items.set([]);
return JSON.stringify({ orderId: order.id, status: order.status });
},
enabled: cart$.items.get().length > 0,
});
13. Authentication & Session Sharing
WebMCP tools run in the same origin as your page, automatically inheriting the current auth context:
// lib/api-client.ts
class ApiClient {
private getAuthHeaders(): Record<string, string> {
const token = useAuthStore.getState().accessToken;
return token ? { Authorization: `Bearer ${token}` } : {};
}
async searchProducts(params: SearchParams) {
const response = await fetch("/api/products/search", {
method: "POST",
headers: {
"Content-Type": "application/json",
...this.getAuthHeaders(),
},
body: JSON.stringify(params),
});
return response.json();
}
}
-
Cookies:
SameSitecookies are automatically included in tool calls - Authorization Header: Read from the store and added manually
- CSRF Token: Read from the DOM or cookies
14. Secure Tools
Annotation Hints
const tool = {
name: "getUserComments",
annotations: {
readOnlyHint: false, // State-mutating
untrustedContentHint: true, // Contains UGC — signals the agent
},
};
Cross-Origin Exposure
| Tool Type | Exposure Policy |
|---|---|
Read-only (getProducts) |
Only to sites you'd already share this data with |
Read-Write (postComment) |
Only to origins you absolutely trust to act on behalf of users |
Sensitive (deleteAccount) |
Never expose cross-origin |
Prompt Injection Protection
// DANGEROUS — using agent-provided text directly
execute: async ({ comment }) => {
await db.execute(`INSERT INTO comments (text) VALUES ('${comment}')`);
};
// SAFE — parameterized query
execute: async ({ comment }) => {
const sanitized = sanitizeInput(comment);
await db.comments.create({ data: { text: sanitized } });
};
Granular Tools
deleteAllUsers(); // DANGEROUS
deleteUser({ id: "user_abc123" }); // SAFE
requestUserDeletion({ id: "abc", reason: "GDPR" }); // SAFER
Character Budgets
| Item | Maximum |
|---|---|
| Tool description | 500 characters |
| Parameter description | 150 characters |
| Tool name / param name | 30 characters |
| Tool output (per invocation) | 1,500 characters |
Origin Isolation & Permissions
Origin-Agent-Cluster: ?1 # Required for WebMCP
Permissions-Policy: tools=(self "https://trusted.com")
Audit Logging
export class WebMCPAuditLogger {
logExecution(entry: {
toolName: string;
args: Record<string, unknown>;
result: string | null;
error: string | null;
timestamp: string;
userId: string;
sessionId: string;
origin: string;
isAgentInvoked: boolean;
}) {
analytics.track("webmcp_tool_executed", entry);
}
}
15. Best Practices
- Each tool should perform a single function. Overlapping tools confuse the agent.
- Manage tool registration by state. Register tools only when they're useful in the current page state.
- Use clear, semantic naming:
// BAD
{ name: 'helper1', description: "Don't use this for anything except search." }
// GOOD
{
name: 'searchProducts',
description: 'Search the product catalog by keywords. Returns matching products with name, price, and availability.',
}
- Distinguish execution from initiation:
"create-event"; // Creates immediately
"start-event-creation-process"; // Navigates to a form
- Minimize cognitive load: Don't ask the agent to do math or string transformations. Use semantic enums:
// BAD
shipping_id: { type: 'number', enum: [1, 2, 3] }
// GOOD
shipping: { type: 'string', enum: ['Standard', 'Express', 'Overnight'] }
- Validate strictly in code, loosely in schema.
- Optimize tool count. More tools = more context window usage and slower completions.
- Don't patch with narrow rules. Abstract and adjust your tool instead of fixing a single model's quirk.
- Graceful rate limit failures. Return meaningful errors.
- Update UI state after function completion. Agents may rely on the interface to plan next steps.
16. Evaluation (Evals)
Failure Modes
| Failure | Example | Check |
|---|---|---|
| Wrong tool selection | Calls checkout instead of addToCart
|
Is the description clear? |
| Wrong ordering |
checkout → addToCart
|
Is state transition correct? |
| Wrong arguments | Adds shoes instead of a t-shirt | Is inputSchema sufficient? |
| Wrong output | Returns total price instead of cart contents | Is output format clear? |
| Mid-chain failure | Invalid coupon but checkout proceeds | Is each tool independently testable? |
Deterministic Tests
// tools/__tests__/searchProducts.test.ts
describe("searchProducts", () => {
it("should return matching products as JSON", async () => {
const mockApi = {
searchProducts: vi
.fn()
.mockResolvedValue([{ id: "1", name: "Wireless Mouse", price: 29.99 }]),
};
const execute = searchProductsTool.createExecute({
api: mockApi as any,
analytics: {} as any,
});
const result = await execute({ query: "mouse" });
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
expect(parsed[0].name).toBe("Wireless Mouse");
expect(mockApi.searchProducts).toHaveBeenCalledWith({
query: "mouse",
limit: 10,
});
});
});
Probabilistic Tests
// evals/checkout.eval.ts
const evalCases = [
{
name: "basic checkout",
messages: [{ role: "user", content: "Checkout with my saved Visa card." }],
expectedCall: [
{
functionName: "checkout",
arguments: {
paymentMethodId: "visa_789",
shippingAddressId: "home_123",
},
},
],
},
{
name: "empty cart checkout",
messages: [{ role: "user", content: "Complete my purchase." }],
notExpectedCall: ["checkout"],
},
];
End-to-End Testing
{
messages: [{
role: 'user',
content: 'I want to buy a black jacket and jeans.',
}],
expectedCall: [{
unordered: [
{ ordered: [
{ functionName: 'searchProducts', arguments: { query: 'black jacket' } },
{ functionName: 'addToCart', arguments: { productId: 'JACKET002' } },
]},
{ ordered: [
{ functionName: 'searchProducts', arguments: { query: 'jeans' } },
{ functionName: 'addToCart', arguments: { productId: 'JEANS001' } },
]},
],
}],
}
17. Use Cases
E-Commerce
searchProducts({ query: "wireless headphones", maxPrice: 150 });
addToCart({ productId: "WH-1000XM5", quantity: 1 });
checkout({ paymentMethodId: "card_456", shippingAddressId: "addr_789" });
Admin Panel
createUser({ email: "new@example.com", role: "editor" });
assignRole({ userId: "usr_123", role: "admin" });
generateReport({
type: "sales",
startDate: "2026-01-01",
endDate: "2026-06-30",
});
Dashboard
getMetrics({ period: "last_30_days" });
filterData({ metric: "revenue", segment: "enterprise" });
exportCsv({ dataset: "users", filters: { status: "active" } });
Travel
searchFlights({ from: "IST", to: "JFK", date: "2026-08-15", passengers: 2 });
bookHotel({ city: "New York", checkIn: "2026-08-15", maxPrice: 300 });
Real-World Flow: Repeat Purchase
User: "Reorder the cheese sticks I bought last month."
1. Agent: getOrderHistory({ startDate: "2026-06-01", endDate: "2026-06-30" })
> [{ productName: "Cheddar Peelers", productId: "CH-42" }]
2. Agent: searchProducts({ query: "Cheddar Peelers" })
> [{ productId: "CH-42", name: "Cheddar Peelers", price: 4.99, inStock: true }]
3. Agent: addToCart({ productId: "CH-42", quantity: 1 })
> { cartTotal: 4.99, itemCount: 1 }
4. Agent > User: "I found your Cheddar Peelers from last month. Added to cart. Checkout?"
18. Production Considerations
HTTP Headers
Origin-Trial: <webmcp-token>
Origin-Agent-Cluster: ?1
Permissions-Policy: tools=(self "https://trusted-partner.com")
Feature Detection & Graceful Degradation
// lib/webmcp.ts
export const webmcp = {
isSupported(): boolean {
return typeof document !== "undefined" && "modelContext" in document;
},
async isOriginTrialActive(): Promise<boolean> {
if (!this.isSupported()) return false;
try {
const ctrl = new AbortController();
await document.modelContext!.registerTool(
{
name: "__health_check__",
description: "Internal health check",
inputSchema: { type: "object", properties: {} },
execute: async () => "ok",
},
{ signal: ctrl.signal },
);
ctrl.abort();
return true;
} catch {
return false;
}
},
};
function App() {
if (!webmcp.isSupported()) return <MainApp />;
return (
<WebMCPProvider dependencies={deps}>
<MainApp />
</WebMCPProvider>
);
}
Rate Limiting & Error Handling
const withRateLimit = <TInput, TOutput>(
tool: WebMCPToolDefinition<TInput, TOutput>,
maxCallsPerMinute = 60,
): WebMCPToolDefinition<TInput, TOutput> => {
const calls: number[] = [];
return {
...tool,
execute: async (args) => {
const now = Date.now();
const recentCalls = calls.filter((t) => now - t < 60_000);
if (recentCalls.length >= maxCallsPerMinute) {
return JSON.stringify({
error: "RATE_LIMITED",
message: `Limited to ${maxCallsPerMinute} calls/min.`,
retryAfterSeconds: Math.ceil((recentCalls[0] + 60_000 - now) / 1000),
}) as unknown as TOutput;
}
calls.push(now);
try {
return await tool.execute(args);
} catch (error) {
return JSON.stringify({
error: "TOOL_ERROR",
message: error instanceof Error ? error.message : "Unknown error",
}) as unknown as TOutput;
}
},
};
};
Monitoring
const withTelemetry = <TInput, TOutput>(
tool: WebMCPToolDefinition<TInput, TOutput>,
): WebMCPToolDefinition<TInput, TOutput> => ({
...tool,
execute: async (args) => {
const startTime = performance.now();
try {
const result = await tool.execute(args);
analytics.track("webmcp_tool_success", {
tool: tool.name,
durationMs: performance.now() - startTime,
});
return result;
} catch (error) {
analytics.track("webmcp_tool_error", {
tool: tool.name,
error: error instanceof Error ? error.message : "Unknown",
durationMs: performance.now() - startTime,
});
throw error;
}
},
});
19. Limitations
- Browsing Context Required: A browser tab or webview must be open. No headless support.
- Overhead for Complex Interfaces: Highly complex sites may need JavaScript additions or refactoring.
- Tool Discoverability: No global tool registry. Agents must visit the site directly.
- Ephemeral Lifecycle: Tools exist only while the tab is open.
- Origin Trial Stage: APIs may change. Chrome 149+ required.
- Context Window Limits: Too many tools = lower selection accuracy.
- Chrome Only: Other browsers don't support it yet.
- No Built-in Rate Limiting: You must implement your own.
20. Demo Project
Based on Chrome's React Travel Search demo:
// features/travel/FlightSearch.tsx
export function FlightSearch() {
const [searchResults, setSearchResults] = useState<Flight[]>([]);
const [isLoading, setIsLoading] = useState(false);
const searchFlights = useCallback(
async (args: {
from: string;
to: string;
date: string;
passengers: number;
maxPrice?: number;
}) => {
setIsLoading(true);
try {
const flights = await flightApi.search(args);
setSearchResults(flights);
return JSON.stringify({
count: flights.length,
flights: flights.map((f) => ({
id: f.id,
airline: f.airline,
departure: f.departureTime,
arrival: f.arrivalTime,
price: f.price,
currency: f.currency,
stops: f.stops,
})),
});
} finally {
setIsLoading(false);
}
},
[],
);
useWebMcp({
name: "searchFlights",
description:
"Search for available flights. Returns flight options with airline, times, price, and number of stops.",
inputSchema: {
type: "object",
properties: {
from: {
type: "string",
description: "Departure airport code (IST, JFK, LHR)",
},
to: { type: "string", description: "Arrival airport code" },
date: { type: "string", format: "date", description: "YYYY-MM-DD" },
passengers: {
type: "number",
description: "Passengers (1-9)",
minimum: 1,
maximum: 9,
},
maxPrice: {
type: "number",
description: "Max price per passenger in USD",
},
},
required: ["from", "to", "date", "passengers"],
},
execute: searchFlights,
annotations: { readOnlyHint: true },
});
return (
<div>
{isLoading ? <Spinner /> : <FlightList flights={searchResults} />}
</div>
);
}
Running the Demo
git clone https://github.com/GoogleChromeLabs/webmcp-tools.git
cd webmcp-tools/demos/react-flightsearch
npm install && npm run dev
# In Chrome: chrome://flags/#enable-webmcp-testing → Enabled
21. Future Standards
-
requestUserInteraction(): Async user input during tool execution - Consent management: Cross-party consent handling
- Character limits: Official limits being added to the spec
- Angular Support: Experimental WebMCP support — DI lifecycle + Signal Forms
Origin Trial (Chrome 149+) → Specification Draft → W3C Standard →
Other Browsers → Stable Release
Conclusion
WebMCP fundamentally changes how web applications and AI agents interact. For years, we've been saying "click this button" to guide agents. Now we say "this application can do these things" — letting agents interact directly with application capabilities.
"WebMCP makes websites agent-ready. Instead of teaching agents how to click your UI, you teach them what your application can do."
Architecture Summary
┌───────────────────────┐
│ AI Agent │
│ (Chrome Built-in) │
└──────────┬────────────┘
│ document.modelContext
▼
┌──────────────────────────────────────┐
│ React Application │
│ ┌──────────────────────────────┐ │
│ │ WebMCPProvider │ │
│ │ • searchFlights() │ │
│ │ • bookHotel() │ │
│ │ • checkout() │ │
│ │ • generateReport() │ │
│ └──────────────────────────────┘ │
│ ┌──────────────────────────────┐ │
│ │ State (Zustand / Legend) │ │
│ └──────────────────────────────┘ │
└──────────────────┬───────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Backend APIs │
│ (MCP Server for persistent tasks) │
└──────────────────────────────────────┘
Resources
- WebMCP Specification (GitHub)
- Chrome WebMCP Documentation
- WebMCP Imperative API
- WebMCP Declarative API
- WebMCP Evals
- WebMCP Use Cases
- WebMCP Best Practices
- WebMCP Secure Tools
- WebMCP vs MCP
- WebMCP Demos (GitHub)
- WebMCP Origin Trial Registration
- Chrome Status: WebMCP
- Model Context Tool Inspector Extension
Top comments (0)