![Erro em ferramenta de navegador: [object Promise] como resultado](/seo-search-engine-optimization-concept-businessman-touch-ai.jpg)
Erro em ferramenta de navegador: [object Promise] como resultado
O relatório do bug tinha três palavras: "navigate_and_read retorna lixo."
safari_navigate_and_read deve navegar o Safari para uma URL e devolver o texto da página para o agente de IA que a controla. Então eu o chamei. Ele não lançou erro. Não expirou. Ele retornou. O campo de conteúdo dizia:
[object Promise]
Não o título da página. Não o texto do corpo. A string literal [object Promise], como se a página que eu acabara de carregar contivesse nada além da sombra stringificada de um objeto JavaScript.
E aqui está a parte que me manteve acordado: o agente que havia chamado essa ferramenta a semana toda nunca reclamou. Ele não podia. Para ele, pediu uma página e recebeu uma string de volta. Strings são como as páginas se parecem. Ele apenas continuou a construir sobre isso.
Então deixe-me dizer isso claramente, antes de qualquer código, porque é o ponto principal deste postmortem: uma ferramenta que retorna a resposta errada sem erro é mais perigosa do que uma ferramenta que trava. Uma falha teria sido um presente. Uma falha é um sinal. Isso foi uma falsificação. O agente não tem exceção para capturar, nenhum código de status para verificar, nada para combinar — ele resume [object Promise], decide que a página está vazia ou quebrada, e age com base nessa conclusão. A falha é silenciosa, e se propaga.
Esta é uma história sobre uma única restrição arquitetônica — uma que eu já havia resolvido uma vez e depois falhei em aplicar em todos os lugares — e as onze ferramentas que ela quebrou silenciosamente. Se você nunca tocou em AppleScript na sua vida, fique comigo mesmo assim. Você quase certamente tem uma versão dessa ponte em algum lugar na sua própria pilha.
A configuração
safari-mcp é um servidor MCP que controla o real macOS Safari — o navegador real do usuário que está logado — para que agentes de codificação de IA possam navegar, ler, preencher formulários, tirar capturas de tela, todas as 80 e tantas ferramentas. A forma como ele se comunica com o Safari é o comando do JavaScript do AppleScript. Você entrega ao AppleScript uma string de JavaScript, ele a executa dentro da página e devolve o resultado. O safari-mcp envolve isso em um helper runJS(...).
Aqui está a única frase em que todo esse bug se baseia:
O
do JavaScriptdo AppleScript é síncrono. Ele retorna no momento em que a parte síncrona do seu JavaScript termina de executar.
Leia isso novamente com uma função assíncrona em mente. Porque algumas dessas ferramentas precisavam esperar — esperar que uma página carregasse, esperar que a navegação se estabilizasse, loop até que um elemento aparecesse — e a maneira natural de expressar "esperar" em JavaScript é await. Então o código antigo fez a coisa natural. Ele envolveu o trabalho em uma IIFE assíncrona e a entregou através da ponte:
// A forma quebrada — uma IIFE assíncrona passada para uma ponte síncrona.
(async () => {
history.back();
await waitForLoad(); // ← o controle retorna ao AppleScript AQUI
return document.body.innerText;
})()
Vamos analisar o que o AppleScript realmente vê:
- A IIFE começa a rodar.
- Ela executa seu prólogo síncrono, atinge o primeiro
await, e — como todo motor JS deve — imediatamente retorna uma Promise pendente para seu chamador. Esse chamador é o AppleScript. - O AppleScript é síncrono. Ele não tem conceito de uma Promise, nenhum loop de eventos para drenar, nada para
.then()em cima. Então ele faz a única coisa que pode fazer com um objeto que lhe foi entregue: ele o stringifica.String(promise)retorna"[object Promise]"(Object.prototype.toStringlê oSymbol.toStringTagda Promise). Essa string viaja de volta pela ponte, sobe através do Node, e sai para o agente como "o resultado." - Enquanto isso, o trabalho assíncrono real — o
await waitForLoad(), oreturn document.body.innerText— eventualmente roda dentro do loop de eventos da página. Seu valor de resolução se resolve no vazio milissegundos depois, sem ninguém ouvindo. Fire-and-forget.
O await nunca aconteceu do lado que importava. A ponte não consegue ver através da fronteira sync/async, então ela pegou a única coisa que existia de forma síncrona: o objeto Promise.
O diagnóstico
Aqui está a parte que doeu. Essa restrição não era novidade. safari_evaluate e safari_wait_for lidaram com isso corretamente por um longo tempo — eles nunca aguardam dentro da página; eles verificam o estado observável da página do lado do Node em vez disso, onde o Node realmente pode aguardar. O padrão certo já existia na base de código. Era estrutural. Funcionava.
O bug foi que onze outras ferramentas nunca foram migradas para isso. E no momento em que eu entendi por que navigate_and_read falhou, a satisfação se transformou em medo. Porque a causa não era um erro de digitação. Era uma forma. E formas são copiadas.
Então eu fui procurar a forma: qualquer ferramenta passando uma async IIFE para do JavaScript e esperando um retorno significativo. Essa única busca revelou mais dez. Onze ferramentas, uma causa raiz — e elas falharam em dois sabores distintos.
Modo 1 — retornou a Promise como dados. Esses são os casos de traição de confiança, porque o valor ruim flui diretamente para o contexto do agente:
-
safari_navigate_and_read— totalmente quebrado, retornou[object Promise]em vez do conteúdo da página. -
safari_get_indexed_db/safari_list_indexed_dbs— o mesmo:[object Promise]onde dados estruturados deveriam estar. -
safari_screenshot_element(caminho do canvas) — retornou uma Promise e nunca caiu no fallback confiável descreencapturee recorte. O fallback de canvas dosafari_screenshote o fallback de upload dosafari_upload_filetinham a mesma falha.
Modo 2 — disparou e esqueceu um efeito colateral, depois agiu sobre um estado obsoleto. Argumentavelmente pior, porque nada de errado aparece nesta chamada. Essas realizaram uma ação visível — então pareciam boas em uma demonstração — mas o await que deveria esperar pelo resultado evaporou:
-
safari_go_back/safari_go_forward— navegou, mas nunca atualizou a URL rastreada, então a próxima operação poderia direcionar para a aba errada. -
safari_reload— recarregou, mas nunca esperou pelo carregamento ou atualizou a URL. -
safari_click_and_wait— clicou, mas nunca esperou. -
safari_fill_and_submit— enviou, mas não esperou pela página de resultado. -
safari_scroll_to(modo texto) — rolou uma vez em vez de fazer um loop. -
safari_emulate/safari_reset_emulation— não esperou pelo recarregamento.
O modo 2 é o realmente perigoso. O agente chama go_back, recebe um sucesso limpo, então chama fill — e
Esse erro destaca a importância de uma integração correta entre ferramentas de IA e navegadores. Empresas brasileiras que utilizam automação web devem estar atentas a esses problemas para evitar falhas silenciosas em suas operações.


