1

MCP stateless não elimina estado: sua aplicação precisa torná-lo explícito

Imagine um servidor MCP que controla navegadores remotos. Uma ferramenta abre o navegador, outra acessa uma URL e a terceira tira uma captura de tela.

No primeiro desenho da aplicação, o servidor guarda o navegador numa sessão em memória. A chamada seguinte chega à mesma instância, encontra o recurso e continua o trabalho. Tudo parece simples até o serviço ganhar uma segunda instância, um balanceador ou um retry.

Com o ciclo stateless da especificação MCP 2026-07-28, a próxima requisição pode cair em qualquer instância. Se ela não trouxer um browser_id, a identidade de quem fez a chamada e a operação esperada, o servidor não tem como adivinhar qual navegador deve recuperar.

É aí que a palavra "stateless" costuma confundir.

O protocolo deixa de manter uma sessão implícita. O navegador, o carrinho, o job de vídeo e a tarefa longa continuam existindo. Esse estado não desaparece. Ele precisa ganhar nome, contrato e regras de acesso.

O que realmente sai do protocolo

A nova revisão remove o handshake initialize/initialized, o header Mcp-Session-Id e a sessão mantida no nível do protocolo. Versão, identificação e capacidades do cliente passam a acompanhar cada requisição em _meta.

Na prática, isso reduz a afinidade entre uma conexão e uma instância específica do servidor. Uma chamada não precisa encontrar a mesma memória de processo usada pela chamada anterior. Fica mais simples distribuir tráfego, trocar uma instância com problema e operar o serviço sem sticky session.

Mas essa simplificação vale para o transporte. Ela não decide onde seu produto guarda recursos que vivem por mais de uma chamada.

Vale separar três coisas que antes podiam acabar misturadas:

  1. o contexto do protocolo, como versão e capacidades do cliente;
  2. o estado do produto, como um navegador aberto ou um carrinho em edição;
  3. o progresso de uma operação, como uma exportação que espera aprovação ou mais dados.

O primeiro passa em _meta. Os outros dois continuam sendo responsabilidade da aplicação.

Um handle explícito muda o contrato

Vamos voltar ao navegador remoto. Um fluxo mínimo pode funcionar assim:

create_browser() -> { browser_id: "brw_7f2..." }

navigate({
  browser_id: "brw_7f2...",
  url: "https://example.com"
})

create_browser persiste o recurso e devolve um handle opaco. navigate recebe esse handle, valida o acesso e recupera o navegador no armazenamento adequado. A instância que criou o recurso não precisa ser a mesma que executa a navegação.

Essa mudança melhora a rastreabilidade. Logs, métricas e erros podem apontar para o recurso envolvido sem depender de uma sessão escondida. Também deixa mais claro quais ferramentas criam estado e quais apenas consultam ou alteram algo já existente.

Só que um identificador enviado pelo cliente não é uma autorização.

Se o servidor aceita qualquer browser_id válido sem conferir o dono ou o escopo, ele cria uma porta para um usuário operar o recurso de outro. O contrato precisa responder a perguntas bem menos empolgantes do que "qual será o nome da ferramenta?", mas muito mais importantes:

  • quem pode usar este handle;
  • por quanto tempo ele existe;
  • o que acontece depois da expiração;
  • se ele pode atravessar organizações ou projetos;
  • quais operações são permitidas para aquela identidade.

O ideal é que o handle não revele detalhes internos e que toda operação verifique a autorização no recurso recuperado. Não conte com o modelo para respeitar esse limite. O controle precisa existir no servidor.

O estado escondido costuma aparecer em cinco lugares

Antes de atualizar o SDK, procure onde a implementação atual depende da sessão. Uma busca por initialize, Mcp-Session-Id, Redis, caches em memória e configuração de sticky routing costuma revelar parte do problema.

Depois, acompanhe um fluxo real por duas ou três chamadas. O que a segunda chamada espera encontrar sem receber como argumento?

Em servidores pequenos, o estado escondido aparece bastante nestes pontos:

  • recursos abertos, como navegador, terminal, arquivo temporário ou conexão;
  • projeto, workspace, conta ou ambiente selecionado;
  • permissões calculadas apenas durante a inicialização;
  • resultados parciais de tarefas demoradas;
  • chaves usadas para correlacionar logs, cobranças ou limites.

Nem tudo precisa virar um campo enorme repetido em cada chamada. Muitas vezes basta passar um identificador curto e recuperar os dados no servidor. O ponto é tornar a dependência visível no contrato, em vez de deixá-la presa à memória de uma conexão.

Também vale resistir à tentação de criar um context_id genérico que guarda tudo. Ele pode recriar a antiga sessão com outro nome. Prefira handles ligados a recursos e operações concretas: browser_id, cart_id, export_id, task_id.

Retry sem idempotência duplica o problema

Uma arquitetura stateless facilita repetir uma chamada quando a conexão cai ou uma instância some. Para leituras, isso tende a ser tranquilo. Para operações com efeito colateral, o retry pode abrir dois navegadores, criar duas cobranças ou iniciar duas exportações.

Considere este fluxo:

create_browser({
  idempotency_key: "run_42:create_browser"
})

Se a resposta se perder e o cliente repetir a chamada com a mesma chave, o servidor devolve o recurso já criado em vez de abrir outro. A forma exata depende do domínio, mas a pergunta é sempre a mesma: "Se esta requisição chegar duas vezes, o efeito também acontece duas vezes?"

Não jogue essa decisão para o cliente por acidente. Defina quais operações são seguras para retry, quais exigem uma chave e por quanto tempo o resultado dessa chave fica armazenado.

Para operações que aguardam nova entrada, o novo ciclo também pede um desenho explícito. Requisições iniciadas pelo servidor só podem acontecer durante uma chamada ativa. Quando faltar informação, o fluxo pode retornar um InputRequiredResult; o cliente responde depois em uma nova requisição. A retomada precisa carregar dados suficientes para localizar a operação anterior.

O estado continua lá. O que muda é a forma de retomá-lo.

Compatibilidade do SDK não migra seu produto

O GitHub MCP Server é um bom exemplo do ganho operacional. Na adaptação para a nova especificação, o servidor removeu gravações no Redis durante initialize e leituras de sessão em cada chamada. Informações usadas em logs e detecção de segredos passaram a vir dos novos headers, sem precisar inspecionar profundamente o payload.

O SDK Go também mantém uma camada de compatibilidade para o fluxo de elicitation funcionar com clientes antigos e novos. Isso reduz o trabalho de quem usa o SDK.

O cuidado está em transformar esse caso em regra universal. Um wrapper consegue resolver detalhes de compatibilidade do protocolo. Ele não sabe que seu current_workspace vivia na sessão, que uma ferramenta espera um token calculado na inicialização ou que um job precisa retomar do último checkpoint.

Atualizar a dependência é o começo da migração, não a prova de que ela terminou.

Se o servidor ainda aceita clientes anteriores, escolha o ciclo de vida pela versão negociada. Versões até 2025-11-25 usam o handshake; a revisão 2026-07-28 usa os metadados por requisição. Tentar empurrar os dois comportamentos por um único caminho implícito deixa a compatibilidade difícil de observar e ainda pior de remover depois.

Teste o protocolo e o seu domínio separadamente

Uma conexão bem-sucedida não prova que a migração está correta. Ela só mostra que cliente e servidor conseguiram conversar naquele cenário.

O framework oficial de conformidade permite testar clientes e servidores de forma separada. A suíte draft, configurada para --spec-version 2026-07-28, ajuda a verificar o ciclo stateless e a distinguir regressões novas de falhas já conhecidas.

Essa é a primeira camada. A segunda pertence ao produto.

Para o exemplo do navegador, eu testaria pelo menos:

CenárioResultado esperado
outra instância recebe navigaterecupera o navegador pelo browser_id
usuário informa handle de outra contaacesso negado sem revelar dados do recurso
handle expirouerro claro e recuperável
create_browser é repetido com a mesma chavenenhum navegador extra é criado
cliente antigo inicia uma sessãocaminho compatível é selecionado pela versão
operação pede mais dadosretomada localiza a mesma operação

A conformidade cobre o protocolo. Esses testes cobrem autorização, expiração, idempotência e continuidade, que são regras da sua aplicação.

Um mapa de migração que cabe num PR

Se eu tivesse de preparar um servidor MCP próprio para essa revisão, dividiria o trabalho em passos pequenos:

  1. mapear dependências de initialize, sessão, memória local e sticky routing;
  2. listar recursos e operações que atravessam chamadas;
  3. criar handles opacos para os recursos que precisam ser retomados;
  4. validar identidade e autorização em toda operação que recebe um handle;
  5. definir idempotência para escritas e criações;
  6. separar o comportamento por versão enquanto houver clientes antigos;
  7. rodar a suíte de conformidade;
  8. adicionar testes de domínio para retry, expiração, acesso indevido e troca de instância.

O PR não precisa redesenhar todo o produto de uma vez. Comece pelo fluxo que mais depende da sessão e prove que outra instância consegue continuá-lo apenas com a requisição e os recursos referenciados.

Se isso ainda não for possível, você encontrou estado escondido. E esse é justamente o trabalho que a mudança stateless coloca à vista.

O teste que eu faria antes do merge

Remover a sessão do núcleo do MCP pode simplificar bastante a infraestrutura. O servidor ganha liberdade para rotear chamadas e trocar instâncias sem depender da memória de uma conexão específica.

Em compensação, a aplicação precisa assumir o trabalho que a sessão fazia silenciosamente. Os handles entram nos contratos, cada recurso exige verificação de acesso e as novas tentativas precisam ser idempotentes. Uma operação interrompida também precisa dizer como será retomada.

Antes de considerar a migração pronta, faça um teste simples: crie um recurso numa instância e envie a próxima chamada para outra. Se ela consegue continuar usando apenas a nova requisição, a identidade autorizada e os recursos referenciados, o estado está explícito o bastante para ser operado.

Se não consegue, o problema não é que o MCP perdeu contexto. É que sua aplicação ainda guardava contexto num lugar que o contrato não mostrava.

Notas de fonte

Carregando publicação patrocinada...