WebMCP dans React : Exposer l'État Client et les Actions de Formulaire aux Agents IA
Transformez les applications web React 19 en surfaces exploitables par des agents grâce au standard W3C WebMCP, aux formulaires déclaratifs et aux hooks useWebMCPTool.

Lorsque les ingénieurs logiciels connectent des modèles de langage à des systèmes tiers, le Model Context Protocol (MCP) s'est imposé comme le protocole de référence pour l'exécution d'outils. Toutefois, la quasi-totalité des architectures MCP existantes repose sur une relation client-serveur : un client (tel que Claude Desktop ou une extension d'IDE) échange avec un processus d'arrière-plan via stdio ou Server-Sent Events (SSE) pour interroger des bases de données ou appeler des API cloud.
Ce modèle montre ses limites dès lors qu'il s'applique au navigateur web.
Les applications web contemporaines sont des Single Page Applications (SPA) riches gérant des états complexes. Une grande partie de cet état—les sélections actives sur canvas, les formulaires en cours de saisie, la pagination locale, les paramètres de tri ou les fenêtres modales ouvertes—réside uniquement dans la mémoire vive du client (état des composants React, stores Zustand ou DOM). Lorsqu'un agent autonome ou une extension de navigateur tente d'interagir avec l'application, il en est réduit à analyser le DOM visuel, inspecter l'arbre d'accessibilité ou simuler des clics de souris artificiels.
La spécification émergente WebMCP (Web Model Context Protocol)—élaborée au sein du W3C Web Machine Learning Community Group et disponible en préversion dans Chromium—résout ce problème en intégrant le Model Context Protocol au sein même de l'onglet du navigateur.
Ce guide examine le fonctionnement de WebMCP, détaille ses différences par rapport au MCP côté serveur et montre comment exposer l'état et les actions de formulaire React 19 aux agents de navigateur grâce à des attributs HTML déclaratifs et des hooks React idiomatiques.
+-------------------------------------------------------------------------+
| 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 |
+-------------------------------------------------------------------------+Le Vide d'Outillage Côté Client
Pour mesurer l'intérêt de WebMCP, observons la façon dont un agent autonome tente aujourd'hui d'effectuer une tâche simple, telle que « filtrer le tableau pour afficher les commandes supérieures à 500 $ et les exporter » :
- Extraction de l'arbre d'accessibilité : L'agent parcourt l'arbre d'accessibilité ou capture l'écran. Il doit identifier le bouton de filtre parmi des dizaines de balises
<div>et<button>imbriquées. - Fragilité des sélecteurs : Toute modification des modules CSS, des classes Tailwind ou de la hiérarchie du DOM invalide immédiatement les sélecteurs automatisés.
- Poids du contexte : Transmettre l'intégralité du DOM ou des instantanés visuels à chaque étape consomme des dizaines de milliers de jetons et ajoute plusieurs secondes de latence réseau.
- Limites des clics synthétiques : L'émission d'événements
MouseEventsynthétiques contourne fréquemment le système d'événements de React, provoquant des fermetures obsolètes (stale closures) ou le contournement des validations.
WebMCP remplace ces approximations par des appels RPC structurés et typés exécutés au cœur du contexte d'exécution du client. Au lieu de déduire des sélecteurs, la page enregistre des outils explicites décrits par des schémas JSON stricts.
WebMCP face au MCP Serveur
La séparation entre le MCP serveur et WebMCP est fondamentale :
| Dimension | MCP Serveur (Node.js / Python) | WebMCP (Natif au Navigateur) |
|---|---|---|
| Contexte d'Exécution | Démon système, conteneur, fonction serverless | Thread d'exécution de l'onglet actif |
| Transport | stdio, Server-Sent Events (SSE), WebSockets |
Référence de fonction JavaScript directe |
| Données Ciblées | Bases de données distantes, fichiers disque, API tierces | État React, routeur client, stockage local, DOM |
| Primitives Supportées | Tools, Resources, Prompts | Uniquement Tools (liés au document actif) |
| Frontière de Sécurité | Isolation de processus, droits OS, clés d'API | Sandbox du navigateur, Same-Origin Policy, accord utilisateur |
WebMCP n'utilise aucun socket réseau ni processus enfant. Le document web sert directement de registre d'outils via l'interface document.modelContext.
L'Architecture Navigateur : document.modelContext
Dans les versions Chromium récentes (dès Chromium 146 avec le flag #enable-webmcp-testing), le navigateur expose un gestionnaire d'outils accessible depuis l'objet global document :
// 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;
}
}Cette interface obéit à deux règles architecturales :
- Portée Documentaire : Les outils sont rattachés au cycle de vie du document. Si l'utilisateur navigue vers une autre URL ou ferme l'onglet, le registre est détruit.
- Désenregistrement par AbortSignal : Au lieu d'une méthode
unregisterTool, la gestion du cycle de vie repose sur la primitive standardAbortSignal. Dès que le signal s'interrompt, le navigateur désenregistre l'outil proprement.
Motif 1 : WebMCP Déclaratif avec les Formulaires React 19
La manière la plus directe de rendre une fonctionnalité accessible aux agents est l'API Déclarative WebMCP. Celle-ci enrichit les formulaires HTML traditionnels d'attributs dédiés :
toolname: L'identifiant unique de l'outil.tooldescription: Une description synthétique du comportement attendu et des cas d'usage.
Dans React 19, les formulaires déclaratifs s'articulent naturellement avec useActionState et les 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>
);
}Lorsqu'un agent consulte la page, le navigateur analyse le DOM, repère les formulaires pourvus d'un attribut toolname et génère un schéma JSON basé sur les noms des champs, leurs types et les contraintes de validation. Dès que l'agent invoque filter-orders, le navigateur transmet l'appel au gestionnaire formAction de React.
Motif 2 : Le Hook Impératif useWebMCPTool dans React
Si les formulaires déclaratifs conviennent aux formulaires courants, les fonctionnalités avancées nécessitent un enregistrement impératif. Un agent peut avoir besoin de modifier le zoom d'un canvas, d'interroger un index en mémoire ou d'orchestrer un parcours en plusieurs étapes.
Pour intégrer cela de façon idiomatique dans React, nous concevons un hook personnalisé répondant à trois exigences :
- Cycle de Vie Dynamique : Les outils doivent être enregistrés au montage du composant et retirés lors de son démontage.
- Accès aux États Frais : Le callback doit accéder aux props et à l'état les plus récents sans forcer un réenregistrement perpétuel.
- Nettoyage par AbortSignal : Libération propre des ressources lors des navigations SPA.
Voici l'implémentation complète du hook 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]);
}Application dans un Composant Concret
Le composant suivant implémente une visionneuse interactive de documents permettant à l'agent de changer de page et d'extraire la sélection active :
"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>
);
}Motif 3 : Garde-fous et Validation Humaine (Human-in-the-Loop)
Permettre aux agents IA de déclencher directement des fonctions dans le navigateur impose des précautions de sécurité strictes :
- Un agent pourrait valider une suppression de données critiques ou une transaction irréversible sans contrôle.
- Des attaques par injection de prompts dissimulées dans des pages web pourraient tenter de manipuler des outils locaux.
WebMCP propose deux leviers de protection : les indications de lecture seule (readOnlyHint) et les barrières d'approbation humaine.
1. Les Indications de Lecture Seule
Lors de l'enregistrement d'outils d'interrogation, incluez toujours annotations: { readOnlyHint: true }. Cela informe l'agent et le moteur du navigateur que l'opération est dénuée d'effets secondaires, permettant au modèle d'enchaîner ses requêtes d'information sans exiger d'accord préalable.
2. Le Modèle de Barrière d'Approbation
Pour les opérations destructives ou sensibles, il ne faut pas résoudre la promesse de l'outil immédiatement. Le hook met plutôt l'interface React en attente et présente un dialogue modal à l'utilisateur. La promesse ne se dénoue que lorsque l'utilisateur clique sur « Confirmer ».
"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>
)}
</>
);
}Motif 4 : Boucle Locale Autonome : WebMCP + Chrome Gemini Nano
L'un des apports majeurs de WebMCP réside dans sa complémentarité avec les modèles de langage embarqués. En associant la Prompt API du W3C (window.LanguageModel) à document.modelContext, on met en place un agent autonome fonctionnant à 100 % dans l'onglet.
+-------------------------------------------------------------+
| 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();
}
}L'ensemble du traitement s'exécute sur le processeur local :
- Le délai d'émission du premier jeton (TTFT) reste inférieur à 30 millisecondes.
- Aucune donnée ne quitte le poste de travail, respectant ainsi le RGPD et les exigences strictes de confidentialité.
- Aucun coût récurrent d'infrastructure d'inférence n'est engagé.
Bonnes Pratiques en Production et Amélioration Progressive
Dans l'attente de la ratification définitive de WebMCP, les applications de production doivent aborder cette technologie comme une amélioration progressive (progressive enhancement) :
- Détection de Présence : Encadrez systématiquement l'usage de
document.modelContext. L'application React doit rester parfaitement utilisable pour un humain, qu'un agent soit présent ou non. - Schémas Conçus pour la Concision : Les modèles locaux fonctionnent avec des fenêtres de contexte réduites (souvent de 4k à 8k jetons). Privilégiez des descriptions précises et bannissez les schémas JSON profondément imbriqués.
- Mises à Jour Atomiques : Veillez à ce que les promesses d'outils ne soient résolues qu'une fois les états React répercutés dans le DOM. Si un agent appelle un outil B consécutivement à un outil A, l'outil B doit impérativement observer l'état consolidé pour éviter tout problème de concurrence.
En conjuguant les formulaires React 19 et les hooks WebMCP, les équipes d'ingénierie peuvent faire évoluer leurs interfaces web en espaces d'action directement pilotables par les agents d'IA.