ClickHouse MCP et les limites du mode lecture seule : accès structuré pour les agents
L’accès MCP en lecture seule empêche les écritures, mais pas les erreurs de chiffres, les fuites de données ou la divulgation du schéma. Découvrez ce que montrent les benchmarks de ClickHouse et comment les outils de métriques structurées réduisent le risque.
Presque tous les tutoriels sur la connexion d’un agent IA à une base de données se terminent par une recommandation : utiliser une connexion en lecture seule. Activez readonly=1, accordez uniquement le droit SELECT, et l’agent pourra examiner les données sans jamais les modifier. C’est raisonnable, mais c’est la réponse à une question qui n’était pas le véritable problème.
ClickHouse a récemment testé 28 modèles sur 201 questions réelles issues de son agent d’analyse interne. Le meilleur modèle a eu raison 76,6 % du temps. Peu des réponses incorrectes provenaient de requêtes qui ont échoué ; la plupart étaient des requêtes qui se sont exécutées sans problème mais ont renvoyé des chiffres erronés, et une connexion en lecture seule ne peut empêcher aucune d’entre elles.
Le mode lecture seule détermine si un agent peut écrire. Il ne dit rien sur ce que l’agent peut demander. En fournissant à un modèle un outil SQL, on délègue l’élaboration des requêtes : l’agent choisit les tables, les jointures, le niveau d’agrégation, les filtres ainsi que le nombre de lignes retournées. Le mode lecture seule n’affecte rien de tout cela. Il empêche vos données d’être détruites, mais ne fait rien pour éviter qu’elles soient incorrectes. L’axe important n’est pas lecture contre écriture, mais arbitraire contre structuré.
Ce que fait le serveur MCP officiel de ClickHouse
Le serveur officiel, ClickHouse/mcp-clickhouse, inclut les fonctions list_databases, list_tables et run_query. Par défaut, il ne permet que la lecture. Activer l’écriture nécessite une variable d’environnement, et des instructions dangereuses comme DROP en exigent une autre. Pour les transports HTTP et SSE, l’authentification est obligatoire dès le départ (les tokens porteurs ainsi que OAuth/OIDC fonctionnent tous deux), et le démarrage échoue tant qu’elle n’a pas été configurée. La documentation conseille de traiter le compte de base de données MCP comme n’importe quel client externe disposant de privilèges limités.
Deux points méritent attention. Le délai d’expiration des requêtes est appliqué du côté du client, de sorte que le serveur MCP cesse d’attendre, mais la requête continue de s’exécuter sur votre cluster. De plus, l’option readonly=1 s’applique à chaque requête et ne offre que peu de protection. Même en vous connectant en tant qu’utilisateur ClickHouse ayant un profil souple et des droits étendus, ce paramètre ne protège rien.
Même avec tout parfaitement configuré, une connexion en lecture seule restreinte permet toujours à un agent de composer des requêtes SQL arbitraires sur votre entrepôt. C’est là le véritable problème.
L’échec que aucune configuration ne peut résoudre
La correction des résultats passe en premier, car aucune option de ClickHouse ne peut y remédier. Permettre à un agent d’utiliser du SQL suppose que les modèles sont devenus doués pour l’écrire. Cela est en partie vrai, mais c’est la partie fausse qui est dangereuse.
La meilleure preuve provient de ClickHouse lui-même. Son Agentic Analytics Benchmark, publié le 1er septembre 2026, a réexécuté 201 requêtes issues du trafic de production de son assistant analytique interne contre une reconstruction synthétique de son propre entrepôt. Le meilleur modèle a obtenu 76,6 % de réussite.
Ces questions ont été posées par des employés de l’entreprise, une personne connaissant le domaine lisant chaque résultat. Lorsque près d’un quart des réponses sont erronées, une personne impliquée dans le processus détecte la plupart des erreurs conséquentes. Si l’on met le même modèle au service d’une fonctionnalité destinée aux clients, personne ne peut vérifier la fiabilité des résultats, ni même soupçonner qu’ils pourraient être faux. La précision reste la même, mais le risque est bien plus élevé.
La répartition des échecs est plus révélatrice que le meilleur score. Les erreurs en temps de exécution ont presque disparu, seuls quatre des 28 modèles en ayant généré. Parmi tous les modèles, la principale cause d’échec était le plan : choisir la mauvaise approche expliquait de 53 % à 82 % des erreurs. Selon ClickHouse, la syntaxe et l’exécution sont désormais résolues, et ce qui compte désormais, c’est la modélisation des données et le contexte du schéma.
Le meilleur performeur l’illustre bien. Fable 5.1 présentait la plus faible proportion d’erreurs de plan parmi les dix meilleurs, mais aussi le taux le plus élevé d’échecs dus à des données incorrectes. Il élabore un bon plan puis sélectionne le champ ou la granularité inappropriés. Rien de tout cela ne peut être corrigé par un modèle plus intelligent. Ces problèmes disparaissent lorsque les dimensions, mesures et granularités temporelles disponibles sont prédéfinies.
C’est pourquoi ces erreurs sont dangereuses, et non simplement irritantes. Un plan incorrect qui s’exécute correctement génère une requête qui se parse, s’exécute, renvoie des lignes et produit un chiffre dont l’apparence semble plausible. Il n’y a ni exception ni ligne rouge dans le journal des événements. Quelqu’un lit ces résultats sur un tableau de bord ou dans un résumé Slack et prend des mesures en conséquence.
ClickHouse souligne lui-même une réservation : les réponses de référence ont été établies lorsque trois modèles de fournisseurs différents étaient d’accord, et non à la suite d’une vérification humaine ; seules environ la moitié de ces concordances étaient unanimes, et le jury est qualifié de clément. Le consensus n’est pas une preuve, donc 76,6 % est incertain et probablement légèrement surestimé. Le jugement dépend ici de la nature des erreurs, et non du pourcentage exact.
ClickHouse rend également les choses plus difficiles que de nombreuses bases de données. Les jointures sont coûteuses et complexes ; sa documentation conseille de dénormaliser les données et de limiter le nombre de jointures utilisées. Pour la plupart des algorithmes de jointure, la table de droite est stockée en mémoire sous forme de tableau hash. Lors d’une jointure basée sur un dictionnaire dans une relation un-à-plusieurs, ClickHouse conserve discrètement une seule ligne correspondante et élimine le reste, ce qui donne une réponse erronée mais plus concise, sans générer d’erreur. Un agent examinant votre schéma tombera avec assurance dans de telles pièges. Ni readonly=1, ni un profil de configuration, ni une quota ne sont utiles. Seul le fait de retirer l’autorité d’écriture aux modèles permet de résoudre ce problème.
Le champ de portée du tenant en tant que paramètre, jamais comme limite
Le deuxième type d’échec concerne les fuites de données. Dans un produit multi-locataires, l’identité du locataire est le critère le plus important dans chaque requête. Avec un outil SQL arbitraire, l’agent inscrit ce critère dans une clause WHERE générée en temps réel, ce qui permet au modèle de l’omettre, de se tromper ou d’en être dissuadé par du contenu lu précédemment au cours de la conversation.
Un problème structurel plus profond se cache ici. Lorsqu’on fournit à un serveur MCP une chaîne de connexion, celui-ci se connecte en tant qu’utilisateur ClickHouse unique lié à un seul ensemble de politiques de ligne, sans aucune idée du client derrière chaque requête. Cette information se trouve dans votre application, dans la session authentifiée, et un outil SQL générique la perd avant même que l’agent n’écrive une ligne.
La découverte transmet le schéma
Le troisième problème est plus subtil. Pour interroger un entrepôt, un agent doit d’abord le voir, c’est pourquoi il appelle list_tables, qui renvoie chaque table avec ses colonnes, le nombre de lignes et l’instruction complète CREATE TABLE. Les noms de colonnes, le vocabulaire interne de l’entreprise ainsi que les tables dont on avait oublié l’existence finissent tous dans le contexte d’un modèle, avant de passer dans les journaux, les traces et les fournisseurs d’inférence externes. Il ne s’agit pas d’une fuite théorique, car c’est la première étape obligatoire. Autoriser du SQL arbitraire et exposer le schéma complet reviennent au même.
Accès structuré
Un outil SQL plus performant n’est pas la solution ; en l’absence d’outil, c’est bien là le problème. Notez une fois pour toutes votre vocabulaire analytique, à savoir les dimensions, les mesures, les métriques nommées, les règles propres à chaque utilisateur ainsi que le fonctionnement du temps, et proposez uniquement cela à l’agent. Le modèle choisit parmi un catalogue de questions autorisées sans jamais écrire de requête.
@hypequery/mcp, une bibliothèque conçue à cet effet, propose quatre outils :
list_datasets: liste les ensembles de données enregistrés avec le nombre de leurs dimensions, mesures et métriques.get_dataset_schema: affiche les champs, les métriques nommées, les filtres et les limites d’un ensemble de données.query_metric: exécute une métrique nommée.query_dataset: permet de créer une requête ad hoc à partir des dimensions et mesures sélectionnées.
Il n’existe pas de fonction run_sql, même pas une version restreinte. Une appel se fait de la manière suivante :
{
"name": "query_metric",
"arguments": {
"dataset": "orders",
"metric": "revenue",
"dimensions": ["country"],
"filters": [
{ "field": "status", "operator": "eq", "value": "completed" }
],
"orderBy": [{ "field": "revenue", "direction": "desc" }],
}
}
L’agent a sélectionné une métrique, un regroupement et un filtre. Il n’a pas décidé de la signification de revenue. Cette définition se trouve dans votre base de code, en TypeScript, revue et versionnée comme tout le reste, et votre API ainsi que votre tableau de bord utilisent la même. Les opérateurs de filtre et les intervalles temporels proviennent de ensembles prédéfinis, et les champs sont validés avant toute exécution. Le registre constitue la limite :
export const datasets = {
orders: {
...Orders,
metrics: { revenue },
},
};
Un ensemble de données absent de cet objet ne peut pas être listé, exploré ou interrogé, de sorte que l’ensemble des questions qu’un agent peut poser devient un objet révisable dans votre repository. La portée tenant cesse également d’être quelque chose fourni par le modèle :
await createMCPServer({
datasets,
analytics,
tenantId: 'tenant_123',
});
Cette valeur provient du processus hôte, qui sait déjà qui est authentifié. Si vous tentez de enregistrer un ensemble de données utilisant une clé d’abonné sans la fournir, le serveur refuse de démarrer :
MCP server tenantId is required for tenant-scoped datasets
Par défaut, les requêtes SQL générées ainsi que les expressions de champs basées sur SQL sont également supprimées des réponses, de sorte que le modèle ne voit que les nombres et les noms de champs, et non la structure de votre requête.
Quelques mots sur le coût
Le coût est une question secondaire. Le serveur officiel ne transmet au client que des données readonly, sans paramètres tels que max_rows_to_read, max_bytes_to_read ou max_memory_usage, et sans délai d’exécution imposé par le serveur. Dans les services facturés en fonction de l’utilisation, un agent qui réessaie indéfiniment des scans de tables complètes constitue un problème de facturation, et un ensemble limité de requêtes permet de limiter les dommages. Cependant, LIMIT restreint uniquement le nombre de lignes retournées, pas le nombre d’octets scannés, et une requête groupée sur une longue période lit toujours toutes les données nécessaires. Les restrictions d’accès structurées déterminent quelles requêtes sont possibles, pas leur coût respectif. Quelle que soit votre choix, fixez des limites réelles pour le nombre de lignes, d’octets, de mémoire et de temps d’exécution dans un profil de configuration destiné à l’utilisateur MCP, et ajoutez une quote-part.
Ce que cela ne résout pas
Rien de tout cela n’est nouveau. La « triplé mortel » de Simon Willison ainsi que la démonstration Supabase MCP de 2025 ont toutes deux souligné que le mode lecture seule ne constitue pas une barrière de sécurité, et Cube avec dbt ont depuis longtemps promu une couche sémantique destinée aux agents.
Le compromis honnête réside dans la flexibilité. Une surface structurée ne répond qu’à ce qui a été modélisé, de sorte qu’une question hors du catalogue ne reçoit aucune réponse, alors qu’un agent capable d’utiliser SQL pourrait improviser quelque chose d’utile. De plus, cela ne bloque pas tout le reste non plus. Les étiquettes et descriptions des ensembles de données parviennent au modèle et influencent son comportement ; il s’agit d’une version plus restreinte du mécanisme de manipulation des outils, et non d’une version éliminée, bien que ces chaînes de caractères se trouvent dans du TypeScript vérifié et non sur un serveur tiers. Enfin, une métrique contrôlée ne pourra pas empêcher un modèle de mal interpréter un nombre correct dans son résumé.
Commencer petit
Il n’est pas nécessaire de modéliser l’entier entrepôt. Choisissez une seule métrique sur laquelle dépend votre produit, celle pour laquelle une réponse erronée serait préjudiciable, définitez-la une fois, enregistrez-la et transmettez-la uniquement à l’agent. Comparez ensuite ce que l’agent renvoie avec ce que montre votre tableau de bord. Si les chiffres correspondent, la définition se trouve désormais dans un endroit consulté par les deux parties, et c’est là toute l’idée.