Skip to content

Composable MCP Tools

If you already have an MCP server (e.g., a PostgREST MCP providing CRUD tools), you can add drizzle-cube’s analytics tools alongside your existing tools — no need to run a separate MCP server.

The drizzle-cube/mcp export provides composable tool definitions and handlers that work with any MCP server, including @modelcontextprotocol/sdk.

Terminal window
npm install drizzle-cube

No additional dependencies required — drizzle-cube/mcp has zero dependency on @modelcontextprotocol/sdk. It just produces objects that match the MCP spec.

import { getCubeTools } from 'drizzle-cube/mcp'
import { createDrizzleSemanticLayer } from 'drizzle-cube/server'
// 1. Create your semantic layer
const semanticLayer = createDrizzleSemanticLayer({ drizzle: db, schema })
semanticLayer.registerCube(ordersCube)
semanticLayer.registerCube(customersCube)
// 2. Get composable tools
const cubeTools = getCubeTools({
semanticLayer,
getSecurityContext: async (meta) => ({
orgId: meta?.authInfo?.orgId ?? 'default'
})
})
// 3. Use them however you like
cubeTools.definitions // tool schemas for tools/list
cubeTools.handle(name, args) // tool executor for tools/call
cubeTools.handles(name) // check if a tool name is ours
cubeTools.prompts // MCP prompts for prompts/list
cubeTools.resources // MCP resources for resources/list
cubeTools.toolNames // list of registered tool names
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import {
ListToolsRequestSchema,
CallToolRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema
} from '@modelcontextprotocol/sdk/types.js'
import { getCubeTools } from 'drizzle-cube/mcp'
import { createDrizzleSemanticLayer } from 'drizzle-cube/server'
const semanticLayer = createDrizzleSemanticLayer({ drizzle: db, schema })
semanticLayer.registerCube(ordersCube)
const cubeTools = getCubeTools({
semanticLayer,
getSecurityContext: async (meta) => ({
orgId: meta?.authInfo?.orgId
})
})
const server = new Server(
{ name: 'my-analytics-server', version: '1.0.0' },
{ capabilities: { tools: {}, prompts: {}, resources: {} } }
)
// Merge cube tools with your own tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [...myExistingTools, ...cubeTools.definitions]
}))
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (cubeTools.handles(req.params.name)) {
return cubeTools.handle(req.params.name, req.params.arguments, req)
}
return handleMyTools(req)
})
// Optionally expose prompts and resources
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: cubeTools.prompts.map(p => ({ name: p.name, description: p.description }))
}))
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
const prompt = cubeTools.prompts.find(p => p.name === req.params.name)
if (!prompt) throw new Error('Prompt not found')
return { name: prompt.name, description: prompt.description, messages: prompt.messages }
})

Combine CRUD and analytics tools on a single MCP server:

import { Hono } from 'hono'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StreamableHTTPTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { getCubeTools } from 'drizzle-cube/mcp'
import { createDrizzleSemanticLayer } from 'drizzle-cube/server'
import { postgrestTools } from './postgrest-tools'
// Set up semantic layer
const semanticLayer = createDrizzleSemanticLayer({ drizzle: db, schema })
semanticLayer.registerCube(ordersCube)
semanticLayer.registerCube(customersCube)
const cubeTools = getCubeTools({
semanticLayer,
getSecurityContext: async (meta) => ({
orgId: meta?.authInfo?.orgId
})
})
const app = new Hono()
app.post('/mcp', async (c) => {
const authToken = c.req.header('Authorization')?.replace('Bearer ', '')
const server = new Server(
{ name: 'my-api', version: '1.0.0' },
{ capabilities: { tools: {} } }
)
// One MCP server, both CRUD and analytics tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [...postgrestTools, ...cubeTools.definitions]
}))
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (cubeTools.handles(req.params.name)) {
return cubeTools.handle(req.params.name, req.params.arguments, {
authInfo: { orgId: authToken }
})
}
return handlePostgrestTool(req)
})
const transport = new StreamableHTTPTransport({ sessionIdGenerator: undefined })
await server.connect(transport)
return transport.handleRequest(c.req.raw)
})

Creates a composable tools object.

OptionTypeDefaultDescription
semanticLayerSemanticLayerCompilerrequiredSemantic layer with registered cubes
getSecurityContext(meta?) => SecurityContextrequiredExtracts security context for load tool
toolPrefixstring'drizzle_cube_'Prefix for tool names
toolsstring[]['discover', 'validate', 'load', 'chart']Which tools to expose
promptsMCPPrompt[]built-inCustom MCP prompts
resourcesMCPResource[]built-inCustom MCP resources
PropertyTypeDescription
definitionsMCPToolDefinition[]Tool schemas for tools/list
handle(name, args, meta?)Promise<MCPToolResult>Execute a tool call
handles(name)booleanCheck if name is a cube tool
promptsMCPPrompt[]Prompts for prompts/list
resourcesMCPResource[]Resources for resources/list
toolNamesstring[]Registered tool names

By default, tools are prefixed with drizzle_cube_:

ToolDefault Name
discoverdrizzle_cube_discover
validatedrizzle_cube_validate
loaddrizzle_cube_load
chartdrizzle_cube_chart

Customize the prefix:

// No prefix
const cubeTools = getCubeTools({ ..., toolPrefix: '' })
// → discover, validate, load, chart
// Custom prefix
const cubeTools = getCubeTools({ ..., toolPrefix: 'analytics_' })
// → analytics_discover, analytics_validate, analytics_load, analytics_chart

The handles() and handle() methods accept tool names both with and without the prefix.

Expose only specific tools:

const cubeTools = getCubeTools({
semanticLayer,
getSecurityContext,
tools: ['discover', 'load'] // no validate
})

The getSecurityContext callback is called every time the load tool executes. It receives whatever meta argument you pass to handle(), so you can thread auth info through from your MCP server’s request handling:

const cubeTools = getCubeTools({
semanticLayer,
getSecurityContext: async (meta) => {
// meta is whatever you pass as the 3rd arg to handle()
const user = await validateToken(meta?.authToken)
return { orgId: user.orgId, userId: user.id }
}
})
// In your tool handler, pass auth context as meta
cubeTools.handle(name, args, { authToken: request.headers.authorization })

The discover and validate tools don’t execute queries, so they don’t invoke getSecurityContext. Access to these tools is gated by whatever authentication your MCP server applies. The chart tool works identically to load but renders an interactive chart via MCP App.

FeatureBuilt-in (/mcp endpoint)Composable (drizzle-cube/mcp)
SetupZero-config with any adapterManual registration
Use caseStandalone MCP serverAdd to existing MCP server
ToolsSame 4 toolsSame 4 tools
Prompts & ResourcesIncludedIncluded
Protocol handlingBuilt-in JSON-RPC, SSEYou provide (or use SDK)
AuthFramework middlewareYour MCP server’s auth