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 :

json
{
  "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}"
}
json
[
  {
    "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.

typescript
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 :

  1. 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.
  2. Nous utilisons evaluateExecuteCode au lieu de evaluateSearchCode. 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.

text
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.

typescript
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.

typescript
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
  }))
}
Exécution d'une requête API qui ne renvoie que les champs sélectionnés pour les produits.

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.

typescript
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 directsCode Mode
Tâches dépendantesLe 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 volumineuseLe 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éesLe 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 :

Liste des 10 Workers qui consomment le plus de CPU avec le Code Mode.

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.

  1. « 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.
  2. « 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.
  3. « Publie le produit product-desk-lamp en passant son statut à active. »

    • 17 800 jetons
    Publication d'un produit avec le Code Mode.
  4. « 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é.

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 !

Continuer la lectureRemplacer l'évaluateur par des Workers Cloudflare isolés

Réactions

Discussions

Ajouter un commentaire

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