Skip to main content
Glama
README.md
# mcp-mongo

MCP Server com acesso a MongoDB — Clean Architecture, Repository Pattern, Motor (async).

Compatível com **qualquer agente MCP**: Claude Code, Claude Desktop, LangChain, LlamaIndex e outros via HTTP.

---

## Índice

- [Visão Geral](#visão-geral)
- [Arquitetura](#arquitetura)
- [Estrutura de Ficheiros](#estrutura-de-ficheiros)
- [Instalação](#instalação)
- [Configuração](#configuração)
  - [Conexão com o Banco](#conexão-com-o-banco)
  - [Transporte MCP](#transporte-mcp)
  - [Referência completa de variáveis](#referência-completa-de-variáveis)
- [Execução](#execução)
- [Capacidades MCP](#capacidades-mcp)
  - [Tools](#tools)
  - [Resources](#resources)
  - [Prompts](#prompts)
- [Segurança](#segurança)
- [Testes](#testes)
- [Integração com Agentes](#integração-com-agentes)
  - [Claude Code / Claude Desktop](#claude-code--claude-desktop)
  - [LangChain / LlamaIndex](#langchain--llamaindex)
  - [Agentes HTTP genéricos](#agentes-http-genéricos)
  - [Docker](#docker)
- [Decisões de Arquitetura](#decisões-de-arquitetura)

---

## Visão Geral

Este servidor implementa o **Model Context Protocol (MCP)** para expor um banco de dados MongoDB a modelos de linguagem e agentes de IA.

O agente pode:
- Executar queries `find()` e `aggregate()` com filtros, projeções e ordenação
- Inspecionar databases, collections e inferir esquemas via amostragem de documentos
- Listar índices e obter estatísticas de collections
- Executar operações de escrita (quando explicitamente habilitado)

O servidor é **read-only por padrão** e funciona com **qualquer instância MongoDB** — basta mudar o `.env` ou a variável `MONGODB_URI`.

---

## Arquitetura

```
┌─────────────────────────────────────────────────────────┐
│              Agente (Claude / LangChain / …)            │
└────────────────────────┬────────────────────────────────┘
                         │ MCP Protocol
                    stdio │ ou HTTP (SSE / streamable-http)
┌────────────────────────▼────────────────────────────────┐
│                    MCP Server (FastMCP)                  │
│                                                         │
│   ┌──────────┐   ┌───────────┐   ┌──────────────────┐  │
│   │  Tools   │   │ Resources │   │    Prompts       │  │
│   └────┬─────┘   └─────┬─────┘   └──────────────────┘  │
│        │               │                                │
│   ┌────▼───────────────▼────────────────────────────┐   │
│   │              Repositories                       │   │
│   │   BaseRepository → QueryRepository              │   │
│   │                  → SchemaRepository             │   │
│   └────────────────────┬────────────────────────────┘   │
│                        │                                │
│   ┌────────────────────▼────────────────────────────┐   │
│   │         Database (Motor AsyncIOMotorClient)     │   │
│   └────────────────────┬────────────────────────────┘   │
│                        │                                │
│   ┌────────────────────▼────────────────────────────┐   │
│   │         Config (Pydantic Settings + .env)       │   │
│   └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
                         │ TCP
┌────────────────────────▼────────────────────────────────┐
│                      MongoDB                            │
└─────────────────────────────────────────────────────────┘
```

**Regra de dependência:** cada camada conhece apenas a camada imediatamente abaixo. Tools não conhecem o banco diretamente; Config não conhece ninguém.

---

## Estrutura de Ficheiros

```
mcp-mongo/
│
├── .env                             # Configuração activa (não commitado)
├── pyproject.toml                   # Dependências, scripts, ruff, mypy, pytest
├── .gitignore
│
├── src/
│   └── mcp_mongo/
│       ├── __init__.py
│       ├── server.py                # Entry point: cria FastMCP e selecciona transporte
│       │
│       ├── config/
│       │   ├── __init__.py
│       │   └── settings.py          # Pydantic Settings — MONGODB_URI ou MONGODB_*
│       │
│       ├── database/
│       │   ├── __init__.py
│       │   └── connection.py        # Singleton DatabaseClient (Motor) + lifecycle
│       │
│       ├── repositories/
│       │   ├── __init__.py
│       │   ├── base.py              # find_many, find_one, aggregate, writes + _serialize BSON
│       │   ├── query_repository.py  # find/aggregate com guard de writes
│       │   └── schema_repository.py # Introspecção: databases, collections, schema inferido, índices, stats
│       │
│       ├── tools/
│       │   ├── __init__.py
│       │   ├── query_tools.py       # find, find_one, aggregate, count, insert, update, delete
│       │   └── schema_tools.py      # list_databases, list_collections, describe_collection, list_indexes, get_collection_stats
│       │
│       ├── resources/
│       │   ├── __init__.py
│       │   └── schema_resources.py  # URIs: mongo://databases, mongo://db/{db}/collections, …
│       │
│       └── prompts/
│           ├── __init__.py
│           └── mongo_prompts.py     # explore_database, analyse_collection, write_query, write_aggregation, optimise_query
│
└── tests/
    ├── __init__.py
    ├── conftest.py
    └── tools/
        ├── test_query_repository.py
        └── test_schema_repository.py
```

---

## Instalação

**Pré-requisitos:** Python 3.11+, [uv](https://docs.astral.sh/uv/), MongoDB acessível.

```bash
cd mcp-mongo

# Instalar dependências
uv sync

# Com dependências de desenvolvimento
uv sync --extra dev
```

---

## Configuração

Toda a configuração é feita no ficheiro `.env` na raiz do projeto.

```bash
cp .env .env.local
# editar com os dados do teu ambiente
```

### Conexão com o Banco

Existem duas formas de configurar a conexão — usa a que for mais conveniente:

**Opção A — `MONGODB_URI` (tem prioridade)**

Uma única variável com a URI completa:

```dotenv
MONGODB_URI=mongodb://user:password@host:27017/dbname
```

Também suporta URIs com replica sets e opções adicionais:

```dotenv
MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/dbname
```

**Opção B — variáveis individuais**

```dotenv
MONGODB_HOST=localhost
MONGODB_PORT=27017
MONGODB_DB=mydb
MONGODB_USER=myuser
MONGODB_PASSWORD=secret
```

> Se `MONGODB_URI` estiver definida, os valores de `MONGODB_HOST`, `MONGODB_PORT`, etc., são ignorados. Caso contrário, as vars individuais são usadas. Se nenhuma for fornecida, o servidor tenta ligar a `localhost:27017/test`.

### Transporte MCP

A variável `MCP_TRANSPORT` define como o servidor comunica com o agente:

| Valor | Protocolo | Endpoint | Indicado para |
|---|---|---|---|
| `stdio` *(padrão)* | stdin/stdout | — | Claude Code, Claude Desktop, agentes locais |
| `sse` | HTTP Server-Sent Events | `http://host:port/sse` | LangChain, LlamaIndex, agentes HTTP legados |
| `streamable-http` | HTTP streaming | `http://host:port/mcp` | Agentes MCP modernos via HTTP |

Para HTTP, define também o host e a porta:

```dotenv
MCP_TRANSPORT=sse
MCP_HOST=0.0.0.0
MCP_PORT=8080
```

### Referência completa de variáveis

| Variável | Padrão | Descrição |
|---|---|---|
| `MONGODB_URI` | — | URI completa (prioridade sobre vars individuais) |
| `MONGODB_HOST` | `localhost` | Host do MongoDB |
| `MONGODB_PORT` | `27017` | Porta |
| `MONGODB_DB` | `test` | Database padrão |
| `MONGODB_USER` | *(vazio)* | Utilizador |
| `MONGODB_PASSWORD` | *(vazio)* | Password |
| `MONGODB_AUTH_SOURCE` | `admin` | Database de autenticação |
| `MONGODB_MIN_POOL_SIZE` | `2` | Conexões mínimas no pool |
| `MONGODB_MAX_POOL_SIZE` | `10` | Conexões máximas no pool |
| `MONGODB_SERVER_SELECTION_TIMEOUT` | `5000` | Timeout de seleção de servidor (ms) |
| `MONGODB_ALLOWED_DATABASES` | *(vazio = todos)* | Databases expostos (vírgula separados) |
| `MONGODB_ALLOW_WRITES` | `false` | Habilita insert/update/delete |
| `MCP_SERVER_NAME` | `mongo-mcp` | Nome do servidor MCP |
| `MCP_LOG_LEVEL` | `INFO` | Nível de log (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
| `MCP_TRANSPORT` | `stdio` | Transporte: `stdio`, `sse`, `streamable-http` |
| `MCP_HOST` | `0.0.0.0` | Host do servidor HTTP (só para SSE/streamable-http) |
| `MCP_PORT` | `8080` | Porta do servidor HTTP (só para SSE/streamable-http) |

---

## Execução

```bash
# Modo stdio (padrão)
uv run mcp-mongo

# Modo SSE — servidor HTTP na porta 8080
MCP_TRANSPORT=sse uv run mcp-mongo

# Modo desenvolvimento com MCP Inspector
uv run mcp dev src/mcp_mongo/server.py
```

---

## Capacidades MCP

### Tools

Tools são funções que o agente chama activamente para executar operações.

---

#### `find`

Executa um `find()` em uma collection e retorna os documentos como JSON.

| Parâmetro | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
| `database` | `str` | sim | — | Nome do database MongoDB |
| `collection` | `str` | sim | — | Nome da collection |
| `filter` | `dict` | não | `{}` | Filtro MongoDB (ex: `{"status": "active"}`) |
| `projection` | `dict` | não | `{}` | Campos a incluir/excluir (ex: `{"name": 1, "_id": 0}`) |
| `sort` | `list` | não | — | Lista de pares `[campo, direção]` (ex: `[["age", -1]]`) |
| `limit` | `int` | não | `100` | Número máximo de documentos |
| `skip` | `int` | não | `0` | Documentos a saltar (paginação) |

```json
// Exemplo
{
  "database": "shop",
  "collection": "orders",
  "filter": {"status": "pending"},
  "sort": [["created_at", -1]],
  "limit": 20
}
```

---

#### `find_one`

Executa um `find_one()` e retorna o primeiro documento encontrado.

| Parâmetro | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
| `database` | `str` | sim | — | Nome do database |
| `collection` | `str` | sim | — | Nome da collection |
| `filter` | `dict` | não | `{}` | Filtro MongoDB |
| `projection` | `dict` | não | `{}` | Campos a incluir/excluir |

---

#### `aggregate`

Executa um aggregation pipeline e retorna os resultados como JSON.

| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `database` | `str` | sim | Nome do database |
| `collection` | `str` | sim | Nome da collection |
| `pipeline` | `list` | sim | Lista de estágios de agregação |

```json
// Exemplo — vendas por categoria
{
  "database": "shop",
  "collection": "orders",
  "pipeline": [
    { "$match": { "status": "completed" } },
    { "$group": { "_id": "$category", "total": { "$sum": "$amount" } } },
    { "$sort": { "total": -1 } }
  ]
}
```

---

#### `count_documents`

Conta documentos que correspondem ao filtro.

| Parâmetro | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
| `database` | `str` | sim | — | Nome do database |
| `collection` | `str` | sim | — | Nome da collection |
| `filter` | `dict` | não | `{}` | Filtro MongoDB (vazio = conta todos) |

---

#### `insert_documents`

Insere um ou mais documentos. Requer `MONGODB_ALLOW_WRITES=true`.

| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `database` | `str` | sim | Nome do database |
| `collection` | `str` | sim | Nome da collection |
| `documents` | `list` | sim | Lista de documentos a inserir |

Retorna os IDs gerados (`inserted_id` para um, `inserted_ids` para múltiplos).

---

#### `update_documents`

Atualiza documentos correspondentes ao filtro. Requer `MONGODB_ALLOW_WRITES=true`.

| Parâmetro | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
| `database` | `str` | sim | — | Nome do database |
| `collection` | `str` | sim | — | Nome da collection |
| `filter` | `dict` | sim | — | Filtro para selecionar documentos |
| `update` | `dict` | sim | — | Operação de atualização (ex: `{"$set": {...}}`) |
| `upsert` | `bool` | não | `false` | Cria o documento se não existir |

Retorna `matched_count`, `modified_count` e `upserted_id`.

---

#### `delete_documents`

Remove documentos correspondentes ao filtro. Requer `MONGODB_ALLOW_WRITES=true`.

| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `database` | `str` | sim | Nome do database |
| `collection` | `str` | sim | Nome da collection |
| `filter` | `dict` | sim | Filtro para selecionar documentos a remover |

> **Atenção:** `filter: {}` remove **todos** os documentos da collection.

Retorna `deleted_count`.

---

#### `list_databases`

Lista todos os databases não-sistema disponíveis no servidor MongoDB (`admin`, `local` e `config` são sempre excluídos).

---

#### `list_collections`

Lista collections de um database.

| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `database` | `str` | sim | Nome do database |

---

#### `describe_collection`

Infere o esquema de uma collection via amostragem de documentos. Retorna campos, tipos dominantes e frequência de presença.

| Parâmetro | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
| `database` | `str` | sim | — | Nome do database |
| `collection` | `str` | sim | — | Nome da collection |
| `sample_size` | `int` | não | `100` | Número de documentos a amostrar |

Exemplo de saída:

```json
[
  { "field": "_id",            "dominant_type": "string",  "presence_pct": 100.0 },
  { "field": "name",           "dominant_type": "string",  "presence_pct": 100.0 },
  { "field": "age",            "dominant_type": "int",     "presence_pct": 98.0  },
  { "field": "address.city",   "dominant_type": "string",  "presence_pct": 75.0  },
  { "field": "address.zip",    "dominant_type": "string",  "presence_pct": 60.0  },
  { "field": "tags",           "dominant_type": "array",   "presence_pct": 45.0  }
]
```

Campos de documentos embutidos (`address.city`) são expandidos automaticamente.

---

#### `list_indexes`

Lista os índices de uma collection com unicidade, esparsidade e campos cobertos.

| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `database` | `str` | sim | Nome do database |
| `collection` | `str` | sim | Nome da collection |

---

#### `get_collection_stats`

Retorna estatísticas de uma collection via `$collStats` (MongoDB 3.6+).

| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `database` | `str` | sim | Nome do database |
| `collection` | `str` | sim | Nome da collection |

Inclui: `document_count`, `size_kb`, `avg_document_size_bytes`, `storage_size_kb`, `index_count`, `total_index_size_kb`.

---

### Resources

Resources expõem dados como URIs navegáveis — o agente lê-os para obter contexto antes de agir.

| URI | Descrição |
|---|---|
| `mongo://databases` | Lista todos os databases não-sistema |
| `mongo://db/{database}/collections` | Lista collections de um database |
| `mongo://db/{database}/collection/{collection}` | Schema inferido + índices + stats de uma collection |

---

### Prompts

Prompts são templates reutilizáveis que guiam o agente numa tarefa complexa.

| Prompt | Parâmetros | Descrição |
|---|---|---|
| `explore_database` | — | Roteiro para explorar um banco MongoDB desconhecido |
| `analyse_collection` | `database`, `collection` | Análise detalhada de uma collection |
| `write_query` | `question` | Gera um `find()` ou `aggregate()` a partir de linguagem natural |
| `write_aggregation` | `question` | Gera um aggregation pipeline para análise de dados |
| `optimise_query` | `database`, `collection`, `filter_hint` | Analisa e sugere optimizações para uma query |

---

## Segurança

| Mecanismo | Detalhe |
|---|---|
| **Read-only por padrão** | Writes bloqueados por guard em cada método — sem acesso ao banco |
| **Databases permitidos** | `MONGODB_ALLOWED_DATABASES` limita a exposição de dados |
| **Serialização BSON segura** | `ObjectId`, `Decimal128` e outros tipos BSON são convertidos para strings — sem falhas de serialização |
| **Logs sanitizados** | Password nunca aparece em logs (`safe_uri`) |
| **Erros sanitizados** | Exceções retornam mensagem simples ao agente, sem stack trace |
| **Pool limitado** | `max_pool_size=10` por padrão — evita saturar o servidor |
| **Ping no startup** | Falha imediatamente se as credenciais ou o host estiverem incorrectos |

Para habilitar escritas:

```dotenv
MONGODB_ALLOW_WRITES=true
```

---

## Testes

Os testes são de integração e requerem uma instância MongoDB acessível. Usam o database temporário `test_mcp_tmp`, que é criado e destruído automaticamente.

```bash
# Todos os testes (usa .env por padrão)
uv run pytest

# Sobrepor a URI para os testes
TEST_MONGODB_URI=mongodb://localhost:27017/test uv run pytest

# Com coverage
uv run pytest --cov=src/mcp_mongo --cov-report=html

# Verbose
uv run pytest -v
```

---

## Integração com Agentes

### Claude Code / Claude Desktop

Adicionar ao `~/.claude.json` (user-level, disponível em todos os projetos):

```json
{
  "mcpServers": {
    "mongo": {
      "type": "stdio",
      "command": "uv",
      "args": [
        "--directory", "/caminho/para/mcp-mongo",
        "run", "mcp-mongo"
      ],
      "env": {
        "MONGODB_URI": "mongodb://user:pass@host:27017/mydb"
      }
    }
  }
}
```

---

### LangChain / LlamaIndex

Iniciar o servidor em modo SSE:

```bash
MCP_TRANSPORT=sse MCP_PORT=8080 uv run mcp-mongo
```

Conectar a partir do agente:

```python
# LangChain + MCP
from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({
    "mongo": {
        "url": "http://localhost:8080/sse",
        "transport": "sse",
    }
})
tools = await client.get_tools()
```

---

### Agentes HTTP genéricos

Iniciar em modo `streamable-http`:

```bash
MCP_TRANSPORT=streamable-http MCP_PORT=8080 uv run mcp-mongo
```

Endpoint disponível em `http://localhost:8080/mcp`.

---

### Docker

```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install uv && uv sync
EXPOSE 8080
CMD ["uv", "run", "mcp-mongo"]
```

```bash
docker run -p 8080:8080 \
  -e MONGODB_URI=mongodb://user:pass@host:27017/mydb \
  -e MCP_TRANSPORT=sse \
  mcp-mongo
```

---

## Decisões de Arquitetura

### Conexão dinâmica — `MONGODB_URI` vs `MONGODB_*`

`MONGODB_URI` é o padrão de facto em ambientes cloud (MongoDB Atlas, Railway, Render, etc.) e suporta opções avançadas como replica sets e `mongodb+srv`. As vars individuais são mais legíveis para desenvolvimento local. O `model_validator` do Pydantic extrai os campos da URI se fornecida, garantindo que a URI interna está sempre correta independentemente de qual forma foi usada.

### Motor em vez de PyMongo síncrono

Motor é o driver oficial async do MongoDB para Python. Construído sobre PyMongo mas com interface `asyncio` nativa — não é um wrapper de thread pool. Necessário para coexistir com FastMCP que opera em loop de eventos asyncio.

### Schema por amostragem — sem `$jsonSchema`

MongoDB é schemaless por design. A abordagem escolhida (`$sample` + inferência de tipos) devolve um esquema descritivo de documentos reais sem exigir validação de schema configurada no banco. O `presence_pct` indica quais campos são obrigatórios na prática vs opcionais, o que é informação mais útil para o agente do que uma definição formal.

### Serialização BSON centralizada em `_serialize()`

Tipos BSON como `ObjectId`, `Decimal128` e `datetime` não são serializáveis em JSON nativo. A função `_serialize()` no `BaseRepository` converte recursivamente todos os valores antes de retornar ao agente, eliminando erros de serialização em qualquer tool ou resource.

### Transporte configurável

O protocolo MCP suporta vários transportes. `stdio` é o padrão para agentes locais (Claude Code lança o processo e comunica por stdin/stdout). Para agentes remotos ou multi-tenant, `sse` e `streamable-http` expõem o servidor como um serviço HTTP sem qualquer alteração de código — só muda a variável de ambiente.

### `src/` layout

Previne que o Python encontre o módulo via path local sem instalação — o que mascararia erros de packaging e tornaria os testes menos fiáveis.

### Repository Pattern

Isola as operações Motor das tools MCP. As tools expressam intenção (`describe_collection`), os repositórios expressam implementação (`$sample` + inferência). Trocar a estratégia de introspecção não exige tocar nas tools.

### Lifespan para o client Motor

Garante que o client abre (e valida com `ping`) antes do servidor aceitar requests, e fecha sempre ao terminar — mesmo com `Ctrl+C` ou sinal do OS. Não há risco de conexões a vazar ou de o agente receber requests antes do banco estar pronto.