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 HTMLTipo no SchemaNotas
<input type="text">string
<input type="email">stringSem format validation no schema
<input type="number">number
<input type="checkbox">boolean
<input type="hidden">stringIncluído no schema
<textarea>string
<select>string + enumOptions viram enum values
<select multiple>array de string
Campo com requiredAdicionado ao required[]

Boas práticas para formulários declarativos

Nomes de tools

❌ Ruim✅ BomPor quê
form1book_appointmentDescritivo, indica ação
myFormfilter_productsVerbo + substantivo
submitsubmit_reviewEspecí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áriotoolautosubmit?Justificativa
Busca/filtroBaixo risco, reversível
Newsletter signupAção simples, não destrutiva
Compra/pagamentoAlto risco, irreversível
Exclusão de contaDestrutiva, requer confirmação
Envio de mensagemDependeSe 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

ProblemaCausaSolução
Tool não apareceFalta toolname ou tooldescriptionAmbos são obrigatórios
Campos não no schemaForm fora do DOMGarantir que form está renderizado
agentInvoked sempre falseTestando manualmenteUse a extensão Model Context Tool Inspector
Pseudo-classes não aplicamBrowser sem suporteVerificar Chrome 149+ com flag
respondWith não funcionaFaltou preventDefaultChamar e.preventDefault() antes

Próximos passos