- 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>
277 lines
14 KiB
Markdown
277 lines
14 KiB
Markdown
# 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
|
|
```
|