Skip to main content
Glama
marcosvb1

trinks-mcp-server

by marcosvb1

trinks-mcp-server

Deploy to Cloudflare

Servidor MCP privado (atrás de auth) para a API do Trinks (agendamento de salão/clínica), rodando como um Cloudflare Worker. Cobre agendamentos, clientes, profissionais, serviços, produtos, financeiro (transações, NFS-e, lançamentos), fidelidade, comissões e clube de assinaturas — 66 tools, uma para cada operação da API.

Multi-unidade: se você tem mais de um estabelecimento (cada um com conta Trinks própria), configure TRINKS_UNIDADES e toda tool ganha o param opcional unidade. Ver Unidades.

As tools cobrem todas as 66 operações do OpenAPI oficial do Trinks (/swagger/v1/swagger.json), 1:1 — mesmo método, path, query params e corpo, com as regras do spec (limites, enums, campos obrigatórios) nos schemas e descrições.

Reescrito do zero; o Vlarrate/trinks-mcp serviu de referência inicial (mas não segue as práticas de MCP nem tinha gate de auth). Ver Diferenças.

Arquitetura

  • Transporte: Streamable HTTP via createMcpHandler (Cloudflare Agents SDK) — stateless, um McpServer novo por request (exigência do SDK 1.26+). Sem Durable Object: é um proxy REST fino, não guarda estado.

  • Auth (2 modos, decididos por config):

    • Produção → Cloudflare Access (Managed OAuth). Set ACCESS_TEAM_DOMAIN + ACCESS_AUD e o Worker exige um Cf-Access-Jwt-Assertion válido. O client MCP faz o fluxo OAuth (login no browser), restrito à política da tua organização. Ver Deploy com Access.

    • Dev local → header x-mcp-secret comparado contra MCP_SHARED_SECRET.

    • Sem nenhum dos dois configurado, o server falha fechado (nega tudo).

  • Secrets: .dev.vars no local; Worker secrets / Secrets Store em produção.

src/
  index.ts          entry: health check + authorize (Access JWT | secret) + createMcpHandler
  access.ts         verificação do Cf-Access-Jwt-Assertion (jose + JWKS do team)
  env.ts            interface Env
  trinks-client.ts  client REST: timeout 30s, erro actionable por status
  tools.ts          catálogo das 66 tools (data-driven, 1:1 com o OpenAPI) + registro

Related MCP server: Zenoti MCP Server

Unidades

Uma unidade (padrão): dois valores, TRINKS_API_KEY e TRINKS_ESTABELECIMENTO_ID. As tools não expõem o param unidade.

Várias unidades: TRINKS_UNIDADES mapeia cada unidade pro seu estabelecimento e pro nome do binding que guarda a API key dela (Worker secret ou binding do Secrets Store — a key nunca vai no JSON):

"vars": {
  "TRINKS_UNIDADE_PADRAO": "matriz",
  "TRINKS_UNIDADES": {
    "matriz": { "estabelecimentoId": "123456", "apiKey": "MATRIZ_APIKEY" },
    "filial": { "estabelecimentoId": "654321", "apiKey": "FILIAL_APIKEY" }
  }
}
  • Com mais de uma unidade, defina TRINKS_UNIDADE_PADRAO — a unidade usada quando a tool é chamada sem unidade. É obrigatório: a Cloudflare guarda o JSON com as chaves em ordem alfabética, então "a primeira" não é confiável.

  • Nomes de unidade: minúsculas, dígitos e hífen.

  • No .dev.vars, TRINKS_UNIDADES vai como string JSON numa linha (exemplo no .dev.vars.example).

  • Config inválida → o /mcp responde 500 com a mensagem do problema (só pra quem passou na auth).

Rodar local

npm install
cp .dev.vars.example .dev.vars   # preencha os valores
npm run typecheck                # opcional: checagem de tipos
npm run dev                      # wrangler dev → http://localhost:8787

Smoke test do gate (sem secret = 401, com secret = handshake MCP):

# 401 esperado
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:8787/mcp

# handshake (lista tools) — troque SEU_SEGREDO
curl -s -X POST http://localhost:8787/mcp \
  -H "content-type: application/json" \
  -H "accept: application/json, text/event-stream" \
  -H "x-mcp-secret: SEU_SEGREDO" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Conferir as tools contra o OpenAPI do Trinks (o CI roda isso em todo push/PR e toda semana — falha se uma tool sair do contrato ou se o Trinks adicionar uma operação sem tool):

npm run check:spec              # spec ao vivo
npm run check:spec -- spec.json # cópia local

Ou via inspector visual: npm run inspect e aponte pra http://localhost:8787/mcp com o header x-mcp-secret.

Deploy

Botão (uma unidade)

Deploy to Cloudflare

Cria uma cópia do repo na sua conta GitHub, faz o deploy via Workers Builds e pede três secrets: TRINKS_API_KEY, TRINKS_ESTABELECIMENTO_ID e MCP_SHARED_SECRET. Depois, ponha auth na frente (ver Auth em produção) — sem portal/Access, o acesso é só pelo header x-mcp-secret. Para várias unidades, configure TRINKS_UNIDADES na cópia (ver Unidades).

CLI

O wrangler.jsonc versionado é genérico (modo uma unidade, sem account_id). Para um deploy com config própria — conta, várias unidades, Secrets Store — copie-o para wrangler.local.jsonc (gitignored), ajuste, e use -c:

# uma unidade, config genérica
npx wrangler secret put TRINKS_API_KEY
npx wrangler secret put TRINKS_ESTABELECIMENTO_ID
npx wrangler secret put MCP_SHARED_SECRET
npm run deploy

# config própria
npm run deploy -- -c wrangler.local.jsonc

⚠️ npm run deploy sem -c usa o wrangler.jsonc genérico e substitui as vars e bindings do Worker com o mesmo nome na conta logada.

Auth em produção

  • Atrás de um MCP Server Portal (Cloudflare Access): o portal faz o OAuth do usuário e autentica no Worker via header x-mcp-secret — cadastre no portal o mesmo valor de MCP_SHARED_SECRET.

  • Managed OAuth do Access direto no Worker: set ACCESS_TEAM_DOMAIN + ACCESS_AUD e o Worker valida o Cf-Access-Jwt-Assertion (ver src/access.ts). Docs: Managed OAuth.

Configurar no client MCP

Produção (portal ou Access): só a URL — o OAuth resolve o login no primeiro acesso.

{ "mcpServers": { "trinks": { "url": "https://SEU-PORTAL/mcp" } } }

Dev local (secret header):

{ "mcpServers": { "trinks": { "url": "http://localhost:8787/mcp", "headers": { "x-mcp-secret": "SEU_SEGREDO" } } } }

Diferenças do repo de referência

O Vlarrate/trinks-mcp serviu de referência inicial, mas aqui:

  • Auth: ele expunha /mcp público sem gate → aqui Cloudflare Access (Managed OAuth) em prod, secret header em dev (o repo original vira um proxy aberto pra sua conta Trinks se deployado como documentado).

  • SDK: ele usa a API antiga (setRequestHandler) → aqui registerTool (moderna).

  • Annotations: ele não tinha nenhuma → aqui readOnlyHint/destructiveHint/idempotentHint por tool (excluir_cliente e cancelar_agendamento marcados destrutivos).

  • Nomes: sem prefixo → aqui trinks_* (evita colisão com outros MCPs).

  • Erros: catch genérico → aqui mensagem específica por status (401/403/404/429/5xx) + timeout de 30s + guard de tamanho de resposta.

  • Contrato: params e corpos seguem o OpenAPI oficial (as 66 operações), não o mapa do repo de referência.

Pendências / a validar

  • Contrato validado, execução parcial. Método, path, query params e corpo de cada tool foram conferidos contra o swagger.json, mas nem todas as tools foram exercitadas contra a API viva. O client faz passthrough do corpo da resposta + guard de tamanho (não inventa contrato has_more).

Licença

MIT.

Related MCP Connectors

Related MCP Servers