WebMCP in React: Client-Side State en Formulieracties Beschikbaar Maken voor Browser-AI-Agents
Transformeer React 19-webapplicaties in agent-vriendelijke interfaces met de W3C WebMCP-standaard, declaratieve formulierattributen en useWebMCPTool-hooks.

Wanneer softwareontwikkelaars grote taalmodellen integreren met externe systemen, is het Model Context Protocol (MCP) uitgegroeid tot de standaardcommunicatielaag voor tool-aanroepen. Vrijwel alle bestaande MCP-implementaties hanteren echter een traditioneel client-servermodel: een client (zoals Claude Desktop of een IDE-assistent) communiceert via stdio of Server-Sent Events (SSE) met een achtergrondproces om databases te bevragen, cloud-API's aan te roepen of bestandssystemen te inspecteren.
Binnen de webbrowser schiet dit model echter tekort.
Moderne webapplicaties zijn complexe Single Page Applications (SPA's) met een rijke interne status. Essentiële applicatiestatus—zoals actieve selecties op een canvas, niet-opgeslagen invoervelden, lokale paginering, actieve filterinstellingen en interactieve modale dialoogvensters—leeft uitsluitend in het geheugen van de client (React-componentstatus, Zustand-stores of de DOM). Wanneer een autonome browser-agent of browserextensie probeert te interageren met zo'n applicatie, is deze genoodzaakt terug te vallen op fragiele technieken zoals visual DOM-scraping, het parsen van de accessibility-tree of gesimuleerde muisklikken.
De nieuwe specificatie WebMCP (Web Model Context Protocol)—in ontwikkeling binnen de W3C Web Machine Learning Community Group en als experimentele preview beschikbaar in Chromium—lost dit probleem op door het Model Context Protocol rechtstreeks in het browsertabblad te introduceren.
Deze handleiding legt uit hoe WebMCP functioneert, waarin het verschilt van MCP op de server, en hoe React 19-componentstatus en formulieracties met behulp van declaratieve HTML-attributen en React-hooks direct beschikbaar worden gemaakt voor browser-agents.
+-------------------------------------------------------------------------+
| Browser Tab (Main Thread / React 19 Context) |
| |
| [ React Component State ] <---> [ useWebMCPTool Hook ] |
| | | |
| v v |
| [ <form toolname="..." /> ] [ document.modelContext ] |
| | (In-Memory Tool Registry) |
| +--------------------------------+ |
+---------------------------------------------|---------------------------+
| Local In-Tab Dispatch
+---------------------------------------------v---------------------------+
| In-Browser Agent Context (Chrome Assistant / Extension / Gemini Nano) |
| |
| 1. Inspects active tab tools via document.modelContext.listTools() |
| 2. Invokes tool with structured JSON arguments |
| 3. React action dispatches state transition -> UI updates instantly |
+-------------------------------------------------------------------------+Het Vacuüm van Client-Side Tools
Om het belang van WebMCP te begrijpen, kijken we naar hoe een autonome agent vandaag de dag een ogenschijnlijk eenvoudige opdracht uitvoert, zoals "filter de tabel op bestellingen boven $ 500 en exporteer de resultaten":
- Scraping van de Accessibility-Tree: De agent leest de accessibility-tree uit of maakt een schermafbeelding. Hij probeert de filterknop te raden tussen tientallen geneste
<div>- en<button>-elementen. - Kwetsbare Selectors: Zodra het frontend-team CSS-modules, Tailwind-classes of de DOM-hiërarchie aanpast, falen geautomatiseerde CSS-selectors onmiddellijk.
- Contextuele Overhead: Het verzenden van een complete DOM-structuur of schermafbeelding bij elke interactie kost tienduizenden tokens en veroorzaakt aanzienlijke netwerklatentie.
- Problemen met Synthetische Kliks: Het afvuren van synthetische
MouseEvent- ofKeyboardEvent-gebeurtenissen omzeilt vaak Reacts synthetische event-handlers, met verouderde closures (stale closures) of overgeslagen validaties tot gevolg.
WebMCP vervangt deze onzekere methoden door gestructureerde, programmatische RPC-aanroepen binnen de JavaScript-uitvoeringscontext van de client. In plaats van te gokken naar selectors, registreert de pagina expliciete tools met getypeerde JSON-schema's.
WebMCP versus Backend-MCP
Het onderscheid tussen MCP op de backend en WebMCP is fundamenteel:
| Dimensie | Backend-MCP (Node.js / Python) | WebMCP (Browser-Natief) |
|---|---|---|
| Uitvoeringscontext | Achtergronddaemon, container, serverless functie | Uitvoeringsthread van het actieve browsertabblad |
| Transport | stdio, Server-Sent Events (SSE), WebSockets |
Directe JavaScript-functiereferentie |
| Doelgegevens | Externe databases, bestandssystemen, API's van derden | React-status, client-router, lokale opslag, DOM |
| Ondersteunde Primitieven | Tools, Resources, Prompts | Uitsluitend Tools (gebonden aan het actieve document) |
| Beveiligingsgrens | Procesisolatie, OS-rechten, API-sleutels | Browser-sandbox, Same-Origin Policy, gebruikersbevestiging |
WebMCP maakt geen gebruik van netwerk-sockets of subprocessen. Het webdocument fungeert zelf als gereedschapsregister via de interface document.modelContext.
De Browser-Architectuur: document.modelContext
In recente Chromium-versies (vanaf Chromium 146 achter de vlag #enable-webmcp-testing) biedt de browser een in-memory tool-broker gekoppeld aan het globale document-object:
// Core interface for browser-native WebMCP
interface ModelContextTool {
name: string;
description: string;
inputSchema: Record<string, unknown>;
execute: (input: Record<string, unknown>) => Promise<Record<string, unknown>>;
annotations?: {
readOnlyHint?: boolean;
};
}
interface ModelContext {
registerTool(tool: ModelContextTool, options?: { signal?: AbortSignal }): void;
listTools(): Promise<ModelContextTool[]>;
}
declare global {
interface Document {
modelContext?: ModelContext;
}
}Twee architecturale regels beheersen deze interface:
- Documentbereik: Gereedschappen zijn gekoppeld aan de levenscyclus van het document. Zodra de gebruiker navigeert of het tabblad sluit, wordt het register vernietigd.
- Afmelden via AbortSignal: In plaats van een
unregisterTool-methode wordt de levenscyclus beheerd via het standaardAbortSignal. Wanneer het signaal wordt geaborteerd, verwijdert de browser het gereedschap automatisch.
Patroon 1: Declaratieve WebMCP met React 19-Formulieren
De meest directe manier om functionaliteit aan browser-agents aan te bieden is via de Declaratieve WebMCP-API. WebMCP breidt reguliere HTML-formulieren uit met annotaties:
toolname: De unieke identificatie van de tool.tooldescription: Een beknopte uitleg van de werking en wanneer de agent deze moet aanroepen.
In React 19 sluiten declaratieve formulieren naadloos aan op useActionState en Server/Client Actions.
"use client";
import React, { useActionState } from "react";
interface FilterState {
minAmount: number;
category: string;
status: "idle" | "applied";
}
async function applyFilterAction(
prevState: FilterState,
formData: FormData
): Promise<FilterState> {
const minAmount = Number(formData.get("minAmount") || 0);
const category = String(formData.get("category") || "all");
// Perform client-side filter computation or query
return {
minAmount,
category,
status: "applied",
};
}
export function AgenticOrderFilter() {
const [state, formAction, isPending] = useActionState(applyFilterAction, {
minAmount: 0,
category: "all",
status: "idle",
});
return (
<form
action={formAction}
// WebMCP Declarative Tool Annotations
toolname="filter-orders"
tooldescription="Filters the current order ledger by minimum dollar amount and product category."
className="filter-form"
>
<label htmlFor="minAmount">Minimum Amount ($)</label>
<input
id="minAmount"
name="minAmount"
type="number"
defaultValue={state.minAmount}
required
/>
<label htmlFor="category">Category</label>
<select id="category" name="category" defaultValue={state.category}>
<option value="all">All Categories</option>
<option value="hardware">Hardware</option>
<option value="software">Software</option>
</select>
<button type="submit" disabled={isPending}>
{isPending ? "Filtering..." : "Apply Filter"}
</button>
{state.status === "applied" && (
<p className="status-text">
Showing orders > ${state.minAmount} in category "{state.category}"
</p>
)}
</form>
);
}Wanneer een agent de pagina bezoekt, analyseert de browser de DOM op formulieren met een toolname-attribuut en genereert dynamisch een JSON-schema op basis van veldnamen, types en vereiste validaties. Zodra de agent filter-orders aanroept, stuurt de browser de gegevens rechtstreeks door naar Reacts formAction.
Patroon 2: De Imperatieve useWebMCPTool-Hook in React
Hoewel declaratieve formulieren volstaan voor standaardinvoer, vereisen geavanceerde applicaties imperatieve registratie. Een agent moet bijvoorbeeld het zoomniveau van een canvas kunnen wijzigen, een in-memory tabel doorzoeken of een meerstapsdialoog doorlopen.
Om dit op een nette React-conforme manier in te richten, bouwen we een aangepaste hook die drie vereisten adresseert:
- Dynamische Levenscyclus: Gereedschappen moeten registreren bij het mounten en verdwijnen bij het unmounten.
- Toegang tot Actuele State: De uitvoeringsfunctie moet altijd beschikken over de recentste props en state zonder continue herregistraties.
- Opruimen met AbortSignal: Nette afhandeling van resources bij SPA-routenavigatie.
Hier is de volledige implementatie van useWebMCPTool:
"use client";
import { useEffect, useRef } from "react";
export interface ToolDefinition<TInput = Record<string, unknown>, TOutput = Record<string, unknown>> {
name: string;
description: string;
inputSchema: Record<string, unknown>;
execute: (input: TInput) => Promise<TOutput>;
readOnlyHint?: boolean;
}
/**
* Registers an imperative tool with document.modelContext, ensuring
* safe teardown with AbortSignal and fresh closure references.
*/
export function useWebMCPTool<TInput = Record<string, unknown>, TOutput = Record<string, unknown>>(
tool: ToolDefinition<TInput, TOutput>,
enabled: boolean = true
) {
// Store the latest executor in a ref to avoid re-registering on every state change
const executeRef = useRef(tool.execute);
useEffect(() => {
executeRef.current = tool.execute;
});
useEffect(() => {
// Feature detection: Check for browser WebMCP support
if (typeof document === "undefined" || !document.modelContext || !enabled) {
return;
}
const abortController = new AbortController();
try {
document.modelContext.registerTool(
{
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
execute: async (args: Record<string, unknown>) => {
return await executeRef.current(args as TInput);
},
annotations: tool.readOnlyHint ? { readOnlyHint: true } : undefined,
},
{ signal: abortController.signal }
);
} catch (err) {
console.warn(`[WebMCP] Failed to register tool "${tool.name}":`, err);
}
// Teardown: AbortSignal unregisters the tool automatically
return () => {
abortController.abort();
};
}, [tool.name, tool.description, tool.readOnlyHint, enabled]);
}Praktijkvoorbeeld in een Component
In het onderstaande voorbeeld tonen we een interactieve documentviewer waarin de agent van pagina kan wisselen en de huidige selectie kan uitlezen:
"use client";
import React, { useState, useCallback } from "react";
import { useWebMCPTool } from "./useWebMCPTool";
interface DocumentViewerProps {
totalPages: number;
documentTitle: string;
}
export function DocumentViewer({ totalPages, documentTitle }: DocumentViewerProps) {
const [currentPage, setCurrentPage] = useState<number>(1);
const [selection, setSelection] = useState<string>("");
// Tool 1: Jump to a specific page (Mutating action)
useWebMCPTool({
name: "navigate-document-page",
description: "Navigates the interactive PDF viewer to a specific page number.",
inputSchema: {
type: "object",
properties: {
pageNumber: {
type: "integer",
minimum: 1,
maximum: totalPages,
description: "Target page index to display",
},
},
required: ["pageNumber"],
},
execute: async (args: { pageNumber: number }) => {
if (args.pageNumber < 1 || args.pageNumber > totalPages) {
return {
success: false,
error: `Page ${args.pageNumber} out of bounds (1-${totalPages}).`,
};
}
setCurrentPage(args.pageNumber);
return {
success: true,
activePage: args.pageNumber,
documentTitle,
};
},
});
// Tool 2: Read current selection (Read-only query)
useWebMCPTool({
name: "get-active-selection",
description: "Retrieves the currently highlighted text snippet in the viewer.",
inputSchema: {
type: "object",
properties: {},
},
readOnlyHint: true,
execute: async () => {
return {
hasSelection: selection.length > 0,
text: selection,
pageNumber: currentPage,
};
},
});
return (
<div className="viewer-container">
<header>
<h3>{documentTitle}</h3>
<span>Page {currentPage} of {totalPages}</span>
</header>
<main
onMouseUp={() => {
const selectedText = window.getSelection()?.toString() || "";
setSelection(selectedText);
}}
className="document-canvas"
>
<p>Displaying page content for page {currentPage}...</p>
</main>
</div>
);
}Patroon 3: Beveiligingsmaatregelen en Menselijke Bevestiging (Human-in-the-Loop)
Het direct openstellen van client-functies voor AI-agents brengt operationele en veiligheidsrisico's met zich mee:
- Een agent zou per ongeluk niet-opgeslagen formulieren kunnen wissen of ongewenste transacties kunnen starten.
- Schadelijke prompt injections in bekeken webteksten kunnen proberen gevoelige tools ongeoorloofd aan te sturen.
WebMCP biedt twee methoden om deze risico's te beheersen: Read-Only Hints (readOnlyHint) en Interactieve Bevestigingsbarrières.
1. Read-Only Hints
Voorzie opvragende tools altijd van annotations: { readOnlyHint: true }. Dit laat de agent en de browser weten dat de aanroep geen bijwerkingen heeft, waardoor het model gerichte informatie kan verzamelen zonder voortdurend om toestemming te vragen.
2. Het Bevestigingsbarrièrepatroon
Bij wijzigende of gevoelige acties mag de promise van het gereedschap niet meteen resolveren. In plaats daarvan toont de React-interface een expliciet bevestigingsdialoogvenster aan de gebruiker. De promise resolveert pas zodra de gebruiker op "Bevestigen" klikt.
"use client";
import React, { useState, useRef } from "react";
import { useWebMCPTool } from "./useWebMCPTool";
interface PendingAction {
id: string;
description: string;
resolve: (value: { approved: boolean }) => void;
}
export function DestructiveActionShield() {
const [pendingAction, setPendingAction] = useState<PendingAction | null>(null);
useWebMCPTool({
name: "delete-active-workspace",
description: "Permanently deletes the current active workspace. Requires user confirmation.",
inputSchema: {
type: "object",
properties: {
reason: { type: "string", description: "Reason for deletion" },
},
required: ["reason"],
},
execute: async (input: { reason: string }) => {
// Pause tool execution and wait for manual user approval
return new Promise((resolve) => {
setPendingAction({
id: crypto.randomUUID(),
description: `Delete workspace: "${input.reason}"`,
resolve,
});
});
},
});
return (
<>
{pendingAction && (
<aside className="confirmation-modal" role="alertdialog">
<h4>Agent Action Confirmation</h4>
<p>An AI assistant requested permission to:</p>
<blockquote>{pendingAction.description}</blockquote>
<div className="button-row">
<button
type="button"
onClick={() => {
pendingAction.resolve({ approved: false });
setPendingAction(null);
}}
>
Reject
</button>
<button
type="button"
className="danger-btn"
onClick={() => {
pendingAction.resolve({ approved: true });
setPendingAction(null);
}}
>
Confirm Deletion
</button>
</div>
</aside>
)}
</>
);
}Patroon 4: Lokale Autonome Loop: WebMCP + Chrome Gemini Nano
Een bijzonder krachtige toepassing van WebMCP ontstaat in combinatie met on-device taalmodellen. Door de W3C Prompt API (window.LanguageModel) te koppelen aan document.modelContext, realiseert u een volledig lokale agent met minimale latentie binnen het tabblad.
+-------------------------------------------------------------+
| Browser Tab Memory Space |
| |
| [ User Goal ] ---> [ window.LanguageModel Session ] |
| | |
| v |
| [ Tool Call JSON Output ] |
| | |
| v |
| [ document.modelContext.execute ] |
| | |
| v |
| [ React State Mutates DOM ] |
| | |
| v |
| (Observation Result fed back to model) |
+-------------------------------------------------------------+// Client-side agent loop executing in the browser tab
export async function runLocalTabAgent(userPrompt: string): Promise<string> {
// 1. Verify availability of on-device LLM and WebMCP
if (!("ai" in window) || !document.modelContext) {
throw new Error("On-device AI or WebMCP is not supported in this browser.");
}
// 2. Discover available tools exposed by mounted React components
const availableTools = await document.modelContext.listTools();
// Format tool descriptions for system prompt
const toolDeclarations = availableTools.map((t) => ({
name: t.name,
description: t.description,
parameters: t.inputSchema,
}));
// 3. Instantiate local session with Gemini Nano
const session = await (window as unknown as {
ai: {
languageModel: {
create: (options: { systemPrompt: string }) => Promise<{
prompt: (msg: string) => Promise<string>;
destroy: () => void;
}>;
};
};
}).ai.languageModel.create({
systemPrompt: `You are an in-tab browser assistant. You can control the active page using tools: ${JSON.stringify(
toolDeclarations
)}. Output JSON tool invocations as {"tool": string, "args": object}.`,
});
try {
const response = await session.prompt(userPrompt);
// Parse structured tool call
const action = JSON.parse(response);
const targetTool = availableTools.find((t) => t.name === action.tool);
if (targetTool) {
const result = await targetTool.execute(action.args);
return `Tool executed successfully: ${JSON.stringify(result)}`;
}
return response;
} finally {
session.destroy();
}
}Deze volledige cyclus draait op de lokale processor:
- De tijd tot het eerste token (TTFT) ligt onder de 30 milliseconden.
- Er worden geen gegevens naar externe servers verzonden, in overeenstemming met AVG-vereisten.
- Er zijn geen terugkerende cloud-inferentiekosten.
Productiepraktijk en Progressive Enhancement
Zolang de WebMCP-specificatie in ontwikkeling is bij normalisatie-instanties, dienen productie-applicaties deze te benaderen volgens het principe van geleidelijke verbetering (progressive enhancement):
- Feature-Detectie: Controleer altijd op de aanwezigheid van
document.modelContext. De React-applicatie moet vlekkeloos functioneren voor menselijke gebruikers, ook wanneer de browser geen agent-API's ondersteunt. - Compacte Schema's: Lokale modellen beschikken over een kleiner contextvenster (meestal 4k tot 8k tokens) dan grote cloud-modellen. Houd parameterbeschrijvingen beknopt en vermijd diepe JSON-nesting.
- Atomaire Statusupdates: Zorg ervoor dat tool-acties pas resolveren nadat React alle state-updates in de DOM heeft afgerond. Dit voorkomt race conditions wanneer een agent meerdere acties achter elkaar uitvoert.
Door React 19-formulierbouwstenen te combineren met WebMCP-hooks kunnen developers statische interfaces omvormen tot dynamische actievelden voor moderne AI-agents.