Code Mode, deux outils et un MCP pour sauver le contexte

L'un des outils que je construis est un serveur MCP personnalisé, basé sur l'idée de Code Mode de Cloudflare, qui me permet d'interagir avec les services locaux que je développe via leurs API comme un client local, sans avoir à le faire moi-même.

Il fonctionne bien mieux que ce à quoi je m'attendais. J'ai été à la fois impressionné et enthousiasmé par les possibilités offertes par cette approche. Mais je me suis heurté à un mur : absolument personne parmi les personnes à qui j'en ai parlé ne partageait mon enthousiasme ou n'avait une idée de la puissance que pouvait avoir cette nouvelle approche.

J'ai donc fait ce que je sais faire de mieux : j'ai écrit à ce sujet.

Cet article est le résultat de cette réflexion.

Le contexte, c'est tout

L'une des choses auxquelles nous devons accorder le plus d'importance lorsque nous utilisons des LLM est le contexte. Le contexte est cette fenêtre de texte que le LLM voit et utilise pour générer sa réponse, alimentée par le fournisseur, l'utilisateur et le modèle lui-même.

La spécification des modèles d'OpenAI définit cinq rôles qui peuvent être utilisés pour remplir le contexte :

  • system : messages ajoutés par OpenAI
  • developer : messages provenant du développeur de l'application (et éventuellement d'OpenAI)
  • user : entrées des utilisateurs finaux, ou catégorie générale pour les données que nous voulons fournir au modèle
  • assistant : messages générés par le modèle de langage
  • tool : messages générés par un programme, comme l'exécution de code ou un appel d'API

Tous ces rôles et leurs messages sont empilés pour former ce que nous appelons le contexte. Imaginez-le comme un long historique de conversation, dans lequel le LLM peut voir tout ce qui a été dit et fait.

Context window

A conversation grows as user, assistant, and tool messages enter the model’s context window.

Step 5 of 5

User

Extract the titles from my-file.md.

Assistant

Read my-file.md

Tool

File content

Large tool result

Assistant

Create titles.txt

Tool

titles.txt created

Le contexte est limité par la longueur maximale de contexte du modèle, qui est généralement mesurée en tokens. Par exemple, GPT-5.5 a une longueur maximale de contexte pouvant atteindre 1M de tokens. Mais il y a un piège : le prix augmente au-delà de 272k tokens, et les performances du modèle commencent à se dégrader au-delà de 50 % de la longueur maximale du contexte (ce qui n'est pas vraiment vrai pour les modèles d'OpenAI si tu restes dans la limite de 272k tokens). Et si le contexte dépasse la longueur maximale de contexte, les anciens messages peuvent être tronqués ou supprimés, ce qui entraîne la perte d'informations précieuses que le modèle aurait pu utiliser pour générer de meilleures réponses.

Context window

A bounded context window makes room for new messages by removing the oldest ones.

Step 1 of 4
User

Summarize my notes

Assistant

Read the notes

Tool

Notes returned

Assistant

Draft a summary

Bien sûr, supprimer les anciens messages n'est pas la seule façon de faire de la place. Une application peut condenser le contexte en demandant à un modèle de résumer ces messages, puis en les remplaçant par un seul message condensé.

Context compaction

A single summary replaces older turns, leaving more room for the current conversation.

Step 1 of 4

User

Use the billing API and preserve the customer’s plan.

Assistant

I will update the plan without changing the billing cycle.

Tool

{ plan: "pro", billingCycle: "monthly", … }

Current conversation

User

Add the analytics add-on too.

Cela permet de conserver les décisions et les contraintes importantes tout en libérant de l'espace. Ce résumé omettra ou déformera certains détails, il est donc essentiel de guider le LLM lorsqu'il résume. Tu peux indiquer au LLM quelles seront les prochaines actions afin de l'aider à conserver les informations pertinentes.

Il faut donc faire attention au contexte et en prendre soin comme de la prunelle de ses yeux.

Cela signifie que tu ne peux pas simplement remplir le contexte avec une grande quantité de texte et t'attendre à ce que le modèle trouve les informations pertinentes tout en générant une bonne réponse. Tu dois lui fournir uniquement le contexte pertinent.

C'est l'une des raisons pour lesquelles le RAG, ou Retrieval-Augmented Generation, est si important.

Mais le RAG ne peut pas tout résoudre. Il est efficace pour ajouter à la demande des connaissances externes et spécifiques dans le contexte, mais il ne permet pas d'exécuter quoi que ce soit. C'est un problème.

Les LLM connaissent le code

Je suis ingénieur logiciel. Je sais écrire du code et, si tu lis ceci, tu sais probablement aussi en écrire.

Depuis la fin de l'année 2025, impossible de ne pas avoir remarqué que les LLM sont désormais capables d'écrire du code, et de bien l'écrire. On en est arrivé au point où plus de 95 % du code que je mets en production ou que j'utilise est généré par des LLM avec l'aide d'un agent.

Nous pouvons tirer parti de cela pour résoudre le problème du contexte. Cela peut sembler contre-intuitif. Mais regardons l'article sur Cloudflare Code Mode.

Les concepteurs de serveurs MCP sont encouragés à proposer des API largement simplifiées par rapport aux API plus traditionnelles qu'ils pourraient exposer aux développeurs. [...] Les LLM à qui l'on demande d'écrire du code avec les API complètes et complexes normalement exposées aux développeurs ne semblent pas rencontrer trop de difficultés. Pourquoi les interfaces MCP devraient-elles alors être « simplifiées à l'extrême » ?

Que veulent-ils dire par là ?

Les LLM peuvent appeler plusieurs outils à la suite, et parfois en parallèle. Mais lorsqu'un appel dépend du résultat d'un autre, le LLM doit recevoir ce résultat au tour suivant. Imagine le scénario suivant : tu disposes d'un outil capable de lire un fichier sur un disque et d'un autre capable de créer un nouveau fichier sur ce disque. Tu veux ensuite créer un nouveau fichier contenant tous les titres d'un fichier présent sur le disque.

Que va faire le LLM ?

Pour résoudre ce problème, le LLM effectuera deux appels sur deux tours :

  1. Appeler le premier outil pour lire le fichier. Cette action renverra le contenu du fichier, qui sera ajouté au contexte.
  2. Appeler le second outil pour créer le nouveau fichier contenant tous les titres. L'appel de l'outil, qui contient les titres extraits de la sortie du premier outil, sera ajouté au contexte.

Visuellement, cela ressemble à ceci :

Two tools, multiple LLM turns

Two independent tool calls require two LLM turns, so the large file result returns in the next input.

Step 1 of 5

LLM context window

Before turn 1

User

Extract the titles from my-file.md.

What is happening

LLM output

Assistant

Assistant writes a tool call

read_file(path="my-file.md")

Mais c'est loin d'être optimal.

Le contexte est encombré par le contenu du fichier, que le LLM n'a pas vraiment besoin de voir. Il est possible d'extraire les titres sans que le LLM connaisse le contenu du fichier. Cela signifie également que, pour un fichier confidentiel, le LLM et, par extension, le fournisseur auront accès à son contenu, ce qui n'est pas idéal. La nature non déterministe des LLM signifie aussi que l'extraction des titres peut ne pas être parfaite. Cela pourrait causer de vrais problèmes à une entreprise. Enfin, cette approche ne passe pas à l'échelle. Si le document fait 1M de tokens, le LLM devra tout lire et le contexte sera gonflé par une grande quantité d'informations inutiles.

Même si cet exemple peut sembler simple et irréaliste, il ne l'est pas. Si nous voulons que notre IA effectue des tâches complexes, nous devons trouver une meilleure façon de procéder. C'est là que Code Mode entre en jeu.

Avec Code Mode, tu demandes au LLM d'écrire du code qui effectuera la tâche à ta place. Pour l'informer de l'API qu'il peut utiliser, tu fournis des interfaces de types dans la description de l'outil. Le LLM génère alors du code qui appellera les outils dans le bon ordre et avec les bons paramètres pour effectuer la tâche.

Dans notre exemple, la description de l'outil ressemblera à ceci :

md
Exécute du code JavaScript contre l'API Drive.

Disponible dans ton code :
interface Drive {
  listFiles: () => Promise<{ title: string, filePath: string }[]>;
  readFile: (filePath: string) => Promise<string>;
  createFile: (filePath: string, content: string) => Promise<void>;
}

declare const drive: Drive;

Ton code doit être une fonction fléchée asynchrone.

Exemple :

// Crée une table des matières à partir de tous les fichiers du disque
async () => {
  const files = await drive.listFiles();
  const titles = files.map(file => file.title);
  await drive.createFile('table-of-contents.txt', titles.join('\n'));
};

Le LLM générera ensuite le code suivant pour effectuer la tâche :

ts
// Crée un nouveau fichier contenant tous les titres d'un fichier présent sur le disque
async () => {
  const content = await drive.readFile('my-file.md')
  const titles = content.split('\n').filter(line => line.startsWith('#')).map(line => line.replace(/^#+\s*/, ''))
  await drive.createFile('titles.txt', titles.join('\n'))
}

L'outil renvoie { success: true }. Le contexte du LLM ne contient donc plus que l'appel de l'outil et le résultat de cet appel, ce qui est bien mieux que d'avoir tout le contenu du fichier dans le contexte. À condition que l'environnement d'exécution ne puisse pas le divulguer, le LLM n'a pas besoin d'accéder au contenu du fichier.

One code tool, one LLM turn

One generated program coordinates file operations, keeping the large file content outside the LLM context.

Step 1 of 5

LLM context window

Before turn 1

User

Extract the titles from my-file.md.

Tool

code tool · Drive API

interface Drive {
  readFile: (path: string) => Promise<string>;
  writeFile: (path: string, content: string) => Promise<void>;
}

What is happening

Ready

Le LLM peut toujours générer du mauvais code. Mais le traitement des données est désormais explicite, reproductible et testable. Une fois le code validé, l'extraction des titres n'est plus laissée au LLM. C'est énorme.

Deux façons de découper un texte

Imaginez la situation suivante.

Tu récupères le contenu d'une page web pour en extraire le contenu d'un article et l'envoyer à un agrégateur d'actualités. Pour cela, tu utilises Cloudflare Browser Run, qui peut récupérer une page web et renvoyer son contenu au format Markdown.

Mais voici le problème : la page web complète est renvoyée. Elle comprend le contenu de l'article, mais aussi l'en-tête, le pied de page, la barre latérale et tout le reste de ce qui n'est pas pertinent pour le contexte. Tu veux uniquement extraire le contenu de l'article.

À partir de là, deux façons de procéder s'offrent à toi. Trois, en réalité, si l'on considère que nous pouvons ignorer le problème et transmettre l'intégralité du contenu au LLM.

Réécrire le contenu

La première façon, la plus simple, serait de demander au LLM de réécrire le contenu et de ne conserver que la partie pertinente. Le LLM produirait alors le contenu de l'article. Simple. Et très inefficace.

Tu as peut-être remarqué que les tokens de sortie sont beaucoup plus chers que les tokens d'entrée. Pour GPT-5.5, les tokens d'entrée coûtent 5 $ par million de tokens, tandis que les tokens de sortie coûtent 30 $ par million de tokens. Ainsi, si tu as une page web de 100k tokens et que le contenu de l'article ne fait que 10k tokens, tu paieras 0,50 $ pour les tokens d'entrée et 0,30 $ pour les tokens de sortie. Cela représente un total de 0,80 $ pour extraire 10k tokens de contenu, sans aucune certitude que le LLM réécrira parfaitement l'article.

Découper le contenu

La seconde façon est un peu plus complexe, mais bien plus efficace. L'idée est de donner l'intégralité de la page web au LLM et de lui demander de générer une fonction qui en extraira le contenu de l'article. Ainsi, le LLM ne produira que la fonction, qui ne fera que quelques tokens, puis tu pourras exécuter cette fonction pour extraire le contenu de l'article.

Pour te donner une idée, la fonction pourrait ressembler à ceci :

ts
() => content.split('#')[1].split('next steps.')[0].trim() // Le contenu de l'article se trouve entre le premier H1 et le texte « next steps. ». C'est toute la puissance d'un LLM : il s'adapte au contenu de la page web et génère une fonction qui extrait le contenu de l'article sans avoir à le coder en dur.

Cette fonction ne fait que 18 tokens. En utilisant GPT-5.5 pour les deux approches, tu paieras 0,50 $ pour les tokens d'entrée et 0,00054 $ pour les tokens de sortie. Cela représente un total de 0,50054 $ pour extraire 10k tokens de contenu, avec une petite fonction que tu peux inspecter et tester.

Code Mode permet également d'utiliser des modèles moins chers pour écrire la fonction. Par exemple, GPT-5.4-nano coûte respectivement 0,20 $ et 1,25 $ par million de tokens en entrée et en sortie. Avec ce modèle, tu paieras 0,02 $ pour les tokens d'entrée et 0,0000225 $ pour les tokens de sortie. Cela représente un total de 0,0200225 $ pour extraire 10k tokens de contenu, avec une petite fonction que tu peux inspecter et tester.

0,80 $ pour une réécriture par un modèle contre 0,50054 $ pour une petite fonction avec le même modèle, ou 0,0200225 $ avec un modèle moins cher.

Le choix t'appartient, mais le mien est clair. Code Mode est la voie à suivre.

Le problème de la spécification OpenAPI

Imaginez une spécification OpenAPI contenant 1,7 million de caractères, soit environ 425k tokens.

C'est beaucoup trop pour être transmis au LLM, et cette approche ne peut pas passer à l'échelle. Si la spécification OpenAPI dépasse la longueur maximale du contexte, le LLM ne pourra pas voir l'intégralité de la spécification et, même s'il le peut, le contexte sera encombré par des informations inutiles.

Mais avec uniquement la spécification OpenAPI et sans aucune connaissance de la logique métier, le LLM a été capable de réaliser une tâche assez complexe en quelques minutes en utilisant moins de 100k tokens de contexte. C'est incroyable.

C'est une histoire vraie, résolue grâce au serveur MCP personnalisé que j'ai mentionné plus tôt. Mais c'est aussi l'histoire de Cloudflare avec son serveur MCP. La première ligne du README rend explicite le problème du contexte :

Un serveur MCP économe en tokens pour l'ensemble de l'API Cloudflare. 2 500 endpoints en 1k tokens, propulsé par Code Mode.

Leur spécification OpenAPI fait environ 2M tokens. À ce stade, ce n'est pas énorme. C'est inutilisable.

Code Mode à la rescousse

Pour résoudre notre problème de spécification OpenAPI, notre solution Code Mode sera composée de deux outils :

  1. Search, qui permet au LLM de rechercher les endpoints et les paramètres pertinents dans la spécification OpenAPI.
  2. Execute, qui permet au LLM d'exécuter les endpoints pertinents avec les bons paramètres.

Chaque outil recevra { code: string } en entrée.

Un déroulement nominal ressemblera à ceci :

  1. Le LLM recherchera les endpoints et les paramètres pertinents dans la spécification OpenAPI à l'aide de l'outil Search. L'outil renverra une liste d'endpoints et de paramètres pertinents, qui sera ajoutée au contexte. Le code généré ressemblera à ceci :
ts
async () => {
  const results = []
  for (const [path, methods] of Object.entries(spec.paths)) {
    for (const [method, op] of Object.entries(methods)) {
      if (op.tags?.some(tag => tag.toLowerCase() === '<tag>')) { // Dans un scénario réel, le LLM remplacera <tag> par le tag pertinent à rechercher dans la spécification OpenAPI.
        results.push({ method: method.toUpperCase(), path, summary: op.summary })
      }
    }
  }
  return results
}

Il s'agit simplement d'une fonction qui sera exécutée sur la spécification OpenAPI afin de trouver les endpoints et les paramètres pertinents.

  1. Le LLM générera du code pour exécuter les endpoints pertinents avec les bons paramètres à l'aide de l'outil Execute. L'outil renverra le résultat de l'exécution, qui sera ajouté au contexte. Le code généré ressemblera à ceci :
ts
async () => api.request({
  method: 'GET',
  path: '/api/v1/resources', // Dans un scénario réel, le LLM remplacera ceci par l'endpoint pertinent à exécuter.
  query: { max: 10 },
})

Une fois encore, il s'agit simplement d'une fonction qui sera exécutée au sein du serveur MCP afin d'appeler l'endpoint pertinent avec les bons paramètres. C'est tout.

En coulisses, le serveur MCP exécutera le code généré par le LLM et renverra le résultat au LLM. Bien sûr, tu dois exécuter le code dans un bac à sable sécurisé pour éviter les problèmes de sécurité. Ne fais pas comme moi : le constructeur Function n'est pas un bac à sable sécurisé et l'exécution de code généré par un LLM peut entraîner des problèmes de sécurité majeurs, notamment l'exécution de code arbitraire et l'exfiltration de données. Je l'utilise actuellement uniquement pour simplifier le développement, mais lorsque je commencerai à partager le serveur MCP, j'utiliserai isolate-vm pour exécuter le code dans un bac à sable sécurisé. Une injection de prompt pourrait demander au LLM d'exfiltrer des données du serveur MCP, tu dois donc être prudent. Je sais que les Workers dynamiques de Cloudflare permettent de bloquer les requêtes réseau sortantes.

Avec ces deux outils et toutes les informations fournies par la spécification OpenAPI, comme le chemin, la description, le nom du paramètre et toute autre information pertinente lisible par un humain, le LLM peut comprendre comment interagir avec ton API et parfois déduire une partie de ta logique métier, sans avoir à écrire un outil personnalisé pour chaque endpoint. Mais une spécification OpenAPI ne décrit pas toujours complètement la logique métier, et ses descriptions peuvent être incomplètes ou ambiguës. Combine-la avec un accès à la documentation métier et ton LLM devient l'agent idéal pour interagir avec ton API.

C'est toute la puissance de Code Mode.

Démonstration du MCP de Cloudflare

Jusqu'ici, tout était théorique. Regardons maintenant comment le serveur MCP de Cloudflare se comporte en pratique :

Le serveur MCP de Cloudflare en action.

Tu te demandes peut-être maintenant : « C'était une bonne lecture, mais comment puis-je construire mon propre serveur MCP pour résoudre mon problème de contexte ? »

Patience. J'aurai quelque chose à te proposer dans les prochaines semaines !

Pd

Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web et le parcours humain qui l'entoure.

Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.

Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.

J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous, cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !

Réactions

Discussions

Ajouter un commentaire

Vous devez être connecté pour accéder à cette fonctionnalité.

Soutenez mon travail
Suivez-moi sur