CodeGym /Cours /ChatGPT Apps /Handshake et capabilities : comment le client découv...

Handshake et capabilities : comment le client découvre ce que sait faire le serveur

ChatGPT Apps
Niveau 6 , Leçon 2
Disponible

1. Pourquoi a‑t‑on besoin du handshake

Si des endpoints REST sont un ensemble de portes distinctes auxquelles on frappe via des URL, MCP ressemble plutôt à un dialogue permanent sur un seul canal. Le client n’envoie pas seulement des requêtes isolées : il commence par établir une session. Le handshake est le moment de la prise de contact au début de cette session.

Dans MCP, ce moment est implémenté comme une requête spéciale initialize que le client envoie juste après l’établissement du transport (STDIO, HTTP/stream, WebSocket — peu importe). Dans la requête, il indique : « Je parle telle version de MCP, voici ce que je sais faire, et voici qui je suis. » Le serveur répond : « Je supporte telle version et telles fonctionnalités, ravi de faire votre connaissance. »

Après un échange réussi, le client envoie la notification notifications/initialized et seulement après cela commence la vie opérationnelle : tools/list, resources/list, tools/call et autres fonctionnalités utiles.

Par analogie, le handshake MCP — c’est comme un contrat de location avant d’amener un serveur dans un datacenter. Tant que vous ne vous êtes pas mis d’accord sur les règles (format du protocole, services fournis par le datacenter, qui paie quoi), amener des serveurs n’a aucun sens.

Sur le plan pratique, le handshake couvre trois objectifs :

  1. Vérifier la compatibilité des versions du protocole.
  2. Déclarer quels « primitives » MCP le serveur supporte : tools, resources, prompts, journalisation, notifications, etc.
  3. Fournir des métadonnées sur le client et le serveur — nom et version de l’implémentation.

2. Cycle de vie d’une connexion MCP : où vit le handshake

Pour rendre le tableau moins abstrait, regardons un scénario typique (flow) de connexion, fortement simplifié :

sequenceDiagram
    participant C as Client (ChatGPT/Inspector)
    participant S as Serveur MCP

    C->>S: (1) Établir le transport (STDIO/HTTP-stream)
    C->>S: (2) Requête: "initialize"
    S-->>C: (3) Résultat: "initialize" (capabilities, serverInfo)
    C->>S: (4) Notification: "notifications/initialized"
    C->>S: (5) Requête: "tools/list" / "resources/list"
    S-->>C: (6) Résultat: listes des outils/ressources
    C->>S: (7) Requête: "tools/call", etc.

Techniquement, les étapes ressemblent à ceci :

  1. Le transport est établi : par exemple, ChatGPT lance votre serveur comme sous‑processus et se connecte via STDIO, ou bien Inspector effectue une requête HTTP/stream vers /mcp.
  2. Le client envoie une requête JSON-RPC initialize.
  3. Le serveur répond avec un résultat JSON-RPC contenant les champs protocolVersion, capabilities et serverInfo.
  4. Le client envoie la notification notifications/initialized — signal : « j’ai tout lu, on peut travailler ».
  5. Le client appelle les méthodes de découverte (tools/list, resources/list, prompts/list) en fonction de ce qu’il a vu dans les capabilities du serveur.
  6. Le serveur renvoie les métadonnées des outils/ressources/prompts.
  7. Ensuite viennent les requêtes « opérationnelles » : tools/call, resources/read, etc.

Il est important de noter que le handshake n’est rien d’autre qu’un appel JSON-RPC ordinaire initialize. Pas de magie. Après le cours sur le format des messages MCP, vous savez déjà analyser ce type de requêtes ; la seule différence ici — c’est que la méthode est unique et « spéciale », et qu’elle s’exécute en premier.

3. Ce que le client envoie dans initialize

Analysons la requête initialize par parties. Voici à quoi peut ressembler une requête minimale (simplifiée pour le cours) :

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "chatgpt-gift-client",
      "version": "2.3.0"
    }
  }
}

Cet exemple est proche de ce qui est montré dans la documentation officielle de MCP. Les champs principaux de params :

protocolVersion

Une chaîne avec la version de la spécification MCP, le plus souvent au format date, par exemple "2025-06-18". Ce n’est pas la version de votre application, mais celle du protocole lui‑même. Le client dit : « j’attends de parler cette version de MCP ». Le serveur doit soit la confirmer dans sa réponse, soit renvoyer une erreur s’il ne connaît pas cette version.

C’est une protection contre la situation « le client pense une chose, le serveur en implémente une autre ». Si aucune version commune n’est trouvée, il vaut mieux couper honnêtement la connexion que d’échanger des messages incompatibles.

capabilities côté client

Un objet dans lequel le client déclare quelles capacités MCP il supporte lui‑même. Par exemple, le client ChatGPT indique souvent la clé elicitation, signalant qu’il peut gérer des demandes à l’utilisateur (saisie supplémentaire, confirmations, etc.).

Exemple :

"capabilities": {
  "elicitation": {},
  "sampling": {}
}

Le serveur peut utiliser cette information pour savoir quelles fonctionnalités étendues du protocole il est pertinent d’employer. Par exemple, elicitation signifie que le client (ChatGPT) peut poser des questions de clarification à l’utilisateur et demander des données supplémentaires.

clientInfo

De simples métadonnées : nom et version du client.

"clientInfo": {
  "name": "ChatGPT",
  "version": "2.0.0"
}

Du point de vue du développeur du serveur, c’est de l’or pour les logs : vous pouvez toujours voir quel client est connecté — ChatGPT, MCP Inspector, votre propre client de test — et son numéro de version.

4. Ce que renvoie le serveur : le résultat d’initialize

La réponse à initialize est un résultat JSON-RPC ordinaire avec le même id, mais le champ result contient la description de ce que le serveur sait faire.

Dans la requête, nous avons vu les capabilities côté client — ce qu’il supporte lui‑même. Voyons maintenant l’objet miroir dans la réponse : les capabilities du serveur, c’est‑à‑dire ce qu’il supporte. Schématiquement :

{
  "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"
    }
  }
}

Vous verrez une structure similaire dans la description officielle du protocole et/ou dans la description du SDK. Les parties principales :

protocolVersion dans la réponse

Le serveur répète soit la version proposée par le client, soit (théoriquement) pourrait choisir une autre version commune s’il y en a plusieurs. Dans les implémentations typiques, on se contente de confirmer la version du client si le serveur la supporte. Sinon — le serveur doit renvoyer une erreur et interrompre la communication.

serverInfo

Métadonnées sur le serveur : nom, version.

"serverInfo": {
  "name": "gift-genius-backend",
  "version": "0.1.0"
}

Cela a l’air ennuyeux, mais c’est précisément avec ces données que vous filtrerez et rechercherez ensuite dans les logs : « pourquoi ChatGPT en version X ne s’entend‑il pas avec notre serveur en version Y ».

capabilities du serveur

Le champ le plus intéressant. Ici, le serveur déclare quelles primitives MCP et extensions il supporte : peut‑il gérer tools/*, resources/*, prompts/*, sait‑il envoyer des notifications de changement de listes, etc.

Si la section tools est absente des capabilities, aucun client correctement implémenté n’appellera tools/list ni tools/call. De la même manière, l’absence de resources signifie que le client n’enverra pas resources/list ni resources/read.

Ainsi, capabilities est un léger contrat : « ce qu’il est possible ou non de faire avec ce serveur ».

5. Capabilities comme « liste de super‑pouvoirs »

À partir de là, seul nous intéresse l’objet capabilities du serveur — celui qui arrive en réponse à initialize et qui détermine quelles primitives MCP ce serveur supporte.

Regardons sa structure plus en détail. Exemple (simplifié, mais proche de la spécification) :

 {
"capabilities": {
  "tools": {
    "listChanged": true
  },
  "resources": {
    "subscribe": true,
    "listChanged": true
  },
  "prompts": {
    "listChanged": false
  },
  "logging": {}
}

Un tel exemple est analysé dans l’architecture officielle de MCP. Déchiffrons par section.

Capabilities.tools

La présence de la clé tools indique : le serveur sait répondre aux méthodes tools/list et tools/call. S’il y a en plus le drapeau listChanged : true, cela signifie que le serveur pourra à l’avenir envoyer des notifications tools/list_changed lorsque l’ensemble des outils change.

Pour ChatGPT, c’est utile : on peut mettre en cache la liste des outils, et lors de la réception de list_changed, la mettre à jour sans reconnection complète.

Capabilities.resources

La section resources déclare que le serveur supporte la gestion des ressources : resources/list, resources/read, parfois la recherche. Drapeaux internes :

  • subscribe : true — le client peut s’abonner aux changements des ressources (par exemple pour des journaux en direct ou des mises à jour de fichiers).
  • listChanged : true — le serveur peut envoyer la notification resources/list_changed si des ressources ont été ajoutées ou supprimées.

C’est particulièrement important pour de grands catalogues ou des données « vivantes » qui changent en permanence.

Capabilities.prompts

Si le serveur enregistre des prompts prédéfinis (par exemple des gabarits d’appels au modèle, propres à votre domaine), alors la clé prompts apparaît dans capabilities. On peut y trouver aussi le drapeau listChanged.

Le client, voyant cette section, comprend que la méthode prompts/list est disponible, et éventuellement prompts/get.

Capabilities.logging et autres

Certaines implémentations de serveurs annoncent aussi logging — cela signifie que le serveur peut envoyer au client des logs structurés via MCP, par exemple pour le debug.

D’autres sections peuvent également apparaître (par exemple, sampling ou des extensions spécifiques). L’important est que le protocole a été conçu dès le départ pour être extensible : vous pouvez ajouter de nouvelles clés dans capabilities, et les anciens clients les ignoreront simplement s’ils ne les connaissent pas.

Insight

Il a été établi expérimentalement que ChatGPT App ignore les messages listChanged qui lui sont envoyés. À l’heure actuelle, lors de l’écriture d’une application vous ne pouvez pas déclarer un ensemble d’outils puis en ajouter ou en retirer dynamiquement, même si le protocole MCP le permet.

Au moment de la rédaction de ce cours, la situation est la suivante : au moment de l’enregistrement de votre application dans le ChatGPT Store, ChatGPT demande à votre application la liste des tools et des resources et la met en cache indéfiniment. La probabilité que la situation change au cours de 2026 est élevée ; la probabilité que cela change au premier trimestre 2026 est faible.

6. Découverte (discovery) après le handshake : comment obtenir la liste des outils et des ressources

Le handshake répond à la question « que sait faire le serveur en général ». L’étape suivante — la découverte (discovery) — consiste pour le client à extraire les détails via des méthodes concrètes : quels outils existent, quelles ressources sont disponibles, quels prompts sont intégrés.

Pour cela, on utilise des méthodes de découverte : typiquement tools/list, resources/list, prompts/list. Dans la documentation de l’architecture MCP, on recommande d’expliquer ainsi : handshake → discovery → appels d’outils.

Exemple de requête tools/list :

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}

La réponse du serveur contient un tableau d’outils : noms, descriptions, JSON Schema des arguments et parfois des métadonnées comme des catégories ou des icônes.

Ensuite, ChatGPT (ou un autre client) met cette liste en cache et, pendant le dialogue, l’utilise pour :

  • choisir l’outil adapté à la tâche de l’utilisateur ;
  • vérifier que le nom de l’outil existe ;
  • valider les arguments avant d’envoyer tools/call.

Avec les ressources, c’est une histoire similaire, sauf que resources/list supporte souvent la pagination via des curseurs, pour éviter de rapatrier d’emblée un million d’entrées. Cela aussi est décrit dans la spécification MCP et constitue un cas typique pour les grands catalogues.

7. Handshake et capabilities à l’exemple de notre application GiftGen

Dans les modules précédents, nous avons construit une application pédagogique qui aide à choisir des cadeaux. Nous avons déjà un widget, un outil suggest_gifts côté backend, et un certain catalogue de cadeaux. Imaginons maintenant à quoi ressemble le handshake pour le serveur MCP gift-genius.

Exemple de handshake pour GiftGen

Requête du client :

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "ChatGPT",
      "version": "2.1.0"
    }
  }
}

Réponse de notre serveur :

{
  "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"
    }
  }
}

En substance, nous répétons presque les exemples de l’architecture officielle MCP, en adaptant simplement les noms à notre application.

Ce que le client apprend de cette réponse :

  • Il y a des outils (tools), et la liste peut changer dynamiquement (listChanged : true).
  • Il y a des ressources (notre catalogue de cadeaux, possiblement stocké dans des fichiers ou une base de données).
  • Il y a des prompts (par exemple un gabarit « Formule une brève description du cadeau pour l’utilisateur N »).
  • Le serveur peut envoyer des logs (pratique pour les inspecteurs et le debug).

Ensuite, le client fait tools/list et y voit, par exemple, l’outil suivant :

{
  "name": "suggest_gifts",
  "description": "Propose des idées de cadeaux selon le profil du destinataire.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "age": { "type": "integer" },
      "relationship": { "type": "string" },
      "budget": { "type": "number" }
    },
    "required": ["age", "relationship"]
  }
}

Et maintenant, quand l’utilisateur écrit quelque chose comme : « Suggère un cadeau pour ma sœur, 25 ans, budget jusqu’à 50 dollars », le modèle sait déjà qu’il existe un outil suggest_gifts avec un certain ensemble d’arguments, et qu’on peut l’appeler via tools/call.

8. Comment le SDK masque le handshake (et pourquoi il reste important de le comprendre)

Dans le SDK TypeScript pour MCP (celui que nous utiliserons dans la prochaine leçon), toute cette histoire avec initialize et notifications/initialized est encapsulée dans la méthode connect. Code approximatif :

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",
});

// Enregistrement de l’outil — le SDK configurera capabilities.tools en conséquence
server.tool(
  "suggest_gifts",
  {
    description: "Propose des idées de cadeaux.",
    inputSchema: {
      type: "object",
      properties: {
        age: { type: "integer" },
        relationship: { type: "string" },
        budget: { type: "number" },
      },
      required: ["age", "relationship"],
    },
  },
  async (input) => {
    // ... logique de sélection de cadeaux ...
    return { suggestions: [] };
  },
);

const transport = new StdioServerTransport();

// Ici, le SDK :
// 1) reçoit initialize du client,
// 2) répond avec serverInfo et capabilities,
// 3) attend notifications/initialized,
// 4) puis commence à traiter les appels tools/*.
await server.connect(transport);

Le SDK compose automatiquement les capabilities en fonction de ce que vous avez enregistré : s’il y a au moins un server.tool(...), il ajoutera la section tools aux capabilities. Si vous enregistrez des ressources ou des prompts, resources et prompts apparaîtront.

Comprendre le handshake et les capabilities n’est pas nécessaire pour écrire du JSON à la main (ne faites jamais ça), mais pour :

  • lire les logs MCP et comprendre pourquoi le client « ne voit pas » vos outils ;
  • diagnostiquer une incompatibilité de versions du protocole ;
  • implémenter si besoin un serveur personnalisé ou un transport non standard.

9. Versions du protocole et évolution des fonctionnalités

Le champ protocolVersion dans le handshake n’est pas décoratif. La spécification MCP souligne explicitement que c’est le moyen de convenir d’une version compatible du protocole ; si aucune version commune n’est trouvée, il vaut mieux terminer la connexion.

Scénario typique :

  1. Vous déployez un serveur MCP en production avec un SDK qui implémente MCP en version "2025-06-18".
  2. Avec le temps, une nouvelle version MCP sort, vous mettez le client à jour, mais le serveur reste ancien.
  3. Le client envoie protocolVersion : "2026-02-01", le serveur ne connaît pas cette version et renvoie une erreur invalid protocol version (ou similaire).

Dans la pratique, les développeurs ignorent souvent ce champ et s’étonnent ensuite que la connexion ne s’établisse pas.

La bonne approche des versions :

  • Toujours savoir quelle version de MCP est supportée par votre SDK (généralement dans la doc/les notes de version).
  • Lors de la mise à jour du SDK — mettre à jour consciemment la version du protocole.
  • Les logs et le monitoring doivent mettre en évidence les erreurs d’initialisation dues à un décalage de protocolVersion.

L’extension des fonctionnalités via les capabilities est aussi liée à l’évolution : de nouvelles fonctions MCP sont ajoutées comme de nouvelles clés dans capabilities. Les anciens clients les ignorent, les nouveaux peuvent les utiliser. Ce pattern est précisément décrit dans la documentation officielle MCP comme moyen d’assurer la compatibilité ascendante.

10. Le handshake vu par ChatGPT et par les inspecteurs

Ce que fait ChatGPT lors de la connexion MCP

Quand vous rattachez un serveur MCP à ChatGPT en mode Dev, la plateforme fait en coulisses à peu près ceci :

  1. Ouvre le transport (généralement HTTP/stream sur /mcp).
  2. Envoie initialize avec protocolVersion, capabilities et clientInfo (quelque chose comme « ChatGPT Enterprise, telle version »).
  3. Reçoit la réponse et met en cache les capabilities du serveur.
  4. Fait tools/list, resources/list, prompts/list en fonction des capabilities observées.
  5. Pendant le dialogue, lorsque le modèle décide d’appeler un outil, il se réfère à ce cache : l’outil existe‑t‑il, quel est son schéma d’arguments, et comment formater l’appel.

Si les capabilities du serveur ne contiennent pas tools, ChatGPT n’essaiera même pas de proposer votre App comme outil. Si les capabilities contiennent resources mais sans le drapeau listChanged, ChatGPT peut mettre en cache la liste des ressources et ne pas attendre des notifications de changements.

Comment les inspecteurs et MCP Jam aident au debug

Des outils comme MCP Jam / MCP Inspector font pratiquement la même chose : ils établissent la connexion, effectuent le handshake, vous affichent les capabilities du serveur, et vous permettent d’appeler manuellement tools/list, tools/call, etc.

Du point de vue du développeur, c’est indispensable :

  • on voit quelle protocolVersion le serveur a effectivement renvoyée ;
  • on voit immédiatement si les capabilities incluent tools, resources, prompts ;
  • on peut comprendre pourquoi ChatGPT ne voit pas les outils (capabilities non déclarées ou handshake non abouti).

Dans la dernière leçon de ce module, vous utiliserez ces outils de manière plus poussée, mais il est déjà utile de comprendre qu’ils fonctionnent exactement au‑dessus du handshake que nous analysons.

11. Erreurs typiques lors du travail avec le handshake et les capabilities

En théorie tout paraît assez direct, mais en pratique, c’est précisément le handshake et la déclaration des capabilities qui deviennent le plus souvent la source de bugs très basiques — surtout en mode Dev ou dans MCP Inspector. Ci‑dessous — quelques erreurs typiques auxquelles vous serez presque assurément confronté, dans votre code ou dans les logs de collègues.

Erreur n° 1 : format incorrect de la requête initialize.
Problème très fréquent lors d’une implémentation manuelle d’un serveur MCP sans SDK : oublier un champ obligatoire JSON-RPC. Par exemple, omettre jsonrpc : "2.0", confondre method (écrire "init" au lieu de "initialize"), ou rendre capabilities booléen au lieu d’un objet. La spécification MCP attend un format strict ; toute divergence conduit à des erreurs de parsing et à une rupture de la connexion. La documentation et les guides pratiques recommandent d’abord de s’assurer que initialize respecte strictement la spécification, avant d’investiguer autre chose.

Erreur n° 2 : ignorer protocolVersion.
Parfois les développeurs copient simplement un exemple de la doc et y mettent une chaîne arbitraire, sans vérifier le support dans le SDK. En conséquence, client et serveur parlent des versions différentes de MCP et la connexion ne s’établit pas. L’erreur peut se manifester comme « le client ne se connecte pas du tout ». Il faut traiter protocolVersion comme un contrat réel : mettre d’accord cette version entre l’équipe frontend/plateforme d’agents et l’équipe qui écrit le serveur MCP.

Erreur n° 3 : capabilities oubliées.
Cas classique : vous avez enregistré un outil sur le serveur, mais lors d’une implémentation manuelle du handshake, vous avez oublié d’ajouter "tools": {} dans les capabilities de la réponse initialize. Dans l’inspector vous voyez que les outils existent, mais ChatGPT affiche « No tools available » — parce qu’il se fie honnêtement aux capabilities et ne fait pas tools/list si la section tools n’y est pas. Les guides de dépannage pour Apps SDK soulignent : si ChatGPT ne voit pas les outils, vérifiez d’abord les capabilities.

Erreur n° 4 : tenter d’utiliser des méthodes non déclarées dans les capabilities.
Il arrive que des étudiants expérimentent et envoient, par exemple, resources/list à un serveur dont les capabilities n’ont pas de section resources. Formellement, le serveur peut répondre Method not found, mais il est plus correct de ne pas appeler ces méthodes du tout. MCP introduit justement les capabilities comme une protection contre de telles tentatives. Le client doit d’abord vérifier l’existence de la section correspondante dans les capabilities, puis seulement appeler les méthodes.

Erreur n° 5 : le serveur commence à « parler » avant notifications/initialized.
Si le serveur, juste après avoir envoyé la réponse à initialize, commence à envoyer des logs ou des notifications sans attendre notifications/initialized, certains clients peuvent ignorer ces messages ou même couper la connexion. L’architecture officielle MCP souligne que le handshake doit d’abord se terminer, et qu’uniquement après la notification d’initialisation commence la « vie opérationnelle ».

Erreur n° 6 : modifier le schéma des outils sans signaler le changement de liste.
Quand vous modifiez le JSON Schema d’un outil (rendre un champ obligatoire, renommer un argument) sans redémarrer le serveur ni envoyer de notification indiquant que la liste des outils a changé, le cache du client peut contenir l’ancienne version du schéma. Cela conduit à des erreurs de validation étranges. La spécification propose d’utiliser le drapeau listChanged et les notifications tools/list_changed et resources/list_changed pour aider le client à mettre à jour son cache en temps voulu.

Erreur n° 7 : optimisation prématurée et « magie » autour des capabilities.
Parfois, les développeurs conçoivent des schémas compliqués avec génération dynamique des capabilities, versionnement selon les clients, etc., sans maîtriser les mécanismes de base. Au départ, il suffit d’annoncer honnêtement ce que le serveur sait faire : tools, resources, prompts, logging. Il vaut mieux étendre les capabilities au fur et à mesure d’un besoin réel, et non « pour l’avenir ». C’est davantage un anti‑pattern organisationnel qu’une erreur purement protocolaire, mais on le rencontre très souvent en production.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION