Um servidor MCP escrito à mão num Worker
Vitor Ayres (abre em nova aba)
Um app interno de estoque de um distribuidor ganhou um servidor MCP, o Model Context Protocol, para que um assistente de IA consulte o estoque e o catálogo e ponha itens num orçamento que já existe no ERP. É a parte técnica de falar com os seus dados, num caso pequeno: uma chave de acesso compartilhada, sete ferramentas, um Worker no plano grátis da Cloudflare.
Este texto registra como o servidor foi escrito sem biblioteca, o que foi medido com curl e com testes, e o que ainda não foi testado com um cliente de verdade. O código é de 25 de setembro a 5 de outubro de 2026.
Resumo
- Sem SDK: 216 linhas de transporte e 493 de ferramentas, que rodam iguais no Worker e em Node.
- As duas gerações do protocolo no mesmo
POST, com a geração lida de cada mensagem. - Sete ferramentas: quatro de leitura, uma prévia, uma gravação e um histórico de envios.
- A gravação em dois passos, presa por um código derivado da prévia.
- Um resultado incerto não convida a repetir, porque repetir uma escrita é o que o fluxo recusa.
- OAuth só para o ChatGPT, com uma biblioteca da Cloudflare de 367 KiB empacotados.
- Testado com
curle 14 testes automatizados. ChatGPT e Claude.ai de verdade, não.
Por que sem biblioteca
O MCP por HTTP é JSON-RPC 2.0 num POST. Para servir ferramentas bastam seis métodos: initialize, a notificação initialized, ping, server/discover, tools/list e tools/call. Sem sessão e sem SSE, o servidor é uma função que recebe uma mensagem e devolve outra.
A SDK oficial traz o transporte para Node e Express e a dependência do Zod. O pacote agents da Cloudflare prende o código ao Workers. Nenhum dos dois cabia num app cujo roteador precisa rodar igual no Worker e em Node, com SQLite, porque a saída para uma VPS está decidida de antemão. Então o transporte foi escrito à mão: 216 linhas, mais 493 das ferramentas. Se um cliente real não conversar, a troca pela SDK mantém as ferramentas.
Duas gerações do protocolo no mesmo endpoint
Em setembro de 2026 não dava para saber qual geração do protocolo os clientes mandam. A de 2025 começa por initialize e segue com tools/*, com lotes JSON-RPC só na versão 2025-03-26. A de 2026-07-28 não tem initialize: tem server/discover, um _meta com versão e capacidades em toda requisição, e os cabeçalhos Mcp-Method e Mcp-Name, que precisam bater com o corpo.
O servidor aceita as quatro versões, 2026-07-28, 2025-11-25, 2025-06-18 e 2025-03-26, e lê a geração da própria mensagem. initialize é sempre legada. Um _meta com versão, ou o cabeçalho MCP-Protocol-Version com 2026-07-28, é moderna. Na moderna, cabeçalho e corpo diferentes dão o erro -32020, versão desconhecida dá -32022, e método desconhecido dá 404. Na legada, os erros de aplicação vão em 200, porque o cliente antigo trata outro status como falha de transporte.
Dois desvios de propósito. O initialize responde 2025-06-18, a versão que mais clientes conhecem, em vez da última legada. E um lote aceita no máximo 10 mensagens e uma única chamada de ferramenta, para que uma escrita não divida com outra o orçamento de 50 consultas do D1 por requisição.
Uma coisa medida com um cliente real: um Claude Code na geração 2026-07-28 recusou o tools/list sem dois campos que a especificação lista como opcionais, por quanto tempo a lista pode ficar em cache e que ela é igual para todos. O servidor passou a enviar os dois.
Só Bearer, e o porquê
A rota só abre com a chave de acesso como Bearer, ou com um token OAuth que o Worker já validou. O cookie de sessão do app não vale nessa rota: uma página aberta no navegador poderia chamar ferramentas por CSRF. Um Origin presente e diferente da própria origem dá 403. Sem Bearer, 401 com www-authenticate. Corpo que não é JSON, 415. Corpo acima de 256 KiB, 413. Qualquer método que não seja POST, 405.
Ferramentas: o que o modelo pode fazer
São sete: consultar o estoque, buscar produto, buscar cliente, listar tabelas de preço, conferir itens, gravar itens e listar os envios de itens. A busca de estoque usa a mesma função e os mesmos filtros da tela do app, que desceram para o núcleo do domínio para isso.
Cada ferramenta devolve um resumo em português, uma linha por item, e o JSON completo em structuredContent. Os limites são 50 produtos, 200 tabelas e 100 itens por chamada, pelo teto de saída de 25 mil tokens do Claude Code. Um argumento inválido volta como resultado com isError, e não como erro de protocolo -32602, para o modelo corrigir e chamar de novo.
Escrita em dois passos, presa por um código
A única ferramenta que escreve é a que grava itens num orçamento do ERP. Ela nunca é o primeiro passo.
conferir_itensresolve cada item contra o catálogo e contra o orçamento lido do ERP, sem gravar nada, e devolve o que iria, item por item, mais um código de 16 caracteres hexadecimais: o SHA-256 do id do orçamento, do ambiente e do corpo que seria enviado.- A descrição da ferramenta manda o modelo mostrar a prévia e esperar a confirmação da pessoa.
gravar_itensexige o código. Ele recalcula o SHA-256 sobre o que iria agora e, se a lista mudou, responde 409 antes de qualquer requisição ao ERP.
O que o código prova é que a lista não mudou entre a prévia e a gravação. Ele não prova que uma pessoa leu a prévia. Isso depende do cliente de IA seguir a instrução.
Um resultado incerto, quando o envio saiu e a resposta não chegou, volta como texto que diz “os itens podem ter entrado, confira na tela e não chame de novo”, sem a marca de erro, porque isError convida o modelo a repetir. A prévia seguinte do mesmo orçamento avisa que há um envio anterior sem confirmação.
OAuth para quem não manda cabeçalho
O ChatGPT não manda cabeçalho fixo. Ele só conecta um servidor MCP por OAuth. A escolha foi a biblioteca workers-oauth-provider da Cloudflare, na versão 1.2.1, com a própria chave de acesso como login, em vez de um provedor externo com lista de e-mails.
O fluxo tem os metadados da RFC 9728 em /.well-known/oauth-protected-resource, registro de cliente por CIMD, que o ChatGPT prefere, e por DCR para quem não tem CIMD, e um escopo só. A página de autorização mostra o nome do cliente e o domínio verificado, não tem script, tem CSP própria, e uma chave errada responde 401 sem gastar a página.
Três limites assumidos. A biblioteca só roda no Worker, então em Node vale só o Bearer, e a saída para uma VPS é o servidor de autorização da SDK oficial ou perder o ChatGPT. O hash da chave vai dentro do token, então trocar a chave derruba todo token. E todos entram com a mesma chave, então não há como tirar uma pessoa só.
O pacote implantado ficou em 367 KiB, 89 KiB com gzip.
Como verificamos
Em 25 de setembro de 2026, com curl contra o servidor local e os tokens do ambiente de testes do ERP: initialize, 202 na notificação, tools/list com as sete ferramentas, server/discover e conferir_itens na geração moderna, com a prévia de um rascunho em 0,6 segundo, e os erros 405, 401, 403 e 400 para versão desconhecida e Mcp-Name errado.
Oito testes cobrem o transporte, as duas gerações, cada ferramenta e a escrita em dois passos, com dublês do ERP. Seis testes percorrem o OAuth inteiro em Node, com um KV em memória: o desafio 401, os dois metadados, o registro, a tela, a chave errada, o PKCE errado recusado, o refresh e a chave trocada derrubando o token. Duas mutações conferidas: tirar a checagem da chave ou a autorização do token faz testes falharem.
O que não verificamos: o Claude.ai e o ChatGPT de verdade, inclusive se o plano do usuário tem conector personalizado e qual registro de cliente ele usa; o CIMD em si, que em Node fica desligado; e a CPU do Worker na validação do token e no uso real.
O app é de um cliente e não é público. O ERP é o Mercos, e o mesmo app rendeu o post sobre as gambiarras do plano grátis da Cloudflare.
Se os usuários do seu aplicativo querem perguntar ao ChatGPT ou ao Claude sobre os próprios dados, é esse o trabalho que fazemos em falar com o seu banco de dados. Quando a resposta precisa de revisão humana antes de uma escrita, como aqui, entra o que fazemos em soluções com IA.
Perguntas
- Um servidor MCP precisa da SDK oficial?
- Não. O protocolo é JSON-RPC 2.0 sobre HTTP. O nosso trata seis métodos em 216 linhas de transporte, sem sessão e sem SSE, e roda igual num Worker da Cloudflare e em Node.
- Como impedir que a IA grave o que a pessoa não viu?
- A gravação exige um código de 16 caracteres, derivado do SHA-256 da prévia. Se a lista mudou desde a prévia, o código não bate e a gravação é recusada antes de sair qualquer requisição.
- Funciona no ChatGPT?
- O ChatGPT só conecta por OAuth, e o fluxo inteiro foi implementado e testado em Node, com PKCE, refresh e troca de chave. Um ChatGPT de verdade ainda não foi conectado.
- Quais versões do protocolo MCP o servidor aceita?
- A de 2026-07-28 e as três anteriores, 2025-11-25, 2025-06-18 e 2025-03-26, no mesmo endpoint. A geração de cada mensagem é lida da própria mensagem.
- Qual é o tamanho máximo de uma requisição?
- 256 KiB. Acima disso o servidor responde 413. Um lote JSON-RPC, que só existe na versão 2025-03-26, aceita no máximo 10 mensagens e uma única chamada de ferramenta.