Lição 5 de 9: MCP com Postgres real
- Arquivo
.mcp.example.jsonno repo (template público) +.mcp.jsonlocal (referencia${POSTGRES_URL}, não comitado); credencial fica na env varPOSTGRES_URL, fora dos dois arquivos - MCP do Postgres conectado e respondendo a queries do Claude
- Pelo menos 1 relatório real salvo em
docs/reports/<data>.mdgerado via MCP - Commit
feat(mcp): postgres mcp wired + first report
De psql pra "Claude responde em 30s"
Você vai plugar o Claude no banco de produção (via ) pra ele responder perguntas em pt-BR sobre dados reais. Uma pergunta tipo "quantos recibos foram criados na última semana?" passa de "vou ter que abrir o psql, escrever um SELECT, interpretar" pra "Claude responde em 30 segundos com dados reais."
Conectar o Claude ao Postgres muda a sua relação com o produto: você para de perguntar "como faço um SELECT pra X?" e passa a perguntar "X aconteceu na última semana?". A lição é cara em atenção (a configuração tem pegadinhas) e barata em código (10 linhas no total). O retorno compensa: depois disso, toda pergunta sobre dados reais vira uma pergunta ao Claude, não uma sessão no psql.
Vai passar por:
- Criar um usuário de banco que só lê (segurança primeiro):
claude_readonlycomGRANT SELECT - Configurar o Claude pra usar esse usuário via
.mcp.json(fora do git) - Criar
.mcp.example.json(template público) que entra no git - Gerar seu primeiro relatório real via Claude, salvo em
docs/reports/<data>.md
Pré-requisitos desta lição
- Postgres de produção rodando. Provisionado na L02 (Vercel Postgres, Supabase, Neon, Railway, qualquer um). Se você usou SQLite, Vercel KV, ou outro storage que não é Postgres, a lição não se aplica direto: pula pro Build Diary e segue pra L06. Pra dado público em CSV/Parquet, veja logo abaixo o callout "Sem Postgres? Caminho equivalente com DuckDB".
- Acesso de admin ao banco (superuser ou owner): pra rodar
CREATE USEReGRANT. Se você usa Vercel Postgres / Supabase, o painel deles dá esse acesso pelo SQL Editor. Se for Railway/Neon, você tem a connection string com permissões altas no.env.local. opensslno PATH: pra gerar a senha do usuário readonly.
Restrição de stack
Esta lição assume Postgres. Outros sistemas (MySQL, MongoDB, DynamoDB) têm próprios. A lógica é a mesma (user separado read-only, MCP server específico, .mcp.example.json no git), mas os comandos SQL e o nome do pacote npm mudam. Adapte conforme o seu caso.
Passo 1 — Decida read-only ou read-write
Pra esta lição: read-only sempre. Mesmo que você queira "Claude criando recibo de teste" no futuro, comece read-only. As razões:
- O ganho de read-only já é 80% do valor (relatórios, debug, análise)
- Read-write num produto público com tráfego real abre espaço pra mudança não-auditada: se o Claude faz um UPDATE errado, você só descobre depois
- Você pode adicionar write depois, com um segundo MCP de escopo limitado a uma tabela específica; não precisa ser tudo ou nada
Passo 2 — Crie um usuário read-only no Postgres
Não use o DATABASE_URL da aplicação, que tem permissões de write. Crie um usuário separado, só de leitura.
Conecta no Postgres de produção (use o connection string com permissões de superuser/owner — você tem isso no Vercel Postgres / Supabase / wherever). Executa:
CREATE USER claude_readonly WITH PASSWORD '<<gere-com-openssl-rand-hex-32>>';
GRANT CONNECT ON DATABASE <nome-do-db> TO claude_readonly;
GRANT USAGE ON SCHEMA public TO claude_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO claude_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO claude_readonly;
Mostre o SQL antes de executar. Quando passar, me retorna o connection string completo no formato: postgres://claude_readonly:<senha>@<host>:5432/<db>?sslmode=require — vou guardar como a variável de ambiente POSTGRES_URL.Aprove. Anota a connection string num lugar seguro (gestor de senha); você não vai guardar no repo.
Passo 3 — Instale o MCP do Postgres
Verifica se o pacote `@modelcontextprotocol/server-postgres` (ou equivalente atual) está disponível via npx:
npx -y @modelcontextprotocol/server-postgres --help
Se rodar e mostrar usage, está OK. Se der erro de "not found", procura o nome atual do servidor MCP de Postgres mantido pela Anthropic ou comunidade — pode ter mudado. Não invente nome — checa primeiro.Confirma que existe. Esse é o servidor MCP padrão pro Postgres.
Passo 4 — Crie .mcp.example.json (público) + .mcp.json (local)
Cria `.mcp.example.json` na raiz do projeto (entra no git) com:
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-postgres",
"${POSTGRES_URL}"
]
}
}
}
A connection string NÃO entra no JSON literal — Claude Code expande `${POSTGRES_URL}` a partir do ambiente, então o segredo nunca fica hardcoded no arquivo. Por isso o `.mcp.example.json` (com a referência `${POSTGRES_URL}`) pode entrar no git sem vazar credencial.
Depois cria `.mcp.json` (NÃO entra no git — adiciona ao `.gitignore` se ainda não tiver `.mcp.json`) com o MESMO conteúdo. O valor real de `POSTGRES_URL` (o connection string do `claude_readonly`) você define como variável de ambiente — ex: no `.env.local` ou exportada no shell antes de abrir o Claude:
export POSTGRES_URL="postgres://claude_readonly:<senha>@<host>:5432/<db>?sslmode=require"
Mostre o diff antes de aplicar. Confirma:
1. `.mcp.example.json` está stageado pra commit
2. `.mcp.json` está no `.gitignore`
3. `.gitignore` tem entrada explícita: `.mcp.json` (não só `.mcp*` que poderia pegar `.mcp.example.json` por engano)Aprove. Confirma com cat .gitignore | grep mcp.
Passo 5 — Recarregue o Claude Code e teste
O .mcp.json é lido na startup do Claude Code. Saia e entre de novo (/exit, depois claude no projeto). Quando o Claude inicia, ele detecta o MCP server e pede aprovação. Aprove.
Cola no Claude:
Use o MCP do Postgres pra responder: quantos registros tem na tabela [recurso-da-fatia-1] (ex: recibos)? Conta com timestamp de criação dos últimos 7 dias.O Claude vai usar a ferramenta query do MCP (ou nome equivalente), rodar um SELECT COUNT(*) FROM [recurso] WHERE created_at > now() - interval '7 days' e responder. Se funcionar, o MCP está ligado.
Se falhar:
- "Permission denied": o
claude_readonlynão tem GRANT na tabela. RodaGRANT SELECT ON [recurso] TO claude_readonly(a tabela pode ter sido criada depois do GRANT inicial). - "Connection refused": connection string errada, ou o Postgres não aceita conexão externa. Confirma o
sslmode=requiree que o host aceita IPs externos (Vercel Postgres aceita; alguns Supabase precisam de configuração).
Passo 6 — Gere o primeiro relatório real
Esta é a parte que paga a lição. Cola no Claude:
Use o MCP do Postgres pra gerar um relatório semanal da fatia 1. Pergunta a si mesmo, em sequência:
1. Quantos registros foram criados na última semana vs semana anterior?
2. Qual o valor médio (se aplicável)?
3. Tem alguma anomalia que vale anotar (pico em dia específico, queda, etc.)?
Salva o resultado em `docs/reports/<data-de-hoje-ISO>.md` com:
- Cabeçalho com data + período coberto
- 3 perguntas + respostas baseadas em dados reais
- 1 frase de "o que isso significa pro produto" (qualitativo)
Mostre o diff antes de criar o arquivo.Aprove. O relatório vai pra docs/reports/2026-MM-DD.md. Esse arquivo é a primeira evidência tangível de que o MCP está funcionando. Você não rodou SQL, não exportou CSV, não abriu painel: só perguntou.
Passo 7 — Faça o commit
Faça commit de `.mcp.example.json` + `docs/reports/<data>.md` + ajustes em `.gitignore`. Mensagem: `feat(mcp): postgres mcp wired + first report`. **Confirma que `.mcp.json` não está sendo comitado** (pra ter certeza, mostra o `git diff --staged` antes do commit).Aprove. Confira no GitHub que o .mcp.json não aparece, só o .mcp.example.json. Esse passo é crítico.
Build Diary — no CEAP, Claude rodou query iterativa pra refinar o detector de fraude
O CEAP não usa MCP (é um projeto Python de análise estática, não tem banco de produção). Mas tem o equivalente: o Claude conectado ao notebook Jupyter, rodando queries em DataFrames Polars de forma iterativa, até refinar a classificação de "transação suspeita". Trecho do INVESTIGATION.md v3.1:
v3.1 Mapping Update (2026-01-05)
Added to Category 1 (Office Maintenance) to remove false positives:
- 84 - Public Administration (municipalities rent office space to deputies)
- 94 - Associations/Unions (rent space to deputies)
False Positives Successfully Removed:
- Municipalities (CNAE 84) - R$ 445k removed
- Associations/unions (CNAE 94) - R$ 590k removed
A v3.1 nasceu do Claude rodando query iterativa: "lista os CNPJs flagrados que parecem prefeituras", "agrupa por CNAE", "calcula quanto saiu se eu adicionar o CNAE 84 à categoria de escritório". 3 rounds de query → mudança de 4 linhas no código de mapping → R$ 1.5M de falsos positivos removidos.
O que isso muda pra você: o MCP encurta o ciclo de descoberta. Sem MCP, "vou ver se essa hipótese é verdade" passa por abrir o psql, lembrar a sintaxe, escrever a query e interpretar o resultado: fricção que faz você deixar a pergunta de lado em vez de responder. Com MCP, a pergunta em pt-BR vira uma resposta com dados, e você acaba checando hipóteses que antes assumiria.
Takeaways
- Sempre read-only pra produção.
claude_readonlycomGRANT SELECTno schema; nunca permissão de write. .mcp.example.jsonno git (template),.mcp.jsonno.gitignore. A credencial fica na env varPOSTGRES_URL; o JSON só referencia${POSTGRES_URL}. Confirma comgit diff --stagedantes do commit.- O MCP encurta o ciclo de descoberta: a pergunta em pt-BR vira dado, sem psql, e você checa hipóteses que antes assumiria.
- Fallback se o MCP não rola: script Node simples com
pg.Pool. Resolve o mesmo problema sem depender de a configuração do MCP convergir.
Você terminou quando
Quatro coisas:
.mcp.example.jsonno git,.mcp.jsonlocal (não comitado); a credencial mora na env varPOSTGRES_URL, não dentro do JSON- Usuário
claude_readonlycriado no Postgres de produção com permissão só SELECT - Claude responde pelo menos 1 pergunta sobre dados reais usando MCP
docs/reports/<data>.mdcommitado