Skip to main content
Glama

execute

Execute SQL queries or run saved Metabase cards to retrieve up to 500 rows of data from databases, supporting both custom queries and predefined analytics.

Instructions

Unified command to execute SQL queries or run saved cards against Metabase databases. Use Card mode when existing cards have the needed filters. Use SQL mode for custom queries or when cards lack required filters. Returns up to 500 rows per request - for larger datasets, use the export tool instead. SECURITY WARNING: SQL mode can execute ANY valid SQL including destructive operations (DELETE, UPDATE, DROP, TRUNCATE, ALTER). Use with caution and ensure appropriate database permissions are configured in Metabase. Note: When Read-Only Mode is enabled, write operations will be rejected with an error.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
database_idNoDatabase ID to execute query against (SQL mode only)
queryNoSQL query to execute (SQL mode only)
card_idNoID of saved card to execute (card mode only)
native_parametersNoParameters for SQL template variables like {{variable_name}} (SQL mode only)
card_parametersNoParameters for filtering card results (card mode only). Each parameter must follow Metabase format: {id: "uuid", slug: "param_name", target: ["dimension", ["template-tag", "param_name"]], type: "param_type", value: "param_value"}
row_limitNoMaximum number of rows to return (default: 100, max: 500). For larger datasets, use the export tool.

Implementation Reference

  • Primary handler function for the 'execute' tool. Validates input parameters, determines execution mode (SQL query or saved card), and delegates to specialized execute functions.
    export async function handleExecute( request: CallToolRequest, requestId: string, apiClient: MetabaseApiClient, logDebug: (message: string, data?: unknown) => void, logInfo: (message: string, data?: unknown) => void, logWarn: (message: string, data?: unknown, error?: Error) => void, logError: (message: string, error: unknown) => void ): Promise<ExecutionResponse> { const args = request.params?.arguments as ExecuteRequest; const databaseId = args?.database_id; const query = args?.query; const cardId = args?.card_id; const nativeParameters = Array.isArray(args?.native_parameters) ? args.native_parameters : []; const cardParameters = Array.isArray(args?.card_parameters) ? args.card_parameters : []; const rowLimitArg = args?.row_limit; const rowLimit = typeof rowLimitArg === 'number' ? rowLimitArg : 100; // First validate that parameter types are correct if (cardId !== undefined && typeof cardId !== 'number') { logWarn('Invalid card_id parameter - must be a number', { requestId }); throw new McpError(ErrorCode.InvalidParams, 'card_id parameter must be a number'); } if (databaseId !== undefined && typeof databaseId !== 'number') { logWarn('Invalid database_id parameter - must be a number', { requestId }); throw new McpError(ErrorCode.InvalidParams, 'database_id parameter must be a number'); } // Validate that either query+database_id or card_id is provided if (!cardId && !databaseId) { logWarn('Missing required parameters: either card_id or database_id must be provided', { requestId, }); throw new McpError( ErrorCode.InvalidParams, 'Either card_id or database_id parameter is required' ); } if (cardId && databaseId) { logWarn('Both card_id and database_id provided - only one is allowed', { requestId }); throw new McpError( ErrorCode.InvalidParams, 'Cannot specify both card_id and database_id - choose one execution method' ); } // Validate positive integer parameters if (cardId !== undefined) { validatePositiveInteger(cardId, 'card_id', requestId, logWarn); } if (databaseId !== undefined) { validatePositiveInteger(databaseId, 'database_id', requestId, logWarn); } validateRowLimit(rowLimit, 'row_limit', requestId, logWarn); // Strict parameter validation for card execution mode if (cardId) { // For card execution, only card_id, card_parameters, and row_limit are allowed if (query || databaseId || (nativeParameters && nativeParameters.length > 0)) { logWarn('Invalid parameters for card execution mode', { requestId, invalidParams: { query: query ? 'provided' : 'not provided', database_id: databaseId ? 'provided' : 'not provided', native_parameters: nativeParameters?.length > 0 ? 'provided' : 'not provided', }, }); throw new McpError( ErrorCode.InvalidParams, 'Card execution mode only allows card_id, card_parameters, and row_limit parameters' ); } } // Strict parameter validation for SQL execution mode if (databaseId) { // For SQL execution, only database_id, query, native_parameters, and row_limit are allowed if (cardId || (cardParameters && cardParameters.length > 0)) { logWarn('Invalid parameters for SQL execution mode', { requestId, invalidParams: { card_id: cardId ? 'provided' : 'not provided', card_parameters: cardParameters?.length > 0 ? 'provided' : 'not provided', }, }); throw new McpError( ErrorCode.InvalidParams, 'SQL execution mode only allows database_id, query, native_parameters, and row_limit parameters' ); } } // If executing a card if (cardId) { validatePositiveInteger(cardId, 'card_id', requestId, logWarn); // Validate row limit for cards if (rowLimit < 1 || rowLimit > 500) { logWarn(`Invalid row_limit parameter: ${rowLimit}. Must be between 1 and 500.`, { requestId, }); throw new McpError( ErrorCode.InvalidParams, 'Row limit must be between 1 and 500. For larger datasets, use the export tool instead.' ); } // Validate card parameters format if provided if (cardParameters.length > 0) { try { validateCardParameters(cardParameters, requestId, logWarn); } catch (error) { logWarn(`Card parameter validation failed for card ${cardId}`, { error, requestId }); throw new McpError( ErrorCode.InvalidParams, `Invalid card parameters format. If parameter issues persist, consider using execute_query with the card's underlying SQL query instead, which provides more reliable parameter handling. Original error: ${error instanceof Error ? error.message : String(error)}` ); } } const cardParams: CardExecutionParams = { cardId, cardParameters, rowLimit, }; return await executeCard( cardParams, requestId, apiClient, logDebug, logInfo, logWarn, logError ); } // If executing a SQL query if (!query || typeof query !== 'string') { logWarn('Missing or invalid query parameter in execute request', { requestId }); throw new McpError( ErrorCode.InvalidParams, 'SQL query parameter is required and must be a string' ); } // Validate row limit for SQL queries if (rowLimit < 1 || rowLimit > 500) { logWarn(`Invalid row_limit parameter: ${rowLimit}. Must be between 1 and 500.`, { requestId }); throw new McpError( ErrorCode.InvalidParams, 'Row limit must be between 1 and 500. For larger datasets, use the export tool instead.' ); } const sqlParams: SqlExecutionParams = { databaseId: databaseId as number, query, nativeParameters, rowLimit, }; return await executeSqlQuery( sqlParams, requestId, apiClient, logDebug, logInfo, logWarn, logError ); }
  • src/server.ts:530-541 (registration)
    Tool registration in MCP server: defines 'execute' tool schema in ListTools response and dispatches CallTool requests to handleExecute function.
    case 'execute': return safeCall(() => handleExecute( request, requestId, this.apiClient, this.logDebug.bind(this), this.logInfo.bind(this), this.logWarn.bind(this), this.logError.bind(this) ) );
  • TypeScript interfaces defining the input parameters, execution params for SQL/card modes, and response structure for the execute tool.
    export interface ExecuteRequest { database_id?: number; query?: string; card_id?: number; native_parameters?: any[]; card_parameters?: any[]; row_limit?: number; } export interface SqlExecutionParams { databaseId: number; query: string; nativeParameters: any[]; rowLimit: number; } export interface CardExecutionParams { cardId: number; cardParameters: any[]; rowLimit: number; } export interface ExecutionResponse { content: Array<{ type: 'text'; text: string; }>; }
  • Helper function to execute a saved Metabase card with parameters, handles API request, row limiting, and response formatting.
    export async function executeCard( params: CardExecutionParams, requestId: string, apiClient: MetabaseApiClient, logDebug: (message: string, data?: unknown) => void, logInfo: (message: string, data?: unknown) => void, logWarn: (message: string, data?: unknown, error?: Error) => void, logError: (message: string, error: unknown) => void ): Promise<ExecutionResponse> { const { cardId, cardParameters, rowLimit } = params; // Validate positive integer parameters validatePositiveInteger(cardId, 'card_id', requestId, logWarn); validatePositiveInteger(rowLimit, 'row_limit', requestId, logWarn); logDebug(`Executing card ID: ${cardId} with row limit: ${rowLimit}`); // Build card execution request body const cardRequestBody = { parameters: cardParameters, pivot_results: false, format_rows: false, }; try { const response = await apiClient.request<any>(`/api/card/${cardId}/query/json`, { method: 'POST', body: JSON.stringify(cardRequestBody), }); // Check for embedded errors in the response (Metabase returns 200 with embedded errors) validateMetabaseResponse( response, { operation: 'Card execution', resourceId: cardId }, logError ); // Handle different response formats from Metabase cards let originalRowCount = 0; let limitedData = response; // Check if response has numbered keys (actual card response format) const numberedKeys = Object.keys(response || {}).filter( key => /^\d+$/.test(key) && key !== 'data' ); if (numberedKeys.length > 0) { // Response format: {"0": {...}, "1": {...}, "2": {...}, "data": {...}} originalRowCount = numberedKeys.length; // Apply row limit by keeping only the first N numbered entries const limitedKeys = numberedKeys.slice(0, rowLimit); limitedData = { ...response, }; // Remove entries beyond the limit numberedKeys.forEach(key => { if (!limitedKeys.includes(key)) { delete limitedData[key]; } }); } else if (response?.data?.rows) { // Standard format: {"data": {"rows": [...]}} originalRowCount = response.data.rows.length; const limitedRows = response.data.rows.slice(0, rowLimit); limitedData = { ...response, data: { ...response.data, rows: limitedRows, }, }; } const finalRowCount = Math.min(originalRowCount, rowLimit); logInfo( `Successfully executed card: ${cardId}, returned ${finalRowCount} rows (original: ${originalRowCount})` ); if (originalRowCount > rowLimit) { logDebug( `Applied row limit: ${rowLimit} to card results (truncated from ${originalRowCount} rows)` ); } return { content: [ { type: 'text', text: formatJson({ success: true, card_id: cardId, row_count: finalRowCount, original_row_count: originalRowCount, applied_limit: rowLimit, data: limitedData, }), }, ], }; } catch (error: any) { // Check if this is a structured Metabase error response with parameter validation details if (error?.response?.data?.error_type === 'invalid-parameter') { logError(`Card parameter validation failed for card ${cardId}`, error.response.data); } throw handleApiError( error, { operation: 'Card execution', resourceType: 'card', resourceId: cardId }, logError ); } }
  • Helper function to execute raw SQL queries against a database, manages LIMIT clauses, read-only restrictions, parameters, and optimizes response data.
    export async function executeSqlQuery( params: SqlExecutionParams, requestId: string, apiClient: MetabaseApiClient, logDebug: (message: string, data?: unknown) => void, logInfo: (message: string, data?: unknown) => void, logWarn: (message: string, data?: unknown, error?: Error) => void, logError: (message: string, error: unknown) => void ): Promise<ExecutionResponse> { const { databaseId, query, nativeParameters, rowLimit } = params; // Validate positive integer parameters validatePositiveInteger(databaseId, 'database_id', requestId, logWarn); validatePositiveInteger(rowLimit, 'row_limit', requestId, logWarn); // Check read-only mode restriction if (config.METABASE_READ_ONLY_MODE && !isReadOnlyQuery(query)) { logWarn('Write operation blocked by read-only mode', { requestId, query: query.substring(0, 100), }); throw new McpError( ErrorCode.InvalidRequest, 'Read-only mode is enabled. Only SELECT queries are permitted. Write operations (INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, TRUNCATE, etc.) are blocked. To execute write queries, disable read-only mode by setting METABASE_READ_ONLY_MODE=false.' ); } logDebug(`Executing SQL query against database ID: ${databaseId} with row limit: ${rowLimit}`); // Handle LIMIT clause: only override if our limit is more restrictive than existing limit let limitedQuery = query.trim(); let finalLimit = rowLimit; let shouldAddLimit = false; // Look for existing LIMIT clause at the end of the query (most common case) // This regex properly handles LIMIT with optional OFFSET and accounts for trailing semicolons/whitespace const limitRegex = /\bLIMIT\s+(\d+)(?:\s+OFFSET\s+\d+)?\s*;?\s*$/i; const limitMatch = limitedQuery.match(limitRegex); if (limitMatch) { const existingLimit = parseInt(limitMatch[1], 10); logDebug(`Found existing LIMIT clause: ${existingLimit}, requested limit: ${rowLimit}`); if (existingLimit <= rowLimit) { // Existing limit is more restrictive or equal, keep it logDebug( `Keeping existing LIMIT ${existingLimit} as it's more restrictive than or equal to requested ${rowLimit}` ); finalLimit = existingLimit; // Don't modify the query } else { // Our limit is more restrictive, replace the existing LIMIT clause logDebug(`Replacing existing LIMIT ${existingLimit} with more restrictive limit ${rowLimit}`); limitedQuery = limitedQuery.replace(limitRegex, '').trim(); shouldAddLimit = true; } } else { // No LIMIT clause found at the end, add ours logDebug(`No existing LIMIT clause found, adding limit ${rowLimit}`); shouldAddLimit = true; } // Add LIMIT clause if needed if (shouldAddLimit) { if (limitedQuery.endsWith(';')) { limitedQuery = limitedQuery.slice(0, -1) + ` LIMIT ${rowLimit};`; } else { limitedQuery = limitedQuery + ` LIMIT ${rowLimit}`; } } // Build query request body const queryData = { type: 'native', native: { query: limitedQuery, template_tags: {}, }, parameters: nativeParameters, database: databaseId, }; try { const response = await apiClient.request<any>('/api/dataset', { method: 'POST', body: JSON.stringify(queryData), }); // Check for embedded errors in the response (Metabase returns 202 with errors for invalid queries) validateMetabaseResponse( response, { operation: 'SQL query execution', resourceId: databaseId }, logError ); const rowCount = response?.data?.rows?.length || 0; logInfo( `Successfully executed SQL query against database: ${databaseId}, returned ${rowCount} rows (limit: ${finalLimit})` ); // Create optimized response with only essential data const optimizedData = optimizeExecuteData(response?.data); return { content: [ { type: 'text', text: formatJson({ success: true, database_id: databaseId, row_count: rowCount, applied_limit: finalLimit, data: optimizedData, }), }, ], }; } catch (error: any) { throw handleApiError( error, { operation: 'SQL query execution', resourceType: 'database', resourceId: databaseId as number, }, logError ); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jerichosequitin/Metabase'

If you have feedback or need assistance with the MCP directory API, please join our Discord server