L’Architecture du protocole MCP décrit la communication comme circulant principalement du client vers le serveur : le client découvre les outils, le client les invoque, le client récupère les ressources. Mais MCP est un protocole bidirectionnel. Les serveurs peuvent également initier des requêtes vers les clients — et ces capacités serveur-vers-client sont appelées primitives client.
Il y en a trois : sampling, elicitation et roots. La plupart des serveurs MCP que vous construirez n’en utiliseront jamais les trois. Mais les comprendre change la façon dont vous concevez des workflows de données multi-étapes, et deux d’entre elles (elicitation et roots) ont des implications directes pour la sécurité.
Sampling : complétions LLM demandées par le serveur
Le sampling permet à un serveur MCP de demander au modèle IA du host de traiter ou d’analyser quelque chose. Au lieu que le serveur retourne des données brutes pour que l’IA les interprète, le serveur peut demander une complétion en cours d’exécution et utiliser le résultat pour décider quoi faire ensuite.
Le flux ressemble à ceci :
- L’utilisateur demande : « Analyse la qualité de la table orders »
- Le host invoque l’outil
analyze_tablesur votre serveur MCP - Votre serveur interroge la table, obtient des statistiques (taux de nulls, comptages distincts, nombre de lignes)
- Votre serveur envoie une requête de sampling au client : « Voici les statistiques — résumez les problèmes de qualité clés »
- Le LLM du client génère le résumé
- Votre serveur reçoit le résumé et l’inclut dans la réponse finale de l’outil
Sans sampling, les étapes 4-6 n’existent pas. Le serveur retourne des statistiques brutes et l’IA génère le résumé après avoir obtenu le résultat de l’outil. Avec le sampling, le serveur peut effectuer un raisonnement multi-étapes plus sophistiqué — calculer quelque chose, l’interpréter, brancher selon l’interprétation, retourner un résultat plus riche.
Quand le sampling est utile pour le data engineering
La plupart des serveurs MCP de data engineering n’ont pas besoin du sampling. Le pattern courant — l’outil récupère des données, l’IA les interprète — couvre la majorité des cas d’usage.
Le sampling est utile lorsque le serveur doit prendre des décisions basées sur le jugement de l’IA en cours d’exécution :
- Triage automatisé : un outil de monitoring de pipeline récupère les logs d’erreur, les envoie au LLM pour la classification de gravité, puis route différentes gravités vers différents chemins de réponse
- Inférence de schéma : un serveur récupère des lignes d’exemple d’une table inconnue, demande au LLM d’inférer les types de données et de suggérer des descriptions de colonnes, puis structure cette sortie en YAML dbt
- Analyse itérative : un serveur récupère un plan de requête, demande au LLM d’identifier les problèmes de performance potentiels, exécute des requêtes explain ciblées sur les parties signalées, et assemble un rapport complet
Dans chaque cas, le jugement de l’IA est nécessaire à mi-chemin de l’exécution du serveur — pas seulement à la fin.
Déclaration de capacité
Le sampling ne fonctionne que si le client l’a déclaré lors de l’initialisation :
{ "capabilities": { "sampling": {} }}Tous les clients ne supportent pas le sampling. Avant de concevoir une logique serveur qui en dépend, vérifiez si vos clients cibles disposent de cette capacité. Claude Desktop et Claude Code le supportent tous les deux. Vérifiez pour les autres.
Elicitation : saisie utilisateur demandée par le serveur
L’elicitation permet à un serveur MCP de mettre en pause l’exécution d’un outil et de demander à l’utilisateur des informations supplémentaires avant de continuer. Le serveur ne sait pas tout ce dont il a besoin au départ ; il découvre un manque en cours d’exécution et a besoin de données pour continuer.
Un exemple simple : un outil pour configurer une nouvelle connexion de pipeline de données. Le serveur démarre, détecte que l’utilisateur n’a pas fourni les identifiants pour une source de données particulière, et utilise l’elicitation pour les demander à l’utilisateur — plutôt que d’échouer ou d’exiger que l’utilisateur sache à l’avance exactement quels identifiants sont nécessaires.
# Exemple conceptuel — l'API d'elicitation réelle varie selon le SDK@mcp.tool()async def configure_pipeline(source: str, ctx: Context) -> str: """Configure un nouveau pipeline de données.""" config = get_source_config(source)
if not config.has_credentials: # Demander à l'utilisateur ce dont nous avons besoin credentials = await ctx.elicit( message=f"Le connecteur {source} nécessite des identifiants.", schema={ "type": "object", "properties": { "api_key": {"type": "string", "title": "Clé API"}, "endpoint": {"type": "string", "title": "URL du point d'accès"} }, "required": ["api_key"] } ) config.apply_credentials(credentials)
return configure_source(config)Le client reçoit la requête d’elicitation, présente un formulaire à l’utilisateur, et renvoie la réponse au serveur. L’outil continue avec les données fournies.
Elicitation vs. paramètres d’outil
La question évidente : pourquoi ne pas simplement faire de ces éléments des paramètres d’outil requis ?
Les paramètres d’outil fonctionnent lorsque vous savez à l’avance quelles informations sont nécessaires. L’elicitation fonctionne lorsque le serveur découvre dynamiquement ce dont il a besoin — selon l’état actuel du système, la ressource spécifique à laquelle on accède, ou des informations qui ne deviennent pertinentes qu’à mi-chemin de l’exécution.
Pour le data engineering : configurer l’accès à une API inconnue où les identifiants requis varient selon le type de compte ; définir des paramètres de pipeline où les options disponibles dépendent de ce que le serveur découvre sur le système cible ; demander une confirmation avant une opération destructrice (tronquer une table, supprimer un modèle).
Ce dernier pattern — confirmation avant les opérations destructrices — est particulièrement utile. Le serveur peut décrire exactement ce qu’il est sur le point de faire et demander une approbation explicite, même lorsque la requête originale de l’utilisateur était générale (« nettoie les vieilles tables de staging »).
Implications pour la sécurité
L’elicitation est également un vecteur d’attaque potentiel. Un serveur MCP malveillant pourrait utiliser l’elicitation pour hameçonner les utilisateurs — en présentant une demande d’identifiants qui semble provenir d’une source fiable. Les clients réputés atténuent cela en étiquetant clairement les requêtes d’elicitation avec l’identité du serveur, mais vous devez en être conscient lors de l’évaluation de serveurs tiers. Voir Posture de sécurité pour les agents IA pour la vue d’ensemble.
Roots : frontières du système de fichiers
Les roots sont les plus axées sur la sécurité des trois primitives client. Un root est un chemin de système de fichiers (ou URI) que le client expose au serveur comme une frontière explicite — « vous êtes autorisé à accéder dans ce périmètre ».
Le mécanisme fonctionne ainsi :
- Lors ou après l’initialisation, le client envoie au serveur une liste de roots autorisés
- Ces roots peuvent être
file:///home/user/projects/analyticsougit:///repo/data-pipeline - Le serveur utilise ces roots pour comprendre quelles parties du système de fichiers sont dans son périmètre
- Un serveur bien conçu n’accède qu’aux chemins déclarés dans les roots
{ "jsonrpc": "2.0", "method": "notifications/roots/list_changed"}Lorsque les roots changent (l’utilisateur ouvre un dossier de projet différent, par exemple), le client notifie le serveur afin qu’il puisse mettre à jour son périmètre.
Pourquoi les roots sont importants
Sans roots, rien n’empêche un serveur MCP d’accéder à n’importe quelle partie du système de fichiers qu’il peut atteindre. Un serveur malveillant ou compromis pourrait lire /etc/passwd, parcourir les fichiers d’identifiants (~/.aws/credentials, ~/.ssh/id_rsa) ou exfiltrer tout fichier lisible.
Les roots appliquent le principe du moindre privilège au niveau du système de fichiers. Un serveur qui a accès à votre projet analytics n’a pas besoin de voir votre répertoire personnel.
Pour les serveurs de data engineering qui accèdent à des fichiers locaux — répertoires de projet dbt, configurations de pipeline, jeux de données locaux — les roots offrent un moyen propre de définir ce que le serveur est autorisé à toucher. Le client communique la frontière explicitement plutôt que de s’appuyer uniquement sur des vérifications de permissions côté serveur.
Les roots en pratique
Si vous construisez un serveur qui opère sur des fichiers locaux, interrogez la liste des roots pour valider que les chemins demandés sont dans le périmètre :
@mcp.tool()async def read_dbt_model(model_path: str, ctx: Context) -> str: """Lit le contenu d'un fichier de modèle dbt.""" # Dans une implémentation roots-aware, validez par rapport aux roots autorisés # avant de lire le fichier allowed_roots = await ctx.get_roots() if not any(model_path.startswith(root.uri) for root in allowed_roots): return f"Erreur : {model_path} est en dehors du périmètre autorisé."
with open(model_path) as f: return f.read()Les clients qui supportent les roots le déclarent lors de l’initialisation :
{ "capabilities": { "roots": { "listChanged": true } }}Le flag listChanged: true signifie que le client notifiera le serveur si la liste des roots change en cours de session — utile pour les éditeurs où l’utilisateur peut ouvrir un dossier de projet différent pendant que le serveur est en cours d’exécution.
Les trois ensemble
La plupart des serveurs MCP pour le data engineering utiliseront les roots (s’ils touchent au système de fichiers), utiliseront parfois l’elicitation (pour les flux de configuration guidés), et rarement le sampling (pour les serveurs qui ont besoin du jugement de l’IA en cours d’exécution).
La division des responsabilités vaut la peine d’être clarifiée :
- Primitives serveur (outils, ressources, prompts) — ce que le serveur expose à l’IA et à l’utilisateur
- Primitives client (sampling, elicitation, roots) — ce que le client expose au serveur, permettant des workflows bidirectionnels et des frontières de système de fichiers
Le côté primitives serveur est là où se trouve la majorité du travail créatif lors de la construction d’intégrations de data engineering. Le côté primitives client est là où vous débloquez le raisonnement multi-étapes, l’UX de configuration guidée et les frontières de sécurité rigoureuses. Ils fonctionnent ensemble.