MCP server
Saiku Cloud ships a built-in Model Context Protocol (MCP) server that exposes your cubes to LLM agents as typed tools. Agents discover what cubes are available, ask for their structure, and run queries — all through a small, validated tool surface that prevents them from inventing measure names or hallucinating column references.
The MCP endpoint sits at
https://api.saiku.bi/rest/saiku/api/mcp. It speaks
streamable-HTTP MCP (the current protocol version) and
authenticates via the same Bearer API key as every other endpoint
on this surface.
Why MCP instead of plain SQL?
LLMs are reliably bad at writing analytical SQL. On Spider 2.0, the standard real-world text-to-SQL benchmark, top frontier models score around 24%. The failures aren’t subtle — invented column names, wrong joins, hallucinated tables, schemas confused across databases.
A Saiku cube prevents that class of failure structurally:
- The agent picks measures and dimensions by name from a self-describing schema.
- Validation runs server-side. If the agent invents a name, we return a structured 400 listing the valid alternatives.
- Aggregation, joins, and totalling are part of the cube definition — the agent doesn’t compose joins or pick aggregations, it just picks which measures and dimensions it wants.
The result is an analytical surface that LLMs can use reliably in production, not just demos.
The handshake
MCP is JSON-RPC 2.0 over HTTP. The full flow:
- Client sends
initialize(no session header required). - Server responds with
serverInfoand a session ID in theMcp-Session-Idresponse header. - Client includes
Mcp-Session-Idon every subsequent request. - Client calls
tools/listto discover tools, thentools/callto invoke them.
A minimal initialize:
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" } } }'The response includes the session ID header — extract it from
Mcp-Session-Id and send it on every subsequent call.
If you’re using a high-level MCP client (the official SDKs handle the handshake for you), this is all invisible. Configure the client with the URL + your Bearer key and it works.
The six tools
The server exposes six tools, designed to be the minimum useful set for analytical work.
Cube IDs
Every tool that names a cube takes a cube ID in the format
connectionName/catalog/schema/cubeName. You get the parts from
the list_cubes response. Example:
cloud__62e7bf54__v1__foodmart-globex-demo/FoodMart/FoodMart/SalesUse that whole slash-separated string as the cube argument
everywhere the tools below want one.
list_cubes
Lists every OLAP cube the current user can query. Always your first call when you don’t know what’s available.
{ "name": "list_cubes", "arguments": {}}Response:
{ "cubes": [ { "connectionName": "cloud__62e7bf54__v1__foodmart-globex-demo", "catalog": "FoodMart", "schema": "FoodMart", "cubeName": "Sales", "cubeCaption": "Sales", "defaultMeasure": "Unit Sales", "measureCount": 8 } ]}Returns up to a few dozen entries. Not paginated — Saiku cubes are counted in tens per tenant, not thousands.
describe_cube
Get the complete queryable structure of one cube. Always call
this before run_query if you haven’t seen the cube structure
yet — it tells you exactly which names are valid and includes
ready-made example query bodies.
{ "name": "describe_cube", "arguments": { "cube": "cloud__62e7bf54__v1__foodmart-globex-demo/FoodMart/FoodMart/Sales" }}Returns measures (keyed by lowercase name), dimensions,
hierarchies, levels, sample members with MDX unique names
([Customer].[Customers].[USA].[CA].[San Diego] style), and a
ready-to-use requestSchema + examples block for the
run_query tool.
search_members
Find the MDX unique names of members on a level by substring
match. Use when the cube has more members at a level than
describe_cube’s sample covered (e.g. searching for a specific
city, customer, or product brand), or when the user says
“filter by Italy” and you need to confirm the spelling.
{ "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 is required when the dimension has more than one
(common — Time dimensions typically have several). For
single-hierarchy dimensions you can omit it.
Returns up to limit hits with caption, name, and
uniqueName.
run_query
The primary tool. Most user questions land here. Build the
request against the structure from describe_cube; the server
validates every name and returns a structured VALIDATION_ERROR
with valid alternatives if any name is wrong, so don’t
pre-validate yourself.
{ "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 }}Shape rules pulled from the live requestSchema:
measures— array of objects. Each item has anamematching the bare caption fromdescribe_cube’smeasuresmap (e.g."Unit Sales", not"[Measures].[Unit Sales]").rows/columns/filters— array of axis selections. Each item hasdimensionandlevel(required),hierarchy(required when the dimension has more than one), and an optionalmembersarray of MDX unique names to restrict to.cube— the full cube ID string (see above) or an object{connectionName, catalog, schema, cubeName}.format—"records"(default) or"matrix". Agents almost always want records.limit— row cap. Default 100; max 10 000 on the Mondrian path.
Response:
{ "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" } } } ]}The cell envelope ({value, formatted, properties}) carries
both the parsed numeric value and Mondrian’s formatted display
string. Use value for arithmetic, formatted for display.
The queryId in the response can be passed to drillthrough to
inspect the underlying rows behind any cell.
preview_query
Compile a query to MDX without executing it. Use when you want to show the user what the query will do, audit a generated query, or estimate cost before running an expensive aggregation.
{ "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" }] }}Response:
{ "status": "PREVIEW", "queryId": "f4e2a890-…", "generatedMdx": "SELECT NON EMPTY {[Measures].[Unit Sales]} ON COLUMNS,\nNON EMPTY [Time].[Time].[Year].Members ON ROWS\nFROM [Sales]"}Validation runs the same as run_query — preview returns the
same VALIDATION_ERROR shape if names don’t resolve.
drillthrough
Fetch the raw fact-table rows behind a specific query. Use when the user asks “show me the underlying transactions” or wants to inspect detail for a single cell.
{ "name": "drillthrough", "arguments": { "queryId": "eb623b5f-f32d-4b84-96fa-2bde148556a0", "maxrows": 100 }}Pass the queryId returned by an earlier run_query call (it’s
the queryId field in the response, not the id of the
JSON-RPC envelope). Cells in the response use the same typed
envelope as run_query.
Validation errors
Every tool that accepts cube / measure / dimension names runs server-side validation. When something doesn’t resolve, you get a structured error rather than a generic 500:
{ "isError": true, "structuredContent": { "code": "VALIDATION_ERROR", "message": "Unknown measure: [Measures].[Reveue]", "field": "measures[0]", "alternatives": [ "[Measures].[Revenue]", "[Measures].[Repeat Revenue]" ] }}The alternatives field is the killer feature for LLM agents —
when the model misremembers a name, the server tells it the
nearest valid options, and the agent can self-correct in one
retry instead of guessing.
Wiring an agent
For most agent frameworks (Claude Desktop, OpenAI Assistants, LangChain, anything that speaks MCP), the configuration is two fields: the MCP server URL and the Bearer key.
Claude Desktop / Anthropic SDK:
{ "mcpServers": { "saiku": { "url": "https://api.saiku.bi/rest/saiku/api/mcp", "headers": { "Authorization": "Bearer sk_live_..." } } }}Programmatic via the official MCP SDKs: instantiate a
StreamableHttpClientTransport against the URL with the Bearer
header, then call the standard MCP client API.
After this, your agent’s prompt typically needs one line —
“You have access to a saiku MCP server with tools for querying
analytics cubes. Use list_cubes to start.” Everything else is
the agent discovering and using the tools the way they’re
described in the schema.
Rate limits
MCP traffic counts toward your tenant’s standard rate-limit budget (see Authentication). Heavy agents on the Starter tier may hit the limit; consider Team or Business for production agent workloads.
What MCP doesn’t expose
The MCP surface is read-only. Tools that modify state — saving workbooks, creating schemas, adding connections, mutating cube definitions — aren’t exposed via MCP. Those live on the dashboard or the REST API, where a human is in the loop.
This is deliberate: an agent that can read your data but can’t modify your schemas can never accidentally break your analytics setup. If you want an agent that authors cubes, use the AI inference API and put a human review step in the middle.
Where to go next
- AI inference API — for authoring cubes rather than querying them.
- Authentication — Bearer tokens, rate limits, error shapes.
- Tenant isolation — how MCP keeps your data invisible to other tenants’ agents.