All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 21s
Mejoras implementadas:
- Parámetro verbose en metabase_cards, metabase_databases, metabase_collections
- Paginación en metabase_cards (page, pageSize)
- Funciones helper para filtrado de campos (filterCardFields, filterDatabaseFields, filterCollectionFields)
- Corrección de bug en metabase_databases con include_tables (API devuelve {data: []} en lugar de array directo)
Resultados:
- Reducción 94.7% en tamaño de respuestas de cards (60KB → 3KB por 5 cards)
- Reducción 90.2% en tamaño de respuestas de databases (2KB → 198 bytes)
- 100% de pruebas exitosas (9/9)
Documentación:
- Agregado ANALISIS-RESULTADOS.md con análisis detallado
- Agregado SPEC-VERBOSE.md con especificación técnica
- Agregado README-TESTING.md con guía de pruebas
- Actualizado README.md con ejemplos de uso de verbose y paginación
- Actualizado .gitignore para excluir scripts de prueba con API keys
10 KiB
10 KiB
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
- Una herramienta, dos modos: Cada herramienta debe soportar modo ligero (default) y modo completo (verbose)
- Default ligero: Por defecto (
verbose: false), devolver solo campos esenciales - Verbose completo: Con
verbose: true, devolver todos los campos disponibles - Backward compatible: No romper implementaciones existentes
- 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:
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:
// 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:
// 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:
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:
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:
async ({ verbose, include_tables }) => {
try {
// FIX: La API devuelve { data: [...] }
const response = await metabaseFetch<any>('/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:
inputSchema: {
verbose: z.boolean().optional().default(false).describe('Incluir permisos y metadata completa'),
}
Campos por modo:
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 escriturametabase_create_card: Operación de escriturametabase_dashboard_info: Ya devuelve info completa de UN dashboardmetabase_dashboards: Lista es pequeña, no justifica verbose
Implementación de Paginación
Agregar a metabase_cards:
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<any[]>(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:
// 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:
- Default ligero: Sin
verbose, respuesta reducida - Verbose completo: Con
verbose: true, respuesta completa - Paginación: Correcta división de resultados
- Límites: pageSize máximo de 100
- Backward compatibility: Llamadas sin
verbosefuncionan
// 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<any[]>('/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<any[]>('/api/card');
return { cards: cards.slice(0, 5), total: cards.length };
}
);
Migración
Fase 1: Implementación
- Crear funciones helper
filter*Fields() - Actualizar schemas con parámetro
verbose - Modificar handlers para usar filtros
- Corregir bug de
databases
Fase 2: Testing
- Ejecutar test-tools.ts actualizado
- Validar tamaños de respuesta
- Verificar campos en cada modo
Fase 3: Documentación
- Actualizar README.md
- Agregar ejemplos de uso
- Documentar campos por modo
Fase 4: Despliegue
- Build nueva imagen
- Deploy a producción
- 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
databasescorregido - ✅ Backward compatibility mantenida
- ✅ Todas las pruebas pasando
Notas de Implementación
- Preservar null values: Si un campo es
null, mantenerlo en modo ligero - Nested objects: En modo ligero, simplificar objetos anidados (ej:
collection) - Arrays grandes: En modo ligero, omitir arrays grandes (
features,parameters) - IDs de relación: Siempre incluir IDs para permitir navegación
- Timestamps: Incluir timestamps importantes (
created_at,updated_at)
Autor: Análisis automático del MCP Metabase Fecha: 2025-10-28