API Declarativa do WebMCP: Formulários HTML como Tools para IA

O que é a API Declarativa

A API Declarativa transforma formulários HTML padrão em tools acessíveis por agentes de IA — sem escrever uma linha de JavaScript. Adicione atributos especiais ao <form> e aos campos, e o browser gera o JSON Schema sozinho.

Pra quem isso resolve problema:

  • Formulários de contato, busca e cadastro que já existem no site
  • Sites que dependem de <form> para interações (a maioria, convenhamos)
  • Progressive enhancement — funciona pra humanos mesmo sem WebMCP
  • Times que não querem (ou não precisam) JavaScript complexo no frontend

Nota: A API Declarativa e a API Imperativa coexistem sem atrito. Este site (prompt.api.br) usa a API Imperativa via astro-webmcp para busca e navegação, enquanto a API Declarativa pode ser adicionada a qualquer formulário individual.


Atributos do formulário

toolname (obrigatório)

Define o nome da tool. O agente usa esse nome para identificar e chamar a ferramenta.

<form toolname="subscribe_newsletter">

Regras:

  • Deve ser único na página
  • Use snake_case ou camelCase
  • Máximo recomendado: 30 caracteres
  • Verbos que descrevem a ação: submit_feedback, search_products, book_appointment

tooldescription (obrigatório)

Descreve o que a tool faz. O agente usa essa descrição pra decidir quando chamar.

<form toolname="subscribe_newsletter"
      tooldescription="Subscribe to the weekly newsletter with email address.">

Regras:

  • Máximo recomendado: 500 caracteres
  • Descreva o resultado da ação, não o formulário em si
  • Linguagem positiva (o que faz, não o que não faz)

toolautosubmit (opcional)

Quando presente, o formulário é submetido automaticamente ao ser invocado pelo agente. Sem esse atributo, o agente preenche os campos mas o usuário precisa clicar Submit manualmente.

<!-- Submissão automática pelo agente -->
<form toolname="quick_search"
      tooldescription="Search products by keyword"
      toolautosubmit>

Dica: Use toolautosubmit para ações de baixo risco (busca, filtros). Pra ações irreversíveis (compras, exclusões), omita — força confirmação humana. Parece paranoia, mas é o tipo de detalhe que evita dor de cabeça com usuário real.


Atributos dos campos

toolparamdescription

Dá contexto semântico sobre o que cada campo espera. Sem ele, o browser usa o conteúdo do <label> associado ou aria-description.

<select name="department" required
  toolparamdescription="Determines which team handles this request">
  <option value="support">Technical Support</option>
  <option value="billing">Billing Questions</option>
  <option value="sales">Sales Inquiry</option>
</select>

Hierarquia de fallback para descrição do parâmetro:

  1. toolparamdescription (prioridade máxima)
  2. <label> associado ao campo
  3. aria-description
  4. Sem descrição (campo incluído no schema sem description)

Exemplo completo — Formulário de suporte

<form toolname="support_request"
      tooldescription="Submit a customer support request. Routes to appropriate team based on category."
      action="/api/support"
      method="POST">

  <label for="name">Full Name</label>
  <input type="text" id="name" name="name" required
    toolparamdescription="Customer's full name for identification">

  <label for="email">Email</label>
  <input type="email" id="email" name="email" required
    toolparamdescription="Email address for response delivery">

  <label for="category">Category</label>
  <select id="category" name="category" required
    toolparamdescription="Determines which team handles this request">
    <option value="returns">Return my purchase</option>
    <option value="shipping">Check package status</option>
    <option value="technical">Website help</option>
  </select>

  <label for="message">Message</label>
  <textarea id="message" name="message" required
    toolparamdescription="Detailed description of the issue (max 1000 chars)"></textarea>

  <label>
    <input type="checkbox" name="urgent">
    Mark as urgent
  </label>

  <button type="submit">Submit Request</button>
</form>

JSON Schema gerado pelo browser

Para o formulário acima, o browser gera automaticamente este schema e expõe ao agente:

[{
  "name": "support_request",
  "description": "Submit a customer support request. Routes to appropriate team based on category.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Customer's full name for identification"
      },
      "email": {
        "type": "string",
        "description": "Email address for response delivery"
      },
      "category": {
        "type": "string",
        "anyOf": [
          { "type": "string", "const": "returns", "title": "Return my purchase" },
          { "type": "string", "const": "shipping", "title": "Check package status" },
          { "type": "string", "const": "technical", "title": "Website help" }
        ],
        "enum": ["returns", "shipping", "technical"],
        "description": "Determines which team handles this request"
      },
      "message": {
        "type": "string",
        "description": "Detailed description of the issue (max 1000 chars)"
      },
      "urgent": {
        "type": "boolean"
      }
    },
    "required": ["name", "email", "category", "message"]
  }
}]

Repare:

  • Campos com required viram required no schema
  • <select> gera enum + anyOf com os values e textos dos <option>
  • <input type="checkbox"> vira boolean
  • <input type="email"> vira string (o browser não infere formatos)
  • toolparamdescription vira description na property

SubmitEvent ampliado

O WebMCP estende o SubmitEvent com duas propriedades novas:

agentInvoked (boolean)

Indica se o submit veio de um agente de IA (true) ou de ação humana (false).

respondWith(promise)

Permite retornar um resultado estruturado ao agente. Exige preventDefault() antes.

Exemplo — Validação e resposta ao agente

const form = document.querySelector('form[toolname="support_request"]');

form.addEventListener('submit', (event) => {
  event.preventDefault();
  
  // Validação customizada
  const formData = new FormData(form);
  const message = formData.get('message');
  
  if (message.length < 10) {
    if (event.agentInvoked) {
      // Retorna erro estruturado ao agente
      event.respondWith(Promise.resolve(JSON.stringify({
        error: 'Message too short. Minimum 10 characters required.',
        currentLength: message.length
      })));
    } else {
      // Feedback visual para humano
      showError('Mensagem muito curta. Mínimo 10 caracteres.');
    }
    return;
  }
  
  // Processamento
  const resultPromise = submitSupportRequest(formData)
    .then(result => JSON.stringify({
      success: true,
      ticketId: result.id,
      message: `Ticket #${result.id} created. Response within 24h.`
    }))
    .catch(err => JSON.stringify({
      error: 'Submission failed. Please try again.',
      details: err.message
    }));
  
  if (event.agentInvoked) {
    event.respondWith(resultPromise);
  } else {
    resultPromise.then(() => showSuccess('Ticket criado!'));
  }
});

Atenção: Se você não chamar respondWith() quando agentInvoked é true, o agente fica sem feedback. Sempre forneça um resultado — nem que seja só um “ok, deu certo”.


Eventos de ativação

toolactivated

Dispara quando o agente começa a preencher campos do formulário. Non-cancelable — você não pode impedir o preenchimento, mas pode reagir a ele.

window.addEventListener('toolactivated', (event) => {
  const { toolName } = event;
  console.log(`Agente está preenchendo: ${toolName}`);
  
  // Possíveis reações:
  // - Mostrar indicador visual
  // - Desabilitar edição humana temporariamente
  // - Carregar dados necessários
  highlightForm(toolName);
});

toolcancel

Dispara quando a operação é cancelada pelo agente ou quando form.reset() é invocado.

window.addEventListener('toolcancel', (event) => {
  const { toolName } = event;
  console.log(`Operação cancelada: ${toolName}`);
  
  // Limpar indicadores visuais
  removeHighlight(toolName);
});

CSS Pseudo-classes

O WebMCP adiciona pseudo-classes CSS para sinalizar visualmente quando um agente está interagindo com um formulário:

:tool-form-active

Aplicada ao <form> enquanto o agente está interagindo (preenchendo campos).

:tool-submit-active

Aplicada ao botão de submit enquanto o agente está processando a submissão.

Exemplo de estilos

/* Indicador visual: agente está preenchendo o form */
form:tool-form-active {
  outline: 2px dashed light-dark(#2563eb, #60a5fa);
  outline-offset: 4px;
  position: relative;
}

form:tool-form-active::before {
  content: '🤖 AI Agent is filling this form';
  position: absolute;
  top: -2rem;
  left: 0;
  font-size: 0.75rem;
  color: light-dark(#2563eb, #60a5fa);
  background: light-dark(#eff6ff, #1e3a5f);
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
}

/* Indicador no botão de submit */
button:tool-submit-active,
input[type="submit"]:tool-submit-active {
  outline: 2px dashed light-dark(#dc2626, #f87171);
  outline-offset: 2px;
  opacity: 0.7;
  pointer-events: none;
}

Quando desativam

As pseudo-classes são removidas quando:

  • O formulário é submetido com sucesso
  • O agente cancela a operação
  • O usuário chama form.reset()

Dica: Use essas pseudo-classes pra dar feedback visual claro ao usuário. Tem um agente operando ali — transparência gera confiança.


Registro e desregistro dinâmico

A API Declarativa suporta registro dinâmico com base na presença dos atributos:

Registrar: Adicionar atributos

// Transformar form existente em tool
const form = document.getElementById('search-form');
form.setAttribute('toolname', 'search_products');
form.setAttribute('tooldescription', 'Search products by keyword and category');
// Tool registrada na hora!

Desregistrar: Remover atributos

// Remover tool
form.removeAttribute('toolname');
// Ou remover description (ambos são required)
form.removeAttribute('tooldescription');
// Tool desregistrada!

Isso abre patterns como:

  • Registrar tools depois que o usuário faz login
  • Remover tools enquanto um processo está em andamento
  • Condicionar tools ao estado da aplicação

Combinando Declarativa + Imperativa

As duas APIs convivem na mesma página sem conflito. O padrão natural:

  • Imperativa para lógica (busca, navegação, cálculos)
  • Declarativa para formulários (contato, checkout, reservas)
<!-- Tool declarativa: formulário de contato -->
<form toolname="send_message"
      tooldescription="Send a contact message to our team"
      toolautosubmit
      action="/api/contact">
  <label for="email">Email</label>
  <input type="email" name="email" required>
  <label for="msg">Message</label>
  <textarea name="msg" required></textarea>
  <button type="submit">Send</button>
</form>

<script>
// Tool imperativa: busca de FAQ
const mc = document.modelContext ?? navigator.modelContext;
if (mc?.registerTool) {
  mc.registerTool({
    name: 'search_faq',
    description: 'Search frequently asked questions',
    inputSchema: {
      type: 'object',
      properties: { query: { type: 'string' } },
      required: ['query']
    },
    execute: async ({ query }) => {
      const results = await searchFAQ(query);
      return JSON.stringify(results);
    },
    annotations: { readOnlyHint: true }
  });
}
</script>

O agente enxerga as duas — send_message (declarativa) e search_faq (imperativa) — e escolhe qual usar com base no que o usuário pediu.


Mapeamento HTML → JSON Schema

Elemento HTML Tipo no Schema Notas
<input type="text"> string —
<input type="email"> string Sem format validation no schema
<input type="number"> number —
<input type="checkbox"> boolean —
<input type="hidden"> string Incluído no schema
<textarea> string —
<select> string + enum Options viram enum values
<select multiple> array de string —
Campo com required Adicionado ao required[] —

Boas práticas para formulários declarativos

Nomes de tools

❌ Ruim ✅ Bom Por quê
form1 book_appointment Descritivo, indica ação
myForm filter_products Verbo + substantivo
submit submit_review Específico sobre o que submete

Descrições

❌ Ruim ✅ Bom
“A form” “Submit a product review with rating (1-5) and text feedback”
“Use this to do stuff” “Book a restaurant table for a specific date, time and party size”
“Don’t use for orders” “Search our FAQ by keyword. Returns top 5 matching questions.”

Quando usar toolautosubmit

Cenário toolautosubmit? Justificativa
Busca/filtro ✅ Baixo risco, reversível
Newsletter signup ✅ Ação simples, não destrutiva
Compra/pagamento ❌ Alto risco, irreversível
Exclusão de conta ❌ Destrutiva, requer confirmação
Envio de mensagem Depende Se for suporte sim, se for público não

Acessibilidade e progressive enhancement

Para visitantes humanos

Formulários com atributos WebMCP continuam funcionando normalmente:

  • Os atributos toolname, tooldescription, toolparamdescription são ignorados por browsers sem suporte
  • O formulário submete normalmente via action e method
  • Estilos CSS padrão se aplicam (pseudo-classes WebMCP são no-op em browsers sem suporte)

Para agentes

  • O browser gera JSON Schema automaticamente dos campos
  • O agente recebe a mesma interface estruturada da API Imperativa
  • toolautosubmit define se o agente pode completar a ação sozinho

Esse é o ponto forte: um formulário, dois públicos, zero código duplicado. Já pensou no ganho de manutenção?


Formulários multi-step

Formulários com múltiplas etapas? Registre cada etapa como tool separada:

<!-- Step 1 -->
<form toolname="booking_select_date"
      tooldescription="Select date and party size for restaurant booking"
      toolautosubmit>
  <input type="date" name="date" required>
  <input type="number" name="guests" min="1" max="12" required
    toolparamdescription="Number of guests (1-12)">
  <button type="submit">Next</button>
</form>

<!-- Step 2 (aparece depois do submit do step 1) -->
<form toolname="booking_select_time"
      tooldescription="Select preferred time slot for the booking"
      toolautosubmit>
  <select name="time" required
    toolparamdescription="Available time slot">
    <option value="19:00">7:00 PM</option>
    <option value="20:00">8:00 PM</option>
    <option value="21:00">9:00 PM</option>
  </select>
  <button type="submit">Confirm</button>
</form>

O agente vê apenas a tool da etapa atual — conforme a UI muda, as tools declarativas se registram/desregistram automaticamente.


Troubleshooting

Problema Causa Solução
Tool não aparece Falta toolname ou tooldescription Ambos são obrigatórios
Campos não no schema Form fora do DOM Garantir que form está renderizado
agentInvoked sempre false Testando manualmente Use a extensão Model Context Tool Inspector
Pseudo-classes não aplicam Browser sem suporte Verificar Chrome 149+ com flag
respondWith não funciona Faltou preventDefault Chamar e.preventDefault() antes

Próximos passos