
Construindo Co-Shop: Um Carrinho Compartilhado para Humanos e Agentes de IA com WebMCP
O problema com aplicativos web "agentes" hoje
Peça à maioria dos agentes de IA para fazer compras online para você agora, e aqui está aproximadamente o que acontece nos bastidores: o agente abre um navegador headless, carrega a página, tenta analisar o DOM, adivinha qual é o botão "Adicionar ao Carrinho", clica nele, espera, reanalisa e espera que nada tenha mudado no layout desde que o modelo foi treinado sobre como aquele site "geralmente" se parece.
Funciona às vezes. Também é lento, frágil e completamente opaco para o humano que está ali. Você pede a um agente para "adicionar alguns jantares vegetarianos ao meu carrinho", ele vai embora por um minuto e você recebe um resumo de volta: "Pronto! Adicionei 3 itens." Você não tinha ideia do que ele estava fazendo enquanto fazia isso, e se ele clicou na coisa errada, você descobre depois.
WebMCP é um padrão web proposto que remove completamente a adivinhação. Em vez de um agente reverter a engenharia da sua interface, sua página informa ao agente o que ele pode fazer:
document.modelContext.registerTool({
name: "add_to_cart",
description: "Adicionar um produto ao carrinho",
inputSchema: {
type: "object",
properties: {
productId: { type: "string" },
quantity: { type: "number" },
},
required: ["productId"],
},
async execute({ productId, quantity }) {
// sua lógica real do carrinho
},
});
Um agente visitando sua página agora, no navegador desktop habilitado para WebMCP do ChatGPT, ou no Chrome com a flag #enable-webmcp-testing pode chamar add_to_cart diretamente. Sem cliques, sem scraping, sem adivinhações.
Eu queria ver o que isso realmente desbloqueia além de "o agente agora pode clicar em botões mais rápido", então eu construí o Co-Shop para o Desafio WebMCP da OpenAI: uma loja de supermercado onde um humano e um agente de IA compartilham o mesmo carrinho literal, ao vivo, na mesma aba do navegador.
Aqui está o que eu aprendi construindo isso.
A ideia central: um estado, dois atores
A versão fácil deste projeto teria sido "o agente tem sua própria ferramenta de construção de carrinho, o humano tem uma interface, sincronize-as de alguma forma." Eu não queria isso. O ponto todo do WebMCP, eu acho, é que não precisa haver uma etapa de sincronização se a ferramenta do agente e a interface do humano ambos mutam a mesma peça de estado, eles nunca estarão fora de sincronia em primeiro lugar.
Então, no Co-Shop, app/page.js é um único componente cliente mantendo o carrinho no estado do React, e tanto os manipuladores de clique da interface quanto as funções execute das ferramentas WebMCP chamam as mesmas funções auxiliares:
function addToCart(productId, quantity, actor) {
const product = PRODUCTS.find((p) => p.id === productId);
if (!product) return { ok: false, error: Nenhum produto com id "${productId}". };
const qty = Math.max(1, Math.floor(quantity || 1));
setCart((prev) => {
const existing = prev.find((i) => i.productId === productId);
if (existing) {
return prev.map((i) =>
i.productId === productId ? { ...i, quantity: i.quantity + qty, addedBy: actor } : i
);
}
return [...prev, { productId, quantity: qty, addedBy: actor }];
});
logActivity(actor, adicionado ${qty}× ${product.emoji} ${product.name} ao carrinho);
return { ok: true, product: product.name, quantity: qty };
}
A única diferença entre um humano clicando em "Adicionar" e um agente chamando a ferramenta add_to_cart é a string do ator passada: "humano" vs "agente". Esse único parâmetro é o que aciona o badge roxo "AGENTE", a animação de pulso e a entrada do feed de atividade. Todo o resto, o estado, o total, a renderização é idêntico.
Registrando as ferramentas
Todas as sete ferramentas são registradas em um único useEffect, usando um AbortController para que a limpeza seja automática se o componente for desmontado:
_useEffect(() => {
if (typeof window === "undefined" || !("modelContext" in document)) {
setWebmcpStatus("unsupported");
return;
}
const controller = new AbortController();
const opts = { signal: controller.signal };
async function registerAll() {
await document.modelContext.registerTool(
{
name: "search_products",
description: "Pesquisar no catálogo de supermercado Co-Shop...",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
category: { type: "string" },
tags: { type: "array", items: { type: "string" } },
maxPrice: { type: "number" },
},
},
async execute({ query, category, tags, maxPrice } = {}) {
const results = findProducts({ query, category, tags, maxPrice });
return { content: [{ type: "text", text: JSON.stringify({ count: results.length, results }) }] };
},
},
opts
);// ... seis outras ferramentas registradas da mesma forma
setWebmcpStatus("ready");
}
registerAll();
return () => controller.abort(); // desregistra tudo
}, []);_
Dois detalhes me atrapalharam aqui, e eu acho que vale a pena destacar para qualquer um que esteja construindo sua primeira ferramenta WebMCP:
Fechamentos obsoletos. As ferramentas são registradas uma vez, na montagem. Se sua função execute captura o carrinho diretamente de um valor useState no momento da registro, ela continuará lendo aquele carrinho vazio original para sempre. Atualizações de estado do React não atualizam retroativamente fechamentos já criados. A solução é direcionar cada leitura e escrita através de setCart (a função, não o valor) e derivar instantâneas sob demanda em vez de confiar em uma variável capturada.
A forma de retorno importa. As funções execute do WebMCP esperam retornar uma forma de array de conteúdo:
{ content: [{ type: "text", text: "..." }] }
Eu originalmente apenas retornei objetos simples. Isso "funcionou" tecnicamente em meus próprios testes manuais (já que eu controlava ambas as extremidades), mas uma vez que um agente real chamou as ferramentas, JSON estruturado naquele campo de texto não uma string simples é o que permitiu que o agente realmente raciocinasse sobre o resultado (por exemplo, sabendo exatamente quais 5 dos 7 jantares solicitados se encaixavam em um orçamento, e por quê).
A ferramenta que mais importou: ações compostas
As ferramentas únicas (add_to_cart, remove_from_cart, get_cart) são o "olá mundo" do WebMCP, praticamente o exemplo exato no próprio briefing do desafio da OpenAI. A que realmente tornou a demonstração interessante foi uma ferramenta composta:
async execute({ days, dietary, avoidTags, maxBudget }) {
return textResult(planDinners({ days, dietary, avoidTags, maxBudget }));
}
plan_dinners leva um pedido em formato de linguagem natural "7 dias, vegetariano, abaixo de $40" e internamente faz a filtragem, escolhe refeições distintas correspondentes e chama addToCart para cada uma, tudo em uma única viagem de ferramenta. Essa é a diferença entre um agente fazendo uma coisa de cada vez para sempre e um agente realmente executando um plano. Quando eu testei isso contra o navegador in-app do ChatGPT, pedindo para "planejar uma semana de jantares vegetarianos abaixo de $40" resultou exatamente nesta resposta, gerada pela própria ferramenta:
"Adicionados 5 jantares vegetarianos ao carrinho por $39,50. Sete não se encaixam abaixo do limite de $40."
Essa frase não é o LLM improvisando, é a saída estruturada diretamente do valor de retorno de plan_dinners, que o modelo então retransmitiu. Essa é a parte do WebMCP que eu acho genuinamente diferente do comportamento de agente de prompt-e-esperar: o agente não está adivinhando o que aconteceu, ele é informado exatamente o que aconteceu, em um formato que ele pode agir.
Testando contra um agente real
Duas maneiras de verificar se uma integração WebMCP realmente funciona, em ordem crescente de "isso prova algo real":
document.modelContext existe no console. Prova que a superfície da API está lá. Não prova que suas ferramentas estão sendo chamadas corretamente.
Uma registerTool simulada em um teste headless. Eu fiz isso com o Playwright antes de tocar
O WebMCP permite que empresas brasileiras integrem agentes de IA de forma mais eficiente em suas plataformas de e-commerce. Isso pode resultar em uma experiência de compra mais fluida e intuitiva, aumentando a satisfação do cliente e potencialmente as vendas.

