Plus facile à lire avec un fond crème, une teinte, plus de contraste ou un texte plus grand ? Choisissez vos couleurs et votre texte, une fois pour toutes les pages.

Leçon gratuite

Lire et écrire les messages MCP

Après cette leçon: Je peux lire et écrire les messages JSON qu’échangent un serveur et un client.

Leçon de 70 min8 commandesLab agent de code IA : utiliser opencode pour expliquer et corriger du code, et vérifier son travail

Comment un serveur MCP lit ses messages

Un serveur MCP est un programme qui lit des messages JSON sur son entrée et en écrit sur sa sortie, un message par ligne. Un serveur qui fait cela mal marche seul, puis échoue avec tous les vrais clients.

Le module 67 a montré le côté client et les sortes de message. Ici, vous écrivez la boucle de lecture d’un serveur.

À la fin de cette leçon

Remarque

Je peux lire et écrire les messages JSON qu’échangent un serveur et un client.

Mots clés

MotCe que ça veut direExemple
Tamponle texte gardé jusqu’à l’arrivée d’une ligne entière
Trameun message entier coupé dans le flux au saut de ligne
stdoutle flux de sortie : seuls les messages y vont
stderrle flux d’erreur : les traces y vont
Erreur d’analysela ligne n’était pas du JSON-32700
Requête invalidedu JSON, mais pas une requête-32600

Les cellules que les étudiants remplissent restent vides dans cet aperçu.

Un flux se coupe en lignes

L’entrée n’arrive pas en messages entiers. Elle arrive par bouts. Un bout peut contenir la moitié d’un message, un message, ou deux et demi.

Le serveur garde un tampon. Il y ajoute chaque bout, coupe chaque ligne complète et garde le reste. Un message ne contient jamais de saut de ligne brut : un saut de ligne en termine donc toujours un.

Exemple : trois bouts de texte

Bout arrivéTampon ensuiteMessages prêts
{"id":1} {"id{"id1 message : {"id":1}
":2} (vide)1 message : {"id":2}
{"i{"iAucun pour l’instant

Les cellules que les étudiants remplissent restent vides dans cet aperçu.

Une mauvaise ligne reçoit quand même une réponse

Une ligne qui n’est pas du JSON reçoit une erreur d’analyse. Le serveur ne peut lire aucun id : l’id est donc null.

Une ligne qui est du JSON sans être une requête reçoit Requête invalide. Si un id se lit, on le recopie. Une requête sans id est une notification, et même pour une méthode inconnue elle ne reçoit rien.

Ce que le serveur renvoie

LigneRéponse
pas du jsonErreur -32700, id null
{"jsonrpc":"2.0","id":5,"method":5}Erreur -32600, id 5
[1,2]Erreur -32600, id null
{"jsonrpc":"2.0","method":"truc"}Rien : c’est une notification

Les cellules que les étudiants remplissent restent vides dans cet aperçu.

Les réponses reviennent dans n’importe quel ordre

Une requête lente ne doit pas retenir une requête rapide. Le serveur répond à chacune quand sa réponse est prête.

Un client ne peut donc pas associer les réponses par leur place. Il les associe par l’id. C’est pourquoi un serveur recopie l’id tel quel, nombre ou texte, comme il l’a reçu.

Une erreur courante

Attention

Beaucoup de gens déboguent avec un affichage. Sur stdout, cette ligne n’est pas un message, et le client échoue dessus. Seuls les messages vont sur stdout. Écrivez les traces sur stderr : le client peut les montrer ou les ignorer.

Écrire l’assistant du modèle

Commande
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

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'let tampon = \"\";' 'function envoyer(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function echec(id, code, texte) { envoyer({ id: id, error: { code: code, message: texte } }); }' 'function journal(texte) { process.stderr.write(\"journal : \" + texte + \"\n\"); }' > serveur1.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

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function traiter(ligne) {' '  let msg;' '  try { msg = JSON.parse(ligne); } catch (erreur) { return echec(null, -32700, \"Erreur d’analyse\"); }' '  const vu = msg && (typeof msg.id === \"number\" || typeof msg.id === \"string\") ? msg.id : null;' '  const bon = msg && typeof msg === \"object\" && msg.jsonrpc === \"2.0\" && typeof msg.method === \"string\";' '  if (Boolean(bon) === false) return echec(vu, -32600, \"Requête invalide\");' '  journal(\"reçu \" + msg.method);' '  if (msg.id === undefined) return;' '  const ms = (msg.params || {}).ms || 0;' '  if (msg.method === \"ping\") return envoyer({ id: msg.id, result: {} });' '  if (msg.method === \"attendre\") return setTimeout(function () { envoyer({ id: msg.id, result: { attendu: ms } }); }, ms);' '  echec(msg.id, -32601, \"Méthode inconnue : \" + msg.method);' '}' >> serveur1.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

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'process.stdin.on(\"data\", function (morceau) {' '  tampon += morceau.toString();' '  let fin = tampon.indexOf(\"\n\");' '  while (fin >= 0) {' '    const ligne = tampon.slice(0, fin);' '    tampon = tampon.slice(fin + 1);' '    if (ligne.trim().length > 0) traiter(ligne);' '    fin = tampon.indexOf(\"\n\");' '  }' '});' 'journal(\"prêt\");' >> serveur1.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

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'const serveur = require(\"child_process\").spawn(\"node\", [\"serveur1.js\"]);' 'const m = function (obj) { return JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, obj)) + \"\n\"; };' 'const attendre = function (id, ms) { return m({ id: id, method: \"attendre\", params: { ms: ms } }); };' 'const ping = m({ id: 3, method: \"ping\" });' 'const mauvais = \"pas du json\n\" + m({ id: 5, method: 5 }) + m({ method: \"notifications/x\" });' 'const etapes = [[0, attendre(1, 80)], [10, attendre(2, 10)], [20, ping.slice(0, 12)], [40, ping.slice(12) + mauvais]];' > client1.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

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function demarrer() {' '  etapes.forEach(function (e) {' '    setTimeout(function () { console.log(\"-> \" + e[1].trim().split(\"\n\").join(\" | \")); serveur.stdin.write(e[1]); }, e[0]);' '  });' '  setTimeout(function () { serveur.stdin.end(); }, 200);' '}' 'require(\"readline\").createInterface({ input: serveur.stderr }).on(\"line\", function (ligne) {' '  console.log(\"stderr \" + ligne);' '  if (ligne === \"journal : prêt\") demarrer();' '});' 'require(\"readline\").createInterface({ input: serveur.stdout }).on(\"line\", function (ligne) { console.log(\"<- \" + ligne); });' >> client1.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

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'serveur.on(\"close\", async function () {' '  const q = \"En une phrase, pourquoi une réponse doit-elle porter l’id de sa requête ?\";' '  const r = await demander([{ role: \"user\", content: q }], \"Pour que le client sache à quelle requête chaque réponse appartient, car les réponses peuvent arriver dans n’importe quel ordre.\", 60);' '  console.log(\"le modèle dit : \" + r.texte);' '});' >> client1.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

Commande
docker exec -w /home/node/project agent node client1.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.

Ce que fait le client

Le client lance le serveur et attend sa ligne prêt sur stderr. Il envoie deux requêtes d’attente, la moitié d’un ping, puis la fin du ping suivie de trois autres lignes.

Un appel en direct termine le script : il demande une phrase à un modèle. Sans modèle, le Run affiche un Échantillon enregistré pour cette ligne.

Les réponses, dans l’ordre où elles arrivent

Ligne affichée avec <-Pourquoi elle arrive à ce moment
id 2, attendu 10La courte attente a fini la première
id 3, un résultat videLe ping était entier quand sa seconde moitié est arrivée
id null, erreur -32700La ligne « pas du json »
id 5, erreur -32600Une méthode qui est un nombre
id 1, attendu 80La longue attente a fini la dernière

Les cellules que les étudiants remplissent restent vides dans cet aperçu.

À propos de ce labo et de sa source: Lab agent de code IA : utiliser opencode pour expliquer et corriger du code, et vérifier son travail

Vous enseignez à une classe ? Proposez ce module à vos étudiants avec un guide de l'enseignant, des devoirs corrigés et un laboratoire pour chaque étudiant.