Implementar parámetro verbose y paginación en MCP Metabase
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 21s
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
This commit is contained in:
364
mcp-metabase-server/SPEC-VERBOSE.md
Normal file
364
mcp-metabase-server/SPEC-VERBOSE.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# 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<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:**
|
||||
```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<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:
|
||||
|
||||
```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<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
|
||||
Reference in New Issue
Block a user