Agregar servidor MCP Metabase
Some checks failed
build-and-deploy / build-and-deploy (push) Failing after 9s
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:
@@ -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
|
||||
|
||||
39
README.md
39
README.md
@@ -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
|
||||
|
||||
@@ -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
6
mcp-metabase-server/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
npm-debug.log
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
22
mcp-metabase-server/Dockerfile
Normal file
22
mcp-metabase-server/Dockerfile
Normal 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"]
|
||||
155
mcp-metabase-server/README.md
Normal file
155
mcp-metabase-server/README.md
Normal 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
|
||||
30
mcp-metabase-server/package.json
Normal file
30
mcp-metabase-server/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
348
mcp-metabase-server/src/index.ts
Normal file
348
mcp-metabase-server/src/index.ts
Normal 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`);
|
||||
});
|
||||
21
mcp-metabase-server/tsconfig.json
Normal file
21
mcp-metabase-server/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user