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":{...}}
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 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
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
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.