cloudbeds-mcp-server
README.md
# cloudbeds-mcp-server




Servidor MCP que expõe ocupação, check-ins/check-outs e reservas do Cloudbeds como *tools* de linguagem natural, **sem nenhum caminho de escrita na v1 — por decisão de design, não por limitação técnica**.
## A coisa em ação
Sem UI — é um servidor MCP (stdio). A imagem abaixo é a suíte de testes rodando de verdade; o restante das capturas reais (setup rejeitando uma key inválida contra a API ao vivo do Cloudbeds, tools nos dois estados do servidor) está na seção [Prints](#prints--o-projeto-rodando).

## Links
```
Repositório: https://github.com/ronaldoribeirosm/cloudbeds-mcp-server
Demo: sem demo pública — cada instalação precisa da API key e do
Property ID do Cloudbeds de um hotel real; não dá pra expor
isso num ambiente compartilhado sem vazar dado de hóspede
Docs/Swagger: não se aplica — não é uma API HTTP. A superfície de tools
(nomes, parâmetros, descrições) está documentada na tabela
abaixo e é a mesma coisa que o cliente MCP lista via
`list_tools()`
Vídeo: [ opcional — não gravado ]
```
## O problema
Consultar ocupação, check-ins do dia ou o status de uma reserva no Cloudbeds hoje significa abrir o painel, navegar até a tela certa e ler manualmente — ou interromper quem está na recepção pra perguntar. Isso já tinha sido resolvido uma vez no [`automacao-recepcao`](../automacao-recepcao-main), só que atrás de uma UI Streamlit: resolve pra quem abre aquela tela, não pra quem só quer perguntar "quantos check-ins tenho hoje?" de dentro do assistente de IA que já está usando.
A solução ingênua seria integrar a API do Cloudbeds direto em cada assistente de IA que alguém usa — um conector pro Cursor, outro pra qualquer coisa que apareça depois. Isso não escala: N ferramentas de IA × M integrações de dado vira N×M código de cola. MCP inverte essa conta — escreve-se **um** servidor, e qualquer cliente compatível com o protocolo consegue chamá-lo.
```mermaid
sequenceDiagram
participant Agente as Agente de IA
participant MCP as cloudbeds-mcp-server
participant CB as API Cloudbeds
Agente->>MCP: "quantos check-ins tenho hoje?"
MCP->>CB: GET getReservations (checkInFrom=checkInTo=hoje)
CB-->>MCP: lista de reservas
MCP-->>Agente: texto formatado, pronto pra responder
```
## A solução (visão geral)
- **Camada de tools** (`src/cloudbeds_mcp/tools/`) separa lógica pura (`compute_*`, `format_*`, sem I/O) da tool MCP em si (`register_*_tools`) — a mesma separação que o `automacao-recepcao` já fazia entre regra de negócio e interface Streamlit, só que aqui o "front-end" é o protocolo MCP.
- **Camada de auth** (`src/cloudbeds_mcp/auth.py`) valida a API key com uma chamada real ao Cloudbeds antes de gravar qualquer coisa, criptografa em disco, e nunca devolve a key pro modelo de IA depois de configurada.
- **Cliente HTTP** (`src/cloudbeds_mcp/cloudbeds_client.py`) reaproveita a lógica de retry/paginação e as descobertas de comportamento real da API já validadas no projeto irmão — sem herdar nenhum dos métodos de escrita.
- Detalhes completos na seção **Decisões técnicas** abaixo.
## Prints — o projeto rodando
Sem uma conta Cloudbeds real disponível, três coisas foram validadas de verdade, capturadas em screenshot (não é texto inventado — é a saída real dos comandos):
**1. Suíte de testes e lint:**

**2. `--setup` com uma API key inválida — bateu na API real do Cloudbeds e voltou um 401 de verdade:**

Isso confirma que o endpoint de validação (`getHotels`) existe e se comporta como assumido — o que faltava validar era só o caminho de sucesso (key válida), que segue sem teste ao vivo.
**3. As tools nos dois estados do servidor (antes e depois do setup), capturado chamando `list_tools()` de verdade, mais a saída real de `format_occupancy_snapshot()`/`format_movements()` contra os dados de `tests/test_occupancy.py`:**

## Stack
| Camada | Tecnologia |
|---|---|
| Protocolo | MCP (`mcp` SDK oficial, `MCPServer`, transporte stdio) |
| HTTP | `requests`, com retry/backoff e paginação própria |
| Criptografia da credencial | `cryptography` (Fernet), chave e payload fora do repositório |
| Testes | `pytest` + `responses` (mock de HTTP) |
| Lint | `ruff` |
| CI | GitHub Actions |
## Como rodar localmente
```bash
git clone https://github.com/ronaldoribeirosm/cloudbeds-mcp-server
cd cloudbeds-mcp-server
pip install -e ".[dev]"
cloudbeds-mcp-server --setup
```
O `--setup` pede a API key e o Property ID do Cloudbeds (**Configurações > API** no painel), valida com uma chamada real e salva criptografado em `~/.cloudbeds-mcp/credentials.enc`. Depois disso, aponte um cliente MCP pro binário `cloudbeds-mcp-server` (exemplo de config em [`mcp_client_config.example.json`](mcp_client_config.example.json)).
Rodar os testes:
```bash
pytest -v
ruff check .
```
## Decisões técnicas
### Por que nenhuma tool de escrita existe na v1
O dilema: dava pra implementar `putGuest`/`postReservationNote` reaproveitando o cliente do `automacao-recepcao` — a API já suporta. A escolha foi não implementar nenhum método de escrita no cliente desta v1 — não é uma flag de segurança que dá pra contornar, é a própria classe `CloudbedsClient` deste projeto que não tem o método. Um agente de IA com acesso só-leitura pode, na pior das hipóteses, vazar um dado que já tinha permissão de ver; nunca alterar uma reserva ou um preço sozinho sem confirmação humana. Escrita fica pra v2, atrás de um design de confirmação explícita que ainda não existe.
### Por que a API key nunca "aparece" de volta
O dilema: o jeito mais simples de guardar uma credencial de API é variável de ambiente ou arquivo de texto plano — mas isso é publicável, e qualquer pessoa que rode este servidor está potencialmente compartilhando a key com o histórico de conversa de um agente de IA. A escolha: `configure_cloudbeds_api_key` recebe a key só pra validar (uma chamada real de `getHotels`) e gravar; a função devolve só o nome do hotel conectado, nunca a key — nem em sucesso, nem no texto de erro de falha. O arquivo fica criptografado com Fernet fora da pasta do projeto (`~/.cloudbeds-mcp/`), com permissão restrita ao dono. Isso está coberto por teste: `test_credentials_file_is_encrypted_on_disk` falha se o valor da key aparecer em texto puro no arquivo salvo em disco.
### Por que a busca por nome varre reservas em vez de filtrar no servidor
Descoberta herdada do `automacao-recepcao` (confirmada contra a API real do Cloudbeds em 2026-07-22, não retestada aqui por falta de credencial): `getReservations` ignora filtro por nome, e `getGuestList` só filtra de verdade no servidor por e-mail ou telefone exato — nunca por nome parcial. A alternativa seria buscar todas as páginas de reservas da propriedade inteira, o que não escala pra contas com anos de histórico. A solução adotada limita a varredura a uma janela de 12 meses passados + 3 meses futuros e filtra por substring em memória — resolve o caso de uso real ("achar o hóspede que acabou de ligar") sem paginar a conta inteira, ao custo de não achar hóspedes fora dessa janela (documentado como limitação conhecida, não escondido).
### Por que `get_occupancy_today` detalha reserva por reserva
`getReservations` (lista) não traz quarto nem datas por quarto — só `getReservation` (individual) traz o array `assigned`, que é o que permite saber se um quarto específico está ocupado *hoje*. Isso também foi uma descoberta real do projeto irmão, não uma suposição de documentação. Por isso a tool primeiro filtra candidatos "baratos" pela lista (reservas não canceladas cujo check-out é depois de hoje) e só depois detalha cada uma — evita bater `getReservation` pra cada reserva histórica da propriedade.
## Testes — o que é coberto e por quê
| Arquivo | Cobre | Por quê |
|---|---|---|
| `test_auth.py` | Setup, validação, criptografia, roundtrip e limpeza de credenciais | É a parte mais sensível do projeto — precisa provar que a key nunca fica em texto puro em disco e que falha de validação não grava nada |
| `test_cloudbeds_client.py` | Paginação, erro HTTP, erro de nível de API, achatamento de `getRooms`, extração de hóspede principal | Cobre os comportamentos reais da API (paginação por `total`, não por `count`) com HTTP mockado via `responses`, sem depender de rede |
| `test_occupancy.py` | Cálculo de ocupação (incluindo o limite check-in ≤ dia < check-out), check-ins/check-outs do dia, formatação de texto | Lógica pura de negócio — testável sem tocar o Cloudbeds |
| `test_guests.py` | Busca por substring case-insensitive, ordenação por mais recente, truncamento de lista longa | Garante que a limitação conhecida (busca por nome em memória) pelo menos se comporta de forma previsível |
| `test_reservations.py` | Formatação de status de reserva com e sem quarto atribuído | Cobre o caminho feliz e o caso "reserva sem quarto ainda" |
**Resultado real:** 31/31 testes passando. `ruff check .` limpo.
## O que eu faria diferente
- **Só o caminho de erro foi testado contra a API real** — não tenho uma key válida disponível agora. Rodar `--setup` com uma key inválida confirmou que `getHotels` é um endpoint real e rejeita corretamente (401), mas o formato exato da resposta de sucesso (o `propertyName` que `validate_and_save` espera encontrar em `hotels[0]`) segue assumido a partir do guia original, não confirmado ao vivo. Primeira coisa a fazer antes de publicar: rodar `--setup` contra um hotel de teste de verdade e corrigir o que a API real devolver diferente do assumido.
- **`get_guest_by_name` escala mal em contas com muito histórico** — varrer 15 meses de reservas por substring funciona pra um hotel pequeno/médio, mas numa conta com dezenas de milhares de reservas isso fica lento. Combinar com `getGuestList` quando o usuário já tiver e-mail ou telefone (mais rápido, filtro real no servidor) resolveria a maioria dos casos.
- **Sem cache** — cada chamada de tool bate a API do zero, mesmo pra dados que não mudam a cada segundo (lista de quartos, por exemplo). Um cache curto (TTL de alguns minutos) em `get_rooms` reduziria bastante chamada repetida.
- **v2 do guia original não entrou nesta v1** — `get_occupancy_forecast`, `get_guest_stay_history`, `search_available_rooms` e o resource `daily_report` ficaram de fora por escopo, não por dificuldade técnica; a base de cliente/auth já suporta.
## Licença
MIT.
TDQS
A4.3/5.0
Scored across 1 tool
Disambiguation5/5
Only one tool exists, so there is no possibility of ambiguity between tools. The tool's purpose is clearly distinct from any potential future tools.
Naming Consistency5/5
The single tool name follows a clear verb_noun pattern (configure_cloudbeds_api_key) and is descriptive. With only one tool, consistency is trivially maintained.
Tool Count2/5
The server has 1 tool, which is too few for its apparent scope of managing Cloudbeds data. The description explicitly mentions future tools for occupancy, guests, and reservations, but those are not present.
Completeness1/5
The tool surface is severely incomplete. Only API key configuration is provided, with no operational tools for reservations, guests, or occupancy, making the server non-functional for its intended domain.
Maintenance
ActivitySlowing
ResponsivenessNo issues