Dados públicos para agentes de IA: alvarás de 74 municípios
Como montei a coleta de seis sistemas municipais de alvarás de Massachusetts e um servidor MCP que ensina aos agentes de IA as ressalvas dos dados.
Os alvarás de obra de Massachusetts são públicos, mas ficam espalhados em seis sistemas municipais diferentes, sem API comum. Montei uma coleta de dados que junta os alvarás de 74 municípios numa base PostgreSQL e entrega tudo a agentes de IA por um servidor MCP somente leitura. Este texto trata do que muda quando quem consulta é um agente, e não uma pessoa: manter a coleta de pé e evitar que o agente tire conclusões erradas.
A página do projeto é Cape Cod Permits. Os números abaixo saem do código e do servidor MCP de produção.
O problema: uma base, seis plataformas
Cada município escolhe o seu fornecedor de sistema de alvarás. Neste projeto são OpenGov, PermitEyes, Patriot PermitPro, PermitLink, MapsOnline e, para um município, relatórios mensais em XLSX. Nenhum se parece com o outro:
- O PermitEyes tem uma listagem DataTables e endpoints JSON de detalhe.
- O Patriot PermitPro tem um painel de convidado que devolve JSON, do mais novo para o mais antigo.
- O PermitLink é uma página InterSystems Zen, lida em Chromium headless porque a paginação é uma requisição criptografada.
- O MapsOnline só oferece um PDF "Permits Listing" sem coluna de data. O coletor pede um dia por vez e lê as caixas de texto do PDF.
Hoje a base tem 147.639 registros de 74 municípios: 15 de Cape Cod e 59 do resto de Massachusetts. Essa contagem inclui registros administrativos, então não é contagem de obras. Volto nisso mais adiante, porque é exatamente o tipo de detalhe que o agente erra.
Coleta de dados de várias fontes: um registro, um coletor por plataforma
A primeira lição é normalizar muitas fontes atrás de um registro único. Cada município é uma entrada num arquivo de configuração, com platform, tenant e a URL do portal. O enumerador liga cada plataforma a uma função de passagem:
const PASS = {
opengov: passOpenGov, permiteyes: passPermitEyes, accela: passHarwich,
patriot: passPatriot, permitlink: passPermitLink, mapsonline: passMapsOnline,
};
for (const item of sources) {
await loadTenant(item); // estado retomável deste município
if (!due()) continue;
const run = PASS[item.platform];
if (!run) { state.paused_reason = `unsupported_platform:${item.platform}`; await save(); continue; }
try {
await run();
} catch (error) {
state.last_error = error.message; // este município falha, o laço segue
await save();
}
}
Incluir um município numa plataforma já suportada é só configuração. Uma plataforma nova é um coletor novo e uma linha em PASS. Patriot, PermitLink e MapsOnline geram uma linha de listagem por registro e por isso dividem o mesmo construtor de linhas no normalizador.
Também mapeei 1.262 municípios num raio de 200 milhas com o sistema que cada um usa. Esse arquivo decide qual coletor escrever em seguida.
Guarde o dado bruto para poder reprocessar
Os fornecedores mudam o front-end, e o meu parser também muda. Por isso a linha já processada nunca é a fonte da verdade. Cada resposta bruta do portal é gravada como um snapshot comprimido em gzip, endereçado pelo conteúdo, com hash SHA-256. O normalizador tem um número de versão:
export const PARSER_VERSION = 6; // Subir a versão reprocessa todos os registros.
Cada snapshot normalizado fica gravado com a versão do parser que o gerou. Quando subo a versão, o normalizador refaz todos os registros a partir dos arquivos brutos em disco. Nada é baixado de novo e os portais não recebem tráfego extra. Campos que não cabem no modelo comum ficam em campos dinâmicos e no snapshot bruto, então também não se perdem.
Isole as falhas e salve o progresso por município
Portal público falha o tempo todo: limite de requisições, credencial vencida, mudança de layout, indisponibilidade. Uma coleta de dados para agentes de IA precisa tratar isso como normal.
Duas regras resolvem. A primeira: a falha fica dentro da plataforma. Se um portal recusa a credencial, só os municípios dele pausam, com um paused_reason, e as outras plataformas seguem coletando. A segunda: o progresso é salvo por município no banco. O enumerador grava o estado a cada 20 IDs, a cada página de listagem e no fim de cada passagem. Um redeploy ou um travamento retoma de onde parou, em vez de recomeçar uma varredura que pode levar horas.
Monitore progresso, não ciclo concluído
O primeiro instinto é alertar quando nenhuma passagem de coleta terminou nos últimos minutos. Para esse tipo de carga isso está errado. Um backfill de um ano inteiro pode levar horas para um único município, então "nenhuma passagem terminou" dispararia em todo backfill e em toda atualização semanal.
O health check mede progresso. Como o enumerador salva o estado o tempo todo, o updated_at mais recente entre os municípios funciona como batimento:
const lastBeat = Math.max(0, ...states.map((s) => Date.parse(s.updated_at || 0) || 0));
if (states.length && lastBeat && Date.now() - lastBeat > STALE_BEAT_MS) {
reasons.push({ code: 'stale', text: `Enumerador sem progresso há ${min} min` });
}
Um backfill longo não gera alarme falso, e um travamento de verdade aparece em 20 minutos.
Um servidor MCP que ensina ao agente as ressalvas dos dados
Coletar é metade do trabalho. A outra metade é entregar os dados ao agente sem deixá-lo produzir bobagem com confiança. Esta base tem armadilhas que uma pessoa aprende em semanas e que o agente encontra na primeira consulta:
- Tipo de registro e status não são padronizados entre municípios. Há centenas de grafias.
- Um projeto gera vários alvarás (obra, elétrica, hidráulica, gás), além de registros administrativos. Contar alvarás não é contar obras.
- O
estimated_costé autodeclarado e só existe em 44% dos registros. - A cobertura é desigual. Cada município tem o seu intervalo de datas, e os de fora do Cape entram um por vez.
O servidor MCP tem 7 ferramentas somente leitura e um guia do dataset que o agente lê como recurso ou pela ferramenta dataset_docs. As ressalvas não ficam só no guia. Elas estão nas descrições das ferramentas, que é onde o agente olha quando decide o que chamar. O list_facets devolve o intervalo real de cada município em from_date e to_date, e a descrição manda chamá-lo antes de comparar municípios ou períodos. A descrição do permit_stats diz:
Responda com isto qualquer pergunta de "quantos", "quanto" ou
"quais os maiores", em vez de paginar search_permits. Cada linha traz
`permits`, `with_cost`, `total_cost` e `median_cost`. Como só 44% dos
registros têm custo, compare `with_cost` com `permits` antes de citar
a soma, e prefira a mediana à média.
Assim o servidor empurra o agente para agregados. O banco faz a conta numa consulta só, e o agente não precisa paginar resultados de 200 linhas e somar tudo na própria janela de contexto. O get_permit diz para usá-lo num caso, nunca em laço sobre uma lista. As instruções do servidor também pedem que o agente sempre diga o período e os filtros que usou.
O schema da ferramenta diz ao agente o que ele pode perguntar. As descrições e o guia do dataset dizem o que a resposta significa.
Controle de acesso por chave
O servidor fica atrás de chaves de API. Cada chave tem nome próprio no log de auditoria e limite próprio de requisições, por credencial e não por IP. O motivo está no código: um agente que recebe o pedido "me traga tudo" pagina até acabar, e um único agente não pode travar o banco. Toda leitura de dado de contato fica registrada com os filtros usados.
Lições
- Normalize muitas fontes atrás de um registro único. Uma entrada de configuração por município, um coletor por plataforma. Município novo é configuração.
- Guarde o dado bruto. Resposta bruta comprimida, com hash e versão do parser. Assim, corrigir o parser é reprocessar, não coletar de novo.
- Isole as falhas. Uma plataforma quebrada pausa só os municípios dela, e o progresso sobrevive a um redeploy.
- Monitore progresso. Em coleta longa, um batimento funciona melhor que "o ciclo terminou?".
- Escreva o servidor MCP pensando nos erros do agente. Coloque as ressalvas de cobertura e qualidade nas descrições das ferramentas, entregue um guia do dataset e leve o agente para os agregados.
Construí tudo isso com Claude Code, que é como trabalho em todos os projetos. Se você precisa de dados públicos coletados e entregues a agentes de IA de um jeito confiável, fale comigo.
Todos os artigos