blixify-charts-mcp 0.1.2 → 0.1.4

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 +583 -196
  2. package/package.json +1 -1
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,197 @@ 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
+ {
579
+ name: "add_card_to_dashboard",
580
+ description: "Add an existing card to a dashboard.",
581
+ inputSchema: {
582
+ type: "object",
583
+ properties: {
584
+ dashboard_id: {
585
+ type: "number",
586
+ description: "ID of the dashboard to add the card to",
587
+ },
588
+ card_id: {
589
+ type: "number",
590
+ description: "ID of the card to add",
591
+ },
592
+ row: {
593
+ type: "number",
594
+ description: "Row position (default: 0)",
595
+ default: 0,
596
+ },
597
+ col: {
598
+ type: "number",
599
+ description: "Column position (default: 0)",
600
+ default: 0,
601
+ },
602
+ size_x: {
603
+ type: "number",
604
+ description: "Width in grid units (default: 4)",
605
+ default: 4,
606
+ },
607
+ size_y: {
608
+ type: "number",
609
+ description: "Height in grid units (default: 4)",
610
+ default: 4,
611
+ },
612
+ },
613
+ required: ["dashboard_id", "card_id"],
614
+ },
615
+ },
616
+ {
617
+ name: "remove_card_from_dashboard",
618
+ description: "Remove a card from a dashboard (does not delete the card itself, just removes it from the dashboard).",
619
+ inputSchema: {
620
+ type: "object",
621
+ properties: {
622
+ dashboard_id: {
623
+ type: "number",
624
+ description: "ID of the dashboard",
625
+ },
626
+ dashcard_id: {
627
+ type: "number",
628
+ description: "ID of the dashboard card (dashcard) to remove. Use get_dashboard_cards to find this ID.",
629
+ },
630
+ },
631
+ required: ["dashboard_id", "dashcard_id"],
632
+ },
633
+ },
634
+ {
635
+ name: "update_dashboard_card",
636
+ description: "Update the position or size of a card in a dashboard.",
637
+ inputSchema: {
638
+ type: "object",
639
+ properties: {
640
+ dashboard_id: {
641
+ type: "number",
642
+ description: "ID of the dashboard",
643
+ },
644
+ dashcard_id: {
645
+ type: "number",
646
+ description: "ID of the dashboard card to update",
647
+ },
648
+ row: {
649
+ type: "number",
650
+ description: "New row position",
651
+ },
652
+ col: {
653
+ type: "number",
654
+ description: "New column position",
655
+ },
656
+ size_x: {
657
+ type: "number",
658
+ description: "New width in grid units",
659
+ },
660
+ size_y: {
661
+ type: "number",
662
+ description: "New height in grid units",
663
+ },
664
+ },
665
+ required: ["dashboard_id", "dashcard_id"],
666
+ },
667
+ },
668
+ ],
453
669
  };
454
670
  });
455
671
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
456
- this.logInfo('Calling tool...', { requestStructure: JSON.stringify(request) });
672
+ this.logInfo("Calling tool...", {
673
+ requestStructure: JSON.stringify(request),
674
+ });
457
675
  if (!METABASE_API_KEY) {
458
676
  await this.getSessionToken();
459
677
  }
460
678
  try {
461
679
  switch (request.params?.name) {
462
680
  case "list_dashboards": {
463
- const response = await this.axiosInstance.get('/api/dashboard');
681
+ const response = await this.axiosInstance.get("/api/dashboard");
464
682
  return {
465
- content: [{
683
+ content: [
684
+ {
466
685
  type: "text",
467
- text: JSON.stringify(response.data, null, 2)
468
- }]
686
+ text: JSON.stringify(response.data, null, 2),
687
+ },
688
+ ],
469
689
  };
470
690
  }
471
691
  case "list_cards": {
472
692
  const f = request.params?.arguments?.f || "all";
473
693
  const response = await this.axiosInstance.get(`/api/card?f=${f}`);
474
694
  return {
475
- content: [{
695
+ content: [
696
+ {
476
697
  type: "text",
477
- text: JSON.stringify(response.data, null, 2)
478
- }]
698
+ text: JSON.stringify(response.data, null, 2),
699
+ },
700
+ ],
479
701
  };
480
702
  }
481
703
  case "list_databases": {
482
- const response = await this.axiosInstance.get('/api/database');
704
+ const response = await this.axiosInstance.get("/api/database");
483
705
  return {
484
- content: [{
706
+ content: [
707
+ {
485
708
  type: "text",
486
- text: JSON.stringify(response.data, null, 2)
487
- }]
709
+ text: JSON.stringify(response.data, null, 2),
710
+ },
711
+ ],
712
+ };
713
+ }
714
+ case "get_database": {
715
+ const databaseId = request.params?.arguments?.database_id;
716
+ if (!databaseId) {
717
+ throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
718
+ }
719
+ const response = await this.axiosInstance.get(`/api/database/${databaseId}`);
720
+ return {
721
+ content: [
722
+ {
723
+ type: "text",
724
+ text: JSON.stringify(response.data, null, 2),
725
+ },
726
+ ],
727
+ };
728
+ }
729
+ case "get_database_metadata": {
730
+ const databaseId = request.params?.arguments?.database_id;
731
+ if (!databaseId) {
732
+ throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
733
+ }
734
+ const response = await this.axiosInstance.get(`/api/database/${databaseId}/metadata`);
735
+ // Filter to only include table names/IDs and field names/IDs
736
+ const filteredData = {
737
+ id: response.data.id,
738
+ name: response.data.name,
739
+ tables: response.data.tables?.map((table) => ({
740
+ id: table.id,
741
+ name: table.name,
742
+ fields: table.fields?.map((field) => ({
743
+ id: field.id,
744
+ name: field.name,
745
+ database_type: field.database_type,
746
+ })) || [],
747
+ })) || [],
748
+ };
749
+ return {
750
+ content: [
751
+ {
752
+ type: "text",
753
+ text: JSON.stringify(filteredData, null, 2),
754
+ },
755
+ ],
488
756
  };
489
757
  }
490
758
  case "execute_card": {
@@ -495,10 +763,12 @@ class MetabaseServer {
495
763
  const parameters = request.params?.arguments?.parameters || {};
496
764
  const response = await this.axiosInstance.post(`/api/card/${cardId}/query`, { parameters });
497
765
  return {
498
- content: [{
766
+ content: [
767
+ {
499
768
  type: "text",
500
- text: JSON.stringify(response.data, null, 2)
501
- }]
769
+ text: JSON.stringify(response.data, null, 2),
770
+ },
771
+ ],
502
772
  };
503
773
  }
504
774
  case "get_dashboard_cards": {
@@ -508,43 +778,74 @@ class MetabaseServer {
508
778
  }
509
779
  const response = await this.axiosInstance.get(`/api/dashboard/${dashboardId}`);
510
780
  return {
511
- content: [{
781
+ content: [
782
+ {
512
783
  type: "text",
513
- text: JSON.stringify(response.data.cards, null, 2)
514
- }]
784
+ text: JSON.stringify(response.data.cards, null, 2),
785
+ },
786
+ ],
515
787
  };
516
788
  }
517
789
  case "execute_query": {
518
790
  const databaseId = request.params?.arguments?.database_id;
519
791
  const query = request.params?.arguments?.query;
792
+ const collectionParam = request.params?.arguments?.collection;
520
793
  const nativeParameters = request.params?.arguments?.native_parameters || [];
521
794
  if (!databaseId) {
522
795
  throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
523
796
  }
524
797
  if (!query) {
525
- throw new McpError(ErrorCode.InvalidParams, "SQL query is required");
798
+ throw new McpError(ErrorCode.InvalidParams, "Query is required");
526
799
  }
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);
800
+ // Get database details to check engine type
801
+ const dbResponse = await this.axiosInstance.get(`/api/database/${databaseId}`);
802
+ const dbEngine = dbResponse.data.engine;
803
+ let queryData;
804
+ if (dbEngine === "mongo") {
805
+ // MongoDB query format
806
+ if (!collectionParam) {
807
+ throw new McpError(ErrorCode.InvalidParams, "Collection name is required for MongoDB queries");
808
+ }
809
+ const queryStr = String(query);
810
+ queryData = {
811
+ type: "native",
812
+ native: {
813
+ collection: collectionParam,
814
+ query: queryStr,
815
+ template_tags: {},
816
+ },
817
+ parameters: nativeParameters,
818
+ database: databaseId,
819
+ };
820
+ }
821
+ else {
822
+ // SQL query format
823
+ queryData = {
824
+ type: "native",
825
+ native: {
826
+ query: query,
827
+ template_tags: {},
828
+ },
829
+ parameters: nativeParameters,
830
+ database: databaseId,
831
+ };
832
+ }
833
+ const response = await this.axiosInstance.post("/api/dataset", queryData);
538
834
  return {
539
- content: [{
835
+ content: [
836
+ {
540
837
  type: "text",
541
- text: JSON.stringify(response.data, null, 2)
542
- }]
838
+ text: JSON.stringify(response.data, null, 2),
839
+ },
840
+ ],
543
841
  };
544
842
  }
545
843
  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) {
844
+ const { name, dataset_query, display, visualization_settings, collection_id, description, } = request.params?.arguments || {};
845
+ if (!name ||
846
+ !dataset_query ||
847
+ !display ||
848
+ !visualization_settings) {
548
849
  throw new McpError(ErrorCode.InvalidParams, "Missing required fields for create_card: name, dataset_query, display, visualization_settings");
549
850
  }
550
851
  const createCardBody = {
@@ -557,12 +858,14 @@ class MetabaseServer {
557
858
  createCardBody.collection_id = collection_id;
558
859
  if (description !== undefined)
559
860
  createCardBody.description = description;
560
- const response = await this.axiosInstance.post('/api/card', createCardBody);
861
+ const response = await this.axiosInstance.post("/api/card", createCardBody);
561
862
  return {
562
- content: [{
863
+ content: [
864
+ {
563
865
  type: "text",
564
- text: JSON.stringify(response.data, null, 2)
565
- }]
866
+ text: JSON.stringify(response.data, null, 2),
867
+ },
868
+ ],
566
869
  };
567
870
  }
568
871
  case "update_card": {
@@ -575,10 +878,12 @@ class MetabaseServer {
575
878
  }
576
879
  const response = await this.axiosInstance.put(`/api/card/${card_id}`, updateFields);
577
880
  return {
578
- content: [{
881
+ content: [
882
+ {
579
883
  type: "text",
580
- text: JSON.stringify(response.data, null, 2)
581
- }]
884
+ text: JSON.stringify(response.data, null, 2),
885
+ },
886
+ ],
582
887
  };
583
888
  }
584
889
  case "delete_card": {
@@ -589,22 +894,28 @@ class MetabaseServer {
589
894
  if (hard_delete) {
590
895
  await this.axiosInstance.delete(`/api/card/${card_id}`);
591
896
  return {
592
- content: [{
897
+ content: [
898
+ {
593
899
  type: "text",
594
- text: `Card ${card_id} permanently deleted.`
595
- }]
900
+ text: `Card ${card_id} permanently deleted.`,
901
+ },
902
+ ],
596
903
  };
597
904
  }
598
905
  else {
599
906
  // Soft delete (archive)
600
907
  const response = await this.axiosInstance.put(`/api/card/${card_id}`, { archived: true });
601
908
  return {
602
- content: [{
909
+ content: [
910
+ {
603
911
  type: "text",
604
912
  // Metabase might return the updated card object or just a success status.
605
913
  // 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
- }]
914
+ text: response.data
915
+ ? `Card ${card_id} archived. Details: ${JSON.stringify(response.data, null, 2)}`
916
+ : `Card ${card_id} archived.`,
917
+ },
918
+ ],
608
919
  };
609
920
  }
610
921
  }
@@ -620,12 +931,14 @@ class MetabaseServer {
620
931
  createDashboardBody.parameters = parameters;
621
932
  if (collection_id !== undefined)
622
933
  createDashboardBody.collection_id = collection_id;
623
- const response = await this.axiosInstance.post('/api/dashboard', createDashboardBody);
934
+ const response = await this.axiosInstance.post("/api/dashboard", createDashboardBody);
624
935
  return {
625
- content: [{
936
+ content: [
937
+ {
626
938
  type: "text",
627
- text: JSON.stringify(response.data, null, 2)
628
- }]
939
+ text: JSON.stringify(response.data, null, 2),
940
+ },
941
+ ],
629
942
  };
630
943
  }
631
944
  case "update_dashboard": {
@@ -638,10 +951,12 @@ class MetabaseServer {
638
951
  }
639
952
  const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, updateFields);
640
953
  return {
641
- content: [{
954
+ content: [
955
+ {
642
956
  type: "text",
643
- text: JSON.stringify(response.data, null, 2)
644
- }]
957
+ text: JSON.stringify(response.data, null, 2),
958
+ },
959
+ ],
645
960
  };
646
961
  }
647
962
  case "delete_dashboard": {
@@ -652,43 +967,115 @@ class MetabaseServer {
652
967
  if (hard_delete) {
653
968
  await this.axiosInstance.delete(`/api/dashboard/${dashboard_id}`);
654
969
  return {
655
- content: [{
970
+ content: [
971
+ {
656
972
  type: "text",
657
- text: `Dashboard ${dashboard_id} permanently deleted.`
658
- }]
973
+ text: `Dashboard ${dashboard_id} permanently deleted.`,
974
+ },
975
+ ],
659
976
  };
660
977
  }
661
978
  else {
662
979
  // Soft delete (archive)
663
980
  const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, { archived: true });
664
981
  return {
665
- content: [{
982
+ content: [
983
+ {
666
984
  type: "text",
667
- text: response.data ? `Dashboard ${dashboard_id} archived. Details: ${JSON.stringify(response.data, null, 2)}` : `Dashboard ${dashboard_id} archived.`
668
- }]
985
+ text: response.data
986
+ ? `Dashboard ${dashboard_id} archived. Details: ${JSON.stringify(response.data, null, 2)}`
987
+ : `Dashboard ${dashboard_id} archived.`,
988
+ },
989
+ ],
669
990
  };
670
991
  }
671
992
  }
993
+ case "add_card_to_dashboard": {
994
+ const { dashboard_id, card_id, row = 0, col = 0, size_x = 4, size_y = 4, } = request.params?.arguments || {};
995
+ if (!dashboard_id) {
996
+ throw new McpError(ErrorCode.InvalidParams, "Dashboard ID is required for add_card_to_dashboard");
997
+ }
998
+ if (!card_id) {
999
+ throw new McpError(ErrorCode.InvalidParams, "Card ID is required for add_card_to_dashboard");
1000
+ }
1001
+ const addCardBody = {
1002
+ cardId: card_id,
1003
+ row,
1004
+ col,
1005
+ size_x,
1006
+ size_y,
1007
+ };
1008
+ const response = await this.axiosInstance.post(`/api/dashboard/${dashboard_id}/cards`, addCardBody);
1009
+ return {
1010
+ content: [
1011
+ {
1012
+ type: "text",
1013
+ text: JSON.stringify(response.data, null, 2),
1014
+ },
1015
+ ],
1016
+ };
1017
+ }
1018
+ case "remove_card_from_dashboard": {
1019
+ const { dashboard_id, dashcard_id } = request.params?.arguments || {};
1020
+ if (!dashboard_id) {
1021
+ throw new McpError(ErrorCode.InvalidParams, "Dashboard ID is required for remove_card_from_dashboard");
1022
+ }
1023
+ if (!dashcard_id) {
1024
+ throw new McpError(ErrorCode.InvalidParams, "Dashcard ID is required for remove_card_from_dashboard");
1025
+ }
1026
+ await this.axiosInstance.delete(`/api/dashboard/${dashboard_id}/cards/${dashcard_id}`);
1027
+ return {
1028
+ content: [
1029
+ {
1030
+ type: "text",
1031
+ text: `Card ${dashcard_id} removed from dashboard ${dashboard_id}.`,
1032
+ },
1033
+ ],
1034
+ };
1035
+ }
1036
+ case "update_dashboard_card": {
1037
+ const { dashboard_id, dashcard_id, ...updateFields } = request.params?.arguments || {};
1038
+ if (!dashboard_id) {
1039
+ throw new McpError(ErrorCode.InvalidParams, "Dashboard ID is required for update_dashboard_card");
1040
+ }
1041
+ if (!dashcard_id) {
1042
+ throw new McpError(ErrorCode.InvalidParams, "Dashcard ID is required for update_dashboard_card");
1043
+ }
1044
+ if (Object.keys(updateFields).length === 0) {
1045
+ throw new McpError(ErrorCode.InvalidParams, "No fields provided for update_dashboard_card");
1046
+ }
1047
+ const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}/cards/${dashcard_id}`, updateFields);
1048
+ return {
1049
+ content: [
1050
+ {
1051
+ type: "text",
1052
+ text: JSON.stringify(response.data, null, 2),
1053
+ },
1054
+ ],
1055
+ };
1056
+ }
672
1057
  default:
673
1058
  return {
674
1059
  content: [
675
1060
  {
676
1061
  type: "text",
677
- text: `Unknown tool: ${request.params?.name}`
678
- }
1062
+ text: `Unknown tool: ${request.params?.name}`,
1063
+ },
679
1064
  ],
680
- isError: true
1065
+ isError: true,
681
1066
  };
682
1067
  }
683
1068
  }
684
1069
  catch (error) {
685
1070
  if (axios.isAxiosError(error)) {
686
1071
  return {
687
- content: [{
1072
+ content: [
1073
+ {
688
1074
  type: "text",
689
- text: `Metabase API error: ${error.response?.data?.message || error.message}`
690
- }],
691
- isError: true
1075
+ text: `Metabase API error: ${error.response?.data?.message || error.message}`,
1076
+ },
1077
+ ],
1078
+ isError: true,
692
1079
  };
693
1080
  }
694
1081
  throw error;
@@ -697,35 +1084,35 @@ class MetabaseServer {
697
1084
  }
698
1085
  async run() {
699
1086
  try {
700
- this.logInfo('Starting Metabase MCP server...');
1087
+ this.logInfo("Starting Metabase MCP server...");
701
1088
  const transport = new StdioServerTransport();
702
1089
  await this.server.connect(transport);
703
- this.logInfo('Metabase MCP server running on stdio');
1090
+ this.logInfo("Metabase MCP server running on stdio");
704
1091
  }
705
1092
  catch (error) {
706
- this.logError('Failed to start server', error);
1093
+ this.logError("Failed to start server", error);
707
1094
  throw error;
708
1095
  }
709
1096
  }
710
1097
  }
711
1098
  // Add global error handlers
712
- process.on('uncaughtException', (error) => {
1099
+ process.on("uncaughtException", (error) => {
713
1100
  console.error(JSON.stringify({
714
1101
  timestamp: new Date().toISOString(),
715
- level: 'fatal',
716
- message: 'Uncaught Exception',
1102
+ level: "fatal",
1103
+ message: "Uncaught Exception",
717
1104
  error: error.message,
718
- stack: error.stack
1105
+ stack: error.stack,
719
1106
  }));
720
1107
  process.exit(1);
721
1108
  });
722
- process.on('unhandledRejection', (reason, promise) => {
1109
+ process.on("unhandledRejection", (reason, promise) => {
723
1110
  const errorMessage = reason instanceof Error ? reason.message : String(reason);
724
1111
  console.error(JSON.stringify({
725
1112
  timestamp: new Date().toISOString(),
726
- level: 'fatal',
727
- message: 'Unhandled Rejection',
728
- error: errorMessage
1113
+ level: "fatal",
1114
+ message: "Unhandled Rejection",
1115
+ error: errorMessage,
729
1116
  }));
730
1117
  });
731
1118
  const server = new MetabaseServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blixify-charts-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Blixify Charts MCP",
5
5
  "private": false,
6
6
  "type": "module",