Skip to main content
Glama

search

Search across Metabase items including cards, dashboards, tables, and collections using native search API. Find content by name, description, or metadata with organized results by model type.

Instructions

Search across all Metabase items using native search API. Supports cards, dashboards, tables, collections, databases, and more. Use this first for finding any Metabase content. Returns search metrics, recommendations, and clean results organized by model type.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryNoSearch across names, descriptions, and metadata.
modelsNoModel types to search (default: ["card", "dashboard"]). RESTRICTION: "database" model cannot be mixed with others and must be used exclusively.
max_resultsNoMaximum number of results to return (default: 20, max: 50)
search_native_queryNoSearch within SQL query content of cards (default: false). RESTRICTION: Only works when models=["card"] exclusively.
include_dashboard_questionsNoInclude questions within dashboards in results (default: false). RESTRICTION: Only works when "dashboard" is included in models.
idsNoSearch for specific IDs. RESTRICTIONS: Only works with single model type, cannot be used with "table" or "database" models.
archivedNoSearch archived items only (default: false)
database_idNoSearch items from specific database ID. RESTRICTION: Cannot be used when searching for databases (models=["database"]).
verifiedNoSearch verified items only (requires premium features)

Implementation Reference

  • The core handler function `handleSearch` that implements the 'search' tool logic. Extracts parameters, performs validation, calls Metabase /api/search API, processes results, adds recommendations, and formats output.
    export async function handleSearch( 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 ) { const searchQuery = request.params?.arguments?.query as string; const models = (request.params?.arguments?.models as string[]) || ['card', 'dashboard']; const maxResults = (request.params?.arguments?.max_results as number) ?? 20; const searchNativeQuery = (request.params?.arguments?.search_native_query as boolean) || false; const includeDashboardQuestions = (request.params?.arguments?.include_dashboard_questions as boolean) ?? false; const ids = request.params?.arguments?.ids as number[] | undefined; const archived = request.params?.arguments?.archived as boolean | undefined; const databaseId = request.params?.arguments?.database_id as number | undefined; const verified = request.params?.arguments?.verified as boolean | undefined; // Validate positive integer parameters if (maxResults !== undefined) { validatePositiveInteger(maxResults, 'max_results', requestId, logWarn); if (maxResults > 50) { logWarn('max_results exceeds maximum allowed value', { requestId, maxResults }); throw new McpError(ErrorCode.InvalidParams, 'max_results must be at most 50'); } } if (databaseId !== undefined) { validatePositiveInteger(databaseId, 'database_id', requestId, logWarn); } if (ids !== undefined && ids.length > 0) { ids.forEach((id, index) => { validatePositiveInteger(id, `ids[${index}]`, requestId, logWarn); }); } // Validate models parameter with case insensitive handling if (models && models.length > 0) { const supportedModels = ['card', 'dashboard', 'table', 'database', 'collection'] as const; models.forEach((model, index) => { validateEnumValue(model, supportedModels, `models[${index}]`, requestId, logWarn); }); } // Updated validation: allow searching without query/id if database_id is provided if (!searchQuery && (!ids || ids.length === 0) && !databaseId) { logWarn('Missing query, ids, or database_id parameter in search request', { requestId }); throw ValidationErrorFactory.invalidParameter( 'query_or_ids_or_database_id', 'none provided', 'Either search query, ids parameter, or database_id is required' ); } // Validate that only one search method is used if (searchQuery && ids && ids.length > 0) { logWarn('Cannot use both query and ids parameters simultaneously', { requestId }); throw ValidationErrorFactory.invalidParameter( 'query_and_ids', 'both provided', 'Cannot use both query and ids parameters - use either search query OR ids, not both' ); } // Validate ids usage - only allowed with single model if (ids && ids.length > 0 && models.length > 1) { logWarn('ids parameter can only be used with a single model type', { requestId }); throw new McpError( ErrorCode.InvalidParams, 'ids parameter can only be used when searching a single model type' ); } // Validate ids usage - not allowed with 'table' model if (ids && ids.length > 0 && models.includes('table')) { logWarn('ids parameter cannot be used with table model', { requestId }); throw new McpError( ErrorCode.InvalidParams, 'ids parameter cannot be used when searching for tables - use query or database_id instead' ); } // Validate database searches - only allow query parameter when searching solely for databases if (models.length === 1 && models[0] === 'database') { if (ids && ids.length > 0) { logWarn('ids parameter cannot be used when searching solely for databases', { requestId }); throw new McpError( ErrorCode.InvalidParams, 'ids parameter cannot be used when searching for databases - use query instead' ); } if (databaseId) { logWarn('database_id parameter cannot be used when searching solely for databases', { requestId, }); throw new McpError( ErrorCode.InvalidParams, 'database_id parameter cannot be used when searching for databases - use query instead' ); } } // Validate database model exclusivity - database searches must be exclusive if (models.includes('database') && models.length > 1) { logWarn('database model cannot be mixed with other models', { requestId }); throw new McpError( ErrorCode.InvalidParams, 'database model cannot be combined with other model types - search for databases separately' ); } // Validate model types const validModels = [ 'card', 'dashboard', 'table', 'dataset', 'segment', 'collection', 'database', 'action', 'indexed-entity', 'metric', ]; const invalidModels = models.filter(model => !validModels.includes(model)); if (invalidModels.length > 0) { logWarn(`Invalid model types specified: ${invalidModels.join(', ')}`, { requestId }); throw new McpError( ErrorCode.InvalidParams, `Invalid model types: ${invalidModels.join(', ')}. Valid types are: ${validModels.join(', ')}` ); } // Validate database_id if provided if (databaseId && (typeof databaseId !== 'number' || databaseId <= 0)) { logWarn('Invalid database_id parameter', { requestId }); throw new McpError(ErrorCode.InvalidParams, 'database_id must be a positive integer'); } // Validate search_native_query - only allowed when searching cards exclusively if (searchNativeQuery && (models.length !== 1 || models[0] !== 'card')) { logWarn('search_native_query parameter can only be used when searching cards exclusively', { requestId, }); throw new McpError( ErrorCode.InvalidParams, 'search_native_query parameter can only be used when models=["card"] - it searches within SQL query content of cards' ); } // Validate include_dashboard_questions - only allowed when dashboard is in models if (includeDashboardQuestions && !models.includes('dashboard')) { logWarn( 'include_dashboard_questions parameter can only be used when dashboard model is included', { requestId } ); throw new McpError( ErrorCode.InvalidParams, 'include_dashboard_questions parameter can only be used when "dashboard" is included in the models array' ); } logDebug(`Search with query: "${searchQuery}", models: ${models.join(', ')}`); const searchStartTime = Date.now(); try { // Build search parameters const searchParams = new URLSearchParams(); if (searchQuery) { searchParams.append('q', searchQuery); } // Add models models.forEach(model => searchParams.append('models', model)); // Add optional parameters if (searchNativeQuery) { searchParams.append('search_native_query', 'true'); } if (includeDashboardQuestions) { searchParams.append('include_dashboard_questions', 'true'); } if (ids && ids.length > 0) { ids.forEach(id => searchParams.append('ids', id.toString())); } if (archived === true) { searchParams.append('archived', 'true'); } if (databaseId) { searchParams.append('table_db_id', databaseId.toString()); } if (verified === true) { searchParams.append('verified', 'true'); } const response = await apiClient.request<any>(`/api/search?${searchParams.toString()}`); const searchTime = Date.now() - searchStartTime; // Extract results and limit let results = response.data || response || []; if (Array.isArray(results)) { results = results.slice(0, maxResults); } else { results = []; } // Enhance results with model-specific metadata (without individual recommendations) const enhancedResults = results.map((item: any) => { // Base item without created_at and updated_at return { id: item.id, name: item.name, description: item.description, model: item.model, collection_name: item.collection_name, database_id: item.database_id || null, table_id: item.table_id || null, archived: item.archived || false, }; }); // Group results by model type for better organization const resultsByModel = enhancedResults.reduce((acc: any, item: any) => { if (!acc[item.model]) { acc[item.model] = []; } acc[item.model].push(item); return acc; }, {}); const totalResults = enhancedResults.length; const searchMethod = ids && ids.length > 0 ? 'id_search' : databaseId && !searchQuery ? 'database_search' : 'query_search'; logInfo( `Search found ${totalResults} items across ${Object.keys(resultsByModel).length} model types in ${searchTime}ms` ); // Build standardized parameters object for response const usedParameters: any = { query: searchQuery || null, models: models, max_results: maxResults, }; // Add optional parameters that were actually used if (searchNativeQuery) { usedParameters.search_native_query = searchNativeQuery; } if (includeDashboardQuestions) { usedParameters.include_dashboard_questions = includeDashboardQuestions; } if (ids && ids.length > 0) { usedParameters.ids = ids; } if (archived === true) { usedParameters.archived = archived; } if (databaseId) { usedParameters.database_id = databaseId; } if (verified === true) { usedParameters.verified = verified; } // Generate recommended actions based on found models const foundModels = Object.keys(resultsByModel); const recommendedActions: { [key: string]: string } = {}; foundModels.forEach(model => { switch (model) { case 'card': recommendedActions[model] = 'Use retrieve(model="card", ids=[card_id]) to get the SQL query, then execute_query() with the database_id for reliable execution'; break; case 'dashboard': recommendedActions[model] = 'Use retrieve(model="dashboard", ids=[dashboard_id]) to get all cards in this dashboard and their details'; break; case 'table': recommendedActions[model] = 'Use retrieve(model="table", ids=[table_id]) to get detailed metadata including column information and relationships'; break; case 'database': recommendedActions[model] = 'Use retrieve(model="database", ids=[database_id]) to get database details including available tables'; break; case 'dataset': recommendedActions[model] = 'Use retrieve(model="card", ids=[dataset_id]) to get the dataset definition, then execute_query() to run it'; break; case 'collection': recommendedActions[model] = 'Use retrieve(model="collection", ids=[collection_id]) to get collection details and organizational structure for managing Metabase content like questions and dashboards'; break; case 'field': recommendedActions[model] = 'Use retrieve(model="field", ids=[field_id]) to get detailed field metadata including data types and constraints'; break; case 'segment': recommendedActions[model] = 'Use retrieve(model="card", ids=[segment_id]) to get the segment definition and apply it in your queries'; break; case 'metric': recommendedActions[model] = 'Use retrieve(model="card", ids=[metric_id]) to get the metric definition and incorporate it into your analysis'; break; default: recommendedActions[model] = 'Use the appropriate retrieve() command with the model type and ID to get detailed information'; } }); return { content: [ { type: 'text', text: formatJson({ search_metrics: { method: searchMethod, total_results: totalResults, search_time_ms: searchTime, parameters_used: usedParameters, }, recommended_actions: recommendedActions, results_by_model: Object.keys(resultsByModel).map(model => ({ model, count: resultsByModel[model].length, })), results: enhancedResults, }), }, ], }; } catch (error: any) { throw handleApiError( error, { operation: 'Search', resourceType: databaseId ? 'database' : undefined, resourceId: databaseId, }, logError ); } }
  • The input schema definition for the 'search' tool, including all parameters like query, models, max_results, etc., with detailed descriptions, types, defaults, and restrictions.
    inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search across names, descriptions, and metadata.', }, models: { type: 'array', items: { type: 'string', enum: [ 'card', 'dashboard', 'table', 'dataset', 'segment', 'collection', 'database', 'action', 'indexed-entity', 'metric', ], }, description: 'Model types to search (default: ["card", "dashboard"]). RESTRICTION: "database" model cannot be mixed with others and must be used exclusively.', default: ['card', 'dashboard'], }, max_results: { type: 'number', description: 'Maximum number of results to return (default: 20, max: 50)', minimum: 1, maximum: 50, default: 20, }, search_native_query: { type: 'boolean', description: 'Search within SQL query content of cards (default: false). RESTRICTION: Only works when models=["card"] exclusively.', default: false, }, include_dashboard_questions: { type: 'boolean', description: 'Include questions within dashboards in results (default: false). RESTRICTION: Only works when "dashboard" is included in models.', default: false, }, ids: { type: 'array', items: { type: 'number' }, description: 'Search for specific IDs. RESTRICTIONS: Only works with single model type, cannot be used with "table" or "database" models.', }, archived: { type: 'boolean', description: 'Search archived items only (default: false)', }, database_id: { type: 'number', description: 'Search items from specific database ID. RESTRICTION: Cannot be used when searching for databases (models=["database"]).', }, verified: { type: 'boolean', description: 'Search verified items only (requires premium features)', }, }, required: [], },
  • src/server.ts:504-515 (registration)
    Registration of the 'search' tool handler in the CallToolRequestSchema switch statement, mapping tool name to handleSearch function call.
    case 'search': return safeCall(() => handleSearch( request, requestId, this.apiClient, this.logDebug.bind(this), this.logInfo.bind(this), this.logWarn.bind(this), this.logError.bind(this) ) );
  • Barrel export of handleSearch from src/handlers/search.ts (via .js alias) for easy import in server.ts.
    export { handleSearch } from './search.js';
  • src/server.ts:22-22 (registration)
    Import of handleSearch from handlers/index.ts in server.ts.
    handleSearch,

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