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

Des outils MCP qui contrôlent leur entrée

Après cette leçon: Je peux proposer un outil dont l’entrée est contrôlée avant qu’il tourne.

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

Comment un outil MCP contrôle son entrée

Un outil MCP reçoit ses arguments d’un modèle. Un modèle se trompe parfois : un texte à la place d’un nombre, un nom inventé, un champ oublié. Un serveur qui fait confiance sans regarder agit sur de mauvaises données.

Ici, vous écrivez le contrôle qui a lieu avant que l’outil tourne.

À la fin de cette leçon

Remarque

Je peux proposer un outil dont l’entrée est contrôlée avant qu’il tourne.

Mots clés

MotCe que ça veut direExemple
Schémala liste des règles que l’entrée d’un outil doit suivre
Argumentune valeur donnée à l’outil, avec son nom
Énumérationla liste des seules valeurs permisescroque, wrap, salade
Erreur d’outilun résultat avec isError à vrai, que le modèle peut lire
Erreur de protocoleune erreur du message lui-même, avec un code

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

Un schéma liste les règles de chaque argument

Le schéma d’un outil dit quels arguments existent, quel type chacun a, et lesquels sont obligatoires. Il peut aussi borner une valeur : une liste de choix, un minimum, un maximum, une longueur.

Le serveur annonce ce schéma dans tools/list. Il le vérifie aussi à chaque appel : une annonce n’oblige personne.

Exemple : l’outil commander_repas

ArgumentRègleObligatoire
plattexte : croque, wrap ou saladeoui
quantiteentier de 1 à 5oui
notetexte de 20 caractères au plusnon
tout autre nomrefusésans objet

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

Un refus dit au modèle quoi corriger

Quand un argument est mauvais, le serveur ne casse pas la session. Il renvoie un résultat avec isError à vrai et une phrase qui liste chaque défaut. Le modèle lit cette phrase et refait l’appel.

Un outil qui n’existe pas est autre chose : c’est une erreur de protocole, avec le code -32602.

Une erreur courante

Attention

Beaucoup pensent que le modèle suit le schéma, donc que le contrôle est inutile. Un modèle peut se tromper, et n’importe quel client peut envoyer n’importe quoi. Contrôlez toujours côté serveur, avant d’agir.

É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' 'const SCHEMA = { type: \"object\", properties: { plat: { type: \"string\", enum: [\"croque\", \"wrap\", \"salade\"] }, quantite: { type: \"integer\", minimum: 1, maximum: 5 }, note: { type: \"string\", maxLength: 20 } }, required: [\"plat\", \"quantite\"], additionalProperties: false };' 'const MOTS = { string: \"texte\", integer: \"entier\" };' 'function envoyer(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function bon_type(r, v) { return r.type === \"string\" ? typeof v === \"string\" : Number.isInteger(v); }' > serveur3.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 defauts(args) {' '  const sortie = [];' '  SCHEMA.required.forEach(function (n) { if ((n in args) === false) sortie.push(\"manque : \" + n); });' '  Object.keys(SCHEMA.properties).forEach(function (n) {' '    if ((n in args) === false) return;' '    const r = SCHEMA.properties[n], v = args[n];' >> serveur3.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' '    if (bon_type(r, v) === false) return sortie.push(n + \" : \" + MOTS[r.type] + \" attendu\");' '    if (r.enum && r.enum.indexOf(v) < 0) sortie.push(n + \" : doit être l’un de \" + r.enum.join(\", \"));' '    if (typeof r.minimum === \"number\" && (v < r.minimum || v > r.maximum)) sortie.push(n + \" : doit être entre \" + r.minimum + \" et \" + r.maximum);' '    if (typeof r.maxLength === \"number\" && v.length > r.maxLength) sortie.push(n + \" : au plus \" + r.maxLength + \" caractères\");' '  });' '  Object.keys(args).sort().forEach(function (n) { if ((n in SCHEMA.properties) === false) sortie.push(\"en trop : \" + n); });' '  return sortie;' '}' >> serveur3.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 4

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function mots(texte, mauvais) { return { content: [{ type: \"text\", text: texte }], isError: mauvais }; }' 'function traiter(msg) {' '  if (msg.id === undefined) return;' '  const p = msg.params || {};' '  if (msg.method === \"initialize\") return envoyer({ id: msg.id, result: { protocolVersion: p.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: \"cantine\", version: \"0.3\" } } });' '  if (msg.method === \"tools/list\") return envoyer({ id: msg.id, result: { tools: [{ name: \"commander_repas\", description: \"Commander un repas à la cantine\", inputSchema: SCHEMA }] } });' >> serveur3.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 5

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  if (p.name === \"commander_repas\") {' '    const args = p.arguments || {};' '    const trouves = defauts(args);' '    if (trouves.length > 0) return envoyer({ id: msg.id, result: mots(\"Impossible de lancer commander_repas : \" + trouves.join(\"; \"), true) });' '    return envoyer({ id: msg.id, result: mots(\"Commandé : \" + args.quantite + \" \" + args.plat, false) });' '  }' '  envoyer({ id: msg.id, error: { code: -32602, message: \"Outil inconnu : \" + p.name } });' '}' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (ligne) { traiter(JSON.parse(ligne)); });' >> serveur3.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\", [\"serveur3.js\"]);' 'const commande = function (id, args) { return { id: id, method: \"tools/call\", params: { name: \"commander_repas\", arguments: args } }; };' 'const scenario = [' '  { id: 1, method: \"initialize\", params: { protocolVersion: \"2025-03-26\" } },' '  { method: \"notifications/initialized\" },' '  commande(2, { plat: \"wrap\", quantite: 2 }),' '  commande(3, { plat: \"soupe\", quantite: 9, couleur: \"rouge\" }),' '  commande(4, {}),' '  commande(5, { plat: \"croque\", quantite: \"2\" }),' '  { id: 6, method: \"tools/call\", params: { name: \"effacer_menu\", arguments: {} } },' '];' > client3.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' 'const vues = [];' 'require(\"readline\").createInterface({ input: serveur.stdout }).on(\"line\", function (ligne) {' '  const m = JSON.parse(ligne);' '  console.log(\"<- \" + (m.result && m.result.content ? m.result.content[0].text + \" (isError \" + m.result.isError + \")\" : ligne));' '  vues.push(m);' '  if (vues.length === 6) serveur.stdin.end();' '});' 'scenario.forEach(function (m) { serveur.stdin.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); });' >> client3.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 montre = vues[2].result.content[0].text;' '  const q = \"Un outil a refusé un appel. En une phrase, que doit changer celui qui appelle ? \" + montre;' '  const r = await demander([{ role: \"user\", content: q }], \"Choisir croque, wrap ou salade, une quantité de 1 à 5, et ne mettre aucune couleur.\", 60);' '  console.log(\"le modèle dit : \" + r.texte);' '});' >> client3.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 client3.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 le script appelle

Le script ouvre la session, puis fait cinq appels. Un bon, un avec trois défauts, un sans aucun argument, un avec un nombre écrit en texte et un pour un outil qui n’existe pas.

À la fin, un court appel demande à un modèle ce que l’appelant doit changer. Sans modèle, le Run affiche un Échantillon enregistré.

Ce que chaque appel reçoit

AppelRéponse
2 wrap, quantité 2Commandé : 2 wrap (isError false)
3 soupe, quantité 9, couleurTrois défauts listés (isError true)
4 aucun argumentDeux défauts : manque plat et manque quantite
5 quantité « 2 »quantite : entier attendu
6 effacer_menuErreur de protocole -32602

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.