Ce qu’un serveur d’outils envoie vraiment
Le Model Context Protocol (MCP) est une façon standard, pour un agent, de trouver et d’appeler les outils d’un autre programme. Ses messages sont du JSON simple, un par ligne, sur l’entrée et la sortie standard. Une fois les messages lisibles pour vous, vous pouvez déboguer n’importe quel serveur d’outils.
Un serveur d’outils qui marche seul mais pas avec votre agent envoie souvent un message sous la mauvaise forme.
À la fin de cette leçon
Remarque
Je peux lire et écrire les messages qu’échangent un serveur d’outils et son client.
Mots-clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| JSON-RPC | un format de message pour appeler des méthodes, en JSON | |
| Requête | un message avec un id qui attend une réponse | |
| Notification | un message sans id et sans réponse | |
| stdio | l’entrée et la sortie standard d’un processus | |
| Code d’erreur | un nombre qui dit quelle sorte d’échec | -32601 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
JSON-RPC a trois sortes de messages
Une requête a un id, un nom de méthode et des paramètres facultatifs. Une réponse recopie cet id et contient soit un résultat, soit une erreur. Une notification n’a pas d’id : rien n’y répond.
Sur stdio, chaque message tient sur une ligne de JSON. Le serveur n’écrit que des messages sur sa sortie et envoie ses journaux sur son flux d’erreur.
Exemple travaillé : trois messages
| Sorte | Exemple | Réponse |
|---|---|---|
| Requête | {"jsonrpc":"2.0","id":2,"method":"tools/list"} | Une réponse avec l’id 2 |
| Réponse | {"jsonrpc":"2.0","id":2,"result":{"tools":[]}} | Aucune |
| Notification | {"jsonrpc":"2.0","method":"notifications/initialized"} | Aucune |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
C’est la forme, pas tout le protocole
Important
Le serveur ci-dessous reprend la forme du vrai protocole, en plus petit. Un vrai serveur utilise le SDK officiel, qui gère aussi les versions, l’annulation, d’autres types de contenu et beaucoup d’autres méthodes. Rien n’est installé ici.
Écrire le serveur d’outils, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const PRIX = { baguette: 110, croissant: 130 };' 'const OUTILS = [{ name: \"lire_prix\", description: \"Prix d’un produit de boulangerie en centimes\", inputSchema: { type: \"object\", properties: { produit: { type: \"string\" } }, required: [\"produit\"] } }];' 'function envoyer(message) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, message)) + \"\n\"); }' 'function repondre(id, result) { envoyer({ id: id, result: result }); }' 'function echouer(id, code, texte) { envoyer({ id: id, error: { code: code, message: texte } }); }' 'function texte(mots, isError) { return { content: [{ type: \"text\", text: mots }], isError: isError }; }' > serveur.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 d’outils, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function appeler(id, params) {' ' if (params.name === \"lire_prix\") {' ' const prix = PRIX[(params.arguments || {}).produit];' ' if (prix === undefined) return repondre(id, texte(\"Produit inconnu\", true));' ' return repondre(id, texte(prix + \" centimes\", false));' ' }' ' return echouer(id, -32602, \"Outil inconnu : \" + params.name);' '}' 'function traiter(msg) {' ' if (msg.id === undefined) return;' ' if (msg.method === \"initialize\") return repondre(msg.id, { protocolVersion: \"2024-11-05\", capabilities: { tools: {} }, serverInfo: { name: \"boulangerie\", version: \"0.1\" } });' ' if (msg.method === \"tools/list\") return repondre(msg.id, { tools: OUTILS });' ' if (msg.method === \"tools/call\") return appeler(msg.id, msg.params || {});' ' return echouer(msg.id, -32601, \"Méthode introuvable : \" + msg.method);' '}' >> serveur.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 d’outils, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (ligne) {' ' try { traiter(JSON.parse(ligne)); } catch (erreur) { echouer(null, -32700, \"Erreur d’analyse\"); }' '});' >> serveur.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 serveur = require(\"child_process\").spawn(\"node\", [\"serveur.js\"]);' 'const appel = function (n, nom, args) { return { id: n, method: \"tools/call\", params: { name: nom, arguments: args } }; };' 'const scenario = [' ' { id: 1, method: \"initialize\", params: { protocolVersion: \"2024-11-05\", capabilities: {}, clientInfo: { name: \"demo\", version: \"0.1\" } } },' ' { method: \"notifications/initialized\" },' ' { id: 2, method: \"tools/list\" },' ' appel(3, \"lire_prix\", { produit: \"croissant\" }),' ' appel(4, \"lire_prix\", { produit: \"kiwi\" }),' ' appel(5, \"tout_effacer\", {}),' ' { id: 6, method: \"resources/list\" },' '];' > client.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' 'let attente = 6;' 'require(\"readline\").createInterface({ input: serveur.stdout }).on(\"line\", function (ligne) {' ' console.log(\"<- \" + ligne);' ' attente -= 1;' ' if (attente === 0) serveur.stdin.end();' '});' 'scenario.forEach(function (m) {' ' const complet = Object.assign({ jsonrpc: \"2.0\" }, m);' ' console.log(\"-> \" + JSON.stringify(complet));' ' serveur.stdin.write(JSON.stringify(complet) + \"\n\");' '});' >> client.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 client.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.
Une sortie possible
| Requête | Réponse |
|---|---|
| initialize | Un résultat avec la version du protocole et le nom du serveur |
| tools/list | Un résultat qui liste lire_prix |
| lire_prix pour un croissant | Un résultat avec le texte 130 centimes |
| lire_prix pour un kiwi | Un résultat avec isError à true et Produit inconnu |
| tout_effacer | Une erreur de code -32602 |
| resources/list | Une erreur de code -32601 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une session suit un ordre
Le client envoie d’abord initialize et le serveur répond avec ce qu’il sait faire. Le client envoie ensuite la notification qui dit qu’il est prêt. Alors seulement il demande tools/list et utilise tools/call.
L’agent montre au modèle les outils listés. Quand le modèle en demande un, le client envoie tools/call.
Deux sortes d’échec
Une erreur de protocole est une réponse d’erreur JSON-RPC. La requête elle-même était mauvaise : méthode inconnue, outil inconnu ou JSON illisible. Chacune a un code, comme -32601 pour une méthode absente.
Une erreur d’outil est un résultat normal avec isError à true. L’appel était correct, mais l’outil n’a pas pu faire le travail. Le modèle lit le texte et peut essayer autre chose.
Une erreur fréquente
Attention
Beaucoup pensent que tout échec doit être une erreur JSON-RPC. Une réponse d’erreur dit que la requête est mal formée. Un outil qui a tourné sans trouver le produit doit rendre un résultat avec isError à true. Ainsi le modèle voit la raison et peut se rattraper.