1. Por que precisamos do handshake
Se endpoints REST são um conjunto de portas independentes nas quais se pode bater por URL, o MCP é mais um diálogo contínuo por um único canal. O cliente não apenas envia requisições isoladas; ele primeiro estabelece uma sessão. O handshake é o momento de apresentação no início dessa sessão.
No MCP, esse momento é implementado como uma requisição especial initialize, que o cliente envia imediatamente após estabelecer o transporte (STDIO, HTTP/stream, WebSocket — tanto faz). Na requisição, ele informa: “Falo tal versão do MCP, isto é o que eu suporto, e aqui está quem eu sou”. O servidor responde: “Eu suporto tal versão e tais capacidades, prazer em conhecer”.
Após a troca bem-sucedida, o cliente envia a notificação notifications/initialized e só então começa a “vida de trabalho”: tools/list, resources/list, tools/call e outras coisas úteis.
Fazendo uma analogia, o handshake do MCP é como um contrato de locação antes de levar servidores para o data center. Enquanto vocês não acordarem as regras (formato do protocolo, quais serviços o data center oferece, quem paga o quê) — transportar servidores não faz sentido.
Do ponto de vista prático, o handshake resolve três tarefas:
- Verifica a compatibilidade de versões do protocolo.
- Declara quais “primitivos” do MCP o servidor suporta: tools, resources, prompts, logging, notificações etc.
- Fornece metainformações sobre cliente e servidor — nome e versão da implementação.
2. Ciclo de vida da conexão MCP: onde o handshake se encaixa
Para não soar abstrato, vejamos um cenário típico (flow) de conexão, bastante simplificado:
sequenceDiagram
participant C as Cliente (ChatGPT/Inspector)
participant S as Servidor MCP
C->>S: (1) Estabelecer transporte (STDIO/HTTP-stream)
C->>S: (2) Request: "initialize"
S-->>C: (3) Result: "initialize" (capabilities, serverInfo)
C->>S: (4) Notification: "notifications/initialized"
C->>S: (5) Request: "tools/list" / "resources/list"
S-->>C: (6) Result: listas de ferramentas/recursos
C->>S: (7) Request: "tools/call" e outros
Do ponto de vista técnico, as etapas são assim:
- O transporte é estabelecido: por exemplo, o ChatGPT inicia seu servidor como subprocesso e conecta via STDIO, ou o Inspector faz uma requisição HTTP/stream para /mcp.
- O cliente envia a requisição JSON-RPC initialize.
- O servidor responde com um resultado JSON-RPC contendo os campos protocolVersion, capabilities e serverInfo.
- O cliente envia a notificação notifications/initialized — um sinal: “li tudo, pode trabalhar”.
- O cliente chama os métodos de discovery (tools/list, resources/list, prompts/list) dependendo do que viu nas capabilities do servidor.
- O servidor devolve metadados de ferramentas/recursos/prompts.
- Depois vêm as requisições “de trabalho”: tools/call, resources/read e outras.
É importante notar que o handshake é apenas uma chamada JSON-RPC comum initialize. Sem mágica. Após a aula sobre o formato das mensagens MCP, você já sabe analisar tais requisições; a única diferença é que aqui o método é sempre único e “especial”, e é executado primeiro.
3. O que o cliente envia em initialize
Vamos decompor a requisição initialize. Aproximadamente assim pode parecer uma requisição mínima (simplificada para a aula):
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"elicitation": {}
},
"clientInfo": {
"name": "chatgpt-gift-client",
"version": "2.3.0"
}
}
}
Este exemplo é próximo ao que está na documentação oficial do MCP. Os principais campos em params:
protocolVersion
Uma string com a versão da especificação MCP, geralmente no formato de data, por exemplo "2025-06-18". Não é a versão do seu aplicativo, mas a do próprio protocolo. O cliente diz: “espero falar esta versão do MCP”. O servidor deve confirmar essa versão na resposta ou retornar um erro se não a conhecer.
Isso protege contra a situação “o cliente pensa uma coisa, o servidor implementa outra”. Se não houver uma versão comum, é melhor encerrar a conexão do que trocar mensagens incompatíveis.
capabilities do cliente
Um objeto no qual o cliente declara quais capacidades do MCP ele próprio suporta. Por exemplo, o cliente do ChatGPT frequentemente inclui a chave elicitation, sinalizando que pode lidar com solicitações ao usuário (entrada adicional, confirmações etc.).
Exemplo:
"capabilities": {
"elicitation": {},
"sampling": {}
}
O servidor pode usar essa informação para entender quais capacidades estendidas do protocolo fazem sentido utilizar. Por exemplo, elicitation significa que o cliente (ChatGPT) pode fazer perguntas de esclarecimento ao usuário e solicitar dados adicionais.
clientInfo
Metainformação simples: nome e versão do cliente.
"clientInfo": {
"name": "ChatGPT",
"version": "2.0.0"
}
Do ponto de vista do desenvolvedor do servidor, isso é ouro para os logs: você sempre pode ver qual cliente se conectou — ChatGPT, MCP Inspector, seu próprio cliente de testes — e qual é a versão dele.
4. O que o servidor responde: initialize result
A resposta a initialize é um resultado JSON-RPC comum com o mesmo id, mas no campo result vem a descrição do que o servidor sabe fazer.
Na requisição vimos capabilities pelo lado do cliente — o que ele próprio suporta. Agora vamos ver o objeto espelhado na resposta: as capabilities do servidor, isto é, o que ele suporta. Esquematicamente:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {},
"prompts": {},
"logging": {}
},
"serverInfo": {
"name": "gift-genius-backend",
"version": "0.1.0"
}
}
}
Uma estrutura semelhante aparece na descrição oficial do protocolo e/ou na documentação do SDK. As partes principais:
protocolVersion na resposta
O servidor repete a versão proposta pelo cliente ou (teoricamente) poderia escolher outra versão comum, se houver várias. Nas implementações típicas, a versão do cliente é apenas confirmada, se o servidor a suportar. Caso contrário — o servidor deve retornar um erro e encerrar a conversa.
serverInfo
Metainformação sobre o servidor: nome, versão.
"serverInfo": {
"name": "gift-genius-backend",
"version": "0.1.0"
}
Parece entediante, mas são justamente esses dados que você vai filtrar e procurar nos logs depois: “por que o ChatGPT com a versão X não entra em acordo com nosso servidor na versão Y”.
capabilities do servidor
O campo mais interessante. Aqui o servidor declara quais primitivos e extensões MCP ele suporta: se pode lidar com tools/*, resources/*, prompts/*, se consegue enviar notificações sobre mudanças nas listas etc.
Se em capabilities não houver a seção tools, nenhum cliente corretamente implementado chamará tools/list ou tools/call. Da mesma forma, a ausência de resources significa que o cliente não enviará resources/list e resources/read.
Assim, capabilities são um contrato leve: “o que pode e o que não pode ser feito neste servidor”.
5. Capabilities como “lista de superpoderes”
Daqui em diante nos interessam apenas as capabilities do servidor — o objeto que vem na resposta a initialize e define quais primitivos MCP esse servidor de fato suporta.
Vamos olhar mais de perto sua estrutura. Exemplo (simplificado, mas próximo da especificação):
{
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
},
"prompts": {
"listChanged": false
},
"logging": {}
}
Esse exemplo é tratado na arquitetura oficial do MCP. Vamos decodificar por seção.
Capabilities.tools
A presença da chave tools indica: o servidor consegue responder aos métodos tools/list e tools/call. Se houver ainda o flag listChanged: true, isso significa que o servidor pode enviar no futuro notificações tools/list_changed quando o conjunto de ferramentas mudar.
Para o ChatGPT, isso é útil: é possível fazer cache da lista de ferramentas e, ao receber list_changed, atualizá-la sem reconectar completamente.
Capabilities.resources
A seção resources declara que o servidor suporta trabalhar com recursos: resources/list, resources/read e, às vezes, busca. Flags internos:
- subscribe: true — o cliente pode assinar mudanças em recursos (por exemplo, para live logs ou atualizações de arquivos).
- listChanged: true — o servidor pode enviar a notificação resources/list_changed se recursos forem adicionados ou removidos.
Isso é especialmente importante para diretórios grandes ou dados “vivos” que mudam constantemente.
Capabilities.prompts
Se o servidor registra prompts predefinidos (por exemplo, modelos de chamadas à modelo, vinculados ao seu domínio), então em capabilities aparece a chave prompts. Ali também pode haver o flag listChanged.
Ao ver essa seção, o cliente entende que o método prompts/list está disponível e possivelmente prompts/get.
Capabilities.logging e outros
Algumas implementações de servidores também declaram logging — isso significa que o servidor pode enviar logs estruturados ao cliente via MCP, por exemplo, para depuração.
Outras seções podem aparecer (por exemplo, sampling ou extensões específicas). Importante: o protocolo foi projetado desde o início para ser extensível — você pode adicionar novas chaves em capabilities, e clientes antigos simplesmente irão ignorá-las se não as conhecerem.
Insight
Foi constatado experimentalmente que o ChatGPT App ignora mensagens listChanged que lhe são enviadas. No momento, ao escrever um aplicativo, você não pode declarar um conjunto de tools e depois adicionar ou remover mais tools, embora o protocolo MCP permita isso.
No momento da escrita deste curso, a situação é: no momento do registro do seu aplicativo na ChatGPT Store, o ChatGPT solicita ao seu aplicativo a lista de tools e resources e a mantém em cache para sempre. A probabilidade de que a situação mude ao longo de 2026 é alta; a probabilidade de que mude no primeiro trimestre de 2026 é baixa.
6. Discovery após o handshake: como obter a lista de ferramentas e recursos
O handshake responde à pergunta “o que o servidor sabe fazer”. O passo seguinte é o chamado discovery: o cliente, por métodos específicos, extrai detalhes — quais ferramentas existem, quais recursos estão disponíveis, quais prompts estão embutidos.
Para isso, usam-se os métodos de discovery: em linhas gerais tools/list, resources/list, prompts/list. Na documentação da arquitetura MCP, a recomendação é apresentar assim: handshake → discovery → chamadas de ferramentas.
Exemplo de requisição tools/list:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
A resposta do servidor contém um array de ferramentas: nomes, descrições, JSON Schema dos argumentos e às vezes metadados, como categorias ou ícones.
Depois disso, o ChatGPT (ou outro cliente) faz cache da lista e, durante o diálogo, usa-a para:
- selecionar a ferramenta adequada para a tarefa do usuário;
- verificar se o nome da ferramenta existe;
- validar argumentos antes de enviar tools/call.
Com recursos, a história é semelhante, mas resources/list frequentemente suporta paginação por cursores, para não trazer de uma vez um milhão de registros. Isso também está descrito na especificação do MCP e é tratado como um caso típico para diretórios grandes.
7. Handshake e capabilities no exemplo do nosso aplicativo GiftGen
Nos módulos anteriores, construímos um aplicativo didático que ajuda a escolher presentes. Já temos um widget, a ferramenta suggest_gifts no backend e algum catálogo de presentes. Agora vamos imaginar como é o handshake para o servidor MCP gift-genius.
Exemplo de handshake para o GiftGen
Requisição do cliente:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"elicitation": {}
},
"clientInfo": {
"name": "ChatGPT",
"version": "2.1.0"
}
}
}
Resposta do nosso servidor:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "listChanged": true },
"prompts": {},
"logging": {}
},
"serverInfo": {
"name": "gift-genius-backend",
"version": "0.2.0"
}
}
}
No fundo, estamos quase repetindo os exemplos da arquitetura oficial do MCP, apenas adaptando nomes ao nosso aplicativo.
O que o cliente aprende com essa resposta:
- Há ferramentas (tools) e a lista pode mudar dinamicamente (listChanged: true).
- Há recursos (nosso catálogo de presentes, possivelmente armazenado em arquivos ou banco de dados).
- Há prompts (por exemplo, um modelo “Formule uma descrição curta do presente para o usuário N”).
- O servidor pode enviar logs (útil para inspectors e depuração).
Em seguida, o cliente faz tools/list e vê ali, por exemplo, esta ferramenta:
{
"name": "suggest_gifts",
"description": "Sugere ideias de presentes com base no perfil do destinatário.",
"inputSchema": {
"type": "object",
"properties": {
"age": { "type": "integer" },
"relationship": { "type": "string" },
"budget": { "type": "number" }
},
"required": ["age", "relationship"]
}
}
E agora, quando o usuário escreve algo como: “Sugira um presente para minha irmã, 25 anos, orçamento de até 50 dólares”, o modelo já sabe: existe a ferramenta suggest_gifts com tal conjunto de argumentos, e é possível chamá-la via tools/call.
8. Como o SDK esconde o handshake (mas por que ainda é importante entendê-lo)
No SDK de TypeScript para MCP (aquele que usaremos na próxima aula), toda essa história de initialize e notifications/initialized fica encapsulada no método connect. Código aproximado:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "gift-genius",
version: "1.0.0",
});
// Registro da ferramenta — o SDK, com base nisso, configurará capabilities.tools por conta própria
server.tool(
"suggest_gifts",
{
description: "Sugere ideias de presentes.",
inputSchema: {
type: "object",
properties: {
age: { type: "integer" },
relationship: { type: "string" },
budget: { type: "number" },
},
required: ["age", "relationship"],
},
},
async (input) => {
// ... lógica de seleção de presentes ...
return { suggestions: [] };
},
);
const transport = new StdioServerTransport();
// Aqui o SDK:
// 1) recebe initialize do cliente,
// 2) responde com serverInfo e capabilities,
// 3) aguarda notifications/initialized,
// 4) em seguida começa a processar chamadas tools/*.
await server.connect(transport);
O SDK compila automaticamente as capabilities com base no que você registrou: se houver pelo menos um server.tool(...), ele adicionará a seção tools às capabilities. Se você registrar resources ou prompts, aparecerão resources e prompts.
Entender o handshake e as capabilities não é para escrever JSON na mão (nunca faça isso), mas para:
- ler os logs MCP e entender por que o cliente “não enxerga” suas ferramentas;
- diagnosticar incompatibilidades de versão do protocolo;
- implementar um servidor customizado ou um transporte não padrão, se necessário.
9. Versões do protocolo e evolução de capacidades
O campo protocolVersion no handshake não é decoração. A especificação do MCP enfatiza: é a forma de acordar uma versão compatível do protocolo; se não houver versão comum, a conexão deve ser encerrada.
Cenário típico:
- Você coloca um servidor MCP em produção com um SDK que implementa o MCP versão "2025-06-18".
- Com o tempo sai uma nova versão do MCP, você atualiza o cliente, mas o servidor ainda é o antigo.
- O cliente envia protocolVersion: "2026-02-01", o servidor não conhece essa versão e retorna um erro invalid protocol version (ou similar).
A prática mostra: desenvolvedores frequentemente ignoram esse campo e depois se surpreendem por que a conexão não é estabelecida.
Postura correta em relação às versões:
- Saber sempre qual versão do MCP seu SDK suporta (geralmente na documentação/notas de release).
- Ao atualizar o SDK — atualizar conscientemente a versão do protocolo.
- Logs e monitoramento devem expor claramente erros de inicialização por incompatibilidade de protocolVersion.
A ampliação de capacidades via capabilities também está ligada à evolução: novas funções do MCP são adicionadas como novas chaves em capabilities. Clientes antigos as ignoram, e os novos podem usá-las. Esse padrão é descrito na documentação oficial do MCP como forma de manter compatibilidade retroativa.
10. O handshake pelos olhos do ChatGPT e do Inspector
O que o ChatGPT faz ao se conectar ao MCP
Quando você, no Dev Mode, vincula um servidor MCP ao ChatGPT, a plataforma, por trás dos panos, faz aproximadamente o seguinte:
- Abre o transporte (geralmente HTTP/stream em /mcp).
- Envia initialize com protocolVersion, capabilities e clientInfo (algo como “ChatGPT Enterprise, versão tal”).
- Recebe a resposta e faz cache das capabilities do servidor.
- Faz tools/list, resources/list, prompts/list dependendo das capabilities observadas.
- Durante o diálogo, quando o modelo decide chamar uma ferramenta, consulta esse cache: se a tool existe, qual é seu schema de argumentos e como formatar a chamada.
Se as capabilities do servidor não contêm tools, o ChatGPT nem tentará oferecer seu App como ferramenta. Se as capabilities incluem resources, mas nelas não há o flag listChanged, o ChatGPT pode manter a lista de recursos em cache e não esperar notificações de alterações.
Como o Inspector e o MCP Jam ajudam na depuração
Ferramentas como MCP Jam / MCP Inspector fazem praticamente a mesma coisa: estabelecem a conexão, executam o handshake, mostram as capabilities do servidor e permitem chamar manualmente tools/list, tools/call e outros.
Do ponto de vista do desenvolvedor, são indispensáveis:
- dá para ver qual protocolVersion o servidor realmente retornou;
- fica claro, nas capabilities, se há tools, resources, prompts;
- é possível entender por que o ChatGPT não vê as ferramentas (capabilities não declaradas ou handshake não concluído).
Na última aula deste módulo, você usará essas ferramentas de forma mais intensa, mas já agora é útil entender que elas operam exatamente sobre o handshake que estamos analisando.
11. Erros comuns ao trabalhar com handshake e capabilities
Na teoria tudo parece bem direto, mas, na prática, é justamente o handshake e a declaração de capabilities que mais frequentemente viram fonte de bugs bem básicos — especialmente no Dev Mode ou no MCP Inspector. A seguir, alguns erros comuns com os quais você quase certamente vai se deparar no seu código ou nos logs de colegas.
Erro nº 1: Formato incorreto da requisição initialize.
Um problema muito comum ao implementar um servidor MCP manualmente, sem SDK, é perder algum campo obrigatório do JSON-RPC. Por exemplo, esquecer jsonrpc: "2.0", confundir o method (escrever "init" em vez de "initialize") ou tornar capabilities um valor booleano em vez de um objeto. A especificação do MCP espera um formato estrito; qualquer desvio leva a erros de parsing e ao encerramento da conexão. A documentação e os guias práticos recomendam primeiro garantir que initialize está estritamente conforme a especificação antes de olhar para qualquer outra coisa.
Erro nº 2: Ignorar o protocolVersion.
Às vezes, desenvolvedores apenas copiam o exemplo da documentação e colocam ali uma string arbitrária, sem verificar o suporte no SDK. O resultado é que cliente e servidor falam versões diferentes do MCP, e a conexão não se estabelece. O erro pode se mascarar como “o cliente nem conecta”. Trate protocolVersion como um contrato real: essa versão precisa ser acordada entre a equipe da plataforma/agente e a equipe que escreve o servidor MCP.
Erro nº 3: Capabilities esquecidas.
Situação clássica: você registrou uma ferramenta no servidor, mas, ao implementar o handshake manualmente, esqueceu de adicionar "tools": {} em capabilities da resposta initialize. No inspector você vê que as ferramentas existem, mas o ChatGPT mostra “No tools available” — porque ele confia nas capabilities e não faz tools/list se a seção tools não estiver presente. Os guias de solução de problemas do Apps SDK enfatizam: se o ChatGPT não vê as ferramentas, verifique as capabilities primeiro.
Erro nº 4: Tentar usar métodos não declarados em capabilities.
Às vezes estudantes experimentam e, por exemplo, enviam resources/list para um servidor cujas capabilities não têm a seção resources. Formalmente, o servidor pode responder Method not found, mas o correto é nem chamar tais métodos. O MCP introduz capabilities justamente como proteção contra essas tentativas. O cliente deve primeiro verificar se há a seção correspondente em capabilities e só então chamar os métodos.
Erro nº 5: O servidor começa a “falar” antes de notifications/initialized.
Se o servidor, logo após enviar a resposta a initialize, começa a enviar logs ou notificações ao cliente, sem aguardar notifications/initialized, alguns clientes podem ignorar essas mensagens ou até encerrar a conexão. A arquitetura oficial do MCP enfatiza que o handshake deve se concluir antes e somente após a notificação de inicialização começa a “vida de trabalho”.
Erro nº 6: Alterar o schema das ferramentas sem sinalizar mudança na lista.
Quando você muda o JSON Schema de uma ferramenta (torna um campo obrigatório, renomeia um argumento), mas não reinicia o servidor ou não envia uma notificação de que a lista de ferramentas mudou, o cache do cliente pode conter a versão antiga do schema. Isso leva a erros de validação estranhos. A especificação sugere usar o flag listChanged e as notificações tools/list_changed e resources/list_changed para ajudar o cliente a atualizar o cache oportunamente.
Erro nº 7: Otimização prematura e “mágica” em torno de capabilities.
Às vezes desenvolvedores inventam esquemas complexos com geração dinâmica de capabilities, versionamento por cliente e outras excentricidades, sem dominar os mecanismos básicos. No começo, basta declarar honestamente o que o servidor sabe fazer: tools, resources, prompts, logging. Amplie as capabilities conforme a necessidade real, e não “para o futuro”. É mais um antipadrão organizacional do que um erro puramente de protocolo, mas aparece muito em projetos de produção.
GO TO FULL VERSION