blixify-charts-mcp 0.1.1 → 0.1.3

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 (2) hide show
  1. package/build/index.js +429 -196
  2. package/package.json +2 -2
package/build/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // 为老版本 Node.js 添加 AbortController polyfill
3
- import AbortController from 'abort-controller';
3
+ import AbortController from "abort-controller";
4
4
  global.AbortController = global.AbortController || AbortController;
5
5
  /**
6
6
  * Metabase MCP 服务器
@@ -13,9 +13,9 @@ global.AbortController = global.AbortController || AbortController;
13
13
  */
14
14
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
- import { ListResourcesRequestSchema, ReadResourceRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
17
- import { z } from "zod";
16
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
18
17
  import axios from "axios";
18
+ import { z } from "zod";
19
19
  // 自定义错误枚举
20
20
  var ErrorCode;
21
21
  (function (ErrorCode) {
@@ -38,15 +38,16 @@ const METABASE_URL = process.env.METABASE_URL;
38
38
  const METABASE_USERNAME = process.env.METABASE_USERNAME;
39
39
  const METABASE_PASSWORD = process.env.METABASE_PASSWORD;
40
40
  const METABASE_API_KEY = process.env.METABASE_API_KEY;
41
- if (!METABASE_URL || (!METABASE_API_KEY && (!METABASE_USERNAME || !METABASE_PASSWORD))) {
41
+ if (!METABASE_URL ||
42
+ (!METABASE_API_KEY && (!METABASE_USERNAME || !METABASE_PASSWORD))) {
42
43
  throw new Error("Either (METABASE_URL and METABASE_API_KEY) or (METABASE_URL, METABASE_USERNAME, and METABASE_PASSWORD) environment variables are required");
43
44
  }
44
45
  // 创建自定义 Schema 对象,使用 z.object
45
46
  const ListResourceTemplatesRequestSchema = z.object({
46
- method: z.literal("resources/list_templates")
47
+ method: z.literal("resources/list_templates"),
47
48
  });
48
49
  const ListToolsRequestSchema = z.object({
49
- method: z.literal("tools/list")
50
+ method: z.literal("tools/list"),
50
51
  });
51
52
  class MetabaseServer {
52
53
  server;
@@ -69,28 +70,29 @@ class MetabaseServer {
69
70
  },
70
71
  });
71
72
  if (METABASE_API_KEY) {
72
- this.logInfo('Using Metabase API Key for authentication.');
73
- this.axiosInstance.defaults.headers.common['X-API-Key'] = METABASE_API_KEY;
73
+ this.logInfo("Using Metabase API Key for authentication.");
74
+ this.axiosInstance.defaults.headers.common["X-API-Key"] =
75
+ METABASE_API_KEY;
74
76
  this.sessionToken = "api_key_used"; // Indicate API key is in use
75
77
  }
76
78
  else if (METABASE_USERNAME && METABASE_PASSWORD) {
77
- this.logInfo('Using Metabase username/password for authentication.');
79
+ this.logInfo("Using Metabase username/password for authentication.");
78
80
  // Existing session token logic will apply
79
81
  }
80
82
  else {
81
83
  // This case should ideally be caught by the initial environment variable check
82
84
  // but as a safeguard:
83
- this.logError('Metabase authentication credentials not configured properly.', {});
85
+ this.logError("Metabase authentication credentials not configured properly.", {});
84
86
  throw new Error("Metabase authentication credentials not provided or incomplete.");
85
87
  }
86
88
  this.setupResourceHandlers();
87
89
  this.setupToolHandlers();
88
90
  // Enhanced error handling with logging
89
91
  this.server.onerror = (error) => {
90
- this.logError('Server Error', error);
92
+ this.logError("Server Error", error);
91
93
  };
92
- process.on('SIGINT', async () => {
93
- this.logInfo('Shutting down server...');
94
+ process.on("SIGINT", async () => {
95
+ this.logInfo("Shutting down server...");
94
96
  await this.server.close();
95
97
  process.exit(0);
96
98
  });
@@ -99,9 +101,9 @@ class MetabaseServer {
99
101
  logInfo(message, data) {
100
102
  const logMessage = {
101
103
  timestamp: new Date().toISOString(),
102
- level: 'info',
104
+ level: "info",
103
105
  message,
104
- data
106
+ data,
105
107
  };
106
108
  console.error(JSON.stringify(logMessage));
107
109
  // MCP SDK changed, can't directly access session
@@ -118,15 +120,15 @@ class MetabaseServer {
118
120
  const apiError = error;
119
121
  const logMessage = {
120
122
  timestamp: new Date().toISOString(),
121
- level: 'error',
123
+ level: "error",
122
124
  message,
123
- error: errorObj.message || 'Unknown error',
124
- stack: errorObj.stack
125
+ error: errorObj.message || "Unknown error",
126
+ stack: errorObj.stack,
125
127
  };
126
128
  console.error(JSON.stringify(logMessage));
127
129
  // MCP SDK changed, can't directly access session
128
130
  try {
129
- console.error(`ERROR: ${message} - ${errorObj.message || 'Unknown error'}`);
131
+ console.error(`ERROR: ${message} - ${errorObj.message || "Unknown error"}`);
130
132
  }
131
133
  catch (e) {
132
134
  // Ignore if session not available
@@ -136,25 +138,27 @@ class MetabaseServer {
136
138
  * 获取 Metabase 会话令牌
137
139
  */
138
140
  async getSessionToken() {
139
- if (this.sessionToken) { // Handles both API key ("api_key_used") and actual session tokens
141
+ if (this.sessionToken) {
142
+ // Handles both API key ("api_key_used") and actual session tokens
140
143
  return this.sessionToken;
141
144
  }
142
145
  // This part should only be reached if using username/password and sessionToken is null
143
- this.logInfo('Authenticating with Metabase using username/password...');
146
+ this.logInfo("Authenticating with Metabase using username/password...");
144
147
  try {
145
- const response = await this.axiosInstance.post('/api/session', {
148
+ const response = await this.axiosInstance.post("/api/session", {
146
149
  username: METABASE_USERNAME,
147
150
  password: METABASE_PASSWORD,
148
151
  });
149
152
  this.sessionToken = response.data.id;
150
153
  // 设置默认请求头
151
- this.axiosInstance.defaults.headers.common['X-Metabase-Session'] = this.sessionToken;
152
- this.logInfo('Successfully authenticated with Metabase');
154
+ this.axiosInstance.defaults.headers.common["X-Metabase-Session"] =
155
+ this.sessionToken;
156
+ this.logInfo("Successfully authenticated with Metabase");
153
157
  return this.sessionToken;
154
158
  }
155
159
  catch (error) {
156
- this.logError('Authentication failed', error);
157
- throw new McpError(ErrorCode.InternalError, 'Failed to authenticate with Metabase');
160
+ this.logError("Authentication failed", error);
161
+ throw new McpError(ErrorCode.InternalError, "Failed to authenticate with Metabase");
158
162
  }
159
163
  }
160
164
  /**
@@ -162,27 +166,31 @@ class MetabaseServer {
162
166
  */
163
167
  setupResourceHandlers() {
164
168
  this.server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
165
- this.logInfo('Listing resources...', { requestStructure: JSON.stringify(request) });
169
+ this.logInfo("Listing resources...", {
170
+ requestStructure: JSON.stringify(request),
171
+ });
166
172
  if (!METABASE_API_KEY) {
167
173
  await this.getSessionToken();
168
174
  }
169
175
  try {
170
176
  // 获取仪表板列表
171
- const dashboardsResponse = await this.axiosInstance.get('/api/dashboard');
172
- this.logInfo('Successfully listed resources', { count: dashboardsResponse.data.length });
177
+ const dashboardsResponse = await this.axiosInstance.get("/api/dashboard");
178
+ this.logInfo("Successfully listed resources", {
179
+ count: dashboardsResponse.data.length,
180
+ });
173
181
  // 将仪表板作为资源返回
174
182
  return {
175
183
  resources: dashboardsResponse.data.map((dashboard) => ({
176
184
  uri: `metabase://dashboard/${dashboard.id}`,
177
185
  mimeType: "application/json",
178
186
  name: dashboard.name,
179
- description: `Metabase dashboard: ${dashboard.name}`
180
- }))
187
+ description: `Metabase dashboard: ${dashboard.name}`,
188
+ })),
181
189
  };
182
190
  }
183
191
  catch (error) {
184
- this.logError('Failed to list resources', error);
185
- throw new McpError(ErrorCode.InternalError, 'Failed to list Metabase resources');
192
+ this.logError("Failed to list resources", error);
193
+ throw new McpError(ErrorCode.InternalError, "Failed to list Metabase resources");
186
194
  }
187
195
  });
188
196
  // 资源模板
@@ -190,29 +198,31 @@ class MetabaseServer {
190
198
  return {
191
199
  resourceTemplates: [
192
200
  {
193
- uriTemplate: 'metabase://dashboard/{id}',
194
- name: 'Dashboard by ID',
195
- mimeType: 'application/json',
196
- description: 'Get a Metabase dashboard by its ID',
201
+ uriTemplate: "metabase://dashboard/{id}",
202
+ name: "Dashboard by ID",
203
+ mimeType: "application/json",
204
+ description: "Get a Metabase dashboard by its ID",
197
205
  },
198
206
  {
199
- uriTemplate: 'metabase://card/{id}',
200
- name: 'Card by ID',
201
- mimeType: 'application/json',
202
- description: 'Get a Metabase question/card by its ID',
207
+ uriTemplate: "metabase://card/{id}",
208
+ name: "Card by ID",
209
+ mimeType: "application/json",
210
+ description: "Get a Metabase question/card by its ID",
203
211
  },
204
212
  {
205
- uriTemplate: 'metabase://database/{id}',
206
- name: 'Database by ID',
207
- mimeType: 'application/json',
208
- description: 'Get a Metabase database by its ID',
213
+ uriTemplate: "metabase://database/{id}",
214
+ name: "Database by ID",
215
+ mimeType: "application/json",
216
+ description: "Get a Metabase database by its ID",
209
217
  },
210
218
  ],
211
219
  };
212
220
  });
213
221
  // 读取资源
214
222
  this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
215
- this.logInfo('Reading resource...', { requestStructure: JSON.stringify(request) });
223
+ this.logInfo("Reading resource...", {
224
+ requestStructure: JSON.stringify(request),
225
+ });
216
226
  if (!METABASE_API_KEY) {
217
227
  await this.getSessionToken();
218
228
  }
@@ -224,11 +234,13 @@ class MetabaseServer {
224
234
  const dashboardId = match[1];
225
235
  const response = await this.axiosInstance.get(`/api/dashboard/${dashboardId}`);
226
236
  return {
227
- contents: [{
237
+ contents: [
238
+ {
228
239
  uri: request.params?.uri,
229
240
  mimeType: "application/json",
230
- text: JSON.stringify(response.data, null, 2)
231
- }]
241
+ text: JSON.stringify(response.data, null, 2),
242
+ },
243
+ ],
232
244
  };
233
245
  }
234
246
  // 处理问题/卡片资源
@@ -236,11 +248,13 @@ class MetabaseServer {
236
248
  const cardId = match[1];
237
249
  const response = await this.axiosInstance.get(`/api/card/${cardId}`);
238
250
  return {
239
- contents: [{
251
+ contents: [
252
+ {
240
253
  uri: request.params?.uri,
241
254
  mimeType: "application/json",
242
- text: JSON.stringify(response.data, null, 2)
243
- }]
255
+ text: JSON.stringify(response.data, null, 2),
256
+ },
257
+ ],
244
258
  };
245
259
  }
246
260
  // 处理数据库资源
@@ -248,11 +262,13 @@ class MetabaseServer {
248
262
  const databaseId = match[1];
249
263
  const response = await this.axiosInstance.get(`/api/database/${databaseId}`);
250
264
  return {
251
- contents: [{
265
+ contents: [
266
+ {
252
267
  uri: request.params?.uri,
253
268
  mimeType: "application/json",
254
- text: JSON.stringify(response.data, null, 2)
255
- }]
269
+ text: JSON.stringify(response.data, null, 2),
270
+ },
271
+ ],
256
272
  };
257
273
  }
258
274
  else {
@@ -280,8 +296,8 @@ class MetabaseServer {
280
296
  description: "List all dashboards in Metabase",
281
297
  inputSchema: {
282
298
  type: "object",
283
- properties: {}
284
- }
299
+ properties: {},
300
+ },
285
301
  },
286
302
  {
287
303
  name: "list_cards",
@@ -291,18 +307,46 @@ class MetabaseServer {
291
307
  properties: {
292
308
  f: {
293
309
  type: "string",
294
- description: "Optional filter function, possible values: archived, table, database, using_model, bookmarked, using_segment, all, mine"
295
- }
296
- }
297
- }
310
+ description: "Optional filter function, possible values: archived, table, database, using_model, bookmarked, using_segment, all, mine",
311
+ },
312
+ },
313
+ },
298
314
  },
299
315
  {
300
316
  name: "list_databases",
301
317
  description: "List all databases in Metabase",
302
318
  inputSchema: {
303
319
  type: "object",
304
- properties: {}
305
- }
320
+ properties: {},
321
+ },
322
+ },
323
+ {
324
+ name: "get_database",
325
+ description: "Get detailed information about a specific Metabase database including tables and schema",
326
+ inputSchema: {
327
+ type: "object",
328
+ properties: {
329
+ database_id: {
330
+ type: "number",
331
+ description: "ID of the database",
332
+ },
333
+ },
334
+ required: ["database_id"],
335
+ },
336
+ },
337
+ {
338
+ name: "get_database_metadata",
339
+ description: "Get complete metadata for a database including all tables, fields, and schema information",
340
+ inputSchema: {
341
+ type: "object",
342
+ properties: {
343
+ database_id: {
344
+ type: "number",
345
+ description: "ID of the database",
346
+ },
347
+ },
348
+ required: ["database_id"],
349
+ },
306
350
  },
307
351
  {
308
352
  name: "execute_card",
@@ -312,15 +356,15 @@ class MetabaseServer {
312
356
  properties: {
313
357
  card_id: {
314
358
  type: "number",
315
- description: "ID of the card/question to execute"
359
+ description: "ID of the card/question to execute",
316
360
  },
317
361
  parameters: {
318
362
  type: "object",
319
- description: "Optional parameters for the query"
320
- }
363
+ description: "Optional parameters for the query",
364
+ },
321
365
  },
322
- required: ["card_id"]
323
- }
366
+ required: ["card_id"],
367
+ },
324
368
  },
325
369
  {
326
370
  name: "get_dashboard_cards",
@@ -330,36 +374,40 @@ class MetabaseServer {
330
374
  properties: {
331
375
  dashboard_id: {
332
376
  type: "number",
333
- description: "ID of the dashboard"
334
- }
377
+ description: "ID of the dashboard",
378
+ },
335
379
  },
336
- required: ["dashboard_id"]
337
- }
380
+ required: ["dashboard_id"],
381
+ },
338
382
  },
339
383
  {
340
384
  name: "execute_query",
341
- description: "Execute a SQL query against a Metabase database",
385
+ description: "Execute a SQL query against a Metabase database, or MongoDB aggregation pipeline against MongoDB databases",
342
386
  inputSchema: {
343
387
  type: "object",
344
388
  properties: {
345
389
  database_id: {
346
390
  type: "number",
347
- description: "ID of the database to query"
391
+ description: "ID of the database to query",
348
392
  },
349
393
  query: {
350
394
  type: "string",
351
- description: "SQL query to execute"
395
+ description: "SQL query for SQL databases, or MongoDB aggregation pipeline as JSON string (e.g., '[{\"$limit\": 10}]') for MongoDB databases",
396
+ },
397
+ collection: {
398
+ type: "string",
399
+ description: "MongoDB collection name (required for MongoDB databases, e.g., 'kpj-user-profiles'). Ignored for SQL databases.",
352
400
  },
353
401
  native_parameters: {
354
402
  type: "array",
355
403
  description: "Optional parameters for the query",
356
404
  items: {
357
- type: "object"
358
- }
359
- }
405
+ type: "object",
406
+ },
407
+ },
360
408
  },
361
- required: ["database_id", "query"]
362
- }
409
+ required: ["database_id", "query"],
410
+ },
363
411
  },
364
412
  {
365
413
  name: "create_card",
@@ -368,14 +416,34 @@ class MetabaseServer {
368
416
  type: "object",
369
417
  properties: {
370
418
  name: { type: "string", description: "Name of the card" },
371
- dataset_query: { type: "object", description: "The query for the card (e.g., MBQL or native query)" },
372
- display: { type: "string", description: "Display type (e.g., 'table', 'line', 'bar')" },
373
- visualization_settings: { type: "object", description: "Settings for the visualization" },
374
- collection_id: { type: "number", description: "Optional ID of the collection to save the card in" },
375
- description: { type: "string", description: "Optional description for the card" }
419
+ dataset_query: {
420
+ type: "object",
421
+ description: "The query for the card (e.g., MBQL or native query)",
422
+ },
423
+ display: {
424
+ type: "string",
425
+ description: "Display type (e.g., 'table', 'line', 'bar')",
426
+ },
427
+ visualization_settings: {
428
+ type: "object",
429
+ description: "Settings for the visualization",
430
+ },
431
+ collection_id: {
432
+ type: "number",
433
+ description: "Optional ID of the collection to save the card in",
434
+ },
435
+ description: {
436
+ type: "string",
437
+ description: "Optional description for the card",
438
+ },
376
439
  },
377
- required: ["name", "dataset_query", "display", "visualization_settings"]
378
- }
440
+ required: [
441
+ "name",
442
+ "dataset_query",
443
+ "display",
444
+ "visualization_settings",
445
+ ],
446
+ },
379
447
  },
380
448
  {
381
449
  name: "update_card",
@@ -383,17 +451,32 @@ class MetabaseServer {
383
451
  inputSchema: {
384
452
  type: "object",
385
453
  properties: {
386
- card_id: { type: "number", description: "ID of the card to update" },
454
+ card_id: {
455
+ type: "number",
456
+ description: "ID of the card to update",
457
+ },
387
458
  name: { type: "string", description: "New name for the card" },
388
- dataset_query: { type: "object", description: "New query for the card" },
459
+ dataset_query: {
460
+ type: "object",
461
+ description: "New query for the card",
462
+ },
389
463
  display: { type: "string", description: "New display type" },
390
- visualization_settings: { type: "object", description: "New visualization settings" },
391
- collection_id: { type: "number", description: "New collection ID" },
464
+ visualization_settings: {
465
+ type: "object",
466
+ description: "New visualization settings",
467
+ },
468
+ collection_id: {
469
+ type: "number",
470
+ description: "New collection ID",
471
+ },
392
472
  description: { type: "string", description: "New description" },
393
- archived: { type: "boolean", description: "Set to true to archive the card" }
473
+ archived: {
474
+ type: "boolean",
475
+ description: "Set to true to archive the card",
476
+ },
394
477
  },
395
- required: ["card_id"]
396
- }
478
+ required: ["card_id"],
479
+ },
397
480
  },
398
481
  {
399
482
  name: "delete_card",
@@ -401,11 +484,18 @@ class MetabaseServer {
401
484
  inputSchema: {
402
485
  type: "object",
403
486
  properties: {
404
- card_id: { type: "number", description: "ID of the card to delete" },
405
- hard_delete: { type: "boolean", description: "Set to true for hard delete, false (default) for archive", default: false }
487
+ card_id: {
488
+ type: "number",
489
+ description: "ID of the card to delete",
490
+ },
491
+ hard_delete: {
492
+ type: "boolean",
493
+ description: "Set to true for hard delete, false (default) for archive",
494
+ default: false,
495
+ },
406
496
  },
407
- required: ["card_id"]
408
- }
497
+ required: ["card_id"],
498
+ },
409
499
  },
410
500
  {
411
501
  name: "create_dashboard",
@@ -414,12 +504,22 @@ class MetabaseServer {
414
504
  type: "object",
415
505
  properties: {
416
506
  name: { type: "string", description: "Name of the dashboard" },
417
- description: { type: "string", description: "Optional description for the dashboard" },
418
- parameters: { type: "array", description: "Optional parameters for the dashboard", items: { type: "object" } },
419
- collection_id: { type: "number", description: "Optional ID of the collection to save the dashboard in" }
507
+ description: {
508
+ type: "string",
509
+ description: "Optional description for the dashboard",
510
+ },
511
+ parameters: {
512
+ type: "array",
513
+ description: "Optional parameters for the dashboard",
514
+ items: { type: "object" },
515
+ },
516
+ collection_id: {
517
+ type: "number",
518
+ description: "Optional ID of the collection to save the dashboard in",
519
+ },
420
520
  },
421
- required: ["name"]
422
- }
521
+ required: ["name"],
522
+ },
423
523
  },
424
524
  {
425
525
  name: "update_dashboard",
@@ -427,15 +527,34 @@ class MetabaseServer {
427
527
  inputSchema: {
428
528
  type: "object",
429
529
  properties: {
430
- dashboard_id: { type: "number", description: "ID of the dashboard to update" },
431
- name: { type: "string", description: "New name for the dashboard" },
432
- description: { type: "string", description: "New description for the dashboard" },
433
- parameters: { type: "array", description: "New parameters for the dashboard", items: { type: "object" } },
434
- collection_id: { type: "number", description: "New collection ID" },
435
- archived: { type: "boolean", description: "Set to true to archive the dashboard" }
530
+ dashboard_id: {
531
+ type: "number",
532
+ description: "ID of the dashboard to update",
533
+ },
534
+ name: {
535
+ type: "string",
536
+ description: "New name for the dashboard",
537
+ },
538
+ description: {
539
+ type: "string",
540
+ description: "New description for the dashboard",
541
+ },
542
+ parameters: {
543
+ type: "array",
544
+ description: "New parameters for the dashboard",
545
+ items: { type: "object" },
546
+ },
547
+ collection_id: {
548
+ type: "number",
549
+ description: "New collection ID",
550
+ },
551
+ archived: {
552
+ type: "boolean",
553
+ description: "Set to true to archive the dashboard",
554
+ },
436
555
  },
437
- required: ["dashboard_id"]
438
- }
556
+ required: ["dashboard_id"],
557
+ },
439
558
  },
440
559
  {
441
560
  name: "delete_dashboard",
@@ -443,48 +562,107 @@ class MetabaseServer {
443
562
  inputSchema: {
444
563
  type: "object",
445
564
  properties: {
446
- dashboard_id: { type: "number", description: "ID of the dashboard to delete" },
447
- hard_delete: { type: "boolean", description: "Set to true for hard delete, false (default) for archive", default: false }
565
+ dashboard_id: {
566
+ type: "number",
567
+ description: "ID of the dashboard to delete",
568
+ },
569
+ hard_delete: {
570
+ type: "boolean",
571
+ description: "Set to true for hard delete, false (default) for archive",
572
+ default: false,
573
+ },
448
574
  },
449
- required: ["dashboard_id"]
450
- }
451
- }
452
- ]
575
+ required: ["dashboard_id"],
576
+ },
577
+ },
578
+ ],
453
579
  };
454
580
  });
455
581
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
456
- this.logInfo('Calling tool...', { requestStructure: JSON.stringify(request) });
582
+ this.logInfo("Calling tool...", {
583
+ requestStructure: JSON.stringify(request),
584
+ });
457
585
  if (!METABASE_API_KEY) {
458
586
  await this.getSessionToken();
459
587
  }
460
588
  try {
461
589
  switch (request.params?.name) {
462
590
  case "list_dashboards": {
463
- const response = await this.axiosInstance.get('/api/dashboard');
591
+ const response = await this.axiosInstance.get("/api/dashboard");
464
592
  return {
465
- content: [{
593
+ content: [
594
+ {
466
595
  type: "text",
467
- text: JSON.stringify(response.data, null, 2)
468
- }]
596
+ text: JSON.stringify(response.data, null, 2),
597
+ },
598
+ ],
469
599
  };
470
600
  }
471
601
  case "list_cards": {
472
602
  const f = request.params?.arguments?.f || "all";
473
603
  const response = await this.axiosInstance.get(`/api/card?f=${f}`);
474
604
  return {
475
- content: [{
605
+ content: [
606
+ {
476
607
  type: "text",
477
- text: JSON.stringify(response.data, null, 2)
478
- }]
608
+ text: JSON.stringify(response.data, null, 2),
609
+ },
610
+ ],
479
611
  };
480
612
  }
481
613
  case "list_databases": {
482
- const response = await this.axiosInstance.get('/api/database');
614
+ const response = await this.axiosInstance.get("/api/database");
483
615
  return {
484
- content: [{
616
+ content: [
617
+ {
485
618
  type: "text",
486
- text: JSON.stringify(response.data, null, 2)
487
- }]
619
+ text: JSON.stringify(response.data, null, 2),
620
+ },
621
+ ],
622
+ };
623
+ }
624
+ case "get_database": {
625
+ const databaseId = request.params?.arguments?.database_id;
626
+ if (!databaseId) {
627
+ throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
628
+ }
629
+ const response = await this.axiosInstance.get(`/api/database/${databaseId}`);
630
+ return {
631
+ content: [
632
+ {
633
+ type: "text",
634
+ text: JSON.stringify(response.data, null, 2),
635
+ },
636
+ ],
637
+ };
638
+ }
639
+ case "get_database_metadata": {
640
+ const databaseId = request.params?.arguments?.database_id;
641
+ if (!databaseId) {
642
+ throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
643
+ }
644
+ const response = await this.axiosInstance.get(`/api/database/${databaseId}/metadata`);
645
+ // Filter to only include table names/IDs and field names/IDs
646
+ const filteredData = {
647
+ id: response.data.id,
648
+ name: response.data.name,
649
+ tables: response.data.tables?.map((table) => ({
650
+ id: table.id,
651
+ name: table.name,
652
+ fields: table.fields?.map((field) => ({
653
+ id: field.id,
654
+ name: field.name,
655
+ database_type: field.database_type,
656
+ })) || [],
657
+ })) || [],
658
+ };
659
+ return {
660
+ content: [
661
+ {
662
+ type: "text",
663
+ text: JSON.stringify(filteredData, null, 2),
664
+ },
665
+ ],
488
666
  };
489
667
  }
490
668
  case "execute_card": {
@@ -495,10 +673,12 @@ class MetabaseServer {
495
673
  const parameters = request.params?.arguments?.parameters || {};
496
674
  const response = await this.axiosInstance.post(`/api/card/${cardId}/query`, { parameters });
497
675
  return {
498
- content: [{
676
+ content: [
677
+ {
499
678
  type: "text",
500
- text: JSON.stringify(response.data, null, 2)
501
- }]
679
+ text: JSON.stringify(response.data, null, 2),
680
+ },
681
+ ],
502
682
  };
503
683
  }
504
684
  case "get_dashboard_cards": {
@@ -508,43 +688,74 @@ class MetabaseServer {
508
688
  }
509
689
  const response = await this.axiosInstance.get(`/api/dashboard/${dashboardId}`);
510
690
  return {
511
- content: [{
691
+ content: [
692
+ {
512
693
  type: "text",
513
- text: JSON.stringify(response.data.cards, null, 2)
514
- }]
694
+ text: JSON.stringify(response.data.cards, null, 2),
695
+ },
696
+ ],
515
697
  };
516
698
  }
517
699
  case "execute_query": {
518
700
  const databaseId = request.params?.arguments?.database_id;
519
701
  const query = request.params?.arguments?.query;
702
+ const collectionParam = request.params?.arguments?.collection;
520
703
  const nativeParameters = request.params?.arguments?.native_parameters || [];
521
704
  if (!databaseId) {
522
705
  throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
523
706
  }
524
707
  if (!query) {
525
- throw new McpError(ErrorCode.InvalidParams, "SQL query is required");
708
+ throw new McpError(ErrorCode.InvalidParams, "Query is required");
526
709
  }
527
- // 构建查询请求体
528
- const queryData = {
529
- type: "native",
530
- native: {
531
- query: query,
532
- template_tags: {}
533
- },
534
- parameters: nativeParameters,
535
- database: databaseId
536
- };
537
- const response = await this.axiosInstance.post('/api/dataset', queryData);
710
+ // Get database details to check engine type
711
+ const dbResponse = await this.axiosInstance.get(`/api/database/${databaseId}`);
712
+ const dbEngine = dbResponse.data.engine;
713
+ let queryData;
714
+ if (dbEngine === "mongo") {
715
+ // MongoDB query format
716
+ if (!collectionParam) {
717
+ throw new McpError(ErrorCode.InvalidParams, "Collection name is required for MongoDB queries");
718
+ }
719
+ const queryStr = String(query);
720
+ queryData = {
721
+ type: "native",
722
+ native: {
723
+ collection: collectionParam,
724
+ query: queryStr,
725
+ template_tags: {},
726
+ },
727
+ parameters: nativeParameters,
728
+ database: databaseId,
729
+ };
730
+ }
731
+ else {
732
+ // SQL query format
733
+ queryData = {
734
+ type: "native",
735
+ native: {
736
+ query: query,
737
+ template_tags: {},
738
+ },
739
+ parameters: nativeParameters,
740
+ database: databaseId,
741
+ };
742
+ }
743
+ const response = await this.axiosInstance.post("/api/dataset", queryData);
538
744
  return {
539
- content: [{
745
+ content: [
746
+ {
540
747
  type: "text",
541
- text: JSON.stringify(response.data, null, 2)
542
- }]
748
+ text: JSON.stringify(response.data, null, 2),
749
+ },
750
+ ],
543
751
  };
544
752
  }
545
753
  case "create_card": {
546
- const { name, dataset_query, display, visualization_settings, collection_id, description } = request.params?.arguments || {};
547
- if (!name || !dataset_query || !display || !visualization_settings) {
754
+ const { name, dataset_query, display, visualization_settings, collection_id, description, } = request.params?.arguments || {};
755
+ if (!name ||
756
+ !dataset_query ||
757
+ !display ||
758
+ !visualization_settings) {
548
759
  throw new McpError(ErrorCode.InvalidParams, "Missing required fields for create_card: name, dataset_query, display, visualization_settings");
549
760
  }
550
761
  const createCardBody = {
@@ -557,12 +768,14 @@ class MetabaseServer {
557
768
  createCardBody.collection_id = collection_id;
558
769
  if (description !== undefined)
559
770
  createCardBody.description = description;
560
- const response = await this.axiosInstance.post('/api/card', createCardBody);
771
+ const response = await this.axiosInstance.post("/api/card", createCardBody);
561
772
  return {
562
- content: [{
773
+ content: [
774
+ {
563
775
  type: "text",
564
- text: JSON.stringify(response.data, null, 2)
565
- }]
776
+ text: JSON.stringify(response.data, null, 2),
777
+ },
778
+ ],
566
779
  };
567
780
  }
568
781
  case "update_card": {
@@ -575,10 +788,12 @@ class MetabaseServer {
575
788
  }
576
789
  const response = await this.axiosInstance.put(`/api/card/${card_id}`, updateFields);
577
790
  return {
578
- content: [{
791
+ content: [
792
+ {
579
793
  type: "text",
580
- text: JSON.stringify(response.data, null, 2)
581
- }]
794
+ text: JSON.stringify(response.data, null, 2),
795
+ },
796
+ ],
582
797
  };
583
798
  }
584
799
  case "delete_card": {
@@ -589,22 +804,28 @@ class MetabaseServer {
589
804
  if (hard_delete) {
590
805
  await this.axiosInstance.delete(`/api/card/${card_id}`);
591
806
  return {
592
- content: [{
807
+ content: [
808
+ {
593
809
  type: "text",
594
- text: `Card ${card_id} permanently deleted.`
595
- }]
810
+ text: `Card ${card_id} permanently deleted.`,
811
+ },
812
+ ],
596
813
  };
597
814
  }
598
815
  else {
599
816
  // Soft delete (archive)
600
817
  const response = await this.axiosInstance.put(`/api/card/${card_id}`, { archived: true });
601
818
  return {
602
- content: [{
819
+ content: [
820
+ {
603
821
  type: "text",
604
822
  // Metabase might return the updated card object or just a success status.
605
823
  // If response.data is available and meaningful, include it. Otherwise, a generic success message.
606
- text: response.data ? `Card ${card_id} archived. Details: ${JSON.stringify(response.data, null, 2)}` : `Card ${card_id} archived.`
607
- }]
824
+ text: response.data
825
+ ? `Card ${card_id} archived. Details: ${JSON.stringify(response.data, null, 2)}`
826
+ : `Card ${card_id} archived.`,
827
+ },
828
+ ],
608
829
  };
609
830
  }
610
831
  }
@@ -620,12 +841,14 @@ class MetabaseServer {
620
841
  createDashboardBody.parameters = parameters;
621
842
  if (collection_id !== undefined)
622
843
  createDashboardBody.collection_id = collection_id;
623
- const response = await this.axiosInstance.post('/api/dashboard', createDashboardBody);
844
+ const response = await this.axiosInstance.post("/api/dashboard", createDashboardBody);
624
845
  return {
625
- content: [{
846
+ content: [
847
+ {
626
848
  type: "text",
627
- text: JSON.stringify(response.data, null, 2)
628
- }]
849
+ text: JSON.stringify(response.data, null, 2),
850
+ },
851
+ ],
629
852
  };
630
853
  }
631
854
  case "update_dashboard": {
@@ -638,10 +861,12 @@ class MetabaseServer {
638
861
  }
639
862
  const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, updateFields);
640
863
  return {
641
- content: [{
864
+ content: [
865
+ {
642
866
  type: "text",
643
- text: JSON.stringify(response.data, null, 2)
644
- }]
867
+ text: JSON.stringify(response.data, null, 2),
868
+ },
869
+ ],
645
870
  };
646
871
  }
647
872
  case "delete_dashboard": {
@@ -652,20 +877,26 @@ class MetabaseServer {
652
877
  if (hard_delete) {
653
878
  await this.axiosInstance.delete(`/api/dashboard/${dashboard_id}`);
654
879
  return {
655
- content: [{
880
+ content: [
881
+ {
656
882
  type: "text",
657
- text: `Dashboard ${dashboard_id} permanently deleted.`
658
- }]
883
+ text: `Dashboard ${dashboard_id} permanently deleted.`,
884
+ },
885
+ ],
659
886
  };
660
887
  }
661
888
  else {
662
889
  // Soft delete (archive)
663
890
  const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, { archived: true });
664
891
  return {
665
- content: [{
892
+ content: [
893
+ {
666
894
  type: "text",
667
- text: response.data ? `Dashboard ${dashboard_id} archived. Details: ${JSON.stringify(response.data, null, 2)}` : `Dashboard ${dashboard_id} archived.`
668
- }]
895
+ text: response.data
896
+ ? `Dashboard ${dashboard_id} archived. Details: ${JSON.stringify(response.data, null, 2)}`
897
+ : `Dashboard ${dashboard_id} archived.`,
898
+ },
899
+ ],
669
900
  };
670
901
  }
671
902
  }
@@ -674,21 +905,23 @@ class MetabaseServer {
674
905
  content: [
675
906
  {
676
907
  type: "text",
677
- text: `Unknown tool: ${request.params?.name}`
678
- }
908
+ text: `Unknown tool: ${request.params?.name}`,
909
+ },
679
910
  ],
680
- isError: true
911
+ isError: true,
681
912
  };
682
913
  }
683
914
  }
684
915
  catch (error) {
685
916
  if (axios.isAxiosError(error)) {
686
917
  return {
687
- content: [{
918
+ content: [
919
+ {
688
920
  type: "text",
689
- text: `Metabase API error: ${error.response?.data?.message || error.message}`
690
- }],
691
- isError: true
921
+ text: `Metabase API error: ${error.response?.data?.message || error.message}`,
922
+ },
923
+ ],
924
+ isError: true,
692
925
  };
693
926
  }
694
927
  throw error;
@@ -697,35 +930,35 @@ class MetabaseServer {
697
930
  }
698
931
  async run() {
699
932
  try {
700
- this.logInfo('Starting Metabase MCP server...');
933
+ this.logInfo("Starting Metabase MCP server...");
701
934
  const transport = new StdioServerTransport();
702
935
  await this.server.connect(transport);
703
- this.logInfo('Metabase MCP server running on stdio');
936
+ this.logInfo("Metabase MCP server running on stdio");
704
937
  }
705
938
  catch (error) {
706
- this.logError('Failed to start server', error);
939
+ this.logError("Failed to start server", error);
707
940
  throw error;
708
941
  }
709
942
  }
710
943
  }
711
944
  // Add global error handlers
712
- process.on('uncaughtException', (error) => {
945
+ process.on("uncaughtException", (error) => {
713
946
  console.error(JSON.stringify({
714
947
  timestamp: new Date().toISOString(),
715
- level: 'fatal',
716
- message: 'Uncaught Exception',
948
+ level: "fatal",
949
+ message: "Uncaught Exception",
717
950
  error: error.message,
718
- stack: error.stack
951
+ stack: error.stack,
719
952
  }));
720
953
  process.exit(1);
721
954
  });
722
- process.on('unhandledRejection', (reason, promise) => {
955
+ process.on("unhandledRejection", (reason, promise) => {
723
956
  const errorMessage = reason instanceof Error ? reason.message : String(reason);
724
957
  console.error(JSON.stringify({
725
958
  timestamp: new Date().toISOString(),
726
- level: 'fatal',
727
- message: 'Unhandled Rejection',
728
- error: errorMessage
959
+ level: "fatal",
960
+ message: "Unhandled Rejection",
961
+ error: errorMessage,
729
962
  }));
730
963
  });
731
964
  const server = new MetabaseServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blixify-charts-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Blixify Charts MCP",
5
5
  "private": false,
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "inspector": "npx @modelcontextprotocol/inspector build/index.js"
18
18
  },
19
19
  "dependencies": {
20
- "@modelcontextprotocol/sdk": "^0.6.1",
20
+ "@modelcontextprotocol/sdk": "^1.25.1",
21
21
  "abort-controller": "^3.0.0",
22
22
  "axios": "^1.8.2",
23
23
  "zod": "^3.23.8"