# RiLiGar Messages — Servidor MCP (LLM-Ready) > Documentação integral do suporte a **Model Context Protocol** no RiLiGar > Messages. Complementa `/llms.txt`, que cobre a API REST, a verificação de > domínio e os webhooks. > > **Revisão do protocolo:** `2026-07-28` — a revisão stateless: sem > `initialize`, sem `Mcp-Session-Id`, sem stream GET. > **Endpoint MCP:** `https://messages.worker.riligar.click/mcp/` > **Authorization server:** `https://auth.worker.riligar.click` > > ⚠️ **O endereço é por projeto.** O `` não é opcional: um token > identifica uma PESSOA, e uma pessoa administra vários projetos. Sem o id, o > servidor teria que escolher um sozinho — e escolheria errado sem aviso. Aqui > isso é mais grave do que nos outros produtos da casa: escolher errado > significa **mandar e-mail pela caixa de saída errada, com o domínio de outro > cliente no remetente**. > > ⚠️ **Este servidor tem uma ferramenta que produz efeito FORA do sistema.** > `messages_send_email` entrega uma mensagem a uma pessoa real e não tem > desfazer. Leia a seção 3.3 inteira antes de chamá-la. > > O servidor é construído sobre o SDK oficial: o protocolo — JSON-RPC, headers > espelhados, negociação de era, validação de schema — vem do SDK. Identidade, > isolamento entre projetos e o catálogo são nossos. --- ## Sumário 1. O que dá para fazer 2. Conectar — as formas de autenticar 3. Catálogo de ferramentas 4. Detalhes do transporte (e a armadilha do `_meta`) 5. Fluxo OAuth 2.1 completo 6. Erros e códigos 7. Limites e o que NÃO existe 8. Checklist para agentes de IA --- ## 1. O que dá para fazer Este é um MCP de **operação de e-mail transacional**. Um agente investiga entrega, abre a linha do tempo de uma mensagem, acompanha as taxas que decidem suspensão, cadastra e verifica domínio, cria template — e envia. ``` Você: "o cliente diz que não recebeu o e-mail de ontem" → messages_get_project (qual caixa de saída é esta, e há domínio pronto?) → messages_list_emails (status: bounced) → messages_get_email (a linha do tempo: devolvido, caixa inexistente) ``` O agente não adivinha o id do projeto: ele vem do endereço. E não adivinha se dá para enviar: `messages_get_project` avisa explicitamente quando nenhum domínio está verificado, porque nesse caso **nenhum envio vai passar**. **Sendo justo sobre posicionamento:** ter MCP remoto com OAuth não é o diferencial deste produto — o líder do nicho também tem. O que muda está na operação (retenção de 90 dias, supressão por projeto, e aviso/motivo/apelação antes de qualquer corte), não no protocolo. --- ## 2. Conectar — as formas de autenticar ### 2.0 Antes de tudo: o endereço é por projeto ``` https://messages.worker.riligar.click/mcp/ ^^^^^^^^^^^ copie no painel, em Integração → MCP ``` Cada projeto é um **recurso MCP distinto**, com metadata próprio e audiência própria. Isso não é enfeite de URL: - Um token OAuth identifica uma **pessoa**; uma pessoa administra vários projetos. Sem o id no endereço, o servidor escolheria um sozinho. - O `aud` do token é `…/mcp/`. Um token do projeto A **não é aceito** no B, mesmo sendo do mesmo dono — é a checagem anti *confused deputy* (RFC 8707). Sem ela, um token emitido para o MCP do Storage seria aceito aqui, e aquele servidor viraria uma ponte para a caixa de saída de e-mail de alguém. Para operar dois projetos no mesmo cliente, adicione dois conectores. ### 2.1 As credenciais | | **`sk_…` / `rm_…`** | **OAuth 2.1** | | :--- | :--- | :--- | | Credencial | secretKey do projeto, ou API key | JWT do RiLiGar Auth | | Identifica | o PROJETO | a PESSOA + o projeto do endereço | | Como obtém | copia do painel | login no navegador, pelo cliente | | Para quem | config local, automação, CI | connectors, clientes remotos | | Escopos | `messages:read` + `messages:write` | os três | | `messages:admin` | **não** — ver abaixo | sim, para o dono do projeto | **Por que a chave de projeto não recebe `messages:admin`.** Quem a tem já pode enviar pela API REST, então limitá-la nisso seria teatro. Mas liberar um endereço da supressão é decidir voltar a mandar para quem devolveu ou marcou como spam — e isso merece uma credencial que diga *quem* mandou, não apenas *qual projeto*. ### 2.2 Com a chave do projeto **Claude Code:** ```bash claude mcp add --transport http riligar-messages \ https://messages.worker.riligar.click/mcp/ \ --header "Authorization: Bearer sk_sua_chave" ``` **Claude Desktop** (`claude_desktop_config.json`): ```json { "mcpServers": { "riligar-messages": { "url": "https://messages.worker.riligar.click/mcp/", "headers": { "Authorization": "Bearer sk_sua_chave" } } } } ``` **VS Code** (`.vscode/mcp.json`): ```json { "servers": { "riligar-messages": { "type": "http", "url": "https://messages.worker.riligar.click/mcp/", "headers": { "Authorization": "Bearer sk_sua_chave" } } } } ``` ### 2.3 Com OAuth 2.1 Nenhuma chave no arquivo de config. Na primeira chamada o servidor responde `401` com `WWW-Authenticate` apontando para o Protected Resource Metadata; o cliente descobre o authorization server sozinho, abre o navegador, você faz login e aprova os escopos. ```bash claude mcp add --transport http riligar-messages \ https://messages.worker.riligar.click/mcp/ ``` Sem header. Este é o único caminho que concede `messages:admin`. ### 2.4 SDK oficial Atenção a um detalhe: a negociação de versão é **opt-in**. O padrão do SDK é `'legacy'`, que não fala 2026-07-28. ```javascript import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client' const transport = new StreamableHTTPClientTransport( new URL('https://messages.worker.riligar.click/mcp/'), { requestInit: { headers: { Authorization: 'Bearer sk_sua_chave' } } } ) // ⚠️ sem versionNegotiation: { mode: 'auto' } o SDK usa a era legada e falha const client = new Client({ name: 'meu-agente', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } }) await client.connect(transport) const { tools } = await client.listTools() ``` --- ## 3. Catálogo de ferramentas Onze ferramentas em três escopos. Seis leem, quatro escrevem, uma administra. Uma ferramenta fora do escopo **não é registrada**: ela não aparece em `tools/list` nem pode ser chamada. Não é a interface escondendo — ela não existe naquela sessão. Com uma chave de projeto, `tools/list` devolve dez; com um JWT do dono, onze. **Invariante que atravessa o catálogo inteiro: `projectId` NUNCA é argumento de ferramenta.** Ele vem da credencial e do endereço. Se fosse argumento, um prompt injection num documento lido pelo agente bastaria para mandar e-mail em nome de outro projeto. ### 3.1 Leitura (`messages:read`) | Ferramenta | Parâmetros | O que faz | | :--- | :--- | :--- | | `messages_get_project` | — | Onde o agente está: domínios com status, contagens de envio e supressão. Avisa quando **nenhum domínio está verificado** — nesse caso nada sai. **Comece por aqui.** | | `messages_list_emails` | `limit?`, `status?` | Envios do mais recente ao mais antigo. `limit` até 100 (padrão 20). `status`: `queued`, `sent`, `delivered`, `bounced`, `complained`, `failed`, `rejected` | | `messages_get_email` | `emailId` | Um envio com o corpo e a **linha do tempo completa** de eventos. É a resposta para "por que meu cliente não recebeu" | | `messages_get_metrics` | `dias?` | Contagens e taxas de entrega, devolução e reclamação. Janela até **90** dias (padrão 7). Vem com os limiares da AWS ao lado | | `messages_list_domains` | — | Domínios com status e, para os pendentes, os registros de DNS que faltam | | `messages_list_suppressions` | `limit?` | Endereços bloqueados no projeto, com o motivo | ### 3.2 Escrita (`messages:write`) | Ferramenta | Parâmetros | O que faz | | :--- | :--- | :--- | | `messages_send_email` | `from`, `to`, `subject`, `text?`, `html?`, `replyTo?`, **`idempotencyKey`** | Envia. Ver 3.3 — **leia antes** | | `messages_add_domain` | `dominio` | Cadastra e devolve os registros de DNS (3 CNAME, 1 TXT, 1 MX) | | `messages_verify_domain` | `dominio` | Consulta a AWS e atualiza o status. Chame depois de criar o DNS | | `messages_create_template` | `nome`, `assunto`, `html?`, `texto?` | Cria ou atualiza um template com `{{variaveis}}` | ### 3.3 A ferramenta que exige confirmação humana `messages_send_email` é a operação mais perigosa que a RiLiGar já expôs a um agente. Ler um dado errado é constrangedor; **mandar um e-mail errado chega numa pessoa que não pediu nada, e não tem desfazer.** Três coisas a distinguem de tudo o mais no catálogo da casa: 1. **`destructiveHint: true`**, ainda que nada seja apagado. O que esse campo comunica ao cliente MCP é *"peça confirmação"*, e é disso que se trata. Também vem com `openWorldHint: true`: o efeito é fora do sistema. 2. **`idempotencyKey` é OBRIGATÓRIA aqui**, embora seja opcional na REST. Um agente que erra, repete; um humano que erra, percebe. A chave transforma a repetição em no-op — a resposta volta com `repetido: true` e o envio original, e nenhum e-mail novo sai. Derive a chave do FATO (`boas-vindas/usuario-123`), nunca do relógio. 3. **Teto diário próprio do canal do agente: 200 envios em 24h**, contados só pelas chamadas vindas do MCP. Ao estourar, a mensagem é explícita: *o limite é do canal do agente, não do projeto — a API REST continua enviando normalmente*. Um laço num agente não pode consumir a operação do cliente. Duas recusas que o agente precisa saber ler: - **Domínio não verificado** — o `from` precisa ter domínio verificado NESTE projeto. A checagem acontece antes de qualquer chamada à AWS. - **Endereço suprimido** — o destinatário já devolveu permanentemente ou marcou como spam. A recusa é deliberada e protege a reputação do domínio. **Não contorne isso** sugerindo outro endereço ou outra ferramenta. ### 3.4 Administração (`messages:admin`) | Ferramenta | Parâmetros | O que faz | | :--- | :--- | :--- | | `messages_remove_suppression` | `email` | Libera um endereço da supressão do projeto | Marcada como destrutiva, e só alcançável por JWT. O endereço está na lista porque devolveu ou reclamou; reenviar para endereços ruins é exatamente o que derruba a reputação. Quando o motivo era `complaint`, a resposta traz um aviso: aquela pessoa marcou uma mensagem sua como spam, e reenviar sem consentimento explícito tende a gerar nova reclamação. ### 3.5 Nenhuma ferramenta devolve credencial Nem `secretKey`, nem o valor de uma API key já criada, nem o segredo de um webhook. Um segredo que passa por contexto de LLM vaza em log, em transcript e no prompt da chamada seguinte. --- ## 4. Detalhes do transporte | Aspecto | Comportamento | | :--- | :--- | | Método | Só `POST`. `GET` e `DELETE` respondem `405`. | | Sessão | **Não existe.** `Mcp-Session-Id` é ignorado, nunca emitido. | | `initialize` | **Não existe.** Cada POST se basta. | | Resposta | Sempre `application/json`. As ferramentas não usam SSE. | | Origin | Validado (anti DNS-rebinding). Origem não permitida → `403`. Requisição sem header `Origin` — cliente não-browser — é permitida. | Ser stateless não é detalhe de implementação: é o que permite ao servidor rodar como Cloudflare Worker, sem estado entre isolates. ### 4.1 ⚠️ A armadilha da revisão 2026-07-28 **Verificado na prática em 04/09/2026.** É onde toda chamada artesanal escorrega, e o erro que ela produz não aponta para a causa. **O `_meta` vai dentro de `params` — NÃO na raiz do corpo.** Colocá-lo ao lado de `method` e `id`, que é o palpite natural de quem escreve o JSON à mão, devolve `-32602` nomeando um campo que "falta" — quando ele está lá, só que no nível errado. **E os três headers espelham campos do corpo.** O servidor valida um contra o outro e rejeita divergência. Os três erros são distintos, e cada um aponta para um degrau diferente (medidos contra este servidor em 04/09/2026): | O que falta | Código | Mensagem | | :--- | :--- | :--- | | `MCP-Protocol-Version` | `-32022` | `Unsupported protocol version: the request did not name a protocol version` | | `_meta` (com o header presente) | `-32602` | `...missing the required per-request envelope key(s): _meta` | | `_meta` na raiz em vez de em `params` | `-32600` | `the request body is not a valid JSON-RPC message` | | Header | Espelha | | :--- | :--- | | `MCP-Protocol-Version` | `params._meta["io.modelcontextprotocol/protocolVersion"]` | | `Mcp-Method` | `method` | | `Mcp-Name` | `params.name` (em `tools/call`) | Requisição completa e correta: ```http POST /mcp/ HTTP/1.1 Host: messages.worker.riligar.click Content-Type: application/json Accept: application/json, text/event-stream Authorization: Bearer sk_sua_chave MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/call Mcp-Name: messages_list_emails { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "messages_list_emails", "arguments": { "status": "bounced" }, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { "name": "meu-agente", "version": "1.0.0" }, "io.modelcontextprotocol/clientCapabilities": {} } } } ``` Repare onde o `_meta` está: **dentro de `params`**, irmão de `name` e `arguments`. O SDK oficial monta tudo isso sozinho — headers inclusive. Um cliente artesanal, não, e é por isso que a recomendação da seção 8 é usar o SDK. --- ## 5. Fluxo OAuth 2.1 completo ``` Cliente Messages MCP RiLiGar Auth (AS) | | | |-- tools/call sem token ->| | |<-- 401 WWW-Authenticate -| | | (resource_metadata) | | | | | |-- GET /.well-known/oauth-protected-resource/mcp/| |<-- { authorization_servers: [...] } ----------------| | | | |-- GET /.well-known/oauth-authorization-server ----->| |<-- { authorization_endpoint, token_endpoint, ... } -| | | | |-- /oauth/authorize + PKCE | | + resource=…/mcp/ -------------------->| | | [usuário aprova] | |<-- 302 ?code=...&iss=... ---------------------------| | | | |-- POST /oauth/token + code_verifier + resource ---->| |<-- { access_token (aud=recurso), refresh_token } ---| | | | |-- tools/call + Bearer -->| | | [valida aud] | |<-- resultado ------------| | ``` O Protected Resource Metadata que o desafio `401` aponta: ```json { "resource": "https://messages.worker.riligar.click/mcp/", "authorization_servers": ["https://auth.worker.riligar.click"], "scopes_supported": ["messages:read", "messages:write", "messages:admin"], "bearer_methods_supported": ["header"] } ``` O `resource` (RFC 8707) precisa ser exatamente: ``` https://messages.worker.riligar.click/mcp/ ``` Ele vira o `aud` do token, e o servidor recusa qualquer token cujo `aud` não seja esse. --- ## 6. Erros e códigos | Código | HTTP | Significado | | :--- | :--- | :--- | | `-32001` | 401 | Autenticação necessária, credencial inválida, **ou projeto ausente/alheio** | | `-32600` | 403 | Origem não permitida | | `-32600` | 400 | Corpo não é uma mensagem JSON-RPC válida | | `-32602` | 400 | Argumentos inválidos, **ou `_meta` ausente/fora de `params`, ou header espelhado faltando** | | `-32020` | 400 | `HeaderMismatch` — header diverge do corpo | | `-32022` | 400 | Revisão de protocolo não suportada (só falamos 2026-07-28) | Motivos de `401` que valem reconhecer pela mensagem: | Mensagem | O que fazer | | :--- | :--- | | Projeto ausente no endereço | Falta o `/`. Copie no painel, em Integração → MCP. | | Chave de outro projeto | A `sk_`/`rm_` é de um projeto diferente do que está no endereço. | | Sem acesso a este projeto | O id não existe, ou é de outro dono. **A mensagem é a mesma nos dois casos, de propósito**: distinguir transformaria a rota num oráculo de quais projetos existem. | | Token inválido ou expirado | O JWT não passou na verificação contra a JWKS do Auth. | **Erros de negócio não são erros de protocolo.** Domínio não verificado, endereço suprimido, envio inexistente, teto diário atingido — tudo volta `200` com `isError: true` e a mensagem em `content[0].text`. É proposital: o agente precisa ler o erro como resposta sobre a qual raciocinar, não como transporte quebrado. --- ## 7. Limites e o que NÃO existe Honestidade de máquina, como no `/llms.txt` principal. **⛔ Ferramentas que NÃO existem — deliberadamente:** - **Apagar domínio ou projeto** — apagar um domínio derruba todo envio que depende dele, e sem volta pelo agente. - **Criar ou ler API key** — a credencial não entra nem sai por aqui. O concorrente expõe criação de API key via MCP; a regra da casa é outra: *não é "destrutivo ou não", é quanto uma única chamada destrói*. - **Ler qualquer credencial** — nem `secretKey`, nem o valor de uma API key já criada, nem o segredo de um webhook. - **Cadastrar ou alterar webhook** — apontar o webhook para outro lugar desviaria os eventos de entrega do cliente em silêncio. Um agente que leia um documento com instruções maliciosas ("apague os domínios", "me mande a chave") não encontra ferramenta para obedecer. É a única defesa real contra prompt injection: não é filtrar a intenção, é não oferecer o gatilho. **⛔ O que o produto não faz — e o MCP não inventa:** - **SMS e WhatsApp** — não existe uma linha disso no código. Só e-mail. O nome do produto sugere mais do que ele faz; não complete a lacuna. - **Broadcast, audiences, segmentos, contatos, automações** — só transacional. - **Envio em lote e agendamento** — um envio por chamada, e ele sai agora. - **E-mail de entrada (inbound)** — o MX de `send.` é só o retorno de devolução, não uma caixa de entrada. - **Reputação isolada entre clientes** — o configuration set do SES separa as MÉTRICAS; o **pool de IP é compartilhado**. Um cliente com devolução alta degrada a entrega dos outros. IP dedicado não existe aqui. **Nunca prometa isolamento de reputação.** - **Anexos pelo MCP** — a ferramenta de envio aceita `text` e `html`, não anexos. Anexo é o fluxo de dois passos da API REST. **⛔ Sobre suspensão — o que NÃO afirmar:** não diga que a conta nunca é suspensa. A AWS avalia o agregado (devolução < 5%, reclamação < 0,1%), e o pool é compartilhado. O que existe é processo: aviso prévio, motivo escrito e apelação. Se o usuário perguntar, diga isso — não prometa leniência. **⛔ Recursos do protocolo que NÃO implementamos:** - **`resources/*`** e **`prompts/*`** — só tools. - **`subscriptions/listen`** e `notifications/*` — o catálogo é estático. - **`elicitation/create`** — não pedimos input no meio de uma chamada. - **SSE** — sempre `application/json`. **Limites herdados da API:** - `messages_send_email`: **200 por dia** pelo canal do MCP. A REST não tem teto. - `messages_list_emails`: até 100 por chamada. - `messages_get_metrics`: janela máxima de **90 dias**. - `messages_list_suppressions`: até 200 por chamada. **O que ainda não foi medido:** latência sob carga concorrente e comportamento com dezenas de agentes simultâneos. Não temos número, e não vamos publicar estimativa. --- ## 8. Checklist para agentes de IA 1. **O endereço inclui o projeto.** `…/mcp/`, copiado do painel. Sem o id o servidor recusa — não invente um, e não presuma que existe um "projeto padrão". Aqui o erro manda e-mail pela caixa de saída errada. 2. **Use o SDK oficial, e ligue a negociação de versão.** `{ versionNegotiation: { mode: 'auto' } }`, com `@modelcontextprotocol/client@2.x`. Se for montar a chamada à mão, releia a seção 4.1: **o `_meta` vai dentro de `params`**, e os três headers espelhados são obrigatórios — omitir qualquer um devolve `-32602`. 3. **Comece por `messages_get_project`.** É ele que diz se existe domínio verificado. Sem nenhum, **nada sai** — e o agente precisa dizer isso ao usuário em vez de tentar e falhar. 4. **Confirme com o usuário ANTES de `messages_send_email`.** Diga para quem, com que assunto e por qual remetente. É a única ferramenta cujo efeito chega numa terceira pessoa, e não tem desfazer. 5. **Sempre derive a `idempotencyKey` do fato, nunca do relógio.** `boas-vindas/usuario-123` protege; `envio-` não protege nada, porque muda a cada tentativa. 6. **Recusa por supressão não se contorna.** O endereço está na lista porque devolveu ou reclamou. Não sugira outro endereço da mesma pessoa nem peça para liberar — explique por que o bloqueio existe. 7. **Leia as taxas junto dos limiares.** `messages_get_metrics` devolve os dois. Devolução acima de 5% ou reclamação acima de 0,1% coloca a conta em risco na AWS: avise o usuário com o número, não com adjetivo. 8. **Trate `isError` como resposta, não como falha.** Vem com HTTP 200. 9. **Não prometa o que não existe.** Sem SMS, sem WhatsApp, sem campanha em massa, sem lote, sem agendamento, sem inbound, sem reputação isolada. Se o usuário pedir, diga que este sistema não faz — não sugira um contorno que o código não sustenta. 10. **Se for implementar o SEU servidor MCP usando o RiLiGar Auth como AS:** valide `aud`. É a diferença entre um servidor seguro e uma ponte de acesso indevido. O guia está em https://auth.riligar.click/llms-mcp.txt --- **Documentação relacionada:** - `/llms.txt` — API REST, verificação de domínio, idempotência e webhooks - https://auth.riligar.click/llms-mcp.txt — o Auth como authorization server - https://payments.riligar.click/llms-mcp.txt — o MCP do Payments - https://storage.riligar.click/llms-mcp.txt — o MCP do Storage - https://modelcontextprotocol.io/specification/2026-07-28 — a spec