Protocole MCP et API Rest

SSE vs REST : Comprendre le protocole MCP

🤔 Le malentendu fréquent

Ce que les gens pensent :

"C'est une API REST classique, je fais une requête POST et je reçois du JSON"

La réalité :

"C'est un serveur MCP qui utilise SSE (Server-Sent Events) même pour les requêtes simples"


📊 Comparaison visuelle

API REST classique

POST /api/search HTTP/1.1
Content-Type: application/json
Accept: application/json

{"query": "test"}

Réponse REST classique :

HTTP/1.1 200 OK
Content-Type: application/json

{"results": [...], "count": 42}

MCP avec SSE

POST /legifrance/mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc": "2.0", "method": "tools/list", "params": {}}

Réponse MCP (format SSE) :

HTTP/1.1 200 OK
Content-Type: text/event-stream

event: message
data: {"jsonrpc":"2.0","id":1,"result":{...}}

🔍 Différence clé

Aspect REST classique MCP avec SSE
Content-Type application/json text/event-stream
Format réponse JSON brut event: ...\ndata: {...}
Parsing response.json() Parser ligne par ligne
Header Accept application/json application/json, text/event-stream
Streaming Non (buffer complet) Oui (événements progressifs)

🧩 Pourquoi MCP utilise SSE ?

1. Protocole unifié pour tous les cas d'usage

MCP supporte deux modes de communication :

Mode 1 : Requête/Réponse simple (votre cas)

# Vous envoyez une requête
{"jsonrpc": "2.0", "method": "tools/list", "params": {}}

# Vous recevez UNE réponse
event: message
data: {"jsonrpc":"2.0","result":{...}}

Mode 2 : Notifications asynchrones

# Le serveur peut envoyer des notifications à tout moment
event: notification
data: {"method": "tools/list_changed"}

event: notification
data: {"method": "resources/updated", "params": {...}}

Le SSE permet au serveur d'envoyer des événements au client SANS que le client n'ait fait de requête.

2. Support du streaming de contenu

Pour les outils qui génèrent beaucoup de données :

# Réponse progressive (streaming)
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Début..."}]}}

event: progress
data: {"progress": 50, "total": 100}

event: message
data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"...Suite..."}]}}

3. Maintien de la connexion

Avec SSE, la connexion HTTP reste ouverte : - Le serveur peut pousser des mises à jour - Pas besoin de polling (requêtes répétées) - Plus efficace pour les sessions longues


🎯 "Streamable" vs "Streaming"

Streamable (capacité)

# Le header indique QUE LE CLIENT SUPPORTE le streaming
headers = {
    "Accept": "application/json, text/event-stream"
}

Signification : "Je suis capable de recevoir à la fois du JSON simple ET du streaming SSE"

Streaming (action)

# Le serveur CHOISIT de streamer la réponse
for chunk in long_computation():
    send_sse_event("progress", {"chunk": chunk})

En pratique pour MCP : - Toutes les réponses sont au format SSE (même si elles ne "streament" pas) - C'est le format d'enveloppe du protocole - Même une réponse instantanée utilise event: message\ndata: {...}


💡 Analogie simple

REST classique = Lettre postale

  • Vous envoyez une lettre
  • Vous recevez UNE réponse complète
  • La boîte aux lettres se ferme après

MCP avec SSE = Canal téléphonique

  • Vous appelez et la ligne reste ouverte
  • Vous posez une question (requête)
  • Vous recevez la réponse (event: message)
  • MAIS la ligne reste ouverte
  • Le serveur peut vous rappeler plus tard (notifications)
  • Vous pouvez poser d'autres questions sur la même ligne

🛠️ Implémentation pratique

Code REST classique (NE MARCHE PAS avec MCP)

import requests

# ❌ Ceci échoue avec MCP
response = requests.post(
    "https://mcp.openlegi.fr/legifrance/mcp",
    json={"jsonrpc": "2.0", "method": "tools/list"},
    headers={"Authorization": "Bearer TOKEN"}
)

# ❌ JSONDecodeError: Expecting value
data = response.json()

Pourquoi ça échoue ?

>>> response.text
'event: message\ndata: {"jsonrpc":"2.0",...}\n\n'

>>> response.json()
# Erreur : "event: message" n'est pas du JSON valide !

Code MCP correct (avec parser SSE)

import requests
import json

def parse_sse(text):
    """Extrait le JSON de la ligne 'data:' """
    for line in text.split('\n'):
        if line.startswith('data: '):
            return json.loads(line[6:])  # Retire 'data: '
    raise ValueError("Pas de ligne data: trouvée")

# ✅ Ceci fonctionne
response = requests.post(
    "https://mcp.openlegi.fr/legifrance/mcp",
    json={"jsonrpc": "2.0", "method": "tools/list"},
    headers={
        "Authorization": "Bearer TOKEN",
        "Accept": "application/json, text/event-stream"  # Important !
    }
)

# ✅ Parser le SSE
data = parse_sse(response.text)
print(data)  # {"jsonrpc":"2.0","result":{...}}

📋 Format SSE en détail

Structure complète

event: message
id: 123
data: {"jsonrpc":"2.0","id":1,"result":{...}}
Champ Description Exemple
event: Type d'événement message, notification, progress
id: Identifiant unique (optionnel) 123
data: Payload JSON {"jsonrpc":"2.0",...}
Ligne vide Fin de l'événement \n\n

Événements multiples

event: message
data: {"result": "partie 1"}

event: progress
data: {"percent": 50}

event: message
data: {"result": "partie 2"}

Chaque bloc est séparé par une ligne vide.


⚠️ Erreurs courantes

Erreur 1 : Header Accept manquant

# ❌ ERREUR 406 Not Acceptable
headers = {"Accept": "application/json"}

# ✅ CORRECT
headers = {"Accept": "application/json, text/event-stream"}

Message d'erreur :

{
  "error": {
    "code": -32600,
    "message": "Not Acceptable: Client must accept both application/json and text/event-stream"
  }
}

Erreur 2 : Utiliser response.json() directement

# ❌ JSONDecodeError
data = response.json()

# ✅ Parser le SSE d'abord
data = parse_sse(response.text)

Erreur 3 : Oublier l'initialisation de session

# ❌ 400 Bad Request (pas de session ID)
response = session.post(url, json={"method": "tools/list"})

# ✅ Initialiser d'abord
init_response = session.post(url, json={"method": "initialize"})
session_id = init_response.headers.get('mcp-session-id')
session.headers.update({"mcp-session-id": session_id})

🚀 Avantages du SSE pour MCP

1. Bidirectionnel (serveur → client)

# Le serveur peut notifier le client
event: notification
data: {"method": "tools/list_changed"}

2. Reconnexion automatique

// Côté JavaScript
const eventSource = new EventSource('/mcp');
eventSource.addEventListener('message', (e) => {
  console.log(JSON.parse(e.data));
});
// Se reconnecte automatiquement si déconnecté

3. HTTP standard

  • Pas besoin de WebSocket
  • Passe les proxies/firewalls
  • Compatible avec HTTP/1.1 et HTTP/2

4. Streaming progressif

# Pour les longues réponses
for i in range(100):
    send_event("progress", {"step": i, "total": 100})

send_event("message", {"result": final_data})

📚 Spécification SSE

Format officiel W3C

stream        = [ bom ] *event
event         = *( comment / field ) end-of-line
comment       = colon *any-char end-of-line
field         = 1*name-char [ colon [ space ] *any-char ] end-of-line
end-of-line   = ( cr lf / cr / lf )

Champs standards

Champ Signification
event: Type d'événement (défaut: "message")
data: Données de l'événement (peut être multi-lignes)
id: Identifiant de l'événement (pour reconnexion)
retry: Délai de reconnexion en ms

Exemple multi-lignes

event: message
data: {
data:   "long": "json",
data:   "with": "multiple",
data:   "lines": true
data: }

Le client doit joindre toutes les lignes data:


🔧 Debugging

Voir le format brut avec curl

curl -N \
  -H "Authorization: Bearer TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","params":{}}' \
  https://mcp.openlegi.fr/legifrance/mcp

Sortie :

event: message
data: {"jsonrpc":"2.0","id":null,"result":{"tools":[...]}}

Voir avec Python requests

import requests

response = requests.post(
    "https://mcp.openlegi.fr/legifrance/mcp",
    json={"jsonrpc": "2.0", "method": "tools/list"},
    headers={
        "Authorization": "Bearer TOKEN",
        "Accept": "application/json, text/event-stream"
    }
)

print("=== RAW RESPONSE ===")
print(response.text)
print("=== CONTENT-TYPE ===")
print(response.headers.get('Content-Type'))

📖 Ressources

Spécifications

  • SSE (W3C) : https://html.spec.whatwg.org/multipage/server-sent-events.html
  • MCP Protocol : https://spec.modelcontextprotocol.io/
  • JSON-RPC 2.0 : https://www.jsonrpc.org/specification

Librairies Python

  • requests : HTTP classique (ce que vous utilisez)
  • httpx : HTTP avec support async et streaming
  • sseclient-py : Parser SSE dédié
  • mcp SDK : SDK officiel MCP

🎓 Résumé pour expliquer à quelqu'un

"Le serveur MCP n'est PAS une API REST classique. Il utilise le protocole MCP qui encapsule toutes les réponses au format SSE (Server-Sent Events), même pour les requêtes simples.

C'est pourquoi vous devez : 1. Ajouter text/event-stream dans le header Accept 2. Parser la réponse ligne par ligne pour extraire le JSON de la ligne data: 3. Ne pas utiliser response.json() directement

Le SSE permet au serveur d'envoyer des notifications asynchrones et de streamer les réponses progressivement, même si pour l'instant vous n'utilisez que le mode requête/réponse simple.

C'est comme si vous demandiez une lettre postale mais que vous receviez un télégramme : le contenu est le même, mais le format d'enveloppe est différent."


✅ Checklist de migration REST → MCP

  • [ ] Ajouter text/event-stream au header Accept
  • [ ] Implémenter un parser SSE (parse_sse_response())
  • [ ] Remplacer response.json() par parse_sse(response.text)
  • [ ] Ajouter l'initialisation de session (initialize avec mcp-session-id)
  • [ ] Gérer les notifications asynchrones (optionnel)
  • [ ] Tester avec différents tools
  • [ ] Documenter le changement pour l'équipe

📞 Questions fréquentes

Q: Pourquoi pas du JSON simple ?

R: MCP a besoin de supporter les notifications serveur et le streaming. SSE est le standard HTTP pour ça.

Q: Est-ce que je peux forcer du JSON pur ?

R: Non, c'est le protocole MCP. Utilisez les SDK officiels si vous voulez abstraire ça.

Q: Ça consomme plus de ressources ?

R: Négligeable. Le surcoût est juste quelques lignes de parsing.

Q: Et WebSocket alors ?

R: SSE est plus simple (HTTP standard) et suffisant pour MCP. WebSocket serait overkill.

Q: Ça marche avec tous les clients HTTP ?

R: Oui, mais vous devez parser manuellement. Les SDK MCP le font pour vous.


🎯 Conclusion

MCP avec SSE ≠ API REST classique

Même si vous faites des requêtes POST et recevez des données, le format est différent. C'est une architecture événementielle (event-driven) qui se trouve être synchrone pour vos cas d'usage actuels.

Solution recommandée : Utilisez un SDK MCP officiel ou la classe OpenLegiClient fournie qui gère tout ça pour vous.

Derniere mise a jour : 04 février 2026