Serveur MCP
Saiku Cloud embarque un serveur Model Context Protocol (MCP) intégré qui expose vos cubes aux agents LLM sous forme d’outils typés. Les agents découvrent quels cubes sont disponibles, interrogent leur structure, et exécutent des requêtes — le tout via une petite surface d’outils validée qui les empêche d’inventer des noms de mesures ou d’halluciner des références de colonnes.
L’endpoint MCP se trouve à
https://api.saiku.bi/rest/saiku/api/mcp. Il parle MCP en
streamable-HTTP (la version actuelle du protocole) et prend en
charge deux moyens d’authentification :
- Connecteur OAuth 2.1 — recommandé pour les clients de chat (Claude Desktop, claude.ai, Cursor — tout ce qui dispose d’une option « connecteur personnalisé » / « MCP distant »). Collez l’URL et le client connecte l’utilisateur ; il n’y a aucune clé à copier ou créer. En libre-service, disponible sur Team et au-delà.
- Clé API Bearer — pour les clients programmatiques /
SDK. La même clé
sk_...que tous les autres endpoints de cette surface.
Voir Connexion d’un agent pour les deux.
Pourquoi MCP plutôt que du SQL pur ?
Les LLMs sont notoirement mauvais pour écrire du SQL analytique. Sur Spider 2.0, le benchmark standard de text-to-SQL en conditions réelles, les meilleurs modèles frontière obtiennent environ 24 %. Les échecs ne sont pas subtils — noms de colonnes inventés, mauvaises jointures, tables hallucinées, schémas confondus entre bases de données.
Un cube Saiku empêche structurellement cette classe d’échecs :
- L’agent choisit les mesures et dimensions par nom à partir d’un schema auto-descriptif.
- La validation s’exécute côté serveur. Si l’agent invente un nom, nous renvoyons un 400 structuré listant les alternatives valides.
- L’agrégation, les jointures et les totaux font partie de la définition du cube — l’agent ne compose pas de jointures et ne choisit pas d’agrégations, il choisit simplement quelles mesures et dimensions il veut.
Le résultat est une surface analytique que les LLMs peuvent utiliser de manière fiable en production, et pas seulement en démo.
La poignée de main
MCP est du JSON-RPC 2.0 sur HTTP. Le flux complet :
- Le client envoie
initialize(aucun en-tête de session requis). - Le serveur répond avec
serverInfoet un ID de session dans l’en-tête de réponseMcp-Session-Id. - Le client inclut
Mcp-Session-Iddans chaque requête ultérieure. - Le client appelle
tools/listpour découvrir les outils, puistools/callpour les invoquer.
Un initialize minimal :
curl -X POST https://api.saiku.bi/rest/saiku/api/mcp \ -H "Authorization: Bearer $SAIKU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "my-agent", "version": "1.0" } } }'La réponse inclut l’en-tête d’ID de session — extrayez-le de
Mcp-Session-Id et envoyez-le sur chaque appel suivant.
Si vous utilisez un client MCP de haut niveau (les SDKs officiels gèrent la poignée de main pour vous), tout cela est invisible. Configurez le client avec l’URL + votre clé Bearer et ça fonctionne.
Les six outils
Le serveur expose six outils, conçus pour être l’ensemble minimal utile au travail analytique.
ID de cube
Chaque outil qui nomme un cube prend un ID de cube au
format connectionName/catalog/schema/cubeName. Vous obtenez
les parties depuis la réponse de list_cubes. Exemple :
cloud__62e7bf54__v1__foodmart-globex-demo/FoodMart/FoodMart/SalesUtilisez cette chaîne complète séparée par des barres obliques
comme argument cube partout où les outils ci-dessous en
attendent un.
list_cubes
Liste tous les cubes OLAP que l’utilisateur courant peut interroger. Toujours votre premier appel quand vous ne savez pas ce qui est disponible.
{ "name": "list_cubes", "arguments": {}}Réponse :
{ "cubes": [ { "connectionName": "cloud__62e7bf54__v1__foodmart-globex-demo", "catalog": "FoodMart", "schema": "FoodMart", "cubeName": "Sales", "cubeCaption": "Sales", "defaultMeasure": "Unit Sales", "measureCount": 8 } ]}Renvoie jusqu’à quelques dizaines d’entrées. Non paginé — les cubes Saiku se comptent par dizaines par tenant, pas par milliers.
describe_cube
Obtient la structure interrogeable complète d’un cube. Appelez
toujours cela avant run_query si vous n’avez pas encore vu
la structure du cube — il vous indique exactement quels noms
sont valides et inclut des corps de requête d’exemple prêts à
l’emploi.
{ "name": "describe_cube", "arguments": { "cube": "cloud__62e7bf54__v1__foodmart-globex-demo/FoodMart/FoodMart/Sales" }}Renvoie les mesures (indexées par nom en minuscules), les
dimensions, les hiérarchies, les niveaux, des membres d’exemple
avec des noms uniques MDX (style
[Customer].[Customers].[USA].[CA].[San Diego]), ainsi qu’un
bloc requestSchema + examples prêt à l’emploi pour l’outil
run_query.
search_members
Trouve les noms uniques MDX des membres d’un niveau par
correspondance de sous-chaîne. À utiliser lorsque le cube a
plus de membres à un niveau que l’échantillon de describe_cube
n’en couvre (par exemple, rechercher une ville, un client ou
une marque de produit spécifique), ou lorsque l’utilisateur
dit « filtrer par Italie » et que vous devez confirmer
l’orthographe.
{ "name": "search_members", "arguments": { "cube": "cloud__62e7bf54__v1__foodmart-globex-demo/FoodMart/FoodMart/Sales", "dimension": "Customers", "hierarchy": "Customers", "level": "City", "q": "San", "limit": 25 }}hierarchy est requis lorsque la dimension en a plus d’une
(fréquent — les dimensions Time en ont typiquement plusieurs).
Pour les dimensions à hiérarchie unique, vous pouvez l’omettre.
Renvoie jusqu’à limit résultats avec caption, name et
uniqueName.
run_query
L’outil principal. La plupart des questions utilisateur
aboutissent ici. Construisez la requête à partir de la
structure issue de describe_cube ; le serveur valide chaque
nom et renvoie un VALIDATION_ERROR structuré avec les
alternatives valides si un nom est erroné, donc ne pré-validez
pas vous-même.
{ "name": "run_query", "arguments": { "cube": "cloud__62e7bf54__v1__foodmart-globex-demo/FoodMart/FoodMart/Sales", "measures": [ { "name": "Unit Sales" }, { "name": "Store Sales" } ], "rows": [ { "dimension": "Time", "hierarchy": "Time", "level": "Year" } ], "columns": [ { "dimension": "Customers", "hierarchy": "Customers", "level": "Country", "members": ["[Customer].[Customers].[USA]"] } ], "filters": [ { "dimension": "Promotions", "hierarchy": "Promotions", "level": "Promotion Name", "members": ["[Promotion].[Promotions].[No Promotion]"] } ], "limit": 100 }}Règles de forme tirées du requestSchema en direct :
measures— tableau d’objets. Chaque entrée a unnamecorrespondant à la légende brute de la mapmeasuresdedescribe_cube(par ex."Unit Sales", pas"[Measures].[Unit Sales]").rows/columns/filters— tableau de sélections d’axe. Chaque entrée adimensionetlevel(requis),hierarchy(requis lorsque la dimension en a plus d’une), et un tableaumembersfacultatif de noms uniques MDX auxquels restreindre.cube— la chaîne complète d’ID de cube (voir ci-dessus) ou un objet{connectionName, catalog, schema, cubeName}.format—"records"(par défaut) ou"matrix". Les agents veulent presque toujours records.limit— plafond de lignes. Par défaut 100 ; max 10 000 sur le chemin Mondrian.
Réponse :
{ "status": "SUCCESS", "queryId": "eb623b5f-f32d-4b84-96fa-2bde148556a0", "runtimeMs": 196, "totalRows": 1, "data": [ { "Year": "1997", "Unit Sales": { "value": 266773.0, "formatted": "266,773", "properties": { "formatString": "Standard", "datatype": "Numeric" } }, "Store Sales": { "value": 565238.13, "formatted": "565,238.13", "properties": { "formatString": "#,###.00", "datatype": "Numeric" } } } ]}L’enveloppe de cellule ({value, formatted, properties})
porte à la fois la valeur numérique analysée et la chaîne
d’affichage formatée par Mondrian. Utilisez value pour les
calculs, formatted pour l’affichage.
Le queryId dans la réponse peut être passé à drillthrough
pour inspecter les lignes sous-jacentes derrière une cellule.
preview_query
Compile une requête en MDX sans l’exécuter. À utiliser quand vous voulez montrer à l’utilisateur ce que la requête fera, auditer une requête générée, ou estimer le coût avant d’exécuter une agrégation coûteuse.
{ "name": "preview_query", "arguments": { "cube": "cloud__62e7bf54__v1__foodmart-globex-demo/FoodMart/FoodMart/Sales", "measures": [{ "name": "Unit Sales" }], "rows": [{ "dimension": "Time", "hierarchy": "Time", "level": "Year" }] }}Réponse :
{ "status": "PREVIEW", "queryId": "f4e2a890-…", "generatedMdx": "SELECT NON EMPTY {[Measures].[Unit Sales]} ON COLUMNS,\nNON EMPTY [Time].[Time].[Year].Members ON ROWS\nFROM [Sales]"}La validation s’exécute comme pour run_query — preview
renvoie la même forme VALIDATION_ERROR si les noms ne se
résolvent pas.
drillthrough
Récupère les lignes brutes de la table de faits derrière une requête spécifique. À utiliser quand l’utilisateur demande « montrez-moi les transactions sous-jacentes » ou veut inspecter le détail d’une seule cellule.
{ "name": "drillthrough", "arguments": { "queryId": "eb623b5f-f32d-4b84-96fa-2bde148556a0", "maxrows": 100 }}Passez le queryId renvoyé par un appel run_query antérieur
(c’est le champ queryId dans la réponse, pas l’id de
l’enveloppe JSON-RPC). Les cellules de la réponse utilisent la
même enveloppe typée que run_query.
Erreurs de validation
Chaque outil qui accepte des noms de cube / mesure / dimension exécute une validation côté serveur. Lorsque quelque chose ne se résout pas, vous obtenez une erreur structurée plutôt qu’un 500 générique :
{ "isError": true, "structuredContent": { "code": "VALIDATION_ERROR", "message": "Unknown measure: [Measures].[Reveue]", "field": "measures[0]", "alternatives": [ "[Measures].[Revenue]", "[Measures].[Repeat Revenue]" ] }}Le champ alternatives est la fonctionnalité-clé pour les
agents LLM — quand le modèle se rappelle mal d’un nom, le
serveur lui indique les options valides les plus proches, et
l’agent peut s’auto-corriger en un essai au lieu de deviner.
Connexion d’un agent
Clients de chat — connecteur OAuth (recommandé)
Les clients qui prennent en charge les connecteurs MCP personnalisés / distants (Claude Desktop, claude.ai, Cursor, et d’autres) se connectent sans clé API — ils authentifient l’utilisateur via OAuth à la place. Ajoutez l’URL MCP comme connecteur personnalisé et le client fait le reste :
- Dans les paramètres de connecteur du client, ajoutez un
serveur MCP personnalisé/distant avec l’URL
https://api.saiku.bi/rest/saiku/api/mcp. Laissez les champs optionnels « client ID / secret » vides. - Le client découvre automatiquement les endpoints OAuth de Saiku Cloud et s’enregistre — aucun client ID ou secret à créer (Dynamic Client Registration).
- Un navigateur s’ouvre pour la connexion à Saiku Cloud (votre connexion habituelle à l’espace de travail), puis affiche un écran de consentement nommant le client et l’accès qu’il demande (interrogation de cubes en lecture seule). Approuvez-le.
- Le connecteur est actif — l’agent peut appeler
list_cubeset les autres outils immédiatement.
L’autorisation d’un connecteur nécessite un espace de travail Team, Business ou Enterprise. L’identifiant que reçoit le client est limité à MCP en lecture seule pour votre espace de travail et rien d’autre ; il ne voit jamais vos identifiants d’entrepôt, d’autres tenants ou une quelconque surface d’écriture.
Gestion des connecteurs. Chaque client autorisé apparaît sous Connexions → Agents connectés dans le dashboard, où vous pouvez en révoquer n’importe lequel en un clic. Les jetons d’accès sont de courte durée (1 heure) et se rafraîchissent silencieusement ; une révocation invalide immédiatement le jeton de rafraîchissement, donc l’accès s’arrête au plus tard dans l’heure — généralement tout de suite.
Programmatique — clé API Bearer
Pour votre propre code, vos frameworks d’agent, ou tout client qui injecte un en-tête statique, créez une clé API dans le dashboard (Clés API, limitez-la à MCP) et passez-la comme jeton Bearer :
{ "mcpServers": { "saiku": { "url": "https://api.saiku.bi/rest/saiku/api/mcp", "headers": { "Authorization": "Bearer sk_..." } } }}Via les SDKs MCP officiels : instanciez un
StreamableHttpClientTransport sur l’URL avec l’en-tête
Bearer, puis appelez l’API client MCP standard.
Après connexion (de l’une ou l’autre manière), le prompt de
votre agent n’a généralement besoin que d’une ligne — « Vous
avez accès à un serveur MCP saiku avec des outils pour
interroger des cubes analytiques. Utilisez list_cubes pour
commencer. » Tout le reste est l’agent qui découvre et
utilise les outils tels qu’ils sont décrits dans le schema.
Limites de débit
Le trafic MCP est imputé au budget standard de limites de débit de votre tenant (voir Authentification). Les agents lourds sur le niveau Starter peuvent atteindre la limite ; envisagez Team ou Business pour les charges de travail d’agents en production.
Ce que MCP n’expose pas
La surface MCP est en lecture seule. Les outils qui modifient l’état — enregistrer des classeurs, créer des schemas, ajouter des connexions, modifier les définitions de cube — ne sont pas exposés via MCP. Ils vivent sur le dashboard ou sur l’API REST, où un humain est dans la boucle.
C’est délibéré : un agent qui peut lire vos données mais ne peut pas modifier vos schemas ne peut jamais casser accidentellement votre configuration analytique. Si vous voulez un agent qui crée des cubes, utilisez l’API d’inférence IA et placez une étape de revue humaine au milieu.
Et ensuite
- API d’inférence IA — pour créer des cubes plutôt que les interroger.
- Authentification — jetons Bearer, limites de débit, formes d’erreur.
- Isolation des tenants — comment MCP garde vos données invisibles aux agents d’autres tenants.