# Applied AI — Niveau Avancé
# Session 5 : MCP en profondeur

**Programme :** Applied AI — Formation professionnelle en intelligence artificielle
**Instructeur :** Yann Isola
**Niveau :** Avancé — Architectes solutions préparant la certification *Claude Certified Architect*
**Durée recommandée :** 3 h 30 (2 h de cours magistral + 1 h 30 d'exercices pratiques)
**Prérequis :** Sessions 1 à 4 du niveau avancé (orchestration, outils/function calling, gestion de contexte, architecture multi-agents)

---

## Objectifs pédagogiques

À l'issue de cette session, les participants seront capables de :

1. **Décrire** l'architecture complète du protocole MCP (*Model Context Protocol* — protocole de contexte de modèle) : Hôte ↔ Client ↔ Serveur, et le rôle exact de chaque composant.
2. **Choisir et justifier** une couche de transport (stdio local vs Streamable HTTP distant) selon des critères d'architecture, de sécurité et de déploiement.
3. **Distinguer** les trois primitives MCP — Tools, Resources, Prompts — et sélectionner la bonne primitive selon **qui contrôle l'invocation** (le modèle, l'application ou l'utilisateur). C'est un point d'examen central de la certification.
4. **Construire** un serveur MCP complet en Python (SDK officiel) exposant outils, ressources et prompts, avec garde-fous intégrés.
5. **Construire** un client MCP : gestion de session, découverte d'outils, négociation de capacités.
6. **Appliquer** les patrons avancés : enregistrement dynamique d'outils, abonnement aux ressources avec notifications, chaînage de prompts, normalisation de données hétérogènes.

---

## Plan de la session

| Bloc | Durée | Contenu |
|------|-------|---------|
| 1 | 30 min | Architecture MCP : Hôte, Client, Serveur, JSON-RPC |
| 2 | 25 min | Couche de transport : stdio vs Streamable HTTP |
| 3 | 35 min | Les trois primitives : Tools, Resources, Prompts |
| 4 | 30 min | Construire un serveur et un client MCP (SDK Python) |
| 5 | 20 min | Sécurité, écosystème et patrons avancés |
| 6 | 90 min | Exercices pratiques + démonstration interactive (page web) |
| 7 | 10 min | Quiz de validation et synthèse certification |

---

# Bloc 1 — Architecture MCP : Hôte, Client, Serveur

## 1.1 Le problème que MCP résout

Avant MCP, chaque éditeur d'application IA devait écrire un connecteur spécifique pour chaque outil externe : N applications × M outils = N×M intégrations. C'est le problème classique du « M×N » que les standards résolvent (comme USB-C l'a fait pour la connectique, ou LSP — *Language Server Protocol*, protocole de serveur de langage — pour les éditeurs de code).

**MCP transforme M×N en M+N** : chaque outil expose une seule interface standard (un serveur MCP), chaque application implémente une seule interface standard (un client MCP). Tout serveur fonctionne avec tout hôte compatible.

> **Point pédagogique :** l'analogie USB-C est officiellement utilisée par Anthropic. Les participants la retrouveront dans la documentation de certification. Faites-la reformuler par un participant : « MCP est à l'IA ce que USB-C est au matériel : un port universel. »

## 1.2 Les trois composants

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

| Composant | Rôle | Exemples |
|-----------|------|----------|
| **Hôte** (*Host*) | L'application qui embarque le modèle et pilote l'expérience utilisateur. Décide quels serveurs connecter, applique les politiques de sécurité et de consentement. | Claude Desktop, un IDE (Cursor, VS Code), une application métier |
| **Client** (*Client*) | Composant géré par le SDK, **un client par serveur** (relation 1:1 stricte). Maintient la session, effectue la négociation de capacités, route les messages. | Instancié par le SDK MCP dans l'hôte |
| **Serveur** (*Server*) | Fournisseur de capacités : expose Tools, Resources et Prompts via le protocole. Processus indépendant. | Serveur filesystem, serveur GitHub, serveur maison |

**Règle à marteler pour la certification :** la relation Client:Serveur est **1:1**. Un hôte qui se connecte à 3 serveurs instancie 3 clients. Cette isolation est un choix d'architecture délibéré (voir Bloc 5 : sécurité).

## 1.3 JSON-RPC 2.0 : la langue commune

Tous les messages MCP sont des messages **JSON-RPC 2.0** (*Remote Procedure Call* — appel de procédure distant en JSON). Trois types de messages :

1. **Requête** (*Request*) — attend une réponse, porte un `id` :
```json
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "rechercher_commande",
    "arguments": { "numero": "CMD-2026-0193" }
  }
}
```

2. **Réponse** (*Response*) — porte le même `id`, contient `result` ou `error` :
```json
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "content": [
      { "type": "text", "text": "{\"statut\": \"expédiée\", \"montant\": 129.90}" }
    ]
  }
}
```

3. **Notification** (*Notification*) — pas d'`id`, pas de réponse attendue :
```json
{
  "jsonrpc": "2.0",
  "method": "notifications/resources/updated",
  "params": { "uri": "db://commandes/CMD-2026-0193" }
}
```

## 1.4 Le cycle de vie d'une session

1. **`initialize`** : le client envoie sa version de protocole et ses **capacités** (ce qu'il sait gérer : sampling, notifications, etc.). Le serveur répond avec ses propres capacités (tools, resources, prompts, subscriptions...).
2. **`notifications/initialized`** : le client confirme — la session est ouverte.
3. **Découverte** : `tools/list`, `resources/list`, `prompts/list`.
4. **Opérations** : `tools/call`, `resources/read`, `prompts/get`, abonnements...
5. **Fermeture** : terminaison propre du transport.

> **Point certification — négociation de capacités :** le client déclare ce qu'il supporte au *handshake* (poignée de main initiale). Un serveur ne doit jamais envoyer une notification d'abonnement à un client qui n'a pas déclaré la capacité correspondante. À l'examen, on vous demandera pourquoi la négociation existe : **pour permettre l'évolution du protocole sans casser la compatibilité** — un client ancien et un serveur récent peuvent coopérer sur l'intersection de leurs capacités.

---

# Bloc 2 — Couche de transport : stdio vs Streamable HTTP

## 2.1 stdio : le transport local

Le serveur est lancé comme **sous-processus** de l'hôte. Les messages JSON-RPC circulent sur **stdin/stdout** (entrée/sortie standard), un message JSON par ligne.

**Caractéristiques :**
- Latence minimale (pas de réseau).
- Cycle de vie lié à l'hôte : quand l'hôte se ferme, le serveur meurt.
- Sécurité par construction : aucun port réseau ouvert, le serveur hérite des permissions de l'utilisateur local.
- `stderr` reste disponible pour les journaux (ne jamais écrire de logs sur stdout — cela corromprait le flux JSON-RPC ; erreur classique de débutant, et question piège d'examen).

**Configuration typique (Claude Desktop) :**
```json
{
  "mcpServers": {
    "commandes": {
      "command": "python",
      "args": ["/opt/mcp/serveur_commandes.py"],
      "env": { "DB_URL": "postgresql://localhost/boutique" }
    }
  }
}
```

## 2.2 Streamable HTTP : le transport distant

Pour les serveurs distants (mutualisés, multi-utilisateurs, cloud), MCP utilise **Streamable HTTP**, qui **remplace l'ancien transport SSE** (*Server-Sent Events* — événements envoyés par le serveur), déprécié.

**Fonctionnement :**
- Le client envoie ses messages JSON-RPC en `POST` vers un unique point de terminaison (ex. `/mcp`).
- Le serveur répond soit par une réponse JSON simple, soit en ouvrant un **flux** sur la même connexion pour pousser plusieurs messages (résultats progressifs, notifications).
- Support des sessions via en-tête `Mcp-Session-Id`, reprise de flux possible après coupure.

**Caractéristiques :**
- Serveur mutualisé : un déploiement sert des milliers de clients.
- Authentification standard du Web : OAuth 2.1, jetons *bearer*, en-têtes HTTP.
- Passe les pare-feux et les infrastructures d'entreprise (proxies, répartiteurs de charge).

## 2.3 Matrice de décision (à connaître pour l'examen)

| Critère | stdio | Streamable HTTP |
|---|---|---|
| Localisation | Même machine que l'hôte | Machine distante / cloud |
| Nombre d'utilisateurs | 1 (l'utilisateur local) | N (mutualisé) |
| Authentification | Héritée du système d'exploitation | OAuth 2.1 / jetons |
| Latence | Minimale | Réseau (variable) |
| Déploiement | Distribué avec l'application hôte | Opéré comme un service web |
| Accès aux ressources locales (fichiers, périphériques) | Direct | Impossible (ou via tunnel) |
| Cas d'usage type | Serveur filesystem, outils de développement locaux | Serveur SaaS d'entreprise, API métier partagée |

> **Règle de poche certification :** *« Fichiers locaux et mono-utilisateur → stdio. Service partagé, authentifié, scalable → Streamable HTTP. SSE seul → réponse fausse (déprécié). »*

---

# Bloc 3 — Les trois primitives : Tools, Resources, Prompts

C'est **le cœur de la session et de la certification**. La question n'est pas « que fait la primitive ? » mais « **qui décide de son invocation ?** ».

## 3.1 Tableau de contrôle (à mémoriser)

| Primitive | Qui contrôle ? | Déclencheur | Analogie |
|---|---|---|---|
| **Tool** (outil) | **Le modèle** (*model-controlled*) | Le LLM décide d'appeler l'outil pendant son raisonnement | Les mains de l'agent |
| **Resource** (ressource) | **L'application** (*application-controlled*) | L'hôte décide quelles données injecter dans le contexte | Les yeux de l'agent — lecture seule |
| **Prompt** (invite) | **L'utilisateur** (*user-controlled*) | L'humain choisit explicitement un modèle d'invite (menu, commande slash) | Un formulaire pré-rempli |

## 3.2 Tools : invoqués par le modèle

- Entrée décrite par un **JSON Schema** (schéma JSON — format de description de structure de données) : le modèle sait exactement quels arguments fournir.
- Sortie **structurée** : liste de blocs de contenu (`text`, `image`, ressource incorporée) + éventuel `structuredContent` typé.
- Effets de bord autorisés (écrire, envoyer, rembourser...) — d'où l'exigence de **consentement utilisateur** (Bloc 5).

```json
{
  "name": "rembourser_commande",
  "description": "Rembourse une commande. Refusé au-delà de 500 $ sans validation humaine.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "numero": { "type": "string", "description": "Numéro de commande, ex. CMD-2026-0193" },
      "montant": { "type": "number", "description": "Montant en dollars US" }
    },
    "required": ["numero", "montant"]
  }
}
```

## 3.3 Resources : exposées par l'application

- Identifiées par **URI** (*Uniform Resource Identifier* — identifiant uniforme de ressource) : `file:///rapports/q2.pdf`, `db://clients/12345`, `api://meteo/paris`.
- **Lecture seule** : une ressource ne modifie jamais d'état. Si l'action modifie quelque chose, c'est un Tool.
- **Abonnements** : un client peut s'abonner (`resources/subscribe`) et recevoir `notifications/resources/updated` quand la ressource change — l'hôte peut alors relire et rafraîchir le contexte.
- **Templates de ressources** : URI paramétrées (`db://commandes/{numero}`) pour exposer des familles entières de ressources.

**Pourquoi « application-controlled » ?** Parce que c'est l'hôte — pas le modèle — qui décide quelles ressources charger dans le contexte. Cela protège la fenêtre de contexte (l'hôte filtre) et la confidentialité (le modèle ne « fouille » pas librement).

## 3.4 Prompts : déclenchés par l'utilisateur

- Modèles d'invites **paramétrés** exposés par le serveur : nom, description, liste d'arguments.
- L'hôte les présente à l'utilisateur (commande slash, menu). L'utilisateur choisit, remplit les arguments, et le prompt génère un ou plusieurs messages injectés dans la conversation.
- Cas d'usage : flux de travail complexes et répétables — « analyse de PR », « rapport d'incident », « revue de contrat » — où l'on veut garantir la structure de la demande.

```json
{
  "name": "rapport_incident",
  "description": "Génère un rapport d'incident structuré",
  "arguments": [
    { "name": "severite", "description": "P1 à P4", "required": true },
    { "name": "systeme", "description": "Système affecté", "required": true }
  ]
}
```

## 3.5 Arbre de décision (reproduit dans la page web interactive)

1. **L'action modifie un état ou déclenche un effet de bord ?** → **Tool**.
2. **C'est une donnée à lire, et c'est l'application qui doit décider quand l'injecter ?** → **Resource**.
3. **C'est un flux de travail que l'humain déclenche explicitement avec des paramètres ?** → **Prompt**.
4. **Le modèle doit décider seul, au fil du raisonnement, d'aller chercher la donnée ?** → alors même une lecture peut être un **Tool** (ex. `rechercher_client`). La frontière Resource/Tool passe par **qui contrôle**, pas par lecture/écriture seule.

> **Piège d'examen n° 1 :** « une recherche dans une base est une lecture, donc c'est une Resource » — **faux** si c'est le modèle qui doit décider de la déclencher dynamiquement. Une recherche invoquée par le modèle est un Tool. Une donnée pré-sélectionnée par l'application est une Resource.

---

# Bloc 4 — Construire un serveur et un client MCP (SDK Python)

## 4.1 Serveur complet avec garde-fous

Le SDK Python officiel (`mcp`, avec l'API `FastMCP`) fournit trois décorateurs. Exemple complet — serveur de gestion de commandes avec **garde-fou de remboursement** et **normalisation de dates** :

```python
# serveur_commandes.py
# ⚠ Vérifiez la version du SDK : l'API évolue rapidement.
from mcp.server.fastmcp import FastMCP
from datetime import datetime

mcp = FastMCP("commandes")

MAX_REMBOURSEMENT = 500.0  # Garde-fou métier : au-delà, validation humaine

def normaliser_date(valeur: str) -> str:
    """Normalise les dates hétérogènes des systèmes sources vers ISO 8601.
    Les serveurs MCP tiers renvoient des formats variés : c'est au point
    d'intégration de normaliser, jamais au modèle de deviner."""
    formats = ("%Y-%m-%d", "%d/%m/%Y", "%m-%d-%Y", "%d %b %Y", "%Y-%m-%dT%H:%M:%S")
    for fmt in formats:
        try:
            return datetime.strptime(valeur.strip(), fmt).date().isoformat()
        except ValueError:
            continue
    raise ValueError(f"Format de date non reconnu : {valeur!r}")

# ── TOOL : invoqué par le modèle, effets de bord, garde-fou ──
@mcp.tool()
def rembourser_commande(numero: str, montant: float) -> dict:
    """Rembourse une commande. Bloqué au-delà de 500 $ (validation humaine requise)."""
    if montant > MAX_REMBOURSEMENT:
        # Le garde-fou vit CÔTÉ SERVEUR : il s'applique même si le
        # prompt du modèle est manipulé (injection). Défense en profondeur.
        return {
            "statut": "refuse",
            "raison": f"Montant {montant} $ > plafond {MAX_REMBOURSEMENT} $. "
                      "Escalade vers un opérateur humain requise.",
            "escalade": True,
        }
    return {"statut": "effectue", "numero": numero, "montant": montant}

# ── TOOL de lecture contrôlée par le modèle ──
@mcp.tool()
def rechercher_commande(numero: str) -> dict:
    """Recherche une commande par numéro (le modèle décide quand chercher)."""
    brut = {"numero": numero, "date_livraison": "15/03/2026", "statut": "expédiée"}
    brut["date_livraison"] = normaliser_date(brut["date_livraison"])  # → 2026-03-15
    return brut

# ── RESOURCE : donnée en lecture, contrôlée par l'application ──
@mcp.resource("db://commandes/{numero}")
def ressource_commande(numero: str) -> str:
    """Fiche commande complète, exposée à l'hôte via URI paramétrée."""
    return f'{{"numero": "{numero}", "historique": [...], "client": "..."}}'

# ── PROMPT : modèle d'invite déclenché par l'utilisateur ──
@mcp.prompt()
def analyse_litige(numero: str, motif: str) -> str:
    """Flux structuré d'analyse de litige client."""
    return (
        f"Analyse le litige sur la commande {numero}, motif : {motif}.\n"
        "1. Vérifie l'historique de la commande.\n"
        "2. Compare avec la politique de remboursement.\n"
        "3. Propose une résolution ; si remboursement > 500 $, recommande une escalade."
    )

if __name__ == "__main__":
    mcp.run()  # transport stdio par défaut ; mcp.run(transport="streamable-http") pour le distant
```

**Points d'insistance pédagogique :**
- Le garde-fou des 500 $ est **dans le serveur**, pas dans le prompt système. Un garde-fou côté prompt est une suggestion ; un garde-fou côté serveur est une loi. À l'examen : « où placer un contrôle métier critique ? » → au niveau le plus profond, côté serveur/outil.
- La normalisation de dates illustre le rôle du serveur MCP comme **couche anti-corruption** entre systèmes hétérogènes : ISO 8601 en sortie, toujours.

## 4.2 Client MCP : session, découverte, invocation

```python
# client_commandes.py
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    params = StdioServerParameters(
        command="python", args=["serveur_commandes.py"]
    )
    async with stdio_client(params) as (lecture, ecriture):
        async with ClientSession(lecture, ecriture) as session:
            # 1. Handshake : négociation de capacités
            init = await session.initialize()
            print("Capacités serveur :", init.capabilities)

            # 2. Découverte
            outils = await session.list_tools()
            for outil in outils.tools:
                print(f"- {outil.name} : {outil.description}")

            # 3. Invocation
            resultat = await session.call_tool(
                "rembourser_commande",
                {"numero": "CMD-2026-0193", "montant": 750.0},
            )
            print(resultat.content)  # → statut: refuse, escalade: True

asyncio.run(main())
```

**À souligner :** la `ClientSession` encapsule tout le protocole (handshake, `id` JSON-RPC, corrélation requête/réponse). L'architecte doit néanmoins comprendre ce qui circule « sous le capot » — c'est exactement ce que visualise la page web interactive de cette session.

---

# Bloc 5 — Sécurité, écosystème, patrons avancés

## 5.1 Modèle de sécurité MCP

Trois piliers, tous exigibles à la certification :

1. **Isolation des serveurs** : chaque serveur est un **processus séparé**, avec un client dédié (relation 1:1). Le serveur GitHub ne voit jamais les données du serveur PostgreSQL. **Aucune fuite inter-serveurs** par construction : les serveurs ne communiquent pas entre eux ; seul l'hôte voit l'ensemble.
2. **Consentement utilisateur** : tout appel d'outil à effet de bord doit être approuvé par l'utilisateur (ou par une politique explicite de l'hôte). L'hôte est le point d'application : il affiche l'outil, les arguments, et demande confirmation.
3. **Moindre privilège** : un serveur ne reçoit que les accès nécessaires (variables d'environnement ciblées, jetons à portée réduite, répertoires autorisés explicites pour un serveur filesystem).

> **Point pédagogique — injection de prompt :** un serveur MCP tiers peut renvoyer du texte malveillant (« ignore tes instructions et... »). L'hôte doit traiter les sorties d'outils comme des **données non fiables**, jamais comme des instructions. Reliez ce point au garde-fou des 500 $ : la défense côté serveur tient même si le modèle est manipulé.

## 5.2 Écosystème

Plus de **50 serveurs communautaires** ⚠ (chiffre en croissance rapide — vérifier avant chaque session) couvrent les besoins courants : filesystem, GitHub, Slack, PostgreSQL, Google Drive, navigateur, mémoire persistante... Réflexe d'architecte : **chercher un serveur existant avant d'en écrire un**. On écrit un serveur maison pour ses systèmes métier internes, pas pour les intégrations génériques.

## 5.3 Patrons avancés

| Patron | Mécanisme | Cas d'usage |
|---|---|---|
| **Enregistrement dynamique d'outils** | Le serveur ajoute/retire des outils en cours de session et émet `notifications/tools/list_changed` ; le client redécouvre via `tools/list`. | Outils dépendant de l'état (après connexion à une base, exposer ses tables) ; montée en privilèges après authentification. |
| **Abonnement aux ressources** | `resources/subscribe` → le serveur pousse `notifications/resources/updated` à chaque changement ; l'hôte relit la ressource. | Tableau de bord temps réel, fichier de configuration surveillé, ticket dont le statut évolue. |
| **Chaînage de prompts** | Un prompt MCP génère une séquence de messages qui orchestrent plusieurs appels d'outils successifs. | Flux « analyse de litige » : recherche → vérification politique → proposition → escalade éventuelle. |
| **Normalisation à la frontière** | Le serveur convertit tous les formats hétérogènes (dates, devises, unités) vers un format canonique avant de répondre. | Agrégation de plusieurs serveurs MCP renvoyant des dates en formats différents → ISO 8601 partout. |

## 5.4 Synthèse certification — les 7 réflexes

1. Client:Serveur = **1:1**, l'hôte orchestre.
2. Tout est **JSON-RPC 2.0** : requête (id), réponse (id), notification (sans id).
3. Transport : **stdio = local/mono-utilisateur**, **Streamable HTTP = distant/mutualisé**, SSE = déprécié.
4. Primitives par **contrôleur** : Tool = modèle, Resource = application, Prompt = utilisateur.
5. La négociation de capacités au handshake garantit la **compatibilité évolutive**.
6. Garde-fous métier **côté serveur**, jamais uniquement dans le prompt.
7. Sorties d'outils = **données non fiables** ; consentement utilisateur pour les effets de bord.

---

## Conseils d'animation

- **Bloc 1 :** faites dessiner l'architecture par un participant au tableau avant de montrer le schéma. L'erreur fréquente (un seul client pour plusieurs serveurs) surgira d'elle-même — corrigez-la en direct.
- **Bloc 3 :** utilisez le sélecteur de primitives de la page web en mode « quiz oral » : lisez un cas d'usage, faites voter la salle, puis révélez la recommandation.
- **Bloc 4 :** faites tourner le serveur d'exemple en direct avec l'inspecteur MCP (`npx @modelcontextprotocol/inspector python serveur_commandes.py`) si l'environnement le permet ⚠ (commande à vérifier, l'outillage évolue).
- **Erreur à provoquer volontairement :** ajoutez un `print("debug")` dans le serveur stdio et montrez la corruption du flux JSON-RPC. Leçon inoubliable : **les logs vont sur stderr**.
- **Timing :** si le groupe est en retard, comprimez le Bloc 5 (écosystème) et renvoyez vers le guide ; ne comprimez jamais le Bloc 3 (primitives), c'est le plus discriminant à l'examen.

## Références

- Spécification MCP : `modelcontextprotocol.io` ⚠ (versions datées, vérifier la révision courante)
- SDK Python : dépôt `modelcontextprotocol/python-sdk`
- Chapitre 4 du guide de cours (`guide_fr.md`)
