Finanpy
by Gahbrielms
README.md
# 💰 Finanpy — MCP Server de Finanças Pessoais
Finanpy é um servidor MCP (Model Context Protocol) para análise e gestão de finanças pessoais. Ele conecta um banco de dados SQLite a qualquer cliente MCP compatível, permitindo registrar, consultar e analisar transações financeiras através de linguagem natural.
---
## 📁 Estrutura do Projeto
```
.
├── finance.db # Banco de dados SQLite com as transações
├── database.py # Camada de acesso ao banco de dados
├── mcp_server.py # Servidor MCP com as ferramentas expostas
├── import_csv.py # Script para importar extratos do Nubank (CSV)
└── sample_agent.py # Agente de exemplo com LangChain + LangGraph
```
---
## 🗄️ Banco de Dados
O projeto utiliza SQLite com uma única tabela principal:
### Tabela `transactions`
| Coluna | Tipo | Descrição |
|---------------|---------|--------------------------------------------------------|
| `id` | INTEGER | Chave primária, autoincremento |
| `identifier` | TEXT | Identificador único da transação (evita duplicatas) |
| `type` | TEXT | Tipo da transação: `receita` ou `despesa` |
| `amount` | REAL | Valor da transação (negativo para despesas) |
| `category` | TEXT | Categoria da transação (padrão: `sem categoria`) |
| `description` | TEXT | Descrição ou histórico da transação |
| `date` | TEXT | Data da transação em ISO 8601: `YYYY-MM-DD` |
> **Timezone:** todas as datas são armazenadas em horário local (America/Sao_Paulo). Não há offset UTC explícito no banco.
> **Arredondamento:** valores monetários são armazenados como `REAL` com 2 casas decimais, usando arredondamento half-up no momento da inserção.
---
## ⚙️ Instalação
### Pré-requisitos
- Python 3.10+
- uv
### Dependências
```bash
uv add fastmcp
```
Para utilizar o agente de exemplo (`sample_agent.py`), instale também:
```bash
uv add langchain-openai langchain-mcp-adapters langgraph python-dotenv
```
### Configuração
1. Clone o repositório.
2. Inicialize o banco de dados:
```bash
python database.py
```
3. (Opcional) Importe um extrato do Nubank:
```bash
# Coloque o arquivo CSV exportado do Nubank na raiz do projeto:
# extrato_nubank_05_2026.csv
python import_csv.py
```
---
## 🚀 Executando o Servidor MCP
```bash
python mcp_server.py
```
### Transporte STDIO
O Finanpy utiliza transporte **STDIO**: o cliente MCP inicia o servidor como um **processo filho** e se comunica com ele via `stdin`/`stdout` usando o protocolo MCP. Não há porta de rede envolvida.
**Fluxo de inicialização:**
1. O client executa `python mcp_server.py` como subprocesso.
2. O servidor anuncia suas capacidades (`tools`, `resources`) via protocolo MCP pelo `stdout`.
3. O client recebe o manifesto e passa a chamar ferramentas ou consultar recursos conforme necessário.
4. O processo do servidor vive enquanto o client estiver ativo e é encerrado junto com ele.
**Quando o client decide chamar cada ferramenta/recurso:**
- Consulta `resumo_financeiro_atual` (recurso) para orientar o raciocínio antes de responder perguntas sobre saldo ou categorias.
- Chama `registrar_transacao` quando o usuário pede para registrar, lançar ou salvar uma transação.
- Chama `listar_transacoes` quando o usuário pede consultas, históricos ou totais por período/categoria.
---
## 🔧 Ferramentas e Recursos MCP
### Ferramenta 1 — `registrar_transacao`
Registra uma nova transação financeira no banco de dados de forma idempotente.
**Entradas:**
| Parâmetro | Tipo | Obrigatório | Descrição |
|---------------|--------|-------------|---------------------------------------------------------------------------|
| `type` | string | ✅ | `"receita"` ou `"despesa"` |
| `amount` | float | ✅ | Valor absoluto da transação (sempre positivo; o sinal é inferido do `type`) |
| `category` | string | ✅ | Categoria (ex: `"alimentacao"`, `"transporte"`, `"moradia"`) |
| `description` | string | ✅ | Texto livre descrevendo a transação |
| `date` | string | ❌ | Data em ISO 8601 `YYYY-MM-DD` (padrão: data atual) |
| `identifier` | string | ❌ | UUID ou chave externa para idempotência (ex: ID do extrato bancário) |
**Saída (sucesso):**
```json
{
"sucesso": true,
"id": 42,
"normalizado": {
"amount": 73.90,
"date": "2026-06-23",
"type": "despesa"
},
"avisos": []
}
```
**Saída (duplicata detectada):**
```json
{
"sucesso": false,
"code": "DUPLICATE",
"id_existente": 17,
"mensagem": "Transação com este identifier já existe."
}
```
**Erros e limites:**
| Código | Causa | O que o client deve fazer |
|------------------|-----------------------------------------------------------|----------------------------------------------------------------|
| `INVALID_DATE` | Data fora do formato ISO 8601 ou data futura | Corrigir o formato para `YYYY-MM-DD` antes de reenviar |
| `DUPLICATE` | `identifier` já existe no banco | Informar o usuário; não reenviar |
| `BAD_TYPE` | `type` diferente de `"receita"` ou `"despesa"` | Normalizar para um dos dois valores antes de reenviar |
| `INVALID_AMOUNT` | `amount` negativo, zero ou não numérico | Solicitar o valor correto ao usuário; nunca inferir |
| `DB_BUSY` | SQLite travado por operação concorrente | Retry com backoff exponencial: 200ms, 400ms, 800ms (máx. 3x) |
| `MISSING_FIELD` | Campo obrigatório ausente | Solicitar o campo faltante ao usuário antes de reenviar |
> **Atenção — sinal vs. tipo:** nunca envie `amount` negativo com `type="despesa"`. O servidor armazena despesas com sinal negativo internamente; a entrada esperada é sempre o valor absoluto. Se o client receber `amount` negativo do usuário junto com `type="despesa"`, deve aplicar `abs(amount)` antes de chamar a ferramenta.
---
### Ferramenta 2 — `listar_transacoes`
Lista e filtra transações financeiras armazenadas no banco.
**Entradas:**
| Parâmetro | Tipo | Obrigatório | Descrição |
|------------|--------|-------------|-----------------------------------------------------|
| `period` | string | ❌ | Período no formato `YYYY-MM` (ex: `"2026-05"`) |
| `category` | string | ❌ | Filtra pelo nome exato da categoria |
| `type` | string | ❌ | `"receita"` ou `"despesa"` |
**Saída (sucesso):**
```json
[
{
"id": 1,
"identifier": "69fa0c39-ca20-4683-849a-85519872249a",
"type": "receita",
"amount": 50.0,
"category": "sem categoria",
"description": "Resgate RDB",
"date": "2026-05-05"
}
]
```
Retorna uma lista vazia `[]` quando nenhum registro corresponde aos filtros (não é um erro).
**Erros e limites:**
| Código | Causa | O que o client deve fazer |
|------------------|-----------------------------------------------------------|----------------------------------------------------------------|
| `INVALID_PERIOD` | Formato de período inválido (ex: `"05-2026"`) | Corrigir para `YYYY-MM` antes de reenviar |
| `BAD_TYPE` | `type` diferente de `"receita"` ou `"despesa"` | Normalizar antes de reenviar |
| `DB_BUSY` | SQLite travado por operação concorrente | Retry com backoff exponencial: 200ms, 400ms, 800ms (máx. 3x) |
> **Janela recomendada:** evite consultar períodos superiores a 12 meses sem filtro de categoria, pois o retorno pode ser extenso. Prefira decompor em múltiplas chamadas mensais se necessário.
---
### Recurso MCP — `resumo_financeiro_atual`
**URI:** `finance://resumo_financeiro_atual`
Recurso somente-leitura que retorna um snapshot consolidado das finanças do **mês corrente**. Diferente das ferramentas (que executam ações), este recurso é consultado pelo agente para **orientar seu raciocínio** antes de responder perguntas sobre saldo, categorias e situação financeira geral — evitando chamadas desnecessárias a `listar_transacoes`.
**Saída:**
```json
{
"period": "2026-06",
"total_income": 9947.94,
"total_expense": 9947.94,
"balance": 0.0,
"expenses_by_category": {
"sem categoria": 9947.94
}
}
```
**Quando o client deve consultá-lo:**
- Antes de responder qualquer pergunta sobre saldo atual, total gasto ou total recebido no mês.
- Para verificar se há dados suficientes antes de sugerir categorias ao usuário.
- Como ponto de partida para decidir se é necessário chamar `listar_transacoes` com filtros mais específicos.
---
## 📥 Importação de Extrato do Nubank
O script `import_csv.py` importa automaticamente transações a partir do CSV exportado pelo aplicativo do Nubank.
**Formato esperado do CSV:**
| Data | Valor | Identificador | Descrição |
|------------|---------|---------------|--------------------|
| 05/05/2026 | -50.00 | 69fa0c7e-... | Transferência Pix |
| 06/05/2026 | 188.03 | 69fbee60-... | Resgate RDB |
O tipo da transação é derivado automaticamente pelo sinal do valor:
- Valor **negativo** → `despesa`
- Valor **positivo** → `receita`
Transações com `identifier` já existente são ignoradas automaticamente (idempotência via `INSERT OR IGNORE`).
> **Categorias:** todas as transações importadas via CSV chegam como `"sem categoria"`. Use o agente para categorizá-las após a importação.
---
## 🔄 Cenário de Uso Ponta a Ponta
**Mensagem do usuário:**
> "Quanto gastei em alimentação em maio de 2026? E registra aí um Uber de R$ 73,90 que peguei hoje."
**Raciocínio do agente:**
1. A pergunta envolve o mês atual — consultar o recurso `resumo_financeiro_atual` para ter contexto geral antes de filtrar.
2. Para o total de alimentação em maio, chamar `listar_transacoes` com filtros de período e categoria.
3. Para registrar o Uber, chamar `registrar_transacao` com os dados fornecidos.
---
**Passo 1 — Consulta ao recurso `resumo_financeiro_atual`**
```
GET finance://resumo_financeiro_atual
```
Resposta do servidor:
```json
{
"period": "2026-06",
"total_income": 1835.29,
"total_expense": 462.75,
"balance": 1372.54,
"expenses_by_category": {
"sem categoria": 462.75
}
}
```
*O agente identifica que o mês atual é junho — a pergunta é sobre maio, então precisa de uma chamada específica.*
---
**Passo 2 — Chamada à ferramenta `listar_transacoes`**
```json
{
"period": "2026-05",
"category": "alimentacao",
"type": "despesa"
}
```
Resposta do servidor:
```json
[
{
"id": 8,
"identifier": "nubank-2026-05-12-ifood",
"type": "despesa",
"amount": -49.90,
"category": "alimentacao",
"description": "iFood - Pizza",
"date": "2026-05-12"
},
{
"id": 15,
"identifier": "nubank-2026-05-22-mercado",
"type": "despesa",
"amount": -312.40,
"category": "alimentacao",
"description": "Mercado Extra",
"date": "2026-05-22"
}
]
```
*Total calculado pelo agente: R$ 362,30.*
---
**Passo 3 — Chamada à ferramenta `registrar_transacao`**
```json
{
"type": "despesa",
"amount": 73.90,
"category": "transporte",
"description": "Uber",
"date": "2026-06-23",
"identifier": "manual-2026-06-23-uber-001"
}
```
Resposta do servidor:
```json
{
"sucesso": true,
"id": 27,
"normalizado": {
"amount": 73.90,
"date": "2026-06-23",
"type": "despesa"
},
"avisos": []
}
```
---
**Resposta final ao usuário:**
> "Em maio de 2026 você gastou **R$ 362,30 em alimentação** (iFood R$ 49,90 + Mercado Extra R$ 312,40). Sua despesa de Uber de **R$ 73,90** foi registrada em transporte para hoje, 23/06/2026. ✅"
---
## 🤖 Agente de Exemplo
O arquivo `sample_agent.py` demonstra como integrar o servidor MCP com um agente LangChain + LangGraph usando GPT-4o-mini como modelo de linguagem, via transporte STDIO.
### Configuração
Crie um arquivo `.env` na raiz do projeto:
```env
OPENAI_API_KEY=sua_chave_aqui
```
### Executando o agente
```bash
python sample_agent.py
```
**Exemplo de interação real:**
```
Finanpy - Assistente Financeiro Pessoal
Digite 'sair' para encerrar.
Você: Poderia categorizar as minhas transações realizadas? E também fazer uma somatória transacionada para cada categoria?
Finanpy: processando...
Finanpy: Aqui está a categorização das suas transações e a somatória para cada categoria:
### Receitas
1. Resgate RDB: R$ 50,00
2. Resgate RDB: R$ 188,03
...
**Total de Receitas**: R$ 9.882,74
### Despesas
1. Transferência enviada pelo Pix: R$ -50,00
...
**Total de Despesas**: R$ -10.188,84
```
---
## 🧩 Integração com Claude Desktop
Para usar o Finanpy diretamente no Claude Desktop, adicione ao arquivo de configuração do MCP (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"finanpy": {
"command": "python",
"args": ["/caminho/absoluto/para/mcp_server.py"]
}
}
}
```
> O campo `"transport"` pode ser omitido — o Claude Desktop usa STDIO por padrão. Use o caminho **absoluto** para o `mcp_server.py` para evitar erros de working directory.
Após reiniciar o Claude Desktop, as ferramentas e recursos do Finanpy estarão disponíveis automaticamente.
---
## 🔒 Segurança e Privacidade
- **Dados sensíveis (PII):** o banco contém informações financeiras pessoais, incluindo descrições de transações que podem conter nomes de pessoas (ex: transferências Pix). Não compartilhe o arquivo `finance.db` publicamente.
- **Logs:** evite ativar logging detalhado em produção, pois descrições de transações podem aparecer nos logs. Em desenvolvimento, use nível `WARNING` ou superior.
- **Acesso ao banco:** o SQLite não tem autenticação nativa. Garanta que o arquivo `finance.db` tenha permissões restritas ao usuário do sistema que executa o servidor (`chmod 600 finance.db`).
- **Variáveis de ambiente:** nunca comite o arquivo `.env` com chaves de API no repositório. Adicione-o ao `.gitignore`.
---
## 📌 Observações Técnicas
- **Datas:** o campo `date` é armazenado como texto em formato `YYYY-MM-DD` (ISO 8601). Entradas no formato `DD/MM/YYYY` (como as do CSV do Nubank) são convertidas internamente. O client deve sempre enviar datas em ISO 8601.
- **Categorias:** não há validação de categoria no servidor — qualquer string é aceita. Para padronizar, consulte o recurso `resumo_financeiro_atual` para ver as categorias já existentes antes de registrar uma nova transação.
- **Idempotência:** o campo `identifier` garante que a mesma transação não seja inserida duas vezes (`INSERT OR IGNORE`). Ao importar extratos, use sempre o ID original do banco como `identifier`.
- **Concorrência:** SQLite não suporta múltiplos escritores simultâneos. Se múltiplos agentes estiverem ativos, implemente retry com backoff exponencial em caso de `DB_BUSY`.
- **Banco de dados:** o arquivo `finance.db` é criado automaticamente na raiz do projeto ao executar `database.py` ou `mcp_server.py`.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues