Snapshot: gti-scheduling-mcp (Z-PRO + Klingo adapters) e workflow n8n de agendamento funcionando de ponta a ponta

- gti-scheduling-mcp: MCP server com ZProAdapter e KlingoAdapter (agendar() completo pros dois)
- Workflow n8n 'Agendamento - GChat - Z-PRO': webhook -> AI Agent -> MCP -> Z-PRO, testado com sucesso real
- Documentacao: specs, plano, runbook de problemas reais e export do workflow

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-10 17:23:10 -03:00
co-authored by Claude Sonnet 5
commit 257afe98c0
48 changed files with 12465 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
# gti-scheduling-mcp
## Objetivo do projeto
`gti-scheduling-mcp` é um **MCP Server** responsável **exclusivamente** por
realizar agendamentos em sistemas de clínicas.
O MCP nunca conhece detalhes de nenhuma API específica. Toda a integração
com um sistema de clínica (Z-Pro, Klingo, SysLife, ClinicAll, etc.) é
responsabilidade exclusiva do respectivo **Adapter**, que implementa uma
interface comum e genérica.
Neste MVP existem apenas duas ferramentas (tools) expostas via MCP:
- `consultar_horarios` — consulta horários disponíveis para agendamento.
- `agendar` — realiza um agendamento em um horário disponível.
Nenhuma outra ferramenta, regra de negócio, integração HTTP, banco de
dados ou IA é implementada nesta etapa. Este repositório representa
apenas o esqueleto inicial do projeto.
## Estrutura das pastas
```
src/
adapters/
scheduling-adapter.interface.ts # Contrato comum a todos os Adapters
adapter-factory.ts # Fábrica que resolve o Adapter a usar
zpro/
zpro.adapter.ts # Adapter do sistema Z-Pro
zpro.types.ts # Tipos internos (corpo da requisição verificado)
zpro.mapper.ts # Mapeamento genérico -> corpo da requisição Z-PRO
zpro-http-client.ts # Cliente HTTP (POST appointment/create)
zpro.errors.ts # Erros tipados e classificação de status HTTP
klingo/
klingo.adapter.ts # Adapter do sistema Klingo
klingo.types.ts # Tipos internos (schemas verificados no Swagger)
klingo.mapper.ts # Mapeamento genérico <-> requisições/respostas Klingo
klingo-http-client.ts # Cliente HTTP (identificar, horarios, reservar)
klingo.errors.ts # Erros tipados e classificação de status HTTP
tools/
consultar-horarios.tool.ts # Definição da tool "consultar_horarios"
agendar.tool.ts # Definição da tool "agendar"
mcp-response.ts # Helpers de resposta MCP (sucesso/erro)
server/
mcp.server.ts # Criação/registro do servidor MCP
types/
scheduling.types.ts # Tipos genéricos de domínio
config/
env.config.ts # Leitura/validação de variáveis de ambiente
config.types.ts # Tipos de configuração (Z-Pro e genérico)
diagnostics/
sanitize.ts # Sanitização recursiva de respostas
analyze.ts # Identificação da estrutura de respostas
client.ts # Cliente HTTP mínimo (GET + timeout)
response.ts # Tratamento de resposta JSON/não JSON
index.ts # Ponto de entrada da aplicação
scripts/
diagnose-zpro-list.ts # CLI temporário de descoberta do contrato da API
test/
config.test.ts # Testes da validação de configuração
adapter-factory.test.ts # Testes da AdapterFactory
tools.test.ts # Testes do tratamento de erro das ferramentas
zpro-adapter.test.ts # Testes do ZProAdapter real (mocks de fetch)
klingo-adapter.test.ts # Testes do KlingoAdapter real (mocks de fetch)
sanitize.test.ts # Testes da sanitização recursiva
analyze.test.ts # Testes da identificação de estrutura
diagnose-client.test.ts # Testes do client de diagnóstico e respostas
docs/
zpro-api-mapping.md # Contrato verificado da API do Z-PRO
klingo-openapi.json # Swagger/OpenAPI público da API do Klingo
```
## Fluxo do sistema
1. Um cliente MCP invoca uma das ferramentas registradas
(`consultar_horarios` ou `agendar`).
2. A ferramenta (`tools/`) recebe a chamada, mas **não** contém regra de
negócio — sua responsabilidade é apenas definir o contrato de entrada
e delegar a execução.
3. A execução é delegada ao Adapter apropriado, obtido através da
`adapter-factory.ts`, que recebe a configuração validada e resolve o
Adapter correto para o provider configurado.
4. O Adapter (ex.: `ZProAdapter`) é responsável por toda a comunicação
com o sistema real da clínica, respeitando a interface
`SchedulingAdapter`.
5. O resultado retorna ao cliente MCP no formato genérico definido em
`types/scheduling.types.ts`.
As ferramentas MCP capturam qualquer erro lançado pelo Adapter e devolvem
uma resposta estruturada (`success: false` + código/mensagem), sem
encerrar o servidor.
## Status do ZProAdapter
- **`consultarHorarios()`**: decisão de produto — o Z-PRO não expõe
nenhum endpoint de disponibilidade de horários (ver
`docs/zpro-api-mapping.md`). O método sempre lança um erro claro
(`ZProApiError` com código `NOT_SUPPORTED`); não há solução
alternativa implementada (não se tenta inferir disponibilidade a
partir da lista de agendamentos existentes).
- **`agendar()`**: a chamada HTTP real (`POST appointment/create`) é
implementada de ponta a ponta — autenticação, URL, corpo da requisição
e classificação de erros HTTP (401/403/404/400/422/500, timeout, erro
de rede).
O **mapeamento da resposta de sucesso** foi verificado por um teste
manual único e controlado em 09/08/2026, contra uma instância real do
Z-PRO (ver `docs/zpro-api-mapping.md`, seção "Atualização — resposta
real de sucesso capturada"). Quando o corpo bate com o envelope
observado (`{success: true, data: {id, startAt, endAt, status}}`), o
`Appointment` é construído com o `id` **real** retornado pela API — não
é mais um placeholder.
Mesmo assim, **qualquer HTTP 2xx continua sendo tratado como sucesso
operacional** — o adapter **não lança erro** quando o corpo foge desse
envelope (formato diferente, texto simples, vazio, ou JSON malformado).
Só existe um exemplo real capturado até agora (só cobre o caminho de
sucesso com `status: "pending"`); reportar como falha uma operação que
o provider aceitou (e que pode já ter sido persistida) induziria um
consumidor (agente/n8n) a repeti-la, arriscando um agendamento
duplicado. Nesse caso, `agendar()` retorna um `Appointment` construído
apenas com os dados já conhecidos do próprio input, com `id` ausente e
`responseMappingPending: true`. Consumidores devem tratar
`responseMappingPending: true` como "aceito, detalhes incompletos" —
**nunca** como motivo para reenviar a operação. Apenas respostas HTTP
fora do intervalo 2xx continuam classificadas como erro
(401/403/404/400/422/500, timeout, erro de rede).
Opcionalmente, `new ZProAdapter(config, { onResponseMetadata })` aceita
um callback de diagnóstico chamado após cada resposta do Z-PRO, com
metadados seguros (status, content-type, presença de corpo, formato
aparente) — nunca o corpo, o token ou dados do paciente. Sem esse
callback, nada é registrado (por padrão, para nunca escrever em stdout
e corromper o transporte stdio do MCP).
## Status do KlingoAdapter
Baseado inteiramente em `docs/klingo-openapi.json` (Swagger público da API
do Klingo — bem mais completo que o do Z-PRO: 41 endpoints documentados,
incluindo consulta de disponibilidade real).
- **`consultarHorarios()`**: chama `GET /agenda/horarios` de verdade,
autenticado por `X-APP-TOKEN`. A API confirma a estrutura externa da
resposta (`procedimento`, `horarios[]` com `id`/`data`/`profissional`/
`unidade`), mas **o formato interno de cada horário específico não está
documentado** no Swagger — é descrito apenas como um objeto genérico,
sem `properties`/`items`. Sem essa informação não é possível montar
`AvailableSlot.startDateTime`/`endDateTime` reais sem inventar nomes de
campo. Por isso este método **sempre lança** `KlingoApiError` com código
`SLOT_MAPPING_UNAVAILABLE` quando a API responde com sucesso — é uma
lacuna de contrato conhecida, não uma falha técnica, e não deve ser
tratada como motivo para retry. Assim que uma resposta real de
`GET /agenda/horarios` for capturada, `klingo.adapter.ts` deve ganhar o
mapeamento completo.
- **`agendar()`**: implementado de ponta a ponta.
1. Identifica o paciente via `POST /paciente/identificar`
(`X-APP-TOKEN`), usando `patient.phone` + `patient.document` (como
CPF) — ou `apenas_telefone: true` quando não há `document`. Não
cria nenhum dado no Klingo, então erros aqui (inclusive resposta 2xx
sem `access_token` válido) são reportados normalmente, sem risco de
duplicidade.
2. Reserva o horário via `POST /agenda/reservar`, autenticado pelo
Bearer de sessão obtido no passo anterior, enviando `procedimento`
(de `input.specialtyId`) e `id` (de `input.slotId`) — ambos
obrigatórios pela API, validados antes de qualquer chamada de rede.
Envia também `confirmado` preenchido, para que a marcação já seja
feita como confirmada em vez de uma reserva temporária de 10 minutos
— comportamento documentado literalmente no Swagger
("Se for passado um texto, a marcação já irá ser feita como
confirmada").
3. O schema de sucesso de `reservar` (`reserva`) **é documentado**
(`id`, `data`, `hora`, `profissional`) — diferente do Z-PRO, a
resposta é mapeada de verdade para o `Appointment` genérico, com
`status: "confirmed"`. Mesmo assim, **qualquer HTTP 2xx continua
sendo tratado como sucesso operacional** mesmo que o corpo fuja do
schema documentado (JSON sem `id` numérico, corpo vazio, texto,
JSON malformado) — mesma proteção contra duplicidade do ZProAdapter:
nesse caso, retorna um `Appointment` com `responseMappingPending: true`
em vez de lançar erro.
Variáveis de ambiente: apenas `KLINGO_BASE_URL` e `KLINGO_APP_TOKEN`
(ver seção Configuração). O token de sessão do paciente não é uma
variável de ambiente — é obtido em tempo de execução, por paciente, e
descartado a cada chamada (sem cache, por simplicidade neste MVP).
## Configuração
A configuração é lida exclusivamente por variáveis de ambiente (módulo
`src/config/env.config.ts`), conforme o arquivo de exemplo `.env.example`.
Cada provider só exige suas próprias variáveis — as do provider não
selecionado em `SCHEDULING_PROVIDER` não são exigidas.
| Variável | Obrigatória | Descrição |
| --------------------- | ------------------------------------ | ----------------------------------------------------------------- |
| `SCHEDULING_PROVIDER` | Não (padrão `zpro`) | Provider de agendamento (`zpro` \| `klingo`) |
| `ZPRO_BASE_URL` | Sim, se `SCHEDULING_PROVIDER=zpro` | URL base da API do Z-PRO |
| `ZPRO_API_TOKEN` | Sim, se `SCHEDULING_PROVIDER=zpro` | Token de autenticação da API do Z-PRO |
| `ZPRO_API_ID` | Sim, se `SCHEDULING_PROVIDER=zpro` | Identificador da API (ApiID) usado nas rotas externas |
| `ZPRO_INSTANCE_ID` | Não | Identificador da instância no Z-PRO (mantido por compatibilidade) |
| `KLINGO_BASE_URL` | Sim, se `SCHEDULING_PROVIDER=klingo` | URL base da API do Klingo |
| `KLINGO_APP_TOKEN` | Sim, se `SCHEDULING_PROVIDER=klingo` | Token de aplicação do Klingo, enviado no header `X-APP-TOKEN` |
O token de **sessão do paciente** do Klingo (`Authorization: Bearer`,
obtido via `/externo/login` ou `/paciente/identificar`) não é uma
variável de ambiente — não é um segredo fixo de configuração, e sim algo
obtido em tempo de execução por paciente. Ver `docs/klingo-openapi.json`.
Com configuração inválida, a aplicação encerra a inicialização com uma
mensagem clara, sem expor nenhum token em mensagens de erro ou logs.
Nunca crie um arquivo `.env` real com credenciais; o `.env` está no
`.gitignore`.
## Script de diagnóstico da API Z-PRO
O script `scripts/diagnose-zpro-list.ts` consulta apenas o endpoint de
**listagem** de agendamentos do Z-PRO (GET) para descobrir o contrato real
da resposta, pois a coleção pública do Postman não possui exemplos de
resposta. Veja `docs/zpro-api-mapping.md`.
```bash
npm run diagnose:zpro:list
```
O script:
- faz **uma única** requisição `GET` em
`{ZPRO_BASE_URL}/v2/api/external/{ZPRO_API_ID}/appointment/list`
com `page=1` e `limit=5` (parâmetros conservadores);
- envia `Authorization: Bearer <token>` e `Accept: application/json`;
- usa timeout de 15 segundos e **não** executa retry;
- **não** chama `appointment/create` nem modifica dados;
- imprime informações da requisição (URL sem token, status, content-type,
duração), a resposta sanitizada e um resumo estrutural;
- nunca imprime token, stack trace ou dados pessoais completos dos
pacientes (valores sensíveis são mascarados com `[REDACTED]`).
Em caso de falha (variável ausente, autenticação, ApiID inválido, timeout,
rede, resposta não JSON ou status HTTP fora de 2xx), o processo encerra
com código de saída 1.
## Como adicionar um novo Adapter
1. Criar uma nova pasta em `src/adapters/<nome-do-sistema>/`
(ex.: `src/adapters/klingo/`).
2. Criar a classe do Adapter (ex.: `klingo.adapter.ts`) implementando a
interface `SchedulingAdapter`, definida em
`scheduling-adapter.interface.ts`.
3. Implementar os métodos `consultarHorarios()` e `agendar()`, traduzindo
os dados específicos do sistema para os tipos genéricos definidos em
`types/scheduling.types.ts`.
4. Atualizar `adapter-factory.ts` para que o novo Adapter possa ser
selecionado quando necessário.
5. **Nunca** expor detalhes do sistema específico (nomes de campos,
endpoints, formatos próprios, etc.) fora do próprio Adapter.
## Scripts disponíveis
```bash
npm run dev # Executa o servidor em modo desenvolvimento (tsx)
npm run build # Compila o projeto TypeScript para dist/
npm run start # Executa o projeto já compilado
npm run test # Executa os testes unitários (node:test via tsx)
npm run diagnose:zpro:list # Consulta o endpoint de listagem do Z-PRO (leitura)
npm run lint # Executa o ESLint
npm run format # Formata o código com Prettier
```