DEV Community

Cover image for Building AI-Native React Applications with WebMCP
Serif COLAKEL
Serif COLAKEL

Posted on

Building AI-Native React Applications with WebMCP

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:

  1. Discovery: A standard way for pages to register tools with agents (checkout, filter_results, etc.).
  2. JSON Schemas: Explicit definitions of inputs and outputs, reducing hallucination and misunderstanding.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. MCP handles background API operations, data fetching, and batch jobs. It's platform-agnostic and always available.
  2. 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()  │  │
│  └─────────────┘    └─────────────┘  │
└──────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Note: navigator.modelContext is deprecated as of Chrome 150. Use document.modelContext instead.

Declarative API (HTML)

<form
  toolname="search_cars"
  tooldescription="Search for cars based on criteria"
>
  <!-- Form elements are automatically converted to tool parameters -->
</form>
Enter fullscreen mode Exit fullscreen mode

Origin Isolation & Permission Model

WebMCP APIs are protected by a two-layer security model:

  1. Origin Isolation: WebMCP only works in origin-isolated documents. It is disabled when Origin-Agent-Cluster: ?0 header is set or document.domain is used.

  2. Permissions Policy: Both APIs are gated by the tools permissions policy. Default is self — only top-level and same-origin contexts can register tools. For cross-origin iframes, add allow="tools".

<iframe src="https://example.com/widget" allow="tools"></iframe>
Enter fullscreen mode Exit fullscreen mode

Tool Lifecycle

Register ──► Discover ──► Execute ──► Unregister
   │                         │
   │                         └── Cancellable via AbortSignal
   └── Cross-origin shareable via exposedTo
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

6. Chrome Origin Trial & Requirements

Origin Trial

WebMCP is currently in origin trial phase for Chrome 149+.

Register for the Origin Trial

Local Development Chrome Flag

For local development before production:

  1. Navigate to chrome://flags/#enable-webmcp-testing
  2. Set the flag to Enabled
  3. Relaunch Chrome

Browser Support Check

const isWebMCPSupported = (): boolean => {
  return typeof document !== "undefined" && "modelContext" in document;
};
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

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"],
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.`);
});
Enter fullscreen mode Exit fullscreen mode
/* 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode
// 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>;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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}</>;
}
Enter fullscreen mode Exit fullscreen mode
// 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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),
}));
Enter fullscreen mode Exit fullscreen mode
// 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,
  });
}
Enter fullscreen mode Exit fullscreen mode

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);
  },
});
Enter fullscreen mode Exit fullscreen mode
// 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,
});
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Cookies: SameSite cookies 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
  },
};
Enter fullscreen mode Exit fullscreen mode

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 } });
};
Enter fullscreen mode Exit fullscreen mode

Granular Tools

deleteAllUsers(); // DANGEROUS
deleteUser({ id: "user_abc123" }); // SAFE
requestUserDeletion({ id: "abc", reason: "GDPR" }); // SAFER
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

15. Best Practices

  1. Each tool should perform a single function. Overlapping tools confuse the agent.
  2. Manage tool registration by state. Register tools only when they're useful in the current page state.
  3. 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.',
}
Enter fullscreen mode Exit fullscreen mode
  1. Distinguish execution from initiation:
"create-event"; // Creates immediately
"start-event-creation-process"; // Navigates to a form
Enter fullscreen mode Exit fullscreen mode
  1. 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'] }
Enter fullscreen mode Exit fullscreen mode
  1. Validate strictly in code, loosely in schema.
  2. Optimize tool count. More tools = more context window usage and slower completions.
  3. Don't patch with narrow rules. Abstract and adjust your tool instead of fixing a single model's quirk.
  4. Graceful rate limit failures. Return meaningful errors.
  5. 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 checkoutaddToCart 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,
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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"],
  },
];
Enter fullscreen mode Exit fullscreen mode

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' } },
      ]},
    ],
  }],
}
Enter fullscreen mode Exit fullscreen mode

17. Use Cases

E-Commerce

searchProducts({ query: "wireless headphones", maxPrice: 150 });
addToCart({ productId: "WH-1000XM5", quantity: 1 });
checkout({ paymentMethodId: "card_456", shippingAddressId: "addr_789" });
Enter fullscreen mode Exit fullscreen mode

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",
});
Enter fullscreen mode Exit fullscreen mode

Dashboard

getMetrics({ period: "last_30_days" });
filterData({ metric: "revenue", segment: "enterprise" });
exportCsv({ dataset: "users", filters: { status: "active" } });
Enter fullscreen mode Exit fullscreen mode

Travel

searchFlights({ from: "IST", to: "JFK", date: "2026-08-15", passengers: 2 });
bookHotel({ city: "New York", checkIn: "2026-08-15", maxPrice: 300 });
Enter fullscreen mode Exit fullscreen mode

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?"
Enter fullscreen mode Exit fullscreen mode

18. Production Considerations

HTTP Headers

Origin-Trial: <webmcp-token>
Origin-Agent-Cluster: ?1
Permissions-Policy: tools=(self "https://trusted-partner.com")
Enter fullscreen mode Exit fullscreen mode

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;
    }
  },
};
Enter fullscreen mode Exit fullscreen mode
function App() {
  if (!webmcp.isSupported()) return <MainApp />;

  return (
    <WebMCPProvider dependencies={deps}>
      <MainApp />
    </WebMCPProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
      }
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

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;
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

19. Limitations

  1. Browsing Context Required: A browser tab or webview must be open. No headless support.
  2. Overhead for Complex Interfaces: Highly complex sites may need JavaScript additions or refactoring.
  3. Tool Discoverability: No global tool registry. Agents must visit the site directly.
  4. Ephemeral Lifecycle: Tools exist only while the tab is open.
  5. Origin Trial Stage: APIs may change. Chrome 149+ required.
  6. Context Window Limits: Too many tools = lower selection accuracy.
  7. Chrome Only: Other browsers don't support it yet.
  8. 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)   │
└──────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Resources

Top comments (0)