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.
| Role | Definition officielle (traduite) | Exemple concret |
|---|---|---|
| MCP Host | L'application d'IA qui coordonne et gere un ou plusieurs clients MCP | Claude Code, Claude Desktop, VS Code |
| MCP Client | Un composant qui maintient une connexion vers un serveur MCP et en obtient du contexte pour le host | La connexion interne vers ton serveur Postgres |
| MCP Server | Un programme qui fournit du contexte aux clients MCP | Ton 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.
| Couche | Role | Ce qui casse quand ca casse |
|---|---|---|
| Data layer (couche donnees) | Le protocole JSON-RPC 2.0 : decouverte des capacites, primitives (tools, resources, prompts), notifications | Un outil renvoie une erreur, un schema d'entree est invalide |
| Transport layer (couche transport) | Le canal de communication : etablissement de connexion, cadrage des messages, authentification | Le 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 ?
| Primitive | Ce que c'est | Qui controle | Exemple data |
|---|---|---|---|
| Tools (outils) | Des fonctions executables que l'IA peut appeler pour agir | Le modele | run_select, get_lineage, create_ticket |
| Resources (ressources) | Des sources de donnees passives, en lecture, qui fournissent du contexte | L'application | Le schema d'une base, le contenu d'un fichier, le manifest.json de dbt |
| Prompts | Des modeles d'interaction reutilisables et parametres | L'utilisateur | Un 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.
| Methode | Ce qu'elle fait |
|---|---|
tools/list | Decouvrir les outils disponibles (avec leurs schemas) |
tools/call | Executer un outil |
resources/list | Lister les ressources directes |
resources/templates/list | Decouvrir les modeles de ressources parametres |
resources/read | Lire le contenu d'une ressource |
prompts/list | Lister les prompts disponibles |
prompts/get | Recuperer 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 protocole2026-07-28. La doc recommande desormais de s'integrer directement aux API des fournisseurs de modeles, et d'ecrire les logs surstderr(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, jamaisnull. 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.
| Annotation | Signification | Defaut |
|---|---|---|
title | Titre lisible par un humain, pour l'affichage | aucun |
readOnlyHint | Si vrai, l'outil ne modifie pas son environnement | false |
destructiveHint | Si vrai, l'outil peut faire des modifications destructrices. N'a de sens que si readOnlyHint vaut false | true |
idempotentHint | Si vrai, rappeler l'outil avec les memes arguments n'a pas d'effet supplementaire. N'a de sens que si readOnlyHint vaut false | false |
openWorldHint | Si vrai, l'outil interagit avec un monde ouvert d'entites externes. Une recherche web : monde ouvert. Un outil de memoire locale : monde ferme | true |
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
ToolAnnotationssont des indices. Elles ne garantissent pas une description fidele du comportement de l'outil (y compris les proprietes descriptives commetitle). 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.
| Transport | Comment ca marche | Quand l'utiliser |
|---|---|---|
| stdio | Entree et sortie standard entre processus locaux sur la meme machine. Pas de reseau, performance optimale | Ton serveur maison, un serveur lance par npx ou uv, tout ce qui touche a ta machine |
| Streamable HTTP | HTTP POST pour les messages client vers serveur, avec Server-Sent Events optionnel pour le streaming | Un serveur distant, un service SaaS, un serveur partage par l'equipe |
Deux consequences pratiques a connaitre :
- En stdio, n'ecris jamais sur la sortie standard. La doc est formelle : ecrire sur
stdoutcorrompt les messages JSON-RPC et casse ton serveur. En Python,print()ecrit surstdoutpar defaut. Utilise le modulelogging, qui ecrit surstderr. - 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: trueet un message explicatif danscontent. 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-05et 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 parserver/discover. Les versions2025-11-25et 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 fluxsubscriptions/listenen nommant les types de notifications qu'il veut recevoir. samplingetloggingcote client sont deprecies en2026-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
- Architecture MCP : participants, couches, primitives (2026-07-28)
- Concepts serveur : tools, resources, prompts
- Specification des tools (2026-07-28)
- Specification des resources (2026-07-28)
- Specification des tools (2025-11-25), pour comparer avec la version precedente
- Schema TypeScript officiel : interface ToolAnnotations
- Construire un serveur MCP : regles de logging en stdio
- modelcontextprotocol/modelcontextprotocol - la spec et le schema du protocole, avec un dossier par version de date dans
schema/ - modelcontextprotocol/servers - code d'exemple sur les primitives : le serveur
src/everything/les implemente toutes, une par une