financas-mcp
README.md
# financas-mcp
Servidor MCP (Streamable HTTP) que responde "posso comprar X em N parcelas?" com base na fatura real do cartão Nubank (Pluggy / Open Finance ou CSV) e nos tetos de gastos guardados em SQLite. Feito para ser conectado ao claude.ai como connector remoto via Cloudflare Tunnel.
## Rodar local
Requisitos: Node 22, pnpm 10 (via `corepack enable pnpm`).
```bash
cp .env.example .env
```
Edite `.env` e preencha `MCP_AUTH_TOKEN`. Sem `PLUGGY_*` o servidor usa o gateway CSV lendo `CSV_STATEMENTS_DIR` (padrão `./statements`).
```bash
pnpm install
pnpm dev
```
Confira:
```bash
curl http://localhost:3000/health
```
Outros comandos: `pnpm test`, `pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm db:generate` (nova migration após mudar `src/infrastructure/persistence/schema.ts`).
As migrations em `drizzle/` rodam no boot. Na primeira execução o seed insere tetos, gastos fixos, reserva anual e recorrentes esperados. Depois disso tudo é alterado pelas tools.
### CSV do Nubank
Exporte a fatura pelo app (colunas `date,title,amount`) e salve em `statements/YYYY-MM.csv`, onde `YYYY-MM` é o mês da fatura (a que vence em `YYYY-MM-10`). Sem mês no nome, o gateway deduz pelo dia de fechamento. Exemplo em `test/fixtures/`.
## Criar o token
```bash
openssl rand -hex 32
```
Coloque o valor em `MCP_AUTH_TOKEN`. Toda requisição a `/mcp` precisa de `Authorization: Bearer <token>`; `GET /health` é aberto.
## Docker (local, sem túnel)
```bash
docker compose -f docker-compose.yml -f docker/compose.local.yml up --build mcp
```
Publica `127.0.0.1:3000`. O SQLite fica no volume `mcp-data` (`/data/financas.db`) e `./statements` é montado somente leitura em `/statements`.
## Cloudflare Tunnel
1. No Cloudflare Zero Trust: Networks → Tunnels → Create a tunnel (tipo Cloudflared). Copie o token.
2. Em Public Hostname, aponte um subdomínio (ex.: `financas.seudominio.com`) para `http://mcp:3000`.
3. Coloque o token em `CLOUDFLARE_TUNNEL_TOKEN` no `.env`.
4. Suba os dois serviços:
```bash
docker compose up -d --build
```
O serviço `mcp` não publica porta no host; só o `cloudflared` fala com ele pela rede do compose. Teste com `curl https://financas.seudominio.com/health`.
Opcional: adicione uma Access Policy no Cloudflare para restringir ainda mais o hostname.
## Conectar no claude.ai
Settings → Connectors → Add custom connector:
- URL: `https://<seu-dominio>/mcp`
- Autenticação: Bearer token, valor de `MCP_AUTH_TOKEN`
Depois, em uma conversa, ative o connector e pergunte, por exemplo: "posso comprar um notebook de 4500 em 6x?".
## Tools
| Tool | Uso |
| --- | --- |
| `get_open_invoice` | Status da fatura aberta: à vista, parcelas, recorrentes pendentes, folgas, dias até fechar |
| `get_installment_schedule(months=12)` | Comprometimento com parcelas mês a mês |
| `can_i_buy(amount, installments=1, startMonth?)` | Avalia a compra e sugere `bestMonth` |
| `get_closed_invoices(n=6)` | Resumo das últimas faturas fechadas |
| `list/upsert/remove_recurring_expected` | Assinaturas esperadas no cartão |
| `list/upsert/remove_planned_purchase` | Compras planejadas |
| `get/update_budget_params` | Tetos, dia de fechamento e vencimento |
## Regras
- Fatura fecha no `closingDay` (padrão 2) e vence no `dueDay` (padrão 10). Compra entre `closingDay+1` de M-1 e `closingDay` de M entra na fatura M. Ambos configuráveis via `update_budget_params`.
- À vista: sem parcela (IOF de ida conta). Parcela nova: 1/N. Parcela antiga: k/N com k > 1. Pagamentos, estornos e IOF de volta não contam.
- Cronograma: parcela k/N na fatura aberta segue ativa por mais N-k meses; compras planejadas com mês definido entram como `amount/installments`.
- `can_i_buy` à vista compara com a folga de à vista da fatura aberta (teto - gasto - recorrentes pendentes). Parcelado compara `amount/installments` com a menor folga entre o mês inicial e o último mês.
- Total previsto do mês = cartão previsto + gastos fixos + aporte da reserva anual (`amount / meses da janela` dentro da janela).
## Pendências de validação com o payload real da Pluggy
Tudo isso está isolado em `src/infrastructure/pluggy/pluggy-mapper.ts` e `pluggy-card-statement-gateway.ts`:
1. Sinal e `type` das transações de cartão: assumido `DEBIT` = compra e `CREDIT` = pagamento ("pagamento" na descrição) ou estorno. Valor usa `Math.abs(amount)`.
2. `creditCardMetadata.totalAmount`: assumido como valor total da compra parcelada. O tipo do SDK descreve como "amount of the installment"; se for o valor da parcela, trocar `purchaseTotal` para `totalAmount * totalInstallments`.
3. IOF: detectado por prefixo "IOF" na descrição (e "IOF de volta" para devolução). Conferir como o Nubank via Open Finance nomeia esses lançamentos e se `feeType` ajuda.
4. Agrupamento por fatura: usa `creditCardMetadata.billForecastDate` (YYYY-MM) quando presente; senão cai na regra de calendário por data. Validar se o Nubank preenche esse campo e se o mês bate com o vencimento.
5. Datas: `transaction.date` convertida com `toISOString().slice(0, 10)` (UTC). Se a Pluggy devolver meia-noite local, pode deslocar um dia; ajustar para o fuso em `toIsoDate`.
6. Bills (`fetchCreditCardBills`): o mês da fatura é derivado de `dueDate`; `billClosingDate` pode vir `null`, então o fechamento cai no cálculo por `closingDay`. Conferir se as datas batem com a fatura do app.
7. Fatura aberta: `creditData.balanceCloseDate` e `balanceDueDate` da conta são usados como fechamento e vencimento. Confirmar se referem-se à fatura aberta.
8. Conta: usa a primeira conta `type === "CREDIT"` do item; se houver mais de um cartão, definir `PLUGGY_ACCOUNT_ID`.
9. Transações `PENDING` são incluídas. Decidir se devem entrar no cálculo.
10. Janela de busca: fatura aberta busca desde 5 dias antes do início do período; faturas fechadas buscam `limit` meses para trás numa só chamada (`fetchAllTransactions`). Avaliar volume e rate limit.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues