1

Transformei um vault do Obsidian em um servidor MCP na Cloudflare: 787 personas sintéticas que respondem pesquisa dentro do Claude

Passei as últimas semanas construindo uma prova de conceito que junta três coisas que eu queria testar fazia tempo: pesquisa sintética, MCP e até onde dá pra ir com o free tier da Cloudflare. O resultado é um painel de 787 personas do consumidor bancário brasileiro que qualquer pessoa pluga no Claude como conector remoto. Vou contar como funciona por dentro, incluindo o bug que me custou um dia inteiro de debug.

O problema
Trabalho com new ventures e discovery de produto. Pesquisa qualitativa é cara e lenta, e a alternativa preguiçosa (pedir pro LLM "simular 700 consumidores") produz lixo: o modelo devolve o mesmo respondente N vezes, com viés pra perfil de renda alta e escolaridade alta. Isso está documentado — Santurkar et al. (ICML 2023) mostraram o viés de representação, e Bisbee et al. (Political Analysis 2024) mostraram a variância artificialmente baixa. O caminho que a literatura aponta (Argyle et al., 2023) é condicionar o modelo em atributos sociodemográficos reais em vez de pedir criatividade.

Então a tese do projeto é: a heterogeneidade tem que vir da estrutura de dados, não do modelo. O LLM só dá voz.

A arquitetura
A fonte da verdade é um vault do Obsidian. Sério. Cada persona é uma nota markdown com frontmatter tipado (classe social, região, renda, banco principal, dívida ativa, dispositivo, uns 27 campos), cada estatística é uma nota atômica com um filtro determinístico no frontmatter (campo operador valor, com suporte a AND composto), e um script cruza os dois: pra cada persona, avalia quais fatos casam com os atributos dela e injeta uma seção de grounding idempotente entre marcadores HTML. Persona Classe C endividada do Nordeste recebe os fatos de Classe C, de endividamento e de Nordeste, cada um com percentual, tipo e fonte (IBGE, Bacen, Febraban).

O detalhe que mais deu trabalho conceitual: os fatos são tipados em propensão, prevalência, composição e referência. "95% dos compradores de carro importado são classe A/B" é composição, P(público|métrica) — se você ler isso como propensão, inverteu a probabilidade condicional e a persona vira uma caricatura. O servidor marca o tipo e injeta um aviso na ficha quando tem fato de composição, porque no teste de QA um modelo mais fraco quase tropeçou exatamente nisso.

Pra servir isso pro mundo: um build script compila o vault em JSONs (7 MB de markdown viram ~800 KB gzipado, dentro do limite de 3 MB do plano free), e um Worker na Cloudflare expõe tudo como servidor MCP remoto usando o SDK agents (McpAgent + Durable Objects). Oito ferramentas: sortear amostra com seed reprodutível, filtrar pela mesma DSL do vault, ficha completa, busca de fatos, vozes (verbatim reais de reviews), fichas de instituições e as tabelas de calibração.

Quem quiser usar: Settings → Connectors → Add custom connector no Claude, cola a URL, pronto. A conversa roda no Claude do usuário; meu servidor só responde consulta estruturada. Nunca vejo o que as pessoas perguntam às personas, o que aliás simplificou demais a página de privacidade.

Anti-extração sem autenticação
O painel é aberto e gratuito, mas os dados são o ativo. Não dá pra impedir cópia do que a ferramenta retorna, então o objetivo é outro: tornar a extração da base inteira impraticável. Um Durable Object conta requisições por IP: 120/min de rajada, mil chamadas/dia, e o limite que importa — 100 fichas completas por dia. Raspar 787 fichas num IP só levaria mais de uma semana. Custo disso tudo: zero, o free tier aguenta 100k requisições/dia.

O bug que valeu o post
Depois de tudo pronto e testado, fui testar num Claude de verdade e... nada. O conector aparecia nas configurações, listava as 8 ferramentas bonitinho na tela de permissões, e na conversa o modelo jurava que não tinha ferramenta nenhuma.

Debug do lado do servidor: handshake ok, tools/list ok, tudo 200. Logs mostrando que o claude.ai nem tentava conectar durante a conversa. Passei horas achando que era bug do cliente.

Era meu. Eu tinha fixado agents@0.0.100 no package.json (versão de quando comecei o rascunho), e a atual era 0.17.x. Essa versão antiga respondia 405 pro GET do stream no endpoint MCP. Acontece que a tela de configurações do claude.ai valida o conector só com POST — por isso listava as ferramentas — mas a conversa abre um stream GET, tomava 405 e desistia silenciosamente, sem erro em lugar nenhum. Atualizei o SDK (que de quebra exigiu zod 4), redeployei, e o conector passou a funcionar no app desktop na primeira tentativa.

Lição barata de escrever e cara de aprender: meus testes manuais com curl simulavam só o fluxo POST, então "passava tudo". Quando troquei pro cliente oficial de referência do protocolo (@modelcontextprotocol/sdk Client), a diferença apareceu na hora. Teste com o cliente real, não com o seu curl.

Validação
Pra não ficar só na arquitetura bonita: rodei backtest contra o Estudo idwall 2025 (amostra nacional ponderada por IBGE). Sorteei 100 personas, classifiquei com temperatura 0 e comparei os agregados. Erro médio de 6,3 pontos percentuais nas perguntas de atitude do consumidor, duas de três dentro da margem amostral do método. Transparência: são as 3 melhores de 8 categorias testadas — satisfação e incidência auto-reportada divergem mais, porque as narrativas das personas foram escritas com ênfase em dor. É viés de calibração conhecido e estou ajustando.

E a ressalva de sempre: isso não substitui pesquisa com gente de verdade nem A/B. É a etapa barata que vem antes, pra matar hipótese fraca sem queimar orçamento.

Site com passo a passo: https://painel.concorde-painel.workers.dev

Stack completa: Obsidian (fonte da verdade) → Node script de build → Cloudflare Workers + Durable Objects + agents SDK → MCP Streamable HTTP. Zero banco de dados, zero servidor pra manter, R$ 0/mês.

Feedback técnico é o que estou procurando. Onde vocês acham que isso quebra?

Carregando publicação patrocinada...