Skip to main content
Glama
errorHandling.ts12 kB
/** * Error handling utilities for the Metabase MCP server. */ import { ErrorCode, McpError } from '../types/core.js'; import { createErrorFromHttpResponse, ValidationErrorFactory } from './errorFactory.js'; /** * Extracts and cleans error messages from Metabase responses. * Metabase often includes SQL statements and technical details that aren't useful for users. * * Examples: * - 'Table "ORDERS" not found; SQL statement: SELECT...' -> 'Table "ORDERS" not found' * - 'Column "IDZ" not found; SQL statement: SELECT...' -> 'Column "IDZ" not found' * - 'Only SELECT statements are allowed...' -> (unchanged) */ export function extractCleanErrorMessage(error: string): string { if (!error) { return 'Unknown query error'; } // Remove SQL statement details (everything after "; SQL statement:") let cleaned = error.split('; SQL statement:')[0].trim(); // Remove Metabase query hash comments cleaned = cleaned.replace(/-- Metabase::.*$/gm, '').trim(); // Remove H2 database error codes like [42102-214] cleaned = cleaned.replace(/\s*\[\d+-\d+\]\s*$/, '').trim(); // Ensure it ends with a period if it doesn't have punctuation if (cleaned && !/[.!?]$/.test(cleaned)) { cleaned += '.'; } return cleaned || 'Unknown query error'; } /** * Error handling context for different operations */ export interface ErrorContext { operation: string; resourceType?: string; resourceId?: string | number; } /** * Centralized error handling utility that creates consistent error instances * with descriptive messages for AI agents */ export function handleApiError( error: any, context: ErrorContext, logError: (message: string, error: unknown) => void ): Error { logError(`${context.operation} failed`, error); // Extract detailed error information let errorMessage = `${context.operation} failed`; let errorDetails = ''; let statusCode = 'unknown'; if (error?.response) { // HTTP error response - use the enhanced error factory statusCode = error.response.status?.toString() || 'unknown'; const responseData = error.response.data || error.response; if (typeof responseData === 'string') { errorDetails = responseData; } else if (responseData?.message) { errorDetails = responseData.message; } else if (responseData?.error) { errorDetails = responseData.error; } else { errorDetails = JSON.stringify(responseData); } // Use the enhanced error factory for HTTP responses try { const httpStatus = parseInt(statusCode, 10); if (!isNaN(httpStatus)) { return createErrorFromHttpResponse( httpStatus, responseData, context.operation, context.resourceType, context.resourceId ); } } catch (factoryError) { // Fall back to generic error handling if factory fails logError('Error factory failed, using generic error handling', factoryError); } errorMessage = `Metabase API error (${statusCode})`; errorMessage += getStatusCodeMessage(statusCode, context); } else if (error?.message) { errorDetails = error.message; errorMessage = getGenericErrorMessage(error.message, context); } else { errorDetails = String(error); errorMessage = `Unknown error occurred during ${context.operation.toLowerCase()}`; } // Log detailed error for debugging logError( `Detailed ${context.operation.toLowerCase()} error - Status: ${statusCode}, Details: ${errorDetails}`, error ); return new Error(errorMessage); } /** * Get standard error message based on HTTP status code */ function getStatusCodeMessage(statusCode: string, context: ErrorContext): string { const { operation, resourceType, resourceId } = context; switch (statusCode) { case '400': if (resourceType && resourceId) { if ( resourceType === 'card' && (operation.toLowerCase().includes('execute') || operation.toLowerCase().includes('export')) ) { return `Invalid ${resourceType}_id parameter or card configuration issue. Ensure the ${resourceType} ID is valid and exists. If parameter issues persist, consider using ${operation.toLowerCase().includes('execute') ? 'execute_query' : 'export_query'} with the card's underlying SQL query instead.`; } return `Invalid ${resourceType}_id parameter. Ensure the ${resourceType} ID is valid and exists.`; } return `Invalid parameters or request format. Check your input parameters.`; case '401': return `Authentication failed. Check your API key or session token.`; case '403': if (resourceType) { return `Access denied. You may not have permission to access this ${resourceType}.`; } return `Access denied. You may not have sufficient permissions for this operation.`; case '404': if (resourceType && resourceId) { if ( resourceType === 'card' && (operation.toLowerCase().includes('execute') || operation.toLowerCase().includes('export')) ) { return `${resourceType.charAt(0).toUpperCase() + resourceType.slice(1)} not found. Check that the ${resourceType}_id (${resourceId}) is correct and the ${resourceType} exists. Alternatively, use ${operation.toLowerCase().includes('execute') ? 'execute_query' : 'export_query'} to run the SQL query directly against the database.`; } return `${resourceType.charAt(0).toUpperCase() + resourceType.slice(1)} not found. Check that the ${resourceType}_id (${resourceId}) is correct and the ${resourceType} exists.`; } if (resourceType) { return `${resourceType.charAt(0).toUpperCase() + resourceType.slice(1)} not found. Check that the ${resourceType} exists.`; } return `Metabase item not found. Check your parameters and ensure the item exists.`; case '413': return `Request payload too large. Try reducing the result set size or use query filters.`; case '500': if ( operation.toLowerCase().includes('query') || operation.toLowerCase().includes('execute') ) { if ( resourceType === 'card' && (operation.toLowerCase().includes('execute') || operation.toLowerCase().includes('export')) ) { return `Database server error. The query may have caused a timeout or database issue. Try using ${operation.toLowerCase().includes('execute') ? 'execute_query' : 'export_query'} with the card's SQL query for better error handling and debugging capabilities.`; } return `Database server error. The query may have caused a timeout or database issue.`; } return `Metabase server error. The server may be experiencing issues.`; case '502': case '503': return `Metabase server temporarily unavailable. Try again later.`; default: return `Unexpected server response (${statusCode}). Please check the server status.`; } } /** * Get error message for non-HTTP errors. * Preserves the original error message to avoid hiding meaningful Metabase errors. */ function getGenericErrorMessage(errorMessage: string, context: ErrorContext): string { const { operation } = context; // Only transform truly generic network/infrastructure errors if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('ECONNREFUSED')) { return `Network error connecting to Metabase. Check your connection and Metabase URL.`; } // Avoid double-prefixing if error already contains the operation if (errorMessage.includes(`${operation} failed`)) { return errorMessage; } // Pass through all other error messages - they likely contain meaningful info from Metabase return `${operation} failed: ${errorMessage}`; } /** * Checks if a Metabase response contains embedded error information * and throws appropriate errors if found. * * Metabase sometimes returns HTTP 200 responses with error details embedded * in the response body rather than using proper HTTP error status codes. * * Card and Query APIs return different error structures: * - Card API: Uses `message` field for invalid parameter names, `via[].error` for value errors * - Query API: Uses `status: 'failed'` with `error` field for SQL errors * * @param response - The response from Metabase API * @param context - Context information for error logging * @param logError - Error logging function * @throws {McpError} If the response contains parameter validation errors */ export function validateMetabaseResponse( response: any, context: { operation: string; resourceId?: string | number }, logError: (message: string, data?: unknown) => void ): void { const isCardOperation = context.operation.toLowerCase().includes('card'); // Card-specific error handling if (isCardOperation) { // Test 1 pattern: Invalid parameter name (no error_type, has message with "Invalid parameter") if (response?.message && response.message.includes('Invalid parameter')) { logError( `${context.operation} parameter validation failed${context.resourceId ? ` for ${context.resourceId}` : ''}`, response ); throw new Error(response.message); } // Test 2 pattern: Invalid parameter value (has error_type: 'invalid-parameter') if (response?.error_type === 'invalid-parameter') { logError( `${context.operation} parameter validation failed${context.resourceId ? ` for ${context.resourceId}` : ''}`, response ); // Prefer via[].error for more descriptive message const viaError = response?.via?.[0]?.error; if (viaError) { throw new Error(viaError); } // Fallback to top-level error if (response?.error) { throw new Error(response.error); } // Fallback to generic parameter error throw new McpError( ErrorCode.InvalidParams, `${context.operation} parameter validation failed: Invalid parameter values` ); } } else { // Query-specific error handling (original behavior) if (response?.error_type === 'invalid-parameter') { logError( `${context.operation} parameter validation failed${context.resourceId ? ` for ${context.resourceId}` : ''}`, response ); // Check for parameter errors in the via array const parameterErrors = response?.via?.filter( (error: any) => error?.error_type === 'invalid-parameter' && error?.['ex-data'] ); if (parameterErrors && parameterErrors.length > 0) { // Use the first parameter error found throw ValidationErrorFactory.cardParameterMismatch(parameterErrors[0]['ex-data']); } // Fallback: check top-level ex-data if via array doesn't contain parameter errors const errorDetails = response?.['ex-data']; if (errorDetails) { throw ValidationErrorFactory.cardParameterMismatch(errorDetails); } // Fallback to generic parameter error throw new McpError( ErrorCode.InvalidParams, `${context.operation} parameter validation failed: ${response.error || 'Invalid parameter values'}` ); } } // Check for query execution errors (status: 'failed' with error message) // Metabase returns 202 with these errors for invalid SQL (wrong table/column names, etc.) if (response?.status === 'failed' && response?.error) { const cleanedError = extractCleanErrorMessage(response.error); logError(`${context.operation} failed: ${cleanedError}`, response); throw new Error(cleanedError); } // Check for other common embedded error types (legacy handling) if (response?.error_type && response?.status === 'failed') { logError( `${context.operation} failed with embedded error${context.resourceId ? ` for ${context.resourceId}` : ''}`, response ); throw new Error(response.error || 'Unknown error'); } }

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