Files
nucleoDocs/mcp-metabase-server/SPEC-VERBOSE.md
josedario87 4cbaee3fbd
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 21s
Implementar parámetro verbose y paginación en MCP Metabase
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
2025-10-28 16:42:12 -06:00

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

  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:

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 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:

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:

  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
// 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

  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