Applied AI — Niveau Avancé

Session 5 : MCP en profondeur

Yann Isola — Formation professionnelle en IA
Préparation Claude Certified Architect

🎯 Architecture · Transports · Primitives · Serveurs & Clients · Sécurité

Objectifs de la session

  1. Maîtriser l'architecture Hôte ↔ Client ↔ Serveur et JSON-RPC 2.0
  2. Choisir un transport : stdio vs Streamable HTTP
  3. Distinguer Tools / Resources / Prompts par leur contrôleur ← point d'examen n° 1
  4. Construire un serveur et un client MCP (SDK Python)
  5. Appliquer le modèle de sécurité et les patrons avancés

3 h 30 : 2 h de cours + 90 min d'exercices + quiz

Le problème M×N

Avant MCP : chaque application IA × chaque outil = un connecteur spécifique

  • 10 applications × 20 outils = 200 intégrations à écrire et maintenir
  • Chaque éditeur réinvente l'authentification, les schémas, les erreurs

Avec MCP : M + N

  • Chaque outil expose un serveur MCP
  • Chaque application implémente un client MCP
  • Tout serveur fonctionne avec tout hôte compatible

Analogie officielle : MCP est à l'IA ce que USB-C est au matériel — un port universel.
MCP = Model Context Protocol (protocole de contexte de modèle), standard ouvert initié par Anthropic ⚠ (écosystème en évolution rapide)

Architecture : les trois composants

┌──────────────── HÔTE (IDE, app de chat) ────────────────┐
│   ┌──────────┐     ┌──────────┐     ┌──────────┐        │
│   │ Client 1 │     │ Client 2 │     │ Client 3 │        │
│   └────┬─────┘     └────┬─────┘     └────┬─────┘        │
└────────┼────────────────┼────────────────┼──────────────┘
         │ JSON-RPC       │ JSON-RPC       │ JSON-RPC
    ┌────▼─────┐     ┌────▼─────┐     ┌────▼─────┐
    │filesystem│     │  GitHub  │     │PostgreSQL│
    └──────────┘     └──────────┘     └──────────┘

Règle d'or : 1 client ↔ 1 serveur. 3 serveurs ⇒ 3 clients.

Rôles précis

Composant Responsabilités
Hôte Embarque le modèle, choisit les serveurs, applique consentement & politiques de sécurité
Client Géré par le SDK · session · négociation de capacités · routage des messages
Serveur Processus indépendant · expose Tools, Resources, Prompts

❗ Erreur classique : croire qu'un client se connecte à plusieurs serveurs. Non. C'est l'hôte qui multiplie les clients.

JSON-RPC 2.0 : la langue commune

3 types de messages, sur tous les transports :

Type id ? Réponse attendue ?
Requête ✅ ✅
Réponse ✅ (le même)
Notification ❌ ❌
{ "jsonrpc": "2.0", "id": 42, "method": "tools/call",
  "params": { "name": "rechercher_commande",
              "arguments": { "numero": "CMD-2026-0193" } } }

Cycle de vie d'une session

  1. initialize — client → serveur : version + capacités
  2. Serveur → client : ses propres capacités (tools, resources, prompts, subscriptions...)
  3. notifications/initialized — session ouverte
  4. Découverte : tools/list · resources/list · prompts/list
  5. Opérations : tools/call · resources/read · prompts/get
  6. Fermeture propre du transport

Négociation de capacités — pourquoi ?

Question d'examen garantie.

  • Chaque partie déclare ce qu'elle sait faire au handshake
  • On n'utilise que l'intersection des capacités
  • Un serveur n'envoie jamais une notification que le client n'a pas déclaré supporter

🎓 Finalité : évolution du protocole sans rupture de compatibilité.
Client ancien + serveur récent = coopération sur le socle commun.

Transport 1 : stdio

Le serveur = sous-processus de l'hôte. JSON-RPC sur stdin/stdout.

  • ⚡ Latence minimale, zéro réseau
  • 🔒 Aucun port ouvert · permissions héritées de l'OS
  • 🔁 Cycle de vie lié à l'hôte
  • 📓 Logs sur stderr — jamais stdout !
{ "mcpServers": { "commandes": {
    "command": "python",
    "args": ["/opt/mcp/serveur_commandes.py"] } } }

Le piège stdio (démo en exercice)

@mcp.tool()
def rechercher(numero: str) -> dict:
    print("debug: étape 2 OK")   # ← 💥 BOUM
    ...
  • print() écrit sur stdout → corrompt le flux JSON-RPC
  • Le client échoue à analyser les messages → session cassée

✅ Solution : logging configuré vers stderr, ou fichier.

Question piège d'examen classique.

Transport 2 : Streamable HTTP

Remplace SSE (déprécié) pour les serveurs distants.

  • POST des messages JSON-RPC vers un point de terminaison unique (/mcp)
  • Le serveur peut répondre en JSON simple ou ouvrir un flux (résultats progressifs, notifications)
  • Sessions via en-tête Mcp-Session-Id, reprise après coupure
  • 🔐 Authentification web : OAuth 2.1, jetons bearer

Un déploiement → des milliers de clients.

Matrice de décision transport

Critère stdio Streamable HTTP
Localisation Locale Distante / cloud
Utilisateurs 1 N (mutualisé)
Auth Héritée de l'OS OAuth 2.1 / jetons
Fichiers locaux ✅ direct ❌
Déploiement Avec l'app hôte Service web opéré

🎓 Règle de poche : local mono-utilisateur → stdio · service partagé authentifié → Streamable HTTP · SSE seul → réponse fausse.

Les trois primitives

Le cœur de la certification

La bonne question n'est pas « que fait-elle ? »
mais « QUI décide de l'invoquer ? »

Le tableau à mémoriser

Primitive Contrôleur Déclencheur Analogie
Tool 🤖 Le modèle Le LLM décide pendant son raisonnement Les mains
Resource 🖥️ L'application L'hôte choisit quoi injecter au contexte Les yeux (lecture seule)
Prompt 👤 L'utilisateur Menu / commande slash explicite Formulaire pré-rempli

model-controlled · application-controlled · user-controlled

Tools — invoqués par le modèle

  • Entrée : JSON Schema — le modèle sait quels arguments fournir
  • Sortie : blocs de contenu structurés (text, image, ressource incorporée)
  • Effets de bord autorisés ⇒ consentement utilisateur exigé
{ "name": "rembourser_commande",
  "description": "Rembourse une commande. Refusé > 500 $.",
  "inputSchema": { "type": "object",
    "properties": { "numero": {"type": "string"},
                    "montant": {"type": "number"} },
    "required": ["numero", "montant"] } }

Resources — exposées par l'application

  • Identifiées par URI : file:///rapports/q2.pdf · db://clients/12345 · api://meteo/paris
  • Lecture seule — si ça modifie un état, c'est un Tool
  • Templates : db://commandes/{numero} — familles de ressources
  • Abonnements : resources/subscribenotifications/resources/updated

Pourquoi « application-controlled » ? L'hôte filtre ce qui entre dans le contexte : protection de la fenêtre et de la confidentialité.

Prompts — déclenchés par l'utilisateur

  • Modèles d'invites paramétrés : nom, description, arguments
  • Présentés par l'hôte : commande slash, menu déroulant
  • Cas d'usage : flux répétables et structurés
{ "name": "rapport_incident",
  "arguments": [
    { "name": "severite", "required": true },
    { "name": "systeme",  "required": true } ] }

Exemples : analyse de PR · revue de contrat · rapport d'incident

Le piège d'examen n° 1

« Une recherche en base est une lecture, donc c'est une Resource » — FAUX ❌

  • Si le modèle décide dynamiquement de chercher ⇒ Tool (même en lecture)
  • Si l'application pré-sélectionne la donnée à injecter ⇒ Resource

La frontière passe par le contrôleur, pas par lecture/écriture.

Arbre de décision

  1. Effet de bord / modification d'état ? → Tool
  2. Donnée à lire, injectée sur décision de l'application ? → Resource
  3. Flux déclenché explicitement par l'humain, avec paramètres ? → Prompt
  4. Le modèle doit décider seul d'aller chercher ? → Tool, même en lecture

(Sélecteur interactif dans la page web de la session)

Serveur MCP en Python — squelette

from mcp.server.fastmcp import FastMCP
mcp = FastMCP("commandes")

@mcp.tool()
def rechercher_commande(numero: str) -> dict:
    """Le modèle décide QUAND appeler — rédigez pour lui."""
    ...

@mcp.resource("db://commandes/{numero}")
def fiche(numero: str) -> str: ...

@mcp.prompt()
def analyse_litige(numero: str, motif: str) -> str: ...

mcp.run()   # stdio par défaut

⚠ API du SDK en évolution — vérifier la version.

Garde-fou côté serveur : les 500 $

MAX_REMBOURSEMENT = 500.0

@mcp.tool()
def rembourser_commande(numero: str, montant: float) -> dict:
    if montant > MAX_REMBOURSEMENT:
        return { "statut": "refuse", "escalade": True,
                 "raison": "Validation humaine requise" }
    return { "statut": "effectue", ... }

🛡️ Dans le prompt : une suggestion, contournable par injection.
Dans le serveur : une loi, qui tient même si le modèle est manipulé.
Défense en profondeur — question d'examen.

Normalisation à la frontière

Deux serveurs MCP, deux formats de dates :
15/03/2026 (interne) · 27 Mar 2026 (transporteur)

def normaliser_date(valeur: str) -> str:
    for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%d %b %Y"):
        try:
            return datetime.strptime(valeur, fmt).date().isoformat()
        except ValueError: continue
    raise ValueError(f"Format inconnu : {valeur!r}")

✅ ISO 8601 partout, en code, à la frontière.
❌ Jamais « le modèle devinera » — ambiguïté 03/04 = 3 avril ou 4 mars ?

Client MCP en Python

async with stdio_client(params) as (lecture, ecriture):
    async with ClientSession(lecture, ecriture) as session:
        init = await session.initialize()        # négociation
        outils = await session.list_tools()      # découverte
        resultat = await session.call_tool(      # invocation
            "rembourser_commande",
            {"numero": "CMD-1", "montant": 750.0})
  • La ClientSession gère handshake, id, corrélation requête/réponse
  • Plusieurs serveurs ⇒ plusieurs sessions + namespacing des outils
    (nordcommerce.rechercher / transporteur.statut)

Modèle de sécurité — 3 piliers

  1. Isolation des serveurs — processus séparés, client dédié 1:1, aucune fuite inter-serveurs : ils ne se parlent jamais, seul l'hôte voit tout
  2. Consentement utilisateur — tout appel d'outil à effet de bord est présenté et approuvé
  3. Moindre privilège — jetons à portée réduite, répertoires explicitement autorisés

Les sorties d'outils tiers = données non fiables, jamais des instructions (anti-injection de prompt).

L'écosystème

50+ serveurs communautaires ⚠ (croissance rapide — vérifier)

filesystem · GitHub · Slack · PostgreSQL · Google Drive · navigateur · mémoire...

🧭 Réflexe d'architecte :
chercher un serveur existant avant d'en écrire un.
On développe pour ses systèmes métier internes, pas pour les intégrations génériques.

Patrons avancés (1/2)

Enregistrement dynamique d'outils

  • Le serveur ajoute/retire des outils en session
  • Émet notifications/tools/list_changed → le client relance tools/list
  • Cas : outils d'admin exposés après authentification

Abonnement aux ressources

  • resources/subscribenotifications/resources/updated
  • Cas : configuration surveillée, ticket en évolution, tableau de bord

Patrons avancés (2/2)

Chaînage de prompts

  • Un Prompt MCP orchestre une séquence : recherche → vérification → proposition → escalade

Normalisation à la frontière

  • Le serveur/client convertit dates, devises, unités vers un canon (ISO 8601)
  • Le serveur MCP = couche anti-corruption entre systèmes hétérogènes

Synthèse certification — 7 réflexes

  1. Client:Serveur = 1:1, l'hôte orchestre
  2. Tout est JSON-RPC 2.0 (requête / réponse / notification)
  3. stdio = local · Streamable HTTP = distant · SSE = déprécié
  4. Tool = modèle · Resource = application · Prompt = utilisateur
  5. Négociation de capacités = compatibilité évolutive
  6. Garde-fous métier côté serveur (les 500 $ !)
  7. Sorties d'outils = données non fiables + consentement pour les effets de bord

À vous de jouer 🛠️

Exercice 1 — Construire le serveur NordCommerce (40 min)
Tools + garde-fou 500 $ + normalisation de dates + Resource + Prompt

Exercice 2 — Client multi-serveurs (30 min)
2 sessions, namespacing, fusion de données, ISO 8601 partout

Exercice 3 — Atelier de décision Tools/Resources/Prompts (20 min)

Puis : quiz de validation (10 QCM) — seuil certification : 8/10

Session 5 — terminée ✅

Prochaine session : intégration MCP dans les architectures d'agents en production

📄 Guide complet : doc-prof/guide.md
🌐 Démo interactive : webpage/index.html
❓ Quiz : quiz/quiz.md

Applied AI — Yann Isola