Comment un serveur MCP répond à initialize
La poignée de main est le premier échange entre un client MCP et un serveur. Le client envoie initialize avec la version qu’il veut. Le serveur répond avec la version qu’il va employer et ce qu’il sait faire.
C’est important, car un client n’utilise que ce que le serveur dit offrir. Un serveur qui annonce trop de choses fait échouer le client.
À la fin de cette leçon
Remarque
Je peux répondre à une requête initialize et dire ce que mon serveur sait faire.
Mots clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Poignée de main | le premier échange qui ouvre une session | |
| Version du protocole | la date qui nomme une version des règles | 2024-11-05 |
| Capacité | un groupe de fonctions que le serveur dit avoir | tools |
| État | ce que le serveur retient : nouveau, attente ou prêt | |
| Négocier | choisir une version que les deux côtés parlent |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Le serveur choisit une version qu’il connaît
Le client nomme une version. Le serveur garde la liste des versions qu’il parle. Si la version demandée est dans la liste, il répond avec elle.
Sinon, ce serveur refuse avec le code -32602. Il met dans l’erreur la version demandée et sa propre liste, pour que le client puisse réessayer.
Exemple : trois clients demandent
| Le client demande | Le serveur parle | Le serveur répond |
|---|---|---|
| 2024-11-05 | 2025-03-26, 2024-11-05 | Un résultat en 2024-11-05 |
| 2025-03-26 | 2025-03-26, 2024-11-05 | Un résultat en 2025-03-26 |
| 1999-01-01 | 2025-03-26, 2024-11-05 | Erreur -32602 avec les deux listes |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Un serveur annonce seulement ce qu’il fait
Le résultat de initialize contient des capacités. Chacune est un groupe de méthodes que le serveur a : outils, ressources, prompts ou journalisation.
Une capacité peut porter des drapeaux. listChanged dit que le serveur préviendra le client quand sa liste change. Ce serveur ne le fait jamais : il dit faux. Une méthode d’un groupe non annoncé reçoit -32601.
Une erreur courante
Attention
Beaucoup annoncent toutes les capacités « au cas où ». Le client appelle alors des méthodes que le serveur n’a pas. Annoncez seulement ce que vous avez écrit et testé. Une annonce est une promesse.
Écrire l’assistant du modèle
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function demander(messages, echantillon, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", { method: \"POST\", headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" }, body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 80 }), signal: AbortSignal.timeout(30000) });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' const donnees = await res.json();' ' return { texte: donnees.choices[0].message.content, enregistre: false };' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' return { texte: echantillon, enregistre: true };' ' }' '}' 'module.exports = { demander };' > model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le serveur, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const VERSIONS = [\"2025-03-26\", \"2024-11-05\"];' 'let etat = \"nouveau\";' 'function envoyer(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function repondre(id, result) { envoyer({ id: id, result: result }); }' 'function echec(id, code, texte, data) { envoyer({ id: id, error: { code: code, message: texte, data: data } }); }' > serveur2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le serveur, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function initialiser(msg) {' ' const demandee = (msg.params || {}).protocolVersion;' ' if (VERSIONS.indexOf(demandee) < 0) return echec(msg.id, -32602, \"Version non prise en charge\", { demandee: demandee, acceptees: VERSIONS });' ' etat = \"attente\";' ' repondre(msg.id, { protocolVersion: demandee, capabilities: { tools: { listChanged: false } }, serverInfo: { name: \"cantine\", version: \"0.2\" } });' '}' >> serveur2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le serveur, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function traiter(msg) {' ' if (msg.id === undefined) {' ' if (msg.method === \"notifications/initialized\" && etat === \"attente\") etat = \"pret\";' ' return;' ' }' ' if (msg.method === \"ping\") return repondre(msg.id, {});' ' if (msg.method === \"initialize\") return etat === \"nouveau\" ? initialiser(msg) : echec(msg.id, -32600, \"Déjà initialisé\");' ' if (etat === \"nouveau\" || etat === \"attente\") return echec(msg.id, -32600, \"Pas initialisé\");' ' if (msg.method === \"tools/list\") return repondre(msg.id, { tools: [] });' ' echec(msg.id, -32601, \"Méthode inconnue : \" + msg.method);' '}' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (ligne) {' ' let msg;' ' try { msg = JSON.parse(ligne); } catch (erreur) { return echec(null, -32700, \"Erreur d’analyse\"); }' ' traiter(msg);' '});' >> serveur2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le client scripté, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'const serveur = require(\"child_process\").spawn(\"node\", [\"serveur2.js\"]);' 'const init = function (id, version) { return { id: id, method: \"initialize\", params: { protocolVersion: version, capabilities: {}, clientInfo: { name: \"demo\", version: \"0.1\" } } }; };' 'const scenario = [' ' { id: 1, method: \"tools/list\" },' ' init(2, \"1999-01-01\"),' ' init(3, \"2024-11-05\"),' ' { id: 4, method: \"tools/list\" },' ' { method: \"notifications/initialized\" },' ' { id: 5, method: \"tools/list\" },' ' { id: 6, method: \"resources/list\" },' '];' > client2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le client scripté, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const vues = [];' 'require(\"readline\").createInterface({ input: serveur.stdout }).on(\"line\", function (ligne) {' ' console.log(\"<- \" + ligne);' ' vues.push(JSON.parse(ligne));' ' if (vues.length === 6) serveur.stdin.end();' '});' 'scenario.forEach(function (m) {' ' const plein = Object.assign({ jsonrpc: \"2.0\" }, m);' ' console.log(\"-> \" + JSON.stringify(plein));' ' serveur.stdin.write(JSON.stringify(plein) + \"\n\");' '});' >> client2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le client scripté, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'serveur.on(\"close\", async function () {' ' const capacites = JSON.stringify(vues[2].result.capabilities);' ' const q = \"En une phrase, dites ce que peut faire un serveur qui annonce ces capacités : \" + capacites;' ' const r = await demander([{ role: \"user\", content: q }], \"Il peut lister des outils, et il ne dit pas que sa liste va changer.\", 60);' ' console.log(\"le modèle dit : \" + r.texte);' '});' >> client2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Lancer le client contre le serveur
docker exec -w /home/node/project agent node client2.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Ce que le script envoie
Sept messages partent dans un ordre fixe. D’abord une liste trop tôt et une mauvaise version. Puis une bonne version et une seconde liste trop tôt.
Puis la notification prêt, une liste qui marche et une méthode que le serveur n’a jamais annoncée.
Un court appel au modèle, à la fin, dit ce que veulent dire les capacités. Sans modèle, il affiche un Échantillon enregistré.
Les six réponses
| Requête | Réponse |
|---|---|
| 1 tools/list | Erreur -32600, Pas initialisé |
| 2 initialize, 1999-01-01 | Erreur -32602, avec les deux versions acceptées |
| 3 initialize, 2024-11-05 | Un résultat avec tools annoncé et le nom du serveur |
| 4 tools/list | Erreur -32600, toujours pas prêt |
| 5 tools/list | Un résultat avec une liste vide |
| 6 resources/list | Erreur -32601, jamais annoncé |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.