Accueil / Articles

Conception de surfaces d’outil à efficacité en tokens pour des serveurs MCP agents

Apprenez à regrouper des dizaines de définitions d’outils MCP en un petit nombre d’outils axés sur un domaine et orientés vers des actions, sans perdre aucune fonctionnalité de base.

Éditeur : REACTAPP.TOP19 min de lecture4404 mots

Sources: **From 30 Tools to 3: Designing a Token-Efficient MCP Tool Surface**

Le problème typique avec les systèmes d’agents modernes n’est pas un manque d’outils.

C’est plutôt l’existence de trop nombreux outils.

Au fur et à mesure que les capacités de l’agent s’étendent, il est courant de le connecter à Jira, GitLab, Confluence, Sentry, Elasticsearch, Jaeger, diverses bases de données, des tableaux de bord de surveillance, des API internes, des pipelines de déploiement et d’innombrables autres systèmes.

Chacune de ces intégrations expose généralement une longue liste d’opérations :

Jira
├── searchIssues
├── getIssue
├── createIssue
├── updateIssue
├── addComment
├── transitionIssue
├── getTransitions
├── assignIssue
└── ...

GitLab
├── listProjects
├── getProject
├── listIssues
├── createIssue
├── updateIssue
├── listMergeRequests
├── getMergeRequest
├── createComment
└── ...

Confluence
├── searchPages
├── getPage
├── createPage
├── updatePage
└── ...

Il ne faut pas longtemps pour devoir gérer 30, 50 ou même des centaines de définitions d’outils.

À première vue, cela semble être un problème de capacités brutes.

En réalité, il s’agit d’un problème de surface d’outils.

L’agent n’a pas besoin de pouvoir faire moins de choses.

Ce dont il a besoin, c’est de moins de outils de niveau supérieur parmi lesquels choisir.

Cet article présente une méthode pour réduire les interfaces MCP ou les outils surchargés à un petit nombre d’outils axés sur un domaine donné, sans perdre aucune des fonctionnalités de base.

L’idée principale est simple :

Réduisez l’interface de l’outil, pas l’ensemble des capacités.

Au lieu de mettre à disposition quelque chose comme :

30 MCP tools

on peut mettre à disposition :

3–5 domain tools

et s’appuyer sur un champ action pour diriger chaque appel en interne.

Ainsi, un ensemble comme :

jira_search
jira_get_issue
jira_create_issue
jira_update_issue
jira_add_comment
jira_transition_issue
...

se transforme en quelque chose comme :

jira({
  action: "search",
  ...
})

jira({
  action: "getIssue",
  ...
})

jira({
  action: "createIssue",
  ...
})

Le backend conserve toutes les capacités originales.

Cependant, du point de vue du modèle, l’interface de l’outil est considérablement plus petite.

1. Le problème des interfaces d’outils trop grandes

Un serveur MCP n’est pas uniquement un mécanisme d’exécution.

Ce dernier fait également partie de ce que le modèle doit conserver en contexte.

Chaque fois qu’un agent se connecte à un serveur MCP, le modèle doit généralement traiter :

  • les noms des outils
  • leurs descriptions
  • les schémas d’entrée
  • les paramètres
  • les ensembles de valeurs possibles
  • les contraintes
  • parfois des métadonnées supplémentaires

Imaginez un agent relié à 40 outils différents.

Même si chaque schéma individuel est compact, l’ensemble peut rapidement devenir volumineux.

De plus, le modèle doit maintenant faire face à un éventail de décisions plus important :

User request
     │
     ▼
Which tool?
     │
 ┌───┼────┬────┬────┬────┐
 ▼   ▼    ▼    ▼    ▼    ▼
T1   T2   T3   T4   T5   ...

Il doit distinguer de nombreuses opérations qui sont conceptuellement proches.

Prenons cet exemple :

jira_search_issues
jira_search_projects
jira_get_issue
jira_get_issue_comments
jira_get_issue_transitions
jira_get_issue_worklogs

Tous ces éléments appartiennent au même domaine conceptuel.

Considérer chacun d’eux comme une capacité de niveau supérieur entièrement indépendante n’apporte que peu de valeur.

2. L’idée principale : le multiplexage des outils

Le truc consiste à ajouter une couche intermédiaire, un champ de discrimination :

{
  "action": "search",
  "query": "authentication bug"
}

Plutôt que de définir :

jira_search

on ne montre que :

jira

L’outil lui-même devient un routeur léger.

Structuralement, cela ressemble à :

jira
                      │
                 action field
                      │
       ┌──────────────┼──────────────┐
       ▼              ▼              ▼
    search         getIssue       createIssue
       │              │              │
       ▼              ▼              ▼
 searchHandler   issueHandler   createHandler

L’idée clé est que l’outil n’est pas la même chose que la capacité.

L’outil n’est qu’une interface orientée vers l’extérieur.

C’est le champ d’action qui transporte réellement la capacité.

Résultat :

30 capabilities
       ↓
3 domain-oriented tools

sans perte de fonctionnalités au cours du processus.

3. Utiliser des limites de domaine, pas des regroupements arbitraires

Une erreur fréquente consiste à raisonner comme suit :

"J’ai 30 outils, donc je regrouperai 10 opérations dans chacun d’eux."

Cela n’est pas le but.

Le regroupement doit être guidé par des domaines sémantiques, et non par des tailles de catégories arbitraires.

Par exemple :

jira
├── search
├── issue
├── comment
├── transition
└── project

gitlab
├── project
├── issue
├── mergeRequest
└── pipeline

observability
├── search
├── trace
├── log
└── error

Le regroupement idéal varie en fonction de l’intégration.

Pour une base de données, quelque chose comme :

db_tables
db_query
db_advanced

peut fonctionner très bien.

Pour Jira, vous préférerez peut-être :

jira_issues
jira_projects
jira_search

Pour GitLab, un agencement tel que :

gitlab_repository
gitlab_issues
gitlab_mergeRequests

pourrait être plus adapté.

Il n’existe pas de nombre fixe qui fonctionne partout.

L’objectif est de trouver la surface d’outil la plus réduite qui respecte néanmoins des limites sémantiques claires et significatives.

4. Un exemple de base de données

Un connecteur de base de données constitue une bonne illustration, car les API de bases de données ont tendance à exposer une longue liste d’opérations individuelles.

Au lieu de définir des entrées séparées telles que :

listTables
getTableSchema
getSampleData
getTableSize
executeQuery
getDatabaseInfo
listRelationships
getIndexes
profileColumn
searchSchema
listProcedures
getTriggers
compareSchemas

vous pouvez les regrouper en :

db_tables
db_query
db_advanced

Le premier de ces outils peut s’appuyer sur un schéma tel que :

const tablesSchema = z.discriminatedUnion("action", [
  z.object({
    action: z.literal("list"),
    schema: schemaField,
  }),
  z.object({
    action: z.literal("schema"),
    tableName: z.string(),
    schema: schemaField,
  }),
  z.object({
    action: z.literal("sampleData"),
    tableName: z.string(),
    schema: schemaField,
    rowCount: z.number().optional().default(10),
  }),
  z.object({
    action: z.literal("size"),
    tableName: z.string(),
    schema: schemaField,
  }),
]);

Du point de vue du modèle, il n’y a qu’un seul outil :

db_tables

appuyé par un ensemble clairement défini d’actions possibles :

list
schema
sampleData
size

Au niveau du runtime, les appels sont tout de même acheminés vers quatre gestionnaires distincts :

switch (args.action) {
  case "list":
    return handleListTables(args.schema);

  case "schema":
    return handleGetTableSchema(
      args.tableName,
      args.schema
    );
  case "sampleData":
    return handleGetSampleData(
      args.tableName,
      args.schema,
      args.rowCount
    );
  case "size":
    return handleGetTableSize(
      args.tableName,
      args.schema
    );
}

Cette séparation est importante :

La fusion a lieu au niveau de l’interface MCP, et non au sein de la logique métier sous-jacente.

Chaque gestionnaire interne reste testable de manière indépendante et facile à maintenir.

5. Les unions discriminées de Zod sont une solution idéale

Dans les bases de code TypeScript, z.discriminatedUnion() offre un moyen simple de modéliser ce schéma.

Par exemple :

const querySchema = z.discriminatedUnion("action", [
  z.object({
    action: z.literal("execute"),
    query: z.string(),
    params: z.record(z.string()).optional(),
    limit: z.number().optional().default(100),
  }),
  z.object({
    action: z.literal("info"),
  }),
]);

À partir de là, le type peut être dérivé directement :

type QueryInput = z.infer<typeof querySchema>;

Cette approche vous offre trois avantages pratiques :

  1. Un seul outil MCP.
  2. Un routage explicite basé sur le champ d’action.
  3. Une validation stricte appliquée en temps de exécution.

La structure globale de l’architecture ressemble à ceci :

MCP Tool
                │
                ▼
        Discriminated Union
                │
          ┌─────┴─────┐
          │  action   │
          └─────┬─────┘
                │
       ┌────────┼────────┐
       ▼        ▼        ▼
    Handler A Handler B Handler C
       │        │        │
       └────────┼────────┘
                ▼
             Backend

C’est bien plus simple pour un modèle de naviguer ainsi que d’utiliser des dizaines d’outils de niveau supérieur non liés entre eux.

6. Le nombre d’outils n’est pas égal au nombre de capacités

C’est sans doute l’idée la plus importante de cette approche.

Supposons que vous obteniez :

3 tools
13 actions

Cela ne signifie pas que dix capacités ont disparu. Ce que vous avez réellement, c’est :

3 external interfaces
13 internal capabilities

En bref :

Tool count ≠ capability count

Cette distinction gagne en importance à mesure que les systèmes d’agents s’agrandissent. L’ensemble des outils d’une grande organisation pourrait ressembler à ceci :

Jira             20 operations
GitLab            25 operations
Confluence        15 operations
Sentry             8 operations
ELK               10 operations
Jaeger             6 operations
Database           20 operations

Cela peut facilement entraîner plus de 100 opérations individuelles. Exposer chacune d’elles directement au modèle crée une interface trop encombrée. Au lieu de cela, on peut les regrouper en quelque chose comme :

jira
gitlab
confluence
sentry
observability
database

tout en laissant le nombre total d’opérations sous-jacentes inchangé.

7. Pourquoi cela peut réduire l’utilisation de tokens

Le principal avantage réside dans la diminution du volume de métadonnées des outils que le modèle doit analyser. Au lieu de lister :

Tool 1
Tool 2
Tool 3
Tool 4
...
Tool 30

on présente :

Tool A
Tool B
Tool C

Cela peut réduire :

  • les descriptions des outils
  • les métadonnées de paramètres répétées
  • les explications sémantiques redondantes
  • la complexité du choix parmi les outils de niveau supérieur
  • l’espace de contexte occupé par les définitions des outils

Cela dit, cela ne doit pas être présenté comme une économie proportionnelle garantie. Passer de :

30 tools → 3 tools

Ce n’est pas automatiquement traduit en :

90% lower token cost

car les schémas fusionnés peuvent eux-mêmes devenir plus volumineux. Une manière plus précise de le formuler :

Regrouper plusieurs opérations en un seul outil a tendance à réduire la quantité de métadonnées que l’agent doit lire, bien que l’économie réelle en termes de tokens ou de temps de réponse dépende de facteurs tels que la taille du schéma combiné, le degré de détail des descriptions des champs, les particularités du traitement des définitions d’outils par le fournisseur de modèles, ainsi que la manière spécifique dont votre framework d’agent gère les appels aux outils.

Cette nuance mérite d’être prise en compte.

8. Regrouper tout ensemble n’est pas automatiquement mieux

Il existe un compromis qu’il convient de souligner ici.

Imaginons un seul outil qui expose 40 actions différentes :

enterprise({
  action: ...
})

avec des actions telles que :

createCustomer
deleteCustomer
searchInvoice
rotateCredentials
deployService
createRepository
getTrace
searchLogs
...

Cela est techniquement réalisable.

Cependant, cela constitue un mauvais design.

Le modèle est désormais bloqué dans l’analyse d’un schéma massif.

30 tools

à :

1 giant tool

La meilleure approche est la consolidation selon les lignes de domaine.

Par exemple :

customer
billing
repository
observability
deployment

Cinq outils pourraient être le bon objectif, et non un seul.

Cela mène à un principe directeur :

Réduisez la surface des outils, mais ne le faites pas de manière négligente.

9. Le schéma fait également partie de l’interface de l’agent

Lorsqu’un seul outil couvre plusieurs fonctionnalités, le paramètre action prend une grande importance.

Une version insuffisante :

action: z.string()

Une amélioration :

action: z.enum([
  "search",
  "get",
  "create",
  "update"
])

C’est l’option la plus fiable lorsque chaque action nécessite des paramètres distincts :

z.discriminatedUnion("action", [
  searchSchema,
  getSchema,
  createSchema,
  updateSchema,
])

Ici, l’action choisie et ses arguments attendus sont liés grâce à une véritable sécurité de type.

Par exemple :

action = "search"
→ query required
action = "get"
→ issueId requiredaction = "create"
→ title + description required

Cela est bien plus précis qu’un schéma à type lâche rempli de champs optionnels pour chaque action possible.

10. Complications des schémas JSON sous MCP

Il y a une complication supplémentaire dans la pratique.

Les schémas des outils MCP sont finalement compilés en schémas JSON.

En fonction du convertisseur utilisé, une union discriminée Zod peut générer :

{
  "anyOf": [...]
}

ou bien :

{
  "oneOf": [...]
}

Certaines parties du pipeline de schéma MCP partent du principe que la racine du schéma est un objet simple.

Ce principe n’est pas toujours valable.

Dans au moins une implémentation réelle, la couche de compatibilité Zod intégrée au SDK MCP a été conçue autour des formes d’objets et n’a pas réussi à gérer correctement une union discriminée.

La solution consiste à ajouter une étape de normalisation pour le schéma généré.

Conceptuellement, cela se présente comme suit :

Zod discriminated union
          │
          ▼
     JSON Schema
          │
          ▼
   MCP normalization
          │
          ▼
MCP-compatible object schema

La leçon architecturale est la suivante :

Réorganisez le schéma que vous montrez au modèle sans toucher au niveau de strictesse de votre validateur réel.

Votre schéma Zod original doit rester la seule source de vérité pour la validation.

11. Deux schémas différents : un pour le modèle, un pour l’exécution

Cela souligne une distinction plus large qui mérite d’être clarifiée.

Il s’agit en réalité de deux tâches distinctes :

LLM-facing representation
        │
        ▼
Tool selection and argument generation
Runtime representation
        │
        ▼
Validation and execution

Le schéma affiché à l’LLM doit être :

  • facile à lire
  • compact
  • unambiguous
  • simple à choisir parmi les options
  • Le schéma utilisé en temps de exécution doit être :

    • strict
    • sûr
    • prévisible
    • final

    Ces deux représentations n’ont pas besoin de correspondre exactement. Une forme d’objet simplifiée et aplatie peut aider un modèle à choisir l’action appropriée plus facilement, tandis que l’union discriminée sous-jacente assure toujours la validation en arrière-plan.

    Cela se généralise en une règle plus large pour les systèmes d’agents :

    Ajustez les schémas en fonction de la manière dont les modèles les lisent, mais ne comptez jamais uniquement sur le schéma destiné au modèle comme unique ligne de défense.

    12. Les descriptions ont plus d’importance une fois les outils fusionnés

    Avec 30 outils distincts, chacun peut se permettre une description étroite et spécifique.

    Avec 3 outils, la description doit communiquer l’ensemble des actions possibles.

    Par exemple :

    Unified tool for advanced database analysis.
    
    ACTIONS:
    relationships
      List foreign key relationships.
    indexes
      Get indexes for a table.
    profileColumn
      Analyze nullability, cardinality and top values.
    searchSchema
      Search tables and columns.
    listProcedures
      List stored procedures.
    triggers
      Get triggers.
    compareSchemas
      Compare two table definitions.
    

    Ce n’est pas seulement de la documentation destinée aux humains.

    Celui-ci fait partie du mécanisme par lequel le modèle dirige ses appels.

    Une façon utile de l’envisager :

    Tool name
        +
    Description
        +
    Action enum
        +
    Parameter schema
            ↓
    Agent routing signal
    

    Ainsi, la consolidation des outils devrait vous inciter à rédiger des descriptions plus minutieuses, et non moins nombreuses.

    13. Ce schéma n’est pas limité aux bases de données

    La même approche est encore plus efficace lorsqu’on l’applique à un ensemble plus large de systèmes d’ingénierie.

    Jira

    Au lieu d’exposer :

    searchIssues
    getIssue
    createIssue
    updateIssue
    addComment
    transitionIssue
    assignIssue
    getTransitions
    

    exposer :

    jira_issues
    

    avec un ensemble d’actions tel que :

    search
    get
    create
    update
    comment
    transition
    assign
    

    GitLab

    Au lieu de présenter des outils distincts pour les répertoires, les problèmes, les demandes de fusion et les pipelines :

    gitlab_repository
    gitlab_issues
    gitlab_mergeRequests
    gitlab_pipelines
    

    On peut les regrouper en un seul point d’entrée, chacun disposant de son propre ensemble d’actions ciblées.

    Confluence / Wiki

    Au lieu de disperser les opérations sur plusieurs outils :

    searchPages
    getPage
    createPage
    updatePage
    deletePage
    getChildren
    

    On les fusionne en un seul :

    wiki
    

    Guidé par une liste d’actions telle que :

    search
    get
    create
    update
    children
    

    Sentry

    Une structure similaire fonctionne également ici :

    sentry
    

    Soutenu par :

    searchIssues
    getIssue
    events
    releases
    projects
    

    ELK

    La même logique s’applique à la recherche de logs :

    logs
    

    Avec :

    search
    aggregate
    fields
    indices
    

    Jaeger

    Et pour le traçage distribué :

    tracing
    

    Avec :

    search
    getTrace
    services
    operations
    

    Aucun de ces exemples ne dépend du nom spécifique choisi. Ce qui compte, c’est l’approche architecturale sous-jacente, et non les étiquettes exactes utilisées pour chaque action.

    14. Vous pouvez envelopper des serveurs MCP existants au lieu de les reconstruire

    L’un des avantages pratiques de cette conception est que l’on n’a presque jamais besoin de réimplémenter une intégration à partir de zéro. Des serveurs MCP prêts à l’emploi existent déjà pour de nombreuses plateformes courantes. Le problème, c’est que leur interface d’outils par défaut n’est souvent pas adaptée aux besoins d’un agent donné.

    Au lieu de connecter directement un agent à un ensemble complexe d’outils :

    Agent
      │
      ├── 20 Jira tools
      ├── 20 GitLab tools
      └── 15 Confluence tools
    

    on peut insérer une couche légère et conçue spécifiquement à cet effet entre eux :

    Agent
                       │
                 Custom MCP Layer
                       │
            ┌──────────┼──────────┐
            ▼          ▼          ▼
          Jira       GitLab    Confluence
            │          │          │
         existing   existing   existing
           APIs       MCP        MCP
    

    Cette couche personnalisée agit comme une facade destinée aux agents. Elle est particulièrement utile lorsque l’intégration de base fonctionne correctement mais expose simplement des opérations bien plus détaillées que celles dont l’agent devrait avoir besoin pour raisonner directement.

    15. Considérer MCP comme une couche d’adaptation

    Cela mène à une idée architecturale plus générale. Une configuration conventionnelle ressemble à ceci :

    Agent → MCP → Service
    

    Une configuration personnalisée ressemble plutôt à ceci :

    Agent
      ↓
    Agent-oriented MCP facade
      ↓
    Service-specific MCP/API
      ↓
    Service
    

    La façade intermédiaire peut assumer des responsabilités telles que :

    • consolider les outils
    • appliquer des vérifications de permissions
    • normaliser les entrées
    • normaliser les sorties
    • journaliser
    • traçer
    • mettre en cache
    • limiter le débit
    • appliquer des règles de politique spécifiques à l’organisation

    Sous cet angle, la couche MCP cesse d’être simplement un mécanisme de transport pour devenir une véritable couche d’interface pour l’agent.

    16. Le contrôle d’accès reste au niveau de l’action

    Il existe une conséquence en matière de sécurité qui mérite d’être soulignée. Si vous fusionnez :

    jira_create
    jira_update
    jira_delete
    

    en une seule appelation de type :

    jira({
      action: ...
    })
    

    vous ne pouvez pas simplement accorder une permission générale telle que :

    permission: jira
    

    Et supposer que chaque opération sous-jacente présente un risque égal. Les vérifications de permissions doivent continuer à s’effectuer au niveau de chaque action individuelle, par exemple :

    jira.search       → allowed
    jira.get          → allowed
    jira.create       → allowed
    jira.update       → approval required
    jira.delete       → denied
    

    Le outil consolidé n’est qu’une simplification au niveau de l’interface — il ne doit jamais devenir un raccourci qui accorde silencieusement un accès plus large que prévu.

    17. Les journaux doivent enregistrer l’action, et non seulement l’outil

    Lorsque les outils sont fusionnés, enregistrer uniquement le nom de l’outil dans les journaux cesse d’être utile. Une entrée de journal faible ressemble à ceci :

    tool = jira
    

    Une meilleure version inclut l’action :

    {
      "tool": "jira",
      "action": "search",
      "duration_ms": 142
    }
    

    Et une version encore plus détaillée ajoute des informations supplémentaires sur l’exécution :

    {
      "tool": "jira",
      "action": "search",
      "status": "success",
      "duration_ms": 142,
      "request_id": "...",
      "agent_id": "...",
      "user_id": "..."
    }
    

    La véritable unité d’exécution est désormais le couple :

    tool + action
    

    plutôt que l’outil seul :

    tool
    

    Cette distinction est très importante lorsque vous essayez de déboguer ce qu’un agent a réellement fait.

    18. Conservez les gestionnaires sous-jacents séparés

    La consolidation doit avoir lieu à la frontière exposée à l’agent, et non à l’intérieur de votre implémentation. Évitez de regrouper tout le contenu dans un seul gestionnaire massif :

    async function jira(args) {
      // everything
    }
    

    Préférez plutôt envoyer les requêtes vers des fonctions distinctes :

    async function jira(args: JiraInput) {
      switch (args.action) {
        case "search":
          return searchIssues(args);
        case "get":
          return getIssue(args);
        case "create":
          return createIssue(args);
        case "update":
          return updateIssue(args);
      }
    }
    

    Chaque gestionnaire reste testable de manière indépendante :

    jira()
      │
      ├── searchIssues()
      ├── getIssue()
      ├── createIssue()
      └── updateIssue()
    

    Cela permet de maintenir votre base de code même si le nombre d’outils externes diminue.

    19. Une règle pratique pour décider de ce à fusionner

    Une bonne heuristique pour déterminer si deux opérations appartiennent au même outil est :

    Un ingénieur regrouperait-il naturellement ces opérations sous une seule capacité ?

    Par exemple, celles-ci appartiennent clairement ensemble :

    Jira search
    Jira get issue
    Jira update issue
    

    Mais ceux-ci ne le font pas :

    Jira update issue
    Elasticsearch search logs
    Create AWS infrastructure
    

    Une deuxième vérification utile :

    La décision entre ces opérations nécessite-t-elle généralement les mêmes informations contextuelles ?

    Si la réponse est oui, les fusionner est généralement avantageux. Sinon, il vaut mieux les conserver en outils séparés.

    20. Lorsque fusionner des outils est une mauvaise idée

    Parfois, conserver des outils séparés est simplement la meilleure solution.

    Domaines non liés

    database
    payments
    infrastructure
    email
    

    ne devrait pas être simplifié en quelque chose comme :

    enterprise({
      action: ...
    })
    

    Ensembles d’actions trop volumineux

    Si un outil unique finit par couvrir cinquante actions distinctes, le schéma résultant peut devenir encombré et difficile à gérer pour le modèle, annulant ainsi l’objectif de la consolidation.

    Opérations avec des privilèges élevés

    Des actions telles que :

    deleteProductionData
    rotateCredentials
    deployProduction
    

    pourraient nécessiter leurs propres limites de permissions, même si elles se trouvent techniquement dans le même domaine que des opérations à moindre risque.

    Opérations avec des garanties de cycle de vie différentes

    Les appels en lecture seule et les appels destructeurs fonctionnent parfois mieux en tant qu’interfaces séparées :

    jira_read
    jira_write
    

    plutôt que d’être regroupés dans un seul outil trop volumineux.

    21. Moins d’outils n’est pas l’objectif réel

    L’objectif n’est pas de réduire tout cela à :

    30 → 1
    

    La véritable cible est :

    30 → smallest useful semantic surface
    

    Dans un système, le nombre approprié peut ressembler à ceci :

    30 → 3
    

    Dans un autre :

    30 → 7
    

    Et dans un autre encore :

    30 → 12
    

    Ce que vous optimisez réellement, c’est :

    minimize unnecessary tool metadata
    

    sans sacrifier :

    semantic clarity
    validation
    security
    maintainability
    

    22. Une structure architecturale réutilisable

    Une configuration scalable suit généralement cette structure :

    Agent
                              │
                              ▼
                    ┌──────────────────┐
                    │  Agent MCP Layer │
                    └────────┬─────────┘
                             │
              ┌──────────────┼──────────────┐
              ▼              ▼              ▼
            jira           gitlab          wiki
              │              │              │
         ┌────┼────┐    ┌────┼────┐    ┌────┼────┐
         ▼    ▼    ▼    ▼    ▼    ▼    ▼    ▼    ▼
      search get create issues MR pipeline search get update
              │              │              │
              └──────────────┼──────────────┘
                             ▼
                     Existing APIs/MCPs
    

    Le modèle interagit avec une interface simple et claire. En dessous, la couche d’intégration conserve toujours accès à l’ensemble des opérations possibles.

    23. Repenser l’usage de MCP

    Un modèle mental courant pour MCP est :

    "Transformer chaque appel API en son propre outil."

    C’est un bon point de départ.

    Mais pour les systèmes en production, la question plus utile devient :

    Que le modèle doit-il vraiment voir ?

    Ces deux approches ne conduisent pas toujours au même design.

    Considérons une API REST qui expose :

    GET /issues
    GET /issues/:id
    POST /issues
    PATCH /issues/:id
    POST /issues/:id/comments
    POST /issues/:id/transition
    

    Il n’y a pas de nécessité de créer un outil distinct pour chaque route HTTP. La couche MCP peut plutôt servir d’enveloppe sémantique autour de ces points d’accès — tout comme on conçoit une API en fonction des besoins spécifiques d’un utilisateur, plutôt que de simplement exposer directement le service sous-jacent ou le schéma de base de données.

    24. Considérer la conception des outils comme un problème d’UX pour les agents

    La conception classique d’une API se pose cette question :

    Quels points d’accès existent ?

    La conception destinée aux agents pose une question différente :

    Quelle décision le modèle tente-t-il réellement de prendre ?

    Un ingénieur humain qui intègre une API pourrait recourir à :

    getIssue
    getIssueComments
    getIssueTransitions
    getIssueWorklogs
    

    car chaque appel correspond clairement à une opération.

    D’un autre côté, un agent peut fonctionner mieux avec :

    jira_issue({
      action: "get",
      ...
    })
    

    en combinaison avec :

    jira_issue({
      action: "comments",
      ...
    })
    

    car il choisit d’abord le domaine, puis affine le choix vers l’action spécifique — un processus de routage en deux étapes :

    Domain
      ↓
    Action
      ↓
    Arguments
    

    au lieu de forcer le modèle à résoudre :

    One large global tool-selection problem
    

    25. Un modèle minimal réutilisable

    Cette approche se traduit facilement en TypeScript :

    const toolSchema = z.discriminatedUnion("action", [
      searchSchema,
      getSchema,
      createSchema,
      updateSchema,
    ]);
    
    server.registerTool(
      "jira",
      {
        description: "...",
        inputSchema: toolSchema,
      },
      async (args) => {
        switch (args.action) {
          case "search":
            return search(args);
          case "get":
            return get(args);
          case "create":
            return create(args);
          case "update":
            return update(args);
        }
      }
    );
    

    Le code lui-même reste assez simple. Le véritable travail de conception consiste à répondre aux questions suivantes :

    1. Quelles opérations sont logiquement regroupées ?
    2. Quelle convention de nommage les actions doivent-elles suivre ?
    3. Qu’est-ce qui doit figurer dans la description de l’outil ?
    4. Quels paramètres sont obligatoires pour chaque action ?
    5. Quelles actions nécessitent des vérifications de permissions supplémentaires ?
    6. Comment les réponses doivent-elles être structurées de manière cohérente ?
    7. Comment chaque action doit-elle être enregistrée et traçée ?

    26. Suivez l’impact avec des chiffres réels

    La consolidation des outils ne doit pas reposer sur la foi — vous devez le vérifier à l’aide de données.

    Au préalable du changement, vous pourriez enregistrer quelque chose comme :

    Tools: 32
    Tool schema tokens: X
    Average tool-selection latency: Y
    Tool-selection errors: Z
    

    Après l’introduction du design multiplexé, vous mesurez à nouveau les mêmes indicateurs :

    Tools: 5
    Tool schema tokens: X'
    Average tool-selection latency: Y'
    Tool-selection errors: Z'
    

    Parmi les signaux les plus utiles à suivre, on trouve :

    • le nombre d’outils mis à disposition
    • la taille totale des schémas
    • le nombre de tokens consommés par la requête ou le contexte
    • le degré de précision avec lequel l’agent choisit l’outil approprié
    • la fréquence à laquelle des appels d’outils invalides sont effectués
    • la fréquence à laquelle la validation des arguments échoue
    • le temps de réponse
    • la fréquence à laquelle les exécutions réussissent réellement
    • le fait que l’agent termine la tâche dans son ensemble

    L’objectif n’est pas simplement de poursuivre :

    fewer tools
    

    mais plutôt d’optimiser pour :

    better agent performance per unit of context
    

    27. Un modèle plus général pour les API destinées aux agents

    Cette idée dépasse de loin le cadre du MCP lui-même.

    Vous constaterez que ce même principe s’applique à :

    • l’appel de fonctions
    • les définitions d’outils au style OpenAI
    • les SDK
    • les environnements d’exécution personnalisés pour les agents
    • les moteurs de flux de travail
    • les architectures de plugins
    • les adaptateurs REST vers les agents
    • les ponts GraphQL vers les agents

    En son essence, ce modèle se présente comme suit :

    Traditional API
           ↓
    Many low-level operations
           ↓
    Agent-oriented facade
           ↓
    Small semantic tool surface
           ↓
    Action routing
           ↓
    Strict execution layer
    

    Ce n’est pas une astuce spécifique aux bases de données.

    C’est un modèle général pour concevoir des interfaces consommées par les agents.

    28. Le principe fondamental

    Le modèle mental à retenir est simple :

    ┌──────────────────────┐
                     │      30 APIs         │
                     │    capabilities      │
                     └──────────┬───────────┘
                                │
                         Consolidation
                                │
                                ▼
                     ┌──────────────────────┐
                     │      3–5 tools       │
                     │  semantic domains    │
                     └──────────┬───────────┘
                                │
                         Action routing
                                │
                                ▼
                     ┌──────────────────────┐
                     │    30 handlers       │
                     │  original abilities │
                     └──────────────────────┘
    

    Le changement significatif n’est pas :

    30 capabilities → 3 capabilities
    

    En réalité, il s’agit de :

    30 exposed tools → 3 exposed interfaces
    

    Rien dans les capacités du système ne change.

    Ce qui change, c’est la partie de cette surface que le modèle doit traiter directement.

    Conclusion

    À mesure que les systèmes agents s’intègrent de plus en plus à votre infrastructure, le nombre d’outils à leur disposition ne cesse d’augmenter.

    Des systèmes tels que Jira, GitLab, Confluence, Sentry, ELK, Jaeger, diverses bases de données, des fournisseurs cloud et des services internes peuvent chacun contribuer avec des dizaines ou des centaines d’opérations distinctes.

    Confier chacune de ces opérations au modèle en tant qu’outil séparé n’est que rarement l’architecture appropriée.

    Une stratégie plus efficace consiste à créer une couche d’outils destinée aux agents au-dessus d’eux :

    Many low-level operations
              ↓
    Semantic domain grouping
              ↓
    Discriminated action schemas
              ↓
    Small MCP tool surface
              ↓
    Strict runtime validation
              ↓
    Original capabilities
    

    La règle directrice est :

    Réduisez le nombre d’outils dont le modèle doit tenir compte — et non le nombre de fonctionnalités que votre système offre réellement.

    L’intégration de trente opérations sous-jacentes n’implique pas nécessairement la présence de trente outils visibles pour le modèle.

    Il peut suffire de trois interfaces soigneusement conçues, chacune disposant d’un champ d’action bien défini.

    Cela devient encore plus précieux lorsque l’on cesse de considérer les serveurs MCP existants comme des contrats fixes et intouchables, pour les traiter plutôt comme des éléments de construction que l’on peut envelopper, remodeler et ajuster afin qu’ils répondent aux besoins réels de l’agent.

    À l’avenir, une bonne conception MCP consistera moins à mettre en évidence chaque opération prise en charge par une API, et plus à créer l’interface la plus petite et la plus pratique dont un agent a besoin pour accomplir sa tâche.

    Lectures complémentaires

  • MCP sans état : Échelle des serveurs sans sessions ni échanges — Comment un protocole de contexte de modèle sans état élimine les sessions et les échanges, et comment les requêtes métadonnées multi-étapes, les en-têtes de routage, le cache et les tâches permettent de le maintenir utilisable.