Skip to content

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:

  1. Client sends initialize (no session header required).
  2. Server responds with serverInfo and a session ID in the Mcp-Session-Id response header.
  3. Client includes Mcp-Session-Id on every subsequent request.
  4. Client calls tools/list to discover tools, then tools/call to invoke them.

A minimal initialize:

Terminal window
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/Sales

Use 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 a name matching the bare caption from describe_cube’s measures map (e.g. "Unit Sales", not "[Measures].[Unit Sales]").
  • rows / columns / filters — array of axis selections. Each item has dimension and level (required), hierarchy (required when the dimension has more than one), and an optional members array 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