metabase-ai-assistant 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metabase-ai-assistant",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "mcpName": "io.github.enessari/metabase-ai-assistant",
5
5
  "description": "The most powerful MCP Server for Metabase - 134 tools with structured output, AI-powered SQL generation, dashboard automation, user management & enterprise BI. MCP SDK v1.26.0 compliant. Works with Claude, Cursor, and any MCP-compatible AI.",
6
6
  "main": "src/index.js",
@@ -81,6 +81,7 @@
81
81
  "chalk": "^5.3.0",
82
82
  "dotenv": "^16.3.1",
83
83
  "express": "^4.18.2",
84
+ "he": "^1.2.0",
84
85
  "inquirer": "^12.9.3",
85
86
  "jsonwebtoken": "^9.0.3",
86
87
  "mysql2": "^3.14.3",
@@ -88,8 +89,7 @@
88
89
  "ora": "^8.0.1",
89
90
  "pg": "^8.16.3",
90
91
  "winston": "^3.11.0",
91
- "zod": "^3.22.4",
92
- "he": "^1.2.0"
92
+ "zod": "^3.22.4"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@types/node": "^20.11.5",
package/src/mcp/server.js CHANGED
@@ -53,7 +53,7 @@ class MetabaseMCPServer {
53
53
  this.server = new Server(
54
54
  {
55
55
  name: 'metabase-ai-assistant',
56
- version: '4.1.0',
56
+ version: '4.2.0',
57
57
  description: 'AI-powered database operations, SQL queries, metrics, and dashboard automation for Metabase. 134 tools with structured output for enterprise BI.',
58
58
  },
59
59
  {
@@ -121,25 +121,20 @@ class MetabaseMCPServer {
121
121
  });
122
122
  logger.info('Activity logger initialized');
123
123
 
124
- // Metadata client (optional)
125
- if (process.env.MB_METADATA_ENABLED === 'true') {
126
- this.metadataClient = new MetabaseMetadataClient({
127
- engine: process.env.MB_METADATA_ENGINE || 'postgres',
128
- host: process.env.MB_METADATA_HOST,
129
- port: parseInt(process.env.MB_METADATA_PORT) || 5432,
130
- database: process.env.MB_METADATA_DATABASE,
131
- user: process.env.MB_METADATA_USER,
132
- password: process.env.MB_METADATA_PASSWORD,
133
- ssl: process.env.MB_METADATA_SSL === 'true'
134
- });
135
-
124
+ // Metadata client (optional - uses Metabase API, no direct DB connection needed)
125
+ if (process.env.MB_METADATA_ENABLED === 'true' && appConfig.METABASE_INTERNAL_DB_ID) {
136
126
  try {
137
- await this.metadataClient.connect();
138
- logger.info('Metabase metadata client initialized');
127
+ this.metadataClient = new MetabaseMetadataClient({
128
+ metabaseClient: this.metabaseClient,
129
+ internalDbId: appConfig.METABASE_INTERNAL_DB_ID
130
+ });
131
+ logger.info(`Metabase metadata client initialized (DB ID: ${appConfig.METABASE_INTERNAL_DB_ID})`);
139
132
  } catch (error) {
140
- logger.warn('Metadata client connection failed:', error.message);
133
+ logger.warn('Metadata client initialization failed:', error.message);
141
134
  this.metadataClient = null;
142
135
  }
136
+ } else if (process.env.MB_METADATA_ENABLED === 'true' && !appConfig.METABASE_INTERNAL_DB_ID) {
137
+ logger.warn('MB_METADATA_ENABLED=true but METABASE_INTERNAL_DB_ID is not set. Use meta_find_internal_db tool to find it.');
143
138
  }
144
139
 
145
140
  // Modular handlers (initialized after all deps are ready)
@@ -1,112 +1,40 @@
1
- import { Client } from 'pg';
2
- import mysql from 'mysql2/promise';
3
1
  import { logger } from '../utils/logger.js';
4
2
 
5
3
  /**
6
4
  * Metabase Metadata Database Client
7
5
  *
8
- * Connects to Metabase's application database to query metadata, analytics,
9
- * and usage statistics. This client is READ-ONLY for security.
6
+ * Queries Metabase's application database via the Metabase API
7
+ * using executeNativeQuery. This eliminates the need for direct
8
+ * database credentials — only METABASE_INTERNAL_DB_ID is required.
9
+ *
10
+ * This client is READ-ONLY for security.
10
11
  *
11
12
  * Based on: Metabase Internal Database Reference Guide (ONMARTECH LLC)
12
13
  */
13
14
  export class MetabaseMetadataClient {
14
15
  constructor(config) {
16
+ this.metabaseClient = config.metabaseClient;
17
+ this.internalDbId = config.internalDbId;
15
18
  this.config = {
16
- engine: config.engine || 'postgres',
17
- host: config.host,
18
- port: config.port,
19
- database: config.database,
20
- user: config.user,
21
- password: config.password,
22
- ssl: config.ssl || false
19
+ engine: 'api',
20
+ database: `internal-db-${config.internalDbId}`
23
21
  };
24
- this.client = null;
25
- this.connected = false;
26
- }
27
-
28
- /**
29
- * Connect to Metabase application database
30
- */
31
- async connect() {
32
- try {
33
- if (this.connected) {
34
- return true;
35
- }
36
-
37
- switch (this.config.engine) {
38
- case 'postgres':
39
- this.client = new Client({
40
- host: this.config.host,
41
- port: this.config.port || 5432,
42
- database: this.config.database,
43
- user: this.config.user,
44
- password: this.config.password,
45
- ssl: this.config.ssl,
46
- // Read-only configuration
47
- options: '-c default_transaction_read_only=on'
48
- });
49
- await this.client.connect();
50
- break;
51
-
52
- case 'mysql':
53
- this.client = await mysql.createConnection({
54
- host: this.config.host,
55
- port: this.config.port || 3306,
56
- database: this.config.database,
57
- user: this.config.user,
58
- password: this.config.password,
59
- ssl: this.config.ssl
60
- });
61
- // Set read-only mode for MySQL
62
- await this.client.query('SET SESSION TRANSACTION READ ONLY');
63
- break;
64
-
65
- case 'h2':
66
- logger.warn('H2 database detected - limited support. Consider using PostgreSQL for production.');
67
- throw new Error('H2 database not supported for metadata access. Please use PostgreSQL or MySQL.');
68
-
69
- default:
70
- throw new Error(`Unsupported database engine: ${this.config.engine}`);
71
- }
72
22
 
73
- this.connected = true;
74
- logger.info(`Connected to Metabase metadata database (${this.config.engine}): ${this.config.database}`);
75
- return true;
76
- } catch (error) {
77
- logger.error('Metabase metadata database connection failed:', error);
78
- throw error;
23
+ if (!this.metabaseClient) {
24
+ throw new Error('MetabaseMetadataClient requires a metabaseClient instance');
79
25
  }
80
- }
81
-
82
- /**
83
- * Disconnect from database
84
- */
85
- async disconnect() {
86
- if (this.client) {
87
- try {
88
- if (this.config.engine === 'postgres') {
89
- await this.client.end();
90
- } else if (this.config.engine === 'mysql') {
91
- await this.client.end();
92
- }
93
- this.connected = false;
94
- this.client = null;
95
- logger.info('Metabase metadata database connection closed');
96
- } catch (error) {
97
- logger.error('Error disconnecting from metadata database:', error);
98
- }
26
+ if (!this.internalDbId) {
27
+ throw new Error('MetabaseMetadataClient requires internalDbId (METABASE_INTERNAL_DB_ID)');
99
28
  }
29
+
30
+ logger.info(`MetabaseMetadataClient initialized (API mode, DB ID: ${this.internalDbId})`);
100
31
  }
101
32
 
102
33
  /**
103
- * Execute read-only query
34
+ * Execute read-only query via Metabase API
35
+ * Returns rows as array of objects (like pg client.query().rows)
104
36
  */
105
37
  async executeQuery(sql, params = []) {
106
- if (!this.connected) {
107
- await this.connect();
108
- }
109
-
110
38
  try {
111
39
  // Security check - only SELECT queries allowed
112
40
  const sqlUpper = sql.trim().toUpperCase();
@@ -114,18 +42,26 @@ export class MetabaseMetadataClient {
114
42
  throw new Error('Only SELECT queries are allowed on metadata database');
115
43
  }
116
44
 
117
- logger.debug('Executing metadata query:', { sql: sql.substring(0, 100) });
45
+ logger.debug('Executing metadata query via API:', { sql: sql.substring(0, 100) });
118
46
 
119
- let result;
120
- if (this.config.engine === 'postgres') {
121
- result = await this.client.query(sql, params);
122
- return result.rows;
123
- } else if (this.config.engine === 'mysql') {
124
- const [rows] = await this.client.query(sql, params);
125
- return rows;
126
- }
47
+ const result = await this.metabaseClient.executeNativeQuery(this.internalDbId, sql);
48
+
49
+ // Convert Metabase API response format to row-object format
50
+ // API returns: { data: { rows: [[v1,v2,...], ...], cols: [{name:'col1'}, ...] } }
51
+ // We need: [{ col1: v1, col2: v2, ... }, ...]
52
+ const cols = result.data?.cols || [];
53
+ const rows = result.data?.rows || [];
54
+ const colNames = cols.map(c => c.name);
55
+
56
+ return rows.map(row => {
57
+ const obj = {};
58
+ colNames.forEach((name, i) => {
59
+ obj[name] = row[i];
60
+ });
61
+ return obj;
62
+ });
127
63
  } catch (error) {
128
- logger.error('Metadata query execution failed:', { sql, error: error.message });
64
+ logger.error('Metadata query execution failed:', { sql: sql.substring(0, 200), error: error.message });
129
65
  throw error;
130
66
  }
131
67
  }
@@ -573,17 +509,16 @@ export class MetabaseMetadataClient {
573
509
  // ============================================
574
510
 
575
511
  /**
576
- * Test connection to metadata database
512
+ * Test connection to metadata database via API
577
513
  */
578
514
  async testConnection() {
579
515
  try {
580
- await this.connect();
581
516
  const result = await this.executeQuery('SELECT COUNT(*) as table_count FROM information_schema.tables WHERE table_schema = \'public\'');
582
- await this.disconnect();
583
517
  return {
584
518
  success: true,
585
519
  database: this.config.database,
586
520
  engine: this.config.engine,
521
+ internal_db_id: this.internalDbId,
587
522
  table_count: result[0]?.table_count || 0
588
523
  };
589
524
  } catch (error) {
@@ -913,7 +848,7 @@ export class MetabaseMetadataClient {
913
848
  ...dependencies,
914
849
  impact_analysis: {
915
850
  severity: criticalQuestions.length > 0 || criticalDashboards.length > 0 ? 'HIGH' :
916
- dependencies.questions.length > 0 ? 'MEDIUM' : 'LOW',
851
+ dependencies.questions.length > 0 ? 'MEDIUM' : 'LOW',
917
852
  breaking_changes: {
918
853
  questions_will_break: dependencies.questions.length,
919
854
  dashboards_will_break: dependencies.dashboards.length,
@@ -1294,12 +1229,12 @@ export class MetabaseMetadataClient {
1294
1229
  error_count: parseInt(q.error_count),
1295
1230
  error_rate: parseFloat(q.error_rate),
1296
1231
  severity: parseFloat(q.error_rate) > 50 ? 'CRITICAL' :
1297
- parseFloat(q.error_rate) > 20 ? 'HIGH' : 'MEDIUM',
1232
+ parseFloat(q.error_rate) > 20 ? 'HIGH' : 'MEDIUM',
1298
1233
  recommendation: parseFloat(q.error_rate) > 50
1299
1234
  ? 'CRITICAL: More than half of executions fail. Archive or fix immediately.'
1300
1235
  : parseFloat(q.error_rate) > 20
1301
- ? 'HIGH: Frequent failures. Prioritize fixing this question.'
1302
- : 'MEDIUM: Occasional failures. Monitor and investigate.'
1236
+ ? 'HIGH: Frequent failures. Prioritize fixing this question.'
1237
+ : 'MEDIUM: Occasional failures. Monitor and investigate.'
1303
1238
  }));
1304
1239
  }
1305
1240
 
@@ -1549,7 +1484,7 @@ export class MetabaseMetadataClient {
1549
1484
  impact.recommendations.push('✓ Set approved: true to execute import');
1550
1485
 
1551
1486
  impact.severity = impact.conflicts.length > 10 ? 'HIGH' :
1552
- impact.conflicts.length > 0 ? 'MEDIUM' : 'LOW';
1487
+ impact.conflicts.length > 0 ? 'MEDIUM' : 'LOW';
1553
1488
 
1554
1489
  return impact;
1555
1490
  } catch (error) {
@@ -42,16 +42,6 @@ const envSchema = z.object({
42
42
  .string()
43
43
  .default('false')
44
44
  .transform(val => val.toLowerCase() === 'true'),
45
- MB_METADATA_ENGINE: z.enum(['postgres', 'mysql', 'h2']).default('postgres'),
46
- MB_METADATA_HOST: z.string().optional(),
47
- MB_METADATA_PORT: z.string().optional().transform(val => val ? parseInt(val, 10) : undefined),
48
- MB_METADATA_DATABASE: z.string().optional(),
49
- MB_METADATA_USER: z.string().optional(),
50
- MB_METADATA_PASSWORD: z.string().optional(),
51
- MB_METADATA_SSL: z
52
- .string()
53
- .default('false')
54
- .transform(val => val.toLowerCase() === 'true'),
55
45
 
56
46
  // AI Configuration
57
47
  ANTHROPIC_API_KEY: z.string().optional(),