Servidores MCP sem estado: o que mudou de fato
A revisão 2026-07-28 removeu sessões e o handshake initialize do MCP. Veja os efeitos no proxy reverso, health checks, timeouts e autenticação.
O que é um servidor MCP sem estado
Um servidor MCP sem estado não mantém estado por cliente entre pedidos. Cada pedido inclui a versão do protocolo, as capacidades do cliente e as credenciais de que o servidor precisa para responder. Assim, qualquer processo em qualquer máquina pode responder a qualquer pedido. O MCP (Model Context Protocol, o formato de comunicação que os agentes usam para aceder a ferramentas) transformou isto numa regra na revisão 2026-07-28, que removeu o handshake initialize e a sessão HTTP subjacente.
Esse é o objetivo operacional. Um servidor que não mantém dados por cliente pode ficar atrás de um balanceador de carga normal, sem afinidade de sessão. Também pode ser reiniciado durante uma implementação sem interromper os clientes e ser executado como quatro processos idênticos em vez de um. Um servidor orientado por sessões não oferece estas características sem mecanismos adicionais.
O Model Context Protocol é um protocolo sem estado: toda a informação necessária para processar um pedido está contida no próprio pedido. O servidor processa cada pedido de forma independente. Não deve inferir estado a partir de pedidos anteriores, mesmo que tenham sido feitos na mesma ligação ou stream.
Sem estado não significa que o servidor não armazene nada. A sua base de dados, a sua fila e a sua cache continuam a existir. Significa que o protocolo não transporta estado na ligação. Por isso, o servidor não deve tratar uma ligação, um processo ou um socket aberto como substituto de "este cliente, a meio de uma conversa".
O que a revisão 2026-07-28 removeu
2026-07-28 é a revisão atual da especificação em agosto de 2026. Em comparação com 2025-11-25, remove cinco elementos que existiam para suportar sessões.
- O pedido
initializee a notificaçãonotifications/initialized. Não existe qualquer handshake (SEP-2575). - O cabeçalho
Mcp-Session-Ide o encerramento da sessão com HTTPDELETE(SEP-2567). - O fluxo HTTP
GETautónomo, no qual os servidores enviavam notificações. Foi substituído porsubscriptions/listen, um POST normal cuja resposta é um fluxo de longa duração. - A capacidade de retomar fluxos SSE (server-sent events). O cabeçalho
Last-Event-IDe os IDs por evento foram removidos. Se um fluxo for interrompido, o pedido em curso é perdido e o cliente tem de o enviar novamente como um novo pedido, com um novo ID de pedido. ping,logging/setLevelenotifications/roots/list_changed. O nível de log é agora um campo por pedido,io.modelcontextprotocol/logLevelem_meta.
Foi adicionado um método, que todos os servidores têm de implementar. server/discover devolve, numa única chamada, as versões de protocolo suportadas pelo servidor, as capacidades e a identidade. É o elemento mais próximo de um handshake que resta, e a sua chamada é opcional para os clientes.
Por que o transporte de sessão era difícil de executar em produção
Em 2025-11-25 e anteriores, um servidor podia gerar um ID de sessão durante a inicialização e devolvê-lo no cabeçalho Mcp-Session-Id na InitializeResult. Depois, o cliente tinha de enviar esse cabeçalho em todos os pedidos seguintes. A versão do protocolo negociada e as capacidades do cliente ficavam na memória do servidor, associadas a esse ID. Cada uma dessas escolhas tinha um custo operacional.
- Um reinício eliminava a tabela de sessões. A especificação exigia que o servidor respondesse com
404 Not Founda qualquer pedido que transportasse um ID de sessão inválido e exigia que o cliente começasse de novo com um novoInitializeRequest. Cada deploy tornava-se um evento de reconexão para todos os clientes ligados. - Uma segunda réplica não conhecia as sessões da primeira réplica. Escalar horizontalmente exigia encaminhamento persistente no load balancer ou um armazenamento de sessões partilhado, que todas as réplicas consultariam em cada pedido.
- A tabela de sessões ocupava memória e crescia com os clientes inativos.
DELETEera opcional, e os clientes que fechavam a ligação sem o enviar deixavam entradas para trás. - Os resultados das listas podiam variar consoante a ligação, pelo que era inseguro colocar uma cache à frente do servidor.
Remover as sessões elimina os quatro problemas de uma só vez. Esta é a alteração que deve compreender antes de alterar qualquer configuração.
O que cada pedido transporta agora
Cada pedido POST para o endpoint MCP é autónomo. A versão do protocolo e as capacidades do cliente são enviadas no corpo do pedido, em _meta, e alguns campos são replicados nos cabeçalhos HTTP para que um intermediário possa encaminhar o pedido sem analisar o JSON.
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
Authorization: Bearer <access token>
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {"location": "Seattle, WA"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "ExampleClient", "version": "1.0.0"},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}io.modelcontextprotocol/protocolVersion e io.modelcontextprotocol/clientCapabilities são obrigatórios em todos os pedidos. clientInfo não é obrigatório, embora os clientes devam enviá-lo. Um pedido sem um campo obrigatório está malformado. Por isso, o servidor deve rejeitá-lo com o erro JSON-RPC -32602 e HTTP 400 Bad Request.
O cabeçalho Mcp-Method é obrigatório em todos os pedidos. Mcp-Name é obrigatório em tools/call, resources/read e prompts/get. O valor do cabeçalho deve corresponder ao corpo. Um servidor que processe o corpo deve rejeitar uma divergência com 400 Bad Request e os códigos de erro -32020, HeaderMismatch. Esta regra existe porque um balanceador de carga que encaminha com base no cabeçalho e um servidor que executa com base no corpo usam duas fontes de verdade diferentes. Se encaminhar ou limitar a taxa com base nestes cabeçalhos, verifique primeiro MCP-Protocol-Version. As revisões anteriores nunca validavam o cabeçalho contra o corpo. Nessas versões, o valor do cabeçalho não é fiável.
Uma divergência de versão é agora um erro normal por pedido, e não um handshake falhado. Um servidor que não implemente a versão solicitada responde 400 Bad Request com -32022, UnsupportedProtocolVersion e lista as versões que suporta em data.supported. O cliente escolhe uma versão dessa lista e tenta novamente.
Onde ficou o estado: tokens, cursores, subscrições
O estado não desapareceu. Foi transferido para locais que pode consultar e registar nos logs.
As credenciais passam a fazer parte de cada pedido. Não existe uma sessão à qual associar uma identidade. Por isso, o token de acesso acompanha cada chamada HTTP e é validado em cada pedido. Os detalhes estão na secção de autenticação abaixo.
Os cursores têm de transportar a sua própria posição. A paginação em tools/list, resources/list, prompts/list e resources/templates/list usa uma cadeia de cursor opaca, que os clientes não devem analisar nem modificar. Num servidor com um único processo, era comum manter o offset em memória, associado à sessão. Sem sessão, o cursor tem de ser suficiente para qualquer réplica retomar a listagem. Para isso, codifique a posição dentro do cursor e assine-o, ou mantenha essa posição num armazenamento partilhado por todas as réplicas. Um cursor inválido deve devolver -32602. Assine-o porque um cursor opaco continua a ser uma entrada fornecida pelo cliente que o código descodifica e considera fiável.
As subscrições pertencem a um pedido, não a uma ligação. Um cliente que pretenda receber notificações de alterações envia subscriptions/listen com um filtro que identifica os tipos pretendidos: toolsListChanged, promptsListChanged, resourcesListChanged e resourceSubscriptions. O servidor responde com notifications/subscriptions/acknowledged e mantém aberto o fluxo dessa resposta. Se o fluxo for interrompido, o servidor não guarda nada, e o cliente envia novamente subscriptions/listen para restabelecê-lo.
O estado da aplicação entre chamadas passa a ser um identificador explícito. Quando um servidor tem realmente de memorizar algo entre chamadas, a resposta da especificação é um identificador gerado pelo servidor e devolvido como um argumento normal da ferramenta. Esse identificador aparece no esquema da ferramenta, pode ser registado nos logs e nunca é inferido a partir da ligação. Um servidor com dados reais por utilizador, como um servidor de email MCP autoalojado, usa este padrão em vez de uma sessão: o identificador da caixa de correio ou do rascunho é um argumento da ferramenta, pelo que qualquer réplica pode processar a chamada seguinte.
Implementação: reverse proxy, timeouts e verificações de integridade
O endpoint MCP é um caminho que aceita POST. A maior parte do tráfego consiste numa solicitação curta e numa resposta JSON, que qualquer proxy consegue tratar. A exceção é a resposta em streaming, em que as predefinições do proxy prejudicam o funcionamento. É esta a parte que muda quando passa de uma demonstração num portátil para um servidor MCP executado num VPS.
location /mcp {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}proxy_buffering off é importante porque o nginx armazena as respostas encaminhadas por proxy em buffer por predefinição. Isto retém os eventos SSE até o buffer encher ou a resposta terminar. A especificação também pede que os servidores enviem X-Accel-Buffering: no nas respostas SSE, e o nginx respeita esse cabeçalho. Por isso, um servidor correto informa o proxy da configuração adequada. Defina também a diretiva, porque essa é a parte que controla.
proxy_read_timeout tem o valor predefinido de 60 segundos. Um fluxo subscriptions/listen que permaneça inativo durante mais tempo é fechado pelo nginx, não pelo seu servidor. Assim, os logs mostram um processo saudável, enquanto o cliente mostra um fluxo interrompido. Aumente o valor apenas na localização MCP, não em todo o servidor. Os servidores também devem enviar uma linha de comentário SSE (uma linha que começa com dois-pontos) como keep-alive durante períodos de inatividade. Isto impede que os intermediários terminem o fluxo por timeout.
O Caddy exige menos configuração. Por predefinição, armazena parcialmente os dados em buffer para melhorar a eficiência no envio e faz flush imediato quando a resposta contém Content-Type: text/event-stream. Por isso, o streaming funciona sem diretivas adicionais.
mcp.example.com {
reverse_proxy 127.0.0.1:8080 {
health_uri /healthz
health_interval 10s
}
}Observe o destino dessa verificação de integridade. Não aponte uma verificação ativa para o endpoint MCP com GET. Um servidor que implemente apenas esta revisão responde 405 Method Not Allowed a GET e DELETE, enquanto o método de verificação predefinido do Caddy é GET. O proxy marcaria então um backend perfeitamente saudável como indisponível. Disponibilize para o proxy um caminho simples, como /healthz, e verifique o protocolo separadamente com um POST.
curl -sS https://mcp.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":"health-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'Um 200 que contenha uma lista supportedVersions significa que o processo está ativo e a comunicar através do protocolo. Um 404 com o erro JSON-RPC -32601 significa que o processo está ativo, mas não disponibiliza server/discover, que todos os servidores 2026-07-28 têm de implementar. Um 400 com -32022 significa que o verificador pediu uma versão não suportada por esta compilação. É exatamente isto que pretende detetar depois de atualizar uma dependência. O nginx de código aberto não tem verificações ativas de integridade. Por isso, use max_fails e fail_timeout passivos no upstream e execute a verificação do protocolo no seu sistema de monitorização.
Um reinício gradual passa a perder apenas os pedidos em curso. Drene o serviço, aguarde que os POST abertos terminem, inicie o novo processo e deixe os clientes repetir os pedidos que falharam. A única coisa que continua a ser perdida é qualquer fluxo subscriptions/listen aberto, porque esse fluxo é uma ligação ativa a um processo específico. A ausência de estado eliminou a afinidade de sessão. Não eliminou a afinidade de ligação de um fluxo que está aberto naquele momento, e nenhuma regra de encaminhamento corrige isso. Um cliente consegue distinguir os casos: um fluxo que termina com o resultado subscriptions/listen vazio foi fechado corretamente; um fluxo que termina sem esse resultado foi interrompido, e o cliente pode interpretar isso como motivo para restabelecer a ligação.
A colocação em cache passa a ser possível pela primeira vez. Os resultados dos métodos de listagem passam agora a conter ttlMs e cacheScope, e cacheScope: "public" informa os intermediários partilhados de que podem colocar a resposta em cache. Isto só é seguro porque os resultados das listagens já não variam por ligação. Essa é uma consequência direta da remoção das sessões.
Por que a autenticação muda quando não existe uma sessão
Com uma sessão, era tentador autenticar uma vez em initialize e depois tratar o ID da sessão como prova para tudo o que viesse a seguir. Um ID de sessão usado dessa forma é uma credencial bearer sem audiência, expiração ou mecanismo de revogação, emitida pelo seu próprio servidor. Remover as sessões elimina esse atalho, e a substituição é mais rigorosa.
Um servidor MCP protegido funciona como um servidor de recursos OAuth 2.1. Cada pedido HTTP do cliente deve incluir Authorization: Bearer <access token>, e o servidor valida o token em cada pedido. A validação inclui a audiência: o servidor deve confirmar que o token foi emitido especificamente para si, de acordo com a RFC 8707 (Resource Indicators for OAuth 2.0), e não deve aceitar nem encaminhar tokens destinados a qualquer outro recurso. Os clientes solicitam a audiência correta enviando o parâmetro resource com o URI canónico do servidor.
A descoberta começa com um desafio. Quando chega um pedido sem um token utilizável, o servidor responde com 401 Unauthorized.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"O cliente lê resource_metadata, obtém esse documento (RFC 9728, OAuth 2.0 Protected Resource Metadata, que os servidores MCP devem implementar), identifica o servidor de autorização e executa o fluxo. Um token válido com permissões insuficientes recebe 403 Forbidden com error="insufficient_scope" e os scopes necessários para essa operação.
Isto tem duas consequências para a forma como executa o serviço. A validação do token ocorre agora em cada pedido, em vez de uma vez por sessão. Por isso, uma ida à rede até um endpoint de introspeção por chamada será visível na latência. Prefira tokens que possam ser verificados localmente com base numa assinatura, numa audiência e numa expiração, ou coloque em cache o resultado da validação durante um intervalo curto, indexado pelo token. Além disso, como não existe uma sessão que mantenha uma identidade, a autorização deve ser calculada a partir do token em cada chamada. Isto é mais rigoroso do que o modelo de sessão e combina com a prática mais ampla de manter as credenciais fora do processo do agente, abordada em manter segredos fora de um agente de IA.
O que é válido nesta revisão e o que não é
Tudo o que foi descrito acima aplica-se à revisão 2026-07-28. Não descreve o MCP para sempre nem descreve o servidor que implementou no ano passado.
Os clientes e servidores na 2025-11-25 e anteriores ainda usam o modelo de handshake. A especificação chama essas revisões de legadas e chama de modernas as revisões que usam metadados por pedido. Um servidor que suporte apenas esta revisão, ao comunicar com um cliente mais antigo, deve responder 405 Method Not Allowed a GET ou DELETE no endpoint do MCP, ignorar qualquer cabeçalho Mcp-Session-Id sem gerar nem devolver um, e ignorar Last-Event-ID porque os streams não podem ser retomados. Um servidor compatível com as duas eras pode servir ambas no mesmo endpoint: um pedido que contenha _meta é servido sem estado, e um pedido initialize seleciona a semântica de sessão mais antiga.
Por isso, verifique a string da revisão antes de confiar nestas informações. Se o seu SDK ainda enviar initialize, as sessões continuam a ser efetivas na sua implementação e os problemas relacionados com sessões descritos acima continuam a ser da sua responsabilidade. O mesmo se aplica ao lado do cliente: um processo de agente no seu próprio sistema, como a configuração descrita em executar um agente de programação numa VPS, só é sem estado neste sentido se a biblioteca utilizada falar uma revisão moderna. Leia a versão que o seu runtime negoceia e, em seguida, leia a revisão correspondente da especificação. Considere esta página como a descrição de uma revisão específica, não do protocolo em geral.
FAQ
Um servidor MCP stateless significa que não posso armazenar nada?
Não. Stateless descreve o protocolo, não a sua aplicação. Bases de dados, filas e caches continuam a funcionar exatamente da mesma forma. O que muda é que o estado que abrange várias chamadas tem de ser referenciado por um identificador explícito enviado pelo cliente em cada pedido, como um handle gerado pelo servidor num argumento de ferramenta. O que não pode fazer é inferir contexto a partir da ligação: a especificação determina que um servidor não deve depender de pedidos anteriores na mesma ligação para estabelecer capacidades, versão do protocolo ou identidade do cliente, porque cada pedido fornece esses dados em _meta.
Ainda preciso de sessões persistentes no balanceador de carga?
Não para pedidos normais. Na revisão 2026-07-28, cada POST transporta a sua própria versão do protocolo, capacidades e credenciais, por isso qualquer réplica pode responder a qualquer pedido e o round-robin é suficiente. O único elemento de longa duração que resta é o fluxo de resposta subscriptions/listen, que consiste numa única ligação aberta a um único processo. Termina quando esse processo termina, e o cliente envia subscriptions/listen novamente para o restabelecer. Isto corresponde à duração da ligação, não à afinidade de sessão, e nenhuma regra de encaminhamento o impede.
O que aconteceu ao Mcp-Session-Id e ao fluxo HTTP GET?
Ambos foram removidos na revisão 2026-07-28, ao abrigo de SEP-2567 e SEP-2575. Um servidor que implemente apenas esta revisão deve responder com 405 Method Not Allowed a GET e DELETE no endpoint MCP, e deve ignorar um cabeçalho Mcp-Session-Id em vez de devolver um cabeçalho igual. As notificações de alteração iniciadas pelo servidor passam agora no fluxo de resposta de um pedido subscriptions/listen, em vez de usarem um fluxo GET autónomo. Os servidores que têm de continuar a servir clientes mais antigos implementam o comportamento da revisão anterior juntamente com este.
Como faço um health check a um servidor MCP sem handshake?
Use dois níveis. Aponte a verificação ativa do proxy para um caminho HTTP simples servido pela sua aplicação, porque um GET ao endpoint MCP devolve corretamente 405 e faria o proxy considerar indisponível um backend saudável. Depois, verifique o próprio protocolo enviando server/discover por POST. Todos os servidores 2026-07-28 têm de implementar esse pedido. Confirme que a resposta é HTTP 200 e que lista uma versão do protocolo utilizada pelos seus clientes. Um 404 com o erro JSON-RPC -32601 significa que o processo está em execução, mas não serve esse método. Um 400 com -32022 significa que a versão solicitada não é suportada por essa compilação.