WebMCP in React: Esporre Stato Client e Azioni dei Moduli agli Agenti AI del Browser
Rendi le applicazioni React 19 operabili da agenti AI integrando lo standard W3C WebMCP, attributi dichiarativi dei form e hook useWebMCPTool.

Nell'ambito dell'integrazione tra modelli linguistici e sistemi esterni, il Model Context Protocol (MCP) si è affermato come lo standard de facto per l'invocazione di strumenti computazionali. Tuttavia, la quasi totalità delle implementazioni MCP attuali adotta un'architettura client-server tradizionale: un client (come Claude Desktop o un assistente IDE) comunica con un processo in background via stdio o Server-Sent Events (SSE) per interrogare database, invocare API cloud o esplorare file system.
Questo paradigma incontra un limite strutturale all'interno del browser.
Le applicazioni web contemporanee sono ricche Single Page Applications (SPA) con stato persistente nel client. Informazioni operative cruciali—selezioni su canvas, campi non salvati di un form, paginazione locale, filtri attivi e finestre modali interattive—risiedono interamente nella memoria del client (stato di React, store Zustand o DOM). Quando un agente autonomo basato su browser o un'estensione tenta di interagire con tali applicazioni, deve affidarsi a fragili tecniche di scraping visuale del DOM, analisi dell'albero di accessibilità o simulazione di clic del mouse.
La specifica emergente WebMCP (Web Model Context Protocol)—sviluppata all'interno del W3C Web Machine Learning Community Group e disponibile in anteprima sperimentale su Chromium—risolve questa criticità integrando il Model Context Protocol direttamente nella scheda del browser.
Questa guida illustra il funzionamento di WebMCP, ne definisce le differenze rispetto a MCP lato server e descrive come esporre lo stato dei componenti React 19 e le azioni dei form agli agenti AI del browser tramite attributi HTML dichiarativi e hook React dedicati.
+-------------------------------------------------------------------------+
| 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 |
+-------------------------------------------------------------------------+Il Vuoto degli Strumenti Lato Client
Per comprendere l'utilità di WebMCP, consideriamo come un agente autonomo tenti oggi di eseguire un'operazione ordinaria come "filtrare la tabella per ordini superiori a 500 $ ed esportare i risultati":
- Scraping dell'albero di accessibilità: L'agente analizza l'albero di accessibilità o cattura schermate, cercando di individuare il pulsante del filtro tra decine di tag
<div>e<button>annidati. - Fragilità dei selettori: Qualsiasi aggiornamento ai moduli CSS, alle utility Tailwind o alla struttura del DOM rende obsoleti i selettori automatici.
- Consumo di contesto: Inviare interi alberi DOM o snapshot visivi a ogni passaggio consuma decine di migliaia di token e introduce latenze di rete sensibili.
- Problemi con clic sintetici: L'emissione di eventi
MouseEventoKeyboardEventaggira frequentemente i gestori sintetici di React, provocando chiusure non sincronizzate (stale closures) o bypassando i controlli di validazione.
WebMCP sostituisce queste euristiche instabili con chiamate RPC strutturate ed eseguite direttamente nel thread del client. La pagina web registra strumenti espliciti con schemi JSON tipizzati.
WebMCP a confronto con MCP Backend
La distinzione tra MCP lato server e WebMCP è sostanziale:
| Dimensione | MCP Backend (Node.js / Python) | WebMCP (Nativo nel Browser) |
|---|---|---|
| Ambiente di Esecuzione | Demone di sistema, container, funzione serverless | Thread di esecuzione della scheda attiva |
| Meccanismo di Trasporto | stdio, Server-Sent Events (SSE), WebSockets |
Riferimento diretto a funzione JavaScript |
| Dati di Destinazione | Database remoti, file system, API esterne | Stato di React, router client, local storage, DOM |
| Primitive Supportate | Tools, Resources, Prompts | Soltanto Tools (vincolate al documento attivo) |
| Perimetro di Sicurezza | Isolamento a livello di processo, permessi OS, chiavi API | Sandbox del browser, Same-Origin Policy, conferma dell'utente |
WebMCP non adopera socket di rete né processi figli: il documento web funge direttamente da registro attraverso l'interfaccia document.modelContext.
L'Architettura del Browser: document.modelContext
Nelle versioni recenti di Chromium (a partire da Chromium 146 con il flag #enable-webmcp-testing), il browser espone un gestore di strumenti agganciato all'oggetto globale 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;
}
}Due principi strutturali regolano questo meccanismo:
- Ambito del Documento: Gli strumenti sono strettamente legati al ciclo di vita del documento. Se l'utente chiude la scheda o cambia URL, il registro viene smantellato.
- Disiscrizione mediante AbortSignal: In luogo di un metodo
unregisterTooldedicato, il ciclo di vita si governa tramiteAbortSignal. Quando il segnale viene interrotto, il browser rimuove automaticamente lo strumento registrato.
Modello 1: WebMCP Dichiarativo con i Form di React 19
Il metodo più immediato per abilitare gli agenti consiste nell'uso dell'API Dichiarativa di WebMCP. Essa estende i comuni form HTML mediante attributi specifici:
toolname: Identificatore univoco dello strumento.tooldescription: Spiegazione sintetica dello scopo dell'azione e di quando l'agente deve richiamarla.
In React 19, i form dichiarativi si collegano direttamente a useActionState e alle 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>
);
}Quando un agente accede alla pagina, il motore del browser esamina il DOM, individua i form recanti l'attributo toolname e genera uno schema tipizzato partendo da nomi, tipi e vincoli dei campi. Nel momento in cui l'agente chiama filter-orders, il browser inoltra l'invocazione al gestore formAction di React.
Modello 2: L'Hook Imperativo useWebMCPTool in React
Mentre i form dichiarativi sono indicati per interazioni convenzionali, le interfacce complesse necessitano di una registrazione imperativa degli strumenti. Un agente potrebbe dover modificare il fattore di zoom su una tela, interrogare una tabella in memoria o avanzare lungo un flusso a passaggi multipli.
Per realizzare un'integrazione coerente in React, implementiamo un hook dedicato in grado di soddisfare tre condizioni essenziali:
- Gestione Dinamica del Ciclo di Vita: Gli strumenti devono registrarsi al montaggio e revocarsi allo smontaggio del componente.
- Accesso a Dati Aggiornati: L'esecutore dell'azione deve poter leggere le prop e lo stato più recenti senza costringere a ri-registrazioni continue.
- Smaltimento con AbortSignal: Rilascio pulito delle risorse durante i cambi di rotta nelle SPA.
Di seguito l'implementazione completa di 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]);
}Esempio Pratico di Utilizzo
L'esempio seguente mostra un visualizzatore di documenti in cui l'agente può cambiare pagina ed estrarre il testo attualmente selezionato:
"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>
);
}Modello 3: Presidi di Sicurezza e Approvazione dell'Utente (Human-in-the-Loop)
Consentire a un agente AI di eseguire codice all'interno del client comporta responsabilità operative e di sicurezza:
- Un agente potrebbe eliminare dati non salvati o avviare transazioni non volute.
- Tentativi di prompt injection contenuti nel testo di siti esterni potrebbero mirare a forzare l'esecuzione di azioni riservate.
WebMCP mitiga tali scenari mediante due strumenti: l'indicazione di sola lettura (readOnlyHint) e le barriere di conferma interattive.
1. Indicazioni di Sola Lettura
Per gli strumenti puramente informativi, è opportuno impostare annotations: { readOnlyHint: true }. Ciò informa l'agente e il runtime del browser che l'operazione non genera mutazioni di stato, permettendo al modello di concatenare le proprie richieste conoscitive senza richiedere autorizzazioni superflue.
2. Il Pattern della Barriera di Conferma
Per azioni che modificano o cancellano dati rilevanti, è preferibile non risolvere immediatamente la promise restituita dall'azione. Al contrario, l'hook mostra una schermata o un dialogo modale di conferma all'utente. La promise dello strumento si risolverà unicamente dopo l'approvazione esplicita.
"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>
)}
</>
);
}Modello 4: Ciclo Locale Autonomo: WebMCP + Chrome Gemini Nano
Uno dei traguardi più rilevanti di WebMCP risiede nell'integrazione con i modelli linguistici residenti sul dispositivo. Unendo la Prompt API del W3C (window.LanguageModel) e document.modelContext, è possibile creare un agente autonomo locale e rapido confinato nella scheda attiva.
+-------------------------------------------------------------+
| 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();
}
}Questa pipeline viene eseguita per intero sul dispositivo:
- Il tempo di risposta per il primo token (TTFT) scende sotto i 30 millisecondi.
- Nessun dato sensibile esce dal dispositivo, ottemperando ai requisiti di conformità GDPR.
- I costi di infrastruttura cloud legati all'inferenza rimangono pari a zero.
Considerazioni di Produzione e Miglioramento Progressivo
Finché la specifica WebMCP non sarà stabilizzata, le applicazioni di produzione dovrebbero considerarla secondo il principio del miglioramento progressivo (progressive enhancement):
- Rilevamento delle Funzionalità: Verificare sempre l'esistenza di
document.modelContext. L'applicazione React deve garantire la normale operatività agli utenti umani a prescindere dal supporto per agenti AI da parte del browser. - Schemi Concisi: I modelli su dispositivo dispongono di finestre di contesto contenute (tipicamente tra 4k e 8k token). È importante mantenere brevi le descrizioni ed evitare strutture JSON troppo annidate.
- Aggiornamenti di Stato Atomici: Accertarsi che le azioni completino la propria promise soltanto dopo che React ha terminato il rendering del nuovo stato nel DOM, evitando condizioni di concorrenza se un agente esegue più strumenti consecutivamente.
Unendo i form dichiarativi di React 19 con gli hook imperativi di WebMCP, gli sviluppatori possono convertire interfacce web statiche in superfici pronte per la cooperazione con agenti intelligenti.