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_URL=https://gitea.ejemplo.com
|
||||||
GIT_DOMAIN=gitea.ejemplo.com
|
GIT_DOMAIN=gitea.ejemplo.com
|
||||||
GIT_TOKEN=token-de-gitea-aqui
|
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
|
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
|
### Scopes Disponibles
|
||||||
|
|
||||||
**Para compartir con el equipo** (crea .mcp.json en el proyecto):
|
**Para compartir con el equipo** (crea .mcp.json en el proyecto):
|
||||||
```bash
|
```bash
|
||||||
claude mcp add --transport http nucleodocs-docker --scope project https://docker.nucleoriofrio.com/mcp
|
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-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
|
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
|
```bash
|
||||||
claude mcp add --transport http nucleodocs-docker --scope user https://docker.nucleoriofrio.com/mcp
|
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-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
|
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
|
# Ver detalles de un servidor específico
|
||||||
claude mcp get nucleodocs-docker
|
claude mcp get nucleodocs-docker
|
||||||
claude mcp get nucleodocs-gitea
|
claude mcp get nucleodocs-gitea
|
||||||
|
claude mcp get nucleodocs-metabase
|
||||||
claude mcp get chrome-devtools
|
claude mcp get chrome-devtools
|
||||||
|
|
||||||
# Dentro de Claude Code, verificar el estado
|
# 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
|
- ✅ Claude Code hooks para monitoreo de Actions
|
||||||
- ✅ MCP Server Docker para gestión de contenedores
|
- ✅ MCP Server Docker para gestión de contenedores
|
||||||
- ✅ MCP Server Gitea para API de Gitea
|
- ✅ 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
|
- ✅ MCP Server Chrome DevTools para testing e interacción con navegador
|
||||||
|
|
||||||
## Servicios
|
## Servicios
|
||||||
@@ -115,6 +124,28 @@ Servidor MCP personalizado construido con TypeScript que expone la API de Gitea
|
|||||||
- `POST /mcp` - Protocolo MCP para operaciones Gitea
|
- `POST /mcp` - Protocolo MCP para operaciones Gitea
|
||||||
- `GET /health` - Health check del servicio
|
- `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
|
## Ejemplos de Uso
|
||||||
|
|
||||||
Una vez agregados los servidores MCP, podrás usar las herramientas directamente en Claude Code:
|
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
|
> 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:**
|
**Para Chrome DevTools:**
|
||||||
```
|
```
|
||||||
> Abre https://docs.nucleoriofrio.com y toma un snapshot
|
> 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.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"
|
- "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:
|
networks:
|
||||||
principal:
|
principal:
|
||||||
external: true
|
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