Agregar servidor MCP Metabase
Some checks failed
build-and-deploy / build-and-deploy (push) Failing after 9s

- Implementado mcp-metabase-server con TypeScript
- 9 herramientas para interactuar con Metabase API
- Soporta listar/buscar cards, ejecutar queries con parámetros
- Soporta crear y actualizar cards
- Autenticación con API Key
- Agregado servicio al docker-compose.yml
- Configurado en Traefik sin autenticación Authentik
- Actualizado README con documentación completa
- Variables y secrets configurados en Gitea
This commit is contained in:
2025-10-28 10:49:18 -06:00
parent b57ac8de83
commit 5c6fd8fef3
9 changed files with 658 additions and 0 deletions

View File

@@ -68,3 +68,9 @@ REGISTRY_PASSWORD=mi-password-secreto
GIT_URL=https://gitea.ejemplo.com
GIT_DOMAIN=gitea.ejemplo.com
GIT_TOKEN=token-de-gitea-aqui
# Servidor MCP para Metabase API
# El servidor estará disponible en: METABASE_DOMAIN/mcp (sin autenticación)
METABASE_DOMAIN=metabase.ejemplo.com
METABASE_INTERNAL_URL=http://metabase:3000
METABASE_API_KEY=mb_xxxxxxxxxxxxxxxxxxxxxxxxx

View File

@@ -21,12 +21,18 @@ claude mcp add --transport http nucleodocs-gitea https://gitea.nucleoriofrio.com
claude mcp add chrome-devtools npx -- chrome-devtools-mcp@latest --isolated=true
```
### MCP Metabase Server
```bash
claude mcp add --transport http nucleodocs-metabase https://metabase.nucleoriofrio.com/mcp
```
### Scopes Disponibles
**Para compartir con el equipo** (crea .mcp.json en el proyecto):
```bash
claude mcp add --transport http nucleodocs-docker --scope project https://docker.nucleoriofrio.com/mcp
claude mcp add --transport http nucleodocs-gitea --scope project https://gitea.nucleoriofrio.com/mcp
claude mcp add --transport http nucleodocs-metabase --scope project https://metabase.nucleoriofrio.com/mcp
claude mcp add chrome-devtools --scope project npx -- chrome-devtools-mcp@latest --isolated=true
```
@@ -34,6 +40,7 @@ claude mcp add chrome-devtools --scope project npx -- chrome-devtools-mcp@latest
```bash
claude mcp add --transport http nucleodocs-docker --scope user https://docker.nucleoriofrio.com/mcp
claude mcp add --transport http nucleodocs-gitea --scope user https://gitea.nucleoriofrio.com/mcp
claude mcp add --transport http nucleodocs-metabase --scope user https://metabase.nucleoriofrio.com/mcp
claude mcp add chrome-devtools --scope user npx -- chrome-devtools-mcp@latest --isolated=true
```
@@ -45,6 +52,7 @@ claude mcp list
# Ver detalles de un servidor específico
claude mcp get nucleodocs-docker
claude mcp get nucleodocs-gitea
claude mcp get nucleodocs-metabase
claude mcp get chrome-devtools
# Dentro de Claude Code, verificar el estado
@@ -78,6 +86,7 @@ Este repositorio contiene la documentación del funcionamiento del sistema Nucle
- ✅ Claude Code hooks para monitoreo de Actions
- ✅ MCP Server Docker para gestión de contenedores
- ✅ MCP Server Gitea para API de Gitea
- ✅ MCP Server Metabase para análisis y reportes
- ✅ MCP Server Chrome DevTools para testing e interacción con navegador
## Servicios
@@ -115,6 +124,28 @@ Servidor MCP personalizado construido con TypeScript que expone la API de Gitea
- `POST /mcp` - Protocolo MCP para operaciones Gitea
- `GET /health` - Health check del servicio
### MCP Metabase Server (`mcp-metabase`)
Servidor MCP personalizado construido con TypeScript que expone la API de Metabase a través del protocolo MCP sobre HTTP. Este servicio:
- Implementa 9 herramientas para interactuar con Metabase
- Usa el puerto 3000 para comunicación HTTP
- Se conecta a Metabase mediante URL interna y autenticación con API Key
- Se ejecuta en las redes `principal` y `traefik-network`
- **Expuesto públicamente en `metabase.nucleoriofrio.com/mcp` SIN autenticación Authentik**
- Prioridad 200 en Traefik para evitar conflictos con otros routers
- Herramientas disponibles:
- `metabase_cards` - Listar y buscar cards/questions
- `metabase_card_info` - Obtener detalles de una card
- `metabase_execute_card` - Ejecutar cards con parámetros
- `metabase_create_card` - Crear nuevas cards/questions
- `metabase_update_card` - Actualizar nombre y descripción de cards
- `metabase_collections` - Listar colecciones
- `metabase_databases` - Listar bases de datos
- `metabase_dashboards` - Listar dashboards
- `metabase_dashboard_info` - Obtener detalles de un dashboard
- Endpoints disponibles:
- `POST /mcp` - Protocolo MCP para operaciones Metabase
- `GET /health` - Health check del servicio
## Ejemplos de Uso
Una vez agregados los servidores MCP, podrás usar las herramientas directamente en Claude Code:
@@ -133,6 +164,14 @@ Una vez agregados los servidores MCP, podrás usar las herramientas directamente
> Busca issues abiertos en el repositorio analiticaNucleo
```
**Para Metabase:**
```
> Lista todas las cards de Metabase
> Busca cards que contengan "ventas" en el nombre
> Ejecuta la card 123 con parámetros de fecha
> Crea una nueva card con una query SQL
```
**Para Chrome DevTools:**
```
> Abre https://docs.nucleoriofrio.com y toma un snapshot

View File

@@ -113,6 +113,37 @@ services:
- "traefik.http.middlewares.${APP_NAME}-mcp-gitea-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.routers.${APP_NAME}-mcp-gitea.middlewares=${APP_NAME}-mcp-gitea-stripprefix,${APP_NAME}-mcp-gitea-headers"
mcp-metabase:
image: ${REG}/${REPO_OWNER}/mcp-metabase-server:latest
container_name: ${APP_NAME}-mcp-metabase
restart: unless-stopped
environment:
- PORT=3000
- METABASE_URL=${METABASE_INTERNAL_URL}
- METABASE_API_KEY=${METABASE_API_KEY}
networks:
- principal
- traefik-network
labels:
# Traefik labels - Exposición sin autenticación
- "traefik.enable=true"
- "traefik.docker.network=traefik-network"
# Service
- "traefik.http.services.${APP_NAME}-mcp-metabase.loadbalancer.server.port=3000"
# Router sin autenticación para /mcp en metabase domain
- "traefik.http.routers.${APP_NAME}-mcp-metabase.rule=Host(`${METABASE_DOMAIN}`) && PathPrefix(`/mcp`)"
- "traefik.http.routers.${APP_NAME}-mcp-metabase.entrypoints=websecure"
- "traefik.http.routers.${APP_NAME}-mcp-metabase.tls.certresolver=letsencrypt"
- "traefik.http.routers.${APP_NAME}-mcp-metabase.priority=200"
- "traefik.http.routers.${APP_NAME}-mcp-metabase.service=${APP_NAME}-mcp-metabase"
# Middlewares para MCP Metabase
- "traefik.http.middlewares.${APP_NAME}-mcp-metabase-stripprefix.stripprefix.prefixes=/mcp"
- "traefik.http.middlewares.${APP_NAME}-mcp-metabase-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.routers.${APP_NAME}-mcp-metabase.middlewares=${APP_NAME}-mcp-metabase-stripprefix,${APP_NAME}-mcp-metabase-headers"
networks:
principal:
external: true

6
mcp-metabase-server/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules
dist
npm-debug.log
.env
*.log
.DS_Store

View File

@@ -0,0 +1,22 @@
FROM node:20-alpine
WORKDIR /app
# Copiar archivos de configuración
COPY package*.json ./
COPY tsconfig.json ./
# Instalar dependencias
RUN npm ci
# Copiar código fuente
COPY src ./src
# Compilar TypeScript
RUN npm run build
# Exponer puerto
EXPOSE 3000
# Iniciar servidor
CMD ["npm", "start"]

View File

@@ -0,0 +1,155 @@
# MCP Metabase Server
Servidor MCP (Model Context Protocol) para interactuar con Metabase API.
## Instalación Rápida
```bash
claude mcp add --transport http nucleodocs-metabase https://metabase.nucleoriofrio.com/mcp
```
## Características
Proporciona herramientas MCP para:
- **metabase_cards**: Listar y buscar cards/questions
- **metabase_card_info**: Obtener detalles de una card específica
- **metabase_execute_card**: Ejecutar cards con parámetros dinámicos
- **metabase_create_card**: Crear nuevas cards/questions
- **metabase_update_card**: Actualizar nombre y descripción de cards
- **metabase_collections**: Listar colecciones
- **metabase_databases**: Listar bases de datos (con opción de incluir metadata)
- **metabase_dashboards**: Listar dashboards
- **metabase_dashboard_info**: Obtener detalles de un dashboard
## Variables de Entorno
```bash
PORT=3000 # Puerto del servidor (default: 3000)
METABASE_URL=http://metabase:3000 # URL de Metabase
METABASE_API_KEY=mb_xxxxxxxxxxxxx # API Key de Metabase (requerida)
```
## Autenticación
Este servidor usa **API Keys** de Metabase para autenticación.
### Crear una API Key en Metabase:
1. Ve a Settings → Admin → API Keys
2. Crea una nueva API Key
3. Copia la key y configúrala como `METABASE_API_KEY`
## Desarrollo Local
```bash
# Instalar dependencias
npm install
# Desarrollo con hot reload
npm run dev
# Build
npm run build
# Producción
npm start
```
## Docker
```bash
# Build
docker build -t mcp-metabase-server .
# Run
docker run -p 3000:3000 \
-e METABASE_URL=http://metabase:3000 \
-e METABASE_API_KEY=mb_xxxxx \
mcp-metabase-server
```
## Uso con Claude Code
```bash
claude mcp add --transport http nucleodocs-metabase https://metabase.tudominio.com/mcp
```
## Ejemplos de Uso
### Listar todas las cards
```typescript
{
"action": "list"
}
```
### Buscar cards por nombre
```typescript
{
"action": "search",
"query": "ventas"
}
```
### Ejecutar card con parámetros
```typescript
{
"card_id": 123,
"parameters": [
{
"type": "date/single",
"target": ["variable", ["template-tag", "fecha_desde"]],
"value": "2025-01-01"
}
]
}
```
### Crear nueva card con SQL nativa
```typescript
{
"name": "Reporte de Ventas",
"description": "Ventas del último mes",
"dataset_query": {
"type": "native",
"database": 2,
"native": {
"query": "SELECT * FROM ventas WHERE fecha >= {{fecha_desde}}",
"template_tags": {
"fecha_desde": {
"type": "date",
"name": "fecha_desde",
"display_name": "Fecha Desde"
}
}
}
},
"display": "table",
"collection_id": 5
}
```
## API Metabase
Documentación: https://www.metabase.com/docs/latest/api
Endpoints implementados:
- `GET /api/card` - Listar cards
- `GET /api/card/:id` - Obtener card
- `POST /api/card/:id/query` - Ejecutar card
- `POST /api/card` - Crear card
- `PUT /api/card/:id` - Actualizar card
- `GET /api/collection` - Listar colecciones
- `GET /api/database` - Listar bases de datos
- `GET /api/database/:id/metadata` - Metadata de BD
- `GET /api/dashboard` - Listar dashboards
- `GET /api/dashboard/:id` - Obtener dashboard
## Licencia
MIT

View File

@@ -0,0 +1,30 @@
{
"name": "mcp-metabase-server",
"version": "1.0.0",
"description": "MCP server for Metabase API integration",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts"
},
"keywords": [
"mcp",
"metabase",
"api"
],
"author": "Nucleo Rio Frio",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
"express": "^4.21.2",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/node": "^22.10.5",
"typescript": "^5.7.2",
"tsx": "^4.19.2"
}
}

View File

@@ -0,0 +1,348 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
import { z } from 'zod';
// ==================== Configuración ====================
const METABASE_URL = process.env.METABASE_URL || 'http://metabase:3000';
const METABASE_API_KEY = process.env.METABASE_API_KEY || '';
const PORT = parseInt(process.env.PORT || '3000', 10);
if (!METABASE_API_KEY) {
console.error('ERROR: METABASE_API_KEY no está configurada');
process.exit(1);
}
// ==================== Cliente Metabase ====================
async function metabaseFetch<T = any>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${METABASE_URL}${endpoint}`;
const headers: HeadersInit = {
'Content-Type': 'application/json',
'X-API-KEY': METABASE_API_KEY,
...options.headers,
};
try {
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Metabase API error (${response.status}): ${errorText}`);
}
return await response.json();
} catch (error) {
console.error(`Error en petición a ${endpoint}:`, error);
throw error;
}
}
// ==================== Servidor MCP ====================
const server = new McpServer({
name: 'nucleodocs-metabase',
version: '1.0.0',
});
// ==================== Herramientas ====================
// 1. Listar/buscar cards
server.registerTool({
name: 'metabase_cards',
metadata: {
title: 'Listar o buscar Cards/Questions',
description: 'Lista todas las cards o busca cards por nombre/colección',
},
inputSchema: z.object({
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'),
}),
outputSchema: z.object({
cards: z.array(z.any()),
count: z.number(),
}),
handler: async (input) => {
const { action, query, collection_id } = input;
let endpoint = '/api/card';
const params = new URLSearchParams();
if (collection_id !== undefined) {
params.append('collection', collection_id.toString());
}
if (params.toString()) {
endpoint += `?${params.toString()}`;
}
const cards = await metabaseFetch<any[]>(endpoint);
let filteredCards = cards;
if (action === 'search' && query) {
const searchLower = query.toLowerCase();
filteredCards = cards.filter(card =>
card.name?.toLowerCase().includes(searchLower) ||
card.description?.toLowerCase().includes(searchLower)
);
}
return {
cards: filteredCards,
count: filteredCards.length,
};
},
});
// 2. Obtener detalles de una card
server.registerTool({
name: 'metabase_card_info',
metadata: {
title: 'Obtener detalles de una Card',
description: 'Obtiene información detallada de una card específica por su ID',
},
inputSchema: z.object({
card_id: z.number().describe('ID de la card'),
}),
outputSchema: z.any(),
handler: async (input) => {
const { card_id } = input;
const card = await metabaseFetch(`/api/card/${card_id}`);
return card;
},
});
// 3. Ejecutar card con parámetros
server.registerTool({
name: 'metabase_execute_card',
metadata: {
title: 'Ejecutar Card con parámetros',
description: 'Ejecuta una card/question existente, opcionalmente con parámetros',
},
inputSchema: z.object({
card_id: z.number().describe('ID de la card a ejecutar'),
parameters: z.array(z.object({
type: z.string().describe('Tipo de parámetro (ej: date/single, category, etc.)'),
target: z.any().describe('Target del parámetro'),
value: z.any().describe('Valor del parámetro'),
})).optional().describe('Parámetros para la query'),
}),
outputSchema: z.any(),
handler: async (input) => {
const { card_id, parameters } = input;
const body: any = {};
if (parameters && parameters.length > 0) {
body.parameters = parameters;
}
const result = await metabaseFetch(`/api/card/${card_id}/query`, {
method: 'POST',
body: JSON.stringify(body),
});
return result;
},
});
// 4. Crear nueva card
server.registerTool({
name: 'metabase_create_card',
metadata: {
title: 'Crear nueva Card',
description: 'Crea una nueva card/question en Metabase',
},
inputSchema: z.object({
name: z.string().describe('Nombre de la card'),
description: z.string().optional().describe('Descripción de la card'),
dataset_query: z.object({
type: z.enum(['native', 'query']).describe('Tipo de query'),
database: z.number().describe('ID de la base de datos'),
native: z.object({
query: z.string().describe('Query SQL'),
template_tags: z.record(z.any()).optional().describe('Tags de template para parámetros'),
}).optional().describe('Query nativa SQL (si type=native)'),
query: z.any().optional().describe('Query MBQL (si type=query)'),
}).describe('Configuración de la query'),
display: z.string().default('table').describe('Tipo de visualización (table, bar, line, etc.)'),
visualization_settings: z.record(z.any()).optional().describe('Configuración de visualización'),
collection_id: z.number().optional().describe('ID de la colección donde guardar la card'),
}),
outputSchema: z.any(),
handler: async (input) => {
const body: any = {
name: input.name,
dataset_query: input.dataset_query,
display: input.display,
visualization_settings: input.visualization_settings || {},
};
if (input.description) {
body.description = input.description;
}
if (input.collection_id !== undefined) {
body.collection_id = input.collection_id;
}
const result = await metabaseFetch('/api/card', {
method: 'POST',
body: JSON.stringify(body),
});
return result;
},
});
// 5. Actualizar card (nombre/descripción)
server.registerTool({
name: 'metabase_update_card',
metadata: {
title: 'Actualizar Card',
description: 'Actualiza el nombre y/o descripción de una card existente',
},
inputSchema: z.object({
card_id: z.number().describe('ID de la card a actualizar'),
name: z.string().optional().describe('Nuevo nombre de la card'),
description: z.string().optional().describe('Nueva descripción de la card'),
}),
outputSchema: z.any(),
handler: async (input) => {
const { card_id, name, description } = input;
if (!name && !description) {
throw new Error('Debe proporcionar al menos name o description para actualizar');
}
const body: any = {};
if (name) body.name = name;
if (description !== undefined) body.description = description;
const result = await metabaseFetch(`/api/card/${card_id}`, {
method: 'PUT',
body: JSON.stringify(body),
});
return result;
},
});
// 6. Listar colecciones
server.registerTool({
name: 'metabase_collections',
metadata: {
title: 'Listar Colecciones',
description: 'Lista todas las colecciones disponibles en Metabase',
},
inputSchema: z.object({}),
outputSchema: z.array(z.any()),
handler: async () => {
const collections = await metabaseFetch<any[]>('/api/collection');
return collections;
},
});
// 7. Listar bases de datos
server.registerTool({
name: 'metabase_databases',
metadata: {
title: 'Listar Bases de Datos',
description: 'Lista todas las bases de datos configuradas en Metabase',
},
inputSchema: z.object({
include_tables: z.boolean().optional().default(false).describe('Incluir metadatos de tablas y campos'),
}),
outputSchema: z.any(),
handler: async (input) => {
const { include_tables } = input;
const databases = await metabaseFetch<any[]>('/api/database');
if (include_tables) {
// Obtener metadata completa de cada base de datos
const databasesWithMetadata = await Promise.all(
databases.map(async (db) => {
try {
const metadata = await metabaseFetch(`/api/database/${db.id}/metadata`);
return { ...db, metadata };
} catch (error) {
console.error(`Error obteniendo metadata de DB ${db.id}:`, error);
return db;
}
})
);
return databasesWithMetadata;
}
return databases;
},
});
// 8. Listar dashboards
server.registerTool({
name: 'metabase_dashboards',
metadata: {
title: 'Listar Dashboards',
description: 'Lista todos los dashboards disponibles',
},
inputSchema: z.object({}),
outputSchema: z.array(z.any()),
handler: async () => {
const dashboards = await metabaseFetch<any[]>('/api/dashboard');
return dashboards;
},
});
// 9. Obtener detalles de un dashboard
server.registerTool({
name: 'metabase_dashboard_info',
metadata: {
title: 'Obtener detalles de Dashboard',
description: 'Obtiene información detallada de un dashboard específico incluyendo sus cards',
},
inputSchema: z.object({
dashboard_id: z.number().describe('ID del dashboard'),
}),
outputSchema: z.any(),
handler: async (input) => {
const { dashboard_id } = input;
const dashboard = await metabaseFetch(`/api/dashboard/${dashboard_id}`);
return dashboard;
},
});
// ==================== Servidor Express ====================
const app = express();
app.use(express.json());
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'ok',
server: 'nucleodocs-metabase',
metabase_url: METABASE_URL,
timestamp: new Date().toISOString()
});
});
// Endpoint MCP (Traefik hace StripPrefix de /mcp)
app.post('/', async (req, res) => {
const transport = new StreamableHTTPServerTransport({
endpoint: '/',
sessionIdGenerator: () => Math.random().toString(36).substring(7),
});
await transport.handleRequest(req, res, server);
});
// Iniciar servidor
app.listen(PORT, '0.0.0.0', () => {
console.log(`✅ MCP Metabase Server corriendo en puerto ${PORT}`);
console.log(`📊 Conectado a Metabase: ${METABASE_URL}`);
console.log(`🔑 Usando autenticación con API Key`);
});

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"moduleResolution": "node",
"rootDir": "./src",
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}