MCP stateless em funções edge: arquitetura, trade-offs e implementação
Um guia técnico aprofundado para construir um servidor Model Context Protocol stateless em Vercel Edge Functions — desenho JSON-RPC 2.0, a mudança do spec 2026-07-28, autenticação e rate limiting sem sessões e testes headless com curl.
O Model Context Protocol (MCP) foi concebido em torno de um modelo de sessão herdado de ferramentas locais baseadas em stdio: um cliente inicia um processo, faz um handshake initialize, recebe o reconhecimento notifications/initialized e mantém esse processo — e seu estado em memória — ativo durante toda a conversa. Esse modelo se encaixa bem em um app desktop falando com um subprocesso. Ele se encaixa mal em uma Vercel Edge Function que pode executar em um isolate diferente a cada requisição.
A revisão 2026-07-28 do spec MCP formalizou um transporte HTTP stateless que remove o handshake como pré-requisito obrigatório e permite a um servidor responder a tools/call e tools/list como pares puros, autossuficientes de requisição/resposta. Este post cobre o que mudou, por que o desenho anterior orientado a sessão era ativamente hostil a runtimes edge e como implementar um servidor MCP stateless compatível em Vercel Edge Functions — incluindo as partes que a maioria dos textos omite: autenticação sem estado de sessão no servidor, rate limiting na edge, convenções de códigos de erro JSON-RPC e testes headless com curl.
Este portfólio executa exatamente essa arquitetura em produção em /api/mcp — uma única Edge Function que trata get_resume, get_projects, get_capabilities, check_availability, get_manifest e a única ferramenta com efeito colateral, contact.
Por que sessões no estilo stdio não se encaixam em serverless
A premissa de transporte original do MCP é um processo único e de longa duração. Três propriedades desse modelo quebram assim que você migra para computação serverless ou edge:
- Não há processo persistente. Um servidor
stdiopossui um processo durante a sessão. Uma invocação de Edge Function dura apenas um ciclo de requisição — não há garantia de que a próxima requisição do mesmo cliente chegue ao mesmo isolate, e a maioria das plataformas explicitamente não oferece essa garantia. - Não existe memória compartilhada entre invocações. Qualquer valor guardado em variável de nível de módulo para representar “estado de sessão” (versão de protocolo negociada, lista de ferramentas listadas antes, contexto de autenticação criado durante
initialize) é invisível para a próxima invocação, a menos que seja externalizado para banco, KV store ou a própria requisição. - Handshake como pré-requisito conflita com cold starts. Se
tools/callsó é válido depois de uminitializebem-sucedido na mesma sessão e a plataforma não pode garantir continuidade da sessão, todo isolate frio precisa repetir o handshake ou rejeitar requisições que deveria conseguir atender.
O modo prático de falha antes da atualização 2026-07-28: equipes construíam servidores MCP em Lambda ou Vercel Functions e tentavam simular continuidade de sessão com roteamento sticky, stores de sessão externas chaveadas por um ID gerado no cliente ou, pior, aceitavam tools/call sem initialize torcendo para que os clientes não aplicassem a sequência com rigor. Os três são contornos para uma premissa de protocolo que não existe no runtime-alvo — não são soluções.
O que o spec stateless realmente mudou
- Eliminação do handshake como requisito rígido.
initializedeixou de ser um pré-requisito stateful que bloqueia todo método seguinte. Um servidor pode aceitar umtools/calloutools/listautossuficiente sem contexto de sessão prévio e responder corretamente. server/discovercomo rota direta de capacidades. Em vez de exigir uma sessão para conhecercapabilities, identidade do servidor e versão de protocolo, clientes podem chamarserver/discoverdiretamente e obter uma resposta completa e cacheável em uma ida e volta.- A negociação de protocolo vai para headers, não para estado de sessão. Informações de versão e roteamento de método viajam em cada requisição, em vez de serem negociadas uma vez e lembradas:
Mcp-Protocol-Version— por exemplo,2026-07-28Mcp-Method— por exemplo,tools/calloutools/listMcp-Name— o nome da ferramenta quando o método é uma chamada de execução
O resultado é que toda requisição é interpretável independentemente. Um load balancer, cache edge ou WAF pode inspecionar uma única requisição e saber exatamente o que está vendo sem correlacioná-la ao tráfego anterior. Essa propriedade — não apenas “sem handshake” — é o ganho arquitetural real, pois torna o protocolo compatível com escala horizontal stateless.
POST /api/mcp HTTP/1.1
Host: hdrx.com.br
Content-Type: application/json
Mcp-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: check_availability
{
"jsonrpc": "2.0",
"id": "req-001",
"method": "tools/call",
"params": {
"name": "check_availability",
"arguments": {}
}
}
Uma resposta compatível para uma chamada de ferramenta somente leitura:
{
"jsonrpc": "2.0",
"id": "req-001",
"result": {
"content": [
{
"type": "text",
"text": "{\"status\":\"open\",\"label\":\"Open to full-time roles\"}"
}
],
"isError": false
}
}
Runtime Edge vs. runtime Node.js: o que de fato limita um servidor MCP
Escolher Edge em vez de uma função serverless tradicional de Node.js não é gratuito. O modelo MCP stateless se ajusta bem às restrições do runtime edge, mas você precisa conhecê-las antes de comprometer uma implementação MCP com ele.
| Restrição | Runtime Edge (V8 isolate) | Função serverless Node.js |
|---|---|---|
| Cold start | Baixo, em geral abaixo de um segundo — sem boot de container | Maior — processo Node.js completo + inicialização do grafo de módulos |
| APIs disponíveis | Apenas padrões web (fetch, Request, Response, Web Crypto) — sem fs, módulos Node nativos |
Superfície completa da API Node.js, incluindo fs, addons nativos e a maioria dos pacotes npm |
| Limites de execução | Curtos, teto rígido (dependente da plataforma, em geral dezenas de segundos) | Tetos maiores, configuráveis por plano |
| Memória | Restrita, orçamento compartilhado do isolate | Maior, memória dedicada por invocação |
| Distribuição geográfica | Executa por padrão em PoPs próximos de quem solicita | Executa numa região fixa, salvo configuração multi-região |
| Acesso TCP/raw socket | Indisponível | Disponível — necessário para alguns drivers de DB, SMTP raw etc. |
| Compatibilidade npm | Parcial — pacotes que dependem de built-ins Node falham no build ou runtime | Completa |
| Melhor encaixe para MCP | Ferramentas stateless, intensivas em leitura, com dependências apenas HTTP (fetch para Resend, API REST, módulo de dados estático) | Ferramentas que precisam de driver de DB nativo, sistema de arquivos ou computação longa |
A implicação concreta para um servidor MCP: todo handler de ferramenta deve ser alcançável por I/O compatível com fetch. Para a implementação deste portfólio, isso é trivial — a fonte de dados é um conjunto de módulos TypeScript empacotados com a função (src/data/*.ts), e a única chamada de rede (contact, via Resend) é um POST HTTP comum. Se uma ferramenta precisasse de conexão PostgreSQL direta por TCP ou uma biblioteca de geração de PDF exclusiva de Node, ela precisaria rodar no runtime Node.js — Edge não é substituto universal; é a ferramenta certa para um perfil de I/O específico.
Uma segunda restrição, menos óbvia: não há cache em memória entre invocações em que você possa confiar. Algumas plataformas Edge mantêm um isolate aquecido por pouco tempo e um Map em nível de módulo pode sobreviver a algumas requisições na prática, mas isso é detalhe de implementação do warm pool, não contrato garantido. Lógica de ferramenta que dependa silenciosamente disso (por exemplo, contador de rate limit em memória) passa em testes locais e depois falha de modo intermitente em produção quando o tráfego se espalha entre isolates.
Lidando com autenticação e rate limiting sem sessões no servidor
Esta é a parte que a mudança do spec não resolve por você. Remover o handshake remove o lugar onde um servidor tradicional teria estabelecido contexto de autenticação e um bucket de rate limit por sessão. Um servidor MCP stateless precisa derivar ambos em toda requisição.
Como autenticar um servidor MCP stateless sem sessões?
Use uma credencial bearer por requisição, validada independentemente a cada chamada — nunca uma consulta de sessão no servidor. Dois padrões viáveis:
- Chave de API estática ou com escopo no header
Authorization, conferida contra um valor guardado em variável de ambiente (ou uma pequena allowlist) em cada invocação. Sem sessão, sem estado no servidor — a própria credencial é o contexto completo de autenticação. - Tokens assinados de TTL curto (por exemplo, um JWT emitido por serviço upstream) verificados apenas pela assinatura, sem ida ao banco. Isso mantém o caminho de autenticação da Edge Function como computação pura, o que a torna rápida na edge.
Para uma superfície MCP pública e somente leitura como get_resume ou get_projects, a escolha pragmática costuma ser nenhuma autenticação — os dados já são públicos no site — deixando checagens de credenciais para a ferramenta com efeito colateral (contact). Não acrescente uma camada de autenticação de que uma ferramenta pública e somente leitura não precisa; ela é superfície de ataque e latência sem benefício correspondente.
Como aplicar rate limit a uma função edge sem estado de sessão compartilhado?
Você não pode manter contadores na memória da função e esperar correção — as instâncias não compartilham estado garantidamente. Três abordagens que funcionam de fato de maneira stateless:
- Store externo de contador atômico (Upstash Redis, Vercel KV, Cloudflare Durable Objects), chaveado por IP do cliente ou chave de API, incrementado com
INCR+EXPIREatômicos por requisição. É a solução correta de uso geral e adiciona uma ida de rede por requisição. - Rate limiting edge no nível da plataforma (regras Vercel Firewall, regras Cloudflare Rate Limiting) aplicado antes da função, chaveado por IP, caminho ou header. Não adiciona latência dentro do handler, mas tem controle mais grosseiro — diferenciar
tools/listdecontactnesse nível exige complexidade extra de regras. - Token bucket por estado assinado do cliente — codifique um token de rate limit em cookie ou header, assinado e associado a janela de tempo, renovado pelo servidor a cada requisição aceita. Evita store de apoio, mas é fácil errar (replay, clock skew) e não vale a complexidade para a maioria das superfícies MCP.
Na prática, combinar (2) para proteção grosseira baseada em IP com (1) com escopo específico na ferramenta contact (a única com custo real: um e-mail de saída) cobre a superfície de risco efetiva sem superengenheirar as ferramentas somente leitura.
Convenções de códigos de erro JSON-RPC 2.0 para ferramentas MCP
MCP envolve a execução de ferramentas em JSON-RPC 2.0; por isso o tratamento de erro tem duas camadas distintas, e confundi-las é um erro comum de implementação:
- Erros de nível de protocolo — envelope JSON-RPC malformado, método desconhecido, formato inválido de params. Usam os códigos reservados padrão de JSON-RPC e preenchem o campo
errorde nível superior; a resposta não temresult. - Erros de nível de ferramenta — a chamada RPC teve sucesso, mas a lógica da ferramenta falhou (por exemplo,
contactrejeitou porquebriefexcede 4000 caracteres). São devolvidos como resposta JSON-RPC bem-sucedida comresult.isError: truee mensagem legível emcontent, conforme a convenção MCP — não como erro JSON-RPC.
Códigos reservados padrão JSON-RPC 2.0 para falhas de protocolo:
| Código | Significado | Quando usá-lo em um servidor MCP |
|---|---|---|
-32700 |
Erro de parse | O corpo da requisição não é JSON válido |
-32600 |
Requisição inválida | Faltam jsonrpc, id ou method |
-32601 |
Método não encontrado | Mcp-Method ou params.name não corresponde a ferramenta registrada |
-32602 |
Params inválidos | arguments falha na validação de schema para a ferramenta nomeada |
-32603 |
Erro interno | Exceção não tratada no handler — registre no servidor, jamais exponha stack trace |
-32000 a -32099 |
Erro de servidor (faixa reservada) | Erros específicos da implementação, como rate limit excedido |
Uma chamada rejeitada por entrada inválida (params inválidos) é assim — note que é erro de nível JSON-RPC, pois a requisição não pôde ser despachada:
{
"jsonrpc": "2.0",
"id": "req-002",
"error": {
"code": -32602,
"message": "Invalid params: 'contact' field exceeds maximum length of 160 characters"
}
}
Já uma ferramenta que executou, mas produziu falha de lógica de negócio (por exemplo, o provedor de e-mail rejeitou o envio) devolve um envelope bem-sucedido com payload de erro dentro de result:
{
"jsonrpc": "2.0",
"id": "req-003",
"result": {
"content": [
{ "type": "text", "text": "Message could not be delivered. Please try again later." }
],
"isError": true
}
}
Nunca exponha stack traces, caminhos internos de arquivos ou detalhes específicos de provedores (por exemplo, corpo bruto de erro da API Resend) em nenhum formato de erro — apresente mensagem sanitizada e registre o original apenas no servidor.
Testando um servidor MCP stateless em modo headless com curl
Como toda requisição é autossuficiente, você não precisa de cliente MCP nem de sessão em execução para validar um servidor stateless — curl basta para testes de integração completos, inclusive em CI.
1. Descubra capacidades sem uma sessão:
curl -s -X POST https://hdrx.com.br/api/mcp \
-H "Content-Type: application/json" \
-H "Mcp-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: server/discover" \
-d '{"jsonrpc":"2.0","id":"disc-1","method":"server/discover","params":{}}'
2. Liste as ferramentas disponíveis:
curl -s -X POST https://hdrx.com.br/api/mcp \
-H "Content-Type: application/json" \
-H "Mcp-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tools/list" \
-d '{"jsonrpc":"2.0","id":"list-1","method":"tools/list","params":{}}'
3. Chame diretamente uma ferramenta somente leitura, sem initialize prévio:
curl -s -X POST https://hdrx.com.br/api/mcp \
-H "Content-Type: application/json" \
-H "Mcp-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tools/call" \
-H "Mcp-Name: get_projects" \
-d '{"jsonrpc":"2.0","id":"proj-1","method":"tools/call","params":{"name":"get_projects","arguments":{"featured":true}}}'
4. Verifique que entrada malformada é rejeitada com o código JSON-RPC correto:
curl -s -X POST https://hdrx.com.br/api/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"bad-1","method":"tools/call","params":{"name":"contact","arguments":{"brief":""}}}' \
| jq '.error.code'
# esperado: -32602
5. Confirme que o handshake é realmente opcional executando o passo 3 como a primeira requisição de uma sessão nova (sem chamada initialize anterior na sequência) e verificando um 200 com result válido — esse é o teste de regressão que de fato prova a ausência de estado, em vez de apenas provar que o endpoint funciona.
Isso também significa que load testing (k6, autocannon, hey) contra um servidor MCP não requer configuração de sessão nem lógica de pool de conexão no script: cada usuário virtual apenas dispara POSTs independentes, uma aproximação razoável do padrão de tráfego real.
Benefícios práticos na Vercel e Cloudflare
Executar a arquitetura stateless em Edge Functions entrega três propriedades concretas, em ordem de impacto real:
- Pegada zero de memória persistente. Nenhuma store de sessão para provisionar, monitorar ou deixar vazar. Todo o servidor é função pura de
(request) -> response. - Escala horizontal trivial. Como nenhuma requisição depende de uma anterior cair no mesmo isolate, a plataforma pode rotear cada chamada ao PoP geograficamente mais próximo sem overhead de coordenação.
- Proteção de rate limit baseada em IP na camada de plataforma, composta com a store com escopo de ferramenta descrita acima, sem que o próprio servidor MCP precise acompanhar estado de conexão.
- Latência global baixa e consistente — o número específico depende da fonte de dados e da distância até o PoP mais próximo; trate qualquer afirmação fixa em milissegundos com ceticismo, a menos que a tenha medido na sua implantação.
A lição arquitetural se aplica além do MCP: protocolos desenhados para tooling de agentes de IA devem usar requisições stateless e autodescritivas por padrão, salvo razão específica para manter estado (streaming de uma chamada de ferramenta longa é a principal exceção legítima, e é exatamente por isso que Streamable HTTP existe como preocupação de transporte separada do modelo stateless de requisição/resposta aqui descrito). Um servidor de ferramentas que responde tools/call corretamente sem contexto prévio é um servidor que escala sem uma equipe de operações por trás.
FAQ técnico
O spec MCP stateless remove initialize por completo?
Não. initialize ainda existe e clientes stateful (por exemplo, apps desktop de longa duração) podem usá-lo para negociar capacidades uma vez por conexão. O que mudou é que servidores não são mais obrigados a tratá-lo como pré-requisito — um servidor stateless também precisa aceitar tools/call e tools/list sem initialize anterior no mesmo fluxo de requisições.
Um servidor MCP stateless pode oferecer respostas de ferramentas em streaming?
Sim, pelo transporte Streamable HTTP, que é uma preocupação separada de não ter estado. Uma única requisição ainda pode produzir resposta em stream (útil para chamadas de ferramentas longas ou resultados incrementais); o que continua stateless é que o servidor não precisa lembrar nada sobre o cliente entre requisições HTTP separadas.
Por que não usar apenas WebSockets para MCP na Vercel?
WebSockets exigem uma conexão persistente mantida aberta por um único processo de servidor, algo fundamentalmente incompatível com Edge Functions e a maioria dos modelos de computação serverless — não há processo de longa duração para manter o socket. Um POST HTTP stateless por chamada contorna isso e torna MCP implantável em Edge Functions e Cloudflare Workers.
Como versionar um servidor MCP sem quebrar clientes existentes?
Use o header Mcp-Protocol-Version como ponto de negociação. Um servidor pode inspecioná-lo e ramificar o formato de resposta por versão ou simplesmente rejeitar versões não suportadas com -32600 e uma mensagem clara. Como toda requisição carrega sua própria versão, é possível executar múltiplas versões de protocolo simultaneamente no mesmo deploy — não há versão presa a uma sessão com que se preocupar.
É seguro cachear tools/list na edge?
Sim, para conjuntos de ferramentas que não mudam por chamador (ou seja, sem visibilidade de ferramentas por usuário). Como a resposta depende apenas da configuração do servidor, não de estado do cliente, ela é boa candidata a cache edge com TTL curto (minutos, não segundos) e invalidação no deploy. Não cacheie respostas de tools/call que dependem de arguments, a menos que esteja construindo deliberadamente um cache chaveado no conjunto completo de argumentos.
Qual é a diferença entre um erro JSON-RPC e um erro de ferramenta MCP?
Um erro JSON-RPC significa que a própria chamada RPC falhou ao executar — método inválido, params inválidos, exceção interna — e volta no campo error de nível superior, sem result. Um erro de ferramenta MCP significa que a chamada foi executada com sucesso, mas a própria lógica não pôde cumprir a requisição (por exemplo, uma falha de validação dentro de contact); ele volta como resposta JSON-RPC bem-sucedida com result.isError: true. Confundir os dois quebra o tratamento de erro do cliente, pois a maioria dos clientes MCP só considera o primeiro uma falha de transporte que admite retry.
Por que uma ferramenta MCP somente leitura não precisa de autenticação?
Se os dados subjacentes já são públicos — currículo, lista de projetos, disponibilidade — acrescentar autenticação só aumenta latência e superfície de ataque sem mudar o que alguém poderia raspar diretamente do site público. Reserve credenciais para ferramentas com custo ou efeito colateral real, como enviar um e-mail.
A mesma Edge Function pode servir o endpoint MCP e endpoints REST?
Sim, mas em geral é mais limpo não fazer isso. Compartilhar os módulos de dados subjacentes (src/data/*.ts) entre /api/mcp e endpoints REST como /api/resume mantém uma fonte única da verdade, enquanto manter as rotas separadas evita misturar a lógica de dispatch JSON-RPC e a modelagem de respostas REST em um só handler.
Dúvidas ou ideias de projeto?
Entre em contato para falar sobre arquitetura, otimização ou agentes de IA.