Skip to main content
Glama
Gahbrielms

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`.