# Especificación Técnica: Parámetro `verbose` ## Objetivo Implementar un parámetro `verbose` en las herramientas del MCP Metabase para controlar el nivel de detalle en las respuestas, reduciendo el tamaño de las respuestas por defecto sin perder funcionalidad. ## Principios de Diseño 1. **Una herramienta, dos modos**: Cada herramienta debe soportar modo ligero (default) y modo completo (verbose) 2. **Default ligero**: Por defecto (`verbose: false`), devolver solo campos esenciales 3. **Verbose completo**: Con `verbose: true`, devolver todos los campos disponibles 4. **Backward compatible**: No romper implementaciones existentes 5. **Consistente**: Aplicar el mismo patrón en todas las herramientas ## Reducción de Tamaño Esperada | Herramienta | Sin verbose | Con verbose | Reducción | |-------------|-------------|-------------|-----------| | metabase_cards (10 items) | 117 KB | 12 KB | **~90%** | | metabase_databases | 2 KB | 1 KB | ~50% | | metabase_collections | 590 bytes | 300 bytes | ~50% | ## Implementación por Herramienta ### 1. `metabase_cards` **Schema actualizado:** ```typescript inputSchema: { action: z.enum(['list', 'search']).describe('Acción a realizar'), query: z.string().optional().describe('Término de búsqueda (para action=search)'), collection_id: z.number().optional().describe('ID de colección para filtrar'), verbose: z.boolean().optional().default(false).describe('Incluir todos los campos (default: false)'), page: z.number().optional().default(1).describe('Número de página'), pageSize: z.number().optional().default(20).describe('Items por página (max: 100)'), } ``` **Campos por modo:** ```typescript // Función helper para filtrar campos function filterCardFields(card: any, verbose: boolean) { if (verbose) { return card; // Devolver todo } // Modo ligero: solo campos esenciales return { id: card.id, name: card.name, description: card.description, collection_id: card.collection_id, collection: card.collection ? { id: card.collection.id, name: card.collection.name, } : null, database_id: card.database_id, query_type: card.query_type, display: card.display, type: card.type, created_at: card.created_at, updated_at: card.updated_at, last_used_at: card.last_used_at, view_count: card.view_count, archived: card.archived, }; } // En el handler const filteredCards = cards.map(card => filterCardFields(card, verbose)); ``` **Ejemplo de uso:** ```typescript // Listado ligero (default) { "action": "list", "pageSize": 50 } // Listado completo { "action": "list", "verbose": true, "pageSize": 20 } // Búsqueda ligera { "action": "search", "query": "ventas" } // Búsqueda con detalles completos (incluye creator) { "action": "search", "query": "ventas", "verbose": true } ``` ### 2. `metabase_databases` **Schema actualizado:** ```typescript inputSchema: { verbose: z.boolean().optional().default(false).describe('Incluir features y detalles técnicos'), include_tables: z.boolean().optional().default(false).describe('Incluir metadata de tablas'), } ``` **Campos por modo:** ```typescript function filterDatabaseFields(db: any, verbose: boolean) { if (verbose) { return db; // Devolver todo } // Modo ligero: sin features (que es un array muy grande) return { id: db.id, name: db.name, engine: db.engine, is_sample: db.is_sample, is_on_demand: db.is_on_demand, created_at: db.created_at, updated_at: db.updated_at, // Omitir: features (array de ~100 items), details, etc. }; } ``` **Corrección del bug `include_tables`:** ```typescript async ({ verbose, include_tables }) => { try { // FIX: La API devuelve { data: [...] } const response = await metabaseFetch('/api/database'); const databases = response.data || response; // Fallback if (include_tables) { const databasesWithMetadata = await Promise.all( databases.map(async (db) => { try { const metadata = await metabaseFetch(`/api/database/${db.id}/metadata`); const filtered = filterDatabaseFields(db, verbose); return { ...filtered, metadata }; } catch (error) { console.error(`Error obteniendo metadata de DB ${db.id}:`, error); return filterDatabaseFields(db, verbose); } }) ); return { content: [{ type: 'text', text: JSON.stringify(databasesWithMetadata, null, 2) }], structuredContent: { databases: databasesWithMetadata } }; } const filtered = databases.map(db => filterDatabaseFields(db, verbose)); return { content: [{ type: 'text', text: JSON.stringify(filtered, null, 2) }], structuredContent: { databases: filtered } }; } catch (error) { // ... } } ``` ### 3. `metabase_collections` **Schema actualizado:** ```typescript inputSchema: { verbose: z.boolean().optional().default(false).describe('Incluir permisos y metadata completa'), } ``` **Campos por modo:** ```typescript function filterCollectionFields(collection: any, verbose: boolean) { if (verbose) { return collection; // Devolver todo } // Modo ligero return { id: collection.id, name: collection.name, location: collection.location, description: collection.description, archived: collection.archived, personal_owner_id: collection.personal_owner_id, // Omitir: effective_ancestors, authority_level, etc. }; } ``` ### 4. Herramientas que NO necesitan `verbose` Estas herramientas ya devuelven información específica y no se beneficiarían del parámetro: - **`metabase_card_info`**: Ya devuelve info completa de UNA card (diseñado para detalle) - **`metabase_execute_card`**: Devuelve resultados de query (no metadatos) - **`metabase_update_card`**: Operación de escritura - **`metabase_create_card`**: Operación de escritura - **`metabase_dashboard_info`**: Ya devuelve info completa de UN dashboard - **`metabase_dashboards`**: Lista es pequeña, no justifica verbose ## Implementación de Paginación Agregar a `metabase_cards`: ```typescript inputSchema: { // ... otros campos page: z.number().optional().default(1).describe('Número de página (1-indexed)'), pageSize: z.number().optional().default(20).describe('Items por página (max: 100)'), } // En el handler async ({ action, query, collection_id, verbose, page = 1, pageSize = 20 }) => { // Validar límites const validPageSize = Math.min(Math.max(pageSize, 1), 100); const validPage = Math.max(page, 1); // Obtener cards let cards = await metabaseFetch(endpoint); // Aplicar filtros (search, collection) if (action === 'search' && query) { // ... } // Filtrar campos según verbose const filteredCards = cards.map(card => filterCardFields(card, verbose)); // Aplicar paginación const totalCards = filteredCards.length; const totalPages = Math.ceil(totalCards / validPageSize); const startIndex = (validPage - 1) * validPageSize; const endIndex = startIndex + validPageSize; const paginatedCards = filteredCards.slice(startIndex, endIndex); const result = { cards: paginatedCards, pagination: { page: validPage, pageSize: validPageSize, totalItems: totalCards, totalPages: totalPages, hasNextPage: validPage < totalPages, hasPreviousPage: validPage > 1, } }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ``` ## Actualización de outputSchema Actualizar los schemas de salida para reflejar los campos filtrados: ```typescript // metabase_cards con paginación outputSchema: { cards: z.array(z.any()), pagination: z.object({ page: z.number(), pageSize: z.number(), totalItems: z.number(), totalPages: z.number(), hasNextPage: z.boolean(), hasPreviousPage: z.boolean(), }), } ``` ## Testing Crear pruebas para validar: 1. **Default ligero**: Sin `verbose`, respuesta reducida 2. **Verbose completo**: Con `verbose: true`, respuesta completa 3. **Paginación**: Correcta división de resultados 4. **Límites**: pageSize máximo de 100 5. **Backward compatibility**: Llamadas sin `verbose` funcionan ```typescript // Agregar a test-tools.ts await runTest( 'metabase_cards_default', 'Listar cards en modo default (ligero)', { action: 'list', pageSize: 5 }, async () => { const cards = await metabaseFetch('/api/card'); const filtered = cards.slice(0, 5).map(card => filterCardFields(card, false)); return { cards: filtered, total: cards.length }; } ); await runTest( 'metabase_cards_verbose', 'Listar cards en modo verbose (completo)', { action: 'list', pageSize: 5, verbose: true }, async () => { const cards = await metabaseFetch('/api/card'); return { cards: cards.slice(0, 5), total: cards.length }; } ); ``` ## Migración ### Fase 1: Implementación 1. Crear funciones helper `filter*Fields()` 2. Actualizar schemas con parámetro `verbose` 3. Modificar handlers para usar filtros 4. Corregir bug de `databases` ### Fase 2: Testing 1. Ejecutar test-tools.ts actualizado 2. Validar tamaños de respuesta 3. Verificar campos en cada modo ### Fase 3: Documentación 1. Actualizar README.md 2. Agregar ejemplos de uso 3. Documentar campos por modo ### Fase 4: Despliegue 1. Build nueva imagen 2. Deploy a producción 3. Validar en Claude Code ## Métricas de Éxito - ✅ Reducción de >85% en tamaño de `metabase_cards` (list) - ✅ Paginación funcionando correctamente - ✅ Bug de `databases` corregido - ✅ Backward compatibility mantenida - ✅ Todas las pruebas pasando ## Notas de Implementación 1. **Preservar null values**: Si un campo es `null`, mantenerlo en modo ligero 2. **Nested objects**: En modo ligero, simplificar objetos anidados (ej: `collection`) 3. **Arrays grandes**: En modo ligero, omitir arrays grandes (`features`, `parameters`) 4. **IDs de relación**: Siempre incluir IDs para permitir navegación 5. **Timestamps**: Incluir timestamps importantes (`created_at`, `updated_at`) --- **Autor**: Análisis automático del MCP Metabase **Fecha**: 2025-10-28