Skip to content

MCP

MCP (Model Context Protocol) é o padrão que conecta um modelo de linguagem ao mundo fora dele, aos dados que ele precisa ler e às ações que ele precisa executar, sem que cada aplicação tenha que reescrever essa cola para cada sistema. Em uma frase: ele faz pela integração entre modelos e ferramentas o que o LSP fez pela integração entre editores e linguagens. Quase toda decisão de design do protocolo decorre dessa ambição, e é por isso que vale começar pelo problema antes de falar de transporte ou de estado: sem esse contexto, as escolhas parecem arbitrárias.

Um modelo de linguagem, sozinho, é uma função de texto para texto. Ele não lê seu banco de dados, não consulta sua API interna, não abre o arquivo que você acabou de salvar. Para ser útil de verdade, ele precisa de contexto e de braços: ler dados de onde eles vivem e executar ações no mundo. O jeito óbvio de dar isso a ele é escrever código de integração: uma cola entre o modelo e cada fonte de dado ou ferramenta.

O problema é que essa cola não escala. Se você tem M aplicações que usam modelos (um assistente no editor, um chatbot interno, um agente de suporte) e N sistemas com os quais elas precisam falar (GitHub, Postgres, Slack, seu ERP), você está fadado a escrever algo próximo de M × N integrações. Cada aplicação reimplementa, do seu jeito, a conexão com cada sistema. Quando o Postgres muda algo, todo mundo que falava com Postgres conserta separadamente. É retrabalho multiplicado, e é exatamente o tipo de acoplamento que a engenharia de software passou décadas aprendendo a evitar.

O MCP ataca isso com a mesma jogada que o LSP usou para editores e linguagens. O LSP percebeu que N editores × M linguagens dava um número absurdo de plugins, e transformou isso em N + M: cada editor fala o protocolo uma vez, cada linguagem implementa um servidor uma vez, e qualquer combinação passa a funcionar. O MCP faz o mesmo para o elo entre modelos e o mundo. Você implementa um servidor MCP para o seu sistema uma única vez, e qualquer host que fale MCP (Claude Desktop, uma IDE, um agente customizado) passa a poder usá-lo sem código novo. A integração deixa de ser bespoke e vira plugável.

Esse é o valor central, e é bom não perdê-lo de vista: o MCP não te dá nenhuma capacidade que você não pudesse construir na mão. Ele te dá padronização: a capacidade de construir uma vez e reusar em todo lugar, e de consumir o que os outros construíram sem reescrever a cola.

A forma do protocolo: host, cliente, servidor

Section titled “A forma do protocolo: host, cliente, servidor”

A arquitetura tem três papéis, e a confusão entre eles é a fonte mais comum de mal-entendido. O host é a aplicação que o usuário toca e que contém o modelo: o Claude Desktop, sua IDE, seu agente. Dentro do host vivem um ou mais clientes, e cada cliente mantém uma conexão um-para-um com um servidor. O servidor é o processo que expõe capacidades: ele é quem embrulha o GitHub, o Postgres, o sistema de arquivos. A topologia fica assim, com o host abrigando um cliente por conexão e cada cliente amarrado a um único servidor:

flowchart LR
    subgraph host["Host: Claude Desktop, IDE, agente"]
        direction TB
        llm["Modelo (LLM)"]
        cli1["Cliente"]
        cli2["Cliente"]
        cli3["Cliente"]
    end
    llm --- cli1
    llm --- cli2
    llm --- cli3
    cli1 <-->|"sessão 1:1"| srv1["Servidor GitHub"]
    cli2 <-->|"sessão 1:1"| srv2["Servidor Postgres"]
    cli3 <-->|"sessão 1:1"| srv3["Servidor de arquivos"]

Por que três papéis, se cliente e servidor já dariam conta? Porque uma mesma aplicação costuma falar com vários servidores ao mesmo tempo: o Claude Desktop pode estar ligado ao servidor do GitHub, ao do Postgres e ao do sistema de arquivos de uma vez. O MCP quis que cada uma dessas ligações fosse uma sessão isolada e com estado próprio, em vez de um componente só fazendo malabarismo com todas. Foi isso que partiu o cliente clássico em dois. O cliente, aqui, encolheu até ser a unidade de uma única conexão, daí a proporção de um-para-um com o servidor; e a parte que segura o modelo, conduz a conversa e decide o que é permitido virou o host, do qual há um só, orquestrando os clientes por baixo. Dá para pensar no host como um conjunto de clientes, e em parte é, mas ele é mais que o invólucro que os abriga: é o coordenador que cria e encerra cada cliente, controla permissões e ciclo de vida de cada conexão, agrega o contexto que chega de servidores distintos e é onde a integração com o LLM de fato mora.

Essa divisão não é burocracia, é fronteira de segurança, e está entre os princípios de design declarados do protocolo:

Um servidor não deve conseguir ler a conversa inteira nem enxergar os outros servidores.

Cada cliente é um compartimento estanque. O servidor do GitHub recebe só o que o host resolve rotear para aquela conexão e não tem como observar o que o servidor do Postgres faz ao lado; o histórico completo fica com o host, e qualquer troca entre servidores passa obrigatoriamente por ele. Isolar cada conexão num cliente próprio é o que torna esse confinamento possível, porque a fronteira de segurança passa a coincidir com a fronteira do cliente.

O que o servidor expõe se divide em três primitivas, e a distinção entre elas é sobre quem controla a invocação. Tools são funções que o modelo decide chamar: buscar uma issue, rodar uma query, mandar uma mensagem; o controle é do modelo. Resources são dados que o servidor disponibiliza para serem lidos como contexto, cada um identificado por uma URI própria como file:///projeto/main.rs: o conteúdo de um arquivo, um esquema de banco, um log; o controle costuma ser da aplicação, que decide o que injetar. Prompts são templates de interação que o servidor oferece e que normalmente o usuário escolhe, pense num comando de barra. Essa separação importa porque ela mapeia direto para questões de segurança e de UX: ações que o modelo dispara sozinho (tools) merecem mais cautela e confirmação do que dados que a aplicação resolveu mostrar (resources).

Vale insistir na diferença entre tool e resource, porque é onde a confusão mais mora: um resource não é o que uma tool devolve. Ele é um dado que existe por conta própria, com endereço fixo, e que o cliente lê diretamente pedindo aquela URI (resources/read), sem invocar função nenhuma. A analogia com HTTP fecha a ideia: uma tool é como um POST, uma ação que o modelo dispara para fazer algo e que pode ter efeito colateral; um resource é como um GET, um dado que a aplicação busca por endereço, sem efeito além de ler. O que muda não é só quem puxa o gatilho, é a natureza da coisa, ação contra dado. É comum, aliás, o mesmo servidor oferecer as duas portas para o mesmo arquivo: uma tool ler_arquivo que o modelo chama quando decide, e o arquivo também como resource que o usuário anexa pela interface; a escolha entre elas é de controle, não de capacidade. E se a impressão era de que só existiam tools, ela tem fundamento: por muito tempo vários clientes MCP implementaram só essa primitiva, e ela segue sendo a mais onipresente.

Há também primitivas que correm no sentido contrário: do servidor pedindo algo ao cliente. As principais são sampling (o servidor pede ao host que rode uma inferência do modelo, útil quando a própria ferramenta precisa de um raciocínio do LLM) e elicitation (o servidor pede uma informação ao usuário no meio de uma operação). Essas duas reaparecem na discussão sobre estado: são justamente o tipo de interação que pressiona o protocolo a manter uma conexão viva.

Antes de falar de transporte vale fixar o que de fato trafega entre cliente e servidor, porque é a pergunta que costuma ficar sem resposta: o formato das mensagens. O MCP não inventou um formato próprio. Toda mensagem, em qualquer transporte, é JSON-RPC 2.0 codificado em UTF-8. JSON-RPC é um padrão antigo e deliberadamente minimalista para chamada remota de procedimento: você manda um objeto JSON dizendo qual método quer invocar e com quais parâmetros, e recebe de volta outro objeto JSON com o resultado ou um erro.

São só três formatos de mensagem, e a diferença entre eles dá conta de toda a conversa. Uma requisição carrega um id, um method e um objeto params, e exige resposta. Uma resposta repete o mesmo id e traz ou um result ou um error, nunca os dois. Uma notificação tem method e params mas não tem id: é um aviso de mão única, que por definição não recebe resposta. O id é o que costura pergunta e resposta quando várias estão em voo ao mesmo tempo, e sua ausência é o que marca uma mensagem como “não espere retorno”.

Aqui mora o esclarecimento que desfaz a confusão mais comum de quem vem de REST: o MCP não tem uma porção de endpoints, um /tools, um /resources, um /prompts. Ele tem um punhado de métodos JSON-RPC, e o nome do método viaja dentro do corpo da mensagem, não na URL. Listar as ferramentas disponíveis é o método tools/list; executar uma é tools/call; ler um recurso é resources/read; pegar um prompt é prompts/get; e toda conexão começa por um initialize, em que cliente e servidor negociam versão de protocolo e capacidades. O que num desenho REST seriam rotas diferentes, aqui são valores do campo method de um envelope que é sempre o mesmo.

Uma chamada de tool, concretamente, é assim. O cliente manda:

{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "buscar_cep",
"arguments": { "cep": "01310-100" }
}
}

E o servidor responde, repetindo o id:

{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{ "type": "text", "text": "Avenida Paulista, São Paulo, SP" }
]
}
}

É só isso que viaja. E como o formato independe de como os bytes chegam de um lado ao outro, o mesmo envelope serve igual quando o servidor é um subprocesso local conversando por stdin/stdout e quando é um serviço remoto atrás de HTTP. O transporte é a outra metade da história, e é a única coisa que muda entre esses dois mundos.

A pergunta certa não é “isso poderia ser um servidor MCP?”, porque quase tudo poderia. A pergunta é se a padronização paga seu custo no seu caso.

O MCP brilha quando a mesma integração vai ser consumida por mais de um host, ou por um host que você não controla. Se você quer que seu sistema interno seja acessível a partir do Claude Desktop, da IDE dos seus desenvolvedores e de um agente de produção, escrever isso uma vez como servidor MCP é claramente melhor do que três integrações. Ele também brilha quando você quer aproveitar o ecossistema: há servidores prontos para dezenas de sistemas comuns, e adotá-los é mais barato que reconstruir. E brilha quando a fronteira entre o modelo e a ferramenta é uma fronteira real de responsabilidade ou de segurança: o protocolo te dá um lugar limpo para colocar autenticação, permissões e auditoria.

Onde ele costuma não valer a pena é no caso de uma única aplicação que você controla de ponta a ponta, falando com uma ferramenta que só ela vai usar. Aí o MCP adiciona uma camada de indireção (um processo separado, um protocolo no meio, serialização) para resolver um problema de reuso que você não tem. Uma chamada de função direta, no mesmo processo, é mais simples, mais rápida de depurar e tem menos partes móveis. A indireção do MCP é um investimento que se paga em reuso; sem reuso à vista, é só custo. Vale notar que o limite entre “tool use” cru via API e “MCP” é exatamente esse: tool use é você definir as funções dentro da sua aplicação; MCP é você empurrar essas funções para trás de um protocolo padronizado para que outros as alcancem.

Vale também situar o MCP contra a opção mais óbvia de todas: você já tem uma API REST, por que não deixar o modelo chamá-la direto? A diferença está na natureza do consumidor. Uma API comum é um contrato entre softwares: ela pressupõe que um programador leu a documentação, entendeu a semântica e escreveu de antemão o código que chama cada endpoint do jeito certo, tratando autenticação, paginação e erros. O consumidor é código determinístico, fixado antes de rodar. O MCP foi desenhado para outro tipo de consumidor: um modelo que descobre as capacidades em tempo de execução e decide o que chamar raciocinando sobre linguagem natural. Um tools/list devolve cada ferramenta com nome, descrição e o schema dos argumentos, tudo escrito para o modelo entender e escolher, sem que ninguém tenha programado aquela ligação específica. Em uma frase: uma API é um contrato para programadores, o MCP é um contrato para modelos.

Disso decorre que o MCP em geral não substitui a sua API, ele a embrulha. A API continua lá fazendo o trabalho; o servidor MCP é a camada padronizada que a torna plugável em qualquer host que fale o protocolo, em vez de cada host escrever um cliente sob medida contra ela. E a API crua volta a ganhar exatamente nas condições já apontadas: se o único consumidor é código seu, rodando onde você controla, não há modelo descobrindo nada em tempo de execução nem vários hosts a padronizar, e a chamada direta é mais simples. Dá inclusive para apontar um modelo a uma API REST crua e deixá-lo chamá-la sem MCP nenhum; o que o protocolo acrescenta sobre isso não é poder chamar, é a curadoria (expor um punhado de operações bem descritas em vez de despejar duzentos endpoints CRUD), a portabilidade entre hosts e as primitivas que REST não tem, como assinaturas, sampling e elicitation.

Transporte: onde a questão de estado realmente mora

Section titled “Transporte: onde a questão de estado realmente mora”

A questão de stateless versus sessão se decide aqui. Não são duas APIs diferentes nem dois “tipos de MCP”: são dois modos de operar o transporte, a camada que carrega as mensagens entre cliente e servidor. Entender isso evita a confusão de achar que você está escolhendo um protocolo diferente; você está escolhendo como a conexão se comporta.

Há dois transportes principais. O stdio é para servidores locais: o host inicia o servidor como um subprocesso e conversa com ele por entrada e saída padrão. Por construção, ele é “com estado” no sentido mais trivial possível: existe um processo vivo, dedicado àquela conexão, durante toda a sessão. Não há o que decidir aqui: se o servidor roda na sua máquina como um filho do host, ele é stateful porque é um processo de verdade com memória própria. O stdio é o caminho natural para ferramentas de máquina local: acesso a arquivos, git, coisas que já vivem onde você está.

A decisão interessante aparece no transporte remoto, o Streamable HTTP, que é como servidores acessíveis pela rede se expõem. É aqui que “stateless vs. sessão” deixa de ser trivial e vira uma escolha de arquitetura.

Vale entender como esse transporte funciona no detalhe, porque o nome “Streamable HTTP” esconde uma decisão de desenho elegante. O servidor expõe um único endpoint HTTP que atende tanto POST quanto GET. A spec exige que seja um só caminho para as duas operações, mas não fixa qual: o /mcp é apenas o exemplo que a própria especificação usa (https://example.com/mcp) e que a maioria dos servidores acabou adotando, não uma regra normativa. A URL completa chega ao cliente por configuração, não por descoberta, de modo que o path pode ser o que o autor do servidor quiser. O cliente manda suas mensagens JSON-RPC por POST nesse endpoint, e em toda requisição inclui um cabeçalho Accept anunciando que aceita tanto application/json quanto text/event-stream. Esse “ou um, ou outro” é a parte esperta: para uma chamada simples, o servidor responde com um único JSON e encerra; mas se ele quiser emitir mensagens intermediárias antes da resposta final (o progresso de uma tarefa longa, uma notificação, um pedido de volta ao cliente), responde àquele mesmo POST abrindo um stream SSE, text/event-stream, e vai empurrando eventos até mandar a resposta e fechar. Uma requisição e uma resposta no caso trivial; uma requisição e vários eventos quando há o que transmitir.

Para tirar qualquer dúvida sobre o que isso é no fio: é um POST HTTP comum. A mesma chamada tools/call de antes, agora com o envelope HTTP em volta do corpo JSON-RPC, fica assim:

POST /mcp HTTP/1.1
Host: example.com
Content-Type: application/json
Accept: application/json, text/event-stream
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"buscar_cep","arguments":{"cep":"01310-100"}}}

No caso trivial, a resposta é um único JSON com o status HTTP de sempre, e a conexão se encerra ali:

HTTP/1.1 200 OK
Content-Type: application/json
{"jsonrpc":"2.0","id":7,"result":{"content":[{"type":"text","text":"Avenida Paulista, São Paulo, SP"}]}}

Se o servidor preferisse transmitir em vez de responder de uma vez, a única diferença estaria no Content-Type: text/event-stream da resposta, e o corpo viraria uma sequência de eventos SSE no lugar de um JSON único. O método (POST) e o endpoint (/mcp) seriam os mesmos: o que muda é só o formato da resposta, negociado por aquele cabeçalho Accept.

O GET no mesmo endpoint serve ao caminho inverso: abre um stream SSE em que o servidor fala com o cliente sem que este tenha perguntado nada antes, que é como notificações e pedidos iniciados pelo servidor (as tais sampling e elicitation) chegam. O Streamable HTTP fundiu num único endpoint o que antes eram dois, o que torna servidores triviais (um POST, um JSON de volta, sem stream nenhum) realmente triviais de escrever.

É sobre esse vai-e-vem que a questão do estado se instala. O cabeçalho de sessão e a retomada de stream, que decidem se o servidor lembra de você entre uma requisição e outra, são cabeçalhos HTTP que andam por cima exatamente desse desenho.

No modo com sessão, o fluxo é o seguinte. Quando o cliente faz a chamada de inicialização, o servidor pode responder incluindo um cabeçalho Mcp-Session-Id, um identificador único e criptograficamente seguro. A partir daí, o cliente é obrigado a mandar esse mesmo Mcp-Session-Id em toda requisição seguinte, e o servidor usa esse ID para reencontrar o estado daquela conexão específica: quem é o cliente, o que já foi negociado, quais assinaturas estão ativas, qual o contexto acumulado. É o equivalente, no mundo HTTP que é naturalmente sem memória, a costurar várias requisições isoladas numa conversa contínua, a mesma ideia de um cookie de sessão numa aplicação web.

No modo stateless, não há esse identificador. Cada requisição é uma ilha: chega, é atendida com tudo o que ela mesma carrega, e o servidor não guarda nada sobre ela depois. Não existe “aquele cliente” do ponto de vista do servidor: existe só uma sequência de chamadas independentes, cada uma autossuficiente.

A diferença parece sutil, mas ela determina o que o servidor consegue fazer e como ele escala, e esses dois efeitos puxam em direções opostas. É essa tensão que decide o modo certo.

Do lado das capacidades, manter sessão é o que viabiliza tudo que depende de continuidade. Assinaturas a mudanças de um recurso, o servidor te avisar quando aquele arquivo mudou, exigem que ele saiba quem é “você” para te notificar. Notificações iniciadas pelo servidor, progresso de uma tarefa longa, e a retomada de um stream que caiu no meio (reconectar via Last-Event-ID e continuar de onde parou) só fazem sentido se houver uma sessão à qual essas coisas se ancorem. As primitivas mencionadas antes (sampling e elicitation, em que o servidor inicia uma ida e volta com o cliente) também são muito mais naturais sobre uma conexão com estado. Em resumo: tudo que é conversa, e não pergunta-resposta isolada, quer sessão.

Do lado da operação, porém, sessão é um peso. Um servidor que guarda estado por cliente não pode simplesmente ser replicado atrás de um load balancer e esquecido, porque a segunda requisição daquele cliente precisa cair no mesmo lugar que tem a sua memória: ou você adota afinidade de sessão (sticky sessions), ou externaliza o estado para um Redis da vida, e ambas as saídas adicionam infraestrutura e modos de falha. Você precisa pensar em expiração de sessão, em limpeza de sessões abandonadas, em o que acontece quando o processo que segurava aquela sessão morre. Um servidor stateless não tem nada disso: como cada requisição se basta, qualquer réplica atende qualquer requisição, escalar é só adicionar instâncias, e o servidor encaixa perfeitamente no modelo serverless: uma AWS Lambda, um Cloud Run, um Cloudflare Worker que sobe, atende e morre.

A tensão inteira cabe numa comparação lado a lado:

DimensãoStatelessCom sessão
Estado no servidornenhum; cada requisição se bastapor cliente, reencontrado via Mcp-Session-Id
Escalaqualquer réplica atende; encaixa em serverlessexige sticky sessions ou estado externo (Redis)
Operaçãonada a gerenciarexpiração, limpeza de sessões órfãs, morte de processo
Capacidadesfunções puras: recebe argumentos, devolve resultadoassinaturas, notificações, resumability, sampling e elicitation
Quando adotaro default saudávelquando a continuidade é a funcionalidade

A regra prática cai naturalmente dessa tensão. Vá de stateless quando o seu servidor for, no fundo, uma coleção de funções puras: ferramentas do tipo “recebe argumentos, devolve resultado”, sem nada que precise ser lembrado entre uma chamada e outra. Uma ferramenta que consulta um CEP, que faz uma busca, que roda um cálculo, que lê um registro por ID: nenhuma delas ganha algo com sessão, e todas ganham com a simplicidade operacional e a escala horizontal trivial que o stateless oferece. Se você está pensando em rodar em serverless, ou se “escalar” para você significa só subir mais réplicas sem pensar, stateless é o default saudável.

Vá de sessão quando a continuidade for parte da funcionalidade, não um detalhe. Se o servidor precisa notificar o cliente de mudanças, manter assinaturas a recursos, conduzir interações de múltiplos passos em que o estado intermediário vive no servidor, retomar streams longos que podem cair, ou usar sampling e elicitation de forma intensa, aí o estado não é um peso que você carrega à toa, é o que torna a coisa possível. O custo operacional de gerenciar sessões passa a ser um custo justificado, porque você está comprando capacidades que o stateless simplesmente não entrega.

O caso mais comum é mais simples do que essa discussão sugere: a maioria dos servidores MCP úteis é um punhado de tools que são funções puras, e portanto a maioria deveria ser stateless. A sessão é a exceção que você adota deliberadamente quando o problema pede, não o ponto de partida.

Para onde isso está indo e por que importa hoje

Section titled “Para onde isso está indo e por que importa hoje”

Essa tensão entre capacidade e escala não termina em empate: o próprio projeto do MCP está resolvendo a favor do stateless, e quem escreve um servidor hoje deveria construir já olhando para lá.

O release candidate da especificação, datado de 2026-07-28, é descrito como a maior revisão do protocolo desde o lançamento, e o eixo dela é justamente tornar o MCP stateless por padrão. A ideia que dá nome à mudança, capturada nas propostas SEP-1442 e SEP-2575, é uma distinção que vale internalizar: a aplicação pode ser stateful sem que o protocolo precise ser. São coisas separadas, e confundi-las foi o que inflou a complexidade até aqui.

Na prática, isso significa algumas mudanças concretas que mudam como você desenha um servidor. A sessão de protocolo, com seu Mcp-Session-Id, deixa de ser o jeito de carregar estado entre chamadas. No lugar, o padrão recomendado é o servidor cunhar um handle explícito (um basket_id, um browser_id, o que for) e devolvê-lo como resultado de uma tool, para que o modelo o passe de volta como argumento comum nas chamadas seguintes. O estado continua existindo; ele só sai de uma sessão implícita e gerenciada pelo servidor e vira um identificador explícito que trafega pelos argumentos. E isso, curiosamente, costuma ser mais poderoso, não um remendo: o modelo pode compor handles de ferramentas diferentes, raciocinar sobre eles, passá-los entre passos: coisas que um estado de sessão escondido nunca permitiu, porque o modelo nem sabia que ele existia.

A retomada de streams via Last-Event-ID sai de cena pela mesma lógica: ela contradiz o paradigma stateless, já que pressupõe uma sessão à qual o stream se ancora. A queda de conexão passa a simplesmente cancelar a requisição. Para os casos que genuinamente precisam de durabilidade e de buscar um resultado depois que a conexão caiu, a resposta deixa de ser “mantenha a sessão viva” e passa a ser uma nova primitiva de tasks, com mecanismos explícitos para isso. É uma troca elegante: em vez de toda conexão pagar o custo de poder ser retomada, só os trabalhos que precisam de durabilidade a pedem explicitamente.

Com a revisão ainda em release candidate em meados de 2026, a recomendação prática é construir stateless por default sempre que o problema permitir (o que, na maioria dos casos, é possível) porque é o modo que escala melhor, é mais simples de operar, e está alinhado com a direção do protocolo. Reserve a sessão para quando precisar de verdade das capacidades que só ela dá, sabendo que parte delas (resumability de stream, sobretudo) está sendo deliberadamente movida para fora da sessão e para dentro de mecanismos explícitos como tasks. Em outras palavras: a escolha entre stateless e sessão não é só uma decisão de arquitetura para o seu caso de uso; é também uma aposta sobre para onde o protocolo está indo, e a aposta está bem sinalizada.

As afirmações sobre a spec mais volátil (o cabeçalho de sessão, a virada stateless e a primitiva de tasks) vêm da documentação e do blog oficiais do protocolo, registrados aqui porque são justamente as partes que mais provavelmente mudam de uma revisão para a outra: