@onmartech/metabase-ai-assistant 4.0.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.
Files changed (40) hide show
  1. package/.env.example +38 -0
  2. package/LICENSE +201 -0
  3. package/README.md +364 -0
  4. package/README_MCP.md +279 -0
  5. package/package.json +99 -0
  6. package/src/ai/assistant.js +982 -0
  7. package/src/cli/interactive.js +500 -0
  8. package/src/database/connection-manager.js +350 -0
  9. package/src/database/direct-client.js +686 -0
  10. package/src/index.js +162 -0
  11. package/src/mcp/handlers/actions.js +213 -0
  12. package/src/mcp/handlers/ai.js +207 -0
  13. package/src/mcp/handlers/analytics.js +1647 -0
  14. package/src/mcp/handlers/cards.js +1544 -0
  15. package/src/mcp/handlers/collections.js +244 -0
  16. package/src/mcp/handlers/dashboard.js +207 -0
  17. package/src/mcp/handlers/dashboard_direct.js +292 -0
  18. package/src/mcp/handlers/database.js +322 -0
  19. package/src/mcp/handlers/docs.js +399 -0
  20. package/src/mcp/handlers/index.js +35 -0
  21. package/src/mcp/handlers/metadata.js +190 -0
  22. package/src/mcp/handlers/questions.js +134 -0
  23. package/src/mcp/handlers/schema.js +1699 -0
  24. package/src/mcp/handlers/sql.js +559 -0
  25. package/src/mcp/handlers/users.js +251 -0
  26. package/src/mcp/job-store.js +199 -0
  27. package/src/mcp/server.js +428 -0
  28. package/src/mcp/tool-registry.js +3244 -0
  29. package/src/mcp/tool-router.js +149 -0
  30. package/src/metabase/client.js +737 -0
  31. package/src/metabase/metadata-client.js +1852 -0
  32. package/src/utils/activity-logger.js +489 -0
  33. package/src/utils/cache.js +176 -0
  34. package/src/utils/config.js +131 -0
  35. package/src/utils/definition-tables.js +938 -0
  36. package/src/utils/file-operations.js +496 -0
  37. package/src/utils/logger.js +45 -0
  38. package/src/utils/parametric-questions.js +627 -0
  39. package/src/utils/response-optimizer.js +190 -0
  40. package/src/utils/sql-sanitizer.js +97 -0
@@ -0,0 +1,244 @@
1
+ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
2
+ import { logger } from '../../utils/logger.js';
3
+
4
+ export class CollectionsHandler {
5
+ constructor(metabaseClient) {
6
+ this.metabaseClient = metabaseClient;
7
+ }
8
+
9
+ routes() {
10
+ return {
11
+ 'mb_collection_create': (args) => this.handleCollectionCreate(args),
12
+ 'mb_collection_list': (args) => this.handleCollectionList(args),
13
+ 'mb_collection_move': (args) => this.handleCollectionMove(args),
14
+ 'mb_collection_copy': (args) => this.handleCollectionCopy(args),
15
+ 'mb_collection_permissions_get': (args) => this.handleCollectionPermissionsGet(args),
16
+ 'mb_collection_permissions_update': (args) => this.handleCollectionPermissionsUpdate(args),
17
+ };
18
+ }
19
+
20
+ async handleCollectionCreate(args) {
21
+ try {
22
+ await this.ensureInitialized();
23
+
24
+ const collectionData = {
25
+ name: args.name,
26
+ description: args.description || '',
27
+ parent_id: args.parent_id || null,
28
+ color: args.color || '#509EE3'
29
+ };
30
+
31
+ const collection = await this.metabaseClient.request('POST', '/api/collection', collectionData);
32
+
33
+ return {
34
+ content: [{
35
+ type: 'text',
36
+ text: `✅ **Collection Created!**\\n\\n` +
37
+ `🆔 Collection ID: ${collection.id}\\n` +
38
+ `📁 Name: ${collection.name}\\n` +
39
+ `📝 Description: ${collection.description || 'None'}\\n` +
40
+ `🎨 Color: ${collection.color}\\n` +
41
+ `📂 Parent: ${args.parent_id || 'Root'}`
42
+ }]
43
+ };
44
+
45
+ } catch (error) {
46
+ // Better error messages for common issues
47
+ let userMessage = error.message;
48
+
49
+ if (error.message.includes('already exists') || error.response?.status === 409) {
50
+ userMessage = `Collection already exists with this name: "${args.name}"`;
51
+ } else if (error.message.includes('permission') || error.response?.status === 403) {
52
+ userMessage = `Permission denied. Contact admin for collection creation access.`;
53
+ } else if (error.message.includes('parent') || (error.message.includes('not found') && args.parent_id)) {
54
+ userMessage = `Parent collection not found: ID ${args.parent_id}`;
55
+ }
56
+
57
+ return {
58
+ content: [{ type: 'text', text: `❌ Collection creation failed: ${userMessage}` }]
59
+ };
60
+ }
61
+ }
62
+
63
+ async handleCollectionList(args) {
64
+ try {
65
+ await this.ensureInitialized();
66
+
67
+ let endpoint = '/api/collection';
68
+ if (args.parent_id) {
69
+ endpoint = `/api/collection/${args.parent_id}/items`;
70
+ }
71
+
72
+ const collections = await this.metabaseClient.request('GET', '/api/collection');
73
+
74
+ let output = `📂 **Collections**\\n\\n`;
75
+
76
+ const rootCollections = collections.filter(c => !c.personal_owner_id);
77
+ rootCollections.slice(0, 20).forEach((col, i) => {
78
+ output += `${i + 1}. **${col.name}** (ID: ${col.id})\\n`;
79
+ if (col.description) output += ` ${col.description.substring(0, 50)}...\\n`;
80
+ });
81
+
82
+ output += `\\n📊 Total Collections: ${collections.length}`;
83
+
84
+ return {
85
+ content: [{ type: 'text', text: output }]
86
+ };
87
+
88
+ } catch (error) {
89
+ return {
90
+ content: [{ type: 'text', text: `❌ Collection list failed: ${error.message}` }]
91
+ };
92
+ }
93
+ }
94
+
95
+ async handleCollectionMove(args) {
96
+ try {
97
+ await this.ensureInitialized();
98
+
99
+ let endpoint;
100
+ const updateData = { collection_id: args.target_collection_id };
101
+
102
+ switch (args.item_type) {
103
+ case 'card':
104
+ endpoint = `/api/card/${args.item_id}`;
105
+ break;
106
+ case 'dashboard':
107
+ endpoint = `/api/dashboard/${args.item_id}`;
108
+ break;
109
+ case 'collection':
110
+ endpoint = `/api/collection/${args.item_id}`;
111
+ updateData.parent_id = args.target_collection_id;
112
+ delete updateData.collection_id;
113
+ break;
114
+ default:
115
+ throw new Error(`Unknown item type: ${args.item_type}`);
116
+ }
117
+
118
+ await this.metabaseClient.request('PUT', endpoint, updateData);
119
+
120
+ return {
121
+ content: [{
122
+ type: 'text',
123
+ text: `✅ **Item Moved!**\\n\\n` +
124
+ `📦 Type: ${args.item_type}\\n` +
125
+ `🆔 Item ID: ${args.item_id}\\n` +
126
+ `📂 Target Collection: ${args.target_collection_id || 'Root'}`
127
+ }]
128
+ };
129
+
130
+ } catch (error) {
131
+ return {
132
+ content: [{ type: 'text', text: `❌ Move failed: ${error.message}` }]
133
+ };
134
+ }
135
+ }
136
+
137
+ async handleCollectionPermissionsGet(args) {
138
+ await this.ensureInitialized();
139
+ const { collection_id } = args;
140
+
141
+ try {
142
+ const graph = await this.metabaseClient.request('GET', '/api/collection/graph');
143
+ const collectionPerms = graph.groups;
144
+
145
+ const permissions = [];
146
+ for (const [groupId, perms] of Object.entries(collectionPerms)) {
147
+ const collPerm = perms[collection_id];
148
+ if (collPerm) {
149
+ permissions.push({ group_id: groupId, permission: collPerm });
150
+ }
151
+ }
152
+
153
+ return {
154
+ content: [{
155
+ type: 'text',
156
+ text: `Collection ${collection_id} permissions:\n${permissions.map(p =>
157
+ ` - Group ${p.group_id}: ${p.permission}`
158
+ ).join('\n') || ' No specific permissions set'}`
159
+ }]
160
+ };
161
+ } catch (error) {
162
+ return { content: [{ type: 'text', text: `❌ Collection permissions get error: ${error.message}` }] };
163
+ }
164
+ }
165
+
166
+ async handleCollectionPermissionsUpdate(args) {
167
+ await this.ensureInitialized();
168
+ const { collection_id, group_id, permission } = args;
169
+
170
+ try {
171
+ // Get current graph
172
+ const graph = await this.metabaseClient.request('GET', '/api/collection/graph');
173
+
174
+ // Update the permission
175
+ if (!graph.groups[group_id]) {
176
+ graph.groups[group_id] = {};
177
+ }
178
+ graph.groups[group_id][collection_id] = permission;
179
+
180
+ // Save the updated graph
181
+ await this.metabaseClient.request('PUT', '/api/collection/graph', graph);
182
+
183
+ return {
184
+ content: [{
185
+ type: 'text',
186
+ text: `✅ Collection ${collection_id} permission updated: Group ${group_id} = ${permission}`
187
+ }]
188
+ };
189
+ } catch (error) {
190
+ return { content: [{ type: 'text', text: `❌ Collection permissions update error: ${error.message}` }] };
191
+ }
192
+ }
193
+
194
+ async handleCollectionCopy(args) {
195
+ await this.ensureInitialized();
196
+ const { collection_id, destination_id, new_name } = args;
197
+
198
+ try {
199
+ // Get source collection
200
+ const sourceCollection = await this.metabaseClient.request('GET', `/api/collection/${collection_id}`);
201
+
202
+ // Create new collection
203
+ const newCollection = await this.metabaseClient.request('POST', '/api/collection', {
204
+ name: new_name || `Copy of ${sourceCollection.name}`,
205
+ description: sourceCollection.description,
206
+ parent_id: destination_id || sourceCollection.parent_id
207
+ });
208
+
209
+ // Get items in source collection
210
+ const items = await this.metabaseClient.request('GET', `/api/collection/${collection_id}/items`);
211
+ const allItems = items.data || items;
212
+
213
+ let copiedCards = 0;
214
+ let copiedDashboards = 0;
215
+
216
+ // Copy each item
217
+ for (const item of allItems) {
218
+ if (item.model === 'card') {
219
+ await this.handleCardCopy({
220
+ card_id: item.id,
221
+ collection_id: newCollection.id
222
+ });
223
+ copiedCards++;
224
+ } else if (item.model === 'dashboard') {
225
+ await this.handleDashboardCopy({
226
+ dashboard_id: item.id,
227
+ collection_id: newCollection.id,
228
+ deep_copy: false // Don't deep copy cards as they're already being copied
229
+ });
230
+ copiedDashboards++;
231
+ }
232
+ }
233
+
234
+ return {
235
+ content: [{
236
+ type: 'text',
237
+ text: `✅ Collection copied:\n New Collection ID: ${newCollection.id}\n Name: ${newCollection.name}\n Cards copied: ${copiedCards}\n Dashboards copied: ${copiedDashboards}`
238
+ }]
239
+ };
240
+ } catch (error) {
241
+ return { content: [{ type: 'text', text: `❌ Collection copy error: ${error.message}` }] };
242
+ }
243
+ }
244
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Dashboard Handler Module
3
+ * Handles dashboard creation, management, and visualization operations
4
+ */
5
+
6
+ import { logger } from '../../utils/logger.js';
7
+
8
+ /**
9
+ * Handle create dashboard request
10
+ * @param {object} args
11
+ * @param {object} context
12
+ * @returns {Promise<object>}
13
+ */
14
+ export async function handleCreateDashboard(args, context) {
15
+ const { metabaseClient } = context;
16
+
17
+ const dashboard = await metabaseClient.createDashboard({
18
+ name: args.name,
19
+ description: args.description,
20
+ collection_id: args.collection_id
21
+ });
22
+
23
+ return {
24
+ content: [
25
+ {
26
+ type: 'text',
27
+ text: `✅ **Dashboard Created!**\\n\\n` +
28
+ `• Name: ${dashboard.name}\\n` +
29
+ `• ID: ${dashboard.id}\\n` +
30
+ `• Collection: ${args.collection_id || 'Root'}`,
31
+ },
32
+ ],
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Handle get dashboards list request
38
+ * @param {object} context
39
+ * @returns {Promise<object>}
40
+ */
41
+ export async function handleGetDashboards(context) {
42
+ const { metabaseClient } = context;
43
+
44
+ const dashboards = await metabaseClient.getDashboards();
45
+
46
+ return {
47
+ content: [
48
+ {
49
+ type: 'text',
50
+ text: `📊 **Available Dashboards (${dashboards.length})**\\n\\n` +
51
+ dashboards.map(d =>
52
+ `• **${d.name}** (ID: ${d.id})\\n Collection: ${d.collection_id || 'Root'}`
53
+ ).join('\\n'),
54
+ },
55
+ ],
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Handle add card to dashboard request
61
+ * @param {object} args
62
+ * @param {object} context
63
+ * @returns {Promise<object>}
64
+ */
65
+ export async function handleAddCardToDashboard(args, context) {
66
+ const { metabaseClient } = context;
67
+
68
+ const position = args.position || {};
69
+
70
+ await metabaseClient.addCardToDashboard(args.dashboard_id, args.question_id, {
71
+ row: position.row || 0,
72
+ col: position.col || 0,
73
+ sizeX: position.sizeX || 6,
74
+ sizeY: position.sizeY || 4
75
+ });
76
+
77
+ return {
78
+ content: [
79
+ {
80
+ type: 'text',
81
+ text: `✅ **Card Added to Dashboard!**\\n\\n` +
82
+ `• Dashboard ID: ${args.dashboard_id}\\n` +
83
+ `• Question ID: ${args.question_id}\\n` +
84
+ `• Position: (${position.row || 0}, ${position.col || 0})\\n` +
85
+ `• Size: ${position.sizeX || 6}x${position.sizeY || 4}`,
86
+ },
87
+ ],
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Handle add dashboard filter request
93
+ * @param {object} args
94
+ * @param {object} context
95
+ * @returns {Promise<object>}
96
+ */
97
+ export async function handleAddDashboardFilter(args, context) {
98
+ const { metabaseClient } = context;
99
+
100
+ await metabaseClient.addDashboardFilter(args.dashboard_id, {
101
+ name: args.name,
102
+ type: args.type,
103
+ field_id: args.field_id,
104
+ default_value: args.default_value
105
+ });
106
+
107
+ return {
108
+ content: [
109
+ {
110
+ type: 'text',
111
+ text: `✅ **Filter Added!**\\n\\n` +
112
+ `• Dashboard ID: ${args.dashboard_id}\\n` +
113
+ `• Filter Name: ${args.name}\\n` +
114
+ `• Type: ${args.type}`,
115
+ },
116
+ ],
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Handle optimize dashboard layout request
122
+ * @param {object} args
123
+ * @param {object} context
124
+ * @returns {Promise<object>}
125
+ */
126
+ export async function handleOptimizeDashboardLayout(args, context) {
127
+ const { metabaseClient } = context;
128
+
129
+ const dashboard = await metabaseClient.getDashboard(args.dashboard_id);
130
+ const cards = dashboard.ordered_cards || [];
131
+
132
+ const layoutStyle = args.layout_style || 'executive';
133
+ const gridWidth = args.grid_width || 12;
134
+
135
+ // Calculate optimized positions
136
+ const optimizedCards = cards.map((card, index) => {
137
+ const row = Math.floor(index / 2) * 4;
138
+ const col = (index % 2) * 6;
139
+
140
+ return {
141
+ ...card,
142
+ row,
143
+ col,
144
+ sizeX: 6,
145
+ sizeY: 4
146
+ };
147
+ });
148
+
149
+ // Update dashboard
150
+ await metabaseClient.updateDashboard(args.dashboard_id, {
151
+ ordered_cards: optimizedCards
152
+ });
153
+
154
+ return {
155
+ content: [
156
+ {
157
+ type: 'text',
158
+ text: `✅ **Dashboard Layout Optimized!**\\n\\n` +
159
+ `• Dashboard ID: ${args.dashboard_id}\\n` +
160
+ `• Style: ${layoutStyle}\\n` +
161
+ `• Cards Reorganized: ${cards.length}`,
162
+ },
163
+ ],
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Handle create executive dashboard request
169
+ * @param {object} args
170
+ * @param {object} context
171
+ * @returns {Promise<object>}
172
+ */
173
+ export async function handleCreateExecutiveDashboard(args, context) {
174
+ const { metabaseClient } = context;
175
+
176
+ // Create the dashboard first
177
+ const dashboard = await metabaseClient.createDashboard({
178
+ name: args.name,
179
+ description: `Executive dashboard for ${args.business_domain || 'general'} metrics`,
180
+ collection_id: args.collection_id
181
+ });
182
+
183
+ return {
184
+ content: [
185
+ {
186
+ type: 'text',
187
+ text: `✅ **Executive Dashboard Created!**\\n\\n` +
188
+ `• Dashboard ID: ${dashboard.id}\\n` +
189
+ `• Name: ${dashboard.name}\\n` +
190
+ `• Business Domain: ${args.business_domain || 'general'}\\n` +
191
+ `• Time Period: ${args.time_period || 'last_30_days'}\\n\\n` +
192
+ `📝 Next Steps:\\n` +
193
+ `• Add questions to this dashboard\\n` +
194
+ `• Configure filters for interactive analysis`,
195
+ },
196
+ ],
197
+ };
198
+ }
199
+
200
+ export default {
201
+ handleCreateDashboard,
202
+ handleGetDashboards,
203
+ handleAddCardToDashboard,
204
+ handleAddDashboardFilter,
205
+ handleOptimizeDashboardLayout,
206
+ handleCreateExecutiveDashboard,
207
+ };