Accueil / Articles / MCP pour les agents d’intelligence artificielle : standardisation de l’intégration des outils dans LangGraph

MCP pour les agents d’intelligence artificielle : standardisation de l’intégration des outils dans LangGraph

Cet article explique ce que MCP standardise réellement dans les systèmes d’intelligence artificielle agente, en comparant les intégrations de outils ad hoc à celles basées sur MCP au sein d’un orchestrateur LangGraph.

4323 mots

Introduction et résumé

Au terme de la deuxième partie, le système avait atteint un état véritablement coordonné : un ensemble d’agents spécialisés, chacun responsable d’une tâche précise, opérant sur un état partagé sous la supervision d’un orchestrateur qui décidait de ce qui devait être exécuté ensuite. Cependant, dans tous les exemples, on partait du principe que chaque agent avait déjà accès à tout ce dont il avait besoin au sein de cet état partagé, pouvant le lire chaque fois que c’était nécessaire.

Cette hypothèse est rarement vraie en environnement de production. Un agent a généralement besoin de quelque chose qui se trouve en dehors du graphe : une ligne d’une base de données, une réponse d’une API externe, un extrait d’une base de connaissances, ou tout autre ressource située au-delà des limites du système lui-même. Chaque fois qu’un agent doit accéder à ces ressources externes, il a besoin de son propre chemin pour y parvenir, et si chaque chemin doit être créé manuellement, on se retrouve à répéter le même type de travail d’intégration, légèrement modifié, pour chaque agent et chaque ressource externe.

Cette répétition est précisément le problème abordé dans cette étude, et elle explique pourquoi MCP est devenu un sujet récurrent au cours de l’année écoulée. Avant de décider s’il vaut la peine de l’adopter, il est utile de définir avec précision le problème qu’il résout, ainsi que d’examiner ce qui était nécessaire pour relier un outil à un agent avant même l’apparition de MCP.

Le problème que MCP prétend résoudre

Au préalable de l’apparition de MCP, pour permettre à un agent d’accéder à des ressources externes, il fallait créer manuellement une intégration sur mesure adaptée à cette ressource spécifique, conçue de la manière qui semblait pertinente au moment donné. Une ressource pouvait être accessible via une API REST, une autre via un client de base de données, et une autre encore via un SDK disposant de ses propres règles d’authentification et de gestion des erreurs. Chacune de ces différences devait être intégrée directement dans le code de l’agent lui-même.

C’est un bon compromis lorsque vous avez un seul agent qui communique avec une seule outil. Ce n’est plus le cas dès que le système se développe dans l’une ou l’autre direction. Ajoutez un deuxième agent qui a besoin de la même ressource, et vous devrez soit copier l’intégration, soit quelqu’un finira par la sortir dans un module partagé, généralement seulement après que la duplication se soit déjà installée. Préférez plutôt ajouter un deuxième outil, et le code de l’agent devra alors gérer en même temps deux schémas d’intégration complètement différents.

Ces intégrations se ressemblent rarement, car rien ne l’exige. Un wrapper peut réessayer automatiquement les appels échoués ; un autre ne le fera pas du tout. L’un peut afficher les échecs sous forme d’exceptions ; un autre les cache dans un champ de statut que l’utilisateur doit se souvenir de vérifier. Il n’existe pas de langage commun pour définir ce que signifie réellement « connecter un agent à un outil » — chaque intégration finit par y répondre selon ses propres règles.

C’est précisément cette lacune que MCP s’efforce de combler. Il ne met pas en place de nouvelles capacités dont les agents étaient dépourvus auparavant ; il standardise plutôt le mécanisme permettant d’accéder à des capacités déjà existantes, de sorte que connecter un nouvel agent à une outil existant, ou un nouvel outil à un agent existant, ne signifie plus avoir à rédiger une intégration sur mesure depuis zéro. Il devient beaucoup plus facile d’évaluer si cet objectif est réellement atteint une fois que l’on a vu à quoi ressemble concrètement l’approche ad hoc en code — c’est là que la discussion reprend.

Au préalable de MCP : connecter un outil de manière improvisée

Prenons une intégration plutôt ordinaire : un agent qui a besoin de consulter un système externe, maintenu ensemble par n’importe quel code de liaison permettant à la connexion de fonctionner.

import requests
class LookupToolClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.api_key = api_key
    def lookup(self, query: str) -> dict:
        response = requests.get(
            f"{self.base_url}/search",
            params={"q": query},
            headers={"Authorization": f"Bearer {self.api_key}"},
        )
        if response.status_code != 200:
            return {"error": f"lookup failed: {response.status_code}"}
        return response.json()
def agent_node(state: GraphState) -> dict:
    client = LookupToolClient(base_url="https://internal-tool.example.com", api_key="...")
    result = client.lookup(state["extracted_fields"]["query"])
    return {"tool_result": result}

En soi, il n’y a rien de mal à cela. Il s’agit d’un client HTTP compact, d’un peu de gestion des erreurs, ainsi que d’une fonction qui l’appelle depuis l’intérieur d’un node. Les problèmes commencent dès qu’un deuxième outil fait son apparition — pas une autre API REST cette fois, mais un client de base de données ayant une structure complètement différente :

import psycopg2
class RecordsClient:
    def __init__(self, connection_string: str):
        self.conn = psycopg2.connect(connection_string)
    def fetch_record(self, record_id: str) -> dict:
        with self.conn.cursor() as cur:
            cur.execute("SELECT * FROM records WHERE id = %s", (record_id,))
            row = cur.fetchone()
            if row is None:
                raise ValueError(f"no record found for {record_id}")
            return dict(zip([desc[0] for desc in cur.description], row))

Ces deux clients ne partagent aucune interface commune, aucune convention de nommage, ni même une méthode cohérente pour signaler une erreur : l’un renvoie un dictionnaire d’erreurs, tandis que l’autre lance directement une exception. Tout agent qui doit utiliser les deux doit apprendre ces particularités individuellement et gérer chacune selon ses propres règles. Si l’on ajoute à cela tous les outils supplémentaires dont le système aura besoin par la suite — son propre client, son propre schéma d’authentification, ses propres modes de défaillance — ce qui commençait par quelques petites intégrations se transforme en une véritable charge de maintenance, sans aucune structure commune pour les relier.

C’est là la base à retenir : il ne s’agit pas d’une intégration mal écrite, mais simplement d’une intégration typique, conçue de la manière dont se présentent généralement les intégrations d’outils lorsque rien ne force l’adoption d’une forme commune.

Ce que MCP standardise réellement

Avec cet exemple ad hoc encore en tête, il devient beaucoup plus facile de décrire précisément ce que fait MCP, sans recourir aux affirmations plus larges et plus vagues qui lui sont souvent faites.

À l’essentiel, MCP définit un protocole commun permettant d’exposer des outils à un agent, quel que soit leur fonctionnement interne ou le langage ou framework utilisé pour les créer. Au lieu que chaque outil fournisse son propre client sur mesure avec ses propres conventions, il est exposé via un serveur MCP qui annonce ses capacités dans un format standard et prévisible : un nom, une description, un schéma d’entrée et un schéma de sortie. Tout agent qui comprend ce protocole peut découvrir cet outil et l’appeler de la même manière qu’il appellerait n’importe quel autre outil, que ce soit un point de terminaison REST, une base de données ou quelque chose d’entièrement différent.

Cette standardisation couvre précisément trois domaines, et il est utile de spécifier lesquels, car on a tendance à penser que la portée de MCP est plus large qu’elle ne l’est en réalité.

  • Découverte : Un agent peut interroger un serveur MCP pour obtenir la liste des outils qu’il met à disposition et recevoir une réponse structurée, au lieu de compter sur des informations codées en dur quelque part ou documentées séparément de l’implémentation réelle.
  • Invocation : Chaque appel à un outil suit le même schéma, quel que soit l’outil en question — une requête à une structure définie, une réponse à une structure définie — plutôt que chaque client de devoir définir sa propre signature de méthode et son propre type de retour.
  • Gestion des erreurs : Les pannes sont signalées selon un format uniforme, de sorte qu’un agent n’a pas besoin de vérifier si un outil donné génère une exception, renvoie un champ d’erreur ou échoue par quelque autre moyen. La structure est toujours identique.
  • MCP ne permet pas d’éliminer le travail nécessaire pour écrire la logique sous-jacente d’un outil, ni de garantir que celui-ci se comportera correctement simplement parce qu’il est intégré au protocole. Il standardise le contrat entre un agent et un outil, mais pas la qualité ou la fiabilité de ce qui se trouve derrière ce contrat — un point que l’article aborde directement par la suite, une fois que les comparaisons précédentes ont rendu cette distinction concrète.

    Anatomie d’un serveur MCP

    Compte tenu de la normalisation décrite précédemment, il est utile d’examiner ce qui la met réellement en œuvre. Structuralement, un serveur MCP n’est rien de plus qu’un ensemble déclaré d’outils, chacun possédant son propre schéma, emballés au sein d’une couche de protocole qui permet à un agent de les découvrir et de les invoquer de manière cohérente.

    Pour définir un outil sur un serveur MCP, il faut au minimum déclarer trois éléments : un nom que l’agent utilise pour s’y référer, un schéma décrivant les entrées attendues, ainsi que la fonction qui s’exécute lorsque l’outil est effectivement invoqué.

    from mcp.server import Server
    from mcp.types import Tool
    server = Server("lookup-tools")
    @server.list_tools()
    async def list_tools() -> list[Tool]:
        return [
            Tool(
                name="lookup",
                description="Search for a record by query string",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"}
                    },
                    "required": ["query"],
                },
            )
        ]
    @server.call_tool()
    async def call_tool(name: str, arguments: dict) -> dict:
        if name == "lookup":
            return perform_lookup(arguments["query"])
        raise ValueError(f"unknown tool: {name}")
    

    Lorsque l’on compare cela avec les clients ad hoc évoqués précédemment, deux éléments se distinguent. Premièrement, le schéma d’entrée est déclaré explicitement dès le début, au lieu d’être inféré à partir des paramètres que prend par hasard une fonction — ce qui permet à la fois à un agent et à un réviseur humain de voir exactement ce que l’outil exige, sans avoir à examiner son implémentation. Deuxièmement, le serveur n’a besoin d’exposer que deux points d’entrée, list_tools et call_tool, quel que soit le nombre d’outils qu’il contient ou la manière dont ils fonctionnent en interne. Le fait que l’outil lookup communique avec une API REST, interroge une base de données ou fasse quelque chose d’entièrement différent reste entièrement caché derrière cette même interface à deux fonctions.

    Du côté de l’agent, se connecter à ce serveur a l’air identique, quel que soient les outils qu’il expose :

    from mcp.client import ClientSession
    async def call_lookup_tool(query: str) -> dict:
        async with ClientSession(server_params) as session:
            result = await session.call_tool("lookup", {"query": query})
            return result
    

    Comparez cela aux deux clients ad hoc précédemment mentionnés, l’un basé sur requests et l’autre sur psycopg2, chacun ayant sa propre structure et ses propres conventions. Ici, le code de l’agent reste identique quel que soit le fonctionnement interne d’une outil : il appelle session.call_tool avec un nom et un ensemble d’arguments, et reçoit toujours un résultat dans la même structure. C’est cette uniformité qui constitue le véritable avantage de l’architecture serveur, et non la logique des outils eux-mêmes, qui doivent néanmoins être écrits par quelqu’un dans tous les cas.

    Réorganiser la même intégration via MCP

    Le meilleur moyen de voir la différence pratique est d’ prendre exactement la même intégration mise en place précédemment, l’outil de recherche et le client de fichiers, et de la reconstruire en utilisant MCP à la place. La fonctionnalité reste identique, les systèmes sous-jacents restent identiques, seule l’interface change.

    L’outil de recherche, qui a commencé comme un client indépendant basé sur requests, devient maintenant une déclaration d’outil enregistrée sur un serveur MCP :

    @server.list_tools()
    async def list_tools() -> list[Tool]:
        return [
            Tool(
                name="lookup",
                description="Search for a record by query string",
                inputSchema={
                    "type": "object",
                    "properties": {"query": {"type": "string"}},
                    "required": ["query"],
                },
            ),
            Tool(
                name="fetch_record",
                description="Fetch a record by ID",
                inputSchema={
                    "type": "object",
                    "properties": {"record_id": {"type": "string"}},
                    "required": ["record_id"],
                },
            ),
        ]
    @server.call_tool()
    async def call_tool(name: str, arguments: dict) -> dict:
        if name == "lookup":
            return perform_lookup(arguments["query"])
        elif name == "fetch_record":
            return fetch_record_from_db(arguments["record_id"])
        raise ValueError(f"unknown tool: {name}")
    

    Aucune logique interne n’a été modifiée : perform_lookup continue d’appeler la même API externe, et fetch_record_from_db exécute toujours la même requête de base de données. Ce qui change, c’est que ces deux outils, bien qu’ils reposent sur des systèmes complètement différents, sont maintenant déclarés côte à côte, partageant une structure de schéma identique et étant exposés via les mêmes deux points d’accès.

    Le changement le plus visible se produit dans le nœud chargé de les appeler, car il n’a plus besoin de tenir compte des requests ou de psycopg2:

    async def agent_node(state: GraphState) -> dict:
        async with ClientSession(server_params) as session:
            result = await session.call_tool(
                "lookup", {"query": state["extracted_fields"]["query"]}
            )
        return {"tool_result": result}
    

    Cela contraste avec la version précédente, qui importait une bibliothèque HTTP spécifique, analysait un format d’erreur particulier et nécessitait un chemin de code entièrement distinct rien que pour appeler fetch_record à la place de lookup. Dans cette version, l’utilisation d’un deuxième outil se résume au remplacement d’une chaîne de caractères par un dictionnaire d’arguments, sans avoir à créer un client distinct avec ses propres particularités.

    Rien de ce que ces outils accomplissent réellement n’a changé. Ce qui a changé, c’est que l’agent qui les utilise n’a plus besoin de comprendre leur implémentation, seulement leur nom et le schéma déclaré. C’est la standardisation évoquée précédemment dans cette série : il s’agit maintenant d’un élément concret que l’on peut retrouver dans du code réel, au lieu de se fier uniquement à des affirmations générales.

    Côte à côte : Ad Hoc vs. MCP

    Lorsque les deux approches sont entièrement développées, on peut cesser de réfléchir aux avantages théoriques pour examiner directement ce qui diffère réellement entre la version ad hoc et la version MCP.

    • Effort requis pour chaque nouvel outil : Dans l’approche ad hoc, chaque outil supplémentaire nécessitait son propre client, sa propre logique d’authentification, son propre format d’erreur ainsi que son propre ensemble de conventions à maîtriser avant de pouvoir l’utiliser en toute sécurité. Avec MCP, ajouter un outil signifie simplement insérer une nouvelle entrée dans list_tools et un nouveau branchement à l’intérieur de call_tool, chacun suivant le même schéma que l’outil précédent.
    • Ce que le code appelant doit comprendre : Le nœud d’agent ad hoc devait importer une bibliothèque client spécifique et gérer son format d’erreur particulier. Le nœud d’agent MCP, quant à lui, ne importe rien de spécifique à un outil ; il se contente d’appeler session.call_tool avec un nom et un dictionnaire d’arguments, et cette appelation fonctionne de la même manière que l’outil en question soit un point d’accès REST, une base de données ou tout autre élément.
  • Niveau de réutilisabilité de la configuration entre les agents : Dans une conception ad hoc, si un deuxième agent a besoin de la même capacité de recherche, il doit soit importer directement le même client et rester lié à cette implémentation spécifique, soit quelqu’un doit créer un wrapper partagé pour éviter la duplication. Avec un serveur MCP, un deuxième agent se connecte simplement à ce serveur et hérite du même ensemble d’outils, accessibles et utilisables de la même manière, sans avoir besoin de connaissances supplémentaires par rapport à celles déjà utilisées par le premier agent.
  • Quantité de maintenance requise : Ajuster le comportement de l’outil de recherche, ajouter une politique de tentative répétée ou modifier le délai d’attente implique d’éditer directement LookupToolClient, de sorte que chaque agent qui en dépend intègre automatiquement ce changement. MCP ne supprime pas cette charge de maintenance, mais il la centralise : le comportement de chaque outil se trouve désormais derrière les mêmes deux fonctions, list_tools et call_tool, au lieu d’être dispersé selon le nombre distinct de clients existants pour chaque outil.
  • Rien de tout cela ne simplifie la logique réelle de l’outil. perform_lookup et fetch_record_from_db doivent toujours être écrits et fonctionner correctement, quel que soit le cas. Ce qui change, c’est tout ce qui entoure cette logique : la manière dont elle est découverte, la façon dont elle est invoquée, la présentation des échecs, ainsi que la part de cette charge que le code propre de l’agent doit assumer par rapport à ce qui est géré automatiquement grâce au respect d’un protocole commun.

    Intégrer un outil MCP dans le système LangGraph (suite de la partie 2)

    Jusqu’à présent, chaque exemple s’est concentré sur un seul agent appelant un seul outil de manière isolée. Il est utile de combler cette lacune en réintégrant un outil basé sur MCP dans le graphe multi-agents créé en partie 2, car c’est là que les éléments des trois articles de cette série convergent réellement.

    Rappelons le graphique de la Partie 2 : une étape d’entrée des données, l’interprétation par un LLM, le moteur de règles présenté dans la Partie 1, ainsi que l’acheminement conditionnel qui envoie le flux soit vers un nœud de notification, soit vers une révision humaine. Imaginez que le moteur de règles doive désormais effectuer une vérification auprès d’un système externe avant de pouvoir prendre une décision, un type de dépendance qui aurait autrefois nécessité la création d’un client ad hoc dédié, similaire à celui développé précédemment dans cet article et connecté directement à ce nœud.

    Avec un serveur MCP qui expose cette même fonction de recherche en tant qu’outil, les responsabilités du nœud changent à peine. Il continue de lire l’état du graphique et d’y écrire sa décision, il se contente désormais d’accéder au système externe en appelant session.call_tool plutôt que d’utiliser un client dédié :

    async def rules_engine_node(state: GraphState) -> dict:
        async with ClientSession(server_params) as session:
            lookup_result = await session.call_tool(
                "lookup", {"query": state["extracted_fields"]["category"]}
            )
        decision = evaluate_rules(state["extracted_fields"], lookup_result)
        return {"decision": decision["decision"], "decision_reason": decision["reason"]}
    

    La position du nœud au sein du flux global n’est affectée en rien par tout ceci. Il reste toujours à la même place, en aval de llm_interprétation et en amont du branchement conditionnel déjà présent dans la Partie 2, restant soumis au champ d’autorisation défini dans les directives de cet article, et étant toujours enregistré sous le même identifiant de suivi décrit dans sa section consacrée à l’observabilité. La seule véritable modification concerne la manière dont le nœud communique avec l’extérieur : au lieu de s’appuyer sur un client conçu spécifiquement pour ce nœud, il envoie désormais des appels vers un outil que tout autre nœud, qu’il s’agisse de celui-ci ou d’un futur nœud, peut invoquer via ce même chemin.

    C’est vraiment ici que les trois éléments de la série se rejoignent. Un agent qui ne décide que de ce pour quoi il est explicitement autorisé à le faire, fonctionnant au sein d’un graphe qui coordonne ses actions avec celles des autres agents, et accédant aux systèmes externes via la même interface standardisée partagée par tous les agents. Aucun des trois éléments — le moteur de règles, le graphe ou MCP — n’a besoin d’une connaissance détaillée des deux autres. Ils doivent simplement respecter la même discipline que cette série a soulignée tout au long : des responsabilités restreintes, des contrats explicites, et rien qui ne doive être supposé.

    Cost & Latency: What Orchestration Actually Costs

    Chaque couche ajoutée au cours de cette série apporte une certaine quantité de structure, et aucune de ces structures n’est offerte gratuitement. Plutôt que de laisser ce coût caché, il est utile de suivre ce qui se passe réellement en termes de temps de réponse une fois qu’une demande traverse toutes les étapes décrites jusqu’à présent.

    Considérons une seule demande qui traverse le graphe présenté dans la Partie 2, désormais étendu avec la recherche basée sur MCP décrite ci-dessus. Le parcours se décompose approximativement comme suit : l’étape d’entrée effectue un formatage léger sans appels sortants, ce qui ne coûte au plus que quelques millisecondes. L’étape d’interprétation par le LLM appelle un modèle pour extraire des champs structurés de la demande brute, et c’est généralement la dépense la plus importante dans l’ensemble du parcours, ajoutant souvent plusieurs centaines de millisecondes en fonction du modèle choisi et de la longueur du prompt. L’appel effectué par le moteur de règles vers l’outil de recherche entraîne un aller-retour réseau en plus du temps nécessaire pour que l’outil lui-même réponde ; il s’agit d’un coût significatif, mais généralement inférieur à celui de l’étape du LLM. L’évaluation des règles elles-mêmes, étant donné qu’il s’agit simplement de logique déterministe, est presque gratuite. L’acheminement et l’étape de notification de fin ajoutent seulement une petite somme en plus.

    En additionnant ces valeurs, l’image devient claire : une requête qui effectue un seul appel au modèle et une seule invocation d’une outil voit son temps total déterminé presque entièrement par ces deux opérations, la coordination propre au graphe n’ayant quasiment aucun impact. Les nœuds, les arêtes et l’état partagé introduits dans la partie 2 fournissent une structure sans imposer de latence significative : lire et écrire un objet d’état partagé est peu coûteux. Ce qui prend réellement du temps, c’est de faire appel à un modèle ou à un système externe.

    Cela redéfinit la façon dont on devrait aborder l’optimisation des performances. Ajouter plus de nœuds, davantage de branches de routage ou plus de vérifications de sécurité à un graphe a à peine d’impact sur la vitesse, car il s’agit simplement d’appels de fonctions et de recherches dans un dictionnaire. Ce qui ralentit réellement un système, ce sont tous les appels aux LLM ainsi que tous les appels à des outils externes qui se situent sur le chemin critique de la requête. Un système construit autour de trois agents qui appellent chacun un modèle à tour de rôle fonctionnera nettement plus lentement qu’un système qui n’appelle un modèle qu’une seule fois, quel que soit le degré d’efficacité de l’orchestration qui l’entoure.

    La leçon pratique est simple : lorsque la latence est cruciale pour un flux de travail, ne commencez pas par examiner la forme du graphe. Commencez plutôt en comptant le nombre d’appels au modèle et d’appels à des outils externes que doit effectuer une demande normale avant de s’achever, et demandez-vous si certains d’entre eux peuvent être exécutés en parallèle plutôt qu’en séquence, ou si on peut les omettre complètement pour les demandes qui n’en ont pas besoin.

    Ce que MCP ne résout pas

    Il est utile d’être tout aussi franc sur les limites de MCP que sur ses avantages mentionnés précédemment, car la plupart des écrits sur le sujet mettent l’accent sur les avantages et traitent rarement des limites. Les mêmes trois domaines abordés là, à savoir la découverte, l’appel et le traitement des erreurs, méritent un nouvel examen sous un angle différent.

    • La standardisation de la découverte et de l’appel ne corrige pas un outil mal conçu : le standard MCP standardise la manière dont un outil est localisé et appelé, mais pas ce qui se passe à l’intérieur de lui. Un outil doté d’une implémentation lente, instable ou mal structurée reste lent, instable et mal structuré même lorsqu’il est placé derrière un serveur MCP. Le protocole déplace simplement l’incohérence, la transférant du code d’appel vers l’implémentation même de l’outil, sans pour autant la faire disparaître.
  • Une forme d’erreur uniforme n’est pas synonyme de gestion des erreurs efficace : Les pannes se produisent toujours, les outils restent en délai, et les systèmes externes continuent de tomber en panne. MCP donne à ces pannes une forme prévisible et cohérente lorsqu’elles surviennent, mais le code appelant reste responsable de décider de la suite des opérations : réessayer, recourir à une solution de secours ou transmettre l’erreur vers le haut, exactement comme avant. Un format d’erreur cohérent n’équivaut pas à ce que la panne soit réellement gérée.
  • L’exécution du protocole entraîne des coûts opérationnels supplémentaires : un serveur MCP représente un processus de plus à faire fonctionner, à déployer et à maintenir en bon état. Pour un seul agent appelant une seule outil simple, il s’agit d’une charge réelle, et l’approche client ad hoc décrite précédemment dans cette série aurait été plus rapide à mettre en place et plus facile à comprendre. Cette charge est encore aggravée par la jeunesse de l’écosystème : les outils, le support de débogage et les conventions établies sont encore en retard par rapport à quelque chose d’aussi mature qu’une API REST classique. En pratique, cela signifie passer plus de temps à lire directement le code source et les spécifications, avec moins de modèles éprouvés auxquels on peut recourir.
  • Rien de tout cela ne s’oppose à l’adoption de MCP. Il s’agit plutôt d’un rappel que le choix d’un protocole ne remplace pas le travail d’ingénierie qui doit encore être effectué en arrière-plan. La transformation apportée par MCP est réelle, comme cela a été montré dans les sections précédentes, mais elle est plus restreinte que l’idée générale d’« agents se connectant à des outils », et il est utile d’être précis quant à l’endroit exact où se situe cette frontière avant de décider si son adoption est pertinente, ce dont traite la section suivante.

    Quand il vaut la peine d’adopter MCP

    Compte tenu des compromis évoqués ci-dessus, il n’y a pas de réponse universelle : ni un « adoptez-le toujours » simple, ni un « ne vous en souciez pas ». Ce qui importe, c’est plutôt de vérifier un certain nombre de conditions concrètes avant de l’intégrer dans un système donné.

    • Plus d’un agent aura besoin des mêmes outils. Presque tous les avantages décrits précédemment proviennent de la réutilisation : un deuxième agent peut s’connecter à un serveur déjà mis en place, plutôt que de dupliquer un client ou d’en créer un ultérieurement. Lorsqu’il n’y a qu’un agent et un seul outil, cet avantage n’existe pas encore — il n’y a rien à partager — et l’approche ad hoc évoquée précédemment reste le choix le plus simple pour ce cas particulier.
    • Le nombre d’outils devrait augmenter. Une interface standardisée gagne en valeur à mesure que davantage d’outils s’y rattachent. Avec deux ou trois outils, faire fonctionner un serveur dédié peut ne pas en valoir la peine en termes de coûts supplémentaires. Une fois qu’on traite avec dix ou vingt outils — chacun nécessitant normalement son propre client personnalisé — le fardeau de maintenance lié à l’approche ad hoc commence à représenter un véritable problème.
  • Le système doit pouvoir survivre au-delà de sa première version. L’un des avantages offerts par MCP est que l’ajout d’un nouvel agent ou d’un nouvel outil ultérieurement ne force pas à réorganiser tout ce qui est déjà en place. Cet avantage s’accumule au cours de la durée de vie du système, et il est facile de l’oublier si l’on ne prend en compte que le coût de la configuration initiale.
  • D’un autre côté, cette approche ne convient pas lorsqu’il s’agit d’un seul agent communiquant avec un outil stable et bien compris qui ne changera pas. Dans ce cas, créer un client ad hoc est plus rapide, il y a une pièce mécanique de moins à entretenir, et la coordination offerte par MCP n’a pas d’autre utilisateur pour l’utiliser réellement.

    Le test pratique à appliquer ici est similaire à celui utilisé précédemment pour le moteur de règles lui-même : ajouter cette complexité résout-il un problème réellement rencontré par ce système à son stade actuel, ou est-ce simplement parce que c’est la solution à la mode pour un problème que le système ne connaît pas encore ?

    Conclusion : Fin de la série

    Au cours de trois articles, un système s’est développé progressivement, chaque couche s’appuyant sur la précédente. Le premier article a créé un seul agent suffisamment fiable pour qu’on lui confie une décision réelle, en séparant strictement l’interprétation de l’acte de décision. Le deuxième article a donné à cet agent des partenaires, en rassemblant plusieurs agents spécialisés au sein d’un graphe qui partageait des états entre eux, protégé par des permissions explicites et traçable de bout en bout afin que chaque demande puisse être suivie du moment où elle entre jusqu’au moment où elle sort. Cet article a comblé la dernière lacune — à savoir comment ces agents peuvent aller au-delà du graphe — en standardisant cet accès via MCP dans les situations où cela s’avère véritablement utile, tout en étant tout aussi clair sur les cas où cela ne l’est pas.

    None of these three pieces is especially complex on its own. What actually makes the resulting system dependable is one habit, repeated at every layer without exception: responsibilities stay narrow, contracts stay explicit, and nothing is left to guesswork or allowed to happen quietly in the background. That habit is the real subject of this series, more than any particular tool—LangGraph and MCP just happened to be the frameworks used to put it into practice, but the underlying principles hold regardless of which tools you reach for.