WebMCP en React: Exponiendo Estado del Cliente y Acciones de Formularios a Agentes de IA
Convierte aplicaciones web en React 19 en superficies operables por agentes mediante el estándar W3C WebMCP, atributos declarativos de formulario y hooks useWebMCPTool.

Cuando los ingenieros de software integran modelos de lenguaje con sistemas externos, el Model Context Protocol (MCP) se ha convertido rápidamente en el formato estándar para la invocación de herramientas. Sin embargo, casi todas las implementaciones existentes de MCP asumen una relación cliente-servidor: un cliente (como Claude Desktop o un asistente de IDE) se comunica con un proceso en segundo plano a través de stdio o Server-Sent Events (SSE) para consultar bases de datos, invocar APIs en la nube o inspeccionar árboles de archivos.
Ese modelo no encaja dentro del navegador.
Las aplicaciones web modernas son Single Page Applications (SPAs) ricas y con estado. El estado crítico—como selecciones activas en canvas, entradas de formularios no guardadas, paginación del cliente, parámetros de ordenación y diálogos modales interactivos—reside estrictamente en la memoria del cliente (estado de componentes React, almacenes Zustand o el DOM). Cuando un agente o extensión de navegador autónomo intenta interactuar con una aplicación web, se ve obligado a recurrir a técnicas frágiles como el scraping del DOM visual, el análisis del árbol de accesibilidad o clics sintéticos simulados.
La especificación emergente WebMCP (Web Model Context Protocol)—desarrollada dentro del W3C Web Machine Learning Community Group y disponible en Chromium de forma preliminar—resuelve este problema trasladando el Model Context Protocol directamente a la pestaña del navegador.
Esta guía explica cómo opera WebMCP, en qué se diferencia del MCP del lado del servidor y cómo exponer el estado de componentes React 19 y las acciones de formulario a agentes dentro del navegador utilizando atributos HTML declarativos y hooks idiomáticos de React.
+-------------------------------------------------------------------------+
| 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 |
+-------------------------------------------------------------------------+El Vacío de Herramientas en el Lado del Cliente
Para entender por qué WebMCP es indispensable, consideremos cómo un agente autónomo intenta actualmente ejecutar una tarea directa como "filtrar la tabla para mostrar pedidos superiores a $500 y exportarlos":
- Scraping del Árbol de Accesibilidad: El agente consulta el árbol de accesibilidad o captura una captura de pantalla. Intenta identificar el botón de filtro entre decenas de elementos
<div>y<button>anidados. - Fragilidad de Selectores: Si el equipo actualiza módulos CSS, clases de utilidad Tailwind o jerarquías del DOM, los selectores CSS automatizados fallan de inmediato.
- Sobrecarga de Contexto: Enviar un árbol DOM de varios megabytes o una imagen a un LLM en cada interacción consume decenas de miles de tokens y añade segundos de latencia de red.
- Problemas con Clics Sintéticos: Disparar eventos sintéticos
MouseEventoKeyboardEventsuele eludir los manejadores de eventos sintéticos de React, causando cierres obsoletos (stale closures) o validaciones omitidas.
WebMCP sustituye la interacción DOM sintética por un RPC estructurado y programático ejecutado directamente dentro del hilo del cliente. En lugar de deducir selectores, la página web registra herramientas explícitas con esquemas JSON tipados.
WebMCP frente a MCP de Backend
La distinción entre MCP en el backend y WebMCP es fundamental:
| Dimensión | MCP Backend (Node.js / Python) | WebMCP (Nativo del Navegador) |
|---|---|---|
| Contexto de Ejecución | Demonio en segundo plano, contenedor, función serverless | Hilo de ejecución de la pestaña activa del navegador |
| Transporte | stdio, Server-Sent Events (SSE), WebSockets |
Referencia directa a función JavaScript |
| Datos de Destino | Bases de datos remotas, sistemas de archivos, APIs de terceros | Estado de React, router del cliente, almacenamiento local, DOM |
| Primitivas Soportadas | Tools, Resources, Prompts | Solo Tools (restringidas al documento activo) |
| Límite de Seguridad | Aislamiento de procesos, permisos del SO, claves de API | Sandbox del navegador, Same-Origin Policy, confirmación del usuario |
WebMCP no utiliza sockets de red ni procesos secundarios. El propio documento web actúa como registro de herramientas a través de la interfaz document.modelContext.
La Arquitectura del Navegador: document.modelContext
En las implementaciones de Chromium (disponibles a partir de Chromium 146 con el flag #enable-webmcp-testing), el navegador expone un intermediario de herramientas en memoria accesible en el objeto 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;
}
}Dos principios arquitectónicos rigen esta interfaz:
- Alcance de Documento: Las herramientas están vinculadas al ciclo de vida del documento. Cuando un usuario navega a otra URL o cierra la pestaña, el registro se destruye.
- Desregistro mediante AbortSignal: En lugar de exponer un método
unregisterTool, el ciclo de vida de cada herramienta se gestiona con la primitiva estándarAbortSignal. Al abortar la señal, el navegador desregistra la herramienta de forma limpia.
Patrón 1: WebMCP Declarativo con Formularios de React 19
La forma más directa de exponer funcionalidad a los agentes del navegador es la API Declarativa de WebMCP. WebMCP extiende los formularios HTML estándar con anotaciones para agentes:
toolname: El identificador único de la herramienta.tooldescription: Una explicación concisa del comportamiento de la herramienta y cuándo debe invocarla el agente.
En React 19, los formularios declarativos se integran con useActionState y las 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>
);
}Cuando un agente accede a la página, el navegador inspecciona el DOM buscando formularios con atributos toolname y deriva un esquema dinámico de herramienta a partir de los nombres, tipos y restricciones de los campos de entrada. Cuando el agente ejecuta filter-orders, el navegador canaliza el envío directamente al gestor formAction de React.
Patrón 2: El Hook Imperativo useWebMCPTool en React
Aunque los formularios declarativos resultan idóneos para entradas estándar, las aplicaciones complejas demandan el registro imperativo de herramientas. Por ejemplo, un agente puede necesitar alternar una capa de zoom en canvas, consultar una tabla en memoria o avanzar un asistente multipaso.
Para lograr una integración idiomática en React, implementamos un hook personalizado que solventa tres requisitos esenciales:
- Ciclo de Vida Dinámico: Las herramientas deben registrarse al montar el componente y eliminarse al desmontar.
- Acceso a Cierres Actualizados: El ejecutor de la herramienta debe acceder a las props y estado más recientes sin desencadenar re-registros continuos.
- Limpieza con AbortSignal: Liberación de recursos ordenada durante las transiciones de rutas en SPAs.
A continuación se muestra la implementación completa de 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]);
}Uso del Hook en un Componente Real
A continuación se muestra un visor interactivo de documentos donde el agente puede buscar páginas y saltar directamente a un índice determinado:
"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>
);
}Patrón 3: Barreras de Seguridad y Confirmación con Intervención Humana
Habilitar herramientas del lado del cliente para agentes de IA plantea consideraciones de seguridad:
- Un agente podría ejecutar una acción destructiva (como borrar datos de un formulario sin guardar o procesar una orden de pago).
- Ataques de inyección de prompts contenidos en textos web podrían inducir al asistente a invocar herramientas sensibles.
WebMCP ofrece dos mecanismos para gobernar este comportamiento: Sugerencias de Solo Lectura (readOnlyHint) y Barreras de Confirmación con Intervención Humana.
1. Sugerencias de Solo Lectura
Al registrar herramientas de consulta, añade siempre annotations: { readOnlyHint: true }. Esto comunica al agente y al entorno de ejecución del navegador que la herramienta carece de efectos secundarios, permitiendo al modelo planificar consultas consecutivas de recopilación sin solicitar aprobación explícita.
2. El Patrón de Barrera de Confirmación
Para mutaciones de estado destructivas o críticas, evita resolver la promesa de la herramienta de inmediato. En su lugar, activa un estado en React que muestre al usuario un modal o diálogo de confirmación explícito. La promesa de la herramienta solo se resuelve una vez que el usuario hace clic en "Confirmar".
"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>
)}
</>
);
}Patrón 4: Agente Local en Bucle Cerrado: WebMCP + Chrome Gemini Nano
Una de las aplicaciones más destacadas de WebMCP es cerrar el ciclo con modelos de lenguaje en el dispositivo. Combinando la Prompt API del W3C (window.LanguageModel) con document.modelContext, se estructura un bucle autónomo completamente local y con latencia mínima.
+-------------------------------------------------------------+
| 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();
}
}Esta ejecución opera íntegramente en el hardware local:
- El tiempo hasta el primer token (TTFT) se mantiene por debajo de 30 milisegundos.
- Ningún dato abandona el dispositivo del usuario, cumpliendo normativas estrictas de privacidad y GDPR.
- El coste marginal en infraestructura de inferencia en la nube es cero.
Realidades de Producción y Mejora Progresiva
A medida que la especificación WebMCP madura en los comités de estándares, las aplicaciones web en producción deben enfocarla como una mejora progresiva (progressive enhancement):
- Detección de Características: Comprueba siempre la presencia de
document.modelContext. Los componentes React deben operar sin fricción para los usuarios humanos independientemente de si el navegador admite APIs de agentes. - Esquemas Compactos: Los modelos que se ejecutan en dispositivos locales disponen de ventanas de contexto más reducidas (a menudo entre 4k y 8k tokens) que los clústeres en la nube. Conviene mantener descripciones de parámetros concisas y evitar esquemas JSON excesivamente anidados.
- Actualizaciones Atómicas de Estado: Asegúrate de que las herramientas completen sus promesas únicamente después de que React haya aplicado las actualizaciones de estado en el DOM. Si un agente invoca una herramienta B consecutivamente a una herramienta A, B debe observar el estado más reciente para evitar condiciones de carrera.
Integrando las primitivas de formularios de React 19 con hooks imperativos de WebMCP, los equipos de desarrollo pueden convertir interfaces estáticas en superficies listas para interactuar con agentes inteligentes.