Skip to main content
Glama
client.js12.1 kB
import axios from 'axios'; import { logger } from '../utils/logger.js'; export class MetabaseClient { constructor(config) { this.baseURL = config.url; this.username = config.username; this.password = config.password; this.sessionToken = null; this.client = axios.create({ baseURL: this.baseURL, headers: { 'Content-Type': 'application/json' } }); } async authenticate() { try { const response = await this.client.post('/api/session', { username: this.username, password: this.password }); this.sessionToken = response.data.id; this.client.defaults.headers['X-Metabase-Session'] = this.sessionToken; logger.info('Successfully authenticated with Metabase'); return true; } catch (error) { logger.error('Authentication failed:', error.message); throw new Error('Failed to authenticate with Metabase'); } } // Database Operations async getDatabases() { await this.ensureAuthenticated(); const response = await this.client.get('/api/database'); return response.data; } async getDatabase(id) { await this.ensureAuthenticated(); const response = await this.client.get(`/api/database/${id}`); return response.data; } async getDatabaseConnectionInfo(id) { await this.ensureAuthenticated(); // Önce gerçek credentials'ları MetabaseappDB'den al try { const realCredentials = await this.getRealCredentials(id); if (realCredentials) { return realCredentials; } } catch (error) { logger.warn('Could not get real credentials, using API response:', error.message); } // Fallback: Normal API response const response = await this.client.get(`/api/database/${id}`); const db = response.data; return { id: db.id, name: db.name, engine: db.engine, host: db.details?.host, port: db.details?.port, dbname: db.details?.dbname || db.details?.db, user: db.details?.user, password: db.details?.password, ssl: db.details?.ssl, additional_options: db.details?.['additional-options'], tunnel_enabled: db.details?.['tunnel-enabled'], connection_string: this.buildConnectionString(db) }; } async getRealCredentials(databaseId) { const query = ` SELECT name, engine, details FROM metabase_database WHERE id = ${databaseId} `; const result = await this.executeNativeQuery(6, query, { enforcePrefix: false }); // MetabaseappDB if (result.data.rows.length > 0) { const [name, engine, details] = result.data.rows[0]; const detailsObj = JSON.parse(details); return { id: databaseId, name: name, engine: engine, host: detailsObj.host, port: detailsObj.port, dbname: detailsObj.dbname, user: detailsObj.user, password: detailsObj.password, ssl: detailsObj.ssl || false, additional_options: detailsObj['additional-options'], tunnel_enabled: detailsObj['tunnel-enabled'] || false }; } return null; } buildConnectionString(db) { const details = db.details; switch (db.engine) { case 'postgres': return `postgresql://${details.user}:${details.password}@${details.host}:${details.port}/${details.dbname}`; case 'mysql': return `mysql://${details.user}:${details.password}@${details.host}:${details.port}/${details.dbname}`; case 'h2': return details.db; default: return null; } } async getDatabaseSchemas(databaseId) { await this.ensureAuthenticated(); const response = await this.client.get(`/api/database/${databaseId}/schemas`); return response.data; } async getDatabaseTables(databaseId) { await this.ensureAuthenticated(); const response = await this.client.get(`/api/database/${databaseId}/metadata`); return response.data.tables; } // Model Operations async getCollections() { await this.ensureAuthenticated(); const response = await this.client.get('/api/collection'); return response.data; } async createCollection(name, description, parentId = null) { await this.ensureAuthenticated(); const response = await this.client.post('/api/collection', { name, description, parent_id: parentId, color: '#509EE3' }); return response.data; } async getModels() { await this.ensureAuthenticated(); const response = await this.client.get('/api/card', { params: { f: 'model' } }); return response.data; } async createModel(model) { await this.ensureAuthenticated(); const response = await this.client.post('/api/card', { ...model, type: 'model', display: 'table' }); return response.data; } // Question Operations async getQuestions(collectionId = null) { await this.ensureAuthenticated(); const params = collectionId ? { collection_id: collectionId } : {}; const response = await this.client.get('/api/card', { params }); return response.data; } async createQuestion(question) { await this.ensureAuthenticated(); const response = await this.client.post('/api/card', { ...question, display: question.display || 'table', visualization_settings: question.visualization_settings || {} }); return response.data; } async updateQuestion(id, updates) { await this.ensureAuthenticated(); const response = await this.client.put(`/api/card/${id}`, updates); return response.data; } async runQuery(query) { await this.ensureAuthenticated(); const response = await this.client.post('/api/dataset', query); return response.data; } // SQL Operations async executeNativeQuery(databaseId, sql, options = {}) { await this.ensureAuthenticated(); // Güvenlik kontrolü - DDL operasyonları için prefix zorunluluğu if (options.enforcePrefix !== false && this.isDDLOperation(sql)) { this.validateDDLPrefix(sql); } // DDL operations için farklı endpoint kullan if (this.isDDLOperation(sql)) { return await this.executeDDLOperation(databaseId, sql); } const query = { database: databaseId, type: 'native', native: { query: sql } }; return await this.runQuery(query); } async executeDDLOperation(databaseId, sql) { try { // DDL için action endpoint kullan const response = await this.client.post('/api/action/execute', { database_id: databaseId, sql: sql, type: 'query' }); return { status: 'success', message: 'DDL operation completed', data: { rows: [], cols: [] }, sql: sql }; } catch (error) { // Eğer action endpoint çalışmazsa normal endpoint dene try { const query = { database: databaseId, type: 'native', native: { query: sql } }; await this.runQuery(query); return { status: 'success', message: 'DDL operation completed via dataset endpoint', data: { rows: [], cols: [] }, sql: sql }; } catch (secondError) { logger.warn('DDL execution warning:', secondError.message); // DDL işlemi başarılı olmuş olabilir, kontrol et if (secondError.message.includes('Select statement did not produce a ResultSet')) { return { status: 'success', message: 'DDL operation likely completed (ResultSet warning is normal)', data: { rows: [], cols: [] }, sql: sql, warning: secondError.message }; } throw secondError; } } } isDDLOperation(sql) { const upperSQL = sql.toUpperCase().trim(); return upperSQL.startsWith('CREATE TABLE') || upperSQL.startsWith('CREATE VIEW') || upperSQL.startsWith('CREATE MATERIALIZED VIEW') || upperSQL.startsWith('CREATE INDEX') || upperSQL.startsWith('DROP TABLE') || upperSQL.startsWith('DROP VIEW') || upperSQL.startsWith('DROP MATERIALIZED VIEW') || upperSQL.startsWith('DROP INDEX'); } validateDDLPrefix(sql) { const upperSQL = sql.toUpperCase(); // CREATE operations için prefix kontrolü if (upperSQL.includes('CREATE TABLE') || upperSQL.includes('CREATE VIEW') || upperSQL.includes('CREATE MATERIALIZED VIEW') || upperSQL.includes('CREATE INDEX')) { if (!sql.toLowerCase().includes('claude_ai_')) { throw new Error('DDL operations must use claude_ai_ prefix for object names'); } } // DROP operations için sadece prefix'li objelere izin if (upperSQL.includes('DROP TABLE') || upperSQL.includes('DROP VIEW') || upperSQL.includes('DROP MATERIALIZED VIEW') || upperSQL.includes('DROP INDEX')) { if (!sql.toLowerCase().includes('claude_ai_')) { throw new Error('Can only drop objects with claude_ai_ prefix'); } } } async createSQLQuestion(name, description, databaseId, sql, collectionId) { const question = { name, description, database_id: databaseId, collection_id: collectionId, dataset_query: { database: databaseId, type: 'native', native: { query: sql } } }; return await this.createQuestion(question); } // Metric Operations async getMetrics() { await this.ensureAuthenticated(); const response = await this.client.get('/api/metric'); return response.data; } async createMetric(metric) { await this.ensureAuthenticated(); const response = await this.client.post('/api/metric', metric); return response.data; } async updateMetric(id, updates) { await this.ensureAuthenticated(); const response = await this.client.put(`/api/metric/${id}`, updates); return response.data; } // Dashboard Operations async getDashboards() { await this.ensureAuthenticated(); const response = await this.client.get('/api/dashboard'); return response.data; } async createDashboard(dashboard) { await this.ensureAuthenticated(); const response = await this.client.post('/api/dashboard', { name: dashboard.name, description: dashboard.description, collection_id: dashboard.collection_id }); return response.data; } async addCardToDashboard(dashboardId, cardId, options = {}) { await this.ensureAuthenticated(); const response = await this.client.post('/api/dashboard/:id/cards', { cardId, sizeX: options.sizeX || 4, sizeY: options.sizeY || 4, row: options.row || 0, col: options.col || 0, parameter_mappings: options.parameter_mappings || [] }); return response.data; } async updateDashboard(id, updates) { await this.ensureAuthenticated(); const response = await this.client.put(`/api/dashboard/${id}`, updates); return response.data; } // Segment Operations async getSegments(tableId) { await this.ensureAuthenticated(); const response = await this.client.get('/api/segment', { params: { table_id: tableId } }); return response.data; } async createSegment(segment) { await this.ensureAuthenticated(); const response = await this.client.post('/api/segment', segment); return response.data; } // Helper Methods async ensureAuthenticated() { if (!this.sessionToken) { await this.authenticate(); } } async testConnection() { try { await this.authenticate(); const databases = await this.getDatabases(); logger.info(`Connected to Metabase. Found ${databases.length} databases.`); return true; } catch (error) { logger.error('Connection test failed:', error.message); return false; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/enessari/metabase-ai-assistant'

If you have feedback or need assistance with the MCP directory API, please join our Discord server