MCP Server (AI-Ready Data Layer)
Drizzle Cube includes a built-in MCP server that lets AI agents like Claude, ChatGPT, and n8n query your semantic layer directly. The server follows the Model Context Protocol specification and exposes tools for discovering cubes, validating queries, and executing them.
MCP Server Endpoint
Section titled “MCP Server Endpoint”All framework adapters expose an MCP server at /mcp:
https://your-app.com/mcpThis endpoint implements the full MCP specification including:
- Tools - Functions the AI can call
- Prompts - Pre-built prompts for common tasks
- Server-Sent Events (SSE) - For streaming responses
Available MCP Tools
Section titled “Available MCP Tools”The built-in /mcp endpoint exposes these tools:
| Tool | Purpose |
|---|---|
discover | Find relevant cubes based on topic or intent. Also returns the full query language reference and date-filtering guide (see How guidance reaches the model below). |
validate | Validate queries and get auto-corrections |
load | Execute queries and return data |
chart | Execute queries with interactive chart visualization (only registered when MCP App is enabled) |
How guidance reaches the model
Section titled “How guidance reaches the model”A common surprise: the model often ignores MCP prompts and resources. That isn’t a Drizzle Cube bug — it’s how most clients work. Claude Desktop, Claude Code, and many other MCP clients treat prompts/* and resources/* as user-triggered slash commands, not as actions the LLM can invoke mid-turn. So if you put critical query rules into a prompt and hope the model fetches it, you’ll be disappointed.
Drizzle Cube uses two channels that are guaranteed to reach the model:
-
InitializeResult.instructions— a short, authoritative string returned during the MCPinitializehandshake. Per the MCP spec, clients merge this into the LLM system prompt. The default content mandates the discover-first workflow and inlines the most-violated rule (useinDateRangefor aggregated totals, nottimeDimensions). You can customise it — see theinstructionsoption. -
The
discovertool response itself —discoveralways returns three fields:cubes,queryLanguageReference, anddateFilteringGuide. Because the workflow mandates callingdiscoverfirst, the model receives the full TypeScript DSL on its very first tool call without an extra round-trip:// tools/call name=discover { topic: "salary" }{"cubes": [ /* matched cubes with measures, dimensions, joins */ ],"queryLanguageReference": "// === DRIZZLE CUBE QUERY LANGUAGE (TypeScript DSL) ===\n…","dateFilteringGuide": "# Date Filtering vs Time Grouping\n…"}
The existing prompts/* and resources/* endpoints are still exposed for clients (and end-users) that do consume them, but query correctness no longer depends on them.
Connecting AI Tools
Section titled “Connecting AI Tools”Claude Desktop
Section titled “Claude Desktop”Add to your claude_desktop_config.json:
{ "mcpServers": { "your-app-analytics": { "command": "npx", "args": ["-y", "@anthropic/mcp-remote", "https://your-app.com/mcp"] } }}With authentication (recommended for production):
{ "mcpServers": { "your-app-analytics": { "command": "npx", "args": [ "-y", "@anthropic/mcp-remote", "https://your-app.com/mcp", "--header", "Authorization: Bearer YOUR_TOKEN" ] } }}Claude Code
Section titled “Claude Code”claude mcp add --transport http analytics https://your-app.com/mcp \ --header "Authorization: Bearer YOUR_TOKEN"Claude.ai (Web)
Section titled “Claude.ai (Web)”- Go to Settings → Connectors → Add Connector
- Enter your MCP server URL:
https://your-app.com/mcp - The tools will be available in your conversations
Claude.ai connectors support OAuth 2.1 — if your MCP endpoint has an OAuth discovery document, Claude.ai will handle the auth flow automatically. You can also pass an authorization_token via the Messages API MCP connector for programmatic access.
ChatGPT
Section titled “ChatGPT”- Go to Settings → Connectors → Advanced → Developer Mode
- Add your MCP server URL:
https://your-app.com/mcp - The tools will be available in ChatGPT
Use the MCP Client node:
- Add an MCP Client node to your workflow
- Set the server URL:
https://your-app.com/mcp - Connect it to an AI Agent node
See n8n MCP Client documentation for details.
The AI Workflow
Section titled “The AI Workflow”When an AI agent connects to your MCP server, it follows this workflow (mandated by the default instructions and reinforced by every tool description):
User: "Show me average salary by department" │ ▼┌────────────────────────────────────────────────────────┐│ 1. discover ← MANDATORY first call ││ Find cubes related to "salary" and "department" ││ → Returns: matched cubes (measures, dimensions, ││ joins) + queryLanguageReference (full DSL) ││ + dateFilteringGuide (decision tree) │└────────────────────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────┐│ 2. AI builds query using the discover response ││ Uses cube metadata + queryLanguageReference ││ → Query: { measures: ['Employees.avgSalary'], ││ dimensions: ['Departments.name'] } │└────────────────────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────┐│ 3. validate (optional) ││ Check query validity, get corrections if needed ││ → Returns: { isValid: true, correctedQuery } │└────────────────────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────┐│ 4a. load ││ Execute query, return data as text ││ → Returns: { data: [...], annotation: {...} } ││ ││ 4b. chart (when MCP App is enabled) ││ Execute query + render interactive chart ││ → Returns: data + MCP App visualization │└────────────────────────────────────────────────────────┘Tool Reference
Section titled “Tool Reference”discover
Section titled “discover”Find cubes relevant to a topic or intent. The first call any agent should make — and the only way the model receives the full query language reference.
// Parameters{ "topic": "salary", "intent": "I want to analyze compensation", "limit": 5, "minScore": 0.3}
// Response{ "cubes": [ { "cube": "Employees", "relevanceScore": 0.85, "matchedOn": ["measure:avgSalary", "measure:totalSalary"], "suggestedMeasures": ["Employees.avgSalary", "Employees.totalSalary"], "suggestedDimensions": ["Employees.department", "Employees.location"], "capabilities": { "query": true, "funnel": false, "flow": false, "retention": false } } ], "queryLanguageReference": "// === DRIZZLE CUBE QUERY LANGUAGE (TypeScript DSL) ===\n…", "dateFilteringGuide": "# Date Filtering vs Time Grouping\n…"}The queryLanguageReference and dateFilteringGuide fields are always included so the model has the full DSL and the date-filtering decision tree available before constructing a query. See How guidance reaches the model.
validate
Section titled “validate”Validate a query and get helpful corrections.
// Parameters{ "query": { "measures": ["Employees.cont"], "dimensions": ["Departments.nam"] }}
// Response{ "isValid": false, "errors": [ "Unknown measure 'Employees.cont' - did you mean 'Employees.count'?", "Unknown dimension 'Departments.nam' - did you mean 'Departments.name'?" ], "correctedQuery": { "measures": ["Employees.count"], "dimensions": ["Departments.name"] }}Execute a query and return results. The tool description gates this on discover having been called first.
// Parameters{ "query": { "measures": ["Employees.count", "Employees.avgSalary"], "dimensions": ["Departments.name"] }}
// Response{ "data": [ { "Departments.name": "Engineering", "Employees.count": 45, "Employees.avgSalary": 125000 }, { "Departments.name": "Sales", "Employees.count": 32, "Employees.avgSalary": 85000 } ], "annotation": { "measures": { "Employees.count": { "title": "Total Employees", "type": "count" }, "Employees.avgSalary": { "title": "Average Salary", "type": "avg" } }, "dimensions": { "Departments.name": { "title": "Department Name", "type": "string" } } }}Execute a query and render an interactive chart. Same query format as load, with an optional chart hint to control the visualization. Only registered when MCP App is enabled (mcp: { app: true }).
// Parameters{ "query": { "measures": ["Employees.count"], "dimensions": ["Departments.name"] }, "chart": { "type": "pie", "title": "Employees by Department", "chartConfig": { "xAxis": ["Departments.name"], "yAxis": ["Employees.count"] } }}If no chart hint is provided, the chart type is auto-selected based on the query shape. See MCP App for chart types and configuration details.
Configuration
Section titled “Configuration”The built-in MCP server is configured via the mcp option on every adapter (createCubeRouter for Express, cubePlugin for Fastify, createCubeRoutes for Hono, createCubeHandlers for Next.js). All fields are optional — pass mcp: { enabled: false } to disable the endpoint entirely.
Options reference
Section titled “Options reference”| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Disable to remove the /mcp endpoint entirely. |
basePath | string | '/mcp' | URL path the endpoint is mounted at. |
tools | Array<'discover' | 'validate' | 'load'> | all | Selectively expose tools (the chart tool is controlled by the app option below). |
allowedOrigins | string[] | unrestricted | Origin allowlist enforced per the MCP 2025-11-25 spec. Recommended in production. |
serverName | string | 'drizzle-cube' | Returned in serverInfo.name during the MCP initialize handshake. Override to match your product branding. |
instructions | string | (defaults: string) => string | built-in | Override or extend the InitializeResult.instructions returned during initialize. This is the only string the MCP spec expects clients to merge into the LLM system prompt — see Customising the model’s instructions. |
prompts | MCPPrompt[] | (defaults: MCPPrompt[]) => MCPPrompt[] | built-in | Override or extend the prompts exposed via prompts/list and prompts/get. Useful for clients (or end-users) that consume prompts as slash commands. |
resources | MCPResource[] | (defaults: MCPResource[]) => MCPResource[] | built-in | Override or extend the resources exposed via resources/list and resources/read. The live cube schema (drizzle-cube://schema) is appended automatically. |
app | boolean | McpAppConfig | false | Enable the MCP App interactive chart visualisation and register the chart tool. Pass an object to set locale options. |
resourceMetadataUrl | string | — | OAuth 2.1 Protected Resource Metadata URL (RFC 9728). When set, requests without a Bearer token receive 401 with a WWW-Authenticate challenge pointing here. Token validation remains the responsibility of extractSecurityContext. |
Disabling MCP
Section titled “Disabling MCP”createCubeRouter({ // ... other options mcp: { enabled: false }})Selective tool exposure
Section titled “Selective tool exposure”createCubeRouter({ // ... other options mcp: { enabled: true, tools: ['discover', 'validate', 'load'] // hide chart, etc. }})Origin restrictions
Section titled “Origin restrictions”Restrict which origins can connect to your MCP server (per MCP 2025-11-25 — required when serving browsers in production):
createCubeRouter({ mcp: { enabled: true, allowedOrigins: ['https://claude.ai', 'https://chat.openai.com'] }})Branding the server name
Section titled “Branding the server name”createCubeRouter({ mcp: { enabled: true, serverName: 'Acme Analytics' // appears as serverInfo.name in initialize }})Customising the model’s instructions
Section titled “Customising the model’s instructions”Per the MCP spec, the server returns an instructions string during initialize that clients merge into the LLM system prompt. Drizzle Cube ships with sensible defaults that mandate the discover-first workflow and inline the most-violated date-filtering rule, but you’ll often want to add project-specific guidance (cube semantics, naming conventions, business rules).
Append to the defaults (recommended — keeps the built-in workflow rules in place):
createCubeRouter({ mcp: { instructions: (defaults) => `${defaults}
## Acme-specific rules- Always join Sales with Customers via customerId for revenue analysis.- The "Region" cube has been deprecated — use "Territories" instead.- Quarterly reports use fiscal-year quarters, not calendar quarters.` }})Replace the defaults entirely (advanced — you take full responsibility for telling the model how to use the server):
createCubeRouter({ mcp: { instructions: 'You are an analytics agent for Acme. Always call discover first…' }})Customising prompts and resources
Section titled “Customising prompts and resources”The prompts/* and resources/* endpoints are useful for clients that surface prompts as user-triggered slash commands (and for introspection). Override or extend them with the same resolver pattern:
import type { MCPPrompt, MCPResource } from 'drizzle-cube/adapters/mcp-transport'
createCubeRouter({ mcp: { prompts: (defaults) => [ ...defaults, { name: 'acme-revenue-report', description: 'Build the standard monthly revenue report', messages: [{ role: 'user', content: { type: 'text', text: 'Generate a revenue report grouped by region…' } }] } ], resources: (defaults) => [ ...defaults, { uri: 'acme://kpis', name: 'Acme KPI definitions', description: 'Plain-language definitions of every Acme KPI', mimeType: 'text/markdown', text: '# Acme KPIs\n…' } ] }})Enabling interactive charts (MCP App)
Section titled “Enabling interactive charts (MCP App)”Enable the chart tool and serve the interactive visualisation UI to compatible clients:
createCubeRouter({ mcp: { enabled: true, app: true // or { defaultLocale: 'nl-NL', detectBrowserLocale: false } }})See MCP App for the full guide.
OAuth Protected Resource Metadata
Section titled “OAuth Protected Resource Metadata”Point clients at your authorisation server via RFC 9728:
createCubeRouter({ mcp: { enabled: true, resourceMetadataUrl: 'https://your-app.com/.well-known/oauth-protected-resource' }})When resourceMetadataUrl is set, requests without a Authorization: Bearer … token receive 401 with a WWW-Authenticate header pointing at the metadata document. Token validation itself is up to your extractSecurityContext. See drizby for a complete OAuth 2.1 reference implementation.
Security & Authentication
Section titled “Security & Authentication”Important: The MCP server does not include built-in authentication. You are responsible for adding authentication middleware, just like with the standard Cube API routes.
How It Works
Section titled “How It Works”The MCP endpoint (/mcp) is just an HTTP POST route — standard auth middleware protects it exactly like any other route. The complete flow is:
- Middleware authenticates the request (validates token, session, etc.)
extractSecurityContextextracts the user’s identity and permissions- Cube security filters scope all data access to the authenticated user’s tenant
Protecting MCP Endpoints
Section titled “Protecting MCP Endpoints”Apply authentication middleware before mounting the cube router. Here are examples for each framework:
Express
Section titled “Express”import express from 'express'import { createCubeRouter } from 'drizzle-cube/adapters/express'
const app = express()
// Auth middleware protects ALL routes including /mcpapp.use(async (req, res, next) => { const token = req.headers.authorization?.replace('Bearer ', '') if (!token) return res.status(401).json({ error: 'Unauthorized' }) req.user = await validateToken(token) next()})
// Both /cubejs-api/v1/* AND /mcp are now protectedapp.use('/', createCubeRouter({ cubes: [employeesCube], drizzle: db, schema, extractSecurityContext: async (req) => ({ organisationId: req.user.orgId, userId: req.user.id })}))import { Hono } from 'hono'import { createCubeRoutes } from 'drizzle-cube/adapters/hono'
const app = new Hono()
// Auth middleware protects ALL routes including /mcpapp.use('*', async (c, next) => { const token = c.req.header('Authorization')?.replace('Bearer ', '') if (!token) return c.json({ error: 'Unauthorized' }, 401) const user = await validateToken(token) c.set('user', user) await next()})
// Cube routes (including MCP) are now protectedapp.route('/', createCubeRoutes({ cubes: [employeesCube], drizzle: db, schema, extractSecurityContext: async (c) => ({ organisationId: c.get('user').orgId, userId: c.get('user').id })}))Fastify
Section titled “Fastify”import Fastify from 'fastify'import { registerCubeRoutes } from 'drizzle-cube/adapters/fastify'
const fastify = Fastify()
// Auth hook protects ALL routes including /mcpfastify.addHook('onRequest', async (request, reply) => { const token = request.headers.authorization?.replace('Bearer ', '') if (!token) return reply.code(401).send({ error: 'Unauthorized' }) request.user = await validateToken(token)})
await registerCubeRoutes(fastify, { cubes: [employeesCube], drizzle: db, schema, extractSecurityContext: async (req) => ({ organisationId: req.user.orgId, userId: req.user.id })})Next.js
Section titled “Next.js”// In Next.js, validate auth inside extractSecurityContext// since there's no separate middleware layer for API routes
export const cubeHandlers = createCubeHandlers({ cubes: [employeesCube], drizzle: db, schema, extractSecurityContext: async (req) => { const token = req.headers.authorization?.replace('Bearer ', '') if (!token) throw new Error('Unauthorized') const user = await validateToken(token) return { organisationId: user.orgId, userId: user.id } }})OAuth 2.1 Authentication (Reference Implementation)
Section titled “OAuth 2.1 Authentication (Reference Implementation)”For production MCP servers that need to work with Claude.ai connectors, ChatGPT, or other clients requiring OAuth, see drizby — an open-source analytics app built on drizzle-cube that implements full OAuth 2.1 for MCP authentication.
The implementation includes:
- OAuth 2.1 with PKCE (S256 required) via
@jmondi/oauth2-server - Dynamic Client Registration (RFC 7591)
- Authorization Server Metadata (RFC 8414)
- Protected Resource Metadata (RFC 9728)
- Token revocation and refresh tokens
Key files:
src/routes/oauth.ts— OAuth endpoints (authorize, token, register, revoke)src/auth/oauth-repositories.ts— Drizzle-backed token/client storagesrc/auth/middleware.ts— Unified auth middleware (OAuth Bearer + session cookies)app.ts— MCP + OAuth integration withextractSecurityContext
Security Context Enforcement
Section titled “Security Context Enforcement”All MCP tools respect your security context:
loadandchartexecute queries with the authenticated user’s security contextdiscoverreturns cube metadata (schema information) — access is gated by your auth middleware- Multi-tenant isolation is enforced on all data access via cube
sqlfilters
Enhancing AI Discovery
Section titled “Enhancing AI Discovery”The MCP tools work best when your cubes have rich semantic metadata. See Adding Semantic Metadata for how to add:
- Descriptions for cubes, measures, and dimensions
- Synonyms for alternate names (“revenue” → “sales”, “income”)
- Example questions that help AI understand the cube’s purpose
Try It Live
Section titled “Try It Live”Connect to the demo MCP server to try it out:
https://try.drizzle-cube.dev/mcpAdding Tools to an Existing MCP Server
Section titled “Adding Tools to an Existing MCP Server”If you already have an MCP server (e.g., a PostgREST MCP for CRUD operations) and want to add drizzle-cube’s analytics tools alongside your existing tools, see Composable MCP Tools. This lets you register discover, validate, load, and chart on any MCP server without running the built-in /mcp endpoint.
Next Steps
Section titled “Next Steps”- Composable MCP Tools - Add cube tools to your own MCP server
- Adding Semantic Metadata - Make your cubes more discoverable
- Claude Desktop Setup - Connect Claude Desktop to your data
- Claude Code Plugin - Query from Claude Code