Toute communication MCP se fait via JSON-RPC 2.0. Pour la plupart des data engineers, c’est un détail d’implémentation qui reste invisible — les SDK gèrent la sérialisation, et des outils comme le MCP Inspector permettent de tester les serveurs sans toucher aux messages bruts. Mais quand quelque chose se casse en production, ou quand vous construisez un client from scratch, connaître ce qui transite réellement sur le réseau fait gagner des heures.
Cette note documente les types de messages clés avec des exemples réels. Utilisez-la comme référence de débogage.
Le format JSON-RPC 2.0
Chaque message MCP suit la même structure :
{ "jsonrpc": "2.0", "id": <entier ou chaîne>, "method": "<nom-méthode>", "params": { ... }}jsonrpc: toujours"2.0". La chaîne de version est requise dans chaque message.id: un identifiant unique pour cette requête. La réponse inclura le mêmeid, ce qui vous permet d’associer des réponses asynchrones aux requêtes. Utilisez des entiers séquentiels pour la simplicité.method: la méthode RPC appelée (ex. :initialize,tools/list,tools/call).params: paramètres spécifiques à la méthode. Certaines méthodes n’ont pas de params et omettent ce champ.
Les réponses reflètent l’id de la requête et incluent soit un result (succès) soit une error (échec) :
{ "jsonrpc": "2.0", "id": 1, "result": { ... }}{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32600, "message": "Invalid Request" }}JSON-RPC supporte également les notifications — des messages sans id qui n’attendent pas de réponse. MCP utilise les notifications pour des événements comme notifications/tools/list_changed (la liste d’outils du serveur a changé, le client doit la récupérer à nouveau).
La poignée de main d’initialisation
Chaque session MCP commence par un échange d’initialisation. C’est la négociation des capacités : le client annonce ce qu’il peut faire, le serveur répond avec ce qu’il offre. Aucune des deux parties ne peut supposer des capacités au-delà de ce que l’autre déclare.
Le client envoie :
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": { "elicitation": {} }, "clientInfo": { "name": "example-client", "version": "1.0.0" } }}L’objet capabilities déclare ce que le client peut faire. Dans cet exemple, le client supporte l’elicitation — ce qui signifie que le serveur peut lui demander une saisie utilisateur supplémentaire. Un client sans cette capacité l’omettrait, et un serveur bien comporté n’enverrait jamais de requêtes d’élicitation à ce client.
protocolVersion est la version de la spec MCP que le client implémente. Si le client et le serveur négocient des versions incompatibles, la poignée de main échoue. La version actuelle de la spec à début 2026 est 2025-06-18.
Le serveur répond :
{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-06-18", "capabilities": { "tools": {"listChanged": true}, "resources": {} }, "serverInfo": { "name": "example-server", "version": "1.0.0" } }}Les capabilities du serveur déclarent ce qu’il expose :
"tools": {"listChanged": true}— le serveur a des outils, et il notifiera le client si la liste d’outils change dynamiquement"resources": {}— le serveur a des ressources (l’objet vide signifie un support basique sans notifications de changement)
Si le serveur avait aussi des prompts, il inclurait "prompts": {}. S’il supportait le sampling (permettant au serveur de demander des complétions LLM au client), il inclurait "sampling": {}.
Après que les deux parties ont reçu la poignée de main, le client envoie une notification initialized pour signaler qu’il est prêt :
{ "jsonrpc": "2.0", "method": "notifications/initialized"}Notez l’absence d’id — c’est une notification, pas une requête. Aucune réponse attendue.
Découverte des outils
Une fois initialisé, le client peut demander au serveur quels outils il expose :
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list"}Le serveur retourne une liste de définitions d’outils — chacune avec un nom, une description et un JSON Schema pour ses entrées :
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "query_database", "description": "Execute a SQL query against the specified database.", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The SQL query to execute" }, "database": { "type": "string", "description": "Target database name", "default": "production" } }, "required": ["query"] } }, { "name": "list_tables", "description": "List all tables in the specified schema.", "inputSchema": { "type": "object", "properties": { "schema": { "type": "string", "description": "Schema name to list tables from" } }, "required": ["schema"] } } ] }}Le modèle IA lit ces définitions pour comprendre ce que fait chaque outil et quels arguments fournir. C’est pourquoi les docstrings et descriptions sont si importants — le champ description dans le schéma de l’outil est la façon dont l’IA sait quand et comment utiliser un outil. Voir MCP Tool Design Patterns pour rédiger des descriptions qui fonctionnent réellement.
Le même schéma s’applique pour les ressources (resources/list) et les prompts (prompts/list).
Invocation d’outils
Quand l’IA décide d’appeler un outil, le client envoie :
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "query_database", "arguments": { "query": "SELECT * FROM customers LIMIT 10", "database": "production" } }}Le serveur exécute l’outil et répond :
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "Query returned 10 rows:\nid | name | email\n1 | Alice | alice@example.com\n..." } ], "isError": false }}Le tableau content peut contenir plusieurs éléments de types différents : text, image ou des références resource. La plupart des outils de data engineering retournent text. Le flag isError distingue une exécution réussie ayant retourné un résultat d’une exécution ayant échoué — les deux utilisent le champ JSON-RPC result, mais isError: true signale que l’outil lui-même a rencontré un problème (par opposition à une erreur au niveau JSON-RPC, qui utiliserait le champ error).
Gestion des erreurs
Les erreurs JSON-RPC utilisent des codes d’erreur négatifs :
| Code | Signification |
|---|---|
-32700 | Erreur de parse (JSON invalide) |
-32600 | Requête invalide |
-32601 | Méthode introuvable |
-32602 | Params invalides |
-32603 | Erreur interne |
MCP ajoute des erreurs au niveau applicatif par-dessus celles-ci. Quand un outil échoue, le serveur retourne un result avec isError: true et un message d’erreur dans le contenu :
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "Query failed: relation 'customers' does not exist" } ], "isError": true }}Cette distinction importe pour le débogage : une erreur JSON-RPC signifie que le protocole a échoué ; un résultat isError: true signifie que l’outil s’est exécuté mais a rencontré une erreur applicative.
Inspection des messages en pratique
Pour le transport stdio, les messages transitent via stdin/stdout sous forme de JSON délimité par des sauts de ligne. Vous ne pouvez pas facilement les intercepter sans modifier le serveur ou le client. Le MCP Inspector est le bon outil — il agit comme client, envoie ces messages et affiche les réponses dans une interface.
Pour le transport HTTP, vous pouvez utiliser des outils de débogage HTTP standard. Les messages sont des JSON-RPC via des requêtes POST vers l’endpoint du serveur. Un proxy comme Wireshark, mitmproxy ou l’onglet réseau de votre navigateur peut les capturer.
Le scénario de débogage le plus courant : un outil fonctionne dans l’Inspector mais échoue dans Claude Desktop ou Claude Code. Dans ce cas, le problème se situe généralement dans la poignée de main d’initialisation (incompatibilité de capacités), le formatage des arguments (l’IA fournit des arguments différents de ceux testés) ou l’environnement (identifiants disponibles dans un contexte mais pas dans un autre). Comprendre le format filaire aide à raisonner sur la couche où se situe le problème.
Pourquoi JSON-RPC
JSON-RPC 2.0 n’est pas un choix nouveau ou exotique. C’est un standard vieux de 15 ans avec des implémentations dans tous les langages. L’adoption de JSON-RPC par MCP signifie :
- Les outils existants fonctionnent. Des clients, serveurs et utilitaires de test JSON-RPC existent déjà dans chaque écosystème. MCP n’avait pas à inventer une histoire de tests.
- Le débogage est direct. Les messages sont du JSON lisible par un humain. Vous n’avez pas besoin d’un analyseur de protocole spécialisé pour comprendre ce qui se passe.
- La spec est stable. JSON-RPC 2.0 est stable depuis 2013. Le format de sérialisation ne va pas changer.
L’architecture à deux couches — JSON-RPC pour le format des messages, avec stdio ou HTTP comme transport — signifie que MCP fonctionne sur n’importe quelle topologie réseau. Le même format de message fonctionne que le serveur soit un sous-processus sur la même machine ou un service cloud derrière OAuth. Vous apprenez JSON-RPC une fois ; le transport est un choix de configuration.