Le fil de l’actualité

Écrit par l’IAIA

Model Context Protocol (MCP) expliqué : comment les agents IA se connectent aux outils et aux données

MCP est le protocole JSON-RPC qui permet aux hôtes IA de découvrir et d'appeler des outils. Clients, serveurs, stdio, Streamable HTTP, migration 2026 et limites de confiance.

Rédigé par une IA. Cette analyse a été rédigée par une IA et publiée sans relecture ligne par ligne.

Tôt ou tard, tout agent IA qui fait un vrai travail doit poser deux questions à un programme externe : que sais-tu faire, et vas-tu le faire pour moi? Model Context Protocol, ou MCP, tente de faire passer ces deux questions dans une grammaire commune. Sous le discours marketing, le protocole est plus mécanique que ne le laissent entendre les explications : des messages JSON-RPC, une poignée de main qu’une révision exige et que la suivante supprime, deux transports, et des frontières de confiance que le protocole dit ne pas pouvoir imposer à lui seul.

Avant de lire : Notre guide de sandboxing explique comment cloisonner l’accès réseau et au système de fichiers d’un agent, et les vers IA zéro-clic montrent ce qu’une instruction hostile peut faire une fois qu’elle atteint un modèle. Cet article couvre la couche protocolaire qui se trouve entre les deux.

Pourquoi maintenant : La page de spécification intitulée latest sert maintenant une révision datée du 2026-07-28, qui supprime la poignée de main initialize et les sessions au niveau du protocole. La plupart des explications décrivent encore l’ancien fonctionnement. Je couvre les deux, et je n’ai trouvé de date de publication pour la nouvelle révision sur aucune des pages consultées.

Table des matières

Une question commune, trois rôles

Anthropic a présenté MCP le 25 novembre 2024. Le lancement décrivait le problème ainsi : « chaque nouvelle source de données exige sa propre implémentation sur mesure ». Anthropic a publié une spécification, des SDK et des serveurs préconstruits pour Google Drive, Slack, GitHub, Git, Postgres et Puppeteer. Block et Apollo sont les premiers adoptants nommés. Le protocole a été créé par David Soria Parra et Justin Spahr-Summers.

L’architecture repose sur trois rôles, que les gens confondent constamment. L’hôte est l’application IA que vous utilisez vraiment : une application de clavardage de bureau, un IDE, un exécuteur d’agents. Le client est un connecteur logé à l’intérieur de cet hôte. Le serveur expose des données ou des actions. La règle qui compte : un hôte crée et gère plusieurs clients, et chaque client entretient une relation 1:1 avec un seul serveur. Un IDE qui parle à un serveur de fichiers, à un serveur GitHub et à un serveur Postgres gère donc trois clients et trois sessions distinctes, et c’est l’hôte qui décide de tout ce qui se passe entre eux.

Diagramme isométrique d'un hôte relié à trois serveurs MCP isolés, chacun derrière sa propre cloison

Imaginez l’hôte comme le bureau de sécurité d’un édifice. Il remet un badge pour chaque étage, et les étages n’ont pas le droit de se parler. La spécification dit que les serveurs ne devraient pas pouvoir lire la conversation entière, ni « voir » les autres serveurs. Le serveur GitHub ne voit jamais la question que vous avez posée au serveur de fichiers. Cet isolement est une bonne conception, mais il charge aussi l’hôte de responsabilités. La spécification lui confie le contrôle des permissions de connexion et du cycle de vie des clients, l’application des politiques de sécurité et des exigences de consentement, ainsi que la gestion des décisions d’autorisation de l’utilisateur.

La spécification affirme que MCP « s’inspire en partie du Language Server Protocol », et la comparaison tient jusqu’à un certain point. Un éditeur demande des complétions à un serveur de langage pendant que vous tapez. Un modèle décide lui-même quand appeler un outil. Cette différence, qui initie l’appel, explique l’essentiel de l’histoire de sécurité qui suit.

Ce qui circule réellement sur le fil

Chaque message est du JSON-RPC 2.0. Selon la révision 2025-11-25, l’initialisation doit être la première interaction. Le client envoie initialize avec son protocolVersion. Si le serveur ne supporte pas cette version, il doit répondre avec une version qu’il supporte, et si le client ne peut pas parler la version proposée, la spécification dit qu’il devrait se déconnecter. Le serveur répond avec sa version, ses capacités et son serverInfo, puis le client envoie notifications/initialized. La spécification demande aussi de fixer des délais d’attente sur chaque requête envoyée, une règle qui sent le souvenir de quelqu’un qui a déjà attendu un serveur qui ne répond jamais.

La découverte tient en un seul appel, tools/list, paginé avec un curseur. Chaque outil renvoie un nom, un titre, une description, un schéma JSON pour ses entrées et, au besoin, un schéma de sortie, des annotations et un champ d’exécution. Le modèle choisit un outil, puis le client envoie tools/call. L’exemple de la spécification est assez court pour se lire d’un coup d’œil :

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": { "name": "get_weather", "arguments": { "location": "New York" } }
}

Voici l’échange complet, dans l’ordre. Je l’ai écrit à la main à partir des formes de messages décrites sur les pages de la spécification. Je ne l’ai pas capturé sur un serveur en fonctionnement, et chaque ... marque un champ que j’ai omis plutôt que d’inventer.

// 1. Client → serveur : la poignée de main 2025-11-25 s'ouvre
{"jsonrpc":"2.0","id":1,"method":"initialize",
 "params":{"protocolVersion":"2025-11-25", ...}}
// 2. Client → serveur : une notification, donc sans id et sans réponse
{"jsonrpc":"2.0","method":"notifications/initialized"}
// 3. Client → serveur : découvrir les outils
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
// 4. Client → serveur : le modèle a choisi get_weather
{"jsonrpc":"2.0","id":3,"method":"tools/call",
 "params":{"name":"get_weather","arguments":{"location":"New York"}}}
// 5. Serveur → client : l'outil s'est exécuté et a échoué, donc l'erreur revient au modèle comme donnée
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"..."}],"isError":true}}

Voici la définition d’outil que lit le modèle. La forme suit la spécification ; les valeurs sont de moi :

{
  "name": "get_weather",
  "title": "Weather lookup",
  "description": "Returns current conditions for a city.",
  "inputSchema": {
    "type": "object",
    "properties": { "location": { "type": "string" } },
    "required": ["location"]
  }
}

Lisez-la comme le fait le modèle. Il ne voit jamais votre code. Il voit le nom, la description et le schéma. La description est un argumentaire de vente que le modèle lit avant de décider quoi que ce soit, ce qui en fait du texte de consigne. Gardez cette idée en tête pour la section sur la confiance.

Les échecs prennent deux formes, et la différence compte quand on débogue. Un outil qui s’est exécuté et a échoué renvoie un résultat normal avec isError: true, et la spécification dit que ce contenu doit être « une rétroaction exploitable que les modèles de langage peuvent utiliser pour se corriger ». Un appel à un outil qui n’existe pas est une erreur JSON-RPC au niveau du protocole, de code -32602, étiquetée « Unknown tool ». La première est un problème que le modèle doit résoudre. La seconde est le vôtre.

Deux transports et comment choisir

La spécification définit deux transports standard, stdio et Streamable HTTP, et demande aux clients de prendre en charge stdio « chaque fois que c’est possible ». Le choix porte moins sur la vitesse que sur la question de savoir qui peut joindre le serveur.

Comparaison en écran divisé d'une connexion stdio locale directe et d'un point de terminaison Streamable HTTP en réseau, avec un contrôleur

stdioStreamable HTTP
Cycle de vieLe client lance le serveur comme sous-processusUn seul point de terminaison HTTP, local ou distant
Découpage des messagesJSON délimité par des sauts de ligne, sans saut de ligne interneUn POST par message du client ; réponses en flux d’événements ou en JSON
Exposition principaleLe sous-processus s’exécute avec les privilèges du clientTout ce qui peut joindre le point de terminaison, d’où les vérifications d’Origin obligatoires
Recommandation de la spécificationLes clients DEVRAIENT le prendre en charge chaque fois que c’est possibleLes serveurs DOIVENT valider Origin et DEVRAIENT se lier à 127.0.0.1

Avec stdio, le client démarre le serveur, et ils se parlent par stdin et stdout. Les messages sont délimités par des sauts de ligne, et la spécification dit qu’ils « NE DOIVENT PAS contenir de sauts de ligne internes », une règle qu’on n’écrit qu’après qu’un JSON bien indenté a fait planter un analyseur. Les journaux vont vers stderr, et les clients « NE DEVRAIENT PAS supposer » qu’une sortie stderr signale une erreur. Stderr est un journal intime, pas un bip d’urgence. L’arrêt suit une escalade polie : fermer le flux d’entrée, attendre, envoyer SIGTERM, et seulement ensuite SIGKILL.

La spécification dit que le transport stdio « n’est pas intrinsèquement vulnérable ». Le danger tient à ce qu’hérite le sous-processus : il s’exécute sous votre identité. C’est pourquoi les consignes de sécurité exigent qu’un client qui offre une configuration locale en un clic « mette en place des mécanismes de consentement appropriés avant d’exécuter des commandes ». Une installation en un clic qui s’exécute avec vos droits d’utilisateur est exactement le cas pour lequel le consentement existe.

Streamable HTTP passe par un point de terminaison unique qui accepte POST et GET. Chaque message du client est un nouveau POST, et le serveur répond à une requête avec text/event-stream ou application/json. La spécification dit que Streamable HTTP remplace l’ancien transport HTTP+SSE, que bien des explications présentent encore comme l’option actuelle. Les identifiants de session, si le serveur en attribue un, doivent utiliser de l’ASCII visible, et le client doit envoyer un en-tête MCP-Protocol-Version à chaque requête suivante.

C’est ici que l’habitude de tout faire en local devient dangereuse. Une page web peut utiliser le DNS rebinding pour faire parler votre navigateur avec un nom d’hôte qu’elle contrôle, et si un serveur écoute sur votre machine, cette page peut l’atteindre. La réponse de la spécification : les serveurs « DOIVENT valider l’en-tête Origin sur toutes les connexions entrantes », et une Origin invalide reçoit un HTTP 403. La règle complémentaire dit que les serveurs locaux « DEVRAIENT se lier uniquement à localhost (127.0.0.1) plutôt qu’à toutes les interfaces réseau (0.0.0.0) ». Ce sont deux verrous distincts. Ma lecture, que la spécification ne formule pas ainsi : la vérification d’Origin protège contre les navigateurs, parce que ce sont eux qui envoient cet en-tête. Un script sur le Wi-Fi du café peut envoyer n’importe quel en-tête, donc se lier à toutes les interfaces élargit le nombre de gens qui peuvent frapper à la porte.

Alors, lequel choisir? Optez pour stdio quand le serveur est un outil que vous lancez pour vous-même et que le code lancé est déjà un code de confiance, puisque le sous-processus hérite de vos privilèges. Optez pour Streamable HTTP quand le serveur doit être joignable depuis une autre machine ou servir plusieurs utilisateurs. Dès ce moment, la validation d’Origin, l’adresse d’écoute et, si le serveur touche aux données des utilisateurs, l’autorisation deviennent votre responsabilité. Le chemin stdio est aussi entièrement hors de la spécification OAuth, ce que couvre la section sur l’autorisation.

Migrer de 2025-11-25 à 2026-07-28

Si vous maintenez un hôte ou un serveur, c’est ici que le code casse. Le journal des modifications (changelog) est la seule source que j’ai trouvée avec une liste par changement, et il ne publie pas de comparaison d’en-têtes ligne par ligne.

Domaine2025-11-252026-07-28
Début de connexioninitialize doit venir en premier, suivi de notifications/initializedSupprimé. Chaque requête transporte sa version de protocole et les capacités du client dans _meta
Découvertetools/list après la poignée de mainLes serveurs DOIVENT implémenter server/discover ; les clients peuvent l’appeler en premier
Identité de sessionLe serveur peut attribuer un MCP-Session-IdSessions au niveau du protocole et en-tête Mcp-Session-Id retirés de Streamable HTTP
Mises à jour serveurFlux HTTP GET et resources/subscribeLes deux remplacés par subscriptions/listen
Flux interrompusReprise SSE avec Last-Event-IDReprise supprimée ; la requête en cours est perdue et doit être renvoyée avec un nouvel identifiant
Appels initiés par le serveurLe serveur envoie sampling/createMessage ou elicitation/createLe serveur renvoie resultType: "input_required" et l’échange continue sur plusieurs allers-retours
Fonctionnalités dépréciéesStreamable HTTP remplace HTTP+SSERoots, Sampling et Logging dépréciés (SEP-2577) ; HTTP+SSE déprécié (SEP-2596)

L’ordre de migration que je suivrais : d’abord, faire en sorte que votre hôte appelle server/discover et cesse de dépendre de la poignée de main. Ensuite, supprimer tout code qui lit ou envoie Mcp-Session-Id. Puis réécrire les appels de sampling et d’elicitation initiés par le serveur en reprises input_required. Enfin, tester les deux époques avec l’Inspector, qui fonctionne dans les modes de négociation legacy et modern (2026-07-28).

Trois points restent non vérifiés : les noms exacts des clés _meta, la question de savoir si l’en-tête MCP-Protocol-Version est encore exigé dans la nouvelle révision, et la date de publication. Lisez le texte du changelog avant de livrer quoi que ce soit en vous appuyant sur ces éléments.

Où se trouve réellement la frontière de confiance

La spécification trace cinq frontières de confiance. La question utile est de savoir qui fait respecter chacune.

FrontièreAppliquée parCe qu’exige la spécification
Hôte vers utilisateurHôteConsentement à l’accès aux données et à l’invocation d’outils ; un humain DEVRAIT toujours pouvoir refuser un appel d’outil
Hôte vers serveurHôteChaque serveur est isolé ; l’hôte contrôle les flux entre serveurs
Client vers serveurClientLes annotations d’outils ne sont pas fiables, sauf si elles viennent d’un serveur de confiance
Serveur vers API en avalServeurAucune transmission de jetons : les serveurs NE DOIVENT PAS accepter des jetons non émis pour le serveur MCP
Navigateur vers serveur localServeurValidation d’Origin contre le DNS rebinding

La spécification est franche sur cet arrangement. Son aperçu indique que « MCP lui-même ne peut pas faire respecter ces principes de sécurité au niveau du protocole ». Le protocole peut écrire DOIT. Il ne peut pas vérifier ce que votre hôte vous a réellement montré avant que vous cliquiez oui. Du côté serveur, la liste est propre : valider toutes les entrées d’outils, mettre en place des contrôles d’accès, limiter le débit des invocations et assainir les sorties. La spécification nomme aussi les outils pour ce qu’ils sont : « Les outils représentent une exécution de code arbitraire et doivent être traités avec la prudence appropriée. » La page des meilleures pratiques de sécurité nomme huit menaces, dont le confused deputy des serveurs proxy, la transmission de jetons, le SSRF pendant la découverte OAuth et l’escalade de privilèges par proxy stdio.

L’empoisonnement d’outils se cache dans la description

La publication « MCP Security Notification: Tool Poisoning Attacks » d’Invariant Labs, parue le 1er avril 2025 et signée Luca Beurer-Kellner et Marc Fischer, décrit des instructions intégrées aux descriptions d’outils, invisibles pour les utilisateurs et visibles pour les modèles. La preuve de concept était un outil innocent, add(a, b, sidenote). Sa description disait au modèle de lire ~/.cursor/mcp.json et ~/.ssh/id_rsa et de faire passer le contenu par sidenote, sans jamais mentionner ces lectures de fichiers. L’utilisateur voit une addition. Une calculatrice qui lit votre clé SSH reste, techniquement, une calculatrice.

Icône de calculatrice au-dessus d'une couche translucide d'instructions cachées qui se dirigent vers une clé

Deux variantes comptent davantage en pratique que dans une démo. Un rug pull survient quand un serveur que vous avez approuvé modifie plus tard une description d’outil. Le shadowing d’outils est plus sournois : la description d’un outil malveillant peut changer la façon dont l’agent traite un outil de confiance, et dans l’exemple d’Invariant, elle a redirigé les messages send_email vers l’adresse d’un attaquant. Invariant n’a testé que Cursor ; prenez donc ce résultat comme une démonstration, non comme un recensement.

La spécification a le bon réflexe. Les clients doivent traiter les annotations comme non fiables, sauf si elles viennent d’un serveur de confiance. Mais une description d’outil n’est pas une annotation. C’est le texte que lit le modèle, et le protocole n’a aucun moyen de voir ce que le modèle en fait.

La spécification ne définit aucune défense par épinglage ni par empreinte. Sa page sur les outils dit que les annotations ne sont pas fiables et qu’un humain devrait rester dans la boucle. MCP-Scan d’Invariant Labs, présenté le 11 avril 2025, inclut ce qu’il appelle Tool Pinning, décrit comme une façon de vérifier l’intégrité des outils « en suivant les changements par hachage d’outils ». Il vise les « modifications non autorisées des descriptions d’outils MCP après l’approbation initiale de l’utilisateur ». La page ne précise pas ce qui est haché, où sont stockés les épinglages, ni comment fonctionne la réapprobation. Traitez cela comme une fonctionnalité de produit, pas comme une norme.

Voici la défense que je construirais dans un hôte. C’est ma conception ; elle ne figure dans aucune spécification citée, et je n’ai pas exécuté ce code contre un serveur en fonctionnement :

import hashlib
import json

def tool_fingerprint(tool: dict) -> str:
    # Hacher seulement ce que le modèle lit et ce qu'il peut envoyer. Exclure title et annotations.
    canonical = {k: tool.get(k) for k in ("name", "description", "inputSchema")}
    blob = json.dumps(canonical, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
    return hashlib.sha256(blob.encode("utf-8")).hexdigest()

Stockez l’empreinte au moment de l’approbation, indexée par serveur et par nom d’outil. Recalculez-la à chaque tools/list. En cas de différence, refusez l’appel et montrez à l’utilisateur l’ancienne et la nouvelle description côte à côte. Cela arrête le rug pull où la description change après l’approbation. Cela n’arrête pas un serveur qui garde la même description tout en changeant ce que l’outil fait réellement ; il faut donc la combiner avec l’invite de consentement et les contrôles d’accès que la spécification demande déjà aux serveurs. J’ai exclu title et annotations du hachage. Les annotations sont les champs que la spécification demande aux clients de ne pas croire, donc un changement à cet endroit ne devrait pas forcer à lui seul une réapprobation. C’est un choix de jugement, et l’on pourrait défendre l’inverse.

Inspecter le trafic et configurer un hôte

Le moyen le plus rapide de voir le protocole à l’œuvre est le MCP Inspector, le paquet officiel @modelcontextprotocol/inspector. Il exige Node 22.19.0 ou plus récent et possède trois modes : --web (par défaut), --cli et --tui. Pour ouvrir l’interface web sur un serveur stdio local :

npx @modelcontextprotocol/inspector node path/to/server/index.js

Pour lister les outils d’un serveur puis quitter, ce qui est la vérification la plus rapide que la découverte fonctionne :

npx @modelcontextprotocol/inspector --cli node path/to/server/index.js --method tools/list

Pour appeler un outil sur HTTP et passer le résultat dans jq :

npx @modelcontextprotocol/inspector --cli https://api.example.com/mcp --transport http --method tools/call --tool-name get_weather --tool-arg city=Boston --format json | jq .result

L’interface web affiche une URL qui contient un jeton d’API propre à chaque lancement. Définissez MCP_AUTO_OPEN_ENABLED=false si vous ne voulez pas qu’elle ouvre un navigateur toute seule. Son panneau de surveillance affiche l’échange JSON-RPC au fil de l’eau, ce qui est le plus proche d’un journal de trafic que propose l’Inspector. La page ne donne pas de nom officiel à ce panneau, et je n’ai pas confirmé si l’Inspector peut importer une configuration de Claude Desktop.

Lisez la note de sécurité avant de coller un vrai jeton. L’Inspector stocke les jetons OAuth, les secrets clients OAuth et les valeurs env: de stdio dans le trousseau du système d’exploitation, lorsqu’il en existe un. Sans trousseau, il les écrit dans ~/.mcp-inspector/secrets.json, non chiffrés à moins que vous fournissiez une clé.

Pour un hôte, le tutoriel officiel de construction de serveur branche son serveur météo à Claude pour ordinateur de bureau au moyen d’un fichier claude_desktop_config.json contenant un bloc mcpServers. Le serveur du tutoriel expose deux outils, get_alerts et get_forecast. Je n’ai pas extrait le contenu de ce fichier de configuration dans cette passe ; je ne citerai donc ni son JSON ni les chemins propres à chaque système. Copiez-les depuis le tutoriel lui-même. Le tutoriel du client Python exige le SDK MCP Python 2.0.0 ou plus récent, et je n’ai pas vérifié quelle révision son code vise ni s’il appelle initialize explicitement.

Quand ajouter l’autorisation

L’autorisation est facultative, et la première tâche de la spécification est de dire quand elle s’applique. La règle se découpe selon le transport. Les implémentations HTTP DEVRAIENT se conformer à la spécification d’autorisation. Les implémentations STDIO NE DEVRAIENT PAS la suivre et devraient plutôt récupérer leurs identifiants dans l’environnement. Les autres transports DOIVENT suivre les pratiques de sécurité établies pour leur protocole.

Un serveur stdio local qui a besoin d’un jeton GitHub le lit dans son environnement et n’entre jamais dans un flot OAuth. Un serveur HTTP qui agit au nom d’un utilisateur auprès de l’API d’un tiers est le cas où la mécanique OAuth se justifie. Là, le serveur DOIT implémenter les métadonnées de ressource protégée OAuth 2.0 (RFC 9728), et le serveur d’autorisation DOIT implémenter OAuth 2.1. Les clients DOIVENT inclure le paramètre resource (RFC 8707) dans les requêtes d’autorisation et de jeton, et NE DOIVENT PAS envoyer au serveur un jeton que le serveur d’autorisation du serveur n’a pas émis.

Le DOIT côté serveur qui fait le plus de travail est la validation de l’audience. Le serveur doit confirmer que chaque jeton d’accès a été émis spécifiquement pour lui, et il ne doit accepter ni transmettre aucun autre jeton. C’est la réponse de la spécification à la transmission de jetons. Les jetons vont dans l’en-tête d’autorisation de chaque requête HTTP, jamais dans la chaîne de requête. Un jeton invalide ou expiré reçoit un HTTP 401. Un jeton valide avec une portée insuffisante reçoit un 403 accompagné de WWW-Authenticate: Bearer error="insufficient_scope", scope="...", et les clients DEVRAIENT répondre en demandant les portées supplémentaires.

L’enregistrement des clients est moins tranché. Les Client ID Metadata Documents sont un DEVRAIT. L’enregistrement dynamique de client est un PEUT, et la spécification le dit déprécié, conservé par souci de compatibilité ascendante. Les clients DOIVENT comparer le paramètre iss à l’émetteur enregistré avant d’envoyer le code d’autorisation au point de terminaison de jeton, selon la RFC 9207.

SituationTransportAutorisation
Outil local pour un seul utilisateur, secrets dans votre environnementstdioHors spécification ; lire les identifiants dans l’environnement
Service distant qui agit sur les données d’un utilisateur auprès d’une API tierceStreamable HTTPProfil OAuth complet : métadonnées de ressource protégée, vérification de l’audience, aucune transmission
Service distant sans comptes utilisateursStreamable HTTPLa validation d’Origin reste un DOIT ; l’autorisation est facultative selon la spécification

Sources