🔌 Protocole ouvertJSON-RPC 2.0Du débutant à l'expert⏱ 18 min de lecture

Le MCP, de A à Z. Comment un assistant IA se branche sur vos données, et comment en écrire un.

Schémas, trames JSON-RPC réelles, tableaux de décision, un serveur fonctionnel en cinquante lignes sans aucune dépendance, et les huit pièges rencontrés en construisant celui de ce site. Aucune connaissance préalable requise ; aucun raccourci à l'arrivée.

Commencer par le débutÉcrire un serveur →Le serveur de ce site →

Essayer avant de lire. Les 17 outils, exécutés pour de vrai.

Cette console appelle le même code que le serveur MCP : mêmes validations, mêmes référentiels, même texte de sortie. Posez une question, ou dépliez la trame JSON-RPC pour voir exactement ce qui circule entre le client et le serveur. C'est ce que le reste de la page explique.

Console MCP paypedia17 outils

Initialisation des outils paypedia…
Essayez :
    Aucun IBAN, aucun numéro de carte complet ne quitte la machine.Outils du site →

    En une phrase

    Le MCP (Model Context Protocol) est un protocole ouvert qui décrit comment un assistant IA parle à un outil extérieur : une base, une API, un référentiel, un dossier de fichiers. Il ne sert pas à faire parler deux modèles entre eux, mais à donner au modèle des actions et des données vérifiables, décrites dans un format qu’il comprend.

    🔑
    L’analogie qui suffit
    Avant, chaque intégration IA était un câble sur mesure : un prompt qui décrit une API, un bout de code de collage, à refaire pour chaque assistant. Le MCP est le connecteur normalisé : on câble une fois côté outil, et n’importe quel client compatible s’y branche.
    1
    protocole (JSON-RPC 2.0)
    3
    rôles : hôte, client, serveur
    4
    primitives, dont une suffit
    2
    transports : stdio, HTTP

    Publié fin 2024 par Anthropic et depuis adopté par d’autres éditeurs, le MCP est une spécification, pas un produit : il n’impose ni langage, ni bibliothèque, ni hébergeur. Le serveur de ce site, par exemple, tient dans un fichier Node sans aucune dépendance.

    Le problème qu’il résout

    Un modèle seul ne connaît que ce qu’il a appris. Pour qu’il réponde juste sur vos données, trois approches ont coexisté, et deux vieillissent mal.

    ApprocheCommentLimite
    Tout dans le promptOn colle les données dans la conversationCoûteux, périmé dès la minute suivante, plafonné par la fenêtre de contexte
    Intégration maisonDu code de collage par assistant et par outilN × M intégrations à maintenir ; changer d’assistant, c’est tout réécrire
    MCPL’outil s’expose une fois, tous les clients s’y branchentN + M : il faut écrire le serveur, mais une seule fois
    Trois façons de donner des données à un modèle
    ℹ️
    Le gain le moins évident
    Ce n’est pas l’économie de code, c’est la traçabilité. Un outil renvoie une donnée que l’on peut citer, dater et vérifier, au lieu d’une reformulation de mémoire par le modèle. Sur un sujet réglementé comme le paiement, c’est la différence entre une réponse utilisable et une réponse à ne pas transmettre à un client.

    Les trois rôles

    L'hôteClaude Desktop, Claude Code, votre agentClient MCPune connexion par serveurlance et piloteJSON-RPC 2.0paypedia17 outils · stdioréférentiels locauxJSON-RPC 2.0githubdépôts · HTTPAPI distanteJSON-RPC 2.0votre serveurmétier maisonbase, fichiers…Le modèle ne touche jamais la ressource : il demande, le serveur exécute et répond.
    • L’hôte : l’application que vous utilisez (Claude Desktop, Claude Code, votre propre agent). C’est elle qui décide quels serveurs lancer et qui demande votre consentement.
    • Le client : la partie de l’hôte qui parle le protocole. Une connexion par serveur, isolée des autres : un serveur ne voit ni les autres serveurs, ni la conversation entière.
    • Le serveur : votre code. Il déclare ce qu’il sait faire et exécute les appels. Il ne « parle » pas au modèle : il répond au client, qui remet la réponse au modèle.
    ⚠️
    Ce que le serveur ne voit pas
    Un serveur MCP reçoit les arguments d’un appel, pas votre historique de conversation. C’est une garantie de cloisonnement, et une contrainte de conception : votre outil doit être utile avec les seuls arguments déclarés dans son schéma.

    Les deux transports

    stdioHTTP (streamable / SSE)
    Où tourne le serveurSur votre machine, lancé par l’hôteSur un serveur distant
    CanalEntrée/sortie standard du processusRequêtes HTTP + flux d’événements
    AuthentificationAucune : c’est déjà votre sessionObligatoire (OAuth, jeton…)
    Données sensiblesNe quittent jamais la machineSortent de votre réseau
    Multi-utilisateursNon : un processus par clientOui
    Bon pourFichiers locaux, référentiels, outils personnelsSaaS, API d’entreprise, équipes
    stdio ou HTTP : choisir selon où vivent les données
    ⚠️
    La règle d’or du stdio
    En stdio, la sortie standard est le protocole. Un console.log de débogage, une bannière de démarrage, un avertissement de dépendance : n’importe quel octet hors JSON casse la session, et le client affiche une erreur de parsing incompréhensible. Tous les logs vont sur stderr.

    La poignée de main, étape par étape

    Client MCPServeurinitializeversions et capacités échangéesnotifications/initializedaucune réponse attenduetools/listle client découvre les outilstools/callname + arguments validés par le schémaresult.content[]texte remis au modèleSur stdio, un seul octet hors protocole sur la sortie standard casse la session.

    Tout passe par JSON-RPC 2.0 : un message JSON par ligne. Une requête porte un id et attend une réponse ; une notification n’en a pas et n’attend rien. Voici une session réelle, réduite à l’essentiel.

    1. Le client se présente, le serveur répond
    → {"jsonrpc":"2.0","id":1,"method":"initialize","params":{
         "protocolVersion":"2025-06-18","capabilities":{},
         "clientInfo":{"name":"claude-code","version":"1.0"}}}
    
    ← {"jsonrpc":"2.0","id":1,"result":{
         "protocolVersion":"2025-06-18",
         "capabilities":{"tools":{"listChanged":false}},
         "serverInfo":{"name":"paypedia","version":"1.0.0"}}}
    
    → {"jsonrpc":"2.0","method":"notifications/initialized"}      ← pas d'id : AUCUNE réponse
    2. Le client découvre les outils, puis en appelle un
    → {"jsonrpc":"2.0","id":2,"method":"tools/list"}
    ← {"jsonrpc":"2.0","id":2,"result":{"tools":[
         {"name":"iban_check","description":"Valide un ou plusieurs IBAN…",
          "inputSchema":{"type":"object","properties":{"input":{"type":"string"}},
                         "required":["input"],"additionalProperties":false}} ]}}
    
    → {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
         "name":"iban_check","arguments":{"input":"FR7630006000011234567890189"}}}
    ← {"jsonrpc":"2.0","id":3,"result":{
         "content":[{"type":"text","text":"## FR76 3000 6000 …\n**VALIDE** · France (FR)…"}],
         "isError":false}}
    MéthodeTypeRôle
    initializerequêteNégocier la version du protocole et annoncer ses capacités
    notifications/initializednotificationLe client est prêt. Ne rien renvoyer
    tools/listrequêteRenvoyer le catalogue : nom, description, schéma d’entrée
    tools/callrequêteExécuter et renvoyer content[]
    pingrequêteRépondre {} (sert au client à vérifier que le process vit)
    Toute autre méthoderequêteRépondre l’erreur -32601 (méthode inconnue)
    Les méthodes qui suffisent à un serveur d’outils
    ℹ️
    Deux façons d’échouer, à ne pas confondre
    Une erreur de protocole (JSON invalide, outil inexistant) se signale par un objet error JSON-RPC. Une erreur d’exécution (IBAN illisible, fichier absent) se renvoie dans le résultat avec isError: true : le modèle la lit et peut se corriger tout seul. Confondre les deux, c’est soit casser la session, soit rendre l’échec invisible.

    Les quatre primitives

    🔧
    Tools : le modèle agit
    Des fonctions que le modèle décide d’appeler. C’est la primitive centrale : la quasi-totalité des serveurs n’expose que ça.
    📄
    Resources : le modèle lit
    Des contenus adressables par URI, que l’hôte peut joindre à la conversation. Utile pour des documents, pas pour des actions.
    💬
    Prompts : l’utilisateur invoque
    Des modèles de requête préparés par le serveur, proposés dans l’interface (une commande, un formulaire).
    🧠
    Sampling : le serveur demande
    Le serveur peut demander une complétion au modèle, via le client. Rare, et à manier avec prudence.
    ✅
    Commencez par les outils, seulement les outils
    Un serveur utile peut n’exposer que tools. C’est le cas de celui de ce site : 17 outils, aucune ressource, aucun prompt. Ajouter des primitives « au cas où » augmente la surface à maintenir sans rien apporter au modèle.

    Anatomie d’un outil

    Un outil, c’est quatre choses : un nom, une description, un schéma d’entrée et du code. Voici la déclaration réelle d’un des 17 outils de ce site, telle quelle.

    La déclaration complète d’un outil (extrait de src/lib/mcpToolsBanking.ts)
    {
      name: 'bank_resolve',                       // identifiant stable, snake_case
      title: 'Résoudre un code banque',           // libellé lisible (facultatif)
      description:
        "Donne le nom et le BIC d'une banque à partir de son code banque national (celui " +
        "embarqué dans l'IBAN) et du pays. Registre de ~22 900 codes dans 44 pays. " +
        "À appeler quand on a un code banque (ex 30004 en France) sans savoir de quel " +
        "établissement il s'agit.",                // ← c'est ELLE qui déclenche l'appel
      inputSchema: {
        type: 'object',
        properties: {
          country:  { type: 'string', maxLength: 8,  description: 'Code pays ISO 3166-1 alpha-2, ex "FR", "DE", "ES".' },
          bankCode: { type: 'string', maxLength: 16, description: 'Code banque national, ex "30004".' },
        },
        required: ['country', 'bankCode'],
        additionalProperties: false,
      },
      run: async (args) => '…le texte remis au modèle…',
    }
    ChampRôleErreur classique
    nameIdentifiant appelé par le clientLe renommer : toutes les configurations existantes cassent
    descriptionDécide si le modèle appelle l’outilDécrire ce que fait l’outil sans dire quand l’appeler
    inputSchemaContrat d’entrée, affiché au modèleLe déclarer sans jamais le valider : le schéma devient de la documentation
    SortieDu texte, lu par un modèleRenvoyer du JSON brut là où une phrase serait comprise du premier coup
    Chaque champ a un rôle, et une façon classique de le rater
    🔑
    La description est du code
    C’est le seul élément qui pilote le comportement du modèle. Une description prescriptive (« À appeler dès qu’un IBAN est mentionné ») change le taux d’appel bien plus qu’un schéma parfait. Écrivez-la pour un collègue pressé, pas pour une documentation d’API.

    Un serveur minimal, sans dépendance

    Il existe des SDK officiels, mais ils ne sont pas obligatoires : le protocole est assez simple pour être implémenté à la main. Voici un serveur stdio complet et fonctionnel. C’est la trame de celui de ce site.

    serveur.mjs (lançable par `node serveur.mjs`)
    const TOOLS = [{
      name: 'somme',
      description: 'Additionne deux nombres. À appeler pour tout calcul de somme.',
      inputSchema: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } },
                     required: ['a', 'b'], additionalProperties: false },
      run: ({ a, b }) => `${a} + ${b} = ${Number(a) + Number(b)}`,
    }]
    
    const send = (m) => process.stdout.write(JSON.stringify(m) + '\n')   // stdout = protocole
    const log  = (m) => process.stderr.write(m + '\n')                   // stderr = logs
    
    let buf = ''
    process.stdin.setEncoding('utf8')
    process.stdin.on('data', (chunk) => {
      buf += chunk
      let nl
      while ((nl = buf.indexOf('\n')) >= 0) {
        const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1)
        if (!line) continue
        let msg
        try { msg = JSON.parse(line) } catch { send({ jsonrpc: '2.0', id: null,
          error: { code: -32700, message: 'JSON invalide' } }); continue }
        if (!msg || typeof msg !== 'object') continue      // « null » est un JSON valide
        const isRequest = msg.id !== undefined && msg.id !== null
    
        if (msg.method === 'initialize') {
          send({ jsonrpc: '2.0', id: msg.id, result: {
            protocolVersion: msg.params?.protocolVersion ?? '2025-06-18',
            capabilities: { tools: {} },
            serverInfo: { name: 'exemple', version: '1.0.0' } } })
        } else if (msg.method === 'tools/list' && isRequest) {
          send({ jsonrpc: '2.0', id: msg.id, result: {
            tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) } })
        } else if (msg.method === 'tools/call' && isRequest) {
          const tool = TOOLS.find((t) => t.name === msg.params?.name)
          if (!tool) return send({ jsonrpc: '2.0', id: msg.id,
            error: { code: -32602, message: 'Outil inconnu' } })
          try {
            send({ jsonrpc: '2.0', id: msg.id,
              result: { content: [{ type: 'text', text: tool.run(msg.params.arguments ?? {}) }] } })
          } catch (e) {                                     // erreur d'EXÉCUTION : dans le résultat
            send({ jsonrpc: '2.0', id: msg.id,
              result: { content: [{ type: 'text', text: 'Erreur : ' + e.message }], isError: true } })
          }
        } else if (isRequest) {
          send({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'Méthode inconnue' } })
        }
        // sans id : c'est une notification → on ne répond RIEN
      }
    })
    log('prêt')
    ℹ️
    Le tester sans client
    Deux lignes suffisent : printf les trames dans le processus et lisez la sortie. C’est ainsi que le serveur de ce site est vérifié par 28 appels réels couvrant ses 17 outils, cas tordus compris : IBAN illisible, BIN trop court, identifiant d’article qui tente de sortir du dossier.

    Huit pièges (tous rencontrés en vrai)

    Cette liste ne vient pas de la spécification mais de la construction du serveur de ce site : chaque ligne est un défaut qui a existé, avec son symptôme et son correctif.

    SymptômeCause réelleCorrectif
    Le client n’arrive pas à parler au serveurUn console.log (ou une bannière de dépendance) sur stdoutTout log sur stderr, sans exception
    Le client se plaint d’une réponse inattendueLe serveur répond à une notification (message sans id)Ne répondre que si id est présent et non nul
    Le serveur meurt en cours de sessionUne ligne null (JSON valide) passée au destructuring, puis le gestionnaire d’erreur plante à son tourVérifier que le message est bien un objet avant tout accès
    Une réponse déverse des dizaines de milliers de ligneslimit: -1 : les bornes du inputSchema ne sont validées par personneBorner les arguments numériques dans le code de l’outil
    Un outil lit un fichier hors de son dossierUn argument (country) concaténé dans un chemin : "../autre" sort du dossierValider le format (/^[A-Z]{2}$/) avant toute lecture
    Le serveur sert des données périmées après quelques heuresUn cache mémoire jamais invalidé alors qu’un cron réécrit les fichiersInvalider sur la date de modification du fichier
    Les horaires renvoyés sont faux de deux heurestoISOString() puis troncature : la conversion en UTC est conservée, le marqueur de fuseau disparaîtFormater en gardant le décalage explicite (16:45 UTC+02:00)
    Les tests passent, la démo est videLes tests appelaient les outils dans un environnement sans accès aux fichiers : chaque outil tombait dans son repliBrancher l’accès aux données dans les tests et affirmer sur le contenu, pas sur l’absence d’erreur
    Le symptôme, la cause, le correctif

    Sécurité : ce qu’il faut avoir en tête

    ⚠️
    Un serveur local tourne avec vos droits
    Il lit ce que vous pouvez lire et écrit ce que vous pouvez écrire. Un serveur MCP installé est un programme de confiance, au même titre qu’une extension de navigateur : on regarde le code, on préfère les sources vérifiables, et on n’expose que le stricte nécessaire.
    • Consentement par action. C’est l’hôte qui demande l’autorisation, mais c’est votre serveur qui décide de sa granularité : un outil « lire » et un outil « écrire » séparés valent mieux qu’un outil « faire ».
    • Contenu lu ≠ instruction. Si votre outil renvoie du texte venu d’ailleurs (page web, fichier, ticket), ce texte peut contenir des instructions destinées au modèle. Ne l’exécutez pas, ne le présentez pas comme une consigne.
    • Validez les arguments. Le schéma est une documentation pour le modèle, pas un pare-feu : rien ne le vérifie à votre place.
    • Ne renvoyez que ce qui est nécessaire. Un outil qui déverse un fichier entier fait fuir plus de données que prévu, et sature le contexte au passage.
    • Secrets. En stdio, ils restent sur la machine ; en HTTP, ils traversent le réseau : ce n’est pas la même analyse de risque.

    Étude de cas : le serveur de ce site

    Le serveur paypedia expose l’expertise de ce site à n’importe quel assistant : 17 outils, zéro dépendance à l’exécution, zéro clé d’API, zéro appel réseau. Tout est calculé en local sur les mêmes référentiels que le site.

    FamilleOutilsCe qu’ils résolvent
    🏦 Bancaire (7)iban_check bin_check bank_resolve bic_search cib_search iban_country_info referentials_infoValidation ISO 13616 / mod-97 / Luhn, résolution d’établissement, annuaire SWIFT
    📰 Actualités (4)news_latest news_search news_get_article jobs_searchVeille paiement publiée, avec ses sources, et les offres d’emploi du secteur
    ⚖️ Litige (3)litige_start litige_step litige_searchParcours chargeback pas à pas, textes de loi, modèles de courrier
    🎓 Formation (3)formation_search formation_get glossary_lookupCours, dossiers encyclopédiques, définitions exactes
    Les 17 outils, par famille

    Le choix d’architecture qui rend la démo possible

    Les outils sont définis une seule fois, dans le code du site, avec une source de données injectée : le serveur lui passe la lecture disque, la page web lui passe fetch. C’est pour ça que la console de démonstration peut afficher le texte exact qu’un client MCP reçoit. Ce n’est pas une maquette, c’est le même code.

    Une définition, deux exécutions
    src/lib
    définit les 17 outils
    validation + mise en forme, sans I/O
    mcp/
    injecte le disque
    public/referentials/** lus en local
    le site
    injecte fetch
    les mêmes fichiers, servis par HTTP
    garde-fou
    un test compare
    catalogue affiché ↔ tools/list du serveur compilé
    ✅
    Ce que ça change à l’usage
    On peut y passer de vrais IBAN et de vrais numéros de carte : rien ne sort de la machine, aucune clé n’est nécessaire, et la réponse cite le référentiel utilisé, y compris quand deux référentiels se contredisent, ce qui arrive avec les listes BIN publiques.

    Le brancher chez vous

    .mcp.json, Claude Code (portée projet)
    {
      "mcpServers": {
        "paypedia": {
          "command": "node",
          "args": ["/path/to/paypedia/mcp/dist/paypedia-mcp.mjs"],
          "env": { "PAYPEDIA_ROOT": "/path/to/paypedia" }
        }
      }
    }
    ClientFichierPortée
    Claude Code.mcp.json à la racine du projetLe projet (approbation demandée au premier lancement)
    Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonL’application (redémarrage nécessaire)
    Votre agentCe que vous voulezÀ vous : lancez le process et parlez-lui en JSON-RPC
    Où déclarer un serveur, selon le client
    ℹ️
    Vérifier que ça marche
    Un serveur bien branché apparaît dans la liste des outils du client, préfixé par son nom. S’il n’apparaît pas : regardez stderr (c’est là que sont vos logs), vérifiez le chemin absolu du fichier, et testez le processus à la main avant d’accuser le client.

    Aller plus loin

    • La spécification (modelcontextprotocol.io) : la référence, versionnée par date (2024-11-05, 2025-03-26, 2025-06-18). Un serveur poli accepte la version demandée par le client s’il la connaît, sinon annonce la sienne.
    • Les SDK officiels : Python et TypeScript en tête, plus une demi-douzaine d’autres langages (Java, Kotlin, C#, Go, Ruby…). Utiles pour les serveurs riches (ressources, prompts, sampling) ; superflus pour un serveur d’outils en stdio.
    • L’inspecteur, l’outil de test officiel : il ouvre une session, liste vos outils et laisse les appeler à la main. Le réflexe avant de conclure « le client est cassé ».
    • Les serveurs de référence (filesystem, git, fetch, mémoire) : à lire comme des exemples de découpage d’outils, pas comme des modèles d’architecture.
    🔑
    Le conseil qui compte
    Écrivez d’abord un outil, avec une description prescriptive et un schéma strict, et vérifiez-le depuis un vrai client. Un serveur de 17 outils bien décrits vaut mieux qu’un serveur de 60 outils que le modèle n’appelle jamais au bon moment.