CodeGym /Cursos /ChatGPT Apps /Handshake e capabilities: como o cliente descobre o que o...

Handshake e capabilities: como o cliente descobre o que o servidor consegue fazer

ChatGPT Apps
Nível 6 , Lição 2
Disponível

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:

  1. Verifica a compatibilidade de versões do protocolo.
  2. Declara quais “primitivos” do MCP o servidor suporta: tools, resources, prompts, logging, notificações etc.
  3. 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:

  1. 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.
  2. O cliente envia a requisição JSON-RPC initialize.
  3. O servidor responde com um resultado JSON-RPC contendo os campos protocolVersion, capabilities e serverInfo.
  4. O cliente envia a notificação notifications/initialized — um sinal: “li tudo, pode trabalhar”.
  5. O cliente chama os métodos de discovery (tools/list, resources/list, prompts/list) dependendo do que viu nas capabilities do servidor.
  6. O servidor devolve metadados de ferramentas/recursos/prompts.
  7. 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:

  1. Você coloca um servidor MCP em produção com um SDK que implementa o MCP versão "2025-06-18".
  2. Com o tempo sai uma nova versão do MCP, você atualiza o cliente, mas o servidor ainda é o antigo.
  3. 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:

  1. Abre o transporte (geralmente HTTP/stream em /mcp).
  2. Envia initialize com protocolVersion, capabilities e clientInfo (algo como “ChatGPT Enterprise, versão tal”).
  3. Recebe a resposta e faz cache das capabilities do servidor.
  4. Faz tools/list, resources/list, prompts/list dependendo das capabilities observadas.
  5. 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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION