Appeler des API avec le Code Mode et garder l'essentiel
Fait partie de la série Construire le Code Mode pour les API avec MCP et Workers
Nous avons vu comment laisser le modèle écrire du code JavaScript pour rechercher dans une spécification OpenAPI plus volumineuse que la fenêtre de contexte. La spécification peut continuer à grandir, même si l'environnement d'exécution conserve des limites concrètes sur le temps d'exécution, la mémoire et les données renvoyées.
Voyons maintenant comment laisser le modèle écrire du JavaScript pour interagir avec notre API à partir des connaissances recueillies dans la spécification OpenAPI.
Notre MCP a besoin d'un deuxième outil
Notre objectif était de créer un système qui facilite les interactions entre le LLM et notre API. Pour le moment, notre LLM peut explorer et comprendre l'API :
{
"code": "async () => {\n const results = [];\n for (const [path, methods] of Object.entries(spec.paths)) {\n for (const [method, operation] of Object.entries(methods)) {\n if (operation?.tags?.some(tag => tag.toLowerCase() === 'returns')) {\n results.push({\n method: method.toUpperCase(),\n path,\n operationId: operation.operationId,\n summary: operation.summary,\n description: operation.description,\n parameters: operation.parameters,\n requestBody: operation.requestBody,\n });\n }\n }\n }\n return results;\n}"
}[
{
"method": "GET",
"path": "/returns",
"operationId": "listReturns",
"summary": "List return requests",
"description": "List return requests, optionally narrowed to an order, order item, seller, or return status.",
"parameters": [],
"requestBody": null
}
]Il ne peut toutefois pas encore interagir avec elle.
Nous devons pour cela lui fournir un deuxième outil : l'exécuteur.
L'outil d'exécution
L'outil d'exécution complète l'outil de recherche. Sous le capot, il enveloppe un client API préconfiguré, ici une fonction fetch, qui permet au modèle d'appeler l'API grâce aux informations recueillies dans la spécification OpenAPI.
Dans les grandes lignes, il ressemble beaucoup à l'outil de recherche. Nous fournissons au modèle un environnement préconfiguré dans lequel écrire du code pour une API typée et prédéfinie. Le modèle orchestre une ou plusieurs requêtes selon la complexité de la tâche, puis ne renvoie que les données utiles.
export function registerExecuteTool(server: McpServer): void {
server.registerTool(
'execute',
{
title: 'MarketHub API Code Executor',
description: '...',
inputSchema: z.object({
code: z
.string()
.max(MAX_CODE_LENGTH)
.describe('An async arrow function that executes a MarketHub API request')
}),
},
async ({ code }) => {
try {
const result = await evaluateExecuteCode(code)
return { content: [{ type: 'text', text: formatExecuteResult(result) }] }
}
catch (error) {
return {
content: [{ type: 'text', text: `Execute failed: ${errorMessage(error)}` }],
isError: true
}
}
}
)
}La ressemblance avec l'outil de recherche saute aux yeux. Nous recevons une fonction fléchée JavaScript, nous l'évaluons et nous renvoyons son résultat.
Deux différences comptent :
- La description indique au modèle qu'il peut écrire du code pour l'API et qu'il doit ne renvoyer que les données utiles.
- Nous utilisons
evaluateExecuteCodeau lieu deevaluateSearchCode. Les deux fonctions évaluent du code, mais les données disponibles dans leur contexte diffèrent.
La description commence par expliquer brièvement le rôle de l'outil.
Execute JavaScript code against the local MarketHub API.
First use the 'search' tool to find the right endpoint, method, parameters, and request body, then write code using api.request().Elle fournit ensuite une interface TypeScript qui indique précisément au modèle comment écrire son code.
interface ApiRequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
path: string
query?: Record<string, string | number | boolean | undefined>
body?: unknown
contentType?: string
rawBody?: boolean
}
interface ApiResponse<T = unknown> {
success: boolean
status: number
result: T
errors: Array<{ code: string | number, message: string }>
messages: Array<{ code: string | number, message: string }>
}
declare const api: {
request: <T = unknown>(options: ApiRequestOptions) => Promise<ApiResponse<T>>
}Nous pouvons maintenant écrire du code pour l'API. L'interface ApiRequestOptions montre clairement que nous ne faisons rien de plus qu'envelopper fetch. Les paramètres sont presque identiques, mais simplifiés pour ne présenter au modèle que les informations nécessaires. Pour la réponse, nous préparons les données afin de réduire le travail du modèle.
Enfin, la description contient un petit exemple d'utilisation de l'outil.
async () => {
const response = await api.request({
method: 'GET',
path: '/products',
query: { 'status:eq': 'active', '_per_page': 5 }
})
return response.result.map(product => ({
id: product.id,
name: product.name,
status: product.status
}))
}Code Mode ou simple appel d'outil
Vous vous demandez peut-être pourquoi ne pas simplement fournir au modèle un appel d'outil classique, comme nous le faisons pour la plupart des MCP. Nous pourrions par exemple imaginer un outil nommé list_returns qui recevrait l'identifiant d'un vendeur et renverrait la liste des retours. Le modèle n'aurait qu'à l'appeler avec les bons paramètres pour obtenir le résultat.
Vous avez raison. Nous aurions pu procéder ainsi. Mais une API de 150 endpoints nécessiterait 150 outils. Nous pourrions réduire ce nombre en regroupant certains endpoints derrière un paramètre, comme le fait le MCP de GitHub. Son outil pull_request_read possède par exemple un paramètre method qui accepte les valeurs get, get_diff, get_status, get_files, get_commits, get_review_comments, get_reviews, get_comments et get_check_runs. Je vous laisse imaginer la complexité sous-jacente et le casse-tête du regroupement. Si GitHub ajoute une méthode demain, son MCP doit être mis à jour.
Le Code Mode permet aussi au modèle d'adapter l'entrée et la sortie de la requête. S'il sait qu'un endpoint renvoie une réponse très détaillée, il peut ne conserver que les champs nécessaires. Il peut également enchaîner les requêtes, créer des boucles, utiliser des conditions, réessayer lorsque le résultat ne correspond pas à ses attentes, etc. Le modèle orchestre librement la requête selon ses besoins.
async () => {
const sellers = (await api.request({
method: 'GET',
path: '/sellers',
query: { 'status:eq': 'active', '_per_page': 5 }
})).result
const results = []
for (const seller of sellers) {
const products = (await api.request({
method: 'GET',
path: '/products',
query: {
sellerId: seller.id,
_embed: 'variants',
_per_page: 10
}
})).result
for (const product of products) {
for (const variant of product.variants ?? []) {
results.push({
seller: seller.name,
product: product.name,
sku: variant.sku
})
}
}
}
return results
}Avec des appels d'outils directs, ce processus nécessite plusieurs allers-retours avec l'Agent. Le modèle reçoit chaque résultat, décide de la suite et envoie un nouvel appel. Le Code Mode conserve le contrôle du flux, le filtrage et la préparation du résultat dans le programme généré.
Ne renvoyer que les données utiles
Avec un peu de recul, cette approche peut sembler inutilement complexe. Comparons les deux options avant d'étudier les mesures en jetons.
L'instantané OpenAPI de Cloudflare utilisé dans cette série contient 23 792 523 caractères. Une division par quatre donne une estimation d'environ 6 millions de jetons, même si le nombre exact varie selon le modèle et le contenu.
| Point étudié | Appels d'outils directs | Code Mode |
|---|---|---|
| Tâches dépendantes | Le modèle reçoit chaque résultat intermédiaire avant de choisir l'appel suivant. | Le programme généré transmet les valeurs intermédiaires entre les appels et ne renvoie que sa projection finale. |
| API volumineuse | Le serveur doit exposer et documenter un ensemble d'outils parmi lesquels le modèle choisit. | Le modèle peut rechercher dans une spécification conservée côté serveur, puis composer les appels ciblés dont il a besoin. |
| Préparation des données | Le modèle reçoit souvent la réponse de chaque outil avant de pouvoir la réduire. | Le programme généré peut filtrer, agréger et projeter les données avant de les renvoyer. |
Un serveur d'outils directs bien conçu peut lui aussi traiter les données par lots et garder des réponses compactes. Le Code Mode devient surtout utile lorsqu'une tâche nécessite des appels dépendants, un traitement local ou une API qui évolue.
Pour l'illustrer, demandons à notre Agent de lister les 10 Workers qui consomment le plus de CPU :
Lors de cet enregistrement, l'Agent a utilisé 34 000 jetons.
Dans notre exemple, la spécification OpenAPI contient 89 105 caractères, soit environ 22 000 jetons avec la même approximation de quatre caractères par jeton.
Essayons maintenant quelques exemples pour mesurer le nombre de jetons nécessaires au modèle avec notre MCP Code Mode.
« Liste les demandes de retour au statut requested pour le vendeur seller-aurora, triées de la plus récemment mise à jour à la plus ancienne, et renvoie les 10 premiers résultats. »
- 18 100 jetons
Liste des demandes de retour d'un vendeur avec le Code Mode. « Crée un vendeur en attente nommé North Star Living, avec le slug north-star-living et l'adresse hello@north-star-living.example. »
- 16 900 jetons
Création d'un vendeur en attente avec le Code Mode. « Publie le produit product-desk-lamp en passant son statut à active. »
- 17 800 jetons
Publication d'un produit avec le Code Mode. « Supprime le produit product-desk-lamp. »
- 17 500 jetons
Suppression d'un produit avec le Code Mode.
Ces mesures proviennent d'un environnement local de test. Il ne s'agit pas d'un véritable benchmark. Retenez surtout que le modèle n'a pas besoin de toute la description de l'API pour interagir avec elle. Il construit son contexte à partir des seules informations nécessaires.
Vers une exécution plus sûre
Nous avons donné au modèle un outil pour rechercher dans la spécification OpenAPI. Il peut désormais exécuter lui-même les appels API et ne renvoyer que les données utiles. Mais est-ce sûr ?
Le code du modèle n'est pas fiable. C'est la première règle de la sécurité web : ne jamais faire confiance aux entrées utilisateur. Ici, le code généré par le modèle est une entrée utilisateur exécutée par un évaluateur local de confiance basé sur le constructeur Function. Dans le prochain article, nous verrons comment rendre cette exécution plus sûre grâce à un environnement dédié.
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 !
Discussions
Ajouter un commentaire
Vous devez être connecté pour accéder à cette fonctionnalité.