IAMaîtriser l'IA générative Plan du corpus
Accueil/MCP, le Model Context Protocol/Architecture et primitives de MCP

Architecture et primitives de MCP

Trois roles, trois primitives, deux transports. Une fois que tu as ca en tete, tous les messages d'erreur MCP deviennent lisibles.

Temps de lecture : 15 min | Niveau : Intermediaire

Ce que tu sauras faire apres

  • Distinguer host, client et serveur sans hesiter.
  • Choisir entre un tool, une resource et un prompt quand tu concois un serveur.
  • Choisir entre le transport stdio et le transport HTTP selon ton cas.
  • Lire une trace JSON-RPC et dire a quelle etape du cycle tu es.
  • Reperer les differences entre les versions du protocole dans la doc.

🏗️ Les trois participants

La doc officielle definit trois roles. Ne les melange jamais, c'est la source numero un des malentendus.

RoleDefinition officielle (traduite)Exemple concret
MCP HostL'application d'IA qui coordonne et gere un ou plusieurs clients MCPClaude Code, Claude Desktop, VS Code
MCP ClientUn composant qui maintient une connexion vers un serveur MCP et en obtient du contexte pour le hostLa connexion interne vers ton serveur Postgres
MCP ServerUn programme qui fournit du contexte aux clients MCPTon serveur entrepot, le serveur Git, le serveur Sentry

La regle a retenir : un client par serveur. Le host cree un client dedie pour chaque serveur auquel il se connecte. Si tu branches trois serveurs, ton host contient trois clients.

Un point qui trompe tout le monde : serveur designe le programme qui fournit les donnees, peu importe ou il tourne. Un serveur en stdio tourne sur ta machine (on parle de serveur local). Un serveur en HTTP tourne chez un editeur (serveur distant). C'est le meme mot pour les deux.

Schema d'architecture

+---------------------------------------------------+
|  HOST : Claude Code (l'application d'IA)          |
|                                                   |
|   +----------+   +----------+   +----------+      |
|   | Client 1 |   | Client 2 |   | Client 3 |      |
|   +----+-----+   +----+-----+   +----+-----+      |
+--------|--------------|--------------|------------+
         |              |              |
   stdio |        stdio |              | HTTP (Streamable)
         |              |              |
   +-----v-----+  +-----v------+  +----v---------+
   | Serveur   |  | Serveur    |  | Serveur      |
   | fichiers  |  | entrepot   |  | Sentry       |
   | (local)   |  | (local)    |  | (distant)    |
   +-----------+  +-----+------+  +--------------+
                        |
                  +-----v------+
                  | PostgreSQL |
                  +------------+

Remarque importante sur ce schema : le serveur entrepot est le seul a parler a PostgreSQL. Le host ne voit jamais la base directement. C'est ce qui rend le controle des permissions possible : tu verrouilles au niveau du serveur, pas au niveau de l'IA.


🧱 Les deux couches

MCP est decoupe en deux couches. Tu vas les croiser dans les messages d'erreur, donc sache lesquelles font quoi.

CoucheRoleCe qui casse quand ca casse
Data layer (couche donnees)Le protocole JSON-RPC 2.0 : decouverte des capacites, primitives (tools, resources, prompts), notificationsUn outil renvoie une erreur, un schema d'entree est invalide
Transport layer (couche transport)Le canal de communication : etablissement de connexion, cadrage des messages, authentificationLe serveur ne demarre pas, erreur 401, connexion refusee

Retiens le reflexe de debug : si ton serveur n'apparait pas du tout, c'est le transport. Si ton serveur apparait mais un outil echoue, c'est la couche donnees.

MCP utilise JSON-RPC 2.0 comme protocole d'appel. Client et serveur s'envoient des requetes et repondent. Les notifications servent quand aucune reponse n'est attendue.


🧩 Les primitives serveur

Un serveur peut exposer trois primitives. La question qui tranche : qui decide de l'utiliser ?

PrimitiveCe que c'estQui controleExemple data
Tools (outils)Des fonctions executables que l'IA peut appeler pour agirLe modelerun_select, get_lineage, create_ticket
Resources (ressources)Des sources de donnees passives, en lecture, qui fournissent du contexteL'applicationLe schema d'une base, le contenu d'un fichier, le manifest.json de dbt
PromptsDes modeles d'interaction reutilisables et parametresL'utilisateurUn modele "audit de qualite d'une table"

Cette colonne "qui controle" est la cle de la conception :

  • Un tool est choisi tout seul par le modele. Donc sa description doit etre parfaite, et une action dangereuse doit etre protegee.
  • Une resource est choisie par l'application ou par toi. Elle ne s'execute pas toute seule.
  • Un prompt demande une invocation explicite. Dans beaucoup d'applications, les prompts apparaissent sous forme de commandes slash.

Les methodes du protocole

Chaque primitive a ses methodes. Apprends ce tableau, tu le reliras souvent.

MethodeCe qu'elle fait
tools/listDecouvrir les outils disponibles (avec leurs schemas)
tools/callExecuter un outil
resources/listLister les ressources directes
resources/templates/listDecouvrir les modeles de ressources parametres
resources/readLire le contenu d'une ressource
prompts/listLister les prompts disponibles
prompts/getRecuperer la definition complete d'un prompt

Le schema general : */list pour decouvrir, */get ou */read pour recuperer, tools/call pour executer.

Les primitives cote client

Le serveur peut aussi demander des choses au client. Attention, c'est la que les versions divergent.

  • Elicitation : le serveur demande une information supplementaire a l'utilisateur, ou une confirmation. Methode elicitation/create.
  • Sampling (sampling/createMessage) et Logging sont deprecies depuis la version de protocole 2026-07-28. La doc recommande desormais de s'integrer directement aux API des fournisseurs de modeles, et d'ecrire les logs sur stderr (en stdio) ou via OpenTelemetry.

🏷️ Anatomie d'un outil

Voici ce que contient la definition d'un outil, telle que renvoyee par tools/list :

{
  "name": "get_weather",
  "title": "Weather Information Provider",
  "description": "Get current weather information for a location",
  "inputSchema": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "City name or zip code"
      }
    },
    "required": ["location"]
  }
}

Les champs qui comptent :

  • name : identifiant unique. La spec recommande 1 a 128 caracteres, sensible a la casse, uniquement lettres ASCII, chiffres, _, - et .. Pas d'espaces.
  • title : nom lisible pour l'affichage.
  • description : c'est ce que le modele lit pour decider d'appeler l'outil. C'est le champ le plus important.
  • inputSchema : un JSON Schema. Doit etre un objet valide, jamais null. Pour un outil sans parametre, la forme recommandee est { "type": "object", "additionalProperties": false }.
  • outputSchema : optionnel, decrit la sortie structuree. Si tu le fournis, le serveur DOIT renvoyer des resultats conformes.
  • annotations : des proprietes optionnelles qui decrivent le comportement de l'outil.

Les annotations, et pourquoi elles ne sont pas une securite

Ou chercher l'information : la page de specification dit seulement annotations : proprietes optionnelles decrivant le comportement de l'outil. Le detail des champs vit dans le schema TypeScript officiel, fichier schema/2026-07-28/schema.ts, interface ToolAnnotations. J'ai verifie qu'ils y sont toujours dans la version la plus recente du protocole.

AnnotationSignificationDefaut
titleTitre lisible par un humain, pour l'affichageaucun
readOnlyHintSi vrai, l'outil ne modifie pas son environnementfalse
destructiveHintSi vrai, l'outil peut faire des modifications destructrices. N'a de sens que si readOnlyHint vaut falsetrue
idempotentHintSi vrai, rappeler l'outil avec les memes arguments n'a pas d'effet supplementaire. N'a de sens que si readOnlyHint vaut falsefalse
openWorldHintSi vrai, l'outil interagit avec un monde ouvert d'entites externes. Une recherche web : monde ouvert. Un outil de memoire locale : monde fermetrue

Regarde bien les defauts : un outil non annote est considere comme potentiellement destructif et ouvert sur l'exterieur. C'est volontaire, c'est la posture prudente.

Mais le commentaire du schema officiel est sans ambiguite :

Toutes les proprietes de ToolAnnotations sont des indices. Elles ne garantissent pas une description fidele du comportement de l'outil (y compris les proprietes descriptives comme title). Les clients ne devraient jamais prendre de decision d'utilisation d'outil sur la base d'annotations recues de serveurs non fiables.

La page de specification dit la meme chose, en plus ferme :

Pour des raisons de confiance et de securite, les clients DOIVENT considerer les annotations d'outil comme non fiables, sauf si elles viennent de serveurs de confiance.

Traduction pour toi : un serveur malveillant peut ecrire readOnlyHint: true sur un outil qui supprime ta base. Les annotations servent a ameliorer l'ergonomie, pas a te proteger. La protection, c'est le fichier 06.


🔗 Les ressources et leurs URI

Chaque ressource a une URI unique. La spec definit des schemas d'URI standards :

  • https:// : une ressource disponible sur le web, que le client peut aller chercher lui-meme.
  • file:// : des ressources qui se comportent comme un systeme de fichiers (sans forcement correspondre a un vrai disque).
  • git:// : integration avec le controle de version Git.
  • Des schemas personnalises sont autorises, tant qu'ils respectent la RFC 3986.

Les modeles de ressources (resource templates) permettent des URI parametrees, avec le champ uriTemplate :

{
  "uriTemplate": "file:///{path}",
  "name": "Project Files",
  "title": "Project Files",
  "description": "Access files in the project directory",
  "mimeType": "application/octet-stream"
}

Pour un data engineer, un bon exemple maison serait entrepot://schema/{base}/{table} : une URI par table, et le contenu renvoie sa structure.


🚚 Les deux transports

MCP supporte deux mecanismes de transport.

TransportComment ca marcheQuand l'utiliser
stdioEntree et sortie standard entre processus locaux sur la meme machine. Pas de reseau, performance optimaleTon serveur maison, un serveur lance par npx ou uv, tout ce qui touche a ta machine
Streamable HTTPHTTP POST pour les messages client vers serveur, avec Server-Sent Events optionnel pour le streamingUn serveur distant, un service SaaS, un serveur partage par l'equipe

Deux consequences pratiques a connaitre :

  1. En stdio, n'ecris jamais sur la sortie standard. La doc est formelle : ecrire sur stdout corrompt les messages JSON-RPC et casse ton serveur. En Python, print() ecrit sur stdout par defaut. Utilise le module logging, qui ecrit sur stderr.
  2. Le transport HTTP supporte l'authentification standard : jetons bearer, cles d'API, en-tetes personnalises. La recommandation officielle est d'utiliser OAuth pour obtenir les jetons.

Un serveur local en stdio sert typiquement un seul client. Un serveur distant en HTTP sert typiquement de nombreux clients.


🔄 Le cycle d'une requete, de bout en bout

Voici ce qui se passe quand tu demandes "combien de commandes hier ?" avec un serveur entrepot branche.

Etape 1 - Decouverte. Le client envoie server/discover. Le serveur repond avec les versions de protocole supportees et ses capacites.

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "supportedVersions": ["2026-07-28"],
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": {}
    },
    "ttlMs": 3600000,
    "cacheScope": "public"
  }
}

Etape 2 - Liste des outils. Le client envoie tools/list. Il recoit le tableau tools avec, pour chaque outil, son name, sa description et son inputSchema. Le host agrege les outils de tous les serveurs branches dans un registre unique que le modele peut voir.

Etape 3 - Le modele choisit. Le modele lit ta question et les descriptions d'outils, et decide d'appeler run_select.

Etape 4 - Validation humaine. Selon la configuration du host, tu vois une demande d'autorisation. La spec le recommande explicitement :

Pour des raisons de confiance, de securite et de surete, il DEVRAIT toujours y avoir un humain dans la boucle, avec la capacite de refuser les invocations d'outils.

Etape 5 - Appel. Le client envoie tools/call :

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "run_select",
    "arguments": {
      "sql": "SELECT count(*) FROM commandes WHERE date_commande = current_date - 1"
    }
  }
}

Etape 6 - Resultat. Le serveur repond avec un tableau content. Il peut aussi renvoyer structuredContent si un outputSchema est defini.

Etape 7 - Reponse. Le host injecte le resultat dans la conversation, et le modele formule sa reponse.

Les deux types d'erreur

A l'etape 6, deux mecanismes d'erreur existent, et la distinction est utile quand tu ecris un serveur :

  • Erreur de protocole : une erreur JSON-RPC standard (outil inconnu, requete malformee). Le modele a peu de chances de s'en sortir tout seul.
  • Erreur d'execution d'outil : un resultat normal avec isError: true et un message explicatif dans content. Le modele peut lire le message et corriger son appel tout seul.
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Invalid departure date: must be in the future. Current date is 08/08/2025."
      }
    ],
    "isError": true
  }
}

Retiens ca pour le fichier 04 : renvoie tes erreurs metier avec isError: true et un message qui dit quoi corriger. C'est ce qui permet a l'IA de se rattraper sans t'embeter.


⚠️ Ce qui a change entre les versions

Point d'honnetete, parce que tu vas tomber sur des tutoriels perimes.

  • Le protocole est versionne par date. La doc officielle publie 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05 et une version brouillon.
  • En 2026-07-28, le protocole est sans etat (stateless). Chaque requete porte la version de protocole et les capacites dans son champ _meta. La decouverte passe par server/discover. Les versions 2025-11-25 et anterieures utilisaient des identifiants de session assignes par le serveur.
  • Les notifications de changement sont opt-in en 2026-07-28 : le client ouvre un flux subscriptions/listen en nommant les types de notifications qu'il veut recevoir.
  • sampling et logging cote client sont deprecies en 2026-07-28.

Bonne nouvelle : les SDK masquent une grande partie de ces details. Tu n'ecriras quasiment jamais ce JSON a la main. Mais quand tu debugges, tu dois savoir ce que tu lis.


📋 Template : cadrer les primitives de ton futur serveur

Remplis ce tableau avant d'ecrire la moindre ligne de code. La question a te poser pour chaque ligne est toujours la meme : qui declenche ca ?

Serveur MCP : [NOM_DU_SERVEUR]
But en une phrase : [BUT_DU_SERVEUR]

TOOLS - le modele les appelle tout seul
Nom              : [NOM_DE_L_OUTIL_EN_SNAKE_CASE]
Description      : [QUAND_L_UTILISER] / [QUAND_NE_PAS_L_UTILISER] / [CE_QU_IL_RENVOIE]
Entrees          : [NOM_DU_PARAMETRE] : [TYPE] - [FORMAT_ATTENDU]
Lecture seule ?  : [OUI_OU_NON]
Limite de taille : [NOMBRE_DE_LIGNES_MAX]

RESOURCES - l'application les lit
URI       : [SCHEMA_D_URI]://[SEGMENT_1]/[SEGMENT_2]
Contenu   : [CE_QUE_CA_CONTIENT]
Type MIME : [TYPE_MIME]

PROMPTS - l'utilisateur les declenche
Nom       : [NOM_DU_PROMPT]
Ce que ca fait : [DESCRIPTION_EN_UNE_PHRASE]
Arguments : [NOM_ARG_1], [NOM_ARG_2]

Transport choisi : [STDIO_OU_HTTP]
Pourquoi         : [RAISON_EN_UNE_PHRASE]
Actions qui exigent une validation humaine : [LISTE_DES_ACTIONS]

Exemple rempli, pour un serveur qui expose un entrepot de donnees :

Serveur MCP : entrepot
But en une phrase : donner a l'IA le schema reel et un acces SELECT a l'entrepot.

TOOLS - le modele les appelle tout seul
Nom              : describe_table
Description      : Appelle-le AVANT d'ecrire du SQL sur une table. Ne l'utilise pas
                   pour compter des lignes. Renvoie colonnes, types et 3 exemples.
Entrees          : table : string - nom exact renvoye par list_tables
Lecture seule ?  : OUI
Limite de taille : 3 lignes d'exemple

RESOURCES - l'application les lit
URI       : entrepot://schema/ventes/commandes
Contenu   : la structure complete de la table commandes
Type MIME : application/json

Transport choisi : STDIO
Pourquoi         : le serveur tourne sur ma machine et lit une base locale.
Actions qui exigent une validation humaine : aucune, ce serveur est en lecture seule.

A retenir

  • Host, client, serveur : un client par serveur. Le mot "serveur" designe le programme, qu'il soit local ou distant.
  • Deux couches : data layer (JSON-RPC, primitives) et transport layer. Le reflexe de debug decoule de la : serveur absent = transport, outil en echec = donnees.
  • Trois primitives serveur : tools (le modele decide), resources (l'application decide), prompts (l'utilisateur decide).
  • Les annotations d'outil sont des indices, pas une securite. Un serveur non fiable peut mentir.
  • Deux transports : stdio en local (ne jamais ecrire sur stdout), Streamable HTTP a distance.
  • Renvoie tes erreurs metier avec isError: true : le modele peut se corriger tout seul.

Sources

Corpus personnel de formation · genere le 26/09/2026 · source : 02-architecture-et-primitives.md