Skip to main content
Glama
ronaldoribeirosm

cloudbeds-mcp-server

README.md
# cloudbeds-mcp-server

![CI](https://github.com/ronaldoribeirosm/cloudbeds-mcp-server/actions/workflows/ci.yml/badge.svg)
![Linguagem](https://img.shields.io/badge/python-3.10+-blue?logo=python)
![Protocolo](https://img.shields.io/badge/protocolo-MCP-6b4fbb)
![License](https://img.shields.io/badge/license-MIT-blue.svg)

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

![31 testes passando e ruff limpo](docs/screenshots/tests.jpg)

## 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:**

![pytest e ruff rodando](docs/screenshots/tests.jpg)

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

![setup rejeitando key inválida contra a API real](docs/screenshots/setup-invalido.jpg)

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`:**

![tools expostas antes e depois do setup](docs/screenshots/tools.jpg)

## 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