Haz que agentes de IA puedan operar tu app React con UI SDK y WebMCP
Deja que agentes de IA lean y usen tu UI React con UI SDK de Vercel: createAgent, snapshot, act, allow, source agent, definePrimitive y registerTools de WebMCP.
Preet Suthar · · 10 min de lectura
- AI
- React
- UI SDK
- WebMCP
La mayoría de los agentes de IA usan una web app como lo haría una persona con un lector de pantalla lento. Hacen una captura o leen el DOM, adivinan qué elemento es el desplegable "Region", hacen clic, vuelven a leer la página y hacen clic en una opción. Cada ida y vuelta cuesta tokens y tiempo, y cada suposición puede fallar.
UI SDK, que Vercel Labs publicó el 12 de noviembre de 2026, toma otro camino. Sus componentes se registran en un puente que le dice a un agente qué hay en la página, en qué estado está cada componente y qué puede hacer, y luego ejecuta esas acciones a través de la propia API del componente. El mismo puente puede publicar sus herramientas para un agente integrado en el navegador mediante WebMCP.
Este post construye una pequeña página de ajustes en React, le conecta un agente y recorre las partes de la API que importan: createAgent, snapshot(), act(), el callback allow, source: "agent" en los callbacks de cambio, un componente personalizado hecho con definePrimitive y registerTools. Cada salida de abajo está copiada de una ejecución en una app de prueba con React 19.3, Vite 8 y @ui-sdk/react y @ui-sdk/core en 0.1.0-beta.1. UI SDK está en beta, así que los nombres y el comportamiento pueden cambiar.
Cómo funciona el puente
Tu app React
- Input
- Select
- Switch
- Tus partes de definePrimitive
El puente: createAgent()
snapshot()lista componentes, estado y accionesquery()encuentra componentes y sus opcionesact()ejecuta los pasos a través de la API de cada componenteallowaprueba o rechaza cada paso
Tu propio agente
Definiciones de herramientas de agentTools, conectadas al SDK de tu modelo
El agente de un navegador
registerTools() publica las herramientas en document.modelContext (WebMCP), con un segundo allow
Cada cambio que hace un agente llega a tus callbacks de cambio con details.source === "agent", y details.cancel() todavía puede rechazarlo.
El puente es un objeto que devuelve createAgent(). Los componentes de UI SDK de la página se registran en él al montarse. Tiene tres métodos que usa un agente:
snapshot()devuelve cada componente registrado con unrefcorto comoc3, su tipo, su label accesible, su estado y las acciones que acepta en ese momento.query()encuentra componentes por texto o lista las opciones dentro de uno, como las de un Select.act()ejecuta una lista de pasos en orden. Cada paso nombra un componente y una acción, comosetValue, y el puente llama a esa acción en el componente.
agentTools describe estos tres métodos como definiciones de herramientas con JSON Schemas, para tu propio bucle de modelo. registerTools(), en cambio, los registra en el navegador.
La decisión de diseño importante es que act() no simula clics. Llama a la misma API que llamaría un controlador, así que el cambio pasa por el callback de cambio del componente con un source de "agent". Tu app ve los cambios del agente en el mismo sitio donde ve los de una persona, y puede rechazarlos ahí.
El benchmark de Vercel
La documentación de Vercel incluye un benchmark. Los agentes renombraron un proyecto, actualizaron los miembros y los ajustes de notificaciones, y luego guardaron. Un agente usó herramientas de navegador en una página hecha con otra biblioteca de componentes. El otro usó el puente para agentes en la misma pantalla hecha con UI SDK.
- Llamadas a herramientas−69%
- 165
- Texto leído (caracteres)−83%
- 39.8k6.8k
- Tiempo−61%
- 41 s16 s
- Coste−56%
- $0.073$0.032
Fuente: documentación del UI SDK de Vercel. Medianas de tres ejecuciones por enfoque, Sonnet en Claude Code. No reproducido por HextaUI.
Son números de Vercel, no míos. Son medianas de tres ejecuciones por enfoque, ambos agentes usaron Sonnet en Claude Code con la misma tarea y el mismo system prompt, y todas las ejecuciones completaron la tarea. "Texto leído" cuenta los caracteres que devolvieron las herramientas. No reproduje el benchmark. Como referencia, un snapshot completo de la demo de abajo ocupa 729 caracteres de JSON. El HTML de la página en el servidor de desarrollo de Vite ronda los 28.000 caracteres. El snapshot ARIA de Playwright de la misma página fue aún más corto, unos 370 caracteres, pero solo lista roles y nombres. No dice qué acciones acepta cada control, qué opciones tiene un Select cerrado ni qué botón necesita a una persona. Esa estructura extra es lo que le ahorra a un agente las llamadas de seguimiento, no el tamaño en bruto.
Prepara la demo
Parte de la plantilla de React y TypeScript de Vite y añade los dos paquetes:
Crea el puente en su propio módulo. Impórtalo en main.tsx antes de que se renderice la app, para que vea los componentes según se crean. La documentación dice que también encuentra los componentes que ya están en la página, así que puedes crearlo más tarde, cuando se abra un asistente.
Los dos callbacks allow vuelven a aparecer más abajo. Object.assign(window, { agent }) pone el puente en window para que puedas probarlo desde la consola.
La página es un formulario de ajustes con un campo de texto, un Select, un Switch, un contador de plazas personalizado y un botón Save. Este extracto quita las clases, el layout y el componente de log, y conserva las partes que le importan al agente:
record añade una línea a un log en la página, para que veas quién cambió qué. Aquí los labels importan más de lo habitual. El puente nombra los componentes por su nombre accesible, así que FieldLabel, SelectLabel y el <label> que envuelve son lo que permite a un agente decir "Region" en vez de adivinar.
Lee la página con snapshot()
Con la página renderizada, agent.snapshot() devolvió esto:
Algunas cosas a tener en cuenta:
- No hay
c2. El Field también se registró, pero un Field solo aparece en el snapshot mientras tiene errores, ya que su input ya lleva el label. El hueco en los refs no tiene importancia. - El Select lista sus items aunque esté cerrado, porque la demo pasa
items. Sin ellos,query({ within: "Region" })lee las opciones, y el puente puede abrir la lista un momento para hacerlo. - El botón Save lleva el texto
confirmdedata-agent-confirm, así que el agente sabe antes de intentarlo que una persona tiene que aprobarlo. seat-stepperes mi propio componente. Listaaddyremoveporque así lo indiqué en su definición, que se explica más abajo.
Cambia cosas con act()
Una llamada a act() puede llevar todos los pasos que el agente ya conoce. Los pasos nombran componentes por ref o por label, y el puente compara palabras completas sin distinguir mayúsculas ni acentos. Un item también puede nombrarse por su label:
El resultado informa de los cambios de cada paso y del valor actual de cada campo:
"North America" se resolvió al valor del item, north-america. Como values vuelve con cada llamada, un agente no necesita otro snapshot para comprobar su trabajo.
Los errores están escritos para que los lea un modelo. Dos que provoqué a propósito:
Un paso fallido detiene la llamada, y los pasos anteriores se quedan aplicados. No hay rollback.
Una regla de coincidencia me sorprendió. Un paso con ref: "Name" cambió "Project name", porque los labels coinciden por palabras completas. Está documentado, y el puente rechaza un nombre que coincide con más de un componente, pero da a los componentes labels distintos si un agente va a tener que adivinarlos.
Permisos: allow, marcas de confirmación y cancel
Hay tres sitios donde decir que no, y cada uno hace un trabajo distinto.
allow en createAgent ve cada paso antes de que se ejecute, con su ref, component, label, action, args y confirm. Devuelve false para rechazarlo, o una promesa para tu propia UI de confirmación. Mi política rechaza todo clear, y el resultado (sin values aquí) es:
data-agent-confirm marca un control que siempre necesita a una persona, digan lo que digan las reglas. Sin un callback allow, un paso sobre él falla con un mensaje "Needs a person". Con uno, step.confirm lleva la frase para que puedas mostrarla. Mi allow la pasa a window.confirm, y en la ejecución el diálogo decía "Saves the settings for every member of Atlas" antes de pulsar Save. La marca cubre todo lo que hay dentro de su elemento, así que puedes ponerla en un formulario o un diálogo entero.
Tu callback de cambio tiene la última palabra sobre un valor. El Select Region cancela la petición de un agente para Asia Pacific:
Fíjate en que ok es true. El puente ejecutó el paso y el callback canceló el cambio, así que el paso informa result: false y el valor no cambia. La documentación lo dice claramente: ok: true no significa que se aceptaran todos los valores. Revisa changes y values.
También hay dos atributos para lo que un agente puede ver. data-agent-hidden quita una región de snapshot y query. data-agent-secret muestra un campo con su valor como "[redacted]". Los campos de contraseña, tarjeta y código de un solo uso se tratan como secretos sin necesidad de marca.
Distingue los cambios de agentes con source
Cada callback de cambio en UI SDK recibe details.source: keyboard, mouse, touch, pen, virtual, programmatic o agent. Tras la ejecución de arriba, el log de la demo quedó así:

Al pulsar después el botón de más a mano se registró Seats → 2 (source: mouse). Ese único campo basta para crear un badge de "editado por tu asistente", enviar los cambios de agentes a una cola de revisión u omitir eventos de analítica que no causó una persona.
Un matiz de la documentación: un manejador de Button que espera algo con await y cambia el estado después informa "programmatic", porque para entonces la pulsación ya ha terminado. Envuelve el cambio posterior en withSource(source, …) de @ui-sdk/react/observe para mantener al agente como origen.
Tu propio componente: definePrimitive y la clave agent
Los componentes integrados se unen al puente solos. Los componentes que escribes con definePrimitive también. Este es el contador de plazas de la demo, con una clave agent que nombra los dos métodos que un agente puede llamar:
createParts(seatStepper, { root: "div", remove: "button", add: "button" }) de @ui-sdk/react/primitive lo convierte en componentes React. La raíz acepta aria-labelledby como cualquier elemento, y de ahí salió el label "Seats" del snapshot.
Por qué importa la clave agent: sin ella, el puente deduce las acciones a partir de los nombres de los métodos. Reconoce set seguido de una mayúscula, como setValue, y verbos como open, toggle y clear. No deduciría add ni remove, así que el stepper no mostraría ninguna acción. Con la clave, solo se ofrecen los métodos listados y cualquier otro se rechaza, como mostró el error de keydown de arriba. La documentación pide poner agent al final, después de slots, para que TypeScript ya haya leído la API, y marca la clave como experimental.
Como el cambio pasa por createChange, el onValueChange de la app sigue viendo source: "agent" y puede cancelarlo. Cuando el agente pidió quitar cinco plazas de tres, el valor se limitó a 1 y el botón de quitar se desactivó, igual que lo habrían dejado los clics de una persona.
Dos notas de cuando lo escribí. Los valores de atributos en attrs son strings, así que disabled es "" o undefined, no un booleano; TypeScript lo detectó. Y la documentación de Core dice que nunca pongas role en attrs y que uses la clave role del slot, mientras que un ejemplo de Core sí pone role en attrs. Yo usé la clave role.
Registra las herramientas para WebMCP
registerTools(agent, { allow }) publica snapshot, query y act en document.modelContext, la API del navegador de la propuesta WebMCP. Un navegador que la ofrezca puede pasar las herramientas a su agente integrado. En un navegador sin ella, registerTools no hace nada.
El Chromium headless con el que probé no ofrece document.modelContext, así que para ver qué se registra instalé un objeto sustituto antes de que cargara la página. Las tres herramientas llegaron con estas anotaciones:
untrustedContentHint está ahí porque los resultados de las herramientas llevan texto de la página escrito por usuarios, como un nombre en una fila de una tabla. Las descripciones de las herramientas también le dicen al modelo que trate ese texto como datos, nunca como instrucciones. Es una pista, no una garantía, así que deja todo lo arriesgado detrás de allow y data-agent-confirm.
El allow de registerTools es obligatorio, y se ejecuta después del allow propio del puente, solo para los pasos que vienen del agente del navegador. Eso te da dos políticas: una para el agente que escribiste tú y otra más estricta para un agente que no escribiste.
- Encuentra el objetivo
refUn ref como c3, luego un label, un item y después un tipo. - allow del puente
createAgentTu política para cualquier llamante. False detiene la llamada. - allow de WebMCP
registerToolsUna segunda política, solo para el agente del navegador. - API del componente
setValueEl mismo método que llamaría un controlador. - Tu callback
details.sourceVe"agent"y puede llamar acancel().
La guía de UI SDK explica cómo usar las herramientas desde el navegador integrado de la app de escritorio de ChatGPT, donde las herramientas de sitio hay que activarlas y solo algunos modelos y workspaces las soportan. No probé esa vía. Comprueba typeof document.modelContext?.registerTool en el navegador al que apuntas. Devuelve "function" donde el registro está soportado.
Un hallazgo de la beta: un rechazo que culpa a la persona
En mi configuración, el allow del puente pedía a la persona confirmar Save, y el allow de WebMCP rechaza todo press. Cuando llamé a la herramienta registrada act para pulsar Save, apareció el diálogo de confirmación, lo acepté, y después la política de WebMCP rechazó el paso. El error decía:
La persona dijo que sí. El código dijo que no. El paso se rechazó, lo cual es correcto, pero un modelo que lea ese mensaje podría decirle al usuario que rechazó algo que aprobó. Además, preguntó a la persona antes de comprobar una política que iba a rechazarlo de todos modos. Si combinas marcas de confirmación con un allow de WebMCP que rechaza pulsaciones, rechaza antes: comprueba step.action también en el allow del puente, para que nunca se pregunte a la persona. Lo reporto contra 0.1.0-beta.1 y espero que cambie.
Cuándo merece la pena
El puente se gana su sitio cuando un agente forma parte del producto: un panel de asistente que rellena formularios, una herramienta de soporte que cambia ajustes por el usuario o un agente de navegador que quieres que funcione bien en tu sitio. También es útil en tests, ya que act() te da una forma tipada de manejar componentes sin selectores.
No es motivo por sí solo para migrar una app estable. El puente solo ve componentes de UI SDK, así que una página que mezcla bibliotecas le da al agente una vista parcial. Y es una beta, con la clave agent y los helpers de historial marcados como experimentales.
Si construyes sobre él, algunos hábitos mantienen a los agentes útiles y seguros:
- da a cada control un nombre accesible, porque es lo que escribe el agente;
- pasa
itemsa los Selects y Comboboxes para que las opciones se puedan leer con la lista cerrada; - rechaza en
allowlo que un agente nunca debería hacer, y marca condata-agent-confirmlo que necesita a una persona; - valida los valores en los callbacks de cambio, porque
setValuellama directamente a la API y se salta tus manejadores de teclado y puntero; - lee
valuesdel resultado de cadaact()en vez de fiarte deok.
HextaUI está trabajando en una variante con estilos de sus componentes sobre UI SDK, así que estos son los patrones en torno a los que estoy construyendo. Para ver cómo se compara UI SDK con Radix y Base UI más allá de los agentes, lee UI SDK vs Base UI vs Radix. Para estilizar los componentes con Tailwind, lee Estilizar el UI SDK de Vercel con Tailwind.
