import { z } from "zod";
// Custom error enum
export enum ErrorCode {
InternalError = "internal_error",
InvalidRequest = "invalid_request",
InvalidParams = "invalid_params",
MethodNotFound = "method_not_found"
}
// Custom error class
export class McpError extends Error {
code: ErrorCode;
constructor(code: ErrorCode, message: string) {
super(message);
this.code = code;
this.name = "McpError";
}
}
// API error type definition
interface ApiError {
status?: number;
message?: string;
data?: { message?: string };
}
// Logger level enum
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
FATAL = 'fatal'
}
// Authentication method enum
export enum AuthMethod {
SESSION = 'session',
API_KEY = 'api_key'
}
// Define request schema for tool listing
export const ListToolsRequestSchema = z.object({
method: z.literal("tools/list")
});
// Tool handler interface
export interface ToolHandler {
setupToolHandlers(): void;
}
// Tool definitions
export const TOOL_DEFINITIONS = [
{
name: "list_dashboards",
description: "List all dashboards in Metabase",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "list_cards",
description: "List all questions/cards in Metabase",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "list_databases",
description: "List all databases in Metabase",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "execute_card",
description: "Execute a Metabase question/card and get results",
inputSchema: {
type: "object",
properties: {
card_id: {
type: "number",
description: "ID of the card/question to execute"
},
parameters: {
type: "object",
description: "Optional parameters for the query"
}
},
required: ["card_id"]
}
},
{
name: "get_dashboard_cards",
description: "Get all cards in a dashboard",
inputSchema: {
type: "object",
properties: {
dashboard_id: {
type: "number",
description: "ID of the dashboard"
}
},
required: ["dashboard_id"]
}
},
{
name: "execute_query",
description: "Execute a SQL query against a Metabase database",
inputSchema: {
type: "object",
properties: {
database_id: {
type: "number",
description: "ID of the database to query"
},
query: {
type: "string",
description: "SQL query to execute"
},
native_parameters: {
type: "array",
description: "Optional parameters for the query",
items: {
type: "object"
}
}
},
required: ["database_id", "query"]
}
},
{
name: "create_card",
description: "Create a new question/card in Metabase",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the card"
},
database_id: {
type: "number",
description: "ID of the database to query"
},
query: {
type: "string",
description: "SQL query for the card"
},
description: {
type: "string",
description: "Description of the card"
},
viz_settings: {
type: "object",
description: "Visualization settings for the card"
},
collection_id: {
type: "number",
description: "ID of the collection to put the card in (optional)"
},
collection_position: {
type: "number",
description: "Position within the collection (optional)"
}
},
required: ["name", "database_id", "query"]
}
},
{
name: "update_card_visualization",
description: "Update visualization settings for a card",
inputSchema: {
type: "object",
properties: {
card_id: {
type: "number",
description: "ID of the card to update"
},
viz_settings: {
type: "object",
description: "New visualization settings"
}
},
required: ["card_id", "viz_settings"]
}
},
{
name: "add_card_to_dashboard",
description: "Add a card to a dashboard",
inputSchema: {
type: "object",
properties: {
dashboard_id: {
type: "number",
description: "ID of the dashboard"
},
card_id: {
type: "number",
description: "ID of the card to add"
},
row: {
type: "number",
description: "Row position in the dashboard grid"
},
col: {
type: "number",
description: "Column position in the dashboard grid"
},
size_x: {
type: "number",
description: "Width of the card in dashboard grid units"
},
size_y: {
type: "number",
description: "Height of the card in dashboard grid units"
},
parameter_mappings: {
type: "array",
description: "Parameter mappings for dashboard filters",
items: {
type: "object"
}
},
series: {
type: "array",
description: "Additional series to include with this card",
items: {
type: "object"
}
},
tab_id: {
type: "number",
description: "ID of the tab to add the card to (optional)"
}
},
required: ["dashboard_id", "card_id"]
}
},
{
name: "create_dashboard",
description: "Create a new dashboard in Metabase",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the dashboard"
},
description: {
type: "string",
description: "Description of the dashboard"
},
parameters: {
type: "array",
description: "Dashboard filter parameters (optional)",
items: {
type: "object"
}
},
collection_id: {
type: "number",
description: "ID of the collection to put the dashboard in (optional)"
},
collection_position: {
type: "number",
description: "Position within the collection (optional)"
},
cache_ttl: {
type: "number",
description: "Cache time-to-live in seconds"
},
auto_apply_filters: {
type: "boolean",
description: "Whether filters should auto-apply"
},
enable_embedding: {
type: "boolean",
description: "Whether to enable embedding for this dashboard"
}
},
required: ["name"]
}
},
{
name: "list_collections",
description: "List all collections in Metabase",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "create_collection",
description: "Create a new collection in Metabase",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the collection"
},
description: {
type: "string",
description: "Description of the collection"
},
color: {
type: "string",
description: "Color for the collection (optional)"
},
parent_id: {
type: "number",
description: "ID of the parent collection (optional)"
}
},
required: ["name"]
}
},
{
name: "list_tables",
description: "List all tables in a database",
inputSchema: {
type: "object",
properties: {
database_id: {
type: "number",
description: "ID of the database"
},
include_tables: {
type: "boolean",
description: "Whether to include tables in the response"
},
include_cards: {
type: "boolean",
description: "Whether to include saved questions/cards in the response"
}
},
required: ["database_id"]
}
},
{
name: "get_table_fields",
description: "Get all fields/columns in a table",
inputSchema: {
type: "object",
properties: {
table_id: {
type: "number",
description: "ID of the table"
}
},
required: ["table_id"]
}
},
{
name: "update_dashboard",
description: "Update an existing dashboard",
inputSchema: {
type: "object",
properties: {
dashboard_id: {
type: "number",
description: "ID of the dashboard to update"
},
name: {
type: "string",
description: "New name for the dashboard"
},
description: {
type: "string",
description: "New description for the dashboard"
},
parameters: {
type: "array",
description: "Updated dashboard filter parameters",
items: {
type: "object"
}
},
collection_id: {
type: "number",
description: "New collection ID for the dashboard"
},
collection_position: {
type: "number",
description: "New position within the collection"
},
cache_ttl: {
type: "number",
description: "Cache time-to-live in seconds"
},
auto_apply_filters: {
type: "boolean",
description: "Whether filters should auto-apply"
},
enable_embedding: {
type: "boolean",
description: "Whether to enable embedding"
},
embedding_params: {
type: "object",
description: "Parameters configuration for embedding"
},
archived: {
type: "boolean",
description: "Whether to archive the dashboard"
}
},
required: ["dashboard_id"]
}
},
{
name: "delete_dashboard",
description: "Delete a dashboard",
inputSchema: {
type: "object",
properties: {
dashboard_id: {
type: "number",
description: "ID of the dashboard to delete"
}
},
required: ["dashboard_id"]
}
}
];
// Tool execution handler class that implements the tool handler functionality
export class ToolExecutionHandler {
private log: (level: LogLevel, message: string, data?: unknown, error?: Error) => void;
private request: <T>(path: string, options?: RequestInit) => Promise<T>;
private getSessionToken: () => Promise<string>;
private generateRequestId: () => string;
constructor(
logFn: (level: LogLevel, message: string, data?: unknown, error?: Error) => void,
requestFn: <T>(path: string, options?: RequestInit) => Promise<T>,
getSessionTokenFn: () => Promise<string>,
generateRequestIdFn: () => string,
) {
this.log = logFn;
this.request = requestFn;
this.getSessionToken = getSessionTokenFn;
this.generateRequestId = generateRequestIdFn;
}
/**
* Handle tool execution requests
*/
async executeToolRequest(request: any): Promise<any> {
const toolName = request.params?.name || 'unknown';
const requestId = this.generateRequestId();
this.log(LogLevel.INFO, `Processing tool execution request: ${toolName}`, {
requestId,
toolName,
arguments: request.params?.arguments
});
await this.getSessionToken();
try {
switch (request.params?.name) {
case "list_dashboards": {
this.log(LogLevel.DEBUG, 'Fetching all dashboards from Metabase');
const response = await this.request<any[]>('/api/dashboard');
this.log(LogLevel.INFO, `Successfully retrieved ${response.length} dashboards`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "list_cards": {
this.log(LogLevel.DEBUG, 'Fetching all cards/questions from Metabase');
const response = await this.request<any[]>('/api/card');
this.log(LogLevel.INFO, `Successfully retrieved ${response.length} cards/questions`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "list_databases": {
this.log(LogLevel.DEBUG, 'Fetching all databases from Metabase');
const response = await this.request<any[]>('/api/database');
this.log(LogLevel.INFO, `Successfully retrieved ${response.length} databases`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "execute_card": {
const cardId = request.params?.arguments?.card_id;
if (!cardId) {
this.log(LogLevel.WARN, 'Missing card_id parameter in execute_card request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Card ID parameter is required"
);
}
this.log(LogLevel.DEBUG, `Executing card with ID: ${cardId}`);
const parameters = request.params?.arguments?.parameters || {};
const response = await this.request<any>(`/api/card/${cardId}/query`, {
method: 'POST',
body: JSON.stringify({ parameters })
});
this.log(LogLevel.INFO, `Successfully executed card: ${cardId}`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "get_dashboard_cards": {
const dashboardId = request.params?.arguments?.dashboard_id;
if (!dashboardId) {
this.log(LogLevel.WARN, 'Missing dashboard_id parameter in get_dashboard_cards request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Dashboard ID parameter is required"
);
}
this.log(LogLevel.DEBUG, `Fetching cards for dashboard with ID: ${dashboardId}`);
const response = await this.request<any>(`/api/dashboard/${dashboardId}`);
const cardCount = response.cards?.length || 0;
this.log(LogLevel.INFO, `Successfully retrieved ${cardCount} cards from dashboard: ${dashboardId}`);
return {
content: [{
type: "text",
text: JSON.stringify(response.cards, null, 2)
}]
};
}
case "execute_query": {
const databaseId = request.params?.arguments?.database_id;
const query = request.params?.arguments?.query;
const nativeParameters = request.params?.arguments?.native_parameters || [];
if (!databaseId) {
this.log(LogLevel.WARN, 'Missing database_id parameter in execute_query request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Database ID parameter is required"
);
}
if (!query) {
this.log(LogLevel.WARN, 'Missing query parameter in execute_query request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"SQL query parameter is required"
);
}
this.log(LogLevel.DEBUG, `Executing SQL query against database ID: ${databaseId}`);
// Build query request body
const queryData = {
type: "native",
native: {
query: query,
template_tags: {}
},
parameters: nativeParameters,
database: databaseId
};
const response = await this.request<any>('/api/dataset', {
method: 'POST',
body: JSON.stringify(queryData)
});
this.log(LogLevel.INFO, `Successfully executed SQL query against database: ${databaseId}`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "create_card": {
const { name, database_id, query, description, viz_settings } = request.params?.arguments || {};
if (!name || !database_id || !query) {
this.log(LogLevel.WARN, 'Missing required parameters in create_card request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Name, database ID and query parameters are required"
);
}
this.log(LogLevel.DEBUG, `Creating card with name: ${name}`);
const cardData = {
name,
dataset_query: {
type: "native",
native: {
query,
template_tags: {}
},
database: database_id
},
description: description || "",
viz_settings: viz_settings || {}
};
const response = await this.request<any>('/api/card', {
method: 'POST',
body: JSON.stringify(cardData)
});
this.log(LogLevel.INFO, `Successfully created card: ${name} with ID: ${response.id}`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "update_card_visualization": {
const { card_id, viz_settings } = request.params?.arguments || {};
if (!card_id || !viz_settings) {
this.log(LogLevel.WARN, 'Missing required parameters in update_card_visualization request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Card ID and visualization settings parameters are required"
);
}
this.log(LogLevel.DEBUG, `Updating visualization settings for card ID: ${card_id}`);
// First get the current card
const card = await this.request<any>(`/api/card/${card_id}`);
// Update the visualization settings
const updateData = {
...card,
viz_settings
};
const response = await this.request<any>(`/api/card/${card_id}`, {
method: 'PUT',
body: JSON.stringify(updateData)
});
this.log(LogLevel.INFO, `Successfully updated visualization settings for card: ${card_id}`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "add_card_to_dashboard": {
const { dashboard_id, card_id, row, col, size_x, size_y, parameter_mappings, series, tab_id } = request.params?.arguments || {};
if (!dashboard_id || !card_id) {
this.log(LogLevel.WARN, 'Missing required parameters in add_card_to_dashboard request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Dashboard ID and card ID parameters are required"
);
}
this.log(LogLevel.DEBUG, `Adding card ${card_id} to dashboard ${dashboard_id}`);
// First get the current dashboard to get existing cards
const currentDashboard = await this.request<any>(`/api/dashboard/${dashboard_id}`);
// Create the new card entry
const newCard: any = {
id: card_id,
row: row || 0,
col: col || 0,
size_x: size_x || 4,
size_y: size_y || 4,
parameter_mappings: parameter_mappings || []
};
if (series && series.length > 0) {
newCard.series = series;
}
// Get existing cards and add the new one
const existingCards = currentDashboard.ordered_cards || [];
const updatedCards = [...existingCards.map((card: any) => ({
id: card.card_id,
row: card.row,
col: card.col,
size_x: card.size_x,
size_y: card.size_y,
parameter_mappings: card.parameter_mappings || [],
...(card.series && card.series.length > 0 ? { series: card.series } : {})
})), newCard];
// Prepare tabs info if provided
const payload: any = {
cards: updatedCards
};
if (tab_id || (currentDashboard.tabs && currentDashboard.tabs.length > 0)) {
payload.tabs = currentDashboard.tabs || [];
// If tab_id is provided, ensure the card is assigned to that tab
if (tab_id) {
newCard.tab_id = tab_id;
}
}
// Update the dashboard with PUT
const response = await this.request<any>(`/api/dashboard/${dashboard_id}/cards`, {
method: 'PUT',
body: JSON.stringify(payload)
});
this.log(LogLevel.INFO, `Successfully added card ${card_id} to dashboard ${dashboard_id}`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "create_dashboard": {
const { name, description, parameters, collection_id, collection_position, cache_ttl, auto_apply_filters, enable_embedding } = request.params?.arguments || {};
if (!name) {
this.log(LogLevel.WARN, 'Missing name parameter in create_dashboard request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Dashboard name parameter is required"
);
}
this.log(LogLevel.DEBUG, `Creating dashboard with name: ${name}`);
const dashboardData: any = {
name,
description: description || "",
parameters: parameters || []
};
if (collection_id) {
dashboardData.collection_id = collection_id;
}
if (collection_position) {
dashboardData.collection_position = collection_position;
}
if (cache_ttl) {
dashboardData.cache_ttl = cache_ttl;
}
if (auto_apply_filters !== undefined) {
dashboardData.auto_apply_filters = auto_apply_filters;
}
if (enable_embedding !== undefined) {
dashboardData.enable_embedding = enable_embedding;
}
const response = await this.request<any>('/api/dashboard', {
method: 'POST',
body: JSON.stringify(dashboardData)
});
this.log(LogLevel.INFO, `Successfully created dashboard: ${name} with ID: ${response.id}`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "list_collections": {
this.log(LogLevel.DEBUG, 'Fetching all collections from Metabase');
const response = await this.request<any[]>('/api/collection');
this.log(LogLevel.INFO, `Successfully retrieved ${response.length} collections`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "create_collection": {
const { name, description, color, parent_id } = request.params?.arguments || {};
if (!name) {
this.log(LogLevel.WARN, 'Missing name parameter in create_collection request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Collection name parameter is required"
);
}
this.log(LogLevel.DEBUG, `Creating collection with name: ${name}`);
const collectionData: any = {
name,
description: description || ""
};
if (color) {
collectionData.color = color;
}
if (parent_id) {
collectionData.parent_id = parent_id;
}
const response = await this.request<any>('/api/collection', {
method: 'POST',
body: JSON.stringify(collectionData)
});
this.log(LogLevel.INFO, `Successfully created collection: ${name} with ID: ${response.id}`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "list_tables": {
const { database_id, include_tables, include_cards } = request.params?.arguments || {};
if (!database_id) {
this.log(LogLevel.WARN, 'Missing database_id parameter in list_tables request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Database ID parameter is required"
);
}
this.log(LogLevel.DEBUG, `Fetching tables for database ID: ${database_id}`);
let url = `/api/database/${database_id}/tables`;
const queryParams = [];
if (include_tables !== undefined) {
queryParams.push(`include_tables=${include_tables}`);
}
if (include_cards !== undefined) {
queryParams.push(`include_cards=${include_cards}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join('&')}`;
}
const response = await this.request<any[]>(url);
this.log(LogLevel.INFO, `Successfully retrieved ${response.length} tables from database: ${database_id}`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "get_table_fields": {
const { table_id } = request.params?.arguments || {};
if (!table_id) {
this.log(LogLevel.WARN, 'Missing table_id parameter in get_table_fields request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Table ID parameter is required"
);
}
this.log(LogLevel.DEBUG, `Fetching fields for table ID: ${table_id}`);
const response = await this.request<any[]>(`/api/table/${table_id}/fields`);
this.log(LogLevel.INFO, `Successfully retrieved ${response.length} fields from table: ${table_id}`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "update_dashboard": {
const { dashboard_id, name, description, parameters, collection_id, collection_position,
cache_ttl, auto_apply_filters, enable_embedding, embedding_params, archived } = request.params?.arguments || {};
if (!dashboard_id) {
this.log(LogLevel.WARN, 'Missing dashboard_id parameter in update_dashboard request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Dashboard ID parameter is required"
);
}
this.log(LogLevel.DEBUG, `Updating dashboard with ID: ${dashboard_id}`);
// Build update data
const updateData: any = {};
if (name) updateData.name = name;
if (description !== undefined) updateData.description = description;
if (parameters) updateData.parameters = parameters;
if (collection_id) updateData.collection_id = collection_id;
if (collection_position) updateData.collection_position = collection_position;
if (cache_ttl !== undefined) updateData.cache_ttl = cache_ttl;
if (auto_apply_filters !== undefined) updateData.auto_apply_filters = auto_apply_filters;
if (enable_embedding !== undefined) updateData.enable_embedding = enable_embedding;
if (embedding_params) updateData.embedding_params = embedding_params;
if (archived !== undefined) updateData.archived = archived;
const response = await this.request<any>(`/api/dashboard/${dashboard_id}`, {
method: 'PUT',
body: JSON.stringify(updateData)
});
this.log(LogLevel.INFO, `Successfully updated dashboard with ID: ${dashboard_id}`);
return {
content: [{
type: "text",
text: JSON.stringify(response, null, 2)
}]
};
}
case "delete_dashboard": {
const { dashboard_id } = request.params?.arguments || {};
if (!dashboard_id) {
this.log(LogLevel.WARN, 'Missing dashboard_id parameter in delete_dashboard request', { requestId });
throw new McpError(
ErrorCode.InvalidParams,
"Dashboard ID parameter is required"
);
}
this.log(LogLevel.DEBUG, `Deleting dashboard with ID: ${dashboard_id}`);
await this.request<any>(`/api/dashboard/${dashboard_id}`, {
method: 'DELETE'
});
this.log(LogLevel.INFO, `Successfully deleted dashboard with ID: ${dashboard_id}`);
return {
content: [{
type: "text",
text: JSON.stringify({ success: true, id: dashboard_id }, null, 2)
}]
};
}
default:
this.log(LogLevel.WARN, `Received request for unknown tool: ${request.params?.name}`, { requestId });
return {
content: [
{
type: "text",
text: `Unknown tool: ${request.params?.name}`
}
],
isError: true
};
}
} catch (error) {
const apiError = error as ApiError;
const errorMessage = apiError.data?.message || apiError.message || 'Unknown error';
this.log(LogLevel.ERROR, `Tool execution failed: ${errorMessage}`, error);
return {
content: [{
type: "text",
text: `Metabase API error: ${errorMessage}`
}],
isError: true
};
}
}
}