Corregir implementación MCP Metabase con API correcta
Some checks failed
build-and-deploy / build-and-deploy (push) Failing after 12s
Some checks failed
build-and-deploy / build-and-deploy (push) Failing after 12s
- Usar registerTool con 3 argumentos (nombre, metadata, handler)
- InputSchema y outputSchema como objetos planos con propiedades Zod
- Handlers retornan {content, structuredContent} o {content, isError}
- Transport sin endpoint, usar sessionIdGenerator: undefined
- Corregir tipo HeadersInit a Record<string, string>
- Soluciona errores de TypeScript en el build
This commit is contained in:
@@ -20,12 +20,15 @@ async function metabaseFetch<T = any>(
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const url = `${METABASE_URL}${endpoint}`;
|
const url = `${METABASE_URL}${endpoint}`;
|
||||||
|
|
||||||
const headers: HeadersInit = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-API-KEY': METABASE_API_KEY,
|
'X-API-KEY': METABASE_API_KEY,
|
||||||
...options.headers,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (options.headers) {
|
||||||
|
Object.assign(headers, options.headers);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
@@ -53,268 +56,362 @@ const server = new McpServer({
|
|||||||
// ==================== Herramientas ====================
|
// ==================== Herramientas ====================
|
||||||
|
|
||||||
// 1. Listar/buscar cards
|
// 1. Listar/buscar cards
|
||||||
server.registerTool({
|
server.registerTool(
|
||||||
name: 'metabase_cards',
|
'metabase_cards',
|
||||||
metadata: {
|
{
|
||||||
title: 'Listar o buscar Cards/Questions',
|
title: 'Listar o buscar Cards/Questions',
|
||||||
description: 'Lista todas las cards o busca cards por nombre/colección',
|
description: 'Lista todas las cards o busca cards por nombre/colección',
|
||||||
|
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'),
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
cards: z.array(z.any()),
|
||||||
|
count: z.number(),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
inputSchema: z.object({
|
async ({ action, query, collection_id }) => {
|
||||||
action: z.enum(['list', 'search']).describe('Acción a realizar'),
|
try {
|
||||||
query: z.string().optional().describe('Término de búsqueda (para action=search)'),
|
let endpoint = '/api/card';
|
||||||
collection_id: z.number().optional().describe('ID de colección para filtrar'),
|
const params = new URLSearchParams();
|
||||||
}),
|
|
||||||
outputSchema: z.object({
|
|
||||||
cards: z.array(z.any()),
|
|
||||||
count: z.number(),
|
|
||||||
}),
|
|
||||||
handler: async (input) => {
|
|
||||||
const { action, query, collection_id } = input;
|
|
||||||
|
|
||||||
let endpoint = '/api/card';
|
if (collection_id !== undefined) {
|
||||||
const params = new URLSearchParams();
|
params.append('collection', collection_id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
if (collection_id !== undefined) {
|
if (params.toString()) {
|
||||||
params.append('collection', collection_id.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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
cards: filteredCards,
|
||||||
|
count: filteredCards.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
||||||
|
structuredContent: result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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
|
// 2. Obtener detalles de una card
|
||||||
server.registerTool({
|
server.registerTool(
|
||||||
name: 'metabase_card_info',
|
'metabase_card_info',
|
||||||
metadata: {
|
{
|
||||||
title: 'Obtener detalles de una Card',
|
title: 'Obtener detalles de una Card',
|
||||||
description: 'Obtiene información detallada de una card específica por su ID',
|
description: 'Obtiene información detallada de una card específica por su ID',
|
||||||
|
inputSchema: {
|
||||||
|
card_id: z.number().describe('ID de la card'),
|
||||||
|
},
|
||||||
|
outputSchema: z.any()
|
||||||
},
|
},
|
||||||
inputSchema: z.object({
|
async ({ card_id }) => {
|
||||||
card_id: z.number().describe('ID de la card'),
|
try {
|
||||||
}),
|
const card = await metabaseFetch(`/api/card/${card_id}`);
|
||||||
outputSchema: z.any(),
|
return {
|
||||||
handler: async (input) => {
|
content: [{ type: 'text', text: JSON.stringify(card, null, 2) }],
|
||||||
const { card_id } = input;
|
structuredContent: card
|
||||||
const card = await metabaseFetch(`/api/card/${card_id}`);
|
};
|
||||||
return card;
|
} catch (error) {
|
||||||
},
|
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
|
||||||
});
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 3. Ejecutar card con parámetros
|
// 3. Ejecutar card con parámetros
|
||||||
server.registerTool({
|
server.registerTool(
|
||||||
name: 'metabase_execute_card',
|
'metabase_execute_card',
|
||||||
metadata: {
|
{
|
||||||
title: 'Ejecutar Card con parámetros',
|
title: 'Ejecutar Card con parámetros',
|
||||||
description: 'Ejecuta una card/question existente, opcionalmente con parámetros',
|
description: 'Ejecuta una card/question existente, opcionalmente con parámetros',
|
||||||
|
inputSchema: {
|
||||||
|
card_id: z.number().describe('ID de la card a ejecutar'),
|
||||||
|
parameters: z.array(z.object({
|
||||||
|
type: z.string().describe('Tipo de parámetro'),
|
||||||
|
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()
|
||||||
},
|
},
|
||||||
inputSchema: z.object({
|
async ({ card_id, parameters }) => {
|
||||||
card_id: z.number().describe('ID de la card a ejecutar'),
|
try {
|
||||||
parameters: z.array(z.object({
|
const body: any = {};
|
||||||
type: z.string().describe('Tipo de parámetro (ej: date/single, category, etc.)'),
|
if (parameters && parameters.length > 0) {
|
||||||
target: z.any().describe('Target del parámetro'),
|
body.parameters = parameters;
|
||||||
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 = {};
|
const result = await metabaseFetch(`/api/card/${card_id}/query`, {
|
||||||
if (parameters && parameters.length > 0) {
|
method: 'POST',
|
||||||
body.parameters = parameters;
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
||||||
|
structuredContent: result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const result = await metabaseFetch(`/api/card/${card_id}/query`, {
|
);
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Crear nueva card
|
// 4. Crear nueva card
|
||||||
server.registerTool({
|
server.registerTool(
|
||||||
name: 'metabase_create_card',
|
'metabase_create_card',
|
||||||
metadata: {
|
{
|
||||||
title: 'Crear nueva Card',
|
title: 'Crear nueva Card',
|
||||||
description: 'Crea una nueva card/question en Metabase',
|
description: 'Crea una nueva card/question en Metabase',
|
||||||
|
inputSchema: {
|
||||||
|
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'),
|
||||||
|
}).optional(),
|
||||||
|
query: z.any().optional(),
|
||||||
|
}).describe('Configuración de la query'),
|
||||||
|
display: z.string().default('table').describe('Tipo de visualización'),
|
||||||
|
visualization_settings: z.record(z.any()).optional().describe('Configuración de visualización'),
|
||||||
|
collection_id: z.number().optional().describe('ID de la colección'),
|
||||||
|
},
|
||||||
|
outputSchema: z.any()
|
||||||
},
|
},
|
||||||
inputSchema: z.object({
|
async ({ name, description, dataset_query, display, visualization_settings, collection_id }) => {
|
||||||
name: z.string().describe('Nombre de la card'),
|
try {
|
||||||
description: z.string().optional().describe('Descripción de la card'),
|
const body: any = {
|
||||||
dataset_query: z.object({
|
name,
|
||||||
type: z.enum(['native', 'query']).describe('Tipo de query'),
|
dataset_query,
|
||||||
database: z.number().describe('ID de la base de datos'),
|
display: display || 'table',
|
||||||
native: z.object({
|
visualization_settings: visualization_settings || {},
|
||||||
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) {
|
if (description) {
|
||||||
body.description = input.description;
|
body.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collection_id !== undefined) {
|
||||||
|
body.collection_id = collection_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await metabaseFetch('/api/card', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
||||||
|
structuredContent: result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (input.collection_id !== undefined) {
|
// 5. Actualizar card
|
||||||
body.collection_id = input.collection_id;
|
server.registerTool(
|
||||||
}
|
'metabase_update_card',
|
||||||
|
{
|
||||||
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',
|
title: 'Actualizar Card',
|
||||||
description: 'Actualiza el nombre y/o descripción de una card existente',
|
description: 'Actualiza el nombre y/o descripción de una card existente',
|
||||||
|
inputSchema: {
|
||||||
|
card_id: z.number().describe('ID de la card a actualizar'),
|
||||||
|
name: z.string().optional().describe('Nuevo nombre'),
|
||||||
|
description: z.string().optional().describe('Nueva descripción'),
|
||||||
|
},
|
||||||
|
outputSchema: z.any()
|
||||||
},
|
},
|
||||||
inputSchema: z.object({
|
async ({ card_id, name, description }) => {
|
||||||
card_id: z.number().describe('ID de la card a actualizar'),
|
try {
|
||||||
name: z.string().optional().describe('Nuevo nombre de la card'),
|
if (!name && !description) {
|
||||||
description: z.string().optional().describe('Nueva descripción de la card'),
|
throw new Error('Debe proporcionar al menos name o description');
|
||||||
}),
|
}
|
||||||
outputSchema: z.any(),
|
|
||||||
handler: async (input) => {
|
|
||||||
const { card_id, name, description } = input;
|
|
||||||
|
|
||||||
if (!name && !description) {
|
const body: any = {};
|
||||||
throw new Error('Debe proporcionar al menos name o description para actualizar');
|
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 {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
||||||
|
structuredContent: result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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
|
// 6. Listar colecciones
|
||||||
server.registerTool({
|
server.registerTool(
|
||||||
name: 'metabase_collections',
|
'metabase_collections',
|
||||||
metadata: {
|
{
|
||||||
title: 'Listar Colecciones',
|
title: 'Listar Colecciones',
|
||||||
description: 'Lista todas las colecciones disponibles en Metabase',
|
description: 'Lista todas las colecciones disponibles en Metabase',
|
||||||
|
inputSchema: {},
|
||||||
|
outputSchema: z.array(z.any())
|
||||||
},
|
},
|
||||||
inputSchema: z.object({}),
|
async () => {
|
||||||
outputSchema: z.array(z.any()),
|
try {
|
||||||
handler: async () => {
|
const collections = await metabaseFetch<any[]>('/api/collection');
|
||||||
const collections = await metabaseFetch<any[]>('/api/collection');
|
return {
|
||||||
return collections;
|
content: [{ type: 'text', text: JSON.stringify(collections, null, 2) }],
|
||||||
},
|
structuredContent: collections
|
||||||
});
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 7. Listar bases de datos
|
// 7. Listar bases de datos
|
||||||
server.registerTool({
|
server.registerTool(
|
||||||
name: 'metabase_databases',
|
'metabase_databases',
|
||||||
metadata: {
|
{
|
||||||
title: 'Listar Bases de Datos',
|
title: 'Listar Bases de Datos',
|
||||||
description: 'Lista todas las bases de datos configuradas en Metabase',
|
description: 'Lista todas las bases de datos configuradas en Metabase',
|
||||||
|
inputSchema: {
|
||||||
|
include_tables: z.boolean().optional().default(false).describe('Incluir metadata de tablas'),
|
||||||
|
},
|
||||||
|
outputSchema: z.any()
|
||||||
},
|
},
|
||||||
inputSchema: z.object({
|
async ({ include_tables }) => {
|
||||||
include_tables: z.boolean().optional().default(false).describe('Incluir metadatos de tablas y campos'),
|
try {
|
||||||
}),
|
const databases = await metabaseFetch<any[]>('/api/database');
|
||||||
outputSchema: z.any(),
|
|
||||||
handler: async (input) => {
|
|
||||||
const { include_tables } = input;
|
|
||||||
const databases = await metabaseFetch<any[]>('/api/database');
|
|
||||||
|
|
||||||
if (include_tables) {
|
if (include_tables) {
|
||||||
// Obtener metadata completa de cada base de datos
|
const databasesWithMetadata = await Promise.all(
|
||||||
const databasesWithMetadata = await Promise.all(
|
databases.map(async (db) => {
|
||||||
databases.map(async (db) => {
|
try {
|
||||||
try {
|
const metadata = await metabaseFetch(`/api/database/${db.id}/metadata`);
|
||||||
const metadata = await metabaseFetch(`/api/database/${db.id}/metadata`);
|
return { ...db, metadata };
|
||||||
return { ...db, metadata };
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(`Error obteniendo metadata de DB ${db.id}:`, error);
|
||||||
console.error(`Error obteniendo metadata de DB ${db.id}:`, error);
|
return db;
|
||||||
return db;
|
}
|
||||||
}
|
})
|
||||||
})
|
);
|
||||||
);
|
return {
|
||||||
return databasesWithMetadata;
|
content: [{ type: 'text', text: JSON.stringify(databasesWithMetadata, null, 2) }],
|
||||||
|
structuredContent: databasesWithMetadata
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(databases, null, 2) }],
|
||||||
|
structuredContent: databases
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return databases;
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 8. Listar dashboards
|
// 8. Listar dashboards
|
||||||
server.registerTool({
|
server.registerTool(
|
||||||
name: 'metabase_dashboards',
|
'metabase_dashboards',
|
||||||
metadata: {
|
{
|
||||||
title: 'Listar Dashboards',
|
title: 'Listar Dashboards',
|
||||||
description: 'Lista todos los dashboards disponibles',
|
description: 'Lista todos los dashboards disponibles',
|
||||||
|
inputSchema: {},
|
||||||
|
outputSchema: z.array(z.any())
|
||||||
},
|
},
|
||||||
inputSchema: z.object({}),
|
async () => {
|
||||||
outputSchema: z.array(z.any()),
|
try {
|
||||||
handler: async () => {
|
const dashboards = await metabaseFetch<any[]>('/api/dashboard');
|
||||||
const dashboards = await metabaseFetch<any[]>('/api/dashboard');
|
return {
|
||||||
return dashboards;
|
content: [{ type: 'text', text: JSON.stringify(dashboards, null, 2) }],
|
||||||
},
|
structuredContent: dashboards
|
||||||
});
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 9. Obtener detalles de un dashboard
|
// 9. Obtener detalles de un dashboard
|
||||||
server.registerTool({
|
server.registerTool(
|
||||||
name: 'metabase_dashboard_info',
|
'metabase_dashboard_info',
|
||||||
metadata: {
|
{
|
||||||
title: 'Obtener detalles de Dashboard',
|
title: 'Obtener detalles de Dashboard',
|
||||||
description: 'Obtiene información detallada de un dashboard específico incluyendo sus cards',
|
description: 'Obtiene información detallada de un dashboard específico incluyendo sus cards',
|
||||||
|
inputSchema: {
|
||||||
|
dashboard_id: z.number().describe('ID del dashboard'),
|
||||||
|
},
|
||||||
|
outputSchema: z.any()
|
||||||
},
|
},
|
||||||
inputSchema: z.object({
|
async ({ dashboard_id }) => {
|
||||||
dashboard_id: z.number().describe('ID del dashboard'),
|
try {
|
||||||
}),
|
const dashboard = await metabaseFetch(`/api/dashboard/${dashboard_id}`);
|
||||||
outputSchema: z.any(),
|
return {
|
||||||
handler: async (input) => {
|
content: [{ type: 'text', text: JSON.stringify(dashboard, null, 2) }],
|
||||||
const { dashboard_id } = input;
|
structuredContent: dashboard
|
||||||
const dashboard = await metabaseFetch(`/api/dashboard/${dashboard_id}`);
|
};
|
||||||
return dashboard;
|
} catch (error) {
|
||||||
},
|
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
|
||||||
});
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// ==================== Servidor Express ====================
|
// ==================== Servidor Express ====================
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -332,12 +429,31 @@ app.get('/health', (req, res) => {
|
|||||||
|
|
||||||
// Endpoint MCP (Traefik hace StripPrefix de /mcp)
|
// Endpoint MCP (Traefik hace StripPrefix de /mcp)
|
||||||
app.post('/', async (req, res) => {
|
app.post('/', async (req, res) => {
|
||||||
const transport = new StreamableHTTPServerTransport({
|
try {
|
||||||
endpoint: '/',
|
const transport = new StreamableHTTPServerTransport({
|
||||||
sessionIdGenerator: () => Math.random().toString(36).substring(7),
|
sessionIdGenerator: undefined,
|
||||||
});
|
enableJsonResponse: true
|
||||||
|
});
|
||||||
|
|
||||||
await transport.handleRequest(req, res, server);
|
res.on('close', () => {
|
||||||
|
transport.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.connect(transport);
|
||||||
|
await transport.handleRequest(req, res, req.body);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling MCP request:', error);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: {
|
||||||
|
code: -32603,
|
||||||
|
message: 'Internal server error'
|
||||||
|
},
|
||||||
|
id: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Iniciar servidor
|
// Iniciar servidor
|
||||||
|
|||||||
Reference in New Issue
Block a user