Torne seu app React operável por agentes de IA com UI SDK e WebMCP
Agentes de IA leem e usam sua UI React com o UI SDK da Vercel: createAgent, snapshot, act, allow, source agent, definePrimitive e registerTools do WebMCP.
Preet Suthar · · 10 min de leitura
- AI
- React
- UI SDK
- WebMCP
A maioria dos agentes de IA usa um app web como uma pessoa com um leitor de tela lento usaria. Eles tiram um screenshot ou leem o DOM, adivinham qual elemento é o dropdown "Region", clicam nele, leem a página de novo e clicam em uma opção. Cada ida e volta custa tokens e tempo, e cada palpite pode estar errado.
O UI SDK, que a Vercel Labs publicou em 12 de novembro de 2026, segue outro caminho. Os componentes dele se registram em uma ponte que diz ao agente o que há na página, em que estado cada componente está e o que ele pode fazer, e depois executa essas ações pela própria API do componente. A mesma ponte pode publicar suas ferramentas para um agente embutido no navegador via WebMCP.
Este post constrói uma pequena página de configurações em React, conecta um agente a ela e passa pelas partes da API que importam: createAgent, snapshot(), act(), o callback allow, source: "agent" nos callbacks de mudança, um componente customizado feito com definePrimitive e registerTools. Toda saída abaixo foi copiada de uma execução em um app de teste com React 19.3, Vite 8 e @ui-sdk/react e @ui-sdk/core na versão 0.1.0-beta.1. O UI SDK está em beta, então nomes e comportamento podem mudar.
Como a ponte funciona
Seu app React
- Input
- Select
- Switch
- Suas partes de definePrimitive
A ponte: createAgent()
snapshot()lista componentes, estado e açõesquery()encontra componentes e suas opçõesact()executa os passos pela API de cada componenteallowaprova ou recusa cada passo
Seu próprio agente
Definições de ferramentas do agentTools, ligadas ao SDK do seu modelo
O agente do navegador
registerTools() publica as ferramentas em document.modelContext (WebMCP), com um segundo allow
Toda mudança feita por um agente chega aos seus callbacks de mudança com details.source === "agent", e details.cancel() ainda pode recusá-la.
A ponte é um objeto que createAgent() retorna. Os componentes do UI SDK na página se registram nela ao serem montados. Ela tem três métodos que um agente usa:
snapshot()retorna todo componente registrado com umrefcurto comoc3, seu tipo, seu label acessível, seu estado e as ações que ele aceita naquele momento.query()encontra componentes por texto ou lista as opções dentro de um, como as opções de um Select.act()executa uma lista de passos em ordem. Cada passo nomeia um componente e uma ação, comosetValue, e a ponte chama essa ação no componente.
agentTools descreve esses três métodos como definições de ferramentas com JSON Schemas, para o seu próprio loop de modelo. registerTools() os registra no navegador.
A escolha de design importante é que act() não simula cliques. Ele chama a mesma API que um controller chamaria, então a mudança passa pelo callback de mudança do componente com source igual a "agent". Seu app vê as mudanças do agente no mesmo lugar em que vê as de uma pessoa, e pode recusá-las ali.
O benchmark da Vercel
A documentação da Vercel inclui um benchmark. Agentes renomearam um projeto, atualizaram membros e configurações de notificação e depois salvaram. Um agente usou ferramentas de navegador em uma página feita com outra biblioteca de componentes. O outro usou a ponte para agentes na mesma tela feita com UI SDK.
- Chamadas de ferramenta−69%
- 165
- Texto lido (caracteres)−83%
- 39.8k6.8k
- Tempo−61%
- 41 s16 s
- Custo−56%
- $0.073$0.032
Fonte: documentação do UI SDK da Vercel. Medianas de três execuções por abordagem, Sonnet via Claude Code. Não reproduzido pela HextaUI.
Esses são números da Vercel, não meus. São medianas de três execuções por abordagem, os dois agentes usaram Sonnet via Claude Code com a mesma tarefa e o mesmo system prompt, e todas as execuções concluíram a tarefa. "Texto lido" conta os caracteres retornados pelas ferramentas. Não reproduzi o benchmark. Para ter uma escala, um snapshot completo da demo abaixo tem 729 caracteres de JSON. O HTML da página no servidor de dev do Vite tem cerca de 28.000 caracteres. O snapshot ARIA do Playwright da mesma página foi ainda menor, cerca de 370 caracteres, mas ele só lista roles e nomes. Ele não diz quais ações cada controle aceita, quais opções um Select fechado tem nem qual botão precisa de uma pessoa. É essa estrutura extra que poupa o agente de chamadas de acompanhamento, não o tamanho bruto.
Monte a demo
Comece pelo template React com TypeScript do Vite e adicione os dois pacotes:
Crie a ponte no próprio módulo. Importe-a no main.tsx antes de o app renderizar, para que ela veja os componentes conforme são criados. A documentação diz que ela também encontra componentes que já estão na página, então você pode criá-la depois, quando um assistente abrir.
Os dois callbacks allow aparecem de novo abaixo. Object.assign(window, { agent }) coloca a ponte no window para você testar pelo console.
A página é um formulário de configurações com um campo de texto, um Select, um Switch, um contador de assentos customizado e um botão Save. Este trecho tira as classes, o layout e o componente de log, e mantém as partes que importam para o agente:
record adiciona uma linha a um log na página, para você ver quem mudou o quê. Aqui os labels importam mais que o normal. A ponte nomeia os componentes pelo nome acessível, então FieldLabel, SelectLabel e o <label> em volta são o que permite a um agente dizer "Region" em vez de adivinhar.
Leia a página com snapshot()
Com a página renderizada, agent.snapshot() retornou isto:
Algumas coisas para notar:
- Não existe
c2. O Field também se registrou, mas um Field só aparece no snapshot enquanto tem erros, já que seu input já carrega o label. O buraco nos refs é inofensivo. - O Select lista seus itens mesmo fechado, porque a demo passa
items. Sem eles,query({ within: "Region" })lê as opções, e a ponte pode abrir a lista por um instante para isso. - O botão Save carrega o texto de
confirmvindo dedata-agent-confirm, então o agente sabe antes de tentar que uma pessoa precisa aprovar. seat-stepperé meu próprio componente. Ele listaadderemoveporque eu disse isso na definição dele, que é explicada mais abaixo.
Mude coisas com act()
Uma chamada de act() pode levar todos os passos que o agente já conhece. Os passos nomeiam componentes por ref ou por label, e a ponte casa palavras inteiras sem diferenciar maiúsculas e acentos. Um item também pode ser nomeado pelo label:
O resultado informa as mudanças de cada passo e o valor atual de cada campo:
"North America" foi resolvido para o valor do item, north-america. Como values volta em toda chamada, o agente não precisa de outro snapshot para conferir o próprio trabalho.
Os erros são escritos para um modelo ler. Dois que provoquei de propósito:
Um passo que falha interrompe a chamada, e os passos anteriores continuam aplicados. Não há rollback.
Uma regra de correspondência me surpreendeu. Um passo com ref: "Name" mudou "Project name", porque labels casam por palavras inteiras. Isso está documentado, e a ponte recusa um nome que casa com mais de um componente, mas dê labels distintos aos componentes se um agente for tentar adivinhá-los.
Permissões: allow, marcas de confirmação e cancel
Há três lugares para dizer não, e eles fazem trabalhos diferentes.
allow no createAgent vê cada passo antes de ele rodar, com ref, component, label, action, args e confirm. Retorne false para recusar, ou uma promise para sua própria UI de confirmação. Minha política recusa todo clear, e o resultado (sem values aqui) é:
data-agent-confirm marca um controle que sempre precisa de uma pessoa, digam o que disserem as regras. Sem um callback allow, um passo nele falha com a mensagem "Needs a person". Com um, step.confirm traz a frase para você exibir. Meu allow a passa para window.confirm, e na execução o diálogo dizia "Saves the settings for every member of Atlas" antes de Save ser pressionado. A marca cobre tudo dentro do elemento, então você pode colocá-la em um formulário ou diálogo inteiro.
Seu callback de mudança tem a palavra final sobre um valor. O Select Region cancela um pedido do agente por Asia Pacific:
Repare que ok é true. A ponte executou o passo, e o callback cancelou a mudança, então o passo informa result: false e o valor não muda. A documentação diz isso com todas as letras: ok: true não significa que todo valor foi aceito. Confira changes e values.
Também há dois atributos para o que um agente pode ver. data-agent-hidden remove uma região de snapshot e query. data-agent-secret mostra um campo com o valor como "[redacted]". Campos de senha, cartão e código de uso único são tratados como secretos sem marca.
Diferencie mudanças de agentes com source
Todo callback de mudança no UI SDK recebe details.source: keyboard, mouse, touch, pen, virtual, programmatic ou agent. Depois da execução acima, o log da demo ficou assim:

Clicar no botão de mais à mão depois registrou Seats → 2 (source: mouse). Esse campo sozinho basta para criar um selo de "editado pelo seu assistente", mandar mudanças de agentes para uma fila de revisão ou pular eventos de analytics que uma pessoa não causou.
Uma ressalva da documentação: um handler de Button que espera algo com await e muda o estado depois informa "programmatic", porque o clique já acabou a essa altura. Envolva a mudança posterior em withSource(source, …) de @ui-sdk/react/observe para manter o agente como origem.
Seu próprio componente: definePrimitive e a chave agent
Componentes nativos entram na ponte sozinhos. Componentes que você escreve com definePrimitive também entram. Aqui está o contador de assentos da demo, com uma chave agent que nomeia os dois métodos que um agente pode chamar:
createParts(seatStepper, { root: "div", remove: "button", add: "button" }) de @ui-sdk/react/primitive transforma isso em componentes React. O root aceita aria-labelledby como qualquer elemento, e é daí que veio o label "Seats" do snapshot.
Por que a chave agent importa: sem ela, a ponte adivinha ações pelos nomes dos métodos. Ela reconhece set seguido de letra maiúscula, como setValue, e verbos como open, toggle e clear. Ela não adivinharia add nem remove, então o stepper não mostraria nenhuma ação. Com a chave, só os métodos listados são oferecidos e qualquer outro é recusado, como mostrou o erro de keydown acima. A documentação pede que você coloque agent por último, depois de slots, para que o TypeScript já tenha lido a API, e marca a chave como experimental.
Como a mudança passa por createChange, o onValueChange do app ainda vê source: "agent" e pode cancelar. Quando o agente pediu para remover cinco assentos de três, o valor travou em 1 e o botão de remover ficou desabilitado, exatamente como os cliques de uma pessoa o teriam deixado.
Duas notas de quando escrevi isso. Valores de atributos em attrs são strings, então disabled é "" ou undefined, não um booleano; o TypeScript pegou isso. E a documentação do Core diz para nunca colocar role em attrs e usar a chave role do slot, enquanto um exemplo do Core coloca role em attrs. Usei a chave role.
Registre as ferramentas para o WebMCP
registerTools(agent, { allow }) publica snapshot, query e act em document.modelContext, a API de navegador da proposta WebMCP. Um navegador que a oferece pode entregar as ferramentas ao seu agente embutido. Em um navegador sem ela, registerTools não faz nada.
O Chromium headless com que testei não fornece document.modelContext, então, para ver o que é registrado, instalei um objeto substituto antes de a página carregar. As três ferramentas chegaram com estas anotações:
untrustedContentHint existe porque os resultados das ferramentas trazem texto da página escrito por usuários, como um nome em uma linha de tabela. As descrições das ferramentas também dizem ao modelo para tratar esse texto como dado, nunca como instrução. É uma dica, não uma garantia, então deixe qualquer coisa arriscada atrás de allow e data-agent-confirm.
O allow no registerTools é obrigatório, e roda depois do allow da própria ponte, só para passos que vieram do agente do navegador. Isso te dá duas políticas: uma para o agente que você escreveu e outra, mais rígida, para um agente que você não escreveu.
- Encontre o alvo
refUm ref como c3, depois um label, um item e um tipo. - allow da ponte
createAgentSua política para todo chamador. False interrompe a chamada. - allow do WebMCP
registerToolsUma segunda política, só para o agente do navegador. - API do componente
setValueO mesmo método que um controller chamaria. - Seu callback
details.sourceVê"agent"e pode chamarcancel().
O guia do UI SDK mostra como usar as ferramentas pelo navegador embutido no app desktop do ChatGPT, onde as ferramentas de sites precisam ser ativadas e só alguns modelos e workspaces as suportam. Não testei esse caminho. Confira typeof document.modelContext?.registerTool no navegador que você quer atender. Ele retorna "function" onde o registro é suportado.
Um achado do beta: uma recusa atribuída à pessoa
Na minha configuração, o allow da ponte pedia à pessoa para confirmar o Save, e o allow do WebMCP recusa todo press. Quando chamei a ferramenta act registrada para pressionar Save, o diálogo de confirmação apareceu, eu aceitei, e então a política do WebMCP recusou o passo. O erro dizia:
A pessoa disse sim. O código disse não. O passo foi recusado, o que está correto, mas um modelo lendo essa mensagem poderia dizer ao usuário que ele recusou algo que aprovou. Ele também perguntou à pessoa antes de checar uma política que ia recusar de qualquer jeito. Se você combina marcas de confirmação com um allow do WebMCP que recusa cliques, recuse antes: verifique step.action também no allow da ponte, para que a pessoa nunca seja perguntada. Estou reportando isso contra a 0.1.0-beta.1 e espero que mude.
Quando vale a pena
A ponte vale a pena quando um agente faz parte do produto: um painel de assistente que preenche formulários, uma ferramenta de suporte que muda configurações pelo usuário ou um agente de navegador que você quer que funcione bem no seu site. Ela também é útil em testes, já que act() te dá um jeito tipado de controlar componentes sem seletores.
Isso não é, sozinho, motivo para migrar um app estável. A ponte só enxerga componentes do UI SDK, então uma página que mistura bibliotecas dá ao agente uma visão parcial. E é um beta, com a chave agent e os helpers de histórico marcados como experimentais.
Se você construir em cima disso, alguns hábitos mantêm os agentes úteis e seguros:
- dê a todo controle um nome acessível, porque é isso que o agente digita;
- passe
itemspara Selects e Comboboxes, para que as opções sejam legíveis com eles fechados; - recuse no
allowo que um agente nunca deve fazer, e marque o que precisa de uma pessoa comdata-agent-confirm; - valide valores nos callbacks de mudança, porque
setValuechama a API diretamente e ignora seus handlers de teclado e ponteiro; - leia
valuesde todo resultado deact()em vez de confiar emok.
A HextaUI está trabalhando em uma versão estilizada dos seus componentes sobre o UI SDK, então esses são os padrões em torno dos quais estou construindo. Para ver como o UI SDK se compara ao Radix e ao Base UI além dos agentes, veja UI SDK vs Base UI vs Radix. Para estilizar os componentes com Tailwind, veja Estilizando o UI SDK da Vercel com Tailwind.
